Skip to content

Commit 2471a23

Browse files
committed
Merge branch 'forecast-class' into hazard_forecast_test_select
2 parents bb51bf3 + 35f9517 commit 2471a23

File tree

6 files changed

+175
-90
lines changed

6 files changed

+175
-90
lines changed

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_forecast.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,73 @@
1818
1919
Define Forecast variant of Impact.
2020
"""
21+
22+
import logging
23+
24+
import numpy as np
25+
26+
from ..util import log_level
27+
from ..util.forecast import Forecast
28+
from .impact import Impact
29+
30+
LOGGER = logging.getLogger(__name__)
31+
32+
33+
class ImpactForecast(Forecast, Impact):
34+
"""An impact object with forecast information"""
35+
36+
def __init__(
37+
self,
38+
*,
39+
lead_time: np.ndarray | None,
40+
member: np.ndarray | None,
41+
**impact_kwargs,
42+
):
43+
"""Initialize the impact forecast.
44+
45+
Parameters
46+
----------
47+
lead_time : np.ndarray, optional
48+
The lead time associated with each event entry
49+
member : np.ndarray, optional
50+
The ensemble member associated with each event entry
51+
impact_kwargs
52+
Keyword-arguments passed to ~:py:class`climada.engine.impact.Impact`.
53+
"""
54+
# TODO: Maybe assert array lengths?
55+
super().__init__(lead_time=lead_time, member=member, **impact_kwargs)
56+
57+
@classmethod
58+
def from_impact(
59+
cls, impact: Impact, lead_time: np.ndarray | None, member: np.ndarray | None
60+
):
61+
"""Create an impact forecast from an impact object and forecast information.
62+
63+
Parameters
64+
----------
65+
impact : climada.engine.impact.Impact
66+
The impact object whose data to use in the forecast object
67+
lead_time : np.ndarray, optional
68+
The lead time associated with each event entry
69+
member : np.ndarray, optional
70+
The ensemble member associated with each event entry
71+
"""
72+
with log_level("WARNING", "climada.engine.impact"):
73+
return cls(
74+
lead_time=lead_time,
75+
member=member,
76+
event_id=impact.event_id,
77+
event_name=impact.event_name,
78+
date=impact.date,
79+
frequency=impact.frequency,
80+
frequency_unit=impact.frequency_unit,
81+
coord_exp=impact.coord_exp,
82+
crs=impact.crs,
83+
eai_exp=impact.eai_exp,
84+
at_event=impact.at_event,
85+
tot_value=impact.tot_value,
86+
aai_agg=impact.aai_agg,
87+
unit=impact.unit,
88+
imp_mat=impact.imp_mat,
89+
haz_type=impact.haz_type,
90+
)

climada/engine/test/test_impact.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,26 +47,30 @@
4747
STR_DT = h5py.special_dtype(vlen=str)
4848

4949

50-
def dummy_impact():
51-
"""Return an impact object for testing"""
52-
return Impact(
53-
event_id=np.arange(6) + 10,
54-
event_name=[0, 1, "two", "three", 30, 31],
55-
date=np.arange(6),
56-
coord_exp=np.array([[1, 2], [1.5, 2.5]]),
57-
crs=DEF_CRS,
58-
eai_exp=np.array([7.2, 7.2]),
59-
at_event=np.array([0, 2, 4, 6, 60, 62]),
60-
frequency=np.array([1 / 6, 1 / 6, 1, 1, 1 / 30, 1 / 30]),
61-
tot_value=7,
62-
aai_agg=14.4,
63-
unit="USD",
64-
frequency_unit="1/month",
65-
imp_mat=sparse.csr_matrix(
50+
def impact_kwargs():
51+
return {
52+
"event_id": np.arange(6) + 10,
53+
"event_name": [0, 1, "two", "three", 30, 31],
54+
"date": np.arange(6),
55+
"coord_exp": np.array([[1, 2], [1.5, 2.5]]),
56+
"crs": DEF_CRS,
57+
"eai_exp": np.array([7.2, 7.2]),
58+
"at_event": np.array([0, 2, 4, 6, 60, 62]),
59+
"frequency": np.array([1 / 6, 1 / 6, 1, 1, 1 / 30, 1 / 30]),
60+
"tot_value": 7,
61+
"aai_agg": 14.4,
62+
"unit": "USD",
63+
"frequency_unit": "1/month",
64+
"imp_mat": sparse.csr_matrix(
6665
np.array([[0, 0], [1, 1], [2, 2], [3, 3], [30, 30], [31, 31]])
6766
),
68-
haz_type="TC",
69-
)
67+
"haz_type": "TC",
68+
}
69+
70+
71+
def dummy_impact():
72+
"""Return an impact object for testing"""
73+
return Impact(**impact_kwargs())
7074

7175

7276
def dummy_impact_yearly():

climada/engine/test/test_impact_forecast.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,57 @@
1818
1919
Tests for Impact Forecast.
2020
"""
21+
22+
import numpy as np
23+
import numpy.testing as npt
24+
import pandas as pd
25+
import pytest
26+
from scipy.sparse import csr_matrix
27+
28+
from climada.engine import Impact, ImpactForecast
29+
30+
from .test_impact import impact_kwargs as imp_kwargs
31+
32+
33+
@pytest.fixture
34+
def impact_kwargs():
35+
return imp_kwargs()
36+
37+
38+
@pytest.fixture
39+
def impact(impact_kwargs):
40+
return Impact(**impact_kwargs)
41+
42+
43+
def assert_impact_kwargs(impact: Impact, **kwargs):
44+
for key, value in kwargs.items():
45+
attr = getattr(impact, key)
46+
if isinstance(value, (np.ndarray, list)):
47+
npt.assert_array_equal(attr, value)
48+
elif isinstance(value, csr_matrix):
49+
npt.assert_array_equal(attr.todense(), value.todense())
50+
else:
51+
assert attr == value
52+
53+
54+
class TestImpactForecastInit:
55+
lead_time = pd.date_range("2000-01-01", "2000-01-02", periods=6).to_numpy()
56+
member = np.arange(6)
57+
58+
def test_impact_forecast_init(self, impact_kwargs):
59+
forecast1 = ImpactForecast(
60+
lead_time=self.lead_time,
61+
member=self.member,
62+
**impact_kwargs,
63+
)
64+
npt.assert_array_equal(forecast1.lead_time, self.lead_time)
65+
npt.assert_array_equal(forecast1.member, self.member)
66+
assert_impact_kwargs(forecast1, **impact_kwargs)
67+
68+
def test_impact_forecast_from_impact(self, impact, impact_kwargs):
69+
forecast = ImpactForecast.from_impact(
70+
impact, lead_time=self.lead_time, member=self.member
71+
)
72+
npt.assert_array_equal(forecast.lead_time, self.lead_time)
73+
npt.assert_array_equal(forecast.member, self.member)
74+
assert_impact_kwargs(forecast, **impact_kwargs)

