Skip to content

Commit 443545e

Browse files
committed
Draft for impact function calibration
1 parent 5d8278e commit 443545e

File tree

1 file changed

+70
-35
lines changed

1 file changed

+70
-35
lines changed
Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Module for calibrating impact functions"""
22

33
from abc import ABC, abstractmethod
4-
from dataclasses import dataclass
5-
from typing import Callable, Mapping, Optional, Tuple, Union, Any
4+
from dataclasses import dataclass, field, InitVar
5+
from typing import Callable, Mapping, Optional, Tuple, Union, Any, Dict, List
66
from numbers import Number
77

88
import numpy as np
@@ -16,9 +16,9 @@
1616
)
1717
from bayes_opt import BayesianOptimization
1818

19-
from ....climada.hazard import Hazard
20-
from ....climada.entity import Exposures, ImpactFunc, ImpactFuncSet
21-
from ....climada.engine import Impact, ImpactCalc
19+
from ...hazard import Hazard
20+
from ...entity import Exposures, ImpactFunc, ImpactFuncSet
21+
from ...engine import Impact, ImpactCalc
2222

2323

2424
@dataclass
@@ -34,12 +34,16 @@ class Input:
3434
constraints: Optional[
3535
Mapping[str, Union[LinearConstraint, NonlinearConstraint, Mapping]]
3636
] = None
37+
impact_calc_kwds: Mapping[str, Any] = field(
38+
default_factory=lambda: dict(assign_centroids=False)
39+
)
3740

3841
def __post_init__(self):
3942
"""Prepare input data"""
4043
self.hazard = self.hazard.select(event_id=self.data.index)
4144
self.exposure.assign_centroids(self.hazard)
4245

46+
4347
@dataclass
4448
class Output:
4549
"""Define the output of a calibration task"""
@@ -56,56 +60,87 @@ class Optimizer(ABC):
5660

5761
input: Input
5862

63+
def _target_func(self, impact: Impact, data: pd.DataFrame):
64+
return self.input.cost_func(impact, data)
65+
66+
def _kwargs_to_impact_func_gen(self, *args, **kwargs) -> Dict[str, Any]:
67+
"""Define how the parameters to 'opt_func' must be transformed"""
68+
return kwargs
69+
70+
def _opt_func(self, *args, **kwargs):
71+
"""The optimization function that is iterated"""
72+
params = self._kwargs_to_impact_func_gen(*args, **kwargs)
73+
impf_set = self.input.impact_func_gen(**params)
74+
impact = ImpactCalc(
75+
exposures=self.input.exposure,
76+
impfset=impf_set,
77+
hazard=self.input.hazard,
78+
).impact(assign_centroids=False, **self.input.impact_calc_kwds)
79+
return self._target_func(impact, self.input.data)
80+
5981
@abstractmethod
60-
def run(
61-
self, opt_kwds: Mapping[str, Any], impact_calc_kwds: Mapping[str, Any]
62-
) -> Output:
82+
def run(self, **opt_kwargs) -> Output:
6383
"""Execute the optimization"""
6484
pass
6585

66-
@property
67-
@abstractmethod
68-
def optimize_func(self) -> Callable:
69-
"""The function used for optimizing"""
70-
7186

7287
@dataclass
7388
class ScipyMinimizeOptimizer(Optimizer):
7489
"""An optimization using scipy.optimize.minimize"""
7590

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-
):
91+
_param_names: List[str] = field(default_factory=list)
92+
93+
def _kwargs_to_impact_func_gen(self, *args, **kwargs) -> Dict[str, Any]:
94+
return dict(zip(self._param_names, args[0].flat))
95+
96+
def run(self, params_init: Mapping[str, Number], **opt_kwargs):
8297
"""Execute the optimization"""
83-
param_names = list(params_init.keys())
98+
self._param_names = list(params_init.keys())
8499

85100
# Transform data to match minimize input
86101
bounds = self.input.bounds
87102
if bounds is not None:
88-
bounds = [bounds[name] for name in param_names]
103+
bounds = [bounds.get(name) for name in self._param_names]
89104

90105
constraints = self.input.constraints
91106
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)
107+
constraints = [constraints.get(name) for name in self._param_names]
104108

105109
x0 = np.array(list(params_init.values()))
106110
res = minimize(
107-
fun=fun, x0=x0, bounds=bounds, constraints=constraints, **opt_kwds
111+
fun=lambda x: self._opt_func(x),
112+
x0=x0,
113+
bounds=bounds,
114+
constraints=constraints,
115+
**opt_kwargs,
108116
)
109117

110-
params = {name: value for name, value in zip(param_names, res.x.flat)}
118+
params = dict(zip(self._param_names, res.x.flat))
111119
return Output(params=params, target=res.fun, success=res.success, result=res)
120+
121+
122+
@dataclass
123+
class BayesianOptimizer(Optimizer):
124+
"""An optimization using bayes_opt.BayesianOptimization"""
125+
126+
verbose: InitVar[int] = 1
127+
random_state: InitVar[int] = 1
128+
allow_duplicate_points: InitVar[bool] = True
129+
init_kwds: InitVar[Mapping[str, Any]] = field(default_factory=dict)
130+
131+
def __post_init__(self, **kwargs):
132+
"""Create optimizer"""
133+
init_kwds = kwargs.pop("init_kwds")
134+
self.optimizer = BayesianOptimization(
135+
f=lambda **kwargs: self._opt_func(**kwargs),
136+
pbounds=self.input.bounds,
137+
**kwargs,
138+
**init_kwds,
139+
)
140+
141+
def run(self, init_points: int = 100, n_iter: int = 200, **opt_kwargs):
142+
"""Execute the optimization"""
143+
opt_kwargs.update(init_points=init_points, n_iter=n_iter)
144+
self.optimizer.maximize(**opt_kwargs)
145+
opt = self.optimizer.max
146+
return Output(params=opt["params"], target=opt["target"], success=True)

0 commit comments

Comments
 (0)