Skip to content

Commit 41afa84

Browse files
authored
Issue #1779 Implementation of MetaSWAP model splitter (#1780)
Fixes #1779 # Description Implementation of MetaSWAP model splitter. # 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 - [ ] **If feature added**: Added feature to API documentation - [ ] **If pixi.lock was changed**: Ran `pixi run generate-sbom` and committed changes
1 parent 09cf5eb commit 41afa84

17 files changed

Lines changed: 313 additions & 100 deletions

docs/api/changelog.rst

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Added
1717
:meth:`imod.mf6.Modflow6Simulation.clip_box` to consider a package empty if
1818
its first times step is all nodata. This can save a lot of clipping or masking
1919
transient models with many timesteps.
20+
- Added :meth:`imod.msw.MetaSwapModel.split` to split MetaSWAP models.
2021

2122
Fixed
2223
~~~~~
@@ -55,7 +56,7 @@ Changed
5556
- ``proportion_depth`` and ``proportion_rate`` in
5657
:class:`imod.mf6.Evapotranspiration` are now optional variables. If provided,
5758
now require ``"segment"`` dimension when ``proportion_depth`` and
58-
``proportion_rate``.
59+
``proportion_rate``.
5960

6061

6162
[1.0.0] - 2025-11-11
@@ -134,7 +135,7 @@ Fixed
134135
- Fixed bug where :meth:`imod.mf6.Modflow6Simulation.split`,
135136
:meth:`imod.mf6.Modflow6Simulation.regrid_like`, and
136137
:meth:`imod.mf6.Modflow6Simulation.clip_box` would not copy
137-
:class:`imod.mf6.ValidationSettings`.
138+
:class:`imod.mf6.ValidationSettings`.
138139
- ``landuse``, ``soil_physical_unit``, ``active`` for :class:`imod.msw.GridData`
139140
are now properly regridded with the ``mode`` statistic when using
140141
:meth:`imod.msw.GridData.regrid_like`.
@@ -191,7 +192,7 @@ Fixed
191192
``states_for_boundary`` argument would place these bc at the
192193
incorrect places with unstructured grids.
193194
- Fixed bug where :meth:`imod.mf6.SourceSinkMixing.from_flow_model` would return
194-
an error upon adding a package which cannot have a ``concentration``, such as
195+
an error upon adding a package which cannot have a ``concentration``, such as
195196
:class:`imod.mf6.HorizontalFlowBarrierResistance`.
196197
- Broken names for ``outer_csvfile`` and ``inner_csvfile`` in the
197198
:class:`imod.mf6.Solution` MODFLOW 6 template file.
@@ -345,8 +346,8 @@ Changed
345346
:meth:`imod.mf6.Recharge.from_imod5_data` got extra arguments for
346347
``period_data``, ``time_min`` and ``time_max``.
347348
- :func:`imod.visualize.read_imod_legend` now also returns the labels as an extra
348-
argument. Update your code by changing
349-
``colors, levels = read_imod_legend(...)`` to
349+
argument. Update your code by changing
350+
``colors, levels = read_imod_legend(...)`` to
350351
``colors, levels, labels = read_imod_legend(...)``.
351352

352353

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import List, NamedTuple
2+
3+
import numpy as np
4+
5+
from imod.typing import GridDataArray
6+
from imod.typing.grid import ones_like
7+
8+
9+
class PartitionInfo(NamedTuple):
10+
active_domain: GridDataArray
11+
id: int
12+
13+
14+
def create_partition_info(submodel_labels: GridDataArray) -> List[PartitionInfo]:
15+
"""
16+
A PartitionInfo is used to partition a model or package. The partition
17+
info's of a domain are created using a submodel_labels array. The
18+
submodel_labels provided as input should have the same shape as a single
19+
layer of the model grid (all layers are split the same way), and contains an
20+
integer value in each cell. Each cell in the model grid will end up in the
21+
submodel with the index specified by the corresponding label of that cell.
22+
The labels should be numbers between 0 and the number of partitions.
23+
"""
24+
_validate_submodel_label_array(submodel_labels)
25+
26+
unique_labels = np.unique(submodel_labels.values)
27+
28+
partition_infos = []
29+
for label_id in unique_labels:
30+
active_domain = submodel_labels.where(submodel_labels.values == label_id)
31+
active_domain = ones_like(active_domain).where(active_domain.notnull(), 0)
32+
active_domain = active_domain.astype(submodel_labels.dtype)
33+
34+
submodel_partition_info = PartitionInfo(
35+
id=label_id, active_domain=active_domain
36+
)
37+
partition_infos.append(submodel_partition_info)
38+
39+
return partition_infos
40+
41+
42+
def _validate_submodel_label_array(submodel_labels: GridDataArray) -> None:
43+
unique_labels = np.unique(submodel_labels.values)
44+
45+
if not (
46+
len(unique_labels) == unique_labels.max() + 1
47+
and unique_labels.min() == 0
48+
and np.issubdtype(submodel_labels.dtype, np.integer)
49+
):
50+
raise ValueError(
51+
"The submodel_label array should be integer and contain all the numbers between 0 and the number of "
52+
"partitions minus 1."
53+
)

