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

Allow nonlinear constraints #353 #371

Merged
merged 72 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
54ef586
Add different callback signature for trust-constr optimiser
Jun 17, 2024
93365c6
Add nonlinear constraints example
Jun 17, 2024
316c971
Add trust-constr optimisation
Jun 17, 2024
b0eff21
Update change-log for #353
Jun 18, 2024
260337f
Style: pre-commit fixes
Jun 18, 2024
863bc70
Merge branch 'develop' into master
MarkBlyth Jun 18, 2024
e6f4dca
style: pre-commit fixes
pre-commit-ci[bot] Jun 18, 2024
167dd99
Clarify multiple constraints
Jun 20, 2024
ddc6302
Initial commit
Jun 20, 2024
65215c8
Rename trust-constr example with correct optimiser
Jun 20, 2024
0e1fd32
Merge branch 'master' of https://github.com/MarkBlyth/PyBOP_develop
Jun 20, 2024
ccf4879
style: pre-commit fixes
pre-commit-ci[bot] Jun 20, 2024
aca5582
Add parameter checking capabilities to base model
Jul 30, 2024
bfb6dd5
Allow user-defined check_parameters function
Jul 30, 2024
6983374
Use user-defined check_parameters for constraints
Jul 30, 2024
722672d
Merge branch 'develop' of https://github.com/MarkBlyth/PyBOP_develop
Jul 30, 2024
5839922
style: pre-commit fixes
pre-commit-ci[bot] Jul 30, 2024
ac0d58b
Add tests for user-defined check_params
Jul 30, 2024
a54e173
Merge branch 'master' of https://github.com/MarkBlyth/PyBOP_develop
Jul 30, 2024
7e07e51
style: pre-commit fixes
pre-commit-ci[bot] Jul 30, 2024
74c0a4a
Move change from bottom to top of list
Jul 31, 2024
cce4507
Remove unused variable
Jul 31, 2024
b10dae2
Update plotting kwargs to new API
Jul 31, 2024
da8851e
Merge branch 'master' of https://github.com/MarkBlyth/PyBOP_develop
Jul 31, 2024
50e5e93
Fix code coverage for parameter checkers
Jul 31, 2024
3f62704
style: pre-commit fixes
pre-commit-ci[bot] Jul 31, 2024
d7e2f12
Merge branch 'pybop-team:develop' into master
MarkBlyth Jul 31, 2024
0f72e16
Merge branch 'develop' into master
MarkBlyth Jul 31, 2024
b6d6a1a
Handle unconventional scipy solver APIs
Jul 31, 2024
05bca23
Merge branch 'master' of https://github.com/MarkBlyth/PyBOP_develop
Jul 31, 2024
d4f8dc7
Update changelog
Jul 31, 2024
be6667f
style: pre-commit fixes
pre-commit-ci[bot] Jul 31, 2024
4bfa384
Update CHANGELOG.md
MarkBlyth Aug 2, 2024
613f2f9
Tidy up arg checks
MarkBlyth Aug 2, 2024
cec6ee5
Add docstring to parameter checker
MarkBlyth Aug 2, 2024
c764ad0
More concise callback-function selection
MarkBlyth Aug 2, 2024
97630bc
Update tests/unit/test_optimisation.py
MarkBlyth Aug 2, 2024
ff0429d
Formatting
MarkBlyth Aug 5, 2024
9b64ef6
Add extra comments for clarity
MarkBlyth Aug 6, 2024
53298f6
Merge branch 'develop' into master
MarkBlyth Aug 6, 2024
a0fd21e
style: pre-commit fixes
pre-commit-ci[bot] Aug 6, 2024
48e19c6
Add docstring to explain callback handling
MarkBlyth Aug 6, 2024
d623df4
Merge branch 'master' of https://github.com/MarkBlyth/PyBOP_develop
MarkBlyth Aug 6, 2024
90caf73
Update test for new check_params API
MarkBlyth Aug 6, 2024
759b826
Change type hints to Union for py3.9 support
MarkBlyth Aug 6, 2024
2539538
Catch attribute error when trying to sample from constant parameter
MarkBlyth Aug 13, 2024
1762182
Merge branch 'develop' into master
MarkBlyth Aug 22, 2024
86aa8fd
style: pre-commit fixes
pre-commit-ci[bot] Aug 22, 2024
d1e84b9
Produce warning when solver terminates prematurely
MarkBlyth Aug 22, 2024
ec0e12f
Merge branch 'master' of https://github.com/MarkBlyth/PyBOP_develop
MarkBlyth Aug 22, 2024
0388795
Add missing string formatter
MarkBlyth Aug 22, 2024
9e45768
Move ecm_trust-constr example from script to notebook
MarkBlyth Aug 22, 2024
1f7c3b7
Remove solver termination warning
MarkBlyth Aug 22, 2024
ce19560
Update check_params to new API
MarkBlyth Aug 22, 2024
76d3b9a
style: pre-commit fixes
pre-commit-ci[bot] Aug 22, 2024
373d1f0
Add code coverage for no-prior handling
MarkBlyth Aug 22, 2024
42a5f93
Merge branch 'master' of https://github.com/MarkBlyth/PyBOP_develop
MarkBlyth Aug 22, 2024
211b417
Rescale cost value before logging
MarkBlyth Aug 22, 2024
2c401ed
Merge branch 'develop' into master
MarkBlyth Aug 22, 2024
19b46cf
style: pre-commit fixes
pre-commit-ci[bot] Aug 22, 2024
302024d
Update pybop/optimisers/scipy_optimisers.py
MarkBlyth Aug 23, 2024
267c9e0
style: pre-commit fixes
pre-commit-ci[bot] Aug 23, 2024
2b3d45b
Import warnings
MarkBlyth Aug 23, 2024
7621916
style: pre-commit fixes
pre-commit-ci[bot] Aug 23, 2024
18337c9
Update pybop/optimisers/scipy_optimisers.py
MarkBlyth Aug 23, 2024
b4bc92d
style: pre-commit fixes
pre-commit-ci[bot] Aug 23, 2024
c0466df
Lower sample rate for speed
MarkBlyth Aug 23, 2024
e143e33
Merge branch 'master' of https://github.com/MarkBlyth/PyBOP_develop
MarkBlyth Aug 23, 2024
1eebc18
style: pre-commit fixes
pre-commit-ci[bot] Aug 23, 2024
30b6101
fix: updates scipy bounds with keep_feasible, clipping groundtruth fo…
BradyPlanden Aug 27, 2024
553d478
tests: increase coverage
BradyPlanden Aug 27, 2024
5b53ba7
Merge branch 'develop' into master
BradyPlanden Aug 28, 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Features

