From 5569915eb5114a073df6c6030ccaefc843f33f1a Mon Sep 17 00:00:00 2001 From: 72004 Date: Sat, 17 Oct 2026 20:37:59 +0500 Subject: [PATCH] Raise a clear error when a model result has no data at the observation match() failed with a bare AssertionError from _get_global_start_end when extraction gave an empty model result (e.g. the observation is outside the domain or the location is dry). Check for this up front, alongside the timezone check, and raise a ValueError that names the model result and the observation. Fixes #506 --- src/modelskill/matching.py | 19 +++++++++++++++++-- tests/test_match.py | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/modelskill/matching.py b/src/modelskill/matching.py index 38e5c913a..3ac14a753 100644 --- a/src/modelskill/matching.py +++ b/src/modelskill/matching.py @@ -382,8 +382,6 @@ def _match_single_obs( def _get_global_start_end(idxs: Iterable[pd.DatetimeIndex]) -> Period: - assert all([len(x) > 0 for x in idxs]), "All datetime indices must be non-empty" - starts = [x[0] for x in idxs] ends = [x[-1] for x in idxs] @@ -408,6 +406,22 @@ def _check_timezone_compatibility( ) +def _check_model_results_not_empty( + observation: Observation, + raw_mod_data: Mapping[ + str, + PointModelResult | TrackModelResult | VerticalModelResult | NodeModelResult, + ], +) -> None: + for name, mr in raw_mod_data.items(): + if len(mr.time) == 0: + raise ValueError( + f"Model result '{name}' has no data at observation '{observation.name}'. " + "Check that the observation is inside the model domain and that the " + "model has valid values there (e.g. the location is not dry)." + ) + + def _at_observation(mr: NodeModelResult, obs: TimeSeries) -> NodeModelResult: """The extracted model result, placed where the observation sits. @@ -431,6 +445,7 @@ def _match_space_time( obs_no_overlap: Literal["ignore", "error", "warn"], ) -> xr.Dataset | None: _check_timezone_compatibility(observation, raw_mod_data) + _check_model_results_not_empty(observation, raw_mod_data) idxs = [m.time for m in raw_mod_data.values()] period = _get_global_start_end(idxs) diff --git a/tests/test_match.py b/tests/test_match.py index 0d0032269..dbf370b40 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -963,6 +963,23 @@ def test_match_raises_clear_error_on_tz_aware_vs_naive_mismatch(): ms.match(obs, mr) +def test_match_raises_clear_error_when_model_result_is_empty(): + """An empty extraction (e.g. point outside domain or dry) must raise a clear error, not a bare assert.""" + obs = ms.PointObservation( + data=pd.Series( + [1.0, 2.0, 3.0], index=pd.date_range("2020-01-01", periods=3, freq="h") + ), + name="Station", + ) + mr = ms.PointModelResult( + data=pd.Series([], dtype=float, index=pd.DatetimeIndex([])), + name="Model", + ) + + with pytest.raises(ValueError, match="'Model' has no data.*'Station'"): + ms.match(obs, mr) + + def test_multiple_models_same_name(tmp_path: Path) -> None: obs = ms.PointObservation( "tests/testdata/SW/HKNA_Hm0.dfs0", item=0, x=4.2420, y=52.6887, name="HKNA"