Skip to content

Commit 46c2e85

Browse files
Issue #1222 validation context (#1226)
Fixes #1222 # Description - Add a ``ValidationContext`` dataclass - Add a ``_validation_context`` attribute to ``Modflow6Simulation``; this replaces the ``_is_from_imod5`` attribute. - The ``ValidationContext`` contains an attribute for strict well checks, turned on by default. This is set to False when calling ``from_imod5`` or for split simulations. - Adds a ``_to_mf6_pkg`` method in a similar design as proposed in #1223, this to preserve public API. - Refactor ``WriteContext``, to make it a dataclass again. I had to ignore type annotation for ``write_directory``, otherwise MyPy would throw errors. The whole property shebang presumably started with MyPy throwing errors. Reverting it back to a dataclass reduces the lines of code considerably, which makes it more maintainable. - Use jit for examples run, this speeds them up considerably. The examples ran into a TimeOut on TeamCity, and this reduces the change of that happening again. # Checklist <!--- Before requesting review, please go through this checklist: --> - [x] Links to correct issue - [x] Update changelog, if changes affect users - [x] PR title starts with ``Issue #nr``, e.g. ``Issue #737`` - [ ] Unit tests were added - [ ] **If feature added**: Added/extended example
1 parent fe1c630 commit 46c2e85

File tree

14 files changed

+102
-104
lines changed

14 files changed

+102
-104
lines changed

docs/api/changelog.rst

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,9 @@ Changed
4646
when elevations are above model top for all methods in
4747
:func:`imod.prepare.ALLOCATION_OPTION`.
4848
- :meth:`imod.mf6.Well.to_mf6_pkg` got a new argument:
49-
``error_on_well_removal``, which controls the behavior for when wells are
49+
``strict_well_validation``, which controls the behavior for when wells are
5050
removed entirely during their assignment to layers. This replaces the
51-
``is_partioned`` argument. Note: if ``is_partioned`` was set to True before,
52-
this means you need to set ``error_on_removal`` to False now to get the same
53-
behavior.
54-
51+
``is_partitioned`` argument.
5552

5653
Added
5754
~~~~~

imod/mf6/model.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from imod.mf6.utilities.mf6hfb import merge_hfb_packages
2828
from imod.mf6.utilities.regrid import RegridderWeightsCache, _regrid_like
2929
from imod.mf6.validation import pkg_errors_to_status_info
30+
from imod.mf6.validation_context import ValidationContext
3031
from imod.mf6.wel import GridAgnosticWell
3132
from imod.mf6.write_context import WriteContext
3233
from imod.schemata import ValidationError
@@ -240,7 +241,11 @@ def validate(self, model_name: str = "") -> StatusInfoBase:
240241

241242
@standard_log_decorator()
242243
def write(
243-
self, modelname, globaltimes, validate: bool, write_context: WriteContext
244+
self,
245+
modelname,
246+
globaltimes,
247+
write_context: WriteContext,
248+
validate_context: ValidationContext,
244249
) -> StatusInfoBase:
245250
"""
246251
Write model namefile
@@ -250,7 +255,7 @@ def write(
250255
workdir = write_context.simulation_directory
251256
modeldirectory = workdir / modelname
252257
Path(modeldirectory).mkdir(exist_ok=True, parents=True)
253-
if validate:
258+
if validate_context.validate:
254259
model_status_info = self.validate(modelname)
255260
if model_status_info.has_errors():
256261
return model_status_info
@@ -271,16 +276,12 @@ def write(
271276
if issubclass(type(pkg), GridAgnosticWell):
272277
top, bottom, idomain = self.__get_domain_geometry()
273278
k = self.__get_k()
274-
error_on_well_removal = (not pkg_write_context.is_partitioned) & (
275-
not pkg_write_context.is_from_imod5
276-
)
277-
mf6_well_pkg = pkg.to_mf6_pkg(
279+
mf6_well_pkg = pkg._to_mf6_pkg(
278280
idomain,
279281
top,
280282
bottom,
281283
k,
282-
validate,
283-
error_on_well_removal,
284+
validate_context,
284285
)
285286
mf6_well_pkg.write(
286287
pkgname=pkg_name,

imod/mf6/simulation.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from imod.mf6.statusinfo import NestedStatusInfo
4444
from imod.mf6.utilities.mask import _mask_all_models
4545
from imod.mf6.utilities.regrid import _regrid_like
46+
from imod.mf6.validation_context import ValidationContext
4647
from imod.mf6.write_context import WriteContext
4748
from imod.prepare.topsystem.default_allocation_methods import (
4849
SimulationAllocationOptions,
@@ -97,7 +98,7 @@ def __init__(self, name):
9798
self.name = name
9899
self.directory = None
99100
self._initialize_template()
100-
self._is_from_imod5 = False
101+
self._validation_context = ValidationContext()
101102

102103
def __setitem__(self, key, value):
103104
super().__setitem__(key, value)
@@ -254,10 +255,9 @@ def write(
254255
"""
255256
# create write context
256257
write_context = WriteContext(directory, binary, use_absolute_paths)
258+
self._validation_context.validate = validate
257259
if self.is_split():
258-
write_context.is_partitioned = True
259-
if self._is_from_imod5:
260-
write_context.is_from_imod5 = True
260+
self._validation_context.strict_well_validation = False
261261

