From 574a3a7548be3034108dbc8071184741f3e308ef Mon Sep 17 00:00:00 2001 From: Valentin Gebhart Date: Tue, 9 Dec 2025 17:24:13 +0100 Subject: [PATCH 1/5] first draft impact forecast select --- climada/engine/impact_forecast.py | 36 ++++++ climada/engine/test/test_impact_forecast.py | 128 ++++++++++++-------- 2 files changed, 116 insertions(+), 48 deletions(-) diff --git a/climada/engine/impact_forecast.py b/climada/engine/impact_forecast.py index 1406f4ae5f..674f0d12a2 100644 --- a/climada/engine/impact_forecast.py +++ b/climada/engine/impact_forecast.py @@ -184,3 +184,39 @@ 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_ids=None, + event_names=None, + dates=None, + coord_exp=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_ids = ( + np.intersect1d(event_ids, mask_event_id) + if event_ids is not None + else mask_event_id + ) + + return super().select( + event_ids=event_ids, + event_names=event_names, + dates=dates, + coord_exp=coord_exp, + reset_frequency=reset_frequency, + ) diff --git a/climada/engine/test/test_impact_forecast.py b/climada/engine/test/test_impact_forecast.py index 33566acd5a..e8f3432e83 100644 --- a/climada/engine/test/test_impact_forecast.py +++ b/climada/engine/test/test_impact_forecast.py @@ -92,58 +92,90 @@ def test_impact_forecast_from_impact( self.assert_impact_kwargs(impact_forecast, **impact_kwargs) -@pytest.mark.parametrize( - "var, var_select", - [("event_id", "event_ids"), ("event_name", "event_names"), ("date", "dates")], -) -def test_impact_forecast_select_events( - impact_forecast, lead_time, member, impact_kwargs, var, var_select -): - """Check if Impact.select works on the derived class""" - select_mask = np.array([2, 1]) - ordered_select_mask = np.array([1, 2]) - if var == "date": - # Date needs to be a valid delta - select_mask = np.array([1, 2]) - ordered_select_mask = np.array([1, 2]) +class TestSelect: - var_value = np.array(impact_kwargs[var])[select_mask] - # event_name is a list, convert to numpy array for indexing - impact_fc = impact_forecast.select(**{var_select: var_value}) - # NOTE: Events keep their original order - npt.assert_array_equal( - impact_fc.event_id, - impact_forecast.event_id[ordered_select_mask], - ) - npt.assert_array_equal( - impact_fc.event_name, - np.array(impact_forecast.event_name)[ordered_select_mask], - ) - npt.assert_array_equal(impact_fc.date, impact_forecast.date[ordered_select_mask]) - npt.assert_array_equal( - impact_fc.frequency, impact_forecast.frequency[ordered_select_mask] - ) - npt.assert_array_equal(impact_fc.member, member[ordered_select_mask]) - npt.assert_array_equal(impact_fc.lead_time, lead_time[ordered_select_mask]) - npt.assert_array_equal( - impact_fc.imp_mat.todense(), - impact_forecast.imp_mat.todense()[ordered_select_mask], + @pytest.mark.parametrize( + "var, var_select", + [("event_id", "event_ids"), ("event_name", "event_names"), ("date", "dates")], ) + def test_base_class_select( + self, impact_forecast, lead_time, member, impact_kwargs, var, var_select + ): + """Check if Impact.select works on the derived class""" + select_mask = np.array([2, 1]) + ordered_select_mask = np.array([1, 2]) + if var == "date": + # Date needs to be a valid delta + select_mask = np.array([1, 2]) + ordered_select_mask = np.array([1, 2]) + + var_value = np.array(impact_kwargs[var])[select_mask] + # event_name is a list, convert to numpy array for indexing + impact_fc = impact_forecast.select(**{var_select: var_value}) + # NOTE: Events keep their original order + npt.assert_array_equal( + impact_fc.event_id, + impact_forecast.event_id[ordered_select_mask], + ) + npt.assert_array_equal( + impact_fc.event_name, + np.array(impact_forecast.event_name)[ordered_select_mask], + ) + npt.assert_array_equal( + impact_fc.date, impact_forecast.date[ordered_select_mask] + ) + npt.assert_array_equal( + impact_fc.frequency, impact_forecast.frequency[ordered_select_mask] + ) + npt.assert_array_equal(impact_fc.member, member[ordered_select_mask]) + npt.assert_array_equal(impact_fc.lead_time, lead_time[ordered_select_mask]) + npt.assert_array_equal( + impact_fc.imp_mat.todense(), + impact_forecast.imp_mat.todense()[ordered_select_mask], + ) + def test_impact_forecast_select_exposure( + self, impact_forecast, lead_time, member, impact_kwargs + ): + """Check if Impact.select works on the derived class""" + exp_col = 0 + select_mask = np.array([exp_col]) + coord_exp = impact_kwargs["coord_exp"][select_mask] + impact_fc = impact_forecast.select(coord_exp=coord_exp) + npt.assert_array_equal(impact_fc.member, member) + npt.assert_array_equal(impact_fc.lead_time, lead_time) + npt.assert_array_equal( + impact_fc.imp_mat.todense(), impact_forecast.imp_mat.todense()[:, exp_col] + ) -def test_impact_forecast_select_exposure( - impact_forecast, lead_time, member, impact_kwargs -): - """Check if Impact.select works on the derived class""" - exp_col = 0 - select_mask = np.array([exp_col]) - coord_exp = impact_kwargs["coord_exp"][select_mask] - impact_fc = impact_forecast.select(coord_exp=coord_exp) - npt.assert_array_equal(impact_fc.member, member) - npt.assert_array_equal(impact_fc.lead_time, lead_time) - npt.assert_array_equal( - impact_fc.imp_mat.todense(), impact_forecast.imp_mat.todense()[:, exp_col] - ) + 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]) @pytest.mark.skip("Concat from base class does not work") From 00cfde0b612273efdf4ca3675783fddeec4529b8 Mon Sep 17 00:00:00 2001 From: Valentin Gebhart Date: Tue, 9 Dec 2025 17:59:17 +0100 Subject: [PATCH 2/5] add test and docstrings --- climada/engine/impact_forecast.py | 15 +++++++ climada/engine/test/test_impact_forecast.py | 48 ++++++++++++--------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/climada/engine/impact_forecast.py b/climada/engine/impact_forecast.py index 674f0d12a2..743ac95f04 100644 --- a/climada/engine/impact_forecast.py +++ b/climada/engine/impact_forecast.py @@ -195,6 +195,21 @@ def select( member=None, lead_time=None, ): + """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 + ------- + ImpactForecast + See Also + -------- + :py:meth:`~climada.engine.impact.Impact.select` + """ if member is not None or lead_time is not None: mask_member = ( self.idx_member(member) diff --git a/climada/engine/test/test_impact_forecast.py b/climada/engine/test/test_impact_forecast.py index e8f3432e83..84ecee52b4 100644 --- a/climada/engine/test/test_impact_forecast.py +++ b/climada/engine/test/test_impact_forecast.py @@ -148,34 +148,40 @@ def test_impact_forecast_select_exposure( impact_fc.imp_mat.todense(), impact_forecast.imp_mat.todense()[:, exp_col] ) - 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]) + def test_derived_select(self, impact_forecast, lead_time, member, impact_kwargs): + imp_fc_select = impact_forecast.select(member=[2, 0]) + idx = np.array([0, 2]) + npt.assert_array_equal(imp_fc_select.event_id, impact_forecast.event_id[idx]) + npt.assert_array_equal(imp_fc_select.member, member[idx]) + npt.assert_array_equal(imp_fc_select.lead_time, lead_time[idx]) + + imp_fc_select = impact_forecast.select(lead_time=lead_time[np.array([2, 0])]) + npt.assert_array_equal(imp_fc_select.event_id, impact_forecast.event_id[idx]) + npt.assert_array_equal(imp_fc_select.member, member[idx]) + npt.assert_array_equal(imp_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])]) + imp_fc_select = impact_forecast.select(event_ids=[10, 14], member=[0, 1, 2]) + npt.assert_array_equal( + imp_fc_select.event_id, impact_forecast.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] + imp_fc_select = impact_forecast.select( + event_ids=[10, 11, 13], member=[0, 1, 2], lead_time=lead_time[1:3] + ) + npt.assert_array_equal( + imp_fc_select.event_id, impact_forecast.event_id[np.array([1])] ) - 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 + impact_forecast2 = ImpactForecast( + lead_time=lead_time, + member=np.zeros_like(member, dtype="int"), + **impact_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]) + imp_fc_select = impact_forecast2.select(event_ids=[10, 11, 13], member=[0]) + npt.assert_array_equal(imp_fc_select.event_id, [10, 11, 13]) + npt.assert_array_equal(imp_fc_select.member, [0, 0, 0]) @pytest.mark.skip("Concat from base class does not work") From b8dc687dda51bfa9a3975055bd9456919ff4c868 Mon Sep 17 00:00:00 2001 From: Valentin Gebhart Date: Wed, 10 Dec 2025 09:41:12 +0100 Subject: [PATCH 3/5] change variable name in select --- climada/engine/impact_forecast.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/climada/engine/impact_forecast.py b/climada/engine/impact_forecast.py index 743ac95f04..dbd9e76adc 100644 --- a/climada/engine/impact_forecast.py +++ b/climada/engine/impact_forecast.py @@ -221,11 +221,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_ids = ( - np.intersect1d(event_ids, mask_event_id) + np.intersect1d(event_ids, event_id_from_forecast_mask) if event_ids is not None - else mask_event_id + else event_id_from_forecast_mask ) return super().select( From acaf55f76d95a563ac7d90498587cd1e98a0e85f Mon Sep 17 00:00:00 2001 From: Lukas Riedel <34276446+peanutfun@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:55:29 +0100 Subject: [PATCH 4/5] Apply suggestion from @peanutfun --- climada/engine/impact_forecast.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/climada/engine/impact_forecast.py b/climada/engine/impact_forecast.py index dbd9e76adc..2160b18da7 100644 --- a/climada/engine/impact_forecast.py +++ b/climada/engine/impact_forecast.py @@ -197,15 +197,18 @@ def select( ): """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 ------- ImpactForecast + See Also -------- :py:meth:`~climada.engine.impact.Impact.select` From 96a3f84bcec46a7be8a89fdad334a13e3e40a22f Mon Sep 17 00:00:00 2001 From: Lukas Riedel <34276446+peanutfun@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:21:45 +0100 Subject: [PATCH 5/5] Reorganize tests and add tests for empty selections --- climada/engine/test/test_impact_forecast.py | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/climada/engine/test/test_impact_forecast.py b/climada/engine/test/test_impact_forecast.py index 84ecee52b4..94655c8b17 100644 --- a/climada/engine/test/test_impact_forecast.py +++ b/climada/engine/test/test_impact_forecast.py @@ -148,7 +148,7 @@ def test_impact_forecast_select_exposure( impact_fc.imp_mat.todense(), impact_forecast.imp_mat.todense()[:, exp_col] ) - def test_derived_select(self, impact_forecast, lead_time, member, impact_kwargs): + def test_derived_select_single(self, impact_forecast, lead_time, member): imp_fc_select = impact_forecast.select(member=[2, 0]) idx = np.array([0, 2]) npt.assert_array_equal(imp_fc_select.event_id, impact_forecast.event_id[idx]) @@ -160,7 +160,9 @@ def test_derived_select(self, impact_forecast, lead_time, member, impact_kwargs) npt.assert_array_equal(imp_fc_select.member, member[idx]) npt.assert_array_equal(imp_fc_select.lead_time, lead_time[idx]) - # Test intersections + def test_derived_select_intersections( + self, impact_forecast, lead_time, member, impact_kwargs + ): imp_fc_select = impact_forecast.select(event_ids=[10, 14], member=[0, 1, 2]) npt.assert_array_equal( imp_fc_select.event_id, impact_forecast.event_id[np.array([0])] @@ -183,6 +185,22 @@ def test_derived_select(self, impact_forecast, lead_time, member, impact_kwargs) npt.assert_array_equal(imp_fc_select.event_id, [10, 11, 13]) npt.assert_array_equal(imp_fc_select.member, [0, 0, 0]) + def test_no_select(self, impact_forecast, impact_kwargs): + imp_fc_select = impact_forecast.select() + npt.assert_array_equal( + imp_fc_select.imp_mat.todense(), impact_forecast.imp_mat.todense() + ) + + num_centroids = len(impact_kwargs["coord_exp"]) + imp_fc_select = impact_forecast.select(event_names=["aaaaa", "foo"]) + assert imp_fc_select.imp_mat.shape == (0, num_centroids) + imp_fc_select = impact_forecast.select(event_ids=[-1, 1002]) + assert imp_fc_select.imp_mat.shape == (0, num_centroids) + imp_fc_select = impact_forecast.select(member=[-1]) + assert imp_fc_select.imp_mat.shape == (0, num_centroids) + imp_fc_select = impact_forecast.select(np.timedelta64("3", "Y")) + assert imp_fc_select.imp_mat.shape == (0, num_centroids) + @pytest.mark.skip("Concat from base class does not work") def test_impact_forecast_concat(impact_forecast, member):