imod/mf6/multimodel/modelsplitter.py

Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,18 @@
1-
from typing import List, NamedTuple, cast
2-
3-
import numpy as np
1+
from typing import List, cast
42

53
from imod.common.interfaces.iagnosticpackage import IAgnosticPackage
64
from imod.common.interfaces.imodel import IModel
75
from imod.common.interfaces.ipackage import IPackage
86
from imod.common.utilities.clip import clip_by_grid
7+
from imod.common.utilities.partitioninfo import PartitionInfo
98
from imod.mf6.boundary_condition import BoundaryCondition
109
from imod.mf6.ims import Solution
1110
from imod.mf6.model_gwf import GroundwaterFlowModel
1211
from imod.mf6.model_gwt import GroundwaterTransportModel
1312
from imod.mf6.ssm import SourceSinkMixing
1413
from imod.mf6.validation_settings import ValidationSettings, trim_time_dimension
1514
from imod.typing import GridDataArray
16-
from imod.typing.grid import (
17-
get_non_spatial_dimension_names,
18-
ones_like,
19-
)
20-
21-
22-
class PartitionInfo(NamedTuple):
23-
active_domain: GridDataArray
24-
id: int
25-
26-
27-
def create_partition_info(submodel_labels: GridDataArray) -> List[PartitionInfo]:
28-
"""
29-
A PartitionInfo is used to partition a model or package. The partition
30-
info's of a domain are created using a submodel_labels array. The
31-
submodel_labels provided as input should have the same shape as a single
32-
layer of the model grid (all layers are split the same way), and contains an
33-
integer value in each cell. Each cell in the model grid will end up in the
34-
submodel with the index specified by the corresponding label of that cell.
35-
The labels should be numbers between 0 and the number of partitions.
36-
"""
37-
_validate_submodel_label_array(submodel_labels)
38-
39-
unique_labels = np.unique(submodel_labels.values)
40-
41-
partition_infos = []
42-
for label_id in unique_labels:
43-
active_domain = submodel_labels.where(submodel_labels.values == label_id)
44-
active_domain = ones_like(active_domain).where(active_domain.notnull(), 0)
45-
active_domain = active_domain.astype(submodel_labels.dtype)
46-
47-
submodel_partition_info = PartitionInfo(
48-
id=label_id, active_domain=active_domain
49-
)
50-
partition_infos.append(submodel_partition_info)
51-
52-
return partition_infos
53-
54-
55-
def _validate_submodel_label_array(submodel_labels: GridDataArray) -> None:
56-
unique_labels = np.unique(submodel_labels.values)
57-
58-
if not (
59-
len(unique_labels) == unique_labels.max() + 1
60-
and unique_labels.min() == 0
61-
and np.issubdtype(submodel_labels.dtype, np.integer)
62-
):
63-
raise ValueError(
64-
"The submodel_label array should be integer and contain all the numbers between 0 and the number of "
65-
"partitions minus 1."
66-
)
15+
from imod.typing.grid import get_non_spatial_dimension_names
6716

6817

6918
class ModelSplitter:

