ODE models in PyBaMM
ODE models in PyBaMM
A simple ODE battery model
In this section, we will learn how to develop a simple battery model using PyBaMM. We'll be using the reservoir model, which is a simple ordinary differential equation (ODE) model that represents the battery as two reservoirs of charge, one for the positive electrode and one for the negative electrode. The model is described by the following equations:
where and are the dimensionless stochiometries of the negative and positive electrodes, is the current, and are the capacities of the negative and positive electrodes, and are the open circuit potentials of the positive and negative electrodes, and is the resistance of the battery.
PyBaMM variables
The reservoir model has a number of output variables, which are either the state
variables and that are explicitly solved for, or derived variables
such as the voltage .
In PyBaMM a state variable can be defined using the
pybamm.Variable
class. For example, if you wanted to define a state variable with name "x", you
would writeimport pybamm x = pybamm.Variable("x")
Define the state variables for the reservoir model
Define the state variables for the reservoir model, including the stochiometries
and .
We will leave the derived variables like for now, and come back to them
later once we have defined the expressions for the ODEs.
PyBaMM parameters
The reservoir model has a number of parameters that need to be defined. In PyBaMM a parameter can be defined using the
pybamm.Parameter
class. For example, if you wanted to define a parameter with name "a", you would writea = pybamm.Parameter("a")
The name of the parameter is used to identify it in the model, the name of the Python variable you assign it to is arbitrary.
You can also define a parameter that is defined as a function using the
pybamm.FunctionParameter
class, which is useful for time varying parameters such as the current, or functions like the OCV function parameters and , which are both functions of the stochiometries and . For example, to define a parameter that is a function of time, you would writeP = pybamm.FunctionParameter("Your parameter name here", {"Time [s]": pybamm.t})
where the first argument is a string with the name of your parameter (which is used when passing the parameter values) and the second argument is a dictionary of
name: symbol
of all the variables on which the function parameter depends on. In particular, pybamm.t
is a special variable that represents time.Define the parameters for the reservoir model
Define the parameters for the reservoir model, including the current , the OCV functions and , the capacities and , and the resistance .
A PyBaMM Model
Now that we have defined the parameters and variables for the reservoir model,
we can define the model itself. In PyBaMM a model can be defined using the
pybamm.BaseModel
class. For example, if you wanted to define a model with name "my model", you
would writemodel = pybamm.BaseModel("my model")
By construction, PyBaMM expects the equations to be written in a very specific, with time derivatives playing a central role: ODEs must be written in explicit form, that is . Then, we only need to define the term (called RHS for right hand side) for a given variable , as the left hand side will be assumed to be . PyBaMM can also have equations with no time derivatives, which are called algebraic equations.
Going back to the PyBaMM model, the class has four useful attributes for defining a model, which are:
rhs
- a python dictionary of the right-hand-side equations with the formvariable: rhs
.algebraic
- a python dictionary of the algebraic equations (we won't need this for our ODE model). Should be passed as a dictionary of the formvariable: algebraic
. Note that the variable is only for indexing purposes, and this imposesalgebraic = 0
notvariable = algebraic
.initial_conditions
- a python dictionary of the initial conditions of the formvariable: ic
, which imposesvariable = ic
at the initial time.variables
- a python dictionary of the output variables of the formname: variable
, wherename
is a string.
As an example, lets define a simple model for exponential decay with a single state variable and a single parameter :
We can write this as a PyBaMM model by writing:
x = pybamm.Variable("x") a = pybamm.Parameter("a") model = pybamm.BaseModel("exponential decay") model.rhs = {x: -a * x} model.initial_conditions = {x: 1} model.variables = {"x": x}
Define the reservoir model
Now we have all the pieces we need to define the reservoir model. Define the
model using the parameters and variables you defined earlier.
PyBaMM expressions
It is worth pausing here and discussing the concept of an "expression" in
PyBaMM. Notice that in the model definition we have used expressions like
-i / Q_n
and U_p - U_n - i * R
. These expressions do not evaluate to a single
value like similar expressions involving Python variables would. Instead, they
are symbolic expressions that represent the mathematical equation.The fundamental building blocks of a PyBaMM model are the expressions, either
those for the RHS rate equations, the algebraic equations of the expression for
the output variables such as . PyBaMM encodes each of these expressions
using an "expression tree", which is a tree of operations (e.g. addition,
multiplication, etc.), variables (e.g. , , , etc.), and
functions (e.g. , , etc.), which can be evaluated at any point in
time.
As an example, lets consider the expression for , given by
-i / Q_n
. We can visualise the expression tree for this expression using the
visualise
method:model.rhs[x_n].visualise("x_n_rhs.png")
This will create a file called
x_n_rhs.png
in the current directory, which you
can open to see the expression tree, which will look like this:You can also print the expression as a string using the
print
method:print(model.rhs[x_n])
-Current function [A] / Negative electrode capacity [A.h]
So you can see that the expression tree is a symbolic representation of the
mathematical equation, which can then be later on used by the PyBaMM solvers to
solve the model equations over time.
The variable
children
returns a list of the children nodes of a given parent node. For example, to access the Negative electrode capacity
parameter we could typemodel.rhs[x_n].children[1]
as it is the second children of the division node (remember python starts indexing at 0). The command can be used recursively to navigate across the expression tree. For example, if we want to access the time variable in the expression tree above, we can type
model.rhs[x_n].children[0].children[0].children[0]
This is extremely useful to debug the expression tree as it allows you to access the relevant nodes.
PyBaMM events
An event is a condition that can be used to stop the solver, for example when
the voltage reaches a certain value. In PyBaMM an event can be defined using the
pybamm.Event
class. For example, if you wanted to define an event that stops the solver when
the time reaches 3, you would writestop_at_t_equal_3 = pybamm.Event("Stop at t = 3", pybamm.t - 3)
The expression
pybamm.t - 3
is the condition that the event is looking for,
and the solver will stop when this expression reaches zero. Note that the
expression must evaluate to a non-negative number for the duration of the
simulation, and then become zero when the event is triggered.You can add a list of events to the model using the
events
attribute, the
simulation will stop when any of the events are triggered.model.events = [stop_at_t_equal_3]
Define the stochiometry cut-off events
Define four events that ensure that the stochiometries and are
between 0 and 1. The simulation should stop when either reach 0 or 1.
PyBaMM parameter values
The final thing we need to do before we can solve the model is to define the
values for all the parameters. You should already be familar with the PyBaMM
pybamm.ParameterValues
class, which is used to define the values of the parameters in the model. For
example, if you wanted to define a parameter value for the parameter "a" that we
defined earlier, you would writeparameter_values = pybamm.ParameterValues({"a": 1})
For function paramters, we can define the function using a standard Python
function or lambda function that takes the same number of arguments as the
function parameter. For example, if you wanted to define a function parameter
for the current that is a function of time, you would write
def current(t): return 2 * t parameter_values = pybamm.ParameterValues({"Current function [A]": current})
or using a lambda function
parameter_values = pybamm.ParameterValues({"Current function [A]": lambda t: 2 * t})
Define the parameter values for the reservoir model
Define the parameter values for the reservoir model. You can choose any values
you like for the parameters, but in the solution below we will define the
following values:
- The current is a function of time,
- The initial negative electrode stochiometry is 0.9
- The initial positive electrode stochiometry is 0.1
- The negative electrode capacity is 1 Ah
- The positive electrode capacity is 1 Ah
- The electrode resistance is 0.1 Ohm
- The OCV functions are the LGM50 OCP from the Chen2020 model, which are given by the functions:
import numpy as np def graphite_LGM50_ocp_Chen2020(sto): u_eq = ( 1.9793 * np.exp(-39.3631 * sto) + 0.2482 - 0.0909 * np.tanh(29.8538 * (sto - 0.1234)) - 0.04478 * np.tanh(14.9159 * (sto - 0.2769)) - 0.0205 * np.tanh(30.4444 * (sto - 0.6103)) ) return u_eq def nmc_LGM50_ocp_Chen2020(sto): u_eq = ( -0.8090 * sto + 4.4875 - 0.0428 * np.tanh(18.5138 * (sto - 0.5542)) - 17.7326 * np.tanh(15.7890 * (sto - 0.3117)) + 17.5842 * np.tanh(15.9308 * (sto - 0.3120)) ) return u_eq
Solving the model
Now that we have defined the reservoir model, we can solve it using the PyBaMM simulation class and plot the results like so:
sim = pybamm.Simulation(model, parameter_values=param) sol = sim.solve([0, 1]) sol.plot(["Voltage [V]", "Negative electrode stochiometry", "Positive electrode stochiometry"])
Solve the reservoir model
Solve the reservoir model using the parameter values you defined earlier, and
plot the results. Vary the paramters and see how the solution changes to assure
yourself that the model is working as expected.