Skip to content

Commit

Permalink
Bundle model-specific docstrings and hide their classes from public a…
Browse files Browse the repository at this point in the history
…pi (#98)

* Bundle model-specific docstrings and hide their classes from public api

* Formatting

* Mock fiona to test rtd build

* comma

* mock dateutil

* mock shapely

* Change order of forcing constructors

* Add that model-specific stuff should be in a dict

* Automatically detect new models in docs

* fix init statements

* Add pathparser to util and fix formatting issues

* remove marrmot defaults

* Try conda install on readthedocs

* Revert "Try conda install on readthedocs"

This reverts commit 17addd6.

* Revert "Automatically detect new models in docs"

This reverts commit 6580cd3.

* Fix imports

* Small fixes of rendering of API docs

* Move input parsing to its own branch

* fix small issues

* Keep shape argument optional in init

* Add __eq__ method to default forcing; this makes it possible to compare whether 2 forcing objects share the same properties. It broke when we removed the dataclasses, which implements a default method

* Revert removing marrmot default name and fix remaining tests

* fix sphinx warnings

* fix mypy

* does this fix mypy?

Co-authored-by: Stefan Verhoeven <[email protected]>
  • Loading branch information
Peter9192 and sverhoeven authored Jun 17, 2021
1 parent 955499d commit a7c8d3a
Show file tree
Hide file tree
Showing 20 changed files with 636 additions and 536 deletions.
12 changes: 12 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ def setup(app):
'cftime',
'dask',
'esmvalcore',
'fiona',
'dateutil',
'shapely',
'hydrostats',
'matplotlib',
'numpy',
Expand All @@ -219,3 +222,12 @@ def setup(app):

# Prevent alphabetic sorting of (@data)class attributes/methods
autodoc_member_order = 'bysource'

# Nice formatting of model-specific input parameters
napoleon_custom_sections = [
('hype', 'params_style'),
('lisflood', 'params_style'),
('marrmot', 'params_style'),
('pcrglobwb', 'params_style'),
('wflow', 'params_style'),
]
128 changes: 75 additions & 53 deletions ewatercycle/forcing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,26 @@

from ruamel.yaml import YAML

from . import default, hype, lisflood, marrmot, pcrglobwb, wflow
from .datasets import DATASETS
from .default import DefaultForcing
from . import _hype, _lisflood, _marrmot, _pcrglobwb, _wflow
from ._default import DefaultForcing

FORCING_CLASSES: Dict[str, Type[DefaultForcing]] = {
"hype": hype.HypeForcing,
"lisflood": lisflood.LisfloodForcing,
"marrmot": marrmot.MarrmotForcing,
"pcrglobwb": pcrglobwb.PCRGlobWBForcing,
"wflow": wflow.WflowForcing,
"hype": _hype.HypeForcing,
"lisflood": _lisflood.LisfloodForcing,
"marrmot": _marrmot.MarrmotForcing,
"pcrglobwb": _pcrglobwb.PCRGlobWBForcing,
"wflow": _wflow.WflowForcing,
}


def generate(target_model: str,
dataset: str,
start_time: str,
end_time: str,
shape: str,
model_specific_options: Optional[Dict] = None) -> DefaultForcing:
"""Generate forcing data with ESMValTool.
Args:
target_model: Name of the model
dataset: Name of the source dataset. See :py:data:`.DATASETS`.
start_time: Start time of forcing in UTC and ISO format string e.g. 'YYYY-MM-DDTHH:MM:SSZ'.
end_time: End time of forcing in UTC and ISO format string e.g. 'YYYY-MM-DDTHH:MM:SSZ'.
shape: Path to a shape file. Used for spatial selection.
**model_specific_options: Model specific recipe settings. See `https://ewatercycle.readtherdocs.io/forcing_generate_options`_.
Returns:
Forcing object, e.g. :obj:`.lisflood.LisfloodForcing`
"""
constructor = FORCING_CLASSES.get(target_model, None)
if constructor is None:
raise NotImplementedError(f'Target model `{target_model}` is not supported by the eWatercycle forcing generator')
if model_specific_options is None:
model_specific_options = {}
forcing_info = constructor.generate(dataset, start_time, end_time, shape, **model_specific_options)
forcing_info.save()
return forcing_info


def load(directory):
def load(directory: str) -> DefaultForcing:
"""Load previously generated or imported forcing data.
Args:
directory: forcing data directory; must contain `ewatercycle_forcing.yaml`
directory: forcing data directory; must contain
`ewatercycle_forcing.yaml` file
Returns:
Forcing object, e.g. :obj:`.marrmot.MarrmotForcing`
Returns: Forcing object
"""
yaml = YAML()
source = Path(directory) / 'ewatercycle_forcing.yaml'
Expand All @@ -75,17 +45,19 @@ def load_foreign(target_model,
"""Load existing forcing data generated from an external source.
Args:
target_model: Name of the hydrological model for which the forcing will be used
start_time: Start time of forcing in UTC and ISO format string e.g. 'YYYY-MM-DDTHH:MM:SSZ'.
end_time: End time of forcing in UTC and ISO format string e.g. 'YYYY-MM-DDTHH:MM:SSZ'.
target_model: Name of the hydrological model for which the forcing will
be used
start_time: Start time of forcing in UTC and ISO format string e.g.
'YYYY-MM-DDTHH:MM:SSZ'.
end_time: End time of forcing in UTC and ISO format string e.g.
'YYYY-MM-DDTHH:MM:SSZ'.
directory: forcing data directory
shape: Path to a shape file. Used for spatial selection.
forcing_info: Model specific information about forcing
data. For each model you can see the available information fields
at `https://ewatercycle.readtherdocs.io/forcing_load_info`_.
forcing_info: Dictionary with model-specific information about forcing
data. See below for the available options for each model.
Returns:
Forcing object, e.g. :obj:`.hype.HypeForcing`
Forcing object
Examples:
Expand Down Expand Up @@ -120,16 +92,66 @@ def load_foreign(target_model,
'PrefixES0': 'es.nc',
'PrefixET0': 'et.nc'
})
Model-specific forcing info:
"""
constructor = FORCING_CLASSES.get(target_model, None)
if constructor is None:
raise NotImplementedError(
f'Target model `{target_model}` is not supported by the eWatercycle forcing generator')
f'Target model `{target_model}` is not supported by the '
'eWatercycle forcing generator.')
if forcing_info is None:
forcing_info = {}
return constructor(start_time, end_time, directory, shape, **forcing_info) # type: ignore # each subclass can have different forcing_info
return constructor( # type: ignore # each subclass can have different forcing_info
start_time=start_time,
end_time=end_time,
directory=directory,
shape=shape,
**forcing_info,
)


def generate(target_model: str,
dataset: str,
start_time: str,
end_time: str,
shape: str,
model_specific_options: Optional[Dict] = None) -> DefaultForcing:
"""Generate forcing data with ESMValTool.
Args:
target_model: Name of the model
dataset: Name of the source dataset. See :py:mod:`~.datasets`.
start_time: Start time of forcing in UTC and ISO format string e.g.
'YYYY-MM-DDTHH:MM:SSZ'.
end_time: End time of forcing in UTC and ISO format string e.g.
'YYYY-MM-DDTHH:MM:SSZ'.
shape: Path to a shape file. Used for spatial selection.
model_specific_options: Dictionary with model-specific recipe settings.
See below for the available options for each model.
Returns:
Forcing object
Model-specific options that can be passed to `generate`:
"""
constructor = FORCING_CLASSES.get(target_model, None)
if constructor is None:
raise NotImplementedError(
f'Target model `{target_model}` is not supported by the '
'eWatercycle forcing generator')
if model_specific_options is None:
model_specific_options = {}
forcing_info = constructor.generate(dataset, start_time, end_time, shape,
**model_specific_options)
forcing_info.save()
return forcing_info


# Append docstrings of with model-specific options to existing docstring
load_foreign.__doc__ += "".join( # type:ignore
[f"\n {k}: {v.__init__.__doc__}" for k, v in FORCING_CLASSES.items()])

# TODO fix time conventions
# TODO add / fix tests
# TODO make sure model classes understand new forcing data objects
generate.__doc__ += "".join( # type:ignore
[f"\n {k}: {v.generate.__doc__}" for k, v in FORCING_CLASSES.items()])
56 changes: 56 additions & 0 deletions ewatercycle/forcing/_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Forcing related functionality for default models"""

from pathlib import Path
from typing import Optional

from ruamel.yaml import YAML


class DefaultForcing:
"""Container for forcing data.
Args:
dataset: Name of the source dataset. See :py:data:`.DATASETS`.
start_time: Start time of forcing in UTC and ISO format string e.g.
'YYYY-MM-DDTHH:MM:SSZ'.
end_time: End time of forcing in UTC and ISO format string e.g.
'YYYY-MM-DDTHH:MM:SSZ'.
shape: Path to a shape file. Used for spatial selection.
"""
def __init__(self,
start_time: str,
end_time: str,
directory: str,
shape: Optional[str] = None):
self.start_time = start_time
self.end_time = end_time
self.directory = directory
self.shape = shape

@classmethod
def generate(
cls,
dataset: str,
start_time: str,
end_time: str,
shape: str,
**model_specific_options,
) -> 'DefaultForcing':
"""Generate forcing data with ESMValTool."""
raise NotImplementedError("No default forcing generator available.")

def save(self):
"""Export forcing data for later use."""
yaml = YAML()
yaml.register_class(self.__class__)
target = Path(self.directory) / 'ewatercycle_forcing.yaml'
# TODO remove directory or set to .
with open(target, 'w') as f:
yaml.dump(self, f)
return target

def plot(self):
raise NotImplementedError("No generic plotting method available.")

def __eq__(self, other):
return self.__dict__ == other.__dict__
42 changes: 18 additions & 24 deletions ewatercycle/forcing/hype.py → ewatercycle/forcing/_hype.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,35 @@
"""Forcing related functionality for hype"""

from dataclasses import dataclass
from pathlib import Path
from typing import Optional

from esmvalcore.experimental import get_recipe

from .datasets import DATASETS
from .default import DefaultForcing
from ..util import get_time

GENERATE_DOCS = """Hype does not have model specific options."""
LOAD_DOCS = """Hype does not have model specific info."""
from ._default import DefaultForcing
from .datasets import DATASETS


@dataclass
class HypeForcing(DefaultForcing):
"""Container for hype forcing data."""

# Model-specific attributes (preferably with default values):
# ...
def __init__(
self,
start_time: str,
end_time: str,
directory: str,
shape: Optional[str] = None,
):
"""
None: Hype does not have model-specific load options.
"""
super().__init__(start_time, end_time, directory, shape)

@classmethod
def generate( # type: ignore
cls,
dataset: str,
start_time: str,
end_time: str,
shape: str
) -> 'HypeForcing':
"""Generate HypeForcing with ESMValTool.
Args:
dataset: Name of the source dataset. See :py:data:`.DATASETS`.
start_time: Start time of forcing in UTC and ISO format string e.g. 'YYYY-MM-DDTHH:MM:SSZ'.
end_time: End time of forcing in UTC and ISO format string e.g. 'YYYY-MM-DDTHH:MM:SSZ'.
shape: Path to a shape file. Used for spatial selection.
cls, dataset: str, start_time: str, end_time: str,
shape: str) -> 'HypeForcing':
"""
None: Hype does not have model-specific generate options.
"""
# load the ESMValTool recipe
recipe_name = "hydrology/recipe_hype.yml"
Expand Down Expand Up @@ -68,7 +62,7 @@ def generate( # type: ignore
forcing_path = '/foobar.txt'

forcing_file = Path(forcing_path).name
directory = str(Path(forcing_path).parent)
directory = str(Path(forcing_file).parent)

# instantiate forcing object based on generated data
return HypeForcing(directory=directory,
Expand Down
Loading

0 comments on commit a7c8d3a

Please sign in to comment.