Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Issue #805 Add missing validation concentrations #808

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/api/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~
Expand Down
14 changes: 13 additions & 1 deletion imod/mf6/drn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()],
}
Expand All @@ -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")
Expand Down
14 changes: 13 additions & 1 deletion imod/mf6/evt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()],
}
Expand All @@ -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")
Expand Down
24 changes: 20 additions & 4 deletions imod/mf6/wel.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@
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 AllNoDataSchema, DTypeSchema
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
Expand Down Expand Up @@ -144,8 +150,12 @@ def y(self) -> npt.NDArray[float]:
"concentration": [DTypeSchema(np.floating)],
}
_write_schemata = {
"y": [AllNoDataSchema()],
"x": [AllNoDataSchema()],
"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]] = {}
Expand Down Expand Up @@ -190,6 +200,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
Expand Down Expand Up @@ -512,7 +525,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()
Expand Down
11 changes: 11 additions & 0 deletions imod/schemata.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,17 @@ 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.
Expand Down
61 changes: 58 additions & 3 deletions imod/tests/test_mf6/test_mf6_drn.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,35 @@ 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)
Expand Down Expand Up @@ -95,16 +124,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
# 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():
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 == "conductance"
assert var == "concentration"


def test_discontinuous_layer(drainage):
Expand Down
25 changes: 25 additions & 0 deletions imod/tests/test_mf6/test_mf6_wel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(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.
Expand Down Expand Up @@ -142,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):
Expand Down