Skip to content

Commit ac03762

Browse files
chahankChahan Kropfemanuel-schmidpeanutfun
authored
Add polynomial s shape impact function (#878)
--------- Co-authored-by: Chahan Kropf <[email protected]> Co-authored-by: emanuel-schmid <[email protected]> Co-authored-by: Lukas Riedel <[email protected]>
1 parent f0852a4 commit ac03762

File tree

3 files changed

+145
-8
lines changed

3 files changed

+145
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ CLIMADA tutorials. [#872](https://github.com/CLIMADA-project/climada_python/pull
2323

2424
### Added
2525

26+
- Generic s-shaped impact function via `ImpactFunc.from_poly_s_shape` [#878](https://github.com/CLIMADA-project/climada_python/pull/878)
2627
- climada.hazard.centroids.centr.Centroids.get_area_pixel
2728
- climada.hazard.centroids.centr.Centroids.get_dist_coast
2829
- climada.hazard.centroids.centr.Centroids.get_elevation

climada/entity/impact_funcs/base.py

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,9 @@ def from_step_impf(
173173

174174
""" Step function type impact function.
175175
176-
By default, everything is destroyed above the step.
176+
By default, the impact is 100% above the step.
177177
Useful for high resolution modelling.
178178
179-
This method modifies self (climada.entity.impact_funcs instance)
180-
by assigning an id, intensity, mdd and paa to the impact function.
181-
182179
Parameters
183180
----------
184181
intensity: tuple(float, float, float)
@@ -226,12 +223,14 @@ def from_sigmoid_impf(
226223
haz_type: str,
227224
impf_id: int = 1,
228225
**kwargs):
229-
"""Sigmoid type impact function hinging on three parameter.
226+
r"""Sigmoid type impact function hinging on three parameter.
230227
231228
This type of impact function is very flexible for any sort of study,
232229
hazard and resolution. The sigmoid is defined as:
233230
234-
.. math:: f(x) = \\frac{L}{1+exp^{-k(x-x0)}}
231+
.. math::
232+
233+
f(x) = \frac{L}{1+e^{-k(x-x0)}}
235234
236235
For more information: https://en.wikipedia.org/wiki/Logistic_function
237236
@@ -240,7 +239,7 @@ def from_sigmoid_impf(
240239
241240
Parameters
242241
----------
243-
intensity: tuple(float, float, float)
242+
intensity : tuple(float, float, float)
244243
tuple of 3 intensity numbers along np.arange(min, max, step)
245244
L : float
246245
"top" of sigmoid
@@ -273,3 +272,88 @@ def set_sigmoid_impf(self, *args, **kwargs):
273272
LOGGER.warning("The use of ImpactFunc.set_sigmoid_impf is deprecated."
274273
"Use ImpactFunc.from_sigmoid_impf instead.")
275274
self.__dict__ = ImpactFunc.from_sigmoid_impf(*args, **kwargs).__dict__
275+
276+
@classmethod
277+
def from_poly_s_shape(
278+
cls,
279+
intensity: tuple[float, float, int],
280+
threshold: float,
281+
half_point: float,
282+
scale: float,
283+
exponent: float,
284+
haz_type: str,
285+
impf_id: int = 1,
286+
**kwargs):
287+
r"""S-shape polynomial impact function hinging on four parameter.
288+
289+
.. math::
290+
291+
f(I) = \frac{\textrm{luk}(I)^{\textrm{exponent}}}{
292+
1 + \textrm{luk}(I)^{\textrm{exponent}}
293+
}
294+
\cdot \textrm{scale} \\
295+
\textrm{luk}(I) = \frac{\max[I - \textrm{threshold}, 0]}{
296+
\textrm{half_point} - \textrm{threshold}
297+
}
298+
299+
This function is inspired by Emanuel et al. (2011)
300+
https://doi.org/10.1175/WCAS-D-11-00007.1
301+
302+
This method only specifies mdd, and paa = 1 for all intensities.
303+
304+
Parameters
305+
----------
306+
intensity : tuple(float, float, float)
307+
tuple of 3 intensity numbers along np.linsapce(min, max, num)
308+
threshold : float
309+
Intensity threshold below which there is no impact.
310+
In general choose threshold > 0 for computational efficiency
311+
of impacts.
312+
half_point : float
313+
Intensity at which 50% of maximum impact is expected.
314+
If half_point <= threshold, mdd = 0 (and f(I)=0) for all
315+
intensities.
316+
scale : float
317+
Multiplicative factor for the whole function. Typically,
318+
this sets the maximum value at large intensities.
319+
exponent: float
320+
Exponent of the polynomial. Value must be exponent >= 0.
321+
Emanuel et al. (2011) uses the value 3.
322+
haz_type: str
323+
Reference string for the hazard (e.g., 'TC', 'RF', 'WS', ...)
324+
impf_id : int, optional, default=1
325+
Impact function id
326+
kwargs :
327+
keyword arguments passed to ImpactFunc()
328+
329+
Raises
330+
------
331+
ValueError : if exponent <= 0
332+
333+
Returns
334+
-------
335+
impf : climada.entity.impact_funcs.ImpactFunc
336+
s-shaped polynomial impact function
337+
"""
338+
if exponent < 0:
339+
raise ValueError('Exponent value must larger than 0')
340+
341+
inten = np.linspace(*intensity)
342+
343+
if threshold >= half_point:
344+
mdd = np.zeros_like(inten)
345+
else:
346+
luk = (inten - threshold) / (half_point - threshold)
347+
luk[luk < 0] = 0
348+
mdd = scale * luk**exponent / (1 + luk**exponent)
349+
paa = np.ones_like(inten)
350+
351+
impf = cls(
352+
haz_type=haz_type,
353+
id=impf_id,
354+
intensity=inten,
355+
paa=paa,
356+
mdd=mdd,
357+
**kwargs
358+
)
359+
return impf

climada/entity/impact_funcs/test/test_base.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ def test_from_step(self):
4747
self.assertEqual(imp_fun.haz_type, 'TC')
4848
self.assertEqual(imp_fun.id, 2)
4949

50-
5150
def test_from_sigmoid(self):
5251
"""Check default impact function: sigmoid function"""
5352
inten = (0, 100, 5)
@@ -60,6 +59,59 @@ def test_from_sigmoid(self):
6059
self.assertEqual(imp_fun.haz_type, 'RF')
6160
self.assertEqual(imp_fun.id, 2)
6261

62+
def test_from_poly_s_shape(self):
63+
"""Check default impact function: polynomial s-shape"""
64+
65+
haz_type = 'RF'
66+
threshold = 0.2
67+
half_point = 1
68+
scale = 0.8
69+
exponent = 4
70+
impf_id = 2
71+
unit = 'm'
72+
intensity = (0, 5, 5)
73+
74+
def test_aux_vars(impf):
75+
self.assertTrue(np.array_equal(impf.paa, np.ones(5)))
76+
self.assertTrue(np.array_equal(impf.intensity, np.linspace(0, 5, 5)))
77+
self.assertEqual(impf.haz_type, haz_type)
78+
self.assertEqual(impf.id, impf_id)
79+
self.assertEqual(impf.intensity_unit, unit)
80+
81+
impf = ImpactFunc.from_poly_s_shape(
82+
intensity=intensity, threshold=threshold, half_point=half_point, scale=scale,
83+
exponent=exponent, haz_type=haz_type, impf_id=impf_id, intensity_unit=unit
84+
)
85+
# True value can easily be computed with a calculator
86+
correct_mdd = np.array([0, 0.59836395, 0.78845941, 0.79794213, 0.79938319])
87+
np.testing.assert_array_almost_equal(impf.mdd, correct_mdd)
88+
test_aux_vars(impf)
89+
90+
# If threshold > half_point, mdd should all be 0
91+
impf = ImpactFunc.from_poly_s_shape(
92+
intensity=intensity, threshold=half_point*2, half_point=half_point, scale=scale,
93+
exponent=exponent, haz_type=haz_type, impf_id=impf_id, intensity_unit=unit
94+
)
95+
np.testing.assert_array_almost_equal(impf.mdd, np.zeros(5))
96+
test_aux_vars(impf)
97+
98+
# If exponent = 0, mdd should be constant
99+
impf = ImpactFunc.from_poly_s_shape(
100+
intensity=intensity, threshold=threshold, half_point=half_point, scale=scale,
101+
exponent=0, haz_type=haz_type, impf_id=impf_id, intensity_unit=unit
102+
)
103+
np.testing.assert_array_almost_equal(impf.mdd, np.ones(5) * scale / 2)
104+
test_aux_vars(impf)
105+
106+
# If exponent < 0, raise error.
107+
with self.assertRaisesRegex(ValueError, "Exponent value"):
108+
ImpactFunc.from_poly_s_shape(
109+
intensity=intensity, threshold=half_point,
110+
half_point=half_point, scale=scale,
111+
exponent=-1, haz_type=haz_type,
112+
impf_id=impf_id, intensity_unit=unit
113+
)
114+
63115
# Execute Tests
64116
if __name__ == "__main__":
65117
TESTS = unittest.TestLoader().loadTestsFromTestCase(TestInterpolation)

0 commit comments

Comments
 (0)