- [#353](https://github.com/pybop-team/PyBOP/issues/353) - Allow user-defined check_params functions to enforce nonlinear constraints and enable SciPy constrained optimisation methods
- [#418](https://github.com/pybop-team/PyBOP/issues/418) - Wraps the `get_parameter_info` method from PyBaMM to get a dictionary of parameter names and types.
- [#413](https://github.com/pybop-team/PyBOP/pull/413) - Adds `DesignCost` functionality to `WeightedCost` class with additional tests.
- [#357](https://github.com/pybop-team/PyBOP/pull/357) - Adds `Transformation()` class with `LogTransformation()`, `IdentityTransformation()`, and `ScaledTransformation()`, `ComposedTransformation()` implementations with corresponding examples and tests.
Expand Down
189 changes: 189 additions & 0 deletions examples/scripts/ecm_tau_constraints.py
MarkBlyth marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import numpy as np

import pybop

"""
When fitting empirical models, the parameters we are able to identify
will be constrained from the data that's available. For example, it's
no good trying to fit an RC timescale of 0.1 s from data sampled at
1 Hz! Likewise, an RC timescale of 100 s cannot be meaningfully fitted
to just 10 s of data. To ensure the optimiser doesn't propose
excessively long or short timescales - beyond what can reasonably be
inferred from the data - it is common to apply nonlinear constraints
on the parameter space. This script fits an RC pair with the
constraint 0.5 <= R1 * C1 <= 1, to highlight a possible method for
applying constraints on the timescales.

An alternative approach is given i the ecm_trust-constr notebook,
which can lead to better results and higher optimisation efficiency
when good timescale guesses are available.
"""

# Import the ECM parameter set from JSON
# parameter_set = pybop.ParameterSet(
# json_path="examples/scripts/parameters/initial_ecm_parameters.json"
# )
# parameter_set.import_parameters()


# Alternatively, define the initial parameter set with a dictionary
# Add definitions for R's, C's, and initial overpotentials for any additional RC elements
parameter_set = {
"chemistry": "ecm",
"Initial SoC": 0.5,
"Initial temperature [K]": 25 + 273.15,
"Cell capacity [A.h]": 5,
"Nominal cell capacity [A.h]": 5,
"Ambient temperature [K]": 25 + 273.15,
"Current function [A]": 5,
"Upper voltage cut-off [V]": 4.2,
"Lower voltage cut-off [V]": 3.0,
"Cell thermal mass [J/K]": 1000,
"Cell-jig heat transfer coefficient [W/K]": 10,
"Jig thermal mass [J/K]": 500,
"Jig-air heat transfer coefficient [W/K]": 10,
"Open-circuit voltage [V]": pybop.empirical.Thevenin().default_parameter_values[
"Open-circuit voltage [V]"
],
"R0 [Ohm]": 0.001,
"Element-1 initial overpotential [V]": 0,
"Element-2 initial overpotential [V]": 0,
"R1 [Ohm]": 0.0002,
"R2 [Ohm]": 0.0003,
"C1 [F]": 10000,
"C2 [F]": 5000,
"Entropic change [V/K]": 0.0004,
}


def get_parameter_checker(
tau_mins: float | list[float],
tau_maxs: float | list[float],
fitted_rc_pair_indices: int | list[int],
):
"""Returns a function to check parameters against given tau bounds.
The resulting check_params function will be sent off to PyBOP; the
rest of the code does some light checking of the constraints.

Parameters
----------
tau_mins: float | list[float]
Lower bounds on timescale tau_i = Ri * Ci
tau_maxs: float | list[float]
Upper bounds on timescale tau_i = Ri * Ci
fitted_rc_pair_indices: int | list[float]
The index of each RC pair whose parameters are to be fitted.
Eg. [1, 2] means fitting R1, R2, C1, C2. The timescale of RC
pair fitted_rc_pair_indices[j] is constrained to be in the
range tau_mins[j] <= R * C <= tau_maxs[j]

Returns
-------
check_params
Function to check the proposed parameter values match the
requested constraints

"""

# Ensure inputs are lists
tau_mins = [tau_mins] if not isinstance(tau_mins, list) else tau_mins
tau_maxs = [tau_maxs] if not isinstance(tau_maxs, list) else tau_maxs
fitted_rc_pair_indices = (
[fitted_rc_pair_indices]
if not isinstance(fitted_rc_pair_indices, list)
else fitted_rc_pair_indices
)

# Validate input lengths
if len(tau_mins) != len(fitted_rc_pair_indices) or len(tau_maxs) != len(
fitted_rc_pair_indices
):
raise ValueError(
"tau_mins and tau_maxs must have the same length as fitted_rc_pair_indices"
)

def check_params(
inputs: dict[str, float] = None,
allow_infeasible_solutions: bool = False,
) -> bool:
"""Checks if the given inputs are within the tau bounds."""
# Allow simulation to run if inputs are None
if inputs is None:
return True

# Check every respective R*C against tau bounds
for i, tau_min, tau_max in zip(fitted_rc_pair_indices, tau_mins, tau_maxs):
tau = inputs[f"R{i} [Ohm]"] * inputs[f"C{i} [F]"]
if not tau_min <= tau <= tau_max:
return False
return True

return check_params


# Define the model
params = pybop.ParameterSet(params_dict=parameter_set)
model = pybop.empirical.Thevenin(
parameter_set=params,
check_params=get_parameter_checker(
0, 0.5, 1
), # Set the model up to automatically check parameters
options={"number of rc elements": 2},
)

# Fitting parameters
parameters = [
pybop.Parameter(
"R0 [Ohm]",
prior=pybop.Gaussian(0.0002, 0.0001),
bounds=[1e-4, 1e-2],
),
pybop.Parameter(
"R1 [Ohm]",
prior=pybop.Gaussian(0.0001, 0.0001),
bounds=[1e-5, 1e-2],
),
pybop.Parameter(
"C1 [F]",
prior=pybop.Gaussian(10000, 2500),
bounds=[2500, 5e4],
),
]

sigma = 0.001
t_eval = np.arange(0, 900, 3)
values = model.predict(t_eval=t_eval)
corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))

# Form dataset
dataset = pybop.Dataset(
{
"Time [s]": t_eval,
"Current function [A]": values["Current [A]"].data,
"Voltage [V]": corrupt_values,
}
)

# Generate problem, cost function, and optimisation class
problem = pybop.FittingProblem(model, parameters, dataset)
cost = pybop.SumSquaredError(problem)
optim = pybop.XNES(
cost,
allow_infeasible_solutions=False,
)

x, final_cost = optim.run()
print("Estimated parameters:", x)


# Plot the time series
pybop.plot_dataset(dataset)

# Plot the timeseries output
pybop.quick_plot(problem, problem_inputs=x, title="Optimised Comparison")

# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)
106 changes: 106 additions & 0 deletions examples/scripts/ecm_trust-constr.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please convert this to a notebook. I think having a script and a notebook will be more informative than two scripts.

Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import numpy as np
import scipy.optimize

import pybop

# Import the ECM parameter set from JSON
# parameter_set = pybop.ParameterSet(
# json_path="examples/scripts/parameters/initial_ecm_parameters.json"
# )
# parameter_set.import_parameters()


# Alternatively, define the initial parameter set with a dictionary
# Add definitions for R's, C's, and initial overpotentials for any additional RC elements
parameter_set = {
"chemistry": "ecm",
"Initial SoC": 0.5,
"Initial temperature [K]": 25 + 273.15,
"Cell capacity [A.h]": 5,
"Nominal cell capacity [A.h]": 5,
"Ambient temperature [K]": 25 + 273.15,
"Current function [A]": 5,
"Upper voltage cut-off [V]": 4.2,
"Lower voltage cut-off [V]": 3.0,
"Cell thermal mass [J/K]": 1000,
"Cell-jig heat transfer coefficient [W/K]": 10,
"Jig thermal mass [J/K]": 500,
"Jig-air heat transfer coefficient [W/K]": 10,
"Open-circuit voltage [V]": pybop.empirical.Thevenin().default_parameter_values[
"Open-circuit voltage [V]"
],
"R0 [Ohm]": 0.001,
"Element-1 initial overpotential [V]": 0,
"Element-2 initial overpotential [V]": 0,
"R1 [Ohm]": 0.0002,
"R2 [Ohm]": 0.0003,
"C1 [F]": 10000,
"C2 [F]": 5000,
"Entropic change [V/K]": 0.0004,
}


# Define the model
model = pybop.empirical.Thevenin(
parameter_set=parameter_set, options={"number of rc elements": 2}
)

# Fitting parameters
parameters = [
pybop.Parameter(
"R0 [Ohm]",
prior=pybop.Gaussian(0.0002, 0.0001),
bounds=[1e-4, 1e-2],
),
pybop.Parameter(
"R1 [Ohm]",
prior=pybop.Gaussian(0.0001, 0.0001),
bounds=[1e-5, 1e-2],
),
pybop.Parameter(
"C1 [F]",
prior=pybop.Gaussian(10000, 2500),
bounds=[2500, 5e4],
),
]

sigma = 0.001
t_eval = np.arange(0, 900, 3)
values = model.predict(t_eval=t_eval)
corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))

