def test_convert_array_symbols(self): # Arrays a = np.array([1, 2, 3, 4, 5]) pybamm_a = pybamm.Array(a) self.assert_casadi_equal(pybamm_a.to_casadi(), casadi.MX(a)) casadi_t = casadi.MX.sym("t") casadi_y = casadi.MX.sym("y", 10) casadi_y_dot = casadi.MX.sym("y_dot", 10) pybamm_t = pybamm.Time() pybamm_y = pybamm.StateVector(slice(0, 10)) pybamm_y_dot = pybamm.StateVectorDot(slice(0, 10)) # Time self.assertEqual(pybamm_t.to_casadi(casadi_t, casadi_y), casadi_t) # State Vector self.assert_casadi_equal(pybamm_y.to_casadi(casadi_t, casadi_y), casadi_y) # State Vector Dot self.assert_casadi_equal( pybamm_y_dot.to_casadi(casadi_t, casadi_y, casadi_y_dot), casadi_y_dot)
def test_multislice_raises(self): y1 = pybamm.StateVector(slice(0, 4), slice(7, 8)) y_dot1 = pybamm.StateVectorDot(slice(0, 4), slice(7, 8)) y2 = pybamm.StateVector(slice(4, 7)) with self.assertRaises(NotImplementedError): y1.jac(y1) with self.assertRaises(NotImplementedError): y2.jac(y1) with self.assertRaises(NotImplementedError): y_dot1.jac(y1)
def test_linear_ydot(self): y = pybamm.StateVector(slice(0, 4)) y_dot = pybamm.StateVectorDot(slice(0, 4)) u = pybamm.StateVector(slice(0, 2)) v = pybamm.StateVector(slice(2, 4)) u_dot = pybamm.StateVectorDot(slice(0, 2)) v_dot = pybamm.StateVectorDot(slice(2, 4)) y0 = np.ones(4) y_dot0 = np.ones(4) func = u_dot jacobian = np.array([[1, 0, 0, 0], [0, 1, 0, 0]]) dfunc_dy = func.jac(y_dot).evaluate(y=y0, y_dot=y_dot0) np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) func = -v_dot jacobian = np.array([[0, 0, -1, 0], [0, 0, 0, -1]]) dfunc_dy = func.jac(y_dot).evaluate(y=y0, y_dot=y_dot0) np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) func = u_dot jacobian = np.array([[0, 0, 0, 0], [0, 0, 0, 0]]) dfunc_dy = func.jac(y).evaluate(y=y0, y_dot=y_dot0) np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) func = -v_dot jacobian = np.array([[0, 0, 0, 0], [0, 0, 0, 0]]) dfunc_dy = func.jac(y).evaluate(y=y0, y_dot=y_dot0) np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) func = u jacobian = np.array([[0, 0, 0, 0], [0, 0, 0, 0]]) dfunc_dy = func.jac(y_dot).evaluate(y=y0, y_dot=y_dot0) np.testing.assert_array_equal(jacobian, dfunc_dy.toarray()) func = -v jacobian = np.array([[0, 0, 0, 0], [0, 0, 0, 0]]) dfunc_dy = func.jac(y_dot).evaluate(y=y0, y_dot=y_dot0) np.testing.assert_array_equal(jacobian, dfunc_dy.toarray())
def test_evaluate(self): sv = pybamm.StateVectorDot(slice(0, 10)) y_dot = np.linspace(0, 2, 19) np.testing.assert_array_equal(sv.evaluate(y_dot=y_dot), np.linspace(0, 1, 10)[:, np.newaxis]) # Try evaluating with a y that is too short y_dot2 = np.ones(5) with self.assertRaisesRegex( ValueError, "y_dot is too short, so value with slice is smaller than expected", ): sv.evaluate(y_dot=y_dot2)
def test_errors(self): y = pybamm.StateVector(slice(0, 10)) with self.assertRaisesRegex( ValueError, "Must provide a 'y' for converting state vectors"): y.to_casadi() y_dot = pybamm.StateVectorDot(slice(0, 10)) with self.assertRaisesRegex( ValueError, "Must provide a 'y_dot' for converting state vectors"): y_dot.to_casadi() var = pybamm.Variable("var") with self.assertRaisesRegex(TypeError, "Cannot convert symbol of type"): var.to_casadi()
def test_name(self): sv = pybamm.StateVectorDot(slice(0, 10)) self.assertEqual(sv.name, "y_dot[0:10]")
def test_diff_state_vector_dot(self): a = pybamm.StateVectorDot(slice(0, 1)) b = pybamm.StateVector(slice(1, 2)) self.assertEqual(a.diff(a).id, pybamm.Scalar(1).id) self.assertEqual(a.diff(b).id, pybamm.Scalar(0).id)
def test_check_well_posedness_variables(self): # Well-posed ODE model model = pybamm.BaseModel() whole_cell = ["negative electrode", "separator", "positive electrode"] c = pybamm.Variable("c", domain=whole_cell) d = pybamm.Variable("d", domain=whole_cell) model.rhs = {c: 5 * pybamm.div(pybamm.grad(d)) - 1, d: -c} model.initial_conditions = {c: 1, d: 2} model.boundary_conditions = { c: { "left": (0, "Dirichlet"), "right": (0, "Dirichlet") }, d: { "left": (0, "Dirichlet"), "right": (0, "Dirichlet") }, } model.check_well_posedness() # Well-posed DAE model e = pybamm.Variable("e", domain=whole_cell) model.algebraic = {e: e - c - d} model.check_well_posedness() # Underdetermined model - not enough differential equations model.rhs = {c: 5 * pybamm.div(pybamm.grad(d)) - 1} model.algebraic = {e: e - c - d} with self.assertRaisesRegex(pybamm.ModelError, "underdetermined"): model.check_well_posedness() # Underdetermined model - not enough algebraic equations model.algebraic = {} with self.assertRaisesRegex(pybamm.ModelError, "underdetermined"): model.check_well_posedness() # Overdetermined model - repeated keys model.algebraic = {c: c - d, d: e + d} with self.assertRaisesRegex(pybamm.ModelError, "overdetermined"): model.check_well_posedness() # Overdetermined model - extra keys in algebraic model.rhs = {c: 5 * pybamm.div(pybamm.grad(d)) - 1, d: -d} model.algebraic = {e: c - d} with self.assertRaisesRegex(pybamm.ModelError, "overdetermined"): model.check_well_posedness() model.rhs = {c: 1, d: -1} model.algebraic = {e: c - d} with self.assertRaisesRegex(pybamm.ModelError, "overdetermined"): model.check_well_posedness() # After discretisation, don't check for overdetermined from extra algebraic keys model = pybamm.BaseModel() model.algebraic = {c: 5 * pybamm.StateVector(slice(0, 15)) - 1} # passes with post_discretisation=True model.check_well_posedness(post_discretisation=True) # fails with post_discretisation=False (default) with self.assertRaisesRegex(pybamm.ModelError, "extra algebraic keys"): model.check_well_posedness() # before discretisation, fail if the algebraic eqn keys don't appear in the eqns model = pybamm.BaseModel() model.algebraic = {c: d - 2, d: d - c} with self.assertRaisesRegex( pybamm.ModelError, "each variable in the algebraic eqn keys must appear in the eqn", ): model.check_well_posedness() # passes when we switch the equations around model.algebraic = {c: d - c, d: d - 2} model.check_well_posedness() # after discretisation, algebraic equation without a StateVector fails model = pybamm.BaseModel() model.algebraic = { c: 1, d: pybamm.StateVector(slice(0, 15)) - pybamm.StateVector(slice(15, 30)), } with self.assertRaisesRegex( pybamm.ModelError, "each algebraic equation must contain at least one StateVector", ): model.check_well_posedness(post_discretisation=True) # model must be in semi-explicit form model = pybamm.BaseModel() model.rhs = {c: d.diff(pybamm.t), d: -1} model.initial_conditions = {c: 1, d: 1} with self.assertRaisesRegex(pybamm.ModelError, "time derivative of variable found"): model.check_well_posedness() # model must be in semi-explicit form model = pybamm.BaseModel() model.algebraic = {c: 2 * d - c, d: c * d.diff(pybamm.t) - d} model.initial_conditions = {c: 1, d: 1} with self.assertRaisesRegex(pybamm.ModelError, "time derivative of variable found"): model.check_well_posedness() # model must be in semi-explicit form model = pybamm.BaseModel() model.rhs = {c: d.diff(pybamm.t), d: -1} model.initial_conditions = {c: 1, d: 1} with self.assertRaisesRegex(pybamm.ModelError, "time derivative of variable found"): model.check_well_posedness() # model must be in semi-explicit form model = pybamm.BaseModel() model.algebraic = { d: 5 * pybamm.StateVector(slice(0, 15)) - 1, c: 5 * pybamm.StateVectorDot(slice(0, 15)) - 1, } with self.assertRaisesRegex(pybamm.ModelError, "time derivative of state vector found"): model.check_well_posedness(post_discretisation=True) # model must be in semi-explicit form model = pybamm.BaseModel() model.rhs = {c: 5 * pybamm.StateVectorDot(slice(0, 15)) - 1} model.initial_conditions = {c: 1} with self.assertRaisesRegex(pybamm.ModelError, "time derivative of state vector found"): model.check_well_posedness(post_discretisation=True)
def _process_symbol(self, symbol): """ See :meth:`Discretisation.process_symbol()`. """ if symbol.domain != []: spatial_method = self.spatial_methods[symbol.domain[0]] # If boundary conditions are provided, need to check for BCs on tabs if self.bcs: key_id = list(self.bcs.keys())[0] if any("tab" in side for side in list(self.bcs[key_id].keys())): self.bcs[key_id] = self.check_tab_conditions( symbol, self.bcs[key_id]) if isinstance(symbol, pybamm.BinaryOperator): # Pre-process children left, right = symbol.children disc_left = self.process_symbol(left) disc_right = self.process_symbol(right) if symbol.domain == []: return symbol._binary_new_copy(disc_left, disc_right) else: return spatial_method.process_binary_operators( symbol, left, right, disc_left, disc_right) elif isinstance(symbol, pybamm.UnaryOperator): child = symbol.child disc_child = self.process_symbol(child) if child.domain != []: child_spatial_method = self.spatial_methods[child.domain[0]] if isinstance(symbol, pybamm.Gradient): return child_spatial_method.gradient(child, disc_child, self.bcs) elif isinstance(symbol, pybamm.Divergence): return child_spatial_method.divergence(child, disc_child, self.bcs) elif isinstance(symbol, pybamm.Laplacian): return child_spatial_method.laplacian(child, disc_child, self.bcs) elif isinstance(symbol, pybamm.Gradient_Squared): return child_spatial_method.gradient_squared( child, disc_child, self.bcs) elif isinstance(symbol, pybamm.Mass): return child_spatial_method.mass_matrix(child, self.bcs) elif isinstance(symbol, pybamm.BoundaryMass): return child_spatial_method.boundary_mass_matrix( child, self.bcs) elif isinstance(symbol, pybamm.IndefiniteIntegral): return child_spatial_method.indefinite_integral( child, disc_child, "forward") elif isinstance(symbol, pybamm.BackwardIndefiniteIntegral): return child_spatial_method.indefinite_integral( child, disc_child, "backward") elif isinstance(symbol, pybamm.Integral): out = child_spatial_method.integral(child, disc_child) out.copy_domains(symbol) return out elif isinstance(symbol, pybamm.DefiniteIntegralVector): return child_spatial_method.definite_integral_matrix( child.domains, vector_type=symbol.vector_type) elif isinstance(symbol, pybamm.BoundaryIntegral): return child_spatial_method.boundary_integral( child, disc_child, symbol.region) elif isinstance(symbol, pybamm.Broadcast): # Broadcast new_child to the domain specified by symbol.domain # Different discretisations may broadcast differently if symbol.domain == []: symbol = disc_child * pybamm.Vector(np.array([1])) else: symbol = spatial_method.broadcast( disc_child, symbol.domain, symbol.auxiliary_domains, symbol.broadcast_type, ) return symbol elif isinstance(symbol, pybamm.DeltaFunction): return spatial_method.delta_function(symbol, disc_child) elif isinstance(symbol, pybamm.BoundaryOperator): # if boundary operator applied on "negative tab" or # "positive tab" *and* the mesh is 1D then change side to # "left" or "right" as appropriate if symbol.side in ["negative tab", "positive tab"]: mesh = self.mesh[symbol.children[0].domain[0]] if isinstance(mesh, pybamm.SubMesh1D): symbol.side = mesh.tabs[symbol.side] return child_spatial_method.boundary_value_or_flux( symbol, disc_child, self.bcs) else: return symbol._unary_new_copy(disc_child) elif isinstance(symbol, pybamm.Function): disc_children = [ self.process_symbol(child) for child in symbol.children ] return symbol._function_new_copy(disc_children) elif isinstance(symbol, pybamm.VariableDot): return pybamm.StateVectorDot( *self.y_slices[symbol.get_variable().id], domain=symbol.domain, auxiliary_domains=symbol.auxiliary_domains) elif isinstance(symbol, pybamm.Variable): # Check if variable is a standard variable or an external variable if any(symbol.id == var.id for var in self.external_variables.values()): # Look up dictionary key based on value idx = [x.id for x in self.external_variables.values() ].index(symbol.id) name, parent_and_slice = list( self.external_variables.keys())[idx] if parent_and_slice is None: # Variable didn't come from a concatenation so we can just create a # normal external variable using the symbol's name return pybamm.ExternalVariable( symbol.name, size=self._get_variable_size(symbol), domain=symbol.domain, auxiliary_domains=symbol.auxiliary_domains, ) else: # We have to use a special name since the concatenation doesn't have # a very informative name. Needs improving parent, start, end = parent_and_slice ext = pybamm.ExternalVariable( name, size=self._get_variable_size(parent), domain=parent.domain, auxiliary_domains=parent.auxiliary_domains, ) out = ext[slice(start, end)] out.domain = symbol.domain return out else: # add a try except block for a more informative error if a variable # can't be found. This should usually be caught earlier by # model.check_well_posedness, but won't be if debug_mode is False try: y_slices = self.y_slices[symbol.id] except KeyError: raise pybamm.ModelError(""" No key set for variable '{}'. Make sure it is included in either model.rhs, model.algebraic, or model.external_variables in an unmodified form (e.g. not Broadcasted) """.format(symbol.name)) return pybamm.StateVector( *y_slices, domain=symbol.domain, auxiliary_domains=symbol.auxiliary_domains) elif isinstance(symbol, pybamm.SpatialVariable): return spatial_method.spatial_variable(symbol) elif isinstance(symbol, pybamm.Concatenation): new_children = [ self.process_symbol(child) for child in symbol.children ] new_symbol = spatial_method.concatenation(new_children) return new_symbol elif isinstance(symbol, pybamm.InputParameter): # Return a new copy of the input parameter, but set the expected size # according to the domain of the input parameter expected_size = self._get_variable_size(symbol) new_input_parameter = symbol.new_copy() new_input_parameter.set_expected_size(expected_size) return new_input_parameter else: # Backup option: return new copy of the object try: return symbol.new_copy() except NotImplementedError: raise NotImplementedError( "Cannot discretise symbol of type '{}'".format( type(symbol)))