From 911f44901c0641b76ec93633c3ee1af57bed2c09 Mon Sep 17 00:00:00 2001 From: Peter Kalverla Date: Fri, 2 Jul 2021 14:36:51 +0200 Subject: [PATCH] Add parameterset catalogue (#118) * initial idea for parameterset catalogue * Refactor catalogue into parameter_sets module * Added some tests * Use the fixture * More tests * Even more tests * Make target_model optional * More todos * less todos * Implemented download_example_parameter_sets() + other bits and pieces * formatting * dont write ewatercycle_config to yaml output * write to previous config; add todo's * Refactor download example parameter sets * Config no longer has `.` in keys, removed dead code/test * Added CFG.save_to_file() * Move example parameter set to own files * Format with black * Added docstrings + made ParameterSet public because it is used in api notebook * Added tests * Formatting * Replace model specific parameter set with generic one * Added notebook for download_example_parameter_sets() * Moved MaskMap from parameter set to setup Fixes #121 * Added ParameterSet.supported_model_versions + AbstractModel._check_parameter_set() Needed a lot of refactoring to please mypy and to have all info together for _check_parameter_set() * Replace SubVersionCopier with get_parameter_set() * Replace `Path.is_relative_to()` with Py37 compatible implementation * Correct sphinx links * Remove unused imports They should be part fix for #124 * Use Lisflood.setup{MaskMap) in notebook * example_parameter_sets() return a dict instead of iterator * Fallback to default value when key is missing in config * Make MarrmotM14 a non-generic class * Flatten test hierarchy * Correct nesting * Raise exceptions in available_parameter_sets and return tuple * Update ewatercycle/models/abstract.py Co-authored-by: Peter Kalverla * Check model.version against model.available_versions * List class var before object vars * Remove duplicate bmi in docs * Fix tests * Update ewatercycle/models/abstract.py Co-authored-by: Peter Kalverla * Replace id with name * Add skip_existing arg to download * Revert "Fallback to default value when key is missing in config" This reverts commit f776fce4e581999c527f83022b78557871d98a02. * Fix test * Fix mypy warning Co-authored-by: Stefan Verhoeven Co-authored-by: Stefan Verhoeven --- .gitignore | 3 + docs/examples.rst | 1 + docs/examples/ewatercycle_api_notebook.ipynb | 6 +- docs/examples/lisflood.ipynb | 23 +- docs/examples/pcrglobwb.ipynb | 156 +++++++----- docs/examples/system_setup.ipynb | 239 ++++++++++++++++++ docs/examples/wflow.ipynb | 201 ++++++++------- ewatercycle/config/__init__.py | 11 +- ewatercycle/config/_config_object.py | 71 +++++- ewatercycle/config/_validated_config.py | 8 - ewatercycle/config/_validators.py | 90 +++---- ewatercycle/config/ewatercycle.yaml | 2 + ewatercycle/forcing/_lisflood.py | 10 +- ewatercycle/models/abstract.py | 53 +++- ewatercycle/models/lisflood.py | 117 ++++----- ewatercycle/models/marrmot.py | 45 ++-- ewatercycle/models/pcrglobwb.py | 56 +--- ewatercycle/models/wflow.py | 48 +--- ewatercycle/parameter_sets/__init__.py | 125 +++++++++ ewatercycle/parameter_sets/_example.py | 71 ++++++ ewatercycle/parameter_sets/_lisflood.py | 21 ++ ewatercycle/parameter_sets/_pcrglobwb.py | 21 ++ ewatercycle/parameter_sets/_wflow.py | 21 ++ ewatercycle/parameter_sets/default.py | 65 +++++ tests/config/test_config.py | 16 -- tests/models/test_abstract.py | 115 +++++++-- tests/models/test_lisflood.py | 169 ++++++++----- tests/models/test_marrmotm01.py | 33 ++- tests/models/test_marrmotm14.py | 44 ++-- tests/parameter_sets/__init__.py | 14 + .../test_default_parameterset.py | 58 +++++ tests/parameter_sets/test_example.py | 86 +++++++ tests/test_parameter_sets.py | 113 +++++++++ 33 files changed, 1551 insertions(+), 561 deletions(-) create mode 100644 docs/examples/system_setup.ipynb create mode 100644 ewatercycle/parameter_sets/__init__.py create mode 100644 ewatercycle/parameter_sets/_example.py create mode 100644 ewatercycle/parameter_sets/_lisflood.py create mode 100644 ewatercycle/parameter_sets/_pcrglobwb.py create mode 100644 ewatercycle/parameter_sets/_wflow.py create mode 100644 ewatercycle/parameter_sets/default.py create mode 100644 tests/parameter_sets/__init__.py create mode 100644 tests/parameter_sets/test_default_parameterset.py create mode 100644 tests/parameter_sets/test_example.py create mode 100644 tests/test_parameter_sets.py diff --git a/.gitignore b/.gitignore index 995969d6..ea136519 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ docs/apidocs/ .scannerwork/ .ipynb_checkpoints + +docs/examples/parameter-sets +docs/examples/ewatercycle.yaml diff --git a/docs/examples.rst b/docs/examples.rst index ea1ac30b..f91c2c3f 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -12,3 +12,4 @@ Examples PCRGlobWB Wflow Irrigation experiment + System setup diff --git a/docs/examples/ewatercycle_api_notebook.ipynb b/docs/examples/ewatercycle_api_notebook.ipynb index c0c2d0e9..682f2fcb 100644 --- a/docs/examples/ewatercycle_api_notebook.ipynb +++ b/docs/examples/ewatercycle_api_notebook.ipynb @@ -324,7 +324,7 @@ "parameter_set.doi\n", "# 'https://doi.org/10.1000/182'\n", "\n", - "parameter_set.id\n", + "parameter_set.name\n", "# 'wflow-30-min-global'\n", "\n", "parameter_set.supported_model_versions\n", @@ -576,9 +576,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.4" + "version": "3.9.5" } }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/lisflood.ipynb b/docs/examples/lisflood.ipynb index e46c0ce9..4497b008 100644 --- a/docs/examples/lisflood.ipynb +++ b/docs/examples/lisflood.ipynb @@ -34,7 +34,7 @@ "from ewatercycle import CFG\n", "import ewatercycle.models\n", "from ewatercycle.forcing import load_foreign\n", - "from ewatercycle.models.lisflood import LisfloodParameterSet" + "from ewatercycle.parameter_sets import ParameterSet\n" ] }, { @@ -70,11 +70,14 @@ "metadata": {}, "outputs": [], "source": [ - "parameterset = LisfloodParameterSet(\n", - " PathRoot='/projects/0/wtrcycle/comparison/lisflood_input/Lisflood01degree_masked',\n", - " MaskMap='/projects/0/wtrcycle/comparison/recipes_auxiliary_datasets/LISFLOOD/model_mask.nc',\n", - " config_template='/projects/0/wtrcycle/comparison/lisflood_input/settings_templates/settings_lisflood.xml',\n", - ")" + "parameterset = ParameterSet(\n", + " name='Lisflood01degree_masked',\n", + " directory='/projects/0/wtrcycle/comparison/lisflood_input/Lisflood01degree_masked',\n", + " config='/projects/0/wtrcycle/comparison/lisflood_input/settings_templates/settings_lisflood.xml',\n", + " supported_model_versions={'20.10'},\n", + " target_model='lisflood'\n", + ")\n", + "mask_map = '/projects/0/wtrcycle/comparison/recipes_auxiliary_datasets/LISFLOOD/model_mask.nc'" ] }, { @@ -202,7 +205,9 @@ ], "source": [ "# setup model\n", - "config_file, config_dir = model.setup(IrrigationEfficiency='0.8', end_time='1990-10-10T00:00:00Z')" + "config_file, config_dir = model.setup(IrrigationEfficiency='0.8', \n", + " end_time='1990-10-10T00:00:00Z'\n", + " MaskMap=mask_map)" ] }, { @@ -851,9 +856,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.2" + "version": "3.9.5" } }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/examples/pcrglobwb.ipynb b/docs/examples/pcrglobwb.ipynb index 7ea9e1e6..c381f4ef 100644 --- a/docs/examples/pcrglobwb.ipynb +++ b/docs/examples/pcrglobwb.ipynb @@ -20,24 +20,6 @@ "We will combine them into one `PCRGloBWBParameterSet` object, which we can then pass to the model when we first create it." ] }, - { - "cell_type": "code", - "execution_count": 1, - "id": "30450976", - "metadata": {}, - "outputs": [], - "source": [ - "from ewatercycle.parametersetdb import build_from_urls\n", - "\n", - "# Obtain an example case for testing the model\n", - "parameterset = build_from_urls(\n", - " config_format='ini', config_url='https://raw.githubusercontent.com/UU-Hydro/PCR-GLOBWB_input_example/master/ini_and_batch_files_for_pcrglobwb_course/rhine_meuse_30min_using_input_example/setup_natural_test.ini',\n", - " datafiles_format='svn', datafiles_url='https://github.com/UU-Hydro/PCR-GLOBWB_input_example/trunk/RhineMeuse30min',\n", - ")\n", - "parameterset.save_datafiles('./pcrglobwb_example_case')\n", - "parameterset.save_config('./pcrglobwb_example_case/setup.ini')" - ] - }, { "cell_type": "code", "execution_count": 1, @@ -48,61 +30,105 @@ "name": "stderr", "output_type": "stream", "text": [ - "/home/peter/miniconda3/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/_warnings.py:18: UserWarning: \n", + "/home/verhoes/miniconda39/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/_warnings.py:18: UserWarning: \n", " Thank you for trying out the new ESMValCore API.\n", " Note that this API is experimental and may be subject to change.\n", " More info: https://github.com/ESMValGroup/ESMValCore/issues/498\n", - "/home/peter/miniconda3/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/config/_config_validators.py:254: ESMValToolDeprecationWarning: `write_plots` will be removed in 2.4.0.\n", - "/home/peter/miniconda3/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/config/_config_validators.py:255: ESMValToolDeprecationWarning: `write_netcdf` will be removed in 2.4.0.\n" + "/home/verhoes/miniconda39/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/config/_config_validators.py:254: ESMValToolDeprecationWarning: `write_plots` will be removed in 2.4.0.\n", + "/home/verhoes/miniconda39/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/config/_config_validators.py:255: ESMValToolDeprecationWarning: `write_netcdf` will be removed in 2.4.0.\n", + "/home/verhoes/miniconda39/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/config/_validated_config.py:80: MissingConfigParameter: `drs` is not defined (https://docs.esmvaltool.org/projects/ESMValCore/en/latest/quickstart/configure.html)\n" ] } ], "source": [ "import ewatercycle\n", "import ewatercycle.models\n", - "import ewatercycle.forcing" + "import ewatercycle.forcing\n", + "import ewatercycle.parameter_sets" ] }, { "cell_type": "code", "execution_count": 2, - "id": "b8825726", + "id": "de4e592f-937a-458b-892b-81084b6507fd", "metadata": {}, "outputs": [], "source": [ "ewatercycle.CFG['container_engine'] = 'singularity' # or 'singularity'\n", "ewatercycle.CFG['singularity_dir'] = './'\n", - "ewatercycle.CFG['output_dir'] = './'" + "ewatercycle.CFG['output_dir'] = './'\n", + "ewatercycle.CFG['ewatercycle_config'] = './ewatercycle.yaml'\n", + "ewatercycle.CFG['parameterset_dir'] = './parameter-sets'" ] }, { "cell_type": "code", "execution_count": 3, - "id": "8b863a7b", + "id": "f31f988f-08c6-4ea7-a910-fa7d4485d25d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Wflow parameter set\n", - "-------------------\n", - "Directory: /home/peter/ewatercycle/ewatercycle/examples/pcrglobwb_example_case\n", - "Default configuration file: /home/peter/ewatercycle/ewatercycle/examples/pcrglobwb_example_case/setup.ini\n" + "Downloading example parameter set wflow_rhine_sbm_nc to /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/wflow_rhine_sbm_nc...\n", + "Download complete.\n", + "Adding parameterset wflow_rhine_sbm_nc to ewatercycle.CFG... \n", + "Downloading example parameter set pcrglobwb_example_case to /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/pcrglobwb_example_case...\n", + "Download complete.\n", + "Adding parameterset pcrglobwb_example_case to ewatercycle.CFG... \n", + "Downloading example parameter set lisflood_fraser to /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/lisflood_fraser...\n", + "Download complete.\n", + "Adding parameterset lisflood_fraser to ewatercycle.CFG... \n", + "3 example parameter sets were downloaded\n", + "Config written to /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/ewatercycle.yaml\n", + "Saved parameter sets to configuration file /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/ewatercycle.yaml\n" ] } ], "source": [ - "parameterset = ewatercycle.models.pcrglobwb.PCRGlobWBParameterSet(\n", - " input_dir = './pcrglobwb_example_case', \n", - " default_config = './pcrglobwb_example_case/setup.ini'\n", - ")\n", - "print(parameterset)" + "ewatercycle.parameter_sets.download_example_parameter_sets()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "96661b81-778b-46ef-a390-60e84adf9b0e", + "metadata": {}, + "outputs": [], + "source": [ + "ewatercycle.CFG.reload()" ] }, { "cell_type": "code", "execution_count": 4, + "id": "86e7a979-095b-4941-ab68-42205fcf4514", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parameter set\n", + "-------------\n", + "name='pcrglobwb_example_case'\n", + "directory=PosixPath('/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/pcrglobwb_example_case')\n", + "config=PosixPath('/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/pcrglobwb_example_case/setup_natural_test.ini')\n", + "doi='N/A'\n", + "target_model='pcrglobwb'\n", + "supported_model_versions={'setters'}\n" + ] + } + ], + "source": [ + "parameter_set = ewatercycle.parameter_sets.get_parameter_set('pcrglobwb_example_case')\n", + "print(parameter_set)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, "id": "f57a6f04", "metadata": {}, "outputs": [ @@ -112,7 +138,7 @@ "text": [ "Forcing data for PCRGlobWB\n", "--------------------------\n", - "Directory: ./pcrglobwb_example_case/forcing\n", + "Directory: ./parameter-sets/pcrglobwb_example_case/forcing\n", "Start time: 2001-01-01T00:00:00Z\n", "End time: 2010-12-31T00:00:00Z\n", "Shapefile: None\n", @@ -127,7 +153,7 @@ " target_model = \"pcrglobwb\",\n", " start_time = \"2001-01-01T00:00:00Z\",\n", " end_time = \"2010-12-31T00:00:00Z\",\n", - " directory = \"./pcrglobwb_example_case/forcing\",\n", + " directory = \"./parameter-sets/pcrglobwb_example_case/forcing\",\n", " shape = None,\n", " forcing_info = dict(\n", " precipitationNC = \"precipitation_2001to2010.nc\",\n", @@ -138,17 +164,17 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "id": "ad624243", "metadata": {}, "outputs": [], "source": [ - "pcrglob = ewatercycle.models.PCRGlobWB(version=\"setters\", parameter_set=parameterset, forcing=forcing)" + "pcrglob = ewatercycle.models.PCRGlobWB(version=\"setters\", parameter_set=parameter_set, forcing=forcing)" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "id": "e5d681b5", "metadata": {}, "outputs": [ @@ -161,7 +187,7 @@ " ('max_spinups_in_years', '20')]" ] }, - "execution_count": 6, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -172,7 +198,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "id": "328e0ef2", "metadata": {}, "outputs": [ @@ -180,17 +206,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "Running docker://ewatercycle/pcrg-grpc4bmi:setters singularity container on port 59387\n" + "Running docker://ewatercycle/pcrg-grpc4bmi:setters singularity container on port 35435\n" ] }, { "data": { "text/plain": [ - "(PosixPath('/home/peter/ewatercycle/ewatercycle/examples/pcrglobwb_20210618_160443/pcrglobwb_ewatercycle.ini'),\n", - " PosixPath('/home/peter/ewatercycle/ewatercycle/examples/pcrglobwb_20210618_160443'))" + "(PosixPath('/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/pcrglobwb_20210629_134506/pcrglobwb_ewatercycle.ini'),\n", + " PosixPath('/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/pcrglobwb_20210629_134506'))" ] }, - "execution_count": 7, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -203,7 +229,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "id": "1f0a2404", "metadata": {}, "outputs": [ @@ -216,7 +242,7 @@ " ('max_spinups_in_years', '5')]" ] }, - "execution_count": 8, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -227,7 +253,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "id": "d468b126", "metadata": {}, "outputs": [], @@ -237,7 +263,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "id": "241c5817", "metadata": {}, "outputs": [ @@ -343,7 +369,7 @@ " 'surface_water_abstraction_volume')" ] }, - "execution_count": 10, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -354,7 +380,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "id": "709a9b83", "metadata": {}, "outputs": [ @@ -380,7 +406,7 @@ " 0., nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan])" ] }, - "execution_count": 11, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -391,7 +417,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "id": "329b7d77", "metadata": {}, "outputs": [ @@ -781,7 +807,7 @@ " * latitude (latitude) float64 46.25 46.75 47.25 47.75 ... 51.25 51.75 52.25\n", " time object 2000-12-31 00:00:00\n", "Attributes:\n", - " units: m3.s-1
    • longitude
      (longitude)
      float64
      3.75 4.25 4.75 ... 11.25 11.75
      array([ 3.75,  4.25,  4.75,  5.25,  5.75,  6.25,  6.75,  7.25,  7.75,  8.25,\n",
      +       "        8.75,  9.25,  9.75, 10.25, 10.75, 11.25, 11.75])
    • latitude
      (latitude)
      float64
      46.25 46.75 47.25 ... 51.75 52.25
      array([46.25, 46.75, 47.25, 47.75, 48.25, 48.75, 49.25, 49.75, 50.25, 50.75,\n",
      +       "       51.25, 51.75, 52.25])
    • time
      ()
      object
      2000-12-31 00:00:00
      array(cftime.DatetimeGregorian(2000, 12, 31, 0, 0, 0, 0), dtype=object)
  • units :
    m3.s-1
  • " ], "text/plain": [ "\n", @@ -846,7 +872,7 @@ " units: m3.s-1" ] }, - "execution_count": 12, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -857,7 +883,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "id": "cbb4a75d", "metadata": {}, "outputs": [ @@ -888,23 +914,23 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "id": "d9b57cf2", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 14, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
    " ] @@ -922,7 +948,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "id": "a835ef4f", "metadata": {}, "outputs": [], @@ -956,7 +982,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.2" + "version": "3.9.5" } }, "nbformat": 4, diff --git a/docs/examples/system_setup.ipynb b/docs/examples/system_setup.ipynb new file mode 100644 index 00000000..706983c0 --- /dev/null +++ b/docs/examples/system_setup.ipynb @@ -0,0 +1,239 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# System setup\n", + "\n", + "To use ewatercycle package you need to setup the system with software and data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "logging.basicConfig(level=logging.INFO)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from ewatercycle import CFG\n", + "from ewatercycle.parameter_sets import download_example_parameter_sets\n", + "from ewatercycle.parameter_sets import available_parameter_sets\n", + "from ewatercycle.parameter_sets import get_parameter_set" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration\n", + "\n", + "The ewatercycle package simplifies the API by reading some of the directories and other configurations from a configuration file.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:ewatercycle.config._config_object:Config written to /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/ewatercycle.yaml\n" + ] + }, + { + "data": { + "text/plain": [ + "PosixPath('/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/ewatercycle.yaml')" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "CFG['parameterset_dir'] = './parameter-sets'\n", + "CFG['ewatercycle_config'] = './ewatercycle.yaml'\n", + "CFG.save_to_file()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "CFG.load_from_file('./ewatercycle.yaml')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Download example parameter sets\n", + "\n", + "To quickly run the models it is advised to setup a example parameter sets for each model. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:ewatercycle.parameter_sets._example:Downloading example parameter set wflow_rhine_sbm_nc to /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/wflow_rhine_sbm_nc...\n", + "INFO:ewatercycle.parameter_sets._example:Download complete.\n", + "INFO:ewatercycle.parameter_sets._example:Adding parameterset wflow_rhine_sbm_nc to ewatercycle.CFG... \n", + "INFO:ewatercycle.parameter_sets._example:Downloading example parameter set pcrglobwb_example_case to /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/pcrglobwb_example_case...\n", + "INFO:ewatercycle.parameter_sets._example:Download complete.\n", + "INFO:ewatercycle.parameter_sets._example:Adding parameterset pcrglobwb_example_case to ewatercycle.CFG... \n", + "INFO:ewatercycle.parameter_sets._example:Downloading example parameter set lisflood_fraser to /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/lisflood_fraser...\n", + "INFO:ewatercycle.parameter_sets._example:Download complete.\n", + "INFO:ewatercycle.parameter_sets._example:Adding parameterset lisflood_fraser to ewatercycle.CFG... \n", + "INFO:ewatercycle.parameter_sets:3 example parameter sets were downloaded\n", + "INFO:ewatercycle.config._config_object:Config written to /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/ewatercycle.yaml\n", + "INFO:ewatercycle.parameter_sets:Saved parameter sets to configuration file /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/ewatercycle.yaml\n" + ] + } + ], + "source": [ + "download_example_parameter_sets()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Example parameter sets have been downloaded and added to the configuration file." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "container_engine: null\n", + "esmvaltool_config: None\n", + "grdc_location: None\n", + "output_dir: None\n", + "parameter_sets:\n", + " lisflood_fraser:\n", + " config: lisflood_fraser/settings_lat_lon-Run.xml\n", + " directory: lisflood_fraser\n", + " doi: N/A\n", + " supported_model_versions: !!set {'20.10': null}\n", + " target_model: lisflood\n", + " pcrglobwb_example_case:\n", + " config: pcrglobwb_example_case/setup_natural_test.ini\n", + " directory: pcrglobwb_example_case\n", + " doi: N/A\n", + " supported_model_versions: !!set {setters: null}\n", + " target_model: pcrglobwb\n", + " wflow_rhine_sbm_nc:\n", + " config: wflow_rhine_sbm_nc/wflow_sbm_NC.ini\n", + " directory: wflow_rhine_sbm_nc\n", + " doi: N/A\n", + " supported_model_versions: !!set {2020.1.1: null}\n", + " target_model: wflow\n", + "parameterset_dir: /home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets\n", + "singularity_dir: None\n" + ] + } + ], + "source": [ + "!cat ./ewatercycle.yaml" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('wflow_rhine_sbm_nc', 'pcrglobwb_example_case', 'lisflood_fraser')" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "available_parameter_sets()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ParameterSet(name='pcrglobwb_example_case', directory=PosixPath('/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/pcrglobwb_example_case'), config=PosixPath('/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/pcrglobwb_example_case/setup_natural_test.ini'), doi='N/A', target_model='pcrglobwb', supported_model_versions={'setters'})" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "parameter_set = get_parameter_set('pcrglobwb_example_case')\n", + "parameter_set" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `parameter_set` variable can be passed to a model class constructor." + ] + } + ], + "metadata": { + "interpreter": { + "hash": "4f63c3f017d58640bc2174e5f1b6c1610e3d96c1a3fe90d1d439f265cee739e3" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/examples/wflow.ipynb b/docs/examples/wflow.ipynb index 291e8b86..3412909a 100644 --- a/docs/examples/wflow.ipynb +++ b/docs/examples/wflow.ipynb @@ -8,32 +8,98 @@ "# Running Wflow using the ewatercycle system" ] }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d204fc95", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/verhoes/miniconda39/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/_warnings.py:18: UserWarning: \n", + " Thank you for trying out the new ESMValCore API.\n", + " Note that this API is experimental and may be subject to change.\n", + " More info: https://github.com/ESMValGroup/ESMValCore/issues/498\n", + "/home/verhoes/miniconda39/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/config/_config_validators.py:254: ESMValToolDeprecationWarning: `write_plots` will be removed in 2.4.0.\n", + "/home/verhoes/miniconda39/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/config/_config_validators.py:255: ESMValToolDeprecationWarning: `write_netcdf` will be removed in 2.4.0.\n", + "/home/verhoes/miniconda39/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/config/_validated_config.py:80: MissingConfigParameter: `drs` is not defined (https://docs.esmvaltool.org/projects/ESMValCore/en/latest/quickstart/configure.html)\n" + ] + } + ], + "source": [ + "import ewatercycle\n", + "import ewatercycle.models\n", + "import ewatercycle.parameter_sets" + ] + }, { "cell_type": "markdown", "id": "59bf99be", "metadata": {}, "source": [ "### 1. Copy an example case\n", - "To run WFlow, we need a complete parameterset. The easiest way to obtain a valid model configuration is by copying it from https://github.com/openstreams/wflow/raw/master/examples/. We can use `ewatercycle.parametersetdb` to easily copy on of these example cases to a folder called `./wflow_example_case`." + "To run WFlow, we need a complete parameterset. The package can download a example parameter set for you." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "fd86b376", + "metadata": {}, + "outputs": [], + "source": [ + "ewatercycle.CFG['container_engine'] = \"docker\"\n", + "ewatercycle.CFG['output_dir'] = \"./\"\n", + "ewatercycle.CFG['ewatercycle_config'] = './ewatercycle.yaml'\n", + "ewatercycle.CFG['parameterset_dir'] = './parameter-sets'" ] }, { "cell_type": "code", "execution_count": null, - "id": "f14a0d48", - "metadata": { - "tags": [] - }, + "id": "ead5e58b-3bdc-4f32-a5f6-fa5f02f2622e", + "metadata": {}, "outputs": [], "source": [ - "from ewatercycle.parametersetdb import build_from_urls\n", - "\n", - "# Obtain an example case for testing the model\n", - "parameterset = build_from_urls(\n", - " config_format='ini', config_url='https://github.com/openstreams/wflow/raw/master/examples/wflow_rhine_sbm_nc/wflow_sbm_NC.ini',\n", - " datafiles_format='svn', datafiles_url='https://github.com/openstreams/wflow/trunk/examples/wflow_rhine_sbm_nc',\n", - ")\n", - "parameterset.save_datafiles('./wflow_example_case_nc')" + "ewatercycle.parameter_sets.download_example_parameter_sets()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d7885d5a-101d-4bb9-9b6d-3518b6030bd4", + "metadata": {}, + "outputs": [], + "source": [ + "ewatercycle.CFG.reload()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "26b19af8-5cc4-4d66-9535-fb343bafb85c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parameter set\n", + "-------------\n", + "name='wflow_rhine_sbm_nc'\n", + "directory=PosixPath('/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/wflow_rhine_sbm_nc')\n", + "config=PosixPath('/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/parameter-sets/wflow_rhine_sbm_nc/wflow_sbm_NC.ini')\n", + "doi='N/A'\n", + "target_model='wflow'\n", + "supported_model_versions={'2020.1.1'}\n" + ] + } + ], + "source": [ + "parameter_set = ewatercycle.parameter_sets.get_parameter_set('wflow_rhine_sbm_nc')\n", + "print(parameter_set)" ] }, { @@ -68,79 +134,28 @@ }, { "cell_type": "code", - "execution_count": 1, - "id": "d204fc95", + "execution_count": 6, + "id": "956419c8-2769-4ddb-a429-e640b10c3f38", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/peter/miniconda3/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/_warnings.py:18: UserWarning: \n", - " Thank you for trying out the new ESMValCore API.\n", - " Note that this API is experimental and may be subject to change.\n", - " More info: https://github.com/ESMValGroup/ESMValCore/issues/498\n", - "/home/peter/miniconda3/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/config/_config_validators.py:254: ESMValToolDeprecationWarning: `write_plots` will be removed in 2.4.0.\n", - "/home/peter/miniconda3/envs/ewatercycle/lib/python3.9/site-packages/esmvalcore/experimental/config/_config_validators.py:255: ESMValToolDeprecationWarning: `write_netcdf` will be removed in 2.4.0.\n" - ] - }, { "data": { "text/plain": [ "('2020.1.1',)" ] }, - "execution_count": 1, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import ewatercycle\n", - "import ewatercycle.models\n", "ewatercycle.models.Wflow.available_versions" ] }, { "cell_type": "code", - "execution_count": 2, - "id": "fd86b376", - "metadata": {}, - "outputs": [], - "source": [ - "ewatercycle.CFG['container_engine'] = \"docker\"\n", - "ewatercycle.CFG['output_dir'] = \"./\"" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "a3111da4", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Wflow parameterset\n", - "------------------\n", - "Directory: /home/peter/ewatercycle/ewatercycle/examples/wflow_example_case_nc\n", - "Default configuration file: /home/peter/ewatercycle/ewatercycle/examples/wflow_example_case_nc/wflow_sbm_NC.ini\n" - ] - } - ], - "source": [ - "# Load a parameterset\n", - "parameter_set = ewatercycle.models.wflow.WflowParameterSet(\n", - " input_data = \"./wflow_example_case_nc/\",\n", - " default_config = \"./wflow_example_case_nc/wflow_sbm_NC.ini\",\n", - ")\n", - "print(parameter_set)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, + "execution_count": 7, "id": "081309cd", "metadata": {}, "outputs": [ @@ -150,7 +165,7 @@ "text": [ "Forcing data for Wflow\n", "----------------------\n", - "Directory: ./wflow_example_case_nc\n", + "Directory: ./parameter-sets/wflow_rhine_sbm_nc\n", "Start time: 1991-01-01T00:00:00Z\n", "End time: 1991-12-31T00:00:00Z\n", "Shapefile: None\n", @@ -167,7 +182,7 @@ "# Load forcing data from an external source\n", "import ewatercycle.forcing\n", "forcing = ewatercycle.forcing.load_foreign(\n", - " directory = \"./wflow_example_case_nc\",\n", + " directory = \"./parameter-sets/wflow_rhine_sbm_nc\",\n", " target_model = 'wflow',\n", " start_time = '1991-01-01T00:00:00Z',\n", " end_time = '1991-12-31T00:00:00Z',\n", @@ -184,7 +199,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "id": "1a321c6b", "metadata": {}, "outputs": [ @@ -194,7 +209,7 @@ "[('start_time', '1991-01-01T00:00:00Z'), ('end_time', '1991-12-31T00:00:00Z')]" ] }, - "execution_count": 5, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -207,7 +222,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 9, "id": "00c7a657", "metadata": { "tags": [] @@ -217,7 +232,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Running docker://ewatercycle/wflow-grpc4bmi:2020.1.1 singularity container on port 47585\n" + "Running docker://ewatercycle/wflow-grpc4bmi:2020.1.1 singularity container on port 52441\n" ] } ], @@ -228,7 +243,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 10, "id": "f1c78f7c", "metadata": {}, "outputs": [ @@ -236,8 +251,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "/home/peter/ewatercycle/ewatercycle/examples/wflow_20210614_114036/wflow_ewatercycle.ini\n", - "/home/peter/ewatercycle/ewatercycle/examples/wflow_20210614_114036\n" + "/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/wflow_20210629_135436/wflow_ewatercycle.ini\n", + "/home/verhoes/git/eWaterCycle/ewatercycle/docs/examples/wflow_20210629_135436\n" ] } ], @@ -274,7 +289,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 11, "id": "38e516e4", "metadata": {}, "outputs": [], @@ -285,7 +300,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 12, "id": "61b07b8b", "metadata": {}, "outputs": [ @@ -295,7 +310,7 @@ "array([-999., -999., -999., ..., -999., -999., -999.])" ] }, - "execution_count": 9, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -307,7 +322,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 13, "id": "8aac3b30", "metadata": { "tags": [] @@ -680,13 +695,13 @@ " * latitude (latitude) float64 45.89 45.93 45.97 46.0 ... 51.98 52.02 52.05\n", " time object 1990-12-31 00:00:00\n", "Attributes:\n", - " units: m/s
  • units :
    m/s
  • " ], "text/plain": [ "\n", @@ -764,7 +779,7 @@ " units: m/s" ] }, - "execution_count": 10, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -776,7 +791,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 14, "id": "2487e640", "metadata": {}, "outputs": [ @@ -855,23 +870,23 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 15, "id": "32f9ed0f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 12, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
    " ] @@ -890,7 +905,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 16, "id": "aafce78a", "metadata": {}, "outputs": [], @@ -925,7 +940,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.2" + "version": "3.9.5" } }, "nbformat": 4, diff --git a/ewatercycle/config/__init__.py b/ewatercycle/config/__init__.py index 27f703ac..d7d1d9b5 100644 --- a/ewatercycle/config/__init__.py +++ b/ewatercycle/config/__init__.py @@ -42,7 +42,7 @@ >>> CFG['output_dir'] = 123 InvalidConfigParameter: Key `output_dir`: Expected a path, but got 123 -By default, the config is loaded from the default location (i.e. ``~/.config/.ewatercycle/ewatercycle.yaml``). +By default, the config is loaded from the default location (i.e. ``~/.config/ewatercycle/ewatercycle.yaml``). If it does not exist, it falls back to the default values. to load a different file: @@ -62,8 +62,8 @@ The configuration is loaded from: - 1. ``~/$XDG_CONFIG_HOME/.ewatercycle/ewatercycle.yaml`` - 2. ``~/.config/.ewatercycle/ewatercycle.yaml`` + 1. ``~/$XDG_CONFIG_HOME/ewatercycle/ewatercycle.yaml`` + 2. ``~/.config/ewatercycle/ewatercycle.yaml`` 3. ``/etc/ewatercycle.yaml`` 4. Fall back to empty configuration @@ -81,9 +81,12 @@ wflow.docker_images: ewatercycle/wflow-grpc4bmi:2020.1.1 """ -from ._config_object import CFG, Config +from ._config_object import CFG, Config, SYSTEM_CONFIG, USER_HOME_CONFIG, DEFAULT_CONFIG __all__ = [ 'CFG', 'Config', + 'DEFAULT_CONFIG', + 'SYSTEM_CONFIG', + 'USER_HOME_CONFIG' ] diff --git a/ewatercycle/config/_config_object.py b/ewatercycle/config/_config_object.py index bf7e0555..098a3d46 100644 --- a/ewatercycle/config/_config_object.py +++ b/ewatercycle/config/_config_object.py @@ -1,15 +1,18 @@ """Importable config object.""" import os -from datetime import datetime +from io import StringIO +from logging import getLogger from pathlib import Path -from typing import Union, Optional +from typing import Union, Optional, TextIO -from ruamel import yaml +from ruamel.yaml import YAML from ._validators import _validators from ._validated_config import ValidatedConfig +logger = getLogger(__name__) + class Config(ValidatedConfig): """Configuration object. @@ -67,6 +70,52 @@ def reload(self) -> None: filename = self.get('ewatercycle_config', DEFAULT_CONFIG) self.load_from_file(filename) + def dump_to_yaml(self) -> str: + """Dumps YAML formatted string of Config object + """ + stream = StringIO() + self._save_to_stream(stream) + return stream.getvalue() + + def _save_to_stream(self, stream: TextIO): + cp = self.copy() + + # Exclude own path from dump + cp.pop("ewatercycle_config", None) + + cp["esmvaltool_config"] = str(cp["esmvaltool_config"]) + cp["grdc_location"] = str(cp["grdc_location"]) + cp["singularity_dir"] = str(cp["singularity_dir"]) + cp["output_dir"] = str(cp["output_dir"]) + cp["parameterset_dir"] = str(cp["parameterset_dir"]) + + yaml = YAML(typ='safe') + yaml.dump(cp, stream) + + def save_to_file(self, config_file: Optional[Union[os.PathLike, str]] = None): + """Write conf object to a file. + + Args: + config_file: File to write configuration object to. + If not given then will try to use `CFG['ewatercycle_config']` location + and if `CFG['ewatercycle_config']` is not set then will use the location in users home directory. + """ + # Exclude own path from dump + old_config_file = self.get("ewatercycle_config", None) + + if config_file is None: + config_file = USER_HOME_CONFIG if old_config_file is None else old_config_file + + if config_file == DEFAULT_CONFIG: + raise PermissionError(f'Not allowed to write to {config_file}', config_file) + + with open(config_file, "w") as f: + self._save_to_stream(f) + + logger.info(f"Config written to {config_file}") + + return config_file + def read_config_file(config_file: Union[os.PathLike, str]) -> dict: """Read config user file and store settings in a dictionary.""" @@ -75,15 +124,16 @@ def read_config_file(config_file: Union[os.PathLike, str]) -> dict: raise IOError(f'Config file `{config_file}` does not exist.') with open(config_file, 'r') as file: - cfg = yaml.safe_load(file) + yaml = YAML(typ='safe') + cfg = yaml.load(file) return cfg -def find_user_config(sources: tuple, filename: str) -> Optional[os.PathLike]: +def find_user_config(sources: tuple) -> Optional[os.PathLike]: """Find user config in list of source directories.""" for source in sources: - user_config = source / filename + user_config = source if user_config.exists(): return user_config return None @@ -91,12 +141,15 @@ def find_user_config(sources: tuple, filename: str) -> Optional[os.PathLike]: FILENAME = 'ewatercycle.yaml' +USER_HOME_CONFIG = Path.home() / os.environ.get('XDG_CONFIG_HOME', '.config') / 'ewatercycle' / FILENAME +SYSTEM_CONFIG = Path('/etc') / FILENAME + SOURCES = ( - Path.home() / os.environ.get('XDG_CONFIG_HOME', '.config') / '.ewatercycle', - Path('/etc'), + USER_HOME_CONFIG, + SYSTEM_CONFIG ) -USER_CONFIG = find_user_config(SOURCES, FILENAME) +USER_CONFIG = find_user_config(SOURCES) DEFAULT_CONFIG = Path(__file__).parent / FILENAME CFG_DEFAULT = Config._load_default_config(DEFAULT_CONFIG) diff --git a/ewatercycle/config/_validated_config.py b/ewatercycle/config/_validated_config.py index 6a94aa8e..3ecdbf68 100644 --- a/ewatercycle/config/_validated_config.py +++ b/ewatercycle/config/_validated_config.py @@ -81,11 +81,3 @@ def copy(self): def clear(self): """Clear Config.""" self._mapping.clear() - - def get_subset(self, key: str) -> dict: - """Return a dict with the subset of this config dict whose keys - start with `{key}.` - """ - pattern_re = re.compile(f'^{key}\.') - return dict((pattern_re.sub('', key), value) - for key, value in self.items() if pattern_re.search(key)) diff --git a/ewatercycle/config/_validators.py b/ewatercycle/config/_validators.py index cfa13b63..34a80237 100644 --- a/ewatercycle/config/_validators.py +++ b/ewatercycle/config/_validators.py @@ -20,24 +20,26 @@ def _make_type_validator(cls, *, allow_none=False): Return a validator that converts inputs to *cls* or raises (and possibly allows ``None`` as well). """ + def validator(inp): looks_like_none = isinstance(inp, str) and (inp.lower() == "none") - if (allow_none and (inp is None or looks_like_none)): + if allow_none and (inp is None or looks_like_none): return None try: return cls(inp) except ValueError as err: if isinstance(cls, type): raise ValidationError( - f'Could not convert {repr(inp)} to {cls.__name__}' + f"Could not convert {repr(inp)} to {cls.__name__}" ) from err raise validator.__name__ = f"validate_{cls.__name__}" if allow_none: validator.__name__ += "_or_None" - validator.__qualname__ = (validator.__qualname__.rsplit(".", 1)[0] + "." + - validator.__name__) + validator.__qualname__ = ( + validator.__qualname__.rsplit(".", 1)[0] + "." + validator.__name__ + ) return validator @@ -46,17 +48,17 @@ def validator(inp): # the the 'Python Software Foundation License' # (https://www.python.org/psf/license) @lru_cache() -def _listify_validator(scalar_validator, - allow_stringlist=False, - *, - n_items=None, - docstring=None): +def _listify_validator( + scalar_validator, allow_stringlist=False, *, n_items=None, docstring=None +): """Apply the validator to a list.""" + def func(inp): if isinstance(inp, str): try: inp = [ - scalar_validator(val.strip()) for val in inp.split(',') + scalar_validator(val.strip()) + for val in inp.split(",") if val.strip() ] except Exception: @@ -64,37 +66,44 @@ def func(inp): # Sometimes, a list of colors might be a single string # of single-letter colornames. So give that a shot. inp = [ - scalar_validator(val.strip()) for val in inp + scalar_validator(val.strip()) + for val in inp if val.strip() ] else: raise # Allow any ordered sequence type -- generators, np.ndarray, pd.Series # -- but not sets, whose iteration order is non-deterministic. - elif isinstance(inp, - Iterable) and not isinstance(inp, (set, frozenset)): + elif isinstance(inp, Iterable) and not isinstance( + inp, (set, frozenset) + ): # The condition on this list comprehension will preserve the # behavior of filtering out any empty strings (behavior was # from the original validate_stringlist()), while allowing # any non-string/text scalar values such as numbers and arrays. inp = [ - scalar_validator(val) for val in inp + scalar_validator(val) + for val in inp if not isinstance(val, str) or val ] else: raise ValidationError( - f"Expected str or other non-set iterable, but got {inp}") + f"Expected str or other non-set iterable, but got {inp}" + ) if n_items is not None and len(inp) != n_items: - raise ValidationError(f"Expected {n_items} values, " - f"but there are {len(inp)} values in {inp}") + raise ValidationError( + f"Expected {n_items} values, " + f"but there are {len(inp)} values in {inp}" + ) return inp try: func.__name__ = "{}list".format(scalar_validator.__name__) except AttributeError: # class instance. func.__name__ = "{}List".format(type(scalar_validator).__name__) - func.__qualname__ = func.__qualname__.rsplit(".", - 1)[0] + "." + func.__name__ + func.__qualname__ = ( + func.__qualname__.rsplit(".", 1)[0] + "." + func.__name__ + ) if docstring is not None: docstring = scalar_validator.__doc__ func.__doc__ = docstring @@ -115,36 +124,31 @@ def validate_path(value, allow_none=False): validate_string = _make_type_validator(str) validate_string_or_none = _make_type_validator(str, allow_none=True) -validate_stringlist = _listify_validator(validate_string, - docstring='Return a list of strings.') +validate_stringlist = _listify_validator( + validate_string, docstring="Return a list of strings." +) validate_int = _make_type_validator(int) validate_int_or_none = _make_type_validator(int, allow_none=True) validate_float = _make_type_validator(float) -validate_floatlist = _listify_validator(validate_float, - docstring='Return a list of floats.') +validate_floatlist = _listify_validator( + validate_float, docstring="Return a list of floats." +) validate_path_or_none = _make_type_validator(validate_path, allow_none=True) -validate_pathlist = _listify_validator(validate_path, - docstring='Return a list of paths.') +validate_pathlist = _listify_validator( + validate_path, docstring="Return a list of paths." +) + +validate_dict_parameterset = _make_type_validator(dict, allow_none=True) _validators = { - 'esmvaltool_config': validate_path_or_none, - 'grdc_location': validate_path_or_none, - 'container_engine': validate_string_or_none, - 'singularity_dir': validate_path_or_none, - 'output_dir': validate_path_or_none, - 'ewatercycle_config': validate_path_or_none, - # wflow specific - 'wflow.singularity_image': validate_string_or_none, - 'wflow.docker_image': validate_string_or_none, - # marrmot specific - 'marrmot.singularity_image': validate_string_or_none, - 'marrmot.docker_image': validate_string_or_none, - # lisflood specific - 'lisflood.singularity_image': validate_string_or_none, - 'lisflood.docker_image': validate_string_or_none, - # pcrglobwb specific - 'pcrglobwb.singularity_image': validate_string_or_none, - 'pcrglobwb.docker_image': validate_string_or_none, + "esmvaltool_config": validate_path_or_none, + "grdc_location": validate_path_or_none, + "container_engine": validate_string_or_none, + "singularity_dir": validate_path_or_none, + "output_dir": validate_path_or_none, + "parameterset_dir": validate_path_or_none, + "parameter_sets": validate_dict_parameterset, + "ewatercycle_config": validate_path_or_none, } diff --git a/ewatercycle/config/ewatercycle.yaml b/ewatercycle/config/ewatercycle.yaml index c54121f3..05b1d752 100644 --- a/ewatercycle/config/ewatercycle.yaml +++ b/ewatercycle/config/ewatercycle.yaml @@ -3,3 +3,5 @@ grdc_location: null container_engine: null singularity_dir: null output_dir: null +parameterset_dir: null +parameter_sets: null diff --git a/ewatercycle/forcing/_lisflood.py b/ewatercycle/forcing/_lisflood.py index 4a391ec1..1e8afadd 100644 --- a/ewatercycle/forcing/_lisflood.py +++ b/ewatercycle/forcing/_lisflood.py @@ -20,11 +20,11 @@ def __init__( 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: str = 'pr.nc', + PrefixTavg: str = 'tas.nc', + PrefixE0: str = 'e0.nc', + PrefixES0: str = 'es0.nc', + PrefixET0: str = 'et0.nc', ): """ PrefixPrecipitation: Path to a NetCDF or pcraster file with diff --git a/ewatercycle/models/abstract.py b/ewatercycle/models/abstract.py index ea742287..47c85a86 100644 --- a/ewatercycle/models/abstract.py +++ b/ewatercycle/models/abstract.py @@ -1,20 +1,39 @@ +import logging from abc import ABCMeta, abstractmethod from os import PathLike -from typing import Tuple, Iterable, Any +from typing import Tuple, Iterable, Any, TypeVar, Generic, Optional, ClassVar, Set import numpy as np import xarray as xr from basic_modeling_interface import Bmi +from ewatercycle.forcing import DefaultForcing +from ewatercycle.parameter_sets import ParameterSet -class AbstractModel(metaclass=ABCMeta): +logger = logging.getLogger(__name__) + +ForcingT = TypeVar('ForcingT', bound=DefaultForcing) + + +class AbstractModel(Generic[ForcingT], metaclass=ABCMeta): """Abstract class of a eWaterCycle model. - Attributes - bmi (Bmi): Basic Modeling Interface object """ - def __init__(self): + available_versions: ClassVar[Tuple[str, ...]] = tuple() + """Versions of model that are available in this class""" + + def __init__(self, + version: str, + parameter_set: ParameterSet = None, + forcing: Optional[ForcingT] = None, + ): + self.version = version + self.parameter_set = parameter_set + self.forcing: Optional[ForcingT] = forcing + self._check_version() + self._check_parameter_set() self.bmi: Bmi = None # bmi should set in setup() before calling its methods + """Basic Modeling Interface object""" @abstractmethod def setup(self, *args, **kwargs) -> Tuple[PathLike, PathLike]: @@ -95,7 +114,8 @@ def set_value_at_coords(self, name: str, lat: Iterable[float], lon: Iterable[flo indices = np.array(indices) self.bmi.set_value_at_indices(name, indices, values) - def _coords_to_indices(self, name: str, lat: Iterable[float], lon: Iterable[float]) -> Tuple[Iterable[int], Iterable[float], Iterable[float]]: + def _coords_to_indices(self, name: str, lat: Iterable[float], lon: Iterable[float]) -> Tuple[ + Iterable[int], Iterable[float], Iterable[float]]: """Converts lat/lon values to index. Args: @@ -150,3 +170,24 @@ def time_step(self) -> float: def output_var_names(self) -> Iterable[str]: """List of a model's output variables.""" return self.bmi.get_output_var_names() + + def _check_parameter_set(self): + if not self.parameter_set: + # Nothing to check + return + model_name = self.__class__.__name__.lower() + if model_name != self.parameter_set.target_model: + raise ValueError(f'Parameter set has wrong target model, ' + f'expected {model_name} got {self.parameter_set.target_model}') + if self.parameter_set.supported_model_versions == set(): + logger.info(f'Model version {self.version} is not explicitly listed in the supported model versions ' + f'of this parameter set. This can lead to compatibility issues.') + elif self.version not in self.parameter_set.supported_model_versions: + raise ValueError( + f'Parameter set is not compatible with version {self.version} of model, ' + f'parameter set only supports {self.parameter_set.supported_model_versions}') + + def _check_version(self): + if self.version not in self.available_versions: + raise ValueError(f'Supplied version {self.version} is not supported by this model. ' + f'Available versions are {self.available_versions}.') diff --git a/ewatercycle/models/lisflood.py b/ewatercycle/models/lisflood.py index b69c3b1a..56725262 100644 --- a/ewatercycle/models/lisflood.py +++ b/ewatercycle/models/lisflood.py @@ -1,8 +1,7 @@ import time import xml.etree.ElementTree as ET -from dataclasses import dataclass from pathlib import Path -from typing import Any, Iterable, List, Tuple, Union +from typing import Any, Iterable, Tuple import numpy as np import xarray as xr @@ -13,36 +12,12 @@ from ewatercycle import CFG from ewatercycle.forcing._lisflood import LisfloodForcing from ewatercycle.models.abstract import AbstractModel +from ewatercycle.parameter_sets import ParameterSet from ewatercycle.parametersetdb.config import AbstractConfig from ewatercycle.util import get_time, find_closest_point -@dataclass -class LisfloodParameterSet: - """Input files specific for parameter_set, model boundaries, and configuration template files - - Example: - - .. code-block:: - - parameter_set = LisfloodParameterSet( - PathRoot='/projects/0/wtrcycle/comparison/lisflood_input/Lisflood01degree_masked', - MaskMap='/projects/0/wtrcycle/comparison/recipes_auxiliary_datasets/LISFLOOD/model_mask.nc', - config_template='/projects/0/wtrcycle/comparison/lisflood_input/settings_templates/settings_lisflood.xml', - ) - """ - PathRoot: Path - """Directory with input files""" - MaskMap: Path - """A NetCDF file with model boundaries""" - config_template: Path - """Config file used as template for a lisflood run""" - - def __setattr__(self, name: str, value: Union[str, Path]): - self.__dict__[name] = Path(value).expanduser().resolve() - - -class Lisflood(AbstractModel): +class Lisflood(AbstractModel[LisfloodForcing]): """eWaterCycle implementation of Lisflood hydrological model. Args: @@ -50,22 +25,17 @@ class Lisflood(AbstractModel): parameter_set: LISFLOOD input files. Any included forcing data will be ignored. forcing: a LisfloodForcing object. - Attributes: - bmi (Bmi): Basic Modeling Interface object - Example: See examples/lisflood.ipynb in `ewatercycle repository `_ """ - available_versions = ["20.10"] + available_versions = ("20.10",) """Versions for which ewatercycle grpc4bmi docker images are available.""" - def __init__(self, version: str, parameter_set: LisfloodParameterSet, forcing: LisfloodForcing): + def __init__(self, version: str, parameter_set: ParameterSet, forcing: LisfloodForcing): """Construct Lisflood model with initial values. """ - super().__init__() - self.version = version + super().__init__(version, parameter_set, forcing) self._check_forcing(forcing) - self.parameter_set = parameter_set - self.cfg = XmlConfig(self.parameter_set.config_template) + self.cfg = XmlConfig(parameter_set.config) def _set_docker_image(self): images = { @@ -88,13 +58,14 @@ def _get_textvar_value(self, name: str): f'Name {name} not found in the config file.' ) - # unable to subclass with more specialized arguments so ignore type def setup(self, # type: ignore IrrigationEfficiency: str = None, start_time: str = None, end_time: str = None, - work_dir: Path = None) -> Tuple[Path, Path]: + MaskMap: str = None, + work_dir: Path = None + ) -> Tuple[Path, Path]: """Configure model run 1. Creates config file and config directory based on the forcing variables and time range @@ -104,25 +75,36 @@ def setup(self, # type: ignore IrrigationEfficiency: Field application irrigation efficiency max 1, ~0.90 drip irrigation, ~0.75 sprinkling start_time: Start time of model in UTC and ISO format string e.g. 'YYYY-MM-DDTHH:MM:SSZ'. If not given then forcing start time is used. end_time: End time of model in UTC and ISO format string e.g. 'YYYY-MM-DDTHH:MM:SSZ'. If not given then forcing end time is used. + MaskMap: Mask map to use instead of one supplied in parameter set. + Path to a NetCDF or pcraster file with same dimensions as parameter set map files and a boolean variable. work_dir: a working directory given by user or created for user. Returns: Path to config file and path to config directory """ - #TODO forcing can be a part of parameter_set + # TODO forcing can be a part of parameter_set work_dir = _generate_workdir(work_dir) - config_file = self._create_lisflood_config(work_dir, start_time, end_time, IrrigationEfficiency) + config_file = self._create_lisflood_config(work_dir, start_time, end_time, IrrigationEfficiency, MaskMap) + + assert self.parameter_set is not None + input_dirs = [ + str(self.parameter_set.directory), + str(self.forcing_dir) + ] + if MaskMap is not None: + mask_map = Path(MaskMap).expanduser().resolve() + try: + mask_map.relative_to(self.parameter_set.directory) + except ValueError: + # If not relative add dir + input_dirs.append(str(mask_map.parent)) if CFG['container_engine'].lower() == 'singularity': self._set_singularity_image(CFG['singularity_dir']) self.bmi = BmiClientSingularity( image=str(self.singularity_image), - input_dirs=[ - str(self.parameter_set.PathRoot), - str(self.parameter_set.MaskMap.parent), - str(self.forcing_dir) - ], + input_dirs=input_dirs, work_dir=str(work_dir), ) elif CFG['container_engine'].lower() == 'docker': @@ -130,11 +112,7 @@ def setup(self, # type: ignore self.bmi = BmiClientDocker( image=self.docker_image, image_port=55555, - input_dirs=[ - str(self.parameter_set.PathRoot), - str(self.parameter_set.MaskMap.parent), - str(self.forcing_dir) - ], + input_dirs=input_dirs, work_dir=str(work_dir), ) else: @@ -158,18 +136,21 @@ def _check_forcing(self, forcing): f"Unknown forcing type: {forcing}. Please supply a LisfloodForcing object." ) - def _create_lisflood_config(self, work_dir: Path, start_time_iso: str = None, end_time_iso: str = None, IrrigationEfficiency: str = None) -> Path: + def _create_lisflood_config(self, work_dir: Path, start_time_iso: str = None, end_time_iso: str = None, + IrrigationEfficiency: str = None, MaskMap: str = None) -> Path: """Create lisflood config file""" + assert self.parameter_set is not None + assert self.forcing is not None # overwrite dates if given if start_time_iso is not None: start_time = get_time(start_time_iso) - if self._start <= start_time <= self._end: + if self._start <= start_time <= self._end: self._start = start_time else: raise ValueError('start_time outside forcing time range') if end_time_iso is not None: end_time = get_time(end_time_iso) - if self._start <= end_time <= self._end: + if self._start <= end_time <= self._end: self._end = end_time else: raise ValueError('end_time outside forcing time range') @@ -178,14 +159,16 @@ def _create_lisflood_config(self, work_dir: Path, start_time_iso: str = None, en "CalendarDayStart": self._start.strftime("%d/%m/%Y 00:00"), "StepStart": "1", "StepEnd": str((self._end - self._start).days), - "PathRoot": str(self.parameter_set.PathRoot), - "MaskMap": self.parameter_set.MaskMap.stem, + "PathRoot": str(self.parameter_set.directory), "PathMeteo": str(self.forcing_dir), "PathOut": str(work_dir), } if IrrigationEfficiency is not None: settings['IrrigationEfficiency'] = IrrigationEfficiency + if MaskMap is not None: + mask_map = Path(MaskMap).expanduser().resolve() + settings['MaskMap'] = str(mask_map.with_suffix('')) for textvar in self.cfg.config.iter("textvar"): textvar_name = textvar.attrib["name"] @@ -241,10 +224,11 @@ def get_value_as_xarray(self, name: str) -> xr.DataArray: return da - def _coords_to_indices(self, name: str, lat: Iterable[float], lon: Iterable[float]) -> Tuple[Iterable[int], Iterable[float], Iterable[float]]: + def _coords_to_indices(self, name: str, lat: Iterable[float], lon: Iterable[float]) -> Tuple[ + Iterable[int], Iterable[float], Iterable[float]]: """Convert lat, lon coordinates into model indices.""" grid_id = self.bmi.get_var_grid(name) - shape = self.bmi.get_grid_shape(grid_id) # shape returns (len(y), len(x)) + shape = self.bmi.get_grid_shape(grid_id) # shape returns (len(y), len(x)) x_model = self.bmi.get_grid_x(grid_id) y_model = self.bmi.get_grid_y(grid_id) spacing_model = self.bmi.get_grid_spacing(grid_id) @@ -264,26 +248,29 @@ def _coords_to_indices(self, name: str, lat: Iterable[float], lon: Iterable[floa raise ValueError("This point is outside of the model grid.") indices.append(index) - lon_converted.append(round(x_model[idx], 4)) # use 4 digits in round - lat_converted.append(round(y_model[idy], 4)) # use 4 digits in round + lon_converted.append(round(x_model[idx], 4)) # use 4 digits in round + lat_converted.append(round(y_model[idy], 4)) # use 4 digits in round return np.array(indices), np.array(lon_converted), np.array(lat_converted) @property def parameters(self) -> Iterable[Tuple[str, Any]]: """List the parameters for this model.""" - #TODO fix issue #60 + assert self.parameter_set is not None + assert self.forcing is not None + # TODO fix issue #60 parameters = [ ('IrrigationEfficiency', self._get_textvar_value('IrrigationEfficiency')), - ('PathRoot', str(self.parameter_set.PathRoot)), - ('MaskMap', str(self.parameter_set.MaskMap.parent)), - ('config_template', str(self.parameter_set.config_template)), + ('PathRoot', str(self.parameter_set.directory)), + ('MaskMap', self._get_textvar_value('MaskMap')), + ('config_template', str(self.parameter_set.config)), ('start_time', self._start.strftime("%Y-%m-%dT%H:%M:%SZ")), ('end_time', self._end.strftime("%Y-%m-%dT%H:%M:%SZ")), - ('forcing directory', str(self.forcing_dir)), + ('forcing directory', str(self.forcing_dir)), ] return parameters + # TODO it needs fix regarding forcing # def reindex_forcings(mask_map: Path, forcing: LisfloodForcing, output_dir: Path = None) -> Path: # """Reindex forcing files to match mask map grid diff --git a/ewatercycle/models/marrmot.py b/ewatercycle/models/marrmot.py index 75f8a32d..776f6d6b 100644 --- a/ewatercycle/models/marrmot.py +++ b/ewatercycle/models/marrmot.py @@ -41,7 +41,7 @@ def _generate_work_dir(work_dir: Path = None) -> Path: return work_dir -class MarrmotM01(AbstractModel): +class MarrmotM01(AbstractModel[MarrmotForcing]): """eWaterCycle implementation of Marrmot Collie River 1 (traditional bucket) hydrological model. It sets MarrmotM01 parameter with an initial value that is the mean value of the range specfied in `model parameter range file `_. @@ -49,23 +49,19 @@ class MarrmotM01(AbstractModel): Args: version: pick a version for which an ewatercycle grpc4bmi docker image is available. forcing: a MarrmotForcing object. - If forcing file contains parameter and other settings, those are used and can be changed in :py:meth:`steup`. - - Attributes: - bmi (Bmi): Basic Modeling Interface object + If forcing file contains parameter and other settings, those are used and can be changed in :py:meth:`setup`. Example: See examples/marrmotM01.ipynb in `ewatercycle repository `_ """ model_name = "m_01_collie1_1p_1s" """Name of model in Matlab code.""" - available_versions = ["2020.11"] + available_versions = ("2020.11",) """Versions for which ewatercycle grpc4bmi docker images are available.""" def __init__(self, version: str, forcing: MarrmotForcing): """Construct MarrmotM01 with initial values. """ - super().__init__() - self.version = version + super().__init__(version) self._parameters = [1000.0] self.store_ini = [900.0] self.solver = Solver() @@ -262,14 +258,15 @@ def parameters(self) -> Iterable[Tuple[str, Any]]: M14_PARAMS = ('maximum_soil_moisture_storage', - 'threshold_flow_generation_evap_change', - 'leakage_saturated_zone_flow_coefficient', - 'zero_deficit_base_flow_speed', - 'baseflow_coefficient', - 'gamma_distribution_chi_parameter', - 'gamma_distribution_phi_parameter') - -class MarrmotM14(AbstractModel): + 'threshold_flow_generation_evap_change', + 'leakage_saturated_zone_flow_coefficient', + 'zero_deficit_base_flow_speed', + 'baseflow_coefficient', + 'gamma_distribution_chi_parameter', + 'gamma_distribution_phi_parameter') + + +class MarrmotM14(AbstractModel[MarrmotForcing]): """eWaterCycle implementation of Marrmot Top Model hydrological model. It sets MarrmotM14 parameter with an initial value that is the mean value of the range specfied in `model parameter range file `_. @@ -279,21 +276,17 @@ class MarrmotM14(AbstractModel): forcing: a MarrmotForcing object. If forcing file contains parameter and other settings, those are used and can be changed in :py:meth:`setup`. - Attributes: - bmi (Bmi): Basic Modeling Interface object - Example: See examples/marrmotM14.ipynb in `ewatercycle repository `_ """ model_name = "m_14_topmodel_7p_2s" """Name of model in Matlab code.""" - available_versions = ["2020.11"] + available_versions = ("2020.11",) """Versions for which ewatercycle grpc4bmi docker images are available.""" def __init__(self, version: str, forcing: MarrmotForcing): """Construct MarrmotM14 with initial values. """ - super().__init__() - self.version = version + super().__init__(version) self._parameters = [1000.0, 0.5, 0.5, 100.0, 0.5, 4.25, 2.5] self.store_ini = [900.0, 900.0] self.solver = Solver() @@ -404,12 +397,14 @@ def _check_forcing(self, forcing): if len(forcing_data['parameters']) == len(self._parameters): self._parameters = forcing_data['parameters'] else: - warnings.warn(f"The length of parameters in forcing {self.forcing_file} does not match the length of M14 parameters that is seven.") + warnings.warn( + f"The length of parameters in forcing {self.forcing_file} does not match the length of M14 parameters that is seven.") if 'store_ini' in forcing_data: if len(forcing_data['store_ini']) == len(self.store_ini): self.store_ini = forcing_data['store_ini'] else: - warnings.warn(f"The length of initial stores in forcing {self.forcing_file} does not match the length of M14 iniatial stores that is two.") + warnings.warn( + f"The length of initial stores in forcing {self.forcing_file} does not match the length of M14 iniatial stores that is two.") if 'solver' in forcing_data: forcing_solver = forcing_data['solver'] self.solver.name = forcing_solver['name'][0][0][0] @@ -503,7 +498,7 @@ def get_value_as_xarray(self, name: str) -> xr.DataArray: @property def parameters(self) -> Iterable[Tuple[str, Any]]: """List the parameters for this model.""" - p:List[Tuple[str, Any]] = list(zip(M14_PARAMS, self._parameters)) + p: List[Tuple[str, Any]] = list(zip(M14_PARAMS, self._parameters)) p += [ ('initial_upper_zone_storage', self.store_ini[0]), ('initial_saturated_zone_storage', self.store_ini[1]), diff --git a/ewatercycle/models/pcrglobwb.py b/ewatercycle/models/pcrglobwb.py index 3f9a474e..f8f4b562 100644 --- a/ewatercycle/models/pcrglobwb.py +++ b/ewatercycle/models/pcrglobwb.py @@ -1,64 +1,33 @@ import time -from dataclasses import dataclass from os import PathLike from pathlib import Path -from typing import Any, Iterable, Tuple, Union +from typing import Any, Iterable, Tuple import numpy as np import xarray as xr from cftime import num2date +from grpc import FutureTimeoutError from grpc4bmi.bmi_client_docker import BmiClientDocker from grpc4bmi.bmi_client_singularity import BmiClientSingularity from ewatercycle import CFG from ewatercycle.forcing._pcrglobwb import PCRGlobWBForcing from ewatercycle.models.abstract import AbstractModel +from ewatercycle.parameter_sets import ParameterSet from ewatercycle.parametersetdb.config import CaseConfigParser from ewatercycle.util import get_time -from grpc import FutureTimeoutError - -@dataclass -class PCRGlobWBParameterSet: - """Parameter set for the PCRGlobWB model class. - A valid pcrglobwb parameter set consists of a folder with input data files - and should always include a default configuration file. - """ - - input_dir: Union[str, PathLike] - """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 the parameterset object.""" - return "\n".join( - [ - "Wflow parameter set", - "-------------------", - f"Directory: {self.input_dir}", - f"Default configuration file: {self.default_config}", - ] - ) - - -class PCRGlobWB(AbstractModel): +class PCRGlobWB(AbstractModel[PCRGlobWBForcing]): """eWaterCycle implementation of PCRGlobWB hydrological model. Args: version: pick a version from :py:attr:`~available_versions` - parameter_set: instance of :py:class:`~PCRGlobWBParameterSet`. + parameter_set: instance of :py:class:`~ewatercycle.parameter_sets.default.ParameterSet`. forcing: ewatercycle forcing container; see :py:mod:`ewatercycle.forcing`. - Attributes: - - bmi (Bmi): GRPC4BMI Basic Modeling Interface object """ available_versions = ("setters",) @@ -66,15 +35,10 @@ class PCRGlobWB(AbstractModel): def __init__( self, version: str, - parameter_set: PCRGlobWBParameterSet, + parameter_set: ParameterSet, forcing: PCRGlobWBForcing, ): - super().__init__() - - self.version = version - self.parameter_set = parameter_set - self.forcing = forcing - + super().__init__(version, parameter_set, forcing) self._set_docker_image() self._setup_work_dir() @@ -94,8 +58,8 @@ def _setup_work_dir(self): self.work_dir = work_dir.expanduser().resolve() def _setup_default_config(self): - config_file = self.parameter_set.default_config - input_dir = self.parameter_set.input_dir + config_file = self.parameter_set.config + input_dir = self.parameter_set.directory cfg = CaseConfigParser() cfg.read(config_file) @@ -207,7 +171,7 @@ def _export_config(self) -> PathLike: def _start_container(self): additional_input_dirs = [ - str(self.parameter_set.input_dir), + str(self.parameter_set.directory), str(self.forcing.directory), ] diff --git a/ewatercycle/models/wflow.py b/ewatercycle/models/wflow.py index 9da3da36..49453738 100644 --- a/ewatercycle/models/wflow.py +++ b/ewatercycle/models/wflow.py @@ -1,10 +1,9 @@ import shutil import time -from dataclasses import dataclass from datetime import datetime from os import PathLike from pathlib import Path -from typing import Any, Iterable, Optional, Tuple, Union +from typing import Any, Iterable, Optional, Tuple import numpy as np import xarray as xr @@ -16,46 +15,19 @@ from ewatercycle import CFG from ewatercycle.forcing._wflow import WflowForcing from ewatercycle.models.abstract import AbstractModel +from ewatercycle.parameter_sets import ParameterSet from ewatercycle.parametersetdb.config import CaseConfigParser from ewatercycle.util import get_time -@dataclass -class WflowParameterSet: - """Parameter set for the Wflow model class. - - A valid wflow parameter set consists of a input data files - and should always include a default configuration file. - """ - - input_data: Union[str, PathLike] - """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}", - ]) - - -class Wflow(AbstractModel): +class Wflow(AbstractModel[WflowForcing]): """Create an instance of the Wflow model class. Args: version: pick a version from :py:attr:`~available_versions` - parameter_set: instance of :py:class:`~WflowParameterSet`. + parameter_set: instance of :py:class:`~ewatercycle.parameter_sets.default.ParameterSet`. forcing: instance of :py:class:`~WflowForcing` or None. If None, it is assumed that forcing is included with the parameter_set. - - Attributes: - bmi (Bmi): GRPC4BMI Basic Modeling Interface object """ available_versions = ("2020.1.1", ) @@ -63,15 +35,11 @@ class Wflow(AbstractModel): def __init__( self, version: str, - parameter_set: WflowParameterSet, + parameter_set: ParameterSet, forcing: Optional[WflowForcing] = None, ): - super().__init__() - self.version = version - self.parameter_set = parameter_set - self.forcing = forcing - + super().__init__(version, parameter_set, forcing) self._set_docker_image() self._setup_default_config() @@ -83,7 +51,7 @@ def _set_docker_image(self): self.docker_image = images[self.version] def _setup_default_config(self): - config_file = self.parameter_set.default_config + config_file = self.parameter_set.config forcing = self.forcing cfg = CaseConfigParser() @@ -133,7 +101,7 @@ 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, + shutil.copytree(src=self.parameter_set.directory, dst=working_directory) forcing_path = Path(self.forcing.directory) / self.forcing.netcdfinput shutil.copy(src=forcing_path, dst=working_directory) diff --git a/ewatercycle/parameter_sets/__init__.py b/ewatercycle/parameter_sets/__init__.py new file mode 100644 index 00000000..d9d178ff --- /dev/null +++ b/ewatercycle/parameter_sets/__init__.py @@ -0,0 +1,125 @@ +from itertools import chain +from logging import getLogger +from os import linesep +from typing import Dict, Tuple + +from ewatercycle import CFG +from . import _pcrglobwb, _lisflood, _wflow +from ._example import ExampleParameterSet +from .default import ParameterSet +from ..config import DEFAULT_CONFIG, SYSTEM_CONFIG, USER_HOME_CONFIG + +logger = getLogger(__name__) + + +def _parse_parametersets(): + parametersets = {} + if CFG["parameter_sets"] is None: + return [] + for name, options in CFG["parameter_sets"].items(): + parameterset = ParameterSet(name=name, **options) + parametersets[name] = parameterset + + return parametersets + + +def available_parameter_sets(target_model: str = None) -> Tuple[str, ...]: + """List available parameter sets on this machine. + + Args: + target_model: Filter parameter sets on a model name + + Returns: Names of available parameter sets on current machine. + + """ + all_parameter_sets = _parse_parametersets() + if not all_parameter_sets: + if CFG['ewatercycle_config'] == DEFAULT_CONFIG: + raise ValueError(f'No configuration file found.') + raise ValueError(f'No parameter sets defined in {CFG["ewatercycle_config"]}. ' + f'Use `ewatercycle.parareter_sets.download_example_parameter_sets` to download examples ' + f'or define your own or ask whoever setup the ewatercycle system to do it.') + # TODO explain somewhere how to add new parameter sets + filtered = tuple( + name + for name, ps in all_parameter_sets.items() + if ps.is_available and (target_model is None or ps.target_model == target_model) + ) + if not filtered: + raise ValueError(f'No parameter sets defined for {target_model} model in {CFG["ewatercycle_config"]}. ' + f'Use `ewatercycle.parareter_sets.download_example_parameter_sets` to download examples ' + f'or define your own or ask whoever setup the ewatercycle system to do it.') + return filtered + + +def get_parameter_set(name: str) -> ParameterSet: + """Get parameter set object available on this machine so it can be used in a model. + + Args: + name: Name of parameter set + + Returns: Parameter set object that can be used in an ewatercycle model constructor. + + """ + all_parameter_sets = _parse_parametersets() + + ps = all_parameter_sets.get(name) + if ps is None: + raise KeyError(f"No parameter set available with name {name}") + + if not ps.is_available: + raise ValueError(f"Cannot find parameter set with attributes {ps}") + + return ps + + +def download_parameter_sets(zenodo_doi: str, target_model: str, config: str): + # TODO add docstring + # TODO download archive matching doi from Zenodo + # TODO unpack archive in CFG['parameterset_dir'] subdirectory + # TODO print yaml snippet with target_model and config to add to ewatercycle.yaml + raise NotImplementedError("Auto download of parameter sets not yet supported") + + +def example_parameter_sets() -> Dict[str, ExampleParameterSet]: + """Lists example parameter sets that can be downloaded with :py:func:`~download_example_parameter_sets`. + """ + # TODO how to add a new model docs should be updated with this part + examples = chain( + _wflow.example_parameter_sets(), + _pcrglobwb.example_parameter_sets(), + _lisflood.example_parameter_sets(), + ) + return {e.name: e for e in examples} + + +def download_example_parameter_sets(skip_existing=True): + """Downloads all of the example parameter sets and adds them to the config_file. + + Downloads to `parameterset_dir` directory defined in :py:data:`ewatercycle.config.CFG`. + + Args: + skip_existing: When true will not download any parameter set which already has a local directory. + When false will raise ValueError exception when parameter set already exists. + + """ + examples = example_parameter_sets() + + i = 0 + for example in examples.values(): + example.download(skip_existing) + example.to_config() + i += 1 + + logger.info(f"{i} example parameter sets were downloaded") + + try: + config_file = CFG.save_to_file() + logger.info(f"Saved parameter sets to configuration file {config_file}") + except OSError as e: + raise OSError( + f"Failed to write parameter sets to configuration file. " + f"Manually save content below to {USER_HOME_CONFIG} " + f"or {SYSTEM_CONFIG} file: {linesep}" + f"{CFG.dump_to_yaml()}" + ) from e diff --git a/ewatercycle/parameter_sets/_example.py b/ewatercycle/parameter_sets/_example.py new file mode 100644 index 00000000..c1df98e2 --- /dev/null +++ b/ewatercycle/parameter_sets/_example.py @@ -0,0 +1,71 @@ +import subprocess +from logging import getLogger +from pathlib import Path +from typing import Set +from urllib import request + +from ewatercycle import CFG +from .default import ParameterSet + +logger = getLogger(__name__) + + +class ExampleParameterSet(ParameterSet): + def __init__( + self, + config_url: str, + datafiles_url: str, + name, + directory: str, + config: str, + supported_model_versions: Set[str] = None, + doi="N/A", + target_model="generic", + ): + super().__init__(name, directory, config, doi, target_model, supported_model_versions) + self.config_url = config_url + """URL where model configuration file can be downloaded""" + self.datafiles_url = datafiles_url + """GitHub subversion URL where datafiles can be svn-exported from""" + + def download(self, skip_existing=False): + if self.directory.exists(): + if not skip_existing: + raise ValueError(f"Directory {self.directory} for parameter set {self.name}" + f" already exists, will not overwrite. " + f"Try again with skip_existing=True or remove {self.directory} directory.") + + logger.info(f'Directory {self.directory} for parameter set {self.name}' + f' already exists, skipping download.') + return + logger.info( + f"Downloading example parameter set {self.name} to {self.directory}..." + ) + + subprocess.check_call(["svn", "export", self.datafiles_url, self.directory]) + # TODO replace subversion with alternative see https://stackoverflow.com/questions/33066582/how-to-download-a-folder-from-github/48948711 + response = request.urlopen(self.config_url) + self.config.write_text(response.read().decode()) + + logger.info("Download complete.") + + def to_config(self): + logger.info(f"Adding parameterset {self.name} to ewatercycle.CFG... ") + + if not CFG["parameter_sets"]: + CFG["parameter_sets"] = {} + + CFG["parameter_sets"][self.name] = dict( + directory=str(_abbreviate(self.directory)), + config=str(_abbreviate(self.config)), + doi=self.doi, + target_model=self.target_model, + supported_model_versions=self.supported_model_versions, + ) + + +def _abbreviate(path: Path): + try: + return path.relative_to(CFG["parameterset_dir"]) + except ValueError: + return path diff --git a/ewatercycle/parameter_sets/_lisflood.py b/ewatercycle/parameter_sets/_lisflood.py new file mode 100644 index 00000000..bfb37255 --- /dev/null +++ b/ewatercycle/parameter_sets/_lisflood.py @@ -0,0 +1,21 @@ +from typing import Iterable + +from ._example import ExampleParameterSet + + +def example_parameter_sets() -> Iterable[ExampleParameterSet]: + return [ + ExampleParameterSet( + # Relative to CFG['parameterset_dir'] + directory="lisflood_fraser", + name="lisflood_fraser", + # Relative to CFG['parameterset_dir'] + config="lisflood_fraser/settings_lat_lon-Run.xml", + datafiles_url="https://github.com/ec-jrc/lisflood-usecases/trunk/LF_lat_lon_UseCase", + # Raw url to config file + config_url="https://github.com/ec-jrc/lisflood-usecases/raw/master/LF_lat_lon_UseCase/settings_lat_lon-Run.xml", + doi="N/A", + target_model="lisflood", + supported_model_versions={"20.10"} + ) + ] diff --git a/ewatercycle/parameter_sets/_pcrglobwb.py b/ewatercycle/parameter_sets/_pcrglobwb.py new file mode 100644 index 00000000..60f5e26d --- /dev/null +++ b/ewatercycle/parameter_sets/_pcrglobwb.py @@ -0,0 +1,21 @@ +from typing import Iterable + +from ._example import ExampleParameterSet + + +def example_parameter_sets() -> Iterable[ExampleParameterSet]: + return [ + ExampleParameterSet( + # Relative to CFG['parameterset_dir'] + directory="pcrglobwb_example_case", + name="pcrglobwb_example_case", + # Relative to CFG['parameterset_dir'] + config="pcrglobwb_example_case/setup_natural_test.ini", + datafiles_url="https://github.com/UU-Hydro/PCR-GLOBWB_input_example/trunk/RhineMeuse30min", + # Raw url to config file + config_url="https://raw.githubusercontent.com/UU-Hydro/PCR-GLOBWB_input_example/master/ini_and_batch_files_for_pcrglobwb_course/rhine_meuse_30min_using_input_example/setup_natural_test.ini", + doi="N/A", + target_model="pcrglobwb", + supported_model_versions={"setters"} + ) + ] diff --git a/ewatercycle/parameter_sets/_wflow.py b/ewatercycle/parameter_sets/_wflow.py new file mode 100644 index 00000000..8634b524 --- /dev/null +++ b/ewatercycle/parameter_sets/_wflow.py @@ -0,0 +1,21 @@ +from typing import Iterable + +from ._example import ExampleParameterSet + + +def example_parameter_sets() -> Iterable[ExampleParameterSet]: + return [ + ExampleParameterSet( + # Relative to CFG['parameterset_dir'] + directory="wflow_rhine_sbm_nc", + name="wflow_rhine_sbm_nc", + # Relative to CFG['parameterset_dir'] + config="wflow_rhine_sbm_nc/wflow_sbm_NC.ini", + datafiles_url="https://github.com/openstreams/wflow/trunk/examples/wflow_rhine_sbm_nc", + # Raw url to config file + config_url="https://github.com/openstreams/wflow/raw/master/examples/wflow_rhine_sbm_nc/wflow_sbm_NC.ini", + doi="N/A", + target_model="wflow", + supported_model_versions={"2020.1.1"} + ) + ] diff --git a/ewatercycle/parameter_sets/default.py b/ewatercycle/parameter_sets/default.py new file mode 100644 index 00000000..dd7022c3 --- /dev/null +++ b/ewatercycle/parameter_sets/default.py @@ -0,0 +1,65 @@ +from pathlib import Path +from typing import Set, Optional + +from ewatercycle import CFG + + +class ParameterSet: + """Container object for parameter set options. + + Attributes: + name (str): Name of parameter set + directory (Path): Location on disk where files of parameter set are stored. + If Path is relative then relative to CFG['parameterset_dir']. + config (Path): Model configuration file which uses files from :py:attr:`~directory`. + If Path is relative then relative to CFG['parameterset_dir']. + doi (str): Persistent identifier of parameter set. For a example a DOI for a Zenodo record. + target_model (str): Name of model that parameter set can work with + supported_model_versions (Set[str]): Set of model versions that are supported by this parameter set. + If not set then parameter set will be supported by all versions of model + """ + + def __init__( + self, + name: str, + directory: str, + config: str, + doi="N/A", + target_model="generic", + supported_model_versions: Optional[Set[str]] = None, + ): + self.name = name + self.directory = _make_absolute(directory) + self.config = _make_absolute(config) + self.doi = doi + self.target_model = target_model + self.supported_model_versions = set() if supported_model_versions is None else supported_model_versions + + def __repr__(self): + options = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items()) + return f"ParameterSet({options})" + + def __str__(self): + """Nice formatting of parameter set.""" + return "\n".join( + [ + "Parameter set", + "-------------", + ] + + [f"{k}={v!r}" for k, v in self.__dict__.items()] + ) + + @property + def is_available(self) -> bool: + """Tests if directory and config file is available on this machine""" + return self.directory.exists() and self.config.exists() + + +def _make_absolute(input_path: str) -> Path: + pathlike = Path(input_path) + if pathlike.is_absolute(): + return pathlike + if CFG["parameterset_dir"]: + return CFG["parameterset_dir"] / pathlike + else: + raise ValueError(f'CFG["parameterset_dir"] is not set. Unable to make {input_path} relative to it') diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 2de1d026..03f5410b 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -169,19 +169,3 @@ def test_config_class(): assert isinstance(cfg['grdc_location'], Path) assert isinstance(cfg['output_dir'], Path) assert isinstance(cfg['singularity_dir'], Path) - - -def test_get_subset(): - """Test getting a subset of config params.""" - config = { - 'wflow.singularity_image': "ewatercycle-wflow-grpc4bmi.sif", - 'wflow.docker_image': "ewatercycle/wflow-grpc4bmi:latest", - } - - cfg = Config(config) - subset = cfg.get_subset('wflow') - - assert 'singularity_image' in subset - assert 'docker_image' in subset - assert 'wflow.singularity_image' not in subset - assert 'wflow.docker_image' not in subset diff --git a/tests/models/test_abstract.py b/tests/models/test_abstract.py index 28035e83..f28ab1cd 100644 --- a/tests/models/test_abstract.py +++ b/tests/models/test_abstract.py @@ -1,3 +1,4 @@ +import logging from os import PathLike from pathlib import Path from typing import Any, Iterable, Tuple @@ -9,10 +10,27 @@ from basic_modeling_interface import Bmi from numpy.testing import assert_array_equal +from ewatercycle import CFG +from ewatercycle.config import DEFAULT_CONFIG from ewatercycle.models.abstract import AbstractModel +from ewatercycle.parameter_sets import ParameterSet + + +@pytest.fixture +def setup_config(tmp_path): + CFG['parameterset_dir'] = tmp_path + CFG['ewatercycle_config'] = tmp_path / 'ewatercycle.yaml' + yield CFG + CFG['ewatercycle_config'] = DEFAULT_CONFIG + CFG.reload() class MockedModel(AbstractModel): + available_versions = ('0.4.2',) + + def __init__(self, version: str = '0.4.2', parameter_set: ParameterSet = None): + super().__init__(version, parameter_set) + def setup(self, *args, **kwargs) -> Tuple[PathLike, PathLike]: if 'bmi' in kwargs: # sub-class of AbstractModel should construct bmi @@ -24,18 +42,18 @@ def get_value_as_xarray(self, name: str) -> xr.DataArray: return xr.DataArray( data=[[1.0, 2.0], [3.0, 4.0]], coords={ - "latitude":[42.25, 42.21], - "longitude":[-99.83, -99.32], - "time":'2014-09-06'}, + "latitude": [42.25, 42.21], + "longitude": [-99.83, -99.32], + "time": '2014-09-06'}, dims=["longitude", "latitude"], name='Temperature', attrs=dict(units="degC"), ) - def _coords_to_indices(self, name: str, lat: Iterable[float], lon: Iterable[float]) -> Tuple[Iterable[int], Iterable[float], Iterable[float]]: + def _coords_to_indices(self, name: str, lat: Iterable[float], lon: Iterable[float]) -> Tuple[ + Iterable[int], Iterable[float], Iterable[float]]: return np.array([0]), np.array([-99.83]), np.array([42.25]) - @property def parameters(self) -> Iterable[Tuple[str, Any]]: return [('area', 42)] @@ -53,15 +71,23 @@ def model(bmi: Bmi): m.setup(bmi=bmi) return m + def test_construct(): with pytest.raises(TypeError) as excinfo: - AbstractModel() + AbstractModel(version='0.4.2') msg = str(excinfo.value) assert "Can't instantiate abstract class" in msg assert 'setup' in msg assert 'parameters' in msg +def test_construct_with_unsupported_version(): + with pytest.raises(ValueError) as excinfo: + MockedModel(version='1.2.3') + + assert "Supplied version 1.2.3 is not supported by this model. Available versions are ('0.4.2',)." in str(excinfo.value) + + def test_setup(model): result = model.setup() @@ -161,25 +187,80 @@ def test_time_step(bmi, model: MockedModel): def test_output_var_names(bmi, model: MockedModel): - bmi.get_output_var_names.return_value = ('discharge', ) + bmi.get_output_var_names.return_value = ('discharge',) names = model.output_var_names - assert names == ('discharge', ) + assert names == ('discharge',) def test_get_value_as_xarray(model: MockedModel): expected = xr.DataArray( - data=[[1.0, 2.0], [3.0, 4.0]], - coords={ - "latitude":[42.25, 42.21], - "longitude":[-99.83, -99.32], - "time":'2014-09-06'}, - dims=["longitude", "latitude"], - name='Temperature', - attrs=dict(units="degC"), - ) + data=[[1.0, 2.0], [3.0, 4.0]], + coords={ + "latitude": [42.25, 42.21], + "longitude": [-99.83, -99.32], + "time": '2014-09-06'}, + dims=["longitude", "latitude"], + name='Temperature', + attrs=dict(units="degC"), + ) dataarray = model.get_value_as_xarray("Temperature") xr.testing.assert_equal(dataarray, expected) + + +class TestCheckParameterSet: + def test_correct_version(self, setup_config): + ps = ParameterSet( + name='justatest', + directory='justatest', + config='justatest/config.ini', + target_model='mockedmodel', # == lowered class name + supported_model_versions={'0.4.2'} + ) + m = MockedModel(parameter_set=ps) + assert m.parameter_set == ps + + def test_wrong_model(self, setup_config): + ps = ParameterSet( + name='justatest', + directory='justatest', + config='justatest/config.ini', + target_model='wrongmodel', + supported_model_versions={'0.4.2'} + ) + with pytest.raises(ValueError) as excinfo: + MockedModel(parameter_set=ps) + + expected = 'Parameter set has wrong target model' + assert expected in str(excinfo.value) + + def test_any_version(self, caplog, setup_config): + ps = ParameterSet( + name='justatest', + directory='justatest', + config='justatest/config.ini', + target_model='mockedmodel', # == lowered class name + supported_model_versions=set() + ) + with caplog.at_level(logging.INFO): + MockedModel(parameter_set=ps) + + expected = 'is not explicitly listed in the supported model versions' + assert expected in caplog.text + + def test_unsupported_version(self, setup_config): + ps = ParameterSet( + name='justatest', + directory='justatest', + config='justatest/config.ini', + target_model='mockedmodel', + supported_model_versions={'1.2.3'} + ) + with pytest.raises(ValueError) as excinfo: + MockedModel(parameter_set=ps) + + expected = 'Parameter set is not compatible with version' + assert expected in str(excinfo.value) diff --git a/tests/models/test_lisflood.py b/tests/models/test_lisflood.py index eb31ac39..e850659d 100644 --- a/tests/models/test_lisflood.py +++ b/tests/models/test_lisflood.py @@ -2,17 +2,16 @@ from pathlib import Path from unittest.mock import patch -import pytest -from numpy.testing import assert_array_equal import numpy as np - -from grpc4bmi.bmi_client_singularity import BmiClientSingularity +import pytest from basic_modeling_interface import Bmi +from grpc4bmi.bmi_client_singularity import BmiClientSingularity +from numpy.testing import assert_array_equal from ewatercycle import CFG from ewatercycle.forcing import load_foreign -from ewatercycle.parametersetdb.datafiles import SubversionCopier -from ewatercycle.models.lisflood import Lisflood, LisfloodParameterSet, XmlConfig +from ewatercycle.models.lisflood import Lisflood, XmlConfig +from ewatercycle.parameter_sets import ParameterSet, example_parameter_sets @pytest.fixture @@ -20,50 +19,36 @@ def mocked_config(tmp_path): CFG['output_dir'] = tmp_path CFG['container_engine'] = 'singularity' CFG['singularity_dir'] = tmp_path + CFG['parameterset_dir'] = tmp_path / 'psr' + CFG['parameter_sets'] = {} class TestLFlatlonUseCase: @pytest.fixture - def parameterset(self, tmp_path): - # TODO dont let test download stuff from Internet, it is unreliable, - # copy use case files to this repo instead - source = 'https://github.com/ec-jrc/lisflood-usecases/trunk/LF_lat_lon_UseCase' - copier = SubversionCopier(source) - root = tmp_path / 'input' - copier.save(str(root)) - - mask_dir = tmp_path / 'mask' - mask_dir.mkdir() - shutil.copy( - root / 'maps' / 'masksmall.map', - mask_dir / 'model_mask', - ) - return LisfloodParameterSet( - PathRoot=root, - MaskMap=mask_dir / 'model_mask', - config_template=root / 'settings_lat_lon-Run.xml', - ) + def parameterset(self, mocked_config): + example_parameter_set = example_parameter_sets()['lisflood_fraser'] + example_parameter_set.download() + example_parameter_set.to_config() + return example_parameter_set @pytest.fixture - def generate_forcing(self, tmp_path, parameterset): + def generate_forcing(self, tmp_path, parameterset: ParameterSet): forcing_dir = tmp_path / 'forcing' forcing_dir.mkdir() - meteo_dir = Path(parameterset.PathRoot) / 'meteo' + meteo_dir = Path(parameterset.directory) / 'meteo' # Create the case where forcing data arenot part of parameter_set for file in meteo_dir.glob('*.nc'): shutil.copy(file, forcing_dir) - forcing = load_foreign(target_model='lisflood', - directory=str(forcing_dir), - start_time='1986-01-02T00:00:00Z', - end_time='2018-01-02T00:00:00Z', - forcing_info={ - 'PrefixPrecipitation': 'tp.nc', - 'PrefixTavg': 'ta.nc', - 'PrefixE0': 'e0.nc', - }) - - return forcing + return load_foreign(target_model='lisflood', + directory=str(forcing_dir), + start_time='1986-01-02T00:00:00Z', + end_time='2018-01-02T00:00:00Z', + forcing_info={ + 'PrefixPrecipitation': 'tp.nc', + 'PrefixTavg': 'ta.nc', + 'PrefixE0': 'e0.nc', + }) @pytest.fixture def model(self, parameterset, generate_forcing): @@ -77,22 +62,21 @@ def model(self, parameterset, generate_forcing): def test_default_parameters(self, model: Lisflood, tmp_path): expected_parameters = [ ('IrrigationEfficiency', '0.75'), - ('PathRoot', f'{tmp_path}/input'), - ('MaskMap', f'{tmp_path}/mask'), - ('config_template', f'{tmp_path}/input/settings_lat_lon-Run.xml'), + ('PathRoot', f'{tmp_path}/psr/lisflood_fraser'), + ('MaskMap', '$(PathMaps)/masksmall.map'), + ('config_template', f'{tmp_path}/psr/lisflood_fraser/settings_lat_lon-Run.xml'), ('start_time', '1986-01-02T00:00:00Z'), ('end_time', '2018-01-02T00:00:00Z'), ('forcing directory', f'{tmp_path}/forcing'), ] assert model.parameters == expected_parameters - @pytest.fixture def model_with_setup(self, mocked_config, model: Lisflood): with patch.object(BmiClientSingularity, '__init__', return_value=None) as mocked_constructor, patch( - 'time.strftime', return_value='42'): + 'time.strftime', return_value='42'): config_file, config_dir = model.setup( - IrrigationEfficiency = '0.8', + IrrigationEfficiency='0.8', ) return config_file, config_dir, mocked_constructor @@ -102,8 +86,7 @@ def test_setup(self, model_with_setup, tmp_path): mocked_constructor.assert_called_once_with( image=f'{tmp_path}/ewatercycle-lisflood-grpc4bmi_20.10.sif', input_dirs=[ - f'{tmp_path}/input', - f'{tmp_path}/mask', + f'{tmp_path}/psr/lisflood_fraser', f'{tmp_path}/forcing'], work_dir=f'{tmp_path}/lisflood_42') assert 'lisflood_42' in str(config_dir) @@ -130,43 +113,97 @@ def test_get_value_at_coords_multiple(self, model: Lisflood): def test_get_value_at_coords_faraway(self, model: Lisflood): model.bmi = MockedBmi() with pytest.raises(ValueError) as excinfo: - model.get_value_at_coords('Discharge', lon=[0.0], lat=[0.0]) + model.get_value_at_coords('Discharge', lon=[0.0], lat=[0.0]) msg = str(excinfo.value) assert "This point is outside of the model grid." in msg + class TestCustomMaskMap: + @pytest.fixture + def model(self, tmp_path, parameterset, generate_forcing): + # Create the case where mask map is not part of parameter_set + mask_dir = tmp_path / 'custommask' + mask_dir.mkdir() + mask_file_in_ps = parameterset.directory / 'maps/mask.map' + shutil.copy(mask_file_in_ps, mask_dir / 'mask.map') + forcing = generate_forcing + m = Lisflood(version='20.10', parameter_set=parameterset, forcing=forcing) + yield m + if m.bmi: + # Clean up container + del m.bmi + + @pytest.fixture + def model_with_setup(self, tmp_path, mocked_config, model: Lisflood): + with patch.object(BmiClientSingularity, '__init__', return_value=None) as mocked_constructor, patch( + 'time.strftime', return_value='42'): + config_file, config_dir = model.setup( + MaskMap=str(tmp_path / 'custommask/mask.map') + ) + return config_file, config_dir, mocked_constructor + + def test_setup(self, model_with_setup, tmp_path): + config_file, config_dir, mocked_constructor = model_with_setup + _cfg = XmlConfig(str(config_file)) + mocked_constructor.assert_called_once_with( + image=f'{tmp_path}/ewatercycle-lisflood-grpc4bmi_20.10.sif', + input_dirs=[ + f'{tmp_path}/psr/lisflood_fraser', + f'{tmp_path}/forcing', + f'{tmp_path}/custommask', + ], + work_dir=f'{tmp_path}/lisflood_42') + assert 'lisflood_42' in str(config_dir) + assert config_file.name == 'lisflood_setting.xml' + for textvar in _cfg.config.iter("textvar"): + textvar_name = textvar.attrib["name"] + if textvar_name == 'IrrigationEfficiency': + assert textvar.get('value') in ['0.75', '$(IrrigationEfficiency)'] + if textvar_name == 'MaskMap': + assert textvar.get('value') == f'{tmp_path}/custommask/mask' + + def test_parameters_after_setup(self, model_with_setup, model: Lisflood, tmp_path): + expected_parameters = [ + ('IrrigationEfficiency', '0.75'), + ('PathRoot', f'{tmp_path}/psr/lisflood_fraser'), + ('MaskMap', f'{tmp_path}/custommask/mask'), + ('config_template', f'{tmp_path}/psr/lisflood_fraser/settings_lat_lon-Run.xml'), + ('start_time', '1986-01-02T00:00:00Z'), + ('end_time', '2018-01-02T00:00:00Z'), + ('forcing directory', f'{tmp_path}/forcing'), + ] + assert model.parameters == expected_parameters + class MockedBmi(Bmi): """Mimic a real use case with realistic shape and abitrary high precision.""" + def get_var_grid(self, name): return 0 def get_grid_shape(self, grid_id): - return (14, 31) #shape returns (len(y), len(x)) + return 14, 31 # shape returns (len(y), len(x)) def get_grid_x(self, grid_id): - return np.array([-124.450000000003, -124.350000000003, -124.250000000003, - -124.150000000003, -124.050000000003, -123.950000000003, - -123.850000000003, -123.750000000003, -123.650000000003, - -123.550000000003, -123.450000000003, -123.350000000003, - -123.250000000003, -123.150000000003, -123.050000000003, - -122.950000000003, -122.850000000003, -122.750000000003, - -122.650000000003, -122.550000000003, -122.450000000003, - -122.350000000003, -122.250000000003, -122.150000000003, - -122.050000000003, -121.950000000003, -121.850000000003, - -121.750000000003, -121.650000000003, -121.550000000003, -121.450000000003]) + return np.array([-124.450000000003, -124.350000000003, -124.250000000003, + -124.150000000003, -124.050000000003, -123.950000000003, + -123.850000000003, -123.750000000003, -123.650000000003, + -123.550000000003, -123.450000000003, -123.350000000003, + -123.250000000003, -123.150000000003, -123.050000000003, + -122.950000000003, -122.850000000003, -122.750000000003, + -122.650000000003, -122.550000000003, -122.450000000003, + -122.350000000003, -122.250000000003, -122.150000000003, + -122.050000000003, -121.950000000003, -121.850000000003, + -121.750000000003, -121.650000000003, -121.550000000003, -121.450000000003]) def get_grid_y(self, grid_id): - return np.array([53.950000000002, 53.8500000000021, 53.7500000000021, 53.6500000000021, - 53.5500000000021, 53.4500000000021, 53.3500000000021, 53.2500000000021, - 53.1500000000021, 53.0500000000021, 52.9500000000021, 52.8500000000021, - 52.7500000000021, 52.6500000000021 ]) + return np.array([53.950000000002, 53.8500000000021, 53.7500000000021, 53.6500000000021, + 53.5500000000021, 53.4500000000021, 53.3500000000021, 53.2500000000021, + 53.1500000000021, 53.0500000000021, 52.9500000000021, 52.8500000000021, + 52.7500000000021, 52.6500000000021]) def get_grid_spacing(self, grid_id): - return (0.1, 0.1) + return 0.1, 0.1 def get_value_at_indices(self, name, indices): self.indices = indices return np.array([1.0]) - - - diff --git a/tests/models/test_marrmotm01.py b/tests/models/test_marrmotm01.py index a927bc4d..b53ac3e6 100644 --- a/tests/models/test_marrmotm01.py +++ b/tests/models/test_marrmotm01.py @@ -1,7 +1,6 @@ from datetime import datetime, timezone from pathlib import Path -import numpy as np import pytest import xarray as xr from numpy.testing import assert_almost_equal @@ -10,8 +9,7 @@ from ewatercycle import CFG from ewatercycle.forcing import load_foreign -from ewatercycle.models import MarrmotM01 -from ewatercycle.models.marrmot import Solver +from ewatercycle.models.marrmot import Solver, MarrmotM01 @pytest.fixture @@ -50,7 +48,6 @@ def model_with_setup(self, model: MarrmotM01): return model, cfg_file, cfg_dir def test_parameters(self, model, forcing_file): - expected = [ ('maximum_soil_moisture_storage', 10.0), ('initial_soil_moisture_storage', 5.0), @@ -131,12 +128,12 @@ def forcing_file(self, sample_marrmot_forcing_file): @pytest.fixture def generate_forcing(self, forcing_file): forcing = load_foreign('marrmot', - directory=str(Path(forcing_file).parent), - start_time='1989-01-01T00:00:00Z', - end_time='1992-12-31T00:00:00Z', - forcing_info={ - 'forcing_file': str(Path(forcing_file).name) - }) + directory=str(Path(forcing_file).parent), + start_time='1989-01-01T00:00:00Z', + end_time='1992-12-31T00:00:00Z', + forcing_info={ + 'forcing_file': str(Path(forcing_file).name) + }) return forcing @pytest.fixture @@ -166,8 +163,8 @@ def test_setup(self, model_with_setup): assert actual['model_name'] == "m_01_collie1_1p_1s" assert actual['parameters'] == [[1234]] assert actual['store_ini'] == [[4321]] - assert_almost_equal(actual['time_start'], [[1990, 1, 1, 0, 0, 0]]) - assert_almost_equal(actual['time_end'], [[1991, 12, 31, 0, 0, 0]]) + assert_almost_equal(actual['time_start'], [[1990, 1, 1, 0, 0, 0]]) + assert_almost_equal(actual['time_end'], [[1991, 12, 31, 0, 0, 0]]) class TestWithDatesOutsideRangeSetupAndExampleData: @@ -178,12 +175,12 @@ def forcing_file(self, sample_marrmot_forcing_file): @pytest.fixture def generate_forcing(self, forcing_file): forcing = load_foreign('marrmot', - directory=str(Path(forcing_file).parent), - start_time='1989-01-01T00:00:00Z', - end_time='1992-12-31T00:00:00Z', - forcing_info={ - 'forcing_file': str(Path(forcing_file).name) - }) + directory=str(Path(forcing_file).parent), + start_time='1989-01-01T00:00:00Z', + end_time='1992-12-31T00:00:00Z', + forcing_info={ + 'forcing_file': str(Path(forcing_file).name) + }) return forcing @pytest.fixture diff --git a/tests/models/test_marrmotm14.py b/tests/models/test_marrmotm14.py index a786a7d1..11de990f 100644 --- a/tests/models/test_marrmotm14.py +++ b/tests/models/test_marrmotm14.py @@ -1,7 +1,6 @@ from datetime import datetime, timezone from pathlib import Path -import numpy as np import pytest import xarray as xr from numpy.testing import assert_almost_equal, assert_array_equal @@ -10,8 +9,7 @@ from ewatercycle import CFG from ewatercycle.forcing import load_foreign -from ewatercycle.models import MarrmotM14 -from ewatercycle.models.marrmot import Solver +from ewatercycle.models.marrmot import Solver, MarrmotM14 @pytest.fixture @@ -26,12 +24,12 @@ def generate_forcing(self): # Downloaded from # https://github.com/wknoben/MARRMoT/blob/master/BMI/Config/BMI_testcase_m01_BuffaloRiver_TN_USA.mat forcing = load_foreign('marrmot', - directory=f'{Path(__file__).parent}/data', - start_time='1989-01-01T00:00:00Z', - end_time='1992-12-31T00:00:00Z', - forcing_info={ - 'forcing_file': 'BMI_testcase_m01_BuffaloRiver_TN_USA.mat' - }) + directory=f'{Path(__file__).parent}/data', + start_time='1989-01-01T00:00:00Z', + end_time='1992-12-31T00:00:00Z', + forcing_info={ + 'forcing_file': 'BMI_testcase_m01_BuffaloRiver_TN_USA.mat' + }) return forcing @pytest.fixture @@ -142,12 +140,12 @@ def generate_forcing(self): # Downloaded from # https://github.com/wknoben/MARRMoT/blob/master/BMI/Config/BMI_testcase_m01_BuffaloRiver_TN_USA.mat forcing = load_foreign('marrmot', - directory=f'{Path(__file__).parent}/data', - start_time='1989-01-01T00:00:00Z', - end_time='1992-12-31T00:00:00Z', - forcing_info={ - 'forcing_file': 'BMI_testcase_m01_BuffaloRiver_TN_USA.mat' - }) + directory=f'{Path(__file__).parent}/data', + start_time='1989-01-01T00:00:00Z', + end_time='1992-12-31T00:00:00Z', + forcing_info={ + 'forcing_file': 'BMI_testcase_m01_BuffaloRiver_TN_USA.mat' + }) return forcing @pytest.fixture @@ -178,8 +176,8 @@ def test_setup(self, model_with_setup): assert actual['model_name'] == "m_14_topmodel_7p_2s" assert_array_equal(actual['parameters'], [[1234.0, 0.5, 0.5, 100.0, 0.5, 4.25, 2.5]]) assert_array_equal(actual['store_ini'], [[4321, 900]]) - assert_almost_equal(actual['time_start'], [[1990, 1, 1, 0, 0, 0]]) - assert_almost_equal(actual['time_end'], [[1991, 12, 31, 0, 0, 0]]) + assert_almost_equal(actual['time_start'], [[1990, 1, 1, 0, 0, 0]]) + assert_almost_equal(actual['time_end'], [[1991, 12, 31, 0, 0, 0]]) class TestWithDatesOutsideRangeSetupAndExampleData: @@ -188,12 +186,12 @@ def generate_forcing(self): # Downloaded from # https://github.com/wknoben/MARRMoT/blob/master/BMI/Config/BMI_testcase_m01_BuffaloRiver_TN_USA.mat forcing = load_foreign('marrmot', - directory=f'{Path(__file__).parent}/data', - start_time='1989-01-01T00:00:00Z', - end_time='1992-12-31T00:00:00Z', - forcing_info={ - 'forcing_file': 'BMI_testcase_m01_BuffaloRiver_TN_USA.mat' - }) + directory=f'{Path(__file__).parent}/data', + start_time='1989-01-01T00:00:00Z', + end_time='1992-12-31T00:00:00Z', + forcing_info={ + 'forcing_file': 'BMI_testcase_m01_BuffaloRiver_TN_USA.mat' + }) return forcing @pytest.fixture diff --git a/tests/parameter_sets/__init__.py b/tests/parameter_sets/__init__.py new file mode 100644 index 00000000..fc1a4a3f --- /dev/null +++ b/tests/parameter_sets/__init__.py @@ -0,0 +1,14 @@ +from ewatercycle import CFG +from ewatercycle.parameter_sets import download_example_parameter_sets + + +def test_download_example_parameter_sets(tmp_path): + CFG["parameterset_dir"] = tmp_path / "parameters" + download_example_parameter_sets() + + assert (tmp_path / "parameters" / "pcrglobwb_example_case").exists() + assert (tmp_path / "parameters/pcrglobwb_example_case/setup_natural_test.ini").exists() + + +# TODO test for the case where ewatercycle.yaml cfg is not writable +# TODO test for NoneType paths in CFG diff --git a/tests/parameter_sets/test_default_parameterset.py b/tests/parameter_sets/test_default_parameterset.py new file mode 100644 index 00000000..5cd3e3c8 --- /dev/null +++ b/tests/parameter_sets/test_default_parameterset.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import pytest + +from ewatercycle import CFG +from ewatercycle.parameter_sets import ParameterSet + + +@pytest.fixture +def mocked_config(tmp_path): + CFG['parameterset_dir'] = tmp_path + config = tmp_path / 'mymockedconfig.ini' + config.write_text('Something') + return config + + +class TestDefaults: + @pytest.fixture + def parameter_set(self, tmp_path, mocked_config: Path): + return ParameterSet( + name='justatest', + directory=str(tmp_path), + config=mocked_config.name, + ) + + def test_directory(self, parameter_set: ParameterSet, tmp_path): + assert parameter_set.directory == tmp_path + + def test_config(self, parameter_set: ParameterSet, mocked_config): + assert parameter_set.config == mocked_config + + def test_supported_model_versions(self, parameter_set: ParameterSet): + assert parameter_set.supported_model_versions == set() + + def test_is_available(self, parameter_set: ParameterSet): + assert parameter_set.is_available + + def test_repr(self, parameter_set: ParameterSet, tmp_path): + expected = ( + "ParameterSet(name='justatest', " + f"directory=PosixPath('{tmp_path}'), " + f"config=PosixPath('{tmp_path}/mymockedconfig.ini'), " + "doi='N/A', target_model='generic', supported_model_versions=set())" + ) + assert repr(parameter_set) == expected + + def test_str(self, parameter_set: ParameterSet, tmp_path): + expected = ( + 'Parameter set\n' + '-------------\n' + "name='justatest'\n" + f"directory=PosixPath('{tmp_path}')\n" + f"config=PosixPath('{tmp_path}/mymockedconfig.ini')\n" + "doi='N/A'\n" + "target_model='generic'\n" + "supported_model_versions=set()" + ) + assert str(parameter_set) == expected diff --git a/tests/parameter_sets/test_example.py b/tests/parameter_sets/test_example.py new file mode 100644 index 00000000..99087dc2 --- /dev/null +++ b/tests/parameter_sets/test_example.py @@ -0,0 +1,86 @@ +import logging +from unittest.mock import patch, Mock + +import pytest + +from ewatercycle import CFG +from ewatercycle.parameter_sets import ExampleParameterSet + + +@pytest.fixture +def setup_config(tmp_path): + CFG['parameterset_dir'] = tmp_path + yield CFG + # Rollback changes made to CFG by tests + CFG.reload() + + +@pytest.fixture +def example(setup_config): + return ExampleParameterSet( + name='firstexample', + config_url='https://github.com/mymodelorg/mymodelrepo/raw/master/mymodelexample/config.ini', + datafiles_url='https://github.com/mymodelorg/mymodelrepo/trunk/mymodelexample', + directory='mymodelexample', + config='mymodelexample/config.ini', + supported_model_versions={'0.4.2'}, + ) + + +def test_to_config(example): + example.to_config() + + assert 'firstexample' in CFG['parameter_sets'] + expected = dict( + doi='N/A', + target_model='generic', + directory='mymodelexample', + config='mymodelexample/config.ini', + supported_model_versions={'0.4.2'}, + ) + assert CFG['parameter_sets']['firstexample'] == expected + + +@patch('urllib.request.urlopen') +@patch('subprocess.check_call') +def test_download(mock_check_call, mock_urlopen, example, tmp_path): + ps_dir = tmp_path / 'mymodelexample' + r = Mock() + r.read.return_value = b'somecontent' + mock_urlopen.return_value = r + mock_check_call.side_effect = lambda _: ps_dir.mkdir() + + example.download() + + mock_urlopen.assert_called_once_with('https://github.com/mymodelorg/mymodelrepo/raw/master/mymodelexample/config.ini') + mock_check_call.assert_called_once_with([ + 'svn', 'export', + 'https://github.com/mymodelorg/mymodelrepo/trunk/mymodelexample', + ps_dir + ]) + assert (ps_dir / 'config.ini').read_text() == 'somecontent' + + +def test_download_already_exists(example, tmp_path): + ps_dir = tmp_path / 'mymodelexample' + ps_dir.mkdir() + + with pytest.raises(ValueError) as excinfo: + example.download() + + assert 'already exists, will not overwrite.' in str(excinfo.value) + + +@patch('urllib.request.urlopen') +@patch('subprocess.check_call') +def test_download_already_exists_but_skipped(mock_check_call, mock_urlopen, example, tmp_path, caplog): + ps_dir = tmp_path / 'mymodelexample' + ps_dir.mkdir() + + with caplog.at_level(logging.INFO): + example.download(skip_existing=True) + + mock_urlopen.assert_not_called() + mock_check_call.assert_not_called() + + assert 'already exists, skipping download.' in caplog.text diff --git a/tests/test_parameter_sets.py b/tests/test_parameter_sets.py new file mode 100644 index 00000000..f0846895 --- /dev/null +++ b/tests/test_parameter_sets.py @@ -0,0 +1,113 @@ +from unittest.mock import patch + +import pytest + +from ewatercycle import CFG +from ewatercycle.config import DEFAULT_CONFIG +from ewatercycle.parameter_sets import available_parameter_sets, get_parameter_set, example_parameter_sets, \ + download_example_parameter_sets, ExampleParameterSet + + +@pytest.fixture +def setup_config(tmp_path): + CFG['parameterset_dir'] = tmp_path + CFG['ewatercycle_config'] = tmp_path / 'ewatercycle.yaml' + yield CFG + CFG['ewatercycle_config'] = DEFAULT_CONFIG + CFG.reload() + + +@pytest.fixture +def mocked_parameterset_dir(setup_config, tmp_path): + ps1_dir = tmp_path / 'ps1' + ps1_dir.mkdir() + config1 = ps1_dir / 'mymockedconfig1.ini' + config1.write_text('Something') + ps2_dir = tmp_path / 'ps2' + ps2_dir.mkdir() + config2 = ps2_dir / 'mymockedconfig2.ini' + config2.write_text('Something else') + CFG['parameter_sets'] = { + 'ps1': { + 'directory': str(ps1_dir), + 'config': str(config1.relative_to(tmp_path)), + 'target_model': 'generic', + 'doi': 'somedoi1' + }, + 'ps2': { + 'directory': str(ps2_dir), + 'config': str(config2.relative_to(tmp_path)), + 'target_model': 'generic', + 'doi': 'somedoi2' + }, + 'ps3': { + 'directory': str(tmp_path / 'ps3'), + 'config': 'unavailable_config_file', + 'target_model': 'generic', + 'doi': 'somedoi3' + } + } + + +class TestAvailableParameterSets: + def test_filled(self, mocked_parameterset_dir): + names = available_parameter_sets('generic') + assert set(names) == {'ps1', 'ps2'} # ps3 is filtered due to not being available + + def test_no_config(self, tmp_path): + # Load default config shipped with package + CFG['ewatercycle_config'] = DEFAULT_CONFIG + CFG.reload() + + with pytest.raises(ValueError) as excinfo: + available_parameter_sets() + + assert 'No configuration file found' in str(excinfo.value) + + def test_no_sets_in_config(self, setup_config): + with pytest.raises(ValueError) as excinfo: + available_parameter_sets() + + assert 'No parameter sets defined in' in str(excinfo.value) + + def test_no_sets_for_model(self, mocked_parameterset_dir): + with pytest.raises(ValueError) as excinfo: + available_parameter_sets('somemodel') + + assert 'No parameter sets defined for somemodel model in' in str(excinfo.value) + + +class TestGetParameterSet: + + def test_valid(self, mocked_parameterset_dir, tmp_path): + actual = get_parameter_set('ps1') + + assert actual.name == 'ps1' + assert actual.directory == tmp_path / 'ps1' + assert actual.config == tmp_path / 'ps1' / 'mymockedconfig1.ini' + assert actual.doi == 'somedoi1' + assert actual.target_model == 'generic' + + def test_unknown(self, mocked_parameterset_dir): + with pytest.raises(KeyError): + get_parameter_set('ps9999') + + def test_unavailable(self, mocked_parameterset_dir): + with pytest.raises(ValueError): + get_parameter_set('ps3') + + +def test_example_parameter_sets(setup_config): + examples = example_parameter_sets() + assert len(list(examples)) > 0 + for name in examples: + assert name == examples[name].name + + +@patch.object(ExampleParameterSet, 'download') +def test_download_example_parameter_sets(mocked_download, setup_config, tmp_path): + download_example_parameter_sets() + + assert mocked_download.call_count > 0 + assert CFG['ewatercycle_config'].read_text() == CFG.dump_to_yaml() + assert len(CFG['parameter_sets']) > 0