#
# Basic Doyle-Fuller-Newman (DFN) Model
#
import pybamm
from .base_lithium_ion_model import BaseModel
[docs]
class BasicDFNComposite(BaseModel):
"""Doyle-Fuller-Newman (DFN) model of a lithium-ion battery with composite particles
of graphite and silicon, from :footcite:t:`Ai2022`.
This class differs from the :class:`pybamm.lithium_ion.DFN` model class in that it
shows the whole model in a single class. This comes at the cost of flexibility in
comparing different physical effects, and in general the main DFN class should be
used instead.
Parameters
----------
name : str, optional
The name of the model.
"""
def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
options = {"particle phases": ("2", "1")}
super().__init__(options, name)
pybamm.citations.register("Ai2022")
# `param` is a class containing all the relevant parameters and functions for
# this model. These are purely symbolic at this stage, and will be set by the
# `ParameterValues` class when the model is processed.
######################
# Variables
######################
# Variables that depend on time only are created without a domain
Q = pybamm.Variable("Discharge capacity [A.h]")
# Variables that vary spatially are created with a domain
c_e_n = pybamm.Variable(
"Negative electrolyte concentration [mol.m-3]",
domain="negative electrode",
scale=self.param.c_e_init_av,
)
c_e_s = pybamm.Variable(
"Separator electrolyte concentration [mol.m-3]",
domain="separator",
scale=self.param.c_e_init_av,
)
c_e_p = pybamm.Variable(
"Positive electrolyte concentration [mol.m-3]",
domain="positive electrode",
scale=self.param.c_e_init_av,
)
# Concatenations combine several variables into a single variable, to simplify
# implementing equations that hold over several domains
c_e = pybamm.concatenation(c_e_n, c_e_s, c_e_p)
# Electrolyte potential
phi_e_n = pybamm.Variable(
"Negative electrolyte potential [V]",
domain="negative electrode",
reference=-self.param.n.prim.U_init,
)
phi_e_s = pybamm.Variable(
"Separator electrolyte potential [V]",
domain="separator",
reference=-self.param.n.prim.U_init,
)
phi_e_p = pybamm.Variable(
"Positive electrolyte potential [V]",
domain="positive electrode",
reference=-self.param.n.prim.U_init,
)
phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p)
# Electrode potential
phi_s_n = pybamm.Variable(
"Negative electrode potential [V]", domain="negative electrode"
)
phi_s_p = pybamm.Variable(
"Positive electrode potential [V]",
domain="positive electrode",
reference=self.param.ocv_init,
)
# Particle concentrations are variables on the particle domain, but also vary in
# the x-direction (electrode domain) and so must be provided with auxiliary
# domains
c_s_n_p1 = pybamm.Variable(
"Negative primary particle concentration [mol.m-3]",
domain="negative primary particle",
auxiliary_domains={"secondary": "negative electrode"},
scale=self.param.n.prim.c_max,
)
c_s_n_p2 = pybamm.Variable(
"Negative secondary particle concentration [mol.m-3]",
domain="negative secondary particle",
auxiliary_domains={"secondary": "negative electrode"},
scale=self.param.n.sec.c_max,
)
c_s_p = pybamm.Variable(
"Positive particle concentration [mol.m-3]",
domain="positive particle",
auxiliary_domains={"secondary": "positive electrode"},
scale=self.param.p.prim.c_max,
)
# Constant temperature
T = self.param.T_init
######################
# Other set-up
######################
# Current density
i_cell = self.param.current_density_with_time
# Porosity
# Primary broadcasts are used to broadcast scalar quantities across a domain
# into a vector of the right shape, for multiplying with other vectors
eps_n = pybamm.PrimaryBroadcast(
pybamm.Parameter("Negative electrode porosity"), "negative electrode"
)
eps_s = pybamm.PrimaryBroadcast(
pybamm.Parameter("Separator porosity"), "separator"
)
eps_p = pybamm.PrimaryBroadcast(
pybamm.Parameter("Positive electrode porosity"), "positive electrode"
)
eps = pybamm.concatenation(eps_n, eps_s, eps_p)
# Active material volume fraction (eps + eps_s + eps_inactive = 1)
eps_s_n = pybamm.Parameter(
"Primary: Negative electrode active material volume fraction"
) + pybamm.Parameter(
"Secondary: Negative electrode active material volume fraction"
)
eps_s_p = pybamm.Parameter("Positive electrode active material volume fraction")
# Tortuosity
tor = pybamm.concatenation(
eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e
)
# Open-circuit potentials
c_s_surf_n_p1 = pybamm.surf(c_s_n_p1)
sto_surf_n_p1 = c_s_surf_n_p1 / self.param.n.prim.c_max
ocp_n_p1 = self.param.n.prim.U(sto_surf_n_p1, T)
c_s_surf_n_p2 = pybamm.surf(c_s_n_p2)
sto_surf_n_p2 = c_s_surf_n_p2 / self.param.n.sec.c_max
k = 100
m_lith = pybamm.sigmoid(i_cell, 0, k) # for lithation (current < 0)
m_delith = 1 - m_lith # for delithiation (current > 0)
U_lith = self.param.n.sec.U(sto_surf_n_p2, T, "lithiation")
U_delith = self.param.n.sec.U(sto_surf_n_p2, T, "delithiation")
ocp_n_p2 = m_lith * U_lith + m_delith * U_delith
c_s_surf_p = pybamm.surf(c_s_p)
sto_surf_p = c_s_surf_p / self.param.p.prim.c_max
ocp_p = self.param.p.prim.U(sto_surf_p, T)
# Interfacial reactions
# Surf takes the surface value of a variable, i.e. its boundary value on the
# right side. This is also accessible via `boundary_value(x, "right")`, with
# "left" providing the boundary value of the left side
F_RT = self.param.F / (self.param.R * T)
j0_n_p1 = self.param.n.prim.j0(c_e_n, c_s_surf_n_p1, T)
j_n_p1 = (
2
* j0_n_p1
* pybamm.sinh(
self.param.n.prim.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p1)
)
)
j0_n_p2 = self.param.n.sec.j0(c_e_n, c_s_surf_n_p2, T)
j_n_p2 = (
2
* j0_n_p2
* pybamm.sinh(
self.param.n.sec.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p2)
)
)
j0_p = self.param.p.prim.j0(c_e_p, c_s_surf_p, T)
a_j_s = pybamm.PrimaryBroadcast(0, "separator")
j_p = (
2
* j0_p
* pybamm.sinh(self.param.p.prim.ne / 2 * F_RT * (phi_s_p - phi_e_p - ocp_p))
)
# Volumetric
a_n_p1 = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ
a_n_p2 = 3 * self.param.n.sec.epsilon_s_av / self.param.n.sec.R_typ
a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ
a_j_n_p1 = a_n_p1 * j_n_p1
a_j_n_p2 = a_n_p2 * j_n_p2
a_j_n = a_j_n_p1 + a_j_n_p2
a_j_p = a_p * j_p
a_j = pybamm.concatenation(a_j_n, a_j_s, a_j_p)
######################
# State of Charge
######################
I = self.param.current_with_time
# The `rhs` dictionary contains differential equations, with the key being the
# variable in the d/dt
self.rhs[Q] = I / 3600
# Initial conditions must be provided for the ODEs
self.initial_conditions[Q] = pybamm.Scalar(0)
######################
# Particles
######################
# The div and grad operators will be converted to the appropriate matrix
# multiplication at the discretisation stage
N_s_n_p1 = -self.param.n.prim.D(c_s_n_p1, T) * pybamm.grad(c_s_n_p1)
N_s_n_p2 = -self.param.n.sec.D(c_s_n_p2, T) * pybamm.grad(c_s_n_p2)
N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p)
self.rhs[c_s_n_p1] = -pybamm.div(N_s_n_p1)
self.rhs[c_s_n_p2] = -pybamm.div(N_s_n_p2)
self.rhs[c_s_p] = -pybamm.div(N_s_p)
# Boundary conditions must be provided for equations with spatial derivatives
self.boundary_conditions[c_s_n_p1] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (
-j_n_p1 / self.param.F / pybamm.surf(self.param.n.prim.D(c_s_n_p1, T)),
"Neumann",
),
}
self.boundary_conditions[c_s_n_p2] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (
-j_n_p2 / self.param.F / pybamm.surf(self.param.n.sec.D(c_s_n_p2, T)),
"Neumann",
),
}
self.boundary_conditions[c_s_p] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (
-j_p / self.param.F / pybamm.surf(self.param.p.prim.D(c_s_p, T)),
"Neumann",
),
}
self.initial_conditions[c_s_n_p1] = self.param.n.prim.c_init
self.initial_conditions[c_s_n_p2] = self.param.n.sec.c_init
self.initial_conditions[c_s_p] = self.param.p.prim.c_init
# Events specify points at which a solution should terminate
tolerance = 0.0000001
self.events += [
pybamm.Event(
"Minimum negative particle surface concentration of phase 1",
pybamm.min(sto_surf_n_p1) - tolerance,
),
pybamm.Event(
"Maximum negative particle surface concentration of phase 1",
(1 - tolerance) - pybamm.max(sto_surf_n_p1),
),
pybamm.Event(
"Minimum negative particle surface concentration of phase 2",
pybamm.min(sto_surf_n_p2) - tolerance,
),
pybamm.Event(
"Maximum negative particle surface concentration of phase 2",
(1 - tolerance) - pybamm.max(sto_surf_n_p2),
),
pybamm.Event(
"Minimum positive particle surface concentration",
pybamm.min(sto_surf_p) - tolerance,
),
pybamm.Event(
"Maximum positive particle surface concentration",
(1 - tolerance) - pybamm.max(sto_surf_p),
),
]
######################
# Current in the solid
######################
sigma_eff_n = self.param.n.sigma(T) * eps_s_n**self.param.n.b_s
i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n)
sigma_eff_p = self.param.p.sigma(T) * eps_s_p**self.param.p.b_s
i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p)
# The `algebraic` dictionary contains differential equations, with the key being
# the main scalar variable of interest in the equation
# multiply by Lx**2 to improve conditioning
self.algebraic[phi_s_n] = self.param.L_x**2 * (pybamm.div(i_s_n) + a_j_n)
self.algebraic[phi_s_p] = self.param.L_x**2 * (pybamm.div(i_s_p) + a_j_p)
self.boundary_conditions[phi_s_n] = {
"left": (pybamm.Scalar(0), "Dirichlet"),
"right": (pybamm.Scalar(0), "Neumann"),
}
self.boundary_conditions[phi_s_p] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (i_cell / pybamm.boundary_value(-sigma_eff_p, "right"), "Neumann"),
}
# Initial conditions must also be provided for algebraic equations, as an
# initial guess for a root-finding algorithm which calculates consistent initial
# conditions
# We evaluate c_n_init at x=0 and c_p_init at x=1 (this is just an initial
# guess so actual value is not too important)
self.initial_conditions[phi_s_n] = pybamm.Scalar(0)
self.initial_conditions[phi_s_p] = self.param.ocv_init
######################
# Current in the electrolyte
######################
i_e = (self.param.kappa_e(c_e, T) * tor) * (
self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
)
# multiply by Lx**2 to improve conditioning
self.algebraic[phi_e] = self.param.L_x**2 * (pybamm.div(i_e) - a_j)
self.boundary_conditions[phi_e] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (pybamm.Scalar(0), "Neumann"),
}
self.initial_conditions[phi_e] = -self.param.n.prim.U_init
######################
# Electrolyte concentration
######################
N_e = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e)
self.rhs[c_e] = (1 / eps) * (
-pybamm.div(N_e) + (1 - self.param.t_plus(c_e, T)) * a_j / self.param.F
)
self.boundary_conditions[c_e] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (pybamm.Scalar(0), "Neumann"),
}
self.initial_conditions[c_e] = self.param.c_e_init
######################
# (Some) variables
######################
voltage = pybamm.boundary_value(phi_s_p, "right")
c_s_rav_n_p1 = pybamm.r_average(c_s_n_p1)
c_s_rav_n_p2 = pybamm.r_average(c_s_n_p2)
c_s_xrav_n_p1 = pybamm.x_average(c_s_rav_n_p1)
c_s_xrav_n_p2 = pybamm.x_average(c_s_rav_n_p2)
c_s_rav_p = pybamm.r_average(c_s_p)
c_s_xrav_p = pybamm.x_average(c_s_rav_p)
j_n_p1_av = pybamm.x_average(j_n_p1)
j_n_p2_av = pybamm.x_average(j_n_p2)
ocp_av_n_p1 = pybamm.x_average(ocp_n_p1)
ocp_av_n_p2 = pybamm.x_average(ocp_n_p2)
ocp_av_p = pybamm.x_average(ocp_p)
a_j_n_p1_av = pybamm.x_average(a_j_n_p1)
a_j_n_p2_av = pybamm.x_average(a_j_n_p2)
num_cells = pybamm.Parameter(
"Number of cells connected in series to make a battery"
)
# The `variables` dictionary contains all variables that might be useful for
# visualising the solution of the model
self.variables = {
"Negative primary particle concentration [mol.m-3]": c_s_n_p1,
"Negative secondary particle concentration [mol.m-3]": c_s_n_p2,
"R-averaged negative primary particle concentration "
"[mol.m-3]": c_s_rav_n_p1,
"R-averaged negative secondary particle concentration "
"[mol.m-3]": c_s_rav_n_p2,
"Average negative primary particle concentration [mol.m-3]": c_s_xrav_n_p1,
"Average negative secondary particle concentration "
"[mol.m-3]": c_s_xrav_n_p2,
"Positive particle concentration [mol.m-3]": c_s_p,
"Average positive particle concentration [mol.m-3]": c_s_xrav_p,
"Electrolyte concentration [mol.m-3]": c_e,
"Negative electrolyte concentration [mol.m-3]": c_e_n,
"Separator electrolyte concentration [mol.m-3]": c_e_s,
"Positive electrolyte concentration [mol.m-3]": c_e_p,
"Negative electrode potential [V]": phi_s_n,
"Positive electrode potential [V]": phi_s_p,
"Electrolyte potential [V]": phi_e,
"Negative electrolyte potential [V]": phi_e_n,
"Separator electrolyte potential [V]": phi_e_s,
"Positive electrolyte potential [V]": phi_e_p,
"Current [A]": I,
"Current variable [A]": I, # for compatibility with pybamm.Experiment
"Discharge capacity [A.h]": Q,
"Time [s]": pybamm.t,
"Voltage [V]": voltage,
"Battery voltage [V]": voltage * num_cells,
"Negative electrode primary open-circuit potential [V]": ocp_n_p1,
"Negative electrode secondary open-circuit potential [V]": ocp_n_p2,
"X-averaged negative electrode primary open-circuit potential "
"[V]": ocp_av_n_p1,
"X-averaged negative electrode secondary open-circuit potential "
"[V]": ocp_av_n_p2,
"Positive electrode open-circuit potential [V]": ocp_p,
"X-averaged positive electrode open-circuit potential [V]": ocp_av_p,
"Negative electrode primary interfacial current density [A.m-2]": j_n_p1,
"Negative electrode secondary interfacial current density [A.m-2]": j_n_p2,
"X-averaged negative electrode primary interfacial current density "
"[A.m-2]": j_n_p1_av,
"X-averaged negative electrode secondary interfacial current density "
"[A.m-2]": j_n_p2_av,
"Negative electrode primary volumetric interfacial current density "
"[A.m-3]": a_j_n_p1,
"Negative electrode secondary volumetric interfacial current density "
"[A.m-3]": a_j_n_p2,
"X-averaged negative electrode primary volumetric "
"interfacial current density [A.m-3]": a_j_n_p1_av,
"X-averaged negative electrode secondary volumetric "
"interfacial current density [A.m-3]": a_j_n_p2_av,
}
# Events specify points at which a solution should terminate
self.events += [
pybamm.Event("Minimum voltage [V]", voltage - self.param.voltage_low_cut),
pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - voltage),
]
@property
def default_parameter_values(self):
return pybamm.ParameterValues("Chen2020_composite")