From 07c64cec894db4e586fdf655da396c4bd2e15e5a Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Sun, 29 Nov 2020 11:47:16 +0100 Subject: [PATCH 01/14] #1253 added copy for original DFN half cell --- .../lithium_ion/basic_dfn_half_cell_old.py | 475 ++++++++++++++++++ 1 file changed, 475 insertions(+) create mode 100644 pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell_old.py diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell_old.py b/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell_old.py new file mode 100644 index 0000000000..11b06cf34b --- /dev/null +++ b/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell_old.py @@ -0,0 +1,475 @@ +# +# Basic Doyle-Fuller-Newman (DFN) Half Cell Model +# +import pybamm +from .base_lithium_ion_model import BaseModel + + +class BasicDFNHalfCell(BaseModel): + """Doyle-Fuller-Newman (DFN) model of a lithium-ion battery with lithium counter + electrode, adapted from [2]_. + + This class differs from the :class:`pybamm.lithium_ion.BasicDFN` model class in + that it is for a cell with a lithium counter electrode (half cell). This is a + feature under development (for example, it cannot be used with the Simulation class + for the moment) and in the future it will be incorporated as a standard model with + the full functionality. + + Parameters + ---------- + name : str, optional + The name of the model. + options : dict + A dictionary of options to be passed to the model. For the half cell it should + include which is the working electrode. + + References + ---------- + .. [2] M Doyle, TF Fuller and JS Nwman. “Modeling of Galvanostatic Charge and + Discharge of the Lithium/Polymer/Insertion Cell”. Journal of The + Electrochemical Society, 140(6):1526-1533, 1993 + + **Extends:** :class:`pybamm.lithium_ion.BaseModel` + """ + + 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 new_copy(self, build=False): + new_model = self.__class__(name=self.name, options=self.options) + new_model.use_jacobian = self.use_jacobian + new_model.use_simplify = self.use_simplify + new_model.convert_to_format = self.convert_to_format + new_model.timescale = self.timescale + new_model.length_scales = self.length_scales + return new_model From 81914c0a3af8e3e9eac6a5ac24795dda874423c3 Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Tue, 1 Dec 2020 13:34:50 +0100 Subject: [PATCH 02/14] #1253 added half-cell geometry and variables --- pybamm/geometry/half_cell_geometry.py | 88 +++++++++++++++++++++++ pybamm/geometry/half_cell_spatial_vars.py | 85 ++++++++++++++++++++++ pybamm/parameters/geometric_parameters.py | 2 + 3 files changed, 175 insertions(+) create mode 100644 pybamm/geometry/half_cell_geometry.py create mode 100644 pybamm/geometry/half_cell_spatial_vars.py diff --git a/pybamm/geometry/half_cell_geometry.py b/pybamm/geometry/half_cell_geometry.py new file mode 100644 index 0000000000..f293b70034 --- /dev/null +++ b/pybamm/geometry/half_cell_geometry.py @@ -0,0 +1,88 @@ +# +# Function to create battery geometries +# +import pybamm +from pybamm.geometry import half_cell_spatial_vars + + +def half_cell_geometry(include_particles=True, current_collector_dimension=0, working_electrode="positive"): + """ + A convenience function to create battery geometries. + + Parameters + ---------- + include_particles : bool + Whether to include particle domains + current_collector_dimensions : int, default + The dimensions of the current collector. Should be 0 (default), 1 or 2 + current_collector_dimensions : string + The electrode taking as working electrode. Should be "positive" or "negative" + + Returns + ------- + :class:`pybamm.Geometry` + A geometry class for the battery + + """ + var = half_cell_spatial_vars + geo = pybamm.geometric_parameters + if working_electrode == "positive": + l_w = geo.l_p + elif working_electrode == "negative": + l_w = geo.l_n + else: + raise ValueError( + "The option 'working_electrode' should be either 'positive'" + " or 'negative'" + ) + l_Li = geo.l_Li + l_s = geo.l_s + + geometry = { + "lithium counter electrode": {var.x_Li: {"min": 0, "max": l_Li}}, + "separator": {var.x_s: {"min": l_Li, "max": l_Li + l_s}}, + "working electrode": {var.x_w: {"min": l_Li + l_s, "max": l_Li + l_s + l_w}}, + } + # Add particle domains + if include_particles is True: + geometry.update( + { + "working particle": {var.r_w: {"min": 0, "max": 1}}, + } + ) + + if current_collector_dimension == 0: + geometry["current collector"] = {var.z: {"position": 1}} + elif current_collector_dimension == 1: + geometry["current collector"] = { + var.z: {"min": 0, "max": 1}, + "tabs": { + "negative": {"z_centre": geo.centre_z_tab_n}, + "positive": {"z_centre": geo.centre_z_tab_p}, + }, + } + elif current_collector_dimension == 2: + geometry["current collector"] = { + var.y: {"min": 0, "max": geo.l_y}, + var.z: {"min": 0, "max": geo.l_z}, + "tabs": { + "negative": { + "y_centre": geo.centre_y_tab_n, + "z_centre": geo.centre_z_tab_n, + "width": geo.l_tab_n, + }, + "positive": { + "y_centre": geo.centre_y_tab_p, + "z_centre": geo.centre_z_tab_p, + "width": geo.l_tab_p, + }, + }, + } + else: + raise pybamm.GeometryError( + "Invalid current collector dimension '{}' (should be 0, 1 or 2)".format( + current_collector_dimension + ) + ) + + return pybamm.Geometry(geometry) diff --git a/pybamm/geometry/half_cell_spatial_vars.py b/pybamm/geometry/half_cell_spatial_vars.py new file mode 100644 index 0000000000..e54579a44a --- /dev/null +++ b/pybamm/geometry/half_cell_spatial_vars.py @@ -0,0 +1,85 @@ +import pybamm + +whole_cell = ["separator", "working electrode"] + +# Domains at cell centres +x_Li = pybamm.SpatialVariable( + "x_Li", + domain=["lithium counter electrode"], + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", +) +x_s = pybamm.SpatialVariable( + "x_s", + domain=["separator"], + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", +) +x_w = pybamm.SpatialVariable( + "x_w", + domain=["working electrode"], + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", +) +x = pybamm.SpatialVariable( + "x", + domain=whole_cell, + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", +) + +y = pybamm.SpatialVariable("y", domain="current collector", coord_sys="cartesian") +z = pybamm.SpatialVariable("z", domain="current collector", coord_sys="cartesian") + +r_w = pybamm.SpatialVariable( + "r_w", + domain=["working particle"], + auxiliary_domains={ + "secondary": "working electrode", + "tertiary": "current collector", + }, + coord_sys="spherical polar", +) + +# Domains at cell edges +x_Li_edge = pybamm.SpatialVariableEdge( + "x_Li", + domain=["lithium counter electrode"], + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", +) +x_s_edge = pybamm.SpatialVariableEdge( + "x_s", + domain=["separator"], + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", +) +x_w_edge = pybamm.SpatialVariableEdge( + "x_w", + domain=["working electrode"], + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", +) +x_edge = pybamm.SpatialVariableEdge( + "x", + domain=whole_cell, + auxiliary_domains={"secondary": "current collector"}, + coord_sys="cartesian", +) + +y_edge = pybamm.SpatialVariableEdge( + "y", domain="current collector", coord_sys="cartesian" +) +z_edge = pybamm.SpatialVariableEdge( + "z", domain="current collector", coord_sys="cartesian" +) + +r_w_edge = pybamm.SpatialVariableEdge( + "r_w", + domain=["working particle"], + auxiliary_domains={ + "secondary": "working electrode", + "tertiary": "current collector", + }, + coord_sys="spherical polar", +) diff --git a/pybamm/parameters/geometric_parameters.py b/pybamm/parameters/geometric_parameters.py index 91b7de39a7..04b0bb47af 100644 --- a/pybamm/parameters/geometric_parameters.py +++ b/pybamm/parameters/geometric_parameters.py @@ -34,6 +34,7 @@ def _set_dimensional_parameters(self): self.L = self.L_cn + self.L_x + self.L_cp # Total cell thickness self.L_y = pybamm.Parameter("Electrode width [m]") self.L_z = pybamm.Parameter("Electrode height [m]") + self.L_Li = pybamm.Parameter("Lithium counter electrode thickness [m]") self.A_cc = self.L_y * self.L_z # Area of current collector self.A_cooling = pybamm.Parameter("Cell cooling surface area [m2]") self.V_cell = pybamm.Parameter("Cell volume [m3]") @@ -86,6 +87,7 @@ def _set_dimensionless_parameters(self): self.l_x = self.L_x / self.L_x self.l_y = self.L_y / self.L_z self.l_z = self.L_z / self.L_z + self.l_Li = self.L_Li / self.L_x self.a_cc = self.l_y * self.l_z self.a_cooling = self.A_cooling / (self.L_z ** 2) self.v_cell = self.V_cell / (self.L_x * self.L_z ** 2) From 13ebd4daccb98af4db9cf64315b56c2af1d83714 Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Tue, 1 Dec 2020 13:36:19 +0100 Subject: [PATCH 03/14] #1253 reformatted DFN half-cell model --- .../lithium_ion/basic_dfn_half_cell.py | 554 ++++++++---------- 1 file changed, 244 insertions(+), 310 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py b/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py index 11b06cf34b..c935b41fd3 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py @@ -3,6 +3,8 @@ # import pybamm from .base_lithium_ion_model import BaseModel +from pybamm.geometry import half_cell_spatial_vars +from pybamm.geometry.half_cell_geometry import half_cell_geometry class BasicDFNHalfCell(BaseModel): @@ -60,91 +62,30 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): 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) + # 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) + # phi_Li = pybamm.Variable( + # "Lithium counter electrode potential", domain="lithium counter electrode" + # ) # Constant temperature T = param.T_init @@ -156,57 +97,95 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): # 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 + # 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_n = pybamm.surf(c_s_n) - c_s_surf_p = pybamm.surf(c_s_p) + 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": - 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)) - ) + # 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" ) - j_s = pybamm.PrimaryBroadcast(0, "separator") - j_p = pybamm.PrimaryBroadcast(0, "positive electrode") - j = pybamm.Concatenation(j_n, j_s) + 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) + 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)) - ) + # 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" ) - j_s = pybamm.PrimaryBroadcast(0, "separator") - j_n = pybamm.PrimaryBroadcast(0, "negative electrode") - j = pybamm.Concatenation(j_s, j_p) + 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 + + 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 @@ -221,123 +200,52 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): ###################### # 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",), + } - 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), - ), - ] + # 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 ###################### - 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 - ) + 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 @@ -353,16 +261,10 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): / (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.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( @@ -381,18 +283,13 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): 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.boundary_conditions[phi_e] = { + "left": (ref_potential, "Dirichlet"), + "right": (pybamm.Scalar(0), "Neumann"), + } self.initial_conditions[phi_e] = ref_potential + ###################### # (Some) variables ###################### @@ -402,68 +299,105 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): "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 - ) + 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_s_surf_p_av = pybamm.x_average(c_s_surf_p) - c_s_surf_n_av = pybamm.x_average(c_s_surf_n) + c_e_total = pybamm.x_average(eps * c_e) + c_s_surf_w_av = pybamm.x_average(c_s_surf_w) # 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, + "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, "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, + "Total electrolyte concentration": c_e_total, "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), + "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, - "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, + "Voltage drop in the cell": vdrop_cell, + "Voltage drop in the cell [V]": phi_s_w_ref + U_ref + pot * vdrop_cell, + "Terminal voltage": voltage, + "Terminal voltage [V]": phi_s_w_ref + U_ref + pot * voltage, + } + + @property + def default_geometry(self): + return half_cell_geometry( + current_collector_dimension=self.options["dimensionality"], + working_electrode=self.options["working electrode"], + ) + + @property + def default_var_pts(self): + var = pybamm.geometry.half_cell_spatial_vars + base_var_pts = { + var.x_Li: 20, + var.x_s: 20, + var.x_w: 20, + var.r_w: 30, + var.y: 10, + var.z: 10, + } + # Reduce the default points for 2D current collectors + if self.options["dimensionality"] == 2: + base_var_pts.update({var.x_Li: 10, var.x_s: 10, var.x_w: 10}) + return base_var_pts + + @property + def default_submesh_types(self): + base_submeshes = { + "lithium counter electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "separator": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "working electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + "working particle": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), + } + if self.options["dimensionality"] == 0: + base_submeshes["current collector"] = pybamm.MeshGenerator(pybamm.SubMesh0D) + elif self.options["dimensionality"] == 1: + base_submeshes["current collector"] = pybamm.MeshGenerator( + pybamm.Uniform1DSubMesh + ) + elif self.options["dimensionality"] == 2: + base_submeshes["current collector"] = pybamm.MeshGenerator( + pybamm.ScikitUniform2DSubMesh + ) + return base_submeshes + + @property + def default_spatial_methods(self): + base_spatial_methods = { + "lithium counter electrode": pybamm.FiniteVolume(), + "separator": pybamm.FiniteVolume(), + "working electrode": pybamm.FiniteVolume(), + "working particle": pybamm.FiniteVolume(), } + if self.options["dimensionality"] == 0: + # 0D submesh - use base spatial method + base_spatial_methods[ + "current collector" + ] = pybamm.ZeroDimensionalSpatialMethod() + elif self.options["dimensionality"] == 1: + base_spatial_methods["current collector"] = pybamm.FiniteVolume() + elif self.options["dimensionality"] == 2: + base_spatial_methods["current collector"] = pybamm.ScikitFiniteElement() + return base_spatial_methods def new_copy(self, build=False): new_model = self.__class__(name=self.name, options=self.options) From 61b882b0a683028c0f0328cf9dc550004e97dd62 Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Tue, 1 Dec 2020 13:36:31 +0100 Subject: [PATCH 04/14] #1253 update example script --- examples/scripts/DFN_half_cell.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/scripts/DFN_half_cell.py b/examples/scripts/DFN_half_cell.py index a27c4af046..9d43424394 100644 --- a/examples/scripts/DFN_half_cell.py +++ b/examples/scripts/DFN_half_cell.py @@ -4,6 +4,7 @@ import pybamm import numpy as np +from pybamm.geometry import half_cell_spatial_vars pybamm.set_logging_level("INFO") @@ -33,8 +34,9 @@ param.process_geometry(geometry) # set mesh -var = pybamm.standard_spatial_vars -var_pts = {var.x_n: 30, var.x_s: 30, var.x_p: 30, var.r_n: 10, var.r_p: 10} +# var = pybamm.standard_spatial_vars +# var_pts = {var.x_n: 30, var.x_s: 30, var.x_p: 30, var.r_n: 10, var.r_p: 10} +var_pts = model.default_var_pts mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) # discretise model @@ -50,14 +52,14 @@ plot = pybamm.QuickPlot( solution, [ - "Negative particle surface concentration [mol.m-3]", + "Working particle surface concentration [mol.m-3]", + # "Working particle concentration [mol.m-3]", "Electrolyte concentration [mol.m-3]", - "Positive particle surface concentration [mol.m-3]", "Current [A]", - "Negative electrode potential [V]", + "Working electrode potential [V]", "Electrolyte potential [V]", - "Positive electrode potential [V]", - "Terminal voltage [V]", + "Total electrolyte concentration", + ["Terminal voltage [V]", "Voltage drop in the cell [V]"], ], time_unit="seconds", spatial_unit="um", From f1fb79313b95f7913fd7c787e7505c86bfc2a5de Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Fri, 4 Dec 2020 12:51:27 +0100 Subject: [PATCH 05/14] #1253 working model including conservation checks --- pybamm/expression_tree/symbol.py | 2 ++ pybamm/expression_tree/unary_operators.py | 9 +++-- .../lithium_ion/basic_dfn_half_cell.py | 35 +++++++++++++++++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/pybamm/expression_tree/symbol.py b/pybamm/expression_tree/symbol.py index af2125cf50..0bb42a3ede 100644 --- a/pybamm/expression_tree/symbol.py +++ b/pybamm/expression_tree/symbol.py @@ -27,6 +27,8 @@ def domain_size(domain): "negative electrode": 11, "separator": 13, "positive electrode": 17, + "working electrode": 19, + "working particle": 23, } if isinstance(domain, str): domain = [domain] diff --git a/pybamm/expression_tree/unary_operators.py b/pybamm/expression_tree/unary_operators.py index d5be9ef49c..f90af6556a 100644 --- a/pybamm/expression_tree/unary_operators.py +++ b/pybamm/expression_tree/unary_operators.py @@ -1286,7 +1286,11 @@ def r_average(symbol): raise ValueError("Can't take the r-average of a symbol that evaluates on edges") # Otherwise, if symbol doesn't have a particle domain, # its r-averaged value is itself - elif symbol.domain not in [["positive particle"], ["negative particle"]]: + elif symbol.domain not in [ + ["positive particle"], + ["negative particle"], + ["working particle"], + ]: new_symbol = symbol.new_copy() new_symbol.parent = None return new_symbol @@ -1294,7 +1298,7 @@ def r_average(symbol): # "positive electrode", take the r-average of the child then broadcast back elif isinstance(symbol, pybamm.SecondaryBroadcast) and symbol.domains[ "secondary" - ] in [["positive electrode"], ["negative electrode"]]: + ] in [["positive electrode"], ["negative electrode"], ["working electrode"]]: child = symbol.orphans[0] child_av = pybamm.r_average(child) return pybamm.PrimaryBroadcast(child_av, symbol.domains["secondary"]) @@ -1302,6 +1306,7 @@ def r_average(symbol): elif isinstance(symbol, pybamm.PrimaryBroadcast) and symbol.domain in [ ["positive particle"], ["negative particle"], + ["working particle"], ]: return symbol.orphans[0] else: diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py b/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py index c935b41fd3..89a6c36ea9 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py @@ -52,6 +52,20 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): 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_y, + "current collector z": param.L_z, + } + ###################### # Variables ###################### @@ -142,6 +156,7 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): 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 @@ -177,6 +192,7 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): 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) @@ -309,6 +325,9 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): 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 = { @@ -320,9 +339,19 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): "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, @@ -332,9 +361,11 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): "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 + U_ref + pot * 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 + U_ref + pot * voltage, + "Terminal voltage [V]": phi_s_w_ref + param.U_n_ref + pot * voltage, } @property From 1035e4b0a862001ccc7aa5ae3d7d39c5b5804b8b Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Fri, 4 Dec 2020 12:51:40 +0100 Subject: [PATCH 06/14] #1253 updated working example --- examples/scripts/DFN_half_cell.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/examples/scripts/DFN_half_cell.py b/examples/scripts/DFN_half_cell.py index 9d43424394..dc7f0bd7da 100644 --- a/examples/scripts/DFN_half_cell.py +++ b/examples/scripts/DFN_half_cell.py @@ -9,7 +9,7 @@ pybamm.set_logging_level("INFO") # load model -options = {"working electrode": "positive"} +options = {"working electrode": "negative"} model = pybamm.lithium_ion.BasicDFNHalfCell(options=options) # create geometry @@ -29,13 +29,24 @@ check_already_exists=False, ) +param["Initial concentration in negative electrode [mol.m-3]"] = 1000 +param["Current function [A]"] = 2.5 + # process model and geometry param.process_model(model) param.process_geometry(geometry) # set mesh -# var = pybamm.standard_spatial_vars -# var_pts = {var.x_n: 30, var.x_s: 30, var.x_p: 30, var.r_n: 10, var.r_p: 10} +# var = half_cell_spatial_vars +# var_pts = { +# var.x_Li: 20, +# var.x_s: 200, +# var.x_w: 200, +# var.r_w: 30, +# var.y: 10, +# var.z: 10, +# } + var_pts = model.default_var_pts mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) @@ -44,8 +55,8 @@ disc.process_model(model) # solve model -t_eval = np.linspace(0, 3800, 1000) -solver = pybamm.CasadiSolver(mode="fast", atol=1e-6, rtol=1e-3) +t_eval = np.linspace(0, 7200, 1000) +solver = pybamm.CasadiSolver(mode="safe", atol=1e-6, rtol=1e-3) solution = solver.solve(model, t_eval) # plot @@ -59,6 +70,8 @@ "Working electrode potential [V]", "Electrolyte potential [V]", "Total electrolyte concentration", + "Total lithium in working electrode [mol]", + "Working electrode open circuit potential [V]", ["Terminal voltage [V]", "Voltage drop in the cell [V]"], ], time_unit="seconds", From 8bfda77bdb60c4e05b295e27478ab241ade9d60b Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Fri, 4 Dec 2020 12:55:31 +0100 Subject: [PATCH 07/14] flake8 --- examples/scripts/DFN_half_cell.py | 2 +- pybamm/geometry/half_cell_geometry.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/scripts/DFN_half_cell.py b/examples/scripts/DFN_half_cell.py index dc7f0bd7da..ff3a899587 100644 --- a/examples/scripts/DFN_half_cell.py +++ b/examples/scripts/DFN_half_cell.py @@ -4,7 +4,7 @@ import pybamm import numpy as np -from pybamm.geometry import half_cell_spatial_vars +# from pybamm.geometry import half_cell_spatial_vars pybamm.set_logging_level("INFO") diff --git a/pybamm/geometry/half_cell_geometry.py b/pybamm/geometry/half_cell_geometry.py index f293b70034..3c4b0f8ee6 100644 --- a/pybamm/geometry/half_cell_geometry.py +++ b/pybamm/geometry/half_cell_geometry.py @@ -5,7 +5,9 @@ from pybamm.geometry import half_cell_spatial_vars -def half_cell_geometry(include_particles=True, current_collector_dimension=0, working_electrode="positive"): +def half_cell_geometry( + include_particles=True, current_collector_dimension=0, working_electrode="positive" +): """ A convenience function to create battery geometries. @@ -46,9 +48,7 @@ def half_cell_geometry(include_particles=True, current_collector_dimension=0, wo # Add particle domains if include_particles is True: geometry.update( - { - "working particle": {var.r_w: {"min": 0, "max": 1}}, - } + {"working particle": {var.r_w: {"min": 0, "max": 1}}} ) if current_collector_dimension == 0: From 861141b44917eed18cd9ec229c1053db6279b58b Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Wed, 9 Dec 2020 10:35:40 +0100 Subject: [PATCH 08/14] #1253 allow for half-cell plots --- pybamm/solvers/processed_variable.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pybamm/solvers/processed_variable.py b/pybamm/solvers/processed_variable.py index 1a8a3f176f..ca447f3195 100644 --- a/pybamm/solvers/processed_variable.py +++ b/pybamm/solvers/processed_variable.py @@ -343,9 +343,11 @@ def initialise_2D(self): if self.domain[0] in [ "negative particle", "positive particle", + "working particle", ] and self.auxiliary_domains["secondary"][0] in [ "negative electrode", "positive electrode", + "working electrode", ]: self.first_dimension = "r" self.second_dimension = "x" From adb7264cef7dc551b216355c889c8240a73f6940 Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Wed, 9 Dec 2020 10:36:05 +0100 Subject: [PATCH 09/14] #1253 cleaned example --- examples/scripts/DFN_half_cell.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/examples/scripts/DFN_half_cell.py b/examples/scripts/DFN_half_cell.py index ff3a899587..7c1616a540 100644 --- a/examples/scripts/DFN_half_cell.py +++ b/examples/scripts/DFN_half_cell.py @@ -4,12 +4,11 @@ import pybamm import numpy as np -# from pybamm.geometry import half_cell_spatial_vars pybamm.set_logging_level("INFO") # load model -options = {"working electrode": "negative"} +options = {"working electrode": "positive"} model = pybamm.lithium_ion.BasicDFNHalfCell(options=options) # create geometry @@ -37,16 +36,6 @@ param.process_geometry(geometry) # set mesh -# var = half_cell_spatial_vars -# var_pts = { -# var.x_Li: 20, -# var.x_s: 200, -# var.x_w: 200, -# var.r_w: 30, -# var.y: 10, -# var.z: 10, -# } - var_pts = model.default_var_pts mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) @@ -63,8 +52,7 @@ plot = pybamm.QuickPlot( solution, [ - "Working particle surface concentration [mol.m-3]", - # "Working particle concentration [mol.m-3]", + "Working particle concentration [mol.m-3]", "Electrolyte concentration [mol.m-3]", "Current [A]", "Working electrode potential [V]", From ecbd8533612af63b5e20f3e8a737d4eb8bcb3b54 Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Wed, 9 Dec 2020 10:36:44 +0100 Subject: [PATCH 10/14] #1253 remove old half-cell model --- .../lithium_ion/basic_dfn_half_cell_old.py | 475 ------------------ 1 file changed, 475 deletions(-) delete mode 100644 pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell_old.py diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell_old.py b/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell_old.py deleted file mode 100644 index 11b06cf34b..0000000000 --- a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell_old.py +++ /dev/null @@ -1,475 +0,0 @@ -# -# Basic Doyle-Fuller-Newman (DFN) Half Cell Model -# -import pybamm -from .base_lithium_ion_model import BaseModel - - -class BasicDFNHalfCell(BaseModel): - """Doyle-Fuller-Newman (DFN) model of a lithium-ion battery with lithium counter - electrode, adapted from [2]_. - - This class differs from the :class:`pybamm.lithium_ion.BasicDFN` model class in - that it is for a cell with a lithium counter electrode (half cell). This is a - feature under development (for example, it cannot be used with the Simulation class - for the moment) and in the future it will be incorporated as a standard model with - the full functionality. - - Parameters - ---------- - name : str, optional - The name of the model. - options : dict - A dictionary of options to be passed to the model. For the half cell it should - include which is the working electrode. - - References - ---------- - .. [2] M Doyle, TF Fuller and JS Nwman. “Modeling of Galvanostatic Charge and - Discharge of the Lithium/Polymer/Insertion Cell”. Journal of The - Electrochemical Society, 140(6):1526-1533, 1993 - - **Extends:** :class:`pybamm.lithium_ion.BaseModel` - """ - - 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 new_copy(self, build=False): - new_model = self.__class__(name=self.name, options=self.options) - new_model.use_jacobian = self.use_jacobian - new_model.use_simplify = self.use_simplify - new_model.convert_to_format = self.convert_to_format - new_model.timescale = self.timescale - new_model.length_scales = self.length_scales - return new_model From cf6c9570b0aebe180988721aae4201e49d064ba7 Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Wed, 9 Dec 2020 15:41:57 +0100 Subject: [PATCH 11/14] #1253 added tests --- .../test_geometry/test_half_cell_geometry.py | 116 ++++++++++++++++++ .../test_lithium_ion/test_basic_models.py | 97 +++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 tests/unit/test_geometry/test_half_cell_geometry.py diff --git a/tests/unit/test_geometry/test_half_cell_geometry.py b/tests/unit/test_geometry/test_half_cell_geometry.py new file mode 100644 index 0000000000..a38fe59202 --- /dev/null +++ b/tests/unit/test_geometry/test_half_cell_geometry.py @@ -0,0 +1,116 @@ +# +# Tests for the base model class +# +import pybamm +import unittest +from pybamm.geometry import half_cell_spatial_vars +from pybamm.geometry.half_cell_geometry import half_cell_geometry + + +class TestHalfCellGeometry(unittest.TestCase): + def test_geometry_keys(self): + for working_electrode in ["positive", "negative"]: + for cc_dimension in [0, 1, 2]: + geometry = half_cell_geometry( + current_collector_dimension=cc_dimension, + working_electrode=working_electrode, + ) + for domain_geoms in geometry.values(): + all( + self.assertIsInstance(spatial_var, pybamm.SpatialVariable) + for spatial_var in domain_geoms.keys() + ) + + def test_geometry(self): + var = half_cell_spatial_vars + geo = pybamm.geometric_parameters + for working_electrode in ["positive", "negative"]: + if working_electrode == "positive": + l_w = geo.l_p + else: + l_w = geo.l_n + for cc_dimension in [0, 1, 2]: + geometry = half_cell_geometry( + current_collector_dimension=cc_dimension, + working_electrode=working_electrode, + ) + self.assertIsInstance(geometry, pybamm.Geometry) + self.assertIn("working electrode", geometry) + self.assertIn("working particle", geometry) + self.assertEqual( + geometry["working electrode"][var.x_w]["min"].id, + (geo.l_Li + geo.l_s).id, + ) + self.assertEqual( + geometry["working electrode"][var.x_w]["max"].id, + (geo.l_Li + geo.l_s + l_w).id, + ) + if cc_dimension == 1: + self.assertIn("tabs", geometry["current collector"]) + + geometry = pybamm.battery_geometry(include_particles=False) + self.assertNotIn("working particle", geometry) + + def test_geometry_error(self): + with self.assertRaisesRegex(pybamm.GeometryError, "Invalid current"): + pybamm.battery_geometry(current_collector_dimension=4) + + +class TestReadParameters(unittest.TestCase): + # This is the most complicated geometry and should test the parameters are + # all returned for the deepest dict + def test_read_parameters(self): + geo = pybamm.geometric_parameters + L_n = geo.L_n + L_s = geo.L_s + L_p = geo.L_p + L_y = geo.L_y + L_z = geo.L_z + tab_n_y = geo.Centre_y_tab_n + tab_n_z = geo.Centre_z_tab_n + L_tab_n = geo.L_tab_n + tab_p_y = geo.Centre_y_tab_p + tab_p_z = geo.Centre_z_tab_p + L_tab_p = geo.L_tab_p + L_Li = geo.L_Li + + for working_electrode in ["positive", "negative"]: + geometry = half_cell_geometry( + current_collector_dimension=2, working_electrode=working_electrode + ) + + self.assertEqual( + set([x.name for x in geometry.parameters]), + set( + [ + x.name + for x in [ + L_n, + L_s, + L_p, + L_y, + L_z, + tab_n_y, + tab_n_z, + L_tab_n, + tab_p_y, + tab_p_z, + L_tab_p, + L_Li, + ] + ] + ), + ) + self.assertTrue( + all(isinstance(x, pybamm.Parameter) for x in geometry.parameters) + ) + + +if __name__ == "__main__": + print("Add -v for more debug output") + import sys + + if "-v" in sys.argv: + debug = True + pybamm.settings.debug_mode = True + unittest.main() diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py index 2bb6d1cd81..d6080b2c22 100644 --- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py +++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_basic_models.py @@ -3,6 +3,7 @@ # import pybamm import unittest +from pybamm.geometry import half_cell_spatial_vars class TestBasicModels(unittest.TestCase): @@ -43,6 +44,102 @@ def test_dfn_half_cell_simulation_error(self): ): pybamm.Simulation(model) + def test_dfn_half_cell_defaults(self): + # test default geometry + var = half_cell_spatial_vars + + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 0, "working electrode": "positive"} + ) + self.assertEqual( + model.default_geometry["current collector"][var.z]["position"], 1 + ) + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 1, "working electrode": "positive"} + ) + self.assertEqual(model.default_geometry["current collector"][var.z]["min"], 0) + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 2, "working electrode": "positive"} + ) + self.assertEqual(model.default_geometry["current collector"][var.y]["min"], 0) + + # test default variable points + var = half_cell_spatial_vars + var_pts = { + var.x_Li: 20, + var.x_s: 20, + var.x_w: 20, + var.r_w: 30, + var.y: 10, + var.z: 10, + } + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 0, "working electrode": "positive"} + ) + self.assertDictEqual(var_pts, model.default_var_pts) + + var_pts.update({var.x_Li: 10, var.x_s: 10, var.x_w: 10}) + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 2, "working electrode": "positive"} + ) + self.assertDictEqual(var_pts, model.default_var_pts) + + # test default submesh types + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 0, "working electrode": "positive"} + ) + self.assertTrue( + issubclass( + model.default_submesh_types["current collector"].submesh_type, + pybamm.SubMesh0D, + ) + ) + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 1, "working electrode": "positive"} + ) + self.assertTrue( + issubclass( + model.default_submesh_types["current collector"].submesh_type, + pybamm.Uniform1DSubMesh, + ) + ) + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 2, "working electrode": "positive"} + ) + self.assertTrue( + issubclass( + model.default_submesh_types["current collector"].submesh_type, + pybamm.ScikitUniform2DSubMesh, + ) + ) + # test default spatial methods + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 0, "working electrode": "positive"} + ) + self.assertTrue( + isinstance( + model.default_spatial_methods["current collector"], + pybamm.ZeroDimensionalSpatialMethod, + ) + ) + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 1, "working electrode": "positive"} + ) + self.assertTrue( + isinstance( + model.default_spatial_methods["current collector"], pybamm.FiniteVolume + ) + ) + model = pybamm.lithium_ion.BasicDFNHalfCell( + options={"dimensionality": 2, "working electrode": "positive"} + ) + self.assertTrue( + isinstance( + model.default_spatial_methods["current collector"], + pybamm.ScikitFiniteElement, + ) + ) + if __name__ == "__main__": print("Add -v for more debug output") From b9e9582290f6cc040999e960cf0648b496c801c9 Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Wed, 9 Dec 2020 17:14:33 +0100 Subject: [PATCH 12/14] #1253 improved tests --- tests/unit/test_geometry/test_half_cell_geometry.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_geometry/test_half_cell_geometry.py b/tests/unit/test_geometry/test_half_cell_geometry.py index a38fe59202..b3f02a55b9 100644 --- a/tests/unit/test_geometry/test_half_cell_geometry.py +++ b/tests/unit/test_geometry/test_half_cell_geometry.py @@ -53,7 +53,11 @@ def test_geometry(self): def test_geometry_error(self): with self.assertRaisesRegex(pybamm.GeometryError, "Invalid current"): - pybamm.battery_geometry(current_collector_dimension=4) + half_cell_geometry( + current_collector_dimension=4, working_electrode="positive" + ) + with self.assertRaisesRegex(ValueError, "The option 'working_electrode'"): + half_cell_geometry(working_electrode="bad electrode") class TestReadParameters(unittest.TestCase): From ab1eb5d1870b49e8bc3e90661e73525c5927e1c8 Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Tue, 15 Dec 2020 09:02:25 +0100 Subject: [PATCH 13/14] #1253 Tino's comments --- .../full_battery_models/lithium_ion/basic_dfn_half_cell.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py b/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py index 89a6c36ea9..7caa4a53d5 100644 --- a/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py +++ b/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py @@ -97,9 +97,6 @@ def __init__(self, name="Doyle-Fuller-Newman half cell model", options=None): "Working electrolyte potential", domain="working electrode" ) phi_e = pybamm.Concatenation(phi_e_s, phi_e_w) - # phi_Li = pybamm.Variable( - # "Lithium counter electrode potential", domain="lithium counter electrode" - # ) # Constant temperature T = param.T_init From ad0144ed2442d03af8f39c8695c92a65e6003e25 Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Tue, 15 Dec 2020 09:59:43 +0100 Subject: [PATCH 14/14] #1253 update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6f3848bd..388d3ceef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features +- Reformatted the `BasicDFNHalfCell` to be consistent with the other models ([#1282](https://github.com/pybamm-team/PyBaMM/pull/1282)) - Added option to make the total interfacial current density a state ([#1280](https://github.com/pybamm-team/PyBaMM/pull/1280)) - Added functionality to initialize a model using the solution from another model ([#1278](https://github.com/pybamm-team/PyBaMM/pull/1278)) - Added submodels for active material ([#1262](https://github.com/pybamm-team/PyBaMM/pull/1262)) @@ -14,6 +15,7 @@ ## Bug fixes - Fixed `Simulation` and `model.new_copy` to fix a bug where changes to the model were overwritten ([#1278](https://github.com/pybamm-team/PyBaMM/pull/1278)) + ## Breaking changes - Boolean model options ('sei porosity change', 'convection') must now be given in string format ('true' or 'false' instead of True or False) ([#1280](https://github.com/pybamm-team/PyBaMM/pull/1280))