262262
# Check models for required content
263263
for key, model in self.items():
@@ -299,8 +299,8 @@ def write(
299299
value.write(
300300
modelname=key,
301301
globaltimes=globaltimes,
302-
validate=validate,
303302
write_context=model_write_context,
303+
validate_context=self._validation_context,
304304
)
305305
)
306306
elif isinstance(value, Package):
@@ -1367,7 +1367,7 @@ def from_imod5_data(
13671367
-------
13681368
"""
13691369
simulation = Modflow6Simulation("imported_simulation")
1370-
simulation._is_from_imod5 = True
1370+
simulation._validation_context.strict_well_validation = False
13711371

13721372
# import GWF model,
13731373
groundwaterFlowModel = GroundwaterFlowModel.from_imod5_data(

imod/mf6/validation_context.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class ValidationContext:
6+
validate: bool = True
7+
strict_well_validation: bool = True

imod/mf6/wel.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from imod.mf6.utilities.dataset import remove_inactive
3232
from imod.mf6.utilities.grid import broadcast_to_full_domain
3333
from imod.mf6.validation import validation_pkg_error_message
34+
from imod.mf6.validation_context import ValidationContext
3435
from imod.mf6.write_context import WriteContext
3536
from imod.prepare import assign_wells
3637
from imod.prepare.cleanup import cleanup_wel
@@ -388,7 +389,7 @@ def to_mf6_pkg(
388389
bottom: GridDataArray,
389390
k: GridDataArray,
390391
validate: bool = False,
391-
error_on_well_removal: bool = True,
392+
strict_well_validation: bool = True,
392393
) -> Mf6Wel:
393394
"""
394395
Write package to Modflow 6 package.
@@ -415,9 +416,10 @@ def to_mf6_pkg(
415416
Grid with bottom of model layers.
416417
k: {xarry.DataArray, xugrid.UgridDataArray}
417418
Grid with hydraulic conductivities.
418-
validate: bool
419+
validate: bool, default True
419420
Run validation before converting
420-
error_on_well_removal: bool
421+
strict_well_validation: bool, default True
422+
Set well validation strict:
421423
Throw error if well is removed entirely during its assignment to
422424
layers.
423425
@@ -426,7 +428,20 @@ def to_mf6_pkg(
426428
Mf6Wel
427429
Object with wells as list based input.
428430
"""
429-
if validate:
431+
validation_context = ValidationContext(
432+
validate=validate, strict_well_validation=strict_well_validation
433+
)
434+
return self._to_mf6_pkg(active, top, bottom, k, validation_context)
435+
436+
def _to_mf6_pkg(
437+
self,
438+
active: GridDataArray,
439+
top: GridDataArray,
440+
bottom: GridDataArray,
441+
k: GridDataArray,
442+
validation_context: ValidationContext,
443+
) -> Mf6Wel:
444+
if validation_context.validate:
430445
errors = self._validate(self._write_schemata)
431446
if len(errors) > 0:
432447
message = validation_pkg_error_message(errors)
@@ -446,6 +461,7 @@ def to_mf6_pkg(
446461
message_assign = self.to_mf6_package_information(
447462
filtered_assigned_well_ids, reason_text="permeability/thickness constraints"
448463
)
464+
error_on_well_removal = validation_context.strict_well_validation
449465
if error_on_well_removal and len(filtered_assigned_well_ids) > 0:
450466
logger.log(loglevel=LogLevel.ERROR, message=message_assign)
451467
raise ValidationError(message_assign)

imod/mf6/write_context.py

Lines changed: 15 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from dataclasses import dataclass
55
from os.path import relpath
66
from pathlib import Path
7-
from typing import Optional, Union
87

98

109
@dataclass
@@ -28,23 +27,18 @@ class WriteContext:
2827
it will be set to the simulation_directrory.
2928
"""
3029

31-
def __init__(
32-
self,
33-
simulation_directory: Path = Path("."),
34-
use_binary: bool = False,
35-
use_absolute_paths: bool = False,
36-
write_directory: Optional[Union[str, Path]] = None,
37-
):
38-
self.__simulation_directory = Path(simulation_directory)
39-
self.__use_binary = use_binary
40-
self.__use_absolute_paths = use_absolute_paths
41-
self.__write_directory = (
42-
Path(write_directory)
43-
if write_directory is not None
44-
else self.__simulation_directory
30+
simulation_directory: Path = Path(".")
31+
use_binary: bool = False
32+
use_absolute_paths: bool = False
33+
write_directory: Path = None # type: ignore
34+
35+
def __post_init__(self):
36+
self.simulation_directory = Path(self.simulation_directory)
37+
self.write_directory = (
38+
Path(self.write_directory)
39+
if self.write_directory is not None
40+
else self.simulation_directory
4541
)
46-
self.__is_partitioned = False
47-
self.__is_from_imod5 = False
4842

4943
def get_formatted_write_directory(self) -> Path:
5044
"""
@@ -53,57 +47,21 @@ def get_formatted_write_directory(self) -> Path:
5347
be relative to the simulation directory, which makes it usable by MF6.
5448
"""
5549
if self.use_absolute_paths:
56-
return self.__write_directory
57-
return Path(relpath(self.write_directory, self.__simulation_directory))
50+
return self.write_directory
51+
return Path(relpath(self.write_directory, self.simulation_directory))
5852

5953
def copy_with_new_write_directory(self, new_write_directory: Path) -> WriteContext:
6054
new_context = deepcopy(self)
61-
new_context.__write_directory = Path(new_write_directory)
55+
new_context.write_directory = Path(new_write_directory)
6256
return new_context
6357

64-
@property
65-
def simulation_directory(self) -> Path:
66-
return self.__simulation_directory
67-
68-
@property
69-
def use_binary(self) -> bool:
70-
return self.__use_binary
71-
72-
@use_binary.setter
73-
def use_binary(self, value) -> None:
74-
self.__use_binary = value
75-
76-
@property
77-
def use_absolute_paths(self) -> bool:
78-
return self.__use_absolute_paths
79-
80-
@property
81-
def write_directory(self) -> Path:
82-
return self.__write_directory
83-
8458
@property
8559
def root_directory(self) -> Path:
8660
"""
8761
returns the simulation directory, or nothing, depending on use_absolute_paths; use this to compose paths
8862
that are in agreement with the use_absolute_paths setting.
8963
"""
9064
if self.use_absolute_paths:
91-
return self.__simulation_directory
65+
return self.simulation_directory
9266
else:
9367
return Path("")
94-
95-
@property
96-
def is_partitioned(self) -> bool:
97-
return self.__is_partitioned
98-
99-
@is_partitioned.setter
100-
def is_partitioned(self, value: bool) -> None:
101-
self.__is_partitioned = value
102-
103-
@property
104-
def is_from_imod5(self) -> bool:
105-
return self.__is_from_imod5
106-
107-
@is_from_imod5.setter
108-
def is_from_imod5(self, value: bool) -> None:
109-
self.__is_from_imod5 = value

imod/tests/test_mf6/test_circle.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import imod
1111
from imod.logging import LoggerType, LogLevel
12+
from imod.mf6.validation_context import ValidationContext
1213
from imod.mf6.write_context import WriteContext
1314

1415

@@ -57,8 +58,8 @@ def test_gwfmodel_render(circle_model, tmp_path):
5758
simulation = circle_model
5859
globaltimes = simulation["time_discretization"]["time"].values
5960
gwfmodel = simulation["GWF_1"]
60-
write_context = WriteContext()
61-
actual = gwfmodel.render("GWF_1", write_context)
61+
write_context1 = WriteContext()
62+
actual = gwfmodel.render("GWF_1", write_context1)
6263
path = "GWF_1"
6364
expected = textwrap.dedent(
6465
f"""\
@@ -77,8 +78,9 @@ def test_gwfmodel_render(circle_model, tmp_path):
7778
"""
7879
)
7980
assert actual == expected
80-
context = WriteContext(tmp_path)
81-
gwfmodel.write("GWF_1", globaltimes, True, context)
81+
validation_context = ValidationContext(True)
82+
write_context2 = WriteContext(tmp_path)
83+
gwfmodel.write("GWF_1", globaltimes, write_context2, validation_context)
8284
assert (tmp_path / "GWF_1" / "GWF_1.nam").is_file()
8385
assert (tmp_path / "GWF_1").is_dir()
8486

@@ -110,8 +112,8 @@ def test_gwfmodel_render_evt(circle_model_evt, tmp_path):
110112
simulation = circle_model_evt
111113
globaltimes = simulation["time_discretization"]["time"].values
112114
gwfmodel = simulation["GWF_1"]
113-
write_context = WriteContext()
114-
actual = gwfmodel.render("GWF_1", write_context)
115+
write_context1 = WriteContext()
116+
actual = gwfmodel.render("GWF_1", write_context1)
115117
path = "GWF_1"
116118
expected = textwrap.dedent(
117119
f"""\
@@ -131,7 +133,8 @@ def test_gwfmodel_render_evt(circle_model_evt, tmp_path):
131133
"""
132134
)
133135
assert actual == expected
134-
context = WriteContext(tmp_path)
135-
gwfmodel.write("GWF_1", globaltimes, True, context)
136+
validation_context = ValidationContext(True)
137+
write_context2 = WriteContext(tmp_path)
138+
gwfmodel.write("GWF_1", globaltimes, write_context2, validation_context)
136139
assert (tmp_path / "GWF_1" / "GWF_1.nam").is_file()
137140
assert (tmp_path / "GWF_1").is_dir()

imod/tests/test_mf6/test_ex01_twri.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import xarray as xr
99

1010
import imod
11+
from imod.mf6.validation_context import ValidationContext
1112
from imod.mf6.write_context import WriteContext
1213
from imod.schemata import ValidationError
1314
from imod.typing.grid import ones_like
@@ -351,6 +352,7 @@ def test_gwfmodel_render(twri_model, tmp_path):
351352
globaltimes = simulation["time_discretization"]["time"].values
352353
gwfmodel = simulation["GWF_1"]
353354
path = Path(tmp_path.stem).as_posix()
355+
validation_context = ValidationContext(tmp_path)
354356
write_context = WriteContext(tmp_path)
355357
actual = gwfmodel.render(path, write_context)
356358
expected = textwrap.dedent(
@@ -373,7 +375,7 @@ def test_gwfmodel_render(twri_model, tmp_path):
373375
"""
374376
)
375377
assert actual == expected
376-
gwfmodel.write("GWF_1", globaltimes, True, write_context)
378+
gwfmodel.write("GWF_1", globaltimes, write_context, validation_context)
377379
assert (tmp_path / "GWF_1" / "GWF_1.nam").is_file()
378380
assert (tmp_path / "GWF_1").is_dir()
379381

imod/tests/test_mf6/test_mf6_logging.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import imod
1212
from imod.logging import LoggerType, LogLevel, standard_log_decorator
13+
from imod.mf6.validation_context import ValidationContext
1314
from imod.mf6.write_context import WriteContext
1415

1516
out = StringIO()
@@ -80,6 +81,7 @@ def test_write_model_is_logged(
8081
# arrange
8182
logfile_path = tmp_path / "logfile.txt"
8283
transport_model = flow_transport_simulation["tpt_c"]
84+
validation_context = ValidationContext()
8385
write_context = WriteContext(simulation_directory=tmp_path, use_binary=True)
8486
globaltimes = np.array(
8587
[
@@ -95,7 +97,9 @@ def test_write_model_is_logged(
9597
add_default_file_handler=False,
9698
add_default_stream_handler=True,
9799
)
98-
transport_model.write("model.txt", globaltimes, True, write_context)
100+
transport_model.write(
101+
"model.txt", globaltimes, write_context, validation_context
102+
)
99103

100104
# assert
101105
with open(logfile_path, "r") as log_file:

0 commit comments

Comments
 (0)