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

Skip to content

Commit fdfb9d6

Browse files
authored
Issue #748 Fix failing transport model examples i.c.w. the MODFLOW6 nightly build (#771)
Fixes #748 # Description The unit tests and examples were failing when run against the latest MODFLOW6 nightly. This is caused by a change on the MODFLOW6 side were any gwf-gwt exchanges has to have an actual existing file (even though it is emty). The precious behaviour was that just mentioning the gwf-gwt exchanges in the nam file was enough. This commit adds the gwfgwt package. It uses the same logic as the gwfgwf exchanges in terms of how it is written to disk. Any common methods between the different type of exchanges has been moved to the newly created ExchangeBase class **Note** I didn't add any unit tests for the simulation class. The reason for this is twofold: - I moved code around, no major functional changes - We have to refactor they way the gwf-gwt exchanges are being created the moment we start working on the multimodel support for gwt, making any unit tests I add obsolete # 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`` - [x] Unit tests were added - [ ] **If feature added**: Added/extended example
1 parent 7cf6310 commit fdfb9d6

File tree

12 files changed

+194
-39
lines changed

12 files changed

+194
-39
lines changed

docs/api/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ Changed
2525
advice doing development installations with pixi from now on. `See the
2626
documentation. <https://deltares.github.io/imod-python/installation.html>`_
2727

28+
Added
29+
~~~~~
30+
- Added support for coupling a GroundwaterFlowModel and Transport Model i.c.w.
31+
the 6.4.3 release of MODFLOW. Using an older version of iMOD Python
32+
with this version of MODFLOW will result in an error.
2833

2934
[0.15.1] - 2023-12-22
3035
---------------------

