Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add polynomial s shape impact function #878

Merged
merged 13 commits into from
May 6, 2024
93 changes: 86 additions & 7 deletions climada/entity/impact_funcs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,9 @@

""" Step function type impact function.

By default, everything is destroyed above the step.
By default, the impact is 100% above the step.
Useful for high resolution modelling.

This method modifies self (climada.entity.impact_funcs instance)
by assigning an id, intensity, mdd and paa to the impact function.

Parameters
----------
intensity: tuple(float, float, float)
Expand Down Expand Up @@ -226,12 +223,14 @@
haz_type: str,
impf_id: int = 1,
**kwargs):
"""Sigmoid type impact function hinging on three parameter.
r"""Sigmoid type impact function hinging on three parameter.

This type of impact function is very flexible for any sort of study,
hazard and resolution. The sigmoid is defined as:

.. math:: f(x) = \\frac{L}{1+exp^{-k(x-x0)}}
.. math::

f(x) = \frac{L}{1+e^{-k(x-x0)}}

For more information: https://en.wikipedia.org/wiki/Logistic_function

Expand All @@ -240,7 +239,7 @@

Parameters
----------
intensity: tuple(float, float, float)
intensity : tuple(float, float, float)
tuple of 3 intensity numbers along np.arange(min, max, step)
L : float
"top" of sigmoid
Expand Down Expand Up @@ -273,3 +272,83 @@
LOGGER.warning("The use of ImpactFunc.set_sigmoid_impf is deprecated."
"Use ImpactFunc.from_sigmoid_impf instead.")
self.__dict__ = ImpactFunc.from_sigmoid_impf(*args, **kwargs).__dict__

@classmethod
def from_poly_s_shape(

Check warning on line 277 in climada/entity/impact_funcs/base.py

View check run for this annotation

Jenkins - WCR / Pylint

too-many-arguments

LOW: Too many arguments (8/7)
Raw output
Used when a function or method takes too many arguments.
cls,
intensity: tuple[float, float, int],
threshold: float,
half_point: float,
scale: float,
exponent: float,
haz_type: str,
impf_id: int = 1,
**kwargs):
r"""S-shape polynomial impact function hinging on four parameter.

.. math::

f(I) = \frac{\textrm{luk}(I)^{\textrm{exponent}}}{1 + \textrm{luk}(I)^{\textrm{exponent}}}\cdot \textrm{scale} \\

Check warning on line 291 in climada/entity/impact_funcs/base.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (125/100)
Raw output
Used when a line is longer than a given number of characters.
\textrm{luk}(I) = \frac{\max[I - \textrm{threshold}, 0]}{\textrm{half_point} - \textrm{threshold}}

Check warning on line 292 in climada/entity/impact_funcs/base.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (110/100)
Raw output
Used when a line is longer than a given number of characters.
chahank marked this conversation as resolved.
Show resolved Hide resolved

This function is inspired by Emanuel et al. (2011)
https://doi.org/10.1175/WCAS-D-11-00007.1

This method only specifies mdd, and paa = 1 for all intensities.

Parameters
----------
intensity : tuple(float, float, float)
tuple of 3 intensity numbers along np.linsapce(min, max, num)
threshold : float
Intensity threshold below which there is no impact.
In general choose threshold > 0 for computational efficiency
of impacts.
half_point : float
Intensity at which 50% of maximum impact is expected.
If smaller than threshold, mdd = 0 (and f(I)=0) for all
intensities.
scale : float
Multiplicative factor for the whole function. Typically,
this sets the maximum value at large intensities.
exponent: float
Exponent of the polynomial. Must be larger than 0.
peanutfun marked this conversation as resolved.
Show resolved Hide resolved
Emanuel et al. (2011) uses the value 3.
haz_type: str
Reference string for the hazard (e.g., 'TC', 'RF', 'WS', ...)
impf_id : int, optional, default=1
Impact function id
kwargs :
keyword arguments passed to ImpactFunc()

Raises
------
ValueError : if exponent <= 0

Returns
-------
impf : climada.entity.impact_funcs.ImpactFunc
s-shaped polynomial impact function
"""
if exponent <= 0:
raise ValueError('Exponent value must larger than 0')

inten = np.linspace(*intensity)

if threshold > half_point:
mdd = np.zeros_like(inten)
else:
luk = (inten - threshold) / (half_point - threshold)
luk[luk < 0] = 0
mdd = luk**exponent / (1 + luk**exponent) * scale
chahank marked this conversation as resolved.
Show resolved Hide resolved
paa = np.ones_like(inten)

impf = cls(
haz_type=haz_type,
id=impf_id,
intensity=inten,
paa=paa,
mdd=mdd,
**kwargs
)
return impf
45 changes: 44 additions & 1 deletion climada/entity/impact_funcs/test/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ def test_from_step(self):
self.assertEqual(imp_fun.haz_type, 'TC')
self.assertEqual(imp_fun.id, 2)


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

def test_from_poly_s_shape(self):
"""Check default impact function: polynomial s-shape"""

haz_type = 'RF'
threshold = 0.2
half_point = 1
scale = 0.8
exponent = 4
impf_id = 2
unit = 'm'
intensity = (0, 5, 5)

def test_aux_vars(impf):
self.assertTrue(np.array_equal(impf.paa, np.ones(5)))
self.assertTrue(np.array_equal(impf.intensity, np.linspace(0, 5, 5)))
self.assertEqual(impf.haz_type, haz_type)
self.assertEqual(impf.id, impf_id)
self.assertEqual(impf.intensity_unit, unit)

impf = ImpactFunc.from_poly_s_shape(
intensity=intensity, threshold=threshold, half_point=half_point, scale=scale,
exponent=exponent, haz_type=haz_type, impf_id=impf_id, intensity_unit=unit
)
# True value can easily be computed with a calculator
correct_mdd = np.array([0, 0.59836395, 0.78845941, 0.79794213, 0.79938319])
np.testing.assert_array_almost_equal(impf.mdd, correct_mdd)
test_aux_vars(impf)

# If threshold > half_point, mdd should all be 0
impf = ImpactFunc.from_poly_s_shape(
intensity=intensity, threshold=half_point*2, half_point=half_point, scale=scale,
exponent=exponent, haz_type=haz_type, impf_id=impf_id, intensity_unit=unit
)
np.testing.assert_array_almost_equal(impf.mdd, np.zeros(5))
test_aux_vars(impf)

with self.assertRaises(ValueError):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check actual error

Suggested change
with self.assertRaises(ValueError):
with self.assertRaisesRegex(ValueError, "Exponent value"):

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the correction! Why should we priorize using assertRaisesRegex ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it tests the actual error message. assertRaises would succeed with any ValueError thrown, but you want to test for a specific one here.

Note that this is basically a shortcut for the idiom we were using so far:

with self.assertRaises(Exception) as cm:
    do_stuff()
self.assertIn("Text", str(cm.exception))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes total sense, thanks!

ImpactFunc.from_poly_s_shape(
intensity=intensity, threshold=half_point,
half_point=half_point, scale=scale,
exponent=-1, haz_type=haz_type,
impf_id=impf_id, intensity_unit=unit
)

# Execute Tests
if __name__ == "__main__":
TESTS = unittest.TestLoader().loadTestsFromTestCase(TestInterpolation)
Expand Down
Loading