diff --git a/docs/conf.py b/docs/conf.py index a3eee88c..ec0b280a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -206,6 +206,9 @@ def setup(app): 'cftime', 'dask', 'esmvalcore', + 'fiona', + 'dateutil', + 'shapely', 'hydrostats', 'matplotlib', 'numpy', @@ -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'), +] diff --git a/ewatercycle/forcing/__init__.py b/ewatercycle/forcing/__init__.py index c8fc2e68..42871f8d 100644 --- a/ewatercycle/forcing/__init__.py +++ b/ewatercycle/forcing/__init__.py @@ -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' @@ -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: @@ -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()]) diff --git a/ewatercycle/forcing/_default.py b/ewatercycle/forcing/_default.py new file mode 100644 index 00000000..b3094660 --- /dev/null +++ b/ewatercycle/forcing/_default.py @@ -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__ diff --git a/ewatercycle/forcing/hype.py b/ewatercycle/forcing/_hype.py similarity index 69% rename from ewatercycle/forcing/hype.py rename to ewatercycle/forcing/_hype.py index 68aef87c..da99670a 100644 --- a/ewatercycle/forcing/hype.py +++ b/ewatercycle/forcing/_hype.py @@ -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" @@ -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, diff --git a/ewatercycle/forcing/lisflood.py b/ewatercycle/forcing/_lisflood.py similarity index 51% rename from ewatercycle/forcing/lisflood.py rename to ewatercycle/forcing/_lisflood.py index 6a619b84..4a391ec1 100644 --- a/ewatercycle/forcing/lisflood.py +++ b/ewatercycle/forcing/_lisflood.py @@ -1,40 +1,49 @@ """Forcing related functionality for lisflood""" -from dataclasses import dataclass from pathlib import Path +from typing import Optional from esmvalcore.experimental import get_recipe +from ..util import data_files_from_recipe_output, get_extents, get_time +from ._default import DefaultForcing from .datasets import DATASETS -from .default import DefaultForcing -from ..util import get_time, get_extents, data_files_from_recipe_output - -GENERATE_DOCS = """ -Options: - extract_region (dict): Region specification, dictionary must contain `start_longitude`, - `end_longitude`, `start_latitude`, `end_latitude` -""" -LOAD_DOCS = """ -Fields: - PrefixPrecipitation: Path to a NetCDF or pcraster file with precipitation data - PrefixTavg: Path to a NetCDF or pcraster file with average temperature data - PrefixE0: Path to a NetCDF or pcraster file with potential evaporation rate from open water surface data - PrefixES0: Path to a NetCDF or pcraster file with potential evaporation rate from bare soil surface data - PrefixET0: Path to a NetCDF or pcraster file with potential (reference) evapotranspiration rate data -""" - - -@dataclass + + class LisfloodForcing(DefaultForcing): """Container for lisflood forcing data.""" - # Model-specific attributes (preferably with default values): - PrefixPrecipitation: str = 'pr.nc' - PrefixTavg: str = 'tas.nc' - PrefixE0: str = 'e0.nc' - PrefixES0: str = 'es0.nc' - PrefixET0: str = 'et0.nc' # TODO check whether start/end time are same as in the files + def __init__( + self, + start_time: str, + end_time: str, + directory: str, + shape: Optional[str] = None, + PrefixPrecipitation: Optional[str] = 'pr.nc', + PrefixTavg: Optional[str] = 'tas.nc', + PrefixE0: Optional[str] = 'e0.nc', + PrefixES0: Optional[str] = 'es0.nc', + PrefixET0: Optional[str] = 'et0.nc', + ): + """ + PrefixPrecipitation: Path to a NetCDF or pcraster file with + precipitation data + PrefixTavg: Path to a NetCDF or pcraster file with average + temperature data + PrefixE0: Path to a NetCDF or pcraster file with potential + evaporation rate from open water surface data + PrefixES0: Path to a NetCDF or pcraster file with potential + evaporation rate from bare soil surface data + PrefixET0: Path to a NetCDF or pcraster file with potential + (reference) evapotranspiration rate data + """ + super().__init__(start_time, end_time, directory, shape) + self.PrefixPrecipitation = PrefixPrecipitation + self.PrefixTavg = PrefixTavg + self.PrefixE0 = PrefixE0 + self.PrefixES0 = PrefixES0 + self.PrefixET0 = PrefixET0 @classmethod def generate( # type: ignore @@ -45,14 +54,8 @@ def generate( # type: ignore shape: str, extract_region: dict = None, ) -> 'LisfloodForcing': - """Generate LisfloodForcing 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. - extract_region: Region specification, must contain `start_longitude`, + """ + extract_region (dict): Region specification, dictionary must contain `start_longitude`, `end_longitude`, `start_latitude`, `end_latitude` TODO add regrid options so forcing can be generated for parameter set @@ -70,13 +73,14 @@ def generate( # type: ignore for preproc_name in preproc_names: recipe.data['preprocessors'][preproc_name]['extract_shape'][ 'shapefile'] = shape - recipe.data['diagnostics']['diagnostic_daily']['scripts'][ - 'script']['catchment'] = basin + recipe.data['diagnostics']['diagnostic_daily']['scripts']['script'][ + 'catchment'] = basin if extract_region is None: extract_region = get_extents(shape) for preproc_name in preproc_names: - recipe.data['preprocessors'][preproc_name]['extract_region'] = extract_region + recipe.data['preprocessors'][preproc_name][ + 'extract_region'] = extract_region recipe.data['datasets'] = [DATASETS[dataset]] @@ -99,15 +103,16 @@ def generate( # type: ignore # TODO forcing_files['e0'] = ... # instantiate forcing object based on generated data - return LisfloodForcing(directory=directory, - start_time=start_time, - end_time=end_time, - PrefixPrecipitation=forcing_files["pr"], - PrefixTavg=forcing_files["tas"], - PrefixE0=forcing_files['e0'], - PrefixES0=forcing_files['es0'], - PrefixET0=forcing_files['et0'], - ) + return LisfloodForcing( + directory=directory, + start_time=start_time, + end_time=end_time, + PrefixPrecipitation=forcing_files["pr"], + PrefixTavg=forcing_files["tas"], + PrefixE0=forcing_files['e0'], + PrefixES0=forcing_files['es0'], + PrefixET0=forcing_files['et0'], + ) def plot(self): raise NotImplementedError('Dont know how to plot') diff --git a/ewatercycle/forcing/marrmot.py b/ewatercycle/forcing/_marrmot.py similarity index 66% rename from ewatercycle/forcing/marrmot.py rename to ewatercycle/forcing/_marrmot.py index d984463f..0dc0adba 100644 --- a/ewatercycle/forcing/marrmot.py +++ b/ewatercycle/forcing/_marrmot.py @@ -1,27 +1,32 @@ """Forcing related functionality for marrmot""" -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 = """Marrmot does not have model specific options.""" -LOAD_DOCS = """ -Fields: - forcing_file: Matlab file that contains forcings for Marrmot models. See format forcing file in `model implementation `_. -""" +from ._default import DefaultForcing +from .datasets import DATASETS -@dataclass class MarrmotForcing(DefaultForcing): """Container for marrmot forcing data.""" - - # Model-specific attributes (preferably with default values): - forcing_file: str = 'marrmot.mat' + def __init__( + self, + start_time: str, + end_time: str, + directory: str, + shape: Optional[str] = None, + forcing_file: Optional[str] = 'marrmot.mat', + ): + """ + forcing_file: Matlab file that contains forcings for Marrmot + models. See format forcing file in `model implementation + `_. + """ + super().__init__(start_time, end_time, directory, shape) + self.forcing_file = forcing_file @classmethod def generate( # type: ignore @@ -31,14 +36,10 @@ def generate( # type: ignore end_time: str, shape: str, ) -> 'MarrmotForcing': - """Generate Marrmot forcing data 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. """ + None: Marrmot does not have model-specific generate options. + """ + # load the ESMValTool recipe recipe_name = "hydrology/recipe_marrmot.yml" recipe = get_recipe(recipe_name) @@ -47,8 +48,8 @@ def generate( # type: ignore basin = Path(shape).stem recipe.data['preprocessors']['daily']['extract_shape'][ 'shapefile'] = shape - recipe.data['diagnostics']['diagnostic_daily']['scripts'][ - 'script']['basin'] = basin + recipe.data['diagnostics']['diagnostic_daily']['scripts']['script'][ + 'basin'] = basin recipe.data['diagnostics']['diagnostic_daily'][ 'additional_datasets'] = [DATASETS[dataset]] diff --git a/ewatercycle/forcing/pcrglobwb.py b/ewatercycle/forcing/_pcrglobwb.py similarity index 63% rename from ewatercycle/forcing/pcrglobwb.py rename to ewatercycle/forcing/_pcrglobwb.py index 514f1a83..4801aecc 100644 --- a/ewatercycle/forcing/pcrglobwb.py +++ b/ewatercycle/forcing/_pcrglobwb.py @@ -1,37 +1,33 @@ """Forcing related functionality for pcrglobwb""" -from dataclasses import dataclass from pathlib import Path +from typing import Optional from esmvalcore.experimental import get_recipe +from ..util import data_files_from_recipe_output, get_extents, get_time +from ._default import DefaultForcing from .datasets import DATASETS -from .default import DefaultForcing -from ..util import get_time, get_extents, data_files_from_recipe_output - -GENERATE_DOCS = """ -Options: - start_time_climatology (str): Start time for the climatology data - end_time_climatology (str): End time for the climatology data - extract_region (dict): Region specification, dictionary must contain `start_longitude`, - `end_longitude`, `start_latitude`, `end_latitude` -""" -LOAD_DOCS = """ -Fields: - precipitationNC (str): Input file for precipitation data. - temperatureNC (str): Input file for temperature data. -""" - - -@dataclass + + class PCRGlobWBForcing(DefaultForcing): """Container for pcrglobwb forcing data.""" - - # Model-specific attributes (preferably with default values): - precipitationNC: str = 'pr.nc' - """Input file for precipitation data.""" - temperatureNC: str = 'tas.nc' - """Input file for temperature data.""" + def __init__( + self, + start_time: str, + end_time: str, + directory: str, + shape: Optional[str] = None, + precipitationNC: Optional[str] = 'precipitation.nc', + temperatureNC: Optional[str] = 'temperature.nc', + ): + """ + precipitationNC (str): Input file for precipitation data. + temperatureNC (str): Input file for temperature data. + """ + super().__init__(start_time, end_time, directory, shape) + self.precipitationNC = precipitationNC + self.temperatureNC = temperatureNC @classmethod def generate( # type: ignore @@ -41,20 +37,16 @@ def generate( # type: ignore end_time: str, shape: str, start_time_climatology: str, # TODO make optional, default to start_time - end_time_climatology: str, # TODO make optional, defaults to start_time + 1 year + end_time_climatology: + str, # TODO make optional, defaults to start_time + 1 year extract_region: dict = None, ) -> 'PCRGlobWBForcing': - """Generate WflowForcing data 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. - start_time_climatology: Start time for the climatology data - end_time_climatology: End time for the climatology data - extract_region: Region specification, must contain `start_longitude`, - `end_longitude`, `start_latitude`, `end_latitude` + """ + start_time_climatology (str): Start time for the climatology data + end_time_climatology (str): End time for the climatology data + extract_region (dict): Region specification, dictionary must + contain `start_longitude`, `end_longitude`, `start_latitude`, + `end_latitude` """ # load the ESMValTool recipe recipe_name = "hydrology/recipe_pcrglobwb.yml" @@ -69,8 +61,8 @@ def generate( # type: ignore 'additional_datasets'] = [DATASETS[dataset]] basin = Path(shape).stem - recipe.data['diagnostics']['diagnostic_daily']['scripts'][ - 'script']['basin'] = basin + recipe.data['diagnostics']['diagnostic_daily']['scripts']['script'][ + 'basin'] = basin if extract_region is None: extract_region = get_extents(shape) diff --git a/ewatercycle/forcing/_wflow.py b/ewatercycle/forcing/_wflow.py new file mode 100644 index 00000000..a73be15e --- /dev/null +++ b/ewatercycle/forcing/_wflow.py @@ -0,0 +1,123 @@ +"""Forcing related functionality for wflow""" +from pathlib import Path +from typing import Dict, Optional + +from esmvalcore.experimental import get_recipe + +from ..util import get_extents, get_time +from ._default import DefaultForcing +from .datasets import DATASETS + + +class WflowForcing(DefaultForcing): + """Container for wflow forcing data.""" + def __init__( + self, + start_time: str, + end_time: str, + directory: str, + shape: Optional[str] = None, + netcdfinput: Optional[str] = "inmaps.nc", + Precipitation: Optional[str] = "/pr", + EvapoTranspiration: Optional[str] = "/pet", + Temperature: Optional[str] = "/tas", + Inflow: Optional[str] = None, + ): + """ + netcdfinput (str) = "inmaps.nc": Path to forcing file." + Precipitation (str) = "/pr": Variable name of precipitation data in + input file. + EvapoTranspiration (str) = "/pet": Variable name of + evapotranspiration data in input file. + Temperature (str) = "/tas": Variable name of temperature data in + input file. + Inflow (str) = None: Variable name of inflow data in input file. + """ + super().__init__(start_time, end_time, directory, shape) + self.netcdfinput = netcdfinput + self.Precipitation = Precipitation + self.EvapoTranspiration = EvapoTranspiration + self.Temperature = Temperature + self.Inflow = Inflow + + @classmethod + def generate( # type: ignore + cls, + dataset: str, + start_time: str, + end_time: str, + shape: str, + dem_file: str, + extract_region: Dict[str, float] = None, + ) -> 'WflowForcing': + """ + dem_file (str): Name of the dem_file to use. Also defines the basin + param. + extract_region (dict): Region specification, dictionary must + contain `start_longitude`, `end_longitude`, `start_latitude`, + `end_latitude` + """ + # load the ESMValTool recipe + recipe_name = "hydrology/recipe_wflow.yml" + recipe = get_recipe(recipe_name) + + basin = Path(shape).stem + recipe.data['diagnostics']['wflow_daily']['scripts']['script'][ + 'basin'] = basin + + # model-specific updates + script = recipe.data['diagnostics']['wflow_daily']['scripts']['script'] + script['dem_file'] = dem_file + + if extract_region is None: + extract_region = get_extents(shape) + recipe.data['preprocessors']['rough_cutout'][ + 'extract_region'] = extract_region + + recipe.data['diagnostics']['wflow_daily']['additional_datasets'] = [ + DATASETS[dataset] + ] + + variables = recipe.data['diagnostics']['wflow_daily']['variables'] + var_names = 'tas', 'pr', 'psl', 'rsds', 'rsdt' + + startyear = get_time(start_time).year + for var_name in var_names: + variables[var_name]['start_year'] = startyear + + endyear = get_time(end_time).year + for var_name in var_names: + variables[var_name]['end_year'] = endyear + + # generate forcing data and retreive useful information + recipe_output = recipe.run() + forcing_data = recipe_output['wflow_daily/script'].data_files[0] + + forcing_file = forcing_data.filename + directory = str(forcing_file.parent) + + # instantiate forcing object based on generated data + return WflowForcing(directory=directory, + start_time=start_time, + end_time=end_time, + netcdfinput=forcing_file.name) + + def __str__(self): + """Nice formatting of the forcing data object.""" + return "\n".join([ + "Forcing data for Wflow", + "----------------------", + f"Directory: {self.directory}", + f"Start time: {self.start_time}", + f"End time: {self.end_time}", + f"Shapefile: {self.shape}", + f"Additional information for model config:", + f" - netcdfinput: {self.netcdfinput}", + f" - Precipitation: {self.Precipitation}", + f" - Temperature: {self.Temperature}", + f" - EvapoTranspiration: {self.EvapoTranspiration}", + f" - Inflow: {self.Inflow}", + ]) + + def plot(self): + raise NotImplementedError('Dont know how to plot') diff --git a/ewatercycle/forcing/datasets.py b/ewatercycle/forcing/datasets.py index 8571118a..70d80858 100644 --- a/ewatercycle/forcing/datasets.py +++ b/ewatercycle/forcing/datasets.py @@ -1,3 +1,8 @@ +"""Supported datasets for ESMValTool recipes. + +Currently supported: ERA5 and ERA-Interim. +""" + DATASETS = { 'ERA5': { 'dataset': 'ERA5', diff --git a/ewatercycle/forcing/default.py b/ewatercycle/forcing/default.py deleted file mode 100644 index 562bb709..00000000 --- a/ewatercycle/forcing/default.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Forcing related functionality for default models""" -from dataclasses import dataclass -from pathlib import Path -from typing import Optional, Dict - -from ruamel.yaml import YAML - - -@dataclass -class DefaultForcing: - """Container for forcing data.""" - - # Default attributes that every forcing class should have: - start_time: str - """Start time of the forcing data""" - end_time: str - """End time of the forcing data""" - directory: str = '.' - """Location where the forcing data is stored.""" - shape: Optional[str] = None - """Shape file""" - - # Model-specific attributes (preferably with default values): - # ... - - @classmethod - def generate(cls, - dataset: str, - start_time: str, - end_time: str, - shape: str, - **_kwargs: Dict # Subclasses can have additional named arguments, added to pass mypy - ) -> '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.") diff --git a/ewatercycle/forcing/wflow.py b/ewatercycle/forcing/wflow.py deleted file mode 100644 index c8aee420..00000000 --- a/ewatercycle/forcing/wflow.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Forcing related functionality for wflow""" - -from dataclasses import dataclass -from pathlib import Path -from typing import Dict, Optional - -from esmvalcore.experimental import get_recipe - -from ..util import get_extents, get_time -from .datasets import DATASETS -from .default import DefaultForcing - -GENERATE_DOCS = """ -Options: - dem_file (str): Name of the dem_file to use. Also defines the basin param. - extract_region (dict): Region specification, dictionary must contain `start_longitude`, - `end_longitude`, `start_latitude`, `end_latitude` -""" -LOAD_DOCS = """ -Fields: - netcdfinput (str): Path to forcing file. Default is "inmaps.nc" - Precipitation (str): Variable name of precipitation data in input file. Default is "/pr". - EvapoTranspiration (str): Variable name of evapotranspiration data in input file. Default is"/pet". - Temperature (str): Variable name of temperature data in input file. Default is "/tas". - Inflow Optional[str]: Variable name of inflow data in input file. -""" - - -@dataclass -class WflowForcing(DefaultForcing): - """Container for wflow forcing data.""" - - # Model-specific attributes (ideally should have defaults): - netcdfinput: Optional[str] = "inmaps.nc" - """Input file path.""" - Precipitation: Optional[str] = "/pr" - """Variable name of precipitation data in input file.""" - EvapoTranspiration: Optional[str] = "/pet" - """Variable name of evapotranspiration data in input file.""" - Temperature: Optional[str] = "/tas" - """Variable name of temperature data in input file.""" - Inflow: Optional[str] = None - """Variable name of inflow data in input file.""" - - @classmethod - def generate( # type: ignore - cls, - dataset: str, - start_time: str, - end_time: str, - shape: str, - dem_file: str, - extract_region: Optional[Dict[str, float]] = None, - ) -> "WflowForcing": - """Generate WflowForcing data 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. - extract_region: Region specification, must contain `start_longitude`, - `end_longitude`, `start_latitude`, `end_latitude` - dem_file: Name of the dem_file to use. Also defines the basin param. - """ - # load the ESMValTool recipe - recipe_name = "hydrology/recipe_wflow.yml" - recipe = get_recipe(recipe_name) - - basin = Path(shape).stem - recipe.data["diagnostics"]["wflow_daily"]["scripts"]["script"]["basin"] = basin - - # model-specific updates - script = recipe.data["diagnostics"]["wflow_daily"]["scripts"]["script"] - script["dem_file"] = dem_file - - if extract_region is None: - extract_region = get_extents(shape) - recipe.data["preprocessors"]["rough_cutout"]["extract_region"] = extract_region - - recipe.data["diagnostics"]["wflow_daily"]["additional_datasets"] = [ - DATASETS[dataset] - ] - - variables = recipe.data["diagnostics"]["wflow_daily"]["variables"] - var_names = "tas", "pr", "psl", "rsds", "rsdt" - - startyear = get_time(start_time).year - for var_name in var_names: - variables[var_name]["start_year"] = startyear - - endyear = get_time(end_time).year - for var_name in var_names: - variables[var_name]["end_year"] = endyear - - # generate forcing data and retreive useful information - recipe_output = recipe.run() - forcing_data = recipe_output["wflow_daily/script"].data_files[0] - - forcing_file = forcing_data.filename - directory = str(forcing_file.parent) - - # instantiate forcing object based on generated data - return WflowForcing( - directory=directory, - start_time=start_time, - end_time=end_time, - netcdfinput=forcing_file.name, - ) - - def __str__(self): - """Nice formatting of the forcing data object.""" - return "\n".join( - [ - "Forcing data for Wflow", - "----------------------", - f"Directory: {self.directory}", - f"Start time: {self.start_time}", - f"End time: {self.end_time}", - f"Shapefile: {self.shape}", - f"Additional information for model config:", - f" - netcdfinput: {self.netcdfinput}", - f" - Precipitation: {self.Precipitation}", - f" - Temperature: {self.Temperature}", - f" - EvapoTranspiration: {self.EvapoTranspiration}", - f" - Inflow: {self.Inflow}", - ] - ) - - def plot(self): - raise NotImplementedError("Dont know how to plot") diff --git a/ewatercycle/models/lisflood.py b/ewatercycle/models/lisflood.py index beb40b0f..2ce584cc 100644 --- a/ewatercycle/models/lisflood.py +++ b/ewatercycle/models/lisflood.py @@ -11,7 +11,7 @@ from grpc4bmi.bmi_client_singularity import BmiClientSingularity from ewatercycle import CFG -from ewatercycle.forcing.lisflood import LisfloodForcing +from ewatercycle.forcing._lisflood import LisfloodForcing from ewatercycle.models.abstract import AbstractModel from ewatercycle.parametersetdb.config import AbstractConfig from ewatercycle.util import get_time diff --git a/ewatercycle/models/marrmot.py b/ewatercycle/models/marrmot.py index 0a13ac95..7b5b842c 100644 --- a/ewatercycle/models/marrmot.py +++ b/ewatercycle/models/marrmot.py @@ -12,7 +12,7 @@ from grpc4bmi.bmi_client_singularity import BmiClientSingularity from ewatercycle import CFG -from ewatercycle.forcing.marrmot import MarrmotForcing +from ewatercycle.forcing._marrmot import MarrmotForcing from ewatercycle.models.abstract import AbstractModel from ewatercycle.util import get_time diff --git a/ewatercycle/models/pcrglobwb.py b/ewatercycle/models/pcrglobwb.py index 35ad90dc..867da0bf 100644 --- a/ewatercycle/models/pcrglobwb.py +++ b/ewatercycle/models/pcrglobwb.py @@ -1,8 +1,7 @@ -import shutil import time from os import PathLike from pathlib import Path -from typing import Any, Iterable, Optional, Tuple +from typing import Any, Iterable, Tuple import numpy as np import xarray as xr @@ -30,23 +29,20 @@ def setup( # type: ignore """Start model inside container and return config file and work dir. Args: - - - input_dir: main input directory. Relative paths in the cfg_file - should start from this directory. - - - cfg_file: path to a valid pcrglobwb configuration file, + input_dir: main input directory. Relative paths in the cfg_file + should start from this directory. + cfg_file: path to a valid pcrglobwb configuration file, typically somethig like `setup.ini`. - - - additional_input_dirs: one or more additional data directories - that the model will have access to. - - - **kwargs (optional, dict): any settings in the cfg_file that you - want to overwrite programmatically. Should be passed as a dict, - e.g. `meteoOptions = {"temperatureNC": "era5_tas_1990_2000.nc"}` - where meteoOptions is the section in which the temperatureNC option - may be found. - - Returns: Path to config file and work dir + additional_input_dirs: one or more additional data directories + that the model will have access to. + **kwargs (optional, dict): any settings in the cfg_file that you + want to overwrite programmatically. Should be passed as a dict, + e.g. `meteoOptions = {"temperatureNC": "era5_tas_1990_2000.nc"}` + where meteoOptions is the section in which the temperatureNC option + may be found. + + Returns: + Path to config file and work dir """ self._setup_work_dir() self._setup_config(cfg_file, input_dir, **kwargs) diff --git a/ewatercycle/models/wflow.py b/ewatercycle/models/wflow.py index 8f9149ad..9da3da36 100644 --- a/ewatercycle/models/wflow.py +++ b/ewatercycle/models/wflow.py @@ -14,7 +14,7 @@ from grpc4bmi.bmi_client_singularity import BmiClientSingularity from ewatercycle import CFG -from ewatercycle.forcing.wflow import WflowForcing +from ewatercycle.forcing._wflow import WflowForcing from ewatercycle.models.abstract import AbstractModel from ewatercycle.parametersetdb.config import CaseConfigParser from ewatercycle.util import get_time @@ -32,20 +32,17 @@ class WflowParameterSet: """Input folder path.""" default_config: Union[str, PathLike] """Path to (default) model configuration file consistent with `input_data`.""" - def __setattr__(self, name: str, value: Union[str, PathLike]): self.__dict__[name] = Path(value).expanduser().resolve() def __str__(self): """Nice formatting of parameter set.""" - return "\n".join( - [ - "Wflow parameterset", - "------------------", - f"Directory: {self.input_data}", - f"Default configuration file: {self.default_config}", - ] - ) + return "\n".join([ + "Wflow parameterset", + "------------------", + f"Directory: {self.input_data}", + f"Default configuration file: {self.default_config}", + ]) class Wflow(AbstractModel): @@ -61,9 +58,8 @@ class Wflow(AbstractModel): bmi (Bmi): GRPC4BMI Basic Modeling Interface object """ - available_versions = ("2020.1.1",) + available_versions = ("2020.1.1", ) """Show supported WFlow versions in eWaterCycle""" - def __init__( self, version: str, @@ -95,7 +91,8 @@ def _setup_default_config(self): cfg.set("framework", "netcdfinput", Path(forcing.netcdfinput).name) cfg.set("inputmapstacks", "Precipitation", forcing.Precipitation) - cfg.set("inputmapstacks", "EvapoTranspiration", forcing.EvapoTranspiration) + cfg.set("inputmapstacks", "EvapoTranspiration", + forcing.EvapoTranspiration) cfg.set("inputmapstacks", "Temperature", forcing.Temperature) cfg.set("run", "starttime", _iso_to_wflow(forcing.start_time)) cfg.set("run", "endtime", _iso_to_wflow(forcing.end_time)) @@ -136,7 +133,8 @@ def _setup_working_directory(self): working_directory = CFG["output_dir"] / f"wflow_{timestamp}" self.work_dir = working_directory.resolve() - shutil.copytree(src=self.parameter_set.input_data, dst=working_directory) + shutil.copytree(src=self.parameter_set.input_data, + dst=working_directory) forcing_path = Path(self.forcing.directory) / self.forcing.netcdfinput shutil.copy(src=forcing_path, dst=working_directory) @@ -161,10 +159,10 @@ def _start_container(self): " time limit (15 seconds). You may try building it with " f"`!singularity run docker://{self.docker_image}` and try " "again. Please also inform the system administrator that " - "the singularity image was missing." - ) + "the singularity image was missing.") else: - raise ValueError(f"Unknown container technology: {CFG['container_engine']}") + raise ValueError( + f"Unknown container technology: {CFG['container_engine']}") def get_value_as_xarray(self, name: str) -> xr.DataArray: """Return the value as xarray object.""" diff --git a/tests/forcing/test_default.py b/tests/forcing/test_default.py index 8ddccbed..fabb3941 100644 --- a/tests/forcing/test_default.py +++ b/tests/forcing/test_default.py @@ -1,6 +1,6 @@ import pytest -from ewatercycle.forcing import generate, load_foreign, DefaultForcing, load +from ewatercycle.forcing import generate, load_foreign def test_generate_unknown_model(sample_shape): diff --git a/tests/forcing/test_lisflood.py b/tests/forcing/test_lisflood.py index 627bcaf3..b7b11966 100644 --- a/tests/forcing/test_lisflood.py +++ b/tests/forcing/test_lisflood.py @@ -6,11 +6,12 @@ import xarray as xr from ewatercycle.forcing import generate, load -from ewatercycle.forcing.lisflood import LisfloodForcing +from ewatercycle.forcing._lisflood import LisfloodForcing def test_plot(): f = LisfloodForcing( + directory='.', start_time='1989-01-02T00:00:00Z', end_time='1999-01-02T00:00:00Z', ) @@ -22,16 +23,12 @@ def create_netcdf(var_name, filename): var = 15 + 8 * np.random.randn(2, 2, 3) lon = [[-99.83, -99.32], [-99.79, -99.23]] lat = [[42.25, 42.21], [42.63, 42.59]] - ds = xr.Dataset( - { - var_name: (["longitude", "latitude", "time"], var) - }, - coords={ - "lon": (["longitude", "latitude"], lon), - "lat": (["longitude", "latitude"], lat), - "time": pd.date_range("2014-09-06", periods=3), - } - ) + ds = xr.Dataset({var_name: (["longitude", "latitude", "time"], var)}, + coords={ + "lon": (["longitude", "latitude"], lon), + "lat": (["longitude", "latitude"], lat), + "time": pd.date_range("2014-09-06", periods=3), + }) ds.to_netcdf(filename) return DataFile(filename) @@ -74,136 +71,212 @@ def forcing(self, mock_recipe_run, sample_shape): @pytest.fixture def reference_recipe(self): return { - 'datasets': [{'dataset': 'ERA5', - 'project': 'OBS6', - 'tier': 3, - 'type': 'reanaly', - 'version': 1}], - 'diagnostics': {'diagnostic_daily': {'description': 'LISFLOOD input ' - 'preprocessor for ' - 'ERA-Interim and ERA5 ' - 'data', - 'scripts': {'script': {'catchment': 'Rhine', - 'script': 'hydrology/lisflood.py'}}, - 'variables': {'pr': {'end_year': 1999, - 'mip': 'day', - 'preprocessor': 'daily_water', - 'start_year': 1989}, - 'rsds': {'end_year': 1999, - 'mip': 'day', - 'preprocessor': 'daily_radiation', - 'start_year': 1989}, - 'tas': {'end_year': 1999, - 'mip': 'day', - 'preprocessor': 'daily_temperature', - 'start_year': 1989}, - 'tasmax': {'end_year': 1999, - 'mip': 'day', - 'preprocessor': 'daily_temperature', - 'start_year': 1989}, - 'tasmin': {'end_year': 1999, - 'mip': 'day', - 'preprocessor': 'daily_temperature', - 'start_year': 1989}, - 'tdps': {'end_year': 1999, - 'mip': 'Eday', - 'preprocessor': 'daily_temperature', - 'start_year': 1989}, - 'uas': {'end_year': 1999, - 'mip': 'day', - 'preprocessor': 'daily_windspeed', - 'start_year': 1989}, - 'vas': {'end_year': 1999, - 'mip': 'day', - 'preprocessor': 'daily_windspeed', - 'start_year': 1989}}}}, - 'documentation': {'authors': ['verhoeven_stefan', - 'kalverla_peter', - 'andela_bouwe'], - 'projects': ['ewatercycle'], - 'references': ['acknow_project']}, - 'preprocessors': {'daily_radiation': {'convert_units': {'units': 'J m-2 ' - 'day-1'}, - 'custom_order': True, - 'extract_region': {'end_latitude': 52.2, - 'end_longitude': 11.9, - 'start_latitude': 46.3, - 'start_longitude': 4.1}, - 'extract_shape': {'crop': True, - 'method': 'contains'}, - 'regrid': {'lat_offset': True, - 'lon_offset': True, - 'scheme': 'linear', - 'target_grid': '0.1x0.1'}}, - 'daily_temperature': {'convert_units': {'units': 'degC'}, - 'custom_order': True, - 'extract_region': {'end_latitude': 52.2, - 'end_longitude': 11.9, - 'start_latitude': 46.3, - 'start_longitude': 4.1}, - 'extract_shape': {'crop': True, - 'method': 'contains'}, - 'regrid': {'lat_offset': True, - 'lon_offset': True, - 'scheme': 'linear', - 'target_grid': '0.1x0.1'}}, - 'daily_water': {'convert_units': {'units': 'kg m-2 d-1'}, - 'custom_order': True, - 'extract_region': {'end_latitude': 52.2, - 'end_longitude': 11.9, - 'start_latitude': 46.3, - 'start_longitude': 4.1}, - 'extract_shape': {'crop': True, - 'method': 'contains'}, - 'regrid': {'lat_offset': True, - 'lon_offset': True, - 'scheme': 'linear', - 'target_grid': '0.1x0.1'}}, - 'daily_windspeed': {'custom_order': True, - 'extract_region': {'end_latitude': 52.2, - 'end_longitude': 11.9, - 'start_latitude': 46.3, - 'start_longitude': 4.1}, - 'extract_shape': {'crop': True, - 'method': 'contains'}, - 'regrid': {'lat_offset': True, - 'lon_offset': True, - 'scheme': 'linear', - 'target_grid': '0.1x0.1'}}, - 'general': {'custom_order': True, - 'extract_region': {'end_latitude': 52.2, - 'end_longitude': 11.9, - 'start_latitude': 46.3, - 'start_longitude': 4.1}, - 'extract_shape': {'crop': True, - 'method': 'contains'}, - 'regrid': {'lat_offset': True, - 'lon_offset': True, - 'scheme': 'linear', - 'target_grid': '0.1x0.1'} - } - } + 'datasets': [{ + 'dataset': 'ERA5', + 'project': 'OBS6', + 'tier': 3, + 'type': 'reanaly', + 'version': 1 + }], + 'diagnostics': { + 'diagnostic_daily': { + 'description': + 'LISFLOOD input ' + 'preprocessor for ' + 'ERA-Interim and ERA5 ' + 'data', + 'scripts': { + 'script': { + 'catchment': 'Rhine', + 'script': 'hydrology/lisflood.py' + } + }, + 'variables': { + 'pr': { + 'end_year': 1999, + 'mip': 'day', + 'preprocessor': 'daily_water', + 'start_year': 1989 + }, + 'rsds': { + 'end_year': 1999, + 'mip': 'day', + 'preprocessor': 'daily_radiation', + 'start_year': 1989 + }, + 'tas': { + 'end_year': 1999, + 'mip': 'day', + 'preprocessor': 'daily_temperature', + 'start_year': 1989 + }, + 'tasmax': { + 'end_year': 1999, + 'mip': 'day', + 'preprocessor': 'daily_temperature', + 'start_year': 1989 + }, + 'tasmin': { + 'end_year': 1999, + 'mip': 'day', + 'preprocessor': 'daily_temperature', + 'start_year': 1989 + }, + 'tdps': { + 'end_year': 1999, + 'mip': 'Eday', + 'preprocessor': 'daily_temperature', + 'start_year': 1989 + }, + 'uas': { + 'end_year': 1999, + 'mip': 'day', + 'preprocessor': 'daily_windspeed', + 'start_year': 1989 + }, + 'vas': { + 'end_year': 1999, + 'mip': 'day', + 'preprocessor': 'daily_windspeed', + 'start_year': 1989 + } + } + } + }, + 'documentation': { + 'authors': + ['verhoeven_stefan', 'kalverla_peter', 'andela_bouwe'], + 'projects': ['ewatercycle'], + 'references': ['acknow_project'] + }, + 'preprocessors': { + 'daily_radiation': { + 'convert_units': { + 'units': 'J m-2 ' + 'day-1' + }, + 'custom_order': True, + 'extract_region': { + 'end_latitude': 52.2, + 'end_longitude': 11.9, + 'start_latitude': 46.3, + 'start_longitude': 4.1 + }, + 'extract_shape': { + 'crop': True, + 'method': 'contains' + }, + 'regrid': { + 'lat_offset': True, + 'lon_offset': True, + 'scheme': 'linear', + 'target_grid': '0.1x0.1' + } + }, + 'daily_temperature': { + 'convert_units': { + 'units': 'degC' + }, + 'custom_order': True, + 'extract_region': { + 'end_latitude': 52.2, + 'end_longitude': 11.9, + 'start_latitude': 46.3, + 'start_longitude': 4.1 + }, + 'extract_shape': { + 'crop': True, + 'method': 'contains' + }, + 'regrid': { + 'lat_offset': True, + 'lon_offset': True, + 'scheme': 'linear', + 'target_grid': '0.1x0.1' + } + }, + 'daily_water': { + 'convert_units': { + 'units': 'kg m-2 d-1' + }, + 'custom_order': True, + 'extract_region': { + 'end_latitude': 52.2, + 'end_longitude': 11.9, + 'start_latitude': 46.3, + 'start_longitude': 4.1 + }, + 'extract_shape': { + 'crop': True, + 'method': 'contains' + }, + 'regrid': { + 'lat_offset': True, + 'lon_offset': True, + 'scheme': 'linear', + 'target_grid': '0.1x0.1' + } + }, + 'daily_windspeed': { + 'custom_order': True, + 'extract_region': { + 'end_latitude': 52.2, + 'end_longitude': 11.9, + 'start_latitude': 46.3, + 'start_longitude': 4.1 + }, + 'extract_shape': { + 'crop': True, + 'method': 'contains' + }, + 'regrid': { + 'lat_offset': True, + 'lon_offset': True, + 'scheme': 'linear', + 'target_grid': '0.1x0.1' + } + }, + 'general': { + 'custom_order': True, + 'extract_region': { + 'end_latitude': 52.2, + 'end_longitude': 11.9, + 'start_latitude': 46.3, + 'start_longitude': 4.1 + }, + 'extract_shape': { + 'crop': True, + 'method': 'contains' + }, + 'regrid': { + 'lat_offset': True, + 'lon_offset': True, + 'scheme': 'linear', + 'target_grid': '0.1x0.1' + } + } + } } def test_result(self, forcing, tmp_path): - expected = LisfloodForcing( - directory=str(tmp_path), - start_time='1989-01-02T00:00:00Z', - end_time='1999-01-02T00:00:00Z', - PrefixPrecipitation='lisflood_pr.nc', - PrefixTavg='lisflood_tas.nc', - PrefixE0='lisflood_e0.nc', - PrefixES0='lisflood_es0.nc', - PrefixET0='lisflood_et0.nc' - ) + expected = LisfloodForcing(directory=str(tmp_path), + start_time='1989-01-02T00:00:00Z', + end_time='1999-01-02T00:00:00Z', + PrefixPrecipitation='lisflood_pr.nc', + PrefixTavg='lisflood_tas.nc', + PrefixE0='lisflood_e0.nc', + PrefixES0='lisflood_es0.nc', + PrefixET0='lisflood_et0.nc') assert forcing == expected - def test_recipe_configured(self, forcing, mock_recipe_run, reference_recipe, sample_shape): + def test_recipe_configured(self, forcing, mock_recipe_run, + reference_recipe, sample_shape): actual = mock_recipe_run['data_during_run'] # Remove long description and absolute path so assert is easier actual_desc = actual['documentation']['description'] del actual['documentation']['description'] - actual_shapefile = actual['preprocessors']['general']['extract_shape']['shapefile'] + actual_shapefile = actual['preprocessors']['general']['extract_shape'][ + 'shapefile'] # Will also del other occurrences of shapefile due to extract shape object being shared between preprocessors del actual['preprocessors']['general']['extract_shape']['shapefile'] diff --git a/tests/forcing/test_marrmot.py b/tests/forcing/test_marrmot.py index 1c607566..600957ca 100644 --- a/tests/forcing/test_marrmot.py +++ b/tests/forcing/test_marrmot.py @@ -5,13 +5,15 @@ from esmvalcore.experimental.recipe_output import OutputFile from ewatercycle.forcing import generate, load, load_foreign -from ewatercycle.forcing.marrmot import MarrmotForcing +from ewatercycle.forcing._marrmot import MarrmotForcing def test_plot(): f = MarrmotForcing( + directory='.', start_time='1989-01-02T00:00:00Z', end_time='1999-01-02T00:00:00Z', + forcing_file='marrmot.mat', ) with pytest.raises(NotImplementedError): f.plot() diff --git a/tests/forcing/test_pcrglobwb.py b/tests/forcing/test_pcrglobwb.py index d48071e0..1f526e28 100644 --- a/tests/forcing/test_pcrglobwb.py +++ b/tests/forcing/test_pcrglobwb.py @@ -6,7 +6,7 @@ import xarray as xr from ewatercycle.forcing import generate -from ewatercycle.forcing.pcrglobwb import PCRGlobWBForcing +from ewatercycle.forcing._pcrglobwb import PCRGlobWBForcing def create_netcdf(var_name, filename): diff --git a/tests/forcing/test_wflow.py b/tests/forcing/test_wflow.py index d80f1e13..30c2dad9 100644 --- a/tests/forcing/test_wflow.py +++ b/tests/forcing/test_wflow.py @@ -5,7 +5,7 @@ from esmvalcore.experimental.recipe_output import DataFile from ewatercycle.forcing import generate, load -from ewatercycle.forcing.wflow import WflowForcing +from ewatercycle.forcing._wflow import WflowForcing @pytest.fixture