def get_coupled_variables(self, variables): param = self.param if self.domain in ["Negative", "Positive"]: conductivity, sigma_eff = self._get_conductivities(variables) i_boundary_cc = variables["Current collector current density"] c_e = variables[self.domain + " electrolyte concentration"] delta_phi = variables[ self.domain + " electrode surface potential difference" ] T = variables[self.domain + " electrode temperature"] i_e = conductivity * ( ((1 + param.Theta * T) * param.chi(c_e, T) / c_e) * pybamm.grad(c_e) + pybamm.grad(delta_phi) + i_boundary_cc / sigma_eff ) variables.update(self._get_domain_current_variables(i_e)) phi_s = variables[self.domain + " electrode potential"] phi_e = phi_s - delta_phi variables.update(self._get_domain_potential_variables(phi_e)) elif self.domain == "Separator": x_s = pybamm.standard_spatial_vars.x_s i_boundary_cc = variables["Current collector current density"] c_e_s = variables["Separator electrolyte concentration"] phi_e_n = variables["Negative electrolyte potential"] tor_s = variables["Separator porosity"] T = variables["Separator temperature"] chi_e_s = param.chi(c_e_s, T) kappa_s_eff = param.kappa_e(c_e_s, T) * tor_s phi_e_s = pybamm.boundary_value( phi_e_n, "right" ) + pybamm.IndefiniteIntegral( (1 + param.Theta * T) * chi_e_s / c_e_s * pybamm.grad(c_e_s) - param.C_e * i_boundary_cc / kappa_s_eff, x_s, ) i_e_s = pybamm.PrimaryBroadcast(i_boundary_cc, "separator") variables.update(self._get_domain_potential_variables(phi_e_s)) variables.update(self._get_domain_current_variables(i_e_s)) # Update boundary conditions (for indefinite integral) self.boundary_conditions[c_e_s] = { "left": (pybamm.BoundaryGradient(c_e_s, "left"), "Neumann"), "right": (pybamm.BoundaryGradient(c_e_s, "right"), "Neumann"), } if self.domain == "Positive": variables.update(self._get_whole_cell_variables(variables)) return variables
def set_boundary_conditions(self, variables): if self.domain == "Separator": return None param = self.param conductivity, sigma_eff = self._get_conductivities(variables) i_boundary_cc = variables["Current collector current density"] c_e = variables[self.domain + " electrolyte concentration"] delta_phi = variables[self.domain + " electrode surface potential difference"] if self.domain == "Negative": c_e_flux = pybamm.BoundaryGradient(c_e, "right") flux_left = -i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "left") flux_right = ( (i_boundary_cc / pybamm.BoundaryValue(conductivity, "right")) - pybamm.BoundaryValue(param.chi(c_e) / c_e, "right") * c_e_flux - i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "right") ) lbc = (flux_left, "Neumann") rbc = (flux_right, "Neumann") lbc_c_e = (pybamm.Scalar(0), "Neumann") rbc_c_e = (c_e_flux, "Neumann") elif self.domain == "Positive": c_e_flux = pybamm.BoundaryGradient(c_e, "left") flux_left = ( (i_boundary_cc / pybamm.BoundaryValue(conductivity, "left")) - pybamm.BoundaryValue(param.chi(c_e) / c_e, "left") * c_e_flux - i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "left") ) flux_right = -i_boundary_cc * pybamm.BoundaryValue(1 / sigma_eff, "right") lbc = (flux_left, "Neumann") rbc = (flux_right, "Neumann") lbc_c_e = (c_e_flux, "Neumann") rbc_c_e = (pybamm.Scalar(0), "Neumann") # TODO: check if we still need the boundary conditions for c_e, once we have # internal boundary conditions self.boundary_conditions = { delta_phi: {"left": lbc, "right": rbc}, c_e: {"left": lbc_c_e, "right": rbc_c_e}, } if self.domain == "Negative": phi_e = variables["Electrolyte potential"] self.boundary_conditions.update( { phi_e: { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } } )
def test_symbol_new_copy(self): a = pybamm.Parameter("a") b = pybamm.Parameter("b") v_n = pybamm.Variable("v", "negative electrode") x_n = pybamm.standard_spatial_vars.x_n v_s = pybamm.Variable("v", "separator") vec = pybamm.Vector([1, 2, 3, 4, 5]) mat = pybamm.Matrix([[1, 2], [3, 4]]) mesh = get_mesh_for_testing() for symbol in [ a + b, a - b, a * b, a / b, a**b, -a, abs(a), pybamm.Function(np.sin, a), pybamm.FunctionParameter("function", {"a": a}), pybamm.grad(v_n), pybamm.div(pybamm.grad(v_n)), pybamm.upwind(v_n), pybamm.IndefiniteIntegral(v_n, x_n), pybamm.BackwardIndefiniteIntegral(v_n, x_n), pybamm.BoundaryValue(v_n, "right"), pybamm.BoundaryGradient(v_n, "right"), pybamm.PrimaryBroadcast(a, "domain"), pybamm.SecondaryBroadcast(v_n, "current collector"), pybamm.FullBroadcast(a, "domain", {"secondary": "other domain"}), pybamm.concatenation(v_n, v_s), pybamm.NumpyConcatenation(a, b, v_s), pybamm.DomainConcatenation([v_n, v_s], mesh), pybamm.Parameter("param"), pybamm.InputParameter("param"), pybamm.StateVector(slice(0, 56)), pybamm.Matrix(np.ones((50, 40))), pybamm.SpatialVariable("x", ["negative electrode"]), pybamm.t, pybamm.Index(vec, 1), pybamm.NotConstant(a), pybamm.ExternalVariable( "external variable", 20, domain="test", auxiliary_domains={"secondary": "test2"}, ), pybamm.minimum(a, b), pybamm.maximum(a, b), pybamm.SparseStack(mat, mat), ]: self.assertEqual(symbol.id, symbol.new_copy().id)
def test_boundary_value_checks(self): child = pybamm.Symbol("sym", domain=["negative electrode"]) symbol = pybamm.BoundaryGradient(child, "left") mesh = get_mesh_for_testing() spatial_method = pybamm.SpatialMethod() spatial_method.build(mesh) with self.assertRaisesRegex(TypeError, "Cannot process BoundaryGradient"): spatial_method.boundary_value_or_flux(symbol, child) mesh = get_1p1d_mesh_for_testing() spatial_method = pybamm.SpatialMethod() spatial_method.build(mesh) child = pybamm.Symbol( "sym", domain=["negative electrode"], auxiliary_domains={"secondary": "current collector"}, ) symbol = pybamm.BoundaryGradient(child, "left") with self.assertRaisesRegex(NotImplementedError, "Cannot process 2D symbol"): spatial_method.boundary_value_or_flux(symbol, child)
def _get_diffusion_limited_current_density(self, variables): param = self.param if self.domain == "Negative": tor_s = variables["Separator tortuosity"] c_ox_s = variables["Separator oxygen concentration"] N_ox_neg_sep_interface = (-pybamm.boundary_value(tor_s, "left") * param.curlyD_ox * pybamm.BoundaryGradient(c_ox_s, "left")) N_ox_neg_sep_interface.domain = ["current collector"] j = -N_ox_neg_sep_interface / param.C_e / param.s_ox_Ox / param.l_n return j
def _get_sep_coupled_variables(self, variables): """ A private function to get the coupled variables when the domain is 'Separator'. """ param = self.param x_s = pybamm.standard_spatial_vars.x_s i_boundary_cc = variables["Current collector current density"] c_e_s = variables["Separator electrolyte concentration"] phi_e_n = variables["Negative electrolyte potential"] eps_s = variables["Separator porosity"] T = variables["Separator temperature"] chi_e_s = param.chi(c_e_s) kappa_s_eff = param.kappa_e(c_e_s, T) * (eps_s ** param.b_s) phi_e_s = pybamm.PrimaryBroadcast( pybamm.boundary_value(phi_e_n, "right"), "separator" ) + pybamm.IndefiniteIntegral( chi_e_s / c_e_s * pybamm.grad(c_e_s) - param.C_e * pybamm.PrimaryBroadcast(i_boundary_cc, self.domain_for_broadcast) / kappa_s_eff, x_s, ) i_e_s = pybamm.PrimaryBroadcast(i_boundary_cc, "separator") variables.update(self._get_domain_potential_variables(phi_e_s)) variables.update(self._get_domain_current_variables(i_e_s)) # Update boundary conditions (for indefinite integral) self.boundary_conditions[c_e_s] = { "left": (pybamm.BoundaryGradient(c_e_s, "left"), "Neumann"), "right": (pybamm.BoundaryGradient(c_e_s, "right"), "Neumann"), } return variables
def _get_diffusion_limited_current_density(self, variables): param = self.param if self.domain == "Negative": if self.order == "leading": j_p = variables["X-averaged positive electrode" + self.reaction_name + " interfacial current density"] j = -self.param.l_p * j_p / self.param.l_n elif self.order in ["composite", "full"]: tor_s = variables["Separator tortuosity"] c_ox_s = variables["Separator oxygen concentration"] N_ox_neg_sep_interface = ( -pybamm.boundary_value(tor_s, "left") * param.curlyD_ox * pybamm.BoundaryGradient(c_ox_s, "left")) N_ox_neg_sep_interface.domain = ["current collector"] j = -N_ox_neg_sep_interface / param.C_e / -param.s_ox_Ox / param.l_n return j
def test_symbol_new_copy(self): a = pybamm.Scalar(0) b = pybamm.Scalar(1) v_n = pybamm.Variable("v", "negative electrode") x_n = pybamm.standard_spatial_vars.x_n v_s = pybamm.Variable("v", "separator") vec = pybamm.Vector(np.array([1, 2, 3, 4, 5])) mesh = get_mesh_for_testing() for symbol in [ a + b, a - b, a * b, a / b, a**b, -a, abs(a), pybamm.Function(np.sin, a), pybamm.FunctionParameter("function", {"a": a}), pybamm.grad(v_n), pybamm.div(pybamm.grad(v_n)), pybamm.Integral(a, pybamm.t), pybamm.IndefiniteIntegral(v_n, x_n), pybamm.BackwardIndefiniteIntegral(v_n, x_n), pybamm.BoundaryValue(v_n, "right"), pybamm.BoundaryGradient(v_n, "right"), pybamm.PrimaryBroadcast(a, "domain"), pybamm.SecondaryBroadcast(v_n, "current collector"), pybamm.FullBroadcast(a, "domain", {"secondary": "other domain"}), pybamm.Concatenation(v_n, v_s), pybamm.NumpyConcatenation(a, b, v_s), pybamm.DomainConcatenation([v_n, v_s], mesh), pybamm.Parameter("param"), pybamm.InputParameter("param"), pybamm.StateVector(slice(0, 56)), pybamm.Matrix(np.ones((50, 40))), pybamm.SpatialVariable("x", ["negative electrode"]), pybamm.t, pybamm.Index(vec, 1), ]: self.assertEqual(symbol.id, symbol.new_copy().id)
def test_quadratic_extrapolate_left_right(self): # create discretisation mesh = get_mesh_for_testing() method_options = { "extrapolation": { "order": "quadratic", "use bcs": False } } spatial_methods = { "macroscale": pybamm.FiniteVolume(method_options), "negative particle": pybamm.FiniteVolume(method_options), "current collector": pybamm.ZeroDimensionalMethod(method_options), } disc = pybamm.Discretisation(mesh, spatial_methods) whole_cell = ["negative electrode", "separator", "positive electrode"] macro_submesh = mesh.combine_submeshes(*whole_cell) micro_submesh = mesh["negative particle"] # Macroscale # create variable var = pybamm.Variable("var", domain=whole_cell) # boundary value should work with something more complicated than a variable extrap_left = pybamm.BoundaryValue(2 * var, "left") extrap_right = pybamm.BoundaryValue(4 - var, "right") disc.set_variable_slices([var]) extrap_left_disc = disc.process_symbol(extrap_left) extrap_right_disc = disc.process_symbol(extrap_right) # check constant extrapolates to constant constant_y = np.ones_like(macro_submesh[0].nodes[:, np.newaxis]) np.testing.assert_array_almost_equal( extrap_left_disc.evaluate(None, constant_y), 2.0) np.testing.assert_array_almost_equal( extrap_right_disc.evaluate(None, constant_y), 3.0) # check linear variable extrapolates correctly linear_y = macro_submesh[0].nodes np.testing.assert_array_almost_equal( extrap_left_disc.evaluate(None, linear_y), 0) np.testing.assert_array_almost_equal( extrap_right_disc.evaluate(None, linear_y), 3) # Fluxes extrap_flux_left = pybamm.BoundaryGradient(2 * var, "left") extrap_flux_right = pybamm.BoundaryGradient(1 - var, "right") extrap_flux_left_disc = disc.process_symbol(extrap_flux_left) extrap_flux_right_disc = disc.process_symbol(extrap_flux_right) # check constant extrapolates to constant np.testing.assert_array_almost_equal( extrap_flux_left_disc.evaluate(None, constant_y), 0) self.assertEqual(extrap_flux_right_disc.evaluate(None, constant_y), 0) # check linear variable extrapolates correctly np.testing.assert_array_almost_equal( extrap_flux_left_disc.evaluate(None, linear_y), 2) np.testing.assert_array_almost_equal( extrap_flux_right_disc.evaluate(None, linear_y), -1) # Microscale # create variable var = pybamm.Variable("var", domain="negative particle") surf_eqn = pybamm.surf(var) disc.set_variable_slices([var]) surf_eqn_disc = disc.process_symbol(surf_eqn) # check constant extrapolates to constant constant_y = np.ones_like(micro_submesh[0].nodes[:, np.newaxis]) np.testing.assert_array_almost_equal( surf_eqn_disc.evaluate(None, constant_y), 1) # check linear variable extrapolates correctly linear_y = micro_submesh[0].nodes y_surf = micro_submesh[0].edges[-1] np.testing.assert_array_almost_equal( surf_eqn_disc.evaluate(None, linear_y), y_surf)
def __init__(self, n=100, max_x=10, param=None): # Set fixed parameters here if param is None: param = pybamm.ParameterValues({ "Far-field concentration of A [mol cm-3]": 1e-6, "Diffusion Constant [cm2 s-1]": 7.2e-6, "Faraday Constant [C mol-1]": 96485.3328959, "Gas constant [J K-1 mol-1]": 8.314459848, "Electrode Area [cm2]": 0.07, "Temperature [K]": 297.0, "Voltage frequency [rad s-1]": 9.0152, "Voltage start [V]": 0.5, "Voltage reverse [V]": -0.1, "Voltage amplitude [V]": 0.08, "Scan Rate [V s-1]": 0.08941, }) # Create dimensional fixed parameters c_inf = pybamm.Parameter("Far-field concentration of A [mol cm-3]") D = pybamm.Parameter("Diffusion Constant [cm2 s-1]") F = pybamm.Parameter("Faraday Constant [C mol-1]") R = pybamm.Parameter("Gas constant [J K-1 mol-1]") S = pybamm.Parameter("Electrode Area [cm2]") T = pybamm.Parameter("Temperature [K]") E_start_d = pybamm.Parameter("Voltage start [V]") E_reverse_d = pybamm.Parameter("Voltage reverse [V]") deltaE_d = pybamm.Parameter("Voltage amplitude [V]") v = pybamm.Parameter("Scan Rate [V s-1]") # Create dimensional input parameters E0 = pybamm.InputParameter("Reversible Potential [non-dim]") k0 = pybamm.InputParameter("Reaction Rate [non-dim]") alpha = pybamm.InputParameter("Symmetry factor [non-dim]") Cdl = pybamm.InputParameter("Capacitance [non-dim]") Ru = pybamm.InputParameter("Uncompensated Resistance [non-dim]") omega_d = pybamm.InputParameter("Voltage frequency [rad s-1]") E0_d = pybamm.InputParameter("Reversible Potential [V]") k0_d = pybamm.InputParameter("Reaction Rate [s-1]") alpha = pybamm.InputParameter("Symmetry factor [non-dim]") Cdl_d = pybamm.InputParameter("Capacitance [F]") Ru_d = pybamm.InputParameter("Uncompensated Resistance [Ohm]") # Create scaling factors for non-dimensionalisation E_0 = R * T / F T_0 = E_0 / v L_0 = pybamm.sqrt(D * T_0) I_0 = D * F * S * c_inf / L_0 # Non-dimensionalise parameters E0 = E0_d / E_0 k0 = k0_d * L_0 / D Cdl = Cdl_d * S * E_0 / (I_0 * T_0) Ru = Ru_d * I_0 / E_0 omega = 2 * np.pi * omega_d * T_0 E_start = E_start_d / E_0 E_reverse = E_reverse_d / E_0 t_reverse = E_start - E_reverse deltaE = deltaE_d / E_0 # Input voltage protocol Edc_forward = -pybamm.t Edc_backwards = pybamm.t - 2 * t_reverse Eapp = E_start + \ (pybamm.t <= t_reverse) * Edc_forward + \ (pybamm.t > t_reverse) * Edc_backwards + \ deltaE * pybamm.sin(omega * pybamm.t) # create PyBaMM model object model = pybamm.BaseModel() # Create state variables for model theta = pybamm.Variable("ratio_A", domain="solution") i = pybamm.Variable("Current") # Effective potential Eeff = Eapp - i * Ru # Faradaic current i_f = pybamm.BoundaryGradient(theta, "left") # ODE equations model.rhs = { theta: pybamm.div(pybamm.grad(theta)), i: 1 / (Cdl * Ru) * (-i_f + Cdl * Eapp.diff(pybamm.t) - i), } # algebraic equations (none) model.algebraic = {} # Butler-volmer boundary condition at electrode theta_at_electrode = pybamm.BoundaryValue(theta, "left") butler_volmer = k0 * (theta_at_electrode * pybamm.exp(-alpha * (Eeff - E0)) - (1 - theta_at_electrode) * pybamm.exp( (1 - alpha) * (Eeff - E0))) # Boundary and initial conditions model.boundary_conditions = { theta: { "right": (pybamm.Scalar(1), "Dirichlet"), "left": (butler_volmer, "Neumann"), } } model.initial_conditions = { theta: pybamm.Scalar(1), i: Cdl * (-1.0 + deltaE * omega), } # set spatial variables and solution domain geometry x = pybamm.SpatialVariable('x', domain="solution") default_geometry = pybamm.Geometry({ "solution": { x: { "min": pybamm.Scalar(0), "max": pybamm.Scalar(max_x) } } }) default_var_pts = {x: n} # Using Finite Volume discretisation on an expanding 1D grid for solution default_submesh_types = { "solution": pybamm.MeshGenerator(pybamm.Exponential1DSubMesh, {'side': 'left'}) } default_spatial_methods = {"solution": pybamm.FiniteVolume()} # model variables model.variables = { "Current [non-dim]": i, } #-------------------------------- # Set model parameters param.process_model(model) geometry = default_geometry param.process_geometry(geometry) # Create mesh and discretise model mesh = pybamm.Mesh(geometry, default_submesh_types, default_var_pts) disc = pybamm.Discretisation(mesh, default_spatial_methods) disc.process_model(model) # Create solver solver = pybamm.CasadiSolver( mode="fast", rtol=1e-9, atol=1e-9, extra_options_setup={'print_stats': False}) #model.convert_to_format = 'jax' #solver = pybamm.JaxSolver(method='BDF') #model.convert_to_format = 'python' #solver = pybamm.ScipySolver(method='BDF') # Store discretised model and solver self._model = model self._solver = solver self._fast_solver = None self._omega_d = param["Voltage frequency [rad s-1]"] self._I_0 = param.process_symbol(I_0).evaluate() self._T_0 = param.process_symbol(T_0).evaluate() self._E_0 = param.process_symbol(E_0).evaluate() self._L_0 = param.process_symbol(L_0).evaluate() self._S = param.process_symbol(S).evaluate() self._D = param.process_symbol(D).evaluate() self._default_var_points = default_var_pts