Source code for pybamm.models.submodels.particle.msmr_diffusion

#
# Class for particles using the MSMR model
#
import pybamm

from .base_particle import BaseParticle


[docs] class MSMRDiffusion(BaseParticle): """ Class for molar conservation in particles within the Multi-Species Multi-Reaction framework :footcite:t:`Baker2018`. The thermodynamic model is presented in :footcite:t:`Verbrugge2017`, along with parameter values for a number of substitutional materials. In this submodel, the stoichiometry depends on the potential in the particle and the temperature, so dUdT is not used. See `:meth:`pybamm.LithiumIonParameters.dUdT` for more explanation. Parameters ---------- param : parameter class The parameters to use for this submodel domain : str The domain of the model either 'Negative' or 'Positive' options: dict A dictionary of options to be passed to the model. See :class:`pybamm.BaseBatteryModel` phase : str, optional Phase of the particle (default is "primary") x_average : bool Whether the particle concentration is averaged over the x-direction """ def __init__(self, param, domain, options, phase="primary", x_average=False): super().__init__(param, domain, options, phase) self.x_average = x_average pybamm.citations.register("Baker2018") pybamm.citations.register("Verbrugge2017")
[docs] def get_fundamental_variables(self): domain, Domain = self.domain_Domain phase_name = self.phase_name variables = {} # Define "particle" potential variables. In the MSMR model, we solve for the # potential as a function of position within the electrode and particles (and # particle-size distribution, if applicable). The potential is then used to # calculate the stoichiometry, which is used to calculate the particle # concentration. if self.size_distribution is False: if self.x_average is False: U = pybamm.Variable( f"{Domain} {phase_name}particle potential [V]", f"{domain} {phase_name}particle", auxiliary_domains={ "secondary": f"{domain} electrode", "tertiary": "current collector", }, ) U.print_name = f"U_{domain[0]}" else: U_xav = pybamm.Variable( f"X-averaged {domain} {phase_name}particle potential [V]", f"{domain} {phase_name}particle", auxiliary_domains={"secondary": "current collector"}, ) U_xav.print_name = f"U_{domain[0]}_xav" U = pybamm.SecondaryBroadcast(U_xav, f"{domain} electrode") else: if self.x_average is False: U_distribution = pybamm.Variable( f"{Domain} {phase_name}particle potential distribution [V]", domain=f"{domain} {phase_name}particle", auxiliary_domains={ "secondary": f"{domain} {phase_name}particle size", "tertiary": f"{domain} electrode", "quaternary": "current collector", }, ) R = pybamm.SpatialVariable( f"R_{domain[0]}", domain=[f"{domain} {phase_name}particle size"], auxiliary_domains={ "secondary": f"{domain} electrode", "tertiary": "current collector", }, coord_sys="cartesian", ) variables = self._get_distribution_variables(R) f_v_dist = variables[ f"{Domain} volume-weighted {phase_name}" "particle-size distribution [m-1]" ] else: U_distribution = pybamm.Variable( f"X-averaged {domain} {phase_name}particle " "potential distribution [V]", domain=f"{domain} {phase_name}particle", auxiliary_domains={ "secondary": f"{domain} {phase_name}particle size", "tertiary": "current collector", }, ) R = pybamm.SpatialVariable( f"R_{domain[0]}", domain=[f"{domain} {phase_name}particle size"], auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) variables = self._get_distribution_variables(R) f_v_dist = variables[ f"X-averaged {domain} volume-weighted {phase_name}" "particle-size distribution [m-1]" ] # Standard potential distribution_variables variables.update( self._get_standard_potential_distribution_variables(U_distribution) ) # Standard size-averaged variables. Average potentials using # the volume-weighted distribution since they are volume-based # quantities. Necessary for output variables "Total lithium in # negative electrode [mol]", etc, to be calculated correctly U = pybamm.Integral(f_v_dist * U_distribution, R) if self.x_average is True: U = pybamm.SecondaryBroadcast(U, [f"{domain} electrode"]) # Standard potential variables variables.update(self._get_standard_potential_variables(U)) return variables
[docs] def get_coupled_variables(self, variables): domain, Domain = self.domain_Domain phase_name = self.phase_name if self.size_distribution is False: if self.x_average is False: x = variables[f"{Domain} {phase_name}particle stoichiometry"] dxdU = variables[ f"{Domain} {phase_name}particle differential stoichiometry [V-1]" ] U = variables[f"{Domain} {phase_name}particle potential [V]"] T = pybamm.PrimaryBroadcast( variables[f"{Domain} electrode temperature [K]"], [f"{domain} {phase_name}particle"], ) R_nondim = variables[f"{Domain} {phase_name}particle radius"] j = variables[ f"{Domain} electrode {phase_name}" "interfacial current density [A.m-2]" ] else: x = variables[f"X-averaged {domain} {phase_name}particle stoichiometry"] dxdU = variables[ f"X-averaged {domain} {phase_name}particle differential " "stoichiometry [V-1]" ] U = variables[f"X-averaged {domain} {phase_name}particle potential [V]"] T = pybamm.PrimaryBroadcast( variables[f"X-averaged {domain} electrode temperature [K]"], [f"{domain} {phase_name}particle"], ) R_nondim = 1 j = variables[ f"X-averaged {domain} electrode {phase_name}" "interfacial current density [A.m-2]" ] R_broad_nondim = R_nondim else: R_nondim = variables[f"{Domain} {phase_name}particle sizes"] R_broad_nondim = pybamm.PrimaryBroadcast( R_nondim, [f"{domain} {phase_name}particle"] ) if self.x_average is False: x = variables[ f"{Domain} {phase_name}particle stoichiometry distribution" ] dxdU = variables[ f"{Domain} {phase_name}particle differential stoichiometry " "distribution [V-1]" ] U = variables[ f"{Domain} {phase_name}particle potential distribution [V]" ] # broadcast T to "particle size" domain then again into "particle" T = pybamm.PrimaryBroadcast( variables[f"{Domain} electrode temperature [K]"], [f"{domain} {phase_name}particle size"], ) T = pybamm.PrimaryBroadcast(T, [f"{domain} {phase_name}particle"]) j = variables[ f"{Domain} electrode {phase_name}interfacial " "current density distribution [A.m-2]" ] else: x = variables[ f"X-averaged {domain} {phase_name}particle " "stoichiometry distribution" ] dxdU = variables[ f"X-averaged {domain} {phase_name}particle " "differential stoichiometry distribution [V-1]" ] U = variables[ f"X-averaged {domain} {phase_name}particle " "potential distribution [V]" ] # broadcast to "particle size" domain then again into "particle" T = pybamm.PrimaryBroadcast( variables[f"X-averaged {domain} electrode temperature [K]"], [f"{domain} {phase_name}particle size"], ) T = pybamm.PrimaryBroadcast(T, [f"{domain} {phase_name}particle"]) j = variables[ f"X-averaged {domain} electrode {phase_name}interfacial " "current density distribution [A.m-2]" ] # Note: diffusivity is given as a function of concentration here, # not stoichiometry c_max = self.phase_param.c_max current = variables["Total current density [A.m-2]"] D_eff = self._get_effective_diffusivity(x * c_max, T, current) f = self.param.F / (self.param.R * T) N_s = c_max * x * (1 - x) * f * D_eff * pybamm.grad(U) variables.update( { f"{Domain} {phase_name}particle rhs [V.s-1]": -(1 / (R_broad_nondim**2)) * pybamm.div(N_s) / c_max / dxdU, f"{Domain} {phase_name}particle bc [V.m-1]": j * R_nondim / self.param.F / pybamm.surf(c_max * x * (1 - x) * f * D_eff), } ) if self.x_average is True: if self.size_distribution is True: D_eff = pybamm.TertiaryBroadcast(D_eff, [f"{domain} electrode"]) N_s = pybamm.TertiaryBroadcast(N_s, [f"{domain} electrode"]) else: D_eff = pybamm.SecondaryBroadcast(D_eff, [f"{domain} electrode"]) N_s = pybamm.SecondaryBroadcast(N_s, [f"{domain} electrode"]) if self.size_distribution is True: # Size-dependent flux variables variables.update( self._get_standard_diffusivity_distribution_variables(D_eff) ) variables.update(self._get_standard_flux_distribution_variables(N_s)) # Size-averaged flux variables D_eff = pybamm.size_average(D_eff) R = pybamm.SpatialVariable("R", domains=D_eff.domains) f_a_dist = self.phase_param.f_a_dist(R) N_s = pybamm.Integral(f_a_dist * N_s, R) variables.update(self._get_standard_diffusivity_variables(D_eff)) variables.update(self._get_standard_flux_variables(N_s)) return variables
[docs] def set_rhs(self, variables): domain, Domain = self.domain_Domain phase_name = self.phase_name if self.size_distribution is False: if self.x_average is False: U = variables[f"{Domain} {phase_name}particle potential [V]"] else: U = variables[f"X-averaged {domain} {phase_name}particle potential [V]"] else: if self.x_average is False: U = variables[ f"{Domain} {phase_name}particle potential distribution [V]" ] else: U = variables[ f"X-averaged {domain} {phase_name}particle " "potential distribution [V]" ] self.rhs = {U: variables[f"{Domain} {phase_name}particle rhs [V.s-1]"]}
[docs] def set_boundary_conditions(self, variables): domain, Domain = self.domain_Domain phase_name = self.phase_name if self.size_distribution is False: if self.x_average is False: U = variables[f"{Domain} {phase_name}particle potential [V]"] else: U = variables[f"X-averaged {domain} {phase_name}particle potential [V]"] else: if self.x_average is False: U = variables[ f"{Domain} {phase_name}particle potential distribution [V]" ] else: U = variables[ f"X-averaged {domain} {phase_name}particle " "potential distribution [V]" ] rbc = variables[f"{Domain} {phase_name}particle bc [V.m-1]"] self.boundary_conditions = { U: {"left": (pybamm.Scalar(0), "Neumann"), "right": (rbc, "Neumann")} }
[docs] def set_initial_conditions(self, variables): domain, Domain = self.domain_Domain phase_name = self.phase_name U_init = self.phase_param.U_init if self.size_distribution is False: if self.x_average is False: U = variables[f"{Domain} {phase_name}particle potential [V]"] else: U = variables[f"X-averaged {domain} {phase_name}particle potential [V]"] else: if self.x_average is False: U = variables[ f"{Domain} {phase_name}particle potential distribution [V]" ] else: U = variables[ f"X-averaged {domain} {phase_name}particle " "potential distribution [V]" ] self.initial_conditions = {U: U_init}
def _get_standard_potential_variables(self, U): """ A private function to obtain the standard variables which can be derived from the potential. """ domain, Domain = self.domain_Domain phase_name = self.phase_name U_surf = pybamm.surf(U) U_surf_av = pybamm.x_average(U_surf) U_xav = pybamm.x_average(U) U_rav = pybamm.r_average(U) U_av = pybamm.r_average(U_xav) variables = { f"{Domain} {phase_name}particle potential [V]": U, f"X-averaged {domain} {phase_name}particle potential [V]": U_xav, f"R-averaged {domain} {phase_name}particle potential [V]": U_rav, f"Average {domain} {phase_name}particle potential [V]": U_av, f"{Domain} {phase_name}particle surface potential [V]": U_surf, f"X-averaged {domain} {phase_name}particle " "surface potential [V]": U_surf_av, f"Minimum {domain} {phase_name}particle potential [V]": pybamm.min(U), f"Maximum {domain} {phase_name}particle potential [V]": pybamm.max(U), f"Minimum {domain} {phase_name}particle surface potential [V]": pybamm.min( U_surf ), f"Maximum {domain} {phase_name}particle surface potential [V]": pybamm.max( U_surf ), } return variables def _get_standard_potential_distribution_variables(self, U): """ A private function to obtain the standard variables which can be derived from the potential distribution in particle size. """ domain, Domain = self.domain_Domain phase_name = self.phase_name # Broadcast and x-average when necessary if U.domain == [f"{domain} {phase_name}particle size"] and U.domains[ "secondary" ] != [f"{domain} electrode"]: # X-avg potential distribution U_xav_distribution = pybamm.PrimaryBroadcast( U, [f"{domain} {phase_name}particle"] ) # Surface potential distribution variables U_surf_xav_distribution = U U_surf_distribution = pybamm.SecondaryBroadcast( U_surf_xav_distribution, [f"{domain} electrode"] ) # potential distribution in all domains. U_distribution = pybamm.PrimaryBroadcast( U_surf_distribution, [f"{domain} {phase_name}particle"] ) elif U.domain == [f"{domain} {phase_name}particle"] and ( U.domains["tertiary"] != [f"{domain} electrode"] ): # X-avg potential distribution U_xav_distribution = U # Surface potential distribution variables U_surf_xav_distribution = pybamm.surf(U_xav_distribution) U_surf_distribution = pybamm.SecondaryBroadcast( U_surf_xav_distribution, [f"{domain} electrode"] ) # potential distribution in all domains U_distribution = pybamm.TertiaryBroadcast( U_xav_distribution, [f"{domain} electrode"] ) elif U.domain == [f"{domain} {phase_name}particle size"] and U.domains[ "secondary" ] == [f"{domain} electrode"]: # Surface potential distribution variables U_surf_distribution = U U_surf_xav_distribution = pybamm.x_average(U) # X-avg potential distribution U_xav_distribution = pybamm.PrimaryBroadcast( U_surf_xav_distribution, [f"{domain} {phase_name}particle"] ) # potential distribution in all domains U_distribution = pybamm.PrimaryBroadcast( U_surf_distribution, [f"{domain} {phase_name}particle"] ) else: U_distribution = U # x-average the *tertiary* domain. # NOTE: not yet implemented. Make 0.5 everywhere U_xav_distribution = pybamm.FullBroadcast( 0.5, [f"{domain} {phase_name}particle"], { "secondary": f"{domain} {phase_name}particle size", "tertiary": "current collector", }, ) # Surface potential distribution variables U_surf_distribution = pybamm.surf(U) U_surf_xav_distribution = pybamm.x_average(U_surf_distribution) U_rav_distribution = pybamm.r_average(U_distribution) U_av_distribution = pybamm.x_average(U_rav_distribution) variables = { f"{Domain} {phase_name}particle potential distribution [V]": U_distribution, f"X-averaged {domain} {phase_name}particle potential " "distribution [V]": U_xav_distribution, f"R-averaged {domain} {phase_name}particle potential " "distribution [V]": U_rav_distribution, f"Average {domain} {phase_name}particle potential " "distribution [V]": U_av_distribution, f"{Domain} {phase_name}particle surface potential" " distribution [V]": U_surf_distribution, f"X-averaged {domain} {phase_name}particle surface potential " "distribution [V]": U_surf_xav_distribution, } return variables
[docs] class MSMRStoichiometryVariables(BaseParticle): def __init__(self, param, domain, options, phase="primary", x_average=False): super().__init__(param, domain, options, phase) self.x_average = x_average
[docs] def get_coupled_variables(self, variables): domain, Domain = self.domain_Domain phase_name = self.phase_name U = variables[f"{Domain} {phase_name}particle potential [V]"] T = variables[f"{Domain} electrode temperature [K]"] # Standard fractional occupancy variables (these are indexed by reaction number) variables.update(self._get_standard_fractional_occupancy_variables(U, T)) variables.update( self._get_standard_differential_fractional_occupancy_variables(U, T) ) # Calculate the (total) stoichiometry from the potential x = self.phase_param.x(U, T) dxdU = self.phase_param.dxdU(U, T) # Standard (total) stoichiometry and concentration variables (size-independent) c_max = self.phase_param.c_max c_s = x * c_max variables.update(self._get_standard_concentration_variables(c_s)) variables.update(self._get_standard_differential_stoichiometry_variables(dxdU)) if self.size_distribution is True: U_distribution = variables[ f"{Domain} {phase_name}particle potential distribution [V]" ] T = variables[f"{Domain} electrode temperature [K]"] T_distribution = pybamm.PrimaryBroadcast( pybamm.PrimaryBroadcast(T, f"{domain} particle size"), f"{domain} particle", ) # Calculate the stoichiometry distribution from the potential distribution x_distribution = self.phase_param.x(U_distribution, T_distribution) dxdU_distribution = self.phase_param.dxdU(U_distribution, T_distribution) # Standard stoichiometry and concentration distribution variables # (size-dependent) c_s_distribution = x_distribution * c_max variables.update( self._get_standard_concentration_distribution_variables( c_s_distribution ) ) variables.update( self._get_standard_differential_stoichiometry_distribution_variables( dxdU_distribution ) ) return variables
def _get_standard_fractional_occupancy_variables(self, U, T): options = self.options domain = self.domain d = domain[0] variables = {} # Loop over all reactions N = int(getattr(options, domain)["number of MSMR reactions"]) for i in range(N): x = self.phase_param.x_j(U, T, i) x_surf = pybamm.surf(x) x_surf_av = pybamm.x_average(x_surf) x_xav = pybamm.x_average(x) x_rav = pybamm.r_average(x) x_av = pybamm.r_average(x_xav) variables.update( { f"x_{d}_{i}": x, f"X-averaged x_{d}_{i}": x_xav, f"R-averaged x_{d}_{i}": x_rav, f"Average x_{d}_{i}": x_av, f"Surface x_{d}_{i}": x_surf, f"X-averaged surface x_{d}_{i}": x_surf_av, } ) return variables def _get_standard_differential_fractional_occupancy_variables(self, U, T): options = self.options domain = self.domain d = domain[0] variables = {} # Loop over all reactions N = int(getattr(options, domain)["number of MSMR reactions"]) for i in range(N): dxdU = self.phase_param.dxdU_j(U, T, i) dxdU_surf = pybamm.surf(dxdU) dxdU_surf_av = pybamm.x_average(dxdU_surf) dxdU_xav = pybamm.x_average(dxdU) dxdU_rav = pybamm.r_average(dxdU) dxdU_av = pybamm.r_average(dxdU_xav) variables.update( { f"dxdU_{d}_{i}": dxdU, f"X-averaged dxdU_{d}_{i}": dxdU_xav, f"R-averaged dxdU_{d}_{i}": dxdU_rav, f"Average dxdU_{d}_{i}": dxdU_av, f"Surface dxdU_{d}_{i}": dxdU_surf, f"X-averaged surface dxdU_{d}_{i}": dxdU_surf_av, } ) return variables def _get_standard_differential_stoichiometry_variables(self, dxdU): domain, Domain = self.domain_Domain phase_name = self.phase_name dxdU_surf = pybamm.surf(dxdU) dxdU_surf_av = pybamm.x_average(dxdU_surf) dxdU_xav = pybamm.x_average(dxdU) dxdU_rav = pybamm.r_average(dxdU) dxdU_av = pybamm.r_average(dxdU_xav) variables = { f"{Domain} {phase_name}particle differential stoichiometry [V-1]": dxdU, f"X-averaged {domain} {phase_name}particle " "differential stoichiometry [V-1]": dxdU_xav, f"R-averaged {domain} {phase_name}particle " "differential stoichiometry [V-1]": dxdU_rav, f"Average {domain} {phase_name}particle differential " "stoichiometry [V-1]": dxdU_av, f"{Domain} {phase_name}particle surface differential " "stoichiometry [V-1]": dxdU_surf, f"X-averaged {domain} {phase_name}particle " "surface differential stoichiometry [V-1]": dxdU_surf_av, } return variables def _get_standard_differential_stoichiometry_distribution_variables(self, dxdU): domain, Domain = self.domain_Domain phase_name = self.phase_name # Broadcast and x-average when necessary if dxdU.domain == [f"{domain} {phase_name}particle size"] and dxdU.domains[ "secondary" ] != [f"{domain} electrode"]: # X-avg differential stoichiometry distribution dxdU_xav_distribution = pybamm.PrimaryBroadcast( dxdU, [f"{domain} {phase_name}particle"] ) # Surface differential stoichiometry distribution variables dxdU_surf_xav_distribution = dxdU dxdU_surf_distribution = pybamm.SecondaryBroadcast( dxdU_surf_xav_distribution, [f"{domain} electrode"] ) # Differential stoichiometry distribution in all domains. dxdU_distribution = pybamm.PrimaryBroadcast( dxdU_surf_distribution, [f"{domain} {phase_name}particle"] ) elif dxdU.domain == [f"{domain} {phase_name}particle"] and ( dxdU.domains["tertiary"] != [f"{domain} electrode"] ): # X-avg differential stoichiometry distribution dxdU_xav_distribution = dxdU # Surface differential stoichiometry distribution variables dxdU_surf_xav_distribution = pybamm.surf(dxdU_xav_distribution) dxdU_surf_distribution = pybamm.SecondaryBroadcast( dxdU_surf_xav_distribution, [f"{domain} electrode"] ) # Differential stoichiometry distribution in all domains. dxdU_distribution = pybamm.TertiaryBroadcast( dxdU_xav_distribution, [f"{domain} electrode"] ) elif dxdU.domain == [f"{domain} {phase_name}particle size"] and dxdU.domains[ "secondary" ] == [f"{domain} electrode"]: # Surface differential stoichiometry distribution variables dxdU_surf_distribution = dxdU dxdU_surf_xav_distribution = pybamm.x_average(dxdU) # X-avg differential stoichiometry distribution dxdU_xav_distribution = pybamm.PrimaryBroadcast( dxdU_surf_xav_distribution, [f"{domain} {phase_name}particle"] ) # Differential stoichiometry distribution in all domains dxdU_distribution = pybamm.PrimaryBroadcast( dxdU_surf_distribution, [f"{domain} {phase_name}particle"] ) else: dxdU_distribution = dxdU # x-average the *tertiary* domain. # NOTE: not yet implemented. Make 0.5 everywhere dxdU_xav_distribution = pybamm.FullBroadcast( 0.5, [f"{domain} {phase_name}particle"], { "secondary": f"{domain} {phase_name}particle size", "tertiary": "current collector", }, ) # Surface differential stoichiometry distribution variables dxdU_surf_distribution = pybamm.surf(dxdU) dxdU_surf_xav_distribution = pybamm.x_average(dxdU_surf_distribution) dxdU_rav_distribution = pybamm.r_average(dxdU_distribution) dxdU_av_distribution = pybamm.x_average(dxdU_rav_distribution) variables = { f"{Domain} {phase_name}particle differential stoichiometry distribution " "[V-1]": dxdU_distribution, f"X-averaged {domain} {phase_name}particle differential stoichiometry " "distribution [V-1]": dxdU_xav_distribution, f"R-averaged {domain} {phase_name}particle differential stoichiometry " "distribution [V-1]": dxdU_rav_distribution, f"Average {domain} {phase_name}particle differential stoichiometry " "distribution [V-1]": dxdU_av_distribution, f"{Domain} {phase_name}particle surface differential stoichiometry" " distribution [V-1]": dxdU_surf_distribution, f"X-averaged {domain} {phase_name}particle surface differential " "stoichiometry distribution [V-1]": dxdU_surf_xav_distribution, } return variables