def test_model_solver_with_non_identity_mass(self): model = pybamm.BaseModel() var1 = pybamm.Variable("var1", domain="negative electrode") var2 = pybamm.Variable("var2", domain="negative electrode") model.rhs = {var1: var1} model.algebraic = {var2: 2 * var1 - var2} model.initial_conditions = {var1: 1, var2: 2} disc = get_discretisation_for_testing() disc.process_model(model) # FV discretisation has identity mass. Manually set the mass matrix to # be a diag of 10s here for testing. Note that the algebraic part is all # zeros mass_matrix = 10 * model.mass_matrix.entries model.mass_matrix = pybamm.Matrix(mass_matrix) # Note that mass_matrix_inv is just the inverse of the ode block of the # mass matrix mass_matrix_inv = 0.1 * eye(int(mass_matrix.shape[0] / 2)) model.mass_matrix_inv = pybamm.Matrix(mass_matrix_inv) # Solve solver = pybamm.CasadiSolver(rtol=1e-8, atol=1e-8) 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.full()[0], np.exp(0.1 * solution.t)) np.testing.assert_allclose(solution.y.full()[-1], 2 * np.exp(0.1 * solution.t))
def test_simplify_inner(self): a1 = pybamm.Scalar(0) M1 = pybamm.Matrix(np.zeros((10, 10))) v1 = pybamm.Vector(np.ones(10)) a2 = pybamm.Scalar(1) M2 = pybamm.Matrix(np.ones((10, 10))) a3 = pybamm.Scalar(3) np.testing.assert_array_equal( pybamm.inner(a1, M2).simplify().evaluate().toarray(), M1.entries ) self.assertEqual(pybamm.inner(a1, a2).simplify().evaluate(), 0) np.testing.assert_array_equal( pybamm.inner(M2, a1).simplify().evaluate().toarray(), M1.entries ) self.assertEqual(pybamm.inner(a2, a1).simplify().evaluate(), 0) np.testing.assert_array_equal( pybamm.inner(M1, a3).simplify().evaluate().toarray(), M1.entries ) np.testing.assert_array_equal( pybamm.inner(v1, a3).simplify().evaluate(), 3 * v1.entries ) self.assertEqual(pybamm.inner(a2, a3).simplify().evaluate(), 3) self.assertEqual(pybamm.inner(a3, a2).simplify().evaluate(), 3) self.assertEqual(pybamm.inner(a3, a3).simplify().evaluate(), 9)
def broadcast(self, symbol, domain, auxiliary_domains, broadcast_type): """ Broadcast symbol to a specified domain. Parameters ---------- symbol : :class:`pybamm.Symbol` The symbol to be broadcasted domain : iterable of strings The domain to broadcast to auxiliary_domains : dict of strings The auxiliary domains for broadcasting broadcast_type : str The type of broadcast: 'primary to node', 'primary to edges', 'secondary to nodes', 'secondary to edges', 'full to nodes' or 'full to edges' Returns ------- broadcasted_symbol: class: `pybamm.Symbol` The discretised symbol of the correct size for the spatial method """ primary_domain_size = sum(self.mesh[dom].npts_for_broadcast_to_nodes for dom in domain) secondary_domain_size = self._get_auxiliary_domain_repeats( {k: v for k, v in auxiliary_domains.items() if k == "secondary"}) auxiliary_domains_size = self._get_auxiliary_domain_repeats( auxiliary_domains) full_domain_size = primary_domain_size * auxiliary_domains_size if broadcast_type.endswith("to edges"): # add one point to each domain for broadcasting to edges primary_domain_size += 1 full_domain_size = primary_domain_size * auxiliary_domains_size secondary_domain_size += 1 if broadcast_type.startswith("primary"): # Make copies of the child stacked on top of each other sub_vector = np.ones((primary_domain_size, 1)) if symbol.shape_for_testing == (): out = symbol * pybamm.Vector(sub_vector) else: # Repeat for secondary points matrix = csr_matrix( kron(eye(symbol.shape_for_testing[0]), sub_vector)) out = pybamm.Matrix(matrix) @ symbol out.domain = domain elif broadcast_type.startswith("secondary"): # Make copies of the child stacked on top of each other identity = eye(symbol.shape[0]) matrix = vstack([identity for _ in range(secondary_domain_size)]) out = pybamm.Matrix(matrix) @ symbol elif broadcast_type.startswith("full"): out = symbol * pybamm.Vector(np.ones(full_domain_size), domain=domain) out.auxiliary_domains = auxiliary_domains.copy() return out
def test_sparse_multiply(self): row = np.array([0, 3, 1, 0]) col = np.array([0, 3, 1, 2]) data = np.array([4, 5, 7, 9]) S1 = coo_matrix((data, (row, col)), shape=(4, 5)) S2 = coo_matrix((data, (row, col)), shape=(5, 4)) pybammS1 = pybamm.Matrix(S1) pybammS2 = pybamm.Matrix(S2) D1 = np.ones((4, 5)) D2 = np.ones((5, 4)) pybammD1 = pybamm.Matrix(D1) pybammD2 = pybamm.Matrix(D2) # Multiplication is elementwise np.testing.assert_array_equal( (pybammS1 * pybammS1).evaluate().toarray(), S1.multiply(S1).toarray()) np.testing.assert_array_equal( (pybammS2 * pybammS2).evaluate().toarray(), S2.multiply(S2).toarray()) np.testing.assert_array_equal( (pybammD1 * pybammS1).evaluate().toarray(), S1.toarray() * D1) np.testing.assert_array_equal( (pybammS1 * pybammD1).evaluate().toarray(), S1.toarray() * D1) np.testing.assert_array_equal( (pybammD2 * pybammS2).evaluate().toarray(), S2.toarray() * D2) np.testing.assert_array_equal( (pybammS2 * pybammD2).evaluate().toarray(), S2.toarray() * D2) with self.assertRaisesRegex(pybamm.ShapeError, "inconsistent shapes"): (pybammS1 * pybammS2).test_shape() with self.assertRaisesRegex(pybamm.ShapeError, "inconsistent shapes"): (pybammS2 * pybammS1).test_shape() with self.assertRaisesRegex(pybamm.ShapeError, "inconsistent shapes"): (pybammS2 * pybammS1).evaluate_ignoring_errors() # Matrix multiplication is normal matrix multiplication np.testing.assert_array_equal( (pybammS1 @ pybammS2).evaluate().toarray(), (S1 * S2).toarray()) np.testing.assert_array_equal( (pybammS2 @ pybammS1).evaluate().toarray(), (S2 * S1).toarray()) np.testing.assert_array_equal((pybammS1 @ pybammD2).evaluate(), S1 * D2) np.testing.assert_array_equal((pybammD2 @ pybammS1).evaluate(), D2 * S1) np.testing.assert_array_equal((pybammS2 @ pybammD1).evaluate(), S2 * D1) np.testing.assert_array_equal((pybammD1 @ pybammS2).evaluate(), D1 * S2) with self.assertRaisesRegex(pybamm.ShapeError, "dimension mismatch"): (pybammS1 @ pybammS1).test_shape() with self.assertRaisesRegex(pybamm.ShapeError, "dimension mismatch"): (pybammS2 @ pybammS2).test_shape()
def broadcast(self, symbol, domain, auxiliary_domains, broadcast_type): """ Broadcast symbol to a specified domain. Parameters ---------- symbol : :class:`pybamm.Symbol` The symbol to be broadcasted domain : iterable of strings The domain to broadcast to broadcast_type : str The type of broadcast, either: 'primary' or 'full' Returns ------- broadcasted_symbol: class: `pybamm.Symbol` The discretised symbol of the correct size for the spatial method """ primary_domain_size = sum(self.mesh[dom][0].npts_for_broadcast for dom in domain) full_domain_size = sum(subdom.npts_for_broadcast for dom in domain for subdom in self.mesh[dom]) if broadcast_type == "primary": # Make copies of the child stacked on top of each other sub_vector = np.ones((primary_domain_size, 1)) if symbol.shape_for_testing == (): out = symbol * pybamm.Vector(sub_vector) else: # Repeat for secondary points matrix = csr_matrix( kron(eye(symbol.shape_for_testing[0]), sub_vector)) out = pybamm.Matrix(matrix) @ symbol out.domain = domain elif broadcast_type == "secondary": secondary_domain_size = sum( self.mesh[dom][0].npts_for_broadcast for dom in auxiliary_domains["secondary"]) kron_size = full_domain_size // primary_domain_size # Symbol may be on edges so need to calculate size carefully symbol_primary_size = symbol.shape[0] // kron_size # Make copies of the child stacked on top of each other identity = eye(symbol_primary_size) sub_matrix = vstack( [identity for _ in range(secondary_domain_size)]) # Repeat for secondary points matrix = csr_matrix(kron(eye(kron_size), sub_matrix)) out = pybamm.Matrix(matrix) @ symbol elif broadcast_type == "full": out = symbol * pybamm.Vector(np.ones(full_domain_size), domain=domain) out.auxiliary_domains = auxiliary_domains return out
def test_symbol_new_copy(self): a = pybamm.Parameter("a") b = pybamm.Parameter("b") v_n = pybamm.Variable("v", "negative electrode") x_n = pybamm.standard_spatial_vars.x_n v_s = pybamm.Variable("v", "separator") vec = pybamm.Vector([1, 2, 3, 4, 5]) mat = pybamm.Matrix([[1, 2], [3, 4]]) mesh = get_mesh_for_testing() for symbol in [ a + b, a - b, a * b, a / b, a**b, -a, abs(a), pybamm.Function(np.sin, a), pybamm.FunctionParameter("function", {"a": a}), pybamm.grad(v_n), pybamm.div(pybamm.grad(v_n)), pybamm.upwind(v_n), pybamm.IndefiniteIntegral(v_n, x_n), pybamm.BackwardIndefiniteIntegral(v_n, x_n), pybamm.BoundaryValue(v_n, "right"), pybamm.BoundaryGradient(v_n, "right"), pybamm.PrimaryBroadcast(a, "domain"), pybamm.SecondaryBroadcast(v_n, "current collector"), pybamm.FullBroadcast(a, "domain", {"secondary": "other domain"}), pybamm.concatenation(v_n, v_s), pybamm.NumpyConcatenation(a, b, v_s), pybamm.DomainConcatenation([v_n, v_s], mesh), pybamm.Parameter("param"), pybamm.InputParameter("param"), pybamm.StateVector(slice(0, 56)), pybamm.Matrix(np.ones((50, 40))), pybamm.SpatialVariable("x", ["negative electrode"]), pybamm.t, pybamm.Index(vec, 1), pybamm.NotConstant(a), pybamm.ExternalVariable( "external variable", 20, domain="test", auxiliary_domains={"secondary": "test2"}, ), pybamm.minimum(a, b), pybamm.maximum(a, b), pybamm.SparseStack(mat, mat), ]: self.assertEqual(symbol.id, symbol.new_copy().id)
def internal_neumann_condition( self, left_symbol_disc, right_symbol_disc, left_mesh, right_mesh ): """ A method to find the internal neumann conditions between two symbols on adjacent subdomains. Parameters ---------- left_symbol_disc : :class:`pybamm.Symbol` The discretised symbol on the left subdomain right_symbol_disc : :class:`pybamm.Symbol` The discretised symbol on the right subdomain left_mesh : list The mesh on the left subdomain right_mesh : list The mesh on the right subdomain """ left_npts = left_mesh[0].npts right_npts = right_mesh[0].npts sec_pts = len(left_mesh) if sec_pts != len(right_mesh): raise pybamm.DomainError( """Number of secondary points in subdomains do not match""" ) left_sub_matrix = np.zeros((1, left_npts)) left_sub_matrix[0][left_npts - 1] = 1 left_matrix = pybamm.Matrix(csr_matrix(kron(eye(sec_pts), left_sub_matrix))) right_sub_matrix = np.zeros((1, right_npts)) right_sub_matrix[0][0] = 1 right_matrix = pybamm.Matrix(csr_matrix(kron(eye(sec_pts), right_sub_matrix))) # Remove domains to avoid clash left_domain = left_symbol_disc.domain right_domain = right_symbol_disc.domain left_symbol_disc.domain = [] right_symbol_disc.domain = [] # Finite volume derivative dy = right_matrix @ right_symbol_disc - left_matrix @ left_symbol_disc dx = right_mesh[0].nodes[0] - left_mesh[0].nodes[-1] # Change domains back left_symbol_disc.domain = left_domain right_symbol_disc.domain = right_domain return dy / dx
def boundary_value_or_flux(self, symbol, discretised_child, bcs=None): """ Returns the boundary value or flux using the approriate expression for the spatial method. To do this, we create a sparse vector 'bv_vector' that extracts either the first (for side="left") or last (for side="right") point from 'discretised_child'. Parameters ----------- symbol: :class:`pybamm.Symbol` The boundary value or flux symbol discretised_child : :class:`pybamm.StateVector` The discretised variable from which to calculate the boundary value bcs : dict (optional) The boundary conditions. If these are supplied and "use bcs" is True in the options, then these will be used to improve the accuracy of the extrapolation. Returns ------- :class:`pybamm.MatrixMultiplication` The variable representing the surface value. """ if bcs is None: bcs = {} if self._get_auxiliary_domain_repeats( discretised_child.auxiliary_domains) > 1: raise NotImplementedError( "Cannot process 2D symbol in base spatial method") if isinstance(symbol, pybamm.BoundaryGradient): raise TypeError( "Cannot process BoundaryGradient in base spatial method") n = sum(self.mesh[dom].npts for dom in discretised_child.domain) if symbol.side == "left": # coo_matrix takes inputs (data, (row, col)) and puts data[i] at the point # (row[i], col[i]) for each index of data. Here we just want a single point # with value 1 at (0,0). # Convert to a csr_matrix to allow indexing and other functionality left_vector = csr_matrix( coo_matrix(([1], ([0], [0])), shape=(1, n))) bv_vector = pybamm.Matrix(left_vector) elif symbol.side == "right": # as above, but now we want a single point with value 1 at (0, n-1) right_vector = csr_matrix( coo_matrix(([1], ([0], [n - 1])), shape=(1, n))) bv_vector = pybamm.Matrix(right_vector) out = bv_vector @ discretised_child # boundary value removes domain out.clear_domains() return out
def indefinite_integral_matrix_nodes(self, domain): """ Matrix for finite-volume implementation of the indefinite integral where the integrand is evaluated on mesh nodes. This is just a straightforward cumulative sum of the integrand Parameters ---------- domain : list The domain(s) of integration Returns ------- :class:`pybamm.Matrix` The finite volume integral matrix for the domain """ # Create appropriate submesh by combining submeshes in domain submesh_list = self.mesh.combine_submeshes(*domain) submesh = submesh_list[0] n = submesh.npts sec_pts = len(submesh_list) du_n = submesh.d_edges du_entries = [du_n] * (n) offset = -np.arange(1, n + 1, 1) sub_matrix = diags(du_entries, offset, shape=(n + 1, n)) # Convert to csr_matrix so that we can take the index (row-slicing), which is # not supported by the default kron format # Note that this makes column-slicing inefficient, but this should not be an # issue matrix = csr_matrix(kron(eye(sec_pts), sub_matrix)) return pybamm.Matrix(matrix)
def _binary_simplify(self, left, right): """ See :meth:`pybamm.BinaryOperator.simplify()`. """ # anything multiplied by a scalar zero returns a scalar zero if is_scalar_zero(left): if isinstance(right, pybamm.Array): return pybamm.Array(np.zeros(right.shape)) else: return pybamm.Scalar(0) if is_scalar_zero(right): if isinstance(left, pybamm.Array): return pybamm.Array(np.zeros(left.shape)) else: return pybamm.Scalar(0) # if one of the children is a zero matrix, we have to be careful about shapes if is_matrix_zero(left) or is_matrix_zero(right): shape = (left * right).shape if len(shape) == 1 or shape[1] == 1: return pybamm.Vector(np.zeros(shape)) else: return pybamm.Matrix(csr_matrix(shape)) # anything multiplied by a scalar one returns itself if is_one(left): return right if is_one(right): return left return pybamm.simplify_multiplication_division(self.__class__, left, right)
def test_known_eval(self): # Scalars a = pybamm.Scalar(4) b = pybamm.StateVector(slice(0, 1)) expr = (a + b) - (a + b) * (a + b) value = expr.evaluate(y=np.array([2])) self.assertEqual(expr.evaluate(y=np.array([2]), known_evals={})[0], value) self.assertIn((a + b).id, expr.evaluate(y=np.array([2]), known_evals={})[1]) self.assertEqual( expr.evaluate(y=np.array([2]), known_evals={})[1][(a + b).id], 6 ) # Matrices a = pybamm.Matrix(np.random.rand(5, 5)) b = pybamm.StateVector(slice(0, 5)) expr2 = (a @ b) - (a @ b) * (a @ b) + (a @ b) y_test = np.linspace(0, 1, 5) value = expr2.evaluate(y=y_test) np.testing.assert_array_equal( expr2.evaluate(y=y_test, known_evals={})[0], value ) self.assertIn((a @ b).id, expr2.evaluate(y=y_test, known_evals={})[1]) np.testing.assert_array_equal( expr2.evaluate(y=y_test, known_evals={})[1][(a @ b).id], (a @ b).evaluate(y=y_test), )
def simplify_if_constant(symbol, keep_domains=False): """ Utility function to simplify an expression tree if it evalutes to a constant scalar, vector or matrix """ if keep_domains is True: domain = symbol.domain auxiliary_domains = symbol.auxiliary_domains else: domain = None auxiliary_domains = None if symbol.is_constant(): result = symbol.evaluate_ignoring_errors() if result is not None: if (isinstance(result, numbers.Number) or (isinstance(result, np.ndarray) and result.ndim == 0) or isinstance(result, np.bool_)): return pybamm.Scalar(result) elif isinstance(result, np.ndarray) or issparse(result): if result.ndim == 1 or result.shape[1] == 1: return pybamm.Vector(result, domain=domain, auxiliary_domains=auxiliary_domains) else: # Turn matrix of zeros into sparse matrix if isinstance(result, np.ndarray) and np.all(result == 0): result = csr_matrix(result) return pybamm.Matrix(result, domain=domain, auxiliary_domains=auxiliary_domains) return symbol
def test_symbol_evaluates_to_constant_number(self): a = pybamm.Scalar(3) self.assertTrue(a.evaluates_to_constant_number()) a = pybamm.Parameter("a") self.assertFalse(a.evaluates_to_constant_number()) a = pybamm.Variable("a") self.assertFalse(a.evaluates_to_constant_number()) a = pybamm.Scalar(3) - 2 self.assertTrue(a.evaluates_to_constant_number()) a = pybamm.Vector(np.ones(5)) self.assertFalse(a.evaluates_to_constant_number()) a = pybamm.Matrix(np.ones((4, 6))) self.assertFalse(a.evaluates_to_constant_number()) a = pybamm.StateVector(slice(0, 10)) self.assertFalse(a.evaluates_to_constant_number()) # Time variable returns true a = 3 * pybamm.t + 2 self.assertFalse(a.evaluates_to_constant_number())
def test_symbol_evaluates_to_number(self): a = pybamm.Scalar(3) self.assertTrue(a.evaluates_to_number()) a = pybamm.Parameter("a") self.assertFalse(a.evaluates_to_number()) a = pybamm.Scalar(3) * pybamm.Time() self.assertTrue(a.evaluates_to_number()) # highlight difference between this function and isinstance(a, Scalar) self.assertNotIsInstance(a, pybamm.Scalar) a = pybamm.Variable("a") self.assertFalse(a.evaluates_to_number()) a = pybamm.Scalar(3) - 2 self.assertTrue(a.evaluates_to_number()) a = pybamm.Vector(np.ones(5)) self.assertFalse(a.evaluates_to_number()) a = pybamm.Matrix(np.ones((4, 6))) self.assertFalse(a.evaluates_to_number()) a = pybamm.StateVector(slice(0, 10)) self.assertFalse(a.evaluates_to_number()) # Time variable returns true a = 3 * pybamm.t + 2 self.assertTrue(a.evaluates_to_number())
def divergence_matrix(self, domain): """ Divergence matrix for finite volumes in the appropriate domain. Equivalent to div(N) = (N[1:] - N[:-1])/dx Parameters ---------- domain : list The domain(s) in which to compute the divergence matrix Returns ------- :class:`pybamm.Matrix` The (sparse) finite volume divergence matrix for the domain """ # Create appropriate submesh by combining submeshes in domain submesh_list = self.mesh.combine_submeshes(*domain) # can just use 1st entry of list to obtain the point etc submesh = submesh_list[0] e = 1 / submesh.d_edges # Create matrix using submesh n = submesh.npts + 1 sub_matrix = diags([-e, e], [0, 1], shape=(n - 1, n)) # repeat matrix for each node in secondary dimensions second_dim_len = len(submesh_list) # generate full matrix from the submatrix # Convert to csr_matrix so that we can take the index (row-slicing), which is # not supported by the default kron format # Note that this makes column-slicing inefficient, but this should not be an # issue matrix = csr_matrix(kron(eye(second_dim_len), sub_matrix)) return pybamm.Matrix(matrix)
def create_mass_matrix(self, model): """Creates mass matrix of the discretised model. Note that the model is assumed to be of the form M*y_dot = f(t,y), where M is the (possibly singular) mass matrix. Parameters ---------- model : :class:`pybamm.BaseModel` Discretised model. Must have attributes rhs, initial_conditions and boundary_conditions (all dicts of {variable: equation}) Returns ------- :class:`pybamm.Matrix` The mass matrix """ # Create list of mass matrices for each equation to be put into block # diagonal mass matrix for the model mass_list = [] # get a list of model rhs variables that are sorted according to # where they are in the state vector model_variables = model.rhs.keys() model_slices = [] for v in model_variables: if isinstance(v, pybamm.Concatenation): model_slices.append( slice( self.y_slices[v.children[0].id][0].start, self.y_slices[v.children[-1].id][0].stop, )) else: model_slices.append(self.y_slices[v.id][0]) sorted_model_variables = [ v for _, v in sorted(zip(model_slices, model_variables)) ] # Process mass matrices for the differential equations for var in sorted_model_variables: if var.domain == []: # If variable domain empty then mass matrix is just 1 mass_list.append(1.0) else: mass_list.append( self.spatial_methods[var.domain[0]].mass_matrix( var, self.bcs).entries) # Create lumped mass matrix (of zeros) of the correct shape for the # discretised algebraic equations if model.algebraic.keys(): mass_algebraic_size = model.concatenated_algebraic.shape[0] mass_algebraic = csr_matrix( (mass_algebraic_size, mass_algebraic_size)) mass_list.append(mass_algebraic) # Create block diagonal (sparse) mass matrix mass_matrix = block_diag(mass_list, format="csr") return pybamm.Matrix(mass_matrix)
def test_simplify_kron(self): A = pybamm.Matrix(np.eye(2)) b = pybamm.Vector(np.array([[4], [5]])) kron = pybamm.Kron(A, b) kron_simp = kron.simplify() self.assertIsInstance(kron_simp, pybamm.Matrix) np.testing.assert_array_equal(kron_simp.evaluate().toarray(), np.kron(A.entries, b.entries))
def test_processed_var_1D_interpolation(self): t = pybamm.t var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) eqn = t * var + x 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) eqn_sol = disc.process_symbol(eqn) t_sol = np.linspace(0, 1) y_sol = x_sol[:, np.newaxis] * np.linspace(0, 5) processed_var = pybamm.ProcessedVariable( var_sol, pybamm.Solution(t_sol, y_sol), warn=False ) # 2 vectors np.testing.assert_array_almost_equal(processed_var(t_sol, x_sol), y_sol) # 1 vector, 1 scalar np.testing.assert_array_almost_equal( processed_var(0.5, x_sol)[:, 0], 2.5 * x_sol ) np.testing.assert_array_equal( processed_var(t_sol, x_sol[-1]), x_sol[-1] * np.linspace(0, 5) ) # 2 scalars np.testing.assert_array_almost_equal( processed_var(0.5, x_sol[-1]), 2.5 * x_sol[-1] ) processed_eqn = pybamm.ProcessedVariable( eqn_sol, pybamm.Solution(t_sol, y_sol), warn=False ) # 2 vectors np.testing.assert_array_almost_equal( processed_eqn(t_sol, x_sol), t_sol * y_sol + x_sol[:, np.newaxis] ) # 1 vector, 1 scalar self.assertEqual(processed_eqn(0.5, x_sol[10:30]).shape, (20, 1)) self.assertEqual(processed_eqn(t_sol[4:9], x_sol[-1]).shape, (5,)) # 2 scalars self.assertEqual(processed_eqn(0.5, x_sol[-1]).shape, (1,)) # test x processed_x = pybamm.ProcessedVariable( disc.process_symbol(x), pybamm.Solution(t_sol, y_sol), warn=False ) np.testing.assert_array_almost_equal(processed_x(x=x_sol), x_sol[:, np.newaxis]) # On microscale r_n = pybamm.Matrix( disc.mesh["negative particle"].nodes, domain="negative particle" ) r_n.mesh = disc.mesh["negative particle"] processed_r_n = pybamm.ProcessedVariable( r_n, pybamm.Solution(t_sol, y_sol), warn=False ) np.testing.assert_array_equal(r_n.entries[:, 0], processed_r_n.entries[:, 0])
def test_processed_variable_2D_x_z(self): var = pybamm.Variable( "var", domain=["negative electrode", "separator"], auxiliary_domains={"secondary": "current collector"}, ) x = pybamm.SpatialVariable( "x", domain=["negative electrode", "separator"], auxiliary_domains={"secondary": "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] x_sol = disc.process_symbol(x).entries[:, 0] # Keep only the first iteration of entries x_sol = x_sol[:len(x_sol) // len(z_sol)] var_sol = disc.process_symbol(var) t_sol = np.linspace(0, 1) y_sol = np.ones(len(x_sol) * len(z_sol))[:, np.newaxis] * np.linspace( 0, 5) var_casadi = to_casadi(var_sol, y_sol) processed_var = pybamm.ProcessedVariable( [var_sol], [var_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), warn=False, ) np.testing.assert_array_equal( processed_var.entries, np.reshape( y_sol, [len(x_sol), len(z_sol), len(t_sol)]), ) # On edges x_s_edge = pybamm.Matrix( np.tile(disc.mesh["separator"].edges, len(z_sol)), domain="separator", auxiliary_domains={"secondary": "current collector"}, ) x_s_edge.mesh = disc.mesh["separator"] x_s_edge.secondary_mesh = disc.mesh["current collector"] x_s_casadi = to_casadi(x_s_edge, y_sol) processed_x_s_edge = pybamm.ProcessedVariable( [x_s_edge], [x_s_casadi], pybamm.Solution(t_sol, y_sol, pybamm.BaseModel(), {}), warn=False, ) np.testing.assert_array_equal( x_s_edge.entries.flatten(), processed_x_s_edge.entries[:, :, 0].T.flatten())
def gradient(self, symbol, discretised_symbol, boundary_conditions): """Matrix-vector multiplication to implement the gradient operator. The gradient w of the function u is approximated by the finite element method using the same function space as u, i.e. we solve w = grad(u), which corresponds to the weak form w*v*dx = grad(u)*v*dx, where v is a suitable test function. Parameters ---------- symbol: :class:`pybamm.Symbol` The symbol that we will take the laplacian of. discretised_symbol: :class:`pybamm.Symbol` The discretised symbol of the correct size boundary_conditions : dict The boundary conditions of the model ({symbol.id: {"negative tab": neg. tab bc, "positive tab": pos. tab bc}}) Returns ------- :class: `pybamm.Concatenation` A concatenation that contains the result of acting the discretised gradient on the child discretised_symbol. The first column corresponds to the y-component of the gradient and the second column corresponds to the z component of the gradient. """ domain = symbol.domain[0] mesh = self.mesh[domain][0] # get gradient matrix grad_y_matrix, grad_z_matrix = self.gradient_matrix( symbol, boundary_conditions) # assemble mass matrix (there is no need to zero out entries here, since # boundary conditions are already accounted for in the governing pde # for the symbol we are taking the gradient of. we just want to get the # correct weights) @skfem.bilinear_form def mass_form(u, du, v, dv, w): return u * v mass = skfem.asm(mass_form, mesh.basis) # we need the inverse mass_inv = pybamm.Matrix(inv(csc_matrix(mass))) # compute gradient grad_y = mass_inv @ (grad_y_matrix @ discretised_symbol) grad_z = mass_inv @ (grad_z_matrix @ discretised_symbol) # create concatenation grad = pybamm.Concatenation(grad_y, grad_z, check_domain=False, concat_fun=np.hstack) grad.domain = domain return grad
def _jac(self, variable): """ See :meth:`pybamm.Symbol._jac()`. """ # right cannot be a StateVector, so no need for product rule left, right = self.orphans if left.evaluates_to_number(): # Return zeros of correct size return pybamm.Matrix( csr_matrix((self.size, variable.evaluation_array.count(True)))) else: return pybamm.Kron(left.jac(variable), right)
def test_known_eval(self): # Scalars a = pybamm.Scalar(4) b = pybamm.Scalar(2) expr = (a + b) - (a + b) * (a + b) value = expr.evaluate() self.assertEqual(expr.evaluate(known_evals={})[0], value) self.assertIn((a + b).id, expr.evaluate(known_evals={})[1]) self.assertEqual(expr.evaluate(known_evals={})[1][(a + b).id], 6) # Matrices a = pybamm.Matrix(np.random.rand(5, 5)) b = pybamm.Matrix(np.random.rand(5, 5)) expr2 = (a @ b) - (a @ b) * (a @ b) + (a @ b) value = expr2.evaluate() np.testing.assert_array_equal(expr2.evaluate(known_evals={})[0], value) self.assertIn((a @ b).id, expr2.evaluate(known_evals={})[1]) np.testing.assert_array_equal( expr2.evaluate(known_evals={})[1][(a @ b).id], (a @ b).evaluate())
def test_sign(self): b = pybamm.Scalar(-4) signb = pybamm.sign(b) self.assertEqual(signb.evaluate(), -1) A = diags(np.linspace(-1, 1, 5)) b = pybamm.Matrix(A) signb = pybamm.sign(b) np.testing.assert_array_equal(np.diag(signb.evaluate().toarray()), [-1, -1, 0, 1, 1])
def test_matrix_divide_simplify(self): m = pybamm.Matrix(np.random.rand(30, 20)) zero = pybamm.Scalar(0) expr1 = (zero / m).simplify() self.assertIsInstance(expr1, pybamm.Matrix) self.assertEqual(expr1.shape, m.shape) np.testing.assert_array_equal(expr1.evaluate().toarray(), np.zeros((30, 20))) expr2 = (m / zero).simplify() self.assertIsInstance(expr2, pybamm.Matrix) self.assertEqual(expr2.shape, m.shape) np.testing.assert_array_equal(expr2.evaluate(), np.inf) m = pybamm.Matrix(np.zeros((10, 10))) a = pybamm.Scalar(7) expr3 = (m / a).simplify() self.assertIsInstance(expr3, pybamm.Matrix) self.assertEqual(expr3.shape, m.shape) np.testing.assert_array_equal(expr3.evaluate().toarray(), np.zeros((10, 10)))
def test_find_symbols_jax(self): # test sparse conversion constant_symbols = OrderedDict() variable_symbols = OrderedDict() A = pybamm.Matrix(scipy.sparse.csr_matrix(np.array([[0, 2], [0, 4]]))) pybamm.find_symbols(A, constant_symbols, variable_symbols, output_jax=True) self.assertEqual(len(variable_symbols), 0) self.assertEqual(list(constant_symbols.keys())[0], A.id) np.testing.assert_allclose( list(constant_symbols.values())[0].toarray(), A.entries.toarray() )
class Model: mass_matrix = pybamm.Matrix(np.array([[1.0, 0.0], [0.0, 0.0]])) y0 = np.array([0.0, 1.0]) terminate_events_eval = [] timescale_eval = 1 def residuals_eval(self, t, y, ydot): return np.array([0.5 * np.ones_like(y[0]) - ydot[0], 2 * y[0] - y[1]]) def jacobian_eval(self, t, y): return np.array([[0.0, 0.0], [2.0, -1.0]])
def test_processed_variable_1D(self): t = pybamm.t var = pybamm.Variable("var", domain=["negative electrode", "separator"]) x = pybamm.SpatialVariable("x", domain=["negative electrode", "separator"]) eqn = t * var + x # On nodes 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) eqn_sol = disc.process_symbol(eqn) t_sol = np.linspace(0, 1) y_sol = np.ones_like(x_sol)[:, np.newaxis] * np.linspace(0, 5) processed_var = pybamm.ProcessedVariable( var_sol, pybamm.Solution(t_sol, y_sol), warn=False ) np.testing.assert_array_equal(processed_var.entries, y_sol) np.testing.assert_array_equal(processed_var(t_sol, x_sol), y_sol) processed_eqn = pybamm.ProcessedVariable( eqn_sol, pybamm.Solution(t_sol, y_sol), warn=False ) np.testing.assert_array_equal( processed_eqn(t_sol, x_sol), t_sol * y_sol + x_sol[:, np.newaxis] ) # Test extrapolation np.testing.assert_array_equal(processed_var.entries[0], 2 * y_sol[0] - y_sol[1]) np.testing.assert_array_equal( processed_var.entries[1], 2 * y_sol[-1] - y_sol[-2] ) # On edges x_s_edge = pybamm.Matrix(disc.mesh["separator"].edges, domain="separator") x_s_edge.mesh = disc.mesh["separator"] processed_x_s_edge = pybamm.ProcessedVariable( x_s_edge, pybamm.Solution(t_sol, y_sol), warn=False ) np.testing.assert_array_equal( x_s_edge.entries[:, 0], processed_x_s_edge.entries[:, 0] ) # space only eqn = var + x eqn_sol = disc.process_symbol(eqn) t_sol = np.array([0]) y_sol = np.ones_like(x_sol)[:, np.newaxis] processed_eqn2 = pybamm.ProcessedVariable( eqn_sol, pybamm.Solution(t_sol, y_sol), warn=False ) np.testing.assert_array_equal( processed_eqn2.entries, y_sol + x_sol[:, np.newaxis] )
def _unary_jac(self, child_jac): """ See :meth:`pybamm.UnaryOperator._unary_jac()`. """ # if child.jac returns a matrix of zeros, this subsequently gives a bug # when trying to simplify the node Index(child_jac). Instead, search the # tree for StateVectors and return a matrix of zeros of the correct size # if none are found. if all([not (isinstance(n, pybamm.StateVector)) for n in self.pre_order()]): jac = csr_matrix((1, child_jac.shape[1])) return pybamm.Matrix(jac) else: return Index(child_jac, self.index)
def test_sparse_divide(self): row = np.array([0, 3, 1, 0]) col = np.array([0, 3, 1, 2]) data = np.array([4, 5, 7, 9]) S1 = coo_matrix((data, (row, col)), shape=(4, 5)) pybammS1 = pybamm.Matrix(S1) v1 = np.ones((4, 1)) pybammv1 = pybamm.Vector(v1) np.testing.assert_array_equal( (pybammS1 / pybammv1).evaluate().toarray(), S1.toarray() / v1 )
def assemble_mass_form(self, symbol, boundary_conditions, region="interior"): """ Assembles the form of the finite element mass matrix over the domain interior or boundary. Parameters ---------- symbol: :class:`pybamm.Variable` The variable corresponding to the equation for which we are calculating the mass matrix. boundary_conditions : dict The boundary conditions of the model ({symbol.id: {"negative tab": neg. tab bc, "positive tab": pos. tab bc}}) region: str, optional The domain over which to assemble the mass matrix form. Can be "interior" (default) or "boundary". Returns ------- :class:`pybamm.Matrix` The (sparse) mass matrix for the spatial method. """ # get primary domain mesh domain = symbol.domain[0] mesh = self.mesh[domain][0] # create form for mass @skfem.bilinear_form def mass_form(u, du, v, dv, w): return u * v # assemble mass matrix if region == "interior": mass = skfem.asm(mass_form, mesh.basis) if region == "boundary": mass = skfem.asm(mass_form, mesh.facet_basis) # get boundary conditions and type if symbol.id in boundary_conditions: _, neg_bc_type = boundary_conditions[symbol.id]["negative tab"] _, pos_bc_type = boundary_conditions[symbol.id]["positive tab"] if neg_bc_type == "Dirichlet": # set source terms to zero on boundary by zeroing out mass matrix self.bc_apply(mass, mesh.negative_tab_dofs, zero=True) if pos_bc_type == "Dirichlet": # set source terms to zero on boundary by zeroing out mass matrix self.bc_apply(mass, mesh.positive_tab_dofs, zero=True) return pybamm.Matrix(mass)