def test_exceptions(self): sp_meth = pybamm.SpectralVolume() with self.assertRaises(ValueError): sp_meth.chebyshev_differentiation_matrices(3, 3) mesh = get_mesh_for_testing() spatial_methods = {"macroscale": pybamm.SpectralVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) 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]) sp_meth.build(mesh) bcs = { "left": (pybamm.Scalar(0), "x"), "right": (pybamm.Scalar(3), "Neumann") } with self.assertRaisesRegex(ValueError, "boundary condition must be"): sp_meth.replace_dirichlet_values(var, discretised_symbol, bcs) with self.assertRaisesRegex(ValueError, "boundary condition must be"): sp_meth.replace_neumann_values(var, discretised_symbol, bcs) bcs = { "left": (pybamm.Scalar(0), "Neumann"), "right": (pybamm.Scalar(3), "x") } with self.assertRaisesRegex(ValueError, "boundary condition must be"): sp_meth.replace_dirichlet_values(var, discretised_symbol, bcs) with self.assertRaisesRegex(ValueError, "boundary condition must be"): sp_meth.replace_neumann_values(var, discretised_symbol, bcs)
def test_p2d_spherical_convergence_quadratic(self): # test div( r**2 * sin(r) ) == 4*r*sin(r) - r**2*cos(r) spatial_methods = {"negative particle": pybamm.SpectralVolume()} # Function for convergence testing def get_error(m): # create mesh and discretisation p2d, uniform in x mesh = get_p2d_mesh_for_testing(3, m) disc = pybamm.Discretisation(mesh, spatial_methods) submesh = mesh["negative particle"] r = submesh.nodes r_edge = pybamm.standard_spatial_vars.r_n_edge N = 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(np.ones(mesh["negative electrode"].npts), 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_spherical_div_convergence_linear(self): # test div( r*sin(r) ) == 3*sin(r) + r*cos(r) spatial_methods = {"negative particle": pybamm.SpectralVolume()} # 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.nodes r_edge = pybamm.SpatialVariableEdge("r_n", domain=["negative particle"]) # Define flux and bcs N = r_edge * pybamm.sin(r_edge) div_eqn = pybamm.div(N) # Define exact solutions # N = r*sin(r) --> div(N) = 3*sin(r) + r*cos(r) div_exact = 3 * np.sin(r) + r * 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 linear 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(0.99 * np.ones_like(rates), rates)
def test_cartesian_div_convergence(self): whole_cell = ["negative electrode", "separator", "positive electrode"] spatial_methods = {"macroscale": pybamm.SpectralVolume()} # 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.nodes x_edge = pybamm.standard_spatial_vars.x_edge # Define flux and bcs N = x_edge ** 2 * pybamm.cos(x_edge) 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_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.SpectralVolume()} 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.nodes[:, np.newaxis]) np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, constant_y), np.zeros_like(combined_submesh.edges[:, np.newaxis]), ) # div: test on linear y (should have laplacian zero) so change bcs linear_y = combined_submesh.nodes N = pybamm.grad(var) div_eqn = pybamm.div(N) boundary_conditions = { var.id: { "left": (pybamm.Scalar(0), "Dirichlet"), "right": (pybamm.Scalar(combined_submesh.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.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.nodes[:, np.newaxis]), )
def test_cartesian_spherical_grad_convergence(self): # note that grad function is the same for cartesian and spherical order = 2 spatial_methods = {"macroscale": pybamm.SpectralVolume(order=order)} 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, order=order) 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(5) errs = {n: get_error(int(n)) for n in ns} # expect linear convergence at internal points # (the higher-order convergence is in the integral means, # not in the edge values) 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(0.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.SpectralVolume()} 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"], auxiliary_domains={"secondary": "negative electrode"}, ) grad_eqn = pybamm.grad(var) 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) 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.SpectralVolume()} 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_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.SpectralVolume()} 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.nodes[:, np.newaxis]) np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, constant_y), np.zeros_like(combined_submesh.edges[:][:, np.newaxis]), ) 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]), ) # 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.nodes const = 6 * np.ones(combined_submesh.npts) np.testing.assert_array_almost_equal( div_eqn_disc.evaluate(None, const), np.zeros((combined_submesh.npts, 1)))
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.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]) grad_eqn_disc = disc.process_symbol(grad_eqn) 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]), ) # 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.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_grad_1plus1d(self): mesh = get_1p1d_mesh_for_testing() spatial_methods = {"macroscale": pybamm.SpectralVolume()} disc = pybamm.Discretisation(mesh, spatial_methods) a = pybamm.Variable( "a", domain=["negative electrode"], auxiliary_domains={"secondary": "current collector"}, ) b = pybamm.Variable( "b", domain=["separator"], auxiliary_domains={"secondary": "current collector"}, ) c = pybamm.Variable( "c", domain=["positive electrode"], auxiliary_domains={"secondary": "current collector"}, ) 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.nodes).reshape( -1, 1 ) expected = np.outer( np.linspace(0, 1, 15), np.ones_like(combined_submesh.edges) ).reshape(-1, 1) np.testing.assert_array_almost_equal( grad_eqn_disc.evaluate(None, linear_y), expected )
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, )), )
"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]) # solve model t_eval = np.linspace(0, 3600, 100) casadi_fv = pybamm.CasadiSolver(atol=1e-8, rtol=1e-8).solve(models[0], t_eval)