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

Skip to content

Issue #748 Fix failing transport model examples i.c.w. the MODFLOW6 nightly build #771

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 7 commits into from
Jan 24, 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
5 changes: 5 additions & 0 deletions docs/api/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Changed
advice doing development installations with pixi from now on. `See the
documentation. <https://deltares.github.io/imod-python/installation.html>`_

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

[0.15.1] - 2023-12-22
---------------------
Expand Down
1 change: 1 addition & 0 deletions imod/mf6/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from imod.mf6.evt import Evapotranspiration
from imod.mf6.ghb import GeneralHeadBoundary
from imod.mf6.gwfgwf import GWFGWF
from imod.mf6.gwfgwt import GWFGWT
from imod.mf6.hfb import (
HorizontalFlowBarrierBase,
HorizontalFlowBarrierHydraulicCharacteristic,
Expand Down
42 changes: 42 additions & 0 deletions imod/mf6/exchangebase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import Dict, Tuple

from imod.mf6.package import Package

_pkg_id_to_type = {"gwfgwf": "GWF6-GWF6", "gwfgwt": "GWF6-GWT6"}


class ExchangeBase(Package):
"""
Base class for all the exchanges.
This class enables writing the exchanges to file in a uniform way.
"""

_keyword_map: Dict[str, str] = {}

@property
def model_name1(self) -> str:
if "model_name_1" not in self.dataset:
raise ValueError("model_name_1 not present in dataset")
return self.dataset["model_name_1"].values[()].take(0)

@property
def model_name2(self) -> str:
if "model_name_2" not in self.dataset:
raise ValueError("model_name_2 not present in dataset")
return self.dataset["model_name_2"].values[()].take(0)

def package_name(self) -> str:
return f"{self.model_name1}_{self.model_name2}"

def get_specification(self) -> Tuple[str, str, str, str]:
"""
Returns a tuple containing the exchange type, the exchange file name, and the model names. This can be used
to write the exchange information in the simulation .nam input file
"""
filename = f"{self.package_name()}.{self._pkg_id}"
return (
_pkg_id_to_type[self._pkg_id],
filename,
self.model_name1,
self.model_name2,
)
24 changes: 3 additions & 21 deletions imod/mf6/gwfgwf.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
from typing import Dict, Optional, Tuple
from typing import Optional

import cftime
import numpy as np
import xarray as xr

from imod.mf6.auxiliary_variables import add_periodic_auxiliary_variable
from imod.mf6.exchangebase import ExchangeBase
from imod.mf6.package import Package
from imod.typing import GridDataArray


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

_keyword_map: Dict[str, str] = {}
_auxiliary_data = {"auxiliary_data": "variable"}
_pkg_id = "gwfgwf"
_template = Package._initialize_template(_pkg_id)
Expand Down Expand Up @@ -72,24 +72,6 @@ def set_options(
self.dataset["xt3d"] = xt3d
self.dataset["newton"] = newton

def filename(self) -> str:
return f"{self.packagename()}.{self._pkg_id}"

def packagename(self) -> str:
return f"{self.dataset['model_name_1'].values[()]}_{self.dataset['model_name_2'].values[()]}"

def get_specification(self) -> Tuple[str, str, str, str]:
"""
Returns a tuple containing the exchange type, the exchange file name, and the model names. This can be used
to write the exchange information in the simulation .nam input file
"""
return (
"GWF6-GWF6",
self.filename(),
self.dataset["model_name_1"].values[()].take(0),
self.dataset["model_name_2"].values[()].take(0),
)

def clip_box(
self,
time_min: Optional[cftime.datetime | np.datetime64 | str] = None,
Expand Down
38 changes: 38 additions & 0 deletions imod/mf6/gwfgwt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from copy import deepcopy
from typing import Optional

import cftime
import numpy as np

from imod.mf6.exchangebase import ExchangeBase
from imod.mf6.package import Package
from imod.typing import GridDataArray


class GWFGWT(ExchangeBase):
_pkg_id = "gwfgwt"
_template = Package._initialize_template(_pkg_id)

def __init__(self, model_id1: str, model_id2: str):
super().__init__(locals())
self.dataset["model_name_1"] = model_id1
self.dataset["model_name_2"] = model_id2

def clip_box(
self,
time_min: Optional[cftime.datetime | np.datetime64 | str] = None,
time_max: Optional[cftime.datetime | np.datetime64 | str] = None,
layer_min: Optional[int] = None,
layer_max: Optional[int] = None,
x_min: Optional[float] = None,
x_max: Optional[float] = None,
y_min: Optional[float] = None,
y_max: Optional[float] = None,
top: Optional[GridDataArray] = None,
bottom: Optional[GridDataArray] = None,
state_for_boundary: Optional[GridDataArray] = None,
) -> Package:
"""
The GWF-GWT exchange does not have any spatial coordinates that can be clipped.
"""
return deepcopy(self)
2 changes: 1 addition & 1 deletion imod/mf6/pkgbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from imod.mf6.interfaces.ipackagebase import IPackageBase

TRANSPORT_PACKAGES = ("adv", "dsp", "ssm", "mst", "ist", "src")
EXCHANGE_PACKAGES = "gwfgwf"
EXCHANGE_PACKAGES = ("gwfgwf", "gwfgwt")


class PackageBase(IPackageBase, abc.ABC):
Expand Down
38 changes: 26 additions & 12 deletions imod/mf6/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
import xugrid as xu

import imod
import imod.logging
import imod.mf6.exchangebase
from imod.mf6.gwfgwf import GWFGWF
from imod.mf6.gwfgwt import GWFGWT
from imod.mf6.model import Modflow6Model
from imod.mf6.model_gwf import GroundwaterFlowModel
from imod.mf6.model_gwt import GroundwaterTransportModel
Expand Down Expand Up @@ -236,6 +239,10 @@ def write(
if isinstance(model, Modflow6Model):
model._model_checks(key)

# Generate GWF-GWT exchanges
if gwfgwt_exchanges := self._generate_gwfgwt_exchanges():
self["gwtgwf_exchanges"] = gwfgwt_exchanges

directory = pathlib.Path(directory)
directory.mkdir(exist_ok=True, parents=True)

Expand Down Expand Up @@ -273,9 +280,9 @@ def write(
value.write(key, globaltimes, ims_write_context)
elif isinstance(value, list):
for exchange in value:
if isinstance(exchange, imod.mf6.GWFGWF):
if isinstance(exchange, imod.mf6.exchangebase.ExchangeBase):
exchange.write(
exchange.packagename(), globaltimes, write_context
exchange.package_name(), globaltimes, write_context
)

if status_info.has_errors():
Expand Down Expand Up @@ -791,16 +798,10 @@ def from_file(toml_path):

def get_exchange_relationships(self):
result = []
flowmodels = self.get_models_of_type("gwf6")
transportmodels = self.get_models_of_type("gwt6")
# exchange for flow and transport
if len(flowmodels) == 1 and len(transportmodels) > 0:
exchange_type = "GWF6-GWT6"
modelname_a = list(flowmodels.keys())[0]
for counter, key in enumerate(transportmodels.keys()):
filename = f"simulation{counter}.exg"
modelname_b = key
result.append((exchange_type, filename, modelname_a, modelname_b))

if "gwtgwf_exchanges" in self:
for exchange in self["gwtgwf_exchanges"]:
result.append(exchange.get_specification())

# exchange for splitting models
if is_split(self):
Expand Down Expand Up @@ -1044,3 +1045,16 @@ def __repr__(self) -> str:
else:
content = attrs + ["){}"]
return "\n".join(content)

def _generate_gwfgwt_exchanges(self):
flow_models = self.get_models_of_type("gwf6")
transport_models = self.get_models_of_type("gwt6")

# exchange for flow and transport
exchanges = []
if len(flow_models) == 1 and len(transport_models) > 0:
flow_model_name = list(flow_models.keys())[0]
for transport_model_name in transport_models.keys():
exchanges.append(GWFGWT(flow_model_name, transport_model_name))

return exchanges
Empty file.
1 change: 1 addition & 0 deletions imod/tests/fixtures/package_instance_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ def create_instance_boundary_condition_packages(is_unstructured):
},
),
),
imod.mf6.GWFGWT("model_name1", "model_name2"),
]


Expand Down
71 changes: 71 additions & 0 deletions imod/tests/test_mf6/test_exchangebase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from contextlib import nullcontext as does_not_raise

import pytest

from imod.mf6.exchangebase import ExchangeBase, _pkg_id_to_type


class DummyExchange(ExchangeBase):
_pkg_id = "gwfgwt"

def __init__(self, model_id1: str = None, model_id2: str = None):
super().__init__()
if model_id1:
self.dataset["model_name_1"] = model_id1
if model_id2:
self.dataset["model_name_2"] = model_id2


def test_package_name_construct_name():
# Arrange.
model_name1 = "testmodel1"
model_name2 = "testmodel2"
exchange = DummyExchange(model_name1, model_name2)

# Act.
package_name = exchange.package_name()

# Assert.
assert model_name1 in package_name
assert model_name2 in package_name


@pytest.mark.parametrize(
("model_name1", "model_name2", "expectation"),
(
[None, None, pytest.raises(ValueError)],
["testmodel1", None, pytest.raises(ValueError)],
[None, "testmodel2", pytest.raises(ValueError)],
["testmodel1", "testmodel2", does_not_raise()],
),
)
def test_package_name_missing_name(model_name1, model_name2, expectation):
# Arrange
exchange = DummyExchange(model_name1, model_name2)

# Act/Assert
with expectation:
exchange.package_name()


def test_get_specification():
# Arrange.
model_name1 = "testmodel1"
model_name2 = "testmodel2"
exchange = DummyExchange(model_name1, model_name2)

# Act.
(
spec_exchange_type,
spec_filename,
spec_model_name1,
spec_model_name2,
) = exchange.get_specification()

# Assert
assert spec_exchange_type is _pkg_id_to_type[DummyExchange._pkg_id]
assert model_name1 in spec_filename
assert model_name2 in spec_filename
assert DummyExchange._pkg_id in spec_filename
assert spec_model_name1 == model_name1
assert spec_model_name2 == model_name2
8 changes: 4 additions & 4 deletions imod/tests/test_mf6/test_mf6_gwfgwf.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def test_option_xt3d_propagated(circle_model, xt3d_option, tmp_path):
label_array = get_label_array(circle_model, 3)
split_simulation = circle_model.split(label_array)

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

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

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

# check that the created exchagnes have the same dewatered option
# check that the created exchanges have the same dewatered option
for exchange in split_simulation["split_exchanges"]:
assert exchange.dataset["save_flows"].values[()] == budget_option
textrep = exchange.render(tmp_path, "gwfgwf", [], False)
Expand Down
3 changes: 2 additions & 1 deletion imod/tests/test_mf6/test_mf6_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,8 @@ def test_write_exchanges(
transient_twri_model.write(tmp_path, True, True, True)

# Assert
assert Path.exists(tmp_path / sample_gwfgwf_structured.filename())
_, filename, _, _ = sample_gwfgwf_structured.get_specification()
assert Path.exists(tmp_path / filename)

@pytest.mark.usefixtures("split_transient_twri_model")
def test_prevent_split_after_split(
Expand Down