def test_loqs_spme(self): t_eval = np.linspace(0, 10, 2) for model in [pybamm.lithium_ion.SPMe(), pybamm.lead_acid.LOQS()]: geometry = model.default_geometry param = model.default_parameter_values param.process_model(model) param.process_geometry(geometry) var = pybamm.standard_spatial_vars var_pts = { var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 5, var.r_p: 5 } mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) solver = model.default_solver solution = solver.solve(model, t_eval) pybamm.QuickPlot(solution) # check 1D (space) variables update properly for different time units t = solution["Time [s]"].entries c_e_var = solution["Electrolyte concentration [mol.m-3]"] # 1D variables should be evaluated on edges L_x = param.evaluate(model.param.L_x) c_e = c_e_var(t=t, x=mesh.combine_submeshes(*c_e_var.domain).edges * L_x) for unit, scale in zip(["seconds", "minutes", "hours"], [1, 60, 3600]): quick_plot = pybamm.QuickPlot( solution, ["Electrolyte concentration [mol.m-3]"], time_unit=unit) quick_plot.plot(0) qp_data = ( quick_plot.plots[("Electrolyte concentration [mol.m-3]", )][0][0].get_ydata(), )[0] np.testing.assert_array_almost_equal(qp_data, c_e[:, 0]) quick_plot.slider_update(t_eval[-1] / scale) qp_data = ( quick_plot.plots[("Electrolyte concentration [mol.m-3]", )][0][0].get_ydata(), )[0][:, 0] np.testing.assert_array_almost_equal(qp_data, c_e[:, 1]) # test quick plot of particle for spme if model.name == "Single Particle Model with electrolyte": output_variables = [ "X-averaged negative particle concentration [mol.m-3]", "X-averaged positive particle concentration [mol.m-3]", "Negative particle concentration [mol.m-3]", "Positive particle concentration [mol.m-3]", ] pybamm.QuickPlot(solution, output_variables) # check 2D (space) variables update properly for different time units c_n = solution["Negative particle concentration [mol.m-3]"] for unit, scale in zip(["seconds", "minutes", "hours"], [1, 60, 3600]): quick_plot = pybamm.QuickPlot( solution, ["Negative particle concentration [mol.m-3]"], time_unit=unit, ) quick_plot.plot(0) qp_data = quick_plot.plots[( "Negative particle concentration [mol.m-3]", )][0][1] c_n_eval = c_n(t_eval[0], r=c_n.first_dim_pts, x=c_n.second_dim_pts) np.testing.assert_array_almost_equal(qp_data, c_n_eval) quick_plot.slider_update(t_eval[-1] / scale) qp_data = quick_plot.plots[( "Negative particle concentration [mol.m-3]", )][0][1] c_n_eval = c_n(t_eval[-1], r=c_n.first_dim_pts, x=c_n.second_dim_pts) np.testing.assert_array_almost_equal(qp_data, c_n_eval) pybamm.close_plots()
def test_process_empty_model(self): model = pybamm.BaseModel() disc = pybamm.Discretisation() with self.assertRaisesRegex(pybamm.ModelError, "Cannot discretise empty model"): disc.process_model(model)
def test_simple_ode_model(self): model = pybamm.BaseBatteryModel(name="Simple ODE Model") whole_cell = ["negative electrode", "separator", "positive electrode"] # Create variables: domain is explicitly empty since these variables are only # functions of time a = pybamm.Variable("a", domain=[]) b = pybamm.Variable("b", domain=[]) c = pybamm.Variable("c", domain=[]) # Simple ODEs model.rhs = {a: pybamm.Scalar(2), b: pybamm.Scalar(0), c: -c} # Simple initial conditions model.initial_conditions = { a: pybamm.Scalar(0), b: pybamm.Scalar(1), c: pybamm.Scalar(1), } # no boundary conditions for an ODE model # Broadcast some of the variables model.variables = { "a": a, "b broadcasted": pybamm.FullBroadcast(b, whole_cell, "current collector"), "c broadcasted": pybamm.FullBroadcast(c, ["negative electrode", "separator"], "current collector"), } # ODEs only (don't use jacobian) model.use_jacobian = False # Process and solve geometry = model.default_geometry param = model.default_parameter_values param.process_model(model) param.process_geometry(geometry) mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) solver = model.default_solver t_eval = np.linspace(0, 2, 100) solution = solver.solve(model, t_eval) quick_plot = pybamm.QuickPlot(model, mesh, solution) quick_plot.plot(0) # update the axis new_axis = [0, 0.5, 0, 1] quick_plot.axis.update({("a", ): new_axis}) self.assertEqual(quick_plot.axis[("a", )], new_axis) # and now reset them quick_plot.reset_axis() self.assertNotEqual(quick_plot.axis[("a", )], new_axis) # check dynamic plot loads quick_plot.dynamic_plot(testing=True) quick_plot.update(0.01) # Test with different output variables quick_plot = pybamm.QuickPlot(model, mesh, solution, ["b broadcasted"]) self.assertEqual(len(quick_plot.axis), 1) quick_plot.plot(0) quick_plot = pybamm.QuickPlot( model, mesh, solution, [["a", "a"], ["b broadcasted", "b broadcasted"], "c broadcasted"], ) self.assertEqual(len(quick_plot.axis), 3) quick_plot.plot(0) # update the axis new_axis = [0, 0.5, 0, 1] var_key = ("c broadcasted", ) quick_plot.axis.update({var_key: new_axis}) self.assertEqual(quick_plot.axis[var_key], new_axis) # and now reset them quick_plot.reset_axis() self.assertNotEqual(quick_plot.axis[var_key], new_axis) # check dynamic plot loads quick_plot.dynamic_plot(testing=True) quick_plot.update(0.01) # Test longer name model.variables["Variable with a very long name"] = model.variables[ "a"] quick_plot = pybamm.QuickPlot(model, mesh, solution) quick_plot.plot(0) # Test errors with self.assertRaisesRegex(ValueError, "mismatching variable domains"): pybamm.QuickPlot(model, mesh, solution, [["a", "b broadcasted"]]) model.variables["3D variable"] = disc.process_symbol( pybamm.FullBroadcast(1, "negative particle", {"secondary": "negative electrode"})) with self.assertRaisesRegex(NotImplementedError, "cannot plot 3D variables"): pybamm.QuickPlot(model, mesh, solution, ["3D variable"])
def test_concatenation_external_variables(self): model = pybamm.BaseModel() a = pybamm.Variable("a", domain=["test", "test1"]) b1 = pybamm.Variable("b", domain=["test"]) b2 = pybamm.Variable("c", domain=["test1"]) b = pybamm.Concatenation(b1, b2) model.rhs = {a: a * b} model.boundary_conditions = { a: {"left": (0, "Dirichlet"), "right": (0, "Dirichlet")} } model.initial_conditions = {a: 0} model.external_variables = [b] model.variables = { "a": a, "b": b, "b1": b1, "b2": b2, "c": a * b, "grad b": pybamm.grad(b), "div grad b": pybamm.div(pybamm.grad(b)), } x = pybamm.SpatialVariable("x", domain="test", coord_sys="cartesian") y = pybamm.SpatialVariable("y", domain="test1", coord_sys="cartesian") geometry = { "test": {x: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}}, "test1": {y: {"min": pybamm.Scalar(1), "max": pybamm.Scalar(2)}}, } submesh_types = { "test": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "test1": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), } var_pts = {x: 10, y: 5} mesh = pybamm.Mesh(geometry, submesh_types, var_pts) spatial_methods = { "test": pybamm.FiniteVolume(), "test1": pybamm.FiniteVolume(), } disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) self.assertEqual(disc.y_slices[a.id][0], slice(0, 15, None)) b_test = np.linspace(0, 1, 15)[:, np.newaxis] np.testing.assert_array_equal( model.variables["b"].evaluate(inputs={"b": b_test}), b_test ) np.testing.assert_array_equal( model.variables["b1"].evaluate(inputs={"b": b_test}), b_test[:10] ) np.testing.assert_array_equal( model.variables["b2"].evaluate(inputs={"b": b_test}), b_test[10:] ) # check that b is added to the boundary conditions model.bcs[b.id]["left"] model.bcs[b.id]["right"] # check that grad and div(grad ) produce the correct shapes self.assertEqual(model.variables["b"].shape_for_testing, (15, 1)) self.assertEqual(model.variables["grad b"].shape_for_testing, (16, 1)) self.assertEqual(model.variables["div grad b"].shape_for_testing, (15, 1)) self.assertEqual(model.variables["b1"].shape_for_testing, (10, 1)) self.assertEqual(model.variables["b2"].shape_for_testing, (5, 1))
def test_process_symbol_base(self): # create discretisation mesh = get_mesh_for_testing() spatial_methods = { "macroscale": pybamm.SpatialMethod(), "negative particle": pybamm.SpatialMethod(), "positive particle": pybamm.SpatialMethod(), "current collector": pybamm.SpatialMethod(), } disc = pybamm.Discretisation(mesh, spatial_methods) # variable var = pybamm.Variable("var") var_vec = pybamm.Variable("var vec", domain=["negative electrode"]) disc.y_slices = {var.id: [slice(53)], var_vec.id: [slice(53, 93)]} var_disc = disc.process_symbol(var) self.assertIsInstance(var_disc, pybamm.StateVector) self.assertEqual(var_disc.y_slices[0], disc.y_slices[var.id][0]) # variable dot var_dot = pybamm.VariableDot("var'") var_dot_disc = disc.process_symbol(var_dot) self.assertIsInstance(var_dot_disc, pybamm.StateVectorDot) self.assertEqual(var_dot_disc.y_slices[0], disc.y_slices[var.id][0]) # scalar scal = pybamm.Scalar(5) scal_disc = disc.process_symbol(scal) self.assertIsInstance(scal_disc, pybamm.Scalar) self.assertEqual(scal_disc.value, scal.value) # vector vec = pybamm.Vector(np.array([1, 2, 3, 4])) vec_disc = disc.process_symbol(vec) self.assertIsInstance(vec_disc, pybamm.Vector) np.testing.assert_array_equal(vec_disc.entries, vec.entries) # matrix mat = pybamm.Matrix(np.array([[1, 2, 3, 4], [5, 6, 7, 8]])) mat_disc = disc.process_symbol(mat) self.assertIsInstance(mat_disc, pybamm.Matrix) np.testing.assert_array_equal(mat_disc.entries, mat.entries) # binary operator bin = var + scal bin_disc = disc.process_symbol(bin) self.assertIsInstance(bin_disc, pybamm.Addition) self.assertIsInstance(bin_disc.children[0], pybamm.StateVector) self.assertIsInstance(bin_disc.children[1], pybamm.Scalar) bin2 = scal + var bin2_disc = disc.process_symbol(bin2) self.assertIsInstance(bin2_disc, pybamm.Addition) self.assertIsInstance(bin2_disc.children[0], pybamm.Scalar) self.assertIsInstance(bin2_disc.children[1], pybamm.StateVector) # non-spatial unary operator un1 = -var un1_disc = disc.process_symbol(un1) self.assertIsInstance(un1_disc, pybamm.Negate) self.assertIsInstance(un1_disc.children[0], pybamm.StateVector) un2 = abs(var) un2_disc = disc.process_symbol(un2) self.assertIsInstance(un2_disc, pybamm.AbsoluteValue) self.assertIsInstance(un2_disc.children[0], pybamm.StateVector) # function of one variable def myfun(x): return np.exp(x) func = pybamm.Function(myfun, var) func_disc = disc.process_symbol(func) self.assertIsInstance(func_disc, pybamm.Function) self.assertIsInstance(func_disc.children[0], pybamm.StateVector) func = pybamm.Function(myfun, scal) func_disc = disc.process_symbol(func) self.assertIsInstance(func_disc, pybamm.Function) self.assertIsInstance(func_disc.children[0], pybamm.Scalar) # function of multiple variables def myfun(x, y): return np.exp(x) * y func = pybamm.Function(myfun, var, scal) func_disc = disc.process_symbol(func) self.assertIsInstance(func_disc, pybamm.Function) self.assertIsInstance(func_disc.children[0], pybamm.StateVector) self.assertIsInstance(func_disc.children[1], pybamm.Scalar) # boundary value bv_left = pybamm.BoundaryValue(var_vec, "left") bv_left_disc = disc.process_symbol(bv_left) self.assertIsInstance(bv_left_disc, pybamm.MatrixMultiplication) self.assertIsInstance(bv_left_disc.left, pybamm.Matrix) self.assertIsInstance(bv_left_disc.right, pybamm.StateVector) bv_right = pybamm.BoundaryValue(var_vec, "left") bv_right_disc = disc.process_symbol(bv_right) self.assertIsInstance(bv_right_disc, pybamm.MatrixMultiplication) self.assertIsInstance(bv_right_disc.left, pybamm.Matrix) self.assertIsInstance(bv_right_disc.right, pybamm.StateVector) # not implemented sym = pybamm.Symbol("sym") with self.assertRaises(NotImplementedError): disc.process_symbol(sym)
def test_p2d_spherical_grad_div_shapes_Dirichlet_bcs(self): """ Test grad and div with Dirichlet boundary conditions (applied by grad on var) in the pseudo 2-dimensional case """ mesh = get_p2d_mesh_for_testing() spatial_methods = { "macroscale": pybamm.SpectralVolume(), "negative particle": pybamm.SpectralVolume(), "positive particle": pybamm.SpectralVolume(), } disc = pybamm.Discretisation(mesh, spatial_methods) n_mesh = mesh["negative particle"] mesh.add_ghost_meshes() disc.mesh.add_ghost_meshes() var = pybamm.Variable( "var", domain=["negative particle"], auxiliary_domains={"secondary": "negative electrode"}, ) grad_eqn = pybamm.grad(var) boundary_conditions = { var.id: { "left": (pybamm.Scalar(1), "Dirichlet"), "right": (pybamm.Scalar(1), "Dirichlet"), } } disc.bcs = boundary_conditions disc.set_variable_slices([var]) grad_eqn_disc = disc.process_symbol(grad_eqn) prim_pts = n_mesh.npts sec_pts = mesh["negative electrode"].npts constant_y = np.kron(np.ones(sec_pts), np.ones(prim_pts)) grad_eval = grad_eqn_disc.evaluate(None, constant_y) grad_eval = np.reshape(grad_eval, [sec_pts, prim_pts + 1]) np.testing.assert_array_equal(grad_eval, np.zeros([sec_pts, prim_pts + 1])) # div # div (grad r^2) = 6, N_left = N_right = 0 N = pybamm.grad(var) div_eqn = pybamm.div(N) bc_var = disc.process_symbol( pybamm.SpatialVariable("x_n", domain="negative electrode")) boundary_conditions = { var.id: { "left": (bc_var, "Neumann"), "right": (bc_var, "Neumann") } } disc.bcs = boundary_conditions div_eqn_disc = disc.process_symbol(div_eqn) const = 6 * np.ones(sec_pts * prim_pts) div_eval = div_eqn_disc.evaluate(None, const) div_eval = np.reshape(div_eval, [sec_pts, prim_pts]) np.testing.assert_array_almost_equal(div_eval[:, :-1], np.zeros([sec_pts, prim_pts - 1]))
# Define geometry geometry = {"rod": {x: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(2)}}} # Set parameter values param = pybamm.ParameterValues({"Thermal diffusivity": 0.75}) # Process model and geometry param.process_model(model) param.process_geometry(geometry) # Pick mesh, spatial method, and discretise submesh_types = {"rod": pybamm.Uniform1DSubMesh} var_pts = {x: 30} mesh = pybamm.Mesh(geometry, submesh_types, var_pts) spatial_methods = {"rod": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) # Solve solver = pybamm.ScipySolver() t = np.linspace(0, 1, 100) solution = solver.solve(model, t) # Extract output variables T_out = solution["Temperature"] # Exact solution ------------------------------------------------------- N = 100 # number of Fourier modes to sum k_val = param["Thermal diffusivity"] # extract value of diffusivity
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
def export_casadi_objects(self, variable_names, input_parameter_order=None): """ Export the constituent parts of the model (rhs, algebraic, initial conditions, etc) as casadi objects. Parameters ---------- variable_names : list Variables to be exported alongside the model structure input_parameter_order : list, optional Order in which the input parameters should be stacked. If None, the order returned by :meth:`BaseModel.input_parameters` is used Returns ------- casadi_dict : dict Dictionary of {str: casadi object} pairs representing the model in casadi format """ # Discretise model if it isn't already discretised # This only works with purely 0D models, as otherwise the mesh and spatial # method should be specified by the user if self.is_discretised is False: try: disc = pybamm.Discretisation() disc.process_model(self) except pybamm.DiscretisationError as e: raise pybamm.DiscretisationError( "Cannot automatically discretise model, model should be " "discretised before exporting casadi functions ({})".format(e) ) # Create casadi functions for the model t_casadi = casadi.MX.sym("t") y_diff = casadi.MX.sym("y_diff", self.concatenated_rhs.size) y_alg = casadi.MX.sym("y_alg", self.concatenated_algebraic.size) y_casadi = casadi.vertcat(y_diff, y_alg) # Read inputs inputs_wrong_order = {} for input_param in self.input_parameters: name = input_param.name inputs_wrong_order[name] = casadi.MX.sym(name, input_param._expected_size) # Read external variables external_casadi = {} for external_varaiable in self.external_variables: name = external_varaiable.name ev_size = external_varaiable._evaluate_for_shape().shape[0] external_casadi[name] = casadi.MX.sym(name, ev_size) # Sort according to input_parameter_order if input_parameter_order is None: inputs = inputs_wrong_order else: inputs = {name: inputs_wrong_order[name] for name in input_parameter_order} # Set up external variables and inputs # Put external variables first like the integrator expects ext_and_in = {**external_casadi, **inputs} inputs_stacked = casadi.vertcat(*[p for p in ext_and_in.values()]) # Convert initial conditions to casadi form y0 = self.concatenated_initial_conditions.to_casadi( t_casadi, y_casadi, inputs=inputs ) x0 = y0[: self.concatenated_rhs.size] z0 = y0[self.concatenated_rhs.size :] # Convert rhs and algebraic to casadi form and calculate jacobians rhs = self.concatenated_rhs.to_casadi(t_casadi, y_casadi, inputs=ext_and_in) jac_rhs = casadi.jacobian(rhs, y_casadi) algebraic = self.concatenated_algebraic.to_casadi( t_casadi, y_casadi, inputs=inputs ) jac_algebraic = casadi.jacobian(algebraic, y_casadi) # For specified variables, convert to casadi variables = OrderedDict() for name in variable_names: var = self.variables[name] variables[name] = var.to_casadi(t_casadi, y_casadi, inputs=ext_and_in) casadi_dict = { "t": t_casadi, "x": y_diff, "z": y_alg, "inputs": inputs_stacked, "rhs": rhs, "algebraic": algebraic, "jac_rhs": jac_rhs, "jac_algebraic": jac_algebraic, "variables": variables, "x0": x0, "z0": z0, } return casadi_dict
def test_discretise_equations(self): # get mesh mesh = get_2p1d_mesh_for_testing(include_particles=False) spatial_methods = { "macroscale": pybamm.FiniteVolume(), "current collector": pybamm.ScikitFiniteElement(), } disc = pybamm.Discretisation(mesh, spatial_methods) # discretise some equations var = pybamm.Variable("var", domain="current collector") y = pybamm.SpatialVariable("y", ["current collector"]) z = pybamm.SpatialVariable("z", ["current collector"]) disc.set_variable_slices([var]) y_test = np.ones(mesh["current collector"].npts) unit_source = pybamm.PrimaryBroadcast(1, "current collector") disc.bcs = { var.id: { "negative tab": (pybamm.Scalar(0), "Neumann"), "positive tab": (pybamm.Scalar(0), "Neumann"), } } for eqn in [ pybamm.laplacian(var), pybamm.source(unit_source, var), pybamm.laplacian(var) - pybamm.source(unit_source, var), pybamm.source(var, var), pybamm.laplacian(var) - pybamm.source(2 * var, var), pybamm.laplacian(var) - pybamm.source(unit_source ** 2 + 1 / var, var), pybamm.Integral(var, [y, z]) - 1, pybamm.source(var, var, boundary=True), pybamm.laplacian(var) - pybamm.source(unit_source, var, boundary=True), pybamm.laplacian(var) - pybamm.source(unit_source ** 2 + 1 / var, var, boundary=True), ]: # Check that equation can be evaluated in each case # Dirichlet disc.bcs = { var.id: { "negative tab": (pybamm.Scalar(0), "Dirichlet"), "positive tab": (pybamm.Scalar(1), "Dirichlet"), } } eqn_disc = disc.process_symbol(eqn) eqn_disc.evaluate(None, y_test) # Neumann disc.bcs = { var.id: { "negative tab": (pybamm.Scalar(0), "Neumann"), "positive tab": (pybamm.Scalar(1), "Neumann"), } } eqn_disc = disc.process_symbol(eqn) eqn_disc.evaluate(None, y_test) # One of each disc.bcs = { var.id: { "negative tab": (pybamm.Scalar(0), "Neumann"), "positive tab": (pybamm.Scalar(1), "Dirichlet"), } } eqn_disc = disc.process_symbol(eqn) eqn_disc.evaluate(None, y_test) # One of each disc.bcs = { var.id: { "negative tab": (pybamm.Scalar(0), "Dirichlet"), "positive tab": (pybamm.Scalar(1), "Neumann"), } } eqn_disc = disc.process_symbol(eqn) eqn_disc.evaluate(None, y_test) # check ValueError raised for non Dirichlet or Neumann BCs eqn = pybamm.laplacian(var) - pybamm.source(unit_source, var) disc.bcs = { var.id: { "negative tab": (pybamm.Scalar(0), "Dirichlet"), "positive tab": (pybamm.Scalar(1), "Other BC"), } } with self.assertRaises(ValueError): eqn_disc = disc.process_symbol(eqn) disc.bcs = { var.id: { "negative tab": (pybamm.Scalar(0), "Other BC"), "positive tab": (pybamm.Scalar(1), "Neumann"), } } with self.assertRaises(ValueError): eqn_disc = disc.process_symbol(eqn) # raise ModelError if no BCs provided new_var = pybamm.Variable("new_var", domain="current collector") disc.set_variable_slices([new_var]) eqn = pybamm.laplacian(new_var) with self.assertRaises(pybamm.ModelError): eqn_disc = disc.process_symbol(eqn) # check GeometryError if using scikit-fem not in y or z x = pybamm.SpatialVariable("x", ["current collector"]) with self.assertRaises(pybamm.GeometryError): disc.process_symbol(x)
def test_manufactured_solution_exponential_grid(self): param = pybamm.ParameterValues( values={ "Electrode width [m]": 1, "Electrode height [m]": 1, "Negative tab width [m]": 1, "Negative tab centre y-coordinate [m]": 0.5, "Negative tab centre z-coordinate [m]": 0, "Positive tab width [m]": 1, "Positive tab centre y-coordinate [m]": 0.5, "Positive tab centre z-coordinate [m]": 1, "Negative electrode thickness [m]": 0.3, "Separator thickness [m]": 0.3, "Positive electrode thickness [m]": 0.3, } ) geometry = pybamm.battery_geometry( include_particles=False, current_collector_dimension=2 ) param.process_geometry(geometry) var = pybamm.standard_spatial_vars var_pts = {var.x_n: 3, var.x_s: 3, var.x_p: 3, var.y: 32, var.z: 32} submesh_types = { "negative electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "separator": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "positive electrode": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh), "current collector": pybamm.MeshGenerator( pybamm.ScikitExponential2DSubMesh ), } mesh = pybamm.Mesh(geometry, submesh_types, var_pts) spatial_methods = { "macroscale": pybamm.FiniteVolume(), "current collector": pybamm.ScikitFiniteElement(), } disc = pybamm.Discretisation(mesh, spatial_methods) # laplace of u = cos(pi*y)*sin(pi*z) var = pybamm.Variable("var", domain="current collector") laplace_eqn = pybamm.laplacian(var) # set boundary conditions ("negative tab" = bottom of unit square, # "positive tab" = top of unit square, elsewhere normal derivative is zero) disc.bcs = { var.id: { "negative tab": (pybamm.Scalar(0), "Dirichlet"), "positive tab": (pybamm.Scalar(0), "Dirichlet"), } } disc.set_variable_slices([var]) laplace_eqn_disc = disc.process_symbol(laplace_eqn) y_vertices = mesh["current collector"].coordinates[0, :][:, np.newaxis] z_vertices = mesh["current collector"].coordinates[1, :][:, np.newaxis] u = np.cos(np.pi * y_vertices) * np.sin(np.pi * z_vertices) mass = pybamm.Mass(var) mass_disc = disc.process_symbol(mass) soln = -np.pi ** 2 * u np.testing.assert_array_almost_equal( laplace_eqn_disc.evaluate(None, u), mass_disc.entries @ soln, decimal=1 )
def test_manufactured_solution(self): mesh = get_unit_2p1D_mesh_for_testing(ypts=32, zpts=32, include_particles=False) spatial_methods = { "macroscale": pybamm.FiniteVolume(), "current collector": pybamm.ScikitFiniteElement(), } disc = pybamm.Discretisation(mesh, spatial_methods) # linear u = z (to test coordinates to degree of freedom mapping) var = pybamm.Variable("var", domain="current collector") disc.set_variable_slices([var]) var_disc = disc.process_symbol(var) z_vertices = mesh["current collector"].coordinates[1, :] np.testing.assert_array_almost_equal( var_disc.evaluate(None, z_vertices), z_vertices[:, np.newaxis] ) # linear u = 6*y (to test coordinates to degree of freedom mapping) y_vertices = mesh["current collector"].coordinates[0, :] np.testing.assert_array_almost_equal( var_disc.evaluate(None, 6 * y_vertices), 6 * y_vertices[:, np.newaxis] ) # mixed u = y*z (to test coordinates to degree of freedom mapping) np.testing.assert_array_almost_equal( var_disc.evaluate(None, y_vertices * z_vertices), y_vertices[:, np.newaxis] * z_vertices[:, np.newaxis], ) # laplace of u = sin(pi*z) var = pybamm.Variable("var", domain="current collector") eqn_zz = pybamm.laplacian(var) # set boundary conditions ("negative tab" = bottom of unit square, # "positive tab" = top of unit square, elsewhere normal derivative is zero) disc.bcs = { var.id: { "negative tab": (pybamm.Scalar(0), "Dirichlet"), "positive tab": (pybamm.Scalar(0), "Dirichlet"), } } disc.set_variable_slices([var]) eqn_zz_disc = disc.process_symbol(eqn_zz) z_vertices = mesh["current collector"].coordinates[1, :][:, np.newaxis] u = np.sin(np.pi * z_vertices) mass = pybamm.Mass(var) mass_disc = disc.process_symbol(mass) soln = -np.pi ** 2 * u np.testing.assert_array_almost_equal( eqn_zz_disc.evaluate(None, u), mass_disc.entries @ soln, decimal=3 ) # laplace of u = cos(pi*y)*sin(pi*z) var = pybamm.Variable("var", domain="current collector") laplace_eqn = pybamm.laplacian(var) # set boundary conditions ("negative tab" = bottom of unit square, # "positive tab" = top of unit square, elsewhere normal derivative is zero) disc.bcs = { var.id: { "negative tab": (pybamm.Scalar(0), "Dirichlet"), "positive tab": (pybamm.Scalar(0), "Dirichlet"), } } disc.set_variable_slices([var]) laplace_eqn_disc = disc.process_symbol(laplace_eqn) y_vertices = mesh["current collector"].coordinates[0, :][:, np.newaxis] z_vertices = mesh["current collector"].coordinates[1, :][:, np.newaxis] u = np.cos(np.pi * y_vertices) * np.sin(np.pi * z_vertices) mass = pybamm.Mass(var) mass_disc = disc.process_symbol(mass) soln = -np.pi ** 2 * u np.testing.assert_array_almost_equal( laplace_eqn_disc.evaluate(None, u), mass_disc.entries @ soln, decimal=2 )
def set_up(self, model, inputs=None, t_eval=None): """Unpack model, perform checks, simplify and calculate jacobian. Parameters ---------- model : :class:`pybamm.BaseModel` The model whose solution to calculate. Must have attributes rhs and initial_conditions inputs : dict, optional Any input parameters to pass to the model when solving t_eval : numeric type, optional The times (in seconds) at which to compute the solution """ # Check model.algebraic for ode solvers if self.ode_solver is True and len(model.algebraic) > 0: raise pybamm.SolverError( "Cannot use ODE solver '{}' to solve DAE model".format( self.name)) # Check model.rhs for algebraic solvers if self.algebraic_solver is True and len(model.rhs) > 0: raise pybamm.SolverError( """Cannot use algebraic solver to solve model with time derivatives""" ) # casadi solver won't allow solving algebraic model so we have to raise an # error here if isinstance(self, pybamm.CasadiSolver) and len(model.rhs) == 0: raise pybamm.SolverError( "Cannot use CasadiSolver to solve algebraic model, " "use CasadiAlgebraicSolver instead") # Discretise model if it isn't already discretised # This only works with purely 0D models, as otherwise the mesh and spatial # method should be specified by the user if model.is_discretised is False: try: disc = pybamm.Discretisation() disc.process_model(model) except pybamm.DiscretisationError as e: raise pybamm.DiscretisationError( "Cannot automatically discretise model, " "model should be discretised before solving ({})".format( e)) inputs = inputs or {} # Set model timescale model.timescale_eval = model.timescale.evaluate(inputs=inputs) # Set model lengthscales model.length_scales_eval = { domain: scale.evaluate(inputs=inputs) for domain, scale in model.length_scales.items() } if (isinstance(self, (pybamm.CasadiSolver, pybamm.CasadiAlgebraicSolver)) ) and model.convert_to_format != "casadi": pybamm.logger.warning( "Converting {} to CasADi for solving with CasADi solver". format(model.name)) model.convert_to_format = "casadi" if (isinstance(self.root_method, pybamm.CasadiAlgebraicSolver) and model.convert_to_format != "casadi"): pybamm.logger.warning( "Converting {} to CasADi for calculating ICs with CasADi". format(model.name)) model.convert_to_format = "casadi" if model.convert_to_format != "casadi": simp = pybamm.Simplification() # Create Jacobian from concatenated rhs and algebraic y = pybamm.StateVector( slice(0, model.concatenated_initial_conditions.size)) # set up Jacobian object, for re-use of dict jacobian = pybamm.Jacobian() else: # Convert model attributes to casadi t_casadi = casadi.MX.sym("t") y_diff = casadi.MX.sym("y_diff", model.concatenated_rhs.size) y_alg = casadi.MX.sym("y_alg", model.concatenated_algebraic.size) y_casadi = casadi.vertcat(y_diff, y_alg) p_casadi = {} for name, value in inputs.items(): if isinstance(value, numbers.Number): p_casadi[name] = casadi.MX.sym(name) else: p_casadi[name] = casadi.MX.sym(name, value.shape[0]) p_casadi_stacked = casadi.vertcat(*[p for p in p_casadi.values()]) def process(func, name, use_jacobian=None): def report(string): # don't log event conversion if "event" not in string: pybamm.logger.info(string) if use_jacobian is None: use_jacobian = model.use_jacobian if model.convert_to_format != "casadi": # Process with pybamm functions if model.use_simplify: report(f"Simplifying {name}") func = simp.simplify(func) if model.convert_to_format == "jax": report(f"Converting {name} to jax") jax_func = pybamm.EvaluatorJax(func) if use_jacobian: report(f"Calculating jacobian for {name}") jac = jacobian.jac(func, y) if model.use_simplify: report(f"Simplifying jacobian for {name}") jac = simp.simplify(jac) if model.convert_to_format == "python": report(f"Converting jacobian for {name} to python") jac = pybamm.EvaluatorPython(jac) elif model.convert_to_format == "jax": report(f"Converting jacobian for {name} to jax") jac = jax_func.get_jacobian() jac = jac.evaluate else: jac = None if model.convert_to_format == "python": report(f"Converting {name} to python") func = pybamm.EvaluatorPython(func) if model.convert_to_format == "jax": report(f"Converting {name} to jax") func = jax_func func = func.evaluate else: # Process with CasADi report(f"Converting {name} to CasADi") func = func.to_casadi(t_casadi, y_casadi, inputs=p_casadi) if use_jacobian: report(f"Calculating jacobian for {name} using CasADi") jac_casadi = casadi.jacobian(func, y_casadi) jac = casadi.Function( name, [t_casadi, y_casadi, p_casadi_stacked], [jac_casadi]) else: jac = None func = casadi.Function(name, [t_casadi, y_casadi, p_casadi_stacked], [func]) if name == "residuals": func_call = Residuals(func, name, model) else: func_call = SolverCallable(func, name, model) if jac is not None: jac_call = SolverCallable(jac, name + "_jac", model) else: jac_call = None return func, func_call, jac_call # Check for heaviside and modulo functions in rhs and algebraic and add # discontinuity events if these exist. # Note: only checks for the case of t < X, t <= X, X < t, or X <= t, but also # accounts for the fact that t might be dimensional # Only do this for DAE models as ODE models can deal with discontinuities fine if len(model.algebraic) > 0: for symbol in itertools.chain( model.concatenated_rhs.pre_order(), model.concatenated_algebraic.pre_order(), ): if isinstance(symbol, pybamm.Heaviside): found_t = False # Dimensionless if symbol.right.id == pybamm.t.id: expr = symbol.left found_t = True elif symbol.left.id == pybamm.t.id: expr = symbol.right found_t = True # Dimensional elif symbol.right.id == (pybamm.t * model.timescale).id: expr = symbol.left.new_copy( ) / symbol.right.right.new_copy() found_t = True elif symbol.left.id == (pybamm.t * model.timescale).id: expr = symbol.right.new_copy( ) / symbol.left.right.new_copy() found_t = True # Update the events if the heaviside function depended on t if found_t: model.events.append( pybamm.Event( str(symbol), expr.new_copy(), pybamm.EventType.DISCONTINUITY, )) elif isinstance(symbol, pybamm.Modulo): found_t = False # Dimensionless if symbol.left.id == pybamm.t.id: expr = symbol.right found_t = True # Dimensional elif symbol.left.id == (pybamm.t * model.timescale).id: expr = symbol.right.new_copy( ) / symbol.left.right.new_copy() found_t = True # Update the events if the modulo function depended on t if found_t: if t_eval is None: N_events = 200 else: N_events = t_eval[-1] // expr.value for i in np.arange(N_events): model.events.append( pybamm.Event( str(symbol), expr.new_copy() * pybamm.Scalar(i + 1), pybamm.EventType.DISCONTINUITY, )) # Process initial conditions initial_conditions = process( model.concatenated_initial_conditions, "initial_conditions", use_jacobian=False, )[0] init_eval = InitialConditions(initial_conditions, model) # Process rhs, algebraic and event expressions rhs, rhs_eval, jac_rhs = process(model.concatenated_rhs, "RHS") algebraic, algebraic_eval, jac_algebraic = process( model.concatenated_algebraic, "algebraic") terminate_events_eval = [ process(event.expression, "event", use_jacobian=False)[1] for event in model.events if event.event_type == pybamm.EventType.TERMINATION ] # discontinuity events are evaluated before the solver is called, so don't need # to process them discontinuity_events_eval = [ event for event in model.events if event.event_type == pybamm.EventType.DISCONTINUITY ] # Add the solver attributes model.init_eval = init_eval model.rhs_eval = rhs_eval model.algebraic_eval = algebraic_eval model.jac_algebraic_eval = jac_algebraic model.terminate_events_eval = terminate_events_eval model.discontinuity_events_eval = discontinuity_events_eval # Calculate initial conditions model.y0 = init_eval(inputs) # Save CasADi functions for the CasADi solver # Note: when we pass to casadi the ode part of the problem must be in explicit # form so we pre-multiply by the inverse of the mass matrix if isinstance( self.root_method, pybamm.CasadiAlgebraicSolver) or isinstance( self, (pybamm.CasadiSolver, pybamm.CasadiAlgebraicSolver)): # can use DAE solver to solve model with algebraic equations only if len(model.rhs) > 0: mass_matrix_inv = casadi.MX(model.mass_matrix_inv.entries) explicit_rhs = mass_matrix_inv @ rhs(t_casadi, y_casadi, p_casadi_stacked) model.casadi_rhs = casadi.Function( "rhs", [t_casadi, y_casadi, p_casadi_stacked], [explicit_rhs]) model.casadi_algebraic = algebraic if len(model.rhs) == 0: # No rhs equations: residuals is algebraic only model.residuals_eval = Residuals(algebraic, "residuals", model) model.jacobian_eval = jac_algebraic elif len(model.algebraic) == 0: # No algebraic equations: residuals is rhs only model.residuals_eval = Residuals(rhs, "residuals", model) model.jacobian_eval = jac_rhs # Calculate consistent initial conditions for the algebraic equations else: all_states = pybamm.NumpyConcatenation( model.concatenated_rhs, model.concatenated_algebraic) # Process again, uses caching so should be quick residuals_eval, jacobian_eval = process(all_states, "residuals")[1:] model.residuals_eval = residuals_eval model.jacobian_eval = jacobian_eval pybamm.logger.info("Finish solver set-up")
def test_leading_order_convergence(self): """ Check that the leading-order model solution converges linearly in C_e to the full model solution """ # Create models leading_order_model = pybamm.lead_acid.LOQS() composite_model = pybamm.lead_acid.Composite() full_model = pybamm.lead_acid.Full() # Same parameters, same geometry parameter_values = full_model.default_parameter_values parameter_values["Current function [A]"] = "[input]" parameter_values.process_model(leading_order_model) parameter_values.process_model(composite_model) parameter_values.process_model(full_model) geometry = full_model.default_geometry parameter_values.process_geometry(geometry) # Discretise (same mesh, create different discretisations) var = pybamm.standard_spatial_vars var_pts = {var.x_n: 3, var.x_s: 3, var.x_p: 3} mesh = pybamm.Mesh(geometry, full_model.default_submesh_types, var_pts) method_options = {"extrapolation": {"order": "linear", "use bcs": False}} spatial_methods = { "macroscale": pybamm.FiniteVolume(method_options), "current collector": pybamm.ZeroDimensionalMethod(method_options), } loqs_disc = pybamm.Discretisation(mesh, spatial_methods) loqs_disc.process_model(leading_order_model) comp_disc = pybamm.Discretisation(mesh, spatial_methods) comp_disc.process_model(composite_model) full_disc = pybamm.Discretisation(mesh, spatial_methods) full_disc.process_model(full_model) def get_max_error(current): pybamm.logger.info("current = {}".format(current)) # Solve, make sure times are the same and use tight tolerances t_eval = np.linspace(0, 3600 * 17 / current) solver = pybamm.CasadiSolver() solver.rtol = 1e-8 solver.atol = 1e-8 solution_loqs = solver.solve( leading_order_model, t_eval, inputs={"Current function [A]": current} ) solution_comp = solver.solve( composite_model, t_eval, inputs={"Current function [A]": current} ) solution_full = solver.solve( full_model, t_eval, inputs={"Current function [A]": current} ) # Post-process variables voltage_loqs = solution_loqs["Terminal voltage"] voltage_comp = solution_comp["Terminal voltage"] voltage_full = solution_full["Terminal voltage"] # Compare t_loqs = solution_loqs.t t_comp = solution_comp.t t_full = solution_full.t t = t_full[: np.min([len(t_loqs), len(t_comp), len(t_full)])] loqs_error = np.max(np.abs(voltage_loqs(t) - voltage_full(t))) comp_error = np.max(np.abs(voltage_comp(t) - voltage_full(t))) return (loqs_error, comp_error) # Get errors currents = 0.5 / (2 ** np.arange(3)) errs = np.array([get_max_error(current) for current in currents]) loqs_errs, comp_errs = [np.array(err) for err in zip(*errs)] # Get rates: expect linear convergence for loqs, quadratic for composite loqs_rates = np.log2(loqs_errs[:-1] / loqs_errs[1:]) np.testing.assert_array_less(0.99 * np.ones_like(loqs_rates), loqs_rates) # Composite not converging as expected comp_rates = np.log2(comp_errs[:-1] / comp_errs[1:]) np.testing.assert_array_less(0.99 * np.ones_like(comp_rates), comp_rates) # Check composite more accurate than loqs np.testing.assert_array_less(comp_errs, loqs_errs)
def test_extrapolate_2d_models(self): # create discretisation mesh = get_p2d_mesh_for_testing() method_options = { "extrapolation": { "order": "linear", "use bcs": False } } spatial_methods = { "macroscale": pybamm.FiniteVolume(method_options), "negative particle": pybamm.FiniteVolume(method_options), "positive particle": pybamm.FiniteVolume(method_options), "current collector": pybamm.FiniteVolume(method_options), } disc = pybamm.Discretisation(mesh, spatial_methods) # Microscale var = pybamm.Variable("var", domain="negative particle") extrap_right = pybamm.BoundaryValue(var, "right") disc.set_variable_slices([var]) extrap_right_disc = disc.process_symbol(extrap_right) self.assertEqual(extrap_right_disc.domain, []) # domain for boundary values must now be explicitly set extrap_right.domain = ["negative electrode"] disc.set_variable_slices([var]) extrap_right_disc = disc.process_symbol(extrap_right) self.assertEqual(extrap_right_disc.domain, ["negative electrode"]) # evaluate y_macro = mesh["negative electrode"][0].nodes y_micro = mesh["negative particle"][0].nodes y = np.outer(y_macro, y_micro).reshape(-1, 1) # extrapolate to r=1 --> should evaluate to y_macro np.testing.assert_array_almost_equal( extrap_right_disc.evaluate(y=y)[:, 0], y_macro) var = pybamm.Variable("var", domain="positive particle") extrap_right = pybamm.BoundaryValue(var, "right") disc.set_variable_slices([var]) extrap_right_disc = disc.process_symbol(extrap_right) self.assertEqual(extrap_right_disc.domain, []) # domain for boundary values must now be explicitly set extrap_right.domain = ["positive electrode"] disc.set_variable_slices([var]) extrap_right_disc = disc.process_symbol(extrap_right) self.assertEqual(extrap_right_disc.domain, ["positive electrode"]) # 2d macroscale mesh = get_1p1d_mesh_for_testing() disc = pybamm.Discretisation(mesh, spatial_methods) var = pybamm.Variable("var", domain="negative electrode") extrap_right = pybamm.BoundaryValue(var, "right") disc.set_variable_slices([var]) extrap_right_disc = disc.process_symbol(extrap_right) self.assertEqual(extrap_right_disc.domain, []) # test extrapolate to "negative tab" gives same as "left" and # "positive tab" gives same "right" (see get_mesh_for_testing) var = pybamm.Variable("var", domain="current collector") disc.set_variable_slices([var]) submesh = mesh["current collector"] constant_y = np.ones_like(submesh[0].nodes[:, np.newaxis]) extrap_neg = pybamm.BoundaryValue(var, "negative tab") extrap_neg_disc = disc.process_symbol(extrap_neg) extrap_left = pybamm.BoundaryValue(var, "left") extrap_left_disc = disc.process_symbol(extrap_left) np.testing.assert_array_equal( extrap_neg_disc.evaluate(None, constant_y), extrap_left_disc.evaluate(None, constant_y), ) extrap_pos = pybamm.BoundaryValue(var, "positive tab") extrap_pos_disc = disc.process_symbol(extrap_pos) extrap_right = pybamm.BoundaryValue(var, "right") extrap_right_disc = disc.process_symbol(extrap_right) np.testing.assert_array_equal( extrap_pos_disc.evaluate(None, constant_y), extrap_right_disc.evaluate(None, constant_y), )
), "separator": pybamm.MeshGenerator( pybamm.SpectralVolume1DSubMesh, {"order": order} ), "positive electrode": pybamm.MeshGenerator( pybamm.SpectralVolume1DSubMesh, {"order": order} ), "current collector": pybamm.SubMesh0D, }, var_pts, ) for geometry in geometries ] # discretise model disc_fv = pybamm.Discretisation(meshes[0], models[0].default_spatial_methods) disc_sv = pybamm.Discretisation( meshes[1], { "negative particle": pybamm.SpectralVolume(order=order), "positive particle": pybamm.SpectralVolume(order=order), "negative electrode": pybamm.SpectralVolume(order=order), "separator": pybamm.SpectralVolume(order=order), "positive electrode": pybamm.SpectralVolume(order=order), "current collector": pybamm.ZeroDimensionalSpatialMethod(), }, ) disc_fv.process_model(models[0]) disc_sv.process_model(models[1])
def test_spherical_grad_div_shapes_Dirichlet_bcs(self): """ Test grad and div with Dirichlet boundary conditions (applied by grad on var) """ # create discretisation mesh = get_1p1d_mesh_for_testing() spatial_methods = {"negative particle": pybamm.SpectralVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) submesh = mesh["negative particle"] # grad # grad(r) == 1 var = pybamm.Variable( "var", domain=["negative particle"], auxiliary_domains={ "secondary": "negative electrode", "tertiary": "current collector", }, ) grad_eqn = pybamm.grad(var) boundary_conditions = { var.id: { "left": (pybamm.Scalar(1), "Dirichlet"), "right": (pybamm.Scalar(1), "Dirichlet"), } } disc.bcs = boundary_conditions disc.set_variable_slices([var]) grad_eqn_disc = disc.process_symbol(grad_eqn) total_npts = (submesh.npts * mesh["negative electrode"].npts * mesh["current collector"].npts) total_npts_edges = ((submesh.npts + 1) * mesh["negative electrode"].npts * mesh["current collector"].npts) constant_y = np.ones((total_npts, 1)) np.testing.assert_array_equal(grad_eqn_disc.evaluate(None, constant_y), np.zeros((total_npts_edges, 1))) boundary_conditions = { var.id: { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(1), "Dirichlet"), } } disc.bcs = boundary_conditions y_linear = np.tile( submesh.nodes, mesh["negative electrode"].npts * mesh["current collector"].npts, ) grad_eqn_disc = disc.process_symbol(grad_eqn) np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, y_linear), np.ones((total_npts_edges, 1))) # div: test on linear r^2 # div (grad r^2) = 6 const = 6 * np.ones((total_npts, 1)) N = pybamm.grad(var) div_eqn = pybamm.div(N) boundary_conditions = { var.id: { "left": (pybamm.Scalar(6), "Dirichlet"), "right": (pybamm.Scalar(6), "Dirichlet"), } } disc.bcs = boundary_conditions div_eqn_disc = disc.process_symbol(div_eqn) np.testing.assert_array_almost_equal( div_eqn_disc.evaluate(None, const), np.zeros(( submesh.npts * mesh["negative electrode"].npts * mesh["current collector"].npts, 1, )), )
def __init__(self, e_class): self.dim_dict = e_class.dim_dict self.nd_dict = e_class.nd_param.nd_param_dict self.model = pybamm.BaseModel() self.parameter_dict = {} self.pybam_val_dict = {} self.simulation_options = e_class.simulation_options self.dim_keys = self.nd_dict.keys() self.time = e_class.time_vec for key in self.dim_keys: self.parameter_dict[key] = pybamm.InputParameter(key) self.pybam_val_dict[key] = None self.current = pybamm.Variable("current") self.theta = pybamm.Variable("theta") if self.simulation_options["method"] == "dcv": Edc_forward = pybamm.t Edc_backwards = -(pybamm.t - 2 * self.parameter_dict["tr"]) E_t = self.parameter_dict["E_start"]+ \ (pybamm.t <= self.parameter_dict["tr"]) * Edc_forward + \ (pybamm.t > self.parameter_dict["tr"]) * Edc_backwards elif self.simulation_options["method"] == "sinusoidal": E_t = self.parameter_dict["E_start"] + self.parameter_dict[ "d_E"] + (self.parameter_dict["d_E"] * pybamm.sin( (self.parameter_dict["nd_omega"] * pybamm.t) + self.parameter_dict["phase"])) elif self.simulation_options["method"] == "ramped": Edc_forward = pybamm.t Edc_backwards = -(pybamm.t - 2 * self.parameter_dict["tr"]) E_t = self.parameter_dict["E_start"]+ \ (pybamm.t <= self.parameter_dict["tr"]) * Edc_forward + \ (pybamm.t > self.parameter_dict["tr"]) * Edc_backwards+\ (self.parameter_dict["d_E"]*pybamm.sin((self.parameter_dict["nd_omega"]*pybamm.t)+self.parameter_dict["phase"])) Er = E_t - (self.parameter_dict["Ru"] * self.current) ErE0 = Er - self.parameter_dict["E_0"] alpha = self.parameter_dict["alpha"] Cdlp = self.parameter_dict["Cdl"] * ( 1 + self.parameter_dict["CdlE1"] * Er + self.parameter_dict["CdlE2"] * (Er**2) + self.parameter_dict["CdlE3"] * (Er**3)) if "Cdlinv" not in e_class.optim_list: Cdlp = self.parameter_dict["Cdl"] * ( 1 + self.parameter_dict["CdlE1"] * Er + self.parameter_dict["CdlE2"] * (Er**2) + self.parameter_dict["CdlE3"] * (Er**3)) else: Cdlp=(pybamm.t <= self.parameter_dict["tr"]) *(self.parameter_dict["Cdl"]*(1+self.parameter_dict["CdlE1"]*Er+self.parameter_dict["CdlE2"]*(Er**2)+self.parameter_dict["CdlE3"]*(Er**3)))+\ (pybamm.t > self.parameter_dict["tr"]) *(self.parameter_dict["Cdlinv"]*(1+self.parameter_dict["CdlE1inv"]*Er+self.parameter_dict["CdlE2inv"]*(Er**2)+self.parameter_dict["CdlE3inv"]*(Er**3))) self.model.variables = {"current": self.current, "theta": self.theta} d_thetadt = ( (1 - self.theta) * self.parameter_dict["k_0"] * pybamm.exp( (1 - alpha) * ErE0)) - ( self.theta * self.parameter_dict["k_0"] * pybamm.exp( (-alpha) * ErE0)) dIdt = (E_t.diff(pybamm.t) - (self.current / Cdlp) + self.parameter_dict["gamma"] * d_thetadt * (1 / Cdlp)) / self.parameter_dict["Ru"] self.model.rhs = {self.current: dIdt, self.theta: d_thetadt} self.disc = pybamm.Discretisation() self.model.initial_conditions = { self.theta: pybamm.Scalar(1), self.current: pybamm.Scalar(0) } self.disc.process_model(self.model)
def test_grad_div_shapes_Dirichlet_and_Neumann_bcs(self): """ Test grad and div with Dirichlet boundary conditions (applied by grad on c) on one side and Neumann boundary conditions (applied by div on N) on the other """ whole_cell = ["negative electrode", "separator", "positive electrode"] # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.SpectralVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) combined_submesh = mesh.combine_submeshes(*whole_cell) # grad var = pybamm.Variable("var", domain=whole_cell) grad_eqn = pybamm.grad(var) disc.set_variable_slices([var]) # div N = pybamm.grad(var) div_eqn = pybamm.div(N) boundary_conditions = { var.id: { "left": (pybamm.Scalar(1), "Dirichlet"), "right": (pybamm.Scalar(0), "Neumann"), } } disc.bcs = boundary_conditions grad_eqn_disc = disc.process_symbol(grad_eqn) div_eqn_disc = disc.process_symbol(div_eqn) # Constant y should have gradient and laplacian zero constant_y = np.ones_like(combined_submesh.nodes[:, np.newaxis]) np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, constant_y), np.zeros_like(combined_submesh.edges[:, np.newaxis]), ) np.testing.assert_array_almost_equal( div_eqn_disc.evaluate(None, constant_y), np.zeros_like(combined_submesh.nodes[:, np.newaxis]), ) boundary_conditions = { var.id: { "left": (pybamm.Scalar(1), "Neumann"), "right": (pybamm.Scalar(1), "Dirichlet"), } } disc.bcs = boundary_conditions grad_eqn_disc = disc.process_symbol(grad_eqn) div_eqn_disc = disc.process_symbol(div_eqn) # Linear y should have gradient one and laplacian zero linear_y = combined_submesh.nodes np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, linear_y), np.ones_like(combined_submesh.edges[:, np.newaxis]), ) np.testing.assert_array_almost_equal( div_eqn_disc.evaluate(None, linear_y), np.zeros_like(combined_submesh.nodes[:, np.newaxis]), )
def test_compare_outputs_thermal(self): # load models - for the default params we expect x-full and lumped to # agree as the temperature is practically independent of x options = [{"thermal": opt} for opt in ["lumped", "x-full"]] options.append({"thermal": "lumped", "cell_geometry": "pouch"}) model_combos = [ ([pybamm.lithium_ion.SPM(opt) for opt in options]), ([pybamm.lithium_ion.SPMe(opt) for opt in options]), ([pybamm.lithium_ion.DFN(opt) for opt in options]), ] for models in model_combos: # load parameter values (same for all models) param = models[0].default_parameter_values # for x-full, cooling is only implemented on the surfaces # so set other forms of cooling to zero for comparison. param.update({ "Negative current collector" + " surface heat transfer coefficient [W.m-2.K-1]": 5, "Positive current collector" + " surface heat transfer coefficient [W.m-2.K-1]": 5, "Negative tab heat transfer coefficient [W.m-2.K-1]": 0, "Positive tab heat transfer coefficient [W.m-2.K-1]": 0, "Edge heat transfer coefficient [W.m-2.K-1]": 0, }) for model in models: param.process_model(model) # set mesh var = pybamm.standard_spatial_vars var_pts = { var.x_n: 5, var.x_s: 5, var.x_p: 5, var.r_n: 5, var.r_p: 5 } # discretise models discs = {} for model in models: geometry = model.default_geometry param.process_geometry(geometry) mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) discs[model] = disc # solve model solutions = [] t_eval = np.linspace(0, 3600, 100) for model in models: solution = pybamm.CasadiSolver().solve(model, t_eval) solutions.append(solution) # compare outputs comparison = StandardOutputComparison(solutions) comparison.test_all(skip_first_timestep=True)
def test_simple_ode_model(self): model = pybamm.BaseBatteryModel(name="Simple ODE Model") whole_cell = ["negative electrode", "separator", "positive electrode"] # Create variables: domain is explicitly empty since these variables are only # functions of time a = pybamm.Variable("a", domain=[]) b = pybamm.Variable("b", domain=[]) c = pybamm.Variable("c", domain=[]) # Simple ODEs model.rhs = {a: pybamm.Scalar(2), b: pybamm.Scalar(0), c: -c} # Simple initial conditions model.initial_conditions = { a: pybamm.Scalar(0), b: pybamm.Scalar(1), c: pybamm.Scalar(1), } # no boundary conditions for an ODE model # Broadcast some of the variables model.variables = { "a": a, "b broadcasted": pybamm.FullBroadcast(b, whole_cell, "current collector"), "c broadcasted": pybamm.FullBroadcast(c, ["negative electrode", "separator"], "current collector"), "b broadcasted negative electrode": pybamm.PrimaryBroadcast(b, "negative particle"), "c broadcasted positive electrode": pybamm.PrimaryBroadcast(c, "positive particle"), "x [m]": pybamm.standard_spatial_vars.x, "x": pybamm.standard_spatial_vars.x, "r_n [m]": pybamm.standard_spatial_vars.r_n, "r_n": pybamm.standard_spatial_vars.r_n, "r_p [m]": pybamm.standard_spatial_vars.r_p, "r_p": pybamm.standard_spatial_vars.r_p, } # ODEs only (don't use jacobian) model.use_jacobian = False # Process and solve geometry = model.default_geometry param = model.default_parameter_values param.process_model(model) param.process_geometry(geometry) mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) solver = model.default_solver t_eval = np.linspace(0, 2, 100) solution = solver.solve(model, t_eval) quick_plot = pybamm.QuickPlot( solution, [ "a", "b broadcasted", "c broadcasted", "b broadcasted negative electrode", "c broadcasted positive electrode", ], ) quick_plot.plot(0) # update the axis new_axis = [0, 0.5, 0, 1] quick_plot.axis_limits.update({("a", ): new_axis}) self.assertEqual(quick_plot.axis_limits[("a", )], new_axis) # and now reset them quick_plot.reset_axis() self.assertNotEqual(quick_plot.axis_limits[("a", )], new_axis) # check dynamic plot loads quick_plot.dynamic_plot(testing=True) quick_plot.slider_update(0.01) # Test with different output variables quick_plot = pybamm.QuickPlot(solution, ["b broadcasted"]) self.assertEqual(len(quick_plot.axis_limits), 1) quick_plot.plot(0) quick_plot = pybamm.QuickPlot( solution, [ ["a", "a"], ["b broadcasted", "b broadcasted"], "c broadcasted", "b broadcasted negative electrode", "c broadcasted positive electrode", ], ) self.assertEqual(len(quick_plot.axis_limits), 5) quick_plot.plot(0) # update the axis new_axis = [0, 0.5, 0, 1] var_key = ("c broadcasted", ) quick_plot.axis_limits.update({var_key: new_axis}) self.assertEqual(quick_plot.axis_limits[var_key], new_axis) # and now reset them quick_plot.reset_axis() self.assertNotEqual(quick_plot.axis_limits[var_key], new_axis) # check dynamic plot loads quick_plot.dynamic_plot(testing=True) quick_plot.slider_update(0.01) # Test longer name model.variables["Variable with a very long name"] = model.variables[ "a"] quick_plot = pybamm.QuickPlot(solution, ["Variable with a very long name"]) quick_plot.plot(0) # Test different inputs quick_plot = pybamm.QuickPlot( [solution, solution], ["a"], colors=["r", "g", "b"], linestyles=["-", "--"], figsize=(1, 2), labels=["sol 1", "sol 2"], ) self.assertEqual(quick_plot.colors, ["r", "g", "b"]) self.assertEqual(quick_plot.linestyles, ["-", "--"]) self.assertEqual(quick_plot.figsize, (1, 2)) self.assertEqual(quick_plot.labels, ["sol 1", "sol 2"]) # Test different time units quick_plot = pybamm.QuickPlot(solution, ["a"]) self.assertEqual(quick_plot.time_scaling_factor, 1) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="seconds") quick_plot.plot(0) self.assertEqual(quick_plot.time_scaling_factor, 1) np.testing.assert_array_almost_equal( quick_plot.plots[("a", )][0][0].get_xdata(), t_eval) np.testing.assert_array_almost_equal( quick_plot.plots[("a", )][0][0].get_ydata(), 2 * t_eval) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="minutes") quick_plot.plot(0) self.assertEqual(quick_plot.time_scaling_factor, 60) np.testing.assert_array_almost_equal( quick_plot.plots[("a", )][0][0].get_xdata(), t_eval / 60) np.testing.assert_array_almost_equal( quick_plot.plots[("a", )][0][0].get_ydata(), 2 * t_eval) quick_plot = pybamm.QuickPlot(solution, ["a"], time_unit="hours") quick_plot.plot(0) self.assertEqual(quick_plot.time_scaling_factor, 3600) np.testing.assert_array_almost_equal( quick_plot.plots[("a", )][0][0].get_xdata(), t_eval / 3600) np.testing.assert_array_almost_equal( quick_plot.plots[("a", )][0][0].get_ydata(), 2 * t_eval) with self.assertRaisesRegex(ValueError, "time unit"): pybamm.QuickPlot(solution, ["a"], time_unit="bad unit") # long solution defaults to hours instead of seconds solution_long = solver.solve(model, np.linspace(0, 1e5)) quick_plot = pybamm.QuickPlot(solution_long, ["a"]) self.assertEqual(quick_plot.time_scaling_factor, 3600) # Test different spatial units quick_plot = pybamm.QuickPlot(solution, ["a"]) self.assertEqual(quick_plot.spatial_unit, "$\mu m$") quick_plot = pybamm.QuickPlot(solution, ["a"], spatial_unit="m") self.assertEqual(quick_plot.spatial_unit, "m") quick_plot = pybamm.QuickPlot(solution, ["a"], spatial_unit="mm") self.assertEqual(quick_plot.spatial_unit, "mm") quick_plot = pybamm.QuickPlot(solution, ["a"], spatial_unit="um") self.assertEqual(quick_plot.spatial_unit, "$\mu m$") with self.assertRaisesRegex(ValueError, "spatial unit"): pybamm.QuickPlot(solution, ["a"], spatial_unit="bad unit") # Test 2D variables model.variables["2D variable"] = disc.process_symbol( pybamm.FullBroadcast(1, "negative particle", {"secondary": "negative electrode"})) quick_plot = pybamm.QuickPlot(solution, ["2D variable"]) quick_plot.plot(0) quick_plot.dynamic_plot(testing=True) quick_plot.slider_update(0.01) with self.assertRaisesRegex(NotImplementedError, "Cannot plot 2D variables"): pybamm.QuickPlot([solution, solution], ["2D variable"]) # Test different variable limits quick_plot = pybamm.QuickPlot( solution, ["a", ["c broadcasted", "c broadcasted"]], variable_limits="tight") self.assertEqual(quick_plot.axis_limits[("a", )][2:], [None, None]) self.assertEqual( quick_plot.axis_limits[("c broadcasted", "c broadcasted")][2:], [None, None]) quick_plot.plot(0) quick_plot.slider_update(1) quick_plot = pybamm.QuickPlot(solution, ["2D variable"], variable_limits="tight") self.assertEqual(quick_plot.variable_limits[("2D variable", )], (None, None)) quick_plot.plot(0) quick_plot.slider_update(1) quick_plot = pybamm.QuickPlot( solution, ["a", ["c broadcasted", "c broadcasted"]], variable_limits={ "a": [1, 2], ("c broadcasted", "c broadcasted"): [3, 4] }, ) self.assertEqual(quick_plot.axis_limits[("a", )][2:], [1, 2]) self.assertEqual( quick_plot.axis_limits[("c broadcasted", "c broadcasted")][2:], [3, 4]) quick_plot.plot(0) quick_plot.slider_update(1) quick_plot = pybamm.QuickPlot(solution, ["a", "b broadcasted"], variable_limits={"a": "tight"}) self.assertEqual(quick_plot.axis_limits[("a", )][2:], [None, None]) self.assertNotEqual(quick_plot.axis_limits[("b broadcasted", )][2:], [None, None]) quick_plot.plot(0) quick_plot.slider_update(1) with self.assertRaisesRegex( TypeError, "variable_limits must be 'fixed', 'tight', or a dict"): pybamm.QuickPlot(solution, ["a", "b broadcasted"], variable_limits="bad variable limits") # Test errors with self.assertRaisesRegex(ValueError, "Mismatching variable domains"): pybamm.QuickPlot(solution, [["a", "b broadcasted"]]) with self.assertRaisesRegex(ValueError, "labels"): pybamm.QuickPlot([solution, solution], ["a"], labels=["sol 1", "sol 2", "sol 3"]) # Remove 'x [m]' from the variables and make sure a key error is raise del solution.model.variables["x [m]"] with self.assertRaisesRegex( KeyError, "Can't find spatial scale for 'negative electrode'"): pybamm.QuickPlot(solution, ["b broadcasted"]) # No variable can be NaN model.variables["NaN variable"] = disc.process_symbol( pybamm.Scalar(np.nan)) with self.assertRaisesRegex( ValueError, "All-NaN variable 'NaN variable' provided"): pybamm.QuickPlot(solution, ["NaN variable"])
# mention = api.mentions_timeline() # Setting up a PyBaMM example model = pybamm.BaseModel() x = pybamm.Variable("x") y = pybamm.Variable("y") dxdt = 4 * x - 2 * y dydt = 3 * x - y model.rhs = {x: dxdt, y: dydt} model.initial_conditions = {x: pybamm.Scalar(1), y: pybamm.Scalar(2)} model.variables = {"x": x, "y": y, "z": x + 4 * y} disc = pybamm.Discretisation() # use the default discretisation disc.process_model(model) solver = pybamm.ScipySolver() t = np.linspace(0, 1, 20) solution = solver.solve(model, t) t_sol, y_sol = solution.t, solution.y # get solution times and states x = solution["x"] # extract and process x from the solution y = solution["y"] # extract and process y from the solution t_fine = np.linspace(0, t[-1], 1000) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4)) ax1.plot(t_fine, 2 * np.exp(t_fine) - np.exp(2 * t_fine), t_sol, x(t_sol), "o") ax1.set_xlabel("t")
def test_discretise_slicing(self): # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) whole_cell = ["negative electrode", "separator", "positive electrode"] c = pybamm.Variable("c", domain=whole_cell) variables = [c] disc.set_variable_slices(variables) self.assertEqual(disc.y_slices, {c.id: [slice(0, 100)]}) combined_submesh = mesh.combine_submeshes(*whole_cell) c_true = combined_submesh.nodes ** 2 y = c_true np.testing.assert_array_equal(y[disc.y_slices[c.id][0]], c_true) # Several variables d = pybamm.Variable("d", domain=whole_cell) jn = pybamm.Variable("jn", domain=["negative electrode"]) variables = [c, d, jn] disc.set_variable_slices(variables) self.assertEqual( disc.y_slices, {c.id: [slice(0, 100)], d.id: [slice(100, 200)], jn.id: [slice(200, 240)]}, ) d_true = 4 * combined_submesh.nodes jn_true = mesh["negative electrode"].nodes ** 3 y = np.concatenate([c_true, d_true, jn_true]) np.testing.assert_array_equal(y[disc.y_slices[c.id][0]], c_true) np.testing.assert_array_equal(y[disc.y_slices[d.id][0]], d_true) np.testing.assert_array_equal(y[disc.y_slices[jn.id][0]], jn_true) # Variables with a concatenation js = pybamm.Variable("js", domain=["separator"]) jp = pybamm.Variable("jp", domain=["positive electrode"]) j = pybamm.Concatenation(jn, js, jp) variables = [c, d, j] disc.set_variable_slices(variables) self.assertEqual( disc.y_slices, { c.id: [slice(0, 100)], d.id: [slice(100, 200)], jn.id: [slice(200, 240)], js.id: [slice(240, 265)], jp.id: [slice(265, 300)], }, ) d_true = 4 * combined_submesh.nodes jn_true = mesh["negative electrode"].nodes ** 3 y = np.concatenate([c_true, d_true, jn_true]) np.testing.assert_array_equal(y[disc.y_slices[c.id][0]], c_true) np.testing.assert_array_equal(y[disc.y_slices[d.id][0]], d_true) np.testing.assert_array_equal(y[disc.y_slices[jn.id][0]], jn_true) with self.assertRaisesRegex(TypeError, "y_slices should be"): disc.y_slices = 1
def test_p2d_add_ghost_nodes(self): # create discretisation mesh = get_p2d_mesh_for_testing() spatial_methods = { "macroscale": pybamm.FiniteVolume(), "negative particle": pybamm.FiniteVolume(), "positive particle": pybamm.FiniteVolume(), } disc = pybamm.Discretisation(mesh, spatial_methods) # add ghost nodes c_s_n = pybamm.Variable("c_s_n", domain=["negative particle"]) c_s_p = pybamm.Variable("c_s_p", domain=["positive particle"]) disc.set_variable_slices([c_s_n]) disc_c_s_n = pybamm.StateVector(*disc.y_slices[c_s_n.id]) disc.set_variable_slices([c_s_p]) disc_c_s_p = pybamm.StateVector(*disc.y_slices[c_s_p.id]) bcs = { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(3), "Dirichlet"), } sp_meth = pybamm.FiniteVolume() sp_meth.build(mesh) c_s_n_plus_ghost, _ = sp_meth.add_ghost_nodes(c_s_n, disc_c_s_n, bcs) c_s_p_plus_ghost, _ = sp_meth.add_ghost_nodes(c_s_p, disc_c_s_p, bcs) mesh_s_n = mesh["negative particle"] mesh_s_p = mesh["positive particle"] n_prim_pts = mesh_s_n[0].npts n_sec_pts = len(mesh_s_n) p_prim_pts = mesh_s_p[0].npts p_sec_pts = len(mesh_s_p) y_s_n_test = np.kron(np.ones(n_sec_pts), np.ones(n_prim_pts)) y_s_p_test = np.kron(np.ones(p_sec_pts), np.ones(p_prim_pts)) # evaluate with and without ghost points c_s_n_eval = disc_c_s_n.evaluate(None, y_s_n_test) c_s_n_ghost_eval = c_s_n_plus_ghost.evaluate(None, y_s_n_test) c_s_p_eval = disc_c_s_p.evaluate(None, y_s_p_test) c_s_p_ghost_eval = c_s_p_plus_ghost.evaluate(None, y_s_p_test) # reshape to make easy to deal with c_s_n_eval = np.reshape(c_s_n_eval, [n_sec_pts, n_prim_pts]) c_s_n_ghost_eval = np.reshape(c_s_n_ghost_eval, [n_sec_pts, n_prim_pts + 2]) c_s_p_eval = np.reshape(c_s_p_eval, [p_sec_pts, p_prim_pts]) c_s_p_ghost_eval = np.reshape(c_s_p_ghost_eval, [p_sec_pts, p_prim_pts + 2]) np.testing.assert_array_equal(c_s_n_ghost_eval[:, 1:-1], c_s_n_eval) np.testing.assert_array_equal(c_s_p_ghost_eval[:, 1:-1], c_s_p_eval) np.testing.assert_array_equal( (c_s_n_ghost_eval[:, 0] + c_s_n_ghost_eval[:, 1]) / 2, 0) np.testing.assert_array_equal( (c_s_p_ghost_eval[:, 0] + c_s_p_ghost_eval[:, 1]) / 2, 0) np.testing.assert_array_equal( (c_s_n_ghost_eval[:, -2] + c_s_n_ghost_eval[:, -1]) / 2, 3) np.testing.assert_array_equal( (c_s_p_ghost_eval[:, -2] + c_s_p_ghost_eval[:, -1]) / 2, 3)
def test_no_mesh(self): disc = pybamm.Discretisation(None, None) self.assertEqual(disc._spatial_methods, {})
def test_1D_different_domains(self): # Negative electrode domain var = pybamm.Variable("var", domain=["negative electrode"]) x = pybamm.SpatialVariable("x", domain=["negative electrode"]) disc = tests.get_discretisation_for_testing() disc.set_variable_slices([var]) x_sol = disc.process_symbol(x).entries[:, 0] var_sol = disc.process_symbol(var) t_sol = [0] y_sol = np.ones_like(x_sol)[:, np.newaxis] * 5 sol = pybamm.Solution(t_sol, y_sol) pybamm.ProcessedSymbolicVariable(var_sol, sol) # Particle domain var = pybamm.Variable("var", domain=["negative particle"]) r = pybamm.SpatialVariable("r", domain=["negative particle"]) disc = tests.get_discretisation_for_testing() disc.set_variable_slices([var]) r_sol = disc.process_symbol(r).entries[:, 0] var_sol = disc.process_symbol(var) t_sol = [0] y_sol = np.ones_like(r_sol)[:, np.newaxis] * 5 sol = pybamm.Solution(t_sol, y_sol) pybamm.ProcessedSymbolicVariable(var_sol, sol) # Current collector domain var = pybamm.Variable("var", domain=["current collector"]) z = pybamm.SpatialVariable("z", domain=["current collector"]) disc = tests.get_1p1d_discretisation_for_testing() disc.set_variable_slices([var]) z_sol = disc.process_symbol(z).entries[:, 0] var_sol = disc.process_symbol(var) t_sol = [0] y_sol = np.ones_like(z_sol)[:, np.newaxis] * 5 sol = pybamm.Solution(t_sol, y_sol) pybamm.ProcessedSymbolicVariable(var_sol, sol) # Other domain var = pybamm.Variable("var", domain=["line"]) x = pybamm.SpatialVariable("x", domain=["line"]) geometry = pybamm.Geometry( {"line": {x: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}}} ) submesh_types = {"line": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh)} var_pts = {x: 10} mesh = pybamm.Mesh(geometry, submesh_types, var_pts) disc = pybamm.Discretisation(mesh, {"line": pybamm.FiniteVolume()}) disc.set_variable_slices([var]) x_sol = disc.process_symbol(x).entries[:, 0] var_sol = disc.process_symbol(var) t_sol = [0] y_sol = np.ones_like(x_sol)[:, np.newaxis] * 5 sol = pybamm.Solution(t_sol, y_sol) pybamm.ProcessedSymbolicVariable(var_sol, sol) # 2D fails var = pybamm.Variable( "var", domain=["negative particle"], auxiliary_domains={"secondary": "negative electrode"}, ) r = pybamm.SpatialVariable( "r", domain=["negative particle"], auxiliary_domains={"secondary": "negative electrode"}, ) disc = tests.get_p2d_discretisation_for_testing() disc.set_variable_slices([var]) r_sol = disc.process_symbol(r).entries[:, 0] var_sol = disc.process_symbol(var) t_sol = [0] y_sol = np.ones_like(r_sol)[:, np.newaxis] * 5 sol = pybamm.Solution(t_sol, y_sol) with self.assertRaisesRegex(NotImplementedError, "Shape not recognized"): pybamm.ProcessedSymbolicVariable(var_sol, sol)
def test_model_solver_events(self): # Create model model = pybamm.BaseModel() whole_cell = ["negative electrode", "separator", "positive electrode"] var1 = pybamm.Variable("var1", domain=whole_cell) var2 = pybamm.Variable("var2", domain=whole_cell) model.rhs = {var1: 0.1 * var1} model.algebraic = {var2: 2 * var1 - var2} model.initial_conditions = {var1: 1, var2: 2} model.events = [ pybamm.Event("var1 = 1.5", pybamm.min(var1 - 1.5)), pybamm.Event("var2 = 2.5", pybamm.min(var2 - 2.5)), ] disc = get_discretisation_for_testing() disc.process_model(model) # Solve using "safe" mode solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8) t_eval = np.linspace(0, 5, 100) solution = solver.solve(model, t_eval) np.testing.assert_array_less(solution.y[0], 1.5) np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) np.testing.assert_array_almost_equal(solution.y[0], np.exp(0.1 * solution.t), decimal=5) np.testing.assert_array_almost_equal(solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5) # Solve using "safe" mode with debug off pybamm.settings.debug_mode = False solver = pybamm.CasadiSolver(mode="safe", rtol=1e-8, atol=1e-8, dt_max=1) t_eval = np.linspace(0, 5, 100) solution = solver.solve(model, t_eval) np.testing.assert_array_less(solution.y[0], 1.5) np.testing.assert_array_less(solution.y[-1], 2.5 + 1e-10) # test the last entry is exactly 2.5 np.testing.assert_array_almost_equal(solution.y[-1, -1], 2.5, decimal=2) np.testing.assert_array_almost_equal(solution.y[0], np.exp(0.1 * solution.t), decimal=5) np.testing.assert_array_almost_equal(solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5) pybamm.settings.debug_mode = True # Solve using "old safe" mode solver = pybamm.CasadiSolver(mode="old safe", rtol=1e-8, atol=1e-8) t_eval = np.linspace(0, 5, 100) solution = solver.solve(model, t_eval) np.testing.assert_array_less(solution.y[0], 1.5) np.testing.assert_array_less(solution.y[-1], 2.5) np.testing.assert_array_almost_equal(solution.y[0], np.exp(0.1 * solution.t), decimal=5) np.testing.assert_array_almost_equal(solution.y[-1], 2 * np.exp(0.1 * solution.t), decimal=5) # Test when an event returns nan model = pybamm.BaseModel() var = pybamm.Variable("var") model.rhs = {var: 0.1 * var} model.initial_conditions = {var: 1} model.events = [ pybamm.Event("event", var - 1.02), pybamm.Event("sqrt event", pybamm.sqrt(1.0199 - var)), ] disc = pybamm.Discretisation() disc.process_model(model) solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) solution = solver.solve(model, t_eval) np.testing.assert_array_less(solution.y[0], 1.02 + 1e-10) np.testing.assert_array_almost_equal(solution.y[0, -1], 1.02, decimal=2)
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)
# create geometry geometry = model.default_geometry # load parameter values and process model and geometry param = model.default_parameter_values 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} mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts) # discretise model disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) # solve model t_eval = np.linspace(0, 3600, 100) solver = pybamm.CasadiSolver(mode="fast", atol=1e-6, rtol=1e-3) solution = solver.solve(model, t_eval) # plot plot = pybamm.QuickPlot( solution, [ "Negative particle concentration [mol.m-3]", "Electrolyte concentration [mol.m-3]", "Positive particle concentration [mol.m-3]", "Current [A]",
def test_append_external_variables(self): model = pybamm.lithium_ion.SPM({ "thermal": "lumped", "external submodels": ["thermal", "negative particle"], }) # create geometry geometry = model.default_geometry # load parameter values and process model and geometry param = model.default_parameter_values param.process_model(model) param.process_geometry(geometry) # set mesh mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts) # discretise model disc = pybamm.Discretisation(mesh, model.default_spatial_methods) disc.process_model(model) # solve model solver = model.default_solver var = pybamm.standard_spatial_vars Nr = model.default_var_pts[var.r_n] T_av = 0 c_s_n_av = np.ones((Nr, 1)) * 0.6 external_variables = { "Volume-averaged cell temperature": T_av, "X-averaged negative particle concentration": c_s_n_av, } # Step dt = 0.1 sol_step = None for _ in range(5): sol_step = solver.step(sol_step, model, dt, external_variables=external_variables) np.testing.assert_array_equal( sol_step.inputs["Volume-averaged cell temperature"], np.zeros((1, len(sol_step.t))), ) np.testing.assert_array_equal( sol_step.inputs["X-averaged negative particle concentration"], np.ones((mesh["negative particle"].npts, len(sol_step.t))) * 0.6, ) # Solve t_eval = np.linspace(0, 3600) sol = solver.solve(model, t_eval, external_variables=external_variables) np.testing.assert_array_equal( sol.inputs["Volume-averaged cell temperature"], np.zeros((1, len(sol.t)))) np.testing.assert_array_equal( sol.inputs["X-averaged negative particle concentration"], np.ones((mesh["negative particle"].npts, len(sol.t))) * 0.6, )