Source code for pybamm.expression_tree.variable

#
# Variable class
#
from __future__ import annotations

import numbers

import numpy as np
import sympy

import pybamm
from pybamm.type_definitions import (
    AuxiliaryDomainType,
    DomainsType,
    DomainType,
    Numeric,
)


def _process_bounds(
    values: tuple[Numeric, Numeric] | None,
) -> tuple[pybamm.Symbol, pybamm.Symbol]:
    """Validate ``(lower, upper)`` bounds (default unbounded) and convert them to symbols."""
    if values is None:
        values = (-np.inf, np.inf)

    if all(isinstance(b, numbers.Number) for b in values) and values[0] >= values[1]:
        raise ValueError(
            f"Invalid bounds {values}. "
            + "Lower bound should be strictly less than upper bound."
        )

    if len(values) != 2:
        raise ValueError(f"Invalid bounds {values}. Must be a tuple of length 2.")
    lb, ub = values

    return (pybamm.convert_to_symbol(lb), pybamm.convert_to_symbol(ub))


class VariableBase(pybamm.Symbol):
    """
    A node in the expression tree represending a dependent variable.

    This node will be discretised by :class:`.Discretisation` and converted
    to a :class:`pybamm.StateVector` node.

    Parameters
    ----------
    name : str
        name of the node
    domain : iterable of str
        list of domains that this variable is valid over
    auxiliary_domains : dict
        dictionary of auxiliary domains ({'secondary': ..., 'tertiary': ...,
        'quaternary': ...}). For example, for the single particle model, the particle
        concentration would be a Variable with domain 'negative particle' and secondary
        auxiliary domain 'current collector'. For the DFN, the particle concentration
        would be a Variable with domain 'negative particle', secondary domain
        'negative electrode' and tertiary domain 'current collector'
    domains : dict
        A dictionary equivalent to {'primary': domain, auxiliary_domains}. Either
        'domain' and 'auxiliary_domains', or just 'domains', should be provided
        (not both). In future, the 'domain' and 'auxiliary_domains' arguments may be
        deprecated.
    bounds : tuple, optional
        Physical bounds on the variable
    print_name : str, optional
        The name to use for printing. Default is None, in which case self.name is used.
    scale : float or :class:`pybamm.Symbol`, optional
        The scale of the variable, used for scaling the model when solving. The state
        vector representing this variable will be multiplied by this scale.
        Default is 1.
    reference : float or :class:`pybamm.Symbol`, optional
        The reference value of the variable, used for scaling the model when solving.
        This value will be added to the state vector representing this variable.
        Default is 0.
    """

    __slots__ = ("_bounds", "_reference", "_scale")

    def __init__(
        self,
        name: str,
        domain: DomainType = None,
        auxiliary_domains: AuxiliaryDomainType = None,
        domains: DomainsType = None,
        bounds: tuple[Numeric, Numeric] | None = None,
        print_name: str | None = None,
        scale: Numeric | None = None,
        reference: Numeric | None = None,
    ):
        if scale is None:
            scale = 1
        if reference is None:
            reference = 0
        self._scale = pybamm.convert_to_symbol(scale)
        self._reference = pybamm.convert_to_symbol(reference)
        self._bounds = _process_bounds(bounds)
        super().__init__(
            name,
            domain=domain,
            auxiliary_domains=auxiliary_domains,
            domains=domains,
        )

        if print_name is None:
            print_name = name  # use name by default
        self.print_name = print_name

    _leaf_fields = ("_scale", "_reference", "_bounds")

    bounds = pybamm.expression_tree.legacy_mutation.bounds_property

    def create_copy(
        self,
        new_children=None,
        perform_simplifications=True,
        scale: Numeric | pybamm.Symbol | None = None,
        reference: Numeric | pybamm.Symbol | None = None,
        bounds: tuple[Numeric, Numeric] | None = None,
    ):
        """
        See :meth:`pybamm.Symbol.new_copy()`.

        Parameters
        ----------
        scale, reference, bounds : optional
            Values for the copy. Any left as ``None`` are taken from ``self``.
        """
        return self.__class__(
            self.name,
            domains=self._domains,
            bounds=self.bounds if bounds is None else bounds,
            print_name=self._raw_print_name,
            scale=self.scale if scale is None else scale,
            reference=self.reference if reference is None else reference,
        )

    def _evaluate_for_shape(self):
        """See :meth:`pybamm.Symbol.evaluate_for_shape_using_domain()`"""
        return pybamm.evaluate_for_shape_using_domain(self._domains)

    def to_equation(self):
        """Convert the node and its subtree into a SymPy equation."""
        if self.print_name is not None:
            return sympy.Symbol(self.print_name)
        else:
            return self.name

    def to_json(self):
        return {
            "name": self.name,
            "domains": self.domains,
            "children": [self._scale, self._reference, self.bounds[0], self.bounds[1]],
            "print_name": self._raw_print_name,
        }

    @classmethod
    def _from_json(cls, snippet):
        children = snippet.get("children") or []
        if len(children) == 4:
            # Kernel-era shape: scale/reference/bounds carried as children.
            scale, reference, lower, upper = children
            bounds = (lower, upper)
        else:
            # Legacy compact shape: scale/reference defaulted, bounds stored as a
            # top-level [lower, upper] pair of (undecoded) nodes, or None for the
            # default. The kernel only decodes "children", so decode bounds here.
            from pybamm.expression_tree.operations.serialise_kernel import decode

            scale = reference = None
            bounds = snippet.get("bounds")
            if bounds is not None:
                bounds = tuple(decode(b) for b in bounds)
        return cls(
            snippet["name"],
            domains=snippet["domains"],
            scale=scale,
            reference=reference,
            bounds=bounds,
            print_name=snippet.get("print_name"),
        )


[docs] class Variable(VariableBase): """ A node in the expression tree represending a dependent variable. This node will be discretised by :class:`.Discretisation` and converted to a :class:`pybamm.StateVector` node. Parameters ---------- name : str name of the node domain : iterable of str, optional list of domains that this variable is valid over auxiliary_domains : dict, optional dictionary of auxiliary domains ({'secondary': ..., 'tertiary': ..., 'quaternary': ...}). For example, for the single particle model, the particle concentration would be a Variable with domain 'negative particle' and secondary auxiliary domain 'current collector'. For the DFN, the particle concentration would be a Variable with domain 'negative particle', secondary domain 'negative electrode' and tertiary domain 'current collector' domains : dict A dictionary equivalent to {'primary': domain, auxiliary_domains}. Either 'domain' and 'auxiliary_domains', or just 'domains', should be provided (not both). In future, the 'domain' and 'auxiliary_domains' arguments may be deprecated. bounds : tuple, optional Physical bounds on the variable print_name : str, optional The name to use for printing. Default is None, in which case self.name is used. scale : float or :class:`pybamm.Symbol`, optional The scale of the variable, used for scaling the model when solving. The state vector representing this variable will be multiplied by this scale. Default is 1. reference : float or :class:`pybamm.Symbol`, optional The reference value of the variable, used for scaling the model when solving. This value will be added to the state vector representing this variable. Default is 0. """ __slots__ = ()
[docs] def diff(self, variable: pybamm.Symbol): if variable == self: return pybamm.Scalar(1) elif variable == pybamm.t: # reference gets differentiated out return pybamm.VariableDot( self.name + "'", domains=self._domains, scale=self.scale ) else: return pybamm.Scalar(0)
[docs] class VariableDot(VariableBase): """ A node in the expression tree represending the time derviative of a dependent variable This node will be discretised by :class:`.Discretisation` and converted to a :class:`pybamm.StateVectorDot` node. Parameters ---------- name : str name of the node domain : iterable of str list of domains that this variable is valid over auxiliary_domains : dict dictionary of auxiliary domains ({'secondary': ..., 'tertiary': ..., 'quaternary': ...}). For example, for the single particle model, the particle concentration would be a Variable with domain 'negative particle' and secondary auxiliary domain 'current collector'. For the DFN, the particle concentration would be a Variable with domain 'negative particle', secondary domain 'negative electrode' and tertiary domain 'current collector' domains : dict A dictionary equivalent to {'primary': domain, auxiliary_domains}. Either 'domain' and 'auxiliary_domains', or just 'domains', should be provided (not both). In future, the 'domain' and 'auxiliary_domains' arguments may be deprecated. bounds : tuple, optional Physical bounds on the variable. Included for compatibility with `VariableBase`, but ignored. print_name : str, optional The name to use for printing. Default is None, in which case self.name is used. scale : float or :class:`pybamm.Symbol`, optional The scale of the variable, used for scaling the model when solving. The state vector representing this variable will be multiplied by this scale. Default is 1. reference : float or :class:`pybamm.Symbol`, optional The reference value of the variable, used for scaling the model when solving. This value will be added to the state vector representing this variable. Default is 0. """ __slots__ = ()
[docs] def get_variable(self) -> pybamm.Variable: """ return a :class:`.Variable` corresponding to this VariableDot Note: Variable._jac adds a dash to the name of the corresponding VariableDot, so we remove this here """ return Variable(self.name[:-1], domains=self._domains, scale=self.scale)
[docs] def diff(self, variable: pybamm.Symbol) -> pybamm.Scalar: if variable == self: return pybamm.Scalar(1) elif variable == pybamm.t: raise pybamm.ModelError("cannot take second time derivative of a Variable") else: return pybamm.Scalar(0)