Skip to content

Commit 5d8278e

Browse files
committed
Initial draft for calibration from scipy.optimize
1 parent a3d189a commit 5d8278e

File tree

3 files changed

+112
-0
lines changed

3 files changed

+112
-0
lines changed

climada/util/calibrate/__init__.py

Whitespace-only changes.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for calibration module"""

0 commit comments

Comments
 (0)