Skip to content

Integrates risk trajectories within climada #1037

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

Open
wants to merge 57 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
3692813
Implements equality methods for impf and impfset
spjuhel Mar 18, 2025
1606896
adds a few tests
spjuhel Mar 18, 2025
a8b45c6
Merge remote-tracking branch 'origin/develop' into feature/eq_methods…
spjuhel Mar 18, 2025
3776228
updates changelog
spjuhel Mar 18, 2025
f6d9feb
Improves dict comparison
spjuhel Mar 24, 2025
222644d
Applies suggestion from Lukas (1)
spjuhel Mar 24, 2025
29ecee4
init commit
spjuhel Apr 2, 2025
d7c0f23
Pushes tutorial notebook
spjuhel Apr 2, 2025
31ef976
Working tutorial
spjuhel Apr 2, 2025
10ca243
fixes a typo from mass edit
spjuhel Apr 2, 2025
389787e
Refactor to a more object-oriented design
spjuhel Apr 15, 2025
4ea685d
fixes some bugs
spjuhel Apr 15, 2025
0d6335b
fixses some linter issues
spjuhel Apr 16, 2025
31ea96d
feat(trajectory): interpolation and impactcalc files, eai_gdf metric
spjuhel Apr 17, 2025
3f13c2e
impact func __eq__
spjuhel Apr 24, 2025
519c9af
feat(test): adds test for ImpactFuncSet
spjuhel Apr 24, 2025
adfc7e2
fix(format): removes trailing whitespace
spjuhel Apr 24, 2025
6856117
Merge branch 'feature/eq_methods_for_impf' into feature/risk_trajectory
spjuhel Apr 24, 2025
9c358ca
fix: npv arg not considered in .all_risk_metrics()
spjuhel Apr 24, 2025
b7bf68b
refactor: enforce kwargs in RiskTrajectory init
spjuhel Apr 24, 2025
b75ab91
refactor: adds consistency between attribute
spjuhel Apr 24, 2025
e12b2c4
refactor(risktraj): lays better foundations for MeasureAppraiser
spjuhel Apr 25, 2025
8666197
minor additions, refactors and fixes
spjuhel May 7, 2025
9711379
format,doc: cleans up code, adds docstrings
spjuhel May 14, 2025
9aec92f
test: adds lots of unittests, and placeholders
spjuhel May 14, 2025
7387165
feature(Traj): fixes metrics generic and removes storing values
spjuhel May 19, 2025
20befdf
updates tutorial
spjuhel May 19, 2025
e06a3e7
doc(risk_traj): adds some docstrings
spjuhel May 21, 2025
924bb35
doc(module desc): improves top file desc
spjuhel May 21, 2025
6595e13
fix(log) : Removes endless logs
spjuhel May 21, 2025
94bd00e
fix(risk_traj): Fixes problems with >2 snapshots
spjuhel May 21, 2025
4921528
doc(notebook): adds disclaimers
spjuhel May 21, 2025
02244cb
fix(CalcRiskPeriod): fixes problem with undefined group_id
spjuhel May 22, 2025
b0a9f6d
fix(calcriskperiod): further fix for no group defined
spjuhel May 22, 2025
06afe2f
fix(groups): still fixing groups
spjuhel May 22, 2025
eceeba0
fix(groups): final fix (on the correct branch)
spjuhel May 22, 2025
a7dd0f7
Applies suggestion from Lucas (j/n)
spjuhel May 26, 2025
7989c71
Applies suggestion from Lucas (j/n)
spjuhel May 26, 2025
399572e
Applies suggestion from Lucas (j/n)
spjuhel May 26, 2025
3db32ca
Applies suggestion from Lucas (j/n)
spjuhel May 26, 2025
d2b0f6b
refactor(impact strat): Clarifies/Simplified computation
spjuhel Jun 5, 2025
3f0dfc6
feat(interpolation): Vastly improves interpolation of metrics
spjuhel Jun 5, 2025
d24a5b8
test(naming): updates names
spjuhel Jun 5, 2025
304a641
Squashed commit of the following:
spjuhel Jun 5, 2025
4ef643f
fix(plot, groups): improves plot, fixes group in eai
spjuhel Jun 23, 2025
bc38525
Merge remote-tracking branch 'origin/develop' into feature/risk_traje…
spjuhel Jun 23, 2025
33cfff4
fix(RiskPCalc): sorts the snapshots
spjuhel Jun 24, 2025
dd21493
doc(risk transfer): Improves clarity of risk transfer mechanism
spjuhel Jun 24, 2025
f4db1d0
format(docstring)
spjuhel Jun 24, 2025
c6e147e
test: fix for coverage
spjuhel Jul 1, 2025
25d0d0a
fix,doc(interpolation): minor improvements and fixes
spjuhel Jul 1, 2025
9634bd9
fix: minor fixes from testing
spjuhel Jul 1, 2025
d4af1a7
test: Updates test (98pct coverage o7)
spjuhel Jul 1, 2025
7e8df85
fix(group id): fix categories problem
spjuhel Jul 1, 2025
1bae9ad
still fixing groups
spjuhel Jul 1, 2025
b946f9b
group fixing
spjuhel Jul 1, 2025
622c8f6
fix(tests): fix tests with groups_id attr
spjuhel Jul 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions climada/test/test_trajectories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# 100% Coverage goal:
## Integration test for risk period with periods and freq defined
18 changes: 18 additions & 0 deletions climada/trajectories/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
This file is part of CLIMADA.

Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.

