def test_sinh(self): a = pybamm.InputParameter("a") fun = pybamm.sinh(a) self.assertIsInstance(fun, pybamm.Sinh) self.assertEqual(fun.children[0].id, a.id) self.assertEqual(fun.evaluate(inputs={"a": 3}), np.sinh(3)) h = 0.0000001 self.assertAlmostEqual( fun.diff(a).evaluate(inputs={"a": 3}), (pybamm.sinh(pybamm.Scalar(3 + h)).evaluate() - fun.evaluate(inputs={"a": 3})) / h, places=5, )
def test_sinh(self): a = pybamm.Scalar(3) fun = pybamm.sinh(a) self.assertIsInstance(fun, pybamm.Sinh) self.assertEqual(fun.children[0].id, a.id) self.assertEqual(fun.evaluate(), np.sinh(3)) self.assertEqual(fun.diff(a).evaluate(), np.cosh(3))
def _get_dj_dc(self, variables): """ See :meth:`pybamm.interface.kinetics.BaseKinetics._get_dj_dc` """ c_e, delta_phi, j0, ne, ocp, T = self._get_interface_variables_for_first_order( variables) eta_r = delta_phi - ocp prefactor = ne / (2 * (1 + self.param.Theta * T)) return (2 * j0.diff(c_e) * pybamm.sinh(prefactor * eta_r)) - ( 2 * j0 * prefactor * ocp.diff(c_e) * pybamm.cosh(prefactor * eta_r))
def _get_dj_dc(self, variables): "See :meth:`pybamm.interface.kinetics.BaseModel._get_dj_dc`" c_e, delta_phi, j0, ne, ocp = self._get_interface_variables_for_first_order( variables) eta_r = delta_phi - ocp return (2 * j0.diff(c_e) * pybamm.sinh( (ne / 2) * eta_r)) - (2 * j0 * (ne / 2) * ocp.diff(c_e) * pybamm.cosh( (ne / 2) * eta_r))
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 __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 _get_kinetics(self, j0, ne, eta_r, T): prefactor = ne / (2 * (1 + self.param.Theta * T)) return 2 * j0 * pybamm.sinh(prefactor * eta_r)
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 _get_kinetics(self, j0, ne, eta_r): return 2 * j0 * pybamm.sinh((ne / 2) * eta_r)
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, }