From 35212bb07891bee4afcb5ee3e436a57230ddc5b7 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Wed, 22 Nov 2023 13:35:11 +0000 Subject: [PATCH 1/8] Add pints' optimisers and corresponding examples --- examples/scripts/{CMAES.py => spm_CMAES.py} | 2 +- examples/scripts/spm_IRPropMin.py | 55 ++++++++++++++ examples/scripts/spm_SNES.py | 55 ++++++++++++++ examples/scripts/spm_XNES.py | 55 ++++++++++++++ examples/scripts/spm_adam.py | 59 +++++++++++++++ examples/scripts/spm_pso.py | 55 ++++++++++++++ pybop/__init__.py | 2 +- pybop/optimisers/pints_optimisers.py | 84 ++++++++++++++++++++- 8 files changed, 363 insertions(+), 4 deletions(-) rename examples/scripts/{CMAES.py => spm_CMAES.py} (96%) create mode 100644 examples/scripts/spm_IRPropMin.py create mode 100644 examples/scripts/spm_SNES.py create mode 100644 examples/scripts/spm_XNES.py create mode 100644 examples/scripts/spm_adam.py create mode 100644 examples/scripts/spm_pso.py diff --git a/examples/scripts/CMAES.py b/examples/scripts/spm_CMAES.py similarity index 96% rename from examples/scripts/CMAES.py rename to examples/scripts/spm_CMAES.py index 65315b41..7f044409 100644 --- a/examples/scripts/CMAES.py +++ b/examples/scripts/spm_CMAES.py @@ -3,7 +3,7 @@ import matplotlib.pyplot as plt 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 = [ diff --git a/examples/scripts/spm_IRPropMin.py b/examples/scripts/spm_IRPropMin.py new file mode 100644 index 00000000..2d4dd2ec --- /dev/null +++ b/examples/scripts/spm_IRPropMin.py @@ -0,0 +1,55 @@ +import pybop +import numpy as np +import matplotlib.pyplot as plt + +parameter_set = pybop.ParameterSet("pybamm", "Chen2020") +model = pybop.lithium_ion.SPM(parameter_set=parameter_set) + +# Fitting parameters +parameters = [ + pybop.Parameter( + "Negative electrode active material volume fraction", + prior=pybop.Gaussian(0.7, 0.05), + bounds=[0.6, 0.9], + ), + pybop.Parameter( + "Positive electrode active material volume fraction", + prior=pybop.Gaussian(0.58, 0.05), + bounds=[0.5, 0.8], + ), +] + +sigma = 0.001 +t_eval = np.arange(0, 900, 2) +values = model.predict(t_eval=t_eval) +CorruptValues = values["Terminal voltage [V]"].data + np.random.normal( + 0, sigma, len(t_eval) +) + +dataset = [ + pybop.Dataset("Time [s]", t_eval), + pybop.Dataset("Current function [A]", values["Current [A]"].data), + pybop.Dataset("Terminal voltage [V]", CorruptValues), +] + +# Generate problem, cost function, and optimisation class +problem = pybop.Problem(model, parameters, dataset) +cost = pybop.SumSquaredError(problem) +optim = pybop.Optimisation(cost, optimiser=pybop.IRPropMin) +optim.set_max_iterations(100) + +x, final_cost = optim.run() +print("Estimated parameters:", x) + +# Show the generated data +simulated_values = problem.evaluate(x) + +plt.figure(dpi=100) +plt.xlabel("Time", fontsize=12) +plt.ylabel("Values", fontsize=12) +plt.plot(t_eval, CorruptValues, label="Measured") +plt.fill_between(t_eval, simulated_values - sigma, simulated_values + sigma, alpha=0.2) +plt.plot(t_eval, simulated_values, label="Simulated") +plt.legend(bbox_to_anchor=(0.6, 1), loc="upper left", fontsize=12) +plt.tick_params(axis="both", labelsize=12) +plt.show() diff --git a/examples/scripts/spm_SNES.py b/examples/scripts/spm_SNES.py new file mode 100644 index 00000000..f5db3c9b --- /dev/null +++ b/examples/scripts/spm_SNES.py @@ -0,0 +1,55 @@ +import pybop +import numpy as np +import matplotlib.pyplot as plt + +parameter_set = pybop.ParameterSet("pybamm", "Chen2020") +model = pybop.lithium_ion.SPM(parameter_set=parameter_set) + +# Fitting parameters +parameters = [ + pybop.Parameter( + "Negative electrode active material volume fraction", + prior=pybop.Gaussian(0.7, 0.05), + bounds=[0.6, 0.9], + ), + pybop.Parameter( + "Positive electrode active material volume fraction", + prior=pybop.Gaussian(0.58, 0.05), + bounds=[0.5, 0.8], + ), +] + +sigma = 0.001 +t_eval = np.arange(0, 900, 2) +values = model.predict(t_eval=t_eval) +CorruptValues = values["Terminal voltage [V]"].data + np.random.normal( + 0, sigma, len(t_eval) +) + +dataset = [ + pybop.Dataset("Time [s]", t_eval), + pybop.Dataset("Current function [A]", values["Current [A]"].data), + pybop.Dataset("Terminal voltage [V]", CorruptValues), +] + +# Generate problem, cost function, and optimisation class +problem = pybop.Problem(model, parameters, dataset) +cost = pybop.SumSquaredError(problem) +optim = pybop.Optimisation(cost, optimiser=pybop.SNES) +optim.set_max_iterations(100) + +x, final_cost = optim.run() +print("Estimated parameters:", x) + +# Show the generated data +simulated_values = problem.evaluate(x) + +plt.figure(dpi=100) +plt.xlabel("Time", fontsize=12) +plt.ylabel("Values", fontsize=12) +plt.plot(t_eval, CorruptValues, label="Measured") +plt.fill_between(t_eval, simulated_values - sigma, simulated_values + sigma, alpha=0.2) +plt.plot(t_eval, simulated_values, label="Simulated") +plt.legend(bbox_to_anchor=(0.6, 1), loc="upper left", fontsize=12) +plt.tick_params(axis="both", labelsize=12) +plt.show() diff --git a/examples/scripts/spm_XNES.py b/examples/scripts/spm_XNES.py new file mode 100644 index 00000000..37939245 --- /dev/null +++ b/examples/scripts/spm_XNES.py @@ -0,0 +1,55 @@ +import pybop +import numpy as np +import matplotlib.pyplot as plt + +parameter_set = pybop.ParameterSet("pybamm", "Chen2020") +model = pybop.lithium_ion.SPM(parameter_set=parameter_set) + +# Fitting parameters +parameters = [ + pybop.Parameter( + "Negative electrode active material volume fraction", + prior=pybop.Gaussian(0.7, 0.05), + bounds=[0.6, 0.9], + ), + pybop.Parameter( + "Positive electrode active material volume fraction", + prior=pybop.Gaussian(0.58, 0.05), + bounds=[0.5, 0.8], + ), +] + +sigma = 0.001 +t_eval = np.arange(0, 900, 2) +values = model.predict(t_eval=t_eval) +CorruptValues = values["Terminal voltage [V]"].data + np.random.normal( + 0, sigma, len(t_eval) +) + +dataset = [ + pybop.Dataset("Time [s]", t_eval), + pybop.Dataset("Current function [A]", values["Current [A]"].data), + pybop.Dataset("Terminal voltage [V]", CorruptValues), +] + +# Generate problem, cost function, and optimisation class +problem = pybop.Problem(model, parameters, dataset) +cost = pybop.SumSquaredError(problem) +optim = pybop.Optimisation(cost, optimiser=pybop.XNES) +optim.set_max_iterations(100) + +x, final_cost = optim.run() +print("Estimated parameters:", x) + +# Show the generated data +simulated_values = problem.evaluate(x) + +plt.figure(dpi=100) +plt.xlabel("Time", fontsize=12) +plt.ylabel("Values", fontsize=12) +plt.plot(t_eval, CorruptValues, label="Measured") +plt.fill_between(t_eval, simulated_values - sigma, simulated_values + sigma, alpha=0.2) +plt.plot(t_eval, simulated_values, label="Simulated") +plt.legend(bbox_to_anchor=(0.6, 1), loc="upper left", fontsize=12) +plt.tick_params(axis="both", labelsize=12) +plt.show() diff --git a/examples/scripts/spm_adam.py b/examples/scripts/spm_adam.py new file mode 100644 index 00000000..27949e9a --- /dev/null +++ b/examples/scripts/spm_adam.py @@ -0,0 +1,59 @@ +import pybop +import numpy as np +import matplotlib.pyplot as plt + +# 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.7, 0.05), + bounds=[0.6, 0.9], + ), + pybop.Parameter( + "Positive electrode active material volume fraction", + prior=pybop.Gaussian(0.58, 0.05), + bounds=[0.5, 0.8], + ), +] + +# Generate data +sigma = 0.001 +t_eval = np.arange(0, 900, 2) +values = model.predict(t_eval=t_eval) +corrupt_values = values["Terminal voltage [V]"].data + np.random.normal( + 0, sigma, len(t_eval) +) + +# Dataset definition +dataset = [ + pybop.Dataset("Time [s]", t_eval), + pybop.Dataset("Current function [A]", values["Current [A]"].data), + pybop.Dataset("Terminal voltage [V]", corrupt_values), +] + +# Generate problem, cost function, and optimisation class +problem = pybop.Problem(model, parameters, dataset) +cost = pybop.SumSquaredError(problem) +optim = pybop.Optimisation(cost, optimiser=pybop.Adam) +optim.set_max_iterations(100) + +# Run optimisation +x, final_cost = optim.run() +print("Estimated parameters:", x) + +# Show the generated data +simulated_values = problem.evaluate(x) + +plt.figure(dpi=100) +plt.xlabel("Time", fontsize=12) +plt.ylabel("Values", fontsize=12) +plt.plot(t_eval, corrupt_values, label="Measured") +plt.fill_between(t_eval, simulated_values - sigma, simulated_values + sigma, alpha=0.2) +plt.plot(t_eval, simulated_values, label="Simulated") +plt.legend(bbox_to_anchor=(0.6, 1), loc="upper left", fontsize=12) +plt.tick_params(axis="both", labelsize=12) +plt.show() diff --git a/examples/scripts/spm_pso.py b/examples/scripts/spm_pso.py new file mode 100644 index 00000000..9a9cb5aa --- /dev/null +++ b/examples/scripts/spm_pso.py @@ -0,0 +1,55 @@ +import pybop +import numpy as np +import matplotlib.pyplot as plt + +parameter_set = pybop.ParameterSet("pybamm", "Chen2020") +model = pybop.lithium_ion.SPM(parameter_set=parameter_set) + +# Fitting parameters +parameters = [ + pybop.Parameter( + "Negative electrode active material volume fraction", + prior=pybop.Gaussian(0.7, 0.05), + bounds=[0.6, 0.9], + ), + pybop.Parameter( + "Positive electrode active material volume fraction", + prior=pybop.Gaussian(0.58, 0.05), + bounds=[0.5, 0.8], + ), +] + +sigma = 0.001 +t_eval = np.arange(0, 900, 2) +values = model.predict(t_eval=t_eval) +CorruptValues = values["Terminal voltage [V]"].data + np.random.normal( + 0, sigma, len(t_eval) +) + +dataset = [ + pybop.Dataset("Time [s]", t_eval), + pybop.Dataset("Current function [A]", values["Current [A]"].data), + pybop.Dataset("Terminal voltage [V]", CorruptValues), +] + +# Generate problem, cost function, and optimisation class +problem = pybop.Problem(model, parameters, dataset) +cost = pybop.SumSquaredError(problem) +optim = pybop.Optimisation(cost, optimiser=pybop.PSO) +optim.set_max_iterations(100) + +x, final_cost = optim.run() +print("Estimated parameters:", x) + +# Show the generated data +simulated_values = problem.evaluate(x) + +plt.figure(dpi=100) +plt.xlabel("Time", fontsize=12) +plt.ylabel("Values", fontsize=12) +plt.plot(t_eval, CorruptValues, label="Measured") +plt.fill_between(t_eval, simulated_values - sigma, simulated_values + sigma, alpha=0.2) +plt.plot(t_eval, simulated_values, label="Simulated") +plt.legend(bbox_to_anchor=(0.6, 1), loc="upper left", fontsize=12) +plt.tick_params(axis="both", labelsize=12) +plt.show() diff --git a/pybop/__init__.py b/pybop/__init__.py index 29dcd88b..b4006b3c 100644 --- a/pybop/__init__.py +++ b/pybop/__init__.py @@ -50,7 +50,7 @@ from .optimisers.base_optimiser import BaseOptimiser from .optimisers.nlopt_optimize import NLoptOptimize from .optimisers.scipy_minimize import SciPyMinimize -from .optimisers.pints_optimisers import GradientDescent, CMAES +from .optimisers.pints_optimisers import GradientDescent, Adam, CMAES, IRPropMin, PSO, SNES, XNES # # Parameter classes diff --git a/pybop/optimisers/pints_optimisers.py b/pybop/optimisers/pints_optimisers.py index 6524cb60..741c6651 100644 --- a/pybop/optimisers/pints_optimisers.py +++ b/pybop/optimisers/pints_optimisers.py @@ -4,19 +4,99 @@ class GradientDescent(pints.GradientDescent): """ Gradient descent optimiser. Inherits from the PINTS gradient descent class. + https://github.com/pints-team/pints/blob/main/pints/_optimisers/_gradient_descent.py """ def __init__(self, x0, sigma0=0.1, bounds=None): if bounds is not None: print("Boundaries ignored by GradientDescent") - boundaries = None # Bounds ignored in pints.GradDesc - super().__init__(x0, sigma0, boundaries) + self.boundaries = None # Bounds ignored in pints.GradDesc + super().__init__(x0, sigma0, self.boundaries) + + +class Adam(pints.Adam): + """ + Adam optimiser. Inherits from the PINTS Adam class. + https://github.com/pints-team/pints/blob/main/pints/_optimisers/_adam.py + """ + + def __init__(self, x0, sigma0=0.1, bounds=None): + if bounds is not None: + print("Boundaries ignored by Adam") + + self.boundaries = None # Bounds ignored in pints.Adam + super().__init__(x0, sigma0, self.boundaries) + + +class IRPropMin(pints.IRPropMin): + """ + IRProp- optimiser. Inherits from the PINTS IRPropMinus class. + https://github.com/pints-team/pints/blob/main/pints/_optimisers/_irpropmin.py + """ + + def __init__(self, x0, sigma0=0.1, bounds=None): + if bounds is not None: + self.boundaries = pints.RectangularBoundaries( + bounds["lower"], bounds["upper"] + ) + else: + self.boundaries = None + super().__init__(x0, sigma0, self.boundaries) + + +class PSO(pints.PSO): + """ + Particle swarm optimiser. Inherits from the PINTS PSO class. + https://github.com/pints-team/pints/blob/main/pints/_optimisers/_pso.py + """ + + def __init__(self, x0, sigma0=0.1, bounds=None): + if bounds is not None: + self.boundaries = pints.RectangularBoundaries( + bounds["lower"], bounds["upper"] + ) + else: + self.boundaries = None + super().__init__(x0, sigma0, self.boundaries) + + +class SNES(pints.SNES): + """ + Stochastic natural evolution strategy optimiser. Inherits from the PINTS SNES class. + https://github.com/pints-team/pints/blob/main/pints/_optimisers/_snes.py + """ + + def __init__(self, x0, sigma0=0.1, bounds=None): + if bounds is not None: + self.boundaries = pints.RectangularBoundaries( + bounds["lower"], bounds["upper"] + ) + else: + self.boundaries = None + super().__init__(x0, sigma0, self.boundaries) + + +class XNES(pints.XNES): + """ + Exponential natural evolution strategy optimiser. Inherits from the PINTS XNES class. + https://github.com/pints-team/pints/blob/main/pints/_optimisers/_xnes.py + """ + + def __init__(self, x0, sigma0=0.1, bounds=None): + if bounds is not None: + self.boundaries = pints.RectangularBoundaries( + bounds["lower"], bounds["upper"] + ) + else: + self.boundaries = None + super().__init__(x0, sigma0, self.boundaries) class CMAES(pints.CMAES): """ Class for the PINTS optimisation. Extends the BaseOptimiser class. + https://github.com/pints-team/pints/blob/main/pints/_optimisers/_cmaes.py """ def __init__(self, x0, sigma0=0.1, bounds=None): From 0ea2a1c9a2f6be17b0b6c719abf6007e852823bb Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Wed, 22 Nov 2023 13:51:28 +0000 Subject: [PATCH 2/8] Updt Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4155c6e..7c094508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # [Unreleased](https://github.com/pybop-team/PyBOP) + - Adds PSO, SNES, XNES, ADAM, and IPropMin optimisers to PintsOptimisers() class + # [v23.11](https://github.com/pybop-team/PyBOP/releases/tag/v23.11) - Initial release - Adds Pints, NLOpt, and SciPy optimisers From 95945ce92db86fbccc642ff2bec1292cfce4c809 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Wed, 22 Nov 2023 14:20:34 +0000 Subject: [PATCH 3/8] Add pytest --examples marker, updt. tests for addition optimisers, rm multi-soc from test_spm_optimisers + updt. to SPM --- conftest.py | 18 ++++++++++---- pybop/__init__.py | 10 +++++++- tests/unit/test_examples.py | 2 +- tests/unit/test_parameterisations.py | 37 +++++++++++++++++++++++----- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/conftest.py b/conftest.py index b37cbd0f..3b3e2442 100644 --- a/conftest.py +++ b/conftest.py @@ -12,13 +12,21 @@ def pytest_addoption(parser): def pytest_configure(config): config.addinivalue_line("markers", "unit: mark test as a unit test") + config.addinivalue_line("markers", "examples: mark test as an example") def pytest_collection_modifyitems(config, items): + def skip_marker(marker_name, reason): + skip = pytest.mark.skip(reason=reason) + for item in items: + if marker_name in item.keywords: + item.add_marker(skip) + if config.getoption("--unit"): - # --unit given in cli: do not skip unit tests + skip_marker("examples", "need --examples option to run") + return + + if config.getoption("--examples"): return - skip_unit = pytest.mark.skip(reason="need --unit option to run") - for item in items: - if "unit" in item.keywords: - item.add_marker(skip_unit) + + skip_marker("unit", "need --unit option to run") diff --git a/pybop/__init__.py b/pybop/__init__.py index b4006b3c..0e933b8e 100644 --- a/pybop/__init__.py +++ b/pybop/__init__.py @@ -50,7 +50,15 @@ from .optimisers.base_optimiser import BaseOptimiser from .optimisers.nlopt_optimize import NLoptOptimize from .optimisers.scipy_minimize import SciPyMinimize -from .optimisers.pints_optimisers import GradientDescent, Adam, CMAES, IRPropMin, PSO, SNES, XNES +from .optimisers.pints_optimisers import ( + GradientDescent, + Adam, + CMAES, + IRPropMin, + PSO, + SNES, + XNES, +) # # Parameter classes diff --git a/tests/unit/test_examples.py b/tests/unit/test_examples.py index 6e8fc09e..dffa084e 100644 --- a/tests/unit/test_examples.py +++ b/tests/unit/test_examples.py @@ -9,7 +9,7 @@ class TestExamples: A class to test the example scripts. """ - @pytest.mark.unit + @pytest.mark.examples def test_example_scripts(self): path_to_example_scripts = os.path.join( pybop.script_path, "..", "examples", "scripts" diff --git a/tests/unit/test_parameterisations.py b/tests/unit/test_parameterisations.py index 142e590d..c1810063 100644 --- a/tests/unit/test_parameterisations.py +++ b/tests/unit/test_parameterisations.py @@ -62,12 +62,12 @@ def test_spm(self, init_soc): np.testing.assert_allclose(final_cost, 0, atol=1e-2) np.testing.assert_allclose(x, x0, atol=1e-1) - @pytest.mark.parametrize("init_soc", [0.3, 0.7]) + @pytest.mark.parametrize("init_soc", [0.5]) @pytest.mark.unit - def test_spme_optimisers(self, init_soc): + def test_spm_optimisers(self, init_soc): # Define model parameter_set = pybop.ParameterSet("pybamm", "Chen2020") - model = pybop.lithium_ion.SPMe(parameter_set=parameter_set) + model = pybop.lithium_ion.SPM(parameter_set=parameter_set) # Form dataset x0 = np.array([0.52, 0.63]) @@ -100,16 +100,26 @@ def test_spme_optimisers(self, init_soc): problem = pybop.Problem( model, parameters, dataset, signal=signal, init_soc=init_soc ) - cost = pybop.RootMeanSquaredError(problem) + cost = pybop.SumSquaredError(problem) # Select optimisers - optimisers = [pybop.NLoptOptimize, pybop.SciPyMinimize, pybop.CMAES] + optimisers = [ + pybop.NLoptOptimize, + pybop.SciPyMinimize, + pybop.CMAES, + pybop.Adam, + pybop.GradientDescent, + pybop.PSO, + pybop.XNES, + pybop.SNES, + pybop.IRPropMin, + ] # Test each optimiser for optimiser in optimisers: parameterisation = pybop.Optimisation(cost=cost, optimiser=optimiser) - if optimiser == pybop.CMAES: + if optimiser in [pybop.CMAES]: parameterisation.set_f_guessed_tracking(True) assert parameterisation._use_f_guessed is True parameterisation.set_max_iterations(1) @@ -121,6 +131,21 @@ def test_spme_optimisers(self, init_soc): x, final_cost = parameterisation.run() assert parameterisation._max_iterations == 250 + elif optimiser in [pybop.GradientDescent]: + parameterisation.optimiser.set_learning_rate(0.025) + parameterisation.set_max_iterations(250) + x, final_cost = parameterisation.run() + + elif optimiser in [ + pybop.PSO, + pybop.XNES, + pybop.SNES, + pybop.Adam, + pybop.IRPropMin, + ]: + parameterisation.set_max_iterations(250) + x, final_cost = parameterisation.run() + else: x, final_cost = parameterisation.run() From e0053c802aa6ad1df80a7550c694f66413030ee3 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Wed, 22 Nov 2023 14:47:44 +0000 Subject: [PATCH 4/8] Updt noxfile w/ pytest --show-locals log output argument --- noxfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index c88e483e..b775c34c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -8,14 +8,14 @@ def unit(session): session.run_always("pip", "install", "-e", ".") session.install("pytest") - session.run("pytest", "--unit", "-v") + session.run("pytest", "--unit", "-v", "--showlocals") @nox.session def coverage(session): session.run_always("pip", "install", "-e", ".") session.install("pytest-cov") - session.run("pytest", "--unit", "-v", "--cov", "--cov-report=xml") + session.run("pytest", "--unit", "-v", "--cov", "--cov-report=xml", "--showlocals") @nox.session From 94a561252abe405498be95896f25778b2d99bcde Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Wed, 22 Nov 2023 15:37:55 +0000 Subject: [PATCH 5/8] Add SPMe test for coverage --- tests/unit/test_models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index ce73000a..6925391d 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -34,7 +34,7 @@ def test_predict_without_pybamm(self): @pytest.mark.unit def test_predict_with_inputs(self): - # Define model + # Define SPM model = pybop.lithium_ion.SPM() t_eval = np.linspace(0, 10, 100) inputs = { @@ -45,6 +45,11 @@ def test_predict_with_inputs(self): res = model.predict(t_eval=t_eval, inputs=inputs) assert len(res["Terminal voltage [V]"].data) == 100 + # Define SPMe + model = pybop.lithium_ion.SPMe() + res = model.predict(t_eval=t_eval, inputs=inputs) + assert len(res["Terminal voltage [V]"].data) == 100 + @pytest.mark.unit def test_build(self): model = pybop.lithium_ion.SPM() From 95bb2beba0e55675c0aa9d07461858f51883a587 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Wed, 22 Nov 2023 15:54:49 +0000 Subject: [PATCH 6/8] Updt. pytest markers and logic, add examples to noxfile cov --- conftest.py | 4 ++++ noxfile.py | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 3b3e2442..8aa4af3a 100644 --- a/conftest.py +++ b/conftest.py @@ -8,6 +8,9 @@ def pytest_addoption(parser): parser.addoption( "--unit", action="store_true", default=False, help="run unit tests" ) + parser.addoption( + "--examples", action="store_true", default=False, help="run examples tests" + ) def pytest_configure(config): @@ -27,6 +30,7 @@ def skip_marker(marker_name, reason): return if config.getoption("--examples"): + skip_marker("unit", "need --unit option to run") return skip_marker("unit", "need --unit option to run") diff --git a/noxfile.py b/noxfile.py index b775c34c..195703d6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -15,7 +15,15 @@ def unit(session): def coverage(session): session.run_always("pip", "install", "-e", ".") session.install("pytest-cov") - session.run("pytest", "--unit", "-v", "--cov", "--cov-report=xml", "--showlocals") + session.run( + "pytest", + "--unit", + "--examples", + "-v", + "--cov", + "--cov-report=xml", + "--showlocals", + ) @nox.session From d88430977b706bf80aeb100741eebbb46af42aa0 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Thu, 23 Nov 2023 10:28:10 +0000 Subject: [PATCH 7/8] Refactor optimisation tests, add logic tests for new optimisers w/ bounds=None --- tests/unit/test_optimisation.py | 152 ++++++++++++++------------------ 1 file changed, 65 insertions(+), 87 deletions(-) diff --git a/tests/unit/test_optimisation.py b/tests/unit/test_optimisation.py index 5bbb4998..21ecf16a 100644 --- a/tests/unit/test_optimisation.py +++ b/tests/unit/test_optimisation.py @@ -9,58 +9,17 @@ class TestOptimisation: A class to test the optimisation class. """ - @pytest.mark.unit - def test_standalone(self): - # Build an Optimisation problem with a StandaloneCost - cost = StandaloneCost() - - opt = pybop.Optimisation(cost=cost, optimiser=pybop.NLoptOptimize) - - assert len(opt.x0) == opt.n_parameters - - x, final_cost = opt.run() - - np.testing.assert_allclose(x, 0, atol=1e-2) - np.testing.assert_allclose(final_cost, 42, atol=1e-2) - - @pytest.mark.unit - def test_prior_sampling(self): - # Tests prior sampling - model = pybop.lithium_ion.SPM() - - dataset = [ - pybop.Dataset("Time [s]", np.linspace(0, 3600, 100)), - pybop.Dataset("Current function [A]", np.zeros(100)), - pybop.Dataset("Terminal voltage [V]", np.ones(100)), - ] - - param = [ - pybop.Parameter( - "Negative electrode active material volume fraction", - prior=pybop.Gaussian(0.75, 0.2), - bounds=[0.73, 0.77], - ) - ] - - signal = "Terminal voltage [V]" - problem = pybop.Problem(model, param, dataset, signal=signal) - cost = pybop.RootMeanSquaredError(problem) - - for i in range(50): - opt = pybop.Optimisation(cost=cost, optimiser=pybop.NLoptOptimize) - - assert opt.x0 <= 0.77 and opt.x0 >= 0.73 - - @pytest.mark.unit - def test_optimiser_construction(self): - # Tests construction of optimisers - - dataset = [ + @pytest.fixture + def dataset(self): + return [ pybop.Dataset("Time [s]", np.linspace(0, 360, 10)), pybop.Dataset("Current function [A]", np.zeros(10)), pybop.Dataset("Terminal voltage [V]", np.ones(10)), ] - parameters = [ + + @pytest.fixture + def parameters(self): + return [ pybop.Parameter( "Negative electrode active material volume fraction", prior=pybop.Gaussian(0.75, 0.2), @@ -68,69 +27,88 @@ def test_optimiser_construction(self): ) ] - problem = pybop.Problem( + @pytest.fixture + def problem(self, parameters, dataset): + return pybop.Problem( pybop.lithium_ion.SPM(), parameters, dataset, signal="Terminal voltage [V]" ) - cost = pybop.SumSquaredError(problem) - # Test construction of optimisers - # NLopt - opt = pybop.Optimisation(cost=cost, optimiser=pybop.NLoptOptimize) - assert opt.optimiser is not None - assert opt.optimiser.name == "NLoptOptimize" - assert opt.optimiser.n_param == 1 + @pytest.fixture + def cost(self, problem): + return pybop.SumSquaredError(problem) + + @pytest.mark.parametrize( + "optimiser_class, expected_name", + [ + (pybop.NLoptOptimize, "NLoptOptimize"), + (pybop.SciPyMinimize, "SciPyMinimize"), + (pybop.GradientDescent, "Gradient descent"), + (pybop.Adam, "Adam"), + (pybop.CMAES, "Covariance Matrix Adaptation Evolution Strategy (CMA-ES)"), + (pybop.SNES, "Seperable Natural Evolution Strategy (SNES)"), + (pybop.XNES, "Exponential Natural Evolution Strategy (xNES)"), + (pybop.PSO, "Particle Swarm Optimisation (PSO)"), + (pybop.IRPropMin, "iRprop-"), + ], + ) + @pytest.mark.unit + def test_optimiser_classes(self, cost, optimiser_class, expected_name): + if optimiser_class not in [pybop.NLoptOptimize, pybop.SciPyMinimize]: + cost.bounds = None + opt = pybop.Optimisation(cost=cost, optimiser=optimiser_class) + assert opt.optimiser.boundaries is None + assert opt.optimiser.name() == expected_name + else: + opt = pybop.Optimisation(cost=cost, optimiser=optimiser_class) + assert opt.optimiser.name == expected_name - # Gradient Descent - opt = pybop.Optimisation(cost=cost, optimiser=pybop.GradientDescent) assert opt.optimiser is not None + if optimiser_class == pybop.NLoptOptimize: + assert opt.optimiser.n_param == 1 - # None + @pytest.mark.unit + def test_default_optimiser_with_bounds(self, cost): opt = pybop.Optimisation(cost=cost) - assert opt.optimiser is not None assert ( opt.optimiser.name() == "Covariance Matrix Adaptation Evolution Strategy (CMA-ES)" ) - # None with no bounds + @pytest.mark.unit + def test_default_optimiser_no_bounds(self, cost): cost.bounds = None opt = pybop.Optimisation(cost=cost) assert opt.optimiser.boundaries is None - # SciPy - opt = pybop.Optimisation(cost=cost, optimiser=pybop.SciPyMinimize) - assert opt.optimiser is not None - assert opt.optimiser.name == "SciPyMinimize" - - # Incorrect class - class randomclass: + @pytest.mark.unit + def test_incorrect_optimiser_class(self, cost): + class RandomClass: pass with pytest.raises(ValueError): - pybop.Optimisation(cost=cost, optimiser=randomclass) + pybop.Optimisation(cost=cost, optimiser=RandomClass) @pytest.mark.unit - def test_halting(self): - # Tests halting criteria - model = pybop.lithium_ion.SPM() - - dataset = [ - pybop.Dataset("Time [s]", np.linspace(0, 3600, 100)), - pybop.Dataset("Current function [A]", np.zeros(100)), - pybop.Dataset("Terminal voltage [V]", np.ones(100)), - ] + def test_standalone(self): + # Build an Optimisation problem with a StandaloneCost + cost = StandaloneCost() + opt = pybop.Optimisation(cost=cost, optimiser=pybop.NLoptOptimize) + x, final_cost = opt.run() - param = [ - pybop.Parameter( - "Negative electrode active material volume fraction", - prior=pybop.Gaussian(0.75, 0.2), - bounds=[0.73, 0.77], - ) - ] + assert len(opt.x0) == opt.n_parameters + np.testing.assert_allclose(x, 0, atol=1e-2) + np.testing.assert_allclose(final_cost, 42, atol=1e-2) + + @pytest.mark.unit + def test_prior_sampling(self, cost): + # Tests prior sampling + for i in range(50): + opt = pybop.Optimisation(cost=cost, optimiser=pybop.NLoptOptimize) - problem = pybop.Problem(model, param, dataset, signal="Terminal voltage [V]") - cost = pybop.SumSquaredError(problem) + assert opt.x0 <= 0.77 and opt.x0 >= 0.73 + @pytest.mark.unit + def test_halting(self, cost): # Test max evalutions optim = pybop.Optimisation(cost=cost, optimiser=pybop.GradientDescent) optim.set_max_evaluations(10) From eee379d05c68ad5406030a6496fb4a7ac5e4ea10 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Thu, 23 Nov 2023 19:05:07 +0000 Subject: [PATCH 8/8] Updt. changelog --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c094508..c5780d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # [Unreleased](https://github.com/pybop-team/PyBOP) - - Adds PSO, SNES, XNES, ADAM, and IPropMin optimisers to PintsOptimisers() class +- [#116](https://github.com/pybop-team/PyBOP/issues/116) - Adds PSO, SNES, XNES, ADAM, and IPropMin optimisers to PintsOptimisers() class # [v23.11](https://github.com/pybop-team/PyBOP/releases/tag/v23.11) - - Initial release - - Adds Pints, NLOpt, and SciPy optimisers - - Adds SumofSquareError and RootMeanSquareError cost functions - - Adds Parameter and dataset classes +- Initial release +- Adds Pints, NLOpt, and SciPy optimisers +- Adds SumofSquareError and RootMeanSquareError cost functions +- Adds Parameter and dataset classes