Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #1316 msw model from imod5 data #1328

Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
ddd87e1
Add parser function for parasim.inp
JoerivanEngelen Dec 2, 2024
8734a5d
Start adding a MetaSwapModel.from_imod5_data method
JoerivanEngelen Dec 2, 2024
2e4d8ba
Merge branch 'issue_#1260_from_imod5_data_metaswap' into issue_#1316_…
JoerivanEngelen Dec 3, 2024
435641c
Enable importing Sprinkling from_imod5_data
JoerivanEngelen Dec 3, 2024
c0ad931
Fix input for IdfMapping
JoerivanEngelen Dec 4, 2024
bf228c2
Merge branch 'issue_#1260_from_imod5_data_metaswap' into issue_#1316_…
JoerivanEngelen Dec 4, 2024
213bfc4
Fix some mypy issues
JoerivanEngelen Dec 4, 2024
82b593f
Add docstring, add timeoutputcontrol (so that startime can be set), a…
JoerivanEngelen Dec 5, 2024
7dfb56c
format
JoerivanEngelen Dec 5, 2024
0a86baa
Merge branch 'issue_#1260_from_imod5_data_metaswap' into issue_#1316_…
JoerivanEngelen Dec 5, 2024
6f15441
Fix keyerror
JoerivanEngelen Dec 5, 2024
19e48f3
Take boundary dataarray, as this is more likely to be the grid.
JoerivanEngelen Dec 5, 2024
78f9c61
Drop layer coord and broadcast scalars to grid
JoerivanEngelen Dec 5, 2024
e5acfb3
Add masking utilities
JoerivanEngelen Dec 5, 2024
a6878f0
Call masking utilities
JoerivanEngelen Dec 5, 2024
486fa84
Use MetaSwapActive
JoerivanEngelen Dec 5, 2024
f98111e
Add validate option for write method and add TimeOutputControl,
JoerivanEngelen Dec 5, 2024
d5db271
Type annotate return type
JoerivanEngelen Dec 5, 2024
299d461
mask package data and force soil physical unit to int
JoerivanEngelen Dec 5, 2024
5a347c6
Deactivate bottom resistance
JoerivanEngelen Dec 5, 2024
11a6707
Mask cap data with new function
JoerivanEngelen Dec 5, 2024
5f6d449
force landuse to int dtype
JoerivanEngelen Dec 5, 2024
e5df224
Rename to mask_package_data and implement simplified mask_and_broadca…
JoerivanEngelen Dec 5, 2024
22b12a0
format
JoerivanEngelen Dec 5, 2024
e792095
Move setup_meteo_grids to separate fixture in module
JoerivanEngelen Dec 6, 2024
bb984b6
Add iMOD5 cap fixture
JoerivanEngelen Dec 6, 2024
422faff
Import added fixtures
JoerivanEngelen Dec 6, 2024
6adc1ea
Use tmp_path
JoerivanEngelen Dec 6, 2024
4ec6781
Format
JoerivanEngelen Dec 6, 2024
5c41cc7
Fixes to cap data fixture
JoerivanEngelen Dec 6, 2024
aa6d67f
Type annotate
JoerivanEngelen Dec 6, 2024
a09adfb
Move meteogrids setup outside tests
JoerivanEngelen Dec 6, 2024
d296a1d
Add tests for MetaSwapModel.from_imod5_data
JoerivanEngelen Dec 6, 2024
35d4e6f
Correctly get all active from MetaSwapActive object
JoerivanEngelen Dec 6, 2024
82a3cd6
Fix type annotation
JoerivanEngelen Dec 6, 2024
2b27939
Fix some mypy issues
JoerivanEngelen Dec 6, 2024
de09d3f
Fix mypy errors
JoerivanEngelen Dec 9, 2024
42bb964
Convert Imod5DataDict to TypedDict and remove casting redundancy
JoerivanEngelen Dec 9, 2024
173e98e
Move drop layer coord logic to separate utility
JoerivanEngelen Dec 9, 2024
11ad02d
Better function names
JoerivanEngelen Dec 9, 2024
50078c0
Add tests for mask
JoerivanEngelen Dec 9, 2024
764c064
Add scaling factor if active.
JoerivanEngelen Dec 10, 2024
c8f9ec7
Add from_imod5_data method to public API
JoerivanEngelen Dec 10, 2024
ef7bbf0
Update API docs
JoerivanEngelen Dec 10, 2024
4ef85f0
Fix failing unittests
JoerivanEngelen Dec 10, 2024
628fab4
Add docstring
JoerivanEngelen Dec 10, 2024
4c673cc
Format
JoerivanEngelen Dec 10, 2024
3ae0c8d
Add MaskValues dataclass to store sentinel values
JoerivanEngelen Dec 12, 2024
ea86e4d
Avoid nested try except
JoerivanEngelen Dec 12, 2024
d85c112
Better varname
JoerivanEngelen Dec 12, 2024
78d1e7a
Add newlines in docstrings for indented lists
JoerivanEngelen Dec 12, 2024
439aba6
Import Simulation option dataclasses for public API
JoerivanEngelen Dec 12, 2024
298ad70
Format
JoerivanEngelen Dec 12, 2024
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
6 changes: 6 additions & 0 deletions docs/api/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ Added
MetaSWAP). Currently only griddata (IDF) is supported.
- :meth:`imod.mf6.Recharge.from_imod5_cap_data` to construct a recharge package
for coupling a MODFLOW6 model to MetaSWAP.
- :meth:`imod.msw.MetaSwapModel.from_imod5_data` to construct a MetaSWAP model
from data in an iMOD5 projectfile.
- :meth:`imod.msw.MetaSwapModel.write` has a ``validate`` argument, which can be
used to turn off validation upon writing, use at your own risk!
- :class:`imod.msw.MetaSwapModel` got ``settings`` argument to set simulation
settings.

Fixed
~~~~~
Expand Down
11 changes: 11 additions & 0 deletions docs/api/msw.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ MetaSWAP
:toctree: generated/msw
JoerivanEngelen marked this conversation as resolved.
Show resolved Hide resolved

GridData
GridData.from_imod5_data
Infiltration
Infiltration.from_imod5_data
Ponding
Ponding.from_imod5_data
ScalingFactors
ScalingFactors.from_imod5_data
Sprinkling
Sprinkling.from_imod5_data

IdfMapping
TimeOutputControl
Expand All @@ -25,9 +30,15 @@ MetaSWAP
AnnualCropFactors

MeteoGrid
MeteoGridCopy
MeteoGridCopy.from_imod5_data
EvapotranspirationMapping
EvapotranspirationMapping.from_imod5_data
PrecipitationMapping
PrecipitationMapping.from_imod5_data

CouplerMapping

MetaSwapModel
MetaSwapModel.write
MetaSwapModel.from_imod5_data
3 changes: 2 additions & 1 deletion imod/mf6/rch.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ def from_imod5_cap_data(
cap_data = cast(GridDataDict, imod5_data["cap"])

msw_area = get_cell_area_from_imod5_data(cap_data)
active, _ = is_msw_active_cell(target_dis, cap_data, msw_area)
msw_active = is_msw_active_cell(target_dis, cap_data, msw_area)
active = msw_active.all

data = {}
layer_da = xr.full_like(
Expand Down
62 changes: 37 additions & 25 deletions imod/msw/grid_data.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
from typing import Optional

import numpy as np
import xarray as xr

from imod.mf6 import StructuredDiscretization
from imod.mf6.interfaces.iregridpackage import IRegridPackage
from imod.mf6.regrid.regrid_schemes import (
RegridMethodType,
)
from imod.mf6.utilities.regrid import RegridderWeightsCache
from imod.msw.fixed_format import VariableMetaData
from imod.msw.pkgbase import MetaSwapPackage
from imod.msw.regrid.regrid_schemes import GridDataRegridMethod
Expand All @@ -18,7 +12,8 @@
get_rootzone_depth_from_imod5_data,
is_msw_active_cell,
)
from imod.typing import GridDataDict
from imod.msw.utilities.mask import MetaSwapActive, mask_and_broadcast_pkg_data
from imod.typing import Imod5DataDict
from imod.util.spatial import get_cell_area, spatial_reference


Expand Down Expand Up @@ -129,30 +124,47 @@ def _pkgcheck(self):
@classmethod
def from_imod5_data(
cls,
imod5_data: dict[str, GridDataDict],
imod5_data: Imod5DataDict,
target_dis: StructuredDiscretization,
regridder_types: Optional[RegridMethodType] = None,
regrid_cache: RegridderWeightsCache = RegridderWeightsCache(),
) -> "GridData":
# Get iMOD5 capillary zone data
) -> tuple["GridData", MetaSwapActive]:
"""
Construct a MetaSWAP GridData package from iMOD5 data in the CAP
package, loaded with the :func:`imod.formats.prj.open_projectfile_data`
function.

Method does the following things:
- Computes rural area from the wetted area and urban area grids.
- Sets a second subunit for urban landuse (== 18).
- Rootzone depth is converted from cm's to m's.
- Determines where MetaSWAP should be active based on area grids,
boundary grid, and MODFLOW6 idomain.

Parameters
----------
imod5_data: Imod5DataDict
iMOD5 data as returned by
:func:`imod.formats.prj.open_projectfile_data`
target_dis: imod.mf6.StructuredDiscretization
MODFLOW6 discretization to couple the MetaSWAP model to.

Returns
-------
imod.msw.GridData
GridData package
MetaSwapActive
Dataclass containing where MetaSwAP is active, per subunit, as well
as aggregated over subunits.
"""
imod5_cap = imod5_data["cap"]

data = {}
data["area"] = get_cell_area_from_imod5_data(imod5_cap)
data["landuse"] = get_landuse_from_imod5_data(imod5_cap)
data["rootzone_depth"] = get_rootzone_depth_from_imod5_data(imod5_cap)
data["surface_elevation"] = imod5_cap["surface_elevation"]
data["soil_physical_unit"] = imod5_cap["soil_physical_unit"]

active, subunit_active = is_msw_active_cell(target_dis, imod5_cap, data["area"])
data["soil_physical_unit"] = imod5_cap["soil_physical_unit"].astype(int)

data_active = {
key: (
griddata.where(subunit_active)
if key in cls._with_subunit
else griddata.where(active)
)
for key, griddata in data.items()
}
data_active["active"] = active
return cls(**data_active)
msw_active = is_msw_active_cell(target_dis, imod5_cap, data["area"])
data_active = mask_and_broadcast_pkg_data(cls, data, msw_active)
data_active["active"] = msw_active.all
return cls(**data_active), msw_active
6 changes: 3 additions & 3 deletions imod/msw/infiltration.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from imod.msw.pkgbase import MetaSwapPackage
from imod.msw.regrid.regrid_schemes import InfiltrationRegridMethod
from imod.msw.utilities.common import concat_imod5
from imod.typing import GridDataDict
from imod.typing import GridDataDict, Imod5DataDict
from imod.typing.grid import ones_like


Expand Down Expand Up @@ -101,7 +101,7 @@ def __init__(
self._pkgcheck()

@classmethod
def from_imod5_data(cls, imod5_data: dict[str, GridDataDict]) -> "Infiltration":
def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "Infiltration":
cap_data = imod5_data["cap"]
data = {}
# Use runon resistance as downward resistance, and runoff for downward
Expand All @@ -120,7 +120,7 @@ def from_imod5_data(cls, imod5_data: dict[str, GridDataDict]) -> "Infiltration":
data = deactivate_small_resistances_in_data(data)

like = data["downward_resistance"].isel(subunit=0, drop=True)
data["bottom_resistance"] = ones_like(like)
data["bottom_resistance"] = ones_like(like) * -9999.0
JoerivanEngelen marked this conversation as resolved.
Show resolved Hide resolved
data["extra_storage_coefficient"] = ones_like(like)

return cls(**data)
4 changes: 2 additions & 2 deletions imod/msw/meteo_grid.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import csv
from pathlib import Path
from shutil import copyfile
from typing import Optional, Union, cast
from typing import Optional, Union

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -223,7 +223,7 @@ def write(self, directory: Path | str, *args):

@classmethod
def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "MeteoGridCopy":
paths = cast(list[str], imod5_data["extra"]["paths"])
paths = imod5_data["extra"]["paths"]
filepath = find_in_file_list(cls._file_name, paths)

return cls(filepath)
4 changes: 2 additions & 2 deletions imod/msw/meteo_mapping.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from copy import deepcopy
from pathlib import Path
from textwrap import dedent
from typing import Any, Optional, TextIO, cast
from typing import Any, Optional, TextIO

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -58,7 +58,7 @@ def open_first_meteo_grid(mete_grid_path: str | Path, column_nr: int) -> xr.Data


def open_first_meteo_grid_from_imod5_data(imod5_data: Imod5DataDict, column_nr: int):
paths = cast(list[str], imod5_data["extra"]["paths"])
paths = imod5_data["extra"]["paths"]
metegrid_path = find_in_file_list("mete_grid.inp", paths)
return open_first_meteo_grid(metegrid_path, column_nr=column_nr)

Expand Down
102 changes: 94 additions & 8 deletions imod/msw/model.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import collections
from copy import copy
from datetime import datetime
from pathlib import Path
from typing import Optional, Tuple, Union
from typing import Any, Optional, Tuple, Union, cast

import jinja2
import numpy as np
import xarray as xr

from imod.mf6.dis import StructuredDiscretization
from imod.mf6.mf6_wel_adapter import Mf6Wel
from imod.mf6.utilities.regrid import RegridderWeightsCache
from imod.msw.copy_files import FileCopier
from imod.msw.coupler_mapping import CouplerMapping
from imod.msw.grid_data import GridData
from imod.msw.idf_mapping import IdfMapping
Expand All @@ -20,11 +23,20 @@
InitialConditionsSavedState,
)
from imod.msw.landuse import LanduseOptions
from imod.msw.meteo_grid import MeteoGrid
from imod.msw.meteo_grid import MeteoGrid, MeteoGridCopy
from imod.msw.meteo_mapping import EvapotranspirationMapping, PrecipitationMapping
from imod.msw.output_control import TimeOutputControl
from imod.msw.ponding import Ponding
from imod.msw.scaling_factors import ScalingFactors
from imod.msw.sprinkling import Sprinkling
from imod.msw.timeutil import to_metaswap_timeformat
from imod.msw.utilities.common import find_in_file_list
from imod.msw.utilities.imod5_converter import has_active_scaling_factor
from imod.msw.utilities.mask import mask_and_broadcast_cap_data
from imod.msw.utilities.parse import read_para_sim
from imod.msw.utilities.select import drop_layer_dim_cap_data
from imod.msw.vegetation import AnnualCropFactors
from imod.typing import Imod5DataDict
from imod.util.regrid_method_type import RegridderType

REQUIRED_PACKAGES = (
Expand Down Expand Up @@ -88,6 +100,7 @@ class MetaSwapModel(Model):
----------
unsaturated_database: Path-like or str
Path to the MetaSWAP soil physical database folder.
settings: dict
"""

_pkg_id = "model"
Expand All @@ -99,10 +112,18 @@ class MetaSwapModel(Model):
"{%endfor%}"
)

def __init__(self, unsaturated_database):
def __init__(
self,
unsaturated_database: Path | str,
settings: Optional[dict[str, Any]] = None,
):
super().__init__()

self.simulation_settings = copy(DEFAULT_SETTINGS)
if settings is None:
self.simulation_settings = copy(DEFAULT_SETTINGS)
else:
self.simulation_settings = settings

self.simulation_settings["unsa_svat_path"] = (
self._render_unsaturated_database_path(unsaturated_database)
)
Expand Down Expand Up @@ -210,6 +231,7 @@ def write(
directory: Union[str, Path],
mf6_dis: StructuredDiscretization,
mf6_wel: Mf6Wel,
validate: bool = True,
):
"""
Write packages and simulation settings (para_sim.inp).
Expand All @@ -221,9 +243,10 @@ def write(
"""

# Model checks
self._check_required_packages()
self._check_vegetation_indices_in_annual_crop_factors()
self._check_landuse_indices_in_lookup_options()
if validate:
self._check_required_packages()
self._check_vegetation_indices_in_annual_crop_factors()
self._check_landuse_indices_in_lookup_options()

# Force to Path
directory = Path(directory)
Expand Down Expand Up @@ -258,7 +281,7 @@ def regrid_like(
regrid_context: Optional[RegridderWeightsCache] = None,
regridder_types: Optional[dict[str, Tuple[RegridderType, str]]] = None,
) -> "MetaSwapModel":
unsat_database = self.simulation_settings["unsa_svat_path"]
unsat_database = cast(str, self.simulation_settings["unsa_svat_path"])
regridded_model = MetaSwapModel(unsat_database)

target_grid = mf6_regridded_dis["idomain"]
Expand All @@ -282,3 +305,66 @@ def regrid_like(
regridded_model[mod2svat_name] = CouplerMapping()

return regridded_model

@classmethod
def from_imod5_data(
cls,
imod5_data: Imod5DataDict,
target_dis: StructuredDiscretization,
times: list[datetime],
):
"""
Construct a MetaSWAP model from iMOD5 data in the CAP package, loaded
with the :func:`imod.formats.prj.open_projectfile_data` function.

Parameters
----------
imod5_data: dict
Dictionary with iMOD5 data. This can be constructed from the
:func:`imod.formats.prj.open_projectfile_data` method.
target_dis: imod.mf6.StructuredDiscretization
Target discretization, cells where MODLOW6 is inactive will be
inactive in MetaSWAP as well.
times: list[datetime]
List of datetimes, will be used to set the output control times.
Is also used to infer the starttime of the simulation.

Returns
-------
MetaSwapModel
MetaSWAP model imported from imod5 data.
"""
extra_paths = imod5_data["extra"]["paths"]
path_to_parasim = find_in_file_list("para_sim.inp", extra_paths)
parasim_settings = read_para_sim(path_to_parasim)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What if path_to_parasim is empty? Should this be put in an if statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All iMOD5 usecases contain this file in the "EXTRA" section in the projectfile: Without it they couldn't run their MetaSWAP model with iMOD5. If the para_sim.inp is missing an error will be thrown: "could not find para_sim.inp in list of paths: ..."

unsa_svat_path = cast(str, parasim_settings["unsa_svat_path"])
# Drop layer coord
imod5_cap_no_layer = drop_layer_dim_cap_data(imod5_data)
model = cls(unsa_svat_path, parasim_settings)
model["grid"], msw_active = GridData.from_imod5_data(
imod5_cap_no_layer, target_dis
)
cap_data_masked = mask_and_broadcast_cap_data(
imod5_cap_no_layer["cap"], msw_active
)
imod5_masked: Imod5DataDict = {
"cap": cap_data_masked,
"extra": {"paths": extra_paths},
}
model["infiltration"] = Infiltration.from_imod5_data(imod5_masked)
model["ponding"] = Ponding.from_imod5_data(imod5_masked)
model["sprinkling"] = Sprinkling.from_imod5_data(imod5_masked)
model["meteo_grid"] = MeteoGridCopy.from_imod5_data(imod5_masked)
model["prec_mapping"] = PrecipitationMapping.from_imod5_data(imod5_masked)
model["evt_mapping"] = EvapotranspirationMapping.from_imod5_data(imod5_masked)
if has_active_scaling_factor(imod5_cap_no_layer["cap"]):
model["scaling_factor"] = ScalingFactors.from_imod5_data(imod5_masked)
area = model["grid"]["area"].isel(subunit=0, drop=True)
model["idf_mapping"] = IdfMapping(area, -9999.0)
model["coupling"] = CouplerMapping()
model["extra_files"] = FileCopier.from_imod5_data(imod5_masked)

times_da = xr.DataArray(times, coords={"time": times}, dims=("time",))
model["time_oc"] = TimeOutputControl(times_da)

return model
4 changes: 2 additions & 2 deletions imod/msw/ponding.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from imod.msw.pkgbase import DataDictType, MetaSwapPackage
from imod.msw.regrid.regrid_schemes import PondingRegridMethod
from imod.msw.utilities.common import concat_imod5
from imod.typing import GridDataDict, IntArray
from imod.typing import Imod5DataDict, IntArray


class Ponding(MetaSwapPackage, IRegridPackage):
Expand Down Expand Up @@ -73,7 +73,7 @@ def _render(self, file: TextIO, index: IntArray, svat: xr.DataArray, *args: Any)
return self.write_dataframe_fixed_width(file, dataframe)

@classmethod
def from_imod5_data(cls, imod5_data: dict[str, GridDataDict]) -> "Ponding":
def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "Ponding":
"""
Concatenate ponding depths along subunits
"""
Expand Down
Loading