Skip to content

Commit a39eb36

Browse files
committed
Merge branch 'calibrate-impact-functions' into cross-calibrate-impact-functions
2 parents 6f22366 + b8f1cac commit a39eb36

File tree

9 files changed

+489
-203
lines changed

9 files changed

+489
-203
lines changed

CHANGELOG.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,19 @@ Code freeze date: YYYY-MM-DD
1616

1717
### Fixed
1818

19-
- Fix `util.coordinates.latlon_bounds` for cases where the specified buffer is very large so that the bounds cover more than the full longitudinal range `[-180, 180]` [#839](https://github.com/CLIMADA-project/climada_python/pull/839)
20-
- Fix `climada.hazard.trop_cyclone` for TC tracks crossing the antimeridian [#839](https://github.com/CLIMADA-project/climada_python/pull/839)
21-
2219
### Deprecated
2320

2421
### Removed
2522

23+
## 4.1.1
24+
25+
Release date: 2024-02-21
26+
27+
### Fixed
28+
29+
- Fix `util.coordinates.latlon_bounds` for cases where the specified buffer is very large so that the bounds cover more than the full longitudinal range `[-180, 180]` [#839](https://github.com/CLIMADA-project/climada_python/pull/839)
30+
- Fix `climada.hazard.trop_cyclone` for TC tracks crossing the antimeridian [#839](https://github.com/CLIMADA-project/climada_python/pull/839)
31+
2632
## 4.1.0
2733

2834
Release date: 2024-02-14

climada/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '4.1.1-dev'
1+
__version__ = '4.1.2-dev'

climada/hazard/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,7 @@ def year_month_day_accessor(
840840
return array.dt.strftime("%Y-%m-%d").values
841841

842842
# Handle access errors
843-
except (ValueError, TypeError) as err:
843+
except (ValueError, TypeError, AttributeError) as err:
844844
if strict:
845845
raise err
846846

climada/test/test_util_calibrate.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
BayesianOptimizer,
3636
OutputEvaluator,
3737
BayesianOptimizerOutputEvaluator,
38+
BayesianOptimizerController,
3839
)
3940

4041
from climada.util.calibrate.test.test_base import hazard, exposure
@@ -173,14 +174,18 @@ def setUp(self) -> None:
173174
def test_single(self):
174175
"""Test with single parameter"""
175176
self.input.bounds = {"slope": (-1, 3)}
177+
controller = BayesianOptimizerController(
178+
init_points=10, n_iter=20, max_iterations=1
179+
)
176180
optimizer = BayesianOptimizer(self.input, random_state=1)
177-
output = optimizer.run(init_points=10, n_iter=20)
181+
output = optimizer.run(controller)
178182

179183
# Check result (low accuracy)
180184
self.assertAlmostEqual(output.params["slope"], 1.0, places=2)
181185
self.assertAlmostEqual(output.target, 0.0, places=3)
182186
self.assertEqual(output.p_space.dim, 1)
183187
self.assertTupleEqual(output.p_space_to_dataframe().shape, (30, 2))
188+
self.assertEqual(controller.iterations, 1)
184189

185190
def test_multiple_constrained(self):
186191
"""Test with multiple constrained parameters"""
@@ -204,13 +209,17 @@ def test_multiple_constrained(self):
204209
self.input.bounds = {"intensity_1": (-1, 4), "intensity_2": (-1, 4)}
205210
# Run optimizer
206211
optimizer = BayesianOptimizer(self.input, random_state=1)
207-
output = optimizer.run(n_iter=200)
212+
controller = BayesianOptimizerController.from_input(
213+
self.input, sampling_base=5, max_iterations=3
214+
)
215+
output = optimizer.run(controller)
208216

209217
# Check results (low accuracy)
210218
self.assertEqual(output.p_space.dim, 2)
211219
self.assertAlmostEqual(output.params["intensity_1"], 1.0, places=2)
212220
self.assertAlmostEqual(output.params["intensity_2"], 3.0, places=1)
213221
self.assertAlmostEqual(output.target, 0.0, places=3)
222+
self.assertGreater(controller.iterations, 1)
214223

215224
# Check constraints in parameter space
216225
p_space = output.p_space_to_dataframe()
@@ -224,7 +233,8 @@ def test_multiple_constrained(self):
224233
("Calibration", "Allowed"),
225234
},
226235
)
227-
self.assertTupleEqual(p_space.shape, (300, 5))
236+
self.assertGreater(p_space.shape[0], 50) # Two times random iterations
237+
self.assertEqual(p_space.shape[1], 5)
228238
p_allowed = p_space.loc[p_space["Calibration", "Allowed"], "Parameters"]
229239
npt.assert_array_equal(
230240
(p_allowed["intensity_1"] < p_allowed["intensity_2"]).to_numpy(),
@@ -235,7 +245,10 @@ def test_plots(self):
235245
"""Check if executing the default plots works"""
236246
self.input.bounds = {"slope": (-1, 3)}
237247
optimizer = BayesianOptimizer(self.input, random_state=1)
238-
output = optimizer.run(init_points=10, n_iter=20)
248+
controller = BayesianOptimizerController.from_input(
249+
self.input, max_iterations=1
250+
)
251+
output = optimizer.run(controller)
239252

240253
output_eval = OutputEvaluator(self.input, output)
241254
output_eval.impf_set.plot()

climada/util/calibrate/bayesian_optimizer.py

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import numpy as np
3030
import matplotlib.pyplot as plt
3131
import matplotlib.axes as maxes
32-
from bayes_opt import BayesianOptimization, Events, UtilityFunction
32+
from bayes_opt import BayesianOptimization, Events, UtilityFunction, ScreenLogger
3333
from bayes_opt.target_space import TargetSpace
3434

3535
from .base import Input, Output, Optimizer, OutputEvaluator
@@ -162,7 +162,11 @@ def plot_single(x, y):
162162
# Option 0: Only one parameter
163163
params = p_space_df["Parameters"].columns.to_list()
164164
if len(params) < 2:
165-
return plot_single(x=params[0], y=repeat(0))
165+
# Add zeros for scatter plot
166+
p_space_df["Parameters", "none"] = np.zeros_like(
167+
p_space_df["Parameters", params[0]]
168+
)
169+
return plot_single(x=params[0], y="none")
166170

167171
# Option 1: Only a single plot
168172
if x is not None and y is not None:
@@ -237,8 +241,8 @@ class BayesianOptimizerController(object):
237241
"""
238242

239243
# Init attributes
240-
init_points: int = 0
241-
n_iter: int = 0
244+
init_points: int
245+
n_iter: int
242246
min_improvement: float = 1e-3
243247
min_improvement_count: int = 2
244248
kappa: float = 2.576
@@ -256,9 +260,13 @@ class BayesianOptimizerController(object):
256260

257261
def __post_init__(self):
258262
"""Set the decay factor for :py:attr:`kappa`."""
259-
self.kappa_decay = np.exp(
260-
(np.log(self.kappa_min) - np.log(self.kappa)) / self.n_iter
261-
)
263+
if self.init_points < 0 or self.n_iter < 0:
264+
raise ValueError("'init_points' and 'n_iter' must be 0 or positive")
265+
self.kappa_decay = self._calc_kappa_decay()
266+
267+
def _calc_kappa_decay(self):
268+
"""Compute the decay factor for :py:attr:`kappa`."""
269+
return np.exp((np.log(self.kappa_min) - np.log(self.kappa)) / self.n_iter)
262270

263271
@classmethod
264272
def from_input(cls, inp: Input, sampling_base: float = 4, **kwargs):
@@ -291,10 +299,6 @@ def _previous_max(self):
291299
return -np.inf
292300
return self._improvements[-1].target
293301

294-
def is_converged(self) -> bool:
295-
"""Check if convergence criteria are met"""
296-
return True
297-
298302
def optimizer_params(self) -> dict[str, Union[int, float, str, UtilityFunction]]:
299303
"""Return parameters for the optimizer"""
300304
return {
@@ -365,7 +369,7 @@ def _maybe_stop_early(self, instance):
365369
instance.dispatch(Events.OPTIMIZATION_END)
366370
raise StopEarly()
367371

368-
def update(self, event, instance):
372+
def update(self, event: str, instance: BayesianOptimization):
369373
"""Update the step tracker of this instance.
370374
371375
For step events, check if the latest guess is the new maximum. Also check if the
@@ -441,7 +445,8 @@ class BayesianOptimizer(Optimizer):
441445
input : Input
442446
The input data for this optimizer. See the Notes below for input requirements.
443447
verbose : int, optional
444-
Verbosity of the optimizer output. Defaults to 1.
448+
Verbosity of the optimizer output. Defaults to 0. The output is *not* affected
449+
by the CLIMADA logging settings.
445450
random_state : int, optional
446451
Seed for initializing the random number generator. Defaults to 1.
447452
allow_duplicate_points : bool, optional
@@ -472,14 +477,12 @@ class BayesianOptimizer(Optimizer):
472477
The optimizer instance of this class.
473478
"""
474479

475-
verbose: InitVar[int] = 1
480+
verbose: int = 0
476481
random_state: InitVar[int] = 1
477482
allow_duplicate_points: InitVar[bool] = True
478483
bayes_opt_kwds: InitVar[Optional[Mapping[str, Any]]] = None
479484

480-
def __post_init__(
481-
self, verbose, random_state, allow_duplicate_points, bayes_opt_kwds
482-
):
485+
def __post_init__(self, random_state, allow_duplicate_points, bayes_opt_kwds):
483486
"""Create optimizer"""
484487
if bayes_opt_kwds is None:
485488
bayes_opt_kwds = {}
@@ -491,7 +494,6 @@ def __post_init__(
491494
f=self._opt_func,
492495
pbounds=self.input.bounds,
493496
constraint=self.input.constraints,
494-
verbose=verbose,
495497
random_state=random_state,
496498
allow_duplicate_points=allow_duplicate_points,
497499
**bayes_opt_kwds,
@@ -502,10 +504,7 @@ def _target_func(self, data: pd.DataFrame, predicted: pd.DataFrame) -> Number:
502504
"""Invert the cost function because BayesianOptimization maximizes the target"""
503505
return -self.input.cost_func(data, predicted)
504506

505-
def run(
506-
self,
507-
controller: BayesianOptimizerController,
508-
) -> BayesianOptimizerOutput:
507+
def run(self, controller: BayesianOptimizerController) -> BayesianOptimizerOutput:
509508
"""Execute the optimization
510509
511510
``BayesianOptimization`` *maximizes* a target function. Therefore, this class
@@ -525,14 +524,29 @@ def run(
525524
Optimization output. :py:attr:`BayesianOptimizerOutput.p_space` stores data
526525
on the sampled parameter space.
527526
"""
527+
# Register the controller
528528
for event in (Events.OPTIMIZATION_STEP, Events.OPTIMIZATION_END):
529529
self.optimizer.subscribe(event, controller)
530530

531+
# Register the logger
532+
if self.verbose > 0:
533+
log = ScreenLogger(
534+
verbose=self.verbose, is_constrained=self.optimizer.is_constrained
535+
)
536+
for event in (
537+
Events.OPTIMIZATION_START,
538+
Events.OPTIMIZATION_STEP,
539+
Events.OPTIMIZATION_END,
540+
):
541+
self.optimizer.subscribe(event, log)
542+
543+
# Run the optimization
531544
while controller.iterations < controller.max_iterations:
532545
try:
533546
LOGGER.info(f"Optimization iteration: {controller.iterations}")
534547
self.optimizer.maximize(**controller.optimizer_params())
535548
except StopEarly:
549+
# Start a new iteration
536550
continue
537551
except StopIteration:
538552
# Exit the loop

0 commit comments

Comments
 (0)