コード例 #1
0
    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)
コード例 #2
0
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))
コード例 #3
0
    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)
コード例 #4
0
ファイル: test_broadcasts.py プロジェクト: wpf0221/PyBaMM
    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)
コード例 #5
0
ファイル: unary_operators.py プロジェクト: TomTranter/PyBaMM
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
コード例 #6
0
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))
コード例 #7
0
    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)))
コード例 #8
0
ファイル: unary_operators.py プロジェクト: yonas-y/PyBaMM
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
コード例 #9
0
ファイル: parameter_values.py プロジェクト: dalonsoa/PyBaMM
    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)))
コード例 #10
0
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
コード例 #11
0
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))
コード例 #12
0
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))
コード例 #13
0
    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)