Skip to content

Commit fdc827b

Browse files
Issue #1343 hfb dump (#1346)
Fixes #1343 # Description - Fix crash when calling ``GroundwaterFlowModel.dump`` when a HFB was assigned to the model - Really encode HFB as string this time when calling ``to_netcdf``... - Move MDAL/GDAL logic to ``Package.to_netcdf``, which is a more logical place for it then in ``Model.dump``. This has the advantage that this stuff is automatically not done when we override the method for the HFB package, as the MDAL/CRS stuff is irrelevant for HFBs. - Drop node_dim from UGRID dataset if present, it still remains available in the grid topology. - Ensure non-scalar data not loaded into memory when calling ``from_file``.
1 parent 2fdaeac commit fdc827b

7 files changed

Lines changed: 106 additions & 27 deletions

File tree

docs/api/changelog.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@ Added
3333
Fixed
3434
~~~~~
3535

36-
- Fixed bug were :class:`HorizontalFlowBarrierResistance`,
36+
- Fixed bug where :class:`HorizontalFlowBarrierResistance`,
3737
:class:`HorizontalFlowBarrierSingleLayerResistance` and other HFB packages could not
38-
be dumped, clipped, or copied with xarray >= 2024.10.0.
38+
be clipped or copied with xarray >= 2024.10.0.
39+
- Fixed crash upon calling :meth:`imod.mf6.GroundwaterFlowModel.dump`, when a
40+
:class:`HorizontalFlowBarrierResistance`,
41+
:class:`HorizontalFlowBarrierSingleLayerResistance` or other HFB package was
42+
assigned to the model.
3943
- :meth:`imod.mf6.Modflow6Simulation.regrid_like` can now regrid a structured
4044
model to an unstructured grid.
4145
- :meth:`imod.mf6.Modflow6Simulation.regrid_like` throws a

imod/mf6/hfb.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import typing
55
from copy import deepcopy
66
from enum import Enum
7-
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
7+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
88

99
import cftime
1010
import numpy as np
@@ -491,7 +491,11 @@ def render(self, directory, pkgname, globaltimes, binary):
491491
package, first convert to a Modflow6 package by calling pkg.to_mf6_pkg()"""
492492
)
493493

494-
def to_netcdf(self, *args, **kwargs):
494+
def to_netcdf(
495+
self, *args, mdal_compliant: bool = False, crs: Optional[Any] = None, **kwargs
496+
):
497+
kwargs.update({"encoding": self._netcdf_encoding()})
498+
495499
new = deepcopy(self)
496500
new.dataset["geometry"] = new.line_data.to_json()
497501
new.dataset.to_netcdf(*args, **kwargs)

imod/mf6/model.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
from imod.mf6.write_context import WriteContext
3737
from imod.schemata import ValidationError
3838
from imod.typing import GridDataArray
39-
from imod.typing.grid import is_spatial_grid
4039

4140
HFB_PKGNAME = "hfb_merged"
4241
SUGGESTION_TEXT = (
@@ -465,20 +464,9 @@ def dump(
465464
for pkgname, pkg in self.items():
466465
pkg_path = f"{pkgname}.nc"
467466
toml_content[type(pkg).__name__][pkgname] = pkg_path
468-
dataset = pkg.dataset
469-
if isinstance(dataset, xu.UgridDataset):
470-
if mdal_compliant:
471-
dataset = dataset.ugrid.to_dataset()
472-
mdal_dataset = imod.util.spatial.mdal_compliant_ugrid2d(
473-
dataset, crs=crs
474-
)
475-
mdal_dataset.to_netcdf(modeldirectory / pkg_path)
476-
else:
477-
dataset.ugrid.to_netcdf(modeldirectory / pkg_path)
478-
else:
479-
if is_spatial_grid(dataset):
480-
dataset = imod.util.spatial.gdal_compliant_grid(dataset, crs=crs)
481-
dataset.to_netcdf(modeldirectory / pkg_path)
467+
pkg.to_netcdf(
468+
modeldirectory / pkg_path, crs=crs, mdal_compliant=mdal_compliant
469+
)
482470

483471
toml_path = modeldirectory / f"{modelname}.toml"
484472
with open(toml_path, "wb") as f:

imod/mf6/pkgbase.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
import abc
22
import numbers
33
import pathlib
4-
from typing import Mapping
4+
from typing import Any, Mapping, Optional
55

66
import numpy as np
77
import xarray as xr
88
import xugrid as xu
9+
from xarray.core.utils import is_scalar
910

1011
import imod
1112
from imod.mf6.interfaces.ipackagebase import IPackageBase
12-
from imod.typing.grid import GridDataArray, GridDataset, merge_with_dictionary
13+
from imod.typing.grid import (
14+
GridDataArray,
15+
GridDataset,
16+
is_spatial_grid,
17+
merge_with_dictionary,
18+
)
1319

1420
TRANSPORT_PACKAGES = ("adv", "dsp", "ssm", "mst", "ist", "src")
1521
EXCHANGE_PACKAGES = ("gwfgwf", "gwfgwt", "gwtgwt")
1622

1723

24+
def _is_scalar_nan(da: GridDataArray):
25+
"""
26+
Test if is_scalar_nan, carefully avoid loading grids in memory
27+
"""
28+
scalar_data: bool = is_scalar(da)
29+
if scalar_data:
30+
stripped_value = da.to_numpy()[()]
31+
return isinstance(stripped_value, numbers.Real) and np.isnan(stripped_value) # type: ignore[call-overload]
32+
return False
33+
34+
1835
class PackageBase(IPackageBase, abc.ABC):
1936
"""
2037
This class is used for storing a collection of Xarray DataArrays or UgridDataArrays
@@ -48,15 +65,48 @@ def __getitem__(self, key):
4865
def __setitem__(self, key, value):
4966
self.dataset.__setitem__(key, value)
5067

