Skip to content

Commit a1e7107

Browse files
peanutfunEvelyn-MluseverinValentinGebhartchahank
authored
Add forecast classes (#1115)
* Add HazardForecast and ImpactForecast classes. * Add tests. * Add new forecast tutorial. --------- Co-authored-by: Evelyn Mühlhofer <42063186+Evelyn-M@users.noreply.github.com> Co-authored-by: Luca Severino <91593121+luseverin@users.noreply.github.com> Co-authored-by: Valentin Gebhart <60438839+ValentinGebhart@users.noreply.github.com> Co-authored-by: Chahan M. Kropf <chahan.kropf@usys.ethz.ch> Co-authored-by: Chahan Kropf <chahan.kropf@posteo.com> Co-authored-by: elianekobler <138141646+elianekobler@users.noreply.github.com>
1 parent a99b10b commit a1e7107

21 files changed

Lines changed: 4518 additions & 81 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ Code freeze date: YYYY-MM-DD
1212

1313
### Added
1414

15+
- Support for ensemble forecast data as hazard via `HazardForecast` and `ImpactForecast`, see `doc.user_guide.climada_engine_ImpactForecast.ipynb` [#1115](https://github.com/CLIMADA-project/climada_python/pull/)
1516
- Better type hints and overloads signatures for ImpactFuncSet [#1250](https://github.com/CLIMADA-project/climada_python/pull/1250)
1617
- Adds `MeasureConfig` and related dataclasses for new `Measure` object retrocompatibility and (de)serialization capabilities [#1276](https://github.com/CLIMADA-project/climada_python/pull/1276)
1718
- Add inter- and extrapolation options to `ImpactFreqCurve` with method `interpolate` [#1252](https://github.com/CLIMADA-project/climada_python/pull/1252)
1819
- Add `trajectory` module to ease the handling of multiple risk assessment at different point in time, and enable interpolation in between. [#1197](https://github.com/CLIMADA-project/climada_python/pull/1197), [#1198](https://github.com/CLIMADA-project/climada_python/pull/1198), [#1199](https://github.com/CLIMADA-project/climada_python/pull/1199), [#1200](https://github.com/CLIMADA-project/climada_python/pull/1197), [#1201](https://github.com/CLIMADA-project/climada_python/pull/1197)
1920

2021
### Changed
22+
2123
- Updated Impact Calculation Tutorial (`doc.climada_engine_Impact.ipynb`) [#1095](https://github.com/CLIMADA-project/climada_python/pull/1095).
2224
- Makes current `measure` module a legacy module, moving it to `_legacy_measure`, to retain compatibility with `CostBenefit` class and various tests. [#1274](https://github.com/CLIMADA-project/climada_python/pull/1274)
2325

climada/engine/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@
2222
from .cost_benefit import *
2323
from .impact import *
2424
from .impact_calc import *
25+
from .impact_forecast import ImpactForecast

climada/engine/impact.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,7 @@ def __init__(
188188
)
189189
if len(self.coord_exp) != len(self.eai_exp):
190190
raise AttributeError(
191-
"Number of exposures points is different from"
192-
"number of eai_exp values"
191+
"Number of exposures points is different fromnumber of eai_exp values"
193192
)
194193
if imp_mat is not None:
195194
self.imp_mat = imp_mat
@@ -1226,8 +1225,9 @@ def plot_rp_imp(
12261225

12271226
impacts_stats_vals = impacts_stats.values[:, 1:].T.astype(float)
12281227
if not log10_scale:
1229-
min_impact, max_impact = np.nanmin(impacts_stats_vals), np.nanmax(
1230-
impacts_stats_vals
1228+
min_impact, max_impact = (
1229+
np.nanmin(impacts_stats_vals),
1230+
np.nanmax(impacts_stats_vals),
12311231
)
12321232
kwargs.update(
12331233
{
@@ -1419,6 +1419,8 @@ def write_attribute(group, name, value):
14191419

14201420
def write_dataset(group, name, value):
14211421
"""Write a dataset"""
1422+
if name == "lead_time":
1423+
value = value.astype("timedelta64[ns]").astype("int64")
14221424
group.create_dataset(name, data=value, dtype=_str_type_helper(value))
14231425

14241426
def write_dict(group, name, value):
@@ -1459,7 +1461,6 @@ def write_csr(group, name, value):
14591461

14601462
# Open file in write mode
14611463
with h5py.File(file_path, "w") as file:
1462-
14631464
# Now write all attributes
14641465
# NOTE: Remove leading underscore to write '_tot_value' as regular attribute
14651466
for name, value in self.__dict__.items():
@@ -1600,13 +1601,18 @@ def from_excel(cls, file_name):
16001601
def read_excel(self, *args, **kwargs):
16011602
"""This function is deprecated, use Impact.from_excel instead."""
16021603
LOGGER.warning(
1603-
"The use of Impact.read_excel is deprecated."
1604-
"Use Impact.from_excel instead."
1604+
"The use of Impact.read_excel is deprecated.Use Impact.from_excel instead."
16051605
)
16061606
self.__dict__ = Impact.from_excel(*args, **kwargs).__dict__
16071607

16081608
@classmethod
1609-
def from_hdf5(cls, file_path: Union[str, Path]):
1609+
def from_hdf5(
1610+
cls,
1611+
file_path: Union[str, Path],
1612+
*,
1613+
add_scalar_attrs: Iterable[str] | None = None,
1614+
add_array_attrs: Iterable[str] | None = None,
1615+
):
16101616
"""Create an impact object from an H5 file.
16111617
16121618
This assumes a specific layout of the file. If values are not found in the
@@ -1651,6 +1657,10 @@ def from_hdf5(cls, file_path: Union[str, Path]):
16511657
----------
16521658
file_path : str or Path
16531659
The file path of the file to read.
1660+
add_scalar_attrs : Iterable of str, optional
1661+
Additional scalar attributes to read from file. Defaults to None.
1662+
add_array_attrs : Iterable of str, optional
1663+
Additional array attributes to read from file. Defaults to None.
16541664
16551665
Returns
16561666
-------
@@ -1659,7 +1669,6 @@ def from_hdf5(cls, file_path: Union[str, Path]):
16591669
"""
16601670
kwargs = dict()
16611671
with h5py.File(file_path, "r") as file:
1662-
16631672
# Impact matrix
16641673
if "imp_mat" in file:
16651674
impact_matrix = file["imp_mat"]
@@ -1679,17 +1688,27 @@ def from_hdf5(cls, file_path: Union[str, Path]):
16791688
# Scalar attributes
16801689
scalar_attrs = set(
16811690
("crs", "tot_value", "unit", "aai_agg", "frequency_unit", "haz_type")
1682-
).intersection(file.attrs.keys())
1691+
)
1692+
if add_scalar_attrs is not None:
1693+
scalar_attrs = scalar_attrs.union(add_scalar_attrs)
1694+
scalar_attrs = scalar_attrs.intersection(file.attrs.keys())
16831695
kwargs.update({attr: file.attrs[attr] for attr in scalar_attrs})
16841696

16851697
# Array attributes
16861698
# NOTE: Need [:] to copy array data. Otherwise, it would be a view that is
16871699
# invalidated once we close the file.
16881700
array_attrs = set(
16891701
("event_id", "date", "coord_exp", "eai_exp", "at_event", "frequency")
1690-
).intersection(file.keys())
1702+
)
1703+
if add_array_attrs is not None:
1704+
array_attrs = array_attrs.union(add_array_attrs)
1705+
array_attrs = array_attrs.intersection(file.keys())
16911706
kwargs.update({attr: file[attr][:] for attr in array_attrs})
1692-
1707+
# correct lead_time attribut to timedelta
1708+
if "lead_time" in kwargs:
1709+
kwargs["lead_time"] = np.array(file["lead_time"][:]).astype(
1710+
"timedelta64[ns]"
1711+
)
16931712
# Special handling for 'event_name' because it should be a list of strings
16941713
if "event_name" in file:
16951714
# pylint: disable=no-member
@@ -2196,9 +2215,12 @@ def stack_attribute(attr_name: str) -> np.ndarray:
21962215
imp_mat = sparse.vstack(imp_mats)
21972216

21982217
# Concatenate other attributes
2199-
kwargs = {
2200-
attr: stack_attribute(attr) for attr in ("date", "frequency", "at_event")
2201-
}
2218+
concat_attrs = {
2219+
name.lstrip("_") # Private attributes with getter/setter
2220+
for name, value in first_imp.__dict__.items()
2221+
if isinstance(value, np.ndarray)
2222+
}.difference(("event_id", "coord_exp", "eai_exp", "aai_agg"))
2223+
kwargs = {attr: stack_attribute(attr) for attr in concat_attrs}
22022224

22032225
# Get remaining attributes from first impact object in list
22042226
return cls(

climada/engine/impact_calc.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
from climada import CONFIG
3131
from climada.engine.impact import Impact
32+
from climada.engine.impact_forecast import ImpactForecast
33+
from climada.hazard.forecast import HazardForecast
3234

3335
LOGGER = logging.getLogger(__name__)
3436

@@ -217,7 +219,7 @@ def _return_impact(self, imp_mat_gen, save_mat):
217219
218220
Returns
219221
-------
220-
Impact
222+
Impact or ImpactForecast
221223
Impact Object initialize from the impact matrix
222224
223225
See Also
@@ -230,12 +232,31 @@ def _return_impact(self, imp_mat_gen, save_mat):
230232
at_event, eai_exp, aai_agg = self.risk_metrics(
231233
imp_mat, self.hazard.frequency
232234
)
235+
if isinstance(self.hazard, HazardForecast):
236+
eai_exp = np.full_like(eai_exp, np.nan, dtype=eai_exp.dtype)
237+
aai_agg = np.nan
238+
LOGGER.warning(
239+
"eai_exp and aai_agg are undefined with forecasts. "
240+
"Setting them to NaN arrays."
241+
)
242+
233243
else:
244+
if isinstance(self.hazard, HazardForecast):
245+
raise ValueError(
246+
"Saving impact matrix is required when using HazardForecast."
247+
"Please set save_mat=True."
248+
)
234249
imp_mat = None
235250
at_event, eai_exp, aai_agg = self.stitch_risk_metrics(imp_mat_gen)
236-
return Impact.from_eih(
251+
252+
impact = Impact.from_eih(
237253
self.exposures, self.hazard, at_event, eai_exp, aai_agg, imp_mat
238254
)
255+
if isinstance(self.hazard, HazardForecast):
256+
return ImpactForecast.from_impact(
257+
impact, self.hazard.lead_time, self.hazard.member
258+
)
259+
return impact
239260

240261
def _return_empty(self, save_mat):
241262
"""
@@ -248,21 +269,37 @@ def _return_empty(self, save_mat):
248269
249270
Returns
250271
-------
251-
Impact
272+
Impact or ImpactForecast
252273
Empty impact object with correct array sizes.
253274
"""
254275
at_event = np.zeros(self.n_events)
255-
eai_exp = np.zeros(self.n_exp_pnt)
256-
aai_agg = 0.0
276+
if isinstance(self.hazard, HazardForecast):
277+
eai_exp = np.full(self.n_exp_pnt, np.nan)
278+
aai_agg = np.nan
279+
else:
280+
eai_exp = np.zeros(self.n_exp_pnt)
281+
aai_agg = 0.0
282+
257283
if save_mat:
258284
imp_mat = sparse.csr_matrix(
259285
(self.n_events, self.n_exp_pnt), dtype=np.float64
260286
)
261287
else:
288+
if isinstance(self.hazard, HazardForecast):
289+
raise ValueError(
290+
"Saving impact matrix is required when using HazardForecast. "
291+
"Please set save_mat=True."
292+
)
262293
imp_mat = None
263-
return Impact.from_eih(
294+
295+
impact = Impact.from_eih(
264296
self.exposures, self.hazard, at_event, eai_exp, aai_agg, imp_mat
265297
)
298+
if isinstance(self.hazard, HazardForecast):
299+
return ImpactForecast.from_impact(
300+
impact, self.hazard.lead_time, self.hazard.member
301+
)
302+
return impact
266303

267304
def minimal_exp_gdf(
268305
self, impf_col, assign_centroids, ignore_cover, ignore_deductible

0 commit comments

Comments
 (0)