In this notebook, we present the single particle model with a two dimensional current collector. This is achieved via the potential-pair models introduced in Marquis et al. [1] as implemented in PyBaMM. At a high-level this is accomplished as a potential-pair model which is resolved across the discretised spatial locations.
Before we begin, we need to ensure that we have all the necessary tools. We will install PyBOP and upgrade dependencies:
%pip install --upgrade pip ipywidgets -q
%pip install pybop -q
With the environment set up, we can now import PyBOP alongside other libraries we will need:
import numpy as np
import plotly.graph_objects as go
import pybop
Let's fix the random seed in order to generate consistent output during development, although this does not need to be done in practice.
np.random.seed(8)
To demonstrate parameter estimation, we first need some data. We will generate synthetic data using a forward model, which requires defining a parameter set and the model itself.
We start by creating an example parameter set and then instantiate the single-particle model (SPM):
parameter_set = pybop.ParameterSet.pybamm("Marquis2019")
parameter_set.update(
{
"Negative electrode active material volume fraction": 0.495,
"Positive electrode active material volume fraction": 0.612,
}
)
model = pybop.lithium_ion.SPM(
parameter_set=parameter_set,
options={"current collector": "potential pair", "dimensionality": 2},
)
Next, we update the number of spatial locations to solve the potential-pair model,
model.var_pts["y"] = 5
model.var_pts["z"] = 5
We can then simulate the model using the predict
method, with a default constant current to generate voltage data.
t_eval = np.arange(0, 900, 3)
values = model.predict(t_eval=t_eval)
To make the parameter estimation more realistic, we add Gaussian noise to the data.
sigma = 0.001
corrupt_values = values["Voltage [V]"].data + np.random.normal(0, sigma, len(t_eval))
We will now set up the parameter estimation process by defining the datasets for optimisation and selecting the model parameters we wish to estimate.
The dataset for optimisation is composed of time, current, and the noisy voltage data:
dataset = pybop.Dataset(
{
"Time [s]": t_eval,
"Current function [A]": values["Current [A]"].data,
"Voltage [V]": corrupt_values,
}
)
We select the parameters for estimation and set up their prior distributions and bounds:
parameters = pybop.Parameters(
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.7, 0.05),
bounds=[0.45, 0.9],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.58, 0.05),
bounds=[0.5, 0.8],
),
)
For plotting purposes, we want additional variables to be stored in the problem class. These are defined as,
additional_variables = [
"Negative current collector potential [V]",
"Positive current collector potential [V]",
]
With the datasets and parameters defined, we can set up the optimisation problem, its cost function, and the optimiser.
problem = pybop.FittingProblem(
model, parameters, dataset, additional_variables=additional_variables
)
cost = pybop.SumSquaredError(problem)
optim = pybop.CMAES(cost, max_iterations=30)
We proceed to run the CMA-ES optimisation algorithm to estimate the parameters:
x, final_cost = optim.run()
After the optimisation, we can examine the estimated parameter values:
x # This will output the estimated parameters
array([0.49949096, 0.60907043])
PyBOP provides various plotting utilities to visualise the results of the optimisation.
We can quickly plot the system's response using the estimated parameters compared to the target:
pybop.quick_plot(problem, problem_inputs=x, title="Optimised Comparison");
We can now plot the spatial variables from the solution object. First, the final negative current collector potential can be displayed. In this example, this is just a reference variable, but could be used for fitting or optimisation in the correct workflows.
sol = problem.evaluate(parameters.as_dict(x))
go.Figure(
[
go.Contour(
x=np.arange(0, model.var_pts["y"] - 1, 1),
y=np.arange(0, model.var_pts["z"] - 1, 1),
z=sol["Negative current collector potential [V]"][:, :, -1],
colorscale="Viridis",
)
],
layout=dict(
title="Negative current collector potential [V]",
xaxis_title="x node",
yaxis_title="y node",
width=600,
height=600,
),
)
We plot can then plot the positive current collector potential,
go.Figure(
[
go.Contour(
x=np.arange(0, model.var_pts["y"] - 1, 1),
y=np.arange(0, model.var_pts["z"] - 1, 1),
z=sol["Positive current collector potential [V]"][:, :, -1],
colorscale="Viridis",
)
],
layout=dict(
title="Positive current collector potential [V]",
xaxis_title="x node",
yaxis_title="y node",
width=600,
height=600,
),
)
To assess the optimisation process, we can plot the convergence of the cost function and the trajectories of the parameters:
pybop.plot_convergence(optim)
pybop.plot_parameters(optim);
Finally, we can visualise the cost landscape and the path taken by the optimiser:
pybop.plot2d(optim, steps=10);