def test_yz_average(self): a = pybamm.Scalar(1) z_average_a = pybamm.z_average(a) yz_average_a = pybamm.yz_average(a) self.assertEqual(z_average_a.id, a.id) self.assertEqual(yz_average_a.id, a.id) z_average_broad_a = pybamm.z_average( pybamm.PrimaryBroadcast(a, ["current collector"])) yz_average_broad_a = pybamm.yz_average( pybamm.PrimaryBroadcast(a, ["current collector"])) self.assertEqual(z_average_broad_a.evaluate(), np.array([1])) self.assertEqual(yz_average_broad_a.evaluate(), np.array([1])) a = pybamm.Variable("a", domain=["current collector"]) y = pybamm.SpatialVariable("y", ["current collector"]) z = pybamm.SpatialVariable("z", ["current collector"]) z_av_a = pybamm.z_average(a) yz_av_a = pybamm.yz_average(a) self.assertIsInstance(yz_av_a, pybamm.Division) self.assertIsInstance(z_av_a, pybamm.Division) self.assertIsInstance(z_av_a.children[0], pybamm.Integral) self.assertIsInstance(yz_av_a.children[0], pybamm.Integral) self.assertEqual(z_av_a.children[0].integration_variable[0].domain, z.domain) self.assertEqual(yz_av_a.children[0].integration_variable[0].domain, y.domain) self.assertEqual(yz_av_a.children[0].integration_variable[1].domain, z.domain) self.assertIsInstance(z_av_a.children[1], pybamm.Integral) self.assertIsInstance(yz_av_a.children[1], pybamm.Integral) self.assertEqual(z_av_a.children[1].integration_variable[0].domain, a.domain) self.assertEqual(z_av_a.children[1].children[0].id, pybamm.ones_like(a).id) self.assertEqual(yz_av_a.children[1].integration_variable[0].domain, y.domain) self.assertEqual(yz_av_a.children[1].integration_variable[0].domain, z.domain) self.assertEqual(yz_av_a.children[1].children[0].id, pybamm.ones_like(a).id) self.assertEqual(z_av_a.domain, []) self.assertEqual(yz_av_a.domain, []) a = pybamm.Symbol("a", domain="bad domain") with self.assertRaises(pybamm.DomainError): pybamm.z_average(a) with self.assertRaises(pybamm.DomainError): pybamm.yz_average(a) # average of symbol that evaluates on edges raises error symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") with self.assertRaisesRegex( ValueError, "Can't take the z-average of a symbol that evaluates on edges" ): pybamm.z_average(symbol_on_edges)
def simplified_subtraction(left, right): """ Note ---- We check for scalars first, then matrices. This is because (Zero Matrix) - (Zero Scalar) should return (Zero Matrix), not -(Zero Scalar). """ left, right = simplify_elementwise_binary_broadcasts(left, right) # Check for Concatenations and Broadcasts out = simplified_binary_broadcast_concatenation(left, right, simplified_subtraction) if out is not None: return out # anything added by a scalar zero returns the other child if pybamm.is_scalar_zero(left): return -right if pybamm.is_scalar_zero(right): return left # Check matrices after checking scalars if pybamm.is_matrix_zero(left): if right.evaluates_to_number(): return -right * pybamm.ones_like(left) # See comments in simplified_addition elif all(left_dim_size <= right_dim_size for left_dim_size, right_dim_size in zip( left.shape_for_testing, right.shape_for_testing)) and all( left.evaluates_on_edges(dim) == right.evaluates_on_edges(dim) for dim in ["primary", "secondary", "tertiary"]): return -right if pybamm.is_matrix_zero(right): if left.evaluates_to_number(): return left * pybamm.ones_like(right) # See comments in simplified_addition elif all(left_dim_size >= right_dim_size for left_dim_size, right_dim_size in zip( left.shape_for_testing, right.shape_for_testing)) and all( left.evaluates_on_edges(dim) == right.evaluates_on_edges(dim) for dim in ["primary", "secondary", "tertiary"]): return left # a symbol minus itself is 0s of the same shape if left.id == right.id: return pybamm.zeros_like(left) return pybamm.simplify_if_constant(pybamm.Subtraction(left, right))
def test_x_average(self): a = pybamm.Scalar(1) average_a = pybamm.x_average(a) self.assertEqual(average_a.id, a.id) average_broad_a = pybamm.x_average( pybamm.PrimaryBroadcast(a, ["negative electrode"])) self.assertEqual(average_broad_a.evaluate(), np.array([1])) conc_broad = pybamm.Concatenation( pybamm.PrimaryBroadcast(1, ["negative electrode"]), pybamm.PrimaryBroadcast(2, ["separator"]), pybamm.PrimaryBroadcast(3, ["positive electrode"]), ) average_conc_broad = pybamm.x_average(conc_broad) self.assertIsInstance(average_conc_broad, pybamm.Division) for domain in [ ["negative electrode"], ["separator"], ["positive electrode"], ["negative electrode", "separator", "positive electrode"], ]: a = pybamm.Symbol("a", domain=domain) x = pybamm.SpatialVariable("x", domain) av_a = pybamm.x_average(a) self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[0].integration_variable[0].domain, x.domain) self.assertEqual(av_a.domain, []) a = pybamm.Symbol("a", domain="new domain") av_a = pybamm.x_average(a) self.assertEqual(av_a.domain, []) self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[0].integration_variable[0].domain, a.domain) self.assertIsInstance(av_a.children[1], pybamm.Integral) self.assertEqual(av_a.children[1].integration_variable[0].domain, a.domain) self.assertEqual(av_a.children[1].children[0].id, pybamm.ones_like(a).id) # x-average of symbol that evaluates on edges raises error symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") with self.assertRaisesRegex( ValueError, "Can't take the x-average of a symbol that evaluates on edges" ): pybamm.x_average(symbol_on_edges)
def test_ones_like(self): a = pybamm.Variable("a") ones_like_a = pybamm.ones_like(a) self.assertEqual(ones_like_a.id, pybamm.Scalar(1).id) a = pybamm.Variable( "a", domain="negative electrode", auxiliary_domains={"secondary": "current collector"}, ) ones_like_a = pybamm.ones_like(a) self.assertIsInstance(ones_like_a, pybamm.FullBroadcast) self.assertEqual(ones_like_a.name, "broadcast") self.assertEqual(ones_like_a.domain, a.domain) self.assertEqual(ones_like_a.auxiliary_domains, a.auxiliary_domains) b = pybamm.Variable("b", domain="current collector") ones_like_ab = pybamm.ones_like(b, a) self.assertIsInstance(ones_like_ab, pybamm.FullBroadcast) self.assertEqual(ones_like_ab.name, "broadcast") self.assertEqual(ones_like_ab.domain, a.domain) self.assertEqual(ones_like_ab.auxiliary_domains, a.auxiliary_domains)
def z_average(symbol): """ convenience function for creating an average in the z-direction. Parameters ---------- symbol : :class:`pybamm.Symbol` The function to be averaged Returns ------- :class:`Symbol` the new averaged symbol """ # Can't take average if the symbol evaluates on edges if symbol.evaluates_on_edges("primary"): raise ValueError("Can't take the z-average of a symbol that evaluates on edges") # Symbol must have domain [] or ["current collector"] if symbol.domain not in [[], ["current collector"]]: raise pybamm.DomainError( """z-average only implemented in the 'current collector' domain, but symbol has domains {}""".format( symbol.domain ) ) # If symbol doesn't have a domain, its average value is itself if symbol.domain == []: new_symbol = symbol.new_copy() new_symbol.parent = None return new_symbol # If symbol is a Broadcast, its average value is its child elif isinstance(symbol, pybamm.Broadcast): return symbol.orphans[0] # Otherwise, use Integral to calculate average value else: # We compute the length as Integral(1, z) as this will be easier to identify # for simplifications later on and it gives the correct behaviour when using # ZeroDimensionalSpatialMethod z = pybamm.standard_spatial_vars.z v = pybamm.ones_like(symbol) l = pybamm.Integral(v, z) return Integral(symbol, z) / l
def simplified_power(left, right): left, right = simplify_elementwise_binary_broadcasts(left, right) # Check for Concatenations and Broadcasts out = simplified_binary_broadcast_concatenation(left, right, simplified_power) if out is not None: return out # anything to the power of zero is one if pybamm.is_scalar_zero(right): return pybamm.ones_like(left) # zero to the power of anything is zero if pybamm.is_scalar_zero(left): return pybamm.Scalar(0) # anything to the power of one is itself if pybamm.is_scalar_one(right): return left if isinstance(left, Multiplication): # Simplify (a * b) ** c to (a ** c) * (b ** c) # if (a ** c) is constant or (b ** c) is constant if left.left.is_constant() or left.right.is_constant(): l_left, l_right = left.orphans new_left = l_left**right new_right = l_right**right if new_left.is_constant() or new_right.is_constant(): return new_left * new_right elif isinstance(left, Division): # Simplify (a / b) ** c to (a ** c) / (b ** c) # if (a ** c) is constant or (b ** c) is constant if left.left.is_constant() or left.right.is_constant(): l_left, l_right = left.orphans new_left = l_left**right new_right = l_right**right if new_left.is_constant() or new_right.is_constant(): return new_left / new_right return pybamm.simplify_if_constant(pybamm.Power(left, right))
def _process_symbol(self, symbol): """ See :meth:`ParameterValues.process_symbol()`. """ if isinstance(symbol, pybamm.Parameter): value = self[symbol.name] if isinstance(value, numbers.Number): # Scalar inherits name (for updating parameters) and domain (for # Broadcast) return pybamm.Scalar(value, name=symbol.name, domain=symbol.domain) elif isinstance(value, pybamm.Symbol): new_value = self.process_symbol(value) new_value.domain = symbol.domain return new_value else: raise TypeError("Cannot process parameter '{}'".format(value)) elif isinstance(symbol, pybamm.FunctionParameter): new_children = [] for child in symbol.children: if symbol.diff_variable is not None and any( x.id == symbol.diff_variable.id for x in child.pre_order()): # Wrap with NotConstant to avoid simplification, # which would stop symbolic diff from working properly new_child = pybamm.NotConstant(child.new_copy()) new_children.append(self.process_symbol(new_child)) else: new_children.append(self.process_symbol(child)) function_name = self[symbol.name] # Create Function or Interpolant or Scalar object if isinstance(function_name, tuple): # If function_name is a tuple then it should be (name, data) and we need # to create an Interpolant name, data = function_name function = pybamm.Interpolant(data[:, 0], data[:, 1], *new_children, name=name) # Define event to catch extrapolation. In these events the sign is # important: it should be positive inside of the range and negative # outside of it self.parameter_events.append( pybamm.Event( "Interpolant {} lower bound".format(name), pybamm.min(new_children[0] - min(data[:, 0])), pybamm.EventType.INTERPOLANT_EXTRAPOLATION, )) self.parameter_events.append( pybamm.Event( "Interpolant {} upper bound".format(name), pybamm.min(max(data[:, 0]) - new_children[0]), pybamm.EventType.INTERPOLANT_EXTRAPOLATION, )) elif isinstance(function_name, numbers.Number): # If the "function" is provided is actually a scalar, return a Scalar # object instead of throwing an error. # Also use ones_like so that we get the right shapes function = pybamm.Scalar( function_name, name=symbol.name) * pybamm.ones_like(*new_children) elif (isinstance(function_name, pybamm.Symbol) and function_name.evaluates_to_number()): # If the "function" provided is a pybamm scalar-like, use ones_like to # get the right shape # This also catches input parameters function = function_name * pybamm.ones_like(*new_children) elif callable(function_name): # otherwise evaluate the function to create a new PyBaMM object function = function_name(*new_children) elif isinstance(function_name, pybamm.Interpolant): function = function_name else: raise TypeError( "Parameter provided for '{}' ".format(symbol.name) + "is of the wrong type (should either be scalar-like or callable)" ) # Differentiate if necessary if symbol.diff_variable is None: function_out = function else: # return differentiated function new_diff_variable = self.process_symbol(symbol.diff_variable) function_out = function.diff(new_diff_variable) # Convert possible float output to a pybamm scalar if isinstance(function_out, numbers.Number): return pybamm.Scalar(function_out) # Process again just to be sure return self.process_symbol(function_out) elif isinstance(symbol, pybamm.BinaryOperator): # process children new_left = self.process_symbol(symbol.left) new_right = self.process_symbol(symbol.right) # Special case for averages, which can appear as "integral of a broadcast" # divided by "integral of a broadcast" # this construction seems very specific but can appear often when averaging if (isinstance(symbol, pybamm.Division) # right is integral(Broadcast(1)) and (isinstance(new_right, pybamm.Integral) and isinstance(new_right.child, pybamm.Broadcast) and new_right.child.child.id == pybamm.Scalar(1).id) # left is integral and isinstance(new_left, pybamm.Integral)): # left is integral(Broadcast) if (isinstance(new_left.child, pybamm.Broadcast) and new_left.child.child.domain == []): integrand = new_left.child if integrand.auxiliary_domains == {}: return integrand.orphans[0] else: domain = integrand.auxiliary_domains["secondary"] if "tertiary" not in integrand.auxiliary_domains: return pybamm.PrimaryBroadcast( integrand.orphans[0], domain) else: auxiliary_domains = { "secondary": integrand.auxiliary_domains["tertiary"] } return pybamm.FullBroadcast( integrand.orphans[0], domain, auxiliary_domains) # left is "integral of concatenation of broadcasts" elif isinstance(new_left.child, pybamm.Concatenation) and all( isinstance(child, pybamm.Broadcast) for child in new_left.child.children): return self.process_symbol(pybamm.x_average( new_left.child)) # make new symbol, ensure domain remains the same new_symbol = symbol._binary_new_copy(new_left, new_right) new_symbol.domain = symbol.domain return new_symbol # Unary operators elif isinstance(symbol, pybamm.UnaryOperator): new_child = self.process_symbol(symbol.child) new_symbol = symbol._unary_new_copy(new_child) # ensure domain remains the same new_symbol.domain = symbol.domain return new_symbol # Functions elif isinstance(symbol, pybamm.Function): new_children = [ self.process_symbol(child) for child in symbol.children ] return symbol._function_new_copy(new_children) # Concatenations elif isinstance(symbol, pybamm.Concatenation): new_children = [ self.process_symbol(child) for child in symbol.children ] return symbol._concatenation_new_copy(new_children) else: # Backup option: return new copy of the object try: return symbol.new_copy() except NotImplementedError: raise NotImplementedError( "Cannot process parameters for symbol of type '{}'".format( type(symbol)))
def x_average(symbol): """convenience function for creating an average in the x-direction Parameters ---------- symbol : :class:`pybamm.Symbol` The function to be averaged Returns ------- :class:`Symbol` the new averaged symbol """ # Can't take average if the symbol evaluates on edges if symbol.evaluates_on_edges("primary"): raise ValueError( "Can't take the x-average of a symbol that evaluates on edges") # If symbol doesn't have a domain, its average value is itself if symbol.domain in [[], ["current collector"]]: new_symbol = symbol.new_copy() new_symbol.parent = None return new_symbol # If symbol is a Broadcast, its average value is its child elif isinstance(symbol, pybamm.Broadcast): return symbol.orphans[0] # If symbol is a concatenation of Broadcasts, its average value is its child elif (isinstance(symbol, pybamm.Concatenation) and all( isinstance(child, pybamm.Broadcast) for child in symbol.children) and symbol.domain == ["negative electrode", "separator", "positive electrode"]): a, b, c = [orp.orphans[0] for orp in symbol.orphans] if a.id == b.id == c.id: return a else: geo = pybamm.geometric_parameters l_n = geo.l_n l_s = geo.l_s l_p = geo.l_p return (l_n * a + l_s * b + l_p * c) / (l_n + l_s + l_p) # Otherwise, use Integral to calculate average value else: geo = pybamm.geometric_parameters if symbol.domain == ["negative electrode"]: x = pybamm.standard_spatial_vars.x_n l = geo.l_n elif symbol.domain == ["separator"]: x = pybamm.standard_spatial_vars.x_s l = geo.l_s elif symbol.domain == ["positive electrode"]: x = pybamm.standard_spatial_vars.x_p l = geo.l_p elif symbol.domain == [ "negative electrode", "separator", "positive electrode" ]: x = pybamm.standard_spatial_vars.x l = pybamm.Scalar(1) elif symbol.domain == ["negative particle"]: x = pybamm.standard_spatial_vars.x_n l = geo.l_n elif symbol.domain == ["positive particle"]: x = pybamm.standard_spatial_vars.x_p l = geo.l_p else: x = pybamm.SpatialVariable("x", domain=symbol.domain) v = pybamm.ones_like(symbol) l = pybamm.Integral(v, x) return Integral(symbol, x) / l
def _process_symbol(self, symbol): """ See :meth:`ParameterValues.process_symbol()`. """ if isinstance(symbol, pybamm.Parameter): value = self[symbol.name] if isinstance(value, numbers.Number): # Scalar inherits name (for updating parameters) and domain (for # Broadcast) return pybamm.Scalar(value, name=symbol.name, domain=symbol.domain) elif isinstance(value, pybamm.InputParameter): value.domain = symbol.domain return value elif isinstance(symbol, pybamm.FunctionParameter): new_children = [ self.process_symbol(child) for child in symbol.children ] function_name = self[symbol.name] # Create Function or Interpolant or Scalar object if isinstance(function_name, tuple): # If function_name is a tuple then it should be (name, data) and we need # to create an Interpolant name, data = function_name function = pybamm.Interpolant(data, *new_children, name=name) elif isinstance(function_name, numbers.Number): # If the "function" is provided is actually a scalar, return a Scalar # object instead of throwing an error. # Also use ones_like so that we get the right shapes function = pybamm.Scalar( function_name, name=symbol.name) * pybamm.ones_like(*new_children) elif isinstance(function_name, pybamm.InputParameter): # Replace the function with an input parameter function = function_name elif (isinstance(function_name, pybamm.Symbol) and function_name.evaluates_to_number()): # If the "function" provided is a pybamm scalar-like, use ones_like to # get the right shape function = function_name * pybamm.ones_like(*new_children) elif callable(function_name): # otherwise evaluate the function to create a new PyBaMM object function = function_name(*new_children) else: raise TypeError( "Parameter provided for '{}' ".format(symbol.name) + "is of the wrong type (should either be scalar-like or callable)" ) # Differentiate if necessary if symbol.diff_variable is None: function_out = function else: # return differentiated function new_diff_variable = self.process_symbol(symbol.diff_variable) function_out = function.diff(new_diff_variable) # Convert possible float output to a pybamm scalar if isinstance(function_out, numbers.Number): return pybamm.Scalar(function_out) # Process again just to be sure return self.process_symbol(function_out) elif isinstance(symbol, pybamm.BinaryOperator): # process children new_left = self.process_symbol(symbol.left) new_right = self.process_symbol(symbol.right) # make new symbol, ensure domain remains the same new_symbol = symbol._binary_new_copy(new_left, new_right) new_symbol.domain = symbol.domain return new_symbol # Unary operators elif isinstance(symbol, pybamm.UnaryOperator): new_child = self.process_symbol(symbol.child) new_symbol = symbol._unary_new_copy(new_child) # ensure domain remains the same new_symbol.domain = symbol.domain return new_symbol # Functions elif isinstance(symbol, pybamm.Function): new_children = [ self.process_symbol(child) for child in symbol.children ] return symbol._function_new_copy(new_children) # Concatenations elif isinstance(symbol, pybamm.Concatenation): new_children = [ self.process_symbol(child) for child in symbol.children ] return symbol._concatenation_new_copy(new_children) else: # Backup option: return new copy of the object try: return symbol.new_copy() except NotImplementedError: raise NotImplementedError( "Cannot process parameters for symbol of type '{}'".format( type(symbol)))
def x_average(symbol): """ convenience function for creating an average in the x-direction Parameters ---------- symbol : :class:`pybamm.Symbol` The function to be averaged Returns ------- :class:`Symbol` the new averaged symbol """ # Can't take average if the symbol evaluates on edges if symbol.evaluates_on_edges("primary"): raise ValueError( "Can't take the x-average of a symbol that evaluates on edges") # If symbol doesn't have a domain, its average value is itself if symbol.domain in [[], ["current collector"]]: new_symbol = symbol.new_copy() new_symbol.parent = None return new_symbol # If symbol is a primary or full broadcast, reduce by one dimension if isinstance(symbol, (pybamm.PrimaryBroadcast, pybamm.FullBroadcast)): return symbol.reduce_one_dimension() # If symbol is a concatenation of Broadcasts, its average value is its child elif (isinstance(symbol, pybamm.Concatenation) and all( isinstance(child, pybamm.Broadcast) for child in symbol.children) and symbol.domain == ["negative electrode", "separator", "positive electrode"]): a, b, c = [orp.orphans[0] for orp in symbol.orphans] geo = pybamm.geometric_parameters l_n = geo.l_n l_s = geo.l_s l_p = geo.l_p out = (l_n * a + l_s * b + l_p * c) / (l_n + l_s + l_p) # To respect domains we may need to broadcast the child back out child = symbol.children[0] # If symbol being returned doesn't have empty domain, return it if out.domain != []: return out # Otherwise we may need to broadcast it elif child.auxiliary_domains == {}: return out else: domain = child.auxiliary_domains["secondary"] if "tertiary" not in child.auxiliary_domains: return pybamm.PrimaryBroadcast(out, domain) else: auxiliary_domains = { "secondary": child.auxiliary_domains["tertiary"] } return pybamm.FullBroadcast(out, domain, auxiliary_domains) # Otherwise, use Integral to calculate average value else: geo = pybamm.geometric_parameters # Even if domain is "negative electrode", "separator", or # "positive electrode", and we know l, we still compute it as Integral(1, x) # as this will be easier to identify for simplifications later on if symbol.domain == ["negative particle"]: x = pybamm.standard_spatial_vars.x_n l = geo.l_n elif symbol.domain == ["positive particle"]: x = pybamm.standard_spatial_vars.x_p l = geo.l_p else: x = pybamm.SpatialVariable("x", domain=symbol.domain) v = pybamm.ones_like(symbol) l = pybamm.Integral(v, x) return Integral(symbol, x) / l
def simplified_addition(left, right): """ Note ---- We check for scalars first, then matrices. This is because (Zero Matrix) + (Zero Scalar) should return (Zero Matrix), not (Zero Scalar). """ left, right = simplify_elementwise_binary_broadcasts(left, right) # Check for Concatenations and Broadcasts out = simplified_binary_broadcast_concatenation(left, right, simplified_addition) if out is not None: return out # anything added by a scalar zero returns the other child elif pybamm.is_scalar_zero(left): return right elif pybamm.is_scalar_zero(right): return left # Check matrices after checking scalars elif pybamm.is_matrix_zero(left): if right.evaluates_to_number(): return right * pybamm.ones_like(left) # If left object is zero and has size smaller than or equal to right object in # all dimensions, we can safely return the right object. For example, adding a # zero vector a matrix, we can just return the matrix elif all(left_dim_size <= right_dim_size for left_dim_size, right_dim_size in zip( left.shape_for_testing, right.shape_for_testing)) and all( left.evaluates_on_edges(dim) == right.evaluates_on_edges(dim) for dim in ["primary", "secondary", "tertiary"]): return right elif pybamm.is_matrix_zero(right): if left.evaluates_to_number(): return left * pybamm.ones_like(right) # See comment above elif all(left_dim_size >= right_dim_size for left_dim_size, right_dim_size in zip( left.shape_for_testing, right.shape_for_testing)) and all( left.evaluates_on_edges(dim) == right.evaluates_on_edges(dim) for dim in ["primary", "secondary", "tertiary"]): return left # Return constant if both sides are constant if left.is_constant() and right.is_constant(): return pybamm.simplify_if_constant(pybamm.Addition(left, right)) # Simplify A @ c + B @ c to (A + B) @ c if (A + B) is constant # This is a common construction that appears from discretisation of spatial # operators elif (isinstance(left, MatrixMultiplication) and isinstance(right, MatrixMultiplication) and left.right.id == right.right.id): l_left, l_right = left.orphans r_left = right.orphans[0] new_left = l_left + r_left if new_left.is_constant(): new_sum = new_left @ l_right new_sum.copy_domains(pybamm.Addition(left, right)) return new_sum if isinstance(right, pybamm.Addition) and left.is_constant(): # Simplify a + (b + c) to (a + b) + c if (a + b) is constant if right.left.is_constant(): r_left, r_right = right.orphans return (left + r_left) + r_right # Simplify a + (b + c) to (a + c) + b if (a + c) is constant elif right.right.is_constant(): r_left, r_right = right.orphans return (left + r_right) + r_left if isinstance(left, pybamm.Addition) and right.is_constant(): # Simplify (a + b) + c to a + (b + c) if (b + c) is constant if left.right.is_constant(): l_left, l_right = left.orphans return l_left + (l_right + right) # Simplify (a + b) + c to (a + c) + b if (a + c) is constant elif left.left.is_constant(): l_left, l_right = left.orphans return (l_left + right) + l_right return pybamm.simplify_if_constant(pybamm.Addition(left, right))
def simplified_division(left, right): left, right = simplify_elementwise_binary_broadcasts(left, right) # Check for Concatenations and Broadcasts out = simplified_binary_broadcast_concatenation(left, right, simplified_division) if out is not None: return out # zero divided by anything returns zero (being careful about shape) if pybamm.is_scalar_zero(left): return pybamm.zeros_like(right) # matrix zero divided by anything returns matrix zero (i.e. itself) if pybamm.is_matrix_zero(left): return pybamm.zeros_like(pybamm.Division(left, right)) # anything divided by zero raises error if pybamm.is_scalar_zero(right): raise ZeroDivisionError # anything divided by one is itself if pybamm.is_scalar_one(right): return left # a symbol divided by itself is 1s of the same shape if left.id == right.id: return pybamm.ones_like(left) # anything multiplied by a matrix one returns itself if # - the shapes are the same # - both left and right evaluate on edges, or both evaluate on nodes, in all # dimensions # (and possibly more generally, but not implemented here) try: if left.shape_for_testing == right.shape_for_testing and all( left.evaluates_on_edges(dim) == right.evaluates_on_edges(dim) for dim in ["primary", "secondary", "tertiary"]): if pybamm.is_matrix_one(right): return left # also check for negative one if pybamm.is_matrix_minus_one(right): return -left except NotImplementedError: pass # Return constant if both sides are constant if left.is_constant() and right.is_constant(): return pybamm.simplify_if_constant(pybamm.Division(left, right)) # Simplify (B @ c) / a to (B / a) @ c if (B / a) is constant # This is a common construction that appears from discretisation of averages elif isinstance(left, MatrixMultiplication) and right.is_constant(): l_left, l_right = left.orphans new_left = l_left / right if new_left.is_constant(): # be careful about domains to avoid weird errors new_left.clear_domains() new_division = new_left @ l_right # Keep the domain of the old left new_division.copy_domains(left) return new_division if isinstance(left, Multiplication): # Simplify (a * b) / c to (a / c) * b if (a / c) is constant if left.left.is_constant(): l_left, l_right = left.orphans new_left = l_left / right if new_left.is_constant(): return new_left * l_right # Simplify (a * b) / c to a * (b / c) if (b / c) is constant elif left.right.is_constant(): l_left, l_right = left.orphans new_right = l_right / right if new_right.is_constant(): return l_left * new_right # Negation simplifications elif isinstance(left, pybamm.Negate) and right.is_constant(): # Simplify (-a) / b to a / (-b) if (-b) is constant return left.orphans[0] / (-right) elif isinstance(right, pybamm.Negate) and left.is_constant(): # Simplify a / (-b) to (-a) / b if (-a) is constant return (-left) / right.orphans[0] return pybamm.simplify_if_constant(pybamm.Division(left, right))
def test_x_average(self): a = pybamm.Scalar(4) average_a = pybamm.x_average(a) self.assertEqual(average_a.id, a.id) # average of a broadcast is the child average_broad_a = pybamm.x_average( pybamm.PrimaryBroadcast(a, ["negative electrode"])) self.assertEqual(average_broad_a.id, pybamm.Scalar(4).id) # average of a number times a broadcast is the number times the child average_two_broad_a = pybamm.x_average( 2 * pybamm.PrimaryBroadcast(a, ["negative electrode"])) self.assertEqual(average_two_broad_a.id, pybamm.Scalar(8).id) average_t_broad_a = pybamm.x_average( pybamm.t * pybamm.PrimaryBroadcast(a, ["negative electrode"])) self.assertEqual(average_t_broad_a.id, (pybamm.t * pybamm.Scalar(4)).id) # x-average of concatenation of broadcasts conc_broad = pybamm.concatenation( pybamm.PrimaryBroadcast(1, ["negative electrode"]), pybamm.PrimaryBroadcast(2, ["separator"]), pybamm.PrimaryBroadcast(3, ["positive electrode"]), ) average_conc_broad = pybamm.x_average(conc_broad) self.assertIsInstance(average_conc_broad, pybamm.Division) self.assertEqual(average_conc_broad.domain, []) # with auxiliary domains conc_broad = pybamm.concatenation( pybamm.FullBroadcast( 1, ["negative electrode"], auxiliary_domains={"secondary": "current collector"}, ), pybamm.FullBroadcast( 2, ["separator"], auxiliary_domains={"secondary": "current collector"}), pybamm.FullBroadcast( 3, ["positive electrode"], auxiliary_domains={"secondary": "current collector"}, ), ) average_conc_broad = pybamm.x_average(conc_broad) self.assertIsInstance(average_conc_broad, pybamm.PrimaryBroadcast) self.assertEqual(average_conc_broad.domain, ["current collector"]) conc_broad = pybamm.concatenation( pybamm.FullBroadcast( 1, ["negative electrode"], auxiliary_domains={ "secondary": "current collector", "tertiary": "test", }, ), pybamm.FullBroadcast( 2, ["separator"], auxiliary_domains={ "secondary": "current collector", "tertiary": "test", }, ), pybamm.FullBroadcast( 3, ["positive electrode"], auxiliary_domains={ "secondary": "current collector", "tertiary": "test", }, ), ) average_conc_broad = pybamm.x_average(conc_broad) self.assertIsInstance(average_conc_broad, pybamm.FullBroadcast) self.assertEqual(average_conc_broad.domain, ["current collector"]) self.assertEqual(average_conc_broad.auxiliary_domains, {"secondary": ["test"]}) # x-average of broadcast for domain in [ ["negative electrode"], ["separator"], ["positive electrode"], ]: a = pybamm.Variable("a", domain=domain) x = pybamm.SpatialVariable("x", domain) av_a = pybamm.x_average(a) self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[0].integration_variable[0].domain, x.domain) self.assertEqual(av_a.domain, []) # whole electrode domain is different as the division by 1 gets simplified out domain = ["negative electrode", "separator", "positive electrode"] a = pybamm.Variable("a", domain=domain) x = pybamm.SpatialVariable("x", domain) av_a = pybamm.x_average(a) self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[0].integration_variable[0].domain, x.domain) self.assertEqual(av_a.domain, []) a = pybamm.Variable("a", domain="new domain") av_a = pybamm.x_average(a) self.assertEqual(av_a.domain, []) self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[0].integration_variable[0].domain, a.domain) self.assertIsInstance(av_a.children[1], pybamm.Integral) self.assertEqual(av_a.children[1].integration_variable[0].domain, a.domain) self.assertEqual(av_a.children[1].children[0].id, pybamm.ones_like(a).id) # x-average of symbol that evaluates on edges raises error symbol_on_edges = pybamm.PrimaryBroadcastToEdges(1, "domain") with self.assertRaisesRegex( ValueError, "Can't take the x-average of a symbol that evaluates on edges" ): pybamm.x_average(symbol_on_edges) # Particle domains geo = pybamm.geometric_parameters l_n = geo.l_n l_p = geo.l_p a = pybamm.Symbol( "a", domain="negative particle", auxiliary_domains={"secondary": "negative electrode"}, ) av_a = pybamm.x_average(a) self.assertEqual(a.domain, ["negative particle"]) self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[1].id, l_n.id) a = pybamm.Symbol( "a", domain="positive particle", auxiliary_domains={"secondary": "positive electrode"}, ) av_a = pybamm.x_average(a) self.assertEqual(a.domain, ["positive particle"]) self.assertIsInstance(av_a, pybamm.Division) self.assertIsInstance(av_a.children[0], pybamm.Integral) self.assertEqual(av_a.children[1].id, l_p.id)