def test_model_solver_python(self): # Create model model = pybamm.BaseModel() model.convert_to_format = "python" domain = ["negative electrode", "separator", "positive electrode"] var = pybamm.Variable("var", domain=domain) model.rhs = {var: 0.1 * var} model.initial_conditions = {var: 1} # No need to set parameters; can use base discretisation (no spatial operators) # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) # Solve solver = pybamm.ScipySolver(rtol=1e-8, atol=1e-8, method="RK45") t_eval = np.linspace(0, 1, 100) solution = solver.solve(model, t_eval) np.testing.assert_array_equal(solution.t, t_eval) np.testing.assert_allclose(solution.y[0], np.exp(0.1 * solution.t)) # Test time self.assertEqual(solution.total_time, solution.solve_time + solution.set_up_time) self.assertEqual(solution.termination, "final time")
def test_cartesian_div_convergence(self): whole_cell = ["negative electrode", "separator", "positive electrode"] spatial_methods = {"macroscale": pybamm.FiniteVolume()} # Function for convergence testing def get_error(n): # create mesh and discretisation mesh = get_mesh_for_testing(n) disc = pybamm.Discretisation(mesh, spatial_methods) combined_submesh = mesh.combine_submeshes(*whole_cell) x = combined_submesh[0].nodes x_edge = combined_submesh[0].edges # Define flux and bcs N = pybamm.Vector(x_edge**2 * np.cos(x_edge), domain=whole_cell) div_eqn = pybamm.div(N) # Define exact solutions # N = x**2 * cos(x) --> dNdx = x*(2cos(x) - xsin(x)) div_exact = x * (2 * np.cos(x) - x * np.sin(x)) # Discretise and evaluate div_eqn_disc = disc.process_symbol(div_eqn) div_approx = div_eqn_disc.evaluate() # Return difference between approx and exact return div_approx[:, 0] - div_exact # Get errors ns = 10 * 2**np.arange(6) errs = {n: get_error(int(n)) for n in ns} # expect quadratic convergence everywhere err_norm = np.array([np.linalg.norm(errs[n], np.inf) for n in ns]) rates = np.log2(err_norm[:-1] / err_norm[1:]) np.testing.assert_array_less(1.99 * np.ones_like(rates), rates)
def test_spherical_div_convergence_quadratic(self): # test div( r**2 * sin(r) ) == 4*r*sin(r) - r**2*cos(r) spatial_methods = {"negative particle": pybamm.FiniteVolume()} # Function for convergence testing def get_error(n): # create mesh and discretisation (single particle) mesh = get_mesh_for_testing(rpts=n) disc = pybamm.Discretisation(mesh, spatial_methods) submesh = mesh["negative particle"] r = submesh[0].nodes r_edge = submesh[0].edges # Define flux and bcs N = pybamm.Vector(r_edge**2 * np.sin(r_edge), domain=["negative particle"]) div_eqn = pybamm.div(N) # Define exact solutions # N = r**3 --> div(N) = 5 * r**2 div_exact = 4 * r * np.sin(r) + r**2 * np.cos(r) # Discretise and evaluate div_eqn_disc = disc.process_symbol(div_eqn) div_approx = div_eqn_disc.evaluate() # Return difference between approx and exact return div_approx[:, 0] - div_exact # Get errors ns = 10 * 2**np.arange(6) errs = {n: get_error(int(n)) for n in ns} # expect quadratic convergence everywhere err_norm = np.array([np.linalg.norm(errs[n], np.inf) for n in ns]) rates = np.log2(err_norm[:-1] / err_norm[1:]) np.testing.assert_array_less(1.99 * np.ones_like(rates), rates)
def test_mass_matrix_shape(self): """ Test mass matrix shape """ # one equation whole_cell = ["negative electrode", "separator", "positive electrode"] c = pybamm.Variable("c", domain=whole_cell) N = pybamm.grad(c) model = pybamm.BaseModel() model.rhs = {c: pybamm.div(N)} model.initial_conditions = {c: pybamm.Scalar(0)} model.boundary_conditions = { c: { "left": (0, "Dirichlet"), "right": (0, "Dirichlet") } } model.variables = {"c": c, "N": N} # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) combined_submesh = mesh.combine_submeshes(*whole_cell) disc.process_model(model) # mass matrix mass = np.eye(combined_submesh[0].npts) np.testing.assert_array_equal(mass, model.mass_matrix.entries.toarray())
def test_p2d_mass_matrix_shape(self): """ Test mass matrix shape in the pseudo 2-dimensional case """ c = pybamm.Variable("c", domain=["negative particle"]) N = pybamm.grad(c) model = pybamm.BaseModel() model.rhs = {c: pybamm.div(N)} model.initial_conditions = {c: pybamm.Scalar(0)} model.boundary_conditions = { c: { "left": (0, "Dirichlet"), "right": (0, "Dirichlet") } } model.variables = {"c": c, "N": N} mesh = get_p2d_mesh_for_testing() spatial_methods = {"negative particle": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) prim_pts = mesh["negative particle"][0].npts sec_pts = len(mesh["negative particle"]) mass_local = eye(prim_pts) mass = kron(eye(sec_pts), mass_local) np.testing.assert_array_equal(mass.toarray(), model.mass_matrix.entries.toarray())
def test_grad_1plus1d(self): mesh = get_1p1d_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) a = pybamm.Variable("a", domain=["negative electrode"]) b = pybamm.Variable("b", domain=["separator"]) c = pybamm.Variable("c", domain=["positive electrode"]) var = pybamm.Concatenation(a, b, c) boundary_conditions = { var.id: { "left": (pybamm.Vector(np.linspace(0, 1, 15)), "Neumann"), "right": (pybamm.Vector(np.linspace(0, 1, 15)), "Neumann"), } } disc.bcs = boundary_conditions disc.set_variable_slices([var]) grad_eqn_disc = disc.process_symbol(pybamm.grad(var)) # Evaulate combined_submesh = mesh.combine_submeshes(*var.domain) linear_y = np.outer(np.linspace(0, 1, 15), combined_submesh[0].nodes).reshape(-1, 1) expected = np.outer(np.linspace(0, 1, 15), np.ones_like(combined_submesh[0].edges)).reshape( -1, 1) np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, linear_y), expected)
def test_model_solver_with_event_python(self): # Create model model = pybamm.BaseModel() model.convert_to_format = "python" domain = ["negative electrode", "separator", "positive electrode"] var = pybamm.Variable("var", domain=domain) model.rhs = {var: -0.1 * var} model.initial_conditions = {var: 1} # needs to work with multiple events (to avoid bug where only last event is # used) model.events = [ pybamm.Event("var=0.5", pybamm.min(var - 0.5)), pybamm.Event("var=-0.5", pybamm.min(var + 0.5)), ] # No need to set parameters; can use base discretisation (no spatial operators) # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) # Solve solver = pybamm.ScipySolver(rtol=1e-8, atol=1e-8, method="RK45") t_eval = np.linspace(0, 10, 100) solution = solver.solve(model, t_eval) self.assertLess(len(solution.t), len(t_eval)) np.testing.assert_array_equal(solution.t, t_eval[:len(solution.t)]) np.testing.assert_allclose(solution.y[0], np.exp(-0.1 * solution.t))
def test_model_solver_with_external(self): # Create model model = pybamm.BaseModel() domain = ["negative electrode", "separator", "positive electrode"] var1 = pybamm.Variable("var1", domain=domain) var2 = pybamm.Variable("var2", domain=domain) model.rhs = {var1: -var2} model.initial_conditions = {var1: 1} model.external_variables = [var2] model.variables = {"var2": var2} # No need to set parameters; can use base discretisation (no spatial # operators) # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) # Solve solver = pybamm.ScipySolver(rtol=1e-8, atol=1e-8) t_eval = np.linspace(0, 10, 100) solution = solver.solve( model, t_eval, external_variables={"var2": 0.5 * np.ones(100)}) np.testing.assert_allclose(solution.y[0], 1 - 0.5 * solution.t, rtol=1e-06)
def default_spatial_methods(self): base_spatial_methods = { "lithium counter electrode": pybamm.FiniteVolume(), "separator": pybamm.FiniteVolume(), "working electrode": pybamm.FiniteVolume(), "working particle": pybamm.FiniteVolume(), } if self.options["dimensionality"] == 0: # 0D submesh - use base spatial method base_spatial_methods[ "current collector" ] = pybamm.ZeroDimensionalSpatialMethod() elif self.options["dimensionality"] == 1: base_spatial_methods["current collector"] = pybamm.FiniteVolume() elif self.options["dimensionality"] == 2: base_spatial_methods["current collector"] = pybamm.ScikitFiniteElement() return base_spatial_methods
def test_adding_1D_external_variable(self): model = pybamm.BaseModel() a = pybamm.Variable("a", domain=["test"]) b = pybamm.Variable("b", domain=["test"]) 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, "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") geometry = { "test": { "primary": { x: { "min": pybamm.Scalar(0), "max": pybamm.Scalar(1) } } } } submesh_types = {"test": pybamm.MeshGenerator(pybamm.Uniform1DSubMesh)} var_pts = {x: 10} mesh = pybamm.Mesh(geometry, submesh_types, var_pts) spatial_methods = {"test": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) self.assertEqual(disc.y_slices[a.id][0], slice(0, 10, None)) b_test = np.ones((10, 1)) np.testing.assert_array_equal( model.variables["b"].evaluate(u={"b": b_test}), b_test) # 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, (10, 1)) self.assertEqual(model.variables["grad b"].shape_for_testing, (11, 1)) self.assertEqual(model.variables["div grad b"].shape_for_testing, (10, 1))
def test_add_ghost_nodes(self): # Set up # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) # Add ghost nodes whole_cell = ["negative electrode", "separator", "positive electrode"] var = pybamm.Variable("var", domain=whole_cell) disc.set_variable_slices([var]) discretised_symbol = pybamm.StateVector(*disc.y_slices[var.id]) bcs = { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(3), "Dirichlet"), } # Test sp_meth = pybamm.FiniteVolume() sp_meth.build(mesh) sym_ghost, _ = sp_meth.add_ghost_nodes(var, discretised_symbol, bcs) combined_submesh = mesh.combine_submeshes(*whole_cell) y_test = np.linspace(0, 1, combined_submesh[0].npts) np.testing.assert_array_equal( sym_ghost.evaluate(y=y_test)[1:-1], discretised_symbol.evaluate(y=y_test) ) self.assertEqual( (sym_ghost.evaluate(y=y_test)[0] + sym_ghost.evaluate(y=y_test)[1]) / 2, 0 ) self.assertEqual( (sym_ghost.evaluate(y=y_test)[-2] + sym_ghost.evaluate(y=y_test)[-1]) / 2, 3 ) # test errors bcs = {"left": (pybamm.Scalar(0), "x"), "right": (pybamm.Scalar(3), "Neumann")} with self.assertRaisesRegex(ValueError, "boundary condition must be"): sp_meth.add_ghost_nodes(var, discretised_symbol, bcs) with self.assertRaisesRegex(ValueError, "boundary condition must be"): sp_meth.add_neumann_values(var, discretised_symbol, bcs, var.domain) bcs = {"left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(3), "x")} with self.assertRaisesRegex(ValueError, "boundary condition must be"): sp_meth.add_ghost_nodes(var, discretised_symbol, bcs) with self.assertRaisesRegex(ValueError, "boundary condition must be"): sp_meth.add_neumann_values(var, discretised_symbol, bcs, var.domain)
def test_grad_div_shapes_mixed_domain(self): """ Test grad and div with Dirichlet boundary conditions (applied by grad on var) """ # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) # grad var = pybamm.Variable("var", domain=["negative electrode", "separator"]) 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) combined_submesh = mesh.combine_submeshes("negative electrode", "separator") constant_y = np.ones_like(combined_submesh[0].nodes[:, np.newaxis]) np.testing.assert_array_equal( grad_eqn_disc.evaluate(None, constant_y), np.zeros_like(combined_submesh[0].edges[:, np.newaxis]), ) # div: test on linear y (should have laplacian zero) so change bcs linear_y = combined_submesh[0].nodes N = pybamm.grad(var) div_eqn = pybamm.div(N) boundary_conditions = { var.id: { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(combined_submesh[0].edges[-1]), "Dirichlet"), } } disc.bcs = boundary_conditions grad_eqn_disc = disc.process_symbol(grad_eqn) np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, linear_y), np.ones_like(combined_submesh[0].edges[:, np.newaxis]), ) div_eqn_disc = disc.process_symbol(div_eqn) np.testing.assert_array_almost_equal( div_eqn_disc.evaluate(None, linear_y), np.zeros_like(combined_submesh[0].nodes[:, np.newaxis]), )
def test_add_ghost_nodes_concatenation(self): # Set up # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) # Add ghost nodes whole_cell = ["negative electrode", "separator", "positive electrode"] var_n = pybamm.Variable("var", domain=["negative electrode"]) var_s = pybamm.Variable("var", domain=["separator"]) var_p = pybamm.Variable("var", domain=["positive electrode"]) var = pybamm.Concatenation(var_n, var_s, var_p) disc.set_variable_slices([var]) discretised_symbol = disc.process_symbol(var) bcs = { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(3), "Dirichlet"), } # Test combined_submesh = mesh.combine_submeshes(*whole_cell) y_test = np.ones_like(combined_submesh[0].nodes[:, np.newaxis]) # both sp_meth = pybamm.FiniteVolume() sp_meth.build(mesh) symbol_plus_ghost_both, _ = sp_meth.add_ghost_nodes( var, discretised_symbol, bcs) np.testing.assert_array_equal( symbol_plus_ghost_both.evaluate(None, y_test)[1:-1], discretised_symbol.evaluate(None, y_test), ) self.assertEqual( (symbol_plus_ghost_both.evaluate(None, y_test)[0] + symbol_plus_ghost_both.evaluate(None, y_test)[1]) / 2, 0, ) self.assertEqual( (symbol_plus_ghost_both.evaluate(None, y_test)[-2] + symbol_plus_ghost_both.evaluate(None, y_test)[-1]) / 2, 3, )
def test_extrapolate_on_nonuniform_grid(self): var = pybamm.standard_spatial_vars geometry = { "negative particle": { var.r_n: { "min": 0, "max": 1 } }, "positive particle": { var.r_p: { "min": 0, "max": 1 } }, } submesh_types = { "negative particle": pybamm.MeshGenerator(pybamm.Exponential1DSubMesh), "positive particle": pybamm.MeshGenerator(pybamm.Exponential1DSubMesh), } rpts = 10 var_pts = {var.r_n: rpts, var.r_p: rpts} mesh = pybamm.Mesh(geometry, submesh_types, var_pts) method_options = { "extrapolation": { "order": "linear", "use bcs": False } } spatial_methods = { "negative particle": pybamm.FiniteVolume(method_options) } disc = pybamm.Discretisation(mesh, spatial_methods) 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) micro_submesh = mesh["negative particle"] # check constant extrapolates to constant constant_y = np.ones_like(micro_submesh.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.nodes y_surf = micro_submesh.edges[-1] np.testing.assert_array_almost_equal( surf_eqn_disc.evaluate(None, linear_y), y_surf)
def test_model_solver_ode_with_jacobian_python(self): # Create model model = pybamm.BaseModel() model.convert_to_format = "python" whole_cell = ["negative electrode", "separator", "positive electrode"] var1 = pybamm.Variable("var1", domain=whole_cell) var2 = pybamm.Variable("var2", domain=whole_cell) model.rhs = {var1: var1, var2: 1 - var1} model.initial_conditions = {var1: 1.0, var2: -1.0} model.variables = {"var1": var1, "var2": var2} # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) # Add user-supplied Jacobian to model combined_submesh = mesh.combine_submeshes("negative electrode", "separator", "positive electrode") N = combined_submesh.npts # construct jacobian in order of model.rhs J = [] for var in model.rhs.keys(): if var.id == var1.id: J.append([np.eye(N), np.zeros((N, N))]) else: J.append([-1.0 * np.eye(N), np.zeros((N, N))]) J = np.block(J) def jacobian(t, y): return J # Solve solver = pybamm.ScipySolver(rtol=1e-9, atol=1e-9) t_eval = np.linspace(0, 1, 100) solution = solver.solve(model, t_eval) np.testing.assert_array_equal(solution.t, t_eval) T, Y = solution.t, solution.y np.testing.assert_array_almost_equal( model.variables["var1"].evaluate(T, Y), np.ones((N, T.size)) * np.exp(T[np.newaxis, :]), ) np.testing.assert_array_almost_equal( model.variables["var2"].evaluate(T, Y), np.ones( (N, T.size)) * (T[np.newaxis, :] - np.exp(T[np.newaxis, :])), )
def test_definite_integral_vector(self): mesh = get_mesh_for_testing() spatial_methods = { "macroscale": pybamm.FiniteVolume(), "negative particle": pybamm.FiniteVolume(), "positive particle": pybamm.FiniteVolume(), } disc = pybamm.Discretisation(mesh, spatial_methods) var = pybamm.Variable("var", domain="negative electrode") disc.set_variable_slices([var]) # row (default) vec = pybamm.DefiniteIntegralVector(var) vec_disc = disc.process_symbol(vec) self.assertEqual(vec_disc.shape[0], 1) self.assertEqual(vec_disc.shape[1], mesh["negative electrode"][0].npts) # column vec = pybamm.DefiniteIntegralVector(var, vector_type="column") vec_disc = disc.process_symbol(vec) self.assertEqual(vec_disc.shape[0], mesh["negative electrode"][0].npts) self.assertEqual(vec_disc.shape[1], 1)
def test_p2d_with_x_dep_bcs_spherical_convergence(self): # test div_r( (r**2 * sin(r)) * x ) == (4*r*sin(r) - r**2*cos(r)) * x spatial_methods = { "negative particle": pybamm.FiniteVolume(), "negative electrode": pybamm.FiniteVolume(), } # Function for convergence testing def get_error(m): # create mesh and discretisation p2d, x-dependent mesh = get_p2d_mesh_for_testing(6, m) disc = pybamm.Discretisation(mesh, spatial_methods) submesh_r = mesh["negative particle"] r = submesh_r[0].nodes r_edge = pybamm.standard_spatial_vars.r_n_edge x = pybamm.standard_spatial_vars.x_n N = pybamm.PrimaryBroadcast(x, "negative particle") * ( r_edge ** 2 * pybamm.sin(r_edge) ) div_eqn = pybamm.div(N) # Define exact solutions # N = r**2*sin(r) --> div(N) = 4*r*sin(r) - r**2*cos(r) div_exact = 4 * r * np.sin(r) + r ** 2 * np.cos(r) div_exact = np.kron(mesh["negative electrode"][0].nodes, div_exact) # Discretise and evaluate div_eqn_disc = disc.process_symbol(div_eqn) div_approx = div_eqn_disc.evaluate() return div_approx[:, 0] - div_exact # Get errors ns = 10 * 2 ** np.arange(6) errs = {n: get_error(int(n)) for n in ns} # expect quadratic convergence everywhere err_norm = np.array([np.linalg.norm(errs[n], np.inf) for n in ns]) rates = np.log2(err_norm[:-1] / err_norm[1:]) np.testing.assert_array_less(1.99 * np.ones_like(rates), rates)
def test_cartesian_spherical_grad_convergence(self): # note that grad function is the same for cartesian and spherical spatial_methods = {"macroscale": pybamm.FiniteVolume()} whole_cell = ["negative electrode", "separator", "positive electrode"] # Define variable var = pybamm.Variable("var", domain=whole_cell) grad_eqn = pybamm.grad(var) boundary_conditions = { var.id: { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(np.sin(1)**2), "Dirichlet"), } } # Function for convergence testing def get_error(n): # create mesh and discretisation mesh = get_mesh_for_testing(n) disc = pybamm.Discretisation(mesh, spatial_methods) disc.bcs = boundary_conditions disc.set_variable_slices([var]) # Define exact solutions combined_submesh = mesh.combine_submeshes(*whole_cell) x = combined_submesh.nodes y = np.sin(x)**2 # var = sin(x)**2 --> dvardx = 2*sin(x)*cos(x) x_edge = combined_submesh.edges grad_exact = 2 * np.sin(x_edge) * np.cos(x_edge) # Discretise and evaluate grad_eqn_disc = disc.process_symbol(grad_eqn) grad_approx = grad_eqn_disc.evaluate(y=y) # Return difference between approx and exact return grad_approx[:, 0] - grad_exact # Get errors ns = 100 * 2**np.arange(6) errs = {n: get_error(int(n)) for n in ns} # expect quadratic convergence at internal points errs_internal = np.array( [np.linalg.norm(errs[n][1:-1], np.inf) for n in ns]) rates = np.log2(errs_internal[:-1] / errs_internal[1:]) np.testing.assert_array_less(1.99 * np.ones_like(rates), rates) # expect linear convergence at the boundaries for idx in [0, -1]: err_boundary = np.array([errs[n][idx] for n in ns]) rates = np.log2(err_boundary[:-1] / err_boundary[1:]) np.testing.assert_array_less(0.98 * np.ones_like(rates), rates)
def test_p2d_spherical_grad_div_shapes_Neumann_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 = {"negative particle": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) n_mesh = mesh["negative particle"] mesh.add_ghost_meshes() disc.mesh.add_ghost_meshes() # test grad var = pybamm.Variable("var", domain=["negative particle"]) grad_eqn = pybamm.grad(var) disc.set_variable_slices([var]) grad_eqn_disc = disc.process_symbol(grad_eqn) prim_pts = n_mesh[0].npts sec_pts = len(n_mesh) 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) boundary_conditions = { var.id: { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "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, np.zeros([sec_pts, prim_pts]))
def test_grad_div_broadcast(self): # create mesh and discretisation spatial_methods = {"macroscale": pybamm.FiniteVolume()} mesh = get_mesh_for_testing() disc = pybamm.Discretisation(mesh, spatial_methods) a = pybamm.PrimaryBroadcast(1, "negative electrode") grad_a = disc.process_symbol(pybamm.grad(a)) np.testing.assert_array_equal(grad_a.evaluate(), 0) a_edge = pybamm.PrimaryBroadcastToEdges(1, "negative electrode") div_a = disc.process_symbol(pybamm.div(a_edge)) np.testing.assert_array_equal(div_a.evaluate(), 0) div_grad_a = disc.process_symbol(pybamm.div(pybamm.grad(a))) np.testing.assert_array_equal(div_grad_a.evaluate(), 0)
def test_solver(self): # Create model model = pybamm.BaseModel() model.convert_to_format = "jax" domain = ["negative electrode", "separator", "positive electrode"] var = pybamm.Variable("var", domain=domain) model.rhs = {var: 0.1 * var} model.initial_conditions = {var: 1} # No need to set parameters; can use base discretisation (no spatial operators) # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) # Solve t_eval = np.linspace(0.0, 1.0, 80) y0 = model.concatenated_initial_conditions.evaluate().reshape(-1) rhs = pybamm.EvaluatorJax(model.concatenated_rhs) def fun(y, t): return rhs.evaluate(t=t, y=y).reshape(-1) t0 = time.perf_counter() y = pybamm.jax_bdf_integrate(fun, y0, t_eval, rtol=1e-8, atol=1e-8) t1 = time.perf_counter() - t0 # test accuracy np.testing.assert_allclose(y[:, 0], np.exp(0.1 * t_eval), rtol=1e-6, atol=1e-6) t0 = time.perf_counter() y = pybamm.jax_bdf_integrate(fun, y0, t_eval, rtol=1e-8, atol=1e-8) t2 = time.perf_counter() - t0 # second run should be much quicker self.assertLess(t2, t1) # test second run is accurate np.testing.assert_allclose(y[:, 0], np.exp(0.1 * t_eval), rtol=1e-6, atol=1e-6)
def test_spherical_grad_div_shapes_Neumann_bcs(self): """Test grad and div with Neumann boundary conditions (applied by div on N)""" # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"negative particle": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) combined_submesh = mesh.combine_submeshes("negative particle") # grad var = pybamm.Variable("var", domain="negative particle") grad_eqn = pybamm.grad(var) disc.set_variable_slices([var]) grad_eqn_disc = disc.process_symbol(grad_eqn) constant_y = np.ones_like(combined_submesh[0].nodes[:, np.newaxis]) np.testing.assert_array_equal( grad_eqn_disc.evaluate(None, constant_y), np.zeros_like(combined_submesh[0].edges[1:-1][:, np.newaxis]), ) linear_y = combined_submesh[0].nodes np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, linear_y), np.ones_like(combined_submesh[0].edges[1:-1][:, np.newaxis]), ) # div # div ( grad(r^2) ) == 6 , N_left = N_right = 0 N = pybamm.grad(var) div_eqn = pybamm.div(N) boundary_conditions = { var.id: { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(0), "Neumann"), } } disc.bcs = boundary_conditions div_eqn_disc = disc.process_symbol(div_eqn) linear_y = combined_submesh[0].nodes const = 6 * np.ones(combined_submesh[0].npts) np.testing.assert_array_almost_equal( div_eqn_disc.evaluate(None, const), np.zeros((combined_submesh[0].npts, 1)))
def test_pure_neumann_poisson(self): # grad^2 u = 1, du/dz = 1 at z = 1, du/dn = 0 elsewhere, u has zero average u = pybamm.Variable("u", domain="current collector") c = pybamm.Variable("c") # lagrange multiplier y = pybamm.SpatialVariable("y", ["current collector"]) z = pybamm.SpatialVariable("z", ["current collector"]) model = pybamm.BaseModel() # 0*c hack otherwise gives KeyError model.algebraic = { u: pybamm.laplacian(u) - pybamm.source(1, u) + c * pybamm.DefiniteIntegralVector(u, vector_type="column"), c: pybamm.Integral(u, [y, z]) + 0 * c, } model.initial_conditions = {u: pybamm.Scalar(0), c: pybamm.Scalar(0)} # set boundary conditions ("negative tab" = bottom of unit square, # "positive tab" = top of unit square, elsewhere normal derivative is zero) model.boundary_conditions = { u: { "negative tab": (0, "Neumann"), "positive tab": (1, "Neumann") } } model.variables = {"c": c, "u": u} # create discretisation 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) disc.process_model(model) # solve model solver = pybamm.AlgebraicSolver() solution = solver.solve(model) z = mesh["current collector"].coordinates[1, :][:, np.newaxis] u_exact = z**2 / 2 - 1 / 6 np.testing.assert_array_almost_equal(solution.y[:-1], u_exact, decimal=1)
def test_semi_explicit_model(self): # Create model model = pybamm.BaseModel() model.convert_to_format = "jax" domain = ["negative electrode", "separator", "positive electrode"] var = pybamm.Variable("var", domain=domain) var2 = pybamm.Variable("var2", domain=domain) model.rhs = {var: 0.1 * var} model.algebraic = {var2: var2 - 2.0 * var} # give inconsistent initial conditions, should calculate correct ones model.initial_conditions = {var: 1.0, var2: 1.0} # No need to set parameters; can use base discretisation (no spatial operators) # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) # Solve solver = pybamm.JaxSolver( method='BDF', rtol=1e-8, atol=1e-8 ) t_eval = np.linspace(0, 1, 80) t0 = time.perf_counter() solution = solver.solve(model, t_eval) t_first_solve = time.perf_counter() - t0 np.testing.assert_array_equal(solution.t, t_eval) soln = np.exp(0.1 * solution.t) np.testing.assert_allclose(solution.y[0], soln, rtol=1e-7, atol=1e-7) np.testing.assert_allclose(solution.y[-1], 2 * soln, rtol=1e-7, atol=1e-7) # Test time self.assertEqual( solution.total_time, solution.solve_time + solution.set_up_time ) self.assertEqual(solution.termination, "final time") t0 = time.perf_counter() second_solution = solver.solve(model, t_eval) t_second_solve = time.perf_counter() - t0 self.assertLess(t_second_solve, t_first_solve) np.testing.assert_array_equal(second_solution.y, solution.y)
def test_solver_sensitivities(self): # Create model model = pybamm.BaseModel() model.convert_to_format = "jax" domain = ["negative electrode", "separator", "positive electrode"] var = pybamm.Variable("var", domain=domain) model.rhs = {var: -pybamm.InputParameter("rate") * var} model.initial_conditions = {var: 1} # create discretisation mesh = get_mesh_for_testing(xpts=10) spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) # Solve t_eval = np.linspace(0, 10, 4) y0 = model.concatenated_initial_conditions.evaluate().reshape(-1) rhs = pybamm.EvaluatorJax(model.concatenated_rhs) def fun(y, t, inputs): return rhs.evaluate(t=t, y=y, inputs=inputs).reshape(-1) h = 0.0001 rate = 0.1 # create a dummy "model" where we calculate the sum of the time series @jax.jit def solve_bdf(rate): return jax.numpy.sum( pybamm.jax_bdf_integrate(fun, y0, t_eval, {'rate': rate}, rtol=1e-9, atol=1e-9)) # check answers with finite difference eval_plus = solve_bdf(rate + h) eval_neg = solve_bdf(rate - h) grad_num = (eval_plus - eval_neg) / (2 * h) grad_solve_bdf = jax.jit(jax.grad(solve_bdf)) grad_bdf = grad_solve_bdf(rate) self.assertAlmostEqual(grad_bdf, grad_num, places=3)
def test_grad_div_shapes_Neumann_bcs(self): """Test grad and div with Neumann boundary conditions (applied by div on N)""" whole_cell = ["negative electrode", "separator", "positive electrode"] # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} 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]) grad_eqn_disc = disc.process_symbol(grad_eqn) constant_y = np.ones_like(combined_submesh[0].nodes[:, np.newaxis]) np.testing.assert_array_equal( grad_eqn_disc.evaluate(None, constant_y), np.zeros_like(combined_submesh[0].edges[1:-1][:, np.newaxis]), ) # div N = pybamm.grad(var) div_eqn = pybamm.div(N) boundary_conditions = { var.id: { "left": (pybamm.Scalar(1), "Neumann"), "right": (pybamm.Scalar(1), "Neumann"), } } disc.bcs = boundary_conditions div_eqn_disc = disc.process_symbol(div_eqn) # Linear y should have laplacian zero linear_y = combined_submesh[0].nodes np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, linear_y), np.ones_like(combined_submesh[0].edges[1:-1][:, np.newaxis]), ) np.testing.assert_array_almost_equal( div_eqn_disc.evaluate(None, linear_y), np.zeros_like(combined_submesh[0].nodes[:, np.newaxis]), )
def test_delta_function(self): mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) var = pybamm.Variable("var") delta_fn_left = pybamm.DeltaFunction(var, "left", "negative electrode") delta_fn_right = pybamm.DeltaFunction(var, "right", "negative electrode") disc.set_variable_slices([var]) delta_fn_left_disc = disc.process_symbol(delta_fn_left) delta_fn_right_disc = disc.process_symbol(delta_fn_right) # Basic shape and type tests y = np.ones_like(mesh["negative electrode"][0].nodes[:, np.newaxis]) # Left self.assertEqual(delta_fn_left_disc.domain, delta_fn_left.domain) self.assertEqual(delta_fn_left_disc.auxiliary_domains, delta_fn_left.auxiliary_domains) self.assertIsInstance(delta_fn_left_disc, pybamm.Multiplication) self.assertIsInstance(delta_fn_left_disc.left, pybamm.Matrix) np.testing.assert_array_equal( delta_fn_left_disc.left.evaluate()[:, 1:], 0) self.assertEqual(delta_fn_left_disc.shape, y.shape) # Right self.assertEqual(delta_fn_right_disc.domain, delta_fn_right.domain) self.assertEqual(delta_fn_right_disc.auxiliary_domains, delta_fn_right.auxiliary_domains) self.assertIsInstance(delta_fn_right_disc, pybamm.Multiplication) self.assertIsInstance(delta_fn_right_disc.left, pybamm.Matrix) np.testing.assert_array_equal( delta_fn_right_disc.left.evaluate()[:, :-1], 0) self.assertEqual(delta_fn_right_disc.shape, y.shape) # Value tests # Delta function should integrate to the same thing as variable var_disc = disc.process_symbol(var) x = pybamm.standard_spatial_vars.x_n delta_fn_int_disc = disc.process_symbol( pybamm.Integral(delta_fn_left, x)) np.testing.assert_array_equal( var_disc.evaluate(y=y) * mesh["negative electrode"][0].edges[-1], np.sum(delta_fn_int_disc.evaluate(y=y)), )
def test_get_solve(self): # Create model model = pybamm.BaseModel() model.convert_to_format = "jax" domain = ["negative electrode", "separator", "positive electrode"] var = pybamm.Variable("var", domain=domain) model.rhs = {var: -pybamm.InputParameter("rate") * var} model.initial_conditions = {var: 1} # No need to set parameters; can use base discretisation (no spatial # operators) # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) # test that another method string gives error with self.assertRaises(ValueError): solver = pybamm.JaxSolver(method="not_real") # Solve solver = pybamm.JaxSolver(rtol=1e-8, atol=1e-8) t_eval = np.linspace(0, 5, 80) with self.assertRaisesRegex(RuntimeError, "Model is not set up for solving"): solver.get_solve(model, t_eval) solver.solve(model, t_eval, inputs={"rate": 0.1}) solver = solver.get_solve(model, t_eval) y = solver({"rate": 0.1}) np.testing.assert_allclose(y[0], np.exp(-0.1 * t_eval), rtol=1e-6, atol=1e-6) y = solver({"rate": 0.2}) np.testing.assert_allclose(y[0], np.exp(-0.2 * t_eval), rtol=1e-6, atol=1e-6)
def test_solver_sensitivities(self): # Create model model = pybamm.BaseModel() model.convert_to_format = "jax" domain = ["negative electrode", "separator", "positive electrode"] var = pybamm.Variable("var", domain=domain) model.rhs = {var: -pybamm.InputParameter("rate") * var} model.initial_conditions = {var: 1.0} # No need to set parameters; can use base discretisation (no spatial operators) # create discretisation mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.FiniteVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) disc.process_model(model) for method in ['RK45', 'BDF']: # Solve solver = pybamm.JaxSolver( method=method, rtol=1e-8, atol=1e-8 ) t_eval = np.linspace(0, 1, 80) h = 0.0001 rate = 0.1 # need to solve the model once to get it set up by the base solver solver.solve(model, t_eval, inputs={'rate': rate}) solve = solver.get_solve(model, t_eval) # create a dummy "model" where we calculate the sum of the time series def solve_model(rate): return jax.numpy.sum(solve({'rate': rate})) # check answers with finite difference eval_plus = solve_model(rate + h) eval_neg = solve_model(rate - h) grad_num = (eval_plus - eval_neg) / (2 * h) grad_solve = jax.jit(jax.grad(solve_model)) grad = grad_solve(rate) self.assertAlmostEqual(grad, grad_num, places=1)
def test_definite_integral(self): mesh = get_2p1d_mesh_for_testing() spatial_methods = { "macroscale": pybamm.FiniteVolume(), "current collector": pybamm.ScikitFiniteElement(), } disc = pybamm.Discretisation(mesh, spatial_methods) var = pybamm.Variable("var", domain="current collector") y = pybamm.SpatialVariable("y", ["current collector"]) z = pybamm.SpatialVariable("z", ["current collector"]) integral_eqn = pybamm.Integral(var, [y, z]) disc.set_variable_slices([var]) integral_eqn_disc = disc.process_symbol(integral_eqn) y_test = 6 * np.ones(mesh["current collector"][0].npts) fem_mesh = mesh["current collector"][0] ly = fem_mesh.coordinates[0, -1] lz = fem_mesh.coordinates[1, -1] np.testing.assert_array_almost_equal( integral_eqn_disc.evaluate(None, y_test), 6 * ly * lz)