Skip to content

Commit 0cda1e2

Browse files
committed
adds conftest and how to use file
1 parent 185957a commit 0cda1e2

File tree

2 files changed

+395
-0
lines changed

2 files changed

+395
-0
lines changed

climada/test/conftest.py

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
"""
2+
This file is part of CLIMADA.
3+
4+
Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.
5+
6+
CLIMADA is free software: you can redistribute it and/or modify it under the
7+
terms of the GNU General Public License as published by the Free
8+
Software Foundation, version 3.
9+
10+
CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
11+
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
12+
PARTICULAR PURPOSE. See the GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License along
15+
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.
16+
---
17+
18+
A set of reusable fixtures for testing purpose.
19+
20+
The objective of this file is to provide minimalistic, understandable and consistent
21+
default objects for unit and integration testing.
22+
23+
Values are chosen such that:
24+
- Exposure value of the first points is 0. (First location should always have 0 impacts)
25+
- Category / Group id of all points is 1, except for third point, valued at 2000 (Impacts on that category are always a share of 2000)
26+
- Hazard centroids are the exposure centroids shifted by `HAZARD_JITTER` on both lon and lat.
27+
- There are 4 events, with frequencies == 0.03, 0.01, 0.006, 0.004, 0,
28+
such that impacts for RP250, 100 and 50 and 20 are at_event,
29+
(freq sorted cumulate to 1/250, 1/100, 1/50 and 1/20).
30+
- Hazard intensity is:
31+
* Event 1: zero everywhere (always no impact)
32+
* Event 2: max intensity at first centroid (also always no impact (first centroid is 0))
33+
* Event 3: half max intensity at second centroid (impact == half second centroid)
34+
* Event 4: quarter max intensity everywhere (impact == 1/4 total value)
35+
* Event 5: max intensity everywhere (but zero frequency)
36+
With max intensity set at 100
37+
- Impact function is the "identity function", x intensity is x% damages
38+
- Impact values should be:
39+
* AAI = 18 = 1000*1/2*0.006+(1000+2000+3000+4000+5000)*0.25*0.004
40+
* RP20 = event1 = 0
41+
* RP50 = event2 = 0
42+
* RP100 = event3 = 500 = 1000*1/2
43+
* RP250 = event4 = 3750 = (1000+2000+3000+4000+5000)*0.25
44+
45+
"""
46+
47+
import geopandas as gpd
48+
import numpy as np
49+
import pytest
50+
from scipy.sparse import csr_matrix
51+
from shapely.geometry import Point
52+
53+
from climada.entity import Exposures, ImpactFunc, ImpactFuncSet
54+
from climada.hazard import Centroids, Hazard
55+
56+
# ---------------------------------------------------------------------------
57+
# Coordinate system and metadata
58+
# ---------------------------------------------------------------------------
59+
CRS_WGS84 = "EPSG:4326"
60+
61+
# ---------------------------------------------------------------------------
62+
# Exposure attributes
63+
# ---------------------------------------------------------------------------
64+
EXP_DESC = "Test exposure dataset"
65+
EXPOSURE_REF_YEAR = 2020
66+
EXPOSURE_VALUE_UNIT = "USD"
67+
VALUES = np.array([0, 1000, 2000, 3000, 4000, 5000])
68+
CATEGORIES = np.array([1, 1, 2, 1, 1, 3])
69+
70+
# Exposure coordinates
71+
EXP_LONS = np.array([4, 4.25, 4.5, 4, 4.25, 4.5])
72+
EXP_LATS = np.array([45, 45, 45, 45.25, 45.25, 45.25])
73+
74+
# ---------------------------------------------------------------------------
75+
# Hazard definition
76+
# ---------------------------------------------------------------------------
77+
HAZARD_TYPE = "TEST_HAZARD_TYPE"
78+
HAZARD_UNIT = "TEST_HAZARD_UNIT"
79+
80+
# Hazard centroid positions
81+
HAZ_JITTER = 0.1 # To test centroid matching
82+
HAZ_LONS = EXP_LONS + HAZ_JITTER
83+
HAZ_LATS = EXP_LATS + HAZ_JITTER
84+
85+
# Hazard events
86+
EVENT_IDS = np.array([1, 2, 3, 4, 5])
87+
EVENT_NAMES = ["ev1", "ev2", "ev3", "ev4", "ev5"]
88+
DATES = np.array([1, 2, 3, 4, 5])
89+
90+
# Frequency are choosen so that they cumulate nicely
91+
# to correspond to 250, 100, 50, and 20y return periods (for impacts)
92+
FREQUENCY = np.array([0.03, 0.01, 0.006, 0.004, 0.0])
93+
FREQUENCY_UNIT = "1/year"
94+
95+
# Hazard maximum intensity
96+
# 100 to match 0 to 100% idea
97+
# also in line with linear 1:1 impact function
98+
# for easy mental calculus
99+
HAZARD_MAX_INTENSITY = 100
100+
101+
# ---------------------------------------------------------------------------
102+
# Impact function
103+
# ---------------------------------------------------------------------------
104+
IMPF_ID = 1
105+
IMPF_NAME = "IMPF_1"
106+
107+
# Sanity checks
108+
for const in [VALUES, CATEGORIES, EXP_LONS, EXP_LATS]:
109+
assert len(const) == len(
110+
VALUES
111+
), "VALUES, REGIONS, CATEGORIES, EXP_LONS, EXP_LATS should all have the same lengths."
112+
113+
for const in [EVENT_IDS, EVENT_NAMES, DATES, FREQUENCY]:
114+
assert len(const) == len(
115+
EVENT_IDS
116+
), "EVENT_IDS, EVENT_NAMES, DATES, FREQUENCY should all have the same lengths."
117+
118+
119+
@pytest.fixture(scope="session")
120+
def exposure_values():
121+
return VALUES.copy()
122+
123+
124+
@pytest.fixture(scope="session")
125+
def categories():
126+
return CATEGORIES.copy()
127+
128+
129+
@pytest.fixture(scope="session")
130+
def exposure_geometry():
131+
return [Point(lon, lat) for lon, lat in zip(EXP_LONS, EXP_LATS)]
132+
133+
134+
@pytest.fixture(scope="session")
135+
def exposures_factory(
136+
exposure_values,
137+
exposure_geometry,
138+
):
139+
def _make_exposures(
140+
value_factor=1.0,
141+
ref_year=EXPOSURE_REF_YEAR,
142+
hazard_type=HAZARD_TYPE,
143+
group_id=None,
144+
):
145+
gdf = gpd.GeoDataFrame(
146+
{
147+
"value": exposure_values * value_factor,
148+
f"impf_{hazard_type}": IMPF_ID,
149+
"geometry": exposure_geometry,
150+
},
151+
crs=CRS_WGS84,
152+
)
153+
if group_id is not None:
154+
gdf["group_id"] = group_id
155+
156+
return Exposures(
157+
data=gdf,
158+
description=EXP_DESC,
159+
ref_year=ref_year,
160+
value_unit=EXPOSURE_VALUE_UNIT,
161+
)
162+
163+
return _make_exposures
164+
165+
166+
@pytest.fixture(scope="session")
167+
def exposures(exposures_factory):
168+
return exposures_factory()
169+
170+
171+
@pytest.fixture(scope="session")
172+
def hazard_frequency_factory():
173+
base = FREQUENCY
174+
175+
def _make_frequency(scale=1.0):
176+
return base * scale
177+
178+
return _make_frequency
179+
180+
181+
@pytest.fixture(scope="session")
182+
def hazard_frequency():
183+
return hazard_frequency_factory()
184+
185+
186+
@pytest.fixture(scope="session")
187+
def hazard_intensity_factory():
188+
"""
189+
Intensity matrix designed for analytical expectations:
190+
- Event 1: zero
191+
- Event 2: max intensity at first centroid
192+
- Event 3: half max intensity at second centroid
193+
- Event 4: quarter max intensity everywhere
194+
"""
195+
base = csr_matrix(
196+
[
197+
[0, 0, 0, 0, 0, 0],
198+
[HAZARD_MAX_INTENSITY, 0, 0, 0, 0, 0],
199+
[0, HAZARD_MAX_INTENSITY / 2, 0, 0, 0, 0],
200+
[
201+
HAZARD_MAX_INTENSITY / 4,
202+
HAZARD_MAX_INTENSITY / 4,
203+
HAZARD_MAX_INTENSITY / 4,
204+
HAZARD_MAX_INTENSITY / 4,
205+
HAZARD_MAX_INTENSITY / 4,
206+
HAZARD_MAX_INTENSITY / 4,
207+
],
208+
[
209+
HAZARD_MAX_INTENSITY,
210+
HAZARD_MAX_INTENSITY,
211+
HAZARD_MAX_INTENSITY,
212+
HAZARD_MAX_INTENSITY,
213+
HAZARD_MAX_INTENSITY,
214+
HAZARD_MAX_INTENSITY,
215+
],
216+
]
217+
)
218+
219+
def _make_intensity(scale=1.0):
220+
return base * scale
221+
222+
return _make_intensity
223+
224+
225+
@pytest.fixture(scope="session")
226+
def hazard_intensity_matrix(hazard_intensity_factory):
227+
return hazard_intensity_factory()
228+
229+
230+
@pytest.fixture(scope="session")
231+
def centroids():
232+
return Centroids(lat=HAZ_LATS, lon=HAZ_LONS, crs=CRS_WGS84)
233+
234+
235+
@pytest.fixture(scope="session")
236+
def hazard_factory(
237+
hazard_intensity_factory,
238+
hazard_frequency_factory,
239+
centroids,
240+
):
241+
def _make_hazard(
242+
intensity_scale=1.0,
243+
frequency_scale=1.0,
244+
hazard_type=HAZARD_TYPE,
245+
hazard_unit=HAZARD_UNIT,
246+
):
247+
return Hazard(
248+
haz_type=hazard_type,
249+
units=hazard_unit,
250+
centroids=centroids,
251+
event_id=EVENT_IDS,
252+
event_name=EVENT_NAMES,
253+
date=DATES,
254+
frequency=hazard_frequency_factory(scale=frequency_scale),
255+
frequency_unit=FREQUENCY_UNIT,
256+
intensity=hazard_intensity_factory(scale=intensity_scale),
257+
)
258+
259+
return _make_hazard
260+
261+
262+
@pytest.fixture(scope="session")
263+
def hazard(hazard_factory):
264+
return hazard_factory()
265+
266+
267+
@pytest.fixture(scope="session")
268+
def impf_factory():
269+
def _make_impf(
270+
paa_scale=1.0,
271+
max_intensity=HAZARD_MAX_INTENSITY,
272+
hazard_type=HAZARD_TYPE,
273+
hazard_unit=HAZARD_UNIT,
274+
impf_id=IMPF_ID,
275+
):
276+
return ImpactFunc(
277+
haz_type=hazard_type,
278+
intensity_unit=hazard_unit,
279+
name=IMPF_NAME,
280+
intensity=np.array([0, max_intensity / 2, max_intensity]),
281+
mdd=np.array([0, 0.5, 1]),
282+
paa=np.array([1, 1, 1]) * paa_scale,
283+
id=impf_id,
284+
)
285+
286+
return _make_impf
287+
288+
289+
@pytest.fixture(scope="session")
290+
def linear_impact_function(impf_factory):
291+
return impf_factory()
292+
293+
294+
@pytest.fixture(scope="session")
295+
def impfset_factory(impf_factory):
296+
def _make_impfset(
297+
paa_scale=1.0,
298+
max_intensity=HAZARD_MAX_INTENSITY,
299+
hazard_type=HAZARD_TYPE,
300+
hazard_unit=HAZARD_UNIT,
301+
impf_id=IMPF_ID,
302+
):
303+
return ImpactFuncSet(
304+
[impf_factory(paa_scale, max_intensity, hazard_type, hazard_unit, impf_id)]
305+
)
306+
307+
return _make_impfset
308+
309+
310+
@pytest.fixture(scope="session")
311+
def impfset(impfset_factory):
312+
return impfset_factory()
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""
2+
This file is part of CLIMADA.
3+
4+
Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.
5+
6+
CLIMADA is free software: you can redistribute it and/or modify it under the
7+
terms of the GNU General Public License as published by the Free
8+
Software Foundation, version 3.
9+
10+
CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
11+
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
12+
PARTICULAR PURPOSE. See the GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License along
15+
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.
16+
---
17+
18+
This files shows a few example of how to use the fixtures defined in common_test_fixtures.py
19+
20+
"""
21+
22+
import numpy as np
23+
24+
from climada.engine import ImpactCalc
25+
26+
27+
class TestImpactCalc:
28+
def test_impact(self, exposures, hazard, impfset):
29+
imp = ImpactCalc(exposures, impfset, hazard).impact()
30+
assert imp.aai_agg == (1 / 2 * 1000) * 0.006 + (1 / 4 * 15000) * 0.004
31+
np.testing.assert_array_equal(
32+
imp.eai_exp,
33+
np.array(
34+
[
35+
0.0,
36+
(1000 * 0 * 0.03)
37+
+ (1000 * 0 * 0.01)
38+
+ ((1000 * 1 / 2) * 0.006)
39+
+ ((1000 * 1 / 4) * 0.004)
40+
+ ((1000 * 1) * 0.0),
41+
(2000 * 0 * 0.03)
42+
+ (2000 * 0 * 0.01)
43+
+ ((2000 * 0) * 0.006)
44+
+ ((2000 * 1 / 4) * 0.004)
45+
+ ((2000 * 1) * 0.0),
46+
(3000 * 0 * 0.03)
47+
+ (3000 * 0 * 0.01)
48+
+ ((3000 * 0) * 0.006)
49+
+ ((3000 * 1 / 4) * 0.004)
50+
+ ((3000 * 1) * 0.0),
51+
(4000 * 0 * 0.03)
52+
+ (4000 * 0 * 0.01)
53+
+ ((4000 * 0) * 0.006)
54+
+ ((4000 * 1 / 4) * 0.004)
55+
+ ((4000 * 1) * 0.0),
56+
(5000 * 0 * 0.03)
57+
+ (5000 * 0 * 0.01)
58+
+ ((5000 * 0) * 0.006)
59+
+ ((5000 * 1 / 4) * 0.004)
60+
+ ((5000 * 1) * 0.0),
61+
# (Value * Int * Freq)
62+
]
63+
),
64+
err_msg="eai_exp impacts invalid",
65+
)
66+
np.testing.assert_array_equal(
67+
imp.at_event,
68+
np.array(
69+
[
70+
0.0,
71+
0.0,
72+
1000 * 1 / 2,
73+
(1000 + 2000 + 3000 + 4000 + 5000) * 1 / 4,
74+
(1000 + 2000 + 3000 + 4000 + 5000),
75+
]
76+
),
77+
err_msg="at_event impacts invalid",
78+
)
79+
np.testing.assert_array_equal(
80+
imp.calc_freq_curve([20, 50, 100, 500]).impact,
81+
np.array([0, 0, 500, 3750]),
82+
err_msg="return period impacts invalid",
83+
)

0 commit comments

Comments
 (0)