Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7da50f1
Return impactForecast object in _return_impact
luseverin Dec 8, 2025
bd3502f
Return impactForecast object in _return_empty
luseverin Dec 8, 2025
dfa0198
Add full impactcalc test for impactForecast
luseverin Dec 8, 2025
59c0e5b
Correct mistakes in _return_empty and _return_impact
luseverin Dec 8, 2025
4b5ae95
Raise value error when computing impact with impact forecast without …
luseverin Dec 8, 2025
9a516e1
Cosmetics: Improve error message, move test to own class
Dec 8, 2025
d571bb7
Merge branch 'forecast-class' into impactCalc_block_nonsense_attrs
luseverin Dec 9, 2025
6fe29f0
Merge branch 'impactCalc_return_impactForecast' into impactCalc_block…
luseverin Dec 9, 2025
899d8f0
add test to check that eai_exp and aai_agg are nan for forecasts
luseverin Dec 9, 2025
db32170
Write nans for eai_exp and aai_agg when forecast is used
luseverin Dec 9, 2025
d2f035f
update tests using pytest
luseverin Dec 9, 2025
c10a4b3
Fix error in test fixtures
luseverin Dec 9, 2025
d3a5642
Returns nans for eai_exp and aai_agg when exposures is empty
luseverin Dec 9, 2025
d43a46c
add warning when at_event is used with forecast
luseverin Dec 9, 2025
e197566
Update ImpactCalc tests for forecasts
peanutfun Dec 9, 2025
0a324b2
Merge branch 'impactCalc_return_impactForecast' of https://github.com…
peanutfun Dec 9, 2025
554cfc8
Review ImpactCalc forecast handling
peanutfun Dec 9, 2025
30ed14d
Block local_exceedance_impact
luseverin Dec 9, 2025
f3ab44a
Fix bug in test
peanutfun Dec 9, 2025
612a53c
Merge branch 'impactCalc_return_impactForecast' of https://github.com…
peanutfun Dec 9, 2025
9e4f2bb
Block return_period and exceedance_freq_curve
luseverin Dec 9, 2025
727357e
Log warning for at_event getter
luseverin Dec 9, 2025
0bc5846
Add mean max min methods to impact forecast and tests
luseverin Dec 9, 2025
a55ab3b
Merge branch 'forecast-class' into implement_mean_min_max
luseverin Dec 9, 2025
0aad2a5
Reduce lead time and member and update test
luseverin Dec 10, 2025
102f9be
Update docstrings
luseverin Dec 10, 2025
dfce16f
Remove useless comments
luseverin Dec 10, 2025
0b6e1e4
Add min max mean for hazard forecast
luseverin Dec 10, 2025
3fc25af
Correct some mistakes in mean min max on hazard forecast
luseverin Dec 10, 2025
a4fe3b0
Add tests for hazard forecast mean min max
luseverin Dec 10, 2025
e099868
Set reduced date to 0 and cosmetic changes
luseverin Dec 10, 2025
23660de
Set reduced date to 0 in impact forecast
luseverin Dec 10, 2025
64149df
Simplify code and tests
peanutfun Dec 10, 2025
2e028e3
Merge branch 'forecast-class' into implement_mean_min_max
peanutfun Dec 10, 2025
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
122 changes: 118 additions & 4 deletions climada/engine/impact_forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import logging

import numpy as np
import scipy.sparse as sparse

from ..util import log_level
from ..util.checker import size
Expand Down Expand Up @@ -185,6 +186,123 @@ def _check_sizes(self):
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 _reduce_attrs(self, event_name: str):
"""
Reduce the attributes of an ImpactForecast to a single value.

Attributes are modified as follows:
- lead_time: set to NaT
- member: set to -1
- event_id: set to 0
- event_name: set to the name of the reduction method (default)
- date: set to 0
- frequency: set to 1

Parameters
----------
event_name : str
The event name given to the reduced data.
"""
reduced_attrs = {
"lead_time": np.array([np.timedelta64("NaT")]),
"member": np.array([-1]),
"event_id": np.array([0]),
"event_name": np.array([event_name]),
"date": np.array([0]),
"frequency": np.array([1]),
}

return reduced_attrs