# Form dataset
dataset = pybop.Dataset(
{
"Time [s]": t_eval,
"Current function [A]": values["Current [A]"].data,
"Voltage [V]": corrupt_values,
}
)

# Could pass in a list for further constraints
tau_constraint = scipy.optimize.NonlinearConstraint(lambda x: x[1] * x[2], 0, 0.5)

# Generate problem, cost function, and optimisation class
problem = pybop.FittingProblem(model, parameters, dataset)
cost = pybop.SumSquaredError(problem)
optim = pybop.SciPyMinimize(
cost,
method="trust-constr",
constraints=tau_constraint,
)

x, final_cost = optim.run()
print("Estimated parameters:", x)

# Plot the time series
pybop.plot_dataset(dataset)

# Plot the timeseries output
pybop.quick_plot(problem, problem_inputs=x, title="Optimised Comparison")

# Plot convergence
pybop.plot_convergence(optim)

# Plot the parameter traces
pybop.plot_parameters(optim)
20 changes: 18 additions & 2 deletions pybop/models/base_model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import copy
from dataclasses import dataclass
from typing import Any, Optional, Union
from typing import Any, Callable, Optional, Union

import casadi
import numpy as np
Expand Down Expand Up @@ -45,14 +45,27 @@ class BaseModel:

"""

def __init__(self, name="Base Model", parameter_set=None):
def __init__(
self, name="Base Model", parameter_set=None, check_params: Callable = None
Copy link
Member

@BradyPlanden BradyPlanden Aug 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Optional) Are we able to replace self._check_params() with the callable arg check_params if it is provided? I didn't fully check, but it seems like we could overwrite self._check_params() if this arg is provided. This would clean up the attributes and logic if possible.

):
"""
Initialize the BaseModel with an optional name.

