11"""Module for calibrating impact functions"""
22
33from 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
66from numbers import Number
77
88import numpy as np
1616)
1717from 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
4448class 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
7388class 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