Skip to content

Commit 5ef4a01

Browse files
committed
Separate computing cost from transforming impact objects
1 parent 68c421b commit 5ef4a01

File tree

7 files changed

+50
-79
lines changed

7 files changed

+50
-79
lines changed

climada/test/test_util_calibrate.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99

1010
from climada.entity import ImpactFuncSet, ImpactFunc
1111

12-
from climada.util.calibrate import Input, ScipyMinimizeOptimizer
13-
from climada.util.calibrate.impact_func import cost_func_rmse
12+
from climada.util.calibrate import Input, ScipyMinimizeOptimizer, rmse, impact_at_reg
1413

1514
from climada.util.calibrate.test.test_calibrate import hazard, exposure
1615

@@ -30,10 +29,8 @@ def setUp(self) -> None:
3029
self.data = pd.DataFrame(
3130
data={"a": [3, 1], "b": [0.2, 0.01]}, index=self.events
3231
)
33-
self.cost_func = partial(
34-
cost_func_rmse, impact_proc=lambda impact: impact.impact_at_reg(["a", "b"])
35-
)
36-
self.impact_func_gen = lambda slope: ImpactFuncSet(
32+
self.impact_to_dataframe = partial(impact_at_reg, region_ids=["a", "b"])
33+
self.impact_func_creator = lambda slope: ImpactFuncSet(
3734
[
3835
ImpactFunc(
3936
intensity=np.array([0, 10]),
@@ -44,7 +41,12 @@ def setUp(self) -> None:
4441
]
4542
)
4643
self.input = Input(
47-
self.hazard, self.exposure, self.data, self.cost_func, self.impact_func_gen
44+
self.hazard,
45+
self.exposure,
46+
self.data,
47+
self.impact_func_creator,
48+
self.impact_to_dataframe,
49+
rmse,
4850
)
4951

5052
def test_single(self):
@@ -71,7 +73,7 @@ def test_bound(self):
7173
def test_multiple_constrained(self):
7274
"""Test with multiple constrained parameters"""
7375
# Set new generator
74-
self.input.impact_func_gen = lambda intensity_1, intensity_2: ImpactFuncSet(
76+
self.input.impact_func_creator = lambda intensity_1, intensity_2: ImpactFuncSet(
7577
[
7678
ImpactFunc(
7779
intensity=np.array([0, intensity_1, intensity_2]),

climada/util/calibrate/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
from .base import Input
44
from .bayesian_optimizer import BayesianOptimizer
55
from .scipy_optimizer import ScipyMinimizeOptimizer
6+
from .func import rmse, rmsf, impact_at_reg

climada/util/calibrate/base.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from typing import Callable, Mapping, Optional, Tuple, Union, Any, Dict
66
from numbers import Number
77

8-
import numpy as np
98
import pandas as pd
109
from scipy.optimize import Bounds, LinearConstraint, NonlinearConstraint
1110

@@ -29,14 +28,19 @@ class Input:
2928
data : pandas.Dataframe
3029
The data to compare computed impacts to. Index: Event IDs matching the IDs of
3130
``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
31+
impact_func_creator : Callable
3732
Function that takes the parameters as keyword arguments and returns an impact
3833
function set. This will be called each time the optimization algorithm updates
3934
the parameters.
35+
impact_to_dataframe : Callable
36+
Function that takes an impact object as input and transforms its data into a
37+
pandas.DataFrame that is compatible with the format of :py:attr:`data`.
38+
The return value of this function will be passed to the :py:attr`cost_func`
39+
as first argument.
40+
cost_func : Callable
41+
Function that takes two ``pandas.Dataframe`` objects and returns the scalar
42+
"cost" between them. The optimization algorithm will try to minimize this
43+
number.
4044
bounds : Mapping (str, {Bounds, tuple(float, float)}), optional
4145
The bounds for the parameters. Keys: parameter names. Values:
4246
``scipy.minimize.Bounds`` instance or tuple of minimum and maximum value.
@@ -58,8 +62,9 @@ class Input:
5862
hazard: Hazard
5963
exposure: Exposures
6064
data: pd.DataFrame
61-
cost_func: Callable[[Impact, pd.DataFrame], Number]
62-
impact_func_gen: Callable[..., ImpactFuncSet]
65+
impact_func_creator: Callable[..., ImpactFuncSet]
66+
impact_to_dataframe: Callable[[Impact], pd.DataFrame]
67+
cost_func: Callable[[pd.DataFrame, pd.DataFrame], Number]
6368
bounds: Optional[Mapping[str, Union[Bounds, Tuple[Number, Number]]]] = None
6469
constraints: Optional[Union[ConstraintType, list[ConstraintType]]] = None
6570
impact_calc_kwds: Mapping[str, Any] = field(
@@ -104,7 +109,7 @@ class Optimizer(ABC):
104109

105110
input: Input
106111

107-
def _target_func(self, impact: Impact, data: pd.DataFrame) -> Number:
112+
def _target_func(self, impact: pd.DataFrame, data: pd.DataFrame) -> Number:
108113
"""Target function for the optimizer
109114
110115
The default version of this function simply returns the value of the cost
@@ -123,13 +128,13 @@ def _target_func(self, impact: Impact, data: pd.DataFrame) -> Number:
123128
"""
124129
return self.input.cost_func(impact, data)
125130

126-
def _kwargs_to_impact_func_gen(self, *_, **kwargs) -> Dict[str, Any]:
131+
def _kwargs_to_impact_func_creator(self, *_, **kwargs) -> Dict[str, Any]:
127132
"""Define how the parameters to :py:meth:`_opt_func` must be transformed
128133
129134
Optimizers may implement different ways of representing the parameters (e.g.,
130135
key-value pairs, arrays, etc.). Depending on this representation, the parameters
131136
must be transformed to match the syntax of the impact function generator used,
132-
see :py:attr:`Input.impact_func_gen`.
137+
see :py:attr:`Input.impact_func_creator`.
133138
134139
In this default version, the method simply returns its keyword arguments as
135140
mapping. Override this method if the optimizer used *does not* represent
@@ -162,14 +167,15 @@ def _opt_func(self, *args, **kwargs) -> Number:
162167
-------
163168
Target function value for the given arguments
164169
"""
165-
params = self._kwargs_to_impact_func_gen(*args, **kwargs)
166-
impf_set = self.input.impact_func_gen(**params)
170+
params = self._kwargs_to_impact_func_creator(*args, **kwargs)
171+
impf_set = self.input.impact_func_creator(**params)
167172
impact = ImpactCalc(
168173
exposures=self.input.exposure,
169174
impfset=impf_set,
170175
hazard=self.input.hazard,
171176
).impact(**self.input.impact_calc_kwds)
172-
return self._target_func(impact, self.input.data)
177+
impact_df = self.input.impact_to_dataframe(impact)
178+
return self._target_func(impact_df, self.input.data)
173179

174180
@abstractmethod
175181
def run(self, **opt_kwargs) -> Output:

climada/util/calibrate/bayesian_optimizer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ def __post_init__(
8484
**bayes_opt_kwds,
8585
)
8686

87-
def _target_func(self, impact: Impact, data: pd.DataFrame) -> Number:
87+
def _target_func(self, impact: pd.DataFrame, data: pd.DataFrame) -> Number:
8888
"""Invert the cost function because BayesianOptimization maximizes the target"""
89-
return 1 / self.input.cost_func(impact, data)
89+
return -self.input.cost_func(impact, data)
9090

9191
def run(self, **opt_kwargs):
9292
"""Execute the optimization

climada/util/calibrate/impact_func.py

Lines changed: 0 additions & 43 deletions
This file was deleted.

climada/util/calibrate/scipy_optimizer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __post_init__(self):
3434
"""Create a private attribute for storing the parameter names"""
3535
self._param_names: List[str] = list()
3636

37-
def _kwargs_to_impact_func_gen(self, *args, **_) -> Dict[str, Any]:
37+
def _kwargs_to_impact_func_creator(self, *args, **_) -> Dict[str, Any]:
3838
"""Transform the array of parameters into key-value pairs"""
3939
return dict(zip(self._param_names, args[0].flat))
4040

climada/util/calibrate/test/test_calibrate.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def setUp(self):
5454
self.data = pd.DataFrame(data={"a": [1, 2]}, index=self.data_events)
5555

5656
# Create dummy funcs
57+
self.impact_to_dataframe = lambda _: pd.DataFrame()
5758
self.cost_func = lambda impact, data: 1.0
5859
self.impact_func_gen = lambda **kwargs: ImpactFuncSet()
5960

@@ -68,7 +69,8 @@ def test_post_init_calls(self):
6869
exposure=exposure_mock,
6970
data=self.data,
7071
cost_func=self.cost_func,
71-
impact_func_gen=self.impact_func_gen,
72+
impact_func_creator=self.impact_func_gen,
73+
impact_to_dataframe=self.impact_to_dataframe
7274
)
7375
exposure_mock.assign_centroids.assert_called_once_with(self.hazard)
7476
exposure_mock.reset_mock()
@@ -79,7 +81,8 @@ def test_post_init_calls(self):
7981
exposure=exposure_mock,
8082
data=self.data,
8183
cost_func=self.cost_func,
82-
impact_func_gen=self.impact_func_gen,
84+
impact_func_creator=self.impact_func_gen,
85+
impact_to_dataframe=self.impact_to_dataframe,
8386
assign_centroids=False
8487
)
8588
exposure_mock.assign_centroids.assert_not_called()
@@ -93,7 +96,8 @@ def test_post_init(self):
9396
exposure=self.exposure,
9497
data=self.data,
9598
cost_func=self.cost_func,
96-
impact_func_gen=self.impact_func_gen,
99+
impact_func_creator=self.impact_func_gen,
100+
impact_to_dataframe=self.impact_to_dataframe
97101
)
98102

99103
# Check hazard and exposure
@@ -112,7 +116,8 @@ def setUp(self):
112116
exposure=create_autospec(Exposures, instance=True),
113117
data=create_autospec(pd.DataFrame, instance=True),
114118
cost_func=MagicMock(),
115-
impact_func_gen=MagicMock(),
119+
impact_func_creator=MagicMock(),
120+
impact_to_dataframe=MagicMock(),
116121
assign_centroids=False,
117122
)
118123
)
@@ -127,10 +132,10 @@ def setUp(self):
127132
self.optimizer = ScipyMinimizeOptimizer(self.input)
128133

129134
@patch("climada.util.calibrate.base.ImpactCalc", autospec=True)
130-
def test_kwargs_to_impact_func_gen(self, _):
135+
def test_kwargs_to_impact_func_creator(self, _):
131136
"""Test transform of minimize func arguments to impact_func_gen arguments
132137
133-
We test the method '_kwargs_to_impact_func_gen' through 'run' because it is
138+
We test the method '_kwargs_to_impact_func_creator' through 'run' because it is
134139
private.
135140
"""
136141
# Create stubs
@@ -142,8 +147,8 @@ def test_kwargs_to_impact_func_gen(self, _):
142147
params_init = {"x_2": 1, "x 1": 2, "x_3": 3} # NOTE: Also works with whitespace
143148
self.optimizer.run(params_init=params_init, options={"maxiter": 1})
144149

145-
# Check call to '_kwargs_to_impact_func_gen'
146-
self.input.impact_func_gen.assert_any_call(**params_init)
150+
# Check call to '_kwargs_to_impact_func_creator'
151+
self.input.impact_func_creator.assert_any_call(**params_init)
147152

148153
def test_output(self):
149154
"""Check output reporting"""
@@ -218,10 +223,10 @@ def setUp(self):
218223
super().setUp()
219224

220225
@patch("climada.util.calibrate.base.ImpactCalc", autospec=True)
221-
def test_kwargs_to_impact_func_gen(self, _):
226+
def test_kwargs_to_impact_func_creator(self, _):
222227
"""Test transform of minimize func arguments to impact_func_gen arguments
223228
224-
We test the method '_kwargs_to_impact_func_gen' through 'run' because it is
229+
We test the method '_kwargs_to_impact_func_creator' through 'run' because it is
225230
private.
226231
"""
227232
# Create stubs
@@ -233,7 +238,7 @@ def test_kwargs_to_impact_func_gen(self, _):
233238
self.optimizer.run(init_points=2, n_iter=1)
234239

235240
# Check call to '_kwargs_to_impact_func_gen'
236-
call_args = self.input.impact_func_gen.call_args_list
241+
call_args = self.input.impact_func_creator.call_args_list
237242
self.assertEqual(len(call_args), 3)
238243
for args in call_args:
239244
self.assertSequenceEqual(args.kwargs.keys(), self.input.bounds.keys())

0 commit comments

Comments
 (0)