imod/mf6/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from imod.mf6.evt import Evapotranspiration
1414
from imod.mf6.ghb import GeneralHeadBoundary
1515
from imod.mf6.gwfgwf import GWFGWF
16+
from imod.mf6.gwfgwt import GWFGWT
1617
from imod.mf6.hfb import (
1718
HorizontalFlowBarrierBase,
1819
HorizontalFlowBarrierHydraulicCharacteristic,

imod/mf6/exchangebase.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from typing import Dict, Tuple
2+
3+
from imod.mf6.package import Package
4+
5+
_pkg_id_to_type = {"gwfgwf": "GWF6-GWF6", "gwfgwt": "GWF6-GWT6"}
6+
7+
8+
class ExchangeBase(Package):
9+
"""
10+
Base class for all the exchanges.
11+
This class enables writing the exchanges to file in a uniform way.
12+
"""
13+
14+
_keyword_map: Dict[str, str] = {}
15+
16+
@property
17+
def model_name1(self) -> str:
18+
if "model_name_1" not in self.dataset:
19+
raise ValueError("model_name_1 not present in dataset")
20+
return self.dataset["model_name_1"].values[()].take(0)
21+
22+
@property
23+
def model_name2(self) -> str:
24+
if "model_name_2" not in self.dataset:
25+
raise ValueError("model_name_2 not present in dataset")
26+
return self.dataset["model_name_2"].values[()].take(0)
27+
28+
def package_name(self) -> str:
29+
return f"{self.model_name1}_{self.model_name2}"
30+
31+
def get_specification(self) -> Tuple[str, str, str, str]:
32+
"""
33+
Returns a tuple containing the exchange type, the exchange file name, and the model names. This can be used
34+
to write the exchange information in the simulation .nam input file
35+
"""
36+
filename = f"{self.package_name()}.{self._pkg_id}"
37+
return (
38+
_pkg_id_to_type[self._pkg_id],
39+
filename,
40+
self.model_name1,
41+
self.model_name2,
42+
)

imod/mf6/gwfgwf.py

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1-
from typing import Dict, Optional, Tuple
1+
from typing import Optional
22

33
import cftime
44
import numpy as np
55
import xarray as xr
66

77
from imod.mf6.auxiliary_variables import add_periodic_auxiliary_variable
8+
from imod.mf6.exchangebase import ExchangeBase
89
from imod.mf6.package import Package
910
from imod.typing import GridDataArray
1011

1112

12-
class GWFGWF(Package):
13+
class GWFGWF(ExchangeBase):
1314
"""
1415
This package is for writing an exchange file, used for splitting up a model
1516
into different submodels (that can be solved in parallel). It (usually)
1617
is not instantiated by users, but created by the "split" method of the
1718
simulation class."""
1819

19-
_keyword_map: Dict[str, str] = {}
2020
_auxiliary_data = {"auxiliary_data": "variable"}
2121
_pkg_id = "gwfgwf"
2222
_template = Package._initialize_template(_pkg_id)
@@ -72,24 +72,6 @@ def set_options(
7272
self.dataset["xt3d"] = xt3d
7373
self.dataset["newton"] = newton
7474

75-
def filename(self) -> str:
76-
return f"{self.packagename()}.{self._pkg_id}"
77-
78-
def packagename(self) -> str:
79-
return f"{self.dataset['model_name_1'].values[()]}_{self.dataset['model_name_2'].values[()]}"
80-
81-
def get_specification(self) -> Tuple[str, str, str, str]:
82-
"""
83-
Returns a tuple containing the exchange type, the exchange file name, and the model names. This can be used
84-
to write the exchange information in the simulation .nam input file
85-
"""
86-
return (
87-
"GWF6-GWF6",
88-
self.filename(),
89-
self.dataset["model_name_1"].values[()].take(0),
90-
self.dataset["model_name_2"].values[()].take(0),
91-
)
92-
9375
def clip_box(
9476
self,
9577
time_min: Optional[cftime.datetime | np.datetime64 | str] = None,

imod/mf6/gwfgwt.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from copy import deepcopy
2+
from typing import Optional
3+
4+
import cftime
5+
import numpy as np
6+
7+
from imod.mf6.exchangebase import ExchangeBase
8+
from imod.mf6.package import Package
9+
from imod.typing import GridDataArray
10+
11+
12+
class GWFGWT(ExchangeBase):
13+
_pkg_id = "gwfgwt"
14+
_template = Package._initialize_template(_pkg_id)
15+
16+
def __init__(self, model_id1: str, model_id2: str):
17+
super().__init__(locals())
18+
self.dataset["model_name_1"] = model_id1
19+
self.dataset["model_name_2"] = model_id2
20+
21+
def clip_box(
22+
self,
23+
time_min: Optional[cftime.datetime | np.datetime64 | str] = None,
24+
time_max: Optional[cftime.datetime | np.datetime64 | str] = None,
25+
layer_min: Optional[int] = None,
26+
layer_max: Optional[int] = None,
27+
x_min: Optional[float] = None,
28+
x_max: Optional[float] = None,
29+
y_min: Optional[float] = None,
30+
y_max: Optional[float] = None,
31+
top: Optional[GridDataArray] = None,
32+
bottom: Optional[GridDataArray] = None,
33+
state_for_boundary: Optional[GridDataArray] = None,
34+
) -> Package:
35+
"""
36+
The GWF-GWT exchange does not have any spatial coordinates that can be clipped.
37+
"""
38+
return deepcopy(self)

imod/mf6/pkgbase.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from imod.mf6.interfaces.ipackagebase import IPackageBase
1111

1212
TRANSPORT_PACKAGES = ("adv", "dsp", "ssm", "mst", "ist", "src")
13-
EXCHANGE_PACKAGES = "gwfgwf"
13+
EXCHANGE_PACKAGES = ("gwfgwf", "gwfgwt")
1414

1515

1616
class PackageBase(IPackageBase, abc.ABC):

imod/mf6/simulation.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
import xugrid as xu
1818

1919
import imod
20+
import imod.logging
21+
import imod.mf6.exchangebase
2022
from imod.mf6.gwfgwf import GWFGWF
23+
from imod.mf6.gwfgwt import GWFGWT
2124
from imod.mf6.model import Modflow6Model
2225
from imod.mf6.model_gwf import GroundwaterFlowModel
2326
from imod.mf6.model_gwt import GroundwaterTransportModel
@@ -236,6 +239,10 @@ def write(
236239
if isinstance(model, Modflow6Model):
237240
model._model_checks(key)
238241

242+
# Generate GWF-GWT exchanges
243+
if gwfgwt_exchanges := self._generate_gwfgwt_exchanges():
244+
self["gwtgwf_exchanges"] = gwfgwt_exchanges
245+
239246
directory = pathlib.Path(directory)
240247
directory.mkdir(exist_ok=True, parents=True)
241248

@@ -273,9 +280,9 @@ def write(
273280
value.write(key, globaltimes, ims_write_context)
274281
elif isinstance(value, list):
275282
for exchange in value:
276-
if isinstance(exchange, imod.mf6.GWFGWF):
283+
if isinstance(exchange, imod.mf6.exchangebase.ExchangeBase):
277284
exchange.write(
278-
exchange.packagename(), globaltimes, write_context
285+
exchange.package_name(), globaltimes, write_context
279286
)
280287

281288
if status_info.has_errors():
@@ -791,16 +798,10 @@ def from_file(toml_path):
791798

792799
def get_exchange_relationships(self):
793800
result = []
794-
flowmodels = self.get_models_of_type("gwf6")
795-
transportmodels = self.get_models_of_type("gwt6")
796-
# exchange for flow and transport
797-
if len(flowmodels) == 1 and len(transportmodels) > 0:
798-
exchange_type = "GWF6-GWT6"
799-
modelname_a = list(flowmodels.keys())[0]
800-
for counter, key in enumerate(transportmodels.keys()):
801-
filename = f"simulation{counter}.exg"
802-
modelname_b = key
803-
result.append((exchange_type, filename, modelname_a, modelname_b))
801+
802+
if "gwtgwf_exchanges" in self:
803+
for exchange in self["gwtgwf_exchanges"]:
804+
result.append(exchange.get_specification())
804805

805806
# exchange for splitting models
806807
if is_split(self):
@@ -1044,3 +1045,16 @@ def __repr__(self) -> str:
10441045
else:
10451046
content = attrs + ["){}"]
10461047
return "\n".join(content)
1048+
1049+
def _generate_gwfgwt_exchanges(self):
1050+
flow_models = self.get_models_of_type("gwf6")
1051+
transport_models = self.get_models_of_type("gwt6")
1052+
1053+
# exchange for flow and transport
1054+
exchanges = []
1055+
if len(flow_models) == 1 and len(transport_models) > 0:
1056+
flow_model_name = list(flow_models.keys())[0]
1057+
for transport_model_name in transport_models.keys():
1058+
exchanges.append(GWFGWT(flow_model_name, transport_model_name))
1059+
1060+
return exchanges

imod/templates/mf6/exg-gwfgwt.j2

Whitespace-only changes.

imod/tests/fixtures/package_instance_creation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ def create_instance_boundary_condition_packages(is_unstructured):
299299
},
300300
),
301301
),
302+
imod.mf6.GWFGWT("model_name1", "model_name2"),
302303
]
303304

304305

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from contextlib import nullcontext as does_not_raise
2+
3+
import pytest
4+
5+
from imod.mf6.exchangebase import ExchangeBase, _pkg_id_to_type
6+
7+
8+
class DummyExchange(ExchangeBase):
9+
_pkg_id = "gwfgwt"
10+
11+
def __init__(self, model_id1: str = None, model_id2: str = None):
12+
super().__init__()
13+
if model_id1:
14+
self.dataset["model_name_1"] = model_id1
15+
if model_id2:
16+
self.dataset["model_name_2"] = model_id2
17+
18+
19+
def test_package_name_construct_name():
20+
# Arrange.
21+
model_name1 = "testmodel1"
22+
model_name2 = "testmodel2"
23+
exchange = DummyExchange(model_name1, model_name2)
24+
25+
# Act.
26+
package_name = exchange.package_name()
27+
28+
# Assert.
29+
assert model_name1 in package_name
30+
assert model_name2 in package_name
31+
32+
33+
@pytest.mark.parametrize(
34+
("model_name1", "model_name2", "expectation"),
35+
(
36+
[None, None, pytest.raises(ValueError)],
37+
["testmodel1", None, pytest.raises(ValueError)],
38+
[None, "testmodel2", pytest.raises(ValueError)],
39+
["testmodel1", "testmodel2", does_not_raise()],
40+
),
41+
)
42+
def test_package_name_missing_name(model_name1, model_name2, expectation):
43+
# Arrange
44+
exchange = DummyExchange(model_name1, model_name2)
45+
46+
# Act/Assert
47+
with expectation:
48+
exchange.package_name()
49+
50+
51+
def test_get_specification():
52+
# Arrange.
53+
model_name1 = "testmodel1"
54+
model_name2 = "testmodel2"
55+
exchange = DummyExchange(model_name1, model_name2)
56+
57+
# Act.
58+
(
59+
spec_exchange_type,
60+
spec_filename,
61+
spec_model_name1,
62+
spec_model_name2,
63+
) = exchange.get_specification()
64+
65+
# Assert
66+
assert spec_exchange_type is _pkg_id_to_type[DummyExchange._pkg_id]
67+
assert model_name1 in spec_filename
68+
assert model_name2 in spec_filename
69+
assert DummyExchange._pkg_id in spec_filename
70+
assert spec_model_name1 == model_name1
71+
assert spec_model_name2 == model_name2

imod/tests/test_mf6/test_mf6_gwfgwf.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def test_option_xt3d_propagated(circle_model, xt3d_option, tmp_path):
169169
label_array = get_label_array(circle_model, 3)
170170
split_simulation = circle_model.split(label_array)
171171

172-
# check that the created exchagnes have the same newton option
172+
# check that the created exchanges have the same newton option
173173
for exchange in split_simulation["split_exchanges"]:
174174
assert exchange.dataset["xt3d"].values[()] == xt3d_option
175175
textrep = exchange.render(tmp_path, "gwfgwf", [], False)
@@ -185,7 +185,7 @@ def test_option_variablecv_propagated(circle_model, variablecv_option: bool, tmp
185185
label_array = get_label_array(circle_model, 3)
186186
split_simulation = circle_model.split(label_array)
187187

188-
# check that the created exchagnes have the same variablecv option
188+
# check that the created exchanges have the same variablecv option
189189
for exchange in split_simulation["split_exchanges"]:
190190
assert exchange.dataset["variablecv"].values[()] == variablecv_option
191191
textrep = exchange.render(tmp_path, "gwfgwf", [], False)
@@ -202,7 +202,7 @@ def test_option_dewatered_propagated(circle_model, dewatered_option: bool, tmp_p
202202
label_array = get_label_array(circle_model, 3)
203203
split_simulation = circle_model.split(label_array)
204204

205-
# check that the created exchagnes have the same dewatered option
205+
# check that the created exchanges have the same dewatered option
206206
for exchange in split_simulation["split_exchanges"]:
207207
assert exchange.dataset["dewatered"].values[()] == dewatered_option
208208
textrep = exchange.render(tmp_path, "gwfgwf", [], False)
@@ -219,7 +219,7 @@ def test_save_flows_propagated(circle_model, budget_option: bool, tmp_path):
219219
label_array = get_label_array(circle_model, 3)
220220
split_simulation = circle_model.split(label_array)
221221

222-
# check that the created exchagnes have the same dewatered option
222+
# check that the created exchanges have the same dewatered option
223223
for exchange in split_simulation["split_exchanges"]:
224224
assert exchange.dataset["save_flows"].values[()] == budget_option
225225
textrep = exchange.render(tmp_path, "gwfgwf", [], False)

imod/tests/test_mf6/test_mf6_simulation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,8 @@ def test_write_exchanges(
448448
transient_twri_model.write(tmp_path, True, True, True)
449449

450450
# Assert
451-
assert Path.exists(tmp_path / sample_gwfgwf_structured.filename())
451+
_, filename, _, _ = sample_gwfgwf_structured.get_specification()
452+
assert Path.exists(tmp_path / filename)
452453

453454
@pytest.mark.usefixtures("split_transient_twri_model")
454455
def test_prevent_split_after_split(

0 commit comments

Comments
 (0)