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

import pybamm

from .base_lithium_ion_model import BaseModel


[docs] class Basic3DThermalSPM(BaseModel): """Single Particle Model (SPM) model of a lithium-ion battery, from :footcite:t:`Marquis2019`. This class differs from the :class:`pybamm.lithium_ion.SPM` model class in that it shows the whole model in a single class. This comes at the cost of flexibility in combining different physical effects, and in general the main SPM class should be used instead. The model is based on the SPM, but with a separate cell domain for the temperature variable. This allows for a more detailed treatment of the thermal effects in the cell, as the temperature can vary independently of the other variables in the model. Parameters ---------- name : str, optional The name of the model. """ def __init__(self, options=None, name="SPM with Separate Cell Domain"): super().__init__(options, name) pybamm.citations.register("Marquis2019") # `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_s_n = pybamm.Variable( "X-averaged negative particle concentration [mol.m-3]", domain="negative particle", ) c_s_p = pybamm.Variable( "X-averaged positive particle concentration [mol.m-3]", domain="positive particle", ) T = pybamm.Variable("Cell temperature [K]", domain="cell") if self.options.get("cell geometry") == "pouch": self.x_cell = pybamm.SpatialVariable("x", domain="cell") self.y_cell = pybamm.SpatialVariable("y", domain="cell") self.z_cell = pybamm.SpatialVariable("z", domain="cell") integration_vars = [self.x_cell, self.y_cell, self.z_cell] elif self.options.get("cell geometry") == "cylindrical": self.r_cell = pybamm.SpatialVariable("r_macro", domain="cell") self.z_cell = pybamm.SpatialVariable("z", domain="cell") integration_vars = [self.r_cell, self.z_cell] else: raise ValueError( f"Geometry type '{self.options.get('cell geometry')}' is not supported. " "Supported types are 'pouch' and 'cylindrical'." ) # pragma: no cover volume = pybamm.Integral(pybamm.PrimaryBroadcast(1.0, "cell"), integration_vars) T_av = pybamm.Integral(T, integration_vars) / volume ###################### # Other set-up ###################### # Current density i_cell = self.param.current_density_with_time a_n = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ j_n = i_cell / (self.param.n.L * a_n) j_p = -i_cell / (self.param.p.L * a_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 ###################### N_s_n = -self.param.n.prim.D(c_s_n, T_av) * pybamm.grad(c_s_n) N_s_p = -self.param.p.prim.D(c_s_p, T_av) * 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) # 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 c_s_surf_n = pybamm.surf(c_s_n) c_s_surf_p = pybamm.surf(c_s_p) # Boundary conditions must be provided for equations with spatial derivatives 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_av))), "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_av))), "Neumann", ), } # c_n_init and c_p_init are functions of r and x, but for the SPM we # take the x-averaged value since there is no x-dependence in the particles self.initial_conditions[c_s_n] = pybamm.x_average(self.param.n.prim.c_init) self.initial_conditions[c_s_p] = pybamm.x_average(self.param.p.prim.c_init) # Events specify points at which a solution should terminate sto_surf_n = c_s_surf_n / self.param.n.prim.c_max sto_surf_p = c_s_surf_p / self.param.p.prim.c_max self.events += [ pybamm.Event( "Minimum negative particle surface stoichiometry", pybamm.min(sto_surf_n) - 0.01, ), pybamm.Event( "Maximum negative particle surface stoichiometry", (1 - 0.01) - pybamm.max(sto_surf_n), ), pybamm.Event( "Minimum positive particle surface stoichiometry", pybamm.min(sto_surf_p) - 0.01, ), pybamm.Event( "Maximum positive particle surface stoichiometry", (1 - 0.01) - pybamm.max(sto_surf_p), ), ] # Note that the SPM does not have any algebraic equations, so the `algebraic` # dictionary remains empty ###################### # (Some) variables ###################### # Interfacial reactions RT_F = self.param.R * T_av / self.param.F j0_n = self.param.n.prim.j0(self.param.c_e_init_av, c_s_surf_n, T_av) j0_p = self.param.p.prim.j0(self.param.c_e_init_av, c_s_surf_p, T_av) eta_n = (2 / self.param.n.prim.ne) * RT_F * pybamm.arcsinh(j_n / (2 * j0_n)) eta_p = (2 / self.param.p.prim.ne) * RT_F * pybamm.arcsinh(j_p / (2 * j0_p)) phi_s_n = 0 phi_e = -eta_n - self.param.n.prim.U(sto_surf_n, T_av) phi_s_p = eta_p + phi_e + self.param.p.prim.U(sto_surf_p, T_av) V = phi_s_p num_cells = pybamm.Parameter( "Number of cells connected in series to make a battery" ) # Compute heat source # Reversible (entropic) heating dUdT_n = self.param.n.prim.dUdT(sto_surf_n) dUdT_p = self.param.p.prim.dUdT(sto_surf_p) Q_rev_n = a_n * j_n * T_av * dUdT_n Q_rev_p = a_p * j_p * T_av * dUdT_p # Irreversible (Joule) heating Q_irr_n = a_n * j_n * eta_n Q_irr_p = a_p * j_p * eta_p # Total heating in each electrode domain Q_total_n = Q_rev_n + Q_irr_n Q_total_p = Q_rev_p + Q_irr_p # Average the 1D heat source and broadcast to the 3D 'cell' domain L_n = self.param.n.L L_p = self.param.p.L L_x = self.param.L_x # Total cell thickness # Calculate the true volume-weighted average heat source in W/m^3 Q_vol_avg = (Q_total_n * L_n + Q_total_p * L_p) / L_x # Broadcast this uniform volumetric heat source to the entire 3D cell domain Q_source = pybamm.PrimaryBroadcast(Q_vol_avg, "cell") # Wrap in source term to get the correct mass matrix Q_source = pybamm.source(Q_source, T) # Define the 3D heat equation # Effective parameters are functions of temperature rho_c_p_eff = self.param.rho_c_p_eff(T) lambda_eff = self.param.lambda_eff(T) # The heat equation is d(rho*cp*T)/dt = div(lambda*grad(T)) + Q term1 = lambda_eff * pybamm.laplacian(T) term2 = pybamm.inner(pybamm.grad(lambda_eff), pybamm.grad(T)) self.rhs[T] = (term1 + term2 + Q_source) / rho_c_p_eff # Cooling taken care of by boundary conditions self.initial_conditions[T] = pybamm.PrimaryBroadcast(self.param.T_init, "cell") self.set_thermal_bcs(T) whole_cell = ["negative electrode", "separator", "positive electrode"] # The `variables` dictionary contains all variables that might be useful for # visualising the solution of the model # Primary broadcasts are used to broadcast scalar quantities across a domain # into a vector of the right shape, for multiplying with other vectors self.variables = { "Time [s]": pybamm.t, "Discharge capacity [A.h]": Q, "X-averaged negative particle concentration [mol.m-3]": c_s_n, "Negative particle surface " "concentration [mol.m-3]": pybamm.PrimaryBroadcast( c_s_surf_n, "negative electrode" ), "Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast( self.param.c_e_init_av, whole_cell ), "X-averaged positive particle concentration [mol.m-3]": c_s_p, "Positive particle surface " "concentration [mol.m-3]": pybamm.PrimaryBroadcast( c_s_surf_p, "positive electrode" ), "Current [A]": I, "Current variable [A]": I, # for compatibility with pybamm.Experiment "Negative electrode potential [V]": pybamm.PrimaryBroadcast( phi_s_n, "negative electrode" ), "Electrolyte potential [V]": pybamm.PrimaryBroadcast(phi_e, whole_cell), "Positive electrode potential [V]": pybamm.PrimaryBroadcast( phi_s_p, "positive electrode" ), "Voltage [V]": V, "Battery voltage [V]": V * num_cells, } # Add new thermal variables self.variables.update( { "Cell temperature [K]": T, "Volume-averaged cell temperature [K]": T_av, } ) # Events specify points at which a solution should terminate self.events += [ pybamm.Event("Minimum voltage [V]", V - self.param.voltage_low_cut), pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - V), ] def set_thermal_bcs(self, T): geometry_type = self.options.get("cell geometry", "pouch") if geometry_type == "pouch": # Reuse the spatial variables created in __init__ T_amb = self.param.T_amb(self.y_cell, self.z_cell, pybamm.t) face_params = { "x_min": self.param.h_edge_x_min, "x_max": self.param.h_edge_x_max, "y_min": self.param.h_edge_y_min, "y_max": self.param.h_edge_y_max, "z_min": self.param.h_edge_z_min, "z_max": self.param.h_edge_z_max, } elif geometry_type == "cylindrical": # Reuse the spatial variables created in __init__ T_amb = self.param.T_amb(self.r_cell, self.z_cell, pybamm.t) face_params = { "r_min": self.param.h_edge_radial_min, "r_max": self.param.h_edge_radial_max, "z_min": self.param.h_edge_z_min, "z_max": self.param.h_edge_z_max, } else: raise ValueError( f"Geometry type '{geometry_type}' is not supported. " "Supported types are 'pouch' and 'cylindrical'." ) # pragma: no cover self.boundary_conditions[T] = {} for face, h_coeff in face_params.items(): # Evaluate T and lambda_eff at the boundary T_boundary = pybamm.boundary_value(T, face) lambda_eff_boundary = pybamm.boundary_value(self.param.lambda_eff(T), face) # T_amb is already evaluated at the boundary coordinates q_face = -h_coeff * (T_boundary - T_amb) self.boundary_conditions[T][face] = ( q_face / lambda_eff_boundary, "Neumann", )