diff --git a/CHANGELOG.md b/CHANGELOG.md index e301836bd1..550d76f6a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,13 @@ Code freeze date: YYYY-MM-DD ### Added -- Better type hints and overloads signatures for ImpactFuncSet [#1250](https://github.com/CLIMADA-project/climada_python/pull/1250) +- Better type hints and overloads signatures for `ImpactFuncSet` [#1250](https://github.com/CLIMADA-project/climada_python/pull/1250) +- Adds `CostIncome` class and related tutorial for future new `Measure` class. [#1277](https://github.com/CLIMADA-project/climada_python/pull/1277) +- Adds `MeasureConfig` and related dataclasses for new `Measure` object retrocompatibility and (de)serialization capabilities [#1276](https://github.com/CLIMADA-project/climada_python/pull/1276) ### Changed - Updated Impact Calculation Tutorial (`doc.climada_engine_Impact.ipynb`) [#1095](https://github.com/CLIMADA-project/climada_python/pull/1095). +- Makes current `measure` module a legacy module, moving it to `_legacy_measure`, to retain compatibility with `CostBenefit` class and various tests. [#1274](https://github.com/CLIMADA-project/climada_python/pull/1274) ### Fixed diff --git a/climada/engine/test/test_cost_benefit.py b/climada/engine/test/test_cost_benefit.py index e48afec110..8b0276c665 100644 --- a/climada/engine/test/test_cost_benefit.py +++ b/climada/engine/test/test_cost_benefit.py @@ -33,10 +33,10 @@ risk_rp_100, risk_rp_250, ) +from climada.entity._legacy_measures import Measure +from climada.entity._legacy_measures.base import LOGGER as ILOG from climada.entity.disc_rates import DiscRates from climada.entity.entity_def import Entity -from climada.entity.measures import Measure -from climada.entity.measures.base import LOGGER as ILOG from climada.hazard.base import Hazard from climada.test import get_test_file from climada.util.api_client import Client diff --git a/climada/engine/unsequa/input_var.py b/climada/engine/unsequa/input_var.py index 76e63d766e..9abdd8d3fa 100644 --- a/climada/engine/unsequa/input_var.py +++ b/climada/engine/unsequa/input_var.py @@ -518,7 +518,7 @@ def ent( exp_list : [climada.entity.exposures.base.Exposure] The list of base exposure. Can be one or many to uniformly sample from. - meas_set : climada.entity.measures.measure_set.MeasureSet + meas_set : climada.entity._legacy_measures.measure_set.MeasureSet The base measures. haz_id_dict : dict Dictionary of the impact functions affected by uncertainty. @@ -660,7 +660,7 @@ def entfut( exp_list : [climada.entity.exposures.base.Exposure] The list of base exposure. Can be one or many to uniformly sample from. - meas_set : climada.entity.measures.measure_set.MeasureSet + meas_set : climada.entity._legacy_measures.measure_set.MeasureSet The base measures. haz_id_dict : dict Dictionary of the impact functions affected by uncertainty. diff --git a/climada/entity/__init__.py b/climada/entity/__init__.py index 7b830c2b70..ceb24ee065 100755 --- a/climada/entity/__init__.py +++ b/climada/entity/__init__.py @@ -19,8 +19,8 @@ init entity """ +from ._legacy_measures import * from .disc_rates import * from .entity_def import * from .exposures import * from .impact_funcs import * -from .measures import * diff --git a/climada/entity/measures/__init__.py b/climada/entity/_legacy_measures/__init__.py similarity index 100% rename from climada/entity/measures/__init__.py rename to climada/entity/_legacy_measures/__init__.py diff --git a/climada/entity/_legacy_measures/base.py b/climada/entity/_legacy_measures/base.py new file mode 100755 index 0000000000..4e539f9986 --- /dev/null +++ b/climada/entity/_legacy_measures/base.py @@ -0,0 +1,570 @@ +""" +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 Measure class. +""" + +__all__ = ["Measure"] + +import copy +import logging +from pathlib import Path +from typing import Optional, Tuple + +import numpy as np +import pandas as pd +from geopandas import GeoDataFrame + +import climada.util.checker as u_check +from climada.entity.exposures.base import INDICATOR_CENTR, INDICATOR_IMPF, Exposures +from climada.hazard.base import Hazard + +LOGGER = logging.getLogger(__name__) + +IMPF_ID_FACT = 1000 +"""Factor internally used as id for impact functions when region selected.""" + +NULL_STR = "nil" +"""String considered as no path in measures exposures_set and hazard_set or +no string in imp_fun_map""" + + +class Measure: + """ + Contains the definition of one measure. + + Attributes + ---------- + name : str + name of the measure + haz_type : str + related hazard type (peril), e.g. TC + color_rgb : np.array + integer array of size 3. Color code of this measure in RGB + cost : float + discounted cost (in same units as assets) + hazard_set : str + file name of hazard to use (in h5 format) + hazard_freq_cutoff : float + hazard frequency cutoff + exposures_set : str or climada.entity.Exposure + file name of exposure to use (in h5 format) or Exposure instance + imp_fun_map : str + change of impact function id of exposures, e.g. '1to3' + hazard_inten_imp : tuple(float, float) + parameter a and b of hazard intensity change + mdd_impact : tuple(float, float) + parameter a and b of the impact over the mean damage degree + paa_impact : tuple(float, float) + parameter a and b of the impact over the percentage of affected assets + exp_region_id : int + region id of the selected exposures to consider ALL the previous + parameters + risk_transf_attach : float + risk transfer attachment + risk_transf_cover : float + risk transfer cover + risk_transf_cost_factor : float + factor to multiply to resulting insurance layer to get the total + cost of risk transfer + """ + + def __init__( + self, + name: str = "", + haz_type: str = "", + cost: float = 0, + hazard_set: str = NULL_STR, + hazard_freq_cutoff: float = 0, + exposures_set: str = NULL_STR, + imp_fun_map: str = NULL_STR, + hazard_inten_imp: Tuple[float, float] = (1, 0), + mdd_impact: Tuple[float, float] = (1, 0), + paa_impact: Tuple[float, float] = (1, 0), + exp_region_id: Optional[list] = None, + risk_transf_attach: float = 0, + risk_transf_cover: float = 0, + risk_transf_cost_factor: float = 1, + color_rgb: Optional[np.ndarray] = None, + ): + """Initialize a Measure object with given values. + + Parameters + ---------- + name : str, optional + name of the measure + haz_type : str, optional + related hazard type (peril), e.g. TC + cost : float, optional + discounted cost (in same units as assets) + hazard_set : str, optional + file name of hazard to use (in h5 format) + hazard_freq_cutoff : float, optional + hazard frequency cutoff + exposures_set : str or climada.entity.Exposure, optional + file name of exposure to use (in h5 format) or Exposure instance + imp_fun_map : str, optional + change of impact function id of exposures, e.g. '1to3' + hazard_inten_imp : tuple(float, float), optional + parameter a and b of hazard intensity change + mdd_impact : tuple(float, float), optional + parameter a and b of the impact over the mean damage degree + paa_impact : tuple(float, float), optional + parameter a and b of the impact over the percentage of affected assets + exp_region_id : int, optional + region id of the selected exposures to consider ALL the previous + parameters + risk_transf_attach : float, optional + risk transfer attachment + risk_transf_cover : float, optional + risk transfer cover + risk_transf_cost_factor : float, optional + factor to multiply to resulting insurance layer to get the total + cost of risk transfer + color_rgb : np.array, optional + integer array of size 3. Color code of this measure in RGB. + Default is None (corresponds to black). + """ + self.name = name + self.haz_type = haz_type + self.color_rgb = np.array([0, 0, 0]) if color_rgb is None else color_rgb + self.cost = cost + + # related to change in hazard + self.hazard_set = hazard_set + self.hazard_freq_cutoff = hazard_freq_cutoff + + # related to change in exposures + self.exposures_set = exposures_set + self.imp_fun_map = imp_fun_map + + # related to change in impact functions + self.hazard_inten_imp = hazard_inten_imp + self.mdd_impact = mdd_impact + self.paa_impact = paa_impact + + # related to change in region + self.exp_region_id = [] if exp_region_id is None else exp_region_id + + # risk transfer + self.risk_transf_attach = risk_transf_attach + self.risk_transf_cover = risk_transf_cover + self.risk_transf_cost_factor = risk_transf_cost_factor + + def check(self): + """ + Check consistent instance data. + + Raises + ------ + ValueError + """ + u_check.size([3, 4], self.color_rgb, "Measure.color_rgb") + u_check.size(2, self.hazard_inten_imp, "Measure.hazard_inten_imp") + u_check.size(2, self.mdd_impact, "Measure.mdd_impact") + u_check.size(2, self.paa_impact, "Measure.paa_impact") + + def calc_impact(self, exposures, imp_fun_set, hazard): + """ + Apply measure and compute impact and risk transfer of measure + implemented over inputs. + + Parameters + ---------- + exposures : climada.entity.Exposures + exposures instance + imp_fun_set : climada.entity.ImpactFuncSet + impact function set instance + hazard : climada.hazard.Hazard + hazard instance + + Returns + ------- + climada.engine.Impact + resulting impact and risk transfer of measure + """ + + new_exp, new_impfs, new_haz = self.apply(exposures, imp_fun_set, hazard) + # assign centroids if missing + if new_haz.centr_exp_col not in new_exp.gdf.columns: + LOGGER.warning( + "No assigned hazard centroids in exposure object after the " + "application of the measure. The centroids will be assigned during impact " + "calculation. This is potentiall costly. To silence this warning, make sure " + "that centroids are assigned to all exposures." + ) + new_exp.assign_centroids(new_haz) + + return self._calc_impact(new_exp, new_impfs, new_haz) + + def apply(self, exposures, imp_fun_set, hazard): + """ + Implement measure with all its defined parameters. + + Parameters + ---------- + exposures : climada.entity.Exposures + exposures instance + imp_fun_set : climada.entity.ImpactFuncSet + impact function set instance + hazard : climada.hazard.Hazard + hazard instance + + Returns + ------- + new_exp : climada.entity.Exposure + Exposure with implemented measure with all defined parameters + new_ifs : climada.entity.ImpactFuncSet + Impact function set with implemented measure with all defined parameters + new_haz : climada.hazard.Hazard + Hazard with implemented measure with all defined parameters + """ + # change hazard + new_haz = self._change_all_hazard(hazard) + # change exposures + new_exp = self._change_all_exposures(exposures) + new_exp = self._change_exposures_impf(new_exp) + # change impact functions + new_impfs = self._change_imp_func(imp_fun_set) + # cutoff events whose damage happen with high frequency (in region impf specified) + new_haz = self._cutoff_hazard_damage(new_exp, new_impfs, new_haz) + # apply all previous changes only to the selected exposures + new_exp, new_impfs, new_haz = self._filter_exposures( + exposures, imp_fun_set, hazard, new_exp, new_impfs, new_haz + ) + + return new_exp, new_impfs, new_haz + + def _calc_impact(self, new_exp, new_impfs, new_haz): + """Compute impact and risk transfer of measure implemented over inputs. + + Parameters + ---------- + new_exp : climada.entity.Exposures + exposures once measure applied + new_ifs : climada.entity.ImpactFuncSet + impact function set once measure applied + new_haz : climada.hazard.Hazard + hazard once measure applied + + Returns + ------- + climada.engine.Impact + """ + from climada.engine.impact_calc import ( + ImpactCalc, # pylint: disable=import-outside-toplevel + ) + + imp = ImpactCalc(new_exp, new_impfs, new_haz).impact( + save_mat=False, assign_centroids=False + ) + return imp.calc_risk_transfer(self.risk_transf_attach, self.risk_transf_cover) + + def _change_all_hazard(self, hazard): + """ + Change hazard to provided hazard_set. + + Parameters + ---------- + hazard : climada.hazard.Hazard + hazard instance + + Returns + ------- + new_haz : climada.hazard.Hazard + Hazard + """ + if self.hazard_set == NULL_STR: + return hazard + + LOGGER.debug("Setting new hazard %s", self.hazard_set) + new_haz = Hazard.from_hdf5(self.hazard_set) + new_haz.check() + return new_haz + + def _change_all_exposures(self, exposures): + """ + Change exposures to provided exposures_set. + + Parameters + ---------- + exposures : climada.entity.Exposures + exposures instance + + Returns + ------- + new_exp : climada.entity.Exposures() + Exposures + """ + if isinstance(self.exposures_set, str) and self.exposures_set == NULL_STR: + return exposures + + if isinstance(self.exposures_set, (str, Path)): + LOGGER.debug("Setting new exposures %s", self.exposures_set) + new_exp = Exposures.from_hdf5(self.exposures_set) + new_exp.check() + elif isinstance(self.exposures_set, Exposures): + LOGGER.debug("Setting new exposures. ") + new_exp = self.exposures_set.copy(deep=True) + new_exp.check() + else: + raise ValueError( + f"{self.exposures_set} is neither a string nor an Exposures object" + ) + + if not np.array_equal( + np.unique(exposures.latitude), np.unique(new_exp.latitude) + ) or not np.array_equal( + np.unique(exposures.longitude), np.unique(new_exp.longitude) + ): + LOGGER.warning("Exposures locations have changed.") + + return new_exp + + def _change_exposures_impf(self, exposures): + """Change exposures impact functions ids according to imp_fun_map. + + Parameters + ---------- + exposures : climada.entity.Exposures + exposures instance + + Returns + ------- + new_exp : climada.entity.Exposure + Exposure with updated impact functions ids accordgin to + impf_fun_map + """ + if self.imp_fun_map == NULL_STR: + return exposures + + LOGGER.debug("Setting new exposures impact functions%s", self.imp_fun_map) + new_exp = exposures.copy(deep=True) + from_id = int(self.imp_fun_map[0 : self.imp_fun_map.find("to")]) + to_id = int(self.imp_fun_map[self.imp_fun_map.find("to") + 2 :]) + try: + exp_change = np.argwhere( + new_exp.gdf[INDICATOR_IMPF + self.haz_type].values == from_id + ).reshape(-1) + new_exp.gdf[INDICATOR_IMPF + self.haz_type].values[exp_change] = to_id + except KeyError: + exp_change = np.argwhere( + new_exp.gdf[INDICATOR_IMPF].values == from_id + ).reshape(-1) + new_exp.gdf[INDICATOR_IMPF].values[exp_change] = to_id + return new_exp + + def _change_imp_func(self, imp_set): + """ + Apply measure to impact functions of the same hazard type. + + Parameters + ---------- + imp_set : climada.entity.ImpactFuncSet + impact function set instance to be modified + + Returns + ------- + new_imp_set : climada.entity.ImpactFuncSet + ImpactFuncSet with measure applied to each impact function + according to the defined hazard type + """ + if ( + self.hazard_inten_imp == (1, 0) + and self.mdd_impact == (1, 0) + and self.paa_impact == (1, 0) + ): + return imp_set + + new_imp_set = copy.deepcopy(imp_set) + for imp_fun in new_imp_set.get_func(self.haz_type): + LOGGER.debug("Transforming impact functions.") + imp_fun.intensity = np.maximum( + imp_fun.intensity * self.hazard_inten_imp[0] - self.hazard_inten_imp[1], + 0.0, + ) + imp_fun.mdd = np.maximum( + imp_fun.mdd * self.mdd_impact[0] + self.mdd_impact[1], 0.0 + ) + imp_fun.paa = np.maximum( + imp_fun.paa * self.paa_impact[0] + self.paa_impact[1], 0.0 + ) + + if not new_imp_set.size(): + LOGGER.info("No impact function of hazard %s found.", self.haz_type) + + return new_imp_set + + def _cutoff_hazard_damage(self, exposures, impf_set, hazard): + """Cutoff of hazard events which generate damage with a frequency higher + than hazard_freq_cutoff. + + Parameters + ---------- + exposures : climada.entity.Exposures + exposures instance + imp_set : climada.entity.ImpactFuncSet + impact function set instance + hazard : climada.hazard.Hazard + hazard instance + + Returns + ------- + new_haz : climada.hazard.Hazard + Hazard without events which generate damage with a frequency + higher than hazard_freq_cutoff + """ + if self.hazard_freq_cutoff == 0: + return hazard + + if self.exp_region_id: + # compute impact only in selected region + in_reg = np.logical_or.reduce( + [exposures.region_id == reg for reg in self.exp_region_id] + ) + exp_imp = Exposures(exposures.gdf[in_reg], crs=exposures.crs) + else: + exp_imp = exposures + + from climada.engine.impact_calc import ( + ImpactCalc, # pylint: disable=import-outside-toplevel + ) + + imp = ImpactCalc(exp_imp, impf_set, hazard).impact( + assign_centroids=hazard.centr_exp_col not in exp_imp.gdf + ) + + LOGGER.debug( + "Cutting events whose damage have a frequency > %s.", + self.hazard_freq_cutoff, + ) + new_haz = copy.deepcopy(hazard) + sort_idxs = np.argsort(imp.at_event)[::-1] + exceed_freq = np.cumsum(imp.frequency[sort_idxs]) + cutoff = exceed_freq > self.hazard_freq_cutoff + sel_haz = sort_idxs[cutoff] + for row in sel_haz: + new_haz.intensity.data[ + new_haz.intensity.indptr[row] : new_haz.intensity.indptr[row + 1] + ] = 0 + new_haz.intensity.eliminate_zeros() + return new_haz + + def _filter_exposures( + self, exposures, imp_set, hazard, new_exp, new_impfs, new_haz + ): + """ + Incorporate changes of new elements to previous ones only for the + selected exp_region_id. If exp_region_id is [], all new changes + will be accepted. + + Parameters + ---------- + exposures : climada.entity.Exposures + old exposures instance + imp_set :climada.entity.ImpactFuncSet + old impact function set instance + hazard : climada.hazard.Hazard + old hazard instance + new_exp : climada.entity.Exposures + new exposures instance + new_ifs : climada.entity.ImpactFuncSet + new impact functions instance + new_haz : climada.hazard.Hazard + new hazard instance + + Returns + ------- + new_exp,new_ifs, new_haz : climada.entity.Exposures, + climada.entity.ImpactFuncSet, + climada.hazard.Hazard + Exposures, ImpactFuncSet, Hazard with incoporated elements + for the selected exp_region_id. + """ + if not self.exp_region_id: + return new_exp, new_impfs, new_haz + + if exposures is new_exp: + new_exp = exposures.copy(deep=True) + + if imp_set is not new_impfs: + # provide new impact functions ids to changed impact functions + fun_ids = list(new_impfs.get_func()[self.haz_type].keys()) + for key in fun_ids: + new_impfs.get_func()[self.haz_type][key].id = key + IMPF_ID_FACT + new_impfs.get_func()[self.haz_type][ + key + IMPF_ID_FACT + ] = new_impfs.get_func()[self.haz_type][key] + try: + new_exp.gdf[INDICATOR_IMPF + self.haz_type] += IMPF_ID_FACT + except KeyError: + new_exp.gdf[INDICATOR_IMPF] += IMPF_ID_FACT + # collect old impact functions as well (used by exposures) + new_impfs.get_func()[self.haz_type].update( + imp_set.get_func()[self.haz_type] + ) + + # get the indices for changing and inert regions + chg_reg = exposures.gdf["region_id"].isin(self.exp_region_id) + no_chg_reg = ~chg_reg + + LOGGER.debug("Number of changed exposures: %s", chg_reg.sum()) + + # concatenate previous and new exposures + new_exp.set_gdf( + GeoDataFrame( + pd.concat( + [ + exposures.gdf[no_chg_reg], # old values for inert regions + new_exp.gdf[chg_reg], # new values for changing regions + ] + ).loc[ + exposures.gdf.index, : + ], # re-establish old order + ), + crs=exposures.crs, + ) + + # set missing values of centr_ + if ( + INDICATOR_CENTR + self.haz_type in new_exp.gdf.columns + and np.isnan(new_exp.gdf[INDICATOR_CENTR + self.haz_type].values).any() + ): + new_exp.gdf.drop(columns=INDICATOR_CENTR + self.haz_type, inplace=True) + elif ( + INDICATOR_CENTR in new_exp.gdf.columns + and np.isnan(new_exp.gdf[INDICATOR_CENTR].values).any() + ): + new_exp.gdf.drop(columns=INDICATOR_CENTR, inplace=True) + + # put hazard intensities outside region to previous intensities + if hazard is not new_haz: + if INDICATOR_CENTR + self.haz_type in exposures.gdf.columns: + centr = exposures.gdf[INDICATOR_CENTR + self.haz_type].values[chg_reg] + elif INDICATOR_CENTR in exposures.gdf.columns: + centr = exposures.gdf[INDICATOR_CENTR].values[chg_reg] + else: + exposures.assign_centroids(hazard) + centr = exposures.gdf[INDICATOR_CENTR + self.haz_type].values[chg_reg] + + centr = np.delete(np.arange(hazard.intensity.shape[1]), np.unique(centr)) + new_haz_inten = new_haz.intensity.tolil() + new_haz_inten[:, centr] = hazard.intensity[:, centr] + new_haz.intensity = new_haz_inten.tocsr() + + return new_exp, new_impfs, new_haz diff --git a/climada/entity/measures/measure_set.py b/climada/entity/_legacy_measures/measure_set.py similarity index 99% rename from climada/entity/measures/measure_set.py rename to climada/entity/_legacy_measures/measure_set.py index 90a2bb43c2..228788ba15 100755 --- a/climada/entity/measures/measure_set.py +++ b/climada/entity/_legacy_measures/measure_set.py @@ -32,7 +32,8 @@ from matplotlib import colormaps as cm import climada.util.hdf5_handler as u_hdf5 -from climada.entity.measures.base import Measure + +from .base import Measure LOGGER = logging.getLogger(__name__) diff --git a/climada/entity/measures/test/__init__.py b/climada/entity/_legacy_measures/test/__init__.py similarity index 100% rename from climada/entity/measures/test/__init__.py rename to climada/entity/_legacy_measures/test/__init__.py diff --git a/climada/entity/measures/test/data/.gitignore b/climada/entity/_legacy_measures/test/data/.gitignore similarity index 100% rename from climada/entity/measures/test/data/.gitignore rename to climada/entity/_legacy_measures/test/data/.gitignore diff --git a/climada/entity/measures/test/test_base.py b/climada/entity/_legacy_measures/test/test_base.py similarity index 99% rename from climada/entity/measures/test/test_base.py rename to climada/entity/_legacy_measures/test/test_base.py index 6f76eb7373..430ab7d44b 100644 --- a/climada/entity/measures/test/test_base.py +++ b/climada/entity/_legacy_measures/test/test_base.py @@ -28,12 +28,12 @@ import climada.entity.exposures.test as exposures_test import climada.util.coordinates as u_coord from climada import CONFIG +from climada.entity._legacy_measures.base import IMPF_ID_FACT, Measure +from climada.entity._legacy_measures.measure_set import MeasureSet from climada.entity.entity_def import Entity from climada.entity.exposures.base import Exposures from climada.entity.impact_funcs.base import ImpactFunc from climada.entity.impact_funcs.impact_func_set import ImpactFuncSet -from climada.entity.measures.base import IMPF_ID_FACT, Measure -from climada.entity.measures.measure_set import MeasureSet from climada.hazard.base import Hazard from climada.test import get_test_file from climada.util.constants import HAZ_DEMO_H5 diff --git a/climada/entity/measures/test/test_meas_set.py b/climada/entity/_legacy_measures/test/test_meas_set.py similarity index 98% rename from climada/entity/measures/test/test_meas_set.py rename to climada/entity/_legacy_measures/test/test_meas_set.py index a2cbdc3f16..868510fbe8 100644 --- a/climada/entity/measures/test/test_meas_set.py +++ b/climada/entity/_legacy_measures/test/test_meas_set.py @@ -24,8 +24,8 @@ import numpy as np from climada import CONFIG -from climada.entity.measures.base import Measure -from climada.entity.measures.measure_set import MeasureSet +from climada.entity._legacy_measures.base import Measure +from climada.entity._legacy_measures.measure_set import MeasureSet from climada.util.constants import ENT_DEMO_TODAY, ENT_TEMPLATE_XLS DATA_DIR = CONFIG.measures.test_data.dir() @@ -58,7 +58,7 @@ def test_add_wrong_error(self): """Test error is raised when wrong ImpactFunc provided.""" meas = MeasureSet() with self.assertLogs( - "climada.entity.measures.measure_set", level="WARNING" + "climada.entity._legacy_measures.measure_set", level="WARNING" ) as cm: meas.append(Measure()) self.assertIn("Input Measure's hazard type not set.", cm.output[0]) @@ -76,7 +76,9 @@ def test_remove_measure_pass(self): def test_remove_wrong_error(self): """Test error is raised when invalid inputs.""" meas = MeasureSet(measure_list=[Measure(name="Mangrove", haz_type="FL")]) - with self.assertLogs("climada.entity.measures.measure_set", level="INFO") as cm: + with self.assertLogs( + "climada.entity._legacy_measures.measure_set", level="INFO" + ) as cm: meas.remove_measure(name="Seawall") self.assertIn("No Measure with name Seawall.", cm.output[0]) diff --git a/climada/entity/entity_def.py b/climada/entity/entity_def.py index d58af9efed..c1bc3b2550 100755 --- a/climada/entity/entity_def.py +++ b/climada/entity/entity_def.py @@ -26,10 +26,10 @@ import pandas as pd +from climada.entity._legacy_measures.measure_set import Measure, MeasureSet from climada.entity.disc_rates.base import DiscRates from climada.entity.exposures.base import Exposures from climada.entity.impact_funcs.impact_func_set import ImpactFuncSet -from climada.entity.measures.measure_set import MeasureSet LOGGER = logging.getLogger(__name__) diff --git a/climada/entity/measures/base.py b/climada/entity/measures/base.py index 4e539f9986..1cb9829c6e 100755 --- a/climada/entity/measures/base.py +++ b/climada/entity/measures/base.py @@ -19,552 +19,494 @@ Define Measure class. """ +from __future__ import annotations + +from pandas.tseries.offsets import BaseOffset + __all__ = ["Measure"] import copy +import inspect import logging -from pathlib import Path -from typing import Optional, Tuple +from functools import wraps +from typing import TYPE_CHECKING, Any, Optional, Tuple, TypeVar + +from climada.entity.measures.measure_config import MeasureConfig + +from .cost_income import CostIncome -import numpy as np -import pandas as pd -from geopandas import GeoDataFrame +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 -import climada.util.checker as u_check -from climada.entity.exposures.base import INDICATOR_CENTR, INDICATOR_IMPF, Exposures -from climada.hazard.base import Hazard + T = TypeVar("T", Exposures, ImpactFuncSet, Hazard) LOGGER = logging.getLogger(__name__) -IMPF_ID_FACT = 1000 -"""Factor internally used as id for impact functions when region selected.""" +# TODO: risk transfer? -NULL_STR = "nil" -"""String considered as no path in measures exposures_set and hazard_set or -no string in imp_fun_map""" +T = TypeVar("T", Exposures, ImpactFuncSet, Hazard) -class Measure: - """ - Contains the definition of one measure. +# Note for review: +# This function will moved in helper.py in a future PR which will +# add I/O based on MeasureConfig dataclasses +def identity_function(x: T, **_kwargs: Any) -> T: + """Identity function - Attributes - ---------- - name : str - name of the measure - haz_type : str - related hazard type (peril), e.g. TC - color_rgb : np.array - integer array of size 3. Color code of this measure in RGB - cost : float - discounted cost (in same units as assets) - hazard_set : str - file name of hazard to use (in h5 format) - hazard_freq_cutoff : float - hazard frequency cutoff - exposures_set : str or climada.entity.Exposure - file name of exposure to use (in h5 format) or Exposure instance - imp_fun_map : str - change of impact function id of exposures, e.g. '1to3' - hazard_inten_imp : tuple(float, float) - parameter a and b of hazard intensity change - mdd_impact : tuple(float, float) - parameter a and b of the impact over the mean damage degree - paa_impact : tuple(float, float) - parameter a and b of the impact over the percentage of affected assets - exp_region_id : int - region id of the selected exposures to consider ALL the previous - parameters - risk_transf_attach : float - risk transfer attachment - risk_transf_cover : float - risk transfer cover - risk_transf_cost_factor : float - factor to multiply to resulting insurance layer to get the total - cost of risk transfer + Returns the provided parameter. Usefull to design measures without effect + on some of the risk components (Hazard, Exposures, Impact Function) """ + return x - def __init__( - self, - name: str = "", - haz_type: str = "", - cost: float = 0, - hazard_set: str = NULL_STR, - hazard_freq_cutoff: float = 0, - exposures_set: str = NULL_STR, - imp_fun_map: str = NULL_STR, - hazard_inten_imp: Tuple[float, float] = (1, 0), - mdd_impact: Tuple[float, float] = (1, 0), - paa_impact: Tuple[float, float] = (1, 0), - exp_region_id: Optional[list] = None, - risk_transf_attach: float = 0, - risk_transf_cover: float = 0, - risk_transf_cost_factor: float = 1, - color_rgb: Optional[np.ndarray] = None, - ): - """Initialize a Measure object with given values. - Parameters - ---------- - name : str, optional - name of the measure - haz_type : str, optional - related hazard type (peril), e.g. TC - cost : float, optional - discounted cost (in same units as assets) - hazard_set : str, optional - file name of hazard to use (in h5 format) - hazard_freq_cutoff : float, optional - hazard frequency cutoff - exposures_set : str or climada.entity.Exposure, optional - file name of exposure to use (in h5 format) or Exposure instance - imp_fun_map : str, optional - change of impact function id of exposures, e.g. '1to3' - hazard_inten_imp : tuple(float, float), optional - parameter a and b of hazard intensity change - mdd_impact : tuple(float, float), optional - parameter a and b of the impact over the mean damage degree - paa_impact : tuple(float, float), optional - parameter a and b of the impact over the percentage of affected assets - exp_region_id : int, optional - region id of the selected exposures to consider ALL the previous - parameters - risk_transf_attach : float, optional - risk transfer attachment - risk_transf_cover : float, optional - risk transfer cover - risk_transf_cost_factor : float, optional - factor to multiply to resulting insurance layer to get the total - cost of risk transfer - color_rgb : np.array, optional - integer array of size 3. Color code of this measure in RGB. - Default is None (corresponds to black). - """ - self.name = name - self.haz_type = haz_type - self.color_rgb = np.array([0, 0, 0]) if color_rgb is None else color_rgb - self.cost = cost +def allow_kwargs(func): + """ + Decorator that allows a function to accept (and silently ignore) keyword arguments. - # related to change in hazard - self.hazard_set = hazard_set - self.hazard_freq_cutoff = hazard_freq_cutoff + If the wrapped function already accepts ``**kwargs``, it is called unchanged. + Otherwise, any keyword arguments not present in the function's signature are + filtered out before the call, preventing ``TypeError`` from unexpected keywords. - # related to change in exposures - self.exposures_set = exposures_set - self.imp_fun_map = imp_fun_map + The functions used by `Measure` objects to apply changes on ``Exposures``, ``Hazard``, + and ``ImpactFuncSet`` always receive ``base_exposure, base_hazard, base_impfset`` as + keyword arguments. This decorator is applied to users-defined functions and prevent + them from not accepting these kwargs and raising a ``TypeError``. - # related to change in impact functions - self.hazard_inten_imp = hazard_inten_imp - self.mdd_impact = mdd_impact - self.paa_impact = paa_impact + Parameters + ---------- + func : callable + The function to wrap. + + Returns + ------- + callable + A wrapped version of `func` that accepts keyword arguments. + + Examples + -------- + >>> @allow_kwargs + ... def greet(name, greeting="Hello"): + ... return f"{greeting}, {name}!" + >>> greet("Alice", greeting="Hi", unused_param="ignored") + 'Hi, Alice!' + + >>> @allow_kwargs + ... def add(a, b): + ... return a + b + >>> add(1, 2, extra=99) + 3 + """ - # related to change in region - self.exp_region_id = [] if exp_region_id is None else exp_region_id + @wraps(func) + def wrapper(*args, **kwargs): + # Get the names of arguments the original function accepts + params = inspect.signature(func).parameters - # risk transfer - self.risk_transf_attach = risk_transf_attach - self.risk_transf_cover = risk_transf_cover - self.risk_transf_cost_factor = risk_transf_cost_factor + # Filter kwargs to only include what the function can handle + # (Unless the function already has **kwargs in its signature) + if any(p.kind == p.VAR_KEYWORD for p in params.values()): + return func(*args, **kwargs) - def check(self): - """ - Check consistent instance data. + filtered_kwargs = {k: v for k, v in kwargs.items() if k in params} + return func(*args, **filtered_kwargs) - Raises - ------ - ValueError - """ - u_check.size([3, 4], self.color_rgb, "Measure.color_rgb") - u_check.size(2, self.hazard_inten_imp, "Measure.hazard_inten_imp") - u_check.size(2, self.mdd_impact, "Measure.mdd_impact") - u_check.size(2, self.paa_impact, "Measure.paa_impact") + return wrapper - def calc_impact(self, exposures, imp_fun_set, hazard): - """ - Apply measure and compute impact and risk transfer of measure - implemented over inputs. - Parameters - ---------- - exposures : climada.entity.Exposures - exposures instance - imp_fun_set : climada.entity.ImpactFuncSet - impact function set instance - hazard : climada.hazard.Hazard - hazard instance +class Measure: + """ + Contains a measure to be applied to a set of exposures, impact functions, + and hazard. - Returns - ------- - climada.engine.Impact - resulting impact and risk transfer of measure - """ + A ``Measure`` represents a single adaptation or risk-reduction action. It + holds three (optional) transformation functions, one each for + :class:`Exposures`, :class:`ImpactFuncSet`, and :class:`Hazard`, that are + can be applied to a triplet of ``(Exposures, ImpactFuncSet, Hazard)``, to + reflect the effect of the measure. - new_exp, new_impfs, new_haz = self.apply(exposures, imp_fun_set, hazard) - # assign centroids if missing - if new_haz.centr_exp_col not in new_exp.gdf.columns: - LOGGER.warning( - "No assigned hazard centroids in exposure object after the " - "application of the measure. The centroids will be assigned during impact " - "calculation. This is potentiall costly. To silence this warning, make sure " - "that centroids are assigned to all exposures." - ) - new_exp.assign_centroids(new_haz) + It also holds a `CostIncome` object to define the financial aspects of the + measure (see :class:`CostIncome` and :ref:`cost-income-tutorial`). - return self._calc_impact(new_exp, new_impfs, new_haz) + Finally it holds an `implementation_duration` attribute, in the form of a + pandas ``DateOffset``, which is used when the time dimension is considered. - def apply(self, exposures, imp_fun_set, hazard): - """ - Implement measure with all its defined parameters. + Notes + ----- - Parameters - ---------- - exposures : climada.entity.Exposures - exposures instance - imp_fun_set : climada.entity.ImpactFuncSet - impact function set instance - hazard : climada.hazard.Hazard - hazard instance + The only requirement for each function is to return an object of the same + class (e.g. :class:`Hazard` for ``hazard_change``). Functions can accept + keyword arguments to enable advanced effect (depending on a year of + application for instance). These arguments can be passed when the + :class:`Measure` is applied (see :py:meth:`~Measure.apply`). Note that for + convenience, each functions receive the by default "base" ``(Exposures, + ImpactFuncSet, Hazard)`` triplet as keyword arguments (`base_exposure`, + `base_impfset`, `base_hazard`). - Returns - ------- - new_exp : climada.entity.Exposure - Exposure with implemented measure with all defined parameters - new_ifs : climada.entity.ImpactFuncSet - Impact function set with implemented measure with all defined parameters - new_haz : climada.hazard.Hazard - Hazard with implemented measure with all defined parameters - """ - # change hazard - new_haz = self._change_all_hazard(hazard) - # change exposures - new_exp = self._change_all_exposures(exposures) - new_exp = self._change_exposures_impf(new_exp) - # change impact functions - new_impfs = self._change_imp_func(imp_fun_set) - # cutoff events whose damage happen with high frequency (in region impf specified) - new_haz = self._cutoff_hazard_damage(new_exp, new_impfs, new_haz) - # apply all previous changes only to the selected exposures - new_exp, new_impfs, new_haz = self._filter_exposures( - exposures, imp_fun_set, hazard, new_exp, new_impfs, new_haz - ) + If the ``Measure`` was defined from a ``MeasureConfig`` object, the + configuration is stored and the measure can be serialized to a file. + (see :ref:`measure-config-tutorial` and :ref:`measure-tutorial`). - return new_exp, new_impfs, new_haz + Attributes + ---------- + name : str + Name of the measure. + exposures_change : ExposuresChange + Function to change exposures. + impfset_change : ImpfsetChange + Function to change impact function set. + hazard_change : HazardChange + Function to change hazard. + sub_measures : list of str, optional + List of measure names that this measure is a combination of. + cost_income : climada.entity.measures.cost_income.CostIncome + Cost and income object associated with the measure. + implementation_duration : pd.DateOffset, optional + Duration of implementation before the measure is fully functional. + """ - def _calc_impact(self, new_exp, new_impfs, new_haz): - """Compute impact and risk transfer of measure implemented over inputs. + def __init__( + self, + name: str, + *, + exposures_changes: ExposuresChange = identity_function, + impfset_changes: ImpfsetChange = identity_function, + hazard_changes: HazardChange = identity_function, + sub_measures: Optional[list[str]] = None, + cost_income: Optional[CostIncome] = None, + implementation_duration: Optional[BaseOffset] = None, + color_rgb: Optional[Tuple[float, float, float]] = None, + _config: Optional[MeasureConfig] = None, + ): + """ + Initialize a new Measure object. Parameters ---------- - new_exp : climada.entity.Exposures - exposures once measure applied - new_ifs : climada.entity.ImpactFuncSet - impact function set once measure applied - new_haz : climada.hazard.Hazard - hazard once measure applied - - Returns - ------- - climada.engine.Impact + name : str + Name of the measure. + exposures_change : callable, optional + Transformation function for Exposures. Defaults to identity. + impfset_change : callable, optional + Transformation function for ImpactFuncSet. Defaults to identity. + hazard_change : callable, optional + Transformation function for Hazard. Defaults to identity. + sub_measures : list of str, optional + Names of component measures. + cost_income : CostIncome, optional + Financial data. If None, an empty CostIncome is initialized. + implementation_duration : pd.DateOffset, optional + Time offset for full implementation. """ - from climada.engine.impact_calc import ( - ImpactCalc, # pylint: disable=import-outside-toplevel - ) - imp = ImpactCalc(new_exp, new_impfs, new_haz).impact( - save_mat=False, assign_centroids=False - ) - return imp.calc_risk_transfer(self.risk_transf_attach, self.risk_transf_cover) - - def _change_all_hazard(self, hazard): + self.name = name + self.exposures_changes = allow_kwargs(exposures_changes) + self.hazard_changes = allow_kwargs(hazard_changes) + self.impfset_changes = allow_kwargs(impfset_changes) + self.sub_measures = sub_measures + self.cost_income = cost_income if cost_income is not None else CostIncome() + self.implementation_duration = implementation_duration + 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 + a ``MeasureConfig`` object and can be serialized. """ - Change hazard to provided hazard_set. - Parameters - ---------- - hazard : climada.hazard.Hazard - hazard instance + return self._config is not None - Returns - ------- - new_haz : climada.hazard.Hazard - Hazard - """ - if self.hazard_set == NULL_STR: - return hazard + def apply_exposures_changes( + self, exposures: Exposures, enforce_copy: bool = True, **kwargs + ) -> Exposures: + """Apply the changes from the measure to the given :class:`Exposures` object. - LOGGER.debug("Setting new hazard %s", self.hazard_set) - new_haz = Hazard.from_hdf5(self.hazard_set) - new_haz.check() - return new_haz + This method applies the `exposures_changes` function of the measure to + the provided :class:`Exposures` object. If ``enforce_copy`` is True (default), a + deep copy of the exposures is created before modification to ensure + immutability of the original object. - def _change_all_exposures(self, exposures): - """ - Change exposures to provided exposures_set. + Additional keyword arguments to the function can be passed directly. Parameters ---------- - exposures : climada.entity.Exposures - exposures instance + exposures : Exposures + The input exposures object to be transformed. + enforce_copy : bool, optional + If True (default), creates a deep copy of `exposures` before applying + changes, provided the transformation function is not the identity function. + If False, the original object may be modified in-place depending on the + behavior of `exposures_changes`. + **kwargs : dict, optional + Additional keyword arguments passed directly to the `exposures_changes` + function. Returns ------- - new_exp : climada.entity.Exposures() - Exposures + Exposures + The resulting :class:`Exposures` object after the transformation has been applied. + If `enforce_copy` was True, this is a new object. + + Notes + ----- + The deep copy operation is skipped if `enforce_copy` is False or if + `self.exposures_changes` is the identity function, optimizing performance + when no actual changes are expected or when in-place modification is desired. """ - if isinstance(self.exposures_set, str) and self.exposures_set == NULL_STR: - return exposures - - if isinstance(self.exposures_set, (str, Path)): - LOGGER.debug("Setting new exposures %s", self.exposures_set) - new_exp = Exposures.from_hdf5(self.exposures_set) - new_exp.check() - elif isinstance(self.exposures_set, Exposures): - LOGGER.debug("Setting new exposures. ") - new_exp = self.exposures_set.copy(deep=True) - new_exp.check() - else: - raise ValueError( - f"{self.exposures_set} is neither a string nor an Exposures object" - ) - if not np.array_equal( - np.unique(exposures.latitude), np.unique(new_exp.latitude) - ) or not np.array_equal( - np.unique(exposures.longitude), np.unique(new_exp.longitude) - ): - LOGGER.warning("Exposures locations have changed.") - - return new_exp + changed_exp = ( + copy.deepcopy(exposures) + if enforce_copy and self.exposures_changes is not identity_function + else exposures + ) + try: + return self.exposures_changes(changed_exp, **kwargs) + except TypeError as exc: + # Check if 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\ + that were not provided.\n" + f"Please check the function signature or the helper used and provide the\ + required arguments " + "via kwargs_exposures.\n" + f"Original error: {exc}" + ) from exc + raise + + def apply_impfset_changes( + self, impfset: ImpactFuncSet, enforce_copy: bool = True, **kwargs + ) -> ImpactFuncSet: + """ + Apply the changes from the measure to the given :class:`ImpactFuncSet` object. - def _change_exposures_impf(self, exposures): - """Change exposures impact functions ids according to imp_fun_map. + This method applies the `impfset_changes` function of the measure to + the provided :class:`ImpactFuncSet` object. If `enforce_copy` is True + (default), a deep copy of the impfset is created before modification to + ensure immutability of the original object. Parameters ---------- - exposures : climada.entity.Exposures - exposures instance + impfset : ImpactFuncSet + The input impfset object to be transformed. + enforce_copy : bool, optional + If True (default), creates a deep copy of `impfset` before applying + changes, provided the transformation function is not the identity + function. If False, the original object may be modified in-place + depending on the behavior of `impfset_changes`. + **kwargs : dict, optional + Additional keyword arguments passed directly to the `impfset_changes` + function. Returns ------- - new_exp : climada.entity.Exposure - Exposure with updated impact functions ids accordgin to - impf_fun_map + ImpactFuncSet + The resulting :class:`ImpactFuncSet` after the transformation has + been applied. If `enforce_copy` was True, this is a new object. + + Notes + ----- + The deep copy operation is skipped if `enforce_copy` is False or if + `self.impfset_changes` is the identity function, optimizing performance + when no actual changes are expected or when in-place modification is + desired. """ - if self.imp_fun_map == NULL_STR: - return exposures - LOGGER.debug("Setting new exposures impact functions%s", self.imp_fun_map) - new_exp = exposures.copy(deep=True) - from_id = int(self.imp_fun_map[0 : self.imp_fun_map.find("to")]) - to_id = int(self.imp_fun_map[self.imp_fun_map.find("to") + 2 :]) + changed_impfset = ( + copy.deepcopy(impfset) + if enforce_copy and self.impfset_changes is not identity_function + else impfset + ) try: - exp_change = np.argwhere( - new_exp.gdf[INDICATOR_IMPF + self.haz_type].values == from_id - ).reshape(-1) - new_exp.gdf[INDICATOR_IMPF + self.haz_type].values[exp_change] = to_id - except KeyError: - exp_change = np.argwhere( - new_exp.gdf[INDICATOR_IMPF].values == from_id - ).reshape(-1) - new_exp.gdf[INDICATOR_IMPF].values[exp_change] = to_id - return new_exp - - def _change_imp_func(self, imp_set): + return self.impfset_changes(changed_impfset, **kwargs) + except TypeError as exc: + # Check if 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 impact function set requires\ + additional arguments that were not provided.\n" + f"Please check the function signature or the helper used\ + and provide the required arguments via kwargs_impfset.\n" + f"Original error: {exc}" + ) from exc + raise + + def apply_hazard_changes( + self, hazard: Hazard, enforce_copy: bool = True, **kwargs + ) -> Hazard: """ - Apply measure to impact functions of the same hazard type. + Apply the changes from the measure to the given :class:`Hazard` object. + + This method applies the `hazard_changes` function of the measure to the + provided :class:`Hazard` object. If `enforce_copy` is True (default), a + deep copy of the hazard is created before modification to ensure + immutability of the original object. Parameters ---------- - imp_set : climada.entity.ImpactFuncSet - impact function set instance to be modified + hazard : Hazard + The input hazard object to be transformed. + enforce_copy : bool, optional + If True (default), creates a deep copy of `hazard` before applying + changes, provided the transformation function is not the identity + function. If False, the original object may be modified in-place + depending on the behavior of `hazard_changes`. + **kwargs : dict, optional + Additional keyword arguments passed directly to the `hazard_changes` + function. Returns ------- - new_imp_set : climada.entity.ImpactFuncSet - ImpactFuncSet with measure applied to each impact function - according to the defined hazard type + Hazard + The resulting hazard object after the transformation has been + applied. If `enforce_copy` was True, this is a new object. + + Notes + ----- + The deep copy operation is skipped if `enforce_copy` is False or if + `self.hazard_changes` is the identity function, optimizing performance + when no actual changes are expected or when in-place modification is + desired. """ - if ( - self.hazard_inten_imp == (1, 0) - and self.mdd_impact == (1, 0) - and self.paa_impact == (1, 0) - ): - return imp_set - - new_imp_set = copy.deepcopy(imp_set) - for imp_fun in new_imp_set.get_func(self.haz_type): - LOGGER.debug("Transforming impact functions.") - imp_fun.intensity = np.maximum( - imp_fun.intensity * self.hazard_inten_imp[0] - self.hazard_inten_imp[1], - 0.0, - ) - imp_fun.mdd = np.maximum( - imp_fun.mdd * self.mdd_impact[0] + self.mdd_impact[1], 0.0 - ) - imp_fun.paa = np.maximum( - imp_fun.paa * self.paa_impact[0] + self.paa_impact[1], 0.0 - ) - - if not new_imp_set.size(): - LOGGER.info("No impact function of hazard %s found.", self.haz_type) - - return new_imp_set - def _cutoff_hazard_damage(self, exposures, impf_set, hazard): - """Cutoff of hazard events which generate damage with a frequency higher - than hazard_freq_cutoff. + changed_hazard = ( + copy.deepcopy(hazard) + if enforce_copy and self.hazard_changes is not identity_function + else hazard + ) + try: + return self.hazard_changes(changed_hazard, **kwargs) + except TypeError as exc: + # Check if 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 hazard requires\ + additional arguments that were not provided.\n" + f"Please check the function signature or the helper used\ + and provide the required arguments via kwargs_hazard.\n" + f"Original error: {exc}" + ) from exc + raise + + def apply( + self, + exposures: Exposures, + impfset: ImpactFuncSet, + hazard: Hazard, + enforce_copy: bool = True, + **kwargs, + ) -> Tuple[Exposures, ImpactFuncSet, Hazard]: + """Apply all measure transformations to the provided triplet of + :class:`Exposures`, :class:`ImpactFuncSet`, :class:`Hazard`. + + This method applies the measure changes across all three + risk parts: exposures, impact function set, and hazard data. + + The method implements a flexible keyword arguments merging strategy + where the original triplet is provided as default context to each + transformation, which can then be overridden by entity-specific kwargs + dictionaries. This enables transformation requiring the information + from other risk components (for instance, removing events based on impact + threshold) or additional information (for instance, effect depending on + year of implementation). + + Refer to :ref:`measure-tutorial` for more details. Parameters ---------- - exposures : climada.entity.Exposures - exposures instance - imp_set : climada.entity.ImpactFuncSet - impact function set instance - hazard : climada.hazard.Hazard - hazard instance + exposures : Exposures + The input exposures object to be transformed. + impfset : ImpactFuncSet + The impact function set to be transformed. + hazard : Hazard + The hazard data to be transformed. + enforce_copy : bool, optional + If True (default), creates deep copies of entities before applying + changes, provided the transformation functions are not identity functions. + If False, entities may be modified in-place depending on the behavior + of the underlying transformation methods. + **kwargs : dict, optional + Additional keyword arguments for configuring transformations. Supports + nested dictionaries for entity-specific customization: + + * ``kwargs_exposures``: Dict of kwargs passed to `apply_exposures_changes` + * ``kwargs_impfset``: Dict of kwargs passed to `apply_impfset_changes` + * ``kwargs_hazard``: Dict of kwargs passed to `apply_hazard_changes` + + Each nested dict is merged with the default triplet context (exposures, + impfset, hazard), allowing transformations to access related entities + while permitting entity-specific overrides. Returns ------- - new_haz : climada.hazard.Hazard - Hazard without events which generate damage with a frequency - higher than hazard_freq_cutoff + Tuple[Exposures, ImpactFuncSet, Hazard] + A tuple containing the transformed entities in the order: + (changed_exposures, changed_impfset, changed_hazard). If `enforce_copy` + was True, these are new objects; otherwise, they may reference the + original inputs or modified versions thereof. + + Notes + ----- + The kwargs merging follows this priority order: + + 1. Default context: The original triplet (exposures, impfset, hazard) + 2. Entity-specific overrides: Values from ``kwargs_exposures``, + ``kwargs_impfset``, or ``kwargs_hazard`` respectively + + This ensures that each transformation receives full context about all entities + while allowing fine-grained control over individual transformations. + + The transformation order is: exposures → hazard → impact function set. + Each transformation is independent, so changes to one entity do not affect + the others during processing. """ - if self.hazard_freq_cutoff == 0: - return hazard - if self.exp_region_id: - # compute impact only in selected region - in_reg = np.logical_or.reduce( - [exposures.region_id == reg for reg in self.exp_region_id] - ) - exp_imp = Exposures(exposures.gdf[in_reg], crs=exposures.crs) - else: - exp_imp = exposures - - from climada.engine.impact_calc import ( - ImpactCalc, # pylint: disable=import-outside-toplevel + default_kwargs = { + "base_exposures": exposures, + "base_impfset": impfset, + "base_hazard": hazard, + } + # Always provide the triplet by default, and overwrite by custom kwargs. + kwargs_exp = default_kwargs | kwargs.get("kwargs_exposures", {}) + kwargs_impfset = default_kwargs | kwargs.get("kwargs_impfset", {}) + kwargs_hazard = default_kwargs | kwargs.get("kwargs_hazard", {}) + changed_exposures = self.apply_exposures_changes( + exposures, enforce_copy, **kwargs_exp ) - - imp = ImpactCalc(exp_imp, impf_set, hazard).impact( - assign_centroids=hazard.centr_exp_col not in exp_imp.gdf + changed_hazard = self.apply_hazard_changes( + hazard, enforce_copy, **kwargs_hazard ) - - LOGGER.debug( - "Cutting events whose damage have a frequency > %s.", - self.hazard_freq_cutoff, + changed_impfset = self.apply_impfset_changes( + impfset, enforce_copy, **kwargs_impfset ) - new_haz = copy.deepcopy(hazard) - sort_idxs = np.argsort(imp.at_event)[::-1] - exceed_freq = np.cumsum(imp.frequency[sort_idxs]) - cutoff = exceed_freq > self.hazard_freq_cutoff - sel_haz = sort_idxs[cutoff] - for row in sel_haz: - new_haz.intensity.data[ - new_haz.intensity.indptr[row] : new_haz.intensity.indptr[row + 1] - ] = 0 - new_haz.intensity.eliminate_zeros() - return new_haz - - def _filter_exposures( - self, exposures, imp_set, hazard, new_exp, new_impfs, new_haz - ): - """ - Incorporate changes of new elements to previous ones only for the - selected exp_region_id. If exp_region_id is [], all new changes - will be accepted. + return changed_exposures, changed_impfset, changed_hazard - Parameters - ---------- - exposures : climada.entity.Exposures - old exposures instance - imp_set :climada.entity.ImpactFuncSet - old impact function set instance - hazard : climada.hazard.Hazard - old hazard instance - new_exp : climada.entity.Exposures - new exposures instance - new_ifs : climada.entity.ImpactFuncSet - new impact functions instance - new_haz : climada.hazard.Hazard - new hazard instance + def calc_impact(self, exposures, impfset, hazard): + from climada.engine.impact_calc import ( + ImpactCalc, # pylint: disable=import-outside-toplevel + ) - Returns - ------- - new_exp,new_ifs, new_haz : climada.entity.Exposures, - climada.entity.ImpactFuncSet, - climada.hazard.Hazard - Exposures, ImpactFuncSet, Hazard with incoporated elements - for the selected exp_region_id. - """ - if not self.exp_region_id: - return new_exp, new_impfs, new_haz - - if exposures is new_exp: - new_exp = exposures.copy(deep=True) - - if imp_set is not new_impfs: - # provide new impact functions ids to changed impact functions - fun_ids = list(new_impfs.get_func()[self.haz_type].keys()) - for key in fun_ids: - new_impfs.get_func()[self.haz_type][key].id = key + IMPF_ID_FACT - new_impfs.get_func()[self.haz_type][ - key + IMPF_ID_FACT - ] = new_impfs.get_func()[self.haz_type][key] - try: - new_exp.gdf[INDICATOR_IMPF + self.haz_type] += IMPF_ID_FACT - except KeyError: - new_exp.gdf[INDICATOR_IMPF] += IMPF_ID_FACT - # collect old impact functions as well (used by exposures) - new_impfs.get_func()[self.haz_type].update( - imp_set.get_func()[self.haz_type] + new_exp, new_impfs, new_haz = self.apply(exposures, impfset, hazard) + if new_haz.centr_exp_col not in new_exp.gdf.columns: + LOGGER.warning( + "No assigned hazard centroids in exposure object after the " + "application of the measure. The centroids will be assigned during impact " + "calculation. This is potentiall costly. To silence this warning, make sure " + "that centroids are assigned to all exposures." ) - - # get the indices for changing and inert regions - chg_reg = exposures.gdf["region_id"].isin(self.exp_region_id) - no_chg_reg = ~chg_reg - - LOGGER.debug("Number of changed exposures: %s", chg_reg.sum()) - - # concatenate previous and new exposures - new_exp.set_gdf( - GeoDataFrame( - pd.concat( - [ - exposures.gdf[no_chg_reg], # old values for inert regions - new_exp.gdf[chg_reg], # new values for changing regions - ] - ).loc[ - exposures.gdf.index, : - ], # re-establish old order - ), - crs=exposures.crs, + new_exp.assign_centroids(new_haz) + imp = ImpactCalc(new_exp, new_impfs, new_haz).impact( + save_mat=False, assign_centroids=False ) - - # set missing values of centr_ - if ( - INDICATOR_CENTR + self.haz_type in new_exp.gdf.columns - and np.isnan(new_exp.gdf[INDICATOR_CENTR + self.haz_type].values).any() - ): - new_exp.gdf.drop(columns=INDICATOR_CENTR + self.haz_type, inplace=True) - elif ( - INDICATOR_CENTR in new_exp.gdf.columns - and np.isnan(new_exp.gdf[INDICATOR_CENTR].values).any() - ): - new_exp.gdf.drop(columns=INDICATOR_CENTR, inplace=True) - - # put hazard intensities outside region to previous intensities - if hazard is not new_haz: - if INDICATOR_CENTR + self.haz_type in exposures.gdf.columns: - centr = exposures.gdf[INDICATOR_CENTR + self.haz_type].values[chg_reg] - elif INDICATOR_CENTR in exposures.gdf.columns: - centr = exposures.gdf[INDICATOR_CENTR].values[chg_reg] - else: - exposures.assign_centroids(hazard) - centr = exposures.gdf[INDICATOR_CENTR + self.haz_type].values[chg_reg] - - centr = np.delete(np.arange(hazard.intensity.shape[1]), np.unique(centr)) - new_haz_inten = new_haz.intensity.tolil() - new_haz_inten[:, centr] = hazard.intensity[:, centr] - new_haz.intensity = new_haz_inten.tocsr() - - return new_exp, new_impfs, new_haz + return imp.calc_risk_transfer(0, 0) diff --git a/climada/entity/measures/cost_income.py b/climada/entity/measures/cost_income.py new file mode 100644 index 0000000000..33ece8de1c --- /dev/null +++ b/climada/entity/measures/cost_income.py @@ -0,0 +1,568 @@ +""" +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 the CostIncome class to handle the cash flow of measures. +""" + +from datetime import datetime +from typing import Any, List, Optional, Tuple, cast + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from climada.entity.disc_rates.base import DiscRates +from climada.entity.measures.measure_config import CostIncomeConfig + + +class CostIncome: + """ + Manages costs and incomes related to a measure over time. + + Income are stored a positive numbers and costs as negative + ones. + + Attributes + ---------- + mkt_price_year : datetime, default to today's year. + The reference year for market prices. + init_cost : float + Initial implementation cost (stored as negative). + periodic_cost : float + Recurring cost per period (stored as negative). + periodic_income : float + Recurring income per period. + cost_yearly_growth_rate : float + Yearly growth rate of costs. + income_yearly_growth_rate : float + Yearly growth rate of income. + custom_cash_flows : pd.DataFrame, optional + User-defined cash flows indexed by date. + freq : str + Frequency of the cash flows (e.g., 'Y', '3M', '7D'). + + """ + + def __init__( + self, + *, + mkt_price_year: Optional[int] = None, + init_cost: float = 0.0, + periodic_cost: float = 0.0, + periodic_income: float = 0.0, + cost_yearly_growth_rate: float = 0.0, + income_yearly_growth_rate: float = 0.0, + custom_cash_flows: Optional[pd.DataFrame] = None, + freq: str = "Y", + ): + """Initialize CostIncome with parameters. + + Parameters + ---------- + mkt_price_year : datetime, default to today's year. + The reference year for market prices. + init_cost : float + Initial implementation cost (stored as negative). + periodic_cost : float + Recurring cost per period (stored as negative). + periodic_income : float + Recurring income per period. + cost_yearly_growth_rate : float + Yearly growth rate of costs. + income_yearly_growth_rate : float + Yearly growth rate of income. + custom_cash_flows : pd.DataFrame, optional + User-defined cash flows indexed by date. + freq : str + Frequency of the cash flows (e.g., 'Y', '3M', '7D'). + """ + + self.freq = freq + self.mkt_price_year = datetime(mkt_price_year or datetime.today().year, 1, 1) + self.cost_growth_rate = cost_yearly_growth_rate + + self.init_cost = -abs(init_cost) + self.periodic_cost = -abs(periodic_cost) + self.periodic_income = abs(periodic_income) + + self.income_growth_rate = income_yearly_growth_rate + + if custom_cash_flows is not None: + self.custom_cash_flows = self._prepare_custom_flows(custom_cash_flows) + else: + self.custom_cash_flows = None + + def __repr__(self) -> str: + lines = [ + "CostIncome(", + f" mkt_price_year = {self.mkt_price_year.year}", + f" freq = {self.freq!r}", + f" init_cost = {self.init_cost:,.2f}", + f" periodic_cost = {self.periodic_cost:,.2f}", + f" periodic_income = {self.periodic_income:,.2f}", + f" cost_yearly_growth_rate = {self.cost_growth_rate:.2%}", + f" income_yearly_growth_rate = {self.income_growth_rate:.2%}", + f" custom_cash_flows = {None if self.custom_cash_flows is None else f'DataFrame({len(self.custom_cash_flows)} rows)'}", + ")", + ] + return "\n".join(lines) + + def _prepare_custom_flows(self, df: pd.DataFrame) -> pd.DataFrame: + """Process and resample custom cash flow dataframe + + Enforce costs as negative numbers and date to the correct frequency. + + Parameters + ---------- + df : pd.DataFrame + Custom cashflow + + Returns + ------- + pd.DataFrame + Processed custom cashflow + """ + + df = df.copy() + if "cost" in df.columns: + df["cost"] = -df["cost"].abs() + if "date" in df.columns: + df["date"] = pd.to_datetime(df["date"]) + df = df.set_index("date") + + freq = self._make_offset_compat(self.freq) + return df.resample(freq).sum() + + @staticmethod + def _make_offset_compat(freq: str, start=True) -> str: + suffix = "S" if start else "E" + match freq: + case "Y": + return "Y" + suffix + case "M": + return "M" + suffix + case "Q": + return "Q" + suffix + case _: + return freq + + @classmethod + def from_config(cls, config: CostIncomeConfig) -> "CostIncome": + """Create a `CostIncome` from a `CostIncomeConfig`. + + Parameters + ---------- + config : CostIncomeConfig + + Returns + ------- + CostIncome + """ + + df = None + if config.custom_cash_flows is not None: + df = pd.DataFrame(config.custom_cash_flows) + df["date"] = pd.to_datetime(df["date"]) + return cls( + mkt_price_year=config.mkt_price_year, + init_cost=config.init_cost, + periodic_cost=config.periodic_cost, + periodic_income=config.periodic_income, + cost_yearly_growth_rate=config.cost_yearly_growth_rate, + income_yearly_growth_rate=config.income_yearly_growth_rate, + custom_cash_flows=df, + freq=config.freq, + ) + + @classmethod + def from_dict(cls, args_dict: dict) -> "CostIncome": + """Create a `CostIncome` from a dictionary. + + Parameters + ---------- + args_dict : dict + + Returns + ------- + CostIncome + """ + + return cls.from_config( + CostIncomeConfig( + mkt_price_year=args_dict.get("mkt_price_year"), + init_cost=args_dict.get("init_cost", 0.0), + periodic_cost=args_dict.get("periodic_cost", 0.0), + periodic_income=args_dict.get("periodic_income", 0.0), + cost_yearly_growth_rate=args_dict.get("cost_yearly_growth_rate", 0.0), + income_yearly_growth_rate=args_dict.get( + "income_yearly_growth_rate", 0.0 + ), + freq=args_dict.get("freq", "Y"), + custom_cash_flows=args_dict.get("custom_cash_flows"), + ) + ) + + @classmethod + def from_yaml(cls, path: str) -> "CostIncome": + """Create a `CostIncome` from a yaml file. + + Parameters + ---------- + path : str + Path to the yaml file. + + Returns + ------- + CostIncome + """ + + import yaml + + with open(path) as f: + return cls.from_dict(yaml.safe_load(f)["cost_income"]) + + @classmethod + def _freq_to_days(cls, freq: str) -> str: + """ + Convert a frequency string to the equivalent number of days. + + Parameters: + ----------- + freq : str + A frequency string (e.g., 'D' for daily, 'M' for monthly, 'Y' for yearly). + + Returns: + -------- + float + The equivalent number of days for the given frequency string. + """ + + try: + # Convert the frequency string to a DateOffset object + freq = cls._make_offset_compat(freq, start=False) + offset = pd.tseries.frequencies.to_offset(freq) + + # Calculate the number of days by applying the offset to a base date + base_date = pd.Timestamp("2000-01-01") + end_date = base_date + offset + + # Return the difference in days + return f"{(end_date - base_date).days}d" + except ValueError: + raise ValueError(f"Invalid frequency string: {freq}") + + def _get_width_days(self) -> float: + """Return the number of days in the current frequency.""" + + ref = pd.Timestamp("2000-01-01") + freq = self._make_offset_compat(self.freq, start=False) + offset = pd.tseries.frequencies.to_offset(freq) + return float(((ref + offset) - ref).days) + + def _calc_at_date( + self, impl_date: pd.Timestamp, curr_date: pd.Timestamp + ) -> Tuple[float, float, float]: + r"""Calculate cash flows for a single timestamp. + + Computes the total cash flow, total cost, and total income for a given + evaluation date, accounting for growth rates applied to base costs and + incomes, as well as the custom cash flow if provided. + + The calculation applies compound growth to both costs and incomes based + on the number of years elapsed since the market price reference date + (`self.mkt_price_year`). + + Parameters + ---------- + impl_date : pd.Timestamp + The implementation date that determines which cost/income regime + applies. Dates before this use have no cost or income; "at the date" uses + the implementation cost, and dates after use the initialized or + periodic amounts respectively. + curr_date : pd.Timestamp + The evaluation date for which cash flows are being calculated. This + is compared against `impl_date` to determine the applicable base + amounts and is also used to index into `custom_cash_flows` if present. + + Returns + ------- + Tuple[float, float, float] + A tuple containing: + + * total_cash_flow : float + Net cash flow for the period, calculated as `total_income + total_cost`. + Note: Costs are typically negative values in financial contexts, + so this represents the net position. + * total_cost : float + Total cost amount for the period, including both standard and + custom cost components. + * total_income : float + Total income amount for the period, including both standard and + custom income components. + + Notes + ----- + Growth calculations use compound interest formula: + + .. math:: + factor = (1 + rate)^{years\_passed} + + where `years_passed` is computed as `(curr_date - mkt_price_year).days / 365.0`. + + Cost and income regimes: + + - **Before impl_date**: Both base cost and income are zero + - **At impl_date**: Uses `init_cost` for cost + - **After impl_date**: Uses `periodic_cost` for cost and `periodic_income` for income + + Custom cash flows (if `self.custom_cash_flows` is not None) are added + on top of the calculated standard amounts. Missing dates in the custom + cash flow DataFrame will raise a KeyError. + """ + # Calculate growth factor based on years from market price reference + years_passed = (curr_date - self.mkt_price_year).days / 365.0 + + cost_factor = (1 + self.cost_growth_rate) ** years_passed + inc_factor = (1 + self.income_growth_rate) ** years_passed + + if curr_date < impl_date: + cost, income = 0.0, 0.0 + elif curr_date == impl_date: + cost = self.init_cost * cost_factor + income = 0.0 + else: + cost = self.periodic_cost * cost_factor + income = self.periodic_income * inc_factor + + if ( + self.custom_cash_flows is not None + and curr_date in self.custom_cash_flows.index + ): + c_cost = cast(float, self.custom_cash_flows.loc[curr_date, "cost"]) + c_inc = cast(float, self.custom_cash_flows.loc[curr_date, "income"]) + else: + c_cost, c_inc = 0.0, 0.0 + + total_cost = cost + c_cost + total_inc = income + c_inc + return (total_inc + total_cost), total_cost, total_inc + + def calc_cash_flows( + self, impl_date, start_date, end_date + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Calculate net cash flows, costs, and incomes over a period. + + Computes cash flow metrics across a specified date range by iterating + through each period. + + The method creates a period range based on the configured frequency + (`self.freq`) and evaluates cash flows at the start time of each period. + Results are returned as NumPy arrays for efficient downstream processing. + + Parameters + ---------- + impl_date : + The implementation date that determines which cost/income regime + applies. + start_date : + The beginning of the calculation period. + end_date : + The end of the calculation period. + + Returns + ------- + Tuple[np.ndarray, np.ndarray, np.ndarray] + A tuple containing three NumPy arrays of equal length: + + * net : np.ndarray + Net cash flow for each period (income + cost). + * costs : np.ndarray + Total costs for each period. + * incomes : np.ndarray + Total incomes for each period. + """ + + impl_ts = pd.Timestamp(impl_date) + periods = pd.period_range(start=start_date, end=end_date, freq=self.freq) + + results = [self._calc_at_date(impl_ts, p.start_time) for p in periods] + net, costs, incs = map(np.array, zip(*results)) + return net, costs, incs + + def calc_total(self, impl_date, start_date, end_date) -> Tuple[float, float, float]: + """ + Calculate the total value of the cash flows over a given period. + + Parameters: + ----------- + impl_date: + The date the measure is implemented. + start_year: + The start date of the period. + end_year: int + The end year of the period. + + Returns: + -------- + Tuple[float, float, float] + the total net, cost, and income values over the given period. + """ + + net_cash_flows, costs, incomes = self.calc_cash_flows( + impl_date, start_date, end_date + ) + return np.sum(net_cash_flows), np.sum(costs), np.sum(incomes) + + def plot_cash_flows( + self, + impl_date: Any, + start_date: Any, + end_date: Any, + figsize: Tuple[int, int] = (12, 7), + title: Optional[str] = None, + ): + """Plot periodic and cumulative cash flows over a given period. + + Displays a two-panel figure: + - Top panel: stacked bar chart of costs and incomes per period, + with a net cash flow line overlay. + - Bottom panel: cumulative net cash flow over time. + + Parameters + ---------- + impl_date : + The date the measure is implemented. + start_date : + Start of the evaluation period. + end_date : + End of the evaluation period. + figsize : tuple, optional + Figure size as (width, height). Default is (12, 7). + title : str, optional + Overall figure title. Defaults to 'Cash Flow Analysis'. + + Returns + ------- + plt.Figure + """ + net, costs, incomes = self.calc_cash_flows(impl_date, start_date, end_date) + periods = pd.period_range(start=start_date, end=end_date, freq=self.freq) + dates = [p.start_time for p in periods] + cumulative_net = np.cumsum(net) + + width = pd.Timedelta(days=self._get_width_days() * 0.6) + + fig, (ax_bar, ax_cum) = plt.subplots( + 2, + 1, + figsize=figsize, + sharex=True, + gridspec_kw={"height_ratios": [3, 1], "hspace": 0.08}, + ) + + # --- top panel: stacked bars + net line --- + ax_bar.bar( + dates, incomes, width=width, color="#4C9BE8", label="Income", zorder=2 + ) + ax_bar.bar(dates, costs, width=width, color="#E8604C", label="Cost", zorder=2) + ax_bar.plot( + dates, + net, + color="black", + linewidth=1.5, + marker="o", + markersize=4, + label="Net", + zorder=3, + ) + ax_bar.axhline(0, color="black", linewidth=0.6, linestyle="--", zorder=1) + + ax_bar.set_ylabel("Cash flow") + ax_bar.legend(frameon=False, fontsize=9) + ax_bar.spines[["top", "right"]].set_visible(False) + ax_bar.tick_params(axis="x", which="both", bottom=False) + ax_bar.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x:,.0f}")) + + # --- bottom panel: cumulative net --- + # dates = dates.append() + positive = np.maximum(cumulative_net, 0) + negative = np.minimum(cumulative_net, 0) + ax_cum.fill_between(dates, positive, alpha=0.25, color="#4C9BE8", step="mid") + ax_cum.fill_between(dates, negative, alpha=0.25, color="#E8604C", step="mid") + # ax_cum.plot(dates, cumulative_net, color="black", linewidth=1.5, zorder=3) + ax_cum.axhline(0, color="black", linewidth=0.6, linestyle="--", zorder=1) + + ax_cum.set_ylabel("Cumulative net") + ax_cum.spines[["top", "right"]].set_visible(False) + ax_cum.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x:,.0f}")) + + fig.autofmt_xdate(rotation=30, ha="right") + fig.suptitle(title or "Cash Flow Analysis", fontsize=13, y=1.01) + return (ax_bar, ax_cum) + + def to_dataframe(self, impl_date, start_date, end_date) -> pd.DataFrame: + """Return cash flows as a formatted DataFrame.""" + net, costs, incs = self.calc_cash_flows(impl_date, start_date, end_date) + periods = pd.period_range(start=start_date, end=end_date, freq=self.freq) + + return pd.DataFrame( + {"date": periods, "net": net, "cost": costs, "income": incs} + ) + + @staticmethod + def comb_cost_income(cost_incomes: list["CostIncome"]) -> "CostIncome": + """Combine multiple CostIncomes together. + + Combination sums the costs and incomes from all provided CostIncome + objects. + """ + + first_ci = cost_incomes[0] + + if not all( + [ + first_ci.mkt_price_year.year == c.mkt_price_year.year + for c in cost_incomes + ] + ): + raise ValueError( + "Measure cost incomes have different market price years, combination is not possible." + ) + + if not all( + [first_ci.cost_growth_rate == c.cost_growth_rate for c in cost_incomes] + ): + raise ValueError( + "Measure cost incomes have different cost_growth_rate, combination is not possible." + ) + + if not all( + [first_ci.income_growth_rate == c.income_growth_rate for c in cost_incomes] + ): + raise ValueError( + "Measure cost incomes have different income_growth_rate, combination is not possible." + ) + + return CostIncome( + mkt_price_year=first_ci.mkt_price_year.year, + cost_yearly_growth_rate=first_ci.cost_growth_rate, + init_cost=sum(c.init_cost for c in cost_incomes), + periodic_cost=sum(c.periodic_cost for c in cost_incomes), + periodic_income=sum(c.periodic_income for c in cost_incomes), + income_yearly_growth_rate=first_ci.income_growth_rate, + ) diff --git a/climada/entity/measures/measure_config.py b/climada/entity/measures/measure_config.py new file mode 100644 index 0000000000..59f9278bba --- /dev/null +++ b/climada/entity/measures/measure_config.py @@ -0,0 +1,642 @@ +""" +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 configuration dataclasses for Measure reading and writing. +""" + +from __future__ import annotations + +import logging +import warnings +from abc import ABC +from dataclasses import asdict, dataclass, field, fields +from datetime import datetime +from typing import Dict, Optional, Tuple, Union + +import numpy as np +import pandas as pd + +LOGGER = logging.getLogger(__name__) + + +@dataclass +class _ModifierConfig(ABC): + """ + Abstract base class for all modifier configuration dataclasses. + + Provides shared serialization, deserialization, and representation + logic for all concrete modifier config subclasses. Not intended to + be instantiated directly. + """ + + def _filter_out_default_fields(self): + """ + Partition the instance's fields into non-default and default groups. + + The ``haz_type`` field is always excluded from the output, as it + is managed at the ``MeasureConfig`` level. + + Returns + ------- + non_defaults : dict + Fields whose current value differs from the dataclass default. + defaults : dict + Fields whose current value equals the dataclass default. + """ + + non_defaults = {} + defaults = {} + for defined_field in fields(self): + val = getattr(self, defined_field.name) + default = defined_field.default + if defined_field.default_factory is not field().default_factory: + default = defined_field.default_factory() + + if val != default: + non_defaults[defined_field.name] = val + else: + defaults[defined_field.name] = val + + if "haz_type" in non_defaults: + non_defaults.pop("haz_type") + return non_defaults, defaults + + def to_dict(self): + """ + Serialize the config to a flat dictionary, omitting default values. + + The ``haz_type`` field is always excluded from the output, as it + is managed at the ``MeasureConfig`` level. + + Returns + ------- + dict + Dictionary containing only fields whose values differ from + their dataclass defaults. + """ + non_default, _ = self._filter_out_default_fields() + return non_default + + @classmethod + def from_dict(cls, kwargs_dict: dict): + """ + Instantiate a config from a dictionary, ignoring unknown keys. + + Parameters + ---------- + kwargs_dict : dict + Input dictionary. Keys not matching any dataclass field are + silently discarded. + + Returns + ------- + _ModifierConfig + A new instance of the calling subclass. + """ + + filtered = cls._filter_dict_to_fields(kwargs_dict) + return cls(**filtered) + + @classmethod + def _filter_dict_to_fields(cls, to_filter: dict): + """ + Filter a dictionary to only the keys matching the dataclass fields. + + Parameters + ---------- + to_filter : dict + Input dictionary, potentially containing extra keys. + + Returns + ------- + dict + A copy of ``to_filter`` restricted to keys that correspond to declared + dataclass fields on this class. + """ + + filtered = dict( + filter(lambda k: k[0] in [f.name for f in fields(cls)], to_filter.items()) + ) + return filtered + + def __repr__(self) -> str: + """ + Return a human-readable representation highlighting non-default fields. + + Non-default fields are shown prominently; default fields are shown + below them. This makes it easy to see at a glance what has been + configured on an instance. + + Returns + ------- + str + A formatted string representation of the instance. + """ + + non_defaults, defaults = self._filter_out_default_fields() + ndf_fields_str = ( + "\n\t\t\t".join(f"{k}={v!r}" for k, v in non_defaults.items()) + if non_defaults + else None + ) + _ = ( + "\n\t\t\t".join(f"{k}={v!r}" for k, v in defaults.items()) + if defaults + else None + ) + ndf_fields = ( + "(" "\n\t\tNon default fields:" f"\n\t\t\t{ndf_fields_str}" "\n)" + if ndf_fields_str + else "()" + ) + return f"{self.__class__.__name__}{ndf_fields}" + + +@dataclass(repr=False) +class ImpfsetModifierConfig(_ModifierConfig): + """ + Configuration for modifications to an impact function set. + + Supports scaling or shifting MDD, PAA, and intensity curves, as well + as replacement of the impact function set, loaded from a file path. If + both a new file path and modifier values are provided, modifiers are + applied after the replacement (and a warning is issued). + + Parameters + ---------- + haz_type : str + Hazard type identifier (e.g. ``"TC"``) that this modifier targets. + impf_ids : int or str or list of int or str, optional + Impact function ID(s) to which modifications are applied. + If ``None``, all impact functions are affected. + impf_mdd_mult : float, optional + Multiplicative factor applied to the mean damage degree (MDD) curve. + Default is ``1.0`` (no change). + impf_mdd_add : float, optional + Additive offset applied to the MDD curve after multiplication. + Default is ``0.0``. + impf_paa_mult : float, optional + Multiplicative factor applied to the percentage of affected assets + (PAA) curve. Default is ``1.0``. + impf_paa_add : float, optional + 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. + Default is ``1.0``. + impf_int_add : float, optional + Additive offset applied to the intensity axis after multiplication. + Default is ``0.0``. + new_impfset_path : str, optional + Path to an Excel file containing a replacement impact function set. + If provided alongside modifier values, a warning is issued and + modifiers are applied after loading the new set. + + Warns + ----- + UserWarning + If ``new_impfset_path`` is set alongside any non-default modifier + values. + """ + + haz_type: str + impf_ids: Optional[Union[int, str, list[Union[int, str]]]] = None + impf_mdd_mult: float = 1.0 + impf_mdd_add: float = 0.0 + impf_paa_mult: float = 1.0 + impf_paa_add: float = 0.0 + impf_int_mult: float = 1.0 + impf_int_add: float = 0.0 + new_impfset_path: Optional[str] = None + + def __post_init__(self): + config = self.to_dict() + if "new_impfset_path" in config and any( + key in config + for key in [ + "impf_mdd_add", + "impf_mdd_mult", + "impf_paa_add", + "impf_paa_mult", + "impf_int_add", + "impf_int_mult", + ] + ): + warnings.warn( + "Both new impfset object and impfset modifiers are provided, " + "modifiers will be applied after changing the impfset." + ) + + +@dataclass(repr=False) +class HazardModifierConfig(_ModifierConfig): + """ + Configuration for modifications to a hazard. + + Supports scaling or shifting hazard intensity, applying a return-period + frequency cutoff, and replacement of the hazard, loaded from a file path. + If both a new file path and modifier values are provided, modifiers are + applied after the replacement. + + Parameters + ---------- + haz_type : str + Hazard type identifier (e.g. ``"TC"``) that this modifier targets. + haz_int_mult : float, optional + Multiplicative factor applied to hazard intensity. + Default is ``1.0`` (no change). + haz_int_add : float, optional + Additive offset applied to hazard intensity after multiplication. + Default is ``0.0``. + new_hazard_path : str, optional + Path to an HDF5 file containing a replacement hazard. + If provided alongside modifier values, a warning is issued and + modifiers are applied after loading the new hazard. + impact_rp_cutoff : float, optional + Return period (in years) below which hazard events are discarded. + If ``None``, no cutoff is applied. + + Warns + ----- + UserWarning + If ``new_hazard_path`` is set alongside any non-default modifier + values or a non-``None`` ``impact_rp_cutoff``. + """ + + haz_type: str + haz_int_mult: Optional[float] = 1.0 + haz_int_add: Optional[float] = 0.0 + haz_freq_mult: Optional[float] = 1.0 + haz_freq_add: Optional[float] = 0.0 + new_hazard_path: Optional[str] = None + impact_rp_cutoff: Optional[float] = None + + def __post_init__(self): + config = self.to_dict() + if "new_hazard_path" in config and any( + key in config + for key in [ + "haz_int_mult", + "haz_int_add", + "haz_freq_mult", + "haz_freq_add", + "impact_rp_cutoff", + ] + ): + warnings.warn( + "Both new hazard object and hazard modifiers are provided, " + "modifiers will be applied after changing the hazard." + ) + + +@dataclass(repr=False) +class ExposuresModifierConfig(_ModifierConfig): + """ + Configuration for modifications to an exposures object. + + Supports remapping impact function IDs, zeroing out selected regions, + and replacement of the exposures from a new file. If both a new + file path and modifier values are provided, modifiers are applied after + the replacement. + + Parameters + ---------- + reassign_impf_id : dict of {str: dict of {int or str: int or str}}, optional + Nested mapping ``{haz_type: {old_id: new_id}}`` used to reassign + impact function IDs in the exposures. If ``None``, no remapping + is performed. + set_to_zero : list of int, optional + Region IDs for which exposure values are set to zero. + If ``None``, no zeroing is applied. + new_exposures_path : str, optional + Path to an HDF5 file containing replacement exposures. + If provided alongside modifier values, a warning is issued and + modifiers are applied after loading the new exposures. + + Warns + ----- + UserWarning + If ``new_exposures_path`` is set alongside any non-``None`` + modifier values. + """ + + reassign_impf_id: Optional[Dict[str, Dict[int | str, int | str]]] = None + set_to_zero: Optional[list[int]] = None + new_exposures_path: Optional[str] = None + + def __post_init__(self): + config = self.to_dict() + if "new_exposures_path" in config and any( + key in config for key in ["reassign_impf_id", "set_to_zero"] + ): + warnings.warn( + "Both new exposures object and exposures modifiers are provided, " + "modifiers will be applied after changing the exposures." + ) + + +@dataclass(repr=False) +class CostIncomeConfig(_ModifierConfig): + """ + Serializable configuration for a ``CostIncome`` object. + + Encodes all parameters required to construct a ``CostIncome`` instance, + including optional custom cash flow schedules. + + Parameters + ---------- + mkt_price_year : int, optional + Reference year for market prices. Defaults to the current year. + init_cost : float, optional + One-time initial investment cost (positive value). Default is ``0.0``. + periodic_cost : float, optional + Recurring cost per period (positive value). Default is ``0.0``. + periodic_income : float, optional + Recurring income per period. Default is ``0.0``. + cost_yearly_growth_rate : float, optional + Annual growth rate applied to periodic costs. Default is ``0.0``. + income_yearly_growth_rate : float, optional + Annual growth rate applied to periodic income. Default is ``0.0``. + freq : str, optional + Pandas offset alias defining the period length (e.g. ``"Y"`` for + yearly, ``"M"`` for monthly). Default is ``"Y"``. + custom_cash_flows : list of dict, optional + 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. + """ + + mkt_price_year: Optional[int] = field(default_factory=lambda: datetime.today().year) + init_cost: float = 0.0 + periodic_cost: float = 0.0 + periodic_income: float = 0.0 + cost_yearly_growth_rate: float = 0.0 + income_yearly_growth_rate: float = 0.0 + freq: str = "Y" + custom_cash_flows: Optional[list[dict]] = None + + @classmethod + def from_cost_income(cls, cost_income: "CostIncome") -> "CostIncomeConfig": + """ + Construct a :class:`CostIncomeConfig` from a live + :class:`CostIncome` object. + + Parameters + ---------- + cost_income : CostIncome + The live ``CostIncome`` instance to serialise. + + Returns + ------- + CostIncomeConfig + The config instance equivalent to the ``CostIncome``. + """ + + custom = None + if cost_income.custom_cash_flows is not None: + custom = ( + cost_income.custom_cash_flows.reset_index() + .rename(columns={"index": "date"}) + .assign(date=lambda df: df["date"].dt.strftime("%Y-%m-%d")) + .to_dict(orient="records") + ) + return cls( + mkt_price_year=cost_income.mkt_price_year.year, # datetime → int + init_cost=abs(cost_income.init_cost), # stored negative → positive + periodic_cost=abs(cost_income.periodic_cost), + periodic_income=cost_income.periodic_income, + cost_yearly_growth_rate=cost_income.cost_growth_rate, + income_yearly_growth_rate=cost_income.income_growth_rate, + freq=cost_income.freq, + custom_cash_flows=custom, + ) + + +@dataclass(repr=False) +class MeasureConfig(_ModifierConfig): + """ + Top-level serializable configuration for a single adaptation measure. + + Aggregates all modifier sub-configs (hazard, impact functions, exposures, + cost/income) into a single object that can be round-tripped through dict, + YAML, or a legacy Excel row. + + This class is the primary entry point for defining measures in a + declarative, file-based workflow and serves as the serialization + counterpart to :class:`~climada.entity.measures.base.Measure`. + + Parameters + ---------- + name : str + Unique name identifying this measure. + haz_type : str + Hazard type identifier (e.g. ``"TC"``) this measure is designed for. + impfset_modifier : ImpfsetModifierConfig + Configuration describing modifications to the impact function set. + hazard_modifier : HazardModifierConfig + Configuration describing modifications to the hazard. + exposures_modifier : ExposuresModifierConfig + Configuration describing modifications to the exposures. + cost_income : CostIncomeConfig + Financial parameters associated with implementing this measure. + implementation_duration : str, optional + Pandas offset alias (e.g. ``"2Y"``) representing the time before + the measure is fully operational. If ``None``, the measure takes + effect immediately. + color_rgb : tuple of float, optional + RGB colour triple in the range ``[0, 1]`` used for visualisation. + If ``None``, defaults to black ``(0, 0, 0)``. + """ + + name: str + haz_type: str + impfset_modifier: ImpfsetModifierConfig + hazard_modifier: HazardModifierConfig + exposures_modifier: ExposuresModifierConfig + cost_income: CostIncomeConfig + implementation_duration: Optional[str] = None + color_rgb: Optional[Tuple[float, float, float]] = None + + def __repr__(self) -> str: + """ + Return a detailed string representation of the measure configuration. + + All fields are shown, including sub-configs, with each on its own + indented line. + + Returns + ------- + str + A formatted multi-line string representation. + """ + + fields_str = "\n\t".join(f"{k}={v!r}" for k, v in self.__dict__.items()) + return f"{self.__class__.__name__}(\n\t{fields_str})" + + def to_dict(self) -> dict: + """ + Serialize the measure configuration to a flat dictionary. + + Sub-config dictionaries are merged into the top-level dict (i.e. + their keys are inlined, not nested). ``haz_type`` is always included + at the top level. Fields with ``None`` values are preserved. + + Returns + ------- + dict + Flat dictionary representation suitable for YAML or Excel + serialization. + """ + + return { + "name": self.name, + "haz_type": self.haz_type, + **self.impfset_modifier.to_dict(), + **self.hazard_modifier.to_dict(), + **self.exposures_modifier.to_dict(), + **self.cost_income.to_dict(), + "implementation_duration": self.implementation_duration, + "color_rgb": list(self.color_rgb) if self.color_rgb is not None else None, + } + + @classmethod + def from_dict(cls, kwargs_dict: dict) -> "MeasureConfig": + """ + Instantiate a :class:`MeasureConfig` from a flat dictionary. + + Delegates sub-config construction to the respective + ``from_dict`` classmethods. Unknown keys are silently discarded + by each sub-config parser. + + Parameters + ---------- + kwargs_dict : dict + Flat dictionary, as produced by :meth:`to_dict` or read from + a legacy Excel row. Must contain at minimum ``"name"`` and + ``"haz_type"``. + + Returns + ------- + MeasureConfig + A fully populated configuration instance. + """ + + return cls( + name=kwargs_dict["name"], + haz_type=kwargs_dict["haz_type"], + impfset_modifier=ImpfsetModifierConfig.from_dict(kwargs_dict), + hazard_modifier=HazardModifierConfig.from_dict(kwargs_dict), + exposures_modifier=ExposuresModifierConfig.from_dict(kwargs_dict), + cost_income=CostIncomeConfig.from_dict(kwargs_dict), + implementation_duration=kwargs_dict.get("implementation_duration"), + color_rgb=cls._normalize_color(kwargs_dict.get("color_rgb")), + ) + + @staticmethod + def _normalize_color(color_rgb): + # 1. Handle None and NaN (np.nan, pd.NA, float('nan')) + if color_rgb is None or pd.isna(color_rgb) is True: + return None + + # 2. Convert sequence types (list, np.array, tuple) to a standard tuple + try: + # Flatten in case it's a nested numpy array, then convert to tuple + result = tuple(np.array(color_rgb).flatten().tolist()) + + # 3. Enforce the length of three + if len(result) != 3: + raise ValueError(f"Expected 3 digits, got {len(result)}") + + return result + + except (TypeError, ValueError) as err: + # Handle cases where input isn't iterable or wrong length + raise ValueError(f"Invalid color format: {color_rgb}.") from err + + def to_yaml(self, path: str) -> None: + """ + Write this configuration to a YAML file. + + The file is structured as ``{"measures": []}``, + matching the expected format for :meth:`from_yaml`. + + Parameters + ---------- + path : str + Destination file path. Will be created or overwritten. + """ + + import yaml + + with open(path, "w") as opened_file: + yaml.dump( + {"measures": [self.to_dict()]}, + opened_file, + default_flow_style=False, + sort_keys=False, + ) + + @classmethod + def from_yaml(cls, path: str) -> "MeasureConfig": + """ + Load a :class:`MeasureConfig` from a YAML file. + + Expects the file to contain a top-level ``"measures"`` list; reads + only the first entry. + + Parameters + ---------- + path : str + Path to the YAML file to read. + + Returns + ------- + MeasureConfig + The configuration parsed from the first entry in + ``measures``. + """ + + import yaml + + with open(path) as opened_file: + return cls.from_dict(yaml.safe_load(opened_file)["measures"][0]) + + @classmethod + def from_row(cls, row: pd.Series) -> "MeasureConfig": + """ + Construct a :class:`MeasureConfig` from a legacy Excel row. + + Converts the row to a dictionary and delegates to :meth:`from_dict`. + This is the primary migration path for measures currently stored in + the legacy Excel-based ``MeasureSet`` format. + + Parameters + ---------- + row : pd.Series + A single row from a legacy measures Excel sheet, with column + names matching the flat dictionary keys expected by + :meth:`from_dict`. + + Returns + ------- + MeasureConfig + A configuration instance populated from the row data. + """ + + row_dict = row.to_dict() + return cls.from_dict(row_dict) diff --git a/climada/entity/measures/test/test_cost_income.py b/climada/entity/measures/test/test_cost_income.py new file mode 100644 index 0000000000..f9d6667db3 --- /dev/null +++ b/climada/entity/measures/test/test_cost_income.py @@ -0,0 +1,405 @@ +""" +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 . + +--- + +Unit tests for the CostIncome class. +""" + +from datetime import datetime +from unittest.mock import MagicMock, patch + +import numpy as np +import pandas as pd +import pytest + +from climada.entity.disc_rates.base import DiscRates +from climada.entity.measures.cost_income import CostIncome + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def make_disc_rates(years=None, rates=None): + """Build a minimal DiscRates stub.""" + dr = MagicMock(spec=DiscRates) + dr.years = np.array(years or list(range(2020, 2031))) + dr.rates = np.array(rates or [0.05] * len(dr.years)) + return dr + + +# --------------------------------------------------------------------------- +# __init__ / construction +# --------------------------------------------------------------------------- + + +class TestInit: + def test_defaults(self): + ci = CostIncome() + assert ci.init_cost == 0.0 + assert ci.periodic_cost == 0.0 + assert ci.periodic_income == 0.0 + assert ci.cost_growth_rate == 0.0 + assert ci.income_growth_rate == 0.0 + assert ci.freq == "Y" + assert ci.custom_cash_flows is None + + def test_costs_stored_negative(self): + ci = CostIncome(init_cost=100, periodic_cost=50) + assert ci.init_cost == -100.0 + assert ci.periodic_cost == -50.0 + + def test_costs_already_negative_stay_negative(self): + ci = CostIncome(init_cost=-200, periodic_cost=-30) + assert ci.init_cost == -200.0 + assert ci.periodic_cost == -30.0 + + def test_income_stored_positive(self): + ci = CostIncome(periodic_income=-80) + assert ci.periodic_income == 80.0 + + def test_mkt_price_year_default_is_current_year(self): + ci = CostIncome() + assert ci.mkt_price_year.year == datetime.today().year + + def test_mkt_price_year_custom(self): + ci = CostIncome(mkt_price_year=2015) + assert ci.mkt_price_year.year == 2015 + + def test_custom_cash_flows_processed(self): + df = pd.DataFrame( + { + "date": ["2020-01-01", "2020-06-01"], + "cost": [100, 200], + "income": [50, 60], + } + ) + ci = CostIncome(custom_cash_flows=df, freq="Y") + # After resampling to yearly, should have one row per year + assert isinstance(ci.custom_cash_flows, pd.DataFrame) + assert "cost" in ci.custom_cash_flows.columns + # Costs should be negative after processing + assert (ci.custom_cash_flows["cost"] <= 0).all() + + +# --------------------------------------------------------------------------- +# from_dict / from_config / from_yaml +# --------------------------------------------------------------------------- + + +class TestFromDict: + def test_basic(self): + d = { + "mkt_price_year": 2020, + "init_cost": 500, + "periodic_cost": 100, + "periodic_income": 200, + "cost_yearly_growth_rate": 0.02, + "income_yearly_growth_rate": 0.03, + "freq": "Y", + } + ci = CostIncome.from_dict(d) + assert ci.init_cost == -500.0 + assert ci.periodic_income == 200.0 + assert ci.cost_growth_rate == 0.02 + + def test_defaults_for_missing_keys(self): + ci = CostIncome.from_dict({}) + assert ci.init_cost == 0.0 + assert ci.freq == "Y" + + def test_with_custom_cash_flows(self): + d = { + "freq": "Y", + "custom_cash_flows": [ + {"date": "2021-01-01", "cost": 100, "income": 50}, + ], + } + ci = CostIncome.from_dict(d) + assert ci.custom_cash_flows is not None + + +class TestFromYaml: + def test_from_yaml(self, tmp_path): + yaml_content = """ +cost_income: + mkt_price_year: 2020 + init_cost: 1000 + periodic_cost: 200 + periodic_income: 300 + cost_yearly_growth_rate: 0.01 + income_yearly_growth_rate: 0.02 + freq: Y +""" + p = tmp_path / "ci.yaml" + p.write_text(yaml_content) + ci = CostIncome.from_yaml(str(p)) + assert ci.init_cost == -1000.0 + assert ci.periodic_income == 300.0 + + +# --------------------------------------------------------------------------- +# _freq_to_days +# --------------------------------------------------------------------------- + + +class TestFreqToDays: + def test_yearly(self): + result = CostIncome._freq_to_days("Y") + assert result == "365d" + + def test_monthly(self): + result = CostIncome._freq_to_days("M") + assert result == "30d" + + def test_daily(self): + result = CostIncome._freq_to_days("D") + assert result == "1d" + + def test_invalid(self): + with pytest.raises(ValueError): + CostIncome._freq_to_days("INVALID_FREQ_XYZ") + + +# --------------------------------------------------------------------------- +# _get_width_days +# --------------------------------------------------------------------------- + + +class TestGetWidthDays: + def test_yearly(self): + ci = CostIncome(freq="Y") + assert ci._get_width_days() == 365.0 + + def test_3yearly(self): + ci = CostIncome(freq="3Y") + assert ci._get_width_days() == 3 * 365.0 + + def test_monthly(self): + ci = CostIncome(freq="M") + assert ci._get_width_days() == 30.0 + + def test_daily(self): + ci = CostIncome(freq="D") + assert ci._get_width_days() == 1.0 + + +# --------------------------------------------------------------------------- +# _calc_at_date +# --------------------------------------------------------------------------- + + +class TestCalcAtDate: + def test_before_impl_date_is_zero(self): + ci = CostIncome(mkt_price_year=2020, init_cost=1000, periodic_income=500) + impl = pd.Timestamp("2021-01-01") + curr = pd.Timestamp("2020-01-01") + net, cost, inc = ci._calc_at_date(impl, curr) + assert net == 0.0 + assert cost == 0.0 + assert inc == 0.0 + + def test_at_impl_date_uses_init_cost(self): + ci = CostIncome(mkt_price_year=2021, init_cost=1000, periodic_income=0) + impl = pd.Timestamp("2021-01-01") + net, cost, inc = ci._calc_at_date(impl, impl) + assert cost == pytest.approx(-1000.0, rel=1e-3) + assert inc == pytest.approx(0.0) + + def test_after_impl_date_uses_periodic_cost(self): + ci = CostIncome(mkt_price_year=2020, periodic_cost=200, periodic_income=0) + impl = pd.Timestamp("2021-01-01") + curr = pd.Timestamp("2022-01-01") + net, cost, inc = ci._calc_at_date(impl, curr) + assert cost < 0 + assert abs(cost) == 200 + + def test_income_growth_applied(self): + ci = CostIncome( + mkt_price_year=2020, periodic_income=100, income_yearly_growth_rate=0.10 + ) + impl = pd.Timestamp("2020-01-01") + curr = pd.Timestamp("2021-01-01") + _, _, inc = ci._calc_at_date(impl, curr) + expected = 100 * (1.10**1.0) + assert inc == pytest.approx(expected, rel=1e-2) + + def test_net_equals_income_plus_cost(self): + ci = CostIncome(mkt_price_year=2020, periodic_cost=100, periodic_income=150) + impl = pd.Timestamp("2020-01-01") + curr = pd.Timestamp("2021-01-01") + net, cost, inc = ci._calc_at_date(impl, curr) + assert net == pytest.approx(cost + inc) + + def test_custom_cash_flows_added(self): + df = pd.DataFrame( + { + "date": ["2021-01-01"], + "cost": [500.0], + "income": [200.0], + } + ) + ci = CostIncome(mkt_price_year=2021, custom_cash_flows=df, freq="Y") + print(ci.custom_cash_flows) + impl = pd.Timestamp("2021-01-01") + curr = pd.Timestamp("2021-01-01") + net, cost, inc = ci._calc_at_date(impl, curr) + assert inc == pytest.approx(200.0, rel=1e-3) + assert cost == pytest.approx(-500.0, rel=1e-3) + + +# --------------------------------------------------------------------------- +# calc_cash_flows +# --------------------------------------------------------------------------- + + +class TestCalcCashFlows: + def test_returns_three_arrays(self): + ci = CostIncome(mkt_price_year=2020, periodic_income=100) + net, costs, incs = ci.calc_cash_flows("2020-01-01", "2020-01-01", "2025-01-01") + assert isinstance(net, np.ndarray) + assert isinstance(costs, np.ndarray) + assert isinstance(incs, np.ndarray) + + def test_length_matches_periods(self): + ci = CostIncome(freq="Y") + net, costs, incs = ci.calc_cash_flows("2020-01-01", "2020-01-01", "2024-01-01") + periods = pd.period_range("2020-01-01", "2024-01-01", freq="Y") + assert len(net) == len(periods) + + def test_zero_cost_income(self): + ci = CostIncome() + net, costs, incs = ci.calc_cash_flows("2020-01-01", "2020-01-01", "2023-01-01") + np.testing.assert_array_equal(net, 0.0) + + def test_nonzero_cost_income(self): + ci = CostIncome( + mkt_price_year=2020, init_cost=5000, periodic_cost=200, periodic_income=1000 + ) + net, cost, income = ci.calc_cash_flows( + impl_date="2020-01-01", start_date="2019-01-01", end_date="2025-01-01" + ) + np.testing.assert_array_equal( + net, [0.0, -5000.0, 800.0, 800.0, 800.0, 800.0, 800.0] + ) + np.testing.assert_array_equal( + cost, [0.0, -5000.0, -200.0, -200.0, -200.0, -200.0, -200.0] + ) + np.testing.assert_array_equal( + income, [0.0, 0.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0] + ) + + +# --------------------------------------------------------------------------- +# calc_total +# --------------------------------------------------------------------------- + + +class TestCalcTotal: + def test_total_is_sum_of_cash_flows(self): + ci = CostIncome(mkt_price_year=2020, periodic_income=100, periodic_cost=50) + net_arr, cost_arr, inc_arr = ci.calc_cash_flows( + "2020-01-01", "2020-01-01", "2024-01-01" + ) + total_net, total_cost, total_inc = ci.calc_total( + "2020-01-01", "2020-01-01", "2024-01-01" + ) + assert total_net == pytest.approx(float(np.sum(net_arr))) + assert total_cost == pytest.approx(float(np.sum(cost_arr))) + assert total_inc == pytest.approx(float(np.sum(inc_arr))) + + def test_returns_floats(self): + ci = CostIncome() + result = ci.calc_total("2020-01-01", "2020-01-01", "2022-01-01") + assert all(isinstance(v, (float, np.floating)) for v in result) + + +# --------------------------------------------------------------------------- +# to_dataframe +# --------------------------------------------------------------------------- + + +class TestToDataframe: + def test_columns(self): + ci = CostIncome(periodic_income=100) + df = ci.to_dataframe("2020-01-01", "2020-01-01", "2023-01-01") + assert set(df.columns) == {"date", "net", "cost", "income"} + + def test_row_count(self): + ci = CostIncome(freq="Y") + df = ci.to_dataframe("2020-01-01", "2020-01-01", "2022-01-01") + expected = len(pd.period_range("2020-01-01", "2022-01-01", freq="Y")) + assert len(df) == expected + + +# --------------------------------------------------------------------------- +# comb_cost_income +# --------------------------------------------------------------------------- + + +class TestCombCostIncome: + def test_costs_are_summed(self): + ci1 = CostIncome(mkt_price_year=2020, init_cost=100, periodic_cost=50) + ci2 = CostIncome(mkt_price_year=2020, init_cost=200, periodic_cost=30) + combined = CostIncome.comb_cost_income([ci1, ci2]) + assert combined.init_cost == -300.0 + assert combined.periodic_cost == -80.0 + + def test_incomes_are_summed(self): + ci1 = CostIncome(mkt_price_year=2020, periodic_income=100) + ci2 = CostIncome(mkt_price_year=2020, periodic_income=200) + combined = CostIncome.comb_cost_income([ci1, ci2]) + assert combined.periodic_income == 300.0 + + def test_mismatched_mkt_price_year_raises(self): + ci1 = CostIncome(mkt_price_year=2020) + ci2 = CostIncome(mkt_price_year=2021) + with pytest.raises(ValueError, match="market price years"): + CostIncome.comb_cost_income([ci1, ci2]) + + def test_mismatched_cost_growth_rate_raises(self): + ci1 = CostIncome(mkt_price_year=2020, cost_yearly_growth_rate=0.02) + ci2 = CostIncome(mkt_price_year=2020, cost_yearly_growth_rate=0.05) + with pytest.raises(ValueError, match="cost_growth_rate"): + CostIncome.comb_cost_income([ci1, ci2]) + + def test_mismatched_income_growth_rate_raises(self): + ci1 = CostIncome(mkt_price_year=2020, income_yearly_growth_rate=0.01) + ci2 = CostIncome(mkt_price_year=2020, income_yearly_growth_rate=0.03) + with pytest.raises(ValueError, match="income_growth_rate"): + CostIncome.comb_cost_income([ci1, ci2]) + + def test_single_element_list(self): + ci = CostIncome(mkt_price_year=2020, init_cost=500, periodic_income=100) + combined = CostIncome.comb_cost_income([ci]) + assert combined.init_cost == -500.0 + assert combined.periodic_income == 100.0 + + def test_preserves_growth_rates(self): + ci1 = CostIncome( + mkt_price_year=2020, + cost_yearly_growth_rate=0.02, + income_yearly_growth_rate=0.03, + ) + ci2 = CostIncome( + mkt_price_year=2020, + cost_yearly_growth_rate=0.02, + income_yearly_growth_rate=0.03, + ) + combined = CostIncome.comb_cost_income([ci1, ci2]) + assert combined.cost_growth_rate == 0.02 + assert combined.income_growth_rate == 0.03 diff --git a/climada/entity/measures/test/test_measure_config.py b/climada/entity/measures/test/test_measure_config.py new file mode 100644 index 0000000000..5f0d54badb --- /dev/null +++ b/climada/entity/measures/test/test_measure_config.py @@ -0,0 +1,515 @@ +""" +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 . + +--- + +Tests for MeasureConfig and related dataclasses. +""" + +# tests/entity/measures/test_measure_config.py + +import logging +import warnings +from datetime import datetime + +import pandas as pd +import pytest + +from climada.entity.measures.measure_config import ( + CostIncomeConfig, + ExposuresModifierConfig, + HazardModifierConfig, + ImpfsetModifierConfig, + MeasureConfig, +) + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def minimal_measure_dict(): + return {"name": "seawall", "haz_type": "TC"} + + +@pytest.fixture +def full_measure_dict(): + return { + "name": "seawall", + "haz_type": "TC", + "haz_int_mult": 0.8, + "haz_int_add": -0.1, + "impf_mdd_mult": 0.9, + "impf_paa_mult": 0.95, + "impf_ids": [1, 2], + "reassign_impf_id": {"TC": {1: 3}}, + "set_to_zero": [10, 20], + "init_cost": 1000.0, + "periodic_cost": 50.0, + "color_rgb": [0.1, 0.5, 0.9], + "implementation_duration": "2Y", + } + + +@pytest.fixture +def basic_impfset_config(): + return ImpfsetModifierConfig(haz_type="TC", impf_mdd_mult=0.9) + + +@pytest.fixture +def basic_hazard_config(): + return HazardModifierConfig(haz_type="TC", haz_int_mult=0.8) + + +@pytest.fixture +def basic_exposures_config(): + return ExposuresModifierConfig(reassign_impf_id={"TC": {1: 2}}) + + +@pytest.fixture +def basic_cost_income_config(): + return CostIncomeConfig(init_cost=1000.0, periodic_cost=50.0) + + +@pytest.fixture +def full_measure_config(full_measure_dict): + return MeasureConfig.from_dict(full_measure_dict) + + +# --------------------------------------------------------------------------- +# _ModifierConfig (via concrete subclasses) +# --------------------------------------------------------------------------- + + +def test_modifier_config_to_dict_omits_defaults(): + config = ImpfsetModifierConfig(haz_type="TC") + result = config.to_dict() + assert result == {} + + +def test_modifier_config_to_dict_includes_non_defaults(): + config = ImpfsetModifierConfig(haz_type="TC", impf_mdd_mult=0.5, impf_paa_add=0.1) + result = config.to_dict() + assert result["impf_mdd_mult"] == 0.5 + assert result["impf_paa_add"] == 0.1 + + +def test_modifier_config_from_dict_ignores_unknown_keys(): + d = {"haz_type": "TC", "unknown_field": 99, "another_unknown": "foo"} + config = ImpfsetModifierConfig.from_dict(d) + assert config.haz_type == "TC" + assert not hasattr(config, "unknown_field") + + +def test_modifier_config_from_dict_roundtrip(): + config = ImpfsetModifierConfig(haz_type="TC", impf_mdd_mult=0.5, impf_paa_add=0.1) + d = {**config.to_dict(), "haz_type": "TC"} + recovered = ImpfsetModifierConfig.from_dict(d) + assert recovered.impf_mdd_mult == config.impf_mdd_mult + assert recovered.impf_paa_add == config.impf_paa_add + + +def test_modifier_config_filter_dict_to_fields_filters_extra_keys(): + d = {"haz_type": "TC", "impf_mdd_mult": 0.5, "not_a_field": 123} + filtered = ImpfsetModifierConfig._filter_dict_to_fields(d) + assert "not_a_field" not in filtered + assert "haz_type" in filtered + assert "impf_mdd_mult" in filtered + assert filtered["haz_type"] == "TC" + assert filtered["impf_mdd_mult"] == 0.5 + + +def test_modifier_config_filter_out_default_fields_partitions_correctly(): + config = ImpfsetModifierConfig(haz_type="TC", impf_mdd_mult=0.5) + non_defaults, defaults = config._filter_out_default_fields() + assert "impf_mdd_mult" in non_defaults + assert "impf_mdd_mult" not in defaults + assert "impf_paa_mult" in defaults + assert "impf_paa_mult" not in non_defaults + from dataclasses import fields + + all_field_names = {f.name for f in fields(config) if f.name != "haz_type"} + assert set(non_defaults) | set(defaults) == all_field_names + + +def test_modifier_config_repr_shows_non_defaults_prominently(): + config = ImpfsetModifierConfig(haz_type="TC", impf_mdd_mult=0.5) + r = repr(config) + assert "Non default fields" in r + assert "impf_mdd_mult" in r + + +def test_modifier_config_repr_empty_when_all_defaults(): + config = ImpfsetModifierConfig(haz_type="TC") + r = repr(config) + assert "Non default fields" not in r + + +# --------------------------------------------------------------------------- +# ImpfsetModifierConfig +# --------------------------------------------------------------------------- + + +def test_impfset_modifier_config_defaults(): + config = ImpfsetModifierConfig(haz_type="TC") + assert config.impf_ids is None + assert config.impf_mdd_mult == 1.0 + assert config.impf_mdd_add == 0.0 + assert config.impf_paa_mult == 1.0 + assert config.impf_paa_add == 0.0 + assert config.impf_int_mult == 1.0 + assert config.impf_int_add == 0.0 + assert config.new_impfset_path is None + + +def test_impfset_modifier_config_from_dict_roundtrip(): + config = ImpfsetModifierConfig(haz_type="TC", impf_mdd_mult=0.8, impf_ids=[1, 2]) + d = {**config.to_dict(), "haz_type": "TC"} + recovered = ImpfsetModifierConfig.from_dict(d) + assert recovered.impf_mdd_mult == config.impf_mdd_mult + assert recovered.impf_ids == config.impf_ids + + +def test_impfset_modifier_config_to_dict_roundtrip(): + d = {"haz_type": "TC", "impf_mdd_mult": 0.8, "impf_paa_add": 0.05} + config = ImpfsetModifierConfig.from_dict(d) + result = {**config.to_dict(), "haz_type": "TC"} + assert result["impf_mdd_mult"] == d["impf_mdd_mult"] + assert result["impf_paa_add"] == d["impf_paa_add"] + + +def test_impfset_modifier_config_warns_when_path_and_modifiers_combined(): + with pytest.warns(UserWarning): + ImpfsetModifierConfig( + haz_type="TC", + new_impfset_path="path/to/file.xlsx", + impf_mdd_mult=0.5, + ) + + +def test_impfset_modifier_config_no_warning_when_only_path(): + with warnings.catch_warnings(): + warnings.simplefilter("error") + ImpfsetModifierConfig(haz_type="TC", new_impfset_path="path/to/file.xlsx") + + +def test_impfset_modifier_config_no_warning_when_only_modifiers(): + with warnings.catch_warnings(): + warnings.simplefilter("error") + ImpfsetModifierConfig(haz_type="TC", impf_mdd_mult=0.5) + + +def test_impfset_modifier_config_impf_ids_accepts_int(): + config = ImpfsetModifierConfig(haz_type="TC", impf_ids=1) + assert config.impf_ids == 1 + + +def test_impfset_modifier_config_impf_ids_accepts_str(): + config = ImpfsetModifierConfig(haz_type="TC", impf_ids="1") + assert config.impf_ids == "1" + + +def test_impfset_modifier_config_impf_ids_accepts_list(): + config = ImpfsetModifierConfig(haz_type="TC", impf_ids=[1, 2, "3"]) + assert config.impf_ids == [1, 2, "3"] + + +def test_impfset_modifier_config_impf_ids_accepts_none(): + config = ImpfsetModifierConfig(haz_type="TC", impf_ids=None) + assert config.impf_ids is None + + +# --------------------------------------------------------------------------- +# HazardModifierConfig +# --------------------------------------------------------------------------- + + +def test_hazard_modifier_config_defaults(): + config = HazardModifierConfig(haz_type="TC") + assert config.haz_int_mult == 1.0 + assert config.haz_int_add == 0.0 + assert config.new_hazard_path is None + assert config.impact_rp_cutoff is None + + +def test_hazard_modifier_config_from_dict_roundtrip(): + config = HazardModifierConfig(haz_type="TC", haz_int_mult=0.8, haz_int_add=-0.1) + d = {**config.to_dict(), "haz_type": "TC"} + recovered = HazardModifierConfig.from_dict(d) + assert recovered.haz_int_mult == config.haz_int_mult + assert recovered.haz_int_add == config.haz_int_add + + +def test_hazard_modifier_config_to_dict_roundtrip(): + d = {"haz_type": "TC", "haz_int_mult": 0.7, "haz_int_add": -0.2} + config = HazardModifierConfig.from_dict(d) + result = {**config.to_dict(), "haz_type": "TC"} + assert result["haz_int_mult"] == d["haz_int_mult"] + assert result["haz_int_add"] == d["haz_int_add"] + + +def test_hazard_modifier_config_warns_when_path_and_modifiers_combined(): + with pytest.warns(UserWarning): + HazardModifierConfig( + haz_type="TC", + new_hazard_path="path/to/hazard.h5", + haz_int_mult=0.5, + ) + + +def test_hazard_modifier_config_warns_when_path_and_rp_cutoff_combined(): + with pytest.warns(UserWarning): + HazardModifierConfig( + haz_type="TC", + new_hazard_path="path/to/hazard.h5", + impact_rp_cutoff=100.0, + ) + + +def test_hazard_modifier_config_no_warning_when_only_path(): + with warnings.catch_warnings(): + warnings.simplefilter("error") + HazardModifierConfig(haz_type="TC", new_hazard_path="path/to/hazard.h5") + + +def test_hazard_modifier_config_no_warning_when_only_modifiers(): + with warnings.catch_warnings(): + warnings.simplefilter("error") + HazardModifierConfig(haz_type="TC", haz_int_mult=0.5) + + +# --------------------------------------------------------------------------- +# ExposuresModifierConfig +# --------------------------------------------------------------------------- + + +def test_exposures_modifier_config_defaults(): + config = ExposuresModifierConfig() + assert config.reassign_impf_id is None + assert config.set_to_zero is None + assert config.new_exposures_path is None + + +def test_exposures_modifier_config_from_dict_roundtrip(): + config = ExposuresModifierConfig( + reassign_impf_id={"TC": {1: 2}}, + set_to_zero=[10, 20], + ) + d = config.to_dict() + recovered = ExposuresModifierConfig.from_dict(d) + assert recovered.reassign_impf_id == config.reassign_impf_id + assert recovered.set_to_zero == config.set_to_zero + + +def test_exposures_modifier_config_to_dict_roundtrip(): + d = {"reassign_impf_id": {"TC": {1: 2}}, "set_to_zero": [5, 6]} + config = ExposuresModifierConfig.from_dict(d) + result = config.to_dict() + assert result["reassign_impf_id"] == d["reassign_impf_id"] + assert result["set_to_zero"] == d["set_to_zero"] + + +def test_exposures_modifier_config_warns_when_path_and_modifiers_combined(): + with pytest.warns(UserWarning): + ExposuresModifierConfig( + new_exposures_path="path/to/exp.h5", + reassign_impf_id={"TC": {1: 2}}, + ) + + +def test_exposures_modifier_config_no_warning_when_only_path(): + with warnings.catch_warnings(): + warnings.simplefilter("error") + ExposuresModifierConfig(new_exposures_path="path/to/exp.h5") + + +def test_exposures_modifier_config_no_warning_when_only_modifiers(): + with warnings.catch_warnings(): + warnings.simplefilter("error") + ExposuresModifierConfig(reassign_impf_id={"TC": {1: 2}}) + + +def test_exposures_modifier_config_reassign_impf_id_accepts_int_keys(): + config = ExposuresModifierConfig(reassign_impf_id={"TC": {1: 2}}) + assert config.reassign_impf_id == {"TC": {1: 2}} + + +def test_exposures_modifier_config_reassign_impf_id_accepts_str_keys(): + config = ExposuresModifierConfig(reassign_impf_id={"TC": {"1": "2"}}) + assert config.reassign_impf_id == {"TC": {"1": "2"}} + + +def test_exposures_modifier_config_set_to_zero_accepts_none(): + config = ExposuresModifierConfig(set_to_zero=None) + assert config.set_to_zero is None + + +def test_exposures_modifier_config_set_to_zero_accepts_list(): + config = ExposuresModifierConfig(set_to_zero=[1, 2, 3]) + assert config.set_to_zero == [1, 2, 3] + + +# --------------------------------------------------------------------------- +# CostIncomeConfig +# --------------------------------------------------------------------------- + + +def test_cost_income_config_defaults(): + config = CostIncomeConfig() + assert config.init_cost == 0.0 + assert config.periodic_cost == 0.0 + assert config.periodic_income == 0.0 + assert config.cost_yearly_growth_rate == 0.0 + assert config.income_yearly_growth_rate == 0.0 + assert config.freq == "Y" + assert config.custom_cash_flows is None + + +def test_cost_income_config_default_mkt_price_year_is_current_year(): + config = CostIncomeConfig() + assert config.mkt_price_year == datetime.today().year + + +def test_cost_income_config_from_dict_roundtrip(): + config = CostIncomeConfig(init_cost=1000.0, periodic_cost=50.0, freq="M") + d = config.to_dict() + recovered = CostIncomeConfig.from_dict(d) + assert recovered.init_cost == config.init_cost + assert recovered.periodic_cost == config.periodic_cost + assert recovered.freq == config.freq + + +def test_cost_income_config_to_dict_roundtrip(): + d = {"init_cost": 500.0, "periodic_income": 20.0, "freq": "M"} + config = CostIncomeConfig.from_dict(d) + result = config.to_dict() + assert result["init_cost"] == d["init_cost"] + assert result["periodic_income"] == d["periodic_income"] + assert result["freq"] == d["freq"] + + +# --------------------------------------------------------------------------- +# MeasureConfig +# --------------------------------------------------------------------------- + + +def test_measure_config_from_dict_minimal(minimal_measure_dict): + config = MeasureConfig.from_dict(minimal_measure_dict) + assert config.name == "seawall" + assert config.haz_type == "TC" + assert config.impfset_modifier == ImpfsetModifierConfig(haz_type="TC") + assert config.hazard_modifier == HazardModifierConfig(haz_type="TC") + assert config.exposures_modifier == ExposuresModifierConfig() + assert config.cost_income == CostIncomeConfig() + + +def test_measure_config_from_dict_full(full_measure_dict): + config = MeasureConfig.from_dict(full_measure_dict) + assert config.hazard_modifier.haz_int_mult == full_measure_dict["haz_int_mult"] + assert config.impfset_modifier.impf_mdd_mult == full_measure_dict["impf_mdd_mult"] + assert config.exposures_modifier.set_to_zero == full_measure_dict["set_to_zero"] + assert config.cost_income.init_cost == full_measure_dict["init_cost"] + assert config.color_rgb == tuple(full_measure_dict["color_rgb"]) + assert ( + config.implementation_duration == full_measure_dict["implementation_duration"] + ) + + +def test_measure_config_from_dict_ignores_unknown_keys(minimal_measure_dict): + d = {**minimal_measure_dict, "completely_unknown": 42} + config = MeasureConfig.from_dict(d) + assert config.name == "seawall" + assert not hasattr(config, "completely_unknown") + + +def test_measure_config_to_dict_roundtrip(full_measure_dict): + config = MeasureConfig.from_dict(full_measure_dict) + recovered = MeasureConfig.from_dict(config.to_dict()) + assert recovered.name == config.name + assert recovered.haz_type == config.haz_type + assert recovered.hazard_modifier == config.hazard_modifier + assert recovered.impfset_modifier == config.impfset_modifier + assert recovered.exposures_modifier == config.exposures_modifier + assert recovered.color_rgb == config.color_rgb + assert recovered.implementation_duration == config.implementation_duration + + +def test_measure_config_to_dict_color_rgb_none(minimal_measure_dict): + config = MeasureConfig.from_dict(minimal_measure_dict) + result = config.to_dict() + assert result["color_rgb"] is None + + +def test_measure_config_to_dict_color_rgb_set(minimal_measure_dict): + config = MeasureConfig.from_dict( + {**minimal_measure_dict, "color_rgb": [0.1, 0.5, 0.9]} + ) + result = config.to_dict() + assert result["color_rgb"] == [0.1, 0.5, 0.9] + + +def test_measure_config_to_yaml_roundtrip(tmp_path, full_measure_dict): + path = str(tmp_path / "measure.yaml") + config = MeasureConfig.from_dict(full_measure_dict) + config.to_yaml(path) + recovered = MeasureConfig.from_yaml(path) + assert recovered.name == config.name + assert recovered.haz_type == config.haz_type + assert recovered.hazard_modifier == config.hazard_modifier + assert recovered.impfset_modifier == config.impfset_modifier + assert recovered.color_rgb == config.color_rgb + + +def test_measure_config_from_yaml_reads_first_entry(tmp_path, full_measure_dict): + import yaml + + second = {**full_measure_dict, "name": "second_measure"} + path = str(tmp_path / "measures.yaml") + with open(path, "w") as f: + yaml.dump({"measures": [full_measure_dict, second]}, f) + config = MeasureConfig.from_yaml(path) + assert config.name == full_measure_dict["name"] + + +def test_measure_config_from_row_roundtrip(full_measure_dict): + config = MeasureConfig.from_dict(full_measure_dict) + row = pd.Series(config.to_dict()) + recovered = MeasureConfig.from_row(row) + assert recovered.name == config.name + assert recovered.hazard_modifier == config.hazard_modifier + assert recovered.impfset_modifier == config.impfset_modifier + + +def test_measure_config_from_row_ignores_extra_columns(full_measure_dict): + config = MeasureConfig.from_dict(full_measure_dict) + d = {**config.to_dict(), "extra_column": "garbage"} + row = pd.Series(d) + recovered = MeasureConfig.from_row(row) + assert recovered.name == config.name + + +def test_measure_config_sub_configs_correctly_dispatched(full_measure_dict): + config = MeasureConfig.from_dict(full_measure_dict) + assert config.hazard_modifier.haz_int_mult == full_measure_dict["haz_int_mult"] + assert config.impfset_modifier.impf_mdd_mult == full_measure_dict["impf_mdd_mult"] + assert ( + config.exposures_modifier.reassign_impf_id + == full_measure_dict["reassign_impf_id"] + ) + assert config.cost_income.init_cost == full_measure_dict["init_cost"] + assert not hasattr(config.hazard_modifier, "impf_mdd_mult") + assert not hasattr(config.impfset_modifier, "haz_int_mult") diff --git a/climada/entity/measures/types.py b/climada/entity/measures/types.py new file mode 100644 index 0000000000..1ad90986b1 --- /dev/null +++ b/climada/entity/measures/types.py @@ -0,0 +1,10 @@ +from collections.abc import Callable +from typing import Concatenate + +from climada.entity.exposures.base import Exposures +from climada.entity.impact_funcs.impact_func_set import ImpactFuncSet +from climada.hazard.base import Hazard + +HazardChange = Callable[Concatenate[Hazard, ...], Hazard] +ImpfsetChange = Callable[Concatenate[ImpactFuncSet, ...], ImpactFuncSet] +ExposuresChange = Callable[Concatenate[Exposures, ...], Exposures] diff --git a/climada/entity/test/test_entity.py b/climada/entity/test/test_entity.py index 7805a24e70..4a88fc531a 100644 --- a/climada/entity/test/test_entity.py +++ b/climada/entity/test/test_entity.py @@ -24,11 +24,11 @@ import numpy as np from climada import CONFIG +from climada.entity._legacy_measures.measure_set import MeasureSet from climada.entity.disc_rates.base import DiscRates from climada.entity.entity_def import Entity from climada.entity.exposures.base import Exposures from climada.entity.impact_funcs.impact_func_set import ImpactFuncSet -from climada.entity.measures.measure_set import MeasureSet from climada.util.constants import ENT_TEMPLATE_XLS ENT_TEST_MAT = CONFIG.exposures.test_data.dir().joinpath("demo_today.mat") diff --git a/climada/trajectories/calc_risk_metrics.py b/climada/trajectories/calc_risk_metrics.py index 902fffc2ba..8e5ae8b150 100644 --- a/climada/trajectories/calc_risk_metrics.py +++ b/climada/trajectories/calc_risk_metrics.py @@ -32,7 +32,7 @@ import pandas as pd from climada.engine.impact import Impact -from climada.entity.measures.base import Measure +from climada.entity._legacy_measures.base import Measure from climada.trajectories.constants import ( AAI_METRIC_NAME, COORD_ID_COL_NAME, diff --git a/climada/trajectories/snapshot.py b/climada/trajectories/snapshot.py index 1d5f778135..dbf7f1bbd6 100644 --- a/climada/trajectories/snapshot.py +++ b/climada/trajectories/snapshot.py @@ -31,9 +31,9 @@ import numpy as np import pandas as pd +from climada.entity._legacy_measures.base import Measure from climada.entity.exposures import Exposures from climada.entity.impact_funcs import ImpactFuncSet -from climada.entity.measures.base import Measure from climada.hazard import Hazard LOGGER = logging.getLogger(__name__) diff --git a/climada/trajectories/test/test_calc_risk_metrics.py b/climada/trajectories/test/test_calc_risk_metrics.py index 9c75f78fb4..6fc530d065 100644 --- a/climada/trajectories/test/test_calc_risk_metrics.py +++ b/climada/trajectories/test/test_calc_risk_metrics.py @@ -26,10 +26,10 @@ import pandas as pd import pytest +from climada.entity._legacy_measures.base import Measure from climada.entity.exposures import Exposures from climada.entity.impact_funcs import ImpactFuncSet from climada.entity.impact_funcs.trop_cyclone import ImpfTropCyclone -from climada.entity.measures.base import Measure from climada.hazard import Hazard from climada.trajectories.calc_risk_metrics import CalcRiskMetricsPoints from climada.trajectories.constants import ( diff --git a/climada/trajectories/test/test_snapshot.py b/climada/trajectories/test/test_snapshot.py index 77830d3b54..0acaf148da 100644 --- a/climada/trajectories/test/test_snapshot.py +++ b/climada/trajectories/test/test_snapshot.py @@ -5,9 +5,9 @@ import pandas as pd import pytest +from climada.entity._legacy_measures.base import Measure from climada.entity.exposures import Exposures from climada.entity.impact_funcs import ImpactFunc, ImpactFuncSet -from climada.entity.measures.base import Measure from climada.hazard import Hazard from climada.trajectories.snapshot import Snapshot from climada.util.constants import EXP_DEMO_H5, HAZ_DEMO_H5 diff --git a/doc/api/climada/climada.entity._legacy_measures.rst b/doc/api/climada/climada.entity._legacy_measures.rst new file mode 100644 index 0000000000..19e622c1ef --- /dev/null +++ b/doc/api/climada/climada.entity._legacy_measures.rst @@ -0,0 +1,22 @@ +climada\.entity\._legacy_measures package +========================================= + +.. note:: + This package implements the legacy way of defining measures + and is retained for compatibility. + +climada\.entity\._legacy_measures\.base module +---------------------------------------------- + +.. automodule:: climada.entity._legacy_measures.base + :members: + :undoc-members: + :show-inheritance: + +climada\.entity\._legacy_measures\.measure\_set module +------------------------------------------------------ + +.. automodule:: climada.entity._legacy_measures.measure_set + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/climada/climada.entity.measures.rst b/doc/api/climada/climada.entity.measures.rst index 8e63a2082b..5f97f72bd4 100644 --- a/doc/api/climada/climada.entity.measures.rst +++ b/doc/api/climada/climada.entity.measures.rst @@ -1,6 +1,10 @@ climada\.entity\.measures package ================================= +.. note:: + This package implements the new way of defining measures. + For the previous way, see :ref:`climada.entity._legacy_measures` + climada\.entity\.measures\.base module -------------------------------------- @@ -9,10 +13,27 @@ climada\.entity\.measures\.base module :undoc-members: :show-inheritance: -climada\.entity\.measures\.measure\_set module ----------------------------------------------- -.. automodule:: climada.entity.measures.measure_set +climada\.entity\.measures\.measure_config module +------------------------------------------------ + +.. automodule:: climada.entity.measures.measure_config + :members: + :undoc-members: + :show-inheritance: + +climada\.entity\.measures\.cost_income module +--------------------------------------------- + +.. automodule:: climada.entity.measures.cost_income + :members: + :undoc-members: + :show-inheritance: + +climada\.entity\.measures\.types module +--------------------------------------- + +.. automodule:: climada.entity.measures.types :members: :undoc-members: :show-inheritance: diff --git a/doc/api/climada/climada.entity.rst b/doc/api/climada/climada.entity.rst index f7eac11700..f4f4df0d98 100644 --- a/doc/api/climada/climada.entity.rst +++ b/doc/api/climada/climada.entity.rst @@ -7,6 +7,7 @@ climada\.entity package climada.entity.exposures climada.entity.impact_funcs climada.entity.measures + climada.entity._legacy_measures climada\.entity\.entity\_def module ----------------------------------- diff --git a/doc/user-guide/adaptation.rst b/doc/user-guide/adaptation.rst new file mode 100644 index 0000000000..647eddc7ac --- /dev/null +++ b/doc/user-guide/adaptation.rst @@ -0,0 +1,14 @@ +========================== +Adapation appraisal guides +========================== + +These guides show everything you need to know in order to evaluate adapation options with CLIMADA. + +.. toctree:: + :maxdepth: 1 + + .. Adaptation measures in CLIMADA + Using measure configurations + Defining measure cash flows + .. Cost benefit evaluation + .. Adapation planning evaluation diff --git a/doc/user-guide/climada_cost_income.ipynb b/doc/user-guide/climada_cost_income.ipynb new file mode 100644 index 0000000000..bf1834140e --- /dev/null +++ b/doc/user-guide/climada_cost_income.ipynb @@ -0,0 +1,1020 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2147f042", + "metadata": {}, + "source": [ + "(cost-income-tutorial)=\n", + "# Measure cash flows with `CostIncome`\n", + "\n", + "This notebook introduces the `CostIncome` class from CLIMADA, which is used to model the financial cash flows of adaptation or risk-reduction measures over time.\n", + "\n", + "A `CostIncome` object tracks:\n", + "- **Initial (one-off) costs** — the upfront implementation cost\n", + "- **Periodic costs** — recurring expenses (e.g., maintenance)\n", + "- **Periodic income** — recurring revenues (e.g., insurance savings, avoided losses)\n", + "- **Growth rates** — how costs and incomes evolve over time\n", + "- **Custom cash flows** — arbitrary user-defined flows (layered on top)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a73bc8d1", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# Adjust the import path if running outside the CLIMADA package tree\n", + "from climada.entity.measures.cost_income import CostIncome" + ] + }, + { + "cell_type": "markdown", + "id": "a97e1043", + "metadata": {}, + "source": [ + "## Quickstart\n", + "\n", + "The simplest way to create a `CostIncome` is by passing keyword arguments directly.\n", + "\n", + "| Parameter | Meaning | Sign convention |\n", + "|---|---|---|\n", + "| `init_cost` | One-off implementation cost | stored negative |\n", + "| `periodic_cost` | Recurring cost each period | stored negative |\n", + "| `periodic_income` | Recurring income each period | stored positive |\n", + "| `mkt_price_year` | Reference year for price levels | — |\n", + "| `freq` | Period frequency (e.g. `'Y'`, `'M'`, `'Q'`) | — |\n", + "\n", + "```{note} **Sign convention**\n", + "`CostIncome` stores costs as negative numbers internally.\n", + "```\n", + "\n", + "```{note}\n", + "Financial values in `CostIncome` are currently unitless (no currency is specified)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4ba16743", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CostIncome(\n", + " mkt_price_year = 2020\n", + " freq = 'Y'\n", + " init_cost = -20,000.00\n", + " periodic_cost = -5,000.00\n", + " periodic_income = 12,000.00\n", + " cost_yearly_growth_rate = 0.00%\n", + " income_yearly_growth_rate = 0.00%\n", + " custom_cash_flows = None\n", + ")\n" + ] + } + ], + "source": [ + "ci = CostIncome(\n", + " mkt_price_year=2020,\n", + " init_cost=20_000, # 50 000 upfront\n", + " periodic_cost=5_000, # 5 000 / year maintenance\n", + " periodic_income=12_000, # 8 000 / year in avoided losses\n", + " freq=\"Y\", # annual cash flows\n", + ")\n", + "\n", + "print(ci)" + ] + }, + { + "cell_type": "markdown", + "id": "3c9a8398", + "metadata": {}, + "source": [ + "## Calculating cash flows\n", + "\n", + "Three methods are available:\n", + "\n", + "| Method | Returns |\n", + "|---|---|\n", + "| `calc_cash_flows(impl_date, start_date, end_date)` | Three `np.ndarray`: net, costs, incomes |\n", + "| `calc_total(...)` | Three scalars: summed net, cost, income |\n", + "| `to_dataframe(...)` | A tidy `pd.DataFrame` |\n", + "\n", + "The `impl_date` is when the measure is *deployed*. Cash flows before this date are zero; the initial cost lands on `impl_date`; periodic flows start the following period." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "25bf1e1f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Period | Net | Cost | Income\n", + "---------------------------------------------\n", + "2020 | 0 | 0 | 0\n", + "2021 | 0 | 0 | 0\n", + "2022 | -20000 | -20000 | 0\n", + "2023 | 7000 | -5000 | 12000\n", + "2024 | 7000 | -5000 | 12000\n", + "2025 | 7000 | -5000 | 12000\n", + "2026 | 7000 | -5000 | 12000\n", + "2027 | 7000 | -5000 | 12000\n", + "2028 | 7000 | -5000 | 12000\n", + "2029 | 7000 | -5000 | 12000\n", + "2030 | 7000 | -5000 | 12000\n" + ] + } + ], + "source": [ + "impl_date = \"2022-01-01\"\n", + "start_date = \"2020-01-01\"\n", + "end_date = \"2030-01-01\"\n", + "\n", + "net, costs, incomes = ci.calc_cash_flows(impl_date, start_date, end_date)\n", + "\n", + "print(\"Period | Net | Cost | Income\")\n", + "print(\"-\" * 45)\n", + "periods = pd.period_range(start=start_date, end=end_date, freq=\"Y\")\n", + "for p, n, c, i in zip(periods, net, costs, incomes):\n", + " print(f\"{p} | {n:>9.0f} | {c:>9.0f} | {i:>6.0f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "971ce0dd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total net : 36000\n", + "Total cost : -60000\n", + "Total income : 96000\n" + ] + } + ], + "source": [ + "total_net, total_cost, total_income = ci.calc_total(impl_date, start_date, end_date)\n", + "print(f\"Total net : {total_net:>10.0f}\")\n", + "print(f\"Total cost : {total_cost:>10.0f}\")\n", + "print(f\"Total income : {total_income:>10.0f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f3fd0c35", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
datenetcostincome
020200.00.00.0
120210.00.00.0
22022-20000.0-20000.00.0
320237000.0-5000.012000.0
420247000.0-5000.012000.0
520257000.0-5000.012000.0
620267000.0-5000.012000.0
720277000.0-5000.012000.0
820287000.0-5000.012000.0
920297000.0-5000.012000.0
1020307000.0-5000.012000.0
\n", + "
" + ], + "text/plain": [ + " date net cost income\n", + "0 2020 0.0 0.0 0.0\n", + "1 2021 0.0 0.0 0.0\n", + "2 2022 -20000.0 -20000.0 0.0\n", + "3 2023 7000.0 -5000.0 12000.0\n", + "4 2024 7000.0 -5000.0 12000.0\n", + "5 2025 7000.0 -5000.0 12000.0\n", + "6 2026 7000.0 -5000.0 12000.0\n", + "7 2027 7000.0 -5000.0 12000.0\n", + "8 2028 7000.0 -5000.0 12000.0\n", + "9 2029 7000.0 -5000.0 12000.0\n", + "10 2030 7000.0 -5000.0 12000.0" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = ci.to_dataframe(impl_date, start_date, end_date)\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "612c0b66", + "metadata": {}, + "source": [ + "## Visualising cash flows\n", + "\n", + "`plot_cash_flows` draws a bar chart. You can choose which series to display via the `to_plot` argument." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "315fc0ac", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(, )" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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juHWqJ3SuxRPPfWUk0sNT2Cva08j0JMnT7YsX748x38GuC+++8C1B29k7+8+63aAPa49pflWUl9XpUuXtkyYMIFKdiAG5MI23kwjvjpNl05ZU6tWLROhGzdunBlPO2PGDLONRpH1ofts377dTG2jdIqlXr16mTsXgLOuPY0ojxw5Utq3b8+HgBy//gBnffcBrnD96VhxwF7XntKeydbrSvfVnD6a96JQoUJUtAMRIICt239ERITpEpb8C167+9SrV09++OEH2bVr1/WLxtdXNm7caH5B69ata7oH6S/3uXPnpEiRItQonHLtFS5cmJpHlvDdB2fh2oMzcf3BXa695EOTL168aIYU6PCXe+65xwml9x4ECLzQypUr5dlnn5V3331X/vzzT9vyZs2ayYYNG+TUqVPmfWJiouTOndv8EuovqI77sdJs3TreRzPF6/F0pgJdr+OIAGdce9bMtgDffXA1/L8Lrj94o1v97tNcF4sXLzb5fKpXry6bNm2SmTNnmtk04DgECLzIyZMnTUKPhx9+2EwXoklltCu29RdWX5ctW9YkGUwetWvXrp25c3vgwAHbsQICAkz3Hp1eRKN8GvEDuPbgivjuA9cevBHffXD3a0+HFegNoL1795oggyYsrFq1Kh+sozkywQFcx+XLly39+vWz9OzZ03Lo0CHb8gYNGlj69+9vXut0NvPnzzdTw61fvz7F/g899JCldevWtveRkZE5WHq4M649cP3BG/HdB64/eCN7f/dpokzkLHoQeAkds63d/3V8T7ly5SQhIcEs79y5s+zevdu89vPzkx49epjuPYMGDZI1a9aYyJ12/9m/f7+JAlox3htce3AHfPeBaw/eiO8+eMq1Z51mEznHR6MEOXg+OJHOMapDA5R+7Nqdp0+fPhIcHCxz5syxLbt27ZrJUvv3339LnTp1zHjv0qVLyxdffGHmuQW49uBO+O4D1x68Ed994NpDdhAg8HItW7aURx55xET5NECQlJRkonqnT58209loxngdI9S7d29nFxUehmsPXH/wRnz3gesP3ojvPvdBgMCLHTp0SJo2bSpLly61JRnUbKGBgYHOLho8HNceuP7gjfjuA9cfvBHffe6FQR1eyDqqZN26dZInTx5bcGDChAny3HPPSWRkpJNLCE/FtQeuP3gjvvvA9QdvxHefe/J3dgGQ86xTiehUI/fdd5+Zo/Sxxx6TK1euyKeffipFihThYwHXHjwO333g2oM34rsPXHvICoYYeClNRFizZk05ePCgGVKgvQdGjBjh7GLBC3DtgesP3ojvPnD9wRvx3ed+CBB4sXbt2knFihVl6tSpkitXLmcXB16Eaw9cf/BGfPeB6w/eiO8+90KAwIslJiaaGQsArj14E777wLUHb8R3H7j2kBkECAAAAAAAALMYAAAAAAAAAgQAAAAAAIAAAQAAAAAAIEAAAAAAAAAM3+tPAAAAAADAmxEgAAAAAAAABAgAAAAAAAABAgAAAAAAQIAAAAAAAAAQIAAAAAAAAKL+DwDn5rGa8bABAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ci.plot_cash_flows(\n", + " impl_date,\n", + " start_date,\n", + " end_date,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4bb73614", + "metadata": {}, + "source": [ + "## Growth rates\n", + "\n", + "Costs and incomes can grow year-over-year using compound-interest factors anchored to `mkt_price_year`.\n", + "\n", + "$$\\text{value}(t) = \\text{base} \\times (1 + r)^{\\frac{t - t_0}{365}}$$\n", + "\n", + "Pass `cost_yearly_growth_rate` and / or `income_yearly_growth_rate` (as decimals, e.g. `0.02` for 2 %)." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b69f1ae7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(, )" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABAgAAAJyCAYAAABJ8PKHAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAhBBJREFUeJzt3Qd8VFX2wPEz6QkkoYdeRKo0AanSe1WxICiIiwVZCwKCqEhZBcVd1FVBWQsWFPyvuoLSRZCigBQpCiLSWyCEJEBIQjL/z7kw4yQkpDCTab/v5zOfmXnzys3LY8g9795zLFar1SoAAAAAAMCvBbi7AQAAAAAAwP0IEAAAAAAAAAIEAAAAAACAAAEAAAAAACBAAAAAAAAACBAAAAAAAACDJIUAAAAAAIAAAQAAAAAAIEAAAAAAAAAIEAAA4PlWrlwpFotFZs+eXSjHa9++vVStWrVQjuVt9u/fb34XEydOdOlx9BhDhgxx6TEAAMiKHAQAAOTBhQsX5I033pB27dpJyZIlJTg4WMqUKSPdunWT//znP5KSkuLx51E7nTk9XN3hdaZWrVrRgQYAwAWCXLFTAAB87a5xr1695Ndff5WOHTvK2LFjpXTp0hIXF2fu7g8bNkw2btwos2bNEk/XoEEDeeqpp7Jd7g1+++03+fHHH6VGjRryf//3f/Lvf/9boqKixNckJydLYGCgu5sBAPAzBAgAAMhl5EDv3r1l9+7d8vnnn8udd96Z6fPRo0fLjh07ZOnSpV5xHsuVKyf33nuveKv33ntPihQpInPmzJFmzZrJ3Llz5aGHHhJfExYW5u4mAAD8EFMMAADIpUO6c+dOGTly5BXBAZt69eqZz202bNhg5o/XrFlTIiIiJDIyUlq3bi1fffXVFdseOnRIhg4dKlWqVJHQ0FAzfeGmm24y0xay8+6770rdunXNurrNtGnTCu33t3btWunevbsUK1ZMwsPDpWHDhmbahdVqta/zySefmOH/OrLCJj09XaKjo83yn376yb5cp2Xo+Rk8eHCejp+WliYff/yx3HHHHeYcaYBAfz9Xy6Nw+PBhueuuu6R48eImsKBTQn7//fdM6yYlJclzzz0nzZs3l1KlSplze/3118vTTz8t58+fv2qbTpw4ISEhIXLPPfdk+/njjz9ufm7bMU+fPm2ulerVq5sggLZLR2+8+OKLueYg+Pbbb80UFx29otuWL19e+vbta65PAACcgREEAABchQ5jVw8//HCez5MGArRDOGDAAKlYsaKZivDhhx9Kv379zJ3vgQMHmvUuXrwoXbp0kSNHjsgjjzwitWrVksTERDMi4YcffpAHH3ww035nzpwpsbGx8sADD5gOt3bGdbqDHsO2z7x0sk+dOpVpWUBAgJQoUeKq2y1cuFBuueUW04EeMWKE6dh+8cUXpgO8fft2+/SKTp06mefvvvvOdNKVTr/Qn0uPo8tbtGhhlutUAR1Kr9M28mLBggXm57/vvvvMe33++9//bs6XBmmyOnfunOlQt2zZUqZMmSL79u2T119/3fwcuo1tCL+efw00aABIO/q6fNWqVSb4smXLFlmyZEmObYqJiTH7+/LLLyU+Pt6cF8cAiP6+27Zta4JFSo+hv1u9njTAoj+/XisaUHn22WdzPI62R4MB9evXN4ELDdIcO3ZMvv/+e7P9DTfckKdzCADAVVkBAECOSpQoYY2MjMzXGTp79uwVy86dO2etWbOmtU6dOvZlv/zyi956t06bNu2q+/v+++/NeuXKlbPGx8dn2mepUqWsLVq0yFO7dB/ZPaKjozOt165dO2uVKlXs7y9evGje63k4dOhQpuXdu3c3+1i7dq19ea1atawtW7a0v3/xxRetUVFR1r59+1o7dOhgXz5+/Hiz7YEDB/LU/p49e1qrVq1qzcjIMO9Pnz5tDQ0NtT755JNXrKs/g+775ZdfzrRcz7UuX7x4sX1ZSkqKNS0t7Yp9PPfcc2bd9evX25ft27fPLJswYYJ92dKlS82yN954I9P2c+fONcs/+ugj8/7MmTPm/fDhw3P9WXW9++67z/5ef0ZdFhsbm+u2AAAUFFMMAAC4Cr3znd8keDqU3UaHqOsIAn3WO+WaZE/3qXQUgFqxYoUZqp6b+++/39w5ttHh+Xo3fs+ePXluW9OmTWXZsmWZHl9//fVVt9m8ebMcOHDADHnX0Qo2eqf9mWeeMa/1DrqN/pw6akCH7tt+Ph1N0LVrV1m3bp3J62BbrkP5K1eunGu79S6/3snX6Qg6/F7p3Xq9q67TDlJTU6/YRkcs6AgHR7bRCo7nTKcIBAUF2Ud16EgAHWXRuXNns2z9+vVXbZuud911110x3UHf6+9Yp0QonZahUwN0moUmvswP2+9dR7RoGwEAcAUCBAAAXIUGB2wd3bzSYfCaOE+Hn2uwQIfl67zxt99+23x+5swZ86w5BJ5//nmT4FDnkzdu3NhUGHCcp+9IO6FZac4CDUDkla6vHVrHhw7Dv5o///zTPGc3jF2HvDuuY+uEaydWh9LrMHsNCugyfeh7zWWgw/81V4NtSkJuZs+ebXIZaInDP/74w/7QwIN25ufPn3/FNnpOsyb7059fZT1nM2bMMLkANP+ATrfQ35dtioQGDK5GAxY6HWTr1q0mmKI0oKLTKXTKggYGbIEIneKgOQOqVatmckk8+uijJkiTG12vSZMmZkqFtq9Hjx5mX3kJLAEAkFcECAAAuArtAOsd/7179+bpPGVkZJi8AppzQO92z5s3TxYvXmw6gbY8AbqOzaRJk0xHV8v16Tz1Dz74wMyZz3rnW7mr7J1jEsK86NChg+k0awfZlmdAAwF16tQxVRR0+erVq00+hLzkH9Dj63lRmiRRSxzaHtphVtklK7za+XL8mf71r3+Z/Wjb3nnnHZMMUH9fGpTI+vu62uiO4OBgk0RSaXt1O80X4UgDRzp6QNfTRIuar0JHVtx2221XPY4GBTSgorkInnjiCTMiZdSoUeaacUwICQDAtSBJIQAAV6HDw7VTplUFXnrppVzPlSbs27ZtmxkZoJ1/R7bOY1Z6N1k7qPrQO+ya9E6rAzz55JPmM3fTjPsqu2z5muzPcR3bXXq9G6+BAB1BoSMpbEkENSCgyzU4oEEEDSbkRjvAGqDRoEmbNm2u+FyTJWoJSq1Y4DgFIq802aNWPFi0aJGZlmCjgZ280p9Rpzt8+umn8sorr5jggo4IufHGG69Yt2zZsqZyhT40KKCjD95//31znV3tfGjbNOGhPpROV9FRBRMmTDDbAgBwrRhBAADAVWgnTu98611mx3n2WYMC+rnjXeusd921I521zGFCQoLpKDvSIe62ofxaEs8TaEdXp0PoqAjNBWCjndupU6ea13oH3JEGAvS8/Pe//800SkBfb9q0yUwJ0NEZOpQ/Nzo6QDvHmu9AAzZZH1pVQdtiu+OfX/o702CF4+9Mp0jkJSCUdXSA/k6HDRtmphhkHT2gd/2zlk3Un6tRo0a5/r6zVp5QOnpAS2h6ynUCAPB+jCAAAOAqdP74N998I7169ZLbb7/dzNnXIeGaV0DnseudWy0BaCtJqMEE7eBriTztDGrpQi1Dp0PX9S66bY660hJ12qnU/do6ezqPXdfVO/C2jqO7aQda5+jryAYdFq8l+jRBoAZM9OfXn11zAzjSQMCrr74qu3btktGjR9uX61QDzSWg50Q79rnRfA06QuDmm282d+mz06xZMzNyQO/Ca6lAWxLDvNIgw7hx48y8fi1FqVNKdCSAThnID51aonkidESCXjdZS0/qz6z5HjSYoteIjrTQ86PlKzVfgi0pYnb0HOsICb32NFijI000YaHmu9C8FQAAOAMBAgAAcqGdPr3rrdMM9I643jXXxIXaSda767r83nvvtXemdQ67dor1jrsm49PAgL7+5ZdfMgUIGjZsaDqk2smeM2eO6ThXqlTJbKudPnflHMhOz549TUDjH//4h0yfPt10UDUHgCbKe+yxx65YXzvCWhlA78Q7jiDQzq2eT01qmJf8A9pR16oHtkoA2dGAgJ5HzeOgbczLfh3pudbRAzpSQef36xSA/v37m7wCmkgwr7QdOmpARzrceeed9ioVNvq7/dvf/mbaqJUj9OfSwIDmqnj66aevWN/RoEGDzAgJvY5OnjxpkmfWrl3bnJ8BAwbk6+cFACAnFq11mOOnAAAAyLN//vOfJuCgFRyyy5cAAIAnI0AAAADgBDpaQqeUaGnF7BI6AgDg6ZhiAAAAcA327dtnyjnqtAGdOqHD/gEA8EYECAAAAK6B5pDQfAWauFLLW5ITAADgrZhiAAAAAAAAJIBzAAAAAAAACBAAAAAAAAACBAAAAAAAgAABAAAAAAAgQAAAAAAAAAgQAAAAAAAAgySFAAAAAACAAAEAAAAAACBAAAAAAAAACBAAAAAAAAACBAAAAAAAwCBJIQAAAAAAIEAAAAAAAAAIEAAAAAAAAAIEAAAAAACAAAEAAAAAADBIUggAAAAAAAgQAAAAAAAAAgQAAAAAAIAAAQAAAAAAIEAAAAAAAAAMkhQCAAAAAAACBAAAAAAAgAABAAAAAAAgQAAAAAAAAAgQAAAAAAAAgySFAAAAAACAAAEAAAAAACBAAAAAAAAACBAAAAAAAAACBAAAAAAAwCBJIQAAAAAAIEAAAAAAAAAIEAAAAAAAAAIEAAAAAACAAAEAAAAAADBIUggAAAAAAAgQAAAAAAAAAgQAAAAAAIAAAQAAAAAAIEAAAAAAAAAMkhQCAAAAAAACBAAAAAAAgAABAAAAAAAgQAAAAAAAAAgQAAAAAAAAgySFAAAAAACAAAEAAAAAACBAAAAAAAAACBAAAAAAAAACBAAAAAAAwCBJIQAAAAAAIEAAAAAAAAAIEAAAAAAAAAIEAAAAAACAAAEAAAAAADBIUggAAAAAAAgQAAAAAAAAAgQAAAAAAIAAAQAAAAAAIEAAAAAAAAAMkhQCAAAAAAACBAAAAAAAgAABAAAAAAAgQAAAAAAAAAgQAAAAAAAAgySFAAAAAACAAAEAAAAAACBAAAAAAAAACBAAAAAAAAACBG5gtVolMTHRPAMAAAAA4ClIUljIkpKSJDo62jwDAAAAAOApCBAAAAAAAAACBAAAAAAAgAABAAAAAAAgQAAAAAAAAAgQAAAAAAAAgySFAAAAAACAAAEAAAAAACBAAAAAAAAACBAAAAAAAAACBAAAAAAAwCBJIQAAAAAAIEAAAAAAAAAIECCf2rdvL4GBgbJt2zb7sjNnzojFYpH9+/fnafvXXnuN8w4AAAAAHoYpBj7kyy+/lIYNG0p4eLh51veuULx4cRk3bpxL9g0AAAAAcA8CBB7KarXKuXPn8vz49NNP5fbbb5ft27fLhQsXzLO+1+V52V6Pl1fDhw+XdevWyQ8//JDt53PnzpUGDRpIsWLF5KabbjLrqlGjRsnq1atl7NixUrRoUenRo4fTzhcAAAAA4NpYrPnpGRYC7XS+8sorsmnTJjl27Jh89dVXcuutt9o/1+ZOmjRJZs2aJfHx8dK8eXN566235IYbbrjqfr/44gsZP3687N27V6pXry4vvvii3HbbbZnWmTFjhjm2Hlf3p0Ph27Rpc83HdpSYmCjR0dGSkJAgUVFROa6nnXbtRBeWs2fPSpEiRfI0RUB/H8nJybJgwQLT+dcpBjqqYN++ffLrr7/KQw89JPPnz5dGjRrJ//73P3nwwQfl999/l5IlS9q3HzFiRKH8XAAAAAA8W593T4g3W/BAjPgKjxtBoB1jHR7/5ptvZvv5tGnTZPr06ebzjRs3StmyZaVLly6SlJSU4z5//PFH6d+/vwwaNEh++eUX83zXXXfJ+vXr7evMmzfPdFqfffZZ2bJliwkM6B3ugwcPXtOxfZWeqwMHDpgAgCMNmDz11FPSuHFjCQgIkH79+knt2rVl4cKFbmsrAAAAAMALRxA40sR3jiMItKnly5c3nVMdpq5SUlIkJiZGXn75ZXn44Yez3Y8GB/TO/aJFi+zLunfvbu56f/bZZ+a9jgbQTu3MmTPt69SpU8cce+rUqQU+dkFHEOjxzp8/L3nVokUL2blzZ6apAnr+6tWrZwIkuYmIiDDr58ZxBICOpNBRFjptoFSpUmYEQa9evUyywuDgYPs2aWlpZvTG008/zQgCAAAA+BzugHP+fEWQeBHtgB4/fly6du1qXxYaGirt2rUzQ91z6qRrB/nJJ5/MtKxbt272bPqpqalmSoN2YB3pcWzz5wt6bA0i6MMxQJAX2lnPy5B/G536oDkHdDsNEtiedXl+9pMfQ4cONSMqPvzwQ/uySpUqyWOPPSbDhg3LdhsdVQAAAADPQgcXgPKq3pp20JXetXek722f5bTd1bY5deqUpKenX3Wdgh5bRx/oiAHbQzvQrqBD+TXPgiYHDAsLM89axSBrngVn0nKHmsthypQp9mWPPvqoPYeEbRTE8uXL5fDhw/bzpXkgAAAAAACexatGENhkHQpvu2N+rds4ax1HWg5w5MiRmUYQuDJIoI/CpKMWNCAQFxdn3vfu3dskMNTEhH/++acZZdGsWTOTm0Dp1IQhQ4aYCgc333yzfPPNN4XaXgAA4Ju4Aw4AfhYg0KSASu/YlytXzr48Njb2ijv7WbfLepffcRudP693w6+2TkGPrR1kffiKlStXXrHsp59+yvT+zjvvNI/saK6H3377zWXtAwDAm9HJBQC4k1cFCKpVq2Y66suWLZMbb7zRnj9g1apVJlFgTlq2bGm2ccxDsHTpUmnVqpV5HRISIk2aNDHrOA7J1/e33HLLNR0bAAB/QgcXAADv5XEBgrNnz8off/xhf6/JAbdu3SolSpSQypUrmyHqOue9Ro0a5qGvNQP/wIEDc9znE088IW3btjUdee3wf/3112Ze/Jo1a+zr6DQALX/YtGlTE1DQDP1a4tCWbE+nERTk2AAAAAAAeAOPCxD8/PPP0qFDB/t72/z9++67T2bPni1jxowxc9yHDx8u8fHxZsi6jgaIjIy0b6Nz3LXUnm04vI4UmDt3rjz33HOm3F716tVl3rx5ZlvHUog6j37y5Mly7NgxUx5w4cKFUqVKFfs6eTk2AMC7cQccAAD4K48LELRv394k/suJ3smfOHGieeREgwO6H0d33HGHeVyNdvz1cS3HBgB3o4MLAAAAnwgQXKukpCRTRo/s+AAAAAAA+HGAQIf7Hzp0yN3NAAAAAADAqwS4uwEAAAAAAMD9CBAAAAAAAAACBAAAAAAAwAdzEPiawshGvuCBGJcfAwAAAADg2ZhigHzR8pGvvfYaZw0AAAAAfAwBAgAAAAAAQIAABbNy5UopVqyYvPvuu1KpUiUpWbKkjBkzJtM6y5Ytk+bNm5v1ypUrJ1OnTrV/9sknn0idOnXMZzfffLNs2bIl0yiFsWPHSqdOnaRIkSLSokULOXLkiEycOFFKly4tFStWlK+++sq+vtVqlX//+99Su3Ztsz/d/rfffuNXCwAAAAD5wAgCFFhSUpJs375d9uzZI2vWrJG33nrLBA6UdvhvueUWEzQ4efKk7Nq1Szp06GA+W716tTzyyCPyzjvvmM/uuOMO6datmyQkJNj3PWfOHHn99dclLi7OBAnatGkj0dHRcuzYMZkwYYI8+OCDkpaWZtadOXOmvPfee7JgwQI5deqU9OvXT/r06SOpqan8dgEAAAAgjwgQoMD0zr2OCggLCzOjAVq1aiWbNm0yn82aNUvuvvtuuf322yU4ONh07nUkgProo4/k3nvvlbZt25rPRowYIcWLF5dvv/3Wvm/9vF69embfuo/k5GR58sknJSgoSO655x4TODhw4IBZVwMTkydPlho1apjPH3/8cbP++vXr+e0CAAAAQB4RIECBRUVFSUREhP293unXUQVKO+/aYc/O4cOHpWrVqpmWVatWzSy3KVu2rP21HiMm5q9KC7Zjnj171jzv37/fBBR0eoHtER8fn2l/AAAAAICro8whXKJKlSryxx9/ZPuZ5hDQTr0jfa/LC0JzIGhlhe7duxdoewAAAAAAIwjgIpoj4LPPPjPJBC9evGjyC/z000/mM73brzkG1q5daz574403zJSBnj17FuhYf//73+X555+X3bt3m/eJiYny9ddf20czAAAAAAByxwgCD7fggb+G1nuTxo0byxdffCHjx4+X++67T4oWLSpPPPGEyUPQrl07ExQYOnSoSTqouQYWLVpkpgYUxKOPPiqBgYEmOeGhQ4ckMjLSVEbo2LGj038uAAAAAPBVBAiQL7YqBerMmTOZPvvf//6X6X2PHj3MIzsaNNBHbsdQQ4YMMY+sCRJtLBaLDB8+3DwAAAAAAAVDgAAAAAAA4HOObf5Wfl/wLzl3fK8UKVtdavYZJeUa93J3szwaVQwAAAAAwEM7uKsmdZSFj1Qxz/oeeT93m2YOlaQjv0nGxRTzrO85h1fHCAIAAAAALsEd3Gs7d9qhFYtF59faO7hNHnnvmu6CWzMyxJqRLlZruljT083r06eDJT09PceHJha/2uf5XS/runt/PPNXu+yPjMttvPjX64x0EdtntvWsl1/rerbXGekSt3vd5R/Y+tezxWJGFDCKIGcECAAAAIAc0MF1bwc3U2fWoWNoOokOncH9+5Pz3DF1xWP3xgSRTJ3WLJ3Yyx3cS53bHDq4lx9mP9YMid/785UdXBHZ8u5w+b101RzPjTXj4hX7/+u46dme55JPiH+wWs10A+SMAAEAAICPo5Prmg6uSZqcqXN3ZYftap/bOoL6ev36Ytl2PDMyMq7aMXXW5zu2JWXTqU03P3eOHdlMn2XeVn+2pGO7s+3gbp71sIRGls6mY5u3zmx2qo0Vv5GRliJJRy+fWyfT5N9aHSy3R1BQUJ7Wy+v6q/5MFUtAYA6PALFY/nov+l5fm2WXXwcGicVy+fXlh44UuBB/NOsPKEXLXu+Sc+crCBAAAACPRwc3by7dpXS8w2qV41sWytb3H9O/jHUNSTp8qZNb796XpXTd9g5Ddi+tn7kDa3XoxGXpENo7jH/dGf00IjLbDmnWZbm9L8g6G/8879AZt3XEbefir+HJV56jjGw6qJfWT0mKy7aD6xg0cJYWL4rf0KHgF84cc8q+bJ3B0OC8d1Zd8Vjye4pDJzZAxNZRdezEZunwmnUslhw/2/XVFLlwWju4jteZRSJKV5YGg/6Vab+ZO822YwY5vHY4hsWhfZc/+/qB8ubnCNC2u0Gfd084fZ/BRYpl/rd6+blGn1FOP5YvIUAAAMgTOmjXxl/On2PHzNZBzfz+r86YvTOXqZP5V+fN9hz32xrzh3LWDu71vUZI8euaXHmX9vIxJdt959R5vPR6wqHwTJ3OvLzO63p5eX0gLjXTXdpLP5dDuzOdxyvvUOfy28n0vOMT599yvWeW+I+8BAdsnT9bhyzTHc6/OnTauSsbFZxj51M7bQX5LD+f/3fbhezvzNo7kw5td/hc7D+T4+eXXm//dJwknzp0RQe3SJlq0viht6/Ssb16ZzZTR/yyBQ/EiDu5ooMbGBKWbQe3zh0TpFSdm516rODgYPE1+n+sjvbJ/H/vaCnXuKe7m+bRLFbHgvJwucTERImOjpaEhASJiorijAPwij9Ssg6ztT1fa6Kk7OTnjzz9L8zWuXLW84gvT2a+w2jvoGXt1DoMLb68Tub3f91tPXNgmxxa/ckV7a/QrJ8ULV8z813LTNs73NnM4RhZl7WoHJztz5XXZfldP+syeJfAkHCHO5C2jl3A1TuAlzu0WYfz6vdCo4rhV+2Y5mVZQbebue5sNndnL/88Wdrp2NGVAEum4cuXtr+0zaZ3HpRzJ/Zl7uCaIco1pMWo/2bqRF/Zmb10dzivfLGDm/P/He87vZPmi+cv++Cyazq4vnr+CssCN58/Z2IEgYc7cX8flx8j5oMFLj8GAO+mf5xkGk57+fmXD0bIoTWfOQzJdbx7nPP7q3W6yz+f906/tzuy4Uun7/N/m8XjZep0OnTWLi271Dlz7Nwlxx3OaU8SXaVBlk5h1o5rgEMH96/jia0DfPmzS9sFSu96RTN1PAv79XOLE/7qnF9+trUtU/uz6fzaz1+WDuuaqT0vzVd2vCdksUhUhbrSdsJ3PvNH8sJg53cwavd7NtsObq1bn5aw6DJOP56v4Q6uc86hL442g+ciQIACWbNmjbz44ovy008/mT/oq1SpIvfcc4+MGDFCQkJCCrRP/aNny5Yt0qhRI34rgIcxGX+zGXB28UKSxG5f7tRjOWdWamaOHbHcnuPOW7N0YC91uhzfZ+qE5vJetzvxyxITBLmCJUAq3zwwU8f4ik6tfT+Odz2zHNPh80fbFLui85ldhzQvnxVk2aA5p7IMpc76s+R/fqvW/tbkcNl1cNs8t0Sc6S033wV6bZ/zO7k1+z7FPNwCooN77ejgAt6FAAHy7ZtvvpEBAwbIP/7xD/n444+lVKlSsmvXLnnppZfk2LFjJlgAwLfosMYrOmhikbDi5aRm39GZOsOZ79L+9f6vu6ABWYb1OrwPCJR/9yudrw59bs/5GeLrqmGOOXdw60iDwf906rEednMHNyTS+TMXNV8DiaYKjk7utaGDC8CfECBAvuhogccff1zGjh1rRgvY1K5dW2bPnm1e//zzz/LEE0/Izp07pXz58jJ+/HgTUFCbN2+W4cOHy6+//mpGGrRs2VIWLFggzZo1M5+3atXK/FH/zDPPmAcAz2DvoNlcHmZ7w90vOH0u5I03+s48Phs6uNeGDu61o5MLAMgLAgTIlz179si+ffvsHf6szpw5I927d5cJEybIsGHDZN26ddKrVy+pXLmytG7dWh599FHp06ePWZ6Wlibr1683223YsMHc5dPlTDEAPLNzUbpuOzn56yozfDyyfC0yAefz/JFJ+dqvQebhAgDgWgQIkC8nT540zxUqVMj282+//VZKly4tjz2m9ZZF2rVrJwMHDpQPP/zQBAi0hMqBAwfk6NGjUrFiRWnbti2/AcBLRg+dPf6HeX3Tox9KTP3O7m6S16GDCwAAPF3+MwXBr2m+AXXkyJFsPz98+LBUrVo107LrrrvOLFfvv/++XLhwQZo0aWKmJbz55puF0GoA1+rs0d2SfPqIBASHSamarTihAAAAPogAAfKlZs2aJgAwd+7cbD/XUQH79+/PtEynJOhyVb16dfnoo4/k+PHj8u6778ro0aNl06ZN5rP8JhIDUHhid1wqhVayVisJDI3g1AMAAPggAgTIF+3Ev/HGG6ZigT7HxcWZ5b///rsMHTpUbr75ZomNjZUZM2bIxYsXZfXq1fLpp5/K4MGDzXoaHDhx4oTZT/HixU1CwqCgSzNdYmJiZO/evfxGAA8Uu/1SgKBM/U7ubgoAAABchBwEHi7mgwXiaXr37i2LFi2SF154wVQoUJqEcNCgQVKuXDnzmVY4GDdunKliMHPmTBM4UMuXL5cxY8bI2bNnTUDglVdekYYNG5rPtGyiVkh44IEHTJWEp59+2q0/J4BL0s4nyuk/NpjXZcg9AAAA4LMIEKBAtMO/ePHibD/TkoVajSA7OoIgJxoY0AcAz3Lqtx/Emn5RipS9XoqUruLu5gAAAMBFmGIAAMjb9IJ6TC8AAADwZQQIAABXLW9oS1BI/gEAAADfRoAAAJCjxEM7JCUh1lQuKFGjOWcKAADAhxEgAADkOr2gVJ22EhgcypkCAADwYQQICkBL+FWrVk3CwsKkSZMmppQf8u7LL780lQvCw8PNs74H5w+eifKGAAAA/oMAQT7NmzfPlPB79tlnZcuWLdKmTRvp0aOHHDx40DW/IR+jwYDbb79dtm/fLhcuXDDP+p4gAecPnif17GmJ/3OTeV2mXkd3NwcAAAAuRpnDfJo+fboMHTrUXo7vtddekyVLlsjMmTNl6tSpV6yfkpJiHjaJiYnizyZNmiQWi8UkPlO254ceekhWrFjh5tZ5vrlz55pnx/On53Py5MnSr18/N7cOvubkzlUi1gyJrFBHwktUcHdzAAAA4GIECPIhNTVVNm3aJE8//XSm5V27dpV169Zlu40GDbRTnNVNN90kgYGBMmDAAPn73/8uN998s/2zX3/9VZ588kkTeFDPPfecXH/99TJkyBDzvnr16rJgwQLp06eP7N271yybPXu2/PHHH/LCCy+Y9926dZNXX31V6tata9/vmjVr5K233pLPPvvMvNfj6np9+/Y174sXLy5r166V++67TzZu3GgPiKiRI0fa2/3hhx9K69atJT4+3iybP3++aavuW13tZ9IRA7bOraO4uDj79sgfPZ96fhcvXuy035MnXHvfN6/u1ZdCv99Pu/zfU06/J2edu0d/2CJbROS+4qny3NpLQdHCUHf6Xq69a9D6A/dde/od8W4p8WqfRgxw6/+576718v8LH1jAtVdAXHvee+3pd8S7hfj/pCtsaDDerX0Nb//uu291Cbdde/n5Pek+c2OxZtdbQ7aOHj0qFSpUMBd2q1at7MunTJlifum7d+/O0wiCSpUqSUJCgkRFRfndmdacA9kFCWJiYswoAlzdO++8I7GxsZmW6QiCBg0ayNatW33q9J24v494s5gPFnj1uUvPsEr9ecvkdEqqfNW9pbQsW1L84dwprj3OH9ef/373uRPffZw/rj/vFePmv12ciREEBaAdMke2Yd7ZCQ0NNQ9cMmHCBJNzwDbNwPasUzRuu+02TlMuGjVqlOn82a4/Pa+AM/0Sd8YEB6KCg6RpmeKcXAAAAD9AksJ8KFWqlJkWcPz48UzL9Y6u3gFH7nSe/BdffGHueGsVCH3WBIUEB/J3/mrWrGlf9sknn3D+4HTLD18aqdKuQmkJDuC/CgAAAH/AX335EBISYsoaLlu2LNNyfe845QC5d3J1OHxycrJ5JjiQ//P322+/SY0aNcz74OBgLjk43XeXAwSdKpTh7AIAAPgJAgT5pMkm3n33XXn//fdNJ00TR2iJw2HDhrnmNwRkQ6cY3HLLLeb1119/zTmCU51MTpFf4hLM644VSnN2AQAA/AQ5CPKpf//+JuO+lpU7duyY1KtXTxYuXChVqlRxzW8IyIEGCP75z3+a6y8tLY2RBHCaFUcujR5oUDJaykSEcWYBAAD8BCMICmD48OGyf/9+U51Ayx62bdvW+b8ZIBctW7aU0qVLy5kzZ2T16tWcLzh/ekFFphcAAAD4EwIEgJfShJm9e/c2r5lmAGe5mJEhK4+cNK87k38AAADArxAgALyYYx4CW9lD4Fr8fDJeEtMuSonQYGlUqhgnEwAAwI8QIAC8WJcuXUy5yAMHDsi2bdvc3Rz40PSCDhXKSGCAxd3NAQAAQCEiQAB4sYiICBMkUEwzgDOQfwAAAMB/ESAAvBzlDuEsR88ly6/xSaLjBtqXp7whAACAvyFAAHg5TVRosVhk8+bNcujQIXc3Bz5Q3rBJ6eJSIizE3c0BAABAISNAAHi5mJgYU/JQzZ8/393NgRdjegEAAIB/I0AA+ACmGeBapaSnyw9HT5nXnSqW4YQCAAD4IQIEgA8FCFauXCkJCQnubg680IYT8XLuYrqUCQ+VeiWi3N0cAAAAuAEBAsAH1KpVyzzS0tJk8eLF7m4OvNDyy/kHOlYoIwEWyhsCAAD4IwIEgI9gmgGuxYrDlwIEnSpSvQAAAMBfESAAfCxAsHDhQjOSAMirA0nnZE/CWQm0WKQd5Q0BAAD8FgECwEc0b95cSpcubXIQrFq1yt3NgRf57vBJ89ysTHGJCgl2d3MAAADgJgQIAB8RGBgoffr0Ma+//vprdzcHXuS7y/kHqF4AAADg3wgQAD6ah8Bqtbq7OfACyRfTZe0xyhsCAACAAAHgUzp37izh4eFy6NAh2bp1q7ubAy+w7nicXEjPkApFwqR2sUh3NwcAAABuxAgCwIdERERI165dzWumGSAvvrNVL6hQRiyUNwQAAPBrBAgAH51mMH/+fHc3BR5Op6GQfwAAAAA2BAgAH9O7d28JCAiQLVu2yMGDB93dHHiwvYnn5EDSeQkJCJCby5Vyd3MAAADgZkHubgAA59JSh61atZI1a9aYUQSPPvoop9jPxHywIE/rffrqqyJfrZR2nTrJdZ8scnm7fO38AQAA+BoCBICPTjPQAIHmISBAgJwsXLjQPPfs2ZOTBKchwML5AwB4LwIEgI8GCJ566ilZuXKlnDlzRooVK+buJsHDnD17VlatWmVeEyAA4AsITgHAtSNAAPigGjVqSO3atWXXrl2yaNEiGTBggLubBA/z3XffSVpamlSvXt1cLwAA/0aAhfMHKAIEgA+PItAAgU4zIECAq00voLwhAABwJwJUnoMqBoCPlzvUEQSpqanubg48rLwh+QcAAACQFQECwEc1b95cYmJiJDEx0eQiAGx27Nghhw8flvDwcGnXrh0nBgAAAAYBAsBHBQQESJ8+fcxrnWYA2NhGD3Ts2NEECQAAAADTh+A0AL4/zWD+/PlmWDmgmF4AAACA7BAgAHxYp06dJCIiwgwn37Jli7ubAw+gZS/Xrl1rXvfo0cPdzQEAAIAHIUAA+DAdPt6tWzfzmmkGUMuWLZP09HSpU6eOVKtWjZMCAAAAOwIEgJ9MMyBAAMX0AgAAAOSEAAHg43r16mUSFv7yyy+yf/9+dzcHbpSRkWHKXqqePXvyuwAAAEAmBAgAH1eqVClp3bq1PVkh/JfmoThx4oQULVpUbr75Znc3BwAAAB6GAAHgB5hmAMfpBV26dJGQkBBOCgAAADIhQAD4UYBg1apVEh8f7+7mwE3IPwAAAICrIUAA+IHrr79e6tata7LX2zqJ8C+nTp2S9evXm9eUNwQAAEB2CBAAfjaKgDwE/mnJkiVitVqlYcOGUqFCBXc3BwAAAB6IAAHgJ2wBAs1in5KS4u7moJAxvQAAAABeFSD48ssvpVu3bibrusVika1bt16xjnZsHnvsMbNOkSJFpG/fvnL48OFc9z1jxgypVq2ahIWFSZMmTWT16tWZPtc7axMnTpTy5ctLeHi4tG/fXnbu3OmUYwOe4KabbpJy5cpJUlKSrFy50t3NQSHSqSWLFy82rylvCAAAAK8IEJw7d86UY3vppZdyXGfEiBHy1Vdfydy5c2XNmjVy9uxZ6d27t/kDOCfz5s0z2z377LOmzFebNm3MHNyDBw/a15k2bZpMnz5d3nzzTdm4caOULVvWZPrWztS1HBvwFAEBAdKnTx/z+uuvv3Z3c1CINmzYIKdPn5ZixYpJixYtOPcAAADIlsWqt849zP79+83dfu3MN2rUyL48ISFBSpcuLR9//LH079/fLDt69KhUqlTJDJ/V0QfZad68uTRu3FhmzpxpX1anTh259dZbZerUqWb0gI4c0ADA2LFj7aMFYmJi5OWXX5aHH364wMfOKjExUaKjo83+oqKiruk8Afml12qvXr3MHPRDhw6ZkTqe6sT9l4IZ3irmgwXiKcaPHy8vvPCC+e7SACcAAADg8SMIcrNp0yZJS0uTrl272pdpx75evXqybt26bLdJTU012zluo/S9bZt9+/bJ8ePHM60TGhoq7dq1s69TkGPbAg0aFHB8AO7SsWNHMz3myJEj5pqGfyD/AAAAAHwuQKCd+JCQEClevHim5XqnXz/LqbSXTgHQdXLaxvac2zr5PbbSEQo6YsD20BEHgLtoDg7baBemGfiHY8eOyebNm83r7t27u7s5AAAA8GBuCxDMmTNHihYtan9kTRqYHzpFILeh0lk/z26bvKyT32OPGzfOTCewPXRYN+AJ1QwIEPgHW3JCTVJZpkwZdzcHAAAAHizIXQfWCgCaG8AmL3W5NXGgThmIj4/PdCc/NjZWWrVqle02WnEgMDDwirv8uo1txIDuV+k6muU9p3Xye2zbVAV9AJ5CcxDov4nt27eb6TWa7wO+i+kFAAAA8PgRBJGRkXL99dfbH1paMDdanjA4OFiWLVuWafjsjh07cuyk67QA3c5xG6XvbdtoB0kDAI7raDBg1apV9nUKcmzAE5UsWVJuvvlm85pRBL5N86YsXbrUvKa8IQAAADx2BEF2tAyXlh7U6gBq9+7d5lk77/rQOfxDhw6VUaNGmU5OiRIlZPTo0VK/fn3p3LlzjvsdOXKkDBo0SJo2bSotW7aUWbNmmeMMGzbMfK5TBLSCwZQpU6RGjRrmoa8jIiJk4MCBZp2CHhvw1GkGGgCbP3++ufbhmzSBqiZG1Qos+v0HAAAAeE2AQDsr999/v/393XffbZ4nTJggEydONK9fffVVCQoKkrvuukuSk5OlU6dOMnv2bDNk2qZ9+/ZStWpVs1xpaa+4uDiZPHmyueuvlQd02G2VKlXs24wZM8bsb/jw4WYagU5/0DtvOtLBJi/HBrwlQKCBsx9++MEE5jTgBd+dXqDJCQMCvConLQAAANzAYtUsez5GgwMaUBgyZIh4Gr2bp6MRNGFhVFSUu5sDP6ajX3SKzMcffyz33nuveJoT9/cRbxbzwQKP+R1/9tln9oArAAAAkBOfu6W0a9cuc9d/8ODB7m4K4NGoZuDbdBqVBgd05EDXrl3d3RwAAAB4AZ8LENSuXdtkZ2c4LZC3AIGWwUtJSeF0+ZhFixaZZ827whQSAAAA+GWAAEDeaGUOLet59uxZWbFiBafNx1DeEAAAAPlFgADwUzrKpm/fvuY15Q59i44IWb58uXlNeUMAAADkFQECwI/ZphloBZGMjAx3NwdOotUpzp8/b0aINGzYkPMKAACAPCFAAPixjh07StGiRU35z59//tndzYELphdYLBbOKwAAAPKEAAHgx0JDQ6V79+7mNdMMfAf5BwAAAFAQBAgAP+c4zQDe748//pDff/9dgoKCpHPnzu5uDgAAALwIAQLAz+kw9MDAQNmxY4f8+eef7m4OnFTesE2bNhIVFcX5BAAAQJ4RIAD8XIkSJaRt27bmNdMMvB/TCwAAAFBQBAgA2KcZECDwblq54PvvvzevKW8IAACA/CJAAED69u1rzsLq1aslLi6OM+KlNDiQkpIiVapUkTp16ri7OQAAAPAyBAgASLVq1aR+/fqSkZEh3377LWfES1HeEAAAANeCAAEAg2kG3s1qtZJ/AAAAAIUbILjnnntk1qxZpowWAN8LECxZskQuXLjg7uYgn3bt2iX79++X0NBQ6dChA+cPAAAArg8QFC1aVKZPny61a9eW8uXLy4ABA+Ttt982f5wC8F5NmjSRChUqyLlz5+S7775zd3NQwOkF7du3lyJFinD+AAAA4PoAwTvvvGOCAUePHjWBgujoaHn99dflhhtukHLlyuW/BQA8gsVisScrnD9/vrubg3yivCEAAADcloMgMjJSihcvbh7FihWToKAgKVu27DU3CID7pxlogEATFsI7JCYmmgoUivKGAAAAKLQAwdixY6VFixZSqlQpee655yQ1NVXGjRsnJ06ckC1bthS4IQDcT4ena/Dv+PHjsnHjRnc3B3mkU0LS0tKkRo0acv3113PeAAAAUCBB+d3glVdekdKlS8uECRPM3UZqbQO+QxPc9ejRQz7//HP5+uuvpXnz5u5uEvKA6QUAAABwywgCHSXw7LPPyoYNG6Rt27ZmWkH//v1l5syZ8ttvv/FbAbwc5Q69C+UNAQAA4CwWq/51eQ1++eUXee211+STTz4xc5bT09Od1jhfnSusiR0TEhIkKirK3c0BrhAfHy9lypSRixcvyp49e9w2ZP3E/X3Em8V8sKBQjqPfwY0aNZKIiAiJi4uTsLCwQjkuAAAAfE++pxjYRhGsXLnSPDQxlnZ69Q9Uam8D3k8Tj+rooBUrVphpBqNGjXJ3k5CH6QWdOnUiOAAAAIDCnWKgnYdmzZrJnDlzTEKsjz76SE6fPi0///yzyU8AwPsxzcB7kH8AAAAAbpti8M0335i7iwyPLximGMAbHDhwQKpWrSoBAQGmQolWLSlsTDHI23QQ/d3o9K79+/dLlSpVCuE3AwAAAF+V7xEEvXv3tgcHDh8+LEeOHHFFuwC4kXY0GzZsaDqeGhSEZ1q6dKn5Hd1www0EBwAAAFD4AQL9Y3Ty5Mkm0Z52IipXrizFihWTf/zjH+YzAL41zWD+/PnubgpywPQCAAAAuDVAoCUO33zzTXnppZdMssLNmzfLlClT5I033pDx48c7tXEA3B8gWLJkiSQnJ/Or8DAakF20aJF53bNnT3c3BwAAAP6Yg6B8+fLy9ttvS9++fTMt12znw4cPZ8pBLshBAG+hXw06SujQoUOyYMECM72oMJGD4Oo2btxoEsZGRkaa8obBwcGF9JsBAACAr8r3CAKtWFC7du0rlusy/QyAb7BYLPZAoAYA4ZnTC7p27UpwAAAAAO4JEGjiMp1ikJUu088A+N40Ax1BQI4Rz0L+AQAAADhbUH43mDZtmvTq1UuWL18uLVu2NHcZ161bZ4Yh2/5gBeAb2rVrZ6qWaKnD9evXm3/zcL/Y2FgzxUB1797d3c0BAACAv44g0A7D77//LrfddpucOXPGTCvo16+f7N69W9q0aeOaVgJwi5CQEOnRo4d5zTQDz6GJIzVHxI033mjywgAAAABuGUGg9A/SF1980SkNAOD50wzmzZtnAgRavQTux/QCAAAAuC1AsG3btjzvsEGDBtfSHgAeRkcQBAUFya5du8zooZo1a7q7SX7t4sWLZgSBorwhAAAACj1A0KhRI5NrILeKiLpOenq6s9oGwAMUK1ZM2rdvb/KOzJ8/X0aPHu3uJvk1zQURHx8vJUqUkObNm7u7OQAAAPC3AMG+fftc3xIAHj3NQAMEOs2AAIFnTC/o1q2bBAYGurk1AAAA8LskhZqQUDOZV6lSRT788EMpXbq0eZ3dA4Dv6du3r3nWiiUnT550d3P8mi1AoNVkAAAAgEIPEPz2229y7tw583rSpEly9uxZpzYCgGerXLmyyZifkZEh33zzjbub47eOHDkiW7duNdO5dAQBAAAAUOgBAs1BcP/995vggOYh+Oc//ymTJ0/O9lFQaWlpMnbsWKlfv74UKVLEVEoYPHiwHD16NNN6KSkp8thjj0mpUqXMenpn8/Dhw7nuf8aMGVKtWjUJCwuTJk2ayOrVqzN9rj/XxIkTzXHDw8PNnOudO3c65diAr0wzUJQ7dJ/FixebZ809oN9DAAAAQKEHCGbPni0lS5Y0dw71ztWiRYvkq6++uuLxv//9r8ANOX/+vGzevFnGjx9vnr/88kuTMd02tNlmxIgR5lhz586VNWvWmNEMvXv3vmpyRC3Rpts9++yzsmXLFmnTpo3JzH7w4EH7OtOmTZPp06fLm2++KRs3bpSyZctKly5dJCkp6ZqODfhagGDp0qXm3ysKH+UNAQAA4EoWa26lCbIICAiQ48ePS5kyZcTVtKPerFkzOXDggBninJCQYPIffPzxx9K/f3+zjo4wqFSpkvnDOacht3q3rXHjxjJz5kz7sjp16sitt94qU6dONaMHdOSABgB0FINttEBMTIy8/PLL8vDDDxf42FklJiZKdHS02Z/mdQC8hf47qVq1qgms6SiCrME7Zztxfx/xZjEfLHDq/lJTU82oAQ1a/vzzz2YkFAAAAFDoIwgc6RzkwggOKO1E64gFLbOmNm3aZKYidO3a1b6Oduzr1atnkqfl9Ee1bue4jdL3tm20SoMGPRzXCQ0NlXbt2tnXKcixbYEGDQo4PgBvpP8WbUEBphkUvrVr15rggAYuNR8EAAAA4PYAQWG5cOGCPP300zJw4ED7nXbtxIeEhEjx4sUzrat/MOtn2Tl16pSZAqDr5LSN7Tm3dfJ7bKUjFHTEgO2hIw4Ab59msGDBAqbWuGl6gU6P0pFcAAAAgLO57a/MOXPmSNGiRe0Px6SBeqf+7rvvNqMVNLlgXoY+693Nq8n6eXbb5GWd/B573LhxZiSE7XHo0KGr7g/wZDqqRgNdWurwp59+cndz/Ar5BwAAAOCzAQIdqqzlumyPpk2b2oMDd911lxn2v2zZskzz9DVxoE4ZiI+Pz7Sv2NjYK+7+2+ic3cDAwCvu8jtuo/tVua2T32Pbpiroz+D4ALxVcHCw9OzZ07yeP3++u5vjN/bv3y+//vqr+S7T5KkAAACATwUIIiMj5frrr7c/tLSgLTiwZ88eWb58uamc4EiTcmkHRQMHNseOHZMdO3ZIq1atsj2OTgvQ7Ry3Ufreto2WP9QAgOM6GgxYtWqVfZ2CHBvwRZQ7dN/ogdatW9tzsgAAAADOFlTQDbUDrXfPdRqAI602UBAXL16UO+64w5Q41HKKmjfAdke/RIkSpqOvQ5uHDh0qo0aNMsEDXT569GipX7++dO7cOcd9jxw5UgYNGmRGKbRs2VJmzZplMrEPGzbMfK5TBLSCwZQpU6RGjRrmoa8jIiJMDgRV0GMDvkbnwGuwbPfu3eZRq1YtdzfJ5zG9AAAAAB4ZINC7+3/729+uyNxvm4uvHfuCOHz4sH3IcqNGjTJ99v3330v79u3N61dffVWCgoLMSIPk5GTp1KmTzJ492wy9tdF1tRybLldaljAuLk4mT55s7vpr5QH9g7tKlSr2bcaMGWP2N3z4cDONQEsjar13Helgk5djA75Op8l06NDB/PvQagb6bweuo981K1asMK9t0zsAAAAAV7BYtWefDzrEVTvJWmGgXLlyVyToa9iwobibBgcmTpwoQ4YMEU+jZQ51NIImLCQfAbyVJg/9+9//bqbXaPk9Vzhxfx/xZjEfLHDKfhYvXmxGbVSsWNGMfMotcSoAAABQaCMINKHgpk2bpHbt2uKJdu3aZe76Dx482N1NAXyWJhnVAMGPP/4oJ06cuGqiTjhvegHBAQAAAHhUksK6devKqVOnxFNp4GL79u3UCQdcSO9mN27c2Ewt0pwhcA09v99++615zfQCAAAAeESAQIfF2x4vv/yymXO8cuVKM6/f8TN9APAPVDNwPc358ueff5qkkJrzBAAAAHD7FAMtq+U4tFXvamX9Y/VakxQC8L4AwYQJE0zpz3PnzkmRIkXc3SSfnV7Qrl07KVq0qLubAwAAAB+XpwCBVhEAAEcNGjQwlUAOHDhgggS33norJ8jJKG8IAAAAjwsQ6N0rAHCkI4Z0FMG///1vU6KUAIFznT17VlatWmVek38AAAAAHpmkUEturVmzxv7+rbfekkaNGsnAgQMlPj7e2e0D4AV5CDRRIdOLnGvFihWSmpoq1113ndSsWdPJewcAAACcECB46qmn7MkItVrAyJEjzd0tTaSlrwH4jzZt2pgcJSdPnjQlD+E8lDcEAACAxwcI9u3bZ0odqi+++EL69OkjU6ZMkRkzZsiiRYtc0UYAHkqz6/fq1cu8/vrrr93dHJ+hSV/JPwAAAACPDxCEhITI+fPnzevly5dL165dzesSJUpQ5hDw83KH2rHFtdu5c6ccOnRIwsLCpH379pxSAAAAeE6SQkc333yzmUrQunVr2bBhg8ybN88s//3336VixYquaCMAD9a9e3cTONyzZ4/s2rVL6tSp4+4meT3b6IGOHTtKeHi4u5sDAAAAP5HvEQRvvvmmBAUFyX//+1+ZOXOmVKhQwSzX6QXaUQDgXyIjI6VDhw7mNdMMnIPpBQAAAHAHi5UxwYVKEzxGR0dLQkKCREVFFe7BARfRYOHw4cOlRYsWTktWeOL+PuLNYj5YUKDt9LuhZMmSpirE3r17TRUDAAAAwCNHEDhKTk42HV7HBwD/07dvX/O8fv16OX78uLub49WWLVtmggO1a9cmOAAAAADPDhCcO3dOHn30USlTpowULVpUihcvnukBwP/oVKOmTZuaJIXffPONu5vj1ZheAAAAAK8JEIwZM0ZWrFhhyhqGhobKu+++K5MmTZLy5cvLRx995JpWAvCqagYomIyMDHu52J49e3IaAQAA4NkBggULFpjgwB133GGSFbZp00aee+45mTJlisyZM8c1rQTgNQECLX+qI42Qf1u3bjVTNHR0llaMAQAAADw6QHD69GmpVq2aea1J9vS90j9mf/jhB+e3EIBXqFevnvluuHDhgixdutTdzfHq6QWdO3c2I7QAAAAAjw4QaEbt/fv3m9d169aVzz//3D6yoFixYs5vIQCvYLFYmGZwjcg/AAAAAK8KENx///3yyy+/mNfjxo2z5yJ48skn5amnnnJFGwF42TQDTVR48eJFdzfHq5w6dUp++ukn87pHjx7ubg4AAAD8UFB+N9BAgE2HDh1k165d8vPPP0v16tWlYcOGzm4fAC+iU420mklcXJysW7dO2rZt6+4meQ2dlqFVIBo0aCAVK1Z0d3MAAADgh/I9giCrypUrS79+/QgOADCJS3v16mXOBNUM8ofpBQAAAPCaAIGWNtScA4mJiVd8lpCQIDfccIOsXr3a2e0D4MXlDvWOOHKXnp4uixcvNq8pbwgAAACPDxC89tpr8uCDD5rKBVlFR0fLww8/LNOnT3d2+wB4mW7duklISIjs3btXfv31V3c3xyts3LjRTMvQ79KWLVu6uzkAAADwU3kOEGhiwu7du+f4edeuXWXTpk3OahcALxUZGSmdOnUyr+fPn+/u5njV9AINrug0DQAAAMCjAwQnTpyQ4ODgHD/XP2pPnjzprHYB8JFpBsgd+QcAAADgVQGCChUqyPbt23P8fNu2bVKuXDlntQuAF+vTp495Xr9+vRw7dszdzfFox48ft4++utooLQAAAMBjAgSaOOv555+XCxcuXPFZcnKyTJgwQXr37u3s9gHwQuXLl5dmzZqZ1wsWLHB3czyaLTlh06ZNJSYmxt3NAQAAgB/Lc4Dgueeek9OnT0vNmjVl2rRpZuiwzi9++eWXpVatWuazZ5991rWtBeA1mGaQN0wvAAAAgKewWPNRh+zAgQPyyCOPyJIlS+zlyywWi0msNWPGDKlataor2+oTtEykZirX0pDZVYQAfMXOnTulXr16EhoaKqdOnZKiRYvma/sT91+apuCtYj7IfeREWlqalC5d2nwf/PTTT9K8efNCaRsAAACQnXyly65SpYq52xUfHy9//PGHCRLUqFFDihcvnp/dAPADdevWleuuu07+/PNPE1S8/fbb3d0kj/Pjjz+a4ECpUqXMFAMAAADAK6YYONKAwE033WTmGBMcAJAdHV3ENIO8TS/Q5ISBgYFcSAAAAPC+AAEA5IUtQPDtt9/KxYsXOWlZkH8AAAAAnoQAAQCXad26tZQoUcIkMV27di1n2sGhQ4dM6diAgADp2rUr5wYAAABuR4AAgMsEBQXZy59q5RP8ZdGiRea5RYsWUrJkSU4NAAAA3I4AAQCXcsxDkI+iKT6P6QUAAADwNAQIALiUDp/XUodazUBLH0IkJSVFli9fbk5Fz549OSUAAADwCAQIALhU0aJFpXPnzuY10wwuWb16tZw7d07KlSsnjRo14goEAACARyBAAMDlKHeY/fSCHj16mHKQAAAAgCfwqADBxIkTpXbt2lKkSBEpXry4ueu4fv36K4bmPvbYY1KqVCmzXt++feXw4cO57nvGjBlSrVo1CQsLkyZNmpg7eI50brQev3z58hIeHi7t27e/Yjh0QY8N+Ls+ffqY540bN8rRo0fF35F/AAAAAJ7IowIENWvWlDfffNOU/lqzZo1UrVrVzF8+efKkfZ0RI0bIV199JXPnzjXrnD171mRJT09Pz3G/8+bNM9s9++yzsmXLFmnTpo25c3fw4EH7OtOmTZPp06eb42snpmzZstKlSxdJSkq6pmMDEPPvqXnz5uZUzJ8/369Pyd69e2X37t2mwoNt6gUAAADgCSxWD04rnpiYKNHR0SaZV6dOnSQhIUFKly4tH3/8sfTv39+so3cjK1WqZO7IdevWLdv9aMekcePGMnPmTPuyOnXqyK233ipTp041owd05IAGAMaOHWsfLRATEyMvv/yyPPzwwwU+dk4/k+4vKirKCWcJ8A76b+2ZZ56R7t2720v8Xc2J+y+NOvBWMR8syHa5BiF1JJKOUvr+++8LvV0AAACAV4wgcJSamiqzZs0ynemGDRuaZZs2bZK0tDQzqsBGO/b16tWTdevW5bgf3c5xG6Xvbdvs27dPjh8/nmkdzbrerl07+zoFObYt0KBBAccH4M95CFasWJFpZI6/YXoBAAAAPJXHBQi++eYbk/VccwW8+uqrsmzZMjPnX2knPiQkxOQncKR3+vWz7Jw6dcpMAdB1ctrG9pzbOvk9tu2uqQY5bA8dcQD4Ix21c/3115ug3ZIlS8QfnT9/3j5qgPKGAAAA8DRuCxDMmTPHBAJsD1vSwA4dOsjWrVvNXXkdinzXXXdJbGzsVfelUwRyywSe9fPstsnLOvk99rhx48x0Atvj0KFDV90f4Kv034m/VzNYuXKlXLhwQSpXrix169Z1d3MAAAAAzwgQaAUADQTYHk2bNjXLtTqA3mVs0aKFvPfeeyaRlz7bEp3p3cf4+PhM+9IAQta7/zY6+iAwMPCKu/yO2+h+VW7r5PfYtqkKmmvA8QH4K1uA4NtvvzVTdvx5egHlDQEAAOBp3BYgiIyMNIEA20NLC+Z0h17n8SstTxgcHGymHdgcO3ZMduzYIa1atcp2e50WoNs5bqP0vW0bLX+oAQDHdTQYsGrVKvs6BTk2gMz034oG7TTQppVA/Il+l2lgRDG9AAAAAJ4oSDzEuXPn5MUXXzQjC8qVKydxcXEyY8YMOXz4sNx5551mHZ3DP3ToUBk1apSULFlSSpQoIaNHj5b69etftVzYyJEjZdCgQWaUQsuWLU3yQy1xOGzYMPO53snTCgZTpkyRGjVqmIe+joiIkIEDB17TsQH8RUfzaGnQ2bNnm2kGOqXIX2hpw/3795ugZceOHd3dHAAAAMBzAwTacdi1a5d8+OGHJrGgdsJvuukmk5vghhtusK+niQt12oHmJkhOTjblD7WzodvbaPmwqlWrmuVKyxJqwGHy5Mnmrr9WHtChvlWqVLFvM2bMGLO/4cOHm7ubWhpx6dKlZqRDfo4NIPdpBrYAgf6b8peh9rbpBfr9pFOpAAAAAE9jseq4Vx+jwYGJEyfKkCFDxNNomUMdjaAJC8lHAH+ko4V0moEm6/vll1+kQYMG2a534v4+4s1iPliQ6b2ONPruu+/ktddekyeeeMJt7QIAAAC8pszhtdJRCHrXf/Dgwe5uCoBs6N1z27Qcf6lmkJSUJD/88IN5Tf4BAAAAeCqfCxDUrl1btm/fLgEBPvejAT7D38od6sgBrdqgCVk1xwkAAADgiehFAyh0ffr0MbkHNm3aZBKR+lN5QwAAAMBTESAAUOhiYmKkRYsW5vWCBZnn6vsaTfNCgAAAAADegAABALfwl2kGOuXpyJEjEh4eLu3atXN3cwAAAIAcESAA4NYAwYoVK0x1D19lGz2gZVHDwsLc3RwAAAAgRwQIALgtoWjNmjVN8r7Fixf77G+B6QUAAADwFgQIALiNr08ziI+Pl3Xr1pnXPXr0cHdzAAAAgKsiQADA7QECvcuuIwl8zbJlyyQ9PV3q1q0rVatWdXdzAAAAgKsiQADAbbSSQenSpeXMmTPyww8/+NxvgukFAAAA8CYECAC4TWBgoPTu3dsnpxlkZGTIokWLzOuePXu6uzkAAABArggQAPCYPARWq9VnfhubN2+W2NhYiYyMlNatW7u7OQAAAECuCBAAcKsuXbpIeHi4HDx4ULZt2+Zz0wv05wsJCXF3cwAAAIBcESAA4FYRERGmE+1r0wzIPwAAAABvQ4AAgNv5WrnDUxdSZMOGDeY15Q0BAADgLQgQAHA7TVRosVjMvP1Dhw6Jt1t55KTJp9CoUSMpX768u5sDAAAA5AkBAgBuV6ZMGWnVqpV5PX/+fPF23x2ONc9ULwAAAIA3IUAAwCP4yjSD9AyrfH/kpHlNgAAAAADehAABAI8KEKxcuVISEhLEW20+FS9nUtOkePHi0rx5c3c3BwAAAMgzAgQAPELNmjWlVq1akpaWJosWLRJvn17QrVs3CQoKcndzAAAAgDwjQADAY/jCNAPyDwAAAMBbESAA4HEBgoULF0pqeoZ4m+PnL8j204liuTyCAAAAAPAmBAgAeAyds68VDRITE+WnE3HibVYcuTS9oFGpYubnAAAAALwJAQIAHiMwMFD69OljXi8+eEK8dXpBp4oEBwAAAOB9CBAA8MhpBksOHRer1SreIi0jQ1YdPWVeEyAAAACANyJAAMCjdO7cWSIiIuTIuQuy43SieIsNJ07L2bSLUjIsRBqWjHZ3cwAAAIB8I0AAwKOEh4dL165dzevFB4+Lt/jucv6BjhXKSIBF0xQCAAAA3oUAAQAPnmbgPXkIvjt80jwzvQAAAADeigABAI/Tq1cvCbCImWJw6Ox58XTaxt1nkkyb25cv5e7mAAAAAAVCgACAxyldurTcVLqE14wiWHF59IC2uVhoiLubAwAAABQIAQIAHqlb5RjzvMQLyh3a8g8wvQAAAADejAABAI/U/XKA4MfjcZKQkiae6sLFdFl9jPKGAAAA8H4ECAB4pOuiikqN6KJy0WqVFZfv0Huin06cluSL6VI2IlTqFo90d3MAAACAAiNAAMDjRxEs9uBpBt8dvjy9oEIZsVDeEAAAAF6MAAEAj9W9UlnzrCMIUtMzxBORfwAAAAC+ggABAI91Y+liUiY8VJLSLsq643Hiaf5MPCt/Jp6T4ACLtClHeUMAAAB4NwIEADxWgMUiXStdnmZw6Lh4annD5jElJDIk2N3NAQAAAK4JAQIAHq3b5QDB0oMnxGq1iqfmHwAAAAC8nccGCB5++GGT8Ou1117LtDwlJUUee+wxKVWqlBQpUkT69u0rhw8fznV/M2bMkGrVqklYWJg0adJEVq9enelz7XhMnDhRypcvL+Hh4dK+fXvZuXOnU44NoOBuLldKwoMC5ej5C7ItLsFjTuU5h2kPnSoSIAAAAID388gAwf/+9z9Zv3696axnNWLECPnqq69k7ty5smbNGjl79qz07t1b0tPTc9zfvHnzzHbPPvusbNmyRdq0aSM9evSQgwcP2teZNm2aTJ8+Xd58803ZuHGjlC1bVrp06SJJSUnXdGwA10aDAx3KlzavFx/ynGoGa4/HSUpGhlQqGm7KMQIAAADezuMCBEeOHJFHH31U5syZI8HBmef0JiQkyHvvvSf/+te/pHPnznLjjTfKJ598Itu3b5fly5fnuE/t+A8dOlQeeOABqVOnjhmVUKlSJZk5c6Z99IAu0wBCv379pF69evLhhx/K+fPn5dNPP72mYwO4dt0ulztccvC4500vqEh5QwAAAPgGjwoQZGRkyKBBg+Spp56SG2644YrPN23aJGlpadK1a1f7Mh1loB36devWZbvP1NRUs53jNkrf27bZt2+fHD9+PNM6oaGh0q5dO/s6BTm2bVpCYmJipgeA/OlcMUYCLCK/xifJwaTzbj99GlQk/wAAAAB8jUcFCF5++WUJCgqSxx9/PNvPtRMfEhIixYsXz7Q8JibGfJadU6dOmSkAuk5O29iec1snv8dWU6dOlejoaPtDRy4AyJ+SYSHSrEwJ83qpB0wz+D3hrBw+lyyhAQHSmvKGAAAA8BFuCxDoFIKiRYvaH6tWrZLXX39dZs+ebZIT5vduXm7bZP08u23ysk5+jz1u3DgzPcH2OHTo0FX3ByB73SuX9Zhyh7bRA63KlZSIoEB3NwcAAADw7gCBVgDYunWr/aHD9GNjY6Vy5cpmFIE+Dhw4IKNGjZKqVauabTRxoE4ZiI+Pz7Qv3S7r3X8brTgQGBh4xV1+x210vyq3dfJ7bNtUhaioqEwPAPnX/XK5wx+Pn5YzKakek38AAAAA8BVuCxBERkbK9ddfb3889NBDsm3btkxBA53jr/kIlixZYrbR8oSauHDZsmX2/Rw7dkx27NghrVq1yvY4Oi1At3PcRul72zZa/lADAI7raDBARzXY1inIsQE4T9WoIlKrWKSkO8z/d4ek1DRZf+K0ed2pAgECAAAA+I4g8RAlS5Y0D0faIdeOe61atcx7ncOv1Qh0VIGuW6JECRk9erTUr1/fVBbIyciRI03yw6ZNm0rLli1l1qxZpsThsGHDzOc6RUBLGE6ZMkVq1KhhHvo6IiJCBg4ceE3HBuA83SvHyO4zSabc4e3VK7rl1P5w7JRctFqlelQRqRZVxC1tAAAAAHw6QJBXr776qpl+cNddd0lycrJ06tTJ5C3QaQQ27du3N9MSdLnq37+/xMXFyeTJk81df608sHDhQqlSpYp9mzFjxpj9DR8+3EwjaN68uSxdutSMdMjPsQG4TrdKMfL6tj9kxeFYSUlPl1A3/NtjegEAAAB8lcWqWfZ8jAYHJk6cKEOGDBFPo2UOdTSCJiwkHwGQsxP397liWYbVKjd+vlxOJKfIp52bScdCzgGgX5eNLh9/Xtfm0q586RzXjflgQaG2DQAAAPCpMofOsGvXLnPXf/Dgwe5uCgAnC7BYpOvlZIU6zaCw7TydaIID4UGB0iLmUtlFAAAAwFf4XICgdu3asn37dgkI8LkfDYBOM6h8KUCw9NBxc0e/MH135FJyxLblSrllegMAAADgSvSiAXiVm8uWkoigQDl+PkV+iUso1GOTfwAAAAC+jAABAK8SFhQoHSpcmvu/5GDhTTOIT0mVn0/Gm9cdKW8IAAAAH0SAAIDX6V65rHlefOh4oR1z1ZGTkmEVqV0sUioWDS+04wIAAACFhQABAK/TuWIZCbRY5Lf4JDmQdL5Qjrn8cv6BToVcOQEAAAAoLAQIAHid4qEh0vxyFYElB10/ikDLK644fNIenAAAAAB8EQECAF6peyGWO9x66oycTkmVqOAgaVqmuMuPBwAAALgDAQIAXl3ucP2J0yaBYGFUL2hXobQEU0IVAAAAPooAAQCvVCWyiEkYmG61yvLLHXhX+c6Wf4DqBQAAAPBhBAgAeK3ul0cRLHZhHoKTySmy9VSCed3xcnlFAAAAwBcRIADgtbpdLnf4/ZGTcuFiukuOoftWDUpGS5mIMJccAwAAAPAEBAgAeK2GJaOlbESonL+YLmuPx7k0/wDlDQEAAODrCBAA8FoBFot0rVTWZeUOL2ZkyMqjl8sbkn8AAAAAPo4AAQCfKHe45NAJybBanbrvTSfPSEJqmpQIDZZGpYo5dd8AAACApyFAAMCrtS5XUooGB8kJk0zwjEumF3SoUEYCAyxO3TcAAADgaQgQAPBqoYGB0uFydQEdReBM5B8AAACAPyFAAMBnphksPui8AMGxc8myMz5RdNxA+/KUNwQAAIDvI0AAwOtphYFAi0V2n0mS/YnnnLLPFZfLGzYpXVxKhIU4ZZ8AAACAJyNAAMDrFQsNkRYxJczrxU6aZsD0AgAAAPgbAgQAfEL3yrZpBtde7jA1PUNWXS5vqKMTAAAAAH9AgACAT+hWqax53hB7Wk5fSL2mfek+zl1MlzLhoVKvRJSTWggAAAB4NgIEAHxC5cgIqVs8UjKsIssvlycsKNv2HSuUkQAL5Q0BAADgHwgQAPAZ3SpfGkWw5NBxJ+UfoHoBAAAA/AcBAgA+V+7w+yMn5cLF9ALt40DSOdmTcNZURWhHeUMAAAD4EQIEAHxGg5LRUj4iTM5fTJfVx04VaB/fHb6UnLBZmeISFRLs5BYCAAAAnosAAQCfYbFYpOvlagZLClju8LsjtukFVC8AAACAfyFAAMCndL9czWDpoROSYbXma9vki+my9vLIAwIEAAAA8DcECAD4lJZlS0jR4CCJTU6RLSfP5Gvbdcfj5EJ6hlQoEia1i0W6rI0AAACAJyJAAMCnhAYGSscKl6oPLM5nNQN79YIKZcx0BQAAAMCfECAA4HO628sd5j0PgdVqJf8AAAAA/BoBAgA+R0cABFks8vuZs7Iv8VyettmbeE4OJJ2XkIAAublcKZe3EQAAAPA0BAgA+Jzo0GBpWbakeb344PF8TS/QHAZFgoNc2j4AAADAExEgAOCTuuWz3KFj/gEAAADAHxEgAOCTulW6FCDYEHta4i6kXnXdc2kX5ccTceY15Q0BAADgrwgQAPBJlYpGSL0SUZJhFVmWyyiC1cdOSVqGVapGRsh1UUUKrY0AAACAJyFAAMDnRxEsyaXcoX16QUXKGwIAAMB/ESAA4PPlDlcePSXJF9NzLm9I/gEAAACAAAEA36VTDCoUCTPBAZ1GkJ1dZ5Lk6PkLEh4YYK98AAAAAPgjjxpBMGTIELFYLJkeLVq0yLROSkqKPPbYY1KqVCkpUqSI9O3bVw4fPpzrvmfMmCHVqlWTsLAwadKkiaxevfqKu4gTJ06U8uXLS3h4uLRv31527tzplGMDcA/9Dul6eZpBTuUOl18ePdC6XCkJDwos1PYBAAAAnsSjAgSqe/fucuzYMftj4cKFmT4fMWKEfPXVVzJ37lxZs2aNnD17Vnr37i3p6dkPH1bz5s0z2z377LOyZcsWadOmjfTo0UMOHjxoX2fatGkyffp0efPNN2Xjxo1StmxZ6dKliyQlJV3TsQF4xjSDpYdOSIbVetX8AwAAAIA/87gAQWhoqOmc2x4lSpSwf5aQkCDvvfee/Otf/5LOnTvLjTfeKJ988ols375dli9fnuM+teM/dOhQeeCBB6ROnTry2muvSaVKlWTmzJn20QO6TAMI/fr1k3r16smHH34o58+fl08//fSajg3AvVrGlJTI4CA5dSFVNp88k+mzhJQ02Rgbb153rECAAAAAAP7N4wIEK1eulDJlykjNmjXlwQcflNjYS3f31KZNmyQtLU26du1qX6ZTArRDv27dumz3l5qaarZz3Ebpe9s2+/btk+PHj2daRwMV7dq1s69TkGPbpiUkJiZmegAoPCGBAfbRAVmnGaw6dlLSrVapEV1UqkRG8GsBAACAX/OoAIEO+58zZ46sWLHC3KnXof4dO3Y0nWylnfiQkBApXrx4pu1iYmLMZ9k5deqUmQKg6+S0je05t3Xye2w1depUiY6Otj905AKAwtXdXu7wRKblTC8AAAAAPCBAoIGAokWL2h+aNLB///7Sq1cvc1e+T58+smjRIvn999/l22+/veq+dIqAJiO7mqyfZ7dNXtbJ77HHjRtnpifYHocOHbrq/gA4X8eKZSQ4wCJ7Es7K3oSzZpnmI1hx+KR53Zn8AwAAAID7AgRaAWDr1q32R9OmTa9Yp1y5clKlShXZs2ePea85CXTKQHz8pTnDNjoNIevdfxutOBAYGHjFXX7HbXS/Krd18nts21SFqKioTA8AhSsqJFhaXS5huPjyKILtcQly8kKKFAkKlGZl/sp1AgAAAPgrtwUIIiMj5frrr7c/tLRgVnFxceaOuwYKlJYnDA4OlmXLltnX0UoHO3bskFatWmV7HJ0WoNs5bqP0vW0bLX+oAQDHdTQYsGrVKvs6BTk2AM/RrdKlQOCSy3kIvjtyKb9Ju/KlTZ4CAAAAwN8FiYfQkoETJ06U22+/3QQE9u/fL88884wZAXDbbbeZdXQOv1YjGDVqlJQsWdJUOBg9erTUr1/fVBbIyciRI2XQoEFmlELLli1l1qxZpsThsGHDzOc6RUBLGE6ZMkVq1KhhHvo6IiJCBg4ceE3HBuAZulWOkWfW7zBVC04mp5B/AAAAAPDUAIFOA9CSgR999JGcOXPGBAk6dOgg8+bNM6MNbF599VUJCgqSu+66S5KTk6VTp04ye/Zss71N+/btpWrVqma50twGOhph8uTJ5q6/5jhYuHChmb5gM2bMGLO/4cOHm2kEzZs3l6VLl+b72AA8U4Ui4VK/RJRsP50o8/44ZC952LFCaXc3DQAAAPAIFqtm2fMxGhzQ0QhDhgwRT6NlDnU0giYsJB8BkLMT9/dx+un559bfzUPzDpy7mC43FI+S725p65JfQ8wHC1yyXwAAAMBVfG7i7a5du8xd/8GDB7u7KQA8tNyhBgfU8eQL8u2BY25uFQAAAOAZfC5AULt2bTNVISDA5340ANdof9K5TO9PX0iVod9vIkgAAAAA+GKAAAByMv2XSyVTbXR+lUVE/rX1d04aAAAA/J5P5iDwZOQgANxHy6leuHDhiuVhYWEm8SgAAADgzxhBAMBv1KxZ05Q1daTva9Wq5bY2AQAAAJ6CAAEAvzFhwgTRQVO2IIE+63tdDgAAAPg7AgQA/Ea/fv3kiy++kAYNGphpBfr85Zdfym233ebupgEAAABuRw6CQkYOAgAAAACAJ2IEAQAAAAAAIEAAAAAAAAAIEAAAAAAAAAIEAAAAAACAAAEAAAAAADBIUggAAAAAAAgQAAAAAAAAAgQAAAAAAIAAAQAAAAAAIEAAAAAAAACMoEtPKCxWq9U8JyYmctIBAAAAAIUmMjJSLBZLjp8TIChkSUlJ5rlSpUqFfWgAAAAAgB9LSEiQqKioHD+3WG23tFEoMjIy5OjRo7lGbgAAAAAAcKbc+qEECAAAAAAAgARwDgAAAAAAAAECAAAAAABAgAAAAAAAABAgAAAAAAAABAgAAAAAAAABAgAAAAAAYJCkEAAAAAAAECAAAAAAAAAECAAAAAAAAAECAAAAAABAgAAAAAAAABgkKQQAAAAAAAQIAAAAAAAAAQIAAAAAAECAoPBZrVZJTEw0zwAAAAAAeApyEBSypKQkiY6ONs8AAAAAAHgKAgQFMGPGDKlWrZqEhYVJkyZNZPXq1c7/zQAAAAAAUIgIEOTTvHnzZMSIEfLss8/Kli1bpE2bNtKjRw85ePCga35DAAAAAAAUAouVyfD50rx5c2ncuLHMnDnTvqxOnTpy6623ytSpU3PdXvMP6BSDhIQEiYqKKsjvDAAAAAAApwty/i59V2pqqmzatEmefvrpTMu7du0q69aty3ablJQU83AMEOCSpNOJkupwbuBjAgJEgkPd3QoAAAD4uZDgQImMDHd3M7yDNZ8OHDhgzcjIuGK5LtPPfNmRI0e09IB17dq1mZa/+OKL1po1a2a7zYQJE8w2WR+6fp06dayTJ0+2xsXFmde2hxoxYoT9/Zw5c6zr16+3v+/du7dZR59ty/RzXc/2XrdXjvvV4+jxbO/ffPNN6549e+zvW7VqZbYZPHiwfdmiRYvMw/ZeP1O6rm2Z7kP3ZXufl5/pg/c/sM5750Nr9SrVzKN9qzbW31ZvMs+2Zfr5K8+/YH8/+M6BZh3be338+O0K62NDh9nfP/fkWOviz/5nf39jvYZmm1u697Ivm/XPN8zD9l4/03V0Xdsy3Yfuy/Zej6HHcjy2bqNtsr3XtvIzOfyeKle1Vq9U2TzWz/vK+vi9Q+zvn3/kMevSdz+yv7+xzg3W3Qu/s97aqYt92X8mTzUP23v9TNfRdW3LdB+6L9t7PYYey/ZeH7rNfbfebn//z6eesf7fq2/Z33do1sKso8+2Zfq5rmd7r9vrOo775Wfi98S1x78nviP4Luf/J/7P5e8I7/jbqH2L1tbz55K9uv80xwl9wrzI9xSDwMBAOXbsmJQpUybT8ri4OLMsPT1dfNXRo0elQoUKZrRAy5Yt7ctffPFF+fjjj2XXrl15GkFQqVIlphgwgsC3paZIxoE9IoFBIsEh7m4NAAAA/FVamgRlXJTops0kIDzC3a3xvSkGGk+wWCxXLD979qzJ6u/LSpUqZQIkx48fz7Q8NjZWYmJist0mNDTUPHClyBLkYPBVGcnnJeV4gASEhYuF6x8AAABuYk1JkYwL5zj/zg4QjBw50jxrcGD8+PESEfFX9EVHDaxfv14aNWokviwkJMSUNVy2bJncdttt9uX6/pZbbnFr2wAAAAAAKJQAgZb0s40g2L59u+ks2+jrhg0byujRo8XXaaBk0KBB0rRpUzPNYNasWabE4bBhw9zdNAAAAAAAXB8g+P77783z/fffL6+//rrflujr37+/ybcwefJkk4uhXr16snDhQqlSpYq7mwYAAAAAQIHlO0mhzR9//CF79+6Vtm3bSnh4eI65CZCZJimMjo4mSSF8PwfBto0SEFaEHAQAAABwew6C0AY3kaQwDwIkn06fPi2dOnWSmjVrSs+ePc1ddPXAAw/IqFGj8rs7AAAAAADgjQGCESNGSHBwsJl375ioUIfeL1682NntAwAAAAAAnljmcOnSpbJkyRKpWLFipuU1atSQAwcOOLNtAAAAAADAU0cQnDt3LtPIAZtTp05JKPXOAQAAAADwjwCBJiX86KOP7O81MWFGRoa88sor0qFDB2e3DwAAAAAAeOIUAw0EtG/fXn7++WdJTU2VMWPGyM6dO03ywrVr17qmlQAAAAAAwLNGENStW1e2bdsmzZo1ky5dupgpB/369ZMtW7ZI9erVXdNKAAAAAADgWSMIVNmyZWXSpEnObw0AAAAAAPCeAMGZM2dkw4YNEhsba/IPOBo8eLCz2gYAAAAAADw1QLBgwQK55557zNSCyMhIk6TQRl8TIAAAAAAAwA9yEIwaNUr+9re/SVJSkhlJEB8fb39ookIAAAAAAOAHAYIjR47I448/LhEREa5pEQAAAAAA8PwAQbdu3UyJQwAAAAAA4Mc5CHr16iVPPfWU/Prrr1K/fn0JDg7O9Hnfvn2d2T4AAAAAAFAILFar1ZqfDQICch50oEkK09PTndEun5WYmCjR0dGSkJAgUVFR7m4O4BIZyeclZdtGCQgrIpbQUM4yAAAA3MKakiIZF85JaIObJCCcafJOH0GQtawhAAAAAADwwxwEAAAAAADA9xAgAAAAAAAABAgAAAAAAAABAgAAAAAAQIAAAAAAAAAUOECwd+9eee6552TAgAESGxtrli1evFh27tzJWQUAAAAAwB8CBKtWrZL69evL+vXr5csvv5SzZ8+a5du2bZMJEya4oo0AAAAAAMDTAgRPP/20vPDCC7Js2TIJCQmxL+/QoYP8+OOPzm4fAAAAAADwxADB9u3b5bbbbrtieenSpSUuLs5Z7QIAAAAAAJ4cIChWrJgcO3bsiuVbtmyRChUqOKtdAAAAAADAkwMEAwcOlLFjx8rx48fFYrFIRkaGrF27VkaPHi2DBw92TSsBAAAAAIBnBQhefPFFqVy5shktoAkK69atK23btpVWrVqZygYAAAAAAMD7WKxWq7WgpQ51WoGOILjxxhulRo0azm+dD0pMTJTo6GhJSEiQqKgodzcHcImM5POSsm2jBIQVEUtoKGcZAAAAbmFNSZGMC+cktMFNEhAewW/BFWUOVfXq1eWOO+6Qu+66yynBgbS0NDN1QUsoFilSRMqXL2+mLBw9ejTTeikpKfLYY49JqVKlzHp9+/aVw4cP57r/GTNmSLVq1SQsLEyaNGkiq1evzvS5xkkmTpxojhseHi7t27eXnTt3OuXYAAAAAAD4XICgS5cuZoqBljvcsWOH0xpy/vx52bx5s4wfP948f/nll/L777+bTrijESNGyFdffSVz586VNWvWmGkOvXv3lvT09Bz3PW/ePLPds88+a0Y9tGnTRnr06CEHDx60rzNt2jSZPn26vPnmm7Jx40YpW7as+VmTkpKu6dgAAAAAAPjkFINTp06ZDvJnn30mP/74o9SrV0/uvfdek7ywYsWKTm2cdtSbNWsmBw4cMEEJHZav5RQ//vhj6d+/v1lHRxhUqlRJFi5cKN26dct2P82bN5fGjRvLzJkz7cvq1Kkjt956q0ydOtWMHtCRAxoA0FEMttECMTEx8vLLL8vDDz9c4GNnxRQD+AOmGAAAAMATMMXAxSMIdHj9o48+aioXaB4C7Sx/9NFHUrVqVenYsaM4k3bKtVKCllZUmzZtMlMRunbtal9HO/YapFi3bl22+0hNTTXbOW6j9L1tm3379pmqDI7rhIaGSrt27ezrFOTYtkCDBgUcHwAAAADgK6xpaaYj7pGPi6nuPj1eJehaNtY5/TrVoGHDhmZqgC0/gTNcuHDB7FtHJtiS+WknPiQkRIoXL55pXb3Tr5/lNOJBpwDoOjltY3vObh0dvVDQYysdoTBp0qR8/OQAAAAA4D3BgfTTJ8USFiaeyhIaLpbAQHc3w7cDBDqCYM6cOfLf//7XdOY1V8CUKVPyvL1uq0P3bRYtWmRyAyi9U3/33XebCgmaXDA3OkVARxpcTdbPs9smL+vk99jjxo2TkSNH2t/rCAKdlgAAAAAAXi8jwwQHQmo3EEuIZ1av0uCAp7bN6wMEzzzzjMk/oPPvO3fuLK+99pqZyx8Rkb+SERpQ0NwANhUqVLAHB7Qygg77X7FiRaZSgJo4UKcMxMfHZ7qTHxsbK61atcpxSkRgYOAVd/l1G9uIAd2v0nXKlSuX4zr5PbZtqoI+AAAAAMBXaQecMoJ+mINg5cqVMnr0aDly5Ih8++23ZgpAfoMDKjIyUq6//nr7Q0sL2oIDe/bskeXLl0vJkiUzbaPlCYODg2XZsmX2ZceOHTPVFHLqpOu0AN3OcRul723b6FQJDQA4rqPBAJ0yYVunIMcGAAAAAMBnRxBcLSHftbh48aLccccdpsThN998Y/IG2O76lyhRwnT0o6OjZejQoTJq1CgTPNDlGqyoX7++Gc2QEx3iP2jQIGnatKm0bNlSZs2aZUocDhs2zHyuUwS0goFOkahRo4Z56GsNfGgARBX02AAAAAAA+EyAYP78+dKjRw9zB11f5zZ1oCAOHz5s33ejRo0yffb9999L+/btzetXX31VgoKCzEiD5ORk6dSpk8yePdtMI7DRdbWqgi5XWmkhLi5OJk+ebO76a+UBLU1YpUoV+zZjxowx+xs+fLiZRqDTH5YuXWpGOtjk5dgAAAAAAHgji1Wz7OUiICDA3M0vU6aMeZ3jziwWc+ff3TQ4MHHiRBkyZIh4Gk1SqKMRtISjY34FwJdkJJ+XlG0bJSCsiFjIwQEAAOCztJRgxoVzEtrgJnIQ+MsIAq0mkN1rT7Rr1y5z13/w4MHubgoAAAAAAL6bg+Cjjz4yQ/azZubXpH5z5851e8e8du3asn37dre2AQAAAID3sKalmXJ9KMC5u5jKafO3KQaOdL69zuPX6QaOdI6/LvOEKQaejCkG8AdMMQAAAN4UHEg/fVIsYWHuborXsoSGS2jdhqbUIfxsBIHGEzTXQHZJBnVuPQAAAAB4jYwMExwIqd2ADm4BWQIDOXf+FiC48cYbTWBAH5q9X7P52+iogX379kn37t1d1U4AAAAAcBm9+x0QHsEZhl/Lc4Dg1ltvNc9bt26Vbt26SdGiRe2fhYSEmMoBt99+u2taCQAAAAAAPCNAMGHCBPOsgQBNUhjGHB0AAAAAAPw3B8F9993nmpYAAAAAAADvCRBovoFXX31VPv/8czl48KApb+jo9OnTzmwfAAAAAAAoBAH53WDSpEkyffp0ueuuuyQhIUFGjhwp/fr1k4CAAJk4caJrWgkAAAAAADxrBMGcOXPkP//5j/Tq1csECwYMGCDVq1eXBg0ayE8//SSPP/64a1oKAAAAIFvWtDRTrg/5Z72YeUQ04M/yHSA4fvy41K9f37zWSgY6ikD17t1bxo8f7/wWAgAAALhqcCD99EmxkES8wCyh4WIJDOQqg9/Ld4CgYsWKcuzYMalcubJcf/31snTpUmncuLFs3LhRQkND/f6EAgAAAIUqI8MEB0JqNxBLCH+PF4QGBzh3QAFGENx2223y3XffSfPmzeWJJ54wUwzee+89k7DwySef5JwCAAAAbqAd3IDwCM49gAKzWK1Wa8E3F5N3YN26dWY0Qd++fa9lV34hMTFRoqOjzdSMqKgodzcHcImM5POSsm2jBIQVEQsjiwAAcClrSopkXDgnoQ1uIkAAoHBHEGTVokUL8wAAAAAAAD4eIJg/f36ed8goAgAAAAAAfDRAcOutt+ZpZxaLRdLT06+1TQAAAAAAwBMDBBnUVAUAAAAAwKddcw4CAAAAwBmsaWmmZB/yed4upnLKALgnQDB58uSrfv78889fS3sAAADgp8GB9NMnxRIW5u6meCVLaLhYAgPd3QwA/hYg+OqrrzK9T0tLk3379klQUJBUr16dAAEAAADyLyPDBAdCajcQS0goZzCfNDjAeQNQ6AGCLVu2XLEsMTFRhgwZIrfddts1NwgAAAD+Szu5AeER7m4GAPilAGfsJCoqykw9GD9+vDN2BwAAAAAAvDFAoM6cOSMJCQnO2h0AAAAAAPDkKQb//ve/M723Wq1y7Ngx+fjjj6V79+7ObBsAAAAAAPDUAMGrr76a6X1AQICULl1a7rvvPhk3bpwz2wYAAAAAADw1QKAVCwAAAJB9qT7Nxo/8s15M5bQBgLcFCAAAAJB9cCD99ElTqg8FYwkNN+X6AABeEiC4cOGCvPHGG/L9999LbGysZGSJkm/evNkpDXv44Ydl1qxZZkrDiBEj7MtTUlJk9OjR8tlnn0lycrJ06tRJZsyYIRUrVrzq/nSdV155xeRLuOGGG+S1116TNm3aZMqlMGnSJHPM+Ph4ad68ubz11ltm3Ws9NgAA8AMZGSY4EFK7AfXoC0iDA1rmEADgJQGCv/3tb7Js2TK54447pFmzZmKxWJzeqP/973+yfv16KV++/BWfabBgwYIFMnfuXClZsqSMGjVKevfuLZs2bZLAHCLO8+bNM9tpZ75169byzjvvSI8ePeTXX3+VypUrm3WmTZsm06dPl9mzZ0vNmjXlhRdekC5dusju3bslMjKywMcGAAD+RTu4AeER7m4GAAD5ZrHqrfN8iI6OloULF5qOtiscOXLE3L1fsmSJ9OrVy3TKbSMItIyiJkTUign9+/c3y44ePSqVKlUyberWrVu2+9T9NW7cWGbOnGlfVqdOHbn11ltl6tSpZvSABiP0OGPHjrWPFoiJiZGXX37ZjGYo6LGzSkxMNOdQ9xcVFXXN5wvwRBnJ5yVl20YJCCsillDuBAHwD9aUFMm4cE5CG9xEgAAA4JUC8rtBhQoV7HfUnU2nKwwaNEieeuqpTEP7bfROfVpamnTt2tW+TDv29erVk3Xr1mW7z9TUVLOd4zZK39u20cSLx48fz7ROaGiotGvXzr5OQY5tCzRoUMDxAQAAAACA1wcI/vWvf5m77AcOHHB6Y/RufVBQkDz++OPZfq6d+JCQEClevHim5XqnXz/LzqlTpyQ9Pd2sk9M2tufc1snvsZWOUNARA7aHjjgAAAAAAMDrAwRNmzY1iQqvu+46M5KgRIkSmR55NWfOHClatKj9sWrVKnn99ddNDoD85jXQKQK5bZP18+y2ycs6+T32uHHjzHQC2+PQoUNX3R8AAAAAAF6RpHDAgAEmT8CUKVPM3fOCJins27evyQ1g83//93+mKoItaaDSO/+aCFArDuzfv1/Kli1rpgxolQHHO/m6XatWrbI9TqlSpUwCwax3+XUb24gB3a/SdcqVK5fjOvk9tm2qgj4AAPCWUn2ajR8FOHcXUzltAAD/ChDofPsff/xRGjZseE0H1tEHjrkMHnroIenTp0+mdTTxn+YkuP/++837Jk2aSHBwsKmicNddd5llWrZwx44dpgpBdnRagG6n29x222325fr+lltuMa+rVatmAgC67MYbbzTLNBigoxp02kNBjw0AgLcFB9JPnzSl+lAwltBwU6oPAAC/CBDUrl1bkpOTnd4QLRuoD0faIdeOe61atcx7ncM/dOhQM6pA19UpDaNHj5b69etL586dc9z3yJEjTaBBp0e0bNlSZs2aJQcPHpRhw4aZz3UUhFYw0FERNWrUMA99HRERIQMHDrymYwMA4DUyMkxwIKR2A2rRF5AGB7TMIQAAfhEgeOmll0wn+cUXXzSdY+3EO3J16b5XX33VJDLUu/gaqOjUqZPJW6DTCGzat28vVatWNcuVliWMi4uTyZMnm7v+WnlASxNWqVLFvs2YMWPM/oYPH26mEej0h6VLl2Ya5ZCXYwMA4O20gxsQHuHuZgAAgEJmsWqWvXwICAi4akI/zRvgbhocmDhxogwZMkQ8jZY51NEImrDQ1cEUwF0yks9LyraNEhBWRCzk4AC8hjUlRTIunJPQBjcRIAAAwA/lewTB999/L55s165d5q7/4MGD3d0UAAAAAAB8N0DQrl078WSaI2H79u3ubgYAAAAAAL4dIPjhhx+u+nnbtm2vpT0AAFwTyvRdw7mjTB8AAH4t3wECTQCYlWM+Ak/IQQAA8E+U6bt2lOkDAMB/5TtAoBn+HaWlpcmWLVtk/PjxprIBAABuQ5m+a0aZPgAA/Fe+AwSagT+rLl26SGhoqDz55JOyadMmZ7UNAIACoUwfAABA/l2qWegEpUuXlt27dztrdwAAAAAAwJNHEGzbti3Te6vVKseOHZOXXnpJGjZs6My2AQAAAAAATw0QNGrUyCQl1MCAoxYtWsj777/vzLYBAAAAAABPDRDs27cv0/uAgAAzvSAsLMyZ7QIAv0apvgKeN8r0AQAAFF6AoEqVKgU/GgAgV5TquzaU6QMAAHBxgGDFihXy6KOPyk8//SRRUVGZPktISJBWrVrJ22+/LW3atClgUwAABqX6rgll+gAAAFwcIHjttdfkwQcfvCI4YCt9+PDDD8v06dMJEACAk1CqDwAAAB5Z5vCXX36R7t275/h5165dZdOmTc5qFwAAAAAA8MQAwYkTJyQ4ODjHz4OCguTkyZPOahcAAAAAAPDEAEGFChVk+/btOX6+bds2KVeunLPaBQAAAAAAPDEHQc+ePeX555+XHj16XFHSMDk5WSZMmCC9e/d2RRsBeClKznHeAAAA4D0sVqvVmtcpBo0bN5bAwEBTzaBWrVpisVjkt99+k7feekvS09Nl8+bNEhMT4/pWe7HExEST1FErP2SX8BHwBdbUFEn59RexpiS7uyleS0v1hdZtaBIVAgAAAB4VIFAHDhyQRx55RJYsWSK2zTRI0K1bN5kxY4ZUrVrVlW31CQQI4E9BAmt6urub4bUo1QcAAACPDhDYxMfHyx9//GGCBDVq1JDixYu7pnU+iAABAAAAAMBnAgQoOAIEAAAAAACvTlII57DFYzRQAAAAAABAYYmMjDRpAnJCgKCQJSUlmedKlSoV9qEBAAAAAH4sIZdk+UwxKGQZGRly9OjRXCM3vk5HUGiQ5NChQ1RzANcf/AbffeD6gz/iuw9cf56DEQQeJiAgQCpWrOjuZngMjV5R7hFcf/A3fPeB6w/+iO8+cP15vgB3NwAAAAAAALgfAQIAAAAAAECAAO4RGhoqEyZMMM8A1x/8Bd994PqDP+K7D1x/3oMkhQAAAAAAgBEEAAAAAACAAAEAAAAAACBAAAAAAAAACBAAAAAAAACDMocAAAAAAIAAAVzDarVyauEWXHsAAAD+gb/7nI8RBHC62NhYSUpKsr/nHy4KS0JCgqSnp3PtwS3++OMPWbZsGWcfhe7333+XYcOGyerVqzn7KHSHDh2STZs2ydGjRzn7KFT0OVyDAAGc5uLFizJ06FBp1qyZdO7cWe655x45deqUWCwWzjJcKi0tTf7+979Lz549zeMf//iHCRRw7aGwbNu2TWrWrCkDBgyQAwcOcOJRKDIyMuTJJ5+URo0ayblz5zIF54HC+L/34YcflsaNG8vf/vY3adiwoaxdu5YTD5ejz+FaBAjgtH+oQ4YMkV9//VU+/PBD80ey/sHcr18/+e233zjLcBm9Y1u3bl3ZuXOnPPXUU1KpUiWZM2eOTJw40XzOCBYUhtTUVOnWrZsEBwfLtGnTOOkoFIsWLZKNGzea548//tgESG347oMrnT17Vu644w7Zs2ePLF26VD7//HMTKBg/fjzXH1yKPofrESCAUxw7dkw2bNhg7uK2a9fO3NHQjtuff/4pM2fOlBMnTnCm4XSJiYnmjxLtmOn1duutt5rr7e677zZ/NJ8/f55RBCgUmzdvluLFi5vg1KxZs8z3IeBq7777rhk9oP/vrlq1ynTOZs+eLQcPHuS7Dy6lN4T0BpBeczfeeKPUqlVL7rzzTomMjDQjWxjBB1ehz+F6BAjgFHFxcXL48GFp0aKFeZ+SkiJly5aVcePGmcjyDz/8wJmG0+k0gptvvlkeeOABc+dW75iFhITIhQsXJDk5WSIiIriLBpdxvEMbGhoqVapUkY4dO8pNN90kkyZNsgexAFdcezqdQKfxderUSV544QUTGN2+fbs8//zz5jpcsGABJx4unV6geVf0u0/ptfjWW29J+fLl5f333zf/BwOuQJ/D9QgQIN/07th//vOfTJ3+GjVqmIDAJ598cunCCrh0aemIAo0m6/BHDRoAzrj29E6Z0ju2gwcPNnfQlN61sCUrvO6668xr7mLAVdefXlu2a05HEOiQW6WjCBYvXiw9evQwo1t27drFLwFOv/b0/1btpOkoAk1S+OWXX8p///tfkwOjevXqppPGtQdX/d3XunVrad++vdx///3muy4mJsb8HahBer05dN9995mAFXAtdMreM888Y0aH2uhoFb3e6HO4kBXIo08//dRapkwZa8uWLa2NGjWyli5d2vriiy+azxISEqxjxoyx1qxZ03rixAmzLDk52Tx/+OGH1mLFitnfA8689i5evGhfLyMjwzw3b97c+u6772ZaBjjz+psyZYr5LCUlxTzffffd1uXLl5vX//nPf6zh4eHW4OBg63//+19OPFxy7an33nvParFYzP+9sbGx9uU//PCDtVy5ctZ169Zx9uGS/3vV2bNnrXv27LG2atXK+s9//tO+fMuWLdbrrrvO+vnnn3P2USBz5swx15tee3feeaf5P/Whhx4ynyUmJtLncDECBMjzP9SGDRta3377bfP+yJEj1jfffNNapEgRExxQy5Yts950003W4cOHZ+qYff/99+Y/mF9++YWzDadee/qfRFb79u0z/6ns2rXLvmzv3r3mOT09nd8AXHL93XfffdZBgwaZ70C9/v7xj39YixcvnumPZsDZ/+/u2LHD2r59e2vdunWtx44ds2+rAfmiRYta/+///o+TDpd+923evNlaq1YtE6Cy/d2ngXu+/1BQGmTXgNQ777xj3qelpZnrUYMESUlJZhl9DtdiigFyG2FinnUYY/Pmzc1wbqVzzHRYd4UKFUyiGqVzwQcOHGiqGHz11VdmG6UlbzTLfP369TnbcOq1l12FDB3arZUMdAjali1bzLaaG0Oz3tqmvgDOvP50rq3mGli4cKEp86rX3XPPPSdjx441lTX279/PCYdLvvtq164tI0aMkL1798rbb78tR44cMcvnz59v/s9t27YtZx4u/b9Xc/1oJYNDhw7Zp/Rp/otq1aqZXBhAXtmm7Ol1N3r0aFMdTQUFBcnp06flwQcflLCwMLOsVatWps+hSVnpc7iAiwMQ8FKbNm2yxsfH29+fOXMm01ButXXrVmvZsmWtp0+fti+zDfuJjIy0tmvXzj4s6K233jKfM9wbrrr2bNfWY489Zr3jjjusTz75pDUgIMA6dOhQ64ULFzjxcOl334YNG6w7d+7MtJ5ed9OmTWPkClx67al///vf1vLly5s7ubfddpu5y+s4FBxw1fUXFxdnHTBggDUiIsI6bNgw6+DBg83fgM8//zx/8yHP3316vTly7C88/fTT1qCgIGvt2rXNyIKPP/7YjCpITU21jh49mj6HCwS5IugA7/XFF1+YuxGalVajx5pkRhMNajIQW3TPdhd2xYoVJhGSJorTGuCamEaTJr388ssmi/eOHTtMeUNN3qV3ORQJ4+Cqa8+WME7vnGmJLy37pQmSdPQK4KrvPk2+qtvod15WulxHEACu/H9XPfbYY2b0iv5/q3dyX3rpJalZsyYnHi6//kqUKCHvvfeeGbkXGxtr1vv555+5/pDva09HDDzyyCMm2aXt2ps7d6789NNP8tlnn5nlWhlN14mOjpY+ffrIK6+8YkYc0OdwMldEHeCdNm7caKJzr732mskXMGPGDDOX9pFHHjERYtscbo3aKb1L8fe//93NrYYvcNa1pxHoqVOnWpcsWVLoPwO8F9994NqDP3L2d59tPcAZ157SfCtZr6vKlStbJ02axEl2ISbkwj7fTCO+WqZLS9Y0aNDAROgmTJhg5tPOmDHDrKNRZH3oNtu2bTOlbZSWWBowYIC5cwG469rTiPLTTz8tXbt25ZeAQr/+AHd99wGecP3pXHHAWdee0pHJtutKt9WcPpr3olSpUpxoFyJAAPuw/3379pkhYY5f8Drcp0mTJrJo0SLZuXPnpYsmIEA2btxo/oE2btzYDA/Sf9xxcXFSpkwZzijccu2VLl2aM4984bsP7sK1B3fi+oO3XHuOU5PPnDljphTo9JdbbrnFDa33HwQI/NCyZcvk8ccfl9dff102bNhgX966dWtZt26dHD9+3LxPT0+XIkWKmH+E+g9U5/3YaLZune+jmeJ1f1qpQD/XeUSAO649W2ZbgO8+eBr+3wXXH/zRtX73aa6LefPmmXw+N9xwg2zatElmzpxpqmnAdQgQ+JFjx46ZhB733nuvKReiSWV0KLbtH6y+rlq1qkky6Bi169Kli7lz+8cff9j3FRwcbIb3aHkRjfJpxA/g2oMn4rsPXHvwR3z3wduvPZ1WoDeAdu/ebYIMmrCwTp06/GJdzZUJDuA5zp07Z73vvvus/fv3t/7555/25TfddJN1yJAh5rWWs/noo49Mabi1a9dm2v6ee+6xtm/f3v4+Nja2EFsPb8a1B64/+CO++8D1B3/k7O8+TZSJwsUIAj+hc7Z1+L/O76lWrZpcvHjRLO/du7f89ttv5nVgYKDcddddZnjPAw88IKtWrTKROx3+s2fPHhMFtGG+N7j24A347gPXHvwR333wlWvPVmYThceiUYJCPB7cSGuM6tQApb92Hc4zaNAgCQ8Pl1mzZtmXXbhwwWSp/fXXX6VRo0ZmvnflypXl888/N3VuAa49eBO++8C1B3/Edx+49lAQBAj8XNu2beVvf/ubifJpgCAjI8NE9U6cOGHK2WjGeJ0jNHDgQHc3FT6Gaw9cf/BHfPeB6w/+iO8+70GAwI/9+eef0qpVK/n222/tSQY1W2hISIi7mwYfx7UHrj/4I777wPUHf8R3n3dhUocfss0qWbNmjRQtWtQeHJg0aZI88cQTEhsb6+YWwldx7YHrD/6I7z5w/cEf8d3nnYLc3QAUPlspES01cvvtt5sapQ899JCcP39ePv74YylTpgy/FnDtwefw3QeuPfgjvvvAtYf8YIqBn9JEhPXr15e9e/eaKQU6emDs2LHubhb8ANceuP7gj/juA9cf/BHffd6HAIEf69Kli9SoUUOmT58uYWFh7m4O/AjXHrj+4I/47gPXH/wR333ehQCBH0tPTzcVCwCuPfgTvvvAtQd/xHcfuPaQFwQIAAAAAAAAVQwAAAAAAAABAgAAAAAAQIAAAAAAAAAQIAAAAAAAAEbApScAAAAAAODPCBAAAAAAAAACBAAAAAAAgAABAAAAAAAgQAAAAAAAAAgQAAAAAAAAUf8PADmeM/wp53YAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ci_growth = CostIncome(\n", + " mkt_price_year=2020,\n", + " init_cost=50_000,\n", + " periodic_cost=5_000,\n", + " periodic_income=8_000,\n", + " cost_yearly_growth_rate=0.02, # costs grow 2% / year\n", + " income_yearly_growth_rate=0.03, # income grows 3% / year\n", + " freq=\"Y\",\n", + ")\n", + "\n", + "ci_growth.plot_cash_flows(impl_date, start_date, end_date)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "067c07aa", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " No growth With growth\n", + "Net 36000 -19821\n", + "Cost -60000 -97569\n", + "Income 96000 77748\n" + ] + } + ], + "source": [ + "# Compare totals with and without growth\n", + "no_growth = ci.calc_total(impl_date, start_date, end_date)\n", + "with_growth = ci_growth.calc_total(impl_date, start_date, end_date)\n", + "\n", + "labels = [\"Net\", \"Cost\", \"Income\"]\n", + "print(f\"{'':15s} {'No growth':>12s} {'With growth':>12s}\")\n", + "for label, ng, wg in zip(labels, no_growth, with_growth):\n", + " print(f\"{label:15s} {ng:>12.0f} {wg:>12.0f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "1408d2db", + "metadata": {}, + "source": [ + "## Custom cash flows\n", + "\n", + "For irregular or one-off flows, pass a `pd.DataFrame` with columns `date`, `cost`, and/or `income`.\n", + "\n", + "These are **added on top** of any periodic amounts; dates not present in the DataFrame simply contribute zero." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c469839c-e2e2-43e9-ac33-93de7f04a6d9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
datenetcostincome
020200.00.00.0
120210.00.00.0
22022-50000.0-50000.00.0
320233000.0-5000.08000.0
42024-7000.0-15000.08000.0
520253000.0-5000.08000.0
6202618000.0-5000.023000.0
720273000.0-5000.08000.0
82028-17000.0-25000.08000.0
920293000.0-5000.08000.0
1020303000.0-5000.08000.0
\n", + "
" + ], + "text/plain": [ + " date net cost income\n", + "0 2020 0.0 0.0 0.0\n", + "1 2021 0.0 0.0 0.0\n", + "2 2022 -50000.0 -50000.0 0.0\n", + "3 2023 3000.0 -5000.0 8000.0\n", + "4 2024 -7000.0 -15000.0 8000.0\n", + "5 2025 3000.0 -5000.0 8000.0\n", + "6 2026 18000.0 -5000.0 23000.0\n", + "7 2027 3000.0 -5000.0 8000.0\n", + "8 2028 -17000.0 -25000.0 8000.0\n", + "9 2029 3000.0 -5000.0 8000.0\n", + "10 2030 3000.0 -5000.0 8000.0" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "custom_flows = pd.DataFrame(\n", + " {\n", + " \"date\": [\"2024-01-01\", \"2026-01-01\", \"2028-01-01\"],\n", + " \"cost\": [10_000, 0, 20_000], # extra one-off costs\n", + " \"income\": [0, 15_000, 0], # extra one-off income\n", + " }\n", + ")\n", + "\n", + "ci_custom = CostIncome(\n", + " mkt_price_year=2020,\n", + " init_cost=50_000,\n", + " periodic_cost=5_000,\n", + " periodic_income=8_000,\n", + " custom_cash_flows=custom_flows,\n", + " freq=\"Y\",\n", + ")\n", + "ci_custom.to_dataframe(impl_date, start_date, end_date)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "11f21296", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(, )" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ci_custom.plot_cash_flows(impl_date, start_date, end_date)" + ] + }, + { + "cell_type": "markdown", + "id": "f1f011b6", + "metadata": {}, + "source": [ + "## Sub-annual frequencies\n", + "\n", + "`freq` accepts any pandas-compatible period string. Common options:\n", + "\n", + "| `freq` | Meaning |\n", + "|---|---|\n", + "| `\"Y\"` | Annual |\n", + "| `\"Q\"` | Quarterly |\n", + "| `\"M\"` | Monthly |\n", + "| `\"7D\"` | Every 7 days |\n", + "\n", + "```{note}\n", + "The periodic amounts are interpreted as **per-period** values, not annualised.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "277e6948", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
datenetcostincome
02022-01-5000.0-5000.00.0
12022-02200.0-500.0700.0
22022-03200.0-500.0700.0
32022-04200.0-500.0700.0
42022-05200.0-500.0700.0
52022-06200.0-500.0700.0
62022-07200.0-500.0700.0
72022-08200.0-500.0700.0
82022-09200.0-500.0700.0
92022-10200.0-500.0700.0
102022-11200.0-500.0700.0
112022-12200.0-500.0700.0
\n", + "
" + ], + "text/plain": [ + " date net cost income\n", + "0 2022-01 -5000.0 -5000.0 0.0\n", + "1 2022-02 200.0 -500.0 700.0\n", + "2 2022-03 200.0 -500.0 700.0\n", + "3 2022-04 200.0 -500.0 700.0\n", + "4 2022-05 200.0 -500.0 700.0\n", + "5 2022-06 200.0 -500.0 700.0\n", + "6 2022-07 200.0 -500.0 700.0\n", + "7 2022-08 200.0 -500.0 700.0\n", + "8 2022-09 200.0 -500.0 700.0\n", + "9 2022-10 200.0 -500.0 700.0\n", + "10 2022-11 200.0 -500.0 700.0\n", + "11 2022-12 200.0 -500.0 700.0" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ci_monthly = CostIncome(\n", + " mkt_price_year=2022,\n", + " init_cost=5_000,\n", + " periodic_cost=500,\n", + " periodic_income=700,\n", + " freq=\"M\",\n", + ")\n", + "\n", + "df_monthly = ci_monthly.to_dataframe(\n", + " impl_date=\"2022-01-01\",\n", + " start_date=\"2022-01-01\",\n", + " end_date=\"2022-12-01\",\n", + ")\n", + "df_monthly" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "03a80747", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(, )" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ci_monthly.plot_cash_flows(\n", + " \"2022-01-01\",\n", + " \"2022-01-01\",\n", + " \"2022-12-01\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "10191dfd", + "metadata": {}, + "source": [ + "## Combining multiple CostIncome objects\n", + "\n", + "`CostIncome.comb_cost_income()` aggregates a list of `CostIncome` objects into a single one by summing costs and incomes.\n", + "\n", + "```{warning}\n", + "All objects must share the same `mkt_price_year`, `cost_growth_rate`, and `income_growth_rate`.\n", + "```\n", + "\n", + "```{note}\n", + "`custom_cash_flows` are **not** carried over to the combined object. Merge custom flows manually beforehand if needed.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "666fd82b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CostIncome(\n", + " mkt_price_year = 2020\n", + " freq = 'Y'\n", + " init_cost = -50,000.00\n", + " periodic_cost = -5,000.00\n", + " periodic_income = 8,000.00\n", + " cost_yearly_growth_rate = 0.00%\n", + " income_yearly_growth_rate = 0.00%\n", + " custom_cash_flows = None\n", + ")\n" + ] + } + ], + "source": [ + "ci_a = CostIncome(\n", + " mkt_price_year=2020,\n", + " init_cost=30_000,\n", + " periodic_cost=2_000,\n", + " periodic_income=4_000,\n", + " freq=\"Y\",\n", + ")\n", + "\n", + "ci_b = CostIncome(\n", + " mkt_price_year=2020,\n", + " init_cost=20_000,\n", + " periodic_cost=3_000,\n", + " periodic_income=4_000,\n", + " freq=\"Y\",\n", + ")\n", + "\n", + "ci_combined = CostIncome.comb_cost_income([ci_a, ci_b])\n", + "\n", + "print(ci_combined)" + ] + }, + { + "cell_type": "markdown", + "id": "8a15baaf", + "metadata": {}, + "source": [ + "## Loading from dict / YAML\n", + "\n", + "### From a Python dictionary" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "eeac2088", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CostIncome(\n", + " mkt_price_year = 2020\n", + " freq = 'Y'\n", + " init_cost = -50,000.00\n", + " periodic_cost = -5,000.00\n", + " periodic_income = 8,000.00\n", + " cost_yearly_growth_rate = 2.00%\n", + " income_yearly_growth_rate = 3.00%\n", + " custom_cash_flows = None\n", + ")\n" + ] + } + ], + "source": [ + "config_dict = {\n", + " \"mkt_price_year\": 2020,\n", + " \"init_cost\": 50_000,\n", + " \"periodic_cost\": 5_000,\n", + " \"periodic_income\": 8_000,\n", + " \"cost_yearly_growth_rate\": 0.02,\n", + " \"income_yearly_growth_rate\": 0.03,\n", + " \"freq\": \"Y\",\n", + "}\n", + "\n", + "ci_from_dict = CostIncome.from_dict(config_dict)\n", + "print(ci_from_dict)" + ] + }, + { + "cell_type": "markdown", + "id": "0b2a868e", + "metadata": {}, + "source": [ + "### From a YAML file\n", + "\n", + "Create a YAML file structured as follows, then load it with `CostIncome.from_yaml`.\n", + "\n", + "```yaml\n", + "# measure_cost.yaml\n", + "cost_income:\n", + " mkt_price_year: 2020\n", + " init_cost: 50000\n", + " periodic_cost: 5000\n", + " periodic_income: 8000\n", + " cost_yearly_growth_rate: 0.02\n", + " income_yearly_growth_rate: 0.03\n", + " freq: \"Y\"\n", + " # Optional custom flows:\n", + " # custom_cash_flows:\n", + " # - date: \"2024-01-01\"\n", + " # cost: 10000\n", + " # income: 0\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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/doc/user-guide/climada_engine_CostBenefit.ipynb b/doc/user-guide/climada_engine_CostBenefit.ipynb index de98c79260..a86fee8e3e 100644 --- a/doc/user-guide/climada_engine_CostBenefit.ipynb +++ b/doc/user-guide/climada_engine_CostBenefit.ipynb @@ -7,6 +7,15 @@ "# END-TO-END COST BENEFIT CALCULATION" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{attention}\n", + "Adapation measures and cost-benefit evaluation are being completely revamped. Associated tutorials will be under their own menu \"Adaptation appraisal guides\"\n", + "```" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -1286,9 +1295,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python [conda env:climada_env_dev]", "language": "python", - "name": "python3" + "name": "conda-env-climada_env_dev-py" }, "language_info": { "codemirror_mode": { @@ -1300,7 +1309,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.11.15" }, "latex_envs": { "LaTeX_envs_menu_present": true, diff --git a/doc/user-guide/climada_entity_Exposures.ipynb b/doc/user-guide/climada_entity_Exposures.ipynb index aa1b39fd38..90a0c81ebe 100644 --- a/doc/user-guide/climada_entity_Exposures.ipynb +++ b/doc/user-guide/climada_entity_Exposures.ipynb @@ -4,6 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "(exposure-tutorial)=\n", "# Exposures class" ] }, diff --git a/doc/user-guide/climada_entity_ImpactFuncSet.ipynb b/doc/user-guide/climada_entity_ImpactFuncSet.ipynb index fd349487cd..ad1841a750 100644 --- a/doc/user-guide/climada_entity_ImpactFuncSet.ipynb +++ b/doc/user-guide/climada_entity_ImpactFuncSet.ipynb @@ -4,6 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "(impact-functions-tutorial)=\n", "# Impact Functions" ] }, diff --git a/doc/user-guide/climada_entity_MeasureSet.ipynb b/doc/user-guide/climada_entity_MeasureSet.ipynb index 0af0b37d70..17a07036cb 100644 --- a/doc/user-guide/climada_entity_MeasureSet.ipynb +++ b/doc/user-guide/climada_entity_MeasureSet.ipynb @@ -6,7 +6,11 @@ "source": [ "# Adaptation Measures\n", "\n", - "Adaptation measures are defined by parameters that alter the exposures, hazard or impact functions. Risk transfer options are also considered. Single measures are defined in the `Measure` class, which can be aggregated to a `MeasureSet`." + "Adaptation measures are defined by parameters that alter the exposures, hazard or impact functions. Risk transfer options are also considered. Single measures are defined in the `Measure` class, which can be aggregated to a `MeasureSet`.\n", + "\n", + "```{attention}\n", + "Adapation measures and cost-benefit evaluation are being completely revamped. Associated tutorials will be under their own menu \"Adaptation appraisal guides\".\n", + "```" ] }, { diff --git a/doc/user-guide/climada_hazard_Hazard.ipynb b/doc/user-guide/climada_hazard_Hazard.ipynb index 412346d041..0b6bd40373 100644 --- a/doc/user-guide/climada_hazard_Hazard.ipynb +++ b/doc/user-guide/climada_hazard_Hazard.ipynb @@ -4,6 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "(hazard-tutorial)=\n", "# Hazard class\n", "\n", "## What is a hazard?\n", diff --git a/doc/user-guide/climada_measure_config.ipynb b/doc/user-guide/climada_measure_config.ipynb new file mode 100644 index 0000000000..8fa4739bac --- /dev/null +++ b/doc/user-guide/climada_measure_config.ipynb @@ -0,0 +1,570 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "659605a5-d601-47d3-89f7-b606e3e39c93", + "metadata": {}, + "source": [ + "(measure-config-tutorial)=\n", + "\n", + "# Defining Adaptation Measures with configurations" + ] + }, + { + "cell_type": "markdown", + "id": "c68c6cf1-d0a1-40ae-ae45-838741988ac6", + "metadata": {}, + "source": [ + "## Introduction\n", + "\n", + "CLIMADA uses `Measure` objects to model the effects of adaptation measures. `Measure` objects were formerly defined declaratively (via for instance, a shifting or scaling of the hazard intensity or a change of impact function), and are now defined as python functions to enable more flexibility on the possible changes (see the [tutorial on measure objects](measure-tutorial)'). \n", + "\n", + "The caveat of defining measure effects as python functions is that it cannot be serialized (written to a file), and also makes reading from a file a challenge.\n", + "\n", + "In order to retain close that gap, the `measure` module now ships `MeasureConfig` objects, which handle the reading, writing and \"declarative\" defining of `Measure` objects.\n", + "\n", + "`Measure` objects can be instantiated from `MeasureConfig` objects using `Measure.from_config()`.\n", + "\n", + "### Summary of `Measure` vs `MeasureConfig`\n", + "\n", + "| `Measure` | `MeasureConfig` |\n", + "|-----------|--------------------|\n", + "| Is used for the actual computation | Is transformed into a `Measure` for actual computation |\n", + "| Uses python function to define what change to apply to the `Exposures`, `ImpactFuncSet`, `Hazard` objects | Define the changes (functions) to apply via the former way (scaling/shifting effect, alternate file loading, etc.) |\n", + "| Accepts any possible effect as long as it can be defined as a python function | Is restricted to a set of defined effects |\n", + "| Cannot be written to a file (unless it was created by a `MeasureConfig`) | Can easily be read from/written to a file (`.xlsx` or `.yaml`) |" + ] + }, + { + "cell_type": "markdown", + "id": "6d786faa-5b8c-4ee6-83cd-5fdafc1b2c29", + "metadata": {}, + "source": [ + "### Configuration classes\n", + "\n", + "The definition of measures via `MeasureConfig` is organized into a hierarchy of specialized classes:\n", + "\n", + "- `MeasureConfig`: The top-level container for a single measure.\n", + "- `HazardModifierConfig`: Defines how the hazard is changed (e.g., shifting intensity).\n", + "- `ImpfsetModifierConfig`: Adjusts impact functions (e.g., scaling vulnerability curves).\n", + "- `ExposuresModifierConfig`: Modifies exposure data (e.g., reassigning IDs or zeroing regions).\n", + "- `CostIncomeConfig`: Handles the financial aspects, including initial costs and recurring income.\n", + "\n", + "Note that everything can be defined and accessed directly from the `MeasureConfig` container, the underlying ones are there to keep things organized.\n", + "\n", + "In the following we present each of these subclasses and the possibilities they offer." + ] + }, + { + "cell_type": "markdown", + "id": "4887d2a6-8295-4fda-8442-cbcbd3b16fea", + "metadata": {}, + "source": [ + "## Quickstart" + ] + }, + { + "cell_type": "markdown", + "id": "5c40640d-50a4-4102-8e45-0dc8b9a770f2", + "metadata": {}, + "source": [ + "You can directly define a `MeasureConfig` object with a dictionary, using `MeasureConfig.from_dict()`.\n", + "\n", + "Below are the possible parameters:\n", + "\n", + "| Scope | Parameter | Type | Description |\n", + "| :--- | :--- | :--- | :--- |\n", + "| **Top-Level** | `name` (required) | `str` | Unique identifier for the measure. |\n", + "| | `haz_type` (required) | `str` | The hazard type this measure targets (e.g., \"TC\", \"FL\"). |\n", + "| | `implementation_duration` | `str` | Pandas offset alias (e.g., \"2Y\") for implementation time. |\n", + "| | `color_rgb` | `tuple` | RGB triple (0-1 range) for plotting and visualization. |\n", + "| **Hazard** | `haz_int_mult` | `float` | Multiplier for hazard intensity (default: 1.0). |\n", + "| | `haz_int_add` | `float` | Additive offset for hazard intensity (default: 0.0). |\n", + "| | `new_hazard_path` | | Path to an HDF5 file to replace the current hazard. |\n", + "| | `impact_rp_cutoff` | `float` | Return period (years) threshold; events below this are ignored. |\n", + "| **Impact Function**| `impf_ids` | `list` | Specific impact function IDs to modify (None = all). |\n", + "| | `impf_mdd_mult` / `_add` | `float` | Scale or shift the Mean Damage Degree curve. |\n", + "| | `impf_paa_mult` / `_add` | `float` | Scale or shift the Percentage of Assets Affected curve. |\n", + "| | `impf_int_mult` / `_add` | `float` | Scale or shift the intensity axis of the function. |\n", + "| | `new_impfset_path` | | Path to an Excel file to replace the impact function set. |\n", + "| **Exposures** | `reassign_impf_id` | `dict` | Mapping `{haz_type: {old_id: new_id}}` for reclassification. |\n", + "| | `set_to_zero` | `list` | List of Region IDs where exposure value is set to 0. |\n", + "| | `new_exposures_path` | | Path to an HDF5 file to replace the current exposures. |\n", + "| **Cost & Income** | `init_cost` | `float` | One-time investment cost (absolute value). |\n", + "| | `periodic_cost` | `float` | Recurring maintenance/operational costs. |\n", + "| | `periodic_income` | `float` | Recurring income generated by the measure. |\n", + "| | `mkt_price_year` | `int` | Reference year for pricing (default: current year). |\n", + "| | `freq` | `str` | Frequency of cash flows (e.g., \"Y\" for yearly). |\n", + "| | `custom_cash_flows` | `list[dict]`| Explicit list of dates and values for complex cash flows. (See the [cost income tutorial](cost-income-tutorial)) |" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "62cf6502-7765-452c-be32-eb49a363b4a8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MeasureConfig(\n", + "\tname='Tutorial measure'\n", + "\thaz_type='TC'\n", + "\timpfset_modifier=ImpfsetModifierConfig(\n", + "\t\tNon default fields:\n", + "\t\t\timpf_ids=[1, 2]\n", + "\t\t\timpf_mdd_mult=0.8\n", + ")\n", + "\thazard_modifier=HazardModifierConfig(\n", + "\t\tNon default fields:\n", + "\t\t\tnew_hazard_path='path/to/new_hazard.h5'\n", + ")\n", + "\texposures_modifier=ExposuresModifierConfig(\n", + "\t\tNon default fields:\n", + "\t\t\treassign_impf_id={'TC': {1: 2}}\n", + ")\n", + "\tcost_income=CostIncomeConfig(\n", + "\t\tNon default fields:\n", + "\t\t\tinit_cost=10000\n", + "\t\t\tperiodic_cost=500\n", + ")\n", + "\timplementation_duration=None\n", + "\tcolor_rgb=(0.1, 0.5, 0.3))\n" + ] + } + ], + "source": [ + "from climada.entity.measures.measure_config import MeasureConfig\n", + "\n", + "measure_dict = {\n", + " \"name\": \"Tutorial measure\",\n", + " \"haz_type\": \"TC\",\n", + " \"impf_ids\": [1, 2],\n", + " \"impf_mdd_mult\": 0.8,\n", + " \"new_hazard_path\": \"path/to/new_hazard.h5\",\n", + " \"reassign_impf_id\": {\"TC\": {1: 2}},\n", + " \"color_rgb\": [0.1, 0.5, 0.3],\n", + " \"init_cost\": 10000,\n", + " \"periodic_cost\": 500,\n", + "}\n", + "\n", + "meas_config = MeasureConfig.from_dict(measure_dict)\n", + "\n", + "print(meas_config)" + ] + }, + { + "cell_type": "markdown", + "id": "ac98393f-575f-4580-ac4a-dae578638916", + "metadata": {}, + "source": [ + "## Modifying Impact Functions: `ImpfsetModifierConfig`\n", + "\n", + "The `ImpfsetModifierConfig` is used to define how an adaptation measure changes the vulnerability (refer to the [impact functions tutorial](impact-functions-tutorial)).\n", + "\n", + "When \"translated\" to a `Measure` object the `ImpfsetModifierConfig` populates the `impfset_change` attribute with a function that takes an `ImpactFuncSet` and returns a modified one, according to the specifications.\n", + "\n", + "```{note}\n", + "Modifications are always applied to a specific hazard type (`haz_type` parameter).\n", + "```\n", + "\n", + "`ImpfsetModifierConfig` allows you to modify the main components of an impact function set, as well as to replace it entirely:\n", + "\n", + "- The MDD (Mean Damage Degree) array: via `impf_mdd_mult` to scale it and `impf_mdd_add` to shift it.\n", + "- The PAA (Percentage of Assets Affected) array: via `impf_paa_mult` to scale it and `impf_paa_add` to shift it.\n", + "- The intensity array: via `impf_int_mult` to scale it and `impf_int_add` to shift it.\n", + "- Replacing the set: via providing the `new_impfset_path` parameter. It needs to be a valid `.xlsx` file readable by `ImpactFuncSet.from_excel()`\n", + "\n", + "See below for code examples.\n", + "\n", + "```{warning}\n", + "If you provide a new_impfset_path and other modifiers, CLIMADA will load the new file first and then apply the modifiers to it. (A warning will be issued to ensure this sequence is intended).\n", + "```\n", + "\n", + "```{note}\n", + "By default the changes are applied to all the impact functions in the set, but you can provide the `impf_ids` parameter to apply the changes to a selection of impact function ids.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5ffb447b-1b8f-4e40-9d1c-7db33a11255e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- Scaling Config ---\n", + "ImpfsetModifierConfig(\n", + "\t\tNon default fields:\n", + "\t\t\timpf_ids=[1, 2]\n", + "\t\t\timpf_mdd_mult=0.8\n", + "\t\t\timpf_int_add=5.0\n", + ")\n", + "\n", + "--- Replacement Config ---\n", + "ImpfsetModifierConfig(\n", + "\t\tNon default fields:\n", + "\t\t\tnew_impfset_path='path/to/new_impact_functions.xlsx'\n", + ")\n" + ] + } + ], + "source": [ + "from climada.entity.measures.measure_config import ImpfsetModifierConfig\n", + "\n", + "# 1. Scaling existing Impact Functions\n", + "# Let's say we want to simulate a 20% reduction in MDD\n", + "# and a slight shift in the intensity threshold for Hazard 'TC'.\n", + "impf_mod_scaling = ImpfsetModifierConfig(\n", + " haz_type=\"TC\",\n", + " impf_ids=[1, 2], # Apply only to specific function IDs\n", + " impf_mdd_mult=0.8, # Reduce Mean Damage Degree by 20%\n", + " impf_int_add=5.0, # Shift intensity axis by 5 units (e.g., higher resistance)\n", + ")\n", + "\n", + "print(\"--- Scaling Config ---\")\n", + "print(impf_mod_scaling)\n", + "\n", + "# 2. Replacing the Impact Function Set from a file\n", + "# Useful for measures that implement completely new building standards.\n", + "impf_mod_replace = ImpfsetModifierConfig(\n", + " haz_type=\"TC\", new_impfset_path=\"path/to/new_impact_functions.xlsx\"\n", + ")\n", + "\n", + "print(\"\\n--- Replacement Config ---\")\n", + "print(impf_mod_replace)" + ] + }, + { + "cell_type": "markdown", + "id": "234ebc89-83b0-42b1-8b97-734016306b84", + "metadata": {}, + "source": [ + "## Modifying Hazards: `HazardModifierConfig`\n", + "\n", + "The `HazardModifierConfig` is used to define how an adaptation measure changes the hazard (refer to the [hazard tutorial](hazard-tutorial)).\n", + "\n", + "When \"translated\" to a `Measure` object the `HazardModifierConfig` populates the `hazard_change` attribute with a function that takes a `Hazard` (possibly additional arguments, see below) and returns a modified one, according to the specifications.\n", + "\n", + "```{note}\n", + "Modifications are always applied to a specific hazard type (`haz_type` parameter).\n", + "```\n", + "\n", + "`HazardModifierConfig` allows you to modify the intensity and frequency of the hazard, to apply a cutoff on the return period of impacts, as well as to replace it entirely:\n", + "\n", + "- The intensity matrix: via `haz_int_mult` to scale it and `haz_int_add` to shift it.\n", + "- The frequency array: via `haz_freq_mult` to scale it and `haz_freq_add` to shift it.\n", + "- Replacing the hazard: via providing the `new_hazard_path` parameter. It needs to be a valid hazard HDF5 file readable by `Hazard.from_hdf5()`\n", + "- Applying a cutoff on frequency based on impacts: via `impact_rp_cutoff` (see the note).\n", + "\n", + "```{note}\n", + "Providing a value for `impact_rp_cutoff` \"removes\" (it sets their intensity to 0.) events from the hazard, for which the exceedance frequency (inverse of return period) of impacts is below the given threshold.\n", + "\n", + "For instance providing 1/20, would remove all events whose impacts have a return period below 20 years.\n", + "\n", + "In that case the function changing the hazard (`Measure.hazard_change`) will be a function with the following signature:\n", + "\n", + " f(hazard: Hazard, # The hazard to apply on\n", + " exposures: Exposures, # The exposure for the impact computation\n", + " impfset: ImpactFuncSet, # The impfset for the impact computation\n", + " base_hazard: Hazard, # The hazard for the impact computation\n", + " exposures_region_id: Optional[list[int]] = None, # Region id to filter to\n", + " ) -> Hazard\n", + "```\n", + "\n", + "```{warning}\n", + "If you provide a new_hazard_path and other modifiers, CLIMADA will load the new file first and then apply the modifiers to it. (A warning will be issued to ensure this sequence is intended).\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f6061c1c-b21f-4aef-a394-c172784a25ab", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- Scaling Config ---\n", + "HazardModifierConfig(\n", + "\t\tNon default fields:\n", + "\t\t\thaz_int_add=-10\n", + "\t\t\thaz_freq_mult=0.8\n", + ")\n", + "\n", + "--- Replacement Config ---\n", + "HazardModifierConfig(\n", + "\t\tNon default fields:\n", + "\t\t\tnew_hazard_path='path/to/new_floods.h5'\n", + ")\n", + "\n", + "--- Cutoff Config ---\n", + "HazardModifierConfig(\n", + "\t\tNon default fields:\n", + "\t\t\timpact_rp_cutoff=0.05\n", + ")\n" + ] + } + ], + "source": [ + "from climada.entity.measures.measure_config import HazardModifierConfig\n", + "\n", + "# 1. Scaling existing hazard\n", + "# Let's say we want to simulate a 20% reduction in frequency\n", + "# and a reduction by 10m/s in the intensity for our tropical cyclones.\n", + "haz_mod = HazardModifierConfig(\n", + " haz_type=\"TC\",\n", + " haz_int_add=-10, # Reduce hazard intensity by 10 units\n", + " haz_freq_mult=0.8, # Scale hazard frequency by 20%\n", + ")\n", + "\n", + "print(\"--- Scaling Config ---\")\n", + "print(haz_mod)\n", + "\n", + "# 2. Replacing the hazard from a file\n", + "# Useful for measures that correspond to a different hazard modelling.\n", + "# E.g., a dike leading to a change in (physical) flood modelling.\n", + "haz_mod_new = HazardModifierConfig(\n", + " haz_type=\"FL\", new_hazard_path=\"path/to/new_floods.h5\"\n", + ")\n", + "\n", + "print(\"\\n--- Replacement Config ---\")\n", + "print(haz_mod_new)\n", + "\n", + "# 3. Applying a cutoff on the return period of the impacts\n", + "# Useful when measures are defined to avoid damage for a specific RP (exceedance frequency).\n", + "# Note that it looks a the distribution of the impacts, not the hazard intensity!\n", + "haz_mod_cutoff = HazardModifierConfig(\n", + " haz_type=\"TC\",\n", + " impact_rp_cutoff=1\n", + " / 20, # Set intensity to 0 for events with impacts with a return period below 20 years\n", + ")\n", + "\n", + "print(\"\\n--- Cutoff Config ---\")\n", + "print(haz_mod_cutoff)" + ] + }, + { + "cell_type": "markdown", + "id": "c7499c1a-2491-42c4-bdbb-d224090b85fb", + "metadata": {}, + "source": [ + "## Modifying Exposures: `ExposuresModifierConfig`\n", + "\n", + "The `ExposuresModifierConfig` is used to define how an adaptation measure changes the exposure (refer to the [exposure tutorial](exposure-tutorial)).\n", + "\n", + "When \"translated\" to a `Measure` object the `ExposuresModifierConfig` populates the `exposures_change` attribute with a function that takes an `Exposures` and returns a modified one, according to the specifications.\n", + "\n", + "`ExposuresModifierConfig` allows you to modify the impact function assigned to different hazard, to set a list of points to 0 value, or to load a different Exposures:\n", + "\n", + "- Remapping the impact function: via `reassign_impf_id` with a dictionary of the form `{haz_type: {old_id: new_id}}`.\n", + "- Setting values to zero: via `set_to_zero` with a list of indices of the exposure GeoDataFrame.\n", + "- Replacing the exposure: via providing the `new_exposures_path` parameter. It need to be a valid HDF5 exposure file readable by `Exposures.from_hdf5()`\n", + "\n", + "```{warning}\n", + "If you provide a new_exposures_path and other modifiers, CLIMADA will load the new file first and then apply the modifiers to it. (A warning will be issued to ensure this sequence is intended).\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5237930d-a18c-4498-afe5-373c5dadf882", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--- First Config ---\n", + "ExposuresModifierConfig(\n", + "\t\tNon default fields:\n", + "\t\t\treassign_impf_id={'TC': {1: 2}}\n", + "\t\t\tset_to_zero=[0, 25, 78]\n", + ")\n", + "\n", + "--- Replacement Config ---\n", + "ExposuresModifierConfig(\n", + "\t\tNon default fields:\n", + "\t\t\tnew_exposures_path='path/to/exposures.h5'\n", + ")\n" + ] + } + ], + "source": [ + "from climada.entity.measures.measure_config import ExposuresModifierConfig\n", + "\n", + "# 1. Changing existing Exposures\n", + "exp_mod = ExposuresModifierConfig(\n", + " reassign_impf_id={\"TC\": {1: 2}}, # Remaps exposures points with impf_TC == 1 to 2.\n", + " set_to_zero=[\n", + " 0,\n", + " 25,\n", + " 78,\n", + " ], # Sets the value of exposure points with index 0, 25 and 78 to 0.\n", + ")\n", + "\n", + "print(\"--- First Config ---\")\n", + "print(exp_mod)\n", + "\n", + "# 2. Replacing the expoosure from a file\n", + "exp_mod_new = ExposuresModifierConfig(new_exposures_path=\"path/to/exposures.h5\")\n", + "\n", + "print(\"\\n--- Replacement Config ---\")\n", + "print(exp_mod_new)" + ] + }, + { + "cell_type": "markdown", + "id": "2c2d4488-28e5-4ced-b9cf-e4d5c0cade3e", + "metadata": {}, + "source": [ + "## Defining the financial aspects of the measure\n", + "\n", + "For in depth description of CostIncome objects, refer to the [related tutorial](cost-income-tutorial).\n", + "\n", + "```{note}\n", + "The default for mkt_price_year if not provided is the current year.\n", + "```\n", + "\n", + "You can easily define the CostIncome object to be associated with the measure using `CostIncomeConfig`:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "7c107fc5-606b-4904-8b8e-059f846c2e39", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--- Growth & Income Config ---\n", + "CostIncomeConfig(\n", + "\t\tNon default fields:\n", + "\t\t\tinit_cost=500000.0\n", + "\t\t\tperiodic_cost=20000.0\n", + "\t\t\tperiodic_income=100000.0\n", + "\t\t\tcost_yearly_growth_rate=0.02\n", + "\t\t\tincome_yearly_growth_rate=0.03\n", + ")\n", + "\n", + "--- Custom Schedule Config ---\n", + "CostIncomeConfig(\n", + "\t\tNon default fields:\n", + "\t\t\tcustom_cash_flows=[{'date': '2024-01-01', 'value': -1000000}, {'date': '2029-01-01', 'value': -200000}, {'date': '2034-01-01', 'value': 500000}]\n", + ")\n" + ] + } + ], + "source": [ + "from climada.entity.measures.measure_config import CostIncomeConfig\n", + "\n", + "# This models a measure where costs increase by 2% annually,\n", + "# but it generates 100k in yearly income which grows by 3%.\n", + "growth_finance = CostIncomeConfig(\n", + " init_cost=500_000.0,\n", + " periodic_cost=20_000.0,\n", + " cost_yearly_growth_rate=0.02,\n", + " periodic_income=100_000.0,\n", + " income_yearly_growth_rate=0.03,\n", + " freq=\"Y\",\n", + ")\n", + "\n", + "print(\"\\n--- Growth & Income Config ---\")\n", + "print(growth_finance)\n", + "\n", + "\n", + "# Custom Cash Flow\n", + "# If the investment isn't linear (e.g., a major retrofit in year 5),\n", + "# you can define a list of specific events.\n", + "custom_schedule = [\n", + " {\"date\": \"2024-01-01\", \"value\": -1000000}, # Initial cost\n", + " {\"date\": \"2029-01-01\", \"value\": -200000}, # Mid-term overhaul\n", + " {\"date\": \"2034-01-01\", \"value\": 500000}, # Terminal value\n", + "]\n", + "\n", + "custom_finance = CostIncomeConfig(custom_cash_flows=custom_schedule)\n", + "\n", + "print(\"\\n--- Custom Schedule Config ---\")\n", + "print(custom_finance)" + ] + }, + { + "cell_type": "markdown", + "id": "ab4216dd-fd0b-4939-844d-56bd5ea49504", + "metadata": {}, + "source": [ + "## Reading from and writing to\n", + "\n", + "You can easily write/read measure configurations from YAML, as well as from pandas Series.\n", + "\n", + "You can also create `Measures`/`MeasureSet` directly, using the same methods (these methods first load the file as a `MeasureConfig` and convert it directly to a `Measure`)\n", + "Similarly you can still create `MeasureSet` from legacy Excel or matlab files using `MeasureSet.from_excel()` which takes care of remapping the legacy parameter names to the new ones.\n", + "See the [measure tutorial](measure-tutorial) for more details on that." + ] + }, + { + "cell_type": "markdown", + "id": "63132690-dd6f-4f45-96a9-5519fa2dec07", + "metadata": {}, + "source": [ + "\n", + "```python\n", + "import pandas as pd\n", + "from climada.entity.measures.measure_config import MeasureConfig\n", + "\n", + "# 1. Exporting to YAML\n", + "# Assuming 'my_measure_config' is a MeasureConfig object created previously\n", + "my_measure_config.to_yaml(\"seawall_config.yaml\")\n", + "\n", + "# 2. Loading from YAML\n", + "loaded_measure_config = MeasureConfig.from_yaml(\"seawall_config.yaml\")\n", + "\n", + "# 3. Loading from Pandas\n", + "row_data = pd.Series({\n", + " \"name\": \"Mangrove_Restoration\",\n", + " \"haz_type\": \"TC\",\n", + " \"impf_mdd_mult\": 0.7,\n", + " \"init_cost\": 250000,\n", + " \"color_rgb\": (0.1, 0.8, 0.1)\n", + "})\n", + "\n", + "pandas_measure_config = MeasureConfig.from_row(row_data)\n", + "\n", + "# 4. Measure object directly\n", + "measure = Measure.from_yaml(\"seawall_config.yaml\")\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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/doc/user-guide/index.rst b/doc/user-guide/index.rst index a5f2f709f5..014fc43f50 100644 --- a/doc/user-guide/index.rst +++ b/doc/user-guide/index.rst @@ -19,6 +19,7 @@ You can then go on to more specific tutorial about `Hazard `_, Hazard Exposures Impact + Adaptation appraisal Local exceedance intensities Uncertainty Quantification climada_engine_Forecast