Source code for pytes.models.base_model

#
# Base model for PyTES models
#
"""Base class of PyTES models: a fixed composition of submodels, checked when built."""

import pybamm

from .. import compat, deprecation
from ..submodels.base_submodel import parameter_names
from ..variables import add_unit_twins


[docs] class WiringError(ValueError): """The submodels' inputs and outputs do not fit together"""
[docs] class OptionError(ValueError): """An option, a value or a combination of options that the model does not allow"""
[docs] class BaseModel(pybamm.BaseModel): """ A heat battery model: a fixed composition of submodels (DESIGN D3). Subclasses define :meth:`get_parameters` and :meth:`set_submodels`. Each submodel reads the variables of the others as :class:`pybamm.CoupledVariable` (its ``requires``). When the model is built it merges the submodels with :meth:`pybamm.BaseModel.update`, checks the wiring and resolves every coupled variable, so the finished model contains none: - every required name is provided by exactly one submodel; - no variable depends on itself through coupled variables; - once resolved, each provided variable has the primary domain its coupled variable declares. The equations are the same for charge and discharge, and the model has no events: cut-offs are the terminations of an experiment (DESIGN D11). Variants are chosen through ``options`` (DESIGN D3): each model declares the values it allows in :attr:`allowed_options`, the first being the default. An unknown option or value raises, as does a combination :meth:`check_options` rejects; a model never swaps a submodel the user did not ask for. Parameters ---------- name : str The name of the model. options : dict, optional The model's options; missing ones take their defaults. Attributes ---------- param : object The parameters, from :meth:`get_parameters`. options : dict The model's options, defaults included. submodels : dict The submodels, by name, in the order they were added. controls : dict The model's controls (DESIGN D9): the parameter symbols an experiment may turn into solver unknowns, keyed by the name of the matching variable. """ #: Scale of the temperature variables [K] temperature_scale = 200 #: Allowed values of each option, the first being the default allowed_options = {} def __init__(self, name, options=None): super().__init__(name=name) self.options = self.process_options(options) self.set_spatial_variables() self.param = self.get_parameters() self.submodels = {} self.set_submodels() self.build_model()
[docs] def process_options(self, options): """ The options with their defaults filled in. An option given under an old name (:mod:`pytes.deprecation`) is renamed, with a warning while its alias is active. Raises ------ pytes.OptionError If an option or a value is not allowed (an old name whose alias is retired included), an option is given under both its old and new names, or the combination is rejected by :meth:`check_options`. """ options = dict(options or {}) for key in list(options): rename = deprecation.find("option", key) if rename is None: continue deprecation.check(rename, OptionError) if rename.new in options: raise OptionError( f"{self.name}: option '{rename.new}' given under both its old and " "new names" ) options[rename.new] = options.pop(key) for key, value in options.items(): if key not in self.allowed_options: raise OptionError( f"{self.name}: option '{key}' not recognised; the options are " f"{sorted(self.allowed_options)}" ) if value not in self.allowed_options[key]: raise OptionError( f"{self.name}: value '{value}' of option '{key}' not recognised; " f"it must be one of {list(self.allowed_options[key])}" ) options = { key: options.get(key, values[0]) for key, values in self.allowed_options.items() } self.check_options(options) return options
[docs] def check_options(self, options): """Raise :class:`pytes.OptionError` for an incompatible combination (none yet)"""
[docs] def set_spatial_variables(self): """Create the spatial variables, which may depend on the options"""
[docs] def get_parameters(self): """Return the model's parameters""" raise NotImplementedError
[docs] def set_submodels(self): """Fill ``self.submodels``""" raise NotImplementedError
@property def controls(self): """ The controls, inlet temperature and mass flow rate, as parameter symbols, keyed by the names of the matching variables """ return { "Inlet temperature [K]": self.param.T_in, "Mass flow rate [kg.s-1]": self.param.mdot, }
[docs] def power(self, variables, controls=None): """ Power [W] with some controls replaced by given symbols. The experiment runner uses this to write a step's power equation with the step's target values, so that the equation depends on the control it adjusts. Parameters ---------- variables : dict The model's variables. controls : dict, optional Symbols replacing the controls, keyed by control name. """ controls = controls or {} fluid = self.param.fluid T_in = controls.get("Inlet temperature [K]", variables["Inlet temperature [K]"]) mdot = controls.get( "Mass flow rate [kg.s-1]", variables["Mass flow rate [kg.s-1]"] ) return mdot * (variables["Outlet specific enthalpy [J.kg-1]"] - fluid.h(T_in))
@property def geometry_parameters(self): """Names of the parameters in the default geometry (the domain lengths), sorted""" symbols = [ bound for domain in self.default_geometry.values() for extent in domain.values() for bound in extent.values() if isinstance(bound, pybamm.Symbol) ] return parameter_names(symbols)
[docs] def print_submodel_info(self): """ Print the submodels: for each slot, the class, the variables it requires and provides, the parameters it uses and its modelling assumptions; then the parameters of the model's geometry (DESIGN D3). """ for slot, submodel in self.submodels.items(): print(f"{slot}: {type(submodel).__name__} ({submodel.name})") for heading, names in ( ("requires", submodel.requires), ("provides", submodel.provides), ("parameters", submodel.parameters), ("assumptions", submodel.assumptions), ): print(f" {heading}:") for name in names or ("(none)",): print(f" {name}") print("geometry parameters:") for name in self.geometry_parameters or ("(none)",): print(f" {name}")
[docs] def check_wiring(self): """ Check that the submodels fit together (see the class docstring). Returns ------- dict The submodel providing each variable name. Raises ------ WiringError """ providers = {} for submodel in self.submodels.values(): for name in submodel.provides: if name in providers: raise WiringError( f"'{name}' is provided by both {providers[name].name!r} and " f"{submodel.name!r}" ) providers[name] = submodel for submodel in self.submodels.values(): for name in submodel.coupled_variables: if name not in providers: raise WiringError( f"{submodel.name!r} requires '{name}', which no submodel provides" ) self._check_no_cycles(providers) return providers
def _check_domains(self, providers): """ Raise if a provided variable is not on the primary domain its coupled variable declares. Checked after resolution: before it, an expression built from coupled variables only knows their primary domains. """ for submodel in self.submodels.values(): for name, coupled in submodel.coupled_variables.items(): provided = self.variables[name] if coupled.domain and coupled.domain != provided.domain: raise WiringError( f"{submodel.name!r} requires '{name}' on {coupled.domain}, but " f"{providers[name].name!r} provides it on {provided.domain}" ) def _check_no_cycles(self, providers): """Raise if a provided variable depends on itself through coupled variables""" def coupled_names(symbol): return { node.name for node in symbol.pre_order() if isinstance(node, pybamm.CoupledVariable) } done, active = set(), [] def visit(name): if name in done: return if name in active: cycle = " -> ".join([*active[active.index(name) :], name]) raise WiringError(f"Circular dependency: {cycle}") active.append(name) for other in coupled_names(providers[name].variables[name]): visit(other) active.pop() done.add(name) for name in providers: visit(name)
[docs] def build_model(self): """ Merge the submodels, check the wiring, resolve the coupled variables and add the unit twins (DESIGN D12: the submodels define every quantity in SI units) """ providers = self.check_wiring() self.variables = {"Time [s]": pybamm.t} try: self.update(*self.submodels.values()) except pybamm.ModelError as error: raise WiringError(str(error)) from None compat.resolve_coupled_variables(self) self._check_domains(providers) self.variables = add_unit_twins(dict(self.variables))