Skip to content
3 changes: 3 additions & 0 deletions docs/api/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ Added
- Functionality to dump and load MODFLOW 6 simulations to/from zarr and zipstore
formats. See :meth:`imod.mf6.Modflow6Simulation.dump` and
:meth:`imod.mf6.Modflow6Simulation.from_file` for more information.
- Functionality to dump and load MetaSwap models to/from netcdf
format. See :meth:`imod.msw.MetaSwapModel.dump` and
:meth:`imod.msw.MetaSwapModel.from_file` for more information.

Changed
~~~~~~~
Expand Down
3 changes: 2 additions & 1 deletion docs/api/msw.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Model objects & methods

MetaSwapModel
MetaSwapModel.write
MetaSwapModel.dump
MetaSwapModel.from_imod5_data
MetaSwapModel.regrid_like
MetaSwapModel.clip_box
Expand Down Expand Up @@ -172,4 +173,4 @@ Mappings
CouplerMapping.clip_box
CouplerMapping.from_imod5_data
CouplerMapping.get_regrid_methods
CouplerMapping.write
CouplerMapping.write
86 changes: 86 additions & 0 deletions imod/common/utilities/dump_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import collections
from pathlib import Path
from typing import Any, Optional

import tomli_w

from imod.common.interfaces.imodel import IModel
from imod.common.serializer import EngineType
from imod.logging.logging_decorators import standard_log_decorator
from imod.mf6.validation_settings import ValidationSettings
from imod.schemata import ValidationError


@standard_log_decorator()
def dump_model(
model: IModel,
directory,
modelname,
validate: bool = True,
mdal_compliant: bool = False,
crs: Optional[Any] = None,
engine: EngineType = "netcdf4",
) -> Path:
"""
Dump model to files. Writes a model definition as .TOML file, which
points to data for each package. Each package is stored as a separate
NetCDF. Structured grids are saved as regular NetCDFs, unstructured
grids are saved as UGRID NetCDF. Structured grids are always made GDAL
compliant, unstructured grids can be made MDAL compliant optionally.

Parameters
----------
directory: str or Path
directory to dump simulation into.
modelname: str
modelname, will be used to create a subdirectory.
validate: bool, optional
Whether to validate simulation data. Defaults to True.
mdal_compliant: bool, optional
Convert data with
:func:`imod.prepare.spatial.mdal_compliant_ugrid2d` to MDAL
compliant unstructured grids. Defaults to False.
crs: Any, optional
Anything accepted by rasterio.crs.CRS.from_user_input
Requires ``rioxarray`` installed.
engine : str, optional
File engine used to write packages. Options are ``'netcdf4'``,
``'zarr'``, and ``'zarr.zip'``. NetCDF4 is readable by many other
softwares, for example QGIS. Zarr is optimized for big data, cloud
storage and parallel access. The ``'zarr.zip'`` option is an
experimental option which creates a zipped zarr store in a single
file, which is easier to copy and automatically compresses data as
well. Default is ``'netcdf4'``.

"""
modeldirectory = Path(directory) / modelname
modeldirectory.mkdir(exist_ok=True, parents=True)

# validation currently only supports MF6, but we want to keep the option to turn it on for other
validation_context = ValidationSettings(validate=validate)
if validation_context.validate:
statusinfo = model.validate(modelname, validation_context)
if statusinfo.has_errors():
raise ValidationError(statusinfo.to_string())

toml_content: dict = collections.defaultdict(dict)

for pkgname, pkg in model.items():
pkg_path = pkg.to_file(
modeldirectory,
pkgname,
mdal_compliant=mdal_compliant,
crs=crs,
engine=engine,
)
toml_content[type(pkg).__name__][pkgname] = pkg_path.name

# simulation settings are only relevant/present for MetaSwap models (msw)
if hasattr(model, "simulation_settings"):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this only necessary for MetaSWAP models, right? If so, please add a comment that this is intended for MSW.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only applies to MetaSWAP models, I would rather do isinstance and ask if model is a MetaSWAP model, but that introduces a cyclic problem, therefore this indirect approach. I added a comment stating that this line is only relevant for MetaSWAP models.

toml_content["simulation_settings"] = model.simulation_settings

toml_path = modeldirectory / f"{modelname}.toml"
with open(toml_path, "wb") as f:
tomli_w.dump(toml_content, f)

return toml_path
14 changes: 13 additions & 1 deletion imod/common/utilities/value_filters.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import numbers
from typing import Any

import numpy as np
import xarray as xr
from xarray.core.utils import is_scalar

from imod.typing import GridDataset
from imod.typing import GridDataArray, GridDataset


def is_scalar_nan(da: GridDataArray):
"""
Test if is_scalar_nan, carefully avoid loading grids in memory
"""
scalar_data: bool = is_scalar(da)
if scalar_data:
stripped_value = da.to_numpy()[()]
return isinstance(stripped_value, numbers.Real) and np.isnan(stripped_value) # type: ignore[call-overload]
return False


