#
# Unary operator classes and methods
#
import numbers
import numpy as np
import pybamm
from scipy.sparse import csr_matrix
[docs]class Broadcast(pybamm.SpatialOperator):
"""A node in the expression tree representing a broadcasting operator.
Broadcasts a child to a specified domain. After discretisation, this will evaluate
to an array of the right shape for the specified domain.
For an example of broadcasts in action, see
`this example notebook
<https://github.com/pybamm-team/PyBaMM/blob/develop/examples/notebooks/expression_tree/broadcasts.ipynb>`_
Parameters
----------
child : :class:`Symbol`
child node
broadcast_domain : iterable of str
Primary domain for broadcast. This will become the domain of the symbol
broadcast_auxiliary_domains : dict of str
Auxiliary domains for broadcast.
broadcast_type : str, optional
Whether to broadcast to the full domain (primary and secondary) or only in the
primary direction. Default is "full".
name : str
name of the node
**Extends:** :class:`SpatialOperator`
"""
def __init__(
self,
child,
broadcast_domain,
broadcast_auxiliary_domains=None,
broadcast_type="full to nodes",
name=None,
):
# Convert child to scalar if it is a number
if isinstance(child, numbers.Number):
child = pybamm.Scalar(child)
# Convert domain to list if it's a string
if isinstance(broadcast_domain, str):
broadcast_domain = [broadcast_domain]
if name is None:
name = "broadcast"
# perform some basic checks and set attributes
domain, auxiliary_domains = self.check_and_set_domains(
child, broadcast_type, broadcast_domain, broadcast_auxiliary_domains
)
self.broadcast_type = broadcast_type
self.broadcast_domain = broadcast_domain
super().__init__(name, child, domain, auxiliary_domains)
[docs]class PrimaryBroadcast(Broadcast):
"""A node in the expression tree representing a primary broadcasting operator.
Broadcasts in a `primary` dimension only. That is, makes explicit copies of the
symbol in the domain specified by `broadcast_domain`. This should be used for
broadcasting from a "larger" scale to a "smaller" scale, for example broadcasting
temperature T(x) from the electrode to the particles, or broadcasting current
collector current i(y, z) from the current collector to the electrodes.
Parameters
----------
child : :class:`Symbol`
child node
broadcast_domain : iterable of str
Primary domain for broadcast. This will become the domain of the symbol
name : str
name of the node
**Extends:** :class:`SpatialOperator`
"""
def __init__(self, child, broadcast_domain, name=None):
super().__init__(
child, broadcast_domain, broadcast_type="primary to nodes", name=name
)
[docs] def check_and_set_domains(
self, child, broadcast_type, broadcast_domain, broadcast_auxiliary_domains
):
"See :meth:`Broadcast.check_and_set_domains`"
# Can only do primary broadcast from current collector to electrode or particle
# or from electrode to particle. Note current collector to particle *is* allowed
if child.domain == []:
pass
elif child.domain == ["current collector"] and broadcast_domain[0] not in [
"negative electrode",
"separator",
"positive electrode",
"negative particle",
"positive particle",
]:
raise pybamm.DomainError(
"""Primary broadcast from current collector domain must be to electrode
or separator or particle domains"""
)
elif (
child.domain[0]
in [
"negative electrode",
"separator",
"positive electrode",
]
and broadcast_domain[0] not in ["negative particle", "positive particle"]
):
raise pybamm.DomainError(
"""Primary broadcast from electrode or separator must be to particle
domains"""
)
elif child.domain[0] in ["negative particle", "positive particle"]:
raise pybamm.DomainError("Cannot do primary broadcast from particle domain")
domain = broadcast_domain
auxiliary_domains = {}
if child.domain != []:
auxiliary_domains["secondary"] = child.domain
if "secondary" in child.auxiliary_domains:
auxiliary_domains["tertiary"] = child.auxiliary_domains["secondary"]
return domain, auxiliary_domains
def _unary_new_copy(self, child):
""" See :meth:`pybamm.UnaryOperator._unary_new_copy()`. """
return self.__class__(child, self.broadcast_domain)
def _evaluate_for_shape(self):
"""
Returns a vector of NaNs to represent the shape of a Broadcast.
See :meth:`pybamm.Symbol.evaluate_for_shape_using_domain()`
"""
child_eval = self.children[0].evaluate_for_shape()
vec = pybamm.evaluate_for_shape_using_domain(self.domain)
return np.outer(child_eval, vec).reshape(-1, 1)
[docs]class PrimaryBroadcastToEdges(PrimaryBroadcast):
"""A primary broadcast onto the edges of the domain."""
def __init__(self, child, broadcast_domain, name=None):
name = name or "broadcast to edges"
super().__init__(child, broadcast_domain, name)
self.broadcast_type = "primary to edges"
def _evaluates_on_edges(self, dimension):
return True
[docs]class SecondaryBroadcast(Broadcast):
"""A node in the expression tree representing a primary broadcasting operator.
Broadcasts in a `secondary` dimension only. That is, makes explicit copies of the
symbol in the domain specified by `broadcast_domain`. This should be used for
broadcasting from a "smaller" scale to a "larger" scale, for example broadcasting
SPM particle concentrations c_s(r) from the particles to the electrodes. Note that
this wouldn't be used to broadcast particle concentrations in the DFN, since these
already depend on both x and r.
Parameters
----------
child : :class:`Symbol`
child node
broadcast_domain : iterable of str
Primary domain for broadcast. This will become the domain of the symbol
name : str
name of the node
**Extends:** :class:`SpatialOperator`
"""
def __init__(self, child, broadcast_domain, name=None):
super().__init__(
child, broadcast_domain, broadcast_type="secondary to nodes", name=name
)
[docs] def check_and_set_domains(
self, child, broadcast_type, broadcast_domain, broadcast_auxiliary_domains
):
""" See :meth:`Broadcast.check_and_set_domains` """
if child.domain == []:
raise TypeError(
"Cannot take SecondaryBroadcast of an object with empty domain. "
"Use PrimaryBroadcast instead."
)
# Can only do secondary broadcast from particle to electrode or from
# electrode to current collector
if child.domain[0] in [
"negative particle",
"positive particle",
] and broadcast_domain[0] not in [
"negative electrode",
"separator",
"positive electrode",
]:
raise pybamm.DomainError(
"""Secondary broadcast from particle domain must be to electrode or
separator domains"""
)
elif (
child.domain[0]
in [
"negative electrode",
"separator",
"positive electrode",
]
and broadcast_domain != ["current collector"]
):
raise pybamm.DomainError(
"""Secondary broadcast from electrode or separator must be to
current collector domains"""
)
elif child.domain == ["current collector"]:
raise pybamm.DomainError(
"Cannot do secondary broadcast from current collector domain"
)
# Domain stays the same as child domain and broadcast domain is secondary
# domain
domain = child.domain
auxiliary_domains = {"secondary": broadcast_domain}
# Child's secondary domain becomes tertiary domain
if "secondary" in child.auxiliary_domains:
auxiliary_domains["tertiary"] = child.auxiliary_domains["secondary"]
return domain, auxiliary_domains
def _unary_new_copy(self, child):
""" See :meth:`pybamm.UnaryOperator._unary_new_copy()`. """
return SecondaryBroadcast(child, self.broadcast_domain)
def _evaluate_for_shape(self):
"""
Returns a vector of NaNs to represent the shape of a Broadcast.
See :meth:`pybamm.Symbol.evaluate_for_shape_using_domain()`
"""
child_eval = self.children[0].evaluate_for_shape()
vec = pybamm.evaluate_for_shape_using_domain(self.domain)
return np.outer(vec, child_eval).reshape(-1, 1)
[docs]class SecondaryBroadcastToEdges(SecondaryBroadcast):
"""A secondary broadcast onto the edges of a domain."""
def __init__(self, child, broadcast_domain, name=None):
name = name or "broadcast to edges"
super().__init__(child, broadcast_domain, name)
self.broadcast_type = "secondary to edges"
def _evaluates_on_edges(self, dimension):
return True
[docs]class FullBroadcast(Broadcast):
"""A class for full broadcasts."""
def __init__(self, child, broadcast_domain, auxiliary_domains, name=None):
if isinstance(auxiliary_domains, str):
auxiliary_domains = {"secondary": auxiliary_domains}
super().__init__(
child,
broadcast_domain,
broadcast_auxiliary_domains=auxiliary_domains,
broadcast_type="full to nodes",
name=name,
)
[docs] def check_and_set_domains(
self, child, broadcast_type, broadcast_domain, broadcast_auxiliary_domains
):
""" See :meth:`Broadcast.check_and_set_domains` """
# Variables on the current collector can only be broadcast to 'primary'
if child.domain == ["current collector"]:
raise pybamm.DomainError(
"Cannot do full broadcast from current collector domain"
)
domain = broadcast_domain
auxiliary_domains = broadcast_auxiliary_domains or {}
return domain, auxiliary_domains
def _unary_new_copy(self, child):
""" See :meth:`pybamm.UnaryOperator._unary_new_copy()`. """
return FullBroadcast(child, self.broadcast_domain, self.auxiliary_domains)
def _evaluate_for_shape(self):
"""
Returns a vector of NaNs to represent the shape of a Broadcast.
See :meth:`pybamm.Symbol.evaluate_for_shape_using_domain()`
"""
child_eval = self.children[0].evaluate_for_shape()
vec = pybamm.evaluate_for_shape_using_domain(
self.domain, self.auxiliary_domains
)
return child_eval * vec
[docs]class FullBroadcastToEdges(FullBroadcast):
"""
A full broadcast onto the edges of a domain (edges of primary dimension, nodes of
other dimensions)
"""
def __init__(self, child, broadcast_domain, auxiliary_domains, name=None):
name = name or "broadcast to edges"
super().__init__(child, broadcast_domain, auxiliary_domains, name)
self.broadcast_type = "full to edges"
def _evaluates_on_edges(self, dimension):
return True
[docs]def full_like(symbols, fill_value):
"""
Returns an array with the same shape, domain and auxiliary domains as the sum of the
input symbols, with a constant value given by `fill_value`.
Parameters
----------
symbols : :class:`Symbol`
Symbols whose shape to copy
fill_value : number
Value to assign
"""
# Make a symbol that combines all the children, to get the right domain
# that takes all the child symbols into account
sum_symbol = symbols[0]
for sym in symbols[1:]:
sum_symbol += sym
# Just return scalar if symbol shape is scalar
if sum_symbol.evaluates_to_number():
return pybamm.Scalar(fill_value)
try:
shape = sum_symbol.shape
# use vector or matrix
if shape[1] == 1:
array_type = pybamm.Vector
else:
array_type = pybamm.Matrix
# return dense array, except for a matrix of zeros
if shape[1] != 1 and fill_value == 0:
entries = csr_matrix(shape)
else:
entries = fill_value * np.ones(shape)
return array_type(
entries,
domain=sum_symbol.domain,
auxiliary_domains=sum_symbol.auxiliary_domains,
)
except NotImplementedError:
return FullBroadcast(
fill_value, sum_symbol.domain, sum_symbol.auxiliary_domains
)
[docs]def zeros_like(*symbols):
"""
Returns an array with the same shape, domain and auxiliary domains as the sum of the
input symbols, with each entry equal to zero.
Parameters
----------
symbols : :class:`Symbol`
Symbols whose shape to copy
"""
return full_like(symbols, 0)
[docs]def ones_like(*symbols):
"""
Returns an array with the same shape, domain and auxiliary domains as the sum of the
input symbols, with each entry equal to one.
Parameters
----------
symbols : :class:`Symbol`
Symbols whose shape to copy
"""
return full_like(symbols, 1)