def nasty_hack_to_get_phi_s(self, variables): "This restates what is already in the electrode submodel which we should not do" param = self.param x_n = pybamm.standard_spatial_vars.x_n x_p = pybamm.standard_spatial_vars.x_p tor = variables[self.domain + " electrode tortuosity"] i_boundary_cc = variables["Current collector current density"] i_e = variables[self.domain + " electrolyte current density"] i_s = i_boundary_cc - i_e if self.domain == "Negative": conductivity = param.sigma_n * tor phi_s = -pybamm.IndefiniteIntegral(i_s / conductivity, x_n) elif self.domain == "Positive": phi_e_s = variables["Separator electrolyte potential"] delta_phi_p = variables[ "Positive electrode surface potential difference"] conductivity = param.sigma_p * tor phi_s = -pybamm.IndefiniteIntegral(i_s / conductivity, x_p) + ( pybamm.boundary_value(phi_e_s, "right") + pybamm.boundary_value(delta_phi_p, "left")) return phi_s
def set_boundary_conditions(self, variables): T = variables["Cell temperature"] T_n_left = pybamm.boundary_value(T, "left") T_p_right = pybamm.boundary_value(T, "right") self.boundary_conditions = { T: { "left": (self.param.h * T_n_left / self.param.lambda_n, "Neumann"), "right": (-self.param.h * T_p_right / self.param.lambda_p, "Neumann"), } }
def set_boundary_conditions(self, variables): T_amb = variables["Ambient temperature"] T_av = variables["X-averaged cell temperature"] T_av_top = pybamm.boundary_value(T_av, "right") T_av_bottom = pybamm.boundary_value(T_av, "left") # Tab cooling only implemented for both tabs at the top. negative_tab_area = self.param.l_tab_n * self.param.l_cn positive_tab_area = self.param.l_tab_p * self.param.l_cp total_top_area = self.param.l * self.param.l_y non_tab_top_area = total_top_area - negative_tab_area - positive_tab_area negative_tab_cooling_coefficient = ( self.param.h_tab_n / self.param.delta * negative_tab_area / total_top_area ) positive_tab_cooling_coefficient = ( self.param.h_tab_p / self.param.delta * positive_tab_area / total_top_area ) top_edge_cooling_coefficient = ( self.param.h_edge / self.param.delta * non_tab_top_area / total_top_area ) bottom_edge_cooling_coefficient = ( self.param.h_edge / self.param.delta * total_top_area / total_top_area ) total_top_cooling_coefficient = ( negative_tab_cooling_coefficient + positive_tab_cooling_coefficient + top_edge_cooling_coefficient ) total_bottom_cooling_coefficient = bottom_edge_cooling_coefficient # just use left and right for clarity # left = bottom of cell (z=0) # right = top of cell (z=L_z) self.boundary_conditions = { T_av: { "left": ( total_bottom_cooling_coefficient * (T_av_bottom - T_amb), "Neumann", ), "right": ( -total_top_cooling_coefficient * (T_av_top - T_amb), "Neumann", ), } }
def get_coupled_variables(self, variables): param = self.param if self.domain in ["Negative", "Positive"]: conductivity, sigma_eff = self._get_conductivities(variables) i_boundary_cc = variables["Current collector current density"] c_e = variables[self.domain + " electrolyte concentration"] delta_phi = variables[ self.domain + " electrode surface potential difference" ] T = variables[self.domain + " electrode temperature"] i_e = conductivity * ( ((1 + param.Theta * T) * param.chi(c_e, T) / c_e) * pybamm.grad(c_e) + pybamm.grad(delta_phi) + i_boundary_cc / sigma_eff ) variables.update(self._get_domain_current_variables(i_e)) phi_s = variables[self.domain + " electrode potential"] phi_e = phi_s - delta_phi variables.update(self._get_domain_potential_variables(phi_e)) elif self.domain == "Separator": x_s = pybamm.standard_spatial_vars.x_s i_boundary_cc = variables["Current collector current density"] c_e_s = variables["Separator electrolyte concentration"] phi_e_n = variables["Negative electrolyte potential"] tor_s = variables["Separator porosity"] T = variables["Separator temperature"] chi_e_s = param.chi(c_e_s, T) kappa_s_eff = param.kappa_e(c_e_s, T) * tor_s phi_e_s = pybamm.boundary_value( phi_e_n, "right" ) + pybamm.IndefiniteIntegral( (1 + param.Theta * T) * chi_e_s / c_e_s * pybamm.grad(c_e_s) - param.C_e * i_boundary_cc / kappa_s_eff, x_s, ) i_e_s = pybamm.PrimaryBroadcast(i_boundary_cc, "separator") variables.update(self._get_domain_potential_variables(phi_e_s)) variables.update(self._get_domain_current_variables(i_e_s)) # Update boundary conditions (for indefinite integral) self.boundary_conditions[c_e_s] = { "left": (pybamm.BoundaryGradient(c_e_s, "left"), "Neumann"), "right": (pybamm.BoundaryGradient(c_e_s, "right"), "Neumann"), } if self.domain == "Positive": variables.update(self._get_whole_cell_variables(variables)) return variables
def _get_standard_potential_variables(self, phi_s): """ A private function to obtain the standard variables which can be derived from the potential in the electrode. Parameters ---------- phi_s : :class:`pybamm.Symbol` The potential in the electrode. Returns ------- variables : dict The variables which can be derived from the potential in the electrode. """ param = self.param phi_s_av = pybamm.x_average(phi_s) if self.domain == "Negative": phi_s_dim = param.potential_scale * phi_s phi_s_av_dim = param.potential_scale * phi_s_av delta_phi_s = phi_s elif self.domain == "Positive": phi_s_dim = param.U_p_ref - param.U_n_ref + param.potential_scale * phi_s phi_s_av_dim = (param.U_p_ref - param.U_n_ref + param.potential_scale * phi_s_av) v = pybamm.boundary_value(phi_s, "right") delta_phi_s = phi_s - pybamm.PrimaryBroadcast( v, ["positive electrode"]) delta_phi_s_av = pybamm.x_average(delta_phi_s) delta_phi_s_dim = delta_phi_s * param.potential_scale delta_phi_s_av_dim = delta_phi_s_av * param.potential_scale variables = { self.domain + " electrode potential": phi_s, self.domain + " electrode potential [V]": phi_s_dim, "X-averaged " + self.domain.lower() + " electrode potential": phi_s_av, "X-averaged " + self.domain.lower() + " electrode potential [V]": phi_s_av_dim, self.domain + " electrode ohmic losses": delta_phi_s, self.domain + " electrode ohmic losses [V]": delta_phi_s_dim, "X-averaged " + self.domain.lower() + " electrode ohmic losses": delta_phi_s_av, "X-averaged " + self.domain.lower() + " electrode ohmic losses [V]": delta_phi_s_av_dim, "Gradient of " + self.domain.lower() + " electrode potential": pybamm.grad(phi_s), } return variables
def _get_standard_current_collector_potential_variables( self, phi_s_cn, phi_s_cp): """ A private function to obtain the standard variables which can be derived from the potentials in the current collector. Parameters ---------- phi_cc : :class:`pybamm.Symbol` The potential in the current collector. Returns ------- variables : dict The variables which can be derived from the potential in the current collector. """ pot_scale = self.param.potential_scale U_ref = self.param.U_p_ref - self.param.U_n_ref phi_s_cp_dim = U_ref + phi_s_cp * pot_scale # Local potential difference V_cc = phi_s_cp - phi_s_cn # Terminal voltage # Note phi_s_cn is always zero at the negative tab V = pybamm.boundary_value(phi_s_cp, "positive tab") V_dim = pybamm.boundary_value(phi_s_cp_dim, "positive tab") # Voltage is local current collector potential difference at the tabs, in 1D # this will be equal to the local current collector potential difference variables = { "Negative current collector potential": phi_s_cn, "Negative current collector potential [V]": phi_s_cn * pot_scale, "Positive current collector potential": phi_s_cp, "Positive current collector potential [V]": phi_s_cp_dim, "Local voltage": V_cc, "Local voltage [V]": U_ref + V_cc * pot_scale, "Terminal voltage": V, "Terminal voltage [V]": V_dim, } return variables
def get_coupled_variables(self, variables): param = self.param x_n = pybamm.standard_spatial_vars.x_n x_p = pybamm.standard_spatial_vars.x_p i_boundary_cc = variables["Current collector current density"] i_e = variables[self.domain + " electrolyte current density"] eps = variables[self.domain + " electrode porosity"] phi_s_cn = variables["Negative current collector potential"] i_s = pybamm.PrimaryBroadcast(i_boundary_cc, self.domain_for_broadcast) - i_e if self.domain == "Negative": conductivity = param.sigma_n * (1 - eps) ** param.b_n phi_s = pybamm.PrimaryBroadcast( phi_s_cn, "negative electrode" ) - pybamm.IndefiniteIntegral(i_s / conductivity, x_n) elif self.domain == "Positive": phi_e_s = variables["Separator electrolyte potential"] delta_phi_p = variables["Positive electrode surface potential difference"] conductivity = param.sigma_p * (1 - eps) ** param.b_p phi_s = -pybamm.IndefiniteIntegral( i_s / conductivity, x_p ) + pybamm.PrimaryBroadcast( pybamm.boundary_value(phi_e_s, "right") + pybamm.boundary_value(delta_phi_p, "left"), "positive electrode", ) variables.update(self._get_standard_potential_variables(phi_s)) variables.update(self._get_standard_current_variables(i_s)) if ( "Negative electrode current density" in variables and "Positive electrode current density" in variables ): variables.update(self._get_standard_whole_cell_variables(variables)) return variables
def set_boundary_conditions(self, variables): T = variables["Cell temperature"] T_n_left = pybamm.boundary_value(T, "left") T_p_right = pybamm.boundary_value(T, "right") T_amb = variables["Ambient temperature"] # N.B only y-z surface cooling is implemented for this thermal model. # Tab and edge cooling is not accounted for. self.boundary_conditions = { T: { "left": ( self.param.h_cn * (T_n_left - T_amb) / self.param.lambda_n, "Neumann", ), "right": ( -self.param.h_cp * (T_p_right - T_amb) / self.param.lambda_p, "Neumann", ), } }
def _get_diffusion_limited_current_density(self, variables): param = self.param if self.domain == "Negative": tor_s = variables["Separator tortuosity"] c_ox_s = variables["Separator oxygen concentration"] N_ox_neg_sep_interface = (-pybamm.boundary_value(tor_s, "left") * param.curlyD_ox * pybamm.BoundaryGradient(c_ox_s, "left")) N_ox_neg_sep_interface.domain = ["current collector"] j = -N_ox_neg_sep_interface / param.C_e / param.s_ox_Ox / param.l_n return j
def test_boundary_value(self): a = pybamm.Scalar(1) boundary_a = pybamm.boundary_value(a, "right") self.assertEqual(boundary_a.id, a.id) boundary_broad_a = pybamm.boundary_value( pybamm.Broadcast(a, ["negative electrode"]), "left") self.assertEqual(boundary_broad_a.evaluate(), np.array([1])) a = pybamm.Symbol("a", domain=["separator"]) boundary_a = pybamm.boundary_value(a, "right") self.assertIsInstance(boundary_a, pybamm.BoundaryValue) self.assertEqual(boundary_a.side, "right") self.assertEqual(boundary_a.domain, []) self.assertEqual(boundary_a.auxiliary_domains, {}) # test with secondary domain a_sec = pybamm.Symbol( "a", domain=["separator"], auxiliary_domains={"secondary": "current collector"}, ) boundary_a_sec = pybamm.boundary_value(a_sec, "right") self.assertEqual(boundary_a_sec.domain, ["current collector"]) self.assertEqual(boundary_a_sec.auxiliary_domains, {}) # test with secondary domain and tertiary domain a_tert = pybamm.Symbol( "a", domain=["separator"], auxiliary_domains={ "secondary": "current collector", "tertiary": "bla" }, ) boundary_a_tert = pybamm.boundary_value(a_tert, "right") self.assertEqual(boundary_a_tert.domain, ["current collector"]) self.assertEqual(boundary_a_tert.auxiliary_domains, {"secondary": ["bla"]}) # error if boundary value on tabs and domain is not "current collector" var = pybamm.Variable("var", domain=["negative electrode"]) with self.assertRaisesRegex(pybamm.ModelError, "Can only take boundary"): pybamm.boundary_value(var, "negative tab") pybamm.boundary_value(var, "positive tab")
def set_boundary_conditions(self, variables): T_av = variables["X-averaged cell temperature"] T_av_left = pybamm.boundary_value(T_av, "negative tab") T_av_right = pybamm.boundary_value(T_av, "positive tab") # Three boundary conditions here to handle the cases of both tabs at # the same side (top or bottom), or one either side. For both tabs on the # same side, T_av_left and T_av_right are equal, and the boundary condition # "no tab" is used on the other side. self.boundary_conditions = { T_av: { "negative tab": ( self.param.h * T_av_left / self.param.delta, "Neumann", ), "positive tab": ( -self.param.h * T_av_right / self.param.delta, "Neumann", ), "no tab": (pybamm.Scalar(0), "Neumann"), } }
def set_boundary_conditions(self, variables): if self.domain == "Negative": phi_s_cn = variables["Negative current collector potential"] lbc = (phi_s_cn, "Dirichlet") rbc = (pybamm.Scalar(0), "Neumann") elif self.domain == "Positive": lbc = (pybamm.Scalar(0), "Neumann") i_boundary_cc = variables["Current collector current density"] sigma_eff = self.param.sigma_p * variables[ "Positive electrode tortuosity"] rbc = ( i_boundary_cc / pybamm.boundary_value(-sigma_eff, "right"), "Neumann", ) phi_s = variables[self.domain + " electrode potential"] self.boundary_conditions[phi_s] = {"left": lbc, "right": rbc}
def _get_diffusion_limited_current_density(self, variables): param = self.param if self.domain == "Negative": if self.order == "leading": j_p = variables["X-averaged positive electrode" + self.reaction_name + " interfacial current density"] j = -self.param.l_p * j_p / self.param.l_n elif self.order in ["composite", "full"]: tor_s = variables["Separator tortuosity"] c_ox_s = variables["Separator oxygen concentration"] N_ox_neg_sep_interface = ( -pybamm.boundary_value(tor_s, "left") * param.curlyD_ox * pybamm.BoundaryGradient(c_ox_s, "left")) N_ox_neg_sep_interface.domain = ["current collector"] j = -N_ox_neg_sep_interface / param.C_e / -param.s_ox_Ox / param.l_n return j
def _get_standard_whole_cell_variables(self, variables): """ A private function to obtain the whole-cell versions of the current variables. Parameters ---------- variables : dict The variables in the whole model. Returns ------- variables : dict The variables in the whole model with the whole-cell current variables added. """ pot_scale = self.param.potential_scale U_ref = self.param.U_p_ref - self.param.U_n_ref i_s_n = variables["Negative electrode current density"] i_s_s = pybamm.FullBroadcast(0, ["separator"], "current collector") i_s_p = variables["Positive electrode current density"] phi_s_p = variables["Positive electrode potential"] phi_s_cn = variables["Negative current collector potential"] phi_s_cp = pybamm.boundary_value(phi_s_p, "right") v_boundary_cc = phi_s_cp - phi_s_cn i_s = pybamm.Concatenation(i_s_n, i_s_s, i_s_p) variables = { "Electrode current density": i_s, "Positive current collector potential": phi_s_cp, "Local current collector potential difference": v_boundary_cc, "Local current collector potential difference [V]": U_ref + v_boundary_cc * pot_scale, } return variables
def _get_sep_coupled_variables(self, variables): """ A private function to get the coupled variables when the domain is 'Separator'. """ param = self.param x_s = pybamm.standard_spatial_vars.x_s i_boundary_cc = variables["Current collector current density"] c_e_s = variables["Separator electrolyte concentration"] phi_e_n = variables["Negative electrolyte potential"] eps_s = variables["Separator porosity"] T = variables["Separator temperature"] chi_e_s = param.chi(c_e_s) kappa_s_eff = param.kappa_e(c_e_s, T) * (eps_s ** param.b_s) phi_e_s = pybamm.PrimaryBroadcast( pybamm.boundary_value(phi_e_n, "right"), "separator" ) + pybamm.IndefiniteIntegral( chi_e_s / c_e_s * pybamm.grad(c_e_s) - param.C_e * pybamm.PrimaryBroadcast(i_boundary_cc, self.domain_for_broadcast) / kappa_s_eff, x_s, ) i_e_s = pybamm.PrimaryBroadcast(i_boundary_cc, "separator") variables.update(self._get_domain_potential_variables(phi_e_s)) variables.update(self._get_domain_current_variables(i_e_s)) # Update boundary conditions (for indefinite integral) self.boundary_conditions[c_e_s] = { "left": (pybamm.BoundaryGradient(c_e_s, "left"), "Neumann"), "right": (pybamm.BoundaryGradient(c_e_s, "right"), "Neumann"), } return variables
def _get_standard_whole_cell_variables(self, variables): """ A private function to obtain the whole-cell versions of the current variables. Parameters ---------- variables : dict The variables in the whole model. Returns ------- variables : dict The variables in the whole model with the whole-cell current variables added. """ i_s_n = variables["Negative electrode current density"] i_s_s = pybamm.FullBroadcast(0, ["separator"], "current collector") i_s_p = variables["Positive electrode current density"] i_s = pybamm.concatenation(i_s_n, i_s_s, i_s_p) variables.update({"Electrode current density": i_s}) if self.set_positive_potential: # Get phi_s_cn from the current collector submodel and phi_s_p from the # electrode submodel phi_s_cn = variables["Negative current collector potential"] phi_s_p = variables["Positive electrode potential"] phi_s_cp = pybamm.boundary_value(phi_s_p, "right") variables.update( self._get_standard_current_collector_potential_variables( phi_s_cn, phi_s_cp)) return variables
def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): super().__init__({}, 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. param = self.param options = options or {"working electrode": None} if options["working electrode"] not in ["negative", "positive"]: raise ValueError( "The option 'working electrode' should be either 'positive'" " or 'negative'" ) self.options.update(options) working_electrode = options["working electrode"] if working_electrode == "negative": R_w_typ = param.R_n_typ else: R_w_typ = param.R_p_typ # Set default length scales self.length_scales = { "working electrode": param.L_x, "separator": param.L_x, "working particle": R_w_typ, "current collector y": param.L_z, "current collector z": param.L_z, } ###################### # Variables ###################### # Variables that depend on time only are created without a domain Q = pybamm.Variable("Discharge capacity [A.h]") # Define some useful scalings pot = param.potential_scale i_typ = param.current_scale # Variables that vary spatially are created with a domain. c_e_s = pybamm.Variable( "Separator electrolyte concentration", domain="separator" ) c_e_w = pybamm.Variable( "Working electrolyte concentration", domain="working electrode" ) c_e = pybamm.concatenation(c_e_s, c_e_w) c_s_w = pybamm.Variable( "Working particle concentration", domain="working particle", auxiliary_domains={"secondary": "working electrode"}, ) phi_s_w = pybamm.Variable( "Working electrode potential", domain="working electrode" ) phi_e_s = pybamm.Variable("Separator electrolyte potential", domain="separator") phi_e_w = pybamm.Variable( "Working electrolyte potential", domain="working electrode" ) phi_e = pybamm.concatenation(phi_e_s, phi_e_w) # Constant temperature T = param.T_init ###################### # Other set-up ###################### # Current density i_cell = param.current_with_time # Define particle surface concentration # 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_w = pybamm.surf(c_s_w) # Define parameters. We need to assemble them differently depending on the # working electrode if working_electrode == "negative": # Porosity and Tortuosity # Primary broadcasts are used to broadcast scalar quantities across a domain # into a vector of the right shape, for multiplying with other vectors eps_s = pybamm.PrimaryBroadcast( pybamm.Parameter("Separator porosity"), "separator" ) eps_w = pybamm.PrimaryBroadcast( pybamm.Parameter("Negative electrode porosity"), "working electrode" ) b_e_s = param.b_e_s b_e_w = param.b_e_n # Interfacial reactions j0_w = param.j0_n(c_e_w, c_s_surf_w, T) / param.C_r_n U_w = param.U_n ne_w = param.ne_n # Particle diffusion parameters D_w = param.D_n C_w = param.C_n a_R_w = param.a_R_n gamma_w = pybamm.Scalar(1) c_w_init = param.c_n_init # Electrode equation parameters eps_s_w = pybamm.Parameter( "Negative electrode active material volume fraction" ) b_s_w = param.b_s_n sigma_w = param.sigma_n # Other parameters (for outputs) c_w_max = param.c_n_max U_ref = param.U_n_ref phi_s_w_ref = pybamm.Scalar(0) L_w = param.L_n else: # Porosity and Tortuosity eps_s = pybamm.PrimaryBroadcast( pybamm.Parameter("Separator porosity"), "separator" ) eps_w = pybamm.PrimaryBroadcast( pybamm.Parameter("Positive electrode porosity"), "working electrode" ) b_e_s = param.b_e_s b_e_w = param.b_e_p # Interfacial reactions j0_w = param.gamma_p * param.j0_p(c_e_w, c_s_surf_w, T) / param.C_r_p U_w = param.U_p ne_w = param.ne_p # Particle diffusion parameters D_w = param.D_p C_w = param.C_p a_R_w = param.a_R_p gamma_w = param.gamma_p c_w_init = param.c_p_init # Electrode equation parameters eps_s_w = pybamm.Parameter( "Positive electrode active material volume fraction" ) b_s_w = param.b_s_p sigma_w = param.sigma_p # Other parameters (for outputs) c_w_max = param.c_p_max U_ref = param.U_p_ref phi_s_w_ref = param.U_p_ref - param.U_n_ref L_w = param.L_p eps = pybamm.concatenation(eps_s, eps_w) tor = pybamm.concatenation(eps_s ** b_e_s, eps_w ** b_e_w) j_w = ( 2 * j0_w * pybamm.sinh(ne_w / 2 * (phi_s_w - phi_e_w - U_w(c_s_surf_w, T))) ) j_s = pybamm.PrimaryBroadcast(0, "separator") j = pybamm.concatenation(j_s, j_w) ###################### # State of Charge ###################### I = param.dimensional_current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I * param.timescale / 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_w = -D_w(c_s_w, T) * pybamm.grad(c_s_w) self.rhs[c_s_w] = -(1 / C_w) * pybamm.div(N_s_w) # Boundary conditions must be provided for equations with spatial # derivatives self.boundary_conditions[c_s_w] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( -C_w * j_w / a_R_w / gamma_w / D_w(c_s_surf_w, T), "Neumann", ), } # c_w_init can in general be a function of x # Note the broadcasting, for domains x_w = pybamm.PrimaryBroadcast(half_cell_spatial_vars.x_w, "working particle") self.initial_conditions[c_s_w] = c_w_init(x_w) # Events specify points at which a solution should terminate self.events += [ pybamm.Event( "Minimum working particle surface concentration", pybamm.min(c_s_surf_w) - 0.01, ), pybamm.Event( "Maximum working particle surface concentration", (1 - 0.01) - pybamm.max(c_s_surf_w), ), ] ###################### # Current in the solid ###################### sigma_eff_w = sigma_w * eps_s_w ** b_s_w i_s_w = -sigma_eff_w * pybamm.grad(phi_s_w) self.boundary_conditions[phi_s_w] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( i_cell / pybamm.boundary_value(-sigma_eff_w, "right"), "Neumann", ), } self.algebraic[phi_s_w] = pybamm.div(i_s_w) + j_w # Initial conditions must also be provided for algebraic equations, as an # initial guess for a root-finding algorithm which calculates consistent # initial conditions self.initial_conditions[phi_s_w] = U_w(c_w_init(1), param.T_init) ###################### # Electrolyte concentration ###################### N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) self.rhs[c_e] = (1 / eps) * ( -pybamm.div(N_e) / param.C_e + (1 - param.t_plus(c_e, T)) * j / param.gamma_e ) dce_dx = ( -(1 - param.t_plus(c_e, T)) * i_cell * param.C_e / (tor * param.gamma_e * param.D_e(c_e, T)) ) self.boundary_conditions[c_e] = { "left": (pybamm.boundary_value(dce_dx, "left"), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } self.initial_conditions[c_e] = param.c_e_init self.events.append( pybamm.Event( "Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002 ) ) ###################### # Current in the electrolyte ###################### i_e = (param.kappa_e(c_e, T) * tor * param.gamma_e / param.C_e) * ( param.chi(c_e, T) * pybamm.grad(c_e) / c_e - pybamm.grad(phi_e) ) self.algebraic[phi_e] = pybamm.div(i_e) - j ref_potential = param.U_n_ref / pot self.boundary_conditions[phi_e] = { "left": (ref_potential, "Dirichlet"), "right": (pybamm.Scalar(0), "Neumann"), } self.initial_conditions[phi_e] = ref_potential ###################### # (Some) variables ###################### L_Li = pybamm.Parameter("Lithium counter electrode thickness [m]") sigma_Li = pybamm.Parameter("Lithium counter electrode conductivity [S.m-1]") j_Li = pybamm.Parameter( "Lithium counter electrode exchange-current density [A.m-2]" ) vdrop_cell = pybamm.boundary_value(phi_s_w, "right") - ref_potential vdrop_Li = -( 2 * pybamm.arcsinh(i_cell * i_typ / j_Li) + L_Li * i_typ * i_cell / (sigma_Li * pot) ) voltage = vdrop_cell + vdrop_Li c_e_total = pybamm.x_average(eps * c_e) c_s_surf_w_av = pybamm.x_average(c_s_surf_w) c_s_rav = pybamm.r_average(c_s_w) c_s_vol_av = pybamm.x_average(eps_s_w * c_s_rav) # The `variables` dictionary contains all variables that might be useful for # visualising the solution of the model self.variables = { "Time [s]": param.timescale * pybamm.t, "Working particle surface concentration": c_s_surf_w, "X-averaged working particle surface concentration": c_s_surf_w_av, "Working particle concentration": c_s_w, "Working particle surface concentration [mol.m-3]": c_w_max * c_s_surf_w, "X-averaged working particle surface concentration " "[mol.m-3]": c_w_max * c_s_surf_w_av, "Working particle concentration [mol.m-3]": c_w_max * c_s_w, "Total lithium in working electrode": c_s_vol_av, "Total lithium in working electrode [mol]": c_s_vol_av * c_w_max * L_w * param.A_cc, "Electrolyte concentration": c_e, "Electrolyte concentration [mol.m-3]": param.c_e_typ * c_e, "Total electrolyte concentration": c_e_total, "Total electrolyte concentration [mol]": c_e_total * param.c_e_typ * L_w * param.L_s * param.A_cc, "Current [A]": I, "Working electrode potential": phi_s_w, "Working electrode potential [V]": phi_s_w_ref + pot * phi_s_w, "Working electrode open circuit potential": U_w(c_s_surf_w, T), "Working electrode open circuit potential [V]": U_ref + pot * U_w(c_s_surf_w, T), "Electrolyte potential": phi_e, "Electrolyte potential [V]": -param.U_n_ref + pot * phi_e, "Voltage drop in the cell": vdrop_cell, "Voltage drop in the cell [V]": phi_s_w_ref + param.U_n_ref + pot * vdrop_cell, "Terminal voltage": voltage, "Terminal voltage [V]": phi_s_w_ref + param.U_n_ref + pot * voltage, }
def test_symbol_simplify(self): a = pybamm.Scalar(0) b = pybamm.Scalar(1) c = pybamm.Parameter("c") d = pybamm.Scalar(-1) e = pybamm.Scalar(2) g = pybamm.Variable("g") # negate self.assertIsInstance((-a).simplify(), pybamm.Scalar) self.assertEqual((-a).simplify().evaluate(), 0) self.assertIsInstance((-b).simplify(), pybamm.Scalar) self.assertEqual((-b).simplify().evaluate(), -1) # absolute value self.assertIsInstance((abs(a)).simplify(), pybamm.Scalar) self.assertEqual((abs(a)).simplify().evaluate(), 0) self.assertIsInstance((abs(d)).simplify(), pybamm.Scalar) self.assertEqual((abs(d)).simplify().evaluate(), 1) # function def sin(x): return math.sin(x) f = pybamm.Function(sin, b) self.assertIsInstance((f).simplify(), pybamm.Scalar) self.assertEqual((f).simplify().evaluate(), math.sin(1)) def myfunction(x, y): return x * y f = pybamm.Function(myfunction, a, b) self.assertIsInstance((f).simplify(), pybamm.Scalar) self.assertEqual((f).simplify().evaluate(), 0) # FunctionParameter f = pybamm.FunctionParameter("function", b) self.assertIsInstance((f).simplify(), pybamm.FunctionParameter) self.assertEqual((f).simplify().children[0].id, b.id) f = pybamm.FunctionParameter("function", a, b) self.assertIsInstance((f).simplify(), pybamm.FunctionParameter) self.assertEqual((f).simplify().children[0].id, a.id) self.assertEqual((f).simplify().children[1].id, b.id) # Gradient self.assertIsInstance((pybamm.grad(a)).simplify(), pybamm.Scalar) self.assertEqual((pybamm.grad(a)).simplify().evaluate(), 0) v = pybamm.Variable("v") self.assertIsInstance((pybamm.grad(v)).simplify(), pybamm.Gradient) # Divergence self.assertIsInstance((pybamm.div(a)).simplify(), pybamm.Scalar) self.assertEqual((pybamm.div(a)).simplify().evaluate(), 0) self.assertIsInstance((pybamm.div(v)).simplify(), pybamm.Divergence) # Integral self.assertIsInstance( (pybamm.Integral(a, pybamm.t)).simplify(), pybamm.Integral ) # BoundaryValue v_neg = pybamm.Variable("v", domain=["negative electrode"]) self.assertIsInstance( (pybamm.boundary_value(v_neg, "right")).simplify(), pybamm.BoundaryValue ) # Delta function self.assertIsInstance( (pybamm.DeltaFunction(v_neg, "right", "domain")).simplify(), pybamm.DeltaFunction, ) # addition self.assertIsInstance((a + b).simplify(), pybamm.Scalar) self.assertEqual((a + b).simplify().evaluate(), 1) self.assertIsInstance((b + b).simplify(), pybamm.Scalar) self.assertEqual((b + b).simplify().evaluate(), 2) self.assertIsInstance((b + a).simplify(), pybamm.Scalar) self.assertEqual((b + a).simplify().evaluate(), 1) # subtraction self.assertIsInstance((a - b).simplify(), pybamm.Scalar) self.assertEqual((a - b).simplify().evaluate(), -1) self.assertIsInstance((b - b).simplify(), pybamm.Scalar) self.assertEqual((b - b).simplify().evaluate(), 0) self.assertIsInstance((b - a).simplify(), pybamm.Scalar) self.assertEqual((b - a).simplify().evaluate(), 1) # addition and subtraction with matrix zero v = pybamm.Vector(np.zeros((10, 1))) self.assertIsInstance((b + v).simplify(), pybamm.Array) np.testing.assert_array_equal((b + v).simplify().evaluate(), np.ones((10, 1))) self.assertIsInstance((v + b).simplify(), pybamm.Array) np.testing.assert_array_equal((v + b).simplify().evaluate(), np.ones((10, 1))) self.assertIsInstance((b - v).simplify(), pybamm.Array) np.testing.assert_array_equal((b - v).simplify().evaluate(), np.ones((10, 1))) self.assertIsInstance((v - b).simplify(), pybamm.Array) np.testing.assert_array_equal((v - b).simplify().evaluate(), -np.ones((10, 1))) # multiplication self.assertIsInstance((a * b).simplify(), pybamm.Scalar) self.assertEqual((a * b).simplify().evaluate(), 0) self.assertIsInstance((b * a).simplify(), pybamm.Scalar) self.assertEqual((b * a).simplify().evaluate(), 0) self.assertIsInstance((b * b).simplify(), pybamm.Scalar) self.assertEqual((b * b).simplify().evaluate(), 1) self.assertIsInstance((a * a).simplify(), pybamm.Scalar) self.assertEqual((a * a).simplify().evaluate(), 0) # test when other node is a parameter self.assertIsInstance((a + c).simplify(), pybamm.Parameter) self.assertIsInstance((c + a).simplify(), pybamm.Parameter) self.assertIsInstance((c + b).simplify(), pybamm.Addition) self.assertIsInstance((b + c).simplify(), pybamm.Addition) self.assertIsInstance((a * c).simplify(), pybamm.Scalar) self.assertEqual((a * c).simplify().evaluate(), 0) self.assertIsInstance((c * a).simplify(), pybamm.Scalar) self.assertEqual((c * a).simplify().evaluate(), 0) self.assertIsInstance((b * c).simplify(), pybamm.Parameter) self.assertIsInstance((e * c).simplify(), pybamm.Multiplication) expr = (e * (e * c)).simplify() self.assertIsInstance(expr, pybamm.Multiplication) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertIsInstance(expr.children[1], pybamm.Parameter) expr = (e / (e * c)).simplify() self.assertIsInstance(expr, pybamm.Division) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 1.0) self.assertIsInstance(expr.children[1], pybamm.Parameter) expr = (e * (e / c)).simplify() self.assertIsInstance(expr, pybamm.Division) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 4.0) self.assertIsInstance(expr.children[1], pybamm.Parameter) expr = (e * (c / e)).simplify() self.assertIsInstance(expr, pybamm.Multiplication) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 1.0) self.assertIsInstance(expr.children[1], pybamm.Parameter) expr = ((e * c) * (c / e)).simplify() self.assertIsInstance(expr, pybamm.Multiplication) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 1.0) self.assertIsInstance(expr.children[1], pybamm.Multiplication) self.assertIsInstance(expr.children[1].children[0], pybamm.Parameter) self.assertIsInstance(expr.children[1].children[1], pybamm.Parameter) expr = (e + (e + c)).simplify() self.assertIsInstance(expr, pybamm.Addition) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 4.0) self.assertIsInstance(expr.children[1], pybamm.Parameter) expr = (e + (e - c)).simplify() self.assertIsInstance(expr, pybamm.Addition) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 4.0) self.assertIsInstance(expr.children[1], pybamm.Negate) self.assertIsInstance(expr.children[1].children[0], pybamm.Parameter) expr = (e + (g - c)).simplify() self.assertIsInstance(expr, pybamm.Addition) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 2.0) self.assertIsInstance(expr.children[1], pybamm.Subtraction) self.assertIsInstance(expr.children[1].children[0], pybamm.Variable) self.assertIsInstance(expr.children[1].children[1], pybamm.Parameter) expr = ((2 + c) + (c + 2)).simplify() self.assertIsInstance(expr, pybamm.Addition) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 4.0) self.assertIsInstance(expr.children[1], pybamm.Multiplication) self.assertIsInstance(expr.children[1].children[0], pybamm.Scalar) self.assertEqual(expr.children[1].children[0].evaluate(), 2) self.assertIsInstance(expr.children[1].children[1], pybamm.Parameter) expr = ((-1 + c) - (c + 1) + (c - 1)).simplify() self.assertIsInstance(expr, pybamm.Addition) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), -3.0) # check these don't simplify self.assertIsInstance((c * e).simplify(), pybamm.Multiplication) self.assertIsInstance((e / c).simplify(), pybamm.Division) self.assertIsInstance((c).simplify(), pybamm.Parameter) c1 = pybamm.Parameter("c1") self.assertIsInstance((c1 * c).simplify(), pybamm.Multiplication) # should simplify division to multiply self.assertIsInstance((c / e).simplify(), pybamm.Multiplication) self.assertIsInstance((c / b).simplify(), pybamm.Parameter) self.assertIsInstance((c * b).simplify(), pybamm.Parameter) # negation with parameter self.assertIsInstance((-c).simplify(), pybamm.Negate) self.assertIsInstance((a + b + a).simplify(), pybamm.Scalar) self.assertEqual((a + b + a).simplify().evaluate(), 1) self.assertIsInstance((b + a + a).simplify(), pybamm.Scalar) self.assertEqual((b + a + a).simplify().evaluate(), 1) self.assertIsInstance((a * b * b).simplify(), pybamm.Scalar) self.assertEqual((a * b * b).simplify().evaluate(), 0) self.assertIsInstance((b * a * b).simplify(), pybamm.Scalar) self.assertEqual((b * a * b).simplify().evaluate(), 0) # power simplification self.assertIsInstance((c ** a).simplify(), pybamm.Scalar) self.assertEqual((c ** a).simplify().evaluate(), 1) self.assertIsInstance((a ** c).simplify(), pybamm.Scalar) self.assertEqual((a ** c).simplify().evaluate(), 0) d = pybamm.Scalar(2) self.assertIsInstance((c ** d).simplify(), pybamm.Power) # division self.assertIsInstance((a / b).simplify(), pybamm.Scalar) self.assertEqual((a / b).simplify().evaluate(), 0) self.assertIsInstance((b / a).simplify(), pybamm.Scalar) self.assertEqual((b / a).simplify().evaluate(), np.inf) self.assertIsInstance((a / a).simplify(), pybamm.Scalar) self.assertTrue(np.isnan((a / a).simplify().evaluate())) self.assertIsInstance((b / b).simplify(), pybamm.Scalar) self.assertEqual((b / b).simplify().evaluate(), 1) # not implemented for Symbol sym = pybamm.Symbol("sym") with self.assertRaises(NotImplementedError): sym.simplify() # A + A = 2A (#323) a = pybamm.Parameter("A") expr = (a + a).simplify() self.assertIsInstance(expr, pybamm.Multiplication) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 2) self.assertIsInstance(expr.children[1], pybamm.Parameter) expr = (a + a + a + a).simplify() self.assertIsInstance(expr, pybamm.Multiplication) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 4) self.assertIsInstance(expr.children[1], pybamm.Parameter) expr = (a - a + a - a + a + a).simplify() self.assertIsInstance(expr, pybamm.Multiplication) self.assertIsInstance(expr.children[0], pybamm.Scalar) self.assertEqual(expr.children[0].evaluate(), 2) self.assertIsInstance(expr.children[1], pybamm.Parameter) # A - A = 0 (#323) expr = (a - a).simplify() self.assertIsInstance(expr, pybamm.Scalar) self.assertEqual(expr.evaluate(), 0) # B - (A+A) = B - 2*A (#323) expr = (b - (a + a)).simplify() self.assertIsInstance(expr, pybamm.Addition) self.assertIsInstance(expr.right, pybamm.Negate) self.assertIsInstance(expr.right.child, pybamm.Multiplication) self.assertEqual(expr.right.child.left.id, pybamm.Scalar(2).id) self.assertEqual(expr.right.child.right.id, a.id) # B - (1*A + 2*A) = B - 3*A (#323) expr = (b - (1 * a + 2 * a)).simplify() self.assertIsInstance(expr, pybamm.Addition) self.assertIsInstance(expr.right, pybamm.Negate) self.assertIsInstance(expr.right.child, pybamm.Multiplication) self.assertEqual(expr.right.child.left.id, pybamm.Scalar(3).id) self.assertEqual(expr.right.child.right.id, a.id) # B - (A + C) = B - (A + C) (not B - (A - C)) expr = (b - (a + c)).simplify() self.assertIsInstance(expr, pybamm.Addition) self.assertIsInstance(expr.right, pybamm.Subtraction) self.assertEqual(expr.right.left.id, (-a).id) self.assertEqual(expr.right.right.id, c.id)
def set_voltage_variables(self): ocp_n = self.variables["Negative electrode open circuit potential"] ocp_p = self.variables["Positive electrode open circuit potential"] ocp_n_av = self.variables[ "X-averaged negative electrode open circuit potential"] ocp_p_av = self.variables[ "X-averaged positive electrode open circuit potential"] ocp_n_dim = self.variables[ "Negative electrode open circuit potential [V]"] ocp_p_dim = self.variables[ "Positive electrode open circuit potential [V]"] ocp_n_av_dim = self.variables[ "X-averaged negative electrode open circuit potential [V]"] ocp_p_av_dim = self.variables[ "X-averaged positive electrode open circuit potential [V]"] ocp_n_left = pybamm.boundary_value(ocp_n, "left") ocp_n_left_dim = pybamm.boundary_value(ocp_n_dim, "left") ocp_p_right = pybamm.boundary_value(ocp_p, "right") ocp_p_right_dim = pybamm.boundary_value(ocp_p_dim, "right") ocv_av = ocp_p_av - ocp_n_av ocv_av_dim = ocp_p_av_dim - ocp_n_av_dim ocv = ocp_p_right - ocp_n_left ocv_dim = ocp_p_right_dim - ocp_n_left_dim # overpotentials eta_r_n_av = self.variables[ "X-averaged negative electrode reaction overpotential"] eta_r_n_av_dim = self.variables[ "X-averaged negative electrode reaction overpotential [V]"] eta_r_p_av = self.variables[ "X-averaged positive electrode reaction overpotential"] eta_r_p_av_dim = self.variables[ "X-averaged positive electrode reaction overpotential [V]"] delta_phi_s_n_av = self.variables[ "X-averaged negative electrode ohmic losses"] delta_phi_s_n_av_dim = self.variables[ "X-averaged negative electrode ohmic losses [V]"] delta_phi_s_p_av = self.variables[ "X-averaged positive electrode ohmic losses"] delta_phi_s_p_av_dim = self.variables[ "X-averaged positive electrode ohmic losses [V]"] delta_phi_s_av = delta_phi_s_p_av - delta_phi_s_n_av delta_phi_s_av_dim = delta_phi_s_p_av_dim - delta_phi_s_n_av_dim eta_r_av = eta_r_p_av - eta_r_n_av eta_r_av_dim = eta_r_p_av_dim - eta_r_n_av_dim # SEI film overpotential eta_sei_n_av = self.variables[ "X-averaged negative electrode sei film overpotential"] eta_sei_p_av = self.variables[ "X-averaged positive electrode sei film overpotential"] eta_sei_n_av_dim = self.variables[ "X-averaged negative electrode sei film overpotential [V]"] eta_sei_p_av_dim = self.variables[ "X-averaged positive electrode sei film overpotential [V]"] eta_sei_av = eta_sei_n_av + eta_sei_p_av eta_sei_av_dim = eta_sei_n_av_dim + eta_sei_p_av_dim # TODO: add current collector losses to the voltage in 3D self.variables.update({ "X-averaged open circuit voltage": ocv_av, "Measured open circuit voltage": ocv, "X-averaged open circuit voltage [V]": ocv_av_dim, "Measured open circuit voltage [V]": ocv_dim, "X-averaged reaction overpotential": eta_r_av, "X-averaged reaction overpotential [V]": eta_r_av_dim, "X-averaged sei film overpotential": eta_sei_av, "X-averaged sei film overpotential [V]": eta_sei_av_dim, "X-averaged solid phase ohmic losses": delta_phi_s_av, "X-averaged solid phase ohmic losses [V]": delta_phi_s_av_dim, }) # Battery-wide variables V_dim = self.variables["Terminal voltage [V]"] eta_e_av = self.variables.get("X-averaged electrolyte ohmic losses", 0) eta_c_av = self.variables.get("X-averaged concentration overpotential", 0) eta_e_av_dim = self.variables.get( "X-averaged electrolyte ohmic losses [V]", 0) eta_c_av_dim = self.variables.get( "X-averaged concentration overpotential [V]", 0) num_cells = pybamm.Parameter( "Number of cells connected in series to make a battery") self.variables.update({ "X-averaged battery open circuit voltage [V]": ocv_av_dim * num_cells, "Measured battery open circuit voltage [V]": ocv_dim * num_cells, "X-averaged battery reaction overpotential [V]": eta_r_av_dim * num_cells, "X-averaged battery solid phase ohmic losses [V]": delta_phi_s_av_dim * num_cells, "X-averaged battery electrolyte ohmic losses [V]": eta_e_av_dim * num_cells, "X-averaged battery concentration overpotential [V]": eta_c_av_dim * num_cells, "Battery voltage [V]": V_dim * num_cells, }) # Variables for calculating the equivalent circuit model (ECM) resistance # Need to compare OCV to initial value to capture this as an overpotential ocv_init = self.param.U_p( self.param.c_p_init(1), self.param.T_init) - self.param.U_n( self.param.c_n_init(0), self.param.T_init) ocv_init_dim = (self.param.U_p_ref - self.param.U_n_ref + self.param.potential_scale * ocv_init) eta_ocv = ocv - ocv_init eta_ocv_dim = ocv_dim - ocv_init_dim # Current collector current density for working out euiqvalent resistance # based on Ohm's Law i_cc = self.variables["Current collector current density"] i_cc_dim = self.variables["Current collector current density [A.m-2]"] # Gather all overpotentials v_ecm = -(eta_ocv + eta_r_av + eta_c_av + eta_e_av + delta_phi_s_av) v_ecm_dim = -(eta_ocv_dim + eta_r_av_dim + eta_c_av_dim + eta_e_av_dim + delta_phi_s_av_dim) # Current collector area for turning resistivity into resistance A_cc = self.param.A_cc self.variables.update({ "Change in measured open circuit voltage": eta_ocv, "Change in measured open circuit voltage [V]": eta_ocv_dim, "Local ECM resistance": v_ecm / (i_cc * A_cc), "Local ECM resistance [Ohm]": v_ecm_dim / (i_cc_dim * A_cc), }) # Cut-off voltage voltage = self.variables["Terminal voltage"] self.events.append( pybamm.Event( "Minimum voltage", voltage - self.param.voltage_low_cut, pybamm.EventType.TERMINATION, )) self.events.append( pybamm.Event( "Maximum voltage", voltage - self.param.voltage_high_cut, pybamm.EventType.TERMINATION, )) # Power I_dim = self.variables["Current [A]"] self.variables.update({"Terminal power [W]": I_dim * V_dim})
def set_voltage_variables(self): ocp_n = self.variables["Negative electrode open circuit potential"] ocp_p = self.variables["Positive electrode open circuit potential"] ocp_n_av = self.variables[ "X-averaged negative electrode open circuit potential"] ocp_p_av = self.variables[ "X-averaged positive electrode open circuit potential"] ocp_n_dim = self.variables[ "Negative electrode open circuit potential [V]"] ocp_p_dim = self.variables[ "Positive electrode open circuit potential [V]"] ocp_n_av_dim = self.variables[ "X-averaged negative electrode open circuit potential [V]"] ocp_p_av_dim = self.variables[ "X-averaged positive electrode open circuit potential [V]"] ocp_n_left = pybamm.boundary_value(ocp_n, "left") ocp_n_left_dim = pybamm.boundary_value(ocp_n_dim, "left") ocp_p_right = pybamm.boundary_value(ocp_p, "right") ocp_p_right_dim = pybamm.boundary_value(ocp_p_dim, "right") ocv_av = ocp_p_av - ocp_n_av ocv_av_dim = ocp_p_av_dim - ocp_n_av_dim ocv = ocp_p_right - ocp_n_left ocv_dim = ocp_p_right_dim - ocp_n_left_dim # overpotentials eta_r_n_av = self.variables[ "X-averaged negative electrode reaction overpotential"] eta_r_n_av_dim = self.variables[ "X-averaged negative electrode reaction overpotential [V]"] eta_r_p_av = self.variables[ "X-averaged positive electrode reaction overpotential"] eta_r_p_av_dim = self.variables[ "X-averaged positive electrode reaction overpotential [V]"] delta_phi_s_n_av = self.variables[ "X-averaged negative electrode ohmic losses"] delta_phi_s_n_av_dim = self.variables[ "X-averaged negative electrode ohmic losses [V]"] delta_phi_s_p_av = self.variables[ "X-averaged positive electrode ohmic losses"] delta_phi_s_p_av_dim = self.variables[ "X-averaged positive electrode ohmic losses [V]"] delta_phi_s_av = delta_phi_s_p_av - delta_phi_s_n_av delta_phi_s_av_dim = delta_phi_s_p_av_dim - delta_phi_s_n_av_dim eta_r_av = eta_r_p_av - eta_r_n_av eta_r_av_dim = eta_r_p_av_dim - eta_r_n_av_dim # terminal voltage (Note: phi_s_cn is zero at the negative tab) phi_s_cp = self.variables["Positive current collector potential"] phi_s_cp_dim = self.variables[ "Positive current collector potential [V]"] if self.options["dimensionality"] == 0: V = phi_s_cp V_dim = phi_s_cp_dim elif self.options["dimensionality"] in [1, 2]: V = pybamm.BoundaryValue(phi_s_cp, "positive tab") V_dim = pybamm.BoundaryValue(phi_s_cp_dim, "positive tab") # TODO: add current collector losses to the voltage in 3D self.variables.update({ "X-averaged open circuit voltage": ocv_av, "Measured open circuit voltage": ocv, "X-averaged open circuit voltage [V]": ocv_av_dim, "Measured open circuit voltage [V]": ocv_dim, "X-averaged reaction overpotential": eta_r_av, "X-averaged reaction overpotential [V]": eta_r_av_dim, "X-averaged solid phase ohmic losses": delta_phi_s_av, "X-averaged solid phase ohmic losses [V]": delta_phi_s_av_dim, "Terminal voltage": V, "Terminal voltage [V]": V_dim, }) # Battery-wide variables eta_e_av_dim = self.variables.get( "X-averaged electrolyte ohmic losses [V]", 0) eta_c_av_dim = self.variables.get( "X-averaged concentration overpotential [V]", 0) num_cells = pybamm.Parameter( "Number of cells connected in series to make a battery") self.variables.update({ "X-averaged battery open circuit voltage [V]": ocv_av_dim * num_cells, "Measured battery open circuit voltage [V]": ocv_dim * num_cells, "X-averaged battery reaction overpotential [V]": eta_r_av_dim * num_cells, "X-averaged battery solid phase ohmic losses [V]": delta_phi_s_av_dim * num_cells, "X-averaged battery electrolyte ohmic losses [V]": eta_e_av_dim * num_cells, "X-averaged battery concentration overpotential [V]": eta_c_av_dim * num_cells, "Battery voltage [V]": V_dim * num_cells, }) # Cut-off voltage voltage = self.variables["Terminal voltage"] self.events["Minimum voltage"] = voltage - self.param.voltage_low_cut self.events["Maximum voltage"] = voltage - self.param.voltage_high_cut
def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): super().__init__({}, name) pybamm.citations.register("marquis2019asymptotic") # `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. param = self.param options = options or {"working electrode": None} if options["working electrode"] not in ["negative", "positive"]: raise ValueError( "The option 'working electrode' should be either 'positive'" " or 'negative'" ) self.options.update(options) working_electrode = options["working electrode"] ###################### # Variables ###################### # Variables that depend on time only are created without a domain Q = pybamm.Variable("Discharge capacity [A.h]") # Define some useful scalings pot = param.potential_scale i_typ = param.current_scale # Variables that vary spatially are created with a domain. Depending on # which is the working electrode we need to define a set variables or another if working_electrode == "negative": # Electrolyte concentration c_e_n = pybamm.Variable( "Negative electrolyte concentration", domain="negative electrode" ) c_e_s = pybamm.Variable( "Separator electrolyte concentration", domain="separator" ) # 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) # Electrolyte potential phi_e_n = pybamm.Variable( "Negative electrolyte potential", domain="negative electrode" ) phi_e_s = pybamm.Variable( "Separator electrolyte potential", domain="separator" ) phi_e = pybamm.Concatenation(phi_e_n, phi_e_s) # 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 = pybamm.Variable( "Negative particle concentration", domain="negative particle", auxiliary_domains={"secondary": "negative electrode"}, ) # Set concentration in positive particle to be equal to the initial # concentration as it is not the working electrode x_p = pybamm.PrimaryBroadcast( pybamm.standard_spatial_vars.x_p, "positive particle" ) c_s_p = param.c_n_init(x_p) # Electrode potential phi_s_n = pybamm.Variable( "Negative electrode potential", domain="negative electrode" ) # Set potential in positive electrode to be equal to the initial OCV phi_s_p = param.U_p(pybamm.surf(param.c_p_init(x_p)), param.T_init) else: c_e_p = pybamm.Variable( "Positive electrolyte concentration", domain="positive electrode" ) c_e_s = pybamm.Variable( "Separator electrolyte concentration", domain="separator" ) # Concatenations combine several variables into a single variable, to # simplify implementing equations that hold over several domains c_e = pybamm.Concatenation(c_e_s, c_e_p) # Electrolyte potential phi_e_s = pybamm.Variable( "Separator electrolyte potential", domain="separator" ) phi_e_p = pybamm.Variable( "Positive electrolyte potential", domain="positive electrode" ) phi_e = pybamm.Concatenation(phi_e_s, phi_e_p) # 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_p = pybamm.Variable( "Positive particle concentration", domain="positive particle", auxiliary_domains={"secondary": "positive electrode"}, ) # Set concentration in negative particle to be equal to the initial # concentration as it is not the working electrode x_n = pybamm.PrimaryBroadcast( pybamm.standard_spatial_vars.x_n, "negative particle" ) c_s_n = param.c_n_init(x_n) # Electrode potential phi_s_p = pybamm.Variable( "Positive electrode potential", domain="positive electrode" ) # Set potential in negative electrode to be equal to the initial OCV phi_s_n = param.U_n(pybamm.surf(param.c_n_init(x_n)), param.T_init) # Constant temperature T = param.T_init ###################### # Other set-up ###################### # Current density i_cell = param.current_with_time # Porosity and Tortuosity # 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" ) if working_electrode == "negative": eps = pybamm.Concatenation(eps_n, eps_s) tor = pybamm.Concatenation(eps_n ** param.b_e_n, eps_s ** param.b_e_s) else: eps = pybamm.Concatenation(eps_s, eps_p) tor = pybamm.Concatenation(eps_s ** param.b_e_s, eps_p ** param.b_e_p) # 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 c_s_surf_n = pybamm.surf(c_s_n) c_s_surf_p = pybamm.surf(c_s_p) if working_electrode == "negative": j0_n = param.j0_n(c_e_n, c_s_surf_n, T) / param.C_r_n j_n = ( 2 * j0_n * pybamm.sinh( param.ne_n / 2 * (phi_s_n - phi_e_n - param.U_n(c_s_surf_n, T)) ) ) j_s = pybamm.PrimaryBroadcast(0, "separator") j_p = pybamm.PrimaryBroadcast(0, "positive electrode") j = pybamm.Concatenation(j_n, j_s) else: j0_p = param.gamma_p * param.j0_p(c_e_p, c_s_surf_p, T) / param.C_r_p j_p = ( 2 * j0_p * pybamm.sinh( param.ne_p / 2 * (phi_s_p - phi_e_p - param.U_p(c_s_surf_p, T)) ) ) j_s = pybamm.PrimaryBroadcast(0, "separator") j_n = pybamm.PrimaryBroadcast(0, "negative electrode") j = pybamm.Concatenation(j_s, j_p) ###################### # State of Charge ###################### I = param.dimensional_current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I * param.timescale / 3600 # Initial conditions must be provided for the ODEs self.initial_conditions[Q] = pybamm.Scalar(0) ###################### # Particles ###################### if working_electrode == "negative": # The div and grad operators will be converted to the appropriate matrix # multiplication at the discretisation stage N_s_n = -param.D_n(c_s_n, T) * pybamm.grad(c_s_n) self.rhs[c_s_n] = -(1 / param.C_n) * pybamm.div(N_s_n) # Boundary conditions must be provided for equations with spatial # derivatives self.boundary_conditions[c_s_n] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( -param.C_n * j_n / param.a_R_n / param.D_n(c_s_surf_n, T), "Neumann", ), } # c_n_init can in general be a function of x # Note the broadcasting, for domains x_n = pybamm.PrimaryBroadcast( pybamm.standard_spatial_vars.x_n, "negative particle" ) self.initial_conditions[c_s_n] = param.c_n_init(x_n) # Events specify points at which a solution should terminate self.events += [ pybamm.Event( "Minimum negative particle surface concentration", pybamm.min(c_s_surf_n) - 0.01, ), pybamm.Event( "Maximum negative particle surface concentration", (1 - 0.01) - pybamm.max(c_s_surf_n), ), ] else: # The div and grad operators will be converted to the appropriate matrix # multiplication at the discretisation stage N_s_p = -param.D_p(c_s_p, T) * pybamm.grad(c_s_p) self.rhs[c_s_p] = -(1 / param.C_p) * pybamm.div(N_s_p) # Boundary conditions must be provided for equations with spatial # derivatives self.boundary_conditions[c_s_p] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( -param.C_p * j_p / param.a_R_p / param.gamma_p / param.D_p(c_s_surf_p, T), "Neumann", ), } # c_p_init can in general be a function of x # Note the broadcasting, for domains x_p = pybamm.PrimaryBroadcast( pybamm.standard_spatial_vars.x_p, "positive particle" ) self.initial_conditions[c_s_p] = param.c_p_init(x_p) # Events specify points at which a solution should terminate self.events += [ pybamm.Event( "Minimum positive particle surface concentration", pybamm.min(c_s_surf_p) - 0.01, ), pybamm.Event( "Maximum positive particle surface concentration", (1 - 0.01) - pybamm.max(c_s_surf_p), ), ] ###################### # Current in the solid ###################### eps_s_n = pybamm.Parameter("Negative electrode active material volume fraction") eps_s_p = pybamm.Parameter("Positive electrode active material volume fraction") if working_electrode == "negative": sigma_eff_n = param.sigma_n * eps_s_n ** param.b_s_n i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n) self.boundary_conditions[phi_s_n] = { "left": ( i_cell / pybamm.boundary_value(-sigma_eff_n, "left"), "Neumann", ), "right": (pybamm.Scalar(0), "Neumann"), } # The `algebraic` dictionary contains differential equations, with the key # being the main scalar variable of interest in the equation self.algebraic[phi_s_n] = pybamm.div(i_s_n) + j_n # Initial conditions must also be provided for algebraic equations, as an # initial guess for a root-finding algorithm which calculates consistent # initial conditions self.initial_conditions[phi_s_n] = param.U_n( param.c_n_init(0), param.T_init ) else: sigma_eff_p = param.sigma_p * eps_s_p ** param.b_s_p i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p) self.boundary_conditions[phi_s_p] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( i_cell / pybamm.boundary_value(-sigma_eff_p, "right"), "Neumann", ), } self.algebraic[phi_s_p] = pybamm.div(i_s_p) + j_p # Initial conditions must also be provided for algebraic equations, as an # initial guess for a root-finding algorithm which calculates consistent # initial conditions self.initial_conditions[phi_s_p] = param.U_p( param.c_p_init(1), param.T_init ) ###################### # Electrolyte concentration ###################### N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) self.rhs[c_e] = (1 / eps) * ( -pybamm.div(N_e) / param.C_e + (1 - param.t_plus(c_e)) * j / param.gamma_e ) dce_dx = ( -(1 - param.t_plus(c_e)) * i_cell * param.C_e / (tor * param.gamma_e * param.D_e(c_e, T)) ) if working_electrode == "negative": self.boundary_conditions[c_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.boundary_value(dce_dx, "right"), "Neumann"), } else: self.boundary_conditions[c_e] = { "left": (pybamm.boundary_value(dce_dx, "left"), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } self.initial_conditions[c_e] = param.c_e_init self.events.append( pybamm.Event( "Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002 ) ) ###################### # Current in the electrolyte ###################### i_e = (param.kappa_e(c_e, T) * tor * param.gamma_e / param.C_e) * ( param.chi(c_e, T) * pybamm.grad(c_e) / c_e - pybamm.grad(phi_e) ) self.algebraic[phi_e] = pybamm.div(i_e) - j ref_potential = param.U_n_ref / pot if working_electrode == "negative": self.boundary_conditions[phi_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (ref_potential, "Dirichlet"), } else: self.boundary_conditions[phi_e] = { "left": (ref_potential, "Dirichlet"), "right": (pybamm.Scalar(0), "Neumann"), } self.initial_conditions[phi_e] = ref_potential ###################### # (Some) variables ###################### L_Li = pybamm.Parameter("Lithium counter electrode thickness [m]") sigma_Li = pybamm.Parameter("Lithium counter electrode conductivity [S.m-1]") j_Li = pybamm.Parameter( "Lithium counter electrode exchange-current density [A.m-2]" ) if working_electrode == "negative": voltage = pybamm.boundary_value(phi_s_n, "left") - ref_potential voltage_dim = pot * pybamm.boundary_value(phi_s_n, "left") vdrop_Li = 2 * pybamm.arcsinh( i_cell * i_typ / j_Li ) + L_Li * i_typ * i_cell / (sigma_Li * pot) vdrop_Li_dim = ( 2 * pot * pybamm.arcsinh(i_cell * i_typ / j_Li) + L_Li * i_typ * i_cell / sigma_Li ) else: voltage = pybamm.boundary_value(phi_s_p, "right") - ref_potential voltage_dim = param.U_p_ref + pot * voltage vdrop_Li = -( 2 * pybamm.arcsinh(i_cell * i_typ / j_Li) + L_Li * i_typ * i_cell / (sigma_Li * pot) ) vdrop_Li_dim = -( 2 * pot * pybamm.arcsinh(i_cell * i_typ / j_Li) + L_Li * i_typ * i_cell / sigma_Li ) c_s_surf_p_av = pybamm.x_average(c_s_surf_p) c_s_surf_n_av = pybamm.x_average(c_s_surf_n) # The `variables` dictionary contains all variables that might be useful for # visualising the solution of the model self.variables = { "Time [s]": param.timescale * pybamm.t, "Negative particle surface concentration": c_s_surf_n, "X-averaged negative particle surface concentration": c_s_surf_n_av, "Negative particle concentration": c_s_n, "Negative particle surface concentration [mol.m-3]": param.c_n_max * c_s_surf_n, "X-averaged negative particle surface concentration " "[mol.m-3]": param.c_n_max * c_s_surf_n_av, "Negative particle concentration [mol.m-3]": param.c_n_max * c_s_n, "Electrolyte concentration": c_e, "Electrolyte concentration [mol.m-3]": param.c_e_typ * c_e, "Positive particle surface concentration": c_s_surf_p, "X-averaged positive particle surface concentration": c_s_surf_p_av, "Positive particle concentration": c_s_p, "Positive particle surface concentration [mol.m-3]": param.c_p_max * c_s_surf_p, "X-averaged positive particle surface concentration " "[mol.m-3]": param.c_p_max * c_s_surf_p_av, "Positive particle concentration [mol.m-3]": param.c_p_max * c_s_p, "Current [A]": I, "Negative electrode potential": phi_s_n, "Negative electrode potential [V]": pot * phi_s_n, "Negative electrode open circuit potential": param.U_n(c_s_surf_n, T), "Electrolyte potential": phi_e, "Electrolyte potential [V]": -param.U_n_ref + pot * phi_e, "Positive electrode potential": phi_s_p, "Positive electrode potential [V]": (param.U_p_ref - param.U_n_ref) + pot * phi_s_p, "Positive electrode open circuit potential": param.U_p(c_s_surf_p, T), "Voltage drop": voltage, "Voltage drop [V]": voltage_dim, "Terminal voltage": voltage + vdrop_Li, "Terminal voltage [V]": voltage_dim + vdrop_Li_dim, }
def get_coupled_variables(self, variables): c_e_av = variables["X-averaged electrolyte concentration"] i_boundary_cc_0 = variables[ "Leading-order current collector current density"] c_e_n = variables["Negative electrolyte concentration"] c_e_s = variables["Separator electrolyte concentration"] c_e_p = variables["Positive electrolyte concentration"] c_e_n0 = pybamm.boundary_value(c_e_n, "left") delta_phi_n_av = variables[ "X-averaged negative electrode surface potential difference"] phi_s_n_av = variables["X-averaged negative electrode potential"] tor_n = variables["Negative electrolyte tortuosity"] tor_s = variables["Separator tortuosity"] tor_p = variables["Positive electrolyte tortuosity"] T_av = variables["X-averaged cell temperature"] T_av_n = pybamm.PrimaryBroadcast(T_av, "negative electrode") T_av_s = pybamm.PrimaryBroadcast(T_av, "separator") T_av_p = pybamm.PrimaryBroadcast(T_av, "positive electrode") param = self.param l_n = param.l_n l_p = param.l_p x_n = pybamm.standard_spatial_vars.x_n x_s = pybamm.standard_spatial_vars.x_s x_p = pybamm.standard_spatial_vars.x_p x_n_edge = pybamm.standard_spatial_vars.x_n_edge x_p_edge = pybamm.standard_spatial_vars.x_p_edge chi_av = param.chi(c_e_av, T_av) chi_av_n = pybamm.PrimaryBroadcast(chi_av, "negative electrode") chi_av_s = pybamm.PrimaryBroadcast(chi_av, "separator") chi_av_p = pybamm.PrimaryBroadcast(chi_av, "positive electrode") # electrolyte current i_e_n = i_boundary_cc_0 * x_n / l_n i_e_s = pybamm.PrimaryBroadcast(i_boundary_cc_0, "separator") i_e_p = i_boundary_cc_0 * (1 - x_p) / l_p i_e = pybamm.Concatenation(i_e_n, i_e_s, i_e_p) i_e_n_edge = i_boundary_cc_0 * x_n_edge / l_n i_e_s_edge = pybamm.PrimaryBroadcastToEdges(i_boundary_cc_0, "separator") i_e_p_edge = i_boundary_cc_0 * (1 - x_p_edge) / l_p # electrolyte potential indef_integral_n = (pybamm.IndefiniteIntegral( i_e_n_edge / (param.kappa_e(c_e_n, T_av_n) * tor_n), x_n) * param.C_e / param.gamma_e) indef_integral_s = (pybamm.IndefiniteIntegral( i_e_s_edge / (param.kappa_e(c_e_s, T_av_s) * tor_s), x_s) * param.C_e / param.gamma_e) indef_integral_p = (pybamm.IndefiniteIntegral( i_e_p_edge / (param.kappa_e(c_e_p, T_av_p) * tor_p), x_p) * param.C_e / param.gamma_e) integral_n = indef_integral_n integral_s = indef_integral_s + pybamm.boundary_value( integral_n, "right") integral_p = indef_integral_p + pybamm.boundary_value( integral_s, "right") phi_e_const = ( -delta_phi_n_av + phi_s_n_av - (chi_av * (1 + param.Theta * T_av) * pybamm.x_average( self._higher_order_macinnes_function(c_e_n / c_e_n0))) + pybamm.x_average(integral_n)) phi_e_n = (phi_e_const + (chi_av_n * (1 + param.Theta * T_av_n) * self._higher_order_macinnes_function(c_e_n / c_e_n0)) - integral_n) phi_e_s = (phi_e_const + (chi_av_s * (1 + param.Theta * T_av_s) * self._higher_order_macinnes_function(c_e_s / c_e_n0)) - integral_s) phi_e_p = (phi_e_const + (chi_av_p * (1 + param.Theta * T_av_p) * self._higher_order_macinnes_function(c_e_p / c_e_n0)) - integral_p) # concentration overpotential eta_c_av = (chi_av * (1 + param.Theta * T_av) * (pybamm.x_average( self._higher_order_macinnes_function( c_e_p / c_e_av)) - pybamm.x_average( self._higher_order_macinnes_function(c_e_n / c_e_av)))) # average electrolyte ohmic losses delta_phi_e_av = -(pybamm.x_average(integral_p) - pybamm.x_average(integral_n)) variables.update( self._get_standard_potential_variables(phi_e_n, phi_e_s, phi_e_p)) variables.update(self._get_standard_current_variables(i_e)) variables.update( self._get_split_overpotential(eta_c_av, delta_phi_e_av)) return variables
def set_voltage_variables(self): ocp_n = self.variables["Negative electrode open circuit potential"] ocp_p = self.variables["Positive electrode open circuit potential"] ocp_n_av = self.variables[ "X-averaged negative electrode open circuit potential" ] ocp_p_av = self.variables[ "X-averaged positive electrode open circuit potential" ] ocp_n_dim = self.variables["Negative electrode open circuit potential [V]"] ocp_p_dim = self.variables["Positive electrode open circuit potential [V]"] ocp_n_av_dim = self.variables[ "X-averaged negative electrode open circuit potential [V]" ] ocp_p_av_dim = self.variables[ "X-averaged positive electrode open circuit potential [V]" ] ocp_n_left = pybamm.boundary_value(ocp_n, "left") ocp_n_left_dim = pybamm.boundary_value(ocp_n_dim, "left") ocp_p_right = pybamm.boundary_value(ocp_p, "right") ocp_p_right_dim = pybamm.boundary_value(ocp_p_dim, "right") ocv_av = ocp_p_av - ocp_n_av ocv_av_dim = ocp_p_av_dim - ocp_n_av_dim ocv = ocp_p_right - ocp_n_left ocv_dim = ocp_p_right_dim - ocp_n_left_dim # overpotentials eta_r_n_av = self.variables[ "X-averaged negative electrode reaction overpotential" ] eta_r_n_av_dim = self.variables[ "X-averaged negative electrode reaction overpotential [V]" ] eta_r_p_av = self.variables[ "X-averaged positive electrode reaction overpotential" ] eta_r_p_av_dim = self.variables[ "X-averaged positive electrode reaction overpotential [V]" ] delta_phi_s_n_av = self.variables["X-averaged negative electrode ohmic losses"] delta_phi_s_n_av_dim = self.variables[ "X-averaged negative electrode ohmic losses [V]" ] delta_phi_s_p_av = self.variables["X-averaged positive electrode ohmic losses"] delta_phi_s_p_av_dim = self.variables[ "X-averaged positive electrode ohmic losses [V]" ] delta_phi_s_av = delta_phi_s_p_av - delta_phi_s_n_av delta_phi_s_av_dim = delta_phi_s_p_av_dim - delta_phi_s_n_av_dim eta_r_av = eta_r_p_av - eta_r_n_av eta_r_av_dim = eta_r_p_av_dim - eta_r_n_av_dim # SEI film overpotential eta_sei_n_av = self.variables[ "X-averaged negative electrode sei film overpotential" ] eta_sei_p_av = self.variables[ "X-averaged positive electrode sei film overpotential" ] eta_sei_n_av_dim = self.variables[ "X-averaged negative electrode sei film overpotential [V]" ] eta_sei_p_av_dim = self.variables[ "X-averaged positive electrode sei film overpotential [V]" ] eta_sei_av = eta_sei_n_av + eta_sei_p_av eta_sei_av_dim = eta_sei_n_av_dim + eta_sei_p_av_dim # TODO: add current collector losses to the voltage in 3D self.variables.update( { "X-averaged open circuit voltage": ocv_av, "Measured open circuit voltage": ocv, "X-averaged open circuit voltage [V]": ocv_av_dim, "Measured open circuit voltage [V]": ocv_dim, "X-averaged reaction overpotential": eta_r_av, "X-averaged reaction overpotential [V]": eta_r_av_dim, "X-averaged sei film overpotential": eta_sei_av, "X-averaged sei film overpotential [V]": eta_sei_av_dim, "X-averaged solid phase ohmic losses": delta_phi_s_av, "X-averaged solid phase ohmic losses [V]": delta_phi_s_av_dim, } ) # Battery-wide variables V_dim = self.variables["Terminal voltage [V]"] eta_e_av_dim = self.variables.get("X-averaged electrolyte ohmic losses [V]", 0) eta_c_av_dim = self.variables.get( "X-averaged concentration overpotential [V]", 0 ) num_cells = pybamm.Parameter( "Number of cells connected in series to make a battery" ) self.variables.update( { "X-averaged battery open circuit voltage [V]": ocv_av_dim * num_cells, "Measured battery open circuit voltage [V]": ocv_dim * num_cells, "X-averaged battery reaction overpotential [V]": eta_r_av_dim * num_cells, "X-averaged battery solid phase ohmic losses [V]": delta_phi_s_av_dim * num_cells, "X-averaged battery electrolyte ohmic losses [V]": eta_e_av_dim * num_cells, "X-averaged battery concentration overpotential [V]": eta_c_av_dim * num_cells, "Battery voltage [V]": V_dim * num_cells, } ) # Cut-off voltage voltage = self.variables["Terminal voltage"] self.events.append( pybamm.Event( "Minimum voltage", voltage - self.param.voltage_low_cut, pybamm.EventType.TERMINATION, ) ) self.events.append( pybamm.Event( "Maximum voltage", voltage - self.param.voltage_high_cut, pybamm.EventType.TERMINATION, ) ) # Power I_dim = self.variables["Current [A]"] self.variables.update({"Terminal power [W]": I_dim * V_dim})
def __init__(self, name="Doyle-Fuller-Newman model"): super().__init__({}, name) # `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. param = self.param ###################### # 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", domain="negative electrode", ) c_e_s = pybamm.Variable( "Separator electrolyte concentration", domain="separator", ) c_e_p = pybamm.Variable( "Positive electrolyte concentration", domain="positive electrode", ) # 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", domain="negative electrode", ) phi_e_s = pybamm.Variable( "Separator electrolyte potential", domain="separator", ) phi_e_p = pybamm.Variable( "Positive electrolyte potential", domain="positive electrode", ) phi_e = pybamm.Concatenation(phi_e_n, phi_e_s, phi_e_p) # Electrode potential phi_s_n = pybamm.Variable( "Negative electrode potential", domain="negative electrode", ) phi_s_p = pybamm.Variable( "Positive electrode potential", domain="positive electrode", ) # 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 = pybamm.Variable( "Negative particle concentration", domain="negative particle", auxiliary_domains={"secondary": "negative electrode"}, ) c_s_p = pybamm.Variable( "Positive particle concentration", domain="positive particle", auxiliary_domains={"secondary": "positive electrode"}, ) # Constant temperature T = param.T_init ###################### # Other set-up ###################### # Current density i_cell = param.current_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) # Tortuosity tor = pybamm.Concatenation(eps_n**param.b_e_n, eps_s**param.b_e_s, eps_p**param.b_e_p) # 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 c_s_surf_n = pybamm.surf(c_s_n) j0_n = (param.m_n(T) / param.C_r_n * c_e_n**(1 / 2) * c_s_surf_n**(1 / 2) * (1 - c_s_surf_n)**(1 / 2)) j_n = (2 * j0_n * pybamm.sinh(param.ne_n / 2 * (phi_s_n - phi_e_n - param.U_n(c_s_surf_n, T)))) c_s_surf_p = pybamm.surf(c_s_p) j0_p = (param.gamma_p * param.m_p(T) / param.C_r_p * c_e_p**(1 / 2) * c_s_surf_p**(1 / 2) * (1 - c_s_surf_p)**(1 / 2)) j_s = pybamm.PrimaryBroadcast(0, "separator") j_p = (2 * j0_p * pybamm.sinh(param.ne_p / 2 * (phi_s_p - phi_e_p - param.U_p(c_s_surf_p, T)))) j = pybamm.Concatenation(j_n, j_s, j_p) ###################### # State of Charge ###################### I = param.dimensional_current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I * param.timescale / 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 = -param.D_n(c_s_n, T) * pybamm.grad(c_s_n) N_s_p = -param.D_p(c_s_p, T) * pybamm.grad(c_s_p) self.rhs[c_s_n] = -(1 / param.C_n) * pybamm.div(N_s_n) self.rhs[c_s_p] = -(1 / param.C_p) * pybamm.div(N_s_p) # Boundary conditions must be provided for equations with spatial derivatives self.boundary_conditions[c_s_n] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( -param.C_n * j_n / param.a_n / param.D_n(c_s_surf_n, T), "Neumann", ), } self.boundary_conditions[c_s_p] = { "left": (pybamm.Scalar(0), "Neumann"), "right": ( -param.C_p * j_p / param.a_p / param.gamma_p / param.D_p(c_s_surf_p, T), "Neumann", ), } # c_n_init and c_p_init can in general be functions of x # Note the broadcasting, for domains x_n = pybamm.PrimaryBroadcast(pybamm.standard_spatial_vars.x_n, "negative particle") self.initial_conditions[c_s_n] = param.c_n_init(x_n) x_p = pybamm.PrimaryBroadcast(pybamm.standard_spatial_vars.x_p, "positive particle") self.initial_conditions[c_s_p] = param.c_p_init(x_p) # Events specify points at which a solution should terminate self.events += [ pybamm.Event( "Minimum negative particle surface concentration", pybamm.min(c_s_surf_n) - 0.01, ), pybamm.Event( "Maximum negative particle surface concentration", (1 - 0.01) - pybamm.max(c_s_surf_n), ), pybamm.Event( "Minimum positive particle surface concentration", pybamm.min(c_s_surf_p) - 0.01, ), pybamm.Event( "Maximum positive particle surface concentration", (1 - 0.01) - pybamm.max(c_s_surf_p), ), ] ###################### # Current in the solid ###################### i_s_n = -param.sigma_n * (1 - eps_n)**param.b_s_n * pybamm.grad(phi_s_n) sigma_eff_p = param.sigma_p * (1 - eps_p)**param.b_s_p 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 self.algebraic[phi_s_n] = pybamm.div(i_s_n) + j_n self.algebraic[phi_s_p] = pybamm.div(i_s_p) + 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] = param.U_p( param.c_p_init(1), param.T_init) - param.U_n( param.c_n_init(0), param.T_init) ###################### # Current in the electrolyte ###################### i_e = (param.kappa_e(c_e, T) * tor * param.gamma_e / param.C_e) * ( param.chi(c_e) * pybamm.grad(c_e) / c_e - pybamm.grad(phi_e)) self.algebraic[phi_e] = pybamm.div(i_e) - j self.boundary_conditions[phi_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } self.initial_conditions[phi_e] = -param.U_n(param.c_n_init(0), param.T_init) ###################### # Electrolyte concentration ###################### N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e) self.rhs[c_e] = (1 / eps) * (-pybamm.div(N_e) / param.C_e + (1 - param.t_plus(c_e)) * j / param.gamma_e) self.boundary_conditions[c_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } self.initial_conditions[c_e] = param.c_e_init self.events.append( pybamm.Event("Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002)) ###################### # (Some) variables ###################### voltage = pybamm.boundary_value(phi_s_p, "right") # The `variables` dictionary contains all variables that might be useful for # visualising the solution of the model self.variables = { "Negative particle surface concentration": c_s_surf_n, "Electrolyte concentration": c_e, "Positive particle surface concentration": c_s_surf_p, "Current [A]": I, "Negative electrode potential": phi_s_n, "Electrolyte potential": phi_e, "Positive electrode potential": phi_s_p, "Terminal voltage": voltage, } self.events += [ pybamm.Event("Minimum voltage", voltage - param.voltage_low_cut), pybamm.Event("Maximum voltage", voltage - param.voltage_high_cut), ]
def __init__(self, name="Basic full model"): super().__init__({}, name) # `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. param = self.param ###################### # 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", domain="negative electrode", ) c_e_s = pybamm.Variable( "Separator electrolyte concentration", domain="separator", ) c_e_p = pybamm.Variable( "Positive electrolyte concentration", domain="positive electrode", ) # 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", domain="negative electrode", ) phi_e_s = pybamm.Variable( "Separator electrolyte potential", domain="separator", ) phi_e_p = pybamm.Variable( "Positive electrolyte potential", domain="positive electrode", ) phi_e = pybamm.Concatenation(phi_e_n, phi_e_s, phi_e_p) # Electrode potential phi_s_n = pybamm.Variable( "Negative electrode potential", domain="negative electrode", ) phi_s_p = pybamm.Variable( "Positive electrode potential", domain="positive electrode", ) # Porosity eps_n = pybamm.Variable( "Negative electrode porosity", domain="negative electrode", ) eps_s = pybamm.Variable("Separator porosity", domain="separator") eps_p = pybamm.Variable( "Positive electrode porosity", domain="positive electrode", ) eps = pybamm.Concatenation(eps_n, eps_s, eps_p) # Pressure (for convection) pressure_n = pybamm.Variable( "Negative electrolyte pressure", domain="negative electrode", ) pressure_p = pybamm.Variable( "Positive electrolyte pressure", domain="positive electrode", ) # Constant temperature T = param.T_init ###################### # Other set-up ###################### # Current density i_cell = param.current_with_time # Tortuosity tor = pybamm.Concatenation(eps_n**param.b_e_n, eps_s**param.b_e_s, eps_p**param.b_e_p) # Interfacial reactions j0_n = param.j0_n(c_e_n, T) j_n = (2 * j0_n * pybamm.sinh(param.ne_n / 2 * (phi_s_n - phi_e_n - param.U_n(c_e_n, T)))) j0_p = param.j0_p(c_e_p, T) j_s = pybamm.PrimaryBroadcast(0, "separator") j_p = (2 * j0_p * pybamm.sinh(param.ne_p / 2 * (phi_s_p - phi_e_p - param.U_p(c_e_p, T)))) j = pybamm.Concatenation(j_n, j_s, j_p) ###################### # State of Charge ###################### I = param.dimensional_current_with_time # The `rhs` dictionary contains differential equations, with the key being the # variable in the d/dt self.rhs[Q] = I * param.timescale / 3600 # Initial conditions must be provided for the ODEs self.initial_conditions[Q] = pybamm.Scalar(0) ###################### # Convection ###################### v_n = -pybamm.grad(pressure_n) v_p = -pybamm.grad(pressure_p) l_s = pybamm.geometric_parameters.l_s l_n = pybamm.geometric_parameters.l_n x_s = pybamm.SpatialVariable("x_s", domain="separator") # Difference in negative and positive electrode velocities determines the # velocity in the separator v_n_right = param.beta_n * i_cell v_p_left = param.beta_p * i_cell d_v_s__dx = (v_p_left - v_n_right) / l_s # Simple formula for velocity in the separator div_V_s = -d_v_s__dx v_s = d_v_s__dx * (x_s - l_n) + v_n_right # v is the velocity in the x-direction # div_V is the divergence of the velocity in the yz-directions v = pybamm.Concatenation(v_n, v_s, v_p) div_V = pybamm.Concatenation( pybamm.PrimaryBroadcast(0, "negative electrode"), pybamm.PrimaryBroadcast(div_V_s, "separator"), pybamm.PrimaryBroadcast(0, "positive electrode"), ) # Simple formula for velocity in the separator self.algebraic[pressure_n] = pybamm.div(v_n) - param.beta_n * j_n self.algebraic[pressure_p] = pybamm.div(v_p) - param.beta_p * j_p self.boundary_conditions[pressure_n] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Dirichlet"), } self.boundary_conditions[pressure_p] = { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(0), "Neumann"), } self.initial_conditions[pressure_n] = pybamm.Scalar(0) self.initial_conditions[pressure_p] = pybamm.Scalar(0) ###################### # Current in the electrolyte ###################### i_e = (param.kappa_e(c_e, T) * tor * param.gamma_e / param.C_e) * ( param.chi(c_e) * pybamm.grad(c_e) / c_e - pybamm.grad(phi_e)) self.algebraic[phi_e] = pybamm.div(i_e) - j self.boundary_conditions[phi_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } self.initial_conditions[phi_e] = -param.U_n(param.c_e_init, param.T_init) ###################### # Current in the solid ###################### i_s_n = -param.sigma_n * (1 - eps_n)**param.b_s_n * pybamm.grad(phi_s_n) sigma_eff_p = param.sigma_p * (1 - eps_p)**param.b_s_p 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 self.algebraic[phi_s_n] = pybamm.div(i_s_n) + j_n self.algebraic[phi_s_p] = pybamm.div(i_s_p) + 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 self.initial_conditions[phi_s_n] = pybamm.Scalar(0) self.initial_conditions[phi_s_p] = param.U_p( param.c_e_init, param.T_init) - param.U_n(param.c_e_init, param.T_init) ###################### # Porosity ###################### beta_surf = pybamm.Concatenation( pybamm.PrimaryBroadcast(param.beta_surf_n, "negative electrode"), pybamm.PrimaryBroadcast(0, "separator"), pybamm.PrimaryBroadcast(param.beta_surf_p, "positive electrode"), ) deps_dt = -beta_surf * j self.rhs[eps] = deps_dt self.initial_conditions[eps] = param.epsilon_init self.events.extend([ pybamm.Event("Zero negative electrode porosity cut-off", pybamm.min(eps_n)), pybamm.Event("Max negative electrode porosity cut-off", pybamm.max(eps_n) - 1), pybamm.Event("Zero positive electrode porosity cut-off", pybamm.min(eps_p)), pybamm.Event("Max positive electrode porosity cut-off", pybamm.max(eps_p) - 1), ]) ###################### # Electrolyte concentration ###################### N_e = (-tor * param.D_e(c_e, T) * pybamm.grad(c_e) + param.C_e * param.t_plus(c_e) * i_e / param.gamma_e + param.C_e * c_e * v) s = pybamm.Concatenation( pybamm.PrimaryBroadcast(param.s_plus_n_S, "negative electrode"), pybamm.PrimaryBroadcast(0, "separator"), pybamm.PrimaryBroadcast(param.s_plus_p_S, "positive electrode"), ) self.rhs[c_e] = (1 / eps) * (-pybamm.div(N_e) / param.C_e + s * j / param.gamma_e - c_e * deps_dt - c_e * div_V) self.boundary_conditions[c_e] = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } self.initial_conditions[c_e] = param.c_e_init self.events.append( pybamm.Event("Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002)) ###################### # (Some) variables ###################### voltage = pybamm.boundary_value(phi_s_p, "right") # The `variables` dictionary contains all variables that might be useful for # visualising the solution of the model pot = param.potential_scale self.variables = { "Electrolyte concentration": c_e, "Current [A]": I, "Negative electrode potential [V]": pot * phi_s_n, "Electrolyte potential [V]": -param.U_n_ref + pot * phi_e, "Positive electrode potential [V]": param.U_p_ref - param.U_n_ref + pot * phi_s_p, "Terminal voltage [V]": param.U_p_ref - param.U_n_ref + pot * voltage, "x [m]": pybamm.standard_spatial_vars.x * param.L_x, "x": pybamm.standard_spatial_vars.x, "Porosity": eps, "Volume-averaged velocity": v, "X-averaged separator transverse volume-averaged velocity": div_V_s, } self.events.extend([ pybamm.Event("Minimum voltage", voltage - param.voltage_low_cut), pybamm.Event("Maximum voltage", voltage - param.voltage_high_cut), ])
def set_voltage_variables(self): ocp_n = self.variables["Negative electrode open circuit potential"] ocp_p = self.variables["Positive electrode open circuit potential"] ocp_n_av = self.variables[ "X-averaged negative electrode open circuit potential"] ocp_p_av = self.variables[ "X-averaged positive electrode open circuit potential"] ocp_n_dim = self.variables[ "Negative electrode open circuit potential [V]"] ocp_p_dim = self.variables[ "Positive electrode open circuit potential [V]"] ocp_n_av_dim = self.variables[ "X-averaged negative electrode open circuit potential [V]"] ocp_p_av_dim = self.variables[ "X-averaged positive electrode open circuit potential [V]"] ocp_n_left = pybamm.boundary_value(ocp_n, "left") ocp_n_left_dim = pybamm.boundary_value(ocp_n_dim, "left") ocp_p_right = pybamm.boundary_value(ocp_p, "right") ocp_p_right_dim = pybamm.boundary_value(ocp_p_dim, "right") ocv_av = ocp_p_av - ocp_n_av ocv_av_dim = ocp_p_av_dim - ocp_n_av_dim ocv = ocp_p_right - ocp_n_left ocv_dim = ocp_p_right_dim - ocp_n_left_dim # overpotentials eta_r_n_av = self.variables[ "X-averaged negative electrode reaction overpotential"] eta_r_n_av_dim = self.variables[ "X-averaged negative electrode reaction overpotential [V]"] eta_r_p_av = self.variables[ "X-averaged positive electrode reaction overpotential"] eta_r_p_av_dim = self.variables[ "X-averaged positive electrode reaction overpotential [V]"] delta_phi_s_n_av = self.variables[ "X-averaged negative electrode ohmic losses"] delta_phi_s_n_av_dim = self.variables[ "X-averaged negative electrode ohmic losses [V]"] delta_phi_s_p_av = self.variables[ "X-averaged positive electrode ohmic losses"] delta_phi_s_p_av_dim = self.variables[ "X-averaged positive electrode ohmic losses [V]"] delta_phi_s_av = delta_phi_s_p_av - delta_phi_s_n_av delta_phi_s_av_dim = delta_phi_s_p_av_dim - delta_phi_s_n_av_dim eta_r_av = eta_r_p_av - eta_r_n_av eta_r_av_dim = eta_r_p_av_dim - eta_r_n_av_dim # SEI film overpotential eta_sei_n_av = self.variables[ "X-averaged negative electrode SEI film overpotential"] eta_sei_p_av = self.variables[ "X-averaged positive electrode SEI film overpotential"] eta_sei_n_av_dim = self.variables[ "X-averaged negative electrode SEI film overpotential [V]"] eta_sei_p_av_dim = self.variables[ "X-averaged positive electrode SEI film overpotential [V]"] eta_sei_av = eta_sei_n_av + eta_sei_p_av eta_sei_av_dim = eta_sei_n_av_dim + eta_sei_p_av_dim # TODO: add current collector losses to the voltage in 3D self.variables.update({ "X-averaged open circuit voltage": ocv_av, "Measured open circuit voltage": ocv, "X-averaged open circuit voltage [V]": ocv_av_dim, "Measured open circuit voltage [V]": ocv_dim, "X-averaged reaction overpotential": eta_r_av, "X-averaged reaction overpotential [V]": eta_r_av_dim, "X-averaged SEI film overpotential": eta_sei_av, "X-averaged SEI film overpotential [V]": eta_sei_av_dim, "X-averaged solid phase ohmic losses": delta_phi_s_av, "X-averaged solid phase ohmic losses [V]": delta_phi_s_av_dim, }) # Battery-wide variables V = self.variables["Terminal voltage"] V_dim = self.variables["Terminal voltage [V]"] eta_e_av_dim = self.variables[ "X-averaged electrolyte ohmic losses [V]"] eta_c_av_dim = self.variables[ "X-averaged concentration overpotential [V]"] num_cells = pybamm.Parameter( "Number of cells connected in series to make a battery") self.variables.update({ "X-averaged battery open circuit voltage [V]": ocv_av_dim * num_cells, "Measured battery open circuit voltage [V]": ocv_dim * num_cells, "X-averaged battery reaction overpotential [V]": eta_r_av_dim * num_cells, "X-averaged battery solid phase ohmic losses [V]": delta_phi_s_av_dim * num_cells, "X-averaged battery electrolyte ohmic losses [V]": eta_e_av_dim * num_cells, "X-averaged battery concentration overpotential [V]": eta_c_av_dim * num_cells, "Battery voltage [V]": V_dim * num_cells, }) # Variables for calculating the equivalent circuit model (ECM) resistance # Need to compare OCV to initial value to capture this as an overpotential ocv_init = self.param.U_p( self.param.c_p_init(1), self.param.T_init) - self.param.U_n( self.param.c_n_init(0), self.param.T_init) ocv_init_dim = (self.param.U_p_ref - self.param.U_n_ref + self.param.potential_scale * ocv_init) eta_ocv = ocv - ocv_init eta_ocv_dim = ocv_dim - ocv_init_dim # Current collector current density for working out euiqvalent resistance # based on Ohm's Law i_cc = self.variables["Current collector current density"] i_cc_dim = self.variables["Current collector current density [A.m-2]"] # ECM overvoltage is OCV minus terminal voltage v_ecm = ocv - V v_ecm_dim = ocv_dim - V_dim # Current collector area for turning resistivity into resistance A_cc = self.param.A_cc # Hack to avoid division by zero if i_cc is exactly zero # If i_cc is zero, i_cc_not_zero becomes 1. But multiplying by sign(i_cc) makes # the local resistance 'zero' (really, it's not defined when i_cc is zero) i_cc_not_zero = ((i_cc > 0) + (i_cc < 0)) * i_cc + (i_cc >= 0) * (i_cc <= 0) i_cc_dim_not_zero = ( (i_cc_dim > 0) + (i_cc_dim < 0)) * i_cc_dim + (i_cc_dim >= 0) * (i_cc_dim <= 0) self.variables.update({ "Change in measured open circuit voltage": eta_ocv, "Change in measured open circuit voltage [V]": eta_ocv_dim, "Local ECM resistance": pybamm.sign(i_cc) * v_ecm / (i_cc_not_zero * A_cc), "Local ECM resistance [Ohm]": pybamm.sign(i_cc) * v_ecm_dim / (i_cc_dim_not_zero * A_cc), }) # Cut-off voltage self.events.append( pybamm.Event( "Minimum voltage", V - self.param.voltage_low_cut, pybamm.EventType.TERMINATION, )) self.events.append( pybamm.Event( "Maximum voltage", V - self.param.voltage_high_cut, pybamm.EventType.TERMINATION, )) # Cut-off open-circuit voltage (for event switch with casadi 'fast with events' # mode) # A tolerance of 1 is sufficiently small since the dimensionless voltage is # scaled with the thermal voltage (0.025V) and hence has a range of around 60 tol = 1 self.events.append( pybamm.Event( "Minimum voltage switch", V - (self.param.voltage_low_cut - tol), pybamm.EventType.SWITCH, )) self.events.append( pybamm.Event( "Maximum voltage switch", V - (self.param.voltage_high_cut + tol), pybamm.EventType.SWITCH, )) # Power I_dim = self.variables["Current [A]"] self.variables.update({"Terminal power [W]": I_dim * V_dim})