From b8136e21de582f5817ac109d0a66fe7d494b2d54 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:18:25 +0000 Subject: [PATCH 1/6] Add Nelder-Mead optimiser from PINTS --- pybop/__init__.py | 1 + pybop/optimisers/pints_optimisers.py | 31 +++++++++++++++++++++ tests/integration/test_parameterisations.py | 1 + tests/unit/test_optimisation.py | 1 + 4 files changed, 34 insertions(+) diff --git a/pybop/__init__.py b/pybop/__init__.py index 82b7ea6ed..5ae5c4d23 100644 --- a/pybop/__init__.py +++ b/pybop/__init__.py @@ -88,6 +88,7 @@ Adam, CMAES, IRPropMin, + NelderMead, PSO, SNES, XNES, diff --git a/pybop/optimisers/pints_optimisers.py b/pybop/optimisers/pints_optimisers.py index 1f8b26148..22b7b2bc0 100644 --- a/pybop/optimisers/pints_optimisers.py +++ b/pybop/optimisers/pints_optimisers.py @@ -194,6 +194,37 @@ def __init__(self, x0, sigma0=0.1, bounds=None): super().__init__(x0, sigma0, self.boundaries) +class NelderMead(pints.NelderMead): + """ + Implements the Nelder-Mead downhill simplex method from PINTS. + + This is a deterministic local optimiser. In most update steps it performs + either 1 evaluation, or 2 sequential evaluations, so that it will not + typically benefit from parallelisation. + + Parameters + ---------- + x0 : array_like + The initial parameter vector to optimize. + sigma0 : float, optional + Initial standard deviation of the sampling distribution, defaults to 0.1. Does not appear to be used. + bounds : dict, optional + A dictionary with 'lower' and 'upper' keys containing arrays for lower and upper bounds on the parameters. + If ``None``, no bounds are enforced. + + See Also + -------- + pints.CMAES : PINTS implementation of CMA-ES algorithm. + """ + + def __init__(self, x0, sigma0=0.1, bounds=None): + if bounds is not None: + print("NOTE: Boundaries ignored by NelderMead") + + self.boundaries = None # Bounds ignored in pints.NelderMead + super().__init__(x0, sigma0, self.boundaries) + + class CMAES(pints.CMAES): """ Adapter for the Covariance Matrix Adaptation Evolution Strategy (CMA-ES) optimiser in PINTS. diff --git a/tests/integration/test_parameterisations.py b/tests/integration/test_parameterisations.py index 90c0ac212..26a602580 100644 --- a/tests/integration/test_parameterisations.py +++ b/tests/integration/test_parameterisations.py @@ -84,6 +84,7 @@ def spm_costs(self, model, parameters, cost_class, init_soc): pybop.CMAES, pybop.GradientDescent, pybop.IRPropMin, + pybop.NelderMead, pybop.PSO, pybop.SNES, pybop.XNES, diff --git a/tests/unit/test_optimisation.py b/tests/unit/test_optimisation.py index 4a9ada4ca..47a39162f 100644 --- a/tests/unit/test_optimisation.py +++ b/tests/unit/test_optimisation.py @@ -55,6 +55,7 @@ def cost(self, problem): (pybop.XNES, "Exponential Natural Evolution Strategy (xNES)"), (pybop.PSO, "Particle Swarm Optimisation (PSO)"), (pybop.IRPropMin, "iRprop-"), + (pybop.NelderMead, "Nelder-Mead"), ], ) @pytest.mark.unit From 958d552047cbc168f1d3fecb88b4c6b3ed84aaeb Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:18:38 +0000 Subject: [PATCH 2/6] Create spm_NelderMead.py --- examples/scripts/spm_NelderMead.py | 82 ++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 examples/scripts/spm_NelderMead.py diff --git a/examples/scripts/spm_NelderMead.py b/examples/scripts/spm_NelderMead.py new file mode 100644 index 000000000..5073b2cb2 --- /dev/null +++ b/examples/scripts/spm_NelderMead.py @@ -0,0 +1,82 @@ +import numpy as np + +import pybop + +# Parameter set and model definition +parameter_set = pybop.ParameterSet.pybamm("Chen2020") +model = pybop.lithium_ion.SPMe(parameter_set=parameter_set) + +# Fitting parameters +parameters = [ + pybop.Parameter( + "Negative electrode active material volume fraction", + prior=pybop.Gaussian(0.68, 0.05), + ), + pybop.Parameter( + "Positive electrode active material volume fraction", + prior=pybop.Gaussian(0.58, 0.05), + ), +] + +# Generate data +init_soc = 0.5 +sigma = 0.003 +experiment = pybop.Experiment( + [ + ( + "Discharge at 0.5C for 3 minutes (1 second period)", + "Charge at 0.5C for 3 minutes (1 second period)", + ), + ] + * 2 +) +values = model.predict(init_soc=init_soc, experiment=experiment) + + +def noise(sigma): + return np.random.normal(0, sigma, len(values["Voltage [V]"].data)) + + +# Form dataset +dataset = pybop.Dataset( + { + "Time [s]": values["Time [s]"].data, + "Current function [A]": values["Current [A]"].data, + "Voltage [V]": values["Voltage [V]"].data + noise(sigma), + "Bulk open-circuit voltage [V]": values["Bulk open-circuit voltage [V]"].data + + noise(sigma), + } +) + +signal = ["Voltage [V]", "Bulk open-circuit voltage [V]"] +# Generate problem, cost function, and optimisation class +problem = pybop.FittingProblem( + model, parameters, dataset, signal=signal, init_soc=init_soc +) +cost = pybop.RootMeanSquaredError(problem) +optim = pybop.Optimisation( + cost, + optimiser=pybop.NelderMead, + verbose=True, + allow_infeasible_solutions=True, + sigma0=0.05, +) +optim.set_max_iterations(100) +optim.set_max_unchanged_iterations(45) + +# Run optimisation +x, final_cost = optim.run() +print("Estimated parameters:", x) + +# Plot the timeseries output +pybop.quick_plot(problem, parameter_values=x, title="Optimised Comparison") + +# Plot convergence +pybop.plot_convergence(optim) + +# Plot the parameter traces +pybop.plot_parameters(optim) + +# Plot the cost landscape with optimisation path +bounds = np.array([[0.5, 0.8], [0.4, 0.7]]) +pybop.plot2d(optim, bounds=bounds, steps=15) From 3a79821c92aaa339905083e0020e6d6b79551978 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:21:42 +0000 Subject: [PATCH 3/6] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc36be14..a0949401e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- [#195](https://github.com/pybop-team/PyBOP/issues/195) - Adds the Nelder-Mead optimiser from PINTS as another option. - [#245](https://github.com/pybop-team/PyBOP/pull/245) - Updates ruff config for import linting. - [#198](https://github.com/pybop-team/PyBOP/pull/198) - Adds default subplot trace options, removes `[]` in axis plots as per SI standard, add varying signal length to quick_plot, restores design optimisation execption. - [#224](https://github.com/pybop-team/PyBOP/pull/224) - Updated prediction objects to dictionaries, cost class calculations, added `additional_variables` argument to problem class, updated scipy.minimize defualt method to Nelder-Mead, added gradient cost landscape plots with optional argument. From 7f095bf1dbe1a61cc809ff086cb004549fc80042 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Sat, 23 Mar 2024 11:56:06 +0000 Subject: [PATCH 4/6] Update pints_optimiser comments --- pybop/optimisers/pints_optimisers.py | 35 +++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/pybop/optimisers/pints_optimisers.py b/pybop/optimisers/pints_optimisers.py index 22b7b2bc0..ddb6f9472 100644 --- a/pybop/optimisers/pints_optimisers.py +++ b/pybop/optimisers/pints_optimisers.py @@ -16,7 +16,7 @@ class GradientDescent(pints.GradientDescent): Initial position from which optimization will start. sigma0 : float, optional Initial step size (default is 0.1). - bounds : sequence or ``Bounds``, optional + bounds : dict, optional Ignored by this optimiser, provided for API consistency. See Also @@ -46,7 +46,7 @@ class Adam(pints.Adam): Initial position from which optimization will start. sigma0 : float, optional Initial step size (default is 0.1). - bounds : sequence or ``Bounds``, optional + bounds : dict, optional Ignored by this optimiser, provided for API consistency. See Also @@ -77,7 +77,8 @@ class IRPropMin(pints.IRPropMin): sigma0 : float, optional Initial step size (default is 0.1). bounds : dict, optional - Lower and upper bounds for each optimization parameter. + A dictionary with 'lower' and 'upper' keys containing arrays for lower and upper + bounds on the parameters. See Also -------- @@ -109,7 +110,8 @@ class PSO(pints.PSO): sigma0 : float, optional Spread of the initial particle positions (default is 0.1). bounds : dict, optional - Lower and upper bounds for each optimization parameter. + A dictionary with 'lower' and 'upper' keys containing arrays for lower and upper + bounds on the parameters. See Also -------- @@ -147,7 +149,8 @@ class SNES(pints.SNES): sigma0 : float, optional Initial standard deviation of the sampling distribution, defaults to 0.1. bounds : dict, optional - Lower and upper bounds for each optimization parameter. + A dictionary with 'lower' and 'upper' keys containing arrays for lower and upper + bounds on the parameters. See Also -------- @@ -168,7 +171,9 @@ class XNES(pints.XNES): """ Implements the Exponential Natural Evolution Strategy (XNES) optimiser from PINTS. - XNES is an evolutionary algorithm that samples from a multivariate normal distribution, which is updated iteratively to fit the distribution of successful solutions. + XNES is an evolutionary algorithm that samples from a multivariate normal + distribution, which is updated iteratively to fit the distribution of successful + solutions. Parameters ---------- @@ -177,7 +182,8 @@ class XNES(pints.XNES): sigma0 : float, optional Initial standard deviation of the sampling distribution, defaults to 0.1. bounds : dict, optional - A dictionary with 'lower' and 'upper' keys containing arrays for lower and upper bounds on the parameters. If ``None``, no bounds are enforced. + A dictionary with 'lower' and 'upper' keys containing arrays for lower and upper + bounds on the parameters. If ``None``, no bounds are enforced. See Also -------- @@ -207,14 +213,14 @@ class NelderMead(pints.NelderMead): x0 : array_like The initial parameter vector to optimize. sigma0 : float, optional - Initial standard deviation of the sampling distribution, defaults to 0.1. Does not appear to be used. + Initial standard deviation of the sampling distribution, defaults to 0.1. + Does not appear to be used. bounds : dict, optional - A dictionary with 'lower' and 'upper' keys containing arrays for lower and upper bounds on the parameters. - If ``None``, no bounds are enforced. + Ignored by this optimiser, provided for API consistency. See Also -------- - pints.CMAES : PINTS implementation of CMA-ES algorithm. + pints.NelderMead : PINTS implementation of Nelder-Mead algorithm. """ def __init__(self, x0, sigma0=0.1, bounds=None): @@ -230,7 +236,8 @@ class CMAES(pints.CMAES): Adapter for the Covariance Matrix Adaptation Evolution Strategy (CMA-ES) optimiser in PINTS. CMA-ES is an evolutionary algorithm for difficult non-linear non-convex optimization problems. - It adapts the covariance matrix of a multivariate normal distribution to capture the shape of the cost landscape. + It adapts the covariance matrix of a multivariate normal distribution to capture the shape of + the cost landscape. Parameters ---------- @@ -239,8 +246,8 @@ class CMAES(pints.CMAES): sigma0 : float, optional Initial standard deviation of the sampling distribution, defaults to 0.1. bounds : dict, optional - A dictionary with 'lower' and 'upper' keys containing arrays for lower and upper bounds on the parameters. - If ``None``, no bounds are enforced. + A dictionary with 'lower' and 'upper' keys containing arrays for lower and upper + bounds on the parameters. If ``None``, no bounds are enforced. See Also -------- From cb614a02bd0f36130320bce8097c6cf419f77bb3 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Mon, 25 Mar 2024 16:27:10 +0000 Subject: [PATCH 5/6] Update NelderMead description Co-authored-by: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> --- pybop/optimisers/pints_optimisers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybop/optimisers/pints_optimisers.py b/pybop/optimisers/pints_optimisers.py index ddb6f9472..df9b6af0b 100644 --- a/pybop/optimisers/pints_optimisers.py +++ b/pybop/optimisers/pints_optimisers.py @@ -205,7 +205,7 @@ class NelderMead(pints.NelderMead): Implements the Nelder-Mead downhill simplex method from PINTS. This is a deterministic local optimiser. In most update steps it performs - either 1 evaluation, or 2 sequential evaluations, so that it will not + either one evaluation, or two sequential evaluations, so that it will not typically benefit from parallelisation. Parameters From 1939882f294650cfb75b92b1c3cebc078cef6b8e Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Mon, 25 Mar 2024 16:27:50 +0000 Subject: [PATCH 6/6] Change model in NelderMead example Co-authored-by: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> --- examples/scripts/spm_NelderMead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/scripts/spm_NelderMead.py b/examples/scripts/spm_NelderMead.py index 5073b2cb2..99abf5a8a 100644 --- a/examples/scripts/spm_NelderMead.py +++ b/examples/scripts/spm_NelderMead.py @@ -4,7 +4,7 @@ # Parameter set and model definition parameter_set = pybop.ParameterSet.pybamm("Chen2020") -model = pybop.lithium_ion.SPMe(parameter_set=parameter_set) +model = pybop.lithium_ion.SPM(parameter_set=parameter_set) # Fitting parameters parameters = [