Source code for wntr.msx.elements

# coding: utf-8
"""
The wntr.msx.elements module includes concrete implementations of the 
multi-species water quality model elements, including species, constants,
parameters, terms, and reactions.
"""

from __future__ import annotations

import logging
from typing import Any, Dict, Tuple, Union

from wntr.epanet.util import ENcomment, NoteType
from wntr.utils.disjoint_mapping import KeyExistsError

from .base import (
    ExpressionType,
    ReactionType,
    SpeciesType,
    VariableType,
    VariableBase,
    VariableValuesBase,
    ReactionBase,
    ReactionSystemBase,
)

logger = logging.getLogger(__name__)


[docs] class Species(VariableBase): """Biological or chemical species. Parameters ---------- name Species name species_type Species type units Units of mass for this species, see :attr:`units` property. atol : float | None Absolute tolerance when solving this species' equations, by default None rtol : float | None Relative tolerance when solving this species' equations, by default None note Supplementary information regarding this variable, by default None (see :class:`~wntr.epanet.util.NoteType`) diffusivity Diffusivity of the species in water, by default None _vars Reaction system this species is a part of, by default None _vals Initial quality values for this species, by default None Raises ------ KeyExistsError If the name has already been used TypeError If `atol` and `rtol` are not the same type ValueError If `atol` or `rtol` ≤ 0 Notes ----- EPANET-MSX requires that `atol` and `rtol` either both be omitted, or both be provided. In order to enforce this, the arguments passed for `atol` and `rtol` must both be None or both be positive values. """
[docs] def __init__( self, name: str, species_type: Union[SpeciesType, str], units: str, atol: float = None, rtol: float = None, *, note: NoteType = None, diffusivity: float = None, _vars: ReactionSystemBase = None, _vals: VariableValuesBase = None, ) -> None: super().__init__(name, note=note) if _vars is not None and not isinstance(_vars, ReactionSystemBase): raise TypeError("Invalid type for _vars, {}".format(type(_vars))) if _vals is not None and not isinstance(_vals, InitialQuality): raise TypeError("Invalid type for _vals, {}".format(type(_vals))) if _vars is not None and name in _vars: raise KeyExistsError("This variable name is already taken") species_type = SpeciesType.get(species_type) if species_type is None: raise TypeError("species_type cannot be None") self._species_type: SpeciesType = species_type self._tolerances: Tuple[float, float] = None self.set_tolerances(atol, rtol) self.units: str = units """Units of mass for this species. For bulk species, concentration is this unit divided by liters, for wall species, concentration is this unit divided by the model's area-unit (see options). """ self.diffusivity: float = diffusivity """Diffusivity of this species in water, if being used, by default None""" self._vars: ReactionSystemBase = _vars self._vals: InitialQuality = _vals
[docs] def set_tolerances(self, atol: float, rtol: float): """Set the absolute and relative tolerance for the solvers. The user must set both values, or neither value (None). Values must be positive. Parameters ---------- atol Absolute tolerance to use rtol Relative tolerance to use Raises ------ TypeError If only one of `atol` or `rtol` is a float ValueError If `atol` or `rtol` ≤ 0 """ if (atol is None) ^ (rtol is None): raise TypeError("atol and rtol must both be float or both be None") if atol is None: self._tolerances = None elif atol <= 0 or rtol <= 0: raise ValueError("atol and rtol must both be positive, got atol={}, rtol={}".format(atol, rtol)) else: self._tolerances = (atol, rtol)
[docs] def get_tolerances(self) -> Union[Tuple[float, float], None]: """Get the custom solver tolerances for this species. Returns ------- (atol, rtol) : (float, float) or None Absolute and relative tolerances, respectively, if they are set """ return self._tolerances
[docs] def clear_tolerances(self): """Set both tolerances to None, reverting to the global options value. """ self._tolerances = None
@property def atol(self) -> float: """Absolute tolerance. Must be set using :meth:`set_tolerances`""" if self._tolerances is not None: return self._tolerances[0] return None @property def rtol(self) -> float: """Relative tolerance. Must be set using :meth:`set_tolerances`""" if self._tolerances is not None: return self._tolerances[1] return None @property def var_type(self) -> VariableType: """Type of variable, :attr:`~wntr.msx.base.VariableType.SPECIES`.""" return VariableType.SPECIES @property def species_type(self) -> SpeciesType: """Type of species, either :attr:`~wntr.msx.base.SpeciesType.BULK` or :attr:`~wntr.msx.base.SpeciesType.WALL`""" return self._species_type @property def initial_quality(self) -> 'InitialQuality': """Initial quality values for the network""" if self._vals is not None: return self._vals else: raise TypeError("This species is not linked to a NetworkData obejct, please `relink` your model") @property def pipe_reaction(self) -> 'Reaction': """Pipe reaction definition""" if self._vars is not None: return self._vars.pipe_reactions[self.name] else: raise AttributeError("This species is not connected to a ReactionSystem") @property def tank_reaction(self) -> 'Reaction': """Tank reaction definition""" if self._vars is not None: return self._vars.tank_reactions[self.name] else: raise AttributeError("This species is not connected to a ReactionSystem")
[docs] def to_dict(self) -> Dict[str, Any]: """Dictionary representation of the object The species dictionary has the following format, as described using a json schema. .. code:: json { "title": "Species", "type": "object", "properties": { "name": { "type": "string" }, "species_type": { "enum": ["bulk", "wall"] }, "units": { "type": "string" }, "atol": { "type": "number", "exclusiveMinimum": 0 }, "rtol": { "type": "number", "exclusiveMinimum": 0 }, "note": { "type": "string" }, "diffusivity": { "type": "number", "minimum": 0 } }, "required": ["name", "species_type", "units", "pipe_reaction", "tank_reaction"], "dependentRequired": {"atol": ["rtol"], "rtol":["atol"]} } """ ret = dict(name=self.name, species_type=self.species_type.name.lower(), units=self.units, atol=self.atol, rtol=self.rtol) if self.diffusivity: ret["diffusivity"] = self.diffusivity if isinstance(self.note, ENcomment): ret["note"] = self.note.to_dict() elif isinstance(self.note, (str, dict, list)): ret["note"] = self.note return ret
[docs] class Constant(VariableBase): """Constant coefficient for use in expressions. Parameters ---------- name Name of the variable. value Constant value. units Units for the variable, by default None note Supplementary information regarding this variable, by default None _vars Reaction system this constant is a part of, by default None """
[docs] def __init__(self, name: str, value: float, *, units: str = None, note: Union[str, dict, ENcomment] = None, _vars: ReactionSystemBase = None) -> None: super().__init__(name, note=note) if _vars is not None and not isinstance(_vars, ReactionSystemBase): raise TypeError("Invalid type for _vars, {}".format(type(_vars))) if _vars is not None and name in _vars: raise KeyExistsError("This variable name is already taken") self.value: float = float(value) """Value of the constant""" self.units: str = units """Units of the constant""" self._vars: ReactionSystemBase = _vars
def __call__(self, *, t=None) -> Any: return self.value @property def var_type(self) -> VariableType: """Type of variable, :attr:`~wntr.msx.base.VariableType.CONSTANT`.""" return VariableType.CONSTANT
[docs] def to_dict(self) -> Dict[str, Any]: """Dictionary representation of the object""" ret = dict(name=self.name, value=self.value) if self.units: ret["units"] = self.units if isinstance(self.note, ENcomment): ret["note"] = self.note.to_dict() elif isinstance(self.note, (str, dict, list)): ret["note"] = self.note return ret
[docs] class Parameter(VariableBase): """Parameterized variable for use in expressions. Parameters ---------- name Name of this parameter. global_value Global value for the parameter if otherwise unspecified. units Units for this parameter, by default None note Supplementary information regarding this variable, by default None _vars Reaction system this parameter is a part of, by default None _vals Network-specific values for this parameter, by default None """
[docs] def __init__( self, name: str, global_value: float, *, units: str = None, note: Union[str, dict, ENcomment] = None, _vars: ReactionSystemBase = None, _vals: VariableValuesBase = None ) -> None: super().__init__(name, note=note) if _vars is not None and not isinstance(_vars, ReactionSystemBase): raise TypeError("Invalid type for _vars, {}".format(type(_vars))) if _vals is not None and not isinstance(_vals, ParameterValues): raise TypeError("Invalid type for _vals, {}".format(type(_vals))) if _vars is not None and name in _vars: raise KeyExistsError("This variable name is already taken") self.global_value: float = float(global_value) self.units: str = units self._vars: ReactionSystemBase = _vars self._vals: ParameterValues = _vals
def __call__(self, *, pipe: str = None, tank: str = None) -> float: """Get the value of the parameter for a given pipe or tank. If there is no specific, different value for the requested pipe or tank, then the global value will be returned. *This is true even* *if the pipe or tank does not exist in the network, so caution is* *advised*. Parameters ---------- pipe Name of a pipe to get the parameter value for, by default None tank Name of a pipe to get the parameter value for, by default None Returns ------- float Value at the specified pipe or tank, or the global value Raises ------ TypeError If both pipe and tank are specified ValueError If there is no ParameterValues object defined for and linked to this parameter """ if pipe is not None and tank is not None: raise TypeError("Both pipe and tank cannot be specified at the same time") elif self._vals is None and (pipe is not None or tank is not None): raise ValueError("No link provided to network-specific parameter values") if pipe: return self._vals.pipe_values.get(pipe, self.global_value) elif tank: return self._vals.tank_values.get(pipe, self.global_value) return self.global_value @property def var_type(self) -> VariableType: """Type of variable, :attr:`~wntr.msx.base.VariableType.PARAMETER`.""" return VariableType.PARAMETER
[docs] def to_dict(self) -> Dict[str, Any]: """Dictionary representation of the object""" ret = dict(name=self.name, global_value=self.global_value) if self.units: ret["units"] = self.units if isinstance(self.note, ENcomment): ret["note"] = self.note.to_dict() elif isinstance(self.note, (str, dict, list)): ret["note"] = self.note return ret
[docs] class Term(VariableBase): """Named expression (term) that can be used in expressions Parameters ---------- name Variable name. expression Mathematical expression to be aliased note Supplementary information regarding this variable, by default None _vars Reaction system this species is a part of, by default None """
[docs] def __init__(self, name: str, expression: str, *, note: Union[str, dict, ENcomment] = None, _vars: ReactionSystemBase = None) -> None: super().__init__(name, note=note) if _vars is not None and not isinstance(_vars, ReactionSystemBase): raise TypeError("Invalid type for _vars, {}".format(type(_vars))) if _vars is not None and name in _vars: raise KeyExistsError("This variable name is already taken") self.expression: str = expression """Expression that is aliased by this term""" self._vars: ReactionSystemBase = _vars
@property def var_type(self) -> VariableType: """Type of variable, :attr:`~wntr.msx.base.VariableType.TERM`.""" return VariableType.TERM
[docs] def to_dict(self) -> Dict[str, Any]: """Dictionary representation of the object""" ret = dict(name=self.name, expression=self.expression) if isinstance(self.note, ENcomment): ret["note"] = self.note.to_dict() elif isinstance(self.note, (str, dict, list)): ret["note"] = self.note return ret
[docs] class ReservedName(VariableBase): """Reserved name that should not be used Parameters ---------- name Reserved name. note Supplementary information regarding this variable, by default None Raises ------ KeyExistsError If the name has already been registered """
[docs] def __init__(self, name: str, *, note: Union[str, dict, ENcomment] = None) -> None: self.name: str = name self.note: Union[str, dict, ENcomment] = note
@property def var_type(self) -> VariableType: """Type of variable, :attr:`~wntr.msx.base.VariableType.RESERVED`.""" return VariableType.RESERVED
# def to_dict(self) -> Dict[str, Any]: # """Dictionary representation of the object""" # return "{}({})".format(self.__class__.__name__, ", ".join(["name={}".format(repr(self.name)), "note={}".format(repr(self.note))]))
[docs] class HydraulicVariable(ReservedName): """Reserved name for hydraulic variables The user should not need to create any variables using this class, they are created automatically by the MsxModel object during initialization. Parameters ---------- name Name of the variable (predefined by MSX) units Units for hydraulic variable, by default None note Supplementary information regarding this variable, by default None """
[docs] def __init__(self, name: str, units: str = None, *, note: Union[str, dict, ENcomment] = None) -> None: super().__init__(name, note=note) self.units: str = units """Hydraulic variable's units"""
[docs] class MathFunction(ReservedName): """Reserved name for math functions Parameters ---------- name Function name func Callable function note Supplementary information regarding this variable, by default None """
[docs] def __init__(self, name: str, func: callable, *, note: Union[str, dict, ENcomment] = None) -> None: super().__init__(name, note=note) self.func: callable = func """A callable function or SymPy function"""
def __call__(self, *args: Any, **kwds: Any) -> Any: """Evaluate the function using any specified args and kwds.""" return self.func(*args, **kwds)
[docs] class Reaction(ReactionBase): """Water quality reaction dynamics definition for a specific species. Parameters ---------- species_name Species (object or name) this reaction is applicable to. reaction_type Reaction type (location), from {PIPE, TANK} expression_type Expression type (left-hand-side) of the equation, from {RATE, EQUIL, FORMULA} expression Mathematical expression for the right-hand-side of the reaction equation. note Supplementary information regarding this variable, by default None _vars Reaction system this species is a part of, by default None """
[docs] def __init__( self, species_name: str, reaction_type: ReactionType, expression_type: ExpressionType, expression: str, *, note: Union[str, dict, ENcomment] = None, _vars: ReactionSystemBase = None, ) -> None: super().__init__(species_name=species_name, note=note) if _vars is not None and not isinstance(_vars, ReactionSystemBase): raise TypeError("Invalid type for _vars, {}".format(type(_vars))) expression_type = ExpressionType.get(expression_type) reaction_type = ReactionType.get(reaction_type) if reaction_type is None: raise TypeError("Required argument reaction_type cannot be None") if expression_type is None: raise TypeError("Required argument expression_type cannot be None") self.__rxn_type: ReactionType = reaction_type self._expr_type: ExpressionType = expression_type if not expression: raise TypeError("expression cannot be None") self.expression: str = expression """Mathematical expression (right-hand-side)""" self._vars: ReactionSystemBase = _vars
@property def expression_type(self) -> ExpressionType: """Expression type (left-hand-side), either :attr:`~wntr.msx.base.ExpressionType.RATE`, :attr:`~wntr.msx.base.ExpressionType.EQUIL`, or :attr:`~wntr.msx.base.ExpressionType.FORMULA`""" return self._expr_type @property def reaction_type(self) -> ReactionType: """Reaction type (location), either :attr:`~wntr.msx.base.ReactionType.PIPE` or :attr:`~wntr.msx.base.ReactionType.TANK`""" return self.__rxn_type
[docs] def to_dict(self) -> dict: """Dictionary representation of the object""" ret = dict(species_name=str(self.species_name), expression_type=self.expression_type.name.lower(), expression=self.expression) if isinstance(self.note, ENcomment): ret["note"] = self.note.to_dict() elif isinstance(self.note, (str, dict, list)): ret["note"] = self.note return ret
[docs] class InitialQuality(VariableValuesBase): """Initial quality values for a species in a specific network. Parameters ---------- global_value Global initial quality value, by default 0.0 node_values Any different initial quality values for specific nodes, by default None link_values Any different initial quality values for specific links, by default None """
[docs] def __init__(self, global_value: float = 0.0, node_values: dict = None, link_values: dict = None): self.global_value = global_value """Global initial quality values for this species.""" self._node_values = node_values if node_values is not None else dict() self._link_values = link_values if link_values is not None else dict()
def __repr__(self) -> str: return self.__class__.__name__ + "(global_value={}, node_values=<{} entries>, link_values=<{} entries>)".format( self.global_value, len(self._node_values), len(self._link_values) ) @property def var_type(self) -> VariableType: """Type of variable, :attr:`~wntr.msx.base.VariableType.SPECIES`, this object holds data for.""" return VariableType.SPECIES @property def node_values(self) -> Dict[str, float]: """Mapping that overrides the global_value of the initial quality at specific nodes""" return self._node_values @property def link_values(self) -> Dict[str, float]: """Mapping that overrides the global_value of the initial quality in specific links""" return self._link_values
[docs] def to_dict(self) -> Dict[str, Dict[str, float]]: """Dictionary representation of the object""" return dict(global_value=self.global_value, node_values=self._node_values.copy(), link_values=self._link_values.copy())
[docs] class ParameterValues(VariableValuesBase): """Pipe and tank specific values of a parameter for a specific network. Parameters ---------- pipe_values Any different values for this parameter for specific pipes, by default None tank_values Any different values for this parameter for specific tanks, by default None """
[docs] def __init__(self, *, pipe_values: dict = None, tank_values: dict = None) -> None: self._pipe_values = pipe_values if pipe_values is not None else dict() self._tank_values = tank_values if tank_values is not None else dict()
def __repr__(self) -> str: return self.__class__.__name__ + "(pipe_values=<{} entries>, tank_values=<{} entries>)".format(len(self._pipe_values), len(self._tank_values)) @property def var_type(self) -> VariableType: """Type of variable, :attr:`~wntr.msx.base.VariableType.PARAMETER`, this object holds data for.""" return VariableType.PARAMETER @property def pipe_values(self) -> Dict[str, float]: """Mapping that overrides the global_value of a parameter for a specific pipe""" return self._pipe_values @property def tank_values(self) -> Dict[str, float]: """Mapping that overrides the global_value of a parameter for a specific tank""" return self._tank_values
[docs] def to_dict(self) -> Dict[str, Dict[str, float]]: """Dictionary representation of the object""" return dict(pipe_values=self._pipe_values.copy(), tank_values=self._tank_values.copy())