Parameters
----------
name : str, optional
The name given to the model instance.
parameter_set : dict | pybamm.ParameterValues, optional
Parameter set to run the model with
check_params : Callable, optional
A compatibility check for the model parameters. Function, with
signature
check_params(
inputs: dict,
allow_infeasible_solutions: bool, optional
)
Returns true if parameters are valid, False otherwise. Can be
used to impose constraints on valid parameters.
"""
self.name = name
if parameter_set is None:
Expand All @@ -63,6 +76,7 @@ def __init__(self, name="Base Model", parameter_set=None):
self._parameter_set = parameter_set
else: # a pybop parameter set
self._parameter_set = pybamm.ParameterValues(parameter_set.params)
self.param_checker = check_params

self.pybamm_model = None
self.parameters = Parameters()
Expand Down Expand Up @@ -604,6 +618,8 @@ def _check_params(
bool
A boolean which signifies whether the parameters are compatible.
"""
if self.param_checker:
return self.param_checker(inputs, allow_infeasible_solutions)
return True

def copy(self):
Expand Down
7 changes: 6 additions & 1 deletion pybop/models/empirical/base_ecm.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def __init__(
var_pts=None,
spatial_methods=None,
solver=None,
check_params=None,
**model_kwargs,
):
model_options = dict(build=False)
Expand All @@ -61,7 +62,9 @@ def __init__(
print("Setting open-circuit voltage to default function")
parameter_set["Open-circuit voltage [V]"] = default_ocp

super().__init__(name=name, parameter_set=parameter_set)
super().__init__(
name=name, parameter_set=parameter_set, check_params=check_params
)
self.pybamm_model = pybamm_model
self._unprocessed_model = self.pybamm_model

Expand Down Expand Up @@ -110,4 +113,6 @@ def _check_params(
bool
A boolean which signifies whether the parameters are compatible.
"""
if self.param_checker:
return self.param_checker(inputs, allow_infeasible_solutions)
return True
Loading
Loading