Source code for pybamm.expression_tree.parameter

#
# Parameter classes
#
from __future__ import annotations

import sys
from typing import Literal

import numpy as np
import sympy

import pybamm
from pybamm.expression_tree.symbol import _ImmutableSequence


class InputNames(_ImmutableSequence):
    """An immutable list of a function parameter's input names."""

    __slots__ = ()
    _immutable_message = (
        "Function parameter input names are immutable; construct a new "
        "FunctionParameter with the desired inputs"
    )


[docs] class Parameter(pybamm.Symbol): """ A node in the expression tree representing a parameter. This node will be replaced by a :class:`pybamm.Scalar` node Parameters ---------- name : str name of the node """ __slots__ = () def __init__(self, name: str) -> None: super().__init__(name)
[docs] def create_copy( self, new_children=None, perform_simplifications=True, ) -> pybamm.Parameter: """See :meth:`pybamm.Symbol.new_copy()`.""" return Parameter(self.name)
def _evaluate_for_shape(self) -> float: """ Returns the scalar 'NaN' to represent the shape of a parameter. See :meth:`pybamm.Symbol.evaluate_for_shape()` """ return np.nan
[docs] def is_constant(self) -> Literal[False]: """See :meth:`pybamm.Symbol.is_constant()`.""" # Parameter is not constant since it can become an InputParameter return False
[docs] def to_equation(self) -> sympy.Symbol: """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 sympy.Symbol(self.name)
[docs] def to_json(self): return {"name": self.name, "domains": self.domains}
@classmethod def _from_json(cls, snippet): return cls(snippet["name"])
[docs] class FunctionParameter(pybamm.Symbol): """ A node in the expression tree representing a function parameter. This node will be replaced by a :class:`pybamm.Function` node if a callable function is passed to the parameter values, and otherwise (in some rarer cases, such as constant current) a :class:`pybamm.Scalar` node. Parameters ---------- name : str name of the node inputs : dict A dictionary with string keys and :class:`pybamm.Symbol` values representing the function inputs. The string keys should provide a reasonable description of what the input to the function is (e.g. "Electrolyte concentration [mol.m-3]") diff_variable : :class:`pybamm.Symbol`, optional if diff_variable is specified, the FunctionParameter node will be replaced by a :class:`pybamm.Function` and then differentiated with respect to diff_variable. Default is None. print_name : str, optional The name to show when printing. Default is 'calculate', in which case the name is calculated using sys._getframe(). post_processor : callable, optional A callable that takes a pybamm.Symbol and returns a pybamm.Symbol. If provided, it will be applied to the function output after the function is evaluated during parameter processing. This can be used to apply regularisation or other transformations. Default is None. """ __slots__ = ("_input_names", "diff_variable", "post_processor") _leaf_fields = ("diff_variable",) _id_excluded_fields = ("post_processor",) def __init__( self, name: str, inputs: dict[str, pybamm.Symbol], diff_variable: pybamm.Symbol | None = None, print_name="calculate", post_processor=None, ) -> None: # assign diff variable self.diff_variable = diff_variable self.post_processor = post_processor children_list = list(inputs.values()) # Turn numbers into scalars for idx, child in enumerate(children_list): if isinstance(child, float | int | np.number): children_list[idx] = pybamm.Scalar(child) domains = self._combine_children_domains(children_list) super().__init__(name, children=children_list, domains=domains) self._input_names = InputNames(self._check_input_names(list(inputs.keys()))) # Use the inspect module to find the function's "short name" from the # Parameters module that called it if print_name != "calculate": self.print_name = print_name else: frame = sys._getframe().f_back if frame is not None: print_name = frame.f_code.co_name if print_name.startswith("_"): self.print_name = None else: try: parent_param = frame.f_locals["self"] except KeyError: parent_param = None if ( hasattr(parent_param, "domain") and parent_param.domain is not None ): # add "_n" or "_s" or "_p" if this comes from a Parameter class with # a domain d = parent_param.domain[0] print_name += f"_{d}" self.print_name = print_name def print_input_names(self): if self._input_names: for inp in self._input_names: print(inp) @property def input_names(self) -> InputNames: """The names of the function's inputs, as an immutable list.""" return self._input_names @input_names.setter def input_names(self, value: list[str]) -> None: pybamm.expression_tree.symbol._warn_mutation( "input_names", "Construct a new FunctionParameter with the desired inputs." ) if isinstance(value, InputNames): value = list(value) object.__setattr__( self, "_input_names", InputNames(self._check_input_names(value) or []) ) def __setstate__(self, state): super().__setstate__(state) names = state.get("_input_names") if names is not None and type(names) is not InputNames: # pickled when input names were stored as plain lists object.__setattr__(self, "_input_names", InputNames(names)) @staticmethod def _check_input_names(inp): if inp: if inp.__class__ is list: for i in inp: if i.__class__ is not str: raise TypeError( "Inputs must be a provided as" + "a dictionary of the form:" + "{{str: :class:`pybamm.Symbol`}}" ) else: raise TypeError( "Inputs must be a provided as" + " a dictionary of the form:" + "{{str: :class:`pybamm.Symbol`}}" ) return inp
[docs] def diff(self, variable: pybamm.Symbol) -> pybamm.FunctionParameter: """See :meth:`pybamm.Symbol.diff()`.""" # return a new FunctionParameter, that knows it will need to be differentiated # when the parameters are set children_list = self._children input_names = self._input_names input_dict = {input_names[i]: children_list[i] for i in range(len(input_names))} return FunctionParameter( self.name, input_dict, diff_variable=variable, print_name=self.print_name + "'", post_processor=self.post_processor, )
[docs] def create_copy(self, new_children=None, perform_simplifications=True): """See :meth:`pybamm.Symbol.new_copy()`.""" children = self._children_for_copying(new_children) input_dict = dict(zip(self._input_names, children, strict=True)) return FunctionParameter( self.name, input_dict, diff_variable=self.diff_variable, print_name=self.print_name, post_processor=self.post_processor, )
def _evaluate_for_shape(self): """ Returns the sum of the evaluated children See :meth:`pybamm.Symbol.evaluate_for_shape()` """ # add 1e-300 to avoid division by zero return sum(child.evaluate_for_shape() for child in self._children) + 1e-300
[docs] def to_equation(self) -> sympy.Symbol: """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 sympy.Symbol(self.name)
# inputs -> children + input_names; diff_variable -> a trailing child (flagged # by has_diff_variable); post_processor is a non-serialisable transient callable. _serialise_derived_params = frozenset({"inputs", "diff_variable", "post_processor"})
[docs] def to_json(self): children = list(self._children) if self.diff_variable is not None: children = [*children, self.diff_variable] return { "name": self.name, "domains": self.domains, "input_names": list(self.input_names), "print_name": self.print_name, "has_diff_variable": self.diff_variable is not None, "children": children, }
@classmethod def _from_json(cls, snippet): if "input_names" in snippet: # Kernel-era shape: inputs carried as children keyed by input_names; # diff_variable is the trailing child when present. names = snippet["input_names"] children = snippet["children"] inputs = {names[i]: children[i] for i in range(len(names))} n = len(names) diff_variable = children[n] if snippet["has_diff_variable"] else None print_name = snippet["print_name"] else: # Legacy compact shape: inputs is a name->node dict and diff_variable a # node (or None), both undecoded since they are not in "children". from pybamm.expression_tree.operations.serialise_kernel import decode inputs = {k: decode(v) for k, v in snippet["inputs"].items()} diff_variable = snippet.get("diff_variable") if diff_variable is not None: diff_variable = decode(diff_variable) # Legacy reused the parameter name as print_name to avoid leaking the # serialiser frame name into displays. print_name = snippet["name"] return cls( snippet["name"], inputs, diff_variable=diff_variable, print_name=print_name, )