diff --git a/climada/entity/measures/__init__.py b/climada/entity/measures/__init__.py
index f536fabfa..0676679e9 100644
--- a/climada/entity/measures/__init__.py
+++ b/climada/entity/measures/__init__.py
@@ -22,6 +22,9 @@
"""
+from .cost_income import CostIncome
+from .measure import Measure
from .measure_config import MeasureConfig
+from .measure_set import MeasureSet
-__all__ = ["MeasureConfig"]
+__all__ = ["CostIncome", "Measure", "MeasureConfig", "MeasureSet"]
diff --git a/climada/entity/measures/cost_income.py b/climada/entity/measures/cost_income.py
index b369b6bfe..0a33cdd1a 100644
--- a/climada/entity/measures/cost_income.py
+++ b/climada/entity/measures/cost_income.py
@@ -28,6 +28,8 @@
from climada.entity.measures.measure_config import CostIncomeConfig
+__all__ = ["CostIncome"]
+
class CostIncome:
"""
diff --git a/climada/entity/measures/helper.py b/climada/entity/measures/helper.py
index 771c9deee..6b2130e55 100644
--- a/climada/entity/measures/helper.py
+++ b/climada/entity/measures/helper.py
@@ -22,9 +22,10 @@
from __future__ import annotations
import logging
+from collections.abc import Callable
from functools import reduce
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, cast
+from typing import TYPE_CHECKING, Any, TypeVar, cast
import numpy as np
import pandas as pd
@@ -46,12 +47,22 @@
HazardChange,
ImpfsetChange,
)
- from climada.hazard.base import Hazard
+ from climada.hazard.base import Hazard # noqa: TC004
T = TypeVar("T", Exposures, ImpactFuncSet, Hazard)
LOGGER = logging.getLogger(__name__)
+__all__ = [
+ "change_impfset",
+ "helper_exposure",
+ "helper_hazard",
+ "helper_impfset",
+ "identity_function",
+ "impact_intensity_rp_cutoff_helper",
+ "replace_hazard",
+]
+
def identity_function(x: T, **_kwargs: Any) -> T:
"""Return the input object unchanged.
@@ -193,7 +204,7 @@ def hazard_change(
base_exposures: Exposures,
base_impfset: ImpactFuncSet,
base_hazard: Hazard,
- exposures_region_id: Optional[list[int]] = None,
+ exposures_region_id: list[int] | None = None,
**_kwargs,
) -> Hazard:
exp_imp = base_exposures
@@ -304,7 +315,7 @@ def hazard_change(hazard: Hazard, **_kwargs) -> Hazard:
hazard_change = composite_fun(
impact_intensity_rp_cutoff_helper(hazard_modifier.impact_rp_cutoff),
hazard_change,
- )
+ ) # ty: ignore[invalid-assignment]
if hazard_modifier.new_hazard_path is not None:
if not isinstance(hazard_modifier.new_hazard_path, (str, Path)):
diff --git a/climada/entity/measures/measure.py b/climada/entity/measures/measure.py
index 1c095723a..fd05d1c11 100755
--- a/climada/entity/measures/measure.py
+++ b/climada/entity/measures/measure.py
@@ -29,8 +29,10 @@
import inspect
import logging
from functools import wraps
-from typing import TYPE_CHECKING, Any, TypeVar
+from typing import TYPE_CHECKING, TypeVar
+from climada.entity.exposures.base import Exposures
+from climada.entity.impact_funcs.impact_func_set import ImpactFuncSet
from climada.entity.measures.helper import (
helper_exposure,
helper_hazard,
@@ -38,25 +40,22 @@
identity_function,
)
from climada.entity.measures.measure_config import MeasureConfig
+from climada.hazard.base import Hazard
from .cost_income import CostIncome
if TYPE_CHECKING:
- from climada.entity.exposures.base import Exposures
- from climada.entity.impact_funcs.impact_func_set import ImpactFuncSet
+
from climada.entity.measures.types import (
ExposuresChange,
HazardChange,
ImpfsetChange,
)
- from climada.hazard.base import Hazard
T = TypeVar("T", Exposures, ImpactFuncSet, Hazard)
LOGGER = logging.getLogger(__name__)
-# TODO: risk transfer?
-
def allow_kwargs(func):
"""
@@ -199,10 +198,6 @@ def __init__(
self.color_rgb = (0, 0, 0) if color_rgb is None else color_rgb
self._config = _config
- # DONE always provide exp, impfset and hazard as kwargs by default
- # Have a precedence system (if users provide their own it takes over)
- # TODO Check that it works
-
@property
def is_serializable(self) -> bool:
"""Returns True if the ``Measure`` was created from
@@ -212,7 +207,7 @@ def is_serializable(self) -> bool:
return self._config is not None
@classmethod
- def from_config(cls, config: MeasureConfig) -> "Measure":
+ def from_config(cls, config: MeasureConfig) -> Measure:
"""Create a ``Measure`` from a ``MeasureConfig`` object.
The modifiers defined in the configuration are converted into change
@@ -245,7 +240,7 @@ def from_config(cls, config: MeasureConfig) -> "Measure":
)
@classmethod
- def from_dict(cls, d: dict) -> "Measure":
+ def from_dict(cls, d: dict) -> Measure:
"""Create a ``Measure`` from a dictionary.
The dictionary is first converted to a ``MeasureConfig`` with
@@ -337,9 +332,14 @@ def apply_exposures_changes(
else exposures
)
try:
- return self.exposures_changes(changed_exp, **kwargs)
+ changed_exp = self.exposures_changes(changed_exp, **kwargs)
+ if not isinstance(changed_exp, Exposures):
+ raise TypeError(
+ "Exposures change function did not return an Exposures object."
+ )
+ return changed_exp
except TypeError as exc:
- # Check if it's a missing argument error
+ # Make sure it's a missing argument error
if "missing" in str(exc) and "required positional argument" in str(exc):
raise TypeError(
f"The function to apply to the exposures requires additional arguments\
@@ -395,7 +395,12 @@ def apply_impfset_changes(
else impfset
)
try:
- return self.impfset_changes(changed_impfset, **kwargs)
+ changed_impfset = self.impfset_changes(changed_impfset, **kwargs)
+ if not isinstance(changed_impfset, ImpactFuncSet):
+ raise TypeError(
+ "Impfset change function did not return an ImpactFuncSet object."
+ )
+ return changed_impfset
except TypeError as exc:
# Check if it's a missing argument error
if "missing" in str(exc) and "required positional argument" in str(exc):
@@ -452,7 +457,12 @@ def apply_hazard_changes(
else hazard
)
try:
- return self.hazard_changes(changed_hazard, **kwargs)
+ changed_hazard = self.hazard_changes(changed_hazard, **kwargs)
+ if not isinstance(changed_hazard, Hazard):
+ raise TypeError(
+ "Hazard change function did not return an Hazard object."
+ )
+ return changed_hazard
except TypeError as exc:
# Check if it's a missing argument error
if "missing" in str(exc) and "required positional argument" in str(exc):
diff --git a/climada/entity/measures/measure_config.py b/climada/entity/measures/measure_config.py
index 77ec61d32..265b18553 100644
--- a/climada/entity/measures/measure_config.py
+++ b/climada/entity/measures/measure_config.py
@@ -24,7 +24,6 @@
import logging
import warnings
from dataclasses import MISSING, dataclass, field, fields
-from datetime import datetime
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
@@ -37,10 +36,10 @@
LOGGER = logging.getLogger(__name__)
__all__ = [
- "HazardModifierConfig",
+ "CostIncomeConfig",
"ExposuresModifierConfig",
+ "HazardModifierConfig",
"ImpfsetModifierConfig",
- "CostIncomeConfig",
"MeasureConfig",
]
@@ -209,10 +208,14 @@ class ImpfsetModifierConfig(ModifierConfig):
Additive offset applied to the PAA curve after multiplication.
Default is ``0.0``.
impf_int_mult : float, optional
- Multiplicative factor applied to the intensity axis.
+ Inverse of the multiplicative factor applied to the intensity axis.
+ That is ``0.5`` would double the values of the intensity axis, namely reducing by 50%
+ the damages for a given intensity.
Default is ``1.0``.
impf_int_add : float, optional
- Additive offset applied to the intensity axis after multiplication.
+ Negative additive offset applied to the intensity axis after multiplication.
+ That a positive value shifts the values of the intensity axis to the left, namely
+ shifting the impact function to the right.
Default is ``0.0``.
new_impfset_path : str, optional
Path to an Excel file containing a replacement impact function set.
@@ -400,9 +403,9 @@ class CostIncomeConfig(ModifierConfig):
Explicit cash flow schedule as a list of records with at minimum
a ``"date"`` key (ISO 8601 string) and a value key. If provided,
overrides the periodic cost/income logic.
- """ # noqa
+ """
- mkt_price_year: int | None = field(default_factory=lambda: datetime.today().year)
+ mkt_price_year: int | None = field(default_factory=lambda: pd.Timestamp.now().year)
init_cost: float = 0.0
periodic_cost: float = 0.0
periodic_income: float = 0.0
@@ -412,7 +415,7 @@ class CostIncomeConfig(ModifierConfig):
custom_cash_flows: list[dict] | None = None
@classmethod
- def from_cost_income(cls, cost_income: CostIncome) -> "CostIncomeConfig":
+ def from_cost_income(cls, cost_income: CostIncome) -> CostIncomeConfig:
"""
Construct a :class:`CostIncomeConfig` from a live
:class:`CostIncome` object.
@@ -530,7 +533,7 @@ def to_dict(self, omit_default: bool = True) -> dict:
}
@classmethod
- def from_dict(cls, kwargs_dict: dict) -> "MeasureConfig":
+ def from_dict(cls, kwargs_dict: dict) -> MeasureConfig:
"""
Instantiate a :class:`MeasureConfig` from a flat dictionary.
@@ -604,7 +607,7 @@ def to_yaml(self, path: str) -> None:
)
@classmethod
- def from_yaml(cls, path: str) -> "MeasureConfig":
+ def from_yaml(cls, path: str) -> MeasureConfig:
"""
Load a :class:`MeasureConfig` from a YAML file.
@@ -627,7 +630,7 @@ def from_yaml(cls, path: str) -> "MeasureConfig":
return cls.from_dict(yaml.safe_load(opened_file)["measures"][0])
@classmethod
- def from_row(cls, row: pd.Series) -> "MeasureConfig":
+ def from_row(cls, row: pd.Series) -> MeasureConfig:
"""
Construct a :class:`MeasureConfig` from a legacy Excel row.
diff --git a/climada/entity/measures/types.py b/climada/entity/measures/types.py
index 1ad90986b..d20edeca5 100644
--- a/climada/entity/measures/types.py
+++ b/climada/entity/measures/types.py
@@ -1,3 +1,24 @@
+"""
+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 .
+
+---
+
+Define useful types for the measure module.
+"""
+
from collections.abc import Callable
from typing import Concatenate
diff --git a/climada/test/conftest.py b/climada/test/conftest.py
index 727215b2f..d9c3da54f 100644
--- a/climada/test/conftest.py
+++ b/climada/test/conftest.py
@@ -51,6 +51,7 @@
from shapely.geometry import Point
from climada.entity import Exposures, ImpactFunc, ImpactFuncSet
+from climada.entity.measures import Measure
from climada.hazard import Centroids, Hazard
# ---------------------------------------------------------------------------
@@ -306,3 +307,13 @@ def _make_impfset(
@pytest.fixture
def impfset(impfset_factory):
return impfset_factory()
+
+
+@pytest.fixture
+def measure_on_exposures():
+ def reduce_exposure_value(exposures, reduction_factor):
+ """Reduces the value of all assets by a fixed percentage."""
+ exposures.gdf["value"] *= 1 - reduction_factor
+ return exposures
+
+ return Measure(name="Exposure reduction", exposures_changes=reduce_exposure_value)
diff --git a/climada/test/test_measures.py b/climada/test/test_measures.py
index 78fc2af85..9b709f891 100644
--- a/climada/test/test_measures.py
+++ b/climada/test/test_measures.py
@@ -1,306 +1,354 @@
"""
-Integration tests for the Measure class.
-
-These tests use real CLIMADA objects (Exposures, ImpactFuncSet, Hazard) built
-from the shared conftest fixtures. They verify that Measure transformations
-produce analytically expected results rather than mocking internals.
-
-Expected impact values (from conftest docstring):
- AAI = 18
- RP20 = 0 (event 1, freq 0.03)
- RP50 = 0 (event 2, freq 0.01)
- RP100 = 500 (event 3, freq 0.006 → 1000 * 0.5)
- RP250 = 3750 (event 4, freq 0.004 → 15000 * 0.25)
-"""
+This file is part of CLIMADA.
-import numpy as np
-import pytest
-from scipy.sparse import csr_matrix
+Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.
-from climada.entity.measures.cost_income import CostIncome
-from climada.entity.measures.measure import Measure
-from climada.test.conftest import HAZARD_TYPE
+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.
-# ===========================================================================
-# apply() — structural / immutability tests
-# ===========================================================================
+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 .
-class TestApplyStructural:
- """Verify that apply() correctly transforms the risk triplet."""
+---
- def test_identity_measure_returns_equal_objects(self, exposures, impfset, hazard):
- """An identity measure should return objects equal to the originals."""
- m = Measure("identity")
- new_exp, new_impfset, new_haz = m.apply(exposures, impfset, hazard)
+Integration tests for the measure pipeline:
+MeasureConfig -> Measure -> modified (Exposures, ImpactFuncSet, Hazard) -> Impact.
- assert new_exp.gdf["value"].tolist() == exposures.gdf["value"].tolist()
- assert new_haz.intensity.nnz == hazard.intensity.nnz
- assert list(new_impfset.get_func(haz_type=HAZARD_TYPE)) == list(
- impfset.get_func(haz_type=HAZARD_TYPE)
- )
-
- def test_enforce_copy_true_does_not_mutate_originals(
- self, exposures, impfset, hazard
- ):
- """With enforce_copy=True, the originals must not be mutated."""
- original_values = exposures.gdf["value"].copy()
+Expected values follow the analytical design of ``conftest.py``
+(linear impact function, baseline AAI = 18, at_event = [0, 0, 500, 3750, 0]).
+"""
- def double_values(exp, **kw):
- exp.gdf["value"] *= 2
- return exp
+from unittest.mock import patch
- m = Measure("doubler", exposures_changes=double_values)
- m.apply(exposures, impfset, hazard, enforce_copy=True)
+import numpy as np
+import pytest
- np.testing.assert_array_equal(
- exposures.gdf["value"].values, original_values.values
+from climada.engine.impact_calc import ImpactCalc
+from climada.engine.test.test_impact_calc import exposure
+from climada.entity.measures.measure import Measure
+from climada.entity.measures.measure_config import (
+ CostIncomeConfig,
+ ExposuresModifierConfig,
+ HazardModifierConfig,
+ ImpfsetModifierConfig,
+ MeasureConfig,
+)
+from climada.hazard.base import Hazard
+
+LOGGER = "climada.entity.measures.measure.LOGGER"
+BASELINE_AAI = 18.0
+
+# Note: at_event() does not care about the frequency
+BASELINE_AT_EVENT = [0.0, 0.0, 500.0, 3750.0, 15000.0]
+CATEGORIES = [1, 1, 2, 1, 1, 3]
+
+
+@pytest.fixture
+def triplet(exposures_factory, impfset, hazard):
+ """Return (exposures, impfset, hazard) with centroids assigned."""
+ exposures = exposures_factory(categories=CATEGORIES)
+ exposures.assign_centroids(hazard)
+ return exposures, impfset, hazard
+
+
+@pytest.fixture
+def config_factory(hazard):
+ """Return a function building a MeasureConfig from modifier overrides."""
+
+ def _make_config(
+ name="measure",
+ impfset=None,
+ hazard_mod=None,
+ exposures_mod=None,
+ cost_income=None,
+ ):
+ """
+ Parameters
+ ----------
+ name : str, optional
+ Measure name.
+ impfset, hazard_mod, exposures_mod, cost_income : dict, optional
+ Keyword arguments of the corresponding sub-configuration.
+
+ Returns
+ -------
+ MeasureConfig
+ """
+ haz_type = hazard.haz_type
+ return MeasureConfig(
+ name=name,
+ haz_type=haz_type,
+ impfset_modifier=ImpfsetModifierConfig(
+ haz_type=haz_type, **(impfset or {})
+ ),
+ hazard_modifier=HazardModifierConfig(
+ haz_type=haz_type, **(hazard_mod or {})
+ ),
+ exposures_modifier=ExposuresModifierConfig(**(exposures_mod or {})),
+ cost_income=CostIncomeConfig(**(cost_income or {})),
)
- def test_enforce_copy_false_mutates_original(self, exposures, impfset, hazard):
- """With enforce_copy=False the change function receives the original object."""
- received_ids = []
-
- def record_id(exp, **kw):
- received_ids.append(id(exp))
- return exp
-
- m = Measure("recorder", exposures_changes=record_id)
- m.apply(exposures, impfset, hazard, enforce_copy=False)
-
- assert received_ids[0] == id(exposures)
-
- def test_base_triplet_available_as_kwargs(self, exposures, impfset, hazard):
- """Each change function receives base_exposures/impfset/hazard as kwargs."""
- received = {}
-
- def capture(**kwargs):
- def fn(obj, **kw):
- received.update(kw)
- return obj
-
- return fn
-
- m = Measure(
- "capture",
- exposures_changes=capture(),
+ return _make_config
+
+
+def _aai(measure, triplet, **kwargs):
+ """Return the AAI of the triplet after applying `measure`."""
+ return measure.calc_impact(*triplet, **kwargs).aai_agg
+
+
+class TestBaseline:
+ def test_identity_measure_reproduces_analytical_impact(self, triplet):
+ impact = Measure("baseline").calc_impact(*triplet)
+ assert impact.aai_agg == pytest.approx(BASELINE_AAI)
+ np.testing.assert_allclose(impact.at_event, BASELINE_AT_EVENT)
+
+ def test_default_config_is_identity(self, triplet, config_factory):
+ measure = Measure.from_config(config_factory())
+ assert _aai(measure, triplet) == pytest.approx(BASELINE_AAI)
+
+
+class TestConfigModifiers:
+ @pytest.mark.parametrize(
+ "modifiers, expected_aai",
+ [
+ ({"hazard_mod": {"haz_int_mult": 0.5}}, 9.0),
+ ({"hazard_mod": {"haz_freq_mult": 2.0}}, 36.0),
+ (
+ {"hazard_mod": {"haz_freq_add": -0.005}},
+ 0.5,
+ ), # Only event 3 with 0.001 freq
+ ({"hazard_mod": {"haz_freq_add": -0.01}}, 0.0), # clipped at zero
+ ({"hazard_mod": {"impact_rp_cutoff": 200}}, 15.0), # event 3 removed
+ ({"impfset": {"impf_mdd_mult": 0.5}}, 9.0),
+ ({"impfset": {"impf_paa_mult": 0.5}}, 9.0),
+ ({"impfset": {"impf_int_mult": 0.5}}, 9.0), # inverse intensity axis
+ ({"exposures_mod": {"set_to_zero": [1]}}, 14.0),
+ (
+ {
+ "hazard_mod": {"haz_int_mult": 0.5},
+ "impfset": {"impf_mdd_mult": 0.5},
+ },
+ 4.5,
+ ),
+ ],
+ ids=[
+ "haz_int_mult",
+ "haz_freq_mult",
+ "haz_freq_add",
+ "haz_freq_add_clipped",
+ "rp_cutoff",
+ "mdd_mult",
+ "paa_mult",
+ "impf_int_mult",
+ "set_to_zero",
+ "haz_and_mdd",
+ ],
+ )
+ def test_aai_after_modifier(self, triplet, config_factory, modifiers, expected_aai):
+ measure = Measure.from_config(config_factory(**modifiers))
+ assert _aai(measure, triplet) == pytest.approx(expected_aai)
+
+ def test_rp_cutoff_keeps_event_below_threshold(self, triplet, config_factory):
+ measure = Measure.from_config(
+ config_factory(hazard_mod={"impact_rp_cutoff": 200})
)
- m.apply(exposures, impfset, hazard, enforce_copy=False)
-
- assert "base_exposures" in received
- assert "base_impfset" in received
- assert "base_hazard" in received
-
+ impact = measure.calc_impact(*triplet)
+ np.testing.assert_allclose(impact.at_event, [0.0, 0.0, 0.0, 3750.0, 15000.0])
-# ===========================================================================
-# Exposures transformations
-# ===========================================================================
-
-
-class TestExposuresTransformations:
- """Measures that modify Exposures."""
-
- def test_scale_values(self, exposures, impfset, hazard):
- """Scaling exposure values by 0.5 should halve every entry."""
-
- def scale_half(exp, **kw):
- exp.gdf["value"] *= 0.5
- return exp
+ def test_rp_cutoff_keeps_event_below_threshold_edge(self, triplet, config_factory):
+ measure = Measure.from_config(
+ config_factory(hazard_mod={"impact_rp_cutoff": 250})
+ )
+ impact = measure.calc_impact(*triplet)
+ np.testing.assert_allclose(impact.at_event, [0.0, 0.0, 0.0, 0.0, 15000.0])
- m = Measure("scale_half", exposures_changes=scale_half)
- new_exp, _, _ = m.apply(exposures, impfset, hazard)
- np.testing.assert_allclose(
- new_exp.gdf["value"].values, exposures.gdf["value"].values * 0.5
+class TestImpfIds:
+ @pytest.mark.parametrize("impf_ids", [None, "all", 1, [1]])
+ def test_targeted_function_is_modified(self, triplet, config_factory, impf_ids):
+ measure = Measure.from_config(
+ config_factory(impfset={"impf_mdd_mult": 0.5, "impf_ids": impf_ids})
)
+ assert _aai(measure, triplet) == pytest.approx(9.0)
- def test_zero_all_values(self, exposures, impfset, hazard):
- """Zeroing all exposures should produce zero impacts downstream."""
-
- def zero_exp(exp, **kw):
- exp.gdf["value"] = 0.0
- return exp
+ def test_other_function_leaves_impact_unchanged(self, triplet, config_factory):
+ measure = Measure.from_config(
+ config_factory(impfset={"impf_mdd_mult": 0.5, "impf_ids": [2]})
+ )
+ assert _aai(measure, triplet) == pytest.approx(BASELINE_AAI)
- m = Measure("zero_exp", exposures_changes=zero_exp)
- new_exp, _, _ = m.apply(exposures, impfset, hazard)
+ def test_invalid_type_raises_value_error(self, triplet, config_factory):
+ measure = Measure.from_config(config_factory(impfset={"impf_ids": 2.5}))
+ with pytest.raises(ValueError, match="invalid"):
+ measure.apply_impfset_changes(triplet[1])
- assert new_exp.gdf["value"].sum() == 0.0
- def test_exposures_change_uses_base_hazard_kwarg(
- self, exposures, impfset, hazard_factory
+class TestReplacementFromDisk:
+ def test_hazard_replacement(
+ self, triplet, config_factory, hazard_factory, tmp_path
):
- """Change function can inspect base_hazard via the default kwargs."""
- captured_haz = {}
+ path = tmp_path / "hazard.hdf5"
+ hazard_factory(intensity_scale=0.5).write_hdf5(str(path))
- def fn(exp, base_hazard=None, **kw):
- captured_haz["haz"] = base_hazard
- return exp
-
- haz = hazard_factory()
- m = Measure("haz_aware", exposures_changes=fn)
- m.apply(exposures, impfset, haz, enforce_copy=False)
-
- assert captured_haz["haz"] is haz
-
-
-# ===========================================================================
-# Hazard transformations
-# ===========================================================================
-
-
-class TestHazardTransformations:
- """Measures that modify the Hazard."""
+ measure = Measure.from_config(
+ config_factory(hazard_mod={"new_hazard_path": str(path)})
+ )
+ assert _aai(measure, triplet) == pytest.approx(9.0)
- def test_zero_intensity_eliminates_impacts(
- self, exposures, impfset, hazard_factory
+ def test_hazard_replacement_then_modifier(
+ self, triplet, config_factory, hazard_factory, tmp_path
):
- """Setting hazard intensity to zero should yield zero impacts."""
+ path = tmp_path / "hazard.hdf5"
+ hazard_factory(intensity_scale=0.5).write_hdf5(str(path))
- def zero_intensity(haz, **kw):
- haz.intensity = csr_matrix(haz.intensity.shape)
- return haz
+ with pytest.warns(UserWarning, match="Both new hazard"):
+ config = config_factory(
+ hazard_mod={"new_hazard_path": str(path), "haz_int_mult": 0.5}
+ )
+ assert _aai(Measure.from_config(config), triplet) == pytest.approx(4.5)
- haz = hazard_factory()
- m = Measure("zero_haz", hazard_changes=zero_intensity)
- new_exp, new_impfset, new_haz = m.apply(exposures, impfset, haz)
-
- assert new_haz.intensity.nnz == 0
-
- def test_scale_intensity(self, exposures, impfset, hazard_factory):
- """Halving hazard intensity should halve non-zero entries."""
- original_data = hazard_factory().intensity.data.copy()
+ def test_impfset_replacement(
+ self, triplet, config_factory, impfset_factory, tmp_path
+ ):
+ path = tmp_path / "impfset.xlsx"
+ impfset_factory(paa_scale=0.5).write_excel(str(path))
- def half_intensity(haz, **kw):
- haz.intensity = haz.intensity * 0.5
- return haz
+ measure = Measure.from_config(
+ config_factory(impfset={"new_impfset_path": str(path)})
+ )
+ assert _aai(measure, triplet) == pytest.approx(9.0)
- m = Measure("half_haz", hazard_changes=half_intensity)
- _, _, new_haz = m.apply(exposures, impfset, hazard_factory())
+ def test_impfset_replacement_then_modifier(
+ self, triplet, config_factory, impfset_factory, tmp_path
+ ):
+ path = tmp_path / "impfset.xlsx"
+ impfset_factory(paa_scale=0.5).write_excel(str(path))
- np.testing.assert_allclose(new_haz.intensity.data, original_data * 0.5)
+ with pytest.warns(UserWarning, match="Both new impfset"):
+ config = config_factory(
+ impfset={"new_impfset_path": str(path), "impf_mdd_mult": 0.5}
+ )
+ assert _aai(Measure.from_config(config), triplet) == pytest.approx(4.5)
+ def test_exposures_replacement(
+ self, triplet, config_factory, exposures_factory, tmp_path
+ ):
+ path = tmp_path / "exposures.hdf5"
+ exposures_factory(categories=CATEGORIES, value_factor=0.5).write_hdf5(str(path))
-# ===========================================================================
-# ImpactFuncSet transformations
-# ===========================================================================
+ measure = Measure.from_config(
+ config_factory(exposures_mod={"new_exposures_path": str(path)})
+ )
+ assert _aai(measure, triplet) == pytest.approx(9.0)
+
+ @pytest.mark.parametrize(
+ "override",
+ [
+ {"hazard_mod": {"new_hazard_path": "nil"}},
+ {"impfset": {"new_impfset_path": "nil"}},
+ {"exposures_mod": {"new_exposures_path": "nil"}},
+ ],
+ ids=["hazard", "impfset", "exposures"],
+ )
+ def test_nil_path_means_no_replacement(self, triplet, config_factory, override):
+ measure = Measure.from_config(config_factory(**override))
+ assert _aai(measure, triplet) == pytest.approx(BASELINE_AAI)
+
+
+class TestCopySemantics:
+ @pytest.fixture
+ def measure(self, config_factory):
+ return Measure.from_config(
+ config_factory(
+ hazard_mod={"haz_int_mult": 0.5},
+ impfset={"impf_mdd_mult": 0.5},
+ exposures_mod={"set_to_zero": [1]},
+ )
+ )
+ def test_enforce_copy_preserves_inputs(self, measure, triplet):
+ exposures, impfset, hazard = triplet
+ intensity = hazard.intensity.toarray().copy()
+ values = exposures.gdf["value"].to_numpy().copy()
-class TestImpfsetTransformations:
- """Measures that modify the ImpactFuncSet."""
+ measure.apply(exposures, impfset, hazard, enforce_copy=True)
- def test_scale_paa_to_zero_eliminates_impacts(
- self, exposures, impfset_factory, hazard
- ):
- """Setting PAA to 0 should make all MDD-based impacts vanish."""
+ np.testing.assert_array_equal(hazard.intensity.toarray(), intensity)
+ np.testing.assert_array_equal(exposures.gdf["value"].to_numpy(), values)
+ assert _aai(Measure("baseline"), triplet) == pytest.approx(BASELINE_AAI)
- def zero_paa(ifs, **kw):
- for func in ifs.get_func(haz_type=HAZARD_TYPE):
- func.paa = np.zeros_like(func.paa)
- return ifs
+ def test_no_copy_modifies_inputs_in_place(self, measure, triplet):
+ exposures, impfset, hazard = triplet
+ intensity = hazard.intensity.toarray().copy()
- impfset = impfset_factory()
- m = Measure("zero_paa", impfset_changes=zero_paa)
- _, new_impfset, _ = m.apply(exposures, impfset, hazard)
+ measure.apply(exposures, impfset, hazard, enforce_copy=False)
- for func in new_impfset.get_func(haz_type=HAZARD_TYPE):
- np.testing.assert_array_equal(func.paa, 0)
+ np.testing.assert_allclose(hazard.intensity.toarray(), 0.5 * intensity)
+ assert exposures.gdf["value"].iloc[1] == 0
-# ===========================================================================
-# Combined / multi-component transformations
-# ===========================================================================
+class TestCalcImpact:
+ def test_matches_manual_apply_and_impact_calc(self, triplet, config_factory):
+ measure = Measure.from_config(
+ config_factory(
+ hazard_mod={"haz_int_mult": 0.5},
+ impfset={"impf_paa_mult": 0.5},
+ )
+ )
+ new_exp, new_impfset, new_haz = measure.apply(*triplet)
+ manual = ImpactCalc(new_exp, new_impfset, new_haz).impact(save_mat=False)
+ impact = measure.calc_impact(*triplet)
-class TestCombinedTransformations:
- """Measures that simultaneously affect more than one risk component."""
+ assert impact.aai_agg == pytest.approx(manual.aai_agg)
+ np.testing.assert_allclose(impact.at_event, manual.at_event)
- def test_combined_exposure_and_hazard_change(
- self, exposures, impfset, hazard_factory
+ def test_warns_and_assigns_missing_centroids(
+ self, exposures_factory, impfset, hazard
):
- """Both exposure and hazard transforms should be applied independently."""
+ exposures = exposures_factory(categories=CATEGORIES)
+ with patch(LOGGER) as mock_logger:
+ impact = Measure("baseline").calc_impact(exposures, impfset, hazard)
- def double_values(exp, **kw):
- exp.gdf["value"] *= 2
- return exp
+ mock_logger.warning.assert_called_once()
+ assert hazard.centr_exp_col in exposures.gdf.columns
+ assert impact.aai_agg == pytest.approx(BASELINE_AAI)
- def half_intensity(haz, **kw):
- haz.intensity = haz.intensity * 0.5
- return haz
+ def test_no_warning_when_centroids_assigned(self, triplet):
+ with patch(LOGGER) as mock_logger:
+ Measure("baseline").calc_impact(*triplet)
- haz = hazard_factory()
- original_intensity_data = haz.intensity.data.copy()
+ mock_logger.warning.assert_not_called()
- m = Measure(
- "combined",
- exposures_changes=double_values,
- hazard_changes=half_intensity,
- )
- new_exp, _, new_haz = m.apply(exposures, impfset, haz)
- np.testing.assert_allclose(
- new_exp.gdf["value"].values, exposures.gdf["value"].values * 2
- )
- np.testing.assert_allclose(
- new_haz.intensity.data, original_intensity_data * 0.5
+class TestCustomFunction:
+ def test_custom_measure(self, measure_on_exposures, triplet):
+ impact = measure_on_exposures.calc_impact(
+ *triplet, kwargs_exposures={"reduction_factor": 0.1}
)
+ assert impact.aai_agg == pytest.approx(BASELINE_AAI * 0.9)
- def test_kwargs_exposures_and_kwargs_hazard_routed_correctly(
- self, exposures, impfset, hazard
- ):
- """kwargs_exposures and kwargs_hazard should reach the right function only."""
- exp_extra, haz_extra = {}, {}
-
- def track_exp(exp, my_exp_param=None, **kw):
- exp_extra["val"] = my_exp_param
- return exp
-
- def track_haz(haz, my_haz_param=None, **kw):
- haz_extra["val"] = my_haz_param
- return haz
-
- m = Measure("routed", exposures_changes=track_exp, hazard_changes=track_haz)
- m.apply(
- exposures,
- impfset,
- hazard,
- enforce_copy=False,
- kwargs_exposures={"my_exp_param": "exp_value"},
- kwargs_hazard={"my_haz_param": "haz_value"},
- )
+ def test_custom_measure_missing_kwarg(self, measure_on_exposures, triplet):
+ with pytest.raises(TypeError, match="kwargs_exposures"):
+ measure_on_exposures.calc_impact(*triplet)
- assert exp_extra["val"] == "exp_value"
- assert haz_extra["val"] == "haz_value"
+ def test_wrong_custom_measure(self, triplet):
+ def wrong_change(arg1):
+ return "wrong return type"
+ meas = Measure(name="", exposures_changes=wrong_change)
+ with pytest.raises(TypeError, match="did not return an Exposures"):
+ meas.apply(*triplet)
-# ===========================================================================
-# CostIncome integration
-# ===========================================================================
+ meas = Measure(name="", hazard_changes=wrong_change)
+ with pytest.raises(TypeError, match="did not return an Hazard"):
+ meas.apply(*triplet)
-
-class TestCostIncomeIntegration:
- """Verify that CostIncome financial data survives through Measure construction."""
-
- def test_cost_income_attached(self, exposures, impfset, hazard):
- ci = CostIncome(mkt_price_year=2020, init_cost=10_000, periodic_income=500)
- m = Measure("with_ci", cost_income=ci)
- assert m.cost_income is ci
-
- def test_total_cost_calculable_after_apply(self, exposures, impfset, hazard):
- ci = CostIncome(
- mkt_price_year=2020,
- init_cost=10_000,
- periodic_cost=1_000,
- periodic_income=2_000,
- )
- m = Measure("ci_measure", cost_income=ci)
- m.apply(exposures, impfset, hazard)
-
- total_net, total_cost, total_inc = m.cost_income.calc_total(
- impl_date="2020-01-01",
- start_date="2020-01-01",
- end_date="2030-01-01",
- )
- # Over 10 years: init_cost once + 9 periodic_costs + 10 periodic_incomes
- assert total_cost < 0
- assert total_inc > 0
- assert isinstance(total_net, float)
+ meas = Measure(name="", impfset_changes=wrong_change)
+ with pytest.raises(TypeError, match="did not return an ImpactFuncSet"):
+ meas.apply(*triplet)
diff --git a/doc/user-guide/climada_cost_income.ipynb b/doc/user-guide/climada_cost_income.ipynb
index fd53c2585..381cfe94e 100644
--- a/doc/user-guide/climada_cost_income.ipynb
+++ b/doc/user-guide/climada_cost_income.ipynb
@@ -181,7 +181,7 @@
}
],
"source": [
- "from climada.entity.measures.cost_income import CostIncome\n",
+ "from climada.entity.measures import CostIncome\n",
"\n",
"ci = CostIncome(\n",
" mkt_price_year=2025,\n",
@@ -237,8 +237,6 @@
"metadata": {},
"outputs": [],
"source": [
- "from climada.entity.measures.cost_income import CostIncome\n",
- "\n",
"ci = CostIncome(\n",
" mkt_price_year=2025,\n",
" init_cost=10_000, # 10 000 upfront\n",
@@ -1144,8 +1142,6 @@
}
],
"source": [
- "import pandas as pd\n",
- "\n",
"custom_flows_a = pd.DataFrame(\n",
" {\n",
" \"date\": [\"2024\", \"2026\", \"2028\"],\n",
diff --git a/doc/user-guide/climada_measure_config.ipynb b/doc/user-guide/climada_measure_config.ipynb
index ebbeb6df7..cadca28fe 100644
--- a/doc/user-guide/climada_measure_config.ipynb
+++ b/doc/user-guide/climada_measure_config.ipynb
@@ -136,7 +136,7 @@
}
],
"source": [
- "from climada.entity.measures.measure_config import MeasureConfig\n",
+ "from climada.entity.measures import MeasureConfig\n",
"\n",
"measure_dict = {\n",
" \"name\": \"Tutorial measure\",\n",
diff --git a/doc/user-guide/climada_measures.ipynb b/doc/user-guide/climada_measures.ipynb
index 735fcdf9c..7de605531 100644
--- a/doc/user-guide/climada_measures.ipynb
+++ b/doc/user-guide/climada_measures.ipynb
@@ -1,674 +1,681 @@
{
"cells": [
- {
- "cell_type": "markdown",
- "id": "adc83a35",
- "metadata": {},
- "source": [
- "(measure-tutorial)=\n",
- "\n",
- "# Defining Adaptation Measures with python functions"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "3dca8f82",
- "metadata": {},
- "source": [
- "The `Measure` object is the core component of the **Adaptation** module in CLIMADA. It acts as a wrapper for transformation logic, allowing you to model how specific interventions alter the risk landscape (like building sea walls, retrofitting houses, or implementing early warning systems).\n",
- "\n",
- "The primary purpose of a `Measure` is to modify one or more parts of the \"Risk Triplet\": **Exposures**, **Impact Functions**, and **Hazard**.\n",
- "\n",
- "`Measure` objects can be regrouped into a set with the `MeasureSet` object."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "f51842c7",
- "metadata": {},
- "source": [
- "## Quickstart"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "d369e8bd-1f00-4435-9b35-ef03bba2af97",
- "metadata": {},
- "source": [
- "Assume you want to model two measures as follow:\n",
- "- The first one is a targeted awareness campaign, which effect is modelled as a change of one specific impact function. Costing 500'000.\n",
- "- The second one is both building a sea wall which effect is modelled as a 0.2 meter reduction of the flood height everywhere, and a building retrofit, modelled as an overal reduction of the value at risk of the exposure. Costing 1M."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "id": "2c48b021",
- "metadata": {},
- "outputs": [],
- "source": [
- "from climada.entity.measures.measure import Measure\n",
- "from climada.entity.measures.measure_set import MeasureSet\n",
- "from climada.entity.measures.cost_income import CostIncome\n",
- "\n",
- "\n",
- "# Define the effect with python functions\n",
- "def targeted_campaign(impact_func_set, impf_id, rescale):\n",
- " \"\"\"Rescales the impact function with id impf_id\"\"\"\n",
- " impact_func_set.get_func(fun_id=imp_id).mdd *= rescale\n",
- " return impact_func_set\n",
- "\n",
- "\n",
- "def building_retrofit(exposures, reduction_factor):\n",
- " \"\"\"Reduces the value of all assets by a fixed percentage.\"\"\"\n",
- " exposures.gdf[\"value\"] *= 1 - reduction_factor\n",
- " return exposures\n",
- "\n",
- "\n",
- "def sea_wall_protection(hazard, reduction):\n",
- " \"\"\"Reduces flood intensity by an absolute amount.\"\"\"\n",
- " hazard.intensity.data -= reduction\n",
- " return hazard\n",
- "\n",
- "\n",
- "# Initialize the Measure objects\n",
- "campaign = Measure(\n",
- " name=\"Targeted awareness campaign\",\n",
- " impfset_changes=targeted_campaign,\n",
- " cost_income=CostIncome(init_cost=500_000),\n",
- ")\n",
- "\n",
- "retrofitting_and_seawall = Measure(\n",
- " name=\"Building Retrofit and seawall construction\",\n",
- " exposures_changes=building_retrofit,\n",
- " hazard_changes=sea_wall_protection,\n",
- " cost_income=CostIncome(init_cost=1_000_000),\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "02cd2812",
- "metadata": {},
- "source": [
- "The measure can then be used either to create a new triplet corresponding to the situation with measure in effect\n",
- "or to compute the impact directly (both lines are independent):\n",
- "\n",
- "```python\n",
- "new_exp, new_impf, new_haz = retrofitting_and_seawall.apply(exp, impf, haz, kwargs_exposure={'reduction_factor': 0.1}, kwargs_hazard={'reduction': 0.3})\n",
- "\n",
- "# or directly:\n",
- "impact_with_campaign_active = campaign.calc_impact(exp, impf, haz, kwargs_impfset={'rescale': 0.2})\n",
- "```\n",
- "\n",
- "Much like impact functions, the measure can also be regrouped into a set with `MeasureSet`:\n",
- "\n",
- "```python\n",
- "\n",
- "meas_set = MeasureSet([campaign, retrofitting_and_seawall])\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "6f4650c6",
- "metadata": {},
- "source": [
- "## Core logic of the Measure object\n",
- "\n",
- "When you initialize a `Measure`, you can provide one to three callables (python functions):\n",
- "\n",
- "1. **`exposures_changes`**: Modifies the `Exposures` object, the asset location, value, or metadata (e.g., relocating houses from a flood zone).\n",
- "2. **`impfset_changes`**: Modifies the `ImpactFuncSet` object, the vulnerability of assets (e.g., updating an impact function because houses were elevated).\n",
- "3. **`hazard_changes`**: Modifies the `Hazard` object, the intensity or frequency of events (e.g., a dam reducing flood depths).\n",
- "\n",
- "```{note}\n",
- "By default, changes are the `identity_function`, so if you do not provide a function e.g., \"exposures_changes\" the measure once applied will return the exposure unchanged.\n",
- "```\n",
- "\n",
- "The **only** requirement for these python functions is to **take** and **return** an `Exposures` object (or `ImpactFuncSet` or `Hazard` objects respectively). This enables high modeling flexibility where changes from an adaptation option can be modelled from a simple \"loading another file for the hazard or exposure\", to an advanced function.\n",
- "\n",
- "One thing to note, is that the `apply()` method of a measure always passes the entire triplet (`base_exposures`, `base_impfset`, `base_hazard`, i.e., the risk triplet before the measure is applied) to every transformation function. This means you can easily write complex logic that relie on any of these component. For example:\n",
- "\n",
- "> \"Only reduce the exposure value (Exposure change) in areas where the peak water depth (Hazard context) exceeds 2 meters.\"\n",
- "\n",
- "Could be defined like this:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "id": "7cd53344",
- "metadata": {},
- "outputs": [],
- "source": [
- "import numpy as np\n",
- "\n",
- "\n",
- "def reduce_exposure_by_depth(exposures, reduction_factor=0.2, threshold=2.0, **kwargs):\n",
- " \"\"\"\n",
- " Reduces asset value by a percentage, but only for assets located where\n",
- " the peak hazard intensity (e.g., water depth) exceeds the threshold.\n",
- " \"\"\"\n",
- " # Retrieve the hazard context passed by Measure.apply\n",
- " base_hazard = kwargs.get(\"base_hazard\")\n",
- "\n",
- " # Get the peak intensity for each centroid across all events\n",
- " peak_depths = np.array(base_hazard.intensity.max(axis=0).todense()).flatten()\n",
- "\n",
- " # Ensure exposures are mapped to hazard centroids\n",
- " if \"centr_ID\" not in exposures.gdf.columns:\n",
- " exposures.assign_centroids(base_hazard)\n",
- "\n",
- " # Identify which exposure points meet the depth criteria\n",
- " # 'centr_ID' maps each exposure row to a column index in\n",
- " # the hazard intensity matrix\n",
- " centr_indices = exposures.gdf[\"centr_ID\"].values\n",
- " point_peak_depths = peak_depths[centr_indices]\n",
- "\n",
- " # We apply the reduction only where Depth > threshold\n",
- " mask = point_peak_depths > threshold\n",
- " exposures.gdf.loc[mask, \"value\"] *= 1 - reduction_factor\n",
- "\n",
- " return exposures"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "10a5ad67",
- "metadata": {},
- "source": [
- "```python\n",
- "# --- Usage Example ---\n",
- "# Define the measure\n",
- "flood_protection = Measure(\n",
- " name=\"High-Threshold Value Protection\",\n",
- " exposures_changes=reduce_exposure_by_depth\n",
- ")\n",
- "\n",
- "# When you call apply, the `Hazard` haz is automatically passed to the function\n",
- "# as 'base_hazard'\n",
- "new_exp, _, _ = flood_protection.apply(\n",
- " exp, impf, haz,\n",
- " kwargs_exposures={'reduction_factor': 0.5, 'threshold': 2.0}\n",
- ")\n",
- "\n",
- "# You can also overide this with another \"base\" hazard in the\n",
- "# `kwarg_exposures` if you want.\n",
- "#\n",
- "# This could be for instance if the flood protection is\n",
- "# now defined not for the full probabilistic hazard set\n",
- "# but for a specific event, or a subset.\n",
- "new_exp, _, _ = flood_protection.apply(\n",
- " exp, impf, haz,\n",
- " kwargs_exposures={'base_hazard'= other_hazard,\n",
- " 'reduction_factor': 0.5, 'threshold': 2.0}\n",
- ")\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "8baaf95c",
- "metadata": {},
- "source": [
- "```{note}\n",
- "The functions you define yourself are automatically made to accept keyword arguments if they do not already.\n",
- "```\n",
- "\n",
- "```{note}\n",
- "The functions do not necessarily need to use the Exposures|Hazard|ImpactFuncSet object, but they still have to accept it as an argument.\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "a984d0a7",
- "metadata": {},
- "source": [
- "### Deep copies\n",
- "\n",
- "To prevent accidental \"in-place\" modification of your baseline data, `Measure` utilizes `copy.deepcopy()`.\n",
- "If `enforce_copy=True` in `apply()` (the default case), the object is cloned before the transformation function runs, ensuring the original objects remains untouched."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "6d58543f",
- "metadata": {},
- "source": [
- "### Measure cash flow (`CostIncome`)\n",
- "\n",
- "The `cost_income` attribute allows the measure to carry its own. It can notably store:\n",
- "\n",
- "* Initial investment costs.\n",
- "* Maintenance costs over time.\n",
- "* Potential indirect income generated by the measure.\n",
- "\n",
- "For more information and advanced usage see the [detailed tutorial on cost income object](cost-income-tutorial)."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "b5c1873f",
- "metadata": {},
- "source": [
- "## Defining measures using measure configuration\n",
- "\n",
- "Defining adaptation measures with Python functions can be difficult if you are not familiar with Python. For common changes, a `Measure` can instead be described *declaratively* with a `MeasureConfig` object. A configuration can also be written to, and read from, a YAML file (see the [detailed tutorial](measure-config-tutorial)).\n",
- "\n",
- "A `MeasureConfig` aggregates one configuration object per component of the measure:\n",
- "\n",
- "| Field | Class | Main parameters |\n",
- "|---|---|---|\n",
- "| `hazard_modifier` | `HazardModifierConfig` | `haz_int_mult`, `haz_int_add`, `impact_rp_cutoff`, `new_hazard_path` |\n",
- "| `impfset_modifier` | `ImpfsetModifierConfig` | `impf_ids`, `impf_{mdd,paa,int}_{mult,add}`, `new_impfset_path` |\n",
- "| `exposures_modifier` | `ExposuresModifierConfig` | `reassign_impf_id`, `new_exposures_path` |\n",
- "| `cost_income` | `CostIncomeConfig` | same parameters as `CostIncome` |\n",
- "\n",
- "All parameters default to \"no change\" (multipliers of `1.0`, offsets of `0.0`, `None` otherwise). A `new_*_path` loads a replacement object from file; if modifiers are also given, they are applied after the replacement (and a warning is issued)."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "4e7a2c91",
- "metadata": {},
- "source": [
- "### Creating a measure from a configuration\n",
- "\n",
- "The measure below reduces the mean damage degree of impact function `1` by 25 %, and removes the events that are more frequent than a 1-in-20-year event (ranked by their **impact** in the baseline situation not their intensity).\n",
- "`Measure.from_config` converts the configuration into the corresponding transformation functions."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "id": "c3d90b57",
- "metadata": {},
- "outputs": [],
- "source": [
- "from climada.entity.measures.measure import Measure\n",
- "from climada.entity.measures.measure_config import (\n",
- " CostIncomeConfig,\n",
- " ExposuresModifierConfig,\n",
- " HazardModifierConfig,\n",
- " ImpfsetModifierConfig,\n",
- " MeasureConfig,\n",
- ")\n",
- "\n",
- "config = MeasureConfig(\n",
- " name=\"Flood proofing and protection standard\",\n",
- " haz_type=\"FL\",\n",
- " impfset_modifier=ImpfsetModifierConfig(\n",
- " haz_type=\"FL\", impf_ids=1, impf_mdd_mult=0.75\n",
- " ),\n",
- " hazard_modifier=HazardModifierConfig(haz_type=\"FL\", impact_rp_cutoff=20),\n",
- " exposures_modifier=ExposuresModifierConfig(), # no change, note that you can also just leave it out entirely\n",
- " cost_income=CostIncomeConfig(init_cost=1_000_000, periodic_cost=50_000),\n",
- " color_rgb=(0.1, 0.4, 0.8),\n",
- ")\n",
- "\n",
- "measure = Measure.from_config(config)"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "a81f6d02",
- "metadata": {},
- "source": [
- "The resulting `Measure` is used like any other. The `base_*` triplet needed by `impact_rp_cutoff` is provided automatically by `Measure.apply`.\n",
- "\n",
- "```python\n",
- "imp_with_measure = measure.calc_impact(exp, impfset, haz)\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "824e1dd3",
- "metadata": {},
- "source": [
- "## Advanced examples of measures"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "f1a15910-6de8-416f-982f-dd0e53fc13cf",
- "metadata": {},
- "source": [
- "Measures are often modelled as a fixed change. In practice, the effect of an adaptation action often depends on **when** it is evaluated (for instance, a relocation programme that is phased in over several years) or on **additional parameters** (regions targeted, share of value removed, protection threshold).\n",
- "\n",
- "The examples below illustrate three patterns:\n",
- "\n",
- "1. **Exposures**: a linear phase-in of asset relocation (`year`, `start_year`, `full_year`, `fraction`, `regions`).\n",
- "2. **Impact functions**: a year-dependent reduction of the mean damage ratio.\n",
- "3. **Hazard**: a step-function protection standard that needs the baseline impacts, and therefore the `base_*` triplet."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "id": "7e17dfb9-b760-48df-8878-be1d547290a3",
- "metadata": {},
- "outputs": [],
- "source": [
- "import numpy as np\n",
- "\n",
- "\n",
- "def phase_in(year, start_year, full_year):\n",
- " \"\"\"Linear effectiveness ramp. (Helper function)\n",
- "\n",
- " Parameters\n",
- " ----------\n",
- " year : int\n",
- " Year at which the measure is evaluated.\n",
- " start_year : int\n",
- " Year at which the measure starts having an effect.\n",
- " full_year : int\n",
- " Year at which the measure is fully effective.\n",
- "\n",
- " Returns\n",
- " -------\n",
- " float\n",
- " Effectiveness in [0, 1].\n",
- " \"\"\"\n",
- " return float(np.clip((year - start_year) / (full_year - start_year), 0, 1))\n",
- "\n",
- "\n",
- "def relocate_assets(exposures, *, year, start_year, full_year, fraction, regions):\n",
- " \"\"\"Progressively remove asset value from exposed regions.\n",
- "\n",
- " Parameters\n",
- " ----------\n",
- " exposures : climada.entity.exposures.base.Exposures\n",
- " Exposures to modify (already a copy if ``enforce_copy=True``).\n",
- " year : int\n",
- " Evaluation year.\n",
- " start_year, full_year : int\n",
- " Start and end of the linear phase-in.\n",
- " fraction : float\n",
- " Share of value removed once the measure is fully implemented.\n",
- " regions : list of int\n",
- " ``region_id`` values targeted by the measure.\n",
- "\n",
- " Returns\n",
- " -------\n",
- " climada.entity.exposures.base.Exposures\n",
- " Exposures with reduced value in the targeted regions.\n",
- " \"\"\"\n",
- " ramp = phase_in(year, start_year, full_year)\n",
- " mask = exposures.gdf[\"region_id\"].isin(regions)\n",
- " exposures.gdf.loc[mask, \"value\"] *= 1 - fraction * ramp\n",
- " return exposures\n",
- "\n",
- "\n",
- "def flood_proofing(\n",
- " impfset, *, year, start_year, full_year, reduction, haz_type=\"FL\", impf_id=1\n",
- "):\n",
- " \"\"\"Scale down the mean damage ratio of one impact function.\n",
- "\n",
- " Parameters\n",
- " ----------\n",
- " impfset : climada.entity.impact_funcs.impact_func_set.ImpactFuncSet\n",
- " Impact function set to modify.\n",
- " year : int\n",
- " Evaluation year.\n",
- " start_year, full_year : int\n",
- " Start and end of the linear phase-in.\n",
- " reduction : float\n",
- " Relative MDD reduction once fully effective.\n",
- " haz_type : str, optional\n",
- " Hazard type of the targeted function.\n",
- " impf_id : int, optional\n",
- " Id of the targeted function.\n",
- "\n",
- " Returns\n",
- " -------\n",
- " climada.entity.impact_funcs.impact_func_set.ImpactFuncSet\n",
- " Impact function set with the modified function.\n",
- " \"\"\"\n",
- " ramp = phase_in(year, start_year, full_year)\n",
- " impf = impfset.get_func(haz_type=haz_type, fun_id=impf_id)\n",
- " impf.mdd = impf.mdd * (1 - reduction * ramp)\n",
- " return impfset\n",
- "\n",
- "\n",
- "def protection_standard(\n",
- " hazard, *, year, start_year, threshold, base_exposures, base_impfset, base_hazard\n",
- "):\n",
- " \"\"\"Remove events whose baseline impact is below a protection threshold.\n",
- "\n",
- " Parameters\n",
- " ----------\n",
- " hazard : climada.hazard.base.Hazard\n",
- " Hazard to modify.\n",
- " year : int\n",
- " Evaluation year.\n",
- " start_year : int\n",
- " Year the protection is in place (no effect before).\n",
- " threshold : float\n",
- " Events with baseline total impact below this value are removed.\n",
- " base_exposures, base_impfset, base_hazard\n",
- " Unmodified triplet, provided by ``Measure.apply``.\n",
- "\n",
- " Returns\n",
- " -------\n",
- " climada.hazard.base.Hazard\n",
- " Hazard without the protected events. Frequencies of the remaining\n",
- " events are left unchanged.\n",
- " \"\"\"\n",
- " from climada.engine.impact_calc import ImpactCalc\n",
- "\n",
- " if year < start_year:\n",
- " return hazard\n",
- " imp = ImpactCalc(base_exposures, base_impfset, base_hazard).impact(\n",
- " assign_centroids=False\n",
- " )\n",
- " return hazard.select(event_id=imp.event_id[imp.at_event >= threshold])"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "937ded7c-05be-4708-ad08-8d366e287406",
- "metadata": {},
- "source": [
- "The parameters can then be provided when the measure is applied, or the impact is computed:"
- ]
- },
{
"cell_type": "code",
"execution_count": null,
- "id": "2bcbb4ec-701d-4e38-9321-e86a7dea9538",
- "metadata": {},
- "outputs": [],
- "source": [
- "from climada.entity.measures.measure import Measure\n",
- "\n",
- "meas_relocation = Measure(\n",
- " \"relocation\",\n",
- " exposures_changes=relocate_assets,\n",
- ")\n",
- "\n",
- "meas_relocation_and_standard = Measure(\n",
- " \"relocation_and_standard\",\n",
- " exposures_changes=relocate_assets,\n",
- " hazard_changes=protection_standard,\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "4e2ea806-8bca-4681-8543-750b9b303a59",
- "metadata": {},
- "source": [
- "```python\n",
- "exp_relocated, _, _ = meas_relocation.apply(\n",
- " exp,\n",
- " impfset,\n",
- " haz,\n",
- " kwargs_exposures=dict(\n",
- " year=2030, start_year=2025, full_year=2045, fraction=0.3, regions=[1, 2]\n",
- " ),\n",
- ")\n",
- "\n",
- "imp_with_relocation_and_standards = meas_relocation_and_standard.calc_impact(\n",
- " exp,\n",
- " impfset,\n",
- " haz,\n",
- " kwargs_exposures=dict(\n",
- " year=2030, start_year=2025, full_year=2045, fraction=0.3, regions=[1, 2]\n",
- " ),\n",
- " kwargs_hazard=dict(year=2030, start_year=2025, full_year=2045, threshold=100000),\n",
- ")\n",
- "```"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "fabfd29c-0b5e-442a-912f-3cbfac45f78d",
- "metadata": {},
- "source": [
- "## Using `MeasureSet` objects\n",
- "\n",
- "`MeasureSet` are used to group a set of measure together, notably to compare them in a cost benefit analysis.\n",
- "\n",
- "`MeasureSet` are build from a list of measures, taking the example from the quickstart:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "id": "04e747b4-f856-43c3-ba43-a1878c112afe",
+ "id": "da2aeb5c",
"metadata": {},
"outputs": [],
"source": [
- "from climada.entity.measures.measure import Measure\n",
- "from climada.entity.measures.measure_set import MeasureSet\n",
- "from climada.entity.measures.cost_income import CostIncome\n",
- "\n",
- "\n",
- "# Define the effect with python functions\n",
- "def targeted_campaign(impact_func_set, impf_id, rescale):\n",
- " \"\"\"Rescales the impact function with id impf_id\"\"\"\n",
- " impact_func_set.get_func(fun_id=imp_id).mdd *= rescale\n",
- " return impact_func_set\n",
- "\n",
- "\n",
- "def building_retrofit(exposures, reduction_factor):\n",
- " \"\"\"Reduces the value of all assets by a fixed percentage.\"\"\"\n",
- " exposures.gdf[\"value\"] *= 1 - reduction_factor\n",
- " return exposures\n",
- "\n",
- "\n",
- "def sea_wall_protection(hazard, reduction):\n",
- " \"\"\"Reduces flood intensity by an absolute amount.\"\"\"\n",
- " hazard.intensity.data -= reduction\n",
- " return hazard\n",
- "\n",
- "\n",
- "# Initialize the Measure objects\n",
- "campaign = Measure(\n",
- " name=\"Targeted awareness campaign\",\n",
- " impfset_changes=targeted_campaign,\n",
- " cost_income=CostIncome(init_cost=500_000),\n",
- ")\n",
- "\n",
- "retrofitting_and_seawall = Measure(\n",
- " name=\"Building Retrofit and seawall construction\",\n",
- " exposures_changes=building_retrofit,\n",
- " hazard_changes=sea_wall_protection,\n",
- " cost_income=CostIncome(init_cost=1_000_000),\n",
- ")\n",
- "\n",
- "meas_set = MeasureSet([campaign, retrofitting_and_seawall])"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "024822ed-53fc-406e-a9f0-012b244113c8",
- "metadata": {},
- "source": [
- "You can list the measures names of a set, get its size, check if it contains a measure (by name), `append()` an additional measure, `select()` them by their name."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "id": "2d532d22-6b74-45a4-8b14-3e7b9ede2def",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "There are 2 measures in the set:\n",
- " ['Targeted awareness campaign', 'Building Retrofit and seawall construction']\n",
- "True\n",
- "False\n"
- ]
- }
- ],
- "source": [
- "print(f\"There are {len(meas_set)} measures in the set:\\n\", meas_set.names)\n",
- "print(\"Targeted awareness campaign\" in meas_set)\n",
- "print(\"Mangroves\" in meas_set)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "id": "cdfc53b8-5da4-4b72-8fa5-7a7f5e18a6ea",
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "{'Targeted awareness campaign': }"
- ]
- },
- "execution_count": 8,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "meas_set.select(\"Targeted awareness campaign\")"
- ]
- },
- {
- "cell_type": "markdown",
- "id": "8a29d4b4-983f-4fbd-894b-01716c899dfc",
- "metadata": {},
- "source": [
- "You can also combine or compose measures of a `MeasureSet` into one `Measure`.\n",
- "\n",
- "1. *Combining* (`.combine()`) merges the effects of all the measures, taking the best possible effect when multiple measures affect the same dimension.\n",
- "\n",
- "For instance:\n",
- "- When several measures of the set reduce the intensity of the hazard, the combined effect will be the point-wise highest reduction across all measures.\n",
- "- When several measures modify the same impact function, the combined measure uses the point-wise minimum of all the modified impact functions.\n",
- "\n",
- "2. *Composing* (`.compose()`) does the mathematical composition of the measures, i.e., for a list of measures `[m1, m2, m3]`, the resulting transformation is `m1(m2(m3(x)))`. Note that this means measure `m3` is applied first, and the order of list of measures given to the `MeasureSet` matters!"
+ "{\n",
+ " \"cells\": [\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"adc83a35\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"(measure-tutorial)=\\n\",\n",
+ " \"\\n\",\n",
+ " \"# Defining Adaptation Measures with python functions\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"3dca8f82\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"The `Measure` object is the core component of the **Adaptation** module in CLIMADA. It acts as a wrapper for transformation logic, allowing you to model how specific interventions alter the risk landscape (like building sea walls, retrofitting houses, or implementing early warning systems).\\n\",\n",
+ " \"\\n\",\n",
+ " 'The primary purpose of a `Measure` is to modify one or more parts of the \"Risk Triplet\": **Exposures**, **Impact Functions**, and **Hazard**.\\n',\n",
+ " \"\\n\",\n",
+ " \"`Measure` objects can be regrouped into a set with the `MeasureSet` object.\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"f51842c7\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\"## Quickstart\"],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"d369e8bd-1f00-4435-9b35-ef03bba2af97\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"Assume you want to model two measures as follow:\\n\",\n",
+ " \"- The first one is a targeted awareness campaign, which effect is modelled as a change of one specific impact function. Costing 500'000.\\n\",\n",
+ " \"- The second one is both building a sea wall which effect is modelled as a 0.2 meter reduction of the flood height everywhere, and a building retrofit, modelled as an overal reduction of the value at risk of the exposure. Costing 1M.\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 1,\n",
+ " \"id\": \"2c48b021\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [],\n",
+ " \"source\": [\n",
+ " \"from climada.entity.measures import Measure, CostIncome, MeasureSet\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"# Define the effect with python functions\\n\",\n",
+ " \"def targeted_campaign(impact_func_set, impf_id, rescale):\\n\",\n",
+ " ' \"\"\"Rescales the impact function with id impf_id\"\"\"\\n',\n",
+ " \" impact_func_set.get_func(fun_id=imp_id).mdd *= rescale\\n\",\n",
+ " \" return impact_func_set\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"def building_retrofit(exposures, reduction_factor):\\n\",\n",
+ " ' \"\"\"Reduces the value of all assets by a fixed percentage.\"\"\"\\n',\n",
+ " ' exposures.gdf[\"value\"] *= 1 - reduction_factor\\n',\n",
+ " \" return exposures\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"def sea_wall_protection(hazard, reduction):\\n\",\n",
+ " ' \"\"\"Reduces flood intensity by an absolute amount.\"\"\"\\n',\n",
+ " \" hazard.intensity.data -= reduction\\n\",\n",
+ " \" return hazard\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"# Initialize the Measure objects\\n\",\n",
+ " \"campaign = Measure(\\n\",\n",
+ " ' name=\"Targeted awareness campaign\",\\n',\n",
+ " \" impfset_changes=targeted_campaign,\\n\",\n",
+ " \" cost_income=CostIncome(init_cost=500_000),\\n\",\n",
+ " \")\\n\",\n",
+ " \"\\n\",\n",
+ " \"retrofitting_and_seawall = Measure(\\n\",\n",
+ " ' name=\"Building Retrofit and seawall construction\",\\n',\n",
+ " \" exposures_changes=building_retrofit,\\n\",\n",
+ " \" hazard_changes=sea_wall_protection,\\n\",\n",
+ " \" cost_income=CostIncome(init_cost=1_000_000),\\n\",\n",
+ " \")\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"02cd2812\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"The measure can then be used either to create a new triplet corresponding to the situation with measure in effect\\n\",\n",
+ " \"or to compute the impact directly (both lines are independent):\\n\",\n",
+ " \"\\n\",\n",
+ " \"```python\\n\",\n",
+ " \"new_exp, new_impf, new_haz = retrofitting_and_seawall.apply(exp, impf, haz, kwargs_exposure={'reduction_factor': 0.1}, kwargs_hazard={'reduction': 0.3})\\n\",\n",
+ " \"\\n\",\n",
+ " \"# or directly:\\n\",\n",
+ " \"impact_with_campaign_active = campaign.calc_impact(exp, impf, haz, kwargs_impfset={'rescale': 0.2})\\n\",\n",
+ " \"```\\n\",\n",
+ " \"\\n\",\n",
+ " \"Much like impact functions, the measure can also be regrouped into a set with `MeasureSet`:\\n\",\n",
+ " \"\\n\",\n",
+ " \"```python\\n\",\n",
+ " \"\\n\",\n",
+ " \"meas_set = MeasureSet([campaign, retrofitting_and_seawall])\\n\",\n",
+ " \"```\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"6f4650c6\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"## Core logic of the Measure object\\n\",\n",
+ " \"\\n\",\n",
+ " \"When you initialize a `Measure`, you can provide one to three callables (python functions):\\n\",\n",
+ " \"\\n\",\n",
+ " \"1. **`exposures_changes`**: Modifies the `Exposures` object, the asset location, value, or metadata (e.g., relocating houses from a flood zone).\\n\",\n",
+ " \"2. **`impfset_changes`**: Modifies the `ImpactFuncSet` object, the vulnerability of assets (e.g., updating an impact function because houses were elevated).\\n\",\n",
+ " \"3. **`hazard_changes`**: Modifies the `Hazard` object, the intensity or frequency of events (e.g., a dam reducing flood depths).\\n\",\n",
+ " \"\\n\",\n",
+ " \"```{note}\\n\",\n",
+ " 'By default, changes are the `identity_function`, so if you do not provide a function e.g., \"exposures_changes\" the measure once applied will return the exposure unchanged.\\n',\n",
+ " \"```\\n\",\n",
+ " \"\\n\",\n",
+ " 'The **only** requirement for these python functions is to **take** and **return** an `Exposures` object (or `ImpactFuncSet` or `Hazard` objects respectively). This enables high modeling flexibility where changes from an adaptation option can be modelled from a simple \"loading another file for the hazard or exposure\", to an advanced function.\\n',\n",
+ " \"\\n\",\n",
+ " \"One thing to note, is that the `apply()` method of a measure always passes the entire triplet (`base_exposures`, `base_impfset`, `base_hazard`, i.e., the risk triplet before the measure is applied) to every transformation function. This means you can easily write complex logic that relie on any of these component. For example:\\n\",\n",
+ " \"\\n\",\n",
+ " '> \"Only reduce the exposure value (Exposure change) in areas where the peak water depth (Hazard context) exceeds 2 meters.\"\\n',\n",
+ " \"\\n\",\n",
+ " \"Could be defined like this:\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 2,\n",
+ " \"id\": \"7cd53344\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [],\n",
+ " \"source\": [\n",
+ " \"import numpy as np\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"def reduce_exposure_by_depth(exposures, reduction_factor=0.2, threshold=2.0, **kwargs):\\n\",\n",
+ " ' \"\"\"\\n',\n",
+ " \" Reduces asset value by a percentage, but only for assets located where\\n\",\n",
+ " \" the peak hazard intensity (e.g., water depth) exceeds the threshold.\\n\",\n",
+ " ' \"\"\"\\n',\n",
+ " \" # Retrieve the hazard context passed by Measure.apply\\n\",\n",
+ " ' base_hazard = kwargs.get(\"base_hazard\")\\n',\n",
+ " \"\\n\",\n",
+ " \" # Get the peak intensity for each centroid across all events\\n\",\n",
+ " \" peak_depths = np.array(base_hazard.intensity.max(axis=0).todense()).flatten()\\n\",\n",
+ " \"\\n\",\n",
+ " \" # Ensure exposures are mapped to hazard centroids\\n\",\n",
+ " ' if \"centr_ID\" not in exposures.gdf.columns:\\n',\n",
+ " \" exposures.assign_centroids(base_hazard)\\n\",\n",
+ " \"\\n\",\n",
+ " \" # Identify which exposure points meet the depth criteria\\n\",\n",
+ " \" # 'centr_ID' maps each exposure row to a column index in\\n\",\n",
+ " \" # the hazard intensity matrix\\n\",\n",
+ " ' centr_indices = exposures.gdf[\"centr_ID\"].values\\n',\n",
+ " \" point_peak_depths = peak_depths[centr_indices]\\n\",\n",
+ " \"\\n\",\n",
+ " \" # We apply the reduction only where Depth > threshold\\n\",\n",
+ " \" mask = point_peak_depths > threshold\\n\",\n",
+ " ' exposures.gdf.loc[mask, \"value\"] *= 1 - reduction_factor\\n',\n",
+ " \"\\n\",\n",
+ " \" return exposures\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"10a5ad67\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"```python\\n\",\n",
+ " \"# --- Usage Example ---\\n\",\n",
+ " \"# Define the measure\\n\",\n",
+ " \"flood_protection = Measure(\\n\",\n",
+ " ' name=\"High-Threshold Value Protection\",\\n',\n",
+ " \" exposures_changes=reduce_exposure_by_depth\\n\",\n",
+ " \")\\n\",\n",
+ " \"\\n\",\n",
+ " \"# When you call apply, the `Hazard` haz is automatically passed to the function\\n\",\n",
+ " \"# as 'base_hazard'\\n\",\n",
+ " \"new_exp, _, _ = flood_protection.apply(\\n\",\n",
+ " \" exp, impf, haz,\\n\",\n",
+ " \" kwargs_exposures={'reduction_factor': 0.5, 'threshold': 2.0}\\n\",\n",
+ " \")\\n\",\n",
+ " \"\\n\",\n",
+ " '# You can also overide this with another \"base\" hazard in the\\n',\n",
+ " \"# `kwarg_exposures` if you want.\\n\",\n",
+ " \"#\\n\",\n",
+ " \"# This could be for instance if the flood protection is\\n\",\n",
+ " \"# now defined not for the full probabilistic hazard set\\n\",\n",
+ " \"# but for a specific event, or a subset.\\n\",\n",
+ " \"new_exp, _, _ = flood_protection.apply(\\n\",\n",
+ " \" exp, impf, haz,\\n\",\n",
+ " \" kwargs_exposures={'base_hazard'= other_hazard,\\n\",\n",
+ " \" 'reduction_factor': 0.5, 'threshold': 2.0}\\n\",\n",
+ " \")\\n\",\n",
+ " \"```\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"8baaf95c\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"```{note}\\n\",\n",
+ " \"The functions you define yourself are automatically made to accept keyword arguments if they do not already.\\n\",\n",
+ " \"```\\n\",\n",
+ " \"\\n\",\n",
+ " \"```{note}\\n\",\n",
+ " \"The functions do not necessarily need to use the Exposures|Hazard|ImpactFuncSet object, but they still have to accept it as an argument.\\n\",\n",
+ " \"```\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"a984d0a7\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"### Deep copies\\n\",\n",
+ " \"\\n\",\n",
+ " 'To prevent accidental \"in-place\" modification of your baseline data, `Measure` utilizes `copy.deepcopy()`.\\n',\n",
+ " \"If `enforce_copy=True` in `apply()` (the default case), the object is cloned before the transformation function runs, ensuring the original objects remains untouched.\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"6d58543f\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"### Measure cash flow (`CostIncome`)\\n\",\n",
+ " \"\\n\",\n",
+ " \"The `cost_income` attribute allows the measure to carry its own. It can notably store:\\n\",\n",
+ " \"\\n\",\n",
+ " \"* Initial investment costs.\\n\",\n",
+ " \"* Maintenance costs over time.\\n\",\n",
+ " \"* Potential indirect income generated by the measure.\\n\",\n",
+ " \"\\n\",\n",
+ " \"For more information and advanced usage see the [detailed tutorial on cost income object](cost-income-tutorial).\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"b5c1873f\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"## Defining measures using measure configuration\\n\",\n",
+ " \"\\n\",\n",
+ " \"Defining adaptation measures with Python functions can be difficult if you are not familiar with Python. For common changes, a `Measure` can instead be described *declaratively* with a `MeasureConfig` object. A configuration can also be written to, and read from, a YAML file (see the [detailed tutorial](measure-config-tutorial)).\\n\",\n",
+ " \"\\n\",\n",
+ " \"A `MeasureConfig` aggregates one configuration object per component of the measure:\\n\",\n",
+ " \"\\n\",\n",
+ " \"| Field | Class | Main parameters |\\n\",\n",
+ " \"|---|---|---|\\n\",\n",
+ " \"| `hazard_modifier` | `HazardModifierConfig` | `haz_int_mult`, `haz_int_add`, `impact_rp_cutoff`, `new_hazard_path` |\\n\",\n",
+ " \"| `impfset_modifier` | `ImpfsetModifierConfig` | `impf_ids`, `impf_{mdd,paa,int}_{mult,add}`, `new_impfset_path` |\\n\",\n",
+ " \"| `exposures_modifier` | `ExposuresModifierConfig` | `reassign_impf_id`, `new_exposures_path` |\\n\",\n",
+ " \"| `cost_income` | `CostIncomeConfig` | same parameters as `CostIncome` |\\n\",\n",
+ " \"\\n\",\n",
+ " 'All parameters default to \"no change\" (multipliers of `1.0`, offsets of `0.0`, `None` otherwise). A `new_*_path` loads a replacement object from file; if modifiers are also given, they are applied after the replacement (and a warning is issued).',\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"4e7a2c91\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"### Creating a measure from a configuration\\n\",\n",
+ " \"\\n\",\n",
+ " \"The measure below reduces the mean damage degree of impact function `1` by 25 %, and removes the events that are more frequent than a 1-in-20-year event (ranked by their **impact** in the baseline situation not their intensity).\\n\",\n",
+ " \"`Measure.from_config` converts the configuration into the corresponding transformation functions.\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 3,\n",
+ " \"id\": \"c3d90b57\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [],\n",
+ " \"source\": [\n",
+ " \"from climada.entity.measures.measure_config import (\\n\",\n",
+ " \" CostIncomeConfig,\\n\",\n",
+ " \" ExposuresModifierConfig,\\n\",\n",
+ " \" HazardModifierConfig,\\n\",\n",
+ " \" ImpfsetModifierConfig,\\n\",\n",
+ " \" MeasureConfig,\\n\",\n",
+ " \")\\n\",\n",
+ " \"\\n\",\n",
+ " \"config = MeasureConfig(\\n\",\n",
+ " ' name=\"Flood proofing and protection standard\",\\n',\n",
+ " ' haz_type=\"FL\",\\n',\n",
+ " \" impfset_modifier=ImpfsetModifierConfig(\\n\",\n",
+ " ' haz_type=\"FL\", impf_ids=1, impf_mdd_mult=0.75\\n',\n",
+ " \" ),\\n\",\n",
+ " ' hazard_modifier=HazardModifierConfig(haz_type=\"FL\", impact_rp_cutoff=20),\\n',\n",
+ " \" exposures_modifier=ExposuresModifierConfig(), # no change, note that you can also just leave it out entirely\\n\",\n",
+ " \" cost_income=CostIncomeConfig(init_cost=1_000_000, periodic_cost=50_000),\\n\",\n",
+ " \" color_rgb=(0.1, 0.4, 0.8),\\n\",\n",
+ " \")\\n\",\n",
+ " \"\\n\",\n",
+ " \"measure = Measure.from_config(config)\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"a81f6d02\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"The resulting `Measure` is used like any other. The `base_*` triplet needed by `impact_rp_cutoff` is provided automatically by `Measure.apply`.\\n\",\n",
+ " \"\\n\",\n",
+ " \"```python\\n\",\n",
+ " \"imp_with_measure = measure.calc_impact(exp, impfset, haz)\\n\",\n",
+ " \"```\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"824e1dd3\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\"## Advanced examples of measures\"],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"f1a15910-6de8-416f-982f-dd0e53fc13cf\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"Measures are often modelled as a fixed change. In practice, the effect of an adaptation action often depends on **when** it is evaluated (for instance, a relocation programme that is phased in over several years) or on **additional parameters** (regions targeted, share of value removed, protection threshold).\\n\",\n",
+ " \"\\n\",\n",
+ " \"The examples below illustrate three patterns:\\n\",\n",
+ " \"\\n\",\n",
+ " \"1. **Exposures**: a linear phase-in of asset relocation (`year`, `start_year`, `full_year`, `fraction`, `regions`).\\n\",\n",
+ " \"2. **Impact functions**: a year-dependent reduction of the mean damage ratio.\\n\",\n",
+ " \"3. **Hazard**: a step-function protection standard that needs the baseline impacts, and therefore the `base_*` triplet.\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 4,\n",
+ " \"id\": \"7e17dfb9-b760-48df-8878-be1d547290a3\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [],\n",
+ " \"source\": [\n",
+ " \"def phase_in(year, start_year, full_year):\\n\",\n",
+ " ' \"\"\"Linear effectiveness ramp. (Helper function)\\n',\n",
+ " \"\\n\",\n",
+ " \" Parameters\\n\",\n",
+ " \" ----------\\n\",\n",
+ " \" year : int\\n\",\n",
+ " \" Year at which the measure is evaluated.\\n\",\n",
+ " \" start_year : int\\n\",\n",
+ " \" Year at which the measure starts having an effect.\\n\",\n",
+ " \" full_year : int\\n\",\n",
+ " \" Year at which the measure is fully effective.\\n\",\n",
+ " \"\\n\",\n",
+ " \" Returns\\n\",\n",
+ " \" -------\\n\",\n",
+ " \" float\\n\",\n",
+ " \" Effectiveness in [0, 1].\\n\",\n",
+ " ' \"\"\"\\n',\n",
+ " \" return float(np.clip((year - start_year) / (full_year - start_year), 0, 1))\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"def relocate_assets(exposures, *, year, start_year, full_year, fraction, regions):\\n\",\n",
+ " ' \"\"\"Progressively remove asset value from exposed regions.\\n',\n",
+ " \"\\n\",\n",
+ " \" Parameters\\n\",\n",
+ " \" ----------\\n\",\n",
+ " \" exposures : climada.entity.exposures.base.Exposures\\n\",\n",
+ " \" Exposures to modify (already a copy if ``enforce_copy=True``).\\n\",\n",
+ " \" year : int\\n\",\n",
+ " \" Evaluation year.\\n\",\n",
+ " \" start_year, full_year : int\\n\",\n",
+ " \" Start and end of the linear phase-in.\\n\",\n",
+ " \" fraction : float\\n\",\n",
+ " \" Share of value removed once the measure is fully implemented.\\n\",\n",
+ " \" regions : list of int\\n\",\n",
+ " \" ``region_id`` values targeted by the measure.\\n\",\n",
+ " \"\\n\",\n",
+ " \" Returns\\n\",\n",
+ " \" -------\\n\",\n",
+ " \" climada.entity.exposures.base.Exposures\\n\",\n",
+ " \" Exposures with reduced value in the targeted regions.\\n\",\n",
+ " ' \"\"\"\\n',\n",
+ " \" ramp = phase_in(year, start_year, full_year)\\n\",\n",
+ " ' mask = exposures.gdf[\"region_id\"].isin(regions)\\n',\n",
+ " ' exposures.gdf.loc[mask, \"value\"] *= 1 - fraction * ramp\\n',\n",
+ " \" return exposures\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"def flood_proofing(\\n\",\n",
+ " ' impfset, *, year, start_year, full_year, reduction, haz_type=\"FL\", impf_id=1\\n',\n",
+ " \"):\\n\",\n",
+ " ' \"\"\"Scale down the mean damage ratio of one impact function.\\n',\n",
+ " \"\\n\",\n",
+ " \" Parameters\\n\",\n",
+ " \" ----------\\n\",\n",
+ " \" impfset : climada.entity.impact_funcs.impact_func_set.ImpactFuncSet\\n\",\n",
+ " \" Impact function set to modify.\\n\",\n",
+ " \" year : int\\n\",\n",
+ " \" Evaluation year.\\n\",\n",
+ " \" start_year, full_year : int\\n\",\n",
+ " \" Start and end of the linear phase-in.\\n\",\n",
+ " \" reduction : float\\n\",\n",
+ " \" Relative MDD reduction once fully effective.\\n\",\n",
+ " \" haz_type : str, optional\\n\",\n",
+ " \" Hazard type of the targeted function.\\n\",\n",
+ " \" impf_id : int, optional\\n\",\n",
+ " \" Id of the targeted function.\\n\",\n",
+ " \"\\n\",\n",
+ " \" Returns\\n\",\n",
+ " \" -------\\n\",\n",
+ " \" climada.entity.impact_funcs.impact_func_set.ImpactFuncSet\\n\",\n",
+ " \" Impact function set with the modified function.\\n\",\n",
+ " ' \"\"\"\\n',\n",
+ " \" ramp = phase_in(year, start_year, full_year)\\n\",\n",
+ " \" impf = impfset.get_func(haz_type=haz_type, fun_id=impf_id)\\n\",\n",
+ " \" impf.mdd = impf.mdd * (1 - reduction * ramp)\\n\",\n",
+ " \" return impfset\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"def protection_standard(\\n\",\n",
+ " \" hazard, *, year, start_year, threshold, base_exposures, base_impfset, base_hazard\\n\",\n",
+ " \"):\\n\",\n",
+ " ' \"\"\"Remove events whose baseline impact is below a protection threshold.\\n',\n",
+ " \"\\n\",\n",
+ " \" Parameters\\n\",\n",
+ " \" ----------\\n\",\n",
+ " \" hazard : climada.hazard.base.Hazard\\n\",\n",
+ " \" Hazard to modify.\\n\",\n",
+ " \" year : int\\n\",\n",
+ " \" Evaluation year.\\n\",\n",
+ " \" start_year : int\\n\",\n",
+ " \" Year the protection is in place (no effect before).\\n\",\n",
+ " \" threshold : float\\n\",\n",
+ " \" Events with baseline total impact below this value are removed.\\n\",\n",
+ " \" base_exposures, base_impfset, base_hazard\\n\",\n",
+ " \" Unmodified triplet, provided by ``Measure.apply``.\\n\",\n",
+ " \"\\n\",\n",
+ " \" Returns\\n\",\n",
+ " \" -------\\n\",\n",
+ " \" climada.hazard.base.Hazard\\n\",\n",
+ " \" Hazard without the protected events. Frequencies of the remaining\\n\",\n",
+ " \" events are left unchanged.\\n\",\n",
+ " ' \"\"\"\\n',\n",
+ " \" from climada.engine.impact_calc import ImpactCalc\\n\",\n",
+ " \"\\n\",\n",
+ " \" if year < start_year:\\n\",\n",
+ " \" return hazard\\n\",\n",
+ " \" imp = ImpactCalc(base_exposures, base_impfset, base_hazard).impact(\\n\",\n",
+ " \" assign_centroids=False\\n\",\n",
+ " \" )\\n\",\n",
+ " \" return hazard.select(event_id=imp.event_id[imp.at_event >= threshold])\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"937ded7c-05be-4708-ad08-8d366e287406\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"The parameters can then be provided when the measure is applied, or the impact is computed:\"\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": null,\n",
+ " \"id\": \"2bcbb4ec-701d-4e38-9321-e86a7dea9538\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [],\n",
+ " \"source\": [\n",
+ " \"from climada.entity.measures import Measure\\n\",\n",
+ " \"\\n\",\n",
+ " \"meas_relocation = Measure(\\n\",\n",
+ " ' \"relocation\",\\n',\n",
+ " \" exposures_changes=relocate_assets,\\n\",\n",
+ " \")\\n\",\n",
+ " \"\\n\",\n",
+ " \"meas_relocation_and_standard = Measure(\\n\",\n",
+ " ' \"relocation_and_standard\",\\n',\n",
+ " \" exposures_changes=relocate_assets,\\n\",\n",
+ " \" hazard_changes=protection_standard,\\n\",\n",
+ " \")\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"4e2ea806-8bca-4681-8543-750b9b303a59\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"```python\\n\",\n",
+ " \"exp_relocated, _, _ = meas_relocation.apply(\\n\",\n",
+ " \" exp,\\n\",\n",
+ " \" impfset,\\n\",\n",
+ " \" haz,\\n\",\n",
+ " \" kwargs_exposures=dict(\\n\",\n",
+ " \" year=2030, start_year=2025, full_year=2045, fraction=0.3, regions=[1, 2]\\n\",\n",
+ " \" ),\\n\",\n",
+ " \")\\n\",\n",
+ " \"\\n\",\n",
+ " \"imp_with_relocation_and_standards = meas_relocation_and_standard.calc_impact(\\n\",\n",
+ " \" exp,\\n\",\n",
+ " \" impfset,\\n\",\n",
+ " \" haz,\\n\",\n",
+ " \" kwargs_exposures=dict(\\n\",\n",
+ " \" year=2030, start_year=2025, full_year=2045, fraction=0.3, regions=[1, 2]\\n\",\n",
+ " \" ),\\n\",\n",
+ " \" kwargs_hazard=dict(year=2030, start_year=2025, full_year=2045, threshold=100000),\\n\",\n",
+ " \")\\n\",\n",
+ " \"```\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"fabfd29c-0b5e-442a-912f-3cbfac45f78d\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"## Using `MeasureSet` objects\\n\",\n",
+ " \"\\n\",\n",
+ " \"`MeasureSet` are used to group a set of measure together, notably to compare them in a cost benefit analysis.\\n\",\n",
+ " \"\\n\",\n",
+ " \"`MeasureSet` are build from a list of measures, taking the example from the quickstart:\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 6,\n",
+ " \"id\": \"04e747b4-f856-43c3-ba43-a1878c112afe\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [],\n",
+ " \"source\": [\n",
+ " \"from climada.entity.measures.measure import Measure\\n\",\n",
+ " \"from climada.entity.measures.measure_set import MeasureSet\\n\",\n",
+ " \"from climada.entity.measures.cost_income import CostIncome\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"# Define the effect with python functions\\n\",\n",
+ " \"def targeted_campaign(impact_func_set, impf_id, rescale):\\n\",\n",
+ " ' \"\"\"Rescales the impact function with id impf_id\"\"\"\\n',\n",
+ " \" impact_func_set.get_func(fun_id=imp_id).mdd *= rescale\\n\",\n",
+ " \" return impact_func_set\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"def building_retrofit(exposures, reduction_factor):\\n\",\n",
+ " ' \"\"\"Reduces the value of all assets by a fixed percentage.\"\"\"\\n',\n",
+ " ' exposures.gdf[\"value\"] *= 1 - reduction_factor\\n',\n",
+ " \" return exposures\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"def sea_wall_protection(hazard, reduction):\\n\",\n",
+ " ' \"\"\"Reduces flood intensity by an absolute amount.\"\"\"\\n',\n",
+ " \" hazard.intensity.data -= reduction\\n\",\n",
+ " \" return hazard\\n\",\n",
+ " \"\\n\",\n",
+ " \"\\n\",\n",
+ " \"# Initialize the Measure objects\\n\",\n",
+ " \"campaign = Measure(\\n\",\n",
+ " ' name=\"Targeted awareness campaign\",\\n',\n",
+ " \" impfset_changes=targeted_campaign,\\n\",\n",
+ " \" cost_income=CostIncome(init_cost=500_000),\\n\",\n",
+ " \")\\n\",\n",
+ " \"\\n\",\n",
+ " \"retrofitting_and_seawall = Measure(\\n\",\n",
+ " ' name=\"Building Retrofit and seawall construction\",\\n',\n",
+ " \" exposures_changes=building_retrofit,\\n\",\n",
+ " \" hazard_changes=sea_wall_protection,\\n\",\n",
+ " \" cost_income=CostIncome(init_cost=1_000_000),\\n\",\n",
+ " \")\\n\",\n",
+ " \"\\n\",\n",
+ " \"meas_set = MeasureSet([campaign, retrofitting_and_seawall])\",\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"024822ed-53fc-406e-a9f0-012b244113c8\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"You can list the measures names of a set, get its size, check if it contains a measure (by name), `append()` an additional measure, `select()` them by their name.\"\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 7,\n",
+ " \"id\": \"2d532d22-6b74-45a4-8b14-3e7b9ede2def\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [\n",
+ " {\n",
+ " \"name\": \"stdout\",\n",
+ " \"output_type\": \"stream\",\n",
+ " \"text\": [\n",
+ " \"There are 2 measures in the set:\\n\",\n",
+ " \" ['Targeted awareness campaign', 'Building Retrofit and seawall construction']\\n\",\n",
+ " \"True\\n\",\n",
+ " \"False\\n\",\n",
+ " ],\n",
+ " }\n",
+ " ],\n",
+ " \"source\": [\n",
+ " 'print(f\"There are {len(meas_set)} measures in the set:\\\\n\", meas_set.names)\\n',\n",
+ " 'print(\"Targeted awareness campaign\" in meas_set)\\n',\n",
+ " 'print(\"Mangroves\" in meas_set)',\n",
+ " ],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 8,\n",
+ " \"id\": \"cdfc53b8-5da4-4b72-8fa5-7a7f5e18a6ea\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [\n",
+ " {\n",
+ " \"data\": {\n",
+ " \"text/plain\": [\n",
+ " \"{'Targeted awareness campaign': }\"\n",
+ " ]\n",
+ " },\n",
+ " \"execution_count\": 8,\n",
+ " \"metadata\": {},\n",
+ " \"output_type\": \"execute_result\",\n",
+ " }\n",
+ " ],\n",
+ " \"source\": ['meas_set.select(\"Targeted awareness campaign\")'],\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"8a29d4b4-983f-4fbd-894b-01716c899dfc\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"You can also combine or compose measures of a `MeasureSet` into one `Measure`.\\n\",\n",
+ " \"\\n\",\n",
+ " \"1. *Combining* (`.combine()`) merges the effects of all the measures, taking the best possible effect when multiple measures affect the same dimension.\\n\",\n",
+ " \"\\n\",\n",
+ " \"For instance:\\n\",\n",
+ " \"- When several measures of the set reduce the intensity of the hazard, the combined effect will be the point-wise highest reduction across all measures.\\n\",\n",
+ " \"- When several measures modify the same impact function, the combined measure uses the point-wise minimum of all the modified impact functions.\\n\",\n",
+ " \"\\n\",\n",
+ " \"2. *Composing* (`.compose()`) does the mathematical composition of the measures, i.e., for a list of measures `[m1, m2, m3]`, the resulting transformation is `m1(m2(m3(x)))`. Note that this means measure `m3` is applied first, and the order of list of measures given to the `MeasureSet` matters!\",\n",
+ " ],\n",
+ " },\n",
+ " ],\n",
+ " \"metadata\": {\n",
+ " \"kernelspec\": {\n",
+ " \"display_name\": \"Python [conda env:climada_env_dev]\",\n",
+ " \"language\": \"python\",\n",
+ " \"name\": \"conda-env-climada_env_dev-py\",\n",
+ " },\n",
+ " \"language_info\": {\n",
+ " \"codemirror_mode\": {\"name\": \"ipython\", \"version\": 3},\n",
+ " \"file_extension\": \".py\",\n",
+ " \"mimetype\": \"text/x-python\",\n",
+ " \"name\": \"python\",\n",
+ " \"nbconvert_exporter\": \"python\",\n",
+ " \"pygments_lexer\": \"ipython3\",\n",
+ " \"version\": \"3.11.15\",\n",
+ " },\n",
+ " },\n",
+ " \"nbformat\": 4,\n",
+ " \"nbformat_minor\": 5,\n",
+ "}"
]
}
],
"metadata": {
- "kernelspec": {
- "display_name": "Python [conda env:climada_env_dev]",
- "language": "python",
- "name": "conda-env-climada_env_dev-py"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.11.15"
+ "jupytext": {
+ "cell_metadata_filter": "-all",
+ "main_language": "python",
+ "notebook_metadata_filter": "-all"
}
},
"nbformat": 4,