CLIMADA is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free
Software Foundation, version 3.

CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.

---
"""
163 changes: 163 additions & 0 deletions climada/trajectories/impact_calc_strat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""
This file is part of CLIMADA.

Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.

CLIMADA is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free
Software Foundation, version 3.

CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.

---

This modules implements the Snapshot and SnapshotsCollection classes.

"""

import copy
from abc import ABC, abstractmethod

import numpy as np

from climada.engine.impact import Impact
from climada.engine.impact_calc import ImpactCalc
from climada.trajectories.snapshot import Snapshot


class ImpactComputationStrategy(ABC):

Check warning on line 33 in climada/trajectories/impact_calc_strat.py

View check run for this annotation

Jenkins - WCR / Pylint

too-few-public-methods

LOW: Too few public methods (1/2)
Raw output
Used when class has too few public methods, so be sure it's really worth it.
"""Interface for impact computation strategies."""

@abstractmethod
def compute_impacts(

Check warning on line 37 in climada/trajectories/impact_calc_strat.py

View check run for this annotation

Jenkins - WCR / Pylint

too-many-positional-arguments

LOW: Too many positional arguments (7/5)
Raw output
no description found
self,
snapshot0: Snapshot,
snapshot1: Snapshot,
future: tuple[int, int, int],
risk_transf_attach: float | None,
risk_transf_cover: float | None,
calc_residual: bool,
) -> Impact:
"""Method used to compute impact from the snapshots."""


class ImpactCalcComputation(ImpactComputationStrategy):
"""Default impact computation strategy."""

def compute_impacts(

Check warning on line 52 in climada/trajectories/impact_calc_strat.py

View check run for this annotation

Jenkins - WCR / Pylint

too-many-positional-arguments

LOW: Too many positional arguments (7/5)
Raw output
no description found
self,
snapshot0: Snapshot,
snapshot1: Snapshot,
future: tuple[int, int, int],
risk_transf_attach: float | None,
risk_transf_cover: float | None,
calc_residual: bool = False,
):
impact = self.compute_impacts_pre_transfer(snapshot0, snapshot1, future)
self._apply_risk_transfer(
impact, risk_transf_attach, risk_transf_cover, calc_residual
)
return impact

def compute_impacts_pre_transfer(

Check warning on line 67 in climada/trajectories/impact_calc_strat.py

View check run for this annotation

Jenkins - WCR / Pylint

missing-function-docstring

LOW: Missing function or method docstring
Raw output
no description found
self,
snapshot0: Snapshot,
snapshot1: Snapshot,
future: tuple[int, int, int],
) -> Impact:
exp = snapshot1.exposure if future[0] else snapshot0.exposure
haz = snapshot1.hazard if future[1] else snapshot0.hazard
vul = snapshot1.impfset if future[2] else snapshot0.impfset
return ImpactCalc(exposures=exp, impfset=vul, hazard=haz).impact()

def _apply_risk_transfer(
self,
impact: Impact,
risk_transf_attach: float | None,
risk_transf_cover: float | None,
calc_residual: bool,
):
"""Apply risk transfer to the calculated impacts."""
if risk_transf_attach is not None and risk_transf_cover is not None:
Copy link
Member

Choose a reason for hiding this comment

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

I think canonically in the impactcalc class it is fine if either is defined. Any reason to require both here?

impact.imp_mat = self.calculate_residual_or_risk_transfer_impact_matrix(
impact.imp_mat, risk_transf_attach, risk_transf_cover, calc_residual
)