def is_valid(value: Any) -> bool:
Expand Down
101 changes: 49 additions & 52 deletions imod/mf6/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import jinja2
import numpy as np
import tomli
import tomli_w
import xarray as xr
import xugrid as xu
from jinja2 import Template
Expand All @@ -22,6 +21,7 @@
from imod.common.serializer import EngineType
from imod.common.statusinfo import NestedStatusInfo, StatusInfo, StatusInfoBase
from imod.common.utilities.clip import clip_box_dataset
from imod.common.utilities.dump_model import dump_model
from imod.common.utilities.mask import mask_all_packages
from imod.common.utilities.regrid import _regrid_like
from imod.common.utilities.schemata import (
Expand Down Expand Up @@ -617,60 +617,57 @@ def dump(
engine: EngineType = "netcdf4",
) -> Path:
"""
Dump simulation to files. Writes a model definition as .TOML file, which
points to data for each package. Each package is stored as a separate
NetCDF. Structured grids are saved as regular NetCDFs, unstructured
grids are saved as UGRID NetCDF. Structured grids are always made GDAL
compliant, unstructured grids can be made MDAL compliant optionally.

Parameters
----------
directory: str or Path
directory to dump simulation into.
modelname: str
modelname, will be used to create a subdirectory.
validate: bool, optional
Whether to validate simulation data. Defaults to True.
mdal_compliant: bool, optional
Convert data with
:func:`imod.prepare.spatial.mdal_compliant_ugrid2d` to MDAL
compliant unstructured grids. Defaults to False.
crs: Any, optional
Anything accepted by rasterio.crs.CRS.from_user_input
Requires ``rioxarray`` installed.
engine : str, optional
File engine used to write packages. Options are ``'netcdf4'``,
``'zarr'``, and ``'zarr.zip'``. NetCDF4 is readable by many other
softwares, for example QGIS. Zarr is optimized for big data, cloud
storage and parallel access. The ``'zarr.zip'`` option is an
experimental option which creates a zipped zarr store in a single
file, which is easier to copy and automatically compresses data as
well. Default is ``'netcdf4'``.
Dump simulation to files. Writes a model definition as .TOML file, which
points to data for each package. Each package is stored as a separate
NetCDF. Structured grids are saved as regular NetCDFs, unstructured
grids are saved as UGRID NetCDF. Structured grids are always made GDAL
compliant, unstructured grids can be made MDAL compliant optionally.

Parameters
----------
directory: str or Path
directory to dump simulation into.
modelname: str
modelname, will be used to create a subdirectory.
validate: bool, optional
Whether to validate simulation data. Defaults to True.
mdal_compliant: bool, optional
Convert data with
:func:`imod.prepare.spatial.mdal_compliant_ugrid2d` to MDAL
compliant unstructured grids. Defaults to False.
crs: Any, optional
Anything accepted by rasterio.crs.CRS.from_user_input
Requires ``rioxarray`` installed.
engine : str, optional
File engine used to write packages. Options are ``'netcdf4'``,
``'zarr'``, and ``'zarr.zip'``. NetCDF4 is readable by many other
softwares, for example QGIS. Zarr is optimized for big data, cloud
storage and parallel access. The ``'zarr.zip'`` option is an
experimental option which creates a zipped zarr store in a single
file, which is easier to copy and automatically compresses data as
well. Default is ``'netcdf4'``.

Returns
-------
Path
Path to the created toml file which contains the paths to the dumped package files. The package files are dumped in the same directory as the toml file.

Example
-------
>>> tmp_path = tmpdir_factory.mktemp(name)
>>> toml_path = mf6_model.dump(tmp_path, name, engine=engine, validate=False)
>>> back = ModflowModel.from_file(tmp_path, name)
"""
modeldirectory = pathlib.Path(directory) / modelname
modeldirectory.mkdir(exist_ok=True, parents=True)
validation_context = ValidationSettings(validate=validate)
if validation_context.validate:
statusinfo = self.validate(modelname, validation_context)
if statusinfo.has_errors():
raise ValidationError(statusinfo.to_string())

toml_content: dict[str, Any] = collections.defaultdict(dict)

for pkgname, pkg in self.items():
pkg_path = pkg.to_file(
modeldirectory,
pkgname,
mdal_compliant=mdal_compliant,
crs=crs,
engine=engine,
)
toml_content[type(pkg).__name__][pkgname] = pkg_path.name

toml_path = modeldirectory / f"{modelname}.toml"
with open(toml_path, "wb") as f:
tomli_w.dump(toml_content, f)
toml_path = dump_model(
self,
directory,
modelname,
validate=validate,
mdal_compliant=mdal_compliant,
crs=crs,
engine=engine,
)

return toml_path

Expand Down
17 changes: 2 additions & 15 deletions imod/mf6/pkgbase.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import abc
import numbers
from pathlib import Path
from typing import TYPE_CHECKING, Any, Mapping, Optional, Self, final

import numpy as np
import xarray as xr
import xugrid as xu
from xarray.core.utils import is_scalar

import imod
from imod.common.interfaces.ipackagebase import IPackageBase
from imod.common.serializer import EngineType, create_package_serializer
from imod.common.utilities.value_filters import is_scalar_nan
from imod.typing.grid import (
GridDataArray,
GridDataset,
Expand All @@ -32,17 +30,6 @@
UTIL_PACKAGES = ("ats", "hpc")


def _is_scalar_nan(da: GridDataArray):
"""
Test if is_scalar_nan, carefully avoid loading grids in memory
"""
scalar_data: bool = is_scalar(da)
if scalar_data:
stripped_value = da.to_numpy()[()]
return isinstance(stripped_value, numbers.Real) and np.isnan(stripped_value) # type: ignore[call-overload]
return False


class PackageBase(IPackageBase, abc.ABC):
"""
This class is used for storing a collection of Xarray DataArrays or UgridDataArrays
Expand Down Expand Up @@ -148,7 +135,7 @@ def from_file(cls, path: str | Path, **kwargs) -> Self:

# Replace NaNs by None
for key, value in dataset.items():
if _is_scalar_nan(value):
if is_scalar_nan(value):
dataset[key] = None

# to_netcdf converts strings into NetCDF "variable‑length UTF‑8 strings"
Expand Down
Loading
Loading