From 99457db3c28bf0aec3ec705ba9ba2da9e77b2046 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Tue, 7 Nov 2023 18:44:57 +0000 Subject: [PATCH 01/13] Updt readme + contributing --- CONTRIBUTING.md | 21 ++----- README.md | 145 +++++++++++++++++++++--------------------------- 2 files changed, 68 insertions(+), 98 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f65361cd2..5926bedc1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ If you'd like to contribute to PyBOP, please have a look at the [pre-commit](#pr Before you commit any code, please perform the following checks: -- [All tests pass](#testing): `$ nox -s unit_test` +- [All tests pass](#testing): `$ nox -s unit` ### Installing and using pre-commit @@ -35,7 +35,7 @@ We use [GIT](https://en.wikipedia.org/wiki/Git) and [GitHub](https://en.wikipedi 2. Create a [branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/) of this repo (ideally on your own [fork](https://help.github.com/articles/fork-a-repo/)), where all changes will be made 3. Download the source code onto your local system, by [cloning](https://help.github.com/articles/cloning-a-repository/) the repository (or your fork of the repository). 4. [Install](Developer-Install) PyBOP with the developer options. -5. [Test](#testing) if your installation worked, using the test script: `$ python run-tests.py --unit`. +5. [Test](#testing) if your installation worked: `$ pytest --unit -v`. You now have everything you need to start making changes! @@ -120,13 +120,13 @@ All code requires testing. We use the [pytest](https://docs.pytest.org/en/) pack If you have nox installed, to run unit tests, type ```bash -nox -s unit_test +nox -s unit ``` else, type ```bash -python run-tests.py +pytest --unit -v ``` ### Writing tests @@ -146,24 +146,15 @@ This also means that, if you can't fix the bug yourself, it will be much easier 1. Run individual test scripts instead of the whole test suite: ```bash - python tests/unit/path/to/test + pytest tests/unit/path/to/test ``` You can also run an individual test from a particular script, e.g. ```bash - python tests/unit/test_quick_plot.py TestQuickPlot.test_failure + pytest tests/unit/test_quick_plot.py TestQuickPlot.test_failure ``` - If you want to run several, but not all, the tests from a script, you can restrict which tests are run from a particular script by using the skipping decorator: - - ```python - @unittest.skip("") - def test_bit_of_code(self): - ... - ``` - - or by just commenting out all the tests you don't want to run. 2. Set break-points, either in your IDE or using the Python debugging module. To use the latter, add the following line where you want to set the break point ```python diff --git a/README.md b/README.md index cf39db405..447a35b4f 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,12 @@ ## PyBOP -PyBOP provides a comprehensive suite of tools for parameterisation and optimisation of battery models. It aims to implement Bayesian and frequentist techniques with example workflows to guide the user. PyBOP can be applied to parameterise a wide range of battery models, including the electrochemical and equivalent circuit models available in [PyBaMM](https://pybamm.org/). A major emphasis in PyBOP is understandable and actionable diagnostics for the user, while still providing extensibility for advanced probabilistic methods. By building on the state-of-the-art battery models and leveraging Python's accessibility, PyBOP enables agile and robust parameterisation and optimisation. - -The figure below gives PyBOP's current conceptual structure. The living software specification of PyBOP can be found [here](https://github.com/pybop-team/software-spec). This package is under active development, expect API (Application Programming Interface) evolution with releases. +PyBOP offers a full range of tools for the parameterisation and optimisation of battery models, utilising both Bayesian and frequentist approaches with example workflows to assist the user. PyBOP can be used to parameterise various battery models, which include electrochemical and equivalent circuit models that are present in [PyBaMM](https://pybamm.org/). PyBOP prioritises clear and informative diagnostics for users, while also allowing for advanced probabilistic methods. +The diagram below presents PyBOP's conceptual framework. The PyBOP software specification is available at [this link](https://github.com/pybop-team/software-spec). This product is currently undergoing development, and users can expect the API to evolve with future releases.

- Data flows from battery cycling machines to Galv Harvesters, then to the     Galv server and REST API. Metadata can be updated and data read using the web client, and data can be downloaded by the Python client. + Data flows from battery cycling machines to Galv Harvesters, then to the     Galv server and REST API. Metadata can be updated and data read using the web client, and data can be downloaded by the Python client.

@@ -48,7 +47,7 @@ The figure below gives PyBOP's current conceptual structure. The living software ### Prerequisites -To use and/or contribute to PyBOP, you must first install Python 3 (specifically, 3.8-3.11). For example, on a Debian-based distribution (Debian, Ubuntu - including via WSL, Linux Mint), open a terminal and enter: +To use and/or contribute to PyBOP, first install Python (3.8-3.11). On a Debian-based distribution, this looks like: ```bash sudo apt update @@ -59,38 +58,24 @@ For further information, please refer to the similar [installation instructions ### Installation -Create a virtual environment called `pybop-env` within your current directory using: +Create a virtual environment called `pybop-env` within your current directory: ```bash virtualenv pybop-env ``` -Activate the environment with: +Activate the environment: ```bash source pybop-env/bin/activate ``` -You can check which version of python is installed within the virtual environment by typing: - -```bash -python --version -``` - -Later, you can deactivate the environment and go back to your original system using: +Later, you can deactivate the environment: ```bash deactivate ``` -Note that there are alternative packages that can be used to create and manage [virtual environments](https://realpython.com/python-virtual-environments-a-primer/), for example [pyenv](https://github.com/pyenv/pyenv#installation) and [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv#installation). In this case, follow the instructions to install these packages and then to create, activate and deactivate a virtual environment, use: - -```bash -pyenv virtualenv pybop-env -pyenv activate pybop-env -pyenv deactivate -``` - Within your virtual environment, install the `develop` branch of PyBOP: ```bash @@ -103,18 +88,8 @@ To alternatively install PyBOP from a local directory, use the following templat pip install -e "PATH_TO_PYBOP" ``` -Now, with PyBOP installed in your virtual environment, you can run Python scripts that import and use the functionality of this package. - - -### Usage -PyBOP has two classes of intended use cases: -1. parameter estimation from battery test data -2. design optimisation subject to battery manufacturing/usage constraints - -These classes encompass a wide variety of optimisation problems, which depend on the choice of battery model, the available data and/or the choice of design parameters. - -### Parameter estimation -The example below shows a simple fitting routine that starts by generating synthetic data from a single particle model with modified parameter values. An RMSE cost function using the terminal voltage as the optimised signal is completed to determine the unknown parameter values. First, the synthetic data is generated: +### Example +The example below illustrates a straightforward process that begins by creating artificial data from a solo particle blueprint. The unknown parameter values are discovered by implementing an RMSE cost function using the terminal voltage as the observed signal. Initially, the simulated data is generated. ```python import pybop @@ -122,46 +97,42 @@ import pybamm import pandas as pd import numpy as np -def getdata(x0): - model = pybamm.lithium_ion.SPM() - params = model.default_parameter_values - - params.update( - { - "Negative electrode active material volume fraction": x0[0], - "Positive electrode active material volume fraction": x0[1], - } - ) - experiment = pybamm.Experiment( - [ - ( - "Discharge at 2C for 5 minutes (1 second period)", - "Rest for 2 minutes (1 second period)", - "Charge at 1C for 5 minutes (1 second period)", - "Rest for 2 minutes (1 second period)", - ), - ] - * 2 - ) - sim = pybamm.Simulation(model, experiment=experiment, parameter_values=params) - return sim.solve() - - -# Form observations -x0 = np.array([0.55, 0.63]) -solution = getdata(x0) +def getdata(self, model, x0): + model.parameter_set.update( + { + "Negative electrode active material volume fraction": x0[0], + "Positive electrode active material volume fraction": x0[1], + } + ) + experiment = pybamm.Experiment( + [ + ( + "Discharge at 1C for 3 minutes (1 second period)", + "Rest for 2 minutes (1 second period)", + "Charge at 1C for 3 minutes (1 second period)", + "Rest for 2 minutes (1 second period)", + ), + ] + * 2 + ) + sim = model.predict(init_soc=init_soc, experiment=experiment) + return sim ``` -Next, the observed variables are defined, with the model construction and parameter definitions following. Finally, the parameterisation class is constructed and parameter fitting is completed. +Next, we construct the model, define the dataset, and form the parameters. Lastly, we build the parameterisation class and complete the parameter fitting. ```python -observations = [ - pybop.Observed("Time [s]", solution["Time [s]"].data), - pybop.Observed("Current function [A]", solution["Current [A]"].data), - pybop.Observed("Voltage [V]", solution["Terminal voltage [V]"].data), -] - # Define model -model = pybop.models.lithium_ion.SPM() -model.parameter_set = model.pybamm_model.default_parameter_values +parameter_set = pybop.ParameterSet("pybamm", "Chen2020") +model = pybop.lithium_ion.SPM(parameter_set=parameter_set) + +# Form dataset +x0 = np.array([0.55, 0.63]) +solution = getdata(x0) + +dataset = [ + pybop.Dataset("Time [s]", solution["Time [s]"].data), + pybop.Dataset("Current function [A]", solution["Current [A]"].data), + pybop.Dataset("Voltage [V]", solution["Terminal voltage [V]"].data), +] # Fitting parameters params = [ @@ -177,29 +148,37 @@ params = [ ), ] -parameterisation = pybop.Parameterisation( - model, observations=observations, fit_parameters=params +# Define the cost to optimise +cost = pybop.RMSE() +signal = "Voltage [V]" + +# Select optimiser +optimiser = pybop.NLoptOptimize(n_param=len(parameters)) + +# Build the optimisation problem +parameterisation = pybop.Optimisation( + cost=cost, + model=model, + optimiser=optimiser, + parameters=parameters, + dataset=dataset, + signal=signal, ) -# get RMSE estimate using NLOpt -results, last_optim, num_evals = parameterisation.rmse( - signal="Voltage [V]", method="nlopt" # results = [0.54452026, 0.63064801] -) +# run the parameterisation +results, last_optim, num_evals = parameterisation.run() ``` ## Code of Conduct -PyBOP aims to foster a broad consortium of developers and users, building on and -learning from the success of the [PyBaMM](https://pybamm.org/) community. Our values are: - -- Open-source (code and ideas should be shared) +PyBOP aims to foster a broad consortium of developers and users, building on and learning from the success of the [PyBaMM](https://pybamm.org/) community. Our values are: - Inclusivity and fairness (those who want to contribute may do so, and their input is appropriately recognised) -- Interoperability (aiming for modularity to enable maximum impact and inclusivity) +- Interoperability (Modularity to enable maximum impact and inclusivity) -- User-friendliness (putting user requirements first, thinking about user-assistance & workflows) +- User-friendliness (putting user requirements first via suser-assistance & workflows) From cccdca3e58cd3e0cccf1ffc1c8f4d03c63e10132 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Wed, 8 Nov 2023 09:17:16 +0000 Subject: [PATCH 02/13] Updt. Fig1 size, new example --- README.md | 98 +++++++++++++++++++------------------------------------ 1 file changed, 34 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 447a35b4f..0eb6bf3c4 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ PyBOP offers a full range of tools for the parameterisation and optimisation of The diagram below presents PyBOP's conceptual framework. The PyBOP software specification is available at [this link](https://github.com/pybop-team/software-spec). This product is currently undergoing development, and users can expect the API to evolve with future releases.

- Data flows from battery cycling machines to Galv Harvesters, then to the     Galv server and REST API. Metadata can be updated and data read using the web client, and data can be downloaded by the Python client. + Data flows from battery cycling machines to Galv Harvesters, then to the     Galv server and REST API. Metadata can be updated and data read using the web client, and data can be downloaded by the Python client.

@@ -89,84 +89,54 @@ pip install -e "PATH_TO_PYBOP" ``` ### Example -The example below illustrates a straightforward process that begins by creating artificial data from a solo particle blueprint. The unknown parameter values are discovered by implementing an RMSE cost function using the terminal voltage as the observed signal. Initially, the simulated data is generated. +The example below illustrates a straightforward process that begins by creating artificial data from a solo particle blueprint. The unknown parameter values are discovered by implementing an RMSE cost function using the terminal voltage as the observed signal. ```python import pybop -import pybamm -import pandas as pd import numpy as np +import matplotlib.pyplot as plt -def getdata(self, model, x0): - model.parameter_set.update( - { - "Negative electrode active material volume fraction": x0[0], - "Positive electrode active material volume fraction": x0[1], - } - ) - experiment = pybamm.Experiment( - [ - ( - "Discharge at 1C for 3 minutes (1 second period)", - "Rest for 2 minutes (1 second period)", - "Charge at 1C for 3 minutes (1 second period)", - "Rest for 2 minutes (1 second period)", - ), - ] - * 2 - ) - sim = model.predict(init_soc=init_soc, experiment=experiment) - return sim -``` -Next, we construct the model, define the dataset, and form the parameters. Lastly, we build the parameterisation class and complete the parameter fitting. -```python -# Define model +# Parameter set and model definition parameter_set = pybop.ParameterSet("pybamm", "Chen2020") -model = pybop.lithium_ion.SPM(parameter_set=parameter_set) - -# Form dataset -x0 = np.array([0.55, 0.63]) -solution = getdata(x0) - -dataset = [ - pybop.Dataset("Time [s]", solution["Time [s]"].data), - pybop.Dataset("Current function [A]", solution["Current [A]"].data), - pybop.Dataset("Voltage [V]", solution["Terminal voltage [V]"].data), -] +model = pybop.lithium_ion.SPMe(parameter_set=parameter_set) # Fitting parameters -params = [ +parameters = [ pybop.Parameter( "Negative electrode active material volume fraction", - prior=pybop.Gaussian(0.5, 0.05), - bounds=[0.35, 0.75], + prior=pybop.Gaussian(0.7, 0.05), + bounds=[0.6, 0.9], ), pybop.Parameter( "Positive electrode active material volume fraction", - prior=pybop.Gaussian(0.65, 0.05), - bounds=[0.45, 0.85], - ), + prior=pybop.Gaussian(0.58, 0.05), + bounds=[0.5, 0.8], + ) ] -# Define the cost to optimise -cost = pybop.RMSE() -signal = "Voltage [V]" - -# Select optimiser -optimiser = pybop.NLoptOptimize(n_param=len(parameters)) - -# Build the optimisation problem -parameterisation = pybop.Optimisation( - cost=cost, - model=model, - optimiser=optimiser, - parameters=parameters, - dataset=dataset, - signal=signal, -) - -# run the parameterisation -results, last_optim, num_evals = parameterisation.run() +# Generate data +sigma = 0.005 +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 definition +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) +opt = pybop.Optimisation(cost, optimiser=pybop.GradientDescent()) +opt.optimiser.learning_rate = 0.025 +opt.optimiser.max_iterations = 100 + +# Run optimisation +x, output, final_cost, num_evals = opt.run() +print("Estimated parameters:", x) ``` From f0637861c4377b8dec5e49e7930f4b78630a302d Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:53:16 +0000 Subject: [PATCH 03/13] Move example code Move the example code in the Readme to the example scripts folder. This example will not run until #72 is merged. --- README.md | 76 +++++++++++---------------------- examples/scripts/SPM_example.py | 47 ++++++++++++++++++++ 2 files changed, 73 insertions(+), 50 deletions(-) create mode 100644 examples/scripts/SPM_example.py diff --git a/README.md b/README.md index 0eb6bf3c4..8592bf7dd 100644 --- a/README.md +++ b/README.md @@ -88,55 +88,31 @@ To alternatively install PyBOP from a local directory, use the following templat pip install -e "PATH_TO_PYBOP" ``` -### Example -The example below illustrates a straightforward process that begins by creating artificial data from a solo particle blueprint. The unknown parameter values are discovered by implementing an RMSE cost function using the terminal voltage as the observed signal. - -```python -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.005 -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 definition -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) -opt = pybop.Optimisation(cost, optimiser=pybop.GradientDescent()) -opt.optimiser.learning_rate = 0.025 -opt.optimiser.max_iterations = 100 - -# Run optimisation -x, output, final_cost, num_evals = opt.run() -print("Estimated parameters:", x) +To check whether PyBOP has been installed correctly, run one of the examples in the following section or the full set of unit tests: + +```bash +pytest --unit -v +``` + +### Using PyBOP +PyBOP has two general types of intended use case: +1. parameter estimation from battery test data +2. design optimisation subject to battery manufacturing/usage constraints + +These general cases encompass a wide variety of optimisation problems, which require careful consideration based on the choice of battery model, the available data and/or the choice of design parameters. + +PyBOP comes with a number of example notebooks and scripts which can be found in the examples folder. + +The (`spm_example` script)[https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/spm_example.py] illustrates a straightforward example that begins by creating artificial data from a single particle model (SPM). The unknown parameter values are then discovered by implementing an RMSE cost function using the terminal voltage as the observed signal. The main output is the set of estimated parameters, namely the negative and positive electrode active material volume fractions in this example. To run this example: + +```bash +python examples/scripts/spm_example.py +``` + +The (`RMSE_estimation` script)[https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/rmse_estimation.py] provides a second example which differs by importing the example `Chen_example.csv` dataset and then estimates the same SPM parameters based on the same RMSE cost function. To run this example: + +```bash +python examples/scripts/rmse_estimation.py ``` @@ -148,7 +124,7 @@ PyBOP aims to foster a broad consortium of developers and users, building on and - Interoperability (Modularity to enable maximum impact and inclusivity) -- User-friendliness (putting user requirements first via suser-assistance & workflows) +- User-friendliness (putting user requirements first via user-assistance & workflows) diff --git a/examples/scripts/SPM_example.py b/examples/scripts/SPM_example.py new file mode 100644 index 000000000..82c57ea34 --- /dev/null +++ b/examples/scripts/SPM_example.py @@ -0,0 +1,47 @@ +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.005 +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 definition +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) +opt = pybop.Optimisation(cost, optimiser=pybop.GradientDescent()) +opt.optimiser.learning_rate = 0.025 +opt.optimiser.max_iterations = 100 + +# Run optimisation +x, output, final_cost, num_evals = opt.run() +print("Estimated parameters:", x) From 2f1abcdba9de4eb0b5c8dac8016e518f0674ec59 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:47:15 +0000 Subject: [PATCH 04/13] Update text --- examples/scripts/SPM_example.py | 8 +++--- examples/scripts/grad_descent.py | 22 ++++++++++------ examples/scripts/mle.py | 43 +++++++++++++++++++------------- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/examples/scripts/SPM_example.py b/examples/scripts/SPM_example.py index 82c57ea34..2740b0786 100644 --- a/examples/scripts/SPM_example.py +++ b/examples/scripts/SPM_example.py @@ -38,10 +38,10 @@ # Generate problem, cost function, and optimisation class problem = pybop.Problem(model, parameters, dataset) cost = pybop.SumSquaredError(problem) -opt = pybop.Optimisation(cost, optimiser=pybop.GradientDescent()) -opt.optimiser.learning_rate = 0.025 -opt.optimiser.max_iterations = 100 +optim = pybop.Optimisation(cost, optimiser=pybop.GradientDescent()) +optim.optimiser.learning_rate = 0.025 +optim.optimiser.max_iterations = 100 # Run optimisation -x, output, final_cost, num_evals = opt.run() +x, output, final_cost, num_evals = optim.run() print("Estimated parameters:", x) diff --git a/examples/scripts/grad_descent.py b/examples/scripts/grad_descent.py index 6e62f9b18..61ffaea88 100644 --- a/examples/scripts/grad_descent.py +++ b/examples/scripts/grad_descent.py @@ -3,8 +3,10 @@ import numpy as np import matplotlib.pyplot as plt +# Model definition model = pybop.lithium_ion.SPMe() +# Input current and fitting parameters inputs = { "Negative electrode active material volume fraction": 0.58, "Positive electrode active material volume fraction": 0.44, @@ -13,10 +15,12 @@ t_eval = np.arange(0, 900, 2) model.build(fit_parameters=inputs) +# Generate data values = model.predict(inputs=inputs, t_eval=t_eval) voltage = values["Terminal voltage [V]"].data time = values["Time [s]"].data +# Add noise sigma = 0.001 CorruptValues = voltage + np.random.normal(0, sigma, len(voltage)) @@ -28,26 +32,28 @@ plt.plot(time, voltage) plt.show() - +# Generate problem problem = pints.SingleOutputProblem(model, time, CorruptValues) # Select a score function score = pints.SumOfSquaresError(problem) +# Set the initial parameter values and optimisation class x0 = np.array([0.48, 0.55, 1.4]) -opt = pints.OptimisationController(score, x0, method=pints.GradientDescent) - -opt.optimiser().set_learning_rate(0.025) -opt.set_max_unchanged_iterations(50) -opt.set_max_iterations(200) +optim = pints.OptimisationController(score, x0, method=pints.GradientDescent) +optim.optimiser().set_learning_rate(0.025) +optim.set_max_unchanged_iterations(50) +optim.set_max_iterations(200) -x1, f1 = opt.run() +# Run optimisation +x1, f1 = optim.run() print("Estimated parameters:") print(x1) -# Show the generated data +# Generate data using the estimated parameters simulated_values = problem.evaluate(x1[:3]) +# Show the estimated data plt.figure() plt.xlabel("Time") plt.ylabel("Values") diff --git a/examples/scripts/mle.py b/examples/scripts/mle.py index a508ffa46..3d6175d3b 100644 --- a/examples/scripts/mle.py +++ b/examples/scripts/mle.py @@ -3,8 +3,10 @@ import numpy as np import matplotlib.pyplot as plt +# Model definition model = pybop.lithium_ion.SPMe() +# Input current and fitting parameters inputs = { "Negative electrode active material volume fraction": 0.58, "Positive electrode active material volume fraction": 0.44, @@ -13,44 +15,51 @@ t_eval = np.arange(0, 900, 2) model.build(fit_parameters=inputs) +# Generate data values = model.predict(inputs=inputs, t_eval=t_eval) voltage = values["Terminal voltage [V]"].data time = values["Time [s]"].data +# Add noise sigma = 0.001 -CorruptValues = voltage + np.random.normal(0, sigma, len(voltage)) +corrupt_values = voltage + np.random.normal(0, sigma, len(voltage)) # Show the generated data plt.figure() -plt.xlabel("Time") -plt.ylabel("Values") -plt.plot(time, CorruptValues) -plt.plot(time, voltage) +#plt.title("Synthetic data and corrupted signal") +plt.xlabel("Time (s)") +plt.ylabel("Voltage (V)") +plt.plot(time, corrupt_values, label="corrupted signal") +plt.plot(time, voltage, label="synthetic data") +plt.legend(loc="upper right") plt.show() - -problem = pints.SingleOutputProblem(model, time, CorruptValues) +# Generate problem, cost function, and optimisation class +problem = pints.SingleOutputProblem(model, time, corrupt_values) log_likelihood = pints.GaussianLogLikelihood(problem) boundaries = pints.RectangularBoundaries([0.4, 0.4, 0.7, 1e-5], [0.6, 0.6, 2.1, 1e-1]) - x0 = np.array([0.48, 0.55, 1.4, 1e-3]) -opt = pints.OptimisationController( +optim = pints.OptimisationController( log_likelihood, x0, boundaries=boundaries, method=pints.CMAES ) -opt.set_max_unchanged_iterations(50) -opt.set_max_iterations(200) +optim.set_max_unchanged_iterations(50) +optim.set_max_iterations(200) -x1, f1 = opt.run() +# Run optimisation +x1, f1 = optim.run() print("Estimated parameters:") print(x1) -# Show the generated data +# Generate data using the estimated parameters simulated_values = problem.evaluate(x1[:3]) +# Show the generated data plt.figure() -plt.xlabel("Time") -plt.ylabel("Values") -plt.plot(time, CorruptValues) +#plt.title("Corrupted signal and estimation") +plt.xlabel("Time (s)") +plt.ylabel("Voltage (V)") +plt.plot(time, corrupt_values, label="corrupted signal") plt.fill_between(time, simulated_values - sigma, simulated_values + sigma, alpha=0.2) -plt.plot(time, simulated_values) +plt.plot(time, simulated_values, label="estimation") +plt.legend(loc="upper right") plt.show() From bf350ee1c5437215dc3607f6c708a624c58f1d13 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:48:25 +0000 Subject: [PATCH 05/13] Rename SPM_example to spm_example --- examples/scripts/{SPM_example.py => spm_example.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/scripts/{SPM_example.py => spm_example.py} (100%) diff --git a/examples/scripts/SPM_example.py b/examples/scripts/spm_example.py similarity index 100% rename from examples/scripts/SPM_example.py rename to examples/scripts/spm_example.py From edf4b151500ca65db14764215f8ce5b383ccc2dc Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Thu, 9 Nov 2023 15:52:08 +0000 Subject: [PATCH 06/13] Fix braces --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8592bf7dd..ba56774f3 100644 --- a/README.md +++ b/README.md @@ -103,13 +103,13 @@ These general cases encompass a wide variety of optimisation problems, which req PyBOP comes with a number of example notebooks and scripts which can be found in the examples folder. -The (`spm_example` script)[https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/spm_example.py] illustrates a straightforward example that begins by creating artificial data from a single particle model (SPM). The unknown parameter values are then discovered by implementing an RMSE cost function using the terminal voltage as the observed signal. The main output is the set of estimated parameters, namely the negative and positive electrode active material volume fractions in this example. To run this example: +The [`spm_example` script](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/spm_example.py) illustrates a straightforward example that begins by creating artificial data from a single particle model (SPM). The unknown parameter values are then discovered by implementing an RMSE cost function using the terminal voltage as the observed signal. The main output is the set of estimated parameters, namely the negative and positive electrode active material volume fractions in this example. To run this example: ```bash python examples/scripts/spm_example.py ``` -The (`RMSE_estimation` script)[https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/rmse_estimation.py] provides a second example which differs by importing the example `Chen_example.csv` dataset and then estimates the same SPM parameters based on the same RMSE cost function. To run this example: +The [`RMSE_estimation` script](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/rmse_estimation.py) provides a second example which differs by importing the example `Chen_example.csv` dataset and then estimates the same SPM parameters based on the same RMSE cost function. To run this example: ```bash python examples/scripts/rmse_estimation.py From f41d4263f517a22cefc903692316b9e5927474ad Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Thu, 9 Nov 2023 16:08:27 +0000 Subject: [PATCH 07/13] Switch show to draw so scripts run to the end --- examples/scripts/grad_descent.py | 2 +- examples/scripts/mle.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/scripts/grad_descent.py b/examples/scripts/grad_descent.py index 61ffaea88..22b657574 100644 --- a/examples/scripts/grad_descent.py +++ b/examples/scripts/grad_descent.py @@ -30,7 +30,7 @@ plt.ylabel("Values") plt.plot(time, CorruptValues) plt.plot(time, voltage) -plt.show() +plt.draw() # use draw instead of show so that computation continues # Generate problem problem = pints.SingleOutputProblem(model, time, CorruptValues) diff --git a/examples/scripts/mle.py b/examples/scripts/mle.py index 3d6175d3b..1658d83f7 100644 --- a/examples/scripts/mle.py +++ b/examples/scripts/mle.py @@ -32,7 +32,7 @@ plt.plot(time, corrupt_values, label="corrupted signal") plt.plot(time, voltage, label="synthetic data") plt.legend(loc="upper right") -plt.show() +plt.draw() # use draw instead of show so that computation continues # Generate problem, cost function, and optimisation class problem = pints.SingleOutputProblem(model, time, corrupt_values) From 077dad7e745c97dc9b08f234002802996abafabf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:15:13 +0000 Subject: [PATCH 08/13] style: pre-commit fixes --- examples/scripts/grad_descent.py | 2 +- examples/scripts/mle.py | 6 +++--- examples/scripts/spm_example.py | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/scripts/grad_descent.py b/examples/scripts/grad_descent.py index 22b657574..6ba619b60 100644 --- a/examples/scripts/grad_descent.py +++ b/examples/scripts/grad_descent.py @@ -30,7 +30,7 @@ plt.ylabel("Values") plt.plot(time, CorruptValues) plt.plot(time, voltage) -plt.draw() # use draw instead of show so that computation continues +plt.draw() # use draw instead of show so that computation continues # Generate problem problem = pints.SingleOutputProblem(model, time, CorruptValues) diff --git a/examples/scripts/mle.py b/examples/scripts/mle.py index 1658d83f7..b139809a8 100644 --- a/examples/scripts/mle.py +++ b/examples/scripts/mle.py @@ -26,13 +26,13 @@ # Show the generated data plt.figure() -#plt.title("Synthetic data and corrupted signal") +# plt.title("Synthetic data and corrupted signal") plt.xlabel("Time (s)") plt.ylabel("Voltage (V)") plt.plot(time, corrupt_values, label="corrupted signal") plt.plot(time, voltage, label="synthetic data") plt.legend(loc="upper right") -plt.draw() # use draw instead of show so that computation continues +plt.draw() # use draw instead of show so that computation continues # Generate problem, cost function, and optimisation class problem = pints.SingleOutputProblem(model, time, corrupt_values) @@ -55,7 +55,7 @@ # Show the generated data plt.figure() -#plt.title("Corrupted signal and estimation") +# plt.title("Corrupted signal and estimation") plt.xlabel("Time (s)") plt.ylabel("Voltage (V)") plt.plot(time, corrupt_values, label="corrupted signal") diff --git a/examples/scripts/spm_example.py b/examples/scripts/spm_example.py index 2740b0786..d352260bd 100644 --- a/examples/scripts/spm_example.py +++ b/examples/scripts/spm_example.py @@ -1,6 +1,5 @@ import pybop import numpy as np -import matplotlib.pyplot as plt # Parameter set and model definition parameter_set = pybop.ParameterSet("pybamm", "Chen2020") From f1592968a1fe9ec1526a930d9fed59ffff8404db Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Fri, 17 Nov 2023 11:54:11 +0000 Subject: [PATCH 09/13] Update pytest flags and usage --- CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5926bedc1..a625bedd1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -129,6 +129,19 @@ else, type pytest --unit -v ``` +To run individual test files, you can use + +```bash +pytest tests/unit/path/to/test --unit -v +``` + +And for individual tests, + +```bash +pytest tests/unit/path/to/test.py::TestClass:test_name --unit -v +``` +where `--unit` is a flag to run only unit tests and `-v` is a flag to display verbose output. + ### Writing tests Every new feature should have its own test. To create ones, have a look at the `test` directory and see if there's a test for a similar method. Copy-pasting is a good way to start. From 283ccd0eecbaaff01019aa6ab631fbd9ecce2ca0 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Fri, 17 Nov 2023 12:49:56 +0000 Subject: [PATCH 10/13] Updt example name, links in readme, and phrasing --- README.md | 18 +++++++----------- .../scripts/{spm_example.py => spm_descent.py} | 0 2 files changed, 7 insertions(+), 11 deletions(-) rename examples/scripts/{spm_example.py => spm_descent.py} (100%) diff --git a/README.md b/README.md index ba56774f3..35e0ff0da 100644 --- a/README.md +++ b/README.md @@ -95,34 +95,30 @@ pytest --unit -v ``` ### Using PyBOP -PyBOP has two general types of intended use case: +PyBOP has two general types of intended use cases: 1. parameter estimation from battery test data 2. design optimisation subject to battery manufacturing/usage constraints -These general cases encompass a wide variety of optimisation problems, which require careful consideration based on the choice of battery model, the available data and/or the choice of design parameters. +These general cases encompass a wide variety of optimisation problems that require careful consideration based on the choice of battery model, the available data and/or the choice of design parameters. -PyBOP comes with a number of example notebooks and scripts which can be found in the examples folder. +PyBOP comes with a number of [example](https://github.com/pybop-team/PyBOP/blob/develop/examples) notebooks and scripts which can be found in the examples folder. -The [`spm_example` script](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/spm_example.py) illustrates a straightforward example that begins by creating artificial data from a single particle model (SPM). The unknown parameter values are then discovered by implementing an RMSE cost function using the terminal voltage as the observed signal. The main output is the set of estimated parameters, namely the negative and positive electrode active material volume fractions in this example. To run this example: +The [spm_descent.py](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/spm_descent.py) script illustrates a straightforward example that starts by generating artificial data from a single particle model (SPM). The unknown parameter values are identified by implementing a sum-of-square error cost function using the terminal voltage as the observed signal and a gradient descent optimiser. To run this example: ```bash python examples/scripts/spm_example.py ``` -The [`RMSE_estimation` script](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/rmse_estimation.py) provides a second example which differs by importing the example `Chen_example.csv` dataset and then estimates the same SPM parameters based on the same RMSE cost function. To run this example: - -```bash -python examples/scripts/rmse_estimation.py -``` +In addition, [spm_nlopt.ipynb](https://github.com/pybop-team/PyBOP/blob/develop/examples/notebooks/spm_nlopt.ipynb) provides a second example in notebook form. This example estimates the SPM parameters based on an RMSE cost function and a BOBYQA optimiser. ## Code of Conduct PyBOP aims to foster a broad consortium of developers and users, building on and learning from the success of the [PyBaMM](https://pybamm.org/) community. Our values are: -- Inclusivity and fairness (those who want to contribute may do so, and their input is appropriately recognised) +- Inclusivity and fairness (those who wish to contribute may do so, and their input is appropriately recognised) -- Interoperability (Modularity to enable maximum impact and inclusivity) +- Interoperability (Modularity for maximum impact and inclusivity) - User-friendliness (putting user requirements first via user-assistance & workflows) diff --git a/examples/scripts/spm_example.py b/examples/scripts/spm_descent.py similarity index 100% rename from examples/scripts/spm_example.py rename to examples/scripts/spm_descent.py From 1906f8d56f4fd694af817dac848357349d750240 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Fri, 17 Nov 2023 13:10:30 +0000 Subject: [PATCH 11/13] Add colab badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 35e0ff0da..25d1fbd09 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ license + + Open In Colab

From 4dc8a3d3ad459919739ddb861a0ff9b8c793f22e Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Fri, 17 Nov 2023 18:42:11 +0000 Subject: [PATCH 12/13] Update CONTRIBUTING.md --- CONTRIBUTING.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a625bedd1..d1ea9db24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,14 +159,15 @@ This also means that, if you can't fix the bug yourself, it will be much easier 1. Run individual test scripts instead of the whole test suite: ```bash - pytest tests/unit/path/to/test + pytest tests/unit/path/to/test --unit -v ``` You can also run an individual test from a particular script, e.g. ```bash - pytest tests/unit/test_quick_plot.py TestQuickPlot.test_failure + pytest tests/unit/path/to/test.py::TestClass:test_name --unit -v ``` + where `--unit` is a flag to run only unit tests and `-v` is a flag to display verbose output. 2. Set break-points, either in your IDE or using the Python debugging module. To use the latter, add the following line where you want to set the break point From 500fc682a2a22a48db542c1324630dec4249a718 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Fri, 17 Nov 2023 18:44:12 +0000 Subject: [PATCH 13/13] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 25d1fbd09..11d7e9be8 100644 --- a/README.md +++ b/README.md @@ -103,12 +103,12 @@ PyBOP has two general types of intended use cases: These general cases encompass a wide variety of optimisation problems that require careful consideration based on the choice of battery model, the available data and/or the choice of design parameters. -PyBOP comes with a number of [example](https://github.com/pybop-team/PyBOP/blob/develop/examples) notebooks and scripts which can be found in the examples folder. +PyBOP comes with a number of [example notebooks and scripts](https://github.com/pybop-team/PyBOP/blob/develop/examples) which can be found in the examples folder. The [spm_descent.py](https://github.com/pybop-team/PyBOP/blob/develop/examples/scripts/spm_descent.py) script illustrates a straightforward example that starts by generating artificial data from a single particle model (SPM). The unknown parameter values are identified by implementing a sum-of-square error cost function using the terminal voltage as the observed signal and a gradient descent optimiser. To run this example: ```bash -python examples/scripts/spm_example.py +python examples/scripts/spm_descent.py ``` In addition, [spm_nlopt.ipynb](https://github.com/pybop-team/PyBOP/blob/develop/examples/notebooks/spm_nlopt.ipynb) provides a second example in notebook form. This example estimates the SPM parameters based on an RMSE cost function and a BOBYQA optimiser. @@ -120,7 +120,7 @@ PyBOP aims to foster a broad consortium of developers and users, building on and - Inclusivity and fairness (those who wish to contribute may do so, and their input is appropriately recognised) -- Interoperability (Modularity for maximum impact and inclusivity) +- Interoperability (modularity for maximum impact and inclusivity) - User-friendliness (putting user requirements first via user-assistance & workflows)