From c417ced977a65991679ee0f720aa731e486615ff Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Fri, 5 Jul 2024 16:07:47 +0100 Subject: [PATCH] fix: default cuckoo sigma0, updt integration test settings, flag for applied boundaries with plot2d catch --- examples/scripts/cuckoo.py | 77 +++++++++++++++++++ pybop/optimisers/_cuckoo.py | 2 +- pybop/parameters/parameter.py | 8 +- .../integration/test_spm_parameterisations.py | 15 ++-- tests/unit/test_plots.py | 19 +++++ 5 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 examples/scripts/cuckoo.py diff --git a/examples/scripts/cuckoo.py b/examples/scripts/cuckoo.py new file mode 100644 index 00000000..83354337 --- /dev/null +++ b/examples/scripts/cuckoo.py @@ -0,0 +1,77 @@ +import numpy as np + +import pybop + +# Define model +parameter_set = pybop.ParameterSet.pybamm("Chen2020") +model = pybop.lithium_ion.SPM(parameter_set=parameter_set) + +# Fitting parameters +parameters = pybop.Parameters( + pybop.Parameter( + "Negative electrode active material volume fraction", + prior=pybop.Gaussian(0.6, 0.05), + bounds=[0.4, 0.75], + initial_value=0.41, + true_value=0.7, + ), + pybop.Parameter( + "Positive electrode active material volume fraction", + prior=pybop.Gaussian(0.48, 0.05), + bounds=[0.4, 0.75], + initial_value=0.41, + true_value=0.67, + ), +) +init_soc = 0.7 +experiment = pybop.Experiment( + [ + ( + "Discharge at 0.5C for 3 minutes (4 second period)", + "Charge at 0.5C for 3 minutes (4 second period)", + ), + ] +) +values = model.predict( + init_soc=init_soc, experiment=experiment, inputs=parameters.as_dict("true") +) + +sigma = 0.002 +corrupt_values = values["Voltage [V]"].data + 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]": corrupt_values, + } +) + +# Generate problem, cost function, and optimisation class +problem = pybop.FittingProblem(model, parameters, dataset, init_soc=init_soc) +cost = pybop.GaussianLogLikelihood(problem, sigma0=sigma * 4) +optim = pybop.Optimisation( + cost, + sigma0=None, + optimiser=pybop.CuckooSearch, + max_unchanged_iterations=55, + max_iterations=100, +) + +x, final_cost = optim.run() +print("Estimated parameters:", x) + +# 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) + +# Plot the cost landscape with optimisation path +pybop.plot2d(optim, steps=15) diff --git a/pybop/optimisers/_cuckoo.py b/pybop/optimisers/_cuckoo.py index 0b5907a6..34605272 100644 --- a/pybop/optimisers/_cuckoo.py +++ b/pybop/optimisers/_cuckoo.py @@ -47,7 +47,7 @@ class CuckooSearchImpl(PopulationBasedOptimiser): https://doi.org/10.1016/j.chaos.2011.06.004. """ - def __init__(self, x0, sigma0=0.01, boundaries=None, pa=0.25): + def __init__(self, x0, sigma0=0.05, boundaries=None, pa=0.25): super().__init__(x0, sigma0, boundaries=boundaries) # Problem dimensionality diff --git a/pybop/parameters/parameter.py b/pybop/parameters/parameter.py index 67f1896d..e7aeaa65 100644 --- a/pybop/parameters/parameter.py +++ b/pybop/parameters/parameter.py @@ -46,6 +46,7 @@ def __init__( self.true_value = true_value self.initial_value = initial_value self.value = initial_value + self.applied_prior_bounds = False self.set_bounds(bounds) self.margin = 1e-4 @@ -153,6 +154,7 @@ def set_bounds(self, bounds=None, boundary_multiplier=6): self.lower_bound = bounds[0] self.upper_bound = bounds[1] elif self.prior is not None: + self.applied_prior_bounds = True self.lower_bound = self.prior.mean - boundary_multiplier * self.prior.sigma self.upper_bound = self.prior.mean + boundary_multiplier * self.prior.sigma bounds = [self.lower_bound, self.upper_bound] @@ -417,7 +419,11 @@ def get_bounds_for_plotly(self): bounds = np.empty((len(self), 2)) for i, param in enumerate(self.param.values()): - if param.bounds is not None: + if param.applied_prior_bounds: + raise ValueError( + "Bounds were created from prior distributions. Please provide bounds for plotting." + ) + elif param.bounds is not None: bounds[i] = param.bounds else: raise ValueError("All parameters require bounds for plotting.") diff --git a/tests/integration/test_spm_parameterisations.py b/tests/integration/test_spm_parameterisations.py index 85ddd600..86a84e04 100644 --- a/tests/integration/test_spm_parameterisations.py +++ b/tests/integration/test_spm_parameterisations.py @@ -26,12 +26,12 @@ def parameters(self): return pybop.Parameters( pybop.Parameter( "Negative electrode active material volume fraction", - prior=pybop.Uniform(0.4, 0.7), - bounds=[0.375, 0.725], + prior=pybop.Uniform(0.4, 0.75), + bounds=[0.375, 0.75], ), pybop.Parameter( "Positive electrode active material volume fraction", - prior=pybop.Uniform(0.4, 0.7), + prior=pybop.Uniform(0.4, 0.75), # no bounds ), ) @@ -100,6 +100,7 @@ def test_spm_optimisers(self, optimiser, spm_costs): common_args = { "cost": spm_costs, "max_iterations": 250, + "absolute_tolerance": 1e-6, } # Add sigma0 to ground truth for GaussianLogLikelihood @@ -107,14 +108,16 @@ def test_spm_optimisers(self, optimiser, spm_costs): self.ground_truth = np.concatenate( (self.ground_truth, np.asarray([self.sigma0])) ) - + if isinstance(spm_costs, pybop.MAP): + for i in spm_costs.parameters.keys(): + spm_costs.parameters[i].prior = pybop.Uniform(0.4, 2.0) # Set sigma0 and create optimiser - sigma0 = 0.006 if isinstance(spm_costs, pybop.MAP) else None + sigma0 = 0.05 if isinstance(spm_costs, pybop.MAP) else None optim = optimiser(sigma0=sigma0, **common_args) # Set max unchanged iterations for BasePintsOptimisers if issubclass(optimiser, pybop.BasePintsOptimiser): - optim.set_max_unchanged_iterations(iterations=45, absolute_tolerance=1e-5) + optim.set_max_unchanged_iterations(iterations=55) # AdamW will use lowest sigma0 for learning rate, so allow more iterations if issubclass(optimiser, (pybop.AdamW, pybop.IRPropMin)) and isinstance( diff --git a/tests/unit/test_plots.py b/tests/unit/test_plots.py index 4eb2dc47..3698e674 100644 --- a/tests/unit/test_plots.py +++ b/tests/unit/test_plots.py @@ -193,3 +193,22 @@ def test_plot2d_incorrect_number_of_parameters(self, model, dataset): fitting_problem = pybop.FittingProblem(model, parameters, dataset) cost = pybop.SumSquaredError(fitting_problem) pybop.plot2d(cost) + + # Test with applied prior bounds + parameters = pybop.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), + bounds=[0.4, 0.7], + ), + ) + fitting_problem = pybop.FittingProblem(model, parameters, dataset) + cost = pybop.SumSquaredError(fitting_problem) + with pytest.raises( + ValueError, match="Bounds were created from prior distributions" + ): + pybop.plot2d(cost)