From 73ae20ed6502498ea8eb67675f7337e5d8004f52 Mon Sep 17 00:00:00 2001 From: Eliane Kobler Date: Tue, 9 Dec 2025 14:43:27 +0100 Subject: [PATCH 1/8] add idx boolean selection for member and leadtime --- climada/util/forecast.py | 10 ++++++++ climada/util/test/test_forecast.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/climada/util/forecast.py b/climada/util/forecast.py index eb7cc7fc14..602b9c5437 100644 --- a/climada/util/forecast.py +++ b/climada/util/forecast.py @@ -56,3 +56,13 @@ def __init__( ) self.member = np.asarray(member) if member is not None else np.array([]) super().__init__(**kwargs) + + def idx_member(self, member: np.ndarray) -> np.ndarray: + """Return boolean array where self.member == member using numpy.isin()""" + + return np.isin(self.member, member) + + def idx_lead_time(self, lead_time: np.ndarray) -> np.ndarray: + """Return boolean array where self.lead_time == lead_time using numpy.isin()""" + + return np.isin(self.lead_time, lead_time) diff --git a/climada/util/test/test_forecast.py b/climada/util/test/test_forecast.py index 54d11e6622..ba00637b1a 100644 --- a/climada/util/test/test_forecast.py +++ b/climada/util/test/test_forecast.py @@ -50,3 +50,40 @@ def test_forecast_init(): forecast = Forecast(lead_time=lead_times_seconds, member=[1, 2, 3]) npt.assert_array_equal(forecast.lead_time, lead_times_seconds, strict=True) assert forecast.lead_time.dtype == np.dtype("timedelta64[ns]") + + +def test_idx_member(): + """Test idx_member method of Forecast class.""" + forecast = Forecast(member=np.array([1, 2, 3, 4])) + + idx = forecast.idx_member(1) + npt.assert_array_equal(idx, np.array([True, False, False, False]), strict=True) + + idx = forecast.idx_member(np.array([2, 4])) + npt.assert_array_equal(idx, np.array([False, True, False, True]), strict=True) + + idx = forecast.idx_member([2, 4]) + npt.assert_array_equal(idx, np.array([False, True, False, True]), strict=True) + + idx = forecast.idx_member(None) + npt.assert_array_equal(idx, np.array([False, False, False, False]), strict=True) + + +def test_idx_lead_time(): + """Test idx_lead_time method of Forecast class.""" + forecast = Forecast( + lead_time=pd.timedelta_range(start="1 day", periods=4).to_numpy() + ) + + idx = forecast.idx_lead_time( + pd.timedelta_range(start="1 day", periods=4).to_numpy()[::2] + ) + npt.assert_array_equal(idx, np.array([True, False, True, False]), strict=True) + + idx = forecast.idx_lead_time( + pd.timedelta_range(start="1 day", periods=4).to_numpy()[0] + ) + npt.assert_array_equal(idx, np.array([True, False, False, False]), strict=True) + + idx = forecast.idx_lead_time(None) + npt.assert_array_equal(idx, np.array([False, False, False, False]), strict=True) From 3531c78ad501e65bce9f1bb21fc1bbdc1d19e559 Mon Sep 17 00:00:00 2001 From: Eliane Kobler Date: Tue, 9 Dec 2025 14:50:40 +0100 Subject: [PATCH 2/8] adapt docstrings --- climada/util/forecast.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/climada/util/forecast.py b/climada/util/forecast.py index 602b9c5437..38ea44bd11 100644 --- a/climada/util/forecast.py +++ b/climada/util/forecast.py @@ -58,11 +58,33 @@ def __init__( super().__init__(**kwargs) def idx_member(self, member: np.ndarray) -> np.ndarray: - """Return boolean array where self.member == member using numpy.isin()""" + """Return boolean array where self.member == member using numpy.isin() + + Parameters + ---------- + member : np.ndarray + Forecast ensemble members, given as integers. + + Returns + ------- + np.ndarray + Boolean array where self.member is in member. + """ return np.isin(self.member, member) def idx_lead_time(self, lead_time: np.ndarray) -> np.ndarray: - """Return boolean array where self.lead_time == lead_time using numpy.isin()""" + """Return boolean array where self.lead_time == lead_time using numpy.isin() + + Parameters + ---------- + lead_time : np.ndarray + Forecast lead times, given as timedelta64 objects. + + Returns + ------- + np.ndarray + Boolean array where self.lead_time is in lead_time. + """ return np.isin(self.lead_time, lead_time) From 12bf2b620a2c7cd46d5d34afaf13424ef448a092 Mon Sep 17 00:00:00 2001 From: Lukas Riedel <34276446+peanutfun@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:15:43 +0100 Subject: [PATCH 3/8] Update docstrings --- climada/util/forecast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/climada/util/forecast.py b/climada/util/forecast.py index 38ea44bd11..6884c90ef3 100644 --- a/climada/util/forecast.py +++ b/climada/util/forecast.py @@ -63,7 +63,7 @@ def idx_member(self, member: np.ndarray) -> np.ndarray: Parameters ---------- member : np.ndarray - Forecast ensemble members, given as integers. + Array of ensemble members (ints) for which to return an indexer Returns ------- @@ -79,7 +79,7 @@ def idx_lead_time(self, lead_time: np.ndarray) -> np.ndarray: Parameters ---------- lead_time : np.ndarray - Forecast lead times, given as timedelta64 objects. + Array of lead times (numpy.timedelta64) for which to return an indexer Returns ------- From c1eefdd4b24ced1617453734b4ad8676d99620ef Mon Sep 17 00:00:00 2001 From: Valentin Gebhart Date: Tue, 9 Dec 2025 16:39:07 +0100 Subject: [PATCH 4/8] add HazardForecast.select and extent test --- climada/hazard/forecast.py | 40 ++++++++++++++++++++++++++++ climada/hazard/test/test_forecast.py | 18 +++++++++++++ 2 files changed, 58 insertions(+) diff --git a/climada/hazard/forecast.py b/climada/hazard/forecast.py index 5130e66af1..ef83f01970 100644 --- a/climada/hazard/forecast.py +++ b/climada/hazard/forecast.py @@ -104,3 +104,43 @@ def _check_sizes(self): num_entries = len(self.event_id) size(exp_len=num_entries, var=self.member, var_name="Forecast.member") size(exp_len=num_entries, var=self.lead_time, var_name="Forecast.lead_time") + + def select( + self, + event_names=None, + event_id=None, + date=None, + orig=None, + reg_id=None, + extent=None, + reset_frequency=False, + member=None, + lead_time=None, + ): + if member is not None or lead_time is not None: + mask_member = ( + self.idx_member(member) + if member is not None + else np.full_like(self.member, True, dtype=bool) + ) + mask_lead_time = ( + self.idx_lead_time(lead_time) + if lead_time is not None + else np.full_like(self.lead_time, True, dtype=bool) + ) + mask_event_id = np.asarray(self.event_id)[(mask_member & mask_lead_time)] + event_id = ( + np.intersect1d(event_id, mask_event_id) + if event_id is not None + else mask_event_id + ) + + return super().select( + event_names=event_names, + event_id=event_id, + date=date, + orig=orig, + reg_id=reg_id, + extent=extent, + reset_frequency=reset_frequency, + ) diff --git a/climada/hazard/test/test_forecast.py b/climada/hazard/test/test_forecast.py index 5e975c2885..e22937d1e5 100644 --- a/climada/hazard/test/test_forecast.py +++ b/climada/hazard/test/test_forecast.py @@ -115,6 +115,24 @@ def test_hazard_forecast_select(haz_fc, lead_time, member): npt.assert_array_equal(haz_fc_select.member, member[np.array([3, 0])]) npt.assert_array_equal(haz_fc_select.lead_time, lead_time[np.array([3, 0])]) + haz_fc_select = haz_fc.select(member=[3, 0]) + npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([0, 3])]) + npt.assert_array_equal(haz_fc_select.member, member[np.array([0, 3])]) + npt.assert_array_equal(haz_fc_select.lead_time, lead_time[np.array([0, 3])]) + + haz_fc_select = haz_fc.select(lead_time=lead_time[np.array([3, 0])]) + npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([0, 3])]) + npt.assert_array_equal(haz_fc_select.member, member[np.array([0, 3])]) + npt.assert_array_equal(haz_fc_select.lead_time, lead_time[np.array([0, 3])]) + + haz_fc_select = haz_fc.select(event_id=[1, 4], member=[0, 1, 2]) + npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([0])]) + + haz_fc_select = haz_fc.select( + event_id=[1, 2, 4], member=[0, 1, 2], lead_time=lead_time[1:3] + ) + npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([1])]) + def test_write_read_hazard_forecast(haz_fc, tmp_path): From 64aec3d370dcd05fc649c8a91987e98eb977f150 Mon Sep 17 00:00:00 2001 From: Lukas Riedel <34276446+peanutfun@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:05:28 +0100 Subject: [PATCH 5/8] Update docstring, reoganize tests --- climada/hazard/forecast.py | 26 +++++- climada/hazard/test/test_forecast.py | 129 +++++++++++++++------------ 2 files changed, 96 insertions(+), 59 deletions(-) diff --git a/climada/hazard/forecast.py b/climada/hazard/forecast.py index ef83f01970..f9a3207228 100644 --- a/climada/hazard/forecast.py +++ b/climada/hazard/forecast.py @@ -20,6 +20,7 @@ """ import logging +from typing import Self import numpy as np @@ -107,6 +108,8 @@ def _check_sizes(self): def select( self, + member=None, + lead_time=None, event_names=None, event_id=None, date=None, @@ -114,9 +117,26 @@ def select( reg_id=None, extent=None, reset_frequency=False, - member=None, - lead_time=None, - ): + ) -> Self: + """Select entries based on the parameters and return a new instance. + + The selection will contain the intersection of all given parameters. + + Parameters + ---------- + member : Sequence of ints + Ensemble members to select + lead_time : Sequence of numpy.timedelta64 + Lead times to select + + Returns + ------- + HazardForecast + + See Also + -------- + :py:meth:`~climada.hazard.base.Hazard.select` + """ if member is not None or lead_time is not None: mask_member = ( self.idx_member(member) diff --git a/climada/hazard/test/test_forecast.py b/climada/hazard/test/test_forecast.py index 35c1acb1a8..2ec68a707f 100644 --- a/climada/hazard/test/test_forecast.py +++ b/climada/hazard/test/test_forecast.py @@ -107,64 +107,81 @@ def test_hazard_forecast_concat(haz_fc, lead_time, member): npt.assert_array_equal(haz_fc_concat.member, np.concatenate([member, member])) -@pytest.mark.parametrize( - "var, var_select", - [("event_id", "event_id"), ("event_name", "event_names"), ("date", "date")], -) -def test_hazard_forecast_select(haz_fc, lead_time, member, haz_kwargs, var, var_select): - """Check if Hazard.select works on the derived class""" - - select_mask = np.array([3, 2]) - ordered_select_mask = np.array([3, 2]) - if var == "date": - # Date needs to be a valid delta - select_mask = np.array([2, 3]) - ordered_select_mask = np.array([2, 3]) - - var_value = np.array(haz_kwargs[var])[select_mask] - # event_name is a list, convert to numpy array for indexing - haz_fc_sel = haz_fc.select(**{var_select: var_value}) - # Note: order is preserved - npt.assert_array_equal( - haz_fc_sel.event_id, - haz_fc.event_id[ordered_select_mask], - ) - npt.assert_array_equal( - haz_fc_sel.event_name, - np.array(haz_fc.event_name)[ordered_select_mask], - ) - npt.assert_array_equal(haz_fc_sel.date, haz_fc.date[ordered_select_mask]) - npt.assert_array_equal(haz_fc_sel.frequency, haz_fc.frequency[ordered_select_mask]) - npt.assert_array_equal(haz_fc_sel.member, member[ordered_select_mask]) - npt.assert_array_equal(haz_fc_sel.lead_time, lead_time[ordered_select_mask]) - npt.assert_array_equal( - haz_fc_sel.intensity.todense(), - haz_fc.intensity.todense()[ordered_select_mask], - ) - npt.assert_array_equal( - haz_fc_sel.fraction.todense(), - haz_fc.fraction.todense()[ordered_select_mask], - ) - - assert haz_fc_sel.centroids == haz_fc.centroids - - haz_fc_select = haz_fc.select(member=[3, 0]) - npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([0, 3])]) - npt.assert_array_equal(haz_fc_select.member, member[np.array([0, 3])]) - npt.assert_array_equal(haz_fc_select.lead_time, lead_time[np.array([0, 3])]) - - haz_fc_select = haz_fc.select(lead_time=lead_time[np.array([3, 0])]) - npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([0, 3])]) - npt.assert_array_equal(haz_fc_select.member, member[np.array([0, 3])]) - npt.assert_array_equal(haz_fc_select.lead_time, lead_time[np.array([0, 3])]) - - haz_fc_select = haz_fc.select(event_id=[1, 4], member=[0, 1, 2]) - npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([0])]) +class TestSelect: - haz_fc_select = haz_fc.select( - event_id=[1, 2, 4], member=[0, 1, 2], lead_time=lead_time[1:3] + @pytest.mark.parametrize( + "var, var_select", + [("event_id", "event_id"), ("event_name", "event_names"), ("date", "date")], ) - npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([1])]) + def test_base_class_select( + self, haz_fc, lead_time, member, haz_kwargs, var, var_select + ): + """Check if Hazard.select works on the derived class""" + + select_mask = np.array([3, 2]) + ordered_select_mask = np.array([3, 2]) + if var == "date": + # Date needs to be a valid delta + select_mask = np.array([2, 3]) + ordered_select_mask = np.array([2, 3]) + + var_value = np.array(haz_kwargs[var])[select_mask] + # event_name is a list, convert to numpy array for indexing + haz_fc_sel = haz_fc.select(**{var_select: var_value}) + # Note: order is preserved + npt.assert_array_equal( + haz_fc_sel.event_id, + haz_fc.event_id[ordered_select_mask], + ) + npt.assert_array_equal( + haz_fc_sel.event_name, + np.array(haz_fc.event_name)[ordered_select_mask], + ) + npt.assert_array_equal(haz_fc_sel.date, haz_fc.date[ordered_select_mask]) + npt.assert_array_equal( + haz_fc_sel.frequency, haz_fc.frequency[ordered_select_mask] + ) + npt.assert_array_equal(haz_fc_sel.member, member[ordered_select_mask]) + npt.assert_array_equal(haz_fc_sel.lead_time, lead_time[ordered_select_mask]) + npt.assert_array_equal( + haz_fc_sel.intensity.todense(), + haz_fc.intensity.todense()[ordered_select_mask], + ) + npt.assert_array_equal( + haz_fc_sel.fraction.todense(), + haz_fc.fraction.todense()[ordered_select_mask], + ) + + assert haz_fc_sel.centroids == haz_fc.centroids + + def test_derived_select(self, haz_fc, lead_time, member, haz_kwargs): + haz_fc_select = haz_fc.select(member=[3, 0]) + idx = np.array([0, 3]) + npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[idx]) + npt.assert_array_equal(haz_fc_select.member, member[idx]) + npt.assert_array_equal(haz_fc_select.lead_time, lead_time[idx]) + + haz_fc_select = haz_fc.select(lead_time=lead_time[np.array([3, 0])]) + npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[idx]) + npt.assert_array_equal(haz_fc_select.member, member[idx]) + npt.assert_array_equal(haz_fc_select.lead_time, lead_time[idx]) + + # Test intersections + haz_fc_select = haz_fc.select(event_id=[1, 4], member=[0, 1, 2]) + npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([0])]) + + haz_fc_select = haz_fc.select( + event_id=[1, 2, 4], member=[0, 1, 2], lead_time=lead_time[1:3] + ) + npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([1])]) + + # Test "outer" + haz_fc2 = HazardForecast( + lead_time=lead_time, member=np.zeros_like(member, dtype="int"), **haz_kwargs + ) + haz_fc_select = haz_fc2.select(event_id=[1, 2, 4], member=[0]) + npt.assert_array_equal(haz_fc_select.event_id, [1, 2, 4]) + npt.assert_array_equal(haz_fc_select.member, [0, 0, 0]) def test_write_read_hazard_forecast(haz_fc, tmp_path): From f584008624dddc1749581e4d6d33250451e63d1e Mon Sep 17 00:00:00 2001 From: Lukas Riedel <34276446+peanutfun@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:13:10 +0100 Subject: [PATCH 6/8] Remove 'Self' for Py 3.10 compatibility --- climada/hazard/forecast.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/climada/hazard/forecast.py b/climada/hazard/forecast.py index f9a3207228..a928298711 100644 --- a/climada/hazard/forecast.py +++ b/climada/hazard/forecast.py @@ -20,7 +20,6 @@ """ import logging -from typing import Self import numpy as np @@ -117,7 +116,7 @@ def select( reg_id=None, extent=None, reset_frequency=False, - ) -> Self: + ): """Select entries based on the parameters and return a new instance. The selection will contain the intersection of all given parameters. From c123dec0a95e25ee288194300008f3b06d9b1966 Mon Sep 17 00:00:00 2001 From: Valentin Gebhart Date: Wed, 10 Dec 2025 09:34:17 +0100 Subject: [PATCH 7/8] change variable name in select function --- climada/hazard/forecast.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/climada/hazard/forecast.py b/climada/hazard/forecast.py index a928298711..b09e1a44e3 100644 --- a/climada/hazard/forecast.py +++ b/climada/hazard/forecast.py @@ -147,11 +147,13 @@ def select( if lead_time is not None else np.full_like(self.lead_time, True, dtype=bool) ) - mask_event_id = np.asarray(self.event_id)[(mask_member & mask_lead_time)] + event_id_from_forecast_mask = np.asarray(self.event_id)[ + (mask_member & mask_lead_time) + ] event_id = ( - np.intersect1d(event_id, mask_event_id) + np.intersect1d(event_id, event_id_from_forecast_mask) if event_id is not None - else mask_event_id + else event_id_from_forecast_mask ) return super().select( From 08eb2638eb467f80108053a1b86daec4f488e775 Mon Sep 17 00:00:00 2001 From: Lukas Riedel <34276446+peanutfun@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:31:14 +0100 Subject: [PATCH 8/8] Reorganize tests and and test for null select --- climada/hazard/test/test_forecast.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/climada/hazard/test/test_forecast.py b/climada/hazard/test/test_forecast.py index 2ec68a707f..ac1a726965 100644 --- a/climada/hazard/test/test_forecast.py +++ b/climada/hazard/test/test_forecast.py @@ -154,7 +154,7 @@ def test_base_class_select( assert haz_fc_sel.centroids == haz_fc.centroids - def test_derived_select(self, haz_fc, lead_time, member, haz_kwargs): + def test_derived_select_single(self, haz_fc, lead_time, member): haz_fc_select = haz_fc.select(member=[3, 0]) idx = np.array([0, 3]) npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[idx]) @@ -166,7 +166,7 @@ def test_derived_select(self, haz_fc, lead_time, member, haz_kwargs): npt.assert_array_equal(haz_fc_select.member, member[idx]) npt.assert_array_equal(haz_fc_select.lead_time, lead_time[idx]) - # Test intersections + def test_derived_select_intersections(self, haz_fc, lead_time, member, haz_kwargs): haz_fc_select = haz_fc.select(event_id=[1, 4], member=[0, 1, 2]) npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([0])]) @@ -183,6 +183,19 @@ def test_derived_select(self, haz_fc, lead_time, member, haz_kwargs): npt.assert_array_equal(haz_fc_select.event_id, [1, 2, 4]) npt.assert_array_equal(haz_fc_select.member, [0, 0, 0]) + def test_derived_select_null(self, haz_fc, haz_kwargs): + haz_fc_select = haz_fc.select() + assert_hazard_kwargs(haz_fc_select, **haz_kwargs) + + with pytest.raises(IndexError): + haz_fc.select(event_id=[-1]) + with pytest.raises(IndexError): + haz_fc.select(member=[-1]) + with pytest.raises(IndexError): + haz_fc.select( + lead_time=[np.timedelta64("2", "Y").astype("timedelta64[ns]")] + ) + def test_write_read_hazard_forecast(haz_fc, tmp_path):