From 08719b4db2b04c9cc0906d13aa2672d431cf188f Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Thu, 1 Feb 2024 17:22:42 +0100 Subject: [PATCH 01/16] Add validation for concenctrations --- imod/mf6/drn.py | 14 +++++++++++++- imod/mf6/evt.py | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/imod/mf6/drn.py b/imod/mf6/drn.py index ec04afb17..80adf816e 100644 --- a/imod/mf6/drn.py +++ b/imod/mf6/drn.py @@ -2,7 +2,7 @@ from imod.mf6.boundary_condition import BoundaryCondition from imod.mf6.regridding_utils import RegridderType -from imod.mf6.validation import BOUNDARY_DIMS_SCHEMA +from imod.mf6.validation import BOUNDARY_DIMS_SCHEMA, CONC_DIMS_SCHEMA from imod.schemata import ( AllInsideNoDataSchema, AllNoDataSchema, @@ -78,6 +78,17 @@ class Drainage(BoundaryCondition): CoordsSchema(("layer",)), BOUNDARY_DIMS_SCHEMA, ], + "concentration": [ + DTypeSchema(np.floating), + IndexesSchema(), + CoordsSchema( + ( + "species", + "layer", + ) + ), + CONC_DIMS_SCHEMA, + ], "print_flows": [DTypeSchema(np.bool_), DimsSchema()], "save_flows": [DTypeSchema(np.bool_), DimsSchema()], } @@ -88,6 +99,7 @@ class Drainage(BoundaryCondition): AllInsideNoDataSchema(other="idomain", is_other_notnull=(">", 0)), ], "conductance": [IdentityNoDataSchema("elevation"), AllValueSchema(">", 0.0)], + "concentration": [IdentityNoDataSchema("elevation"), AllValueSchema(">=", 0.0)], } _period_data = ("elevation", "conductance") diff --git a/imod/mf6/evt.py b/imod/mf6/evt.py index 7cf6d2894..f8fb64d30 100644 --- a/imod/mf6/evt.py +++ b/imod/mf6/evt.py @@ -4,7 +4,7 @@ from imod.mf6.boundary_condition import BoundaryCondition from imod.mf6.regridding_utils import RegridderType -from imod.mf6.validation import BOUNDARY_DIMS_SCHEMA +from imod.mf6.validation import BOUNDARY_DIMS_SCHEMA, CONC_DIMS_SCHEMA from imod.schemata import ( AllInsideNoDataSchema, AllNoDataSchema, @@ -128,6 +128,17 @@ class Evapotranspiration(BoundaryCondition): CoordsSchema(("layer",)), SEGMENT_BOUNDARY_DIMS_SCHEMA, ], + "concentration": [ + DTypeSchema(np.floating), + IndexesSchema(), + CoordsSchema( + ( + "species", + "layer", + ) + ), + CONC_DIMS_SCHEMA, + ], "print_flows": [DTypeSchema(np.bool_), DimsSchema()], "save_flows": [DTypeSchema(np.bool_), DimsSchema()], } @@ -145,6 +156,7 @@ class Evapotranspiration(BoundaryCondition): AllValueSchema(">=", 0.0), AllValueSchema("<=", 1.0), ], + "concentration": [IdentityNoDataSchema("surface"), AllValueSchema(">=", 0.0)], } _period_data = ("surface", "rate", "depth", "proportion_depth", "proportion_rate") From a54c38b96087dcdd5bef9b595944f2dadc60e7b3 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Thu, 1 Feb 2024 17:23:20 +0100 Subject: [PATCH 02/16] Add AnyNoDataScheme and implement in Well package --- imod/mf6/wel.py | 10 +++++++--- imod/schemata.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/imod/mf6/wel.py b/imod/mf6/wel.py index 1af603f09..103443040 100644 --- a/imod/mf6/wel.py +++ b/imod/mf6/wel.py @@ -25,7 +25,7 @@ from imod.mf6.utilities.grid import create_layered_top from imod.mf6.write_context import WriteContext from imod.prepare import assign_wells -from imod.schemata import AllNoDataSchema, DTypeSchema +from imod.schemata import AnyNoDataSchema, DTypeSchema from imod.select.points import points_indices, points_values from imod.typing import GridDataArray from imod.typing.grid import is_spatial_2D, ones_like @@ -144,8 +144,12 @@ def y(self) -> npt.NDArray[float]: "concentration": [DTypeSchema(np.floating)], } _write_schemata = { - "y": [AllNoDataSchema()], - "x": [AllNoDataSchema()], + "screen_top": [AnyNoDataSchema()], + "screen_bottom": [AnyNoDataSchema()], + "y": [AnyNoDataSchema()], + "x": [AnyNoDataSchema()], + "rate": [AnyNoDataSchema()], + "concentration": [AnyNoDataSchema()], } _regrid_method: dict[str, Tuple[RegridderType, str]] = {} diff --git a/imod/schemata.py b/imod/schemata.py index 1e22316a6..6162a0b4c 100644 --- a/imod/schemata.py +++ b/imod/schemata.py @@ -471,6 +471,16 @@ def validate(self, obj: Union[xr.DataArray, xu.UgridDataArray], **kwargs): raise ValidationError("all nodata") +class AnyNoDataSchema(NoDataSchema): + """ + Fails when any data is NoData. + """ + + def validate(self, obj: Union[xr.DataArray, xu.UgridDataArray], **kwargs): + valid = self.is_notnull(obj) + if ~valid.all(): + raise ValidationError("found a nodata value") + class NoDataComparisonSchema(BaseSchema): """ Base class for IdentityNoDataSchema and AllInsideNoDataSchema. From df0ec23e359ac9b8c877ac3aef08e1ac3fff4d33 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Thu, 1 Feb 2024 18:29:13 +0100 Subject: [PATCH 03/16] Add test for concentration validation --- imod/tests/test_mf6/test_mf6_drn.py | 60 ++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/imod/tests/test_mf6/test_mf6_drn.py b/imod/tests/test_mf6/test_mf6_drn.py index 1f8220700..3920ba6da 100644 --- a/imod/tests/test_mf6/test_mf6_drn.py +++ b/imod/tests/test_mf6/test_mf6_drn.py @@ -49,6 +49,32 @@ def transient_drainage(): return drn +@pytest.fixture(scope="function") +def transient_concentration_drainage(): + layer = np.arange(1, 4) + y = np.arange(4.5, 0.0, -1.0) + x = np.arange(0.5, 5.0, 1.0) + elevation = xr.DataArray( + np.full((3, 5, 5), 1.0), + coords={"layer": layer, "y": y, "x": x, "dx": 1.0, "dy": -1.0}, + dims=("layer", "y", "x"), + ) + time_multiplier = xr.DataArray( + data=np.arange(1.0, 7.0, 1.0), + coords={"time": pd.date_range("2000-01-01", "2005-01-01", freq="YS")}, + dims=("time",), + ) + species_multiplier = xr.DataArray( + data=[35.0, 1.0], + coords={"species": ["salinity", "temperature"]}, + dims=("species",), + ) + conductance = time_multiplier * elevation + concentration = species_multiplier * conductance + + drn = dict(elevation=elevation, conductance=conductance, concentration=concentration) + return drn + def test_write(drainage, tmp_path): drn = imod.mf6.Drainage(**drainage) write_context = WriteContext(simulation_directory=tmp_path, use_binary=True) @@ -95,18 +121,42 @@ def test_check_conductance_zero(drainage): top = 1.0 bottom = top - idomain.coords["layer"] - dis = imod.mf6.StructuredDiscretization(top=1.0, bottom=bottom, idomain=idomain) - + dis = imod.mf6.StructuredDiscretization(top=top, bottom=bottom, idomain=idomain) drn = imod.mf6.Drainage(**drainage) - errors = drn._validate(drn._write_schemata, **dis.dataset) - assert len(errors) == 1 - for var, error in errors.items(): assert var == "conductance" +def test_validate_concentration(transient_concentration_drainage): + idomain = transient_concentration_drainage["elevation"].astype(np.int16) + top = 1.0 + bottom = top - idomain.coords["layer"] + + dis = imod.mf6.StructuredDiscretization(top=top, bottom=bottom, idomain=idomain) + drn = imod.mf6.Drainage(**transient_concentration_drainage) + + # No errors at start + errors = drn._validate(drn._write_schemata, **dis.dataset) + assert len(errors) == 0 + + # Error with incongruent data + drn.dataset["concentration"] = idomain.where(False) # Set all concentrations to NaN + errors = drn._validate(drn._write_schemata, **dis.dataset) + assert len(errors) == 1 + for var, error in errors.items(): + assert var == "concentration" + + # Error with smaller than zero + drn.dataset["concentration"] = idomain.where(False, -200.0) # Set concentrations negative + errors = drn._validate(drn._write_schemata, **dis.dataset) + assert len(errors) == 1 + for var, error in errors.items(): + assert var == "concentration" + + + def test_discontinuous_layer(drainage): drn = imod.mf6.Drainage(**drainage) drn["layer"] = [1, 3, 5] From 0973b0ddaeb2cc826d1692cee8c140c5a29bf624 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Thu, 1 Feb 2024 18:47:37 +0100 Subject: [PATCH 04/16] Throw validation error --- imod/mf6/wel.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/imod/mf6/wel.py b/imod/mf6/wel.py index 103443040..b78189e8f 100644 --- a/imod/mf6/wel.py +++ b/imod/mf6/wel.py @@ -23,9 +23,10 @@ from imod.mf6.utilities.clip import clip_by_grid from imod.mf6.utilities.dataset import remove_inactive from imod.mf6.utilities.grid import create_layered_top +from imod.mf6.validation import validation_pkg_error_message from imod.mf6.write_context import WriteContext from imod.prepare import assign_wells -from imod.schemata import AnyNoDataSchema, DTypeSchema +from imod.schemata import AnyNoDataSchema, DTypeSchema, ValidationError from imod.select.points import points_indices, points_values from imod.typing import GridDataArray from imod.typing.grid import is_spatial_2D, ones_like @@ -516,7 +517,10 @@ def to_mf6_pkg( Object with wells as list based input. """ if validate: - self._validate(self._write_schemata) + errors = self._validate(self._write_schemata) + if len(errors) > 0: + message = validation_pkg_error_message(errors) + raise ValidationError(message) minimum_k = self.dataset["minimum_k"].item() minimum_thickness = self.dataset["minimum_thickness"].item() From abdb393f699ae551175acb60bcd8a84b0074ccd2 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Thu, 1 Feb 2024 18:47:58 +0100 Subject: [PATCH 05/16] Add tests for wel validation --- imod/tests/test_mf6/test_mf6_wel.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/imod/tests/test_mf6/test_mf6_wel.py b/imod/tests/test_mf6/test_mf6_wel.py index de1afcfcf..d416df3b5 100644 --- a/imod/tests/test_mf6/test_mf6_wel.py +++ b/imod/tests/test_mf6/test_mf6_wel.py @@ -54,6 +54,20 @@ def test_to_mf6_pkg__high_lvl_stationary(basic_dis, well_high_lvl_test_data_stat np.testing.assert_equal(mf6_ds["rate"].values, rate_expected) +def test_to_mf6_pkg__validate(basic_dis, well_high_lvl_test_data_stationary): + # Arrange + wel = imod.mf6.Well(*well_high_lvl_test_data_stationary) + + # Act + errors = wel._validate(wel._write_schemata) + assert len(errors) == 0 + + # Set rates with index exceeding 3 to NaN. + wel.dataset["rate"] = wel.dataset["rate"].where(wel.dataset.coords["index"] < 3) + errors = wel._validate(wel._write_schemata) + assert len(errors) == 1 + + def test_to_mf6_pkg__high_lvl_multilevel(basic_dis, well_high_lvl_test_data_stationary): """ Test with stationary wells where the first 4 well screens extend over 2 layers. From a94b08fcae833ce92e99fda147cd3d7b4ec43999 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Thu, 1 Feb 2024 18:48:34 +0100 Subject: [PATCH 06/16] format --- imod/mf6/out/cbc.py | 5 +---- imod/schemata.py | 1 + imod/tests/test_code_checks.py | 4 ++-- imod/tests/test_mf6/test_mf6_drn.py | 18 +++++++++++------- imod/tests/test_mf6/test_mf6_uzf.py | 8 ++------ imod/wq/pkgbase.py | 4 +--- imod/wq/pkggroup.py | 4 +--- 7 files changed, 19 insertions(+), 25 deletions(-) diff --git a/imod/mf6/out/cbc.py b/imod/mf6/out/cbc.py index a8114bc6e..062b679ce 100644 --- a/imod/mf6/out/cbc.py +++ b/imod/mf6/out/cbc.py @@ -119,10 +119,7 @@ def read_cbc_headers( datasize = ( # Multiply by -1 because ndim3 is stored as a negative for some reason. # (ndim3 is the integer size of the third dimension) - header["ndim1"] - * header["ndim2"] - * header["ndim3"] - * -1 + header["ndim1"] * header["ndim2"] * header["ndim3"] * -1 ) * 8 header["pos"] = f.tell() key = header["text"] diff --git a/imod/schemata.py b/imod/schemata.py index 6162a0b4c..2b3a27a20 100644 --- a/imod/schemata.py +++ b/imod/schemata.py @@ -481,6 +481,7 @@ def validate(self, obj: Union[xr.DataArray, xu.UgridDataArray], **kwargs): if ~valid.all(): raise ValidationError("found a nodata value") + class NoDataComparisonSchema(BaseSchema): """ Base class for IdentityNoDataSchema and AllInsideNoDataSchema. diff --git a/imod/tests/test_code_checks.py b/imod/tests/test_code_checks.py index ec0003fad..7d5eaf4a8 100644 --- a/imod/tests/test_code_checks.py +++ b/imod/tests/test_code_checks.py @@ -30,8 +30,8 @@ def test_check_modules(): paths = glob(test_directory + "/../**/*.py") ok = True for path in paths: - if test_directory in os.path.realpath( - path + if ( + test_directory in os.path.realpath(path) ): # if it's a test we don't care. this very file contains print statements itself. continue try: diff --git a/imod/tests/test_mf6/test_mf6_drn.py b/imod/tests/test_mf6/test_mf6_drn.py index 3920ba6da..aa03b07b8 100644 --- a/imod/tests/test_mf6/test_mf6_drn.py +++ b/imod/tests/test_mf6/test_mf6_drn.py @@ -72,9 +72,12 @@ def transient_concentration_drainage(): conductance = time_multiplier * elevation concentration = species_multiplier * conductance - drn = dict(elevation=elevation, conductance=conductance, concentration=concentration) + drn = dict( + elevation=elevation, conductance=conductance, concentration=concentration + ) return drn + def test_write(drainage, tmp_path): drn = imod.mf6.Drainage(**drainage) write_context = WriteContext(simulation_directory=tmp_path, use_binary=True) @@ -142,19 +145,20 @@ def test_validate_concentration(transient_concentration_drainage): assert len(errors) == 0 # Error with incongruent data - drn.dataset["concentration"] = idomain.where(False) # Set all concentrations to NaN + drn.dataset["concentration"] = idomain.where(False) # Set all concentrations to NaN errors = drn._validate(drn._write_schemata, **dis.dataset) - assert len(errors) == 1 + assert len(errors) == 1 for var, error in errors.items(): assert var == "concentration" - + # Error with smaller than zero - drn.dataset["concentration"] = idomain.where(False, -200.0) # Set concentrations negative + drn.dataset["concentration"] = idomain.where( + False, -200.0 + ) # Set concentrations negative errors = drn._validate(drn._write_schemata, **dis.dataset) - assert len(errors) == 1 + assert len(errors) == 1 for var, error in errors.items(): assert var == "concentration" - def test_discontinuous_layer(drainage): diff --git a/imod/tests/test_mf6/test_mf6_uzf.py b/imod/tests/test_mf6/test_mf6_uzf.py index 9e81c1a0a..f06c89c12 100644 --- a/imod/tests/test_mf6/test_mf6_uzf.py +++ b/imod/tests/test_mf6/test_mf6_uzf.py @@ -93,12 +93,8 @@ def test_to_sparsedata(test_data): layer = bin_data.isel(time=0)["layer"].values struct_array = uzf._to_struct_array(arrdict, layer) expected_iuzno = np.array([1, 2, 3, 4, 5, 6]) - assert struct_array.dtype[0] == np.dtype( - "int32" - ) # pylint: disable=unsubscriptable-object - assert struct_array.dtype[1] == np.dtype( - "float64" - ) # pylint: disable=unsubscriptable-object + assert struct_array.dtype[0] == np.dtype("int32") # pylint: disable=unsubscriptable-object + assert struct_array.dtype[1] == np.dtype("float64") # pylint: disable=unsubscriptable-object assert np.all(struct_array["iuzno"] == expected_iuzno) assert len(struct_array.dtype) == 8 assert len(struct_array) == 6 diff --git a/imod/wq/pkgbase.py b/imod/wq/pkgbase.py index c0694b824..d9c2e8dbe 100644 --- a/imod/wq/pkgbase.py +++ b/imod/wq/pkgbase.py @@ -138,9 +138,7 @@ def _render(self, *args, **kwargs): rendered : str The rendered runfile part for a single boundary condition system. """ - d = { - k: v.values for k, v in self.dataset.data_vars.items() - } # pylint: disable=no-member + d = {k: v.values for k, v in self.dataset.data_vars.items()} # pylint: disable=no-member if hasattr(self, "_keywords"): for key in self._keywords.keys(): self._replace_keyword(d, key) diff --git a/imod/wq/pkggroup.py b/imod/wq/pkggroup.py index 7f1d72152..34c76870b 100644 --- a/imod/wq/pkggroup.py +++ b/imod/wq/pkggroup.py @@ -48,9 +48,7 @@ def render(self, directory, globaltimes, nlayer, nrow, ncol): d["n_systems"] = len(self.keys()) d["n_max_active"] = sum( [ - v._max_active_n( - self._cellcount_varname, nlayer, nrow, ncol - ) # pylint:disable=no-member + v._max_active_n(self._cellcount_varname, nlayer, nrow, ncol) # pylint:disable=no-member for v in self.values() ] ) From c103119dc7a2c5994ed52bc06da21f6d4817bdd7 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Fri, 2 Feb 2024 09:17:47 +0100 Subject: [PATCH 07/16] Remove unused arg --- imod/tests/test_mf6/test_mf6_wel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imod/tests/test_mf6/test_mf6_wel.py b/imod/tests/test_mf6/test_mf6_wel.py index d416df3b5..937a590f4 100644 --- a/imod/tests/test_mf6/test_mf6_wel.py +++ b/imod/tests/test_mf6/test_mf6_wel.py @@ -54,7 +54,7 @@ def test_to_mf6_pkg__high_lvl_stationary(basic_dis, well_high_lvl_test_data_stat np.testing.assert_equal(mf6_ds["rate"].values, rate_expected) -def test_to_mf6_pkg__validate(basic_dis, well_high_lvl_test_data_stationary): +def test_to_mf6_pkg__validate(well_high_lvl_test_data_stationary): # Arrange wel = imod.mf6.Well(*well_high_lvl_test_data_stationary) From 3f5004a7c83615e8724ad37902d2520de3375533 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Fri, 2 Feb 2024 10:15:41 +0100 Subject: [PATCH 08/16] Update changelog --- docs/api/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api/changelog.rst b/docs/api/changelog.rst index a4a098fd1..a2a5c37ba 100644 --- a/docs/api/changelog.rst +++ b/docs/api/changelog.rst @@ -20,6 +20,10 @@ Fixed - Improved performance for merging structured multimodel Modflow 6 output - Bug where :func:`imod.formats.idf.open_subdomains` did not properly support custom patterns +- Added missing validation for ``concentration`` for :class:`imod.mf6.Drainage` and + :class:`imod.mf6.EvapoTranspiration` package +- Added validation :class:`imod.mf6.Well` package, no ``np.nan`` values are + allowed Changed ~~~~~~~ From 4126257c0a194349217fcc2207268f4e43878870 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Fri, 2 Feb 2024 10:34:19 +0100 Subject: [PATCH 09/16] run black --- imod/mf6/out/cbc.py | 5 ++++- imod/tests/test_code_checks.py | 4 ++-- imod/tests/test_mf6/test_mf6_uzf.py | 8 ++++++-- imod/wq/pkgbase.py | 4 +++- imod/wq/pkggroup.py | 4 +++- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/imod/mf6/out/cbc.py b/imod/mf6/out/cbc.py index 062b679ce..a8114bc6e 100644 --- a/imod/mf6/out/cbc.py +++ b/imod/mf6/out/cbc.py @@ -119,7 +119,10 @@ def read_cbc_headers( datasize = ( # Multiply by -1 because ndim3 is stored as a negative for some reason. # (ndim3 is the integer size of the third dimension) - header["ndim1"] * header["ndim2"] * header["ndim3"] * -1 + header["ndim1"] + * header["ndim2"] + * header["ndim3"] + * -1 ) * 8 header["pos"] = f.tell() key = header["text"] diff --git a/imod/tests/test_code_checks.py b/imod/tests/test_code_checks.py index 7d5eaf4a8..ec0003fad 100644 --- a/imod/tests/test_code_checks.py +++ b/imod/tests/test_code_checks.py @@ -30,8 +30,8 @@ def test_check_modules(): paths = glob(test_directory + "/../**/*.py") ok = True for path in paths: - if ( - test_directory in os.path.realpath(path) + if test_directory in os.path.realpath( + path ): # if it's a test we don't care. this very file contains print statements itself. continue try: diff --git a/imod/tests/test_mf6/test_mf6_uzf.py b/imod/tests/test_mf6/test_mf6_uzf.py index f06c89c12..9e81c1a0a 100644 --- a/imod/tests/test_mf6/test_mf6_uzf.py +++ b/imod/tests/test_mf6/test_mf6_uzf.py @@ -93,8 +93,12 @@ def test_to_sparsedata(test_data): layer = bin_data.isel(time=0)["layer"].values struct_array = uzf._to_struct_array(arrdict, layer) expected_iuzno = np.array([1, 2, 3, 4, 5, 6]) - assert struct_array.dtype[0] == np.dtype("int32") # pylint: disable=unsubscriptable-object - assert struct_array.dtype[1] == np.dtype("float64") # pylint: disable=unsubscriptable-object + assert struct_array.dtype[0] == np.dtype( + "int32" + ) # pylint: disable=unsubscriptable-object + assert struct_array.dtype[1] == np.dtype( + "float64" + ) # pylint: disable=unsubscriptable-object assert np.all(struct_array["iuzno"] == expected_iuzno) assert len(struct_array.dtype) == 8 assert len(struct_array) == 6 diff --git a/imod/wq/pkgbase.py b/imod/wq/pkgbase.py index d9c2e8dbe..c0694b824 100644 --- a/imod/wq/pkgbase.py +++ b/imod/wq/pkgbase.py @@ -138,7 +138,9 @@ def _render(self, *args, **kwargs): rendered : str The rendered runfile part for a single boundary condition system. """ - d = {k: v.values for k, v in self.dataset.data_vars.items()} # pylint: disable=no-member + d = { + k: v.values for k, v in self.dataset.data_vars.items() + } # pylint: disable=no-member if hasattr(self, "_keywords"): for key in self._keywords.keys(): self._replace_keyword(d, key) diff --git a/imod/wq/pkggroup.py b/imod/wq/pkggroup.py index 34c76870b..7f1d72152 100644 --- a/imod/wq/pkggroup.py +++ b/imod/wq/pkggroup.py @@ -48,7 +48,9 @@ def render(self, directory, globaltimes, nlayer, nrow, ncol): d["n_systems"] = len(self.keys()) d["n_max_active"] = sum( [ - v._max_active_n(self._cellcount_varname, nlayer, nrow, ncol) # pylint:disable=no-member + v._max_active_n( + self._cellcount_varname, nlayer, nrow, ncol + ) # pylint:disable=no-member for v in self.values() ] ) From 6d70c2f58a056dcb83b517f49fc53814bcfbfce5 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Mon, 5 Feb 2024 10:22:28 +0100 Subject: [PATCH 10/16] Set one cell to NaN instead of all --- imod/tests/test_mf6/test_mf6_drn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imod/tests/test_mf6/test_mf6_drn.py b/imod/tests/test_mf6/test_mf6_drn.py index aa03b07b8..acf8e2cbe 100644 --- a/imod/tests/test_mf6/test_mf6_drn.py +++ b/imod/tests/test_mf6/test_mf6_drn.py @@ -145,7 +145,7 @@ def test_validate_concentration(transient_concentration_drainage): assert len(errors) == 0 # Error with incongruent data - drn.dataset["concentration"] = idomain.where(False) # Set all concentrations to NaN + drn.dataset["concentration"][0, 2, 2] = np.nan # Rivers are located everywhere in the grid. errors = drn._validate(drn._write_schemata, **dis.dataset) assert len(errors) == 1 for var, error in errors.items(): From 19fcc0878476d265f1fe71ca7767a60bd93c0a26 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Mon, 5 Feb 2024 13:27:44 +0100 Subject: [PATCH 11/16] assign index coordinate --- imod/mf6/wel.py | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/imod/mf6/wel.py b/imod/mf6/wel.py index b78189e8f..43cec94b1 100644 --- a/imod/mf6/wel.py +++ b/imod/mf6/wel.py @@ -26,28 +26,49 @@ from imod.mf6.validation import validation_pkg_error_message from imod.mf6.write_context import WriteContext from imod.prepare import assign_wells -from imod.schemata import AnyNoDataSchema, DTypeSchema, ValidationError +from imod.schemata import ( + AnyNoDataSchema, + DTypeSchema, + EmptyIndexesSchema, + ValidationError, +) from imod.select.points import points_indices, points_values from imod.typing import GridDataArray from imod.typing.grid import is_spatial_2D, ones_like from imod.util import values_within_range +def _is_transient_da(arg): + is_da = isinstance(arg, xr.DataArray) + return is_da and "time" in arg.coords + + +def _get_index(arg): + if _is_transient_da(arg): + return np.arange(len(arg.isel(time=0))) + else: + return np.arange(len(arg)) + + def _assign_dims(arg: Any) -> Tuple | xr.DataArray: is_da = isinstance(arg, xr.DataArray) - if is_da and "time" in arg.coords: + index = _get_index(arg) + + if _is_transient_da(arg): if arg.ndim != 2: raise ValueError("time varying variable: must be 2d") if arg.dims[0] != "time": arg = arg.transpose() da = xr.DataArray( - data=arg.values, coords={"time": arg["time"]}, dims=["time", "index"] + data=arg.values, + coords={"time": arg.coords["time"], "index": index}, + dims=["time", "index"], ) return da elif is_da: - return "index", arg.values + return arg.assign_coords(index=index) else: - return "index", arg + return xr.DataArray(data=arg, coords={"index": index}, dims=["index"]) def mask_2D(package: Well, domain_2d: GridDataArray) -> Well: @@ -145,12 +166,12 @@ def y(self) -> npt.NDArray[float]: "concentration": [DTypeSchema(np.floating)], } _write_schemata = { - "screen_top": [AnyNoDataSchema()], - "screen_bottom": [AnyNoDataSchema()], - "y": [AnyNoDataSchema()], - "x": [AnyNoDataSchema()], - "rate": [AnyNoDataSchema()], - "concentration": [AnyNoDataSchema()], + "screen_top": [AnyNoDataSchema(), EmptyIndexesSchema()], + "screen_bottom": [AnyNoDataSchema(), EmptyIndexesSchema()], + "y": [AnyNoDataSchema(), EmptyIndexesSchema()], + "x": [AnyNoDataSchema(), EmptyIndexesSchema()], + "rate": [AnyNoDataSchema(), EmptyIndexesSchema()], + "concentration": [AnyNoDataSchema(), EmptyIndexesSchema()], } _regrid_method: dict[str, Tuple[RegridderType, str]] = {} From d898464057fd5f428c82e57921b13f0d564f4507 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Mon, 5 Feb 2024 13:30:37 +0100 Subject: [PATCH 12/16] Place comment above statements to improve formatting --- imod/tests/test_mf6/test_mf6_drn.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/imod/tests/test_mf6/test_mf6_drn.py b/imod/tests/test_mf6/test_mf6_drn.py index acf8e2cbe..d38cb6859 100644 --- a/imod/tests/test_mf6/test_mf6_drn.py +++ b/imod/tests/test_mf6/test_mf6_drn.py @@ -145,7 +145,8 @@ def test_validate_concentration(transient_concentration_drainage): assert len(errors) == 0 # Error with incongruent data - drn.dataset["concentration"][0, 2, 2] = np.nan # Rivers are located everywhere in the grid. + # Rivers are located everywhere in the grid. + drn.dataset["concentration"][0, 2, 2] = np.nan errors = drn._validate(drn._write_schemata, **dis.dataset) assert len(errors) == 1 for var, error in errors.items(): From 13b241cb7943574ec60a3e2687ae59f9aa9aef9c Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Mon, 5 Feb 2024 14:27:58 +0100 Subject: [PATCH 13/16] Revert "assign index coordinate" This reverts commit 19fcc0878476d265f1fe71ca7767a60bd93c0a26. --- imod/mf6/wel.py | 43 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/imod/mf6/wel.py b/imod/mf6/wel.py index 43cec94b1..b78189e8f 100644 --- a/imod/mf6/wel.py +++ b/imod/mf6/wel.py @@ -26,49 +26,28 @@ from imod.mf6.validation import validation_pkg_error_message from imod.mf6.write_context import WriteContext from imod.prepare import assign_wells -from imod.schemata import ( - AnyNoDataSchema, - DTypeSchema, - EmptyIndexesSchema, - ValidationError, -) +from imod.schemata import AnyNoDataSchema, DTypeSchema, ValidationError from imod.select.points import points_indices, points_values from imod.typing import GridDataArray from imod.typing.grid import is_spatial_2D, ones_like from imod.util import values_within_range -def _is_transient_da(arg): - is_da = isinstance(arg, xr.DataArray) - return is_da and "time" in arg.coords - - -def _get_index(arg): - if _is_transient_da(arg): - return np.arange(len(arg.isel(time=0))) - else: - return np.arange(len(arg)) - - def _assign_dims(arg: Any) -> Tuple | xr.DataArray: is_da = isinstance(arg, xr.DataArray) - index = _get_index(arg) - - if _is_transient_da(arg): + if is_da and "time" in arg.coords: if arg.ndim != 2: raise ValueError("time varying variable: must be 2d") if arg.dims[0] != "time": arg = arg.transpose() da = xr.DataArray( - data=arg.values, - coords={"time": arg.coords["time"], "index": index}, - dims=["time", "index"], + data=arg.values, coords={"time": arg["time"]}, dims=["time", "index"] ) return da elif is_da: - return arg.assign_coords(index=index) + return "index", arg.values else: - return xr.DataArray(data=arg, coords={"index": index}, dims=["index"]) + return "index", arg def mask_2D(package: Well, domain_2d: GridDataArray) -> Well: @@ -166,12 +145,12 @@ def y(self) -> npt.NDArray[float]: "concentration": [DTypeSchema(np.floating)], } _write_schemata = { - "screen_top": [AnyNoDataSchema(), EmptyIndexesSchema()], - "screen_bottom": [AnyNoDataSchema(), EmptyIndexesSchema()], - "y": [AnyNoDataSchema(), EmptyIndexesSchema()], - "x": [AnyNoDataSchema(), EmptyIndexesSchema()], - "rate": [AnyNoDataSchema(), EmptyIndexesSchema()], - "concentration": [AnyNoDataSchema(), EmptyIndexesSchema()], + "screen_top": [AnyNoDataSchema()], + "screen_bottom": [AnyNoDataSchema()], + "y": [AnyNoDataSchema()], + "x": [AnyNoDataSchema()], + "rate": [AnyNoDataSchema()], + "concentration": [AnyNoDataSchema()], } _regrid_method: dict[str, Tuple[RegridderType, str]] = {} From 6733bacc069b623387e0f8549f72507e8c97b127 Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Mon, 5 Feb 2024 14:36:38 +0100 Subject: [PATCH 14/16] Add index coordinate after dataset is merged --- imod/mf6/wel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/imod/mf6/wel.py b/imod/mf6/wel.py index b78189e8f..72eaf4c14 100644 --- a/imod/mf6/wel.py +++ b/imod/mf6/wel.py @@ -195,6 +195,9 @@ def __init__( "concentration_boundary_type": concentration_boundary_type, } super().__init__(dict_dataset) + # Set index as coordinate + index_coord = np.arange(self.dataset.dims["index"]) + self.dataset = self.dataset.assign_coords(index = index_coord) self._validate_init_schemata(validate) @classmethod From 76522e61657b5b9a3d739837c5ca4b0fb69f56bf Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Mon, 5 Feb 2024 15:09:23 +0100 Subject: [PATCH 15/16] Format --- imod/mf6/wel.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/imod/mf6/wel.py b/imod/mf6/wel.py index 72eaf4c14..caa2977e6 100644 --- a/imod/mf6/wel.py +++ b/imod/mf6/wel.py @@ -26,7 +26,12 @@ from imod.mf6.validation import validation_pkg_error_message from imod.mf6.write_context import WriteContext from imod.prepare import assign_wells -from imod.schemata import AnyNoDataSchema, DTypeSchema, ValidationError +from imod.schemata import ( + AnyNoDataSchema, + DTypeSchema, + EmptyIndexesSchema, + ValidationError, +) from imod.select.points import points_indices, points_values from imod.typing import GridDataArray from imod.typing.grid import is_spatial_2D, ones_like @@ -145,12 +150,12 @@ def y(self) -> npt.NDArray[float]: "concentration": [DTypeSchema(np.floating)], } _write_schemata = { - "screen_top": [AnyNoDataSchema()], - "screen_bottom": [AnyNoDataSchema()], - "y": [AnyNoDataSchema()], - "x": [AnyNoDataSchema()], - "rate": [AnyNoDataSchema()], - "concentration": [AnyNoDataSchema()], + "screen_top": [AnyNoDataSchema(), EmptyIndexesSchema()], + "screen_bottom": [AnyNoDataSchema(), EmptyIndexesSchema()], + "y": [AnyNoDataSchema(), EmptyIndexesSchema()], + "x": [AnyNoDataSchema(), EmptyIndexesSchema()], + "rate": [AnyNoDataSchema(), EmptyIndexesSchema()], + "concentration": [AnyNoDataSchema(), EmptyIndexesSchema()], } _regrid_method: dict[str, Tuple[RegridderType, str]] = {} @@ -197,7 +202,7 @@ def __init__( super().__init__(dict_dataset) # Set index as coordinate index_coord = np.arange(self.dataset.dims["index"]) - self.dataset = self.dataset.assign_coords(index = index_coord) + self.dataset = self.dataset.assign_coords(index=index_coord) self._validate_init_schemata(validate) @classmethod From acf56aec2c604cc285425b179b99132c48a5ea1c Mon Sep 17 00:00:00 2001 From: Joeri van Engelen Date: Mon, 5 Feb 2024 15:37:14 +0100 Subject: [PATCH 16/16] Add unittest for is_empty wel --- imod/tests/test_mf6/test_mf6_wel.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/imod/tests/test_mf6/test_mf6_wel.py b/imod/tests/test_mf6/test_mf6_wel.py index 937a590f4..1aa72ec60 100644 --- a/imod/tests/test_mf6/test_mf6_wel.py +++ b/imod/tests/test_mf6/test_mf6_wel.py @@ -156,6 +156,17 @@ def test_to_mf6_pkg__high_lvl_transient(basic_dis, well_high_lvl_test_data_trans np.testing.assert_equal(mf6_ds["rate"].values, rate_expected) +def test_is_empty(well_high_lvl_test_data_transient): + # Arrange + wel = imod.mf6.Well(*well_high_lvl_test_data_transient) + empty_wel_args = ([] for i in range(len(well_high_lvl_test_data_transient))) + wel_empty = imod.mf6.Well(*empty_wel_args, validate=False) + + # Act/Assert + assert not wel.is_empty() + assert wel_empty.is_empty() + + class ClipBoxCases: @staticmethod def case_clip_xy(parameterizable_basic_dis):