Skip to content

Commit bf3dd58

Browse files
committed
Adds conftest, makes test more tractable
1 parent 5a04b1c commit bf3dd58

2 files changed

Lines changed: 366 additions & 158 deletions

File tree

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
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+
21+
import geopandas as gpd
22+
import numpy as np
23+
import pytest
24+
from scipy.sparse import csr_matrix
25+
from shapely.geometry import Point
26+
27+
from climada.entity import Exposures, ImpactFunc, ImpactFuncSet
28+
from climada.hazard import Centroids, Hazard
29+
from climada.trajectories import Snapshot
30+
31+
# ---------------------------------------------------------------------------
32+
# Coordinate system and metadata
33+
# ---------------------------------------------------------------------------
34+
CRS_WGS84 = "EPSG:4326"
35+
36+
# ---------------------------------------------------------------------------
37+
# Exposure attributes
38+
# ---------------------------------------------------------------------------
39+
EXP_DESC = "Test exposure dataset"
40+
EXPOSURE_REF_YEAR = 2020
41+
EXPOSURE_VALUE_UNIT = "USD"
42+
VALUES = np.array([0, 1000, 2000, 3000, 4000, 5000])
43+
CATEGORIES = np.array([1, 1, 2, 1, 1, 3])
44+
45+
# Exposure coordinates
46+
EXP_LONS = np.array([4, 4.25, 4.5, 4, 4.25, 4.5])
47+
EXP_LATS = np.array([45, 45, 45, 45.25, 45.25, 45.25])
48+
49+
# ---------------------------------------------------------------------------
50+
# Hazard definition
51+
# ---------------------------------------------------------------------------
52+
HAZARD_TYPE = "TEST_HAZARD_TYPE"
53+
HAZARD_UNIT = "TEST_HAZARD_UNIT"
54+
55+
# Hazard centroid positions
56+
HAZ_JITTER = 0.1 # To test centroid matching
57+
HAZ_LONS = EXP_LONS + HAZ_JITTER
58+
HAZ_LATS = EXP_LATS + HAZ_JITTER
59+
60+
# Hazard events
61+
EVENT_IDS = np.array([1, 2, 3, 4, 5])
62+
EVENT_NAMES = ["ev1", "ev2", "ev3", "ev4", "ev5"]
63+
DATES = np.array([1, 2, 3, 4, 5])
64+
65+
# Frequency are choosen so that they cumulate nicely
66+
# to correspond to 250, 100, 50, and 20y return periods (for impacts)
67+
FREQUENCY = np.array([0.03, 0.01, 0.006, 0.004, 0.0])
68+
FREQUENCY_UNIT = "1/year"
69+
70+
# Hazard maximum intensity
71+
# 100 to match 0 to 100% idea
72+
# also in line with linear 1:1 impact function
73+
# for easy mental calculus
74+
HAZARD_MAX_INTENSITY = 100
75+
76+
# ---------------------------------------------------------------------------
77+
# Impact function
78+
# ---------------------------------------------------------------------------
79+
IMPF_ID = 1
80+
IMPF_NAME = "IMPF_1"
81+
82+
# Sanity checks
83+
for const in [VALUES, CATEGORIES, EXP_LONS, EXP_LATS]:
84+
assert len(const) == len(
85+
VALUES
86+
), "VALUES, REGIONS, CATEGORIES, EXP_LONS, EXP_LATS should all have the same lengths."
87+
88+
for const in [EVENT_IDS, EVENT_NAMES, DATES, FREQUENCY]:
89+
assert len(const) == len(
90+
EVENT_IDS
91+
), "EVENT_IDS, EVENT_NAMES, DATES, FREQUENCY should all have the same lengths."
92+
93+
94+
@pytest.fixture(scope="session")
95+
def exposure_values():
96+
return VALUES.copy()
97+
98+
99+
@pytest.fixture(scope="session")
100+
def categories():
101+
return CATEGORIES.copy()
102+
103+
104+
@pytest.fixture(scope="session")
105+
def exposure_geometry():
106+
return [Point(lon, lat) for lon, lat in zip(EXP_LONS, EXP_LATS)]
107+
108+
109+
@pytest.fixture(scope="session")
110+
def exposures_factory(
111+
exposure_values,
112+
exposure_geometry,
113+
):
114+
def _make_exposures(
115+
value_factor=1.0,
116+
ref_year=EXPOSURE_REF_YEAR,
117+
hazard_type=HAZARD_TYPE,
118+
group_id=None,
119+
):
120+
gdf = gpd.GeoDataFrame(
121+
{
122+
"value": exposure_values * value_factor,
123+
f"impf_{hazard_type}": IMPF_ID,
124+
"geometry": exposure_geometry,
125+
},
126+
crs=CRS_WGS84,
127+
)
128+
if group_id is not None:
129+
gdf["group_id"] = group_id
130+
131+
return Exposures(
132+
data=gdf,
133+
description=EXP_DESC,
134+
ref_year=ref_year,
135+
value_unit=EXPOSURE_VALUE_UNIT,
136+
)
137+
138+
return _make_exposures
139+
140+
141+
@pytest.fixture(scope="session")
142+
def exposures(exposures_factory):
143+
return exposures_factory()
144+
145+
146+
@pytest.fixture(scope="session")
147+
def hazard_frequency_factory():
148+
base = FREQUENCY
149+
150+
def _make_frequency(scale=1.0):
151+
return base * scale
152+
153+
return _make_frequency
154+
155+
156+
@pytest.fixture(scope="session")
157+
def hazard_frequency():
158+
return hazard_frequency_factory()
159+
160+
161+
@pytest.fixture(scope="session")
162+
def hazard_intensity_factory():
163+
"""
164+
Intensity matrix designed for analytical expectations:
165+
- Event 1: zero
166+
- Event 2: max intensity at first centroid
167+
- Event 3: half max intensity at second centroid
168+
- Event 4: quarter max intensity everywhere
169+
"""
170+
base = csr_matrix(
171+
[
172+
[0, 0, 0, 0, 0, 0],
173+
[HAZARD_MAX_INTENSITY, 0, 0, 0, 0, 0],
174+
[0, HAZARD_MAX_INTENSITY / 2, 0, 0, 0, 0],
175+
[
176+
HAZARD_MAX_INTENSITY / 4,
177+
HAZARD_MAX_INTENSITY / 4,
178+
HAZARD_MAX_INTENSITY / 4,
179+
HAZARD_MAX_INTENSITY / 4,
180+
HAZARD_MAX_INTENSITY / 4,
181+
HAZARD_MAX_INTENSITY / 4,
182+
],
183+
[
184+
HAZARD_MAX_INTENSITY,
185+
HAZARD_MAX_INTENSITY,
186+
HAZARD_MAX_INTENSITY,
187+
HAZARD_MAX_INTENSITY,
188+
HAZARD_MAX_INTENSITY,
189+
HAZARD_MAX_INTENSITY,
190+
],
191+
]
192+
)
193+
194+
def _make_intensity(scale=1.0):
195+
return base * scale
196+
197+
return _make_intensity
198+
199+
200+
@pytest.fixture(scope="session")
201+
def hazard_intensity_matrix(hazard_intensity_factory):
202+
return hazard_intensity_factory()
203+
204+
205+
@pytest.fixture(scope="session")
206+
def centroids():
207+
return Centroids(lat=HAZ_LATS, lon=HAZ_LONS, crs=CRS_WGS84)
208+
209+
210+
@pytest.fixture(scope="session")
211+
def hazard_factory(
212+
hazard_intensity_factory,
213+
hazard_frequency_factory,
214+
centroids,
215+
):
216+
def _make_hazard(
217+
intensity_scale=1.0,
218+
frequency_scale=1.0,
219+
hazard_type=HAZARD_TYPE,
220+
hazard_unit=HAZARD_UNIT,
221+
):
222+
return Hazard(
223+
haz_type=hazard_type,
224+
units=hazard_unit,
225+
centroids=centroids,
226+
event_id=EVENT_IDS,
227+
event_name=EVENT_NAMES,
228+
date=DATES,
229+
frequency=hazard_frequency_factory(scale=frequency_scale),
230+
frequency_unit=FREQUENCY_UNIT,
231+
intensity=hazard_intensity_factory(scale=intensity_scale),
232+
)
233+
234+
return _make_hazard
235+
236+
237+
@pytest.fixture(scope="session")
238+
def hazard(hazard_factory):
239+
return hazard_factory()
240+
241+
242+
@pytest.fixture(scope="session")
243+
def impf_factory():
244+
def _make_impf(
245+
paa_scale=1.0,
246+
max_intensity=HAZARD_MAX_INTENSITY,
247+
hazard_type=HAZARD_TYPE,
248+
hazard_unit=HAZARD_UNIT,
249+
impf_id=IMPF_ID,
250+
):
251+
return ImpactFunc(
252+
haz_type=hazard_type,
253+
intensity_unit=hazard_unit,
254+
name=IMPF_NAME,
255+
intensity=np.array([0, max_intensity / 2, max_intensity]),
256+
mdd=np.array([0, 0.5, 1]),
257+
paa=np.array([1, 1, 1]) * paa_scale,
258+
id=impf_id,
259+
)
260+
261+
return _make_impf
262+
263+
264+
@pytest.fixture(scope="session")
265+
def linear_impact_function(impf_factory):
266+
return impf_factory()
267+
268+
269+
@pytest.fixture(scope="session")
270+
def impfset_factory(impf_factory):
271+
def _make_impfset(
272+
paa_scale=1.0,
273+
max_intensity=HAZARD_MAX_INTENSITY,
274+
hazard_type=HAZARD_TYPE,
275+
hazard_unit=HAZARD_UNIT,
276+
impf_id=IMPF_ID,
277+
):
278+
return ImpactFuncSet(
279+
[impf_factory(paa_scale, max_intensity, hazard_type, hazard_unit, impf_id)]
280+
)
281+
282+
return _make_impfset
283+
284+
285+
@pytest.fixture(scope="session")
286+
def impfset(impfset_factory):
287+
return impfset_factory()
288+
289+
290+
@pytest.fixture(scope="session")
291+
def snapshot_factory(
292+
exposures_factory,
293+
hazard_factory,
294+
impfset_factory,
295+
):
296+
"""
297+
Factory for Snapshot objects.
298+
299+
Allows controlled construction of baseline / future / counterfactual
300+
scenarios by scaling exposure values, hazard intensity, and impact function.
301+
"""
302+
303+
def _make_snapshot(
304+
*,
305+
date=EXPOSURE_REF_YEAR,
306+
exposure_value_factor=1.0,
307+
hazard_intensity_factor=1.0,
308+
hazard_frequency_factor=1.0,
309+
paa_scale=1.0,
310+
group_id=None,
311+
):
312+
exposures = exposures_factory(
313+
value_factor=exposure_value_factor, ref_year=date, group_id=group_id
314+
)
315+
316+
hazard = hazard_factory(
317+
intensity_scale=hazard_intensity_factor,
318+
frequency_scale=hazard_frequency_factor,
319+
)
320+
321+
impfset = impfset_factory(
322+
paa_scale=paa_scale,
323+
)
324+
325+
return Snapshot(
326+
exposure=exposures,
327+
hazard=hazard,
328+
impfset=impfset,
329+
date=str(date),
330+
)
331+
332+
return _make_snapshot
333+
334+
335+
@pytest.fixture(scope="session")
336+
def snapshot_base(snapshot_factory):
337+
return snapshot_factory()
338+
339+
340+
@pytest.fixture(scope="session")
341+
def snapshot_future(snapshot_factory):
342+
return snapshot_factory(
343+
date=2040,
344+
exposure_value_factor=2.0,
345+
hazard_intensity_factor=2.0,
346+
)

0 commit comments

Comments
 (0)