imod/mf6/simulation.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from imod.common.statusinfo import NestedStatusInfo
2626
from imod.common.utilities.dataclass_type import DataclassType
2727
from imod.common.utilities.mask import mask_all_models
28+
from imod.common.utilities.partitioninfo import create_partition_info
2829
from imod.common.utilities.regrid import _regrid_like
2930
from imod.common.utilities.version import (
3031
get_version,
@@ -43,10 +44,7 @@
4344
from imod.mf6.multimodel.exchange_creator_unstructured import (
4445
ExchangeCreator_Unstructured,
4546
)
46-
from imod.mf6.multimodel.modelsplitter import (
47-
ModelSplitter,
48-
create_partition_info,
49-
)
47+
from imod.mf6.multimodel.modelsplitter import ModelSplitter
5048
from imod.mf6.out import open_cbc, open_conc, open_hds
5149
from imod.mf6.package import Package
5250
from imod.mf6.validation_settings import ValidationSettings

imod/msw/coupler_mapping.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import pandas as pd
55
import xarray as xr
66

7+
from imod.common.interfaces.ipackagebase import IPackageBase
78
from imod.mf6.dis import StructuredDiscretization
89
from imod.mf6.mf6_wel_adapter import Mf6Wel
910
from imod.msw.fixed_format import VariableMetaData
1011
from imod.msw.pkgbase import DataDictType, MetaSwapPackage
1112
from imod.typing import IntArray
1213

1314

14-
class CouplerMapping(MetaSwapPackage):
15+
class CouplerMapping(MetaSwapPackage, IPackageBase):
1516
"""
1617
This contains the data to connect MODFLOW 6 cells to MetaSWAP svats.
1718

imod/msw/grid_data.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,29 @@ def __init__(
8383

8484
self._pkgcheck()
8585

86-
def generate_index_array(self) -> tuple[np.ndarray, xr.DataArray]:
86+
def generate_index_array(self) -> np.ndarray:
87+
"""
88+
Generate index array to be used on other packages.
89+
90+
Returns
91+
-------
92+
np.ndarray
93+
Index array and svat grid.
94+
The index array is a 1D array with the index of the active cells.
95+
"""
96+
area = self.dataset["area"]
97+
active = self.dataset["active"]
98+
99+
isactive = area.where(active).notnull()
100+
# Load into memory to avoid dask issue
101+
# https://github.com/dask/dask/issues/11753
102+
isactive.load()
103+
104+
index = isactive.values.ravel()
105+
106+
return index
107+
108+
def generate_index_svat_array(self) -> tuple[np.ndarray, xr.DataArray]:
87109
"""
88110
Generate index array and svat grid to be used on other packages.
89111
@@ -94,17 +116,18 @@ def generate_index_array(self) -> tuple[np.ndarray, xr.DataArray]:
94116
The index array is a 1D array with the index of the active cells.
95117
The svat grid is a 2D array with the SVAT numbers for each cell.
96118
"""
119+
index = self.generate_index_array()
120+
97121
area = self.dataset["area"]
98122
active = self.dataset["active"]
99-
100123
isactive = area.where(active).notnull()
124+
101125
svat = xr.full_like(area, fill_value=0, dtype=np.int64).rename("svat")
102126
# Load into memory to avoid dask issue
103127
# https://github.com/dask/dask/issues/11753
104128
isactive.load()
105129
svat.load()
106130

107-
index = isactive.values.ravel()
108131
svat.data[isactive.data] = np.arange(1, index.sum() + 1)
109132

110133
return index, svat

imod/msw/meteo_mapping.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import xarray as xr
1010

1111
import imod
12+
from imod.common.interfaces.ipackagebase import IPackageBase
1213
from imod.common.utilities.clip import clip_spatial_box, clip_time_slice
1314
from imod.common.utilities.dataclass_type import DataclassType
1415
from imod.msw.fixed_format import VariableMetaData
@@ -65,7 +66,7 @@ def open_first_meteo_grid_from_imod5_data(imod5_data: Imod5DataDict, column_nr:
6566
return open_first_meteo_grid(metegrid_path, column_nr=column_nr)
6667

6768

68-
class MeteoMapping(MetaSwapPackage):
69+
class MeteoMapping(MetaSwapPackage, IPackageBase):
6970
"""
7071
This class provides common methods for creating mappings between
7172
meteorological data and MetaSWAP grids. It should not be instantiated

0 commit comments

Comments
 (0)