Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add module for calibrating impact functions #692

Merged
merged 87 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
5d8278e
Initial draft for calibration from scipy.optimize
peanutfun Mar 31, 2023
443545e
Draft for impact function calibration
peanutfun May 4, 2023
819aab5
Add first unit tests of calibration module
peanutfun May 5, 2023
96e3cb3
ci: Add bayesian-optimization during Jenkins build
peanutfun May 5, 2023
123c632
Add __init__.py for util/calibarte/test module
peanutfun May 8, 2023
107a836
Add climada.util.calibrate.test module to test discovery
peanutfun May 8, 2023
2af6f09
Add unit and integration tests, update code base
peanutfun May 8, 2023
0d6e80b
Start documenting new calibrate module
peanutfun May 8, 2023
23cae6c
Actually add the intregration test
peanutfun May 8, 2023
50f3fd9
Add some documentation
peanutfun May 9, 2023
d321832
commit PLEASE CLEAN UP
peanutfun May 17, 2023
24c0fbc
Add more docstrings and simplify imports through __init__
peanutfun May 24, 2023
096a8d4
Add separate Output classes for each optimizer
peanutfun Jun 9, 2023
37c65d9
Merge branch 'develop' into calibrate-impact-functions
peanutfun Jun 9, 2023
e8abb1a
Restructure calibration module
peanutfun Jun 12, 2023
3d94151
Add tutorial on impact function calibration
peanutfun Jun 12, 2023
ea0eb47
Update tutorial
peanutfun Jun 13, 2023
0e5a557
Remove hazard event selection from calibrate.Input
peanutfun Jun 13, 2023
e1fe68a
Update calibration tutorial
peanutfun Jun 13, 2023
68c421b
Merge branch 'develop' into calibrate-impact-functions
emanuel-schmid Jun 26, 2023
df03b0d
Update climada/util/calibrate/bayesian_optimizer.py
peanutfun Jun 28, 2023
5ef4a01
Separate computing cost from transforming impact objects
peanutfun Jun 29, 2023
91cfd83
Merge branch 'calibrate-impact-functions' of https://github.com/CLIMA…
peanutfun Jul 10, 2023
4e1f104
Add evaluator for calibration output
peanutfun Jul 10, 2023
43f40b3
Add TestBayesianOptimizer test to test loader
peanutfun Jul 13, 2023
97d763a
Update code, docs, and tutorial
peanutfun Aug 3, 2023
d43eb8a
Update tutorial
peanutfun Aug 3, 2023
dda079d
Add option to adjust data frame alignment
peanutfun Aug 18, 2023
185866f
Merge branch 'develop' into calibrate-impact-functions
peanutfun Aug 21, 2023
5fdbf4e
Merge branch 'develop' into calibrate-impact-functions
peanutfun Sep 6, 2023
c2ede47
Merge branch 'develop' into calibrate-impact-functions
peanutfun Sep 20, 2023
c40b85a
Merge branch 'develop' into calibrate-impact-functions
peanutfun Oct 2, 2023
645862a
Merge branch 'develop' into calibrate-impact-functions
peanutfun Oct 26, 2023
357541a
Merge branch 'develop' into calibrate-impact-functions
emanuel-schmid Nov 9, 2023
2e536ef
add seaborn
emanuel-schmid Nov 9, 2023
2ace55f
Add function to plot Impf variability of calibration (#791)
timschmi95 Nov 14, 2023
423b5b8
Improve alignment and handling of NaNs
peanutfun Nov 15, 2023
8349016
Add seaborn to dependencies
peanutfun Nov 15, 2023
67ef797
Merge branch 'calibrate-impact-functions' of https://github.com/CLIMA…
peanutfun Nov 20, 2023
24c1fc3
Split tests into multiple files, finish up
peanutfun Nov 23, 2023
1538e78
Move impact transform and align to Input
peanutfun Nov 23, 2023
832da6a
Use MultiIndex in parameter space dataframe
peanutfun Nov 23, 2023
af959a7
Update tutorial
peanutfun Nov 23, 2023
042e6c9
Fix requirements for calibration module
peanutfun Nov 24, 2023
677fba9
Remove plot_impf_set function and improve exception type
peanutfun Nov 24, 2023
3a046c7
Add tests for OutputEvaluator
peanutfun Nov 24, 2023
066afe5
Fix name of bayes_opt package on PyPI
peanutfun Nov 24, 2023
fbc1701
Make sure latest seaborn is installed on Jenkins
peanutfun Nov 24, 2023
2986b51
Remove unused function definition
peanutfun Nov 24, 2023
8ab8600
Fix linter issues and remove unused code
peanutfun Nov 24, 2023
85a5826
Add BayesianOptimizerOutputEvaluator
peanutfun Nov 24, 2023
472d0c5
Fix typo in tutorial
peanutfun Nov 24, 2023
6ef09e5
Add GNU license header to new files
peanutfun Nov 24, 2023
661f991
Update CHANGELOG.md
peanutfun Nov 24, 2023
90f2749
edit authors.md
Nov 27, 2023
53feef6
Fix a bug in parameter space plot
peanutfun Nov 28, 2023
6fa9315
Merge branch 'develop' into calibrate-impact-functions
peanutfun Jan 30, 2024
d4d6777
Add suggestions from code review
peanutfun Jan 30, 2024
a0bada5
Add iteration controller for BayesianOptimizer
peanutfun Feb 20, 2024
cab68c4
Fix calibrate module init and add first controller tests
peanutfun Feb 20, 2024
a9c45d7
Merge branch 'develop' into calibrate-impact-functions
peanutfun Feb 20, 2024
9bf050c
Fix handling of instance maximum for constrained optimization
peanutfun Feb 21, 2024
17046db
Merge branch 'develop' into calibrate-impact-functions
peanutfun Feb 23, 2024
72f6263
Add tests for BayesianOptimizerController and fix verbosity
peanutfun Feb 23, 2024
5a8ac9f
Add test for plotting parameter space
peanutfun Feb 23, 2024
01f0f1c
Update integration tests for BayesianOptimizer
peanutfun Feb 23, 2024
b8f1cac
Add explanation of BayesianOptimizerController to tutorial
peanutfun Feb 23, 2024
10e9679
Merge branch 'develop' into calibrate-impact-functions
emanuel-schmid Mar 14, 2024
2326db2
Add option to store and load calibration results
peanutfun Apr 26, 2024
e270e1a
Merge branch 'develop' into calibrate-impact-functions
peanutfun Apr 29, 2024
5e2dd75
Update plots and tutorial
peanutfun Apr 29, 2024
10e0014
Add JOSS paper and associated GitHub workflow (#876)
peanutfun May 2, 2024
1c412da
JOSS Paper: Fix typo
peanutfun May 2, 2024
2b1ecda
JOSS Paper: Fix DOIs
peanutfun May 3, 2024
e7e441a
JOSS Paper: Do not call it 'natural disaster'.
peanutfun May 3, 2024
60b3a6e
JOSS: Add missing URL for Rougier et al.
peanutfun May 21, 2024
ba9bb7f
JOSS: Remove unused reference
peanutfun May 21, 2024
1d3d014
Add overview section to tutorial and include review suggestions
peanutfun Jul 8, 2024
9949f3e
Add quickstart section to tutorial
peanutfun Jul 8, 2024
a446edd
Shorten quickstart
peanutfun Jul 8, 2024
07adc6f
Guide readers through quickstart section
peanutfun Jul 8, 2024
1f14859
Replace Riedel et al. 2024 preprint with publication
peanutfun Jul 11, 2024
25fb7c3
Merge branch 'develop' into calibrate-impact-functions
peanutfun Jul 11, 2024
0183307
ci: Remove JOSS paper build job
peanutfun Jul 11, 2024
1511cc1
Fix CHANGELOG.md and add entry in Citation guide
peanutfun Jul 12, 2024
102a335
Merge branch 'develop' into calibrate-impact-functions
peanutfun Jul 12, 2024
2232c8c
Revert changes to Jenkinsfile
peanutfun Jul 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@
* Raphael Portmann
* Nicolas Colombi
* Leonie Villiger
* Timo Schmid
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Code freeze date: YYYY-MM-DD

### Added

- `climada.util.calibrate` module for calibrating impact functions [#692](https://github.com/CLIMADA-project/climada_python/pull/692)

### Changed

- Update `CONTRIBUTING.md` to better explain types of contributions to this repository [#797](https://github.com/CLIMADA-project/climada_python/pull/797)
Expand Down
244 changes: 244 additions & 0 deletions climada/test/test_util_calibrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
"""
This file is part of CLIMADA.

Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.

CLIMADA is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free
Software Foundation, version 3.

CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.

---
Integration tests for calibration module
"""

import unittest

import pandas as pd
import numpy as np
import numpy.testing as npt
from scipy.optimize import NonlinearConstraint
from sklearn.metrics import mean_squared_error
from matplotlib.axes import Axes

from climada.entity import ImpactFuncSet, ImpactFunc

from climada.util.calibrate import (
Input,
ScipyMinimizeOptimizer,
BayesianOptimizer,
OutputEvaluator,
BayesianOptimizerOutputEvaluator,
)

from climada.util.calibrate.test.test_base import hazard, exposure


class TestScipyMinimizeOptimizer(unittest.TestCase):
"""Test the TestScipyMinimizeOptimizer"""

def setUp(self) -> None:
"""Prepare input for optimization"""
self.hazard = hazard()
self.hazard.frequency = np.ones_like(self.hazard.event_id)
self.hazard.date = self.hazard.frequency
self.hazard.event_name = ["event"] * len(self.hazard.event_id)
self.exposure = exposure()
self.events = [10, 1]
self.hazard = self.hazard.select(event_id=self.events)
self.data = pd.DataFrame(
data={"a": [3, 1], "b": [0.2, 0.01]}, index=self.events
)
self.impact_to_dataframe = lambda impact: impact.impact_at_reg(["a", "b"])
self.impact_func_creator = lambda slope: ImpactFuncSet(
[
ImpactFunc(
intensity=np.array([0, 10]),
mdd=np.array([0, 10 * slope]),
paa=np.ones(2),
id=1,
tovogt marked this conversation as resolved.
Show resolved Hide resolved
)
]
)
self.input = Input(
self.hazard,
self.exposure,
self.data,
self.impact_func_creator,
self.impact_to_dataframe,
mean_squared_error,
)

def test_single(self):
"""Test with single parameter"""
optimizer = ScipyMinimizeOptimizer(self.input)
output = optimizer.run(params_init={"slope": 0.1})

# Result should be nearly exact
self.assertTrue(output.result.success)
self.assertAlmostEqual(output.params["slope"], 1.0)
self.assertAlmostEqual(output.target, 0.0)

def test_bound(self):
"""Test with single bound"""
self.input.bounds = {"slope": (-1.0, 0.91)}
optimizer = ScipyMinimizeOptimizer(self.input)
output = optimizer.run(params_init={"slope": 0.1})

# Result should be very close to the bound
self.assertTrue(output.result.success)
self.assertGreater(output.params["slope"], 0.89)
self.assertAlmostEqual(output.params["slope"], 0.91, places=2)

def test_multiple_constrained(self):
"""Test with multiple constrained parameters"""
# Set new generator
self.input.impact_func_creator = lambda intensity_1, intensity_2: ImpactFuncSet(
[
ImpactFunc(
intensity=np.array([0, intensity_1, intensity_2]),
mdd=np.array([0, 1, 3]),
paa=np.ones(3),
id=1,
)
]
)

# Constraint: param[0] < param[1] (intensity_1 < intensity_2)
self.input.constraints = NonlinearConstraint(
lambda params: params[0] - params[1], -np.inf, 0.0
)
self.input.bounds = {"intensity_1": (0, 3.1), "intensity_2": (0, 3.1)}

# Run optimizer
optimizer = ScipyMinimizeOptimizer(self.input)
output = optimizer.run(
params_init={"intensity_1": 2, "intensity_2": 2},
options=dict(gtol=1e-5, xtol=1e-5),
)

# Check results (low accuracy)
self.assertTrue(output.result.success)
print(output.result.message)
print(output.result.status)
self.assertAlmostEqual(output.params["intensity_1"], 1.0, places=2)
self.assertGreater(output.params["intensity_2"], 2.8) # Should be 3.0
self.assertAlmostEqual(output.target, 0.0, places=3)


class TestBayesianOptimizer(unittest.TestCase):
"""Integration tests for the BayesianOptimizer"""

def setUp(self) -> None:
"""Prepare input for optimization"""
self.hazard = hazard()
self.hazard.frequency = np.ones_like(self.hazard.event_id)
self.hazard.date = self.hazard.frequency
self.hazard.event_name = ["event"] * len(self.hazard.event_id)
self.exposure = exposure()
self.events = [10, 1]
self.hazard = self.hazard.select(event_id=self.events)
self.data = pd.DataFrame(
data={"a": [3, 1], "b": [0.2, 0.01]}, index=self.events
)
self.impact_to_dataframe = lambda impact: impact.impact_at_reg(["a", "b"])
self.impact_func_creator = lambda slope: ImpactFuncSet(
[
ImpactFunc(
intensity=np.array([0, 10]),
mdd=np.array([0, 10 * slope]),
paa=np.ones(2),
id=1,
)
]
)
self.input = Input(
self.hazard,
self.exposure,
self.data,
self.impact_func_creator,
self.impact_to_dataframe,
mean_squared_error,
)

def test_single(self):
"""Test with single parameter"""
self.input.bounds = {"slope": (-1, 3)}
optimizer = BayesianOptimizer(self.input)
output = optimizer.run(init_points=10, n_iter=20, random_state=1)
tovogt marked this conversation as resolved.
Show resolved Hide resolved

# Check result (low accuracy)
self.assertAlmostEqual(output.params["slope"], 1.0, places=2)
self.assertAlmostEqual(output.target, 0.0, places=3)
self.assertEqual(output.p_space.dim, 1)
self.assertTupleEqual(output.p_space_to_dataframe().shape, (30, 2))

def test_multiple_constrained(self):
"""Test with multiple constrained parameters"""
# Set new generator
self.input.impact_func_creator = lambda intensity_1, intensity_2: ImpactFuncSet(
[
ImpactFunc(
intensity=np.array([0, intensity_1, intensity_2]),
mdd=np.array([0, 1, 3]),
paa=np.ones(3),
id=1,
)
]
)

# Constraint: param[0] < param[1] (intensity_1 < intensity_2)
self.input.constraints = NonlinearConstraint(
lambda intensity_1, intensity_2: intensity_1 - intensity_2, -np.inf, 0.0
)
self.input.bounds = {"intensity_1": (-1, 4), "intensity_2": (-1, 4)}
# Run optimizer
optimizer = BayesianOptimizer(self.input)
output = optimizer.run(n_iter=200, random_state=1)

# Check results (low accuracy)
self.assertEqual(output.p_space.dim, 2)
self.assertAlmostEqual(output.params["intensity_1"], 1.0, places=2)
self.assertAlmostEqual(output.params["intensity_2"], 3.0, places=1)
self.assertAlmostEqual(output.target, 0.0, places=3)

# Check constraints in parameter space
p_space = output.p_space_to_dataframe()
self.assertSetEqual(
set(p_space.columns.to_list()),
{
("Parameters", "intensity_1"),
("Parameters", "intensity_2"),
("Calibration", "Cost Function"),
("Calibration", "Constraints Function"),
("Calibration", "Allowed"),
},
)
self.assertTupleEqual(p_space.shape, (300, 5))
p_allowed = p_space.loc[p_space["Calibration", "Allowed"], "Parameters"]
npt.assert_array_equal(
(p_allowed["intensity_1"] < p_allowed["intensity_2"]).to_numpy(),
np.full_like(p_allowed["intensity_1"].to_numpy(), True),
)

def test_plots(self):
"""Check if executing the default plots works"""
self.input.bounds = {"slope": (-1, 3)}
optimizer = BayesianOptimizer(self.input)
output = optimizer.run(init_points=10, n_iter=20, random_state=1)

output_eval = OutputEvaluator(self.input, output)
output_eval.impf_set.plot()
output_eval.plot_at_event()
output_eval.plot_at_region()
output_eval.plot_event_region_heatmap()

output_eval = BayesianOptimizerOutputEvaluator(self.input, output)
ax = output_eval.plot_impf_variability()
self.assertIsInstance(ax, Axes)
23 changes: 23 additions & 0 deletions climada/util/calibrate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""
This file is part of CLIMADA.

Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.

CLIMADA is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free
Software Foundation, version 3.

CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.

---
Impact function calibration module
"""

from .base import Input, OutputEvaluator
from .bayesian_optimizer import BayesianOptimizer, BayesianOptimizerOutputEvaluator
from .scipy_optimizer import ScipyMinimizeOptimizer
Loading