def calculate_residual_or_risk_transfer_impact_matrix(

Check warning on line 91 in climada/trajectories/impact_calc_strat.py

View check run for this annotation

Jenkins - WCR / Pylint

invalid-name

LOW: Method name "calculate_residual_or_risk_transfer_impact_matrix" doesn't conform to '(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$' pattern
Raw output
Used when the name doesn't match the regular expression associated to its type(constant, variable, class...).
self, imp_mat, attachement, cover, calc_residual
):
"""Calculate either the residual or the risk transfer impact matrix
from a global risk transfer mechanism.

To compute the transfered risk, the function first computes for each event,
the (positive) difference between their total impact and `attachment`.
The transfered risk for each event is then defined as the minimum between
this value and `cover`. The residual impact is the total impact minus
the transfered risk.
The impact matrix is then adjusted by multiply impact per centroids
by the ratio between residual risk and total impact for each event.
As such, the risk transfer is shared among all impacted exposure points
and equaly distributed.

If `calc_residual` is False, the function returns the transfered risk
at each points instead of the residual risk by using the ratio between
transfered risk and total impact instead.

Parameters
----------
imp_mat : scipy.sparse.csr_matrix
The original impact matrix to be scaled.
attachment : float
The attachment point for the risk layer.
cover : float
The maximum coverage for the risk layer.
calc_residual : bool, default=True
Determines if the function calculates the residual (if True) or the
risk layer (if False).

Returns
-------
scipy.sparse.csr_matrix
The adjusted impact matrix, either residual or risk transfer.

Warnings
--------

This transfer capability is different and not exclusive with the one
implemented in ImpactCalc, which is defined at the centroid level.
The mechanism here corresponds to a global cover applied to the whole
region studied.

"""
imp_mat = copy.deepcopy(imp_mat)
# Calculate the total impact per event
total_at_event = imp_mat.sum(axis=1).A1
# Risk layer at event
transfer_at_event = np.minimum(
np.maximum(total_at_event - attachement, 0), cover
)
residual_at_event = np.maximum(total_at_event - transfer_at_event, 0)

# Calculate either the residual or transfer impact matrix
# Choose the denominator to rescale the impact values
if calc_residual:
numerator = residual_at_event
else:
numerator = transfer_at_event

rescale_impact_values = np.divide(
numerator,
total_at_event,
out=np.zeros_like(numerator, dtype=float),
where=total_at_event != 0,
)

# The multiplication is broadcasted across the columns for each row
result_matrix = imp_mat.multiply(rescale_impact_values[:, np.newaxis])

return result_matrix
177 changes: 177 additions & 0 deletions climada/trajectories/interpolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
This file is part of CLIMADA.

Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.

CLIMADA is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free
Software Foundation, version 3.

CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.

---

This modules implements different sparce matrices interpolation approaches.

"""

import logging
from abc import ABC
from typing import Callable

import numpy as np

LOGGER = logging.getLogger(__name__)


def linear_interp_imp_mat(mat_start, mat_end, interpolation_range) -> list:
"""Linearly interpolates between two impact matrices over an interpolation range.

Returns a list of `interpolation_range` matrices linearly interpolated between
`mat_start` and `mat_end`.
"""
res = []
for point in range(interpolation_range):
ratio = point / (interpolation_range - 1)
mat_interpolated = mat_start + ratio * (mat_end - mat_start)
res.append(mat_interpolated)
return res


def exponential_interp_imp_mat(mat_start, mat_end, interpolation_range, rate) -> list:
"""Exponentially interpolates between two impact matrices over an interpolation range with a growth rate `rate`.

Check warning on line 47 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (116/100)
Raw output
Used when a line is longer than a given number of characters.

Returns a list of `interpolation_range` matrices exponentially (with growth rate `rate`) interpolated between

Check warning on line 49 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (113/100)
Raw output
Used when a line is longer than a given number of characters.
`mat_start` and `mat_end`.
"""
# Convert matrices to logarithmic domain
if rate <= 0:
raise ValueError("Rate for exponential interpolation must be positive")

Check warning on line 54 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Code Coverage

Not covered line

Line 54 is not covered by tests

mat_start = mat_start.copy()
mat_end = mat_end.copy()
mat_start.data = np.log(mat_start.data + np.finfo(float).eps) / np.log(rate)
mat_end.data = np.log(mat_end.data + np.finfo(float).eps) / np.log(rate)

# Perform linear interpolation in the logarithmic domain
res = []
for point in range(interpolation_range):
ratio = point / (interpolation_range - 1)
mat_interpolated = mat_start * (1 - ratio) + ratio * mat_end
mat_interpolated.data = np.exp(mat_interpolated.data * np.log(rate))
res.append(mat_interpolated)
return res


def linear_interp_arrays(arr_start, arr_end):
"""Perform linear interpolation between two arrays of `n` dates of one or multiple scalar metrics.

Check warning on line 72 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (102/100)
Raw output
Used when a line is longer than a given number of characters.

Returns a `n` sized arrays where the values linearly change from `arr_start` to `arr_end` over the `n` dates.

Check warning on line 74 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (113/100)
Raw output
Used when a line is longer than a given number of characters.
"""
if arr_start.shape != arr_end.shape:
raise ValueError(
f"Cannot interpolate arrays of different shapes: {arr_start.shape} and {arr_end.shape}."
)
interpolation_range = arr_start.shape[0]
prop1 = np.linspace(0, 1, interpolation_range)
prop0 = 1 - prop1
if arr_start.ndim > 1:
prop0, prop1 = prop0.reshape(-1, 1), prop1.reshape(-1, 1)

return np.multiply(arr_start, prop0) + np.multiply(arr_end, prop1)


def exponential_interp_arrays(arr_start, arr_end, rate):
"""Perform exponential interpolation between two arrays of `n` dates of one or multiple scalar metrics.

Check warning on line 90 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (107/100)
Raw output
Used when a line is longer than a given number of characters.

Returns a `n` sized arrays where the values exponentially change from `arr_start` to `arr_end` over the `n` dates.

Check warning on line 92 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (118/100)
Raw output
Used when a line is longer than a given number of characters.
"""

if rate <= 0:
raise ValueError("Rate for exponential interpolation must be positive")

if arr_start.shape != arr_end.shape:
raise ValueError(
f"Cannot interpolate arrays of different shapes: {arr_start.shape} and {arr_end.shape}."
)
interpolation_range = arr_start.shape[0]

prop1 = np.linspace(0, 1, interpolation_range)
prop0 = 1 - prop1
if arr_start.ndim > 1:
prop0, prop1 = prop0.reshape(-1, 1), prop1.reshape(-1, 1)

return np.exp(
(
np.multiply(np.log(arr_start + np.finfo(float).eps) / np.log(rate), prop0)
+ np.multiply(np.log(arr_end + np.finfo(float).eps) / np.log(rate), prop1)
)
* np.log(rate)
)


class InterpolationStrategyBase(ABC):

Check warning on line 118 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

missing-class-docstring

LOW: Missing class docstring
Raw output
no description found
exposure_interp: Callable
hazard_interp: Callable
vulnerability_interp: Callable

def interp_exposure_dim(
self, imp_E0, imp_E1, interpolation_range: int, **kwargs

Check warning on line 124 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

invalid-name

LOW: Argument name "imp_E0" doesn't conform to '(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$' pattern
Raw output
Used when the name doesn't match the regular expression associated to its type(constant, variable, class...).

Check warning on line 124 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

invalid-name

LOW: Argument name "imp_E1" doesn't conform to '(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$' pattern
Raw output
Used when the name doesn't match the regular expression associated to its type(constant, variable, class...).
) -> list:
"""Interpolates along the exposure change between two impact matrices.

Returns a list of `interpolation_range` matrices linearly interpolated between
`mat_start` and `mat_end`.
"""
try:
res = self.exposure_interp(imp_E0, imp_E1, interpolation_range, **kwargs)
except ValueError as err:
if str(err) == "inconsistent shapes":
raise ValueError(

Check warning on line 135 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

raise-missing-from

NORMAL: Consider explicitly re-raising using 'raise ValueError('Tried to interpolate impact matrices of different shape. A possible reason could be Exposures of different shapes.') from err'
Raw output
no description found
"Tried to interpolate impact matrices of different shape. A possible reason could be Exposures of different shapes."

Check warning on line 136 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (136/100)
Raw output
Used when a line is longer than a given number of characters.
)

raise err

Check warning on line 139 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Code Coverage

Not covered line

Line 139 is not covered by tests

return res

def interp_hazard_dim(self, metric_0, metric_1, **kwargs) -> np.ndarray:

Check warning on line 143 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

missing-function-docstring

LOW: Missing function or method docstring
Raw output
no description found
return self.hazard_interp(metric_0, metric_1, **kwargs)

def interp_vulnerability_dim(self, metric_0, metric_1, **kwargs) -> np.ndarray:

Check warning on line 146 in climada/trajectories/interpolation.py

View check run for this annotation

Jenkins - WCR / Pylint

missing-function-docstring

LOW: Missing function or method docstring
Raw output
no description found
return self.vulnerability_interp(metric_0, metric_1, **kwargs)


class InterpolationStrategy(InterpolationStrategyBase):
"""Interface for interpolation strategies."""

def __init__(self, exposure_interp, hazard_interp, vulnerability_interp) -> None:
super().__init__()
self.exposure_interp = exposure_interp
self.hazard_interp = hazard_interp
self.vulnerability_interp = vulnerability_interp


class AllLinearStrategy(InterpolationStrategyBase):
"""Linear interpolation strategy."""

def __init__(self) -> None:
super().__init__()
self.exposure_interp = linear_interp_imp_mat
self.hazard_interp = linear_interp_arrays
self.vulnerability_interp = linear_interp_arrays


class ExponentialExposureInterpolation(InterpolationStrategyBase):
"""Exponential interpolation strategy."""

def __init__(self) -> None:
super().__init__()
self.exposure_interp = exponential_interp_imp_mat
self.hazard_interp = linear_interp_arrays
self.vulnerability_interp = linear_interp_arrays
Loading
Loading