Skip to content
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
3 changes: 3 additions & 0 deletions docs/api/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Fixed
:class:`imod.mf6.Recharge`) where IBOUND is less than 0.
- :meth:`imod.msw.MetaSwapModel.from_imod5_data` now masks cells where IBOUND is
less than 0.
- :class:`imod.msw.FileCopier` and :class:`imod.msw.MeteoGridCopy` now force
paths to be stored as strings in the dataset. ``pathlib.Path`` objects could
cause errors when calling :meth:`imod.msw.MetaSwapModel.dump`.

Changed
~~~~~~~
Expand Down
20 changes: 18 additions & 2 deletions imod/msw/copy_files.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pathlib import Path
from shutil import copy2
from typing import cast
from typing import Sequence, cast

import numpy as np
import xarray as xr
Expand All @@ -16,8 +16,24 @@


class FileCopier(MetaSwapPackage):
def __init__(self, paths: list[str]):
"""
Class to handle copying of files for the MetaSWAP package.

This class filters out certain files that will be generated by iMOD Python
and copies the remaining files to a specified directory.

Parameters
----------
paths: Sequence[str | Path]
List of file paths to be copied. These will be enforced to string upon
initialization.
"""

def __init__(self, paths: Sequence[str | Path]):
super().__init__()
# Enforce string so that it can be easily serialized and retrieved from
# the dataset.
paths = [str(p) for p in paths]
paths_da = xr.DataArray(
paths, coords={"file_nr": np.arange(len(paths))}, dims=("file_nr",)
)
Expand Down
10 changes: 7 additions & 3 deletions imod/msw/meteo_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ class MeteoGridCopy(MetaSwapPackage, IRegridPackage):

Parameters
----------
path: Path to mete_grid.inp file
path: str | Path
Path to mete_grid.inp file. This will be enforced to string upon storing
in the dataset.
"""

_file_name = "mete_grid.inp"
Expand All @@ -220,11 +222,13 @@ class MeteoGridCopy(MetaSwapPackage, IRegridPackage):

def __init__(self, path: Path | str):
super().__init__()
self.dataset["path"] = path
# Store as string so that it can be easily serialized and retrieved from
# the dataset.
self.dataset["path"] = str(path)

def write(self, directory: Path | str, *args):
directory = Path(directory)
path_metegrid = Path(str(self.dataset["path"].values[()]))
path_metegrid = Path(self.dataset["path"].item())
new_path = directory / self._file_name
copyfile(path_metegrid, new_path)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ def create_exchange_package() -> list[GWFGWF]:
]


ALL_PACKAGE_INSTANCES = (
ALL_MF6_PACKAGE_INSTANCES = (
GRIDLESS_PACKAGES
+ STRUCTURED_GRID_PACKAGES
+ UNSTRUCTURED_GRID_PACKAGES
Expand Down
196 changes: 196 additions & 0 deletions imod/tests/fixtures/msw_package_instance_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
from pathlib import Path

import numpy as np
import pandas as pd
import xarray as xr

from imod.msw import (
AnnualCropFactors,
CouplerMapping,
EvapotranspirationMapping,
FileCopier,
GridData,
IdfMapping,
Infiltration,
InitialConditionsEquilibrium,
InitialConditionsPercolation,
InitialConditionsRootzonePressureHead,
InitialConditionsSavedState,
LanduseOptions,
MeteoGrid,
MeteoGridCopy,
Ponding,
PrecipitationMapping,
ScalingFactors,
SprinklingGrid,
SprinklingPoints,
TimeOutputControl,
VariableOutputControl,
)


def get_grid_da(dtype, value=1, subunit=True):
"""
This function creates a dataarray with scalar values for a grid of 2 subunits and 9 rows and columns.
"""
shape = nsub, nrow, ncol = 2, 9, 9
dims = ("subunit", "y", "x")

dx = 10.0
dy = -10.0
xmin = 0.0
xmax = dx * ncol
ymin = 0.0
ymax = abs(dy) * nrow

subunits = np.arange(0, nsub)
y = np.arange(ymax, ymin, dy) + 0.5 * dy
x = np.arange(xmin, xmax, dx) + 0.5 * dx
coords = {"subunit": subunits, "y": y, "x": x}

values = np.full(shape, fill_value=value, dtype=dtype)

da = xr.DataArray(values, coords=coords, dims=dims)
if subunit is False:
da = da.sel(subunit=0, drop=True)
return da


def get_time_grid(dtype, value=1):
"""
This function creates a dataarray with scalar values for a time grid of ntimes steps.
"""
ntimes = 10
shape = (ntimes,)
dims = ("time",)
coords = {"time": pd.date_range("2000-01-01", periods=ntimes)}

da_time = xr.DataArray(
np.ones(shape, dtype=dtype) * value, coords=coords, dims=dims
)

da_grid = get_grid_da(dtype, value, subunit=False)

return da_time * da_grid


def get_landuse_da(dtype, value=1):
landuse_index = np.arange(1, 4)
coords = {"landuse_index": landuse_index}

values = np.full((3,), fill_value=value, dtype=dtype)

lu_da = xr.DataArray(data=values, coords=coords, dims=("landuse_index",))
return lu_da


def get_vegetation_da(dtype, value=1):
vegetation_index = np.arange(1, 4)
day_of_year = np.arange(1, 367)
coords = {"vegetation_index": vegetation_index, "day_of_year": day_of_year}

values = np.full((3, 366), fill_value=value, dtype=dtype)

veg_da = xr.DataArray(
data=values, coords=coords, dims=("vegetation_index", "day_of_year")
)
return veg_da


def _paths():
return [Path("path"), "path"]


def get_package_instances():
return [
FileCopier(_paths()),
CouplerMapping(),
GridData(
area=get_grid_da(float),
landuse=get_grid_da(int),
rootzone_depth=get_grid_da(float),
surface_elevation=get_grid_da(float, subunit=False),
soil_physical_unit=get_grid_da(int, subunit=False),
active=get_grid_da(bool, subunit=False),
),
IdfMapping(
area=get_grid_da(float),
nodata=-9999.0,
),
Infiltration(
infiltration_capacity=get_grid_da(float),
downward_resistance=get_grid_da(float),
upward_resistance=get_grid_da(float),
bottom_resistance=get_grid_da(float, subunit=False),
extra_storage_coefficient=get_grid_da(float, subunit=False),
),
InitialConditionsEquilibrium(),
InitialConditionsPercolation(),
InitialConditionsRootzonePressureHead(),
InitialConditionsSavedState(Path("path")),
LanduseOptions(
landuse_name=get_landuse_da(str),
vegetation_index=get_landuse_da(int),
jarvis_o2_stress=get_landuse_da(float),
jarvis_drought_stress=get_landuse_da(float),
feddes_p1=get_landuse_da(float),
feddes_p2=get_landuse_da(float),
feddes_p3h=get_landuse_da(float),
feddes_p3l=get_landuse_da(float),
feddes_p4=get_landuse_da(float),
feddes_t3h=get_landuse_da(float),
feddes_t3l=get_landuse_da(float),
threshold_sprinkling=get_landuse_da(float),
fraction_evaporated_sprinkling=get_landuse_da(float),
gift=get_landuse_da(float),
gift_duration=get_landuse_da(float),
rotational_period=get_landuse_da(float),
start_sprinkling_season=get_landuse_da(float),
end_sprinkling_season=get_landuse_da(float),
interception_option=get_landuse_da(int),
),
MeteoGrid(
get_time_grid(float),
get_time_grid(float),
),
MeteoGridCopy(Path("path")),
EvapotranspirationMapping(get_time_grid(float)),
PrecipitationMapping(get_time_grid(float)),
TimeOutputControl(get_time_grid(float)),
VariableOutputControl(),
Ponding(
get_grid_da(float),
get_grid_da(float),
get_grid_da(float),
),
ScalingFactors(
get_grid_da(float),
get_grid_da(float),
get_grid_da(float),
get_grid_da(float, subunit=False),
),
SprinklingGrid(
get_grid_da(float),
get_grid_da(float),
),
SprinklingPoints(
get_grid_da(int),
[1.0, 2.0, 3.0],
[1.0, 2.0, 3.0],
[1, 2, 3],
[1, 1, 1],
[1.0, 2.0, 3.0],
),
AnnualCropFactors(
get_vegetation_da(float),
get_vegetation_da(float),
get_vegetation_da(float),
get_vegetation_da(float),
get_vegetation_da(float),
get_vegetation_da(float),
get_vegetation_da(float),
),
]


MSW_PACKAGE_INSTANCES = get_package_instances()
2 changes: 1 addition & 1 deletion imod/tests/test_common/test_utilities/test_mask_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
broadcast_and_mask_arrays,
mask_arrays,
)
from imod.tests.fixtures.package_instance_creation import get_grid_da
from imod.tests.fixtures.mf6_package_instance_creation import get_grid_da


class DataArrayCases:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import imod
from imod.mf6.boundary_condition import AdvancedBoundaryCondition, BoundaryCondition
from imod.mf6.package import Package
from imod.tests.fixtures.package_instance_creation import ALL_PACKAGE_INSTANCES
from imod.tests.fixtures.mf6_package_instance_creation import ALL_MF6_PACKAGE_INSTANCES

ALL_PACKAGES = [
item
Expand All @@ -45,6 +45,8 @@
imod.mf6.LayeredWell,
]

NAMES = [type(instance).__name__ for instance in ALL_MF6_PACKAGE_INSTANCES]


def check_attributes(pkg_class):
class_attributes = {
Expand Down Expand Up @@ -72,7 +74,7 @@ def test_adv_boundary_class_attributes(pkg_class):
check_attributes(pkg_class)


@pytest.mark.parametrize("instance", ALL_PACKAGE_INSTANCES)
@pytest.mark.parametrize("instance", ALL_MF6_PACKAGE_INSTANCES, ids=NAMES)
def test_render_twice(instance, tmp_path):
globaltimes = [np.datetime64("2000-01-01")]
modeldir = tmp_path / "testdir"
Expand All @@ -96,7 +98,7 @@ def test_render_twice(instance, tmp_path):
assert text1 == text2


@pytest.mark.parametrize("instance", ALL_PACKAGE_INSTANCES)
@pytest.mark.parametrize("instance", ALL_MF6_PACKAGE_INSTANCES, ids=NAMES)
@pytest.mark.parametrize("engine", ["netcdf4", "zarr", "zarr.zip"])
def test_save_and_load(instance, engine, tmp_path):
pkg_class = type(instance)
Expand All @@ -105,13 +107,13 @@ def test_save_and_load(instance, engine, tmp_path):
assert instance.dataset.equals(back.dataset)


@pytest.mark.parametrize("instance", ALL_PACKAGE_INSTANCES)
@pytest.mark.parametrize("instance", ALL_MF6_PACKAGE_INSTANCES, ids=NAMES)
def test_repr(instance):
assert isinstance(instance.__repr__(), str)
assert isinstance(instance._repr_html_(), str)


@pytest.mark.parametrize("instance", ALL_PACKAGE_INSTANCES)
@pytest.mark.parametrize("instance", ALL_MF6_PACKAGE_INSTANCES, ids=NAMES)
def test_from_dataset(instance):
pkg_class = type(instance)
ds = instance.dataset
Expand Down
14 changes: 14 additions & 0 deletions imod/tests/test_msw/test_msw_package_sanity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import pytest

from imod.tests.fixtures.msw_package_instance_creation import MSW_PACKAGE_INSTANCES

NAMES = [type(instance).__name__ for instance in MSW_PACKAGE_INSTANCES]


@pytest.mark.parametrize("instance", MSW_PACKAGE_INSTANCES, ids=NAMES)
@pytest.mark.parametrize("engine", ["netcdf4", "zarr", "zarr.zip"])
def test_msw_save_and_load(instance, engine, tmp_path):
pkg_class = type(instance)
path = instance.to_file(tmp_path, instance._file_name, engine=engine)
back = pkg_class.from_file(path)
assert instance.dataset.equals(back.dataset)
Loading