|
| 1 | +"""Calibration Base Classes and Interfaces""" |
| 2 | + |
| 3 | +from abc import ABC, abstractmethod |
| 4 | +from dataclasses import dataclass, field, InitVar |
| 5 | +from typing import Callable, Mapping, Optional, Tuple, Union, Any, Dict |
| 6 | +from numbers import Number |
| 7 | + |
| 8 | +import numpy as np |
| 9 | +import pandas as pd |
| 10 | +from scipy.optimize import Bounds, LinearConstraint, NonlinearConstraint |
| 11 | + |
| 12 | +from climada.hazard import Hazard |
| 13 | +from climada.entity import Exposures, ImpactFuncSet |
| 14 | +from climada.engine import Impact, ImpactCalc |
| 15 | + |
| 16 | +ConstraintType = Union[LinearConstraint, NonlinearConstraint, Mapping] |
| 17 | + |
| 18 | + |
| 19 | +@dataclass |
| 20 | +class Input: |
| 21 | + """Define the static input for a calibration task |
| 22 | +
|
| 23 | + Attributes |
| 24 | + ---------- |
| 25 | + hazard : climada.Hazard |
| 26 | + Hazard object to compute impacts from |
| 27 | + exposure : climada.Exposures |
| 28 | + Exposures object to compute impacts from |
| 29 | + data : pandas.Dataframe |
| 30 | + The data to compare computed impacts to. Index: Event IDs matching the IDs of |
| 31 | + ``hazard``. Columns: Arbitrary columns. |
| 32 | + cost_func : Callable |
| 33 | + Function that takes an ``Impact`` object and a ``pandas.Dataframe`` as argument |
| 34 | + and returns a single number. The optimization algorithm will try to minimize this |
| 35 | + number. See this module for a suggestion of cost functions. |
| 36 | + impact_func_gen : Callable |
| 37 | + Function that takes the parameters as keyword arguments and returns an impact |
| 38 | + function set. This will be called each time the optimization algorithm updates |
| 39 | + the parameters. |
| 40 | + bounds : Mapping (str, {Bounds, tuple(float, float)}), optional |
| 41 | + The bounds for the parameters. Keys: parameter names. Values: |
| 42 | + ``scipy.minimize.Bounds`` instance or tuple of minimum and maximum value. |
| 43 | + Unbounded parameters need not be specified here. See the documentation for |
| 44 | + the selected optimization algorithm on which data types are supported. |
| 45 | + constraints : Constraint or list of Constraint, optional |
| 46 | + One or multiple instances of ``scipy.minimize.LinearConstraint``, |
| 47 | + ``scipy.minimize.NonlinearConstraint``, or a mapping. See the documentation for |
| 48 | + the selected optimization algorithm on which data types are supported. |
| 49 | + impact_calc_kwds : Mapping (str, Any), optional |
| 50 | + Keyword arguments to :py:meth:`climada.engine.impact_calc.ImpactCalc.impact`. |
| 51 | + Defaults to ``{"assign_centroids": False}`` (by default, centroids are assigned |
| 52 | + here via the ``align`` parameter, to avoid assigning them each time the impact is |
| 53 | + calculated). |
| 54 | + align : bool, optional |
| 55 | + Match event IDs from ``hazard`` and ``data``, and assign the centroids from |
| 56 | + ``hazard`` to ``exposure``. Defaults to ``True``. |
| 57 | + """ |
| 58 | + |
| 59 | + hazard: Hazard |
| 60 | + exposure: Exposures |
| 61 | + data: pd.DataFrame |
| 62 | + cost_func: Callable[[Impact, pd.DataFrame], Number] |
| 63 | + impact_func_gen: Callable[..., ImpactFuncSet] |
| 64 | + bounds: Optional[Mapping[str, Union[Bounds, Tuple[Number, Number]]]] = None |
| 65 | + constraints: Optional[Union[ConstraintType, list[ConstraintType]]] = None |
| 66 | + impact_calc_kwds: Mapping[str, Any] = field( |
| 67 | + default_factory=lambda: {"assign_centroids": False} |
| 68 | + ) |
| 69 | + align: InitVar[bool] = True |
| 70 | + |
| 71 | + def __post_init__(self, align): |
| 72 | + """Prepare input data""" |
| 73 | + if align: |
| 74 | + event_diff = np.setdiff1d(self.data.index, self.hazard.event_id) |
| 75 | + if event_diff.size > 0: |
| 76 | + raise RuntimeError( |
| 77 | + "Event IDs in 'data' do not match event IDs in 'hazard': \n" |
| 78 | + f"{event_diff}" |
| 79 | + ) |
| 80 | + self.hazard = self.hazard.select(event_id=self.data.index.tolist()) |
| 81 | + self.exposure.assign_centroids(self.hazard) |
| 82 | + |
| 83 | + |
| 84 | +@dataclass |
| 85 | +class Output: |
| 86 | + """Generic output of a calibration task |
| 87 | +
|
| 88 | + Attributes |
| 89 | + ---------- |
| 90 | + params : Mapping (str, Number) |
| 91 | + The optimal parameters |
| 92 | + target : Number |
| 93 | + The target function value for the optimal parameters |
| 94 | + """ |
| 95 | + |
| 96 | + params: Mapping[str, Number] |
| 97 | + target: Number |
| 98 | + |
| 99 | + |
| 100 | +@dataclass |
| 101 | +class Optimizer(ABC): |
| 102 | + """Abstract base class (interface) for an optimization |
| 103 | +
|
| 104 | + This defines the interface for optimizers in CLIMADA. New optimizers can be created |
| 105 | + by deriving from this class and overriding at least the :py:meth:`run` method. |
| 106 | +
|
| 107 | + Attributes |
| 108 | + ---------- |
| 109 | + input : Input |
| 110 | + The input object for the optimization task. See :py:class:`Input`. |
| 111 | + """ |
| 112 | + |
| 113 | + input: Input |
| 114 | + |
| 115 | + def _target_func(self, impact: Impact, data: pd.DataFrame) -> Number: |
| 116 | + """Target function for the optimizer |
| 117 | +
|
| 118 | + The default version of this function simply returns the value of the cost |
| 119 | + function evaluated on the arguments. |
| 120 | +
|
| 121 | + Parameters |
| 122 | + ---------- |
| 123 | + impact : climada.engine.Impact |
| 124 | + The impact object returned by the impact calculation. |
| 125 | + data : pandas.DataFrame |
| 126 | + The data used for calibration. See :py:attr:`Input.data`. |
| 127 | +
|
| 128 | + Returns |
| 129 | + ------- |
| 130 | + The value of the target function for the optimizer. |
| 131 | + """ |
| 132 | + return self.input.cost_func(impact, data) |
| 133 | + |
| 134 | + def _kwargs_to_impact_func_gen(self, *_, **kwargs) -> Dict[str, Any]: |
| 135 | + """Define how the parameters to :py:meth:`_opt_func` must be transformed |
| 136 | +
|
| 137 | + Optimizers may implement different ways of representing the parameters (e.g., |
| 138 | + key-value pairs, arrays, etc.). Depending on this representation, the parameters |
| 139 | + must be transformed to match the syntax of the impact function generator used, |
| 140 | + see :py:attr:`Input.impact_func_gen`. |
| 141 | +
|
| 142 | + In this default version, the method simply returns its keyword arguments as |
| 143 | + mapping. Override this method if the optimizer used *does not* represent |
| 144 | + parameters as key-value pairs. |
| 145 | +
|
| 146 | + Parameters |
| 147 | + ---------- |
| 148 | + kwargs |
| 149 | + The parameters as key-value pairs. |
| 150 | +
|
| 151 | + Returns |
| 152 | + ------- |
| 153 | + The parameters as key-value pairs. |
| 154 | + """ |
| 155 | + return kwargs |
| 156 | + |
| 157 | + def _opt_func(self, *args, **kwargs) -> Number: |
| 158 | + """The optimization function iterated by the optimizer |
| 159 | +
|
| 160 | + This function takes arbitrary arguments from the optimizer, generates a new set |
| 161 | + of impact functions from it, computes the impact, and finally calculates the |
| 162 | + target function value and returns it. |
| 163 | +
|
| 164 | + Parameters |
| 165 | + ---------- |
| 166 | + args, kwargs |
| 167 | + Arbitrary arguments from the optimizer, including parameters |
| 168 | +
|
| 169 | + Returns |
| 170 | + ------- |
| 171 | + Target function value for the given arguments |
| 172 | + """ |
| 173 | + params = self._kwargs_to_impact_func_gen(*args, **kwargs) |
| 174 | + impf_set = self.input.impact_func_gen(**params) |
| 175 | + impact = ImpactCalc( |
| 176 | + exposures=self.input.exposure, |
| 177 | + impfset=impf_set, |
| 178 | + hazard=self.input.hazard, |
| 179 | + ).impact(**self.input.impact_calc_kwds) |
| 180 | + return self._target_func(impact, self.input.data) |
| 181 | + |
| 182 | + @abstractmethod |
| 183 | + def run(self, **opt_kwargs) -> Output: |
| 184 | + """Execute the optimization""" |
0 commit comments