def test_external_variable_diff(self): a = pybamm.ExternalVariable("a", 10) b = pybamm.Variable("b") self.assertIsInstance(a.diff(a), pybamm.Scalar) self.assertEqual(a.diff(a).evaluate(), 1) self.assertIsInstance(a.diff(b), pybamm.Scalar) self.assertEqual(a.diff(b).evaluate(), 0)
def test_external_variable_scalar(self): a = pybamm.ExternalVariable("a", 1) self.assertEqual(a.size, 1) self.assertEqual(a.evaluate(u={"a": 3}), 3) with self.assertRaisesRegex(KeyError, "External variable"): a.evaluate() with self.assertRaisesRegex(TypeError, "inputs u"): a.evaluate(u="not a dictionary")
def test_external_variable_vector(self): a = pybamm.ExternalVariable("a", 10) self.assertEqual(a.size, 10) a_test = 2 * np.ones((10, 1)) np.testing.assert_array_equal(a.evaluate(inputs={"a": a_test}), a_test) np.testing.assert_array_equal(a.evaluate(inputs={"a": 2}), a_test) with self.assertRaisesRegex(ValueError, "External variable"): a.evaluate(inputs={"a": np.ones((5, 1))})
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 test_convert_external_variable(self): casadi_t = casadi.MX.sym("t") casadi_y = casadi.MX.sym("y", 10) casadi_us = { "External 1": casadi.MX.sym("External 1", 3), "External 2": casadi.MX.sym("External 2", 10), } pybamm_y = pybamm.StateVector(slice(0, 10)) pybamm_u1 = pybamm.ExternalVariable("External 1", 3) pybamm_u2 = pybamm.ExternalVariable("External 2", 10) # External only self.assert_casadi_equal( pybamm_u1.to_casadi(casadi_t, casadi_y, casadi_us), casadi_us["External 1"]) # More complex expr = pybamm_u2 + pybamm_y self.assert_casadi_equal( expr.to_casadi(casadi_t, casadi_y, casadi_us), casadi_us["External 2"] + casadi_y, )
def test_time_derivative_of_variable(self): a = (pybamm.Variable("a")).diff(pybamm.t) self.assertIsInstance(a, pybamm.VariableDot) self.assertEqual(a.name, "a'") p = pybamm.Parameter("p") a = (1 + p * pybamm.Variable("a")).diff(pybamm.t).simplify() self.assertIsInstance(a, pybamm.Multiplication) self.assertEqual(a.children[0].name, "p") self.assertEqual(a.children[1].name, "a'") with self.assertRaises(pybamm.ModelError): a = (pybamm.Variable("a")).diff(pybamm.t).diff(pybamm.t) with self.assertRaises(pybamm.ModelError): a = pybamm.ExternalVariable("a", 1).diff(pybamm.t)
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) 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.domain, 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]][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.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: return pybamm.StateVector( *self.y_slices[symbol.id], 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 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)))
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): integral_spatial_method = self.spatial_methods[ symbol.integration_variable[0].domain[0]] out = integral_spatial_method.integral( child, disc_child, symbol._integration_dimension) out.copy_domains(symbol) return out elif isinstance(symbol, pybamm.DefiniteIntegralVector): return child_spatial_method.definite_integral_matrix( child, 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([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) elif isinstance(symbol, pybamm.UpwindDownwind): direction = symbol.name # upwind or downwind return spatial_method.upwind_or_downwind( child, disc_child, self.bcs, direction) 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 = pybamm.Index(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)))