Skip to content

Commit 2e75462

Browse files
committed
Merge branch 'feature/risk_trajectory' into feature/cb_refactoring
2 parents c36c49f + 6856117 commit 2e75462

11 files changed

Lines changed: 1607 additions & 1002 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ Code freeze date: YYYY-MM-DD
1212

1313
### Added
1414

15+
- `climada.entity.impact_funcs.base.ImpactFunc.__eq__` method
16+
- `climada.entity.impact_funcs.impact_func_set.ImpactFuncSet.__eq__` method
17+
1518
### Changed
1619
- `Hazard.local_exceedance_intensity`, `Hazard.local_return_period` and `Impact.local_exceedance_impact`, `Impact.local_return_period`, using the `climada.util.interpolation` module: New default (no binning), binning on decimals, and faster implementation [#1012](https://github.com/CLIMADA-project/climada_python/pull/1012)
1720
### Fixed

climada/entity/impact_funcs/base.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,7 @@ def __eq__(self, value: object, /) -> bool:
108108
and np.array_equal(self.mdd, value.mdd)
109109
and np.array_equal(self.paa, value.paa)
110110
)
111-
else:
112-
return False
111+
return False
113112

114113
def calc_mdr(self, inten: Union[float, np.ndarray]) -> np.ndarray:
115114
"""Interpolate impact function to a given intensity.

climada/entity/impact_funcs/test/test_base.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,74 @@
2626
from climada.entity.impact_funcs.base import ImpactFunc
2727

2828

29+
class TestEquality(unittest.TestCase):
30+
"""Test equality method"""
31+
32+
def setUp(self):
33+
self.impf1 = ImpactFunc(
34+
haz_type="TC",
35+
id=1,
36+
intensity=np.array([1, 2, 3]),
37+
mdd=np.array([0.1, 0.2, 0.3]),
38+
paa=np.array([0.4, 0.5, 0.6]),
39+
intensity_unit="m/s",
40+
name="Test Impact",
41+
)
42+
self.impf2 = ImpactFunc(
43+
haz_type="TC",
44+
id=1,
45+
intensity=np.array([1, 2, 3]),
46+
mdd=np.array([0.1, 0.2, 0.3]),
47+
paa=np.array([0.4, 0.5, 0.6]),
48+
intensity_unit="m/s",
49+
name="Test Impact",
50+
)
51+
self.impf3 = ImpactFunc(
52+
haz_type="FL",
53+
id=2,
54+
intensity=np.array([4, 5, 6]),
55+
mdd=np.array([0.7, 0.8, 0.9]),
56+
paa=np.array([0.1, 0.2, 0.3]),
57+
intensity_unit="m",
58+
name="Another Impact",
59+
)
60+
61+
def test_reflexivity(self):
62+
self.assertEqual(self.impf1, self.impf1)
63+
64+
def test_symmetry(self):
65+
self.assertEqual(self.impf1, self.impf2)
66+
self.assertEqual(self.impf2, self.impf1)
67+
68+
def test_transitivity(self):
69+
impf4 = ImpactFunc(
70+
haz_type="TC",
71+
id=1,
72+
intensity=np.array([1, 2, 3]),
73+
mdd=np.array([0.1, 0.2, 0.3]),
74+
paa=np.array([0.4, 0.5, 0.6]),
75+
intensity_unit="m/s",
76+
name="Test Impact",
77+
)
78+
self.assertEqual(self.impf1, self.impf2)
79+
self.assertEqual(self.impf2, impf4)
80+
self.assertEqual(self.impf1, impf4)
81+
82+
def test_consistency(self):
83+
self.assertEqual(self.impf1, self.impf2)
84+
self.assertEqual(self.impf1, self.impf2)
85+
86+
def test_comparison_with_none(self):
87+
self.assertNotEqual(self.impf1, None)
88+
89+
def test_different_types(self):
90+
self.assertNotEqual(self.impf1, "Not an ImpactFunc")
91+
92+
def test_inequality(self):
93+
self.assertNotEqual(self.impf1, self.impf3)
94+
self.assertTrue(self.impf1 != self.impf3)
95+
96+
2997
class TestInterpolation(unittest.TestCase):
3098
"""Impact function interpolation test"""
3199

@@ -139,5 +207,8 @@ def test_aux_vars(impf):
139207

140208
# Execute Tests
141209
if __name__ == "__main__":
142-
TESTS = unittest.TestLoader().loadTestsFromTestCase(TestInterpolation)
143-
unittest.TextTestRunner(verbosity=2).run(TESTS)
210+
equality_tests = unittest.TestLoader().loadTestsFromTestCase(TestEquality)
211+
interpolation_tests = unittest.TestLoader().loadTestsFromTestCase(TestInterpolation)
212+
unittest.TextTestRunner(verbosity=2).run(
213+
unittest.TestSuite([equality_tests, interpolation_tests])
214+
)

climada/entity/impact_funcs/test/test_imp_fun_set.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,55 @@ def test_remove_add_pass(self):
288288
self.assertEqual([1], imp_fun.get_ids("TC"))
289289

290290

291+
class TestEquality(unittest.TestCase):
292+
"""Test equality method for ImpactFuncSet"""
293+
294+
def setUp(self):
295+
intensity = np.array([0, 20])
296+
paa = np.array([0, 1])
297+
mdd = np.array([0, 0.5])
298+
299+
fun_1 = ImpactFunc("TC", 3, intensity, mdd, paa)
300+
fun_2 = ImpactFunc("TC", 3, intensity, mdd, paa)
301+
fun_3 = ImpactFunc("TC", 4, intensity + 1, mdd, paa)
302+
303+
self.impact_set1 = ImpactFuncSet([fun_1])
304+
self.impact_set2 = ImpactFuncSet([fun_2])
305+
self.impact_set3 = ImpactFuncSet([fun_3])
306+
self.impact_set4 = ImpactFuncSet([fun_1, fun_3])
307+
308+
def test_reflexivity(self):
309+
self.assertEqual(self.impact_set1, self.impact_set1)
310+
311+
def test_symmetry(self):
312+
self.assertEqual(self.impact_set1, self.impact_set2)
313+
self.assertEqual(self.impact_set2, self.impact_set1)
314+
315+
def test_transitivity(self):
316+
impact_set5 = ImpactFuncSet([self.impact_set1._data["TC"][3]])
317+
self.assertEqual(self.impact_set1, self.impact_set2)
318+
self.assertEqual(self.impact_set2, impact_set5)
319+
self.assertEqual(self.impact_set1, impact_set5)
320+
321+
def test_consistency(self):
322+
self.assertEqual(self.impact_set1, self.impact_set2)
323+
self.assertEqual(self.impact_set1, self.impact_set2)
324+
325+
def test_comparison_with_none(self):
326+
self.assertNotEqual(self.impact_set1, None)
327+
328+
def test_different_types(self):
329+
self.assertNotEqual(self.impact_set1, "Not an ImpactFuncSet")
330+
331+
def test_field_comparison(self):
332+
self.assertNotEqual(self.impact_set1, self.impact_set3)
333+
self.assertNotEqual(self.impact_set1, self.impact_set4)
334+
335+
def test_inequality(self):
336+
self.assertNotEqual(self.impact_set1, self.impact_set3)
337+
self.assertTrue(self.impact_set1 != self.impact_set3)
338+
339+
291340
class TestChecker(unittest.TestCase):
292341
"""Test loading funcions from the ImpactFuncSet class"""
293342

@@ -592,6 +641,7 @@ def test_write_read_pass(self):
592641
# Execute Tests
593642
if __name__ == "__main__":
594643
TESTS = unittest.TestLoader().loadTestsFromTestCase(TestContainer)
644+
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestEquality))
595645
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestChecker))
596646
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestExtend))
597647
TESTS.addTests(unittest.TestLoader().loadTestsFromTestCase(TestReaderExcel))
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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+
19+
This modules implements the Snapshot and SnapshotsCollection classes.
20+
21+
"""
22+
23+
import copy
24+
from abc import ABC, abstractmethod
25+
26+
import numpy as np
27+
28+
from climada.engine.impact import Impact
29+
from climada.engine.impact_calc import ImpactCalc
30+
from climada.trajectories.snapshot import Snapshot
31+
32+
33+
class ImpactComputationStrategy(ABC):
34+
"""Interface for impact computation strategies."""
35+
36+
@abstractmethod
37+
def compute_impacts(
38+
self,
39+
snapshot0: Snapshot,
40+
snapshot1: Snapshot,
41+
risk_transf_attach: float | None,
42+
risk_transf_cover: float | None,
43+
calc_residual: bool,
44+
) -> tuple:
45+
pass
46+
47+
48+
class ImpactCalcComputation(ImpactComputationStrategy):
49+
"""Default impact computation strategy."""
50+
51+
def compute_impacts(
52+
self,
53+
snapshot0: Snapshot,
54+
snapshot1: Snapshot,
55+
risk_transf_attach: float | None,
56+
risk_transf_cover: float | None,
57+
calc_residual: bool = False,
58+
):
59+
impacts = self._calculate_impacts_for_snapshots(snapshot0, snapshot1)
60+
self._apply_risk_transfer(
61+
impacts, risk_transf_attach, risk_transf_cover, calc_residual
62+
)
63+
return impacts
64+
65+
def _calculate_impacts_for_snapshots(
66+
self, snapshot0: Snapshot, snapshot1: Snapshot
67+
):
68+
"""Calculate impacts for the given snapshots and impact function set."""
69+
imp_E0H0 = ImpactCalc(
70+
snapshot0.exposure, snapshot0.impfset, snapshot0.hazard
71+
).impact()
72+
imp_E1H0 = ImpactCalc(
73+
snapshot1.exposure, snapshot1.impfset, snapshot0.hazard
74+
).impact()
75+
imp_E0H1 = ImpactCalc(
76+
snapshot0.exposure, snapshot0.impfset, snapshot1.hazard
77+
).impact()
78+
imp_E1H1 = ImpactCalc(
79+
snapshot1.exposure, snapshot1.impfset, snapshot1.hazard
80+
).impact()
81+
return imp_E0H0, imp_E1H0, imp_E0H1, imp_E1H1
82+
83+
def _apply_risk_transfer(
84+
self,
85+
impacts: tuple[Impact, Impact, Impact, Impact],
86+
risk_transf_attach: float | None,
87+
risk_transf_cover: float | None,
88+
calc_residual: bool,
89+
):
90+
"""Apply risk transfer to the calculated impacts."""
91+
if risk_transf_attach is not None and risk_transf_cover is not None:
92+
for imp in impacts:
93+
imp.imp_mat = self.calculate_residual_or_risk_transfer_impact_matrix(
94+
imp.imp_mat, risk_transf_attach, risk_transf_cover, calc_residual
95+
)
96+
97+
def calculate_residual_or_risk_transfer_impact_matrix(
98+
self, imp_mat, risk_transf_attach, risk_transf_cover, calc_residual
99+
):
100+
"""
101+
Calculate either the residual or the risk transfer impact matrix.
102+
103+
The impact matrix is adjusted based on the total impact for each event.
104+
When calculating the residual impact, the result is the total impact minus
105+
the risk layer. The risk layer is defined as the minimum of the cover and
106+
the maximum of the difference between the total impact and the attachment.
107+
If `calc_residual` is False, the function returns the risk layer matrix
108+
instead of the residual.
109+
110+
Parameters
111+
----------
112+
imp_mat : scipy.sparse.csr_matrix
113+
The original impact matrix to be scaled.
114+
attachment : float, optional
115+
The attachment point for the risk layer.
116+
cover : float, optional
117+
The maximum coverage for the risk layer.
118+
calc_residual : bool, default=True
119+
Determines if the function calculates the residual (if True) or the
120+
risk layer (if False).
121+
122+
Returns
123+
-------
124+
scipy.sparse.csr_matrix
125+
The adjusted impact matrix, either residual or risk transfer.
126+
127+
Example
128+
-------
129+
>>> calc_residual_or_risk_transf_imp_mat(imp_mat, attachment=100, cover=500, calc_residual=True)
130+
Residual impact matrix with applied risk layer adjustments.
131+
"""
132+
if risk_transf_attach and risk_transf_cover:
133+
# Make a copy of the impact matrix
134+
imp_mat = copy.deepcopy(imp_mat)
135+
# Calculate the total impact per event
136+
total_at_event = imp_mat.sum(axis=1).A1
137+
# Risk layer at event
138+
transfer_at_event = np.minimum(
139+
np.maximum(total_at_event - risk_transf_attach, 0), risk_transf_cover
140+
)
141+
# Resiudal impact
142+
residual_at_event = np.maximum(total_at_event - transfer_at_event, 0)
143+
144+
# Calculate either the residual or transfer impact matrix
145+
# Choose the denominator to rescale the impact values
146+
if calc_residual:
147+
# Rescale the impact values
148+
numerator = residual_at_event
149+
else:
150+
# Rescale the impact values
151+
numerator = transfer_at_event
152+
153+
# Rescale the impact values
154+
rescale_impact_values = np.divide(
155+
numerator,
156+
total_at_event,
157+
out=np.zeros_like(numerator, dtype=float),
158+
where=total_at_event != 0,
159+
)
160+
161+
# The multiplication is broadcasted across the columns for each row
162+
result_matrix = imp_mat.multiply(rescale_impact_values[:, np.newaxis])
163+
164+
return result_matrix
165+
166+
else:
167+
168+
return imp_mat

0 commit comments

Comments
 (0)