|
| 1 | +"""Module for calibrating impact functions""" |
| 2 | + |
| 3 | +from abc import ABC, abstractmethod |
| 4 | +from dataclasses import dataclass |
| 5 | +from typing import Callable, Mapping, Optional, Tuple, Union, Any |
| 6 | +from numbers import Number |
| 7 | + |
| 8 | +import numpy as np |
| 9 | +import pandas as pd |
| 10 | +from scipy.optimize import ( |
| 11 | + Bounds, |
| 12 | + LinearConstraint, |
| 13 | + NonlinearConstraint, |
| 14 | + OptimizeResult, |
| 15 | + minimize, |
| 16 | +) |
| 17 | +from bayes_opt import BayesianOptimization |
| 18 | + |
| 19 | +from ....climada.hazard import Hazard |
| 20 | +from ....climada.entity import Exposures, ImpactFunc, ImpactFuncSet |
| 21 | +from ....climada.engine import Impact, ImpactCalc |
| 22 | + |
| 23 | + |
| 24 | +@dataclass |
| 25 | +class Input: |
| 26 | + """Define the static input for a calibration task""" |
| 27 | + |
| 28 | + hazard: Hazard |
| 29 | + exposure: Exposures |
| 30 | + data: pd.DataFrame |
| 31 | + cost_func: Callable[[Impact, pd.DataFrame], float] |
| 32 | + impact_func_gen: Callable[..., ImpactFuncSet] |
| 33 | + bounds: Optional[Mapping[str, Union[Bounds, Tuple[Number, Number]]]] = None |
| 34 | + constraints: Optional[ |
| 35 | + Mapping[str, Union[LinearConstraint, NonlinearConstraint, Mapping]] |
| 36 | + ] = None |
| 37 | + |
| 38 | + def __post_init__(self): |
| 39 | + """Prepare input data""" |
| 40 | + self.hazard = self.hazard.select(event_id=self.data.index) |
| 41 | + self.exposure.assign_centroids(self.hazard) |
| 42 | + |
| 43 | +@dataclass |
| 44 | +class Output: |
| 45 | + """Define the output of a calibration task""" |
| 46 | + |
| 47 | + params: Mapping[str, Number] |
| 48 | + target: Number |
| 49 | + success: bool |
| 50 | + result: Optional[OptimizeResult] = None |
| 51 | + |
| 52 | + |
| 53 | +@dataclass |
| 54 | +class Optimizer(ABC): |
| 55 | + """Define the basic interface for an optimization""" |
| 56 | + |
| 57 | + input: Input |
| 58 | + |
| 59 | + @abstractmethod |
| 60 | + def run( |
| 61 | + self, opt_kwds: Mapping[str, Any], impact_calc_kwds: Mapping[str, Any] |
| 62 | + ) -> Output: |
| 63 | + """Execute the optimization""" |
| 64 | + pass |
| 65 | + |
| 66 | + @property |
| 67 | + @abstractmethod |
| 68 | + def optimize_func(self) -> Callable: |
| 69 | + """The function used for optimizing""" |
| 70 | + |
| 71 | + |
| 72 | +@dataclass |
| 73 | +class ScipyMinimizeOptimizer(Optimizer): |
| 74 | + """An optimization using scipy.optimize.minimize""" |
| 75 | + |
| 76 | + def run( |
| 77 | + self, |
| 78 | + params_init: Mapping[str, Number], |
| 79 | + opt_kwds: Mapping[str, Any], |
| 80 | + impact_calc_kwds: Mapping[str, Any], |
| 81 | + ): |
| 82 | + """Execute the optimization""" |
| 83 | + param_names = list(params_init.keys()) |
| 84 | + |
| 85 | + # Transform data to match minimize input |
| 86 | + bounds = self.input.bounds |
| 87 | + if bounds is not None: |
| 88 | + bounds = [bounds[name] for name in param_names] |
| 89 | + |
| 90 | + constraints = self.input.constraints |
| 91 | + if constraints is not None: |
| 92 | + constraints = [constraints[name] for name in param_names] |
| 93 | + |
| 94 | + def fun(params: np.ndarray): |
| 95 | + """Calculate impact and return cost""" |
| 96 | + param_dict = {name: value for name, value in zip(param_names, params.flat)} |
| 97 | + impf_set = self.input.impact_func_gen(**param_dict) |
| 98 | + impact = ImpactCalc( |
| 99 | + exposures=self.input.exposure, |
| 100 | + impfset=impf_set, |
| 101 | + hazard=self.input.hazard, |
| 102 | + ).impact(assign_centroids=False, **impact_calc_kwds) |
| 103 | + return self.input.cost_func(impact, self.input.data) |
| 104 | + |
| 105 | + x0 = np.array(list(params_init.values())) |
| 106 | + res = minimize( |
| 107 | + fun=fun, x0=x0, bounds=bounds, constraints=constraints, **opt_kwds |
| 108 | + ) |
| 109 | + |
| 110 | + params = {name: value for name, value in zip(param_names, res.x.flat)} |
| 111 | + return Output(params=params, target=res.fun, success=res.success, result=res) |
0 commit comments