From 431553e5b6d4a7dc027384f218380978cedba25e Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Wed, 27 Nov 2024 10:31:04 +0100 Subject: [PATCH 01/17] First start --- imod/msw/sprinkling.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/imod/msw/sprinkling.py b/imod/msw/sprinkling.py index 085ed1060..d4256330f 100644 --- a/imod/msw/sprinkling.py +++ b/imod/msw/sprinkling.py @@ -9,8 +9,9 @@ from imod.mf6.mf6_wel_adapter import Mf6Wel from imod.msw.fixed_format import VariableMetaData from imod.msw.pkgbase import MetaSwapPackage +from imod.msw.utilities.common import concat_imod5 from imod.msw.regrid.regrid_schemes import SprinklingRegridMethod -from imod.typing import IntArray +from imod.typing import IntArray, GridDataDict class Sprinkling(MetaSwapPackage, IRegridPackage): @@ -112,3 +113,11 @@ def _render( self._check_range(dataframe) return self.write_dataframe_fixed_width(file, dataframe) + + + @classmethod + def from_imod5_data(cls, imod5_data: dict[str, GridDataDict]) -> "Sprinkling": + cap_data = imod5_data["cap"] + data = {} + + \ No newline at end of file From f532594f791154a7cb5879963e9aaf1d8ee3ac44 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Wed, 27 Nov 2024 14:02:58 +0100 Subject: [PATCH 02/17] Finish first version Sprinkling.from_imod5_data --- imod/msw/sprinkling.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/imod/msw/sprinkling.py b/imod/msw/sprinkling.py index d4256330f..55c9db6aa 100644 --- a/imod/msw/sprinkling.py +++ b/imod/msw/sprinkling.py @@ -9,9 +9,10 @@ from imod.mf6.mf6_wel_adapter import Mf6Wel from imod.msw.fixed_format import VariableMetaData from imod.msw.pkgbase import MetaSwapPackage -from imod.msw.utilities.common import concat_imod5 from imod.msw.regrid.regrid_schemes import SprinklingRegridMethod -from imod.typing import IntArray, GridDataDict +from imod.msw.utilities.common import concat_imod5 +from imod.typing import Imod5DataDict, IntArray +from imod.typing.grid import zeros_like class Sprinkling(MetaSwapPackage, IRegridPackage): @@ -114,10 +115,31 @@ def _render( return self.write_dataframe_fixed_width(file, dataframe) - @classmethod - def from_imod5_data(cls, imod5_data: dict[str, GridDataDict]) -> "Sprinkling": + def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "Sprinkling": cap_data = imod5_data["cap"] + if isinstance(cap_data["artificial_recharge_layer"], pd.DataFrame): + raise NotImplementedError( + "Assigning sprinkling wells with an IPF file is not supported, please specify them as IDF." + ) + drop_layer_kwargs = { + "layer": 0, + "drop": True, + "missing_dims": "ignore", + } + type = cap_data["artificial_recharge"].isel(**drop_layer_kwargs) + capacity = cap_data["artificial_recharge_capacity"].isel(**drop_layer_kwargs) + max_abstraction_groundwater_rural = capacity.where(type == 1) + max_abstraction_surfacewater_rural = capacity.where(type == 2).fillna(0.0) + + max_abstraction_urban = zeros_like(type) + data = {} + data["max_abstraction_groundwater"] = concat_imod5( + max_abstraction_groundwater_rural, max_abstraction_urban + ) + data["max_abstraction_surfacewater"] = concat_imod5( + max_abstraction_surfacewater_rural, max_abstraction_urban + ) - \ No newline at end of file + return cls(**data) From a880e29a4c4760cacae3b956ef77706cedca6a17 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Thu, 28 Nov 2024 14:08:47 +0100 Subject: [PATCH 03/17] Separate logic into option for sprinkling data from point data (to be implemented) and from grids and explain in docstring --- imod/msw/sprinkling.py | 95 ++++++++++++++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/imod/msw/sprinkling.py b/imod/msw/sprinkling.py index 324219576..cf8eb797c 100644 --- a/imod/msw/sprinkling.py +++ b/imod/msw/sprinkling.py @@ -11,7 +11,7 @@ from imod.msw.pkgbase import MetaSwapPackage from imod.msw.regrid.regrid_schemes import SprinklingRegridMethod from imod.msw.utilities.common import concat_imod5 -from imod.typing import Imod5DataDict, IntArray +from imod.typing import Imod5DataDict, IntArray, GridDataDict from imod.typing.grid import zeros_like @@ -21,6 +21,47 @@ def _ravel_per_subunit(da: xr.DataArray) -> np.ndarray: # per defined well element, per defined subunits return array_out[np.isfinite(array_out)] +def _sprinkling_data_from_imod5_ipf(cap_data: GridDataDict) -> GridDataDict: + raise NotImplementedError( + "Assigning sprinkling wells with an IPF file is not supported, please specify them as IDF." + ) + return {} + +def _sprinkling_data_from_imod5_grid(cap_data: GridDataDict) -> GridDataDict: + drop_layer_kwargs = { + "layer": 0, + "drop": True, + "missing_dims": "ignore", + } + type = cap_data["artificial_recharge"].isel(**drop_layer_kwargs) + capacity = cap_data["artificial_recharge_capacity"].isel(**drop_layer_kwargs) + + from_groundwater = type == 1 + from_surfacewater = type == 2 + is_active = type != 0 + + zero_where_active = zeros_like(type).where(is_active) + + # Add zero where active, to have active cells set to 0.0. + max_abstraction_groundwater_rural = ( + capacity.where(from_groundwater) + zero_where_active + ) + max_abstraction_surfacewater_rural = ( + capacity.where(from_surfacewater) + zero_where_active + ) + + # No sprinkling for urban environments + max_abstraction_urban = zero_where_active + + data = {} + data["max_abstraction_groundwater"] = concat_imod5( + max_abstraction_groundwater_rural, max_abstraction_urban + ) + data["max_abstraction_surfacewater"] = concat_imod5( + max_abstraction_surfacewater_rural, max_abstraction_urban + ) + return data + class Sprinkling(MetaSwapPackage, IRegridPackage): """ @@ -127,29 +168,37 @@ def _render( @classmethod def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "Sprinkling": + """ + Import sprinkling data from imod5 data. Abstraction data for sprinkling + is defined in iMOD5 either with grids (IDF) or points (IPF) combined + with a grid. Depending on the type, the method does different conversions: + + - grids (IDF) + The ``"artifical_recharge_layer"`` variable was defined as grid + (IDF), this grid defines in which layer a groundwater abstraction + well should be placed. The ``"artificial_recharge"`` grid contains + types which point to the type of abstraction: + * 0: no abstraction + * 1: groundwater abstraction + * 2: surfacewater abstraction + The ``"artificial_recharge_capacity"`` grid/constant defines the + capacity of each groundwater or surfacewater abstraction. This is an + ``1:1`` mapping: Each grid cell maps to a separate well. + + - points with grid (IPF & IDF) + The ``"artifical_recharge_layer"`` variable was defined as point + data (IPF), this table contains wellids with an abstraction capacity + and layer. The ``"artificial_recharge"`` grid contains a mapping of + grid cells to wellids in the point data. The + ``"artificial_recharge_capacity"`` is ignored as the abstraction + capacity is already defined in the point data. This is an ``n:1`` + mapping: multiple grid cells can map to one well. + + """ cap_data = imod5_data["cap"] if isinstance(cap_data["artificial_recharge_layer"], pd.DataFrame): - raise NotImplementedError( - "Assigning sprinkling wells with an IPF file is not supported, please specify them as IDF." - ) - drop_layer_kwargs = { - "layer": 0, - "drop": True, - "missing_dims": "ignore", - } - type = cap_data["artificial_recharge"].isel(**drop_layer_kwargs) - capacity = cap_data["artificial_recharge_capacity"].isel(**drop_layer_kwargs) - max_abstraction_groundwater_rural = capacity.where(type == 1) - max_abstraction_surfacewater_rural = capacity.where(type == 2).fillna(0.0) - - max_abstraction_urban = zeros_like(type) - - data = {} - data["max_abstraction_groundwater"] = concat_imod5( - max_abstraction_groundwater_rural, max_abstraction_urban - ) - data["max_abstraction_surfacewater"] = concat_imod5( - max_abstraction_surfacewater_rural, max_abstraction_urban - ) + data = _sprinkling_data_from_imod5_ipf(cap_data) + else: + data = _sprinkling_data_from_imod5_grid(cap_data) return cls(**data) From c0cc24cec46c6ce03aa23dc5eef967208fba67dd Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Thu, 28 Nov 2024 14:11:41 +0100 Subject: [PATCH 04/17] Extend docstrings --- imod/msw/sprinkling.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/imod/msw/sprinkling.py b/imod/msw/sprinkling.py index cf8eb797c..10c6fe71b 100644 --- a/imod/msw/sprinkling.py +++ b/imod/msw/sprinkling.py @@ -194,6 +194,15 @@ def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "Sprinkling": capacity is already defined in the point data. This is an ``n:1`` mapping: multiple grid cells can map to one well. + Parameters + ---------- + imod5_data : dict + iMOD5 data as returned by + :func:`imod.formats.prj.open_projectfile_data` + + Returns + ------- + Sprinkling package """ cap_data = imod5_data["cap"] if isinstance(cap_data["artificial_recharge_layer"], pd.DataFrame): From a099cc5f60b9cffcf6230b79ee9f7ab71406e086 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Thu, 28 Nov 2024 15:48:38 +0100 Subject: [PATCH 05/17] format --- imod/msw/sprinkling.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/imod/msw/sprinkling.py b/imod/msw/sprinkling.py index 10c6fe71b..d85ffd889 100644 --- a/imod/msw/sprinkling.py +++ b/imod/msw/sprinkling.py @@ -11,7 +11,7 @@ from imod.msw.pkgbase import MetaSwapPackage from imod.msw.regrid.regrid_schemes import SprinklingRegridMethod from imod.msw.utilities.common import concat_imod5 -from imod.typing import Imod5DataDict, IntArray, GridDataDict +from imod.typing import GridDataDict, Imod5DataDict, IntArray from imod.typing.grid import zeros_like @@ -21,12 +21,14 @@ def _ravel_per_subunit(da: xr.DataArray) -> np.ndarray: # per defined well element, per defined subunits return array_out[np.isfinite(array_out)] + def _sprinkling_data_from_imod5_ipf(cap_data: GridDataDict) -> GridDataDict: raise NotImplementedError( "Assigning sprinkling wells with an IPF file is not supported, please specify them as IDF." ) return {} + def _sprinkling_data_from_imod5_grid(cap_data: GridDataDict) -> GridDataDict: drop_layer_kwargs = { "layer": 0, @@ -172,12 +174,12 @@ def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "Sprinkling": Import sprinkling data from imod5 data. Abstraction data for sprinkling is defined in iMOD5 either with grids (IDF) or points (IPF) combined with a grid. Depending on the type, the method does different conversions: - + - grids (IDF) The ``"artifical_recharge_layer"`` variable was defined as grid (IDF), this grid defines in which layer a groundwater abstraction well should be placed. The ``"artificial_recharge"`` grid contains - types which point to the type of abstraction: + types which point to the type of abstraction: * 0: no abstraction * 1: groundwater abstraction * 2: surfacewater abstraction From 2158d71ba70b9dc2c8b7ec2b920a8d0ffa546db5 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Thu, 28 Nov 2024 16:01:11 +0100 Subject: [PATCH 06/17] extend docstring --- imod/msw/sprinkling.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/imod/msw/sprinkling.py b/imod/msw/sprinkling.py index d85ffd889..b57b2a2b9 100644 --- a/imod/msw/sprinkling.py +++ b/imod/msw/sprinkling.py @@ -198,9 +198,11 @@ def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "Sprinkling": Parameters ---------- - imod5_data : dict - iMOD5 data as returned by - :func:`imod.formats.prj.open_projectfile_data` + imod5_data: dict[str, dict[str, GridDataArray]] + dictionary containing the arrays mentioned in the project file as + xarray datasets, under the key of the package type to which it + belongs, as returned by + :func:`imod.formats.prj.open_projectfile_data`. Returns ------- From a47cbd8194aa9a66e5f05fb066dc0f6bd8bae5de Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Thu, 28 Nov 2024 16:01:41 +0100 Subject: [PATCH 07/17] Add LayeredWell.from_imod5_cap_data method --- imod/mf6/utilities/imod5_converter.py | 71 +++++++++++++++++++++++++++ imod/mf6/wel.py | 43 +++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/imod/mf6/utilities/imod5_converter.py b/imod/mf6/utilities/imod5_converter.py index e599c8522..062505abe 100644 --- a/imod/mf6/utilities/imod5_converter.py +++ b/imod/mf6/utilities/imod5_converter.py @@ -1,8 +1,10 @@ from typing import Union import numpy as np +import pandas as pd import xarray as xr +from imod.typing import GridDataDict, Imod5DataDict from imod.typing.grid import full_like @@ -48,3 +50,72 @@ def fill_missing_layers( """ layer = full.coords["layer"] return source.reindex(layer=layer, fill_value=fillvalue) + + +def _well_from_imod5_cap_point_data(cap_data: GridDataDict) -> GridDataDict: + raise NotImplementedError( + "Assigning sprinkling wells with an IPF file is not supported, please specify them as IDF." + ) + return {} + + +def _well_from_imod5_cap_grid_data(cap_data: GridDataDict) -> dict[str, np.ndarray]: + drop_layer_kwargs = { + "layer": 0, + "drop": True, + "missing_dims": "ignore", + } + type = cap_data["artificial_recharge"].isel(**drop_layer_kwargs).compute() + layer = ( + cap_data["artificial_recharge_layer"] + .isel(**drop_layer_kwargs) + .astype(int) + .compute() + ) + + from_groundwater = (type != 0).to_numpy() + coords = type.coords + y_grid, x_grid = np.meshgrid(coords["y"].to_numpy(), coords["x"].to_numpy()) + + data = {} + data["layer"] = layer.data[from_groundwater] + data["y"] = y_grid[from_groundwater] + data["x"] = x_grid[from_groundwater] + data["rate"] = np.zeros_like(data["x"]) + + return data + + +def well_from_imod5_cap_data(imod5_data: Imod5DataDict) -> GridDataDict: + """ + Abstraction data for sprinkling is defined in iMOD5 either with grids (IDF) + or points (IPF) combined with a grid. Depending on the type, the function does + different conversions + + - grids (IDF) + The ``"artifical_recharge_layer"`` variable was defined as grid + (IDF), this grid defines in which layer a groundwater abstraction + well should be placed. The ``"artificial_recharge"`` grid contains + types which point to the type of abstraction: + * 0: no abstraction + * 1: groundwater abstraction + * 2: surfacewater abstraction + The ``"artificial_recharge_capacity"`` grid/constant defines the + capacity of each groundwater or surfacewater abstraction. This is an + ``1:1`` mapping: Each grid cell maps to a separate well. + + - points with grid (IPF & IDF) + The ``"artifical_recharge_layer"`` variable was defined as point + data (IPF), this table contains wellids with an abstraction capacity + and layer. The ``"artificial_recharge"`` grid contains a mapping of + grid cells to wellids in the point data. The + ``"artificial_recharge_capacity"`` is ignored as the abstraction + capacity is already defined in the point data. This is an ``n:1`` + mapping: multiple grid cells can map to one well. + """ + cap_data = imod5_data["cap"] + + if isinstance(cap_data["artificial_recharge_layer"], pd.DataFrame): + return _well_from_imod5_cap_point_data(cap_data) + else: + return _well_from_imod5_cap_grid_data(cap_data) diff --git a/imod/mf6/wel.py b/imod/mf6/wel.py index 372cb9f48..eda59bdf7 100644 --- a/imod/mf6/wel.py +++ b/imod/mf6/wel.py @@ -30,6 +30,7 @@ from imod.mf6.package import Package from imod.mf6.utilities.dataset import remove_inactive from imod.mf6.utilities.grid import broadcast_to_full_domain +from imod.mf6.utilities.imod5_converter import well_from_imod5_cap_data from imod.mf6.validation import validation_pkg_error_message from imod.mf6.validation_context import ValidationContext from imod.mf6.write_context import WriteContext @@ -44,7 +45,7 @@ ValidationError, ) from imod.select.points import points_indices, points_values -from imod.typing import GridDataArray +from imod.typing import GridDataArray, Imod5DataDict from imod.typing.grid import is_spatial_grid, ones_like from imod.util.expand_repetitions import resample_timeseries from imod.util.structured import values_within_range @@ -1298,6 +1299,46 @@ def _validate_imod5_depth_information( logger.log(loglevel=LogLevel.ERROR, message=log_msg, additional_depth=2) raise ValueError(log_msg) + @classmethod + def from_imod5_cap_data(cls, imod5_data: Imod5DataDict): + """ + Create LayeredWell from imod5_data in "cap" package. Abstraction data + for sprinkling is defined in iMOD5 either with grids (IDF) or points + (IPF) combined with a grid. Depending on the type, the function does + different conversions + + - grids (IDF) + The ``"artifical_recharge_layer"`` variable was defined as grid + (IDF), this grid defines in which layer a groundwater abstraction + well should be placed. The ``"artificial_recharge"`` grid contains + types which point to the type of abstraction: + * 0: no abstraction + * 1: groundwater abstraction + * 2: surfacewater abstraction + The ``"artificial_recharge_capacity"`` grid/constant defines the + capacity of each groundwater or surfacewater abstraction. This is an + ``1:1`` mapping: Each grid cell maps to a separate well. + + - points with grid (IPF & IDF) + The ``"artifical_recharge_layer"`` variable was defined as point + data (IPF), this table contains wellids with an abstraction capacity + and layer. The ``"artificial_recharge"`` grid contains a mapping of + grid cells to wellids in the point data. The + ``"artificial_recharge_capacity"`` is ignored as the abstraction + capacity is already defined in the point data. This is an ``n:1`` + mapping: multiple grid cells can map to one well. + + Parameters + ---------- + imod5_data: dict[str, dict[str, GridDataArray]] + dictionary containing the arrays mentioned in the project file as + xarray datasets, under the key of the package type to which it + belongs, as returned by + :func:`imod.formats.prj.open_projectfile_data`. + """ + data = well_from_imod5_cap_data(imod5_data) + return cls(**data) + class WellDisStructured(DisStructuredBoundaryCondition): """ From 2aa8eb9f09f1e53268841ece69420f4a59983d36 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Thu, 28 Nov 2024 17:09:41 +0100 Subject: [PATCH 08/17] Surfacewater 0 if groundwater abstraction active and vice versa --- imod/msw/sprinkling.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/imod/msw/sprinkling.py b/imod/msw/sprinkling.py index b57b2a2b9..b9bac8f75 100644 --- a/imod/msw/sprinkling.py +++ b/imod/msw/sprinkling.py @@ -45,11 +45,11 @@ def _sprinkling_data_from_imod5_grid(cap_data: GridDataDict) -> GridDataDict: zero_where_active = zeros_like(type).where(is_active) # Add zero where active, to have active cells set to 0.0. - max_abstraction_groundwater_rural = ( - capacity.where(from_groundwater) + zero_where_active + max_abstraction_groundwater_rural = zero_where_active.where( + ~from_groundwater, capacity ) - max_abstraction_surfacewater_rural = ( - capacity.where(from_surfacewater) + zero_where_active + max_abstraction_surfacewater_rural = zero_where_active.where( + ~from_surfacewater, capacity ) # No sprinkling for urban environments From 9d7533d976efee73574f6c88e4655f7c808ffc1f Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Thu, 28 Nov 2024 17:10:28 +0100 Subject: [PATCH 09/17] Add tests for Sprinkling.from_imod5_data --- imod/tests/conftest.py | 4 ++ imod/tests/fixtures/imod5_cap_data.py | 60 ++++++++++++++++++++++++++ imod/tests/test_msw/test_sprinkling.py | 38 ++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 imod/tests/fixtures/imod5_cap_data.py diff --git a/imod/tests/conftest.py b/imod/tests/conftest.py index 593ffb5bb..0677d4b4f 100644 --- a/imod/tests/conftest.py +++ b/imod/tests/conftest.py @@ -22,6 +22,10 @@ ) from .fixtures.flow_example_fixture import imodflow_model from .fixtures.flow_transport_simulation_fixture import flow_transport_simulation +from .fixtures.imod5_cap_data import ( + cap_data_sprinkling_grid, + cap_data_sprinkling_points, +) from .fixtures.imod5_well_data import ( well_duplication_import_prj, well_mixed_ipfs, diff --git a/imod/tests/fixtures/imod5_cap_data.py b/imod/tests/fixtures/imod5_cap_data.py new file mode 100644 index 000000000..c32749eca --- /dev/null +++ b/imod/tests/fixtures/imod5_cap_data.py @@ -0,0 +1,60 @@ +import numpy as np +import pandas as pd +import pytest +import xarray as xr + +from imod.typing import Imod5DataDict + + +def zeros_grid(): + x = [1.0, 2.0, 3.0] + y = [3.0, 2.0, 1.0] + dx = 1.0 + dy = -1.0 + + coords = {"x": x, "y": y, "dx": dx, "dy": dy} + shape = (len(y), len(x)) + data = np.zeros(shape) + + return xr.DataArray(data, coords=coords, dims=("y", "x")) + + +@pytest.fixture(scope="function") +def cap_data_sprinkling_grid() -> Imod5DataDict: + type = zeros_grid() + type[:, 1] = 1 + type[:, 2] = 2 + layer = xr.ones_like(type) + layer[:, 1] = 2 + + cap_data = { + "artificial_recharge": type, + "artificial_recharge_layer": layer, + "artificial_recharge_capacity": xr.DataArray(25.0), + } + + return {"cap": cap_data} + + +@pytest.fixture(scope="function") +def cap_data_sprinkling_points() -> Imod5DataDict: + type = zeros_grid() + type[:, 1] = 3000 + type[:, 2] = 4000 + + data = { + "id": [3000, 4000], + "layer": [2, 3], + "capacity": [15.0, 30.0], + "y": [1.0, 2.0], + "x": [1.0, 2.0], + } + + layer = pd.DataFrame(data=data) + cap_data = { + "artificial_recharge": type, + "artificial_recharge_layer": layer, + "artificial_recharge_capacity": xr.DataArray(25.0), + } + + return {"cap": cap_data} diff --git a/imod/tests/test_msw/test_sprinkling.py b/imod/tests/test_msw/test_sprinkling.py index 3954fcbee..3695e7126 100644 --- a/imod/tests/test_msw/test_sprinkling.py +++ b/imod/tests/test_msw/test_sprinkling.py @@ -2,6 +2,7 @@ from pathlib import Path import numpy as np +import pytest import xarray as xr from numpy import nan from numpy.testing import assert_almost_equal, assert_equal @@ -269,3 +270,40 @@ def test_simple_model_1_subunit(fixed_format_parser): ) assert_equal(results["layer"], np.array([3, 2])) assert_equal(results["svat_groundwater"], np.array([1, 2])) + + +def test_sprinkling_from_imod5_data__points(cap_data_sprinkling_points): + with pytest.raises(NotImplementedError): + msw.Sprinkling.from_imod5_data(cap_data_sprinkling_points) + + +def test_sprinkling_from_imod5_data__grid(cap_data_sprinkling_grid): + # Arrange + # fmt: off + np.nan = nan + expected_gw_abstraction = np.array( + [[nan, 25., 0.], + [nan, 25., 0.], + [nan, 25., 0.]] + ) + expected_sw_abstraction = np.array( + [[nan, 0., 25.], + [nan, 0., 25.], + [nan, 0., 25.]] + ) + # fmt: on + + # Act + sprinkling = msw.Sprinkling.from_imod5_data(cap_data_sprinkling_grid) + + # Assert + assert isinstance(sprinkling, msw.Sprinkling) + ds = sprinkling.dataset + assert (ds.sel(subunit=1) == 0).all() + rural_ds = ds.sel(subunit=0) + np.testing.assert_array_equal( + rural_ds["max_abstraction_groundwater"].to_numpy(), expected_gw_abstraction + ) + np.testing.assert_array_equal( + rural_ds["max_abstraction_surfacewater"].to_numpy(), expected_sw_abstraction + ) From c21ff553383e9285c7a60f60e0c182af251a2ec0 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Thu, 28 Nov 2024 17:39:52 +0100 Subject: [PATCH 10/17] Add tests for LayeredWell.from_imod5_cap_data --- imod/mf6/utilities/imod5_converter.py | 2 +- imod/tests/test_mf6/test_mf6_wel.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/imod/mf6/utilities/imod5_converter.py b/imod/mf6/utilities/imod5_converter.py index 062505abe..7eccc344c 100644 --- a/imod/mf6/utilities/imod5_converter.py +++ b/imod/mf6/utilities/imod5_converter.py @@ -75,7 +75,7 @@ def _well_from_imod5_cap_grid_data(cap_data: GridDataDict) -> dict[str, np.ndarr from_groundwater = (type != 0).to_numpy() coords = type.coords - y_grid, x_grid = np.meshgrid(coords["y"].to_numpy(), coords["x"].to_numpy()) + x_grid, y_grid = np.meshgrid(coords["x"].to_numpy(), coords["y"].to_numpy()) data = {} data["layer"] = layer.data[from_groundwater] diff --git a/imod/tests/test_mf6/test_mf6_wel.py b/imod/tests/test_mf6/test_mf6_wel.py index 8fc66d050..91475cef7 100644 --- a/imod/tests/test_mf6/test_mf6_wel.py +++ b/imod/tests/test_mf6/test_mf6_wel.py @@ -1176,3 +1176,25 @@ def test_logmessage_for_missing_filter_settings( in log ) assert message_required == message_present + + +def test_from_imod5_cap_data__grid(cap_data_sprinkling_grid): + # Arrange + expected_layer = np.array([2, 1, 2, 1, 2, 1]) + expected_y = np.array([3.0, 3.0, 2.0, 2.0, 1.0, 1.0]) + expected_x = np.array([2.0, 3.0, 2.0, 3.0, 2.0, 3.0]) + + # Act + well = LayeredWell.from_imod5_cap_data(cap_data_sprinkling_grid) + + # Assert + ds = well.dataset + np.testing.assert_equal(ds["rate"].to_numpy(), 0.0) + np.testing.assert_array_equal(ds["layer"].to_numpy(), expected_layer) + np.testing.assert_array_equal(ds["x"].to_numpy(), expected_x) + np.testing.assert_array_equal(ds["y"].to_numpy(), expected_y) + + +def test_from_imod5_cap_data__points(cap_data_sprinkling_points): + with pytest.raises(NotImplementedError): + LayeredWell.from_imod5_cap_data(cap_data_sprinkling_points) From f2e95ec05c8bbe04cbb5a342bdf56dfb29417214 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Fri, 29 Nov 2024 11:01:02 +0100 Subject: [PATCH 11/17] Fix mypy errors --- imod/mf6/utilities/imod5_converter.py | 12 ++++++------ imod/mf6/wel.py | 2 +- imod/msw/sprinkling.py | 8 ++++---- imod/typing/__init__.py | 8 +++++++- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/imod/mf6/utilities/imod5_converter.py b/imod/mf6/utilities/imod5_converter.py index 7eccc344c..c3301430f 100644 --- a/imod/mf6/utilities/imod5_converter.py +++ b/imod/mf6/utilities/imod5_converter.py @@ -1,10 +1,10 @@ -from typing import Union +from typing import Union, cast import numpy as np import pandas as pd import xarray as xr -from imod.typing import GridDataDict, Imod5DataDict +from imod.typing import GridDataDict, Imod5DataDict, SelSettingsType from imod.typing.grid import full_like @@ -52,7 +52,7 @@ def fill_missing_layers( return source.reindex(layer=layer, fill_value=fillvalue) -def _well_from_imod5_cap_point_data(cap_data: GridDataDict) -> GridDataDict: +def _well_from_imod5_cap_point_data(cap_data: GridDataDict) -> dict[str, np.ndarray]: raise NotImplementedError( "Assigning sprinkling wells with an IPF file is not supported, please specify them as IDF." ) @@ -60,7 +60,7 @@ def _well_from_imod5_cap_point_data(cap_data: GridDataDict) -> GridDataDict: def _well_from_imod5_cap_grid_data(cap_data: GridDataDict) -> dict[str, np.ndarray]: - drop_layer_kwargs = { + drop_layer_kwargs: SelSettingsType = { "layer": 0, "drop": True, "missing_dims": "ignore", @@ -86,7 +86,7 @@ def _well_from_imod5_cap_grid_data(cap_data: GridDataDict) -> dict[str, np.ndarr return data -def well_from_imod5_cap_data(imod5_data: Imod5DataDict) -> GridDataDict: +def well_from_imod5_cap_data(imod5_data: Imod5DataDict) -> dict[str, np.ndarray]: """ Abstraction data for sprinkling is defined in iMOD5 either with grids (IDF) or points (IPF) combined with a grid. Depending on the type, the function does @@ -113,7 +113,7 @@ def well_from_imod5_cap_data(imod5_data: Imod5DataDict) -> GridDataDict: capacity is already defined in the point data. This is an ``n:1`` mapping: multiple grid cells can map to one well. """ - cap_data = imod5_data["cap"] + cap_data = cast(GridDataDict, imod5_data["cap"]) if isinstance(cap_data["artificial_recharge_layer"], pd.DataFrame): return _well_from_imod5_cap_point_data(cap_data) diff --git a/imod/mf6/wel.py b/imod/mf6/wel.py index eda59bdf7..d30b58d30 100644 --- a/imod/mf6/wel.py +++ b/imod/mf6/wel.py @@ -1337,7 +1337,7 @@ def from_imod5_cap_data(cls, imod5_data: Imod5DataDict): :func:`imod.formats.prj.open_projectfile_data`. """ data = well_from_imod5_cap_data(imod5_data) - return cls(**data) + return cls(**data) # type: ignore class WellDisStructured(DisStructuredBoundaryCondition): diff --git a/imod/msw/sprinkling.py b/imod/msw/sprinkling.py index b9bac8f75..ea9925112 100644 --- a/imod/msw/sprinkling.py +++ b/imod/msw/sprinkling.py @@ -1,4 +1,4 @@ -from typing import TextIO +from typing import TextIO, cast import numpy as np import pandas as pd @@ -11,7 +11,7 @@ from imod.msw.pkgbase import MetaSwapPackage from imod.msw.regrid.regrid_schemes import SprinklingRegridMethod from imod.msw.utilities.common import concat_imod5 -from imod.typing import GridDataDict, Imod5DataDict, IntArray +from imod.typing import GridDataDict, Imod5DataDict, IntArray, SelSettingsType from imod.typing.grid import zeros_like @@ -30,7 +30,7 @@ def _sprinkling_data_from_imod5_ipf(cap_data: GridDataDict) -> GridDataDict: def _sprinkling_data_from_imod5_grid(cap_data: GridDataDict) -> GridDataDict: - drop_layer_kwargs = { + drop_layer_kwargs: SelSettingsType = { "layer": 0, "drop": True, "missing_dims": "ignore", @@ -208,7 +208,7 @@ def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "Sprinkling": ------- Sprinkling package """ - cap_data = imod5_data["cap"] + cap_data = cast(GridDataDict, imod5_data["cap"]) if isinstance(cap_data["artificial_recharge_layer"], pd.DataFrame): data = _sprinkling_data_from_imod5_ipf(cap_data) else: diff --git a/imod/typing/__init__.py b/imod/typing/__init__.py index 8beb7fd13..0217f9782 100644 --- a/imod/typing/__init__.py +++ b/imod/typing/__init__.py @@ -2,7 +2,7 @@ Module to define type aliases. """ -from typing import TYPE_CHECKING, TypeAlias, TypeVar, Union +from typing import TYPE_CHECKING, Literal, TypeAlias, TypedDict, TypeVar, Union import numpy as np import xarray as xr @@ -20,6 +20,12 @@ IntArray: TypeAlias = NDArray[np.int_] +class SelSettingsType(TypedDict, total=False): + layer: int + drop: bool + missing_dims: Literal["raise", "warn", "ignore"] + + # Types for optional dependencies. if TYPE_CHECKING: import geopandas as gpd From 94bb975aa36fd8b792962c92b6d9246c49bdb311 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Fri, 29 Nov 2024 11:10:40 +0100 Subject: [PATCH 12/17] Update changelog --- docs/api/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api/changelog.rst b/docs/api/changelog.rst index dbba23f5e..85feb8042 100644 --- a/docs/api/changelog.rst +++ b/docs/api/changelog.rst @@ -14,6 +14,9 @@ Added - :class:`imod.msw.MeteoGridCopy` to copy existing `mete_grid.inp` files, so ASCII grids in large existing meteo databases do not have to be read. +- :meth:`imod.mf6.LayeredWell.from_imod5_cap_data` to construct a + :class:`imod.mf6.LayeredWell` package from iMOD5 data in the CAP package (for + MetaSWAP). Currently only griddata (IDF) is supported. Fixed ~~~~~ From 006bdd7038c3f3b25a4684a5b015ea1f0729da89 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Fri, 29 Nov 2024 11:44:52 +0100 Subject: [PATCH 13/17] Automatically add sprinkling well to groundwaterflow model if cap data present --- docs/api/changelog.rst | 3 +++ imod/mf6/model_gwf.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/api/changelog.rst b/docs/api/changelog.rst index 85feb8042..6bf15d235 100644 --- a/docs/api/changelog.rst +++ b/docs/api/changelog.rst @@ -36,6 +36,9 @@ Changed ``downward_resistance`` now require a ``subunit`` coordinate. - Variables ``max_abstraction_groundwater`` and ``max_abstraction_surfacewater`` in :class:`imod.msw.Sprinkling` now needs to have a subunit coordinate. +- If ``"cap"`` package present in ``imod5_data``, + :meth:`imod.mf6.GroundwaterFlowModel.from_imod5_data` now automatically adds a + well for metaswap sprinkling named ``"msw-sprinkling"`` [0.18.1] - 2024-11-20 diff --git a/imod/mf6/model_gwf.py b/imod/mf6/model_gwf.py index 3197cfa91..e203788f3 100644 --- a/imod/mf6/model_gwf.py +++ b/imod/mf6/model_gwf.py @@ -303,9 +303,9 @@ def from_imod5_data( ) # now import the non-singleton packages' + imod5_keys = list(imod5_data.keys()) # import wells - imod5_keys = list(imod5_data.keys()) wel_keys = [key for key in imod5_keys if key[0:3] == "wel"] for wel_key in wel_keys: wel_key_truncated = wel_key[:16] @@ -330,8 +330,10 @@ def from_imod5_data( wel_key, imod5_data, times ) + if "cap" in imod5_keys: + result["msw-sprinkling"] = LayeredWell.from_imod5_cap_data(imod5_data) + # import ghb's - imod5_keys = list(imod5_data.keys()) ghb_keys = [key for key in imod5_keys if key[0:3] == "ghb"] for ghb_key in ghb_keys: ghb_pkg = GeneralHeadBoundary.from_imod5_data( From 089cc20650fab7826dff4728a234cbb3ff941371 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Fri, 29 Nov 2024 13:23:50 +0100 Subject: [PATCH 14/17] Fix mypy errors --- imod/mf6/model_gwf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imod/mf6/model_gwf.py b/imod/mf6/model_gwf.py index e203788f3..0f4fc1056 100644 --- a/imod/mf6/model_gwf.py +++ b/imod/mf6/model_gwf.py @@ -331,7 +331,7 @@ def from_imod5_data( ) if "cap" in imod5_keys: - result["msw-sprinkling"] = LayeredWell.from_imod5_cap_data(imod5_data) + result["msw-sprinkling"] = LayeredWell.from_imod5_cap_data(imod5_data) # type: ignore # import ghb's ghb_keys = [key for key in imod5_keys if key[0:3] == "ghb"] From 3548a82b7647edc835c8dea5d12af41c959df1e5 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Fri, 29 Nov 2024 13:24:21 +0100 Subject: [PATCH 15/17] Format --- imod/mf6/model_gwf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imod/mf6/model_gwf.py b/imod/mf6/model_gwf.py index 0f4fc1056..4990e5f46 100644 --- a/imod/mf6/model_gwf.py +++ b/imod/mf6/model_gwf.py @@ -331,7 +331,7 @@ def from_imod5_data( ) if "cap" in imod5_keys: - result["msw-sprinkling"] = LayeredWell.from_imod5_cap_data(imod5_data) # type: ignore + result["msw-sprinkling"] = LayeredWell.from_imod5_cap_data(imod5_data) # type: ignore # import ghb's ghb_keys = [key for key in imod5_keys if key[0:3] == "ghb"] From c68fc160bb762e5b9f46ff62dcd9920f1c27ca60 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Fri, 29 Nov 2024 13:35:29 +0100 Subject: [PATCH 16/17] Add from_imod5_cap_data to public API --- docs/api/mf6.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api/mf6.rst b/docs/api/mf6.rst index 4d43ead80..e7683632e 100644 --- a/docs/api/mf6.rst +++ b/docs/api/mf6.rst @@ -97,6 +97,7 @@ Flow Packages HorizontalFlowBarrierResistance.to_mf6_pkg LayeredWell LayeredWell.from_imod5_data + LayeredWell.from_imod5_cap_data LayeredWell.mask LayeredWell.regrid_like LayeredWell.to_mf6_pkg From 01727e2d83ae9195cb700b911adf043f589720e6 Mon Sep 17 00:00:00 2001 From: JoerivanEngelen Date: Tue, 3 Dec 2024 16:11:10 +0100 Subject: [PATCH 17/17] Remove silly declaration --- imod/tests/test_msw/test_sprinkling.py | 1 - 1 file changed, 1 deletion(-) diff --git a/imod/tests/test_msw/test_sprinkling.py b/imod/tests/test_msw/test_sprinkling.py index 3695e7126..74714d3a4 100644 --- a/imod/tests/test_msw/test_sprinkling.py +++ b/imod/tests/test_msw/test_sprinkling.py @@ -280,7 +280,6 @@ def test_sprinkling_from_imod5_data__points(cap_data_sprinkling_points): def test_sprinkling_from_imod5_data__grid(cap_data_sprinkling_grid): # Arrange # fmt: off - np.nan = nan expected_gw_abstraction = np.array( [[nan, 25., 0.], [nan, 25., 0.],