51-
def to_netcdf(self, *args, **kwargs):
68+
def to_netcdf(
69+
self, *args, mdal_compliant: bool = False, crs: Optional[Any] = None, **kwargs
70+
):
5271
"""
5372
54-
Write dataset contents to a netCDF file.
55-
Custom encoding rules can be provided on package level by overriding the _netcdf_encoding in the package
73+
Write dataset contents to a netCDF file. Custom encoding rules can be
74+
provided on package level by overriding the _netcdf_encoding in the
75+
package
76+
77+
Parameters
78+
----------
79+
*args:
80+
Will be passed on to ``xr.Dataset.to_netcdf`` or
81+
``xu.UgridDataset.to_netcdf``.
82+
mdal_compliant: bool, optional
83+
Convert data with
84+
:func:`imod.prepare.spatial.mdal_compliant_ugrid2d` to MDAL
85+
compliant unstructured grids. Defaults to False.
86+
crs: Any, optional
87+
Anything accepted by rasterio.crs.CRS.from_user_input
88+
Requires ``rioxarray`` installed.
89+
**kwargs:
90+
Will be passed on to ``xr.Dataset.to_netcdf`` or
91+
``xu.UgridDataset.to_netcdf``.
5692
5793
"""
5894
kwargs.update({"encoding": self._netcdf_encoding()})
59-
self.dataset.to_netcdf(*args, **kwargs)
95+
96+
dataset = self.dataset
97+
if isinstance(dataset, xu.UgridDataset):
98+
if mdal_compliant:
99+
dataset = dataset.ugrid.to_dataset()
100+
mdal_dataset = imod.util.spatial.mdal_compliant_ugrid2d(
101+
dataset, crs=crs
102+
)
103+
mdal_dataset.to_netcdf(*args, **kwargs)
104+
else:
105+
dataset.ugrid.to_netcdf(*args, **kwargs)
106+
else:
107+
if is_spatial_grid(dataset):
108+
dataset = imod.util.spatial.gdal_compliant_grid(dataset, crs=crs)
109+
dataset.to_netcdf(*args, **kwargs)
60110

61111
def _netcdf_encoding(self):
62112
"""
@@ -123,11 +173,14 @@ def from_file(cls, path, **kwargs):
123173
if dataset.ugrid_roles.topology:
124174
dataset = xu.UgridDataset(dataset)
125175
dataset = imod.util.spatial.from_mdal_compliant_ugrid2d(dataset)
176+
# Drop node dim, as we don't need in the UgridDataset (it will be
177+
# preserved in the ``grid`` property, so topology stays intact!)
178+
node_dim = dataset.grid.node_dimension
179+
dataset = dataset.drop_dims(node_dim, errors="ignore")
126180

127181
# Replace NaNs by None
128182
for key, value in dataset.items():
129-
stripped_value = value.values[()]
130-
if isinstance(stripped_value, numbers.Real) and np.isnan(stripped_value): # type: ignore[call-overload]
183+
if _is_scalar_nan(value):
131184
dataset[key] = None
132185

133186
return cls._from_dataset(dataset)

imod/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
transient_unconfined_twri_model,
8282
transient_unconfined_twri_result,
8383
twri_model,
84+
twri_model_hfb,
8485
twri_result,
8586
)
8687
from .fixtures.mf6_welltest_fixture import (

imod/tests/fixtures/mf6_twri_fixture.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import geopandas as gpd
12
import numpy as np
23
import pandas as pd
34
import pytest
45
import xarray as xr
6+
from shapely import linestrings
57

68
import imod
79
from imod.typing.grid import zeros_like
@@ -232,6 +234,28 @@ def transient_twri_model():
232234
return simulation
233235

234236

237+
@pytest.fixture(scope="function")
238+
def twri_model_hfb():
239+
"""Returns steady-state confined model."""
240+
simulation = make_twri_model()
241+
gwf_model = simulation["GWF_1"]
242+
243+
barrier_y = [150_000.0, 50_000.0, 0.0]
244+
barrier_x = [50_000.0, 50_000.0, 50_000.0]
245+
246+
geometry = gpd.GeoDataFrame(
247+
geometry=[linestrings(barrier_x, barrier_y)],
248+
data={
249+
"resistance": [1200.0],
250+
"layer": [1],
251+
},
252+
)
253+
254+
gwf_model["hfb"] = imod.mf6.SingleLayerHorizontalFlowBarrierResistance(geometry)
255+
256+
return simulation
257+
258+
235259
@pytest.fixture(scope="function")
236260
def transient_unconfined_twri_model():
237261
"""Returns transient unconfined model, also saves specific discharges."""

imod/tests/test_mf6/test_mf6_simulation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ def test_twri_roundtrip(twri_model, tmpdir_factory):
5252
roundtrip(twri_model, tmpdir_factory, "twri")
5353

5454

55+
@pytest.mark.usefixtures("twri_model_hfb")
56+
def test_twri_hfb_roundtrip(twri_model_hfb, tmpdir_factory):
57+
roundtrip(twri_model_hfb, tmpdir_factory, "twri")
58+
59+
5560
@pytest.mark.usefixtures("transient_twri_model")
5661
def test_twri_transient_roundtrip(transient_twri_model, tmpdir_factory):
5762
roundtrip(transient_twri_model, tmpdir_factory, "twri_transient")

0 commit comments

Comments
 (0)