Skip to content

Commit

Permalink
Add check to CMAES and make XNES default optimiser (#253)
Browse files Browse the repository at this point in the history
* Add CMAES check and make XNES default

* Update CMAES error message
Co-authored-by: Brady Planden <[email protected]>
  • Loading branch information
NicolaCourtier authored Mar 27, 2024
1 parent 34b7790 commit 7b500e4
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

## Bug Fixes

- [#91](https://github.com/pybop-team/PyBOP/issues/91) - Adds a check on the number of parameters for CMAES and makes XNES the default optimiser.

# [v24.3](https://github.com/pybop-team/PyBOP/tree/v24.3) - 2024-03-25

## Features
Expand Down
2 changes: 1 addition & 1 deletion pybop/_optimisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def __init__(
self.pints = True

if self.optimiser is None:
self.optimiser = pybop.CMAES
self.optimiser = pybop.XNES
elif issubclass(self.optimiser, pints.Optimiser):
pass
else:
Expand Down
5 changes: 5 additions & 0 deletions pybop/optimisers/pints_optimisers.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ class CMAES(pints.CMAES):
"""

def __init__(self, x0, sigma0=0.1, bounds=None):
if len(x0) == 1:
raise ValueError(
"CMAES requires optimisation of >= 2 parameters at once. "
+ "Please choose another optimiser."
)
if bounds is not None:
self.boundaries = pints.RectangularBoundaries(
bounds["lower"], bounds["upper"]
Expand Down
61 changes: 47 additions & 14 deletions tests/unit/test_optimisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ def dataset(self):
{
"Time [s]": np.linspace(0, 360, 10),
"Current function [A]": np.zeros(10),
"Terminal voltage [V]": np.ones(10),
"Voltage [V]": np.ones(10),
}
)

@pytest.fixture
def parameters(self):
def one_parameter(self):
return [
pybop.Parameter(
"Negative electrode active material volume fraction",
Expand All @@ -30,17 +30,42 @@ def parameters(self):
]

@pytest.fixture
def problem(self, parameters, dataset):
model = pybop.lithium_ion.SPM()
return pybop.FittingProblem(
def two_parameters(self):
return [
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.6, 0.2),
bounds=[0.58, 0.62],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.55, 0.05),
bounds=[0.53, 0.57],
),
]

@pytest.fixture
def model(self):
return pybop.lithium_ion.SPM()

@pytest.fixture
def cost(self, model, one_parameter, dataset):
problem = pybop.FittingProblem(
model,
parameters,
one_parameter,
dataset,
signal=["Terminal voltage [V]"],
signal=["Voltage [V]"],
)
return pybop.SumSquaredError(problem)

@pytest.fixture
def cost(self, problem):
def two_param_cost(self, model, two_parameters, dataset):
problem = pybop.FittingProblem(
model,
two_parameters,
dataset,
signal=["Voltage [V]"],
)
return pybop.SumSquaredError(problem)

@pytest.mark.parametrize(
Expand All @@ -58,7 +83,9 @@ def cost(self, problem):
],
)
@pytest.mark.unit
def test_optimiser_classes(self, cost, optimiser_class, expected_name):
def test_optimiser_classes(self, two_param_cost, optimiser_class, expected_name):
# Test class construction
cost = two_param_cost
opt = pybop.Optimisation(cost=cost, optimiser=optimiser_class)

assert opt.optimiser is not None
Expand All @@ -77,13 +104,19 @@ def test_optimiser_classes(self, cost, optimiser_class, expected_name):
else:
assert opt.optimiser.boundaries is None

@pytest.mark.unit
def test_single_parameter(self, cost):
# Test catch for optimisers that can only run with multiple parameters
with pytest.raises(
ValueError,
match=r"requires optimisation of >= 2 parameters at once.",
):
pybop.Optimisation(cost=cost, optimiser=pybop.CMAES)

@pytest.mark.unit
def test_default_optimiser(self, cost):
opt = pybop.Optimisation(cost=cost)
assert (
opt.optimiser.name()
== "Covariance Matrix Adaptation Evolution Strategy (CMA-ES)"
)
assert opt.optimiser.name() == "Exponential Natural Evolution Strategy (xNES)"

@pytest.mark.unit
def test_incorrect_optimiser_class(self, cost):
Expand All @@ -97,7 +130,7 @@ class RandomClass:
def test_prior_sampling(self, cost):
# Tests prior sampling
for i in range(50):
opt = pybop.Optimisation(cost=cost, optimiser=pybop.CMAES)
opt = pybop.Optimisation(cost=cost)

assert opt.x0 <= 0.62 and opt.x0 >= 0.58

Expand Down

0 comments on commit 7b500e4

Please sign in to comment.