climada/hazard/forecast.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131

3232
class HazardForecast(Forecast, Hazard):
33+
"""A hazard object with forecast information"""
3334

3435
def __init__(
3536
self,
@@ -42,13 +43,13 @@ def __init__(
4243
4344
Parameters
4445
----------
45-
lead_time : np.ndarray of np.datetime64 or None, optional
46+
lead_time : np.ndarray of np.timedelta64 or None, optional
4647
Forecast lead times. Default is empty array.
4748
member : np.ndarray or None, optional
4849
Ensemble member identifiers as integers. Default is empty array.
4950
**hazard_kwargs
50-
keyword arguments to pass to Hazard.__init__. See Hazard.__init__
51-
docstring for details.
51+
keyword arguments to pass to :py:class:`~climada.hazard.base.Hazard` See
52+
py:meth`~climada.hazard.base.Hazard.__init__` for details.
5253
"""
5354
super().__init__(lead_time=lead_time, member=member, **hazard_kwargs)
5455

@@ -59,8 +60,12 @@ def from_hazard(cls, hazard: Hazard, lead_time: np.ndarray, member: np.ndarray):
5960
6061
Parameters
6162
----------
62-
hazard : Hazard
63+
hazard : climada.hazard.base.Hazard
6364
Hazard object to convert into a HazardForecast.
65+
lead_time : np.ndarray of np.timedelta64 or None, optional
66+
Forecast lead times. Default is empty array.
67+
member : np.ndarray or None, optional
68+
Ensemble member identifiers as integers. Default is empty array.
6469
6570
Returns
6671
-------

climada/hazard/test/test_forecast.py

Lines changed: 19 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import numpy.testing as npt
2424
import pandas as pd
2525
import pytest
26+
from scipy.sparse import csr_matrix
2627

2728
from climada.hazard.base import Hazard
2829
from climada.hazard.forecast import HazardForecast
@@ -37,13 +38,13 @@ def haz_kwargs():
3738

3839

3940
@pytest.fixture
40-
def dummy_hazard(haz_kwargs):
41+
def hazard(haz_kwargs):
4142
return Hazard(**haz_kwargs)
4243

4344

4445
@pytest.fixture
4546
def lead_time():
46-
return pd.date_range("2000-01-01", "2000-01-02", periods=6).to_numpy()
47+
return pd.timedelta_range("1h", periods=6).to_numpy()
4748

4849

4950
@pytest.fixture
@@ -60,80 +61,30 @@ def haz_fc(lead_time, member, haz_kwargs):
6061
)
6162

6263

64+
def assert_hazard_kwargs(hazard: Hazard, **kwargs):
65+
for key, value in kwargs.items():
66+
attr = getattr(hazard, key)
67+
if isinstance(value, (np.ndarray, list)):
68+
npt.assert_array_equal(attr, value)
69+
elif isinstance(value, csr_matrix):
70+
npt.assert_array_equal(attr.todense(), value.todense())
71+
else:
72+
assert attr == value
73+
74+
6375
def test_init_hazard_forecast(haz_fc, member, lead_time, haz_kwargs):
6476
assert isinstance(haz_fc, HazardForecast)
65-
npt.assert_array_equal(
66-
haz_fc.lead_time,
67-
lead_time,
68-
)
77+
npt.assert_array_equal(haz_fc.lead_time, lead_time)
6978
assert haz_fc.lead_time.dtype == lead_time.dtype
7079
npt.assert_array_equal(haz_fc.member, member)
71-
assert haz_fc.haz_type == haz_kwargs["haz_type"]
72-
assert haz_fc.pool == haz_kwargs["pool"]
73-
assert haz_fc.units == haz_kwargs["units"]
74-
assert haz_fc.centroids == haz_kwargs["centroids"]
75-
npt.assert_array_equal(haz_fc.event_id, haz_kwargs["event_id"])
76-
npt.assert_array_equal(haz_fc.frequency, haz_kwargs["frequency"])
77-
assert haz_fc.frequency_unit == haz_kwargs["frequency_unit"]
78-
npt.assert_array_equal(haz_fc.event_name, haz_kwargs["event_name"])
79-
npt.assert_array_equal(haz_fc.date, haz_kwargs["date"])
80-
npt.assert_array_equal(haz_fc.orig, haz_kwargs["orig"])
81-
npt.assert_array_equal(
82-
haz_fc.intensity.todense(), haz_kwargs["intensity"].todense()
83-
)
84-
npt.assert_array_equal(haz_fc.fraction.todense(), haz_kwargs["fraction"].todense())
80+
assert_hazard_kwargs(haz_fc, **haz_kwargs)
8581

8682

87-
def test_from_hazard(lead_time, member, dummy_hazard):
83+
def test_from_hazard(lead_time, member, hazard, haz_kwargs):
8884
haz_fc_from_haz = HazardForecast.from_hazard(
89-
dummy_hazard, lead_time=lead_time, member=member
85+
hazard, lead_time=lead_time, member=member
9086
)
91-
9287
assert isinstance(haz_fc_from_haz, HazardForecast)
9388
npt.assert_array_equal(haz_fc_from_haz.lead_time, lead_time)
9489
npt.assert_array_equal(haz_fc_from_haz.member, member)
95-
assert haz_fc_from_haz.haz_type == dummy_hazard.haz_type
96-
assert haz_fc_from_haz.pool == dummy_hazard.pool
97-
assert haz_fc_from_haz.units == dummy_hazard.units
98-
assert haz_fc_from_haz.centroids == dummy_hazard.centroids
99-
npt.assert_array_equal(haz_fc_from_haz.event_id, dummy_hazard.event_id)
100-
npt.assert_array_equal(haz_fc_from_haz.frequency, dummy_hazard.frequency)
101-
assert haz_fc_from_haz.frequency_unit == dummy_hazard.frequency_unit
102-
npt.assert_array_equal(haz_fc_from_haz.event_name, dummy_hazard.event_name)
103-
npt.assert_array_equal(haz_fc_from_haz.date, dummy_hazard.date)
104-
npt.assert_array_equal(haz_fc_from_haz.orig, dummy_hazard.orig)
105-
npt.assert_array_equal(
106-
haz_fc_from_haz.intensity.todense(), dummy_hazard.intensity.todense()
107-
)
108-
npt.assert_array_equal(
109-
haz_fc_from_haz.fraction.todense(), dummy_hazard.fraction.todense()
110-
)
111-
112-
113-
def test_hazard_forecast_select(haz_fc, lead_time, member):
114-
"""Check if Hazard.select works on the derived class"""
115-
haz_fc_select = haz_fc.select(event_id=[4, 1])
116-
# NOTE: Events keep their original order
117-
npt.assert_array_equal(haz_fc_select.event_id, haz_fc.event_id[np.array([3, 0])])
118-
npt.assert_array_equal(haz_fc_select.member, member[np.array([3, 0])])
119-
npt.assert_array_equal(haz_fc_select.lead_time, lead_time[np.array([3, 0])])
120-
121-
122-
@pytest.fixture
123-
def hazard():
124-
return Hazard()
125-
126-
127-
def test_empty_hazard(hazard):
128-
assert hazard.size == 0
129-
assert hazard.haz_type == ""
130-
131-
132-
class TestSomething:
133-
134-
@pytest.fixture(autouse=True)
135-
def haz_type(self, hazard):
136-
hazard.haz_type = "foo"
137-
138-
def test_haz_type(self, hazard):
139-
assert hazard.haz_type == "foo"
90+
assert_hazard_kwargs(haz_fc_from_haz, **haz_kwargs)

0 commit comments

Comments
 (0)