Skip to content
Open
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
23 changes: 21 additions & 2 deletions src/modelskill/matching.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def match(
gtype: Optional[GeometryTypes] = None,
max_model_gap: Optional[float] = None,
spatial_method: Optional[str] = None,
temporal_method: str = "linear",
obs_no_overlap: Literal["ignore", "error", "warn"] = "error",
) -> Comparer: ...

Expand All @@ -186,6 +187,7 @@ def match(
gtype: Optional[GeometryTypes] = None,
max_model_gap: Optional[float] = None,
spatial_method: Optional[str] = None,
temporal_method: str = "linear",
obs_no_overlap: Literal["ignore", "error", "warn"] = "error",
) -> ComparerCollection: ...

Expand All @@ -199,6 +201,7 @@ def match(
gtype=None,
max_model_gap=None,
spatial_method: Optional[str] = None,
temporal_method: str = "linear",
obs_no_overlap: Literal["ignore", "error", "warn"] = "error",
):
"""Match observation and model result data in space and time
Expand Down Expand Up @@ -233,6 +236,11 @@ def match(
'inverse_distance' (with 5 nearest points), by default "inverse_distance".
- For GridModelResult, passed to xarray.interp() as method argument,
by default 'linear'.
temporal_method : str, optional
Temporal interpolation method passed to xarray.interp(), by default 'linear'
Valid options are: "akima", "barycentric", "cubic", "krogh", "linear",
"makima", "nearest", "pchip", "polynomial", "quadratic",
"quintic", "slinear", "spline", "zero".
obs_no_overlap: str, optional
How to handle observations with no overlap with model results. One of: 'ignore', 'error', 'warn', by default 'error'.

Expand All @@ -256,6 +264,7 @@ def match(
gtype=gtype,
max_model_gap=max_model_gap,
spatial_method=spatial_method,
temporal_method=temporal_method,
obs_no_overlap=obs_no_overlap,
)

Expand Down Expand Up @@ -308,6 +317,7 @@ def _match_single_obs(
gtype: Optional[GeometryTypes] = None,
max_model_gap: Optional[float] = None,
spatial_method: Optional[str] = None,
temporal_method: str = "linear",
obs_no_overlap: Literal["ignore", "error", "warn"] = "error",
) -> Optional[Comparer]:
observation = _parse_single_obs(obs, obs_item, gtype=gtype)
Expand All @@ -324,7 +334,10 @@ def _match_single_obs(

raw_mod_data = {
m.name: (
m.extract(observation, spatial_method=spatial_method)
m.extract(
observation,
spatial_method=spatial_method,
)
if isinstance(m, (DfsuModelResult, GridModelResult, DummyModelResult))
else m
)
Expand All @@ -336,6 +349,7 @@ def _match_single_obs(
raw_mod_data=raw_mod_data,
max_model_gap=max_model_gap,
obs_no_overlap=obs_no_overlap,
temporal_method=temporal_method,
)
if matched_data is None:
return None
Expand All @@ -361,6 +375,7 @@ def match_space_time(
raw_mod_data: Mapping[str, Alignable],
max_model_gap: float | None = None,
obs_no_overlap: Literal["ignore", "error", "warn"] = "error",
temporal_method: str = "linear",
) -> Optional[xr.Dataset]:
"""Match observation with one or more model results in time domain.

Expand All @@ -380,6 +395,10 @@ def match_space_time(
max_model_gap : Optional[TimeDeltaTypes], optional
In case of non-equidistant model results (e.g. event data),
max_model_gap can be given e.g. as seconds, by default None
obs_no_overlap : Literal['ignore', 'error', 'warn'], optional
How to handle observations with no overlap with model results. One of: 'ignore', 'error', 'warn', by default 'error'.
temporal_method : str, optional
Temporal interpolation method passed to xarray.interp(), by default 'linear'

Returns
-------
Expand All @@ -399,7 +418,7 @@ def match_space_time(

for mr in raw_mod_data.values():
# TODO is `align` the correct name for this operation?
aligned = mr.align(observation, max_gap=max_model_gap)
aligned = mr.align(observation, max_gap=max_model_gap, method=temporal_method)

if overlapping := set(aligned.filter_by_attrs(kind="aux").data_vars) & set(
observation.data.filter_by_attrs(kind="aux").data_vars
Expand Down
6 changes: 5 additions & 1 deletion src/modelskill/model/point.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,16 @@ def align(
observation: Observation,
*,
max_gap: float | None = None,
method: str = "linear",
**kwargs: Any,
) -> xr.Dataset:
new_time = observation.time

dati = self.data.dropna("time").interp(
time=new_time, assume_sorted=True, **kwargs
time=new_time,
assume_sorted=True,
method=method, # type: ignore
**kwargs,
)

pmr = PointModelResult(dati)
Expand Down
23 changes: 23 additions & 0 deletions tests/test_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,3 +616,26 @@ def test_multiple_models_same_name(tmp_path: Path) -> None:

with pytest.raises(ValueError, match="HKZN_local_2017_DutchCoast"):
ms.match(obs, [mr1, mr2])


def test_directional_data_use_nearest_temporal_interpolation():
mod = ms.PointModelResult(
name="mod",
data=pd.Series(
[359, 5], index=pd.date_range("2023-01-01", periods=2, freq="3H")
),
)

obs = ms.PointObservation(
name="obs",
data=pd.Series(
np.zeros(5), index=pd.date_range("2023-01-01", periods=5, freq="1H")
),
)

cmp = ms.match(
obs=obs,
mod=mod,
temporal_method="nearest",
)
assert cmp.data["mod"].values[1] == pytest.approx(359.0)