Skip to content

Commit 279a76e

Browse files
Issue #1260 from imod5 data metaswap (#1335)
Fixes #1260 # Description Adds ``MetaSwapModel.from_imod5_data`` # 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 c123cb4 commit 279a76e

45 files changed

Lines changed: 2552 additions & 796 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/api/changelog.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ The format is based on `Keep a Changelog`_, and this project adheres to
1212
Added
1313
~~~~~
1414

15+
- :class:`imod.msw.MeteoGridCopy` to copy existing `mete_grid.inp` files, so
16+
ASCII grids in large existing meteo databases do not have to be read.
17+
- :class:`imod.msw.CopyFiles` to copy settings and lookup tables in existing
18+
``.inp`` files.
19+
- :meth:`imod.mf6.LayeredWell.from_imod5_cap_data` to construct a
20+
:class:`imod.mf6.LayeredWell` package from iMOD5 data in the CAP package (for
21+
MetaSWAP). Currently only griddata (IDF) is supported.
22+
- :meth:`imod.mf6.Recharge.from_imod5_cap_data` to construct a recharge package
23+
for coupling a MODFLOW6 model to MetaSWAP.
24+
- :meth:`imod.msw.MetaSwapModel.from_imod5_data` to construct a MetaSWAP model
25+
from data in an iMOD5 projectfile.
26+
- :meth:`imod.msw.MetaSwapModel.write` has a ``validate`` argument, which can be
27+
used to turn off validation upon writing, use at your own risk!
28+
- :class:`imod.msw.MetaSwapModel` got ``settings`` argument to set simulation
29+
settings.
1530
- :func:`imod.data.tutorial_03` to load data for the iMOD Documentation
1631
tutorial.
1732

@@ -35,8 +50,13 @@ Fixed
3550
Changed
3651
~~~~~~~
3752

53+
- :class:`imod.msw.Infiltration`'s variables ``upward_resistance`` and
54+
``downward_resistance`` now require a ``subunit`` coordinate.
3855
- Variables ``max_abstraction_groundwater`` and ``max_abstraction_surfacewater``
3956
in :class:`imod.msw.Sprinkling` now needs to have a subunit coordinate.
57+
- If ``"cap"`` package present in ``imod5_data``,
58+
:meth:`imod.mf6.GroundwaterFlowModel.from_imod5_data` now automatically adds a
59+
well for metaswap sprinkling named ``"msw-sprinkling"``
4060

4161

4262
[0.18.1] - 2024-11-20

docs/api/mf6.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ Flow Packages
107107
HorizontalFlowBarrierResistance.clip_box
108108
LayeredWell
109109
LayeredWell.from_imod5_data
110+
LayeredWell.from_imod5_cap_data
110111
LayeredWell.mask
111112
LayeredWell.regrid_like
112113
LayeredWell.to_mf6_pkg

docs/api/msw.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ MetaSWAP
77
:toctree: generated/msw
88

99
GridData
10+
GridData.from_imod5_data
1011
Infiltration
12+
Infiltration.from_imod5_data
1113
Ponding
14+
Ponding.from_imod5_data
1215
ScalingFactors
16+
ScalingFactors.from_imod5_data
1317
Sprinkling
18+
Sprinkling.from_imod5_data
1419

1520
IdfMapping
1621
TimeOutputControl
@@ -25,9 +30,15 @@ MetaSWAP
2530
AnnualCropFactors
2631

2732
MeteoGrid
33+
MeteoGridCopy
34+
MeteoGridCopy.from_imod5_data
2835
EvapotranspirationMapping
36+
EvapotranspirationMapping.from_imod5_data
2937
PrecipitationMapping
38+
PrecipitationMapping.from_imod5_data
3039

3140
CouplerMapping
3241

3342
MetaSwapModel
43+
MetaSwapModel.write
44+
MetaSwapModel.from_imod5_data

imod/mf6/model_gwf.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,11 @@ def from_imod5_data(
218218
times: list[datetime]
219219
Time discretization of the simulation. These times are used for the
220220
following:
221-
* Times of wells with associated timeseries are resampled to these times
222-
* Start- and end times in the list are used to repeat the stresses
223-
of periodic data (e.g. river stages in iMOD5 for "summer", "winter")
221+
222+
* Times of wells with associated timeseries are resampled to these times
223+
* Start- and end times in the list are used to repeat the stresses
224+
of periodic data (e.g. river stages in iMOD5 for "summer", "winter")
225+
224226
allocation_options: SimulationAllocationOptions
225227
object containing the allocation options per package type.
226228
If you want a package to have a different allocation option,
@@ -303,9 +305,9 @@ def from_imod5_data(
303305
)
304306

305307
# now import the non-singleton packages'
308+
imod5_keys = list(imod5_data.keys())
306309

307310
# import wells
308-
imod5_keys = list(imod5_data.keys())
309311
wel_keys = [key for key in imod5_keys if key[0:3] == "wel"]
310312
for wel_key in wel_keys:
311313
wel_key_truncated = wel_key[:16]
@@ -330,8 +332,11 @@ def from_imod5_data(
330332
wel_key, imod5_data, times
331333
)
332334

335+
if "cap" in imod5_keys:
336+
result["msw-sprinkling"] = LayeredWell.from_imod5_cap_data(imod5_data) # type: ignore
337+
result["msw-rch"] = Recharge.from_imod5_cap_data(imod5_data) # type: ignore
338+
333339
# import ghb's
334-
imod5_keys = list(imod5_data.keys())
335340
ghb_keys = [key for key in imod5_keys if key[0:3] == "ghb"]
336341
for ghb_key in ghb_keys:
337342
ghb_pkg = GeneralHeadBoundary.from_imod5_data(

imod/mf6/rch.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from copy import deepcopy
2-
from typing import Optional
2+
from typing import Optional, cast
33

44
import numpy as np
5+
import xarray as xr
56

67
from imod.logging import init_log_decorator
78
from imod.mf6.boundary_condition import BoundaryCondition
@@ -11,6 +12,10 @@
1112
from imod.mf6.utilities.imod5_converter import convert_unit_rch_rate
1213
from imod.mf6.utilities.regrid import RegridderWeightsCache, _regrid_package_data
1314
from imod.mf6.validation import BOUNDARY_DIMS_SCHEMA, CONC_DIMS_SCHEMA
15+
from imod.msw.utilities.imod5_converter import (
16+
get_cell_area_from_imod5_data,
17+
is_msw_active_cell,
18+
)
1419
from imod.prepare.topsystem.allocation import ALLOCATION_OPTION, allocate_rch_cells
1520
from imod.schemata import (
1621
AllInsideNoDataSchema,
@@ -23,7 +28,7 @@
2328
IndexesSchema,
2429
OtherCoordsSchema,
2530
)
26-
from imod.typing import GridDataArray
31+
from imod.typing import GridDataArray, GridDataDict, Imod5DataDict
2732
from imod.typing.grid import (
2833
enforce_dim_order,
2934
is_planar_grid,
@@ -218,7 +223,7 @@ def from_imod5_data(
218223
# create an array indicating in which cells rch is active
219224
is_rch_cell = allocate_rch_cells(
220225
ALLOCATION_OPTION.at_first_active,
221-
new_idomain == 1,
226+
new_idomain > 0,
222227
planar_rate_regridded,
223228
)
224229

@@ -227,7 +232,34 @@ def from_imod5_data(
227232
rch_rate = enforce_dim_order(rch_rate)
228233
new_package_data["rate"] = rch_rate
229234

230-
return Recharge(**new_package_data, validate=True, fixed_cell=False)
235+
return cls(**new_package_data, validate=True, fixed_cell=False)
236+
237+
@classmethod
238+
def from_imod5_cap_data(
239+
cls,
240+
imod5_data: Imod5DataDict,
241+
target_dis: StructuredDiscretization,
242+
) -> "Recharge":
243+
"""
244+
Construct an rch-package from iMOD5 data in the CAP package, loaded with
245+
the :func:`imod.formats.prj.open_projectfile_data` function. Package is
246+
used to couple MODFLOW6 to MetaSWAP models. Active cells will have a
247+
recharge rate of 0.0.
248+
"""
249+
cap_data = cast(GridDataDict, imod5_data["cap"])
250+
251+
msw_area = get_cell_area_from_imod5_data(cap_data)
252+
msw_active = is_msw_active_cell(target_dis, cap_data, msw_area)
253+
active = msw_active.all
254+
255+
data = {}
256+
layer_da = xr.full_like(
257+
target_dis.dataset.coords["layer"], np.nan, dtype=np.float64
258+
)
259+
layer_da.loc[{"layer": 1}] = 0.0
260+
data["rate"] = layer_da.where(active)
261+
262+
return cls(**data, validate=True, fixed_cell=False)
231263

232264
@classmethod
233265
def get_regrid_methods(cls) -> RechargeRegridMethod:

imod/mf6/simulation.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,11 +1361,13 @@ def from_imod5_data(
13611361
times: list[datetime]
13621362
time discretization of the model to be imported. This is used for
13631363
the following:
1364+
13641365
* Times of wells with associated timeseries are resampled to these times
13651366
* Start and end times in the list are used to repeat the stresses
13661367
of periodic data (e.g. river stages in iMOD5 for "summer", "winter")
13671368
* The simulation is discretized to these times, using
13681369
:meth:`imod.mf6.Modflow6Simulation.create_time_discretization`
1370+
13691371
allocation_options: SimulationAllocationOptions, optional
13701372
object containing the allocation options per package type. If you
13711373
want a specific package to have a different allocation option, then

imod/mf6/utilities/imod5_converter.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
from typing import Union
1+
from typing import Union, cast
22

33
import numpy as np
4+
import pandas as pd
45
import xarray as xr
56

7+
from imod.typing import GridDataDict, Imod5DataDict, SelSettingsType
68
from imod.typing.grid import full_like
79

810

@@ -48,3 +50,71 @@ def fill_missing_layers(
4850
"""
4951
layer = full.coords["layer"]
5052
return source.reindex(layer=layer, fill_value=fillvalue)
53+
54+
55+
def _well_from_imod5_cap_point_data(cap_data: GridDataDict) -> dict[str, np.ndarray]:
56+
raise NotImplementedError(
57+
"Assigning sprinkling wells with an IPF file is not supported, please specify them as IDF."
58+
)
59+
60+
61+
def _well_from_imod5_cap_grid_data(cap_data: GridDataDict) -> dict[str, np.ndarray]:
62+
drop_layer_kwargs: SelSettingsType = {
63+
"layer": 0,
64+
"drop": True,
65+
"missing_dims": "ignore",
66+
}
67+
type = cap_data["artificial_recharge"].isel(**drop_layer_kwargs).compute()
68+
layer = (
69+
cap_data["artificial_recharge_layer"]
70+
.isel(**drop_layer_kwargs)
71+
.astype(int)
72+
.compute()
73+
)
74+
75+
from_groundwater = (type != 0).to_numpy()
76+
coords = type.coords
77+
x_grid, y_grid = np.meshgrid(coords["x"].to_numpy(), coords["y"].to_numpy())
78+
79+
data = {}
80+
data["layer"] = layer.data[from_groundwater]
81+
data["y"] = y_grid[from_groundwater]
82+
data["x"] = x_grid[from_groundwater]
83+
data["rate"] = np.zeros_like(data["x"])
84+
85+
return data
86+
87+
88+
def well_from_imod5_cap_data(imod5_data: Imod5DataDict) -> dict[str, np.ndarray]:
89+
"""
90+
Abstraction data for sprinkling is defined in iMOD5 either with grids (IDF)
91+
or points (IPF) combined with a grid. Depending on the type, the function does
92+
different conversions
93+
94+
- grids (IDF)
95+
The ``"artifical_recharge_layer"`` variable was defined as grid
96+
(IDF), this grid defines in which layer a groundwater abstraction
97+
well should be placed. The ``"artificial_recharge"`` grid contains
98+
types which point to the type of abstraction:
99+
* 0: no abstraction
100+
* 1: groundwater abstraction
101+
* 2: surfacewater abstraction
102+
The ``"artificial_recharge_capacity"`` grid/constant defines the
103+
capacity of each groundwater or surfacewater abstraction. This is an
104+
``1:1`` mapping: Each grid cell maps to a separate well.
105+
106+
- points with grid (IPF & IDF)
107+
The ``"artifical_recharge_layer"`` variable was defined as point
108+
data (IPF), this table contains wellids with an abstraction capacity
109+
and layer. The ``"artificial_recharge"`` grid contains a mapping of
110+
grid cells to wellids in the point data. The
111+
``"artificial_recharge_capacity"`` is ignored as the abstraction
112+
capacity is already defined in the point data. This is an ``n:1``
113+
mapping: multiple grid cells can map to one well.
114+
"""
115+
cap_data = cast(GridDataDict, imod5_data["cap"])
116+
117+
if isinstance(cap_data["artificial_recharge_layer"], pd.DataFrame):
118+
return _well_from_imod5_cap_point_data(cap_data)
119+
else:
120+
return _well_from_imod5_cap_grid_data(cap_data)

imod/mf6/wel.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from imod.mf6.package import Package
3131
from imod.mf6.utilities.dataset import remove_inactive
3232
from imod.mf6.utilities.grid import broadcast_to_full_domain
33+
from imod.mf6.utilities.imod5_converter import well_from_imod5_cap_data
3334
from imod.mf6.validation import validation_pkg_error_message
3435
from imod.mf6.validation_context import ValidationContext
3536
from imod.mf6.write_context import WriteContext
@@ -44,7 +45,7 @@
4445
ValidationError,
4546
)
4647
from imod.select.points import points_indices, points_values
47-
from imod.typing import GridDataArray
48+
from imod.typing import GridDataArray, Imod5DataDict
4849
from imod.typing.grid import is_spatial_grid, ones_like
4950
from imod.util.expand_repetitions import resample_timeseries
5051
from imod.util.structured import values_within_range
@@ -1298,6 +1299,48 @@ def _validate_imod5_depth_information(
12981299
logger.log(loglevel=LogLevel.ERROR, message=log_msg, additional_depth=2)
12991300
raise ValueError(log_msg)
13001301

1302+
@classmethod
1303+
def from_imod5_cap_data(cls, imod5_data: Imod5DataDict):
1304+
"""
1305+
Create LayeredWell from imod5_data in "cap" package. Abstraction data
1306+
for sprinkling is defined in iMOD5 either with grids (IDF) or points
1307+
(IPF) combined with a grid. Depending on the type, the function does
1308+
different conversions
1309+
1310+
- grids (IDF)
1311+
The ``"artifical_recharge_layer"`` variable was defined as grid
1312+
(IDF), this grid defines in which layer a groundwater abstraction
1313+
well should be placed. The ``"artificial_recharge"`` grid contains
1314+
types which point to the type of abstraction:
1315+
1316+
* 0: no abstraction
1317+
* 1: groundwater abstraction
1318+
* 2: surfacewater abstraction
1319+
1320+
The ``"artificial_recharge_capacity"`` grid/constant defines the
1321+
capacity of each groundwater or surfacewater abstraction. This is an
1322+
``1:1`` mapping: Each grid cell maps to a separate well.
1323+
1324+
- points with grid (IPF & IDF)
1325+
The ``"artifical_recharge_layer"`` variable was defined as point
1326+
data (IPF), this table contains wellids with an abstraction capacity
1327+
and layer. The ``"artificial_recharge"`` grid contains a mapping of
1328+
grid cells to wellids in the point data. The
1329+
``"artificial_recharge_capacity"`` is ignored as the abstraction
1330+
capacity is already defined in the point data. This is an ``n:1``
1331+
mapping: multiple grid cells can map to one well.
1332+
1333+
Parameters
1334+
----------
1335+
imod5_data: dict[str, dict[str, GridDataArray]]
1336+
dictionary containing the arrays mentioned in the project file as
1337+
xarray datasets, under the key of the package type to which it
1338+
belongs, as returned by
1339+
:func:`imod.formats.prj.open_projectfile_data`.
1340+
"""
1341+
data = well_from_imod5_cap_data(imod5_data)
1342+
return cls(**data) # type: ignore
1343+
13011344

13021345
class WellDisStructured(DisStructuredBoundaryCondition):
13031346
"""

imod/msw/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from imod.msw.copy_files import FileCopier
12
from imod.msw.coupler_mapping import CouplerMapping
23
from imod.msw.grid_data import GridData
34
from imod.msw.idf_mapping import IdfMapping
@@ -9,7 +10,7 @@
910
InitialConditionsSavedState,
1011
)
1112
from imod.msw.landuse import LanduseOptions
12-
from imod.msw.meteo_grid import MeteoGrid
13+
from imod.msw.meteo_grid import MeteoGrid, MeteoGridCopy
1314
from imod.msw.meteo_mapping import EvapotranspirationMapping, PrecipitationMapping
1415
from imod.msw.model import MetaSwapModel
1516
from imod.msw.output_control import TimeOutputControl, VariableOutputControl

0 commit comments

Comments
 (0)