Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion climada/entity/measures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
2 changes: 2 additions & 0 deletions climada/entity/measures/cost_income.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

from climada.entity.measures.measure_config import CostIncomeConfig

__all__ = ["CostIncome"]


class CostIncome:
"""
Expand Down
19 changes: 15 additions & 4 deletions climada/entity/measures/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)):
Expand Down
42 changes: 26 additions & 16 deletions climada/entity/measures/measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,33 @@
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,
helper_impfset,
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):
"""
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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\
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
25 changes: 14 additions & 11 deletions climada/entity/measures/measure_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -37,10 +36,10 @@
LOGGER = logging.getLogger(__name__)

__all__ = [
"HazardModifierConfig",
"CostIncomeConfig",
"ExposuresModifierConfig",
"HazardModifierConfig",
"ImpfsetModifierConfig",
"CostIncomeConfig",
"MeasureConfig",
]

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand All @@ -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.

Expand Down
21 changes: 21 additions & 0 deletions climada/entity/measures/types.py
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

---

Define useful types for the measure module.
"""

from collections.abc import Callable
from typing import Concatenate

Expand Down
11 changes: 11 additions & 0 deletions climada/test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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)
Loading
Loading