Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 51aaf58

Browse files
Issue #443 clipping msw model (#1478)
Fixes #443 and #364 # Description * Adds clipping method to ``imod.msw.MetaSwapModel``, this includes a special case for ``clip_box`` method of PrecipitationMapping, and EvapotranspirationMapping: These should not be clipped if a ``MeteoGridCopy`` instance is part of the model. Otherwise nonsensical mappings are generated. * Add ``imod.msw.MetaSwapModel.clip_box`` method to public API * Override ``clip_box`` method in MeteoMapping, should clip meteo attribute instead of dataset attribute. * Refactor: Move time slice clipping logic to ``imod.common.utilities.clip``. * Refactor: Make ``meteo`` class variable an attribute. This makes more sense. I think I made it a class variable before to please mypy. * Boyscouting: Add some extra docstring information on usecase to ``imod.msw.MeteoGridCopy`` # 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
1 parent afd2e34 commit 51aaf58

File tree

13 files changed

+335
-127
lines changed

13 files changed

+335
-127
lines changed

docs/api/changelog.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ All notable changes to this project will be documented in this file.
66
The format is based on `Keep a Changelog`_, and this project adheres to
77
`Semantic Versioning`_.
88

9+
[Unreleased]
10+
------------
11+
12+
Added
13+
~~~~~
14+
15+
- :meth:`imod.msw.MetaSwapModel.clip_box` to clip MetaSWAP models.
16+
17+
918
[1.0.0rc2] - 2025-03-05
1019
-----------------------
1120

docs/api/msw.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ MetaSWAP
4343
MetaSwapModel.write
4444
MetaSwapModel.from_imod5_data
4545
MetaSwapModel.regrid_like
46+
MetaSwapModel.clip_box

docs/faq/imod5_backwards_compatibility.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ An overview of the support for iMOD5's MetaSWAP features:
162162
iMOD5 pkg, MetaSWAP file, functionality,iMOD Python function/method
163163
*Model*,``para_sim.inp``,From grids (IDF),:meth:`imod.msw.MetaSwapModel.from_imod5_data`
164164
*Model*,,Regrid,:meth:`imod.msw.MetaSwapModel.regrid_like`
165-
*Model*,,Clip,
165+
*Model*,,Clip,:meth:`imod.msw.MetaSwapModel.clip_box`
166166
*Model*,``mod2svat.inp``,Coupling,":meth:`imod.msw.MetaSwapModel.from_imod5_data`, :class:`imod.msw.CouplerMapping`"
167167
*Model*,``idf_svat.ipn``,IDF output,":meth:`imod.msw.MetaSwapModel.from_imod5_data`, :class:`imod.msw.IdfMapping`"
168168
CAP,``area_svat.inp``,Grid Data,:meth:`imod.msw.GridData.from_imod5_data`

imod/common/utilities/clip.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
clipped_zbound_linestrings_to_vertical_polygons,
1919
vertical_polygons_to_zbound_linestrings,
2020
)
21+
from imod.common.utilities.value_filters import is_valid
2122
from imod.typing import GeoDataFrameType, GridDataArray, GridDataset
2223
from imod.typing.grid import bounding_polygon, is_spatial_grid
2324
from imod.util.imports import MissingOptionalModule
25+
from imod.util.time import to_datetime_internal
2426

2527
if TYPE_CHECKING:
2628
import geopandas as gpd
@@ -267,3 +269,108 @@ def clip_layer_slice(
267269
else:
268270
selection = selection.sel(layer=layer_slice)
269271
return selection
272+
273+
274+
def _to_datetime(
275+
time: Optional[cftime.datetime | np.datetime64 | str], use_cftime: bool
276+
):
277+
"""
278+
Helper function that converts to datetime, except when None.
279+
"""
280+
if time is None:
281+
return time
282+
else:
283+
return to_datetime_internal(time, use_cftime)
284+
285+
286+
def clip_repeat_stress(
287+
repeat_stress: xr.DataArray,
288+
time: np.ndarray,
289+
time_start: Optional[cftime.datetime | np.datetime64 | str] = None,
290+
time_end: Optional[cftime.datetime | np.datetime64 | str] = None,
291+
):
292+
"""
293+
Selection may remove the original data which are repeated.
294+
These should be re-inserted at the first occuring "key".
295+
Next, remove these keys as they've been "promoted" to regular
296+
timestamps with data.
297+
298+
Say repeat_stress is:
299+
keys : values
300+
2001-01-01 : 2000-01-01
301+
2002-01-01 : 2000-01-01
302+
2003-01-01 : 2000-01-01
303+
304+
And time_start = 2001-01-01, time_end = None
305+
306+
This function returns:
307+
keys : values
308+
2002-01-01 : 2001-01-01
309+
2003-01-01 : 2001-01-01
310+
"""
311+
# First, "pop" and filter.
312+
keys, values = repeat_stress.values.T
313+
within_time_slice = (keys >= time_start) & (keys <= time_end)
314+
clipped_keys = keys[within_time_slice]
315+
clipped_values = values[within_time_slice]
316+
# Now account for "value" entries that have been clipped off, these should
317+
# be updated in the end to ``insert_keys``.
318+
insert_values, index = np.unique(clipped_values, return_index=True)
319+
insert_keys = clipped_keys[index]
320+
# Setup indexer
321+
indexer = xr.DataArray(
322+
data=np.arange(time.size),
323+
coords={"time": time},
324+
dims=("time",),
325+
).sel(time=insert_values)
326+
indexer["time"] = insert_keys
327+
328+
# Update the key-value pairs. Discard keys that have been "promoted" to
329+
# values.
330+
not_promoted = np.isin(clipped_keys, insert_keys, assume_unique=True, invert=True)
331+
not_promoted_keys = clipped_keys[not_promoted]
332+
not_promoted_values = clipped_values[not_promoted]
333+
# Promote the values to their new source.
334+
to_promote = np.searchsorted(insert_values, not_promoted_values)
335+
promoted_values = insert_keys[to_promote]
336+
repeat_stress = xr.DataArray(
337+
data=np.column_stack((not_promoted_keys, promoted_values)),
338+
dims=("repeat", "repeat_items"),
339+
)
340+
return indexer, repeat_stress
341+
342+
343+
def clip_time_slice(
344+
dataset: GridDataset,
345+
time_min: Optional[cftime.datetime | np.datetime64 | str] = None,
346+
time_max: Optional[cftime.datetime | np.datetime64 | str] = None,
347+
):
348+
"""Clip time slice from dataset, account for repeat stress if present."""
349+
selection = dataset
350+
if "time" in selection.coords:
351+
time = selection["time"].values
352+
use_cftime = isinstance(time[0], cftime.datetime)
353+
time_start = _to_datetime(time_min, use_cftime)
354+
time_end = _to_datetime(time_max, use_cftime)
355+
356+
indexer = clip_time_indexer(
357+
time=time,
358+
time_start=time_start,
359+
time_end=time_end,
360+
)
361+
362+
if "repeat_stress" in selection.data_vars and is_valid(
363+
selection["repeat_stress"].values[()]
364+
):
365+
repeat_indexer, repeat_stress = clip_repeat_stress(
366+
repeat_stress=selection["repeat_stress"],
367+
time=time,
368+
time_start=time_start,
369+
time_end=time_end,
370+
)
371+
selection = selection.drop_vars("repeat_stress")
372+
selection["repeat_stress"] = repeat_stress
373+
indexer = repeat_indexer.combine_first(indexer).astype(int)
374+
375+
selection = selection.drop_vars("time").isel(time=indexer)
376+
return selection

imod/mf6/package.py

Lines changed: 2 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@
1313
import xarray as xr
1414
import xugrid as xu
1515

16-
import imod
1716
from imod.common.interfaces.ipackage import IPackage
1817
from imod.common.utilities.clip import (
1918
clip_layer_slice,
2019
clip_spatial_box,
21-
clip_time_indexer,
20+
clip_time_slice,
2221
)
2322
from imod.common.utilities.mask import mask_package
2423
from imod.common.utilities.regrid import (
@@ -376,58 +375,6 @@ def copy(self) -> Any:
376375
# All state should be contained in the dataset.
377376
return type(self)(**self.dataset.copy().to_dict())
378377

379-
@staticmethod
380-
def _clip_repeat_stress(
381-
repeat_stress: xr.DataArray,
382-
time,
383-
time_start,
384-
time_end,
385-
):
386-
"""
387-
Selection may remove the original data which are repeated.
388-
These should be re-inserted at the first occuring "key".
389-
Next, remove these keys as they've been "promoted" to regular
390-
timestamps with data.
391-
"""
392-
# First, "pop" and filter.
393-
keys, values = repeat_stress.values.T
394-
keep = (keys >= time_start) & (keys <= time_end)
395-
new_keys = keys[keep]
396-
new_values = values[keep]
397-
# Now detect which "value" entries have gone missing
398-
insert_values, index = np.unique(new_values, return_index=True)
399-
insert_keys = new_keys[index]
400-
# Setup indexer
401-
indexer = xr.DataArray(
402-
data=np.arange(time.size),
403-
coords={"time": time},
404-
dims=("time",),
405-
).sel(time=insert_values)
406-
indexer["time"] = insert_keys
407-
408-
# Update the key-value pairs. Discard keys that have been "promoted".
409-
keep = np.isin(new_keys, insert_keys, assume_unique=True, invert=True)
410-
new_keys = new_keys[keep]
411-
new_values = new_values[keep]
412-
# Set the values to their new source.
413-
new_values = insert_keys[np.searchsorted(insert_values, new_values)]
414-
repeat_stress = xr.DataArray(
415-
data=np.column_stack((new_keys, new_values)),
416-
dims=("repeat", "repeat_items"),
417-
)
418-
return indexer, repeat_stress
419-
420-
def __to_datetime(
421-
self, time: Optional[cftime.datetime | np.datetime64 | str], use_cftime: bool
422-
):
423-
"""
424-
Helper function that converts to datetime, except when None.
425-
"""
426-
if time is None:
427-
return time
428-
else:
429-
return imod.util.time.to_datetime_internal(time, use_cftime)
430-
431378
def clip_box(
432379
self,
433380
time_min: Optional[cftime.datetime | np.datetime64 | str] = None,
@@ -476,37 +423,10 @@ def clip_box(
476423
raise ValueError("this package does not support clipping.")
477424

478425
selection = self.dataset
479-
if "time" in selection:
480-
time = selection["time"].values
481-
use_cftime = isinstance(time[0], cftime.datetime)
482-
time_start = self.__to_datetime(time_min, use_cftime)
483-
time_end = self.__to_datetime(time_max, use_cftime)
484-
485-
indexer = clip_time_indexer(
486-
time=time,
487-
time_start=time_start,
488-
time_end=time_end,
489-
)
490-
491-
if "repeat_stress" in selection.data_vars and self._valid(
492-
selection["repeat_stress"].values[()]
493-
):
494-
repeat_indexer, repeat_stress = self._clip_repeat_stress(
495-
repeat_stress=selection["repeat_stress"],
496-
time=time,
497-
time_start=time_start,
498-
time_end=time_end,
499-
)
500-
selection = selection.drop_vars("repeat_stress")
501-
selection["repeat_stress"] = repeat_stress
502-
indexer = repeat_indexer.combine_first(indexer).astype(int)
503-
504-
selection = selection.drop_vars("time").isel(time=indexer)
505-
426+
selection = clip_time_slice(selection, time_min=time_min, time_max=time_max)
506427
selection = clip_layer_slice(
507428
selection, layer_min=layer_min, layer_max=layer_max
508429
)
509-
510430
selection = clip_spatial_box(
511431
selection,
512432
x_min=x_min,

imod/msw/meteo_grid.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ class MeteoGridCopy(MetaSwapPackage, IRegridPackage):
204204
PrecipitationMapping and EvapotranspirationMapping are required as well to
205205
specify meteorological information to MetaSWAP.
206206
207+
This is useful for large meteorological datasets, for which a
208+
``mete_grid.inp`` already has been generated, which is common in existing
209+
iMOD5 model databases.
210+
207211
Parameters
208212
----------
209213
path: Path to mete_grid.inp file

imod/msw/meteo_mapping.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from textwrap import dedent
44
from typing import Any, Optional, TextIO
55

6+
import cftime
67
import numpy as np
78
import pandas as pd
89
import xarray as xr
910

1011
import imod
12+
from imod.common.utilities.clip import clip_spatial_box, clip_time_slice
1113
from imod.common.utilities.regrid_method_type import RegridMethodType
1214
from imod.msw.fixed_format import VariableMetaData
1315
from imod.msw.pkgbase import MetaSwapPackage
@@ -71,10 +73,9 @@ class MeteoMapping(MetaSwapPackage):
7173
new packages.
7274
"""
7375

74-
meteo: GridDataArray
75-
76-
def __init__(self):
76+
def __init__(self, meteo_grid: GridDataArray):
7777
super().__init__()
78+
self.meteo = meteo_grid
7879

7980
def _render(
8081
self,
@@ -143,6 +144,29 @@ def regrid_like(
143144
):
144145
return deepcopy(self)
145146

147+
def clip_box(
148+
self,
149+
time_min: Optional[cftime.datetime | np.datetime64 | str] = None,
150+
time_max: Optional[cftime.datetime | np.datetime64 | str] = None,
151+
x_min: Optional[float] = None,
152+
x_max: Optional[float] = None,
153+
y_min: Optional[float] = None,
154+
y_max: Optional[float] = None,
155+
):
156+
"""Clip meteo grid to a box defined by time and space."""
157+
selection = self.meteo.to_dataset(name="meteo") # Force to dataset
158+
selection = clip_time_slice(selection, time_min=time_min, time_max=time_max)
159+
selection = clip_spatial_box(
160+
selection,
161+
x_min=x_min,
162+
x_max=x_max,
163+
y_min=y_min,
164+
y_max=y_max,
165+
)
166+
167+
cls = type(self)
168+
return cls(selection["meteo"])
169+
146170

147171
class PrecipitationMapping(MeteoMapping):
148172
"""
@@ -172,8 +196,7 @@ def __init__(
172196
self,
173197
precipitation: xr.DataArray,
174198
):
175-
super().__init__()
176-
self.meteo = precipitation
199+
super().__init__(precipitation)
177200

178201
@classmethod
179202
def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "PrecipitationMapping":
@@ -230,8 +253,7 @@ def __init__(
230253
self,
231254
evapotranspiration: xr.DataArray,
232255
):
233-
super().__init__()
234-
self.meteo = evapotranspiration
256+
super().__init__(evapotranspiration)
235257

236258
@classmethod
237259
def from_imod5_data(cls, imod5_data: Imod5DataDict) -> "EvapotranspirationMapping":

0 commit comments

Comments
 (0)