Source code for pybamm.models.full_battery_models.lithium_ion.basic_dfn_unstructured

#
# Basic Doyle-Fuller-Newman (DFN) Model — 2D/3D Unstructured FVM
#
from __future__ import annotations

import pybamm
from pybamm.models.full_battery_models.lithium_ion.base_lithium_ion_model import (
    BaseModel,
)


[docs] class BasicDFNUnstructured(BaseModel): """Doyle-Fuller-Newman (DFN) model discretised with :class:`~pybamm.FiniteVolumeUnstructured`. The ``"dimensionality"`` option is the number of directions resolved besides the through-cell *x*: 1 (default) meshes (x, z) with quads and 2 meshes (x, y, z) with hexahedra. Pass ``submesh_types`` to :class:`pybamm.Simulation` for triangles or tetrahedra. Tetrahedra only solve on coarse meshes: on the thin cell they form sliver elements, on which the diffusion operator is unstable at the default ``var_pts``. Parameters ---------- options : dict, optional A dictionary of options to be passed to the model. See :class:`pybamm.BatteryModelOptions`. name : str, optional The name of the model. """ def __init__(self, options=None, name="Doyle-Fuller-Newman model (unstructured)"): options = {"dimensionality": 1, **(options or {})} super().__init__(options, name) if self.options["dimensionality"] not in (1, 2): raise pybamm.OptionError( "BasicDFNUnstructured needs a 'dimensionality' of 1 (x-z mesh) " "or 2 (x-y-z mesh)" ) three_dimensional = self.options["dimensionality"] == 2 pybamm.citations.register("Marquis2019") Q = pybamm.Variable("Discharge capacity [A.h]") whole_cell = ["negative electrode", "separator", "positive electrode"] axes = ["x", "y", "z"] if three_dimensional else ["x", "z"] coords_n = [ pybamm.SpatialVariable( f"{axis}_n", "negative electrode", coord_sys="cartesian" ) for axis in axes ] coords_s = [ pybamm.SpatialVariable(f"{axis}_s", "separator", coord_sys="cartesian") for axis in axes ] coords_p = [ pybamm.SpatialVariable( f"{axis}_p", "positive electrode", coord_sys="cartesian" ) for axis in axes ] coords = [ pybamm.SpatialVariable(axis, whole_cell, coord_sys="cartesian") for axis in axes ] axis_input_names = { "x": "Through-cell distance (x) [m]", "y": "Horizontal distance (y) [m]", "z": "Vertical distance (z) [m]", } input_names = [axis_input_names[axis] for axis in axes] inputs_n = dict(zip(input_names, coords_n, strict=True)) inputs_s = dict(zip(input_names, coords_s, strict=True)) inputs_p = dict(zip(input_names, coords_p, strict=True)) # A 2D slice stands for a cell of width L_y, so volume integrals are # scaled by the width the mesh does not resolve width = 1 if three_dimensional else self.param.L_y c_e_n = pybamm.Variable( "Negative electrolyte concentration [mol.m-3]", domain="negative electrode", ) c_e_s = pybamm.Variable( "Separator electrolyte concentration [mol.m-3]", domain="separator", ) c_e_p = pybamm.Variable( "Positive electrolyte concentration [mol.m-3]", domain="positive electrode", ) c_e = pybamm.concatenation(c_e_n, c_e_s, c_e_p) phi_e_n = pybamm.Variable( "Negative electrolyte potential [V]", domain="negative electrode", ) phi_e_s = pybamm.Variable( "Separator electrolyte potential [V]", domain="separator", ) phi_e_p = pybamm.Variable( "Positive electrolyte potential [V]", domain="positive electrode", ) phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p) phi_s_n = pybamm.Variable( "Negative electrode potential [V]", domain="negative electrode" ) phi_s_p = pybamm.Variable( "Positive electrode potential [V]", domain="positive electrode", ) c_s_n = pybamm.Variable( "Negative particle concentration [mol.m-3]", domain="negative particle", auxiliary_domains={"secondary": "negative electrode"}, ) c_s_p = pybamm.Variable( "Positive particle concentration [mol.m-3]", domain="positive particle", auxiliary_domains={"secondary": "positive electrode"}, ) T = self.param.T_init ###################### # Other set-up ###################### i_cell = self.param.current_density_with_time eps_n = pybamm.FunctionParameter("Negative electrode porosity", inputs_n) eps_s = pybamm.FunctionParameter("Separator porosity", inputs_s) eps_p = pybamm.FunctionParameter("Positive electrode porosity", inputs_p) eps = pybamm.concatenation(eps_n, eps_s, eps_p) eps_s_n = pybamm.FunctionParameter( "Negative electrode active material volume fraction", inputs_n, ) eps_s_p = pybamm.FunctionParameter( "Positive electrode active material volume fraction", inputs_p, ) tor = pybamm.concatenation( eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e ) a_n = 3 * eps_s_n / self.param.n.prim.R_typ a_p = 3 * eps_s_p / self.param.p.prim.R_typ # Interfacial reactions c_s_surf_n = pybamm.surf(c_s_n) sto_surf_n = c_s_surf_n / self.param.n.prim.c_max j0_n = self.param.n.prim.j0(c_e_n, c_s_surf_n, T) delta_phi_n = phi_s_n - phi_e_n eta_n = delta_phi_n - self.param.n.prim.U(sto_surf_n, T) Feta_RT_n = self.param.F * eta_n / (self.param.R * T) j_n = 2 * j0_n * pybamm.sinh(self.param.n.prim.ne / 2 * Feta_RT_n) c_s_surf_p = pybamm.surf(c_s_p) sto_surf_p = c_s_surf_p / self.param.p.prim.c_max j0_p = self.param.p.prim.j0(c_e_p, c_s_surf_p, T) delta_phi_p = phi_s_p - phi_e_p eta_p = delta_phi_p - self.param.p.prim.U(sto_surf_p, T) Feta_RT_p = self.param.F * eta_p / (self.param.R * T) j_s = pybamm.PrimaryBroadcast(0, "separator") j_p = 2 * j0_p * pybamm.sinh(self.param.p.prim.ne / 2 * Feta_RT_p) a_j_n = a_n * j_n a_j_p = a_p * j_p a_j = pybamm.concatenation(a_j_n, j_s, a_j_p) ###################### # State of Charge ###################### current = self.param.current_with_time self.rhs[Q] = current / 3600 self.initial_conditions[Q] = pybamm.Scalar(0) N_s_n = -self.param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n) N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p) self.rhs[c_s_n] = -pybamm.div(N_s_n) self.rhs[c_s_p] = -pybamm.div(N_s_p) self.boundary_conditions[c_s_n] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( -j_n / (self.param.F * pybamm.surf(self.param.n.prim.D(c_s_n, 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] = self.param.n.prim.c_init self.initial_conditions[c_s_p] = self.param.p.prim.c_init c_s_n_av = pybamm.RAverage(c_s_n) c_s_p_av = pybamm.RAverage(c_s_p) solid_lithium_negative = width * pybamm.Integral(c_s_n_av * eps_s_n, coords_n) solid_lithium_positive = width * pybamm.Integral(c_s_p_av * eps_s_p, coords_p) total_solid_lithium = solid_lithium_negative + solid_lithium_positive ###################### # Current in the solid ###################### # Multiply by the squared length of each meshed direction to improve # conditioning L_scale = self.param.L_x**2 * self.param.L_z**2 sides = ["left", "right", "top", "bottom"] if three_dimensional: L_scale *= self.param.L_y**2 sides += ["front", "back"] zero_flux = {side: (pybamm.Scalar(0), "Neumann") for side in sides} sigma_eff_n = self.param.n.sigma(sto_surf_n, T) * eps_s_n**self.param.n.b_s sigma_eff_p = self.param.p.sigma(sto_surf_p, T) * eps_s_p**self.param.p.b_s self.algebraic[phi_s_n] = L_scale * ( pybamm.div(-sigma_eff_n * pybamm.grad(phi_s_n)) + a_j_n ) self.algebraic[phi_s_p] = L_scale * ( pybamm.div(-sigma_eff_p * pybamm.grad(phi_s_p)) + a_j_p ) self.boundary_conditions[phi_s_n] = { **zero_flux, "left": (pybamm.Scalar(0), "Dirichlet"), } self.boundary_conditions[phi_s_p] = { **zero_flux, "right": (i_cell / pybamm.boundary_value(-sigma_eff_p, "right"), "Neumann"), } self.initial_conditions[phi_s_n] = pybamm.Scalar(0) self.initial_conditions[phi_s_p] = self.param.ocv_init ###################### # Current in the electrolyte ###################### kappa_eff = self.param.kappa_e(c_e, T) * tor kappa_D_eff = kappa_eff * self.param.chiRT_over_Fc(c_e, T) i_e = kappa_D_eff * pybamm.grad(c_e) - kappa_eff * pybamm.grad(phi_e) # The unstructured TPFA operator only accepts div(D * grad(u)) products, # so div(i_e) is written out term by term self.algebraic[phi_e] = L_scale * ( pybamm.div(kappa_D_eff * pybamm.grad(c_e)) - pybamm.div(kappa_eff * pybamm.grad(phi_e)) - a_j ) self.boundary_conditions[phi_e] = dict(zero_flux) self.initial_conditions[phi_e] = -self.param.n.prim.U_init ###################### # Electrolyte concentration ###################### D_e_eff = tor * self.param.D_e(c_e, T) t_plus = self.param.t_plus(c_e, T) N_e = -D_e_eff * pybamm.grad(c_e) + t_plus * i_e / self.param.F # The migration term t_plus * i_e / F is kept inside the flux so that the # balance is conservative when t_plus depends on c_e (see #5745) self.rhs[c_e] = (1 / eps) * ( pybamm.div(D_e_eff * pybamm.grad(c_e)) - pybamm.div((t_plus * kappa_D_eff / self.param.F) * pybamm.grad(c_e)) + pybamm.div((t_plus * kappa_eff / self.param.F) * pybamm.grad(phi_e)) + a_j / self.param.F ) self.boundary_conditions[c_e] = dict(zero_flux) self.initial_conditions[c_e] = self.param.c_e_init ###################### # (Some) variables ###################### voltage = pybamm.boundary_value(phi_s_p, "top-right") num_cells = pybamm.Parameter( "Number of cells connected in series to make a battery" ) total_lithium = width * pybamm.Integral(c_e * eps, coords) self.variables = { "Negative particle concentration [mol.m-3]": c_s_n, "Total lithium [mol]": total_lithium, "Negative particle surface concentration [mol.m-3]": c_s_surf_n, "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, "Positive particle concentration [mol.m-3]": c_s_p, "Positive particle surface concentration [mol.m-3]": c_s_surf_p, "Current [A]": current, "Current variable [A]": current, "Negative electrode potential [V]": phi_s_n, "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, "Positive electrode potential [V]": phi_s_p, "Voltage [V]": voltage, "Voltage expression [V]": voltage, "Battery voltage [V]": voltage * num_cells, "Time [s]": pybamm.t, "Discharge capacity [A.h]": Q, "Sum of volumetric interfacial current densities [A.m-3]": a_j, "Electrolyte current density [A.m-2]": i_e, "Negative electrode surface concentration [mol.m-3]": c_s_surf_n, "Negative electrode surface stoichiometry": sto_surf_n, "Positive electrode surface concentration [mol.m-3]": c_s_surf_p, "Positive electrode surface stoichiometry": sto_surf_p, "Positive electrode surface potential difference [V]": delta_phi_p, "Negative electrode surface potential difference [V]": delta_phi_n, "Positive electrode overpotential [V]": eta_p, "Negative electrode overpotential [V]": eta_n, "Positive electrode ocp [V]": self.param.p.prim.U(sto_surf_p, T), "Negative electrode ocp [V]": self.param.n.prim.U(sto_surf_n, T), "Positive electrode interfacial current density [A.m-2]": j_p, "Negative electrode interfacial current density [A.m-2]": j_n, "Electrolyte flux [mol.m-2.s-1]": N_e, "Positive solid lithium [mol]": solid_lithium_positive, "Negative solid lithium [mol]": solid_lithium_negative, "Total solid lithium [mol]": total_solid_lithium, } 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_geometry(self): transverse = {"z": {"min": 0, "max": self.param.L_z}} if self.options["dimensionality"] == 2: transverse = {"y": {"min": 0, "max": self.param.L_y}, **transverse} return { "negative electrode": { "x_n": {"min": 0, "max": self.param.n.L}, **transverse, }, "separator": { "x_s": {"min": self.param.n.L, "max": self.param.n.L + self.param.s.L}, **transverse, }, "positive electrode": { "x_p": { "min": self.param.n.L + self.param.s.L, "max": self.param.n.L + self.param.s.L + self.param.p.L, }, **transverse, }, "positive particle": { "r_p": {"min": 0, "max": self.param.p.prim.R_typ}, }, "negative particle": { "r_n": {"min": 0, "max": self.param.n.prim.R_typ}, }, "current collector": { "z": {"position": 0}, }, } @property def default_spatial_methods(self): return { "negative electrode": pybamm.FiniteVolumeUnstructured(), "separator": pybamm.FiniteVolumeUnstructured(), "positive electrode": pybamm.FiniteVolumeUnstructured(), "positive particle": pybamm.FiniteVolume(), "negative particle": pybamm.FiniteVolume(), "current collector": pybamm.ZeroDimensionalSpatialMethod(), } @property def default_submesh_types(self): if self.options["dimensionality"] == 1: element_type = "quad" else: element_type = "hexahedron" return { "negative electrode": pybamm.UnstructuredMeshGenerator( element_type=element_type ), "separator": pybamm.UnstructuredMeshGenerator(element_type=element_type), "positive electrode": pybamm.UnstructuredMeshGenerator( element_type=element_type ), "positive particle": pybamm.Uniform1DSubMesh, "negative particle": pybamm.Uniform1DSubMesh, "current collector": pybamm.SubMesh0D, } @property def default_var_pts(self): if self.options["dimensionality"] == 1: return {"x_n": 20, "x_s": 30, "x_p": 20, "r_n": 20, "r_p": 20, "z": 10} return { "x_n": 10, "x_s": 10, "x_p": 10, "r_n": 20, "r_p": 20, "y": 5, "z": 5, }