def min(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the goal not to reduce along a specified dimension? I am no sure to see the value of getting the minimum impact accross all exposure points, all lead times, and all ensemble members.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should reduce along the event dimension. Then we can compute the statistics over the member and/or lead time with the appropriate use of the select function. Does that makes sense?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me this method is exposed to the user, so it should directly give back a useful/meaningful result. So I would make the exposed method directly clear. Either over members, or over lead times (or both if the user for some reason wants it). Or make it private if the user is not supposed to use it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chahank We first implement the complete reductions, then we implement reduction over specific dimensions in #1163. This is easier from an implementation perspective, because the latter requires selection and concatenation to work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I would just make the complete reduction methods above private then.

"""
Reduce the impact matrix and at_event of an ImpactForecast to the minimum
value.

Parameters
----------
None

Returns
-------
ImpactForecast
An ImpactForecast object with the min impact matrix and at_event.
"""
red_imp_mat = self.imp_mat.min(axis=0).tocsr()
red_at_event = np.array([red_imp_mat.sum()])
return ImpactForecast(
frequency_unit=self.frequency_unit,
coord_exp=self.coord_exp,
crs=self.crs,
eai_exp=self.eai_exp,
at_event=red_at_event,
tot_value=self.tot_value,
aai_agg=self.aai_agg,
unit=self.unit,
imp_mat=red_imp_mat,
haz_type=self.haz_type,
**self._reduce_attrs("min"),
)

def max(self):
"""
Reduce the impact matrix and at_event of an ImpactForecast to the maximum
value.

Parameters
----------
None

Returns
-------
ImpactForecast
An ImpactForecast object with the max impact matrix and at_event.
"""
red_imp_mat = self.imp_mat.max(axis=0).tocsr()
red_at_event = np.array([red_imp_mat.sum()])
return ImpactForecast(
frequency_unit=self.frequency_unit,
coord_exp=self.coord_exp,
crs=self.crs,
eai_exp=self.eai_exp,
at_event=red_at_event,
tot_value=self.tot_value,
aai_agg=self.aai_agg,
unit=self.unit,
imp_mat=red_imp_mat,
haz_type=self.haz_type,
**self._reduce_attrs("max"),
)

def mean(self):
"""
Reduce the impact matrix and at_event of an ImpactForecast to the mean value.

Parameters
----------
None

Returns
-------
ImpactForecast
An ImpactForecast object with the mean impact matrix and at_event.
"""
red_imp_mat = sparse.csr_matrix(self.imp_mat.mean(axis=0))
red_at_event = np.array([red_imp_mat.sum()])
return ImpactForecast(
frequency_unit=self.frequency_unit,
coord_exp=self.coord_exp,
crs=self.crs,
eai_exp=self.eai_exp,
at_event=red_at_event,
tot_value=self.tot_value,
aai_agg=self.aai_agg,
unit=self.unit,
imp_mat=red_imp_mat,
haz_type=self.haz_type,
**self._reduce_attrs("mean"),
)

def select(
self,
event_ids=None,
Expand All @@ -205,10 +323,6 @@ def select(
lead_time : Sequence of numpy.timedelta64
Lead times to select

Returns
-------
ImpactForecast

See Also
--------
:py:meth:`~climada.engine.impact.Impact.select`
Expand Down
39 changes: 38 additions & 1 deletion climada/engine/test/test_impact_forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def test_impact_forecast_concat(impact_forecast, member):


def test_impact_forecast_blocked_methods(impact_forecast):
"""Check if blocked methods raise NotImplementedError"""
"""Check if ImpactForecast.exceedance_freq_curve raises NotImplementedError"""
with pytest.raises(NotImplementedError):
impact_forecast.local_exceedance_impact(np.array([10, 50, 100]))

Expand All @@ -221,3 +221,40 @@ def test_impact_forecast_blocked_methods(impact_forecast):

with pytest.raises(NotImplementedError):
impact_forecast.calc_freq_curve(np.array([10, 50, 100]))


@pytest.fixture
def impact_forecast_stats(impact_kwargs, lead_time, member):
max_index = 4
for key, val in impact_kwargs.items():
if isinstance(val, (np.ndarray, list)):
impact_kwargs[key] = val[:max_index]
elif isinstance(val, csr_matrix):
impact_kwargs[key] = val[:max_index, :]
impact_kwargs["imp_mat"] = csr_matrix([[1, 0], [0, 1], [3, 2], [2, 3]])
impact_kwargs["at_event"] = np.array([1, 1, 5, 5])
return ImpactForecast(
lead_time=lead_time[:max_index], member=member[:max_index], **impact_kwargs
)


@pytest.mark.parametrize("attr", ["min", "mean", "max"])
def test_impact_forecast_min_mean_max(impact_forecast_stats, attr):
"""Check mean, min, and max methods for ImpactForecast"""
imp_fc_reduced = getattr(impact_forecast_stats, attr)()

# assert imp_mat
npt.assert_array_equal(
imp_fc_reduced.imp_mat.todense(),
getattr(impact_forecast_stats.imp_mat.todense(), attr)(axis=0),
)
at_event_expected = {"min": [0], "mean": [3], "max": [6]}
npt.assert_array_equal(imp_fc_reduced.at_event, at_event_expected[attr])

# check that attributes where reduced correctly
npt.assert_array_equal(np.isnat(imp_fc_reduced.lead_time), [True])
npt.assert_array_equal(imp_fc_reduced.member, [-1])
npt.assert_array_equal(imp_fc_reduced.event_name, [attr])
npt.assert_array_equal(imp_fc_reduced.event_id, [0])
npt.assert_array_equal(imp_fc_reduced.frequency, [1])
npt.assert_array_equal(imp_fc_reduced.date, [0])
114 changes: 110 additions & 4 deletions climada/hazard/forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import logging

import numpy as np
import scipy.sparse as sparse

from ..util.checker import size
from ..util.forecast import Forecast
Expand Down Expand Up @@ -105,6 +106,115 @@ def _check_sizes(self):
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 _reduce_attrs(self, event_name: str):
"""
Reduce the attributes of a HazardForecast to a single value.

Attributes are modified as follows:
- lead_time: set to NaT
- member: set to -1
- event_id: set to 0
- event_name: set to the name of the reduction method (default)
- date: set to 0
- frequency: set to 1

Parameters
----------
event_name : str
The event_name given to the reduced data.
"""
reduced_attrs = {
"lead_time": np.array([np.timedelta64("NaT")]),
"member": np.array([-1]),
"event_id": np.array([0]),
"event_name": np.array([event_name]),
"date": np.array([0]),
"frequency": np.array([1]),
"orig": np.array([True]),
}

return reduced_attrs

def min(self):
"""
Reduce the intensity and fraction of a HazardForecast to the minimum
value.

Parameters
----------
None

Returns
-------
HazardForecast
A HazardForecast object with the min intensity and fraction.
"""
red_intensity = self.intensity.min(axis=0).tocsr()
red_fraction = self.fraction.min(axis=0).tocsr()
return HazardForecast(
haz_type=self.haz_type,
pool=self.pool,
units=self.units,
centroids=self.centroids,
frequency_unit=self.frequency_unit,
intensity=red_intensity,
fraction=red_fraction,
**self._reduce_attrs("min"),
)

def max(self):
"""
Reduce the intensity and fraction of a HazardForecast to the maximum
value.

Parameters
----------
None

Returns
-------
HazardForecast
A HazardForecast object with the min intensity and fraction.
"""
red_intensity = self.intensity.max(axis=0).tocsr()
red_fraction = self.fraction.max(axis=0).tocsr()
return HazardForecast(
haz_type=self.haz_type,
pool=self.pool,
units=self.units,
centroids=self.centroids,
frequency_unit=self.frequency_unit,
intensity=red_intensity,
fraction=red_fraction,
**self._reduce_attrs("max"),
)

def mean(self):
"""
Reduce the intensity and fraction of a HazardForecast to the mean value.

Parameters
----------
None

Returns
-------
HazardForecast
A HazardForecast object with the min intensity and fraction.
"""
red_intensity = sparse.csr_matrix(self.intensity.mean(axis=0))
red_fraction = sparse.csr_matrix(self.fraction.mean(axis=0))
return HazardForecast(
haz_type=self.haz_type,
pool=self.pool,
units=self.units,
centroids=self.centroids,
frequency_unit=self.frequency_unit,
intensity=red_intensity,
fraction=red_fraction,
**self._reduce_attrs("mean"),
)

@classmethod
def concat(cls, haz_list: list):
"""Concatenate multiple HazardForecast instances and return a new object"""
Expand Down Expand Up @@ -138,10 +248,6 @@ def select(
lead_time : Sequence of numpy.timedelta64
Lead times to select

Returns
-------
HazardForecast

See Also
--------
:py:meth:`~climada.hazard.base.Hazard.select`
Expand Down
25 changes: 25 additions & 0 deletions climada/hazard/test/test_forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,28 @@ def test_write_read_hazard_forecast(haz_fc, tmp_path):
else:
# npt.assert_array_equal also works for comparing int, float or list
npt.assert_array_equal(haz_fc.__dict__[key], haz_fc_read.__dict__[key])


@pytest.mark.parametrize("attr", ["min", "mean", "max"])
def test_hazard_forecast_mean_min_max(haz_fc, attr):
"""Check mean, min, and max methods for ImpactForecast"""
haz_fcst_reduced = getattr(haz_fc, attr)()

# Assert sparse matrices
npt.assert_array_equal(
haz_fcst_reduced.intensity.todense(),
getattr(haz_fc.intensity.todense(), attr)(axis=0),
)
npt.assert_array_equal(
haz_fcst_reduced.fraction.todense(),
getattr(haz_fc.fraction.todense(), attr)(axis=0),
)

# Check that attributes where reduced correctly
npt.assert_array_equal(np.isnat(haz_fcst_reduced.lead_time), [True])
npt.assert_array_equal(haz_fcst_reduced.member, [-1])
npt.assert_array_equal(haz_fcst_reduced.event_name, [attr])
npt.assert_array_equal(haz_fcst_reduced.event_id, [0])
npt.assert_array_equal(haz_fcst_reduced.frequency, [1])
npt.assert_array_equal(haz_fcst_reduced.date, [0])
npt.assert_array_equal(haz_fcst_reduced.orig, [True])
Loading