From 6c93a6306efa4ebd0a45bc7d1dce9b4015b1a6fe Mon Sep 17 00:00:00 2001 From: Katherine Klise Date: Fri, 10 Nov 2023 12:29:20 -0800 Subject: [PATCH] API docs update, removed excess imports, removed cite_msx --- documentation/advancedqual.rst | 408 ------------------ documentation/reference.rst | 6 +- wntr/epanet/exceptions.py | 5 +- wntr/epanet/msx/__init__.py | 18 +- wntr/epanet/msx/enums.py | 143 +++--- wntr/epanet/msx/exceptions.py | 75 ++-- wntr/epanet/msx/io.py | 30 +- wntr/epanet/msx/toolkit.py | 267 ++++++------ wntr/msx/__init__.py | 8 +- .../msx/_library_data/arsenic_chloramine.json | 4 +- .../_library_data/batch_chloramine_decay.json | 2 +- wntr/msx/_library_data/lead_ppm.json | 2 +- wntr/msx/_library_data/nicotine.json | 2 +- wntr/msx/_library_data/nicotine_ri.json | 2 +- wntr/msx/base.py | 232 +++++----- wntr/msx/elements.py | 248 ++++++----- wntr/msx/library.py | 140 +++--- wntr/msx/model.py | 269 ++++++------ wntr/msx/options.py | 102 +++-- wntr/network/base.py | 5 +- wntr/tests/test_msx_elements.py | 2 +- wntr/utils/disjoint_mapping.py | 11 +- 22 files changed, 834 insertions(+), 1147 deletions(-) delete mode 100644 documentation/advancedqual.rst diff --git a/documentation/advancedqual.rst b/documentation/advancedqual.rst deleted file mode 100644 index b144cbb38..000000000 --- a/documentation/advancedqual.rst +++ /dev/null @@ -1,408 +0,0 @@ -.. raw:: latex - - \clearpage - -.. _advanced_simulation: - -Advanced water quality techniques -================================= - -This section describes several advanced simulation techniques using WNTR with MSX. - - - -.. _msx_example1_lead: - -Example 1: plumbosolvency of lead ---------------------------------- - -This model is described in [BWMS20]_, and represents plumbosolvency of lead in lead pipes -within a dwelling. -In this case, we will not assume that a water network model has been made for the dwelling, yet. - -Model creation -~~~~~~~~~~~~~~ - -We create a new model, give it a name, title, and description, and we add the reference -for the paper this model was described in. - -.. doctest:: - - >>> import wntr.msx - >>> msx = wntr.msx.MultispeciesQualityModel() - >>> msx.name = "lead_ppm" - >>> msx.title = "Lead Plumbosolvency Model (from Burkhardt et al 2020)" - >>> msx.desc = "Parameters for EPA HPS Simulator Model" - >>> msx.references.append( - ... """J. B. Burkhardt, et al. (2020) https://doi.org/10.1061/(asce)wr.1943-5452.0001304""" - ... ) - >>> msx - MultispeciesQualityModel(name='lead_ppm') - -Next, we will update certain options. - -.. doctest:: - - >>> msx.options = { - ... "report": { - ... "species": {"PB2": "YES"}, - ... "species_precision": {"PB2": 5}, - ... "nodes": "all", - ... "links": "all", - ... }, - ... "timestep": 1, - ... "area_units": "M2", - ... "rate_units": "SEC", - ... "rtol": 1e-08, - ... "atol": 1e-08, - ... } - - -Adding variables -~~~~~~~~~~~~~~~~ -The variables that are needed for a multispecies reaction system are: species, coefficients, and terms. -These are used in expressions to define the dynamics of the reaction. All variables have at least two -attributes: their name and a note. The variable name must be a valid EPANET-MSX id, which primarily -means no spaces are permitted. However, it may be useful to ensure that the name is a valid python -variable name, so that it can be used to identify the variable in your code as well. The note can be -a string, a dictionary with the keys "pre" and "post", or an :class:`~wntr.epanet.util.ENcomment` object -(which has a "pre" and "post" attribute). See the ENcomment documentation for details on the meaning; -in this example we will use the string form of the note. - --------------- - -There is only one species defined in this model, which is dissolved lead. - -======================== =============== ================================= ======================== -Name Type Units Note ------------------------- --------------- --------------------------------- ------------------------ -:math:`Pb` bulk species :math:`\mathrm{μg}_\mathrm{(Pb)}` dissolved lead -======================== =============== ================================= ======================== - -To add this species to the model, we can use the model's :meth:`~wntr.msx.multispecies.MultispeciesQualityModel.add_species` -method. -The method arguments are the name, the species_type (which can either be "bulk" or "wall"), the units, -and an optional note. -This method will add the new species to the model and also return a copy of the new species object. - -.. doctest:: - - >>> msx.add_species(name="PB2", species_type='bulk', units="ug", note="dissolved lead (Pb)") - Species(name='PB2', species_type=, units='ug', atol=None, rtol=None, note='dissolved lead (Pb)') - -The new species can be accessed by using the item's name and indexing on the model's -:attr:`~wntr.msx.multispecies.MultispeciesQualityModel.reaction_system` attribute. - - >>> PB2 = msx.reaction_system['PB2'] - >>> PB2 - Species(name='PB2', species_type=, units='ug', atol=None, rtol=None, note='dissolved lead (Pb)') - --------------- - -There are two different types of coefficients that can be used in reaction expressions: constants -and parameters. Constants have a single value in every expression. Parameters have a global value -that is used by default, but which can be modified on a per-pipe or per-tank basis. This model -has two constants and one parameter. - -=============== =============== =============== ================================= ======================== -Type Name Value Units Note ---------------- --------------- --------------- --------------------------------- ------------------------ -constant :math:`M` 0.117 :math:`\mathrm{μg~m^{-2}~s^{-1}}` desorption rate -constant :math:`E` 140.0 :math:`\mathrm{μg~L^{-1}}` saturation level -parameter :math:`F` 0 `n/a` is pipe made of lead? -=============== =============== =============== ================================= ======================== - -We can add these to the model as follows: - -.. doctest:: - - >>> msx.add_constant("M", value=0.117, note="Desorption rate (ug/m^2/s)", units="ug * m^(-2) * s^(-1)") - >>> msx.add_constant("E", value=140.0, note="saturation/plumbosolvency level (ug/L)", units="ug/L") - >>> msx.add_parameter("F", global_value=0, note="determines which pipes have reactions") - - -Adding reactions -~~~~~~~~~~~~~~~~ - -All species must have two reactions defined for the model to be run successfully in EPANET-MSX by WNTR. -One is a pipe reaction, the other is a tank reaction. In this case, we only have a reactions within -pipes, so we need to set the tank reaction to be unchanging. The system of equations is: - -.. math:: - - \frac{d}{dt}Pb_p &= F_p \, Av_p \, M \frac{\left( E - Pb_p \right)}{E}, &\quad\text{for all pipes}~p~\text{in network} \\ - \frac{d}{dt}Pb_t &= 0, & \quad\text{for all tanks}~t~\text{in network} - -Note that the pipe reaction has a variable that we have not defined, :math:`Av`, in its expression; -this variable is a pre-defined hydraulic variable. The list of these variables can be found in -the EPANET-MSX documentation, and also in the :attr:`~wntr.msx.base.HYDRAULIC_VARIABLES` -documentation. The reactions can be described in WNTR as - -================ ============== ========================================================================== -Reaction type Dynamics type Reaction expression ----------------- -------------- -------------------------------------------------------------------------- -pipe rate :math:`F \cdot Av \cdot M \cdot \left( E - Pb \right) / E` -tank rate :math:`0` -================ ============== ========================================================================== - -and then added to the reaction model using the :meth:`~wntr.msx.multispecies.MultispeciesQualityModel.add_reaction` -method. - -.. doctest:: - - >>> msx.add_reaction("PB2", "pipe", "RATE", expression="F * Av * M * (E - PB2) / E") - >>> msx.add_reaction(PB2, "tank", "rate", expression="0") - - - -Example 2: arsenic oxidation and adsorption -------------------------------------------- - -This example models monochloramine oxidation of arsenite/arsenate and wall -adsorption/desorption, as given in section 3 of the EPANET-MSX user manual [SRU23]_. -First, the model -will be restated here and then the code to create the model in wntr will be shown. - -Model Description -~~~~~~~~~~~~~~~~~ - -The system of equations for the reaction in pipes is given in Eq. (2.4) through (2.7) -in [SRU23]_. This is a simplified model, taken from [GSCL94]_. - -.. math:: - - \frac{d}{dt}{(\mathsf{As}^\mathrm{III})} &= -k_a ~ {(\mathsf{As}^\mathrm{III})} ~ {(\mathsf{NH_2Cl})} \\ - \frac{d}{dt}{(\mathsf{As}^\mathrm{V})} &= k_a ~ {(\mathsf{As}^\mathrm{III})} ~ {(\mathsf{NH_2CL})} - Av \left( k_1 \left(S_\max - {(\mathsf{As}^\mathrm{V}_s)} \right) {(\mathsf{As}^\mathrm{V})} - k_2 ~ {(\mathsf{As}^\mathrm{V}_s)} \right) \\ - \frac{d}{dt}{(\mathsf{NH_2Cl})} &= -k_b ~ {(\mathsf{NH_2Cl})} \\ - {(\mathsf{As}^\mathrm{V}_s)} &= \frac{k_s ~ S_\max ~ {(\mathsf{As}^\mathrm{V})}}{1 + k_s {(\mathsf{As}^\mathrm{V})}} - - -where the various species, coefficients, and expressions are described in the tables below. - - -.. list-table:: Options - :header-rows: 1 - :widths: 3 3 10 - - * - Option - - Code - - Description - * - Rate units - - "HR" - - :math:`\mathrm{h}^{-1}` - * - Area units - - "M2" - - :math:`\mathrm{m}^2` - - -.. list-table:: Species - :header-rows: 1 - :widths: 2 2 2 3 4 6 - - * - Name - - Type - - Value - - Symbol - - Units - - Note - * - AS3 - - Bulk - - "UG" - - :math:`{\mathsf{As}^\mathrm{III}}` - - :math:`\require{upgreek}\upmu\mathrm{g~L^{-1}}` - - dissolved arsenite - * - AS5 - - Bulk - - "UG" - - :math:`{\mathsf{As}^\mathrm{V}}` - - :math:`\require{upgreek}\upmu\mathrm{g~L^{-1}}` - - dissolved arsenate - * - AStot - - Bulk - - "UG" - - :math:`{\mathsf{As}^\mathrm{tot}}` - - :math:`\require{upgreek}\upmu\mathrm{g~L^{-1}}` - - dissolved arsenic (total) - * - NH2CL - - Bulk - - "MG" - - :math:`{\mathsf{NH_2Cl}}` - - :math:`\mathrm{mg~L^{-1}}` - - dissolved monochloramine - * - AS5s - - Wall - - "UG" - - :math:`{\mathsf{As}^\mathrm{V}_{s}}` - - :math:`\require{upgreek}\upmu\mathrm{g}~\mathrm{m}^{-2}` - - adsorped arsenate (surface) - - -.. list-table:: Coefficients - :header-rows: 1 - :widths: 2 2 2 3 4 6 - - * - Name - - Type - - Value - - Symbol - - Units - - Note - * - Ka - - Const - - :math:`10` - - :math:`k_a` - - :math:`\mathrm{mg}^{-1}_{\left(\mathsf{NH_2Cl}\right)}~\mathrm{h}^{-1}` - - arsenite oxidation - * - Kb - - Const - - :math:`0.1` - - :math:`k_b` - - :math:`\mathrm{h}^{-1}` - - chloromine decay - * - K1 - - Const - - :math:`5.0` - - :math:`k_1` - - :math:`\require{upgreek}\textrm{L}~\upmu\mathrm{g}^{-1}_{\left(\mathsf{As}^\mathrm{V}\right)}~\mathrm{h}^{-1}` - - arsenate adsorption - * - K2 - - Const - - :math:`1.0` - - :math:`k_2` - - :math:`\textrm{L}~\mathrm{h}^{-1}` - - arsenate desorption - * - Smax - - Const - - :math:`50.0` - - :math:`S_{\max}` - - :math:`\require{upgreek}\upmu\mathrm{g}_{\left(\mathsf{As}^\mathrm{V}\right)}~\mathrm{m}^{-2}` - - arsenate adsorption limit - - -.. list-table:: Other terms - :header-rows: 1 - :widths: 3 3 2 3 10 - - * - Name - - Symbol - - Expression - - Units - - Note - * - Ks - - :math:`k_s` - - :math:`{k_1}/{k_2}` - - :math:`\require{upgreek}\upmu\mathrm{g}^{-1}_{\left(\mathsf{As}^\mathrm{V}\right)}` - - equilibrium adsorption coefficient - - -.. list-table:: Pipe reactions - :header-rows: 1 - :widths: 3 3 16 - - * - Species - - Type - - Expression - * - AS3 - - Rate - - :math:`-k_a \, {\mathsf{As}^\mathrm{III}} \, {\mathsf{NH_2Cl}}` - * - AS5 - - Rate - - :math:`k_a \, {\mathsf{As}^\mathrm{III}} \, {\mathsf{NH_2Cl}} -Av \left( k_1 \left(S_{\max}-{\mathsf{As}^\mathrm{V}_{s}} \right) {\mathsf{As}^\mathrm{V}} - k_2 \, {\mathsf{As}^\mathrm{V}_{s}} \right)` - * - NH2CL - - Rate - - :math:`-k_b \, {\mathsf{NH_2Cl}}` - * - AStot - - Formula - - :math:`{\mathsf{As}^\mathrm{III}} + {\mathsf{As}^\mathrm{V}}` - * - AS5s - - Equil - - :math:`k_s \, S_{\max} \frac{{\mathsf{As}^\mathrm{V}}}{1 + k_s \, {\mathsf{As}^\mathrm{V}}} - {\mathsf{As}^\mathrm{V}_{s}}` - - -.. list-table:: Tank reactions - :header-rows: 1 - :widths: 3 3 16 - - * - Species - - Type - - Expression - * - AS3 - - Rate - - :math:`-k_a \, {\mathsf{As}^\mathrm{III}} \, {\mathsf{NH_2Cl}}` - * - AS5 - - Rate - - :math:`k_a \, {\mathsf{As}^\mathrm{III}} \, {\mathsf{NH_2Cl}}` - * - NH2CL - - Rate - - :math:`-k_b \, {\mathsf{NH_2Cl}}` - * - AStot - - Formula - - :math:`{\mathsf{As}^\mathrm{III}} + {\mathsf{As}^\mathrm{V}}` - * - AS5s - - Equil - - :math:`0` (`not present in tanks`) - - -Creation in WNTR -~~~~~~~~~~~~~~~~ - -.. doctest:: - - >>> msx = wntr.msx.MultispeciesQualityModel() - >>> msx.name = "arsenic_chloramine" - >>> msx.title = "Arsenic Oxidation/Adsorption Example" - >>> msx.references.append(wntr.msx.library.cite_msx()) - >>> AS3 = msx.add_species(name="AS3", species_type="BULK", units="UG", note="Dissolved arsenite") - >>> AS5 = msx.add_species(name="AS5", species_type="BULK", units="UG", note="Dissolved arsenate") - >>> AStot = msx.add_species(name="AStot", species_type="BULK", units="UG", note="Total dissolved arsenic") - >>> AS5s = msx.add_species(name="AS5s", species_type="WALL", units="UG", note="Adsorbed arsenate") - >>> NH2CL = msx.add_species(name="NH2CL", species_type="BULK", units="MG", note="Monochloramine") - >>> Ka = msx.add_constant("Ka", 10.0, units="1 / (MG * HR)", note="Arsenite oxidation rate coefficient") - >>> Kb = msx.add_constant("Kb", 0.1, units="1 / HR", note="Monochloramine decay rate coefficient") - >>> K1 = msx.add_constant("K1", 5.0, units="M^3 / (UG * HR)", note="Arsenate adsorption coefficient") - >>> K2 = msx.add_constant("K2", 1.0, units="1 / HR", note="Arsenate desorption coefficient") - >>> Smax = msx.add_constant("Smax", 50.0, units="UG / M^2", note="Arsenate adsorption limit") - >>> Ks = msx.add_term(name="Ks", expression="K1/K2", note="Equil. adsorption coeff.") - >>> _ = msx.add_reaction( - ... species_name="AS3", reaction_type="pipes", expression_type="rate", expression="-Ka*AS3*NH2CL", note="Arsenite oxidation" - ... ) - >>> _ = msx.add_reaction( - ... "AS5", "pipes", "rate", "Ka*AS3*NH2CL - Av*(K1*(Smax-AS5s)*AS5 - K2*AS5s)", note="Arsenate production less adsorption" - ... ) - >>> _ = msx.add_reaction( - ... species_name="NH2CL", reaction_type="pipes", expression_type="rate", expression="-Kb*NH2CL", note="Monochloramine decay" - ... ) - >>> _ = msx.add_reaction("AS5s", "pipe", "equil", "Ks*Smax*AS5/(1+Ks*AS5) - AS5s", note="Arsenate adsorption") - >>> _ = msx.add_reaction( - ... species_name="AStot", reaction_type="pipes", expression_type="formula", expression="AS3 + AS5", note="Total arsenic" - ... ) - >>> _ = msx.add_reaction( - ... species_name="AS3", reaction_type="tank", expression_type="rate", expression="-Ka*AS3*NH2CL", note="Arsenite oxidation" - ... ) - >>> _ = msx.add_reaction( - ... species_name="AS5", reaction_type="tank", expression_type="rate", expression="Ka*AS3*NH2CL", note="Arsenate production" - ... ) - >>> _ = msx.add_reaction( - ... species_name="NH2CL", reaction_type="tank", expression_type="rate", expression="-Kb*NH2CL", note="Monochloramine decay" - ... ) - >>> _ = msx.add_reaction( - ... species_name="AStot", reaction_type="tanks", expression_type="formula", expression="AS3 + AS5", note="Total arsenic" - ... ) - >>> msx.options.area_units = "M2" - >>> msx.options.rate_units = "HR" - >>> msx.options.rtol = 0.001 - >>> msx.options.atol = 0.0001 - - -References ----------- - -.. [BWMS20] - J. B. Burkhardt, et al. (2020) - "Framework for Modeling Lead in Premise Plumbing Systems Using EPANET". - `Journal of Water Resources Planning and Management`. - **146** (12). https://doi.org/10.1061/(asce)wr.1943-5452.0001304. PMID:33627937. - -.. [GSCL94] - B. Gu, J. Schmitt, Z. Chen, L. Liang, and J.F. McCarthy. "Adsorption and desorption of - natural organic matter on iron oxide: mechanisms and models". Environ. Sci. Technol., 28:38-46, January 1994. diff --git a/documentation/reference.rst b/documentation/reference.rst index 3fadfd9d5..bd638f8cf 100644 --- a/documentation/reference.rst +++ b/documentation/reference.rst @@ -10,7 +10,7 @@ References Due to limitations with cross referenced citations in reStructuredText (e.g., commas and spaces are not supported), citations are cross referenced using a 6 digit notation [*]_. -.. [ALA01] American Lifelines Alliance. (2001). Seismic Fragility Formulations for Water Systems, Part 1 and 2. Report for the American Lifelines Alliance, ASCE (Ed.) Reston, VA: American Society of Civil Engineers. April 2001. +.. [ALA01] American Lifelines Alliance. (2001). Seismic Fragility Formulations for Water Systems, Part 1 and 2. Report for the American Lifelines Alliance, ASCE (Ed.) Reston, VA: American Society of Civil Engineers. April 2001. .. [AwGB90] Awumah, K., Goulter, I., and Bhatt, S.K. (1990). Assessment of reliability in water distribution networks using entropy based measures. Stochastic Hydrology and Hydraulics, 4(4), 309-320. @@ -38,7 +38,7 @@ References .. [ICC12] International Code Council. (2011). 2012 International Fire Code, Appendix B - Fire-Flow Requirements for Buildings. Country Club Hills, IL: International Code Council, ISBN: 978-1-60983-046-5. -.. [JaSr08] Jayaram, N. and Srinivasan, K. (2008). Performance-based optimal design and rehabilitation of water distribution networks using life cycle costing. Water resources research, 44(1). +.. [JaSr08] Jayaram, N. and Srinivasan, K. (2008). Performance-based optimal design and rehabilitation of water distribution networks using life cycle costing. Water Resources Research, 44(1). .. [JCMG11] Joyner, D., Certik, O., Meurer, A., and Granger, B.E. (2012). Open source computer algebra systems, SymPy. ACM Communications in Computer Algebra, 45(4), 225-234. @@ -62,7 +62,7 @@ References .. [SPHC16] Sievert, C., Parmer, C., Hocking, T., Chamberlain, S., Ram, K., Corvellec, M., and Despouy, P. (2016). plotly: Create interactive web graphics via Plotly’s JavaScript graphing library [Software]. -.. [SRU23] Shang, F., Rossman, L. A., Uber, J.G. (2023). EPANET-MSX 2.0 User Manual. U.S. Environmental Protection Agency, Cincinnati, OH. EPA/600/R-22/199. +.. [SRU23] Shang, F., Rossman, L. A., and Uber, J.G. (2023). EPANET-MSX 2.0 User Manual. U.S. Environmental Protection Agency, Cincinnati, OH. EPA/600/R-22/199. .. [Todi00] Todini, E. (2000). Looped water distribution networks design using a resilience index based heuristic approach. Urban Water, 2(2), 115-122. diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index 82ce50f61..fdf405dd5 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -1,5 +1,8 @@ # coding: utf-8 -"""Exceptions for EPANET toolkit and IO operations.""" +""" +The wntr.epanet.expections module contains Exceptions for EPANET toolkit +and IO operations. +""" from typing import List diff --git a/wntr/epanet/msx/__init__.py b/wntr/epanet/msx/__init__.py index b7ae5c121..5fbc99912 100644 --- a/wntr/epanet/msx/__init__.py +++ b/wntr/epanet/msx/__init__.py @@ -1,15 +1,19 @@ -"""The wntr.epanet.msx package provides EPANET-MSX compatibility functions for WNTR. +# coding: utf-8 +""" +The wntr.epanet.msx package provides EPANET-MSX compatibility functions for +WNTR. -The following environment variable must be set, or the command `set_msx_path` must -be run prior to trying to instantiate the EPANET-MSX toolkit. +The following environment variable must be set, or the command `set_msx_path` +must be run prior to trying to instantiate the EPANET-MSX toolkit. .. envvar:: WNTR_PATH_TO_EPANETMSX - The full path to the directory where EPANET-MSX has been installed. Specifically, - the directory should contain both toolkit files, epanet2.dll and epanetmsx.dll - (or the appropriate equivalent files for your system architecture). - + The full path to the directory where EPANET-MSX has been installed. + Specifically, the directory should contain both toolkit files, epanet2.dll + and epanetmsx.dll (or the appropriate equivalent files for your system + architecture). """ + import os as _os def set_msx_path(path): diff --git a/wntr/epanet/msx/enums.py b/wntr/epanet/msx/enums.py index e2a77fead..73d7134fc 100644 --- a/wntr/epanet/msx/enums.py +++ b/wntr/epanet/msx/enums.py @@ -1,18 +1,22 @@ -"""EPANET-MSX enum types, for use in toolkit API calls.""" +# coding: utf-8 +""" +The wntr.epanet.msx.enums module contains EPANET-MSX enum types, for use in +toolkit API calls. +""" from enum import IntEnum from wntr.utils.enumtools import add_get @add_get(prefix='MSX_') class TkObjectType(IntEnum): - """The enumeration for object type used in EPANET-MSX. - + """Enumeration for object type used in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: NODE @@ -48,14 +52,14 @@ class TkObjectType(IntEnum): @add_get(prefix='MSX_') class TkSourceType(IntEnum): - """The enumeration for source type used in EPANET-MSX. - + """Enumeration for source type used in EPANET-MSX. + .. warning:: These enum values start with -1. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: NOSOURCE @@ -78,14 +82,14 @@ class TkSourceType(IntEnum): @add_get(prefix='MSX_') class TkUnitSystem(IntEnum): - """The enumeration for the units system used in EPANET-MSX. - + """Enumeration for the units system used in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: US @@ -99,14 +103,15 @@ class TkUnitSystem(IntEnum): @add_get(prefix='MSX_') class TkFlowUnits(IntEnum): - """The enumeration for the flow units used in EPANET-MSX (determined from EPANET INP file read in with the toolkit). - + """Enumeration for the flow units used in EPANET-MSX (determined from + EPANET INP file read in with the toolkit). + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: CFS @@ -144,14 +149,14 @@ class TkFlowUnits(IntEnum): @add_get(prefix='MSX_') class TkMixType(IntEnum): - """The enumeration for the mixing model used in EPANET-MSX. - + """Enumeration for the mixing model used in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: MIX1 @@ -171,14 +176,14 @@ class TkMixType(IntEnum): @add_get(prefix='MSX_') class TkSpeciesType(IntEnum): - """The enumeration for species type used in EPANET-MSX. + """Enumeration for species type used in EPANET-MSX. .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: BULK @@ -192,14 +197,14 @@ class TkSpeciesType(IntEnum): @add_get(prefix='MSX_') class TkExpressionType(IntEnum): - """The enumeration for the expression type used in EPANET-MSX. - + """Enumeration for the expression type used in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: NO_EXPR @@ -219,14 +224,14 @@ class TkExpressionType(IntEnum): @add_get(prefix='MSX_') class TkSolverType(IntEnum): - """The enumeration for the solver type used in EPANET-MSX. - + """Enumeration for the solver type used in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: EUL @@ -243,14 +248,14 @@ class TkSolverType(IntEnum): @add_get(prefix='MSX_') class TkCouplingType(IntEnum): - """The enumeration for the coupling type option used in EPANET-MSX. - + """Enumeration for the coupling type option used in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: NO_COUPLING @@ -264,14 +269,14 @@ class TkCouplingType(IntEnum): @add_get(prefix='MSX_') class TkMassUnits(IntEnum): - """The enumeration for mass units used in EPANET-MSX. + """Enumeration for mass units used in EPANET-MSX. .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: MG @@ -291,14 +296,14 @@ class TkMassUnits(IntEnum): @add_get(prefix='MSX_') class TkAreaUnits(IntEnum): - """The enumeration for area units used in EPANET-MSX. - + """Enumeration for area units used in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: FT2 @@ -315,14 +320,14 @@ class TkAreaUnits(IntEnum): @add_get(prefix='MSX_') class TkRateUnits(IntEnum): - """The enumeration for rate units used in EPANET-MSX. - + """Enumeration for rate units used in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: SECONDS @@ -342,14 +347,14 @@ class TkRateUnits(IntEnum): @add_get(prefix='MSX_') class TkUnits(IntEnum): - """The position for units used in EPANET-MSX. - + """Position for units used in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: LENGTH_UNITS @@ -379,14 +384,14 @@ class TkUnits(IntEnum): @add_get(prefix='MSX_') class TkHydVar(IntEnum): - """The enumeration for hydraulic variable used in EPANET-MSX. + """Enumeration for hydraulic variable used in EPANET-MSX. .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: DIAMETER @@ -422,14 +427,14 @@ class TkHydVar(IntEnum): @add_get(prefix='MSX_') class TkTstat(IntEnum): - """The enumeration used for time statistic in EPANET-MSX. - + """Enumeration used for time statistic in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: SERIES @@ -452,14 +457,14 @@ class TkTstat(IntEnum): @add_get(prefix='MSX_') class TkOption(IntEnum): - """The enumeration used for choosing an option in EPANET-MSX toolkit. + """Enumeration used for choosing an option in EPANET-MSX toolkit. .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: AREA_UNITS_OPTION @@ -497,14 +502,14 @@ class TkOption(IntEnum): @add_get(prefix='MSX_') class TkCompiler(IntEnum): - """The enumeration used for specifying compiler options in EPANET-MSX. - + """Enumeration used for specifying compiler options in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: NO_COMPILER @@ -521,14 +526,14 @@ class TkCompiler(IntEnum): @add_get(prefix='MSX_') class TkFileMode(IntEnum): - """The enumeration for file model used in EPANET-MSX. - + """Enumeration for file model used in EPANET-MSX. + .. warning:: These enum values start with 0. .. rubric:: Enum Members - The following enum names are defined, and, if using the :meth:`get` method, then - they are case insensitive and can be optionally prefixed with "MSX\_". + The following enum names are defined, and, if using the :meth:`get` method, + then they are case insensitive and can be optionally prefixed with "MSX\_". .. autosummary:: SCRATCH_FILE diff --git a/wntr/epanet/msx/exceptions.py b/wntr/epanet/msx/exceptions.py index 2ed33e506..a1dcc6ee4 100644 --- a/wntr/epanet/msx/exceptions.py +++ b/wntr/epanet/msx/exceptions.py @@ -1,12 +1,12 @@ # coding: utf-8 -"""EPANET-MSX error and exception classes.""" +""" +The wntr.epanet.msx.exceptions module contains Exceptions for EPANET-MSX +IO operations. +""" -from enum import IntEnum from typing import List -from wntr.utils.enumtools import add_get - -from ..exceptions import EN_ERROR_CODES, EpanetException +from ..exceptions import EpanetException MSX_ERROR_CODES = { # MSX syntax errors @@ -52,21 +52,25 @@ :meta hide-value: """ + class EpanetMsxException(EpanetException): - def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> None: - """An Exception class for EPANET-MSX Toolkit and IO exceptions. + def __init__(self, code: int, *args: List[object], line_num=None, + line=None) -> None: + """Exception class for EPANET-MSX Toolkit and IO exceptions Parameters ---------- code : int or str or MSXErrors - The EPANET-MSX error code (int) or a string mapping to the MSXErrors enum members + EPANET-MSX error code (int) or a string mapping to the MSXErrors + enum members args : additional non-keyword arguments, optional - If there is a string-format within the error code's text, these will be used to - replace the format, otherwise they will be output at the end of the Exception message. + If there is a string-format within the error code's text, these + will be used to replace the format, otherwise they will be output + at the end of the Exception message. line_num : int, optional - The line number, if reading an INP file, by default None + Line number, if reading an INP file, by default None line : str, optional - The contents of the line, by default None + Contents of the line, by default None """ if not code or int(code) < 400: return super().__init__(code, *args, line_num=line_num, line=line) @@ -87,61 +91,68 @@ def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> class MSXSyntaxError(EpanetMsxException, SyntaxError): def __init__(self, code, *args, line_num=None, line=None) -> None: - """An MSX-specific error that is due to a syntax error in an msx-file. + """MSX-specific error that is due to a syntax error in an msx-file. Parameters ---------- code : int or str or MSXErrors - The EPANET-MSX error code (int) or a string mapping to the MSXErrors enum members + EPANET-MSX error code (int) or a string mapping to the MSXErrors + enum members args : additional non-keyword arguments, optional - If there is a string-format within the error code's text, these will be used to - replace the format, otherwise they will be output at the end of the Exception message. + If there is a string-format within the error code's text, these + will be used to replace the format, otherwise they will be output + at the end of the Exception message. line_num : int, optional - The line number, if reading an INP file, by default None + Line number, if reading an INP file, by default None line : str, optional - The contents of the line, by default None + Contents of the line, by default None """ super().__init__(code, *args, line_num=line_num, line=line) class MSXKeyError(EpanetMsxException, KeyError): def __init__(self, code, name, *args, line_num=None, line=None) -> None: - """An MSX-specific error that is due to a missing or unacceptably named variable/speces/etc. + """MSX-specific error that is due to a missing or unacceptably named + variable/speces/etc. Parameters ---------- code : int or str or MSXErrors - The EPANET-MSX error code (int) or a string mapping to the MSXErrors enum members + EPANET-MSX error code (int) or a string mapping to the MSXErrors + enum members name : str - The key/name/id that is missing + Key/name/id that is missing args : additional non-keyword arguments, optional - If there is a string-format within the error code's text, these will be used to - replace the format, otherwise they will be output at the end of the Exception message. + If there is a string-format within the error code's text, these + will be used to replace the format, otherwise they will be output + at the end of the Exception message. line_num : int, optional - The line number, if reading an INP file, by default None + Line number, if reading an INP file, by default None line : str, optional - The contents of the line, by default None + Contents of the line, by default None """ super().__init__(code, name, *args, line_num=line_num, line=line) class MSXValueError(EpanetMsxException, ValueError): def __init__(self, code, value, *args, line_num=None, line=None) -> None: - """An MSX-specific error that is related to an invalid value. + """MSX-specific error that is related to an invalid value. Parameters ---------- code : int or str or MSXErrors - The EPANET-MSX error code (int) or a string mapping to the MSXErrors enum members + EPANET-MSX error code (int) or a string mapping to the MSXErrors + enum members value : Any - The value that is invalid + Value that is invalid args : additional non-keyword arguments, optional - If there is a string-format within the error code's text, these will be used to - replace the format, otherwise they will be output at the end of the Exception message. + If there is a string-format within the error code's text, these + will be used to replace the format, otherwise they will be output + at the end of the Exception message. line_num : int, optional - The line number, if reading an INP file, by default None + Line number, if reading an INP file, by default None line : str, optional - The contents of the line, by default None + Contents of the line, by default None """ super().__init__(code, value, *args, line_num=line_num, line=line) diff --git a/wntr/epanet/msx/io.py b/wntr/epanet/msx/io.py index e1a87ae31..fc271d30e 100644 --- a/wntr/epanet/msx/io.py +++ b/wntr/epanet/msx/io.py @@ -1,5 +1,8 @@ # coding: utf-8 -"""I/O functions for EPANET-MSX toolkit compatibility""" +""" +The wntr.epanet.msx io module contains methods for reading/writing EPANET +MSX input and output files. +""" import datetime import logging @@ -13,8 +16,7 @@ import wntr.network from wntr.epanet.msx.exceptions import EpanetMsxException, MSXValueError from wntr.epanet.util import ENcomment -from wntr.network.elements import Source -from wntr.msx.base import ReactionType, VariableType, SpeciesType +from wntr.msx.base import VariableType, SpeciesType from wntr.msx.model import MsxModel sys_default_enc = sys.getdefaultencoding() @@ -79,20 +81,22 @@ def __init__(self): @classmethod def read(cls, msx_filename: str, rxn_model: MsxModel = None): """ - Read an EPANET-MSX input file (.msx) and load data into a water quality - reactions model. Only MSX 2.0 files are recognized. + Read an EPANET-MSX input file (.msx) and load data into a MsxModel. + Only MSX 2.0 files are recognized. Parameters ---------- msx_file : str - the filename of the .msx file to read in - rxn_model : WaterQualityReactionsModel, optional - the model to put data into, by default None (new model) + Filename of the .msx file to read in + rxn_model : MsxModel, optional + Multi-species water quality model to put data into, + by default None (new model) Returns ------- - WaterQualityReactionsModel - the model with the new species, reactions and other options added + MsxModel + Multi-species water quality model with the new species, reactions + and other options added """ if rxn_model is None: rxn_model = MsxModel() @@ -180,9 +184,9 @@ def write(cls, filename: str, msx: MsxModel): Parameters ---------- filename : str - the filename to write - rxn : MultispeciesQualityModel - the multispecies reaction model + Filename to write + rxn : MsxModel + Multi-species water quality model """ obj = cls() obj.rxn = msx diff --git a/wntr/epanet/msx/toolkit.py b/wntr/epanet/msx/toolkit.py index 20aea7cd8..e7ffdd78e 100644 --- a/wntr/epanet/msx/toolkit.py +++ b/wntr/epanet/msx/toolkit.py @@ -1,18 +1,18 @@ +# coding: utf-8 """ -The wntr.reaction.toolkit module is a Python extension for the EPANET MSX +The wntr.epanet.msx.toolkit module is a Python extension for the EPANET-MSX Programmers Toolkit DLLs. .. note:: - Code in this section based on code from "EPANET-MSX-Python-wrapper", - licensed under the BSD license. See LICENSE.txt for details. + Code in this section is based on code from "EPANET-MSX-Python-wrapper", + licensed under the BSD license. See LICENSE.md for details. """ import ctypes import os import os.path import platform import sys -from ctypes import byref from typing import Union from pkg_resources import resource_filename @@ -20,7 +20,6 @@ from wntr.epanet.msx.enums import TkObjectType, TkSourceType from ..toolkit import ENepanet -from ..util import SizeLimits from .exceptions import MSX_ERROR_CODES, EpanetMsxException, MSXKeyError, MSXValueError epanet_toolkit = "wntr.epanet.toolkit" @@ -119,14 +118,14 @@ def _error(self, *args): raise EpanetMsxException(self.errcode) return - # ----------running the simulation----------------------------------------------------- + # ----------running the simulation----------------------------------------- def MSXopen(self, msxfile): """Opens the MSX Toolkit to analyze a particular distribution system. Parameters ---------- msxfile : str - name of the MSX input file + Name of the MSX input file """ if msxfile is not None: msxfile = ctypes.c_char_p(msxfile.encode()) @@ -141,7 +140,8 @@ def MSXclose(self): raise EpanetMsxException(ierr) def MSXusehydfile(self, filename): - """Uses the contents of the specified file as the current binary hydraulics file + """Uses the contents of the specified file as the current binary + hydraulics file Parameters ---------- @@ -160,22 +160,25 @@ def MSXsolveH(self): raise EpanetMsxException(ierr) def MSXinit(self, saveFlag=0): - """Initializes the MSX system before solving for water quality results in step-wise fashion - set saveFlag to 1 if water quality results should be saved to a scratch binary file, or to 0 is not saved to file""" + """Initializes the MSX system before solving for water quality results + in step-wise fashion set saveFlag to 1 if water quality results should + be saved to a scratch binary file, or to 0 is not saved to file""" saveFlag = int(saveFlag) ierr = self.ENlib.MSXinit(saveFlag) if ierr != 0: raise EpanetMsxException(ierr) def MSXsolveQ(self): - """solves for water quality over the entire simulation period and saves the results to an internal scratch file""" + """Solves for water quality over the entire simulation period and saves + the results to an internal scratch file""" ierr = self.ENlib.MSXsolveQ() if ierr != 0: raise EpanetMsxException(ierr) def MSXstep(self): """Advances the water quality simulation one water quality time step. - The time remaining in the overall simulation is returned as tleft, the current time as t.""" + The time remaining in the overall simulation is returned as tleft, the + current time as t.""" t = ctypes.c_long() tleft = ctypes.c_long() ierr = self.ENlib.MSXstep(ctypes.byref(t), ctypes.byref(tleft)) @@ -185,8 +188,9 @@ def MSXstep(self): return out def MSXsaveoutfile(self, filename): - """saves water quality results computed for each node, link and reporting time period to a named binary file - + """Saves water quality results computed for each node, link and + reporting time period to a named binary file + Parameters ---------- filename : str @@ -197,45 +201,47 @@ def MSXsaveoutfile(self, filename): raise EpanetMsxException(ierr) def MSXsavemsxfile(self, filename): - """saves the data associated with the current MSX project into a new MSX input file - + """Saves the data associated with the current MSX project into a new + MSX input file + Parameters ---------- filename : str - the name of the MSX input file to create + Name of the MSX input file to create """ ierr = self.ENlib.MSXsavemsxfile(ctypes.c_char_p(filename.encode())) if ierr != 0: raise EpanetMsxException(ierr, filename) def MSXreport(self): - """Writes water quality simulations results as instructed by the MSX input file to a text file""" + """Writes water quality simulations results as instructed by the MSX + input file to a text file""" ierr = self.ENlib.MSXreport() if ierr != 0: raise EpanetMsxException(ierr) - # ---------get parameters--------------------------------------------------------------- + # ---------get parameters-------------------------------------------------- def MSXgetindex(self, _type: Union[int, TkObjectType], name): - """Retrieves the internal index of an MSX object given its name. + """Gets the internal index of an MSX object given its name. Parameters ---------- _type : int, str or ObjectType - The type of object to get an index for + Type of object to get an index for name : str - The name of the object to get an index for + Name of the object to get an index for Returns ------- int - The internal index + Internal index Raises ------ MSXKeyError - if an invalid str is passed for _type + If an invalid str is passed for _type MSXValueError - if _type is not a valid MSX object type + If _type is not a valid MSX object type """ try: _type = TkObjectType.get(_type) @@ -249,19 +255,20 @@ def MSXgetindex(self, _type: Union[int, TkObjectType], name): return ind.value def MSXgetIDlen(self, _type, index): - """Retrieves the number of characters in the ID name of an MSX object given its internal index number. + """Get the number of characters in the ID name of an MSX object + given its internal index number. Parameters ---------- _type : int, str or ObjectType - The type of object to get an index for - index : int - The index of the object to get the ID length for + Type of object to get an index for + index : int + Index of the object to get the ID length for Returns ------- int - the length of the object ID + Length of the object ID """ try: _type = TkObjectType.get(_type) @@ -275,19 +282,19 @@ def MSXgetIDlen(self, _type, index): return len.value def MSXgetID(self, _type, index): - """Retrieves the ID name of an object given its internal index number + """Get the ID name of an object given its internal index number Parameters ---------- _type : int, str or ObjectType - The type of object to get an index for - index : int - The index of the object to get the ID for + Type of object to get an index for + index : int + Index of the object to get the ID for Returns ------- str - the object ID + Object ID """ try: _type = TkObjectType.get(_type) @@ -303,31 +310,31 @@ def MSXgetID(self, _type, index): return id.value.decode() def MSXgetinitqual(self, _type, node_link_index, species_index): - """Retrieves the initial concentration of a particular chemical species assigned to a specific node - or link of the pipe network + """Get the initial concentration of a particular chemical species + assigned to a specific node or link of the pipe network Parameters ---------- _type : str, int or ObjectType - the type of object + Type of object node_link_index : int - the object index + Object index species_index : int - the species index + Species index Returns ------- float - the initial quality value for that node or link + Initial quality value for that node or link Raises ------ MSXKeyError - the type passed in for ``_type`` is not valid + Type passed in for ``_type`` is not valid MSXValueError - the value for ``_type`` is not valid + Value for ``_type`` is not valid EpanetMsxException - any other error from the C-API + Any other error from the C-API """ try: _type = TkObjectType.get(_type) @@ -343,30 +350,31 @@ def MSXgetinitqual(self, _type, node_link_index, species_index): return iniqual.value def MSXgetqual(self, _type, node_link_index, species_index): - """Retrieves a chemical species concentration at a given node or the average concentration along a link at the current simulation time step + """Get a chemical species concentration at a given node or the + average concentration along a link at the current simulation time step Parameters ---------- _type : str, int or ObjectType - the type of object + Type of object node_link_index : int - the object index + Object index species_index : int - the species index + Species index Returns ------- float - the current quality value for that node or link + Current quality value for that node or link Raises ------ MSXKeyError - the type passed in for ``_type`` is not valid + Type passed in for ``_type`` is not valid MSXValueError - the value for ``_type`` is not valid + Value for ``_type`` is not valid EpanetMsxException - any other error from the C-API + Any other error from the C-API """ try: _type = TkObjectType.get(_type) @@ -382,22 +390,22 @@ def MSXgetqual(self, _type, node_link_index, species_index): return qual.value def MSXgetconstant(self, constant_index): - """Retrieves the value of a particular reaction constant + """Get the value of a particular reaction constant Parameters ---------- constant_index : int - index to the constant + Index to the constant Returns ------- float - the value of the constant + Value of the constant Raises ------ EpanetMsxException - a toolkit error occurred + Toolkit error occurred """ const = ctypes.c_double() ierr = self.ENlib.MSXgetconstant(constant_index, ctypes.byref(const)) @@ -406,30 +414,31 @@ def MSXgetconstant(self, constant_index): return const.value def MSXgetparameter(self, _type, node_link_index, param_index): - """Retrieves the value of a particular reaction parameter for a given TANK or PIPE. + """Get the value of a particular reaction parameter for a given + TANK or PIPE. Parameters ---------- _type : int or str or Enum - get the type of the parameter + Get the type of the parameter node_link_index : int - the link index + Link index param_index : int - the parameter variable index + Parameter variable index Returns ------- float - the parameter value + Parameter value Raises ------ MSXKeyError - if there is no such _type + If there is no such _type MSXValueError - if the _type is improper + If the _type is improper EpanetMsxException - any other error + Any other error """ try: _type = TkObjectType.get(_type) @@ -445,10 +454,24 @@ def MSXgetparameter(self, _type, node_link_index, param_index): return param.value def MSXgetsource(self, node_index, species_index): - """Retrieves information on any external source of a particular chemical species assigned to a specific node of the pipe network + """Get information on any external source of a particular + chemical species assigned to a specific node of the pipe network + + Parameters + ---------- + node_index : int + Node index + species_index : int + Species index + + Returns + ------- + list + [source type, level, and pattern] where level is the baseline + concentration (or mass flow rate) of the source and pattern the + index of the time pattern used to add variability to the source's + baseline level (0 if no pattern defined for the source) """ - #level is returned with the baseline concentration (or mass flow rate) of the source - #pat is returned with the index of the time pattern used to add variability to the source's baseline level (0 if no pattern defined for the source)""" level = ctypes.c_double() _type = ctypes.c_int() pat = ctypes.c_int() @@ -459,17 +482,17 @@ def MSXgetsource(self, node_index, species_index): return src_out def MSXgetpatternlen(self, pat): - """Retrieves the number of time periods within a SOURCE time pattern. + """Get the number of time periods within a SOURCE time pattern. Parameters ---------- pat : int - pattern index + Pattern index Returns ------- int - number of time periods in the pattern + Number of time periods in the pattern """ len = ctypes.c_int() ierr = self.ENlib.MSXgetpatternlen(pat, ctypes.byref(len)) @@ -478,19 +501,19 @@ def MSXgetpatternlen(self, pat): return len.value def MSXgetpatternvalue(self, pat, period): - """Retrieves the multiplier at a specific time period for a given SOURCE time pattern + """Get the multiplier at a specific time period for a given + SOURCE time pattern Parameters ---------- pat : int - pattern index + Pattern index period : int 1-indexed period of the pattern to retrieve Returns ------- - float - the multiplier + Multiplier """ val = ctypes.c_double() ierr = self.ENlib.MSXgetpatternvalue(pat, period, ctypes.byref(val)) @@ -499,22 +522,22 @@ def MSXgetpatternvalue(self, pat, period): return val.value def MSXgetcount(self, _type): - """Retrieves the number of objects of a specified type. + """Get the number of objects of a specified type. Parameters ---------- _type : int or str or Enum - the type of object to count + Type of object to count Returns ------- int - the number of objects of specified type + Number of objects of specified type Raises ------ MSXKeyError - if the _type is invalid + If the _type is invalid """ try: _type = TkObjectType.get(_type) @@ -528,18 +551,19 @@ def MSXgetcount(self, _type): return count.value def MSXgetspecies(self, species_index): - """Retrieves the attributes of a chemical species given its internal index number. - species is the sequence number of the species (starting from 1 as listed in the MSX input file + """Get the attributes of a chemical species given its internal + index number. Parameters ---------- species_index : int - the species to query + Species index to query (starting from 1 as listed in the MSX input + file) Returns ------- int, str, float, float - the type, units, aTol, and rTol for the species + Type, units, aTol, and rTol for the species """ type_ind = ctypes.c_int() units = ctypes.create_string_buffer(15) @@ -552,25 +576,25 @@ def MSXgetspecies(self, species_index): return spe_out def MSXgeterror(self, errcode, len=100): - """returns the text for an error message given its error code + """Get the text for an error message given its error code Parameters ---------- errcode : int - the error code + Error code len : int, optional - the length of the error message, by default 100 and minimum 80 + Length of the error message, by default 100 and minimum 80 Returns ------- str - returns a string decoded from the DLL + String decoded from the DLL Warning ------- - Getting string parameters in this way is not recommended, because it requires - setting up string arrays that may or may not be the correct size. Use the - wntr.epanet.msx.enums package to get error information. + Getting string parameters in this way is not recommended, because it + requires setting up string arrays that may or may not be the correct + size. Use the wntr.epanet.msx.enums package to get error information. """ errmsg = ctypes.create_string_buffer(len) self.ENlib.MSXgeterror(errcode, ctypes.byref(errmsg), len) @@ -579,39 +603,40 @@ def MSXgeterror(self, errcode, len=100): # --------------set parameters----------------------------------- def MSXsetconstant(self, ind, value): - """assigns a new value to a specific reaction constant + """Set a new value to a specific reaction constant Parameters ---------- ind : int - the index to the variable + Index to the variable value : float - the value to give the constant + Value to give the constant """ ierr = self.ENlib.MSXsetconstant(ctypes.c_int(ind), ctypes.c_double(value)) if ierr != 0: raise EpanetMsxException(ierr) def MSXsetparameter(self, _type, ind, param, value): - """assigns a value to a particular reaction parameter for a given TANK or PIPE + """Set a value to a particular reaction parameter for a given TANK + or PIPE Parameters ---------- _type : int or str or enum - the type of value to set + Type of value to set ind : int - the tank or pipe index + Tank or pipe index param : int - the parameter variable index + Parameter variable index value : float - the value to be set + Value to be set Raises ------ MSXKeyError - if there is no such _type + If there is no such _type MSXValueError - if the _type is invalid + If the _type is invalid """ try: _type = TkObjectType.get(_type) @@ -625,19 +650,19 @@ def MSXsetparameter(self, _type, ind, param, value): raise EpanetMsxException(ierr) def MSXsetinitqual(self, _type, ind, spe, value): - """Retrieves the initial concentration of a particular chemical species assigned to a specific node - or link of the pipe network. + """Set the initial concentration of a particular chemical species + assigned to a specific node or link of the pipe network. Parameters ---------- _type : int or str or enum - what type of network element is being set + Type of network element to set ind : int - the index of the network element + Index of the network element spe : int - the index of the species + Index of the species value : float - the initial quality value + Initial quality value """ try: _type = TkObjectType.get(_type) @@ -651,20 +676,21 @@ def MSXsetinitqual(self, _type, ind, spe, value): raise EpanetMsxException(ierr) def MSXsetsource(self, node, spe, _type, level, pat): - """sets the attributes of an external source of a particular chemical species in a specific node of the pipe network + """Set the attributes of an external source of a particular chemical + species in a specific node of the pipe network Parameters ---------- node : int - the node index + Node index spe : int - the species index + Species index _type : int or str or enum - the type of source + Type of source level : float - the source quality value + Source quality value pat : int - the pattern index + Pattern index """ try: _type = TkSourceType.get(_type) @@ -676,14 +702,14 @@ def MSXsetsource(self, node, spe, _type, level, pat): raise EpanetMsxException(ierr) def MSXsetpattern(self, pat, mult): - """assigns a new set of multipliers to a given MSX SOURCE time pattern + """Set multipliers to a given MSX SOURCE time pattern Parameters ---------- pat : int - the pattern index + Pattern index mult : list-like - the pattern multipliers + Pattern multipliers """ length = len(mult) cfactors_type = ctypes.c_double * length @@ -695,28 +721,29 @@ def MSXsetpattern(self, pat, mult): raise EpanetMsxException(ierr) def MSXsetpatternvalue(self, pat, period, value): - """Sets the multiplier factor for a specific period within a SOURCE time pattern. + """Set the multiplier factor for a specific period within a SOURCE time + pattern. Parameters ---------- pat : int - the pattern index + Pattern index period : int - the 1-indexed pattern time period index + 1-indexed pattern time period index value : float - the value to set at that time period + Value to set at that time period """ ierr = self.ENlib.MSXsetpatternvalue(ctypes.c_int(pat), ctypes.c_int(period), ctypes.c_double(value)) if ierr != 0: raise EpanetMsxException(ierr) def MSXaddpattern(self, patternid): - """Adds a new, empty MSX source time pattern to an MSX project. + """Add a new, empty MSX source time pattern to an MSX project. Parameters ---------- patternid : str - the name of the new pattern + Name of the new pattern """ ierr = self.ENlib.MSXaddpattern(ctypes.c_char_p(patternid.encode())) if ierr != 0: diff --git a/wntr/msx/__init__.py b/wntr/msx/__init__.py index e44b248fe..d563c9a97 100644 --- a/wntr/msx/__init__.py +++ b/wntr/msx/__init__.py @@ -1,11 +1,13 @@ -# -*- coding: utf-8 -*- -"""Contains definitions for EPANET Multispecies Extension (MSX) water quality modeling. +# coding: utf-8 +""" +The wntr.msx package contains methods to define EPANET Multi-species Extension +(MSX) water quality models. """ from .base import VariableType, SpeciesType, ReactionType, ExpressionType from .elements import Species, Constant, Parameter, Term, Reaction, HydraulicVariable, MathFunction from .model import MsxModel from .options import MsxSolverOptions -from .library import ReactionLibrary, cite_msx +from .library import ReactionLibrary from . import base, elements, library, model, options diff --git a/wntr/msx/_library_data/arsenic_chloramine.json b/wntr/msx/_library_data/arsenic_chloramine.json index c67e1dc13..8661afaa3 100644 --- a/wntr/msx/_library_data/arsenic_chloramine.json +++ b/wntr/msx/_library_data/arsenic_chloramine.json @@ -1,10 +1,10 @@ { - "wntr-version": "1.0.0", + "wntr-version": "", "name": "arsenic_chloramine", "title": "Arsenic Oxidation/Adsorption Example", "description": "This example models monochloramine oxidation of arsenite/arsenate and wall adsoption/desorption, as given in section 3 of the EPANET-MSX user manual", "references": [ - "Shang, F. and Rossman, L.A. and Uber, J.G. (2023) \"EPANET-MSX 2.0 User Manual\". (Cincinnati, OH: Water Infrastructure Division (CESER), U.S. Environmental Protection Agency). EPA/600/R-22/199." + "Shang, F., Rossman, L. A., and Uber, J.G. (2023). EPANET-MSX 2.0 User Manual. U.S. Environmental Protection Agency, Cincinnati, OH. EPA/600/R-22/199." ], "reaction_system": { "species": [ diff --git a/wntr/msx/_library_data/batch_chloramine_decay.json b/wntr/msx/_library_data/batch_chloramine_decay.json index fc4c416db..6563c38e0 100644 --- a/wntr/msx/_library_data/batch_chloramine_decay.json +++ b/wntr/msx/_library_data/batch_chloramine_decay.json @@ -1,5 +1,5 @@ { - "wntr-version": "1.0.0", + "wntr-version": "", "name": "batch_chloramine_decay", "title": "Batch chloramine decay example", "description": null, diff --git a/wntr/msx/_library_data/lead_ppm.json b/wntr/msx/_library_data/lead_ppm.json index 63a69e852..7d483200b 100644 --- a/wntr/msx/_library_data/lead_ppm.json +++ b/wntr/msx/_library_data/lead_ppm.json @@ -1,5 +1,5 @@ { - "wntr-version": "1.0.0", + "wntr-version": "", "name": "lead_ppm", "title": "Lead Plumbosolvency Model (from Burkhardt et al 2020)", "description": "Parameters for EPA HPS Simulator Model", diff --git a/wntr/msx/_library_data/nicotine.json b/wntr/msx/_library_data/nicotine.json index 7603d684d..999f7b888 100644 --- a/wntr/msx/_library_data/nicotine.json +++ b/wntr/msx/_library_data/nicotine.json @@ -1,5 +1,5 @@ { - "wntr-version": "1.0.0", + "wntr-version": "", "name": "nicotine", "title": "Nicotine - Chlorine reaction", "description": null, diff --git a/wntr/msx/_library_data/nicotine_ri.json b/wntr/msx/_library_data/nicotine_ri.json index f73267661..6554f0469 100644 --- a/wntr/msx/_library_data/nicotine_ri.json +++ b/wntr/msx/_library_data/nicotine_ri.json @@ -1,5 +1,5 @@ { - "wntr-version": "1.0.0", + "wntr-version": "", "name": "nicotine_ri", "title": "Nicotine - Chlorine reaction with reactive intermediate", "description": null, diff --git a/wntr/msx/base.py b/wntr/msx/base.py index e795616c7..6bc035ddc 100644 --- a/wntr/msx/base.py +++ b/wntr/msx/base.py @@ -1,7 +1,9 @@ -# -*- coding: utf-8 -*- +# coding: utf-8 +""" +The wntr.msx.base module includes base classes for the multi-species water +quality model -"""The base classes for the the WNTR quality extensions module. -Other than the enum classes, the classes in this module are all abstract +Other than the enum classes, the classes in this module are all abstract and/or mixin classes, and should not be instantiated directly. """ @@ -9,8 +11,8 @@ import os from abc import ABC, abstractclassmethod, abstractmethod, abstractproperty from enum import Enum -from typing import Any, Dict, Iterator, List, Union, Generator -from wntr.epanet.util import ENcomment, NoteType +from typing import Any, Dict, Iterator, Generator +from wntr.epanet.util import NoteType from wntr.utils.disjoint_mapping import DisjointMapping from wntr.utils.enumtools import add_get @@ -59,11 +61,11 @@ def coth(x): {"name": "Av", "note": "Surface area per unit volume (area units/L) "}, {"name": "Len", "note": "Pipe length (feet or meters)"}, ] -"""The hydraulic variables defined in EPANET-MSX. +"""Hydraulic variables defined in EPANET-MSX. For reference, the valid values are provided in :numref:`table-msx-hyd-vars`. .. _table-msx-hyd-vars: -.. table:: Valid hydraulic variables in multispecies quality model expressions. +.. table:: Valid hydraulic variables in multi-species quality model expressions. ============== ================================================ **Name** **Description** @@ -149,7 +151,7 @@ def coth(x): + tuple([k.upper() for k in EXPR_FUNCTIONS.keys()]) + tuple([k.capitalize() for k in EXPR_FUNCTIONS.keys()]) ) -"""The WNTR reserved names. This includes the MSX hydraulic variables +"""WNTR reserved names. This includes the MSX hydraulic variables (see :numref:`table-msx-hyd-vars`) and the MSX defined functions (see :numref:`table-msx-funcs`). @@ -167,7 +169,7 @@ def coth(x): @add_get(abbrev=True) class VariableType(Enum): - """The type of reaction variable. + """Type of reaction variable. The following types are defined, and aliases of just the first character are also defined. @@ -190,15 +192,15 @@ class VariableType(Enum): """ SPECIES = 3 - """A chemical or biological water quality species""" + """Chemical or biological water quality species""" TERM = 4 - """A functional term, or named expression, for use in reaction expressions""" + """Functional term, or named expression, for use in reaction expressions""" PARAMETER = 5 - """A reaction expression coefficient that is parameterized by tank or pipe""" + """Reaction expression coefficient that is parameterized by tank or pipe""" CONSTANT = 6 - """A constant coefficient for use in reaction expressions""" + """Constant coefficient for use in reaction expressions""" RESERVED = 9 - """A variable that is either a hydraulic variable or other reserved word""" + """Variable that is either a hydraulic variable or other reserved word""" S = SPEC = SPECIES T = TERM @@ -209,7 +211,7 @@ class VariableType(Enum): @add_get(abbrev=True) class SpeciesType(Enum): - """The enumeration for species type. + """Enumeration for species type .. warning:: These enum values are not the same as the MSX SpeciesType. @@ -228,9 +230,9 @@ class SpeciesType(Enum): """ BULK = 1 - """bulk species""" + """Bulk species""" WALL = 2 - """wall species""" + """Wall species""" B = BULK W = WALL @@ -238,7 +240,7 @@ class SpeciesType(Enum): @add_get(abbrev=True) class ReactionType(Enum): - """What type of network component does this reaction occur in + """Reaction type which specifies the location where the reaction occurs The following types are defined, and aliases of just the first character are also defined. @@ -256,9 +258,9 @@ class ReactionType(Enum): """ PIPE = 1 - """The expression describes a reaction in pipes""" + """Expression describes a reaction in pipes""" TANK = 2 - """The expression describes a reaction in tanks""" + """Expression describes a reaction in tanks""" P = PIPE T = TANK @@ -266,7 +268,7 @@ class ReactionType(Enum): @add_get(abbrev=True) class ExpressionType(Enum): - """The type of reaction expression. + """Type of reaction expression The following types are defined, and aliases of just the first character are also defined. @@ -287,12 +289,14 @@ class ExpressionType(Enum): """ EQUIL = 1 - """used for equilibrium expressions where it is assumed that the expression supplied is being equated to zero""" + """Equilibrium expressions where equation is being equated to zero""" RATE = 2 - """used to supply the equation that expresses the rate of change of the given species with respect to time as a function - of the other species in the model""" + """Rate expression where the equation expresses the rate of change of + the given species with respect to time as a function of the other species + in the model""" FORMULA = 3 - """used when the concentration of the named species is a simple function of the remaining species""" + """Formula expression where the concentration of the named species is a + simple function of the remaining species""" E = EQUIL R = RATE @@ -300,27 +304,27 @@ class ExpressionType(Enum): class ReactionBase(ABC): - """A water quality reaction class. + """Water quality reaction class This is an abstract class for water quality reactions with partial concrete - attribute and method definitions. All parameters - and methods documented here must be defined by a subclass except for the following: + attribute and method definitions. All parameters and methods documented + here must be defined by a subclass except for the following: - .. rubric:: Concrete attribtues + .. rubric:: Concrete attributes - The :meth:`__init__` method defines the following attributes concretely. Thus, - a subclass should call :code:`super().__init__(species_name, note=note)` at the beginning of its own - initialization. + The :meth:`__init__` method defines the following attributes concretely. + Thus, a subclass should call :code:`super().__init__(species_name, note=note)` + at the beginning of its own initialization. .. autosummary:: ~ReactionBase._species_name ~ReactionBase.note - .. rubric:: Concrete properies + .. rubric:: Concrete properties - The species name is protected, and a reaction cannot be manually assigned a new species. - Therefore, the following property is defined concretely. + The species name is protected, and a reaction cannot be manually assigned + a new species. Therefore, the following property is defined concretely. .. autosummary:: @@ -338,14 +342,15 @@ class ReactionBase(ABC): """ def __init__(self, species_name: str, *, note: NoteType = None) -> None: - """A water quality reaction definition. + """Water quality reaction definition This abstract class must be subclassed. Parameters ---------- species_name : str - The name of the chemical or biological species being modeled using this reaction. + Name of the chemical or biological species being modeled using this + reaction note : (str | dict | ENcomment), optional keyword Supplementary information regarding this reaction, by default None (see-also :class:`~wntr.epanet.util.NoteType`) @@ -353,52 +358,54 @@ def __init__(self, species_name: str, *, note: NoteType = None) -> None: Raises ------ TypeError - if expression_type is invalid + If expression_type is invalid """ if species_name is None: raise TypeError("The species_name cannot be None") self._species_name: str = str(species_name) - """The protected name of the species""" + """Protected name of the species""" self.note: NoteType = note - """An optional note regarding the reaction (see :class:`~wntr.epanet.util.NoteType`) + """Optional note regarding the reaction (see :class:`~wntr.epanet.util.NoteType`) """ @property def species_name(self) -> str: - """The name of the species that has a reaction being defined.""" + """Name of the species that has a reaction being defined.""" return self._species_name @abstractproperty def reaction_type(self) -> Enum: - """The reaction type (reaction location).""" + """Reaction type (reaction location).""" raise NotImplementedError def __str__(self) -> str: - """Return the name of the species and the reaction type, indicated by an arrow. E.g., 'HOCL->PIPE for chlorine reaction in pipes.""" + """Return the name of the species and the reaction type, indicated by + an arrow. E.g., 'HOCL->PIPE for chlorine reaction in pipes.""" return "{}->{}".format(self.species_name, self.reaction_type.name) def __repr__(self) -> str: - """Return a representation of the reaction from the dictionary representation - see :meth:`to_dict`""" + """Return a representation of the reaction from the dictionary + representation - see :meth:`to_dict`""" return "{}(".format(self.__class__.__name__) + ", ".join(["{}={}".format(k, repr(getattr(self, k))) for k, v in self.to_dict().items()]) + ")" @abstractmethod def to_dict(self) -> dict: - """Represent the object as a dictionary.""" + """Represent the object as a dictionary""" raise NotImplementedError class VariableBase(ABC): - """A multi-species water quality model variable. + """Multi-species water quality model variable - This is an abstract class for water quality model variables with partial definition - of concrete attributes and methods. Parameters - and methods documented here must be defined by a subclass except for the following: + This is an abstract class for water quality model variables with partial + definition of concrete attributes and methods. Parameters and methods + documented here must be defined by a subclass except for the following: - .. rubric:: Concrete attribtues + .. rubric:: Concrete attributes - The :meth:`__init__` method defines the following attributes concretely. Thus, - a subclass should call :code:`super().__init__()` at the beginning of its own - initialization. + The :meth:`__init__` method defines the following attributes concretely. + Thus, a subclass should call :code:`super().__init__()` at the beginning + of its own initialization. .. autosummary:: @@ -417,12 +424,12 @@ class VariableBase(ABC): """ def __init__(self, name: str, *, note: NoteType = None) -> None: - """Multi-species variable constructor arguments. + """Multi-species variable constructor arguments Parameters ---------- name : str - The name/symbol for the variable. Must be a valid MSX variable name. + Name/symbol for the variable. Must be a valid MSX variable name note : (str | dict | ENcomment), optional keyword Supplementary information regarding this variable, by default None (see-also :class:`~wntr.epanet.util.NoteType`) @@ -430,21 +437,21 @@ def __init__(self, name: str, *, note: NoteType = None) -> None: Raises ------ KeyExistsError - the name is already taken + Name is already taken ValueError - the name is a reserved word + Name is a reserved word """ if name in RESERVED_NAMES: raise ValueError("Name cannot be a reserved name") self.name: str = name - """The name/ID of this variable, must be a valid EPANET/MSX ID""" + """Name/ID of this variable, must be a valid EPANET/MSX ID""" self.note: NoteType = note - """An optional note regarding the variable (see :class:`~wntr.epanet.util.NoteType`) + """Optional note regarding the variable (see :class:`~wntr.epanet.util.NoteType`) """ @abstractproperty def var_type(self) -> Enum: - """The type of reaction variable""" + """Type of reaction variable""" raise NotImplementedError @abstractmethod @@ -457,24 +464,27 @@ def __str__(self) -> str: return self.name def __repr__(self) -> str: - """Return a representation of the variable from the dictionary representation - see :meth:`to_dict`""" + """Return a representation of the variable from the dictionary + representation - see :meth:`to_dict`""" return "{}(".format(self.__class__.__name__) + ", ".join(["{}={}".format(k, repr(getattr(self, k))) for k, v in self.to_dict().items()]) + ")" class ReactionSystemBase(ABC): - """Abstract class for reaction systems, which contains variables and reaction expressions. + """Abstract class for reaction systems, which contains variables and + reaction expressions. This class contains the functions necessary to perform dictionary-style addressing of *variables* by their name. It does not allow dictionary-style addressing of reactions. - This is an abstract class with some concrete attributes and methods. Parameters - and methods documented here must be defined by a subclass except for the following: + This is an abstract class with some concrete attributes and methods. + Parameters and methods documented here must be defined by a subclass + except for the following: .. rubric:: Concrete attributes - The :meth:`__init__` method defines the following attributes concretely. Thus, - a subclass should call :code:`super().__init__()` or :code:`super().__init__(filename)`. + The :meth:`__init__` method defines the following attributes concretely. + Thus, a subclass should call :code:`super().__init__()` or :code:`super().__init__(filename)`. .. autosummary:: @@ -483,8 +493,8 @@ class ReactionSystemBase(ABC): .. rubric:: Concrete methods - The following special methods are concretely provided to directly access items - in the :attr:`_vars` attribute. + The following special methods are concretely provided to directly access + items in the :attr:`_vars` attribute. .. autosummary:: :nosignatures: @@ -498,11 +508,12 @@ class ReactionSystemBase(ABC): """ def __init__(self) -> None: - """The constructor for the reaction system.""" + """Constructor for the reaction system.""" self._vars: DisjointMapping = DisjointMapping() - """The variables registry, which is mapped to dictionary functions on the reaction system object""" + """Variables registry, which is mapped to dictionary functions on the + reaction system object""" self._rxns: Dict[str, Any] = dict() - """The reactions dictionary""" + """Reactions dictionary""" @abstractmethod def add_variable(self, obj: VariableBase) -> None: @@ -516,12 +527,12 @@ def add_reaction(self, obj: ReactionBase) -> None: @abstractmethod def variables(self) -> Generator[Any, Any, Any]: - """A generator looping through all variables""" + """Generator looping through all variables""" raise NotImplementedError @abstractmethod def reactions(self) -> Generator[Any, Any, Any]: - """A generator looping through all reactions""" + """Generator looping through all reactions""" raise NotImplementedError @abstractmethod @@ -549,11 +560,11 @@ def __len__(self) -> int: class VariableValuesBase(ABC): - """Abstract class for a variable's network-specific values. + """Abstract class for a variable's network-specific values This class should contain values for different pipes, tanks, etc., that correspond to a specific network for the reaction - system. It can be used for intial concentration values, or + system. It can be used for initial concentration values, or for initial settings on parameters, but should be information that is clearly tied to a specific type of variable. @@ -563,23 +574,17 @@ class VariableValuesBase(ABC): @abstractproperty def var_type(self) -> Enum: - """The type of variable this object holds data for.""" + """Type of variable this object holds data for.""" raise NotImplementedError @abstractmethod def to_dict(self) -> dict: - """Represent a specific variable's network-specific values as a dictionary. - - Returns - ------- - dict - the network-specific values for a specific variable - """ + """Represent the object as a dictionary""" raise NotImplementedError class NetworkDataBase(ABC): - """Abstract class containing network specific data. + """Abstract class containing network specific data This class should be populated with things like initial quality, sources, parameterized values, etc. @@ -590,26 +595,21 @@ class NetworkDataBase(ABC): @abstractmethod def to_dict(self) -> dict: - """Represent the quality-relevant network-specific data as a dictionary. - - Returns - ------- - dict - the quality-relevant network data - """ + """Represent the object as a dictionary""" raise NotImplementedError class QualityModelBase(ABC): - """Abstract water quality model. + """Abstract multi-species water quality model - This is an abstract class for a water quality model. All parameters - and methods documented here must be defined by a subclass except for the following: + This is an abstract class for a water quality model. All parameters and + methods documented here must be defined by a subclass except for the + following: .. rubric:: Concrete attributes - The :meth:`__init__` method defines the following attributes concretely. Thus, - a subclass should call :code:`super().__init__()` or :code:`super().__init__(filename)`. + The :meth:`__init__` method defines the following attributes concretely. + Thus, a subclass should call :code:`super().__init__()` or :code:`super().__init__(filename)`. .. autosummary:: @@ -624,34 +624,34 @@ class QualityModelBase(ABC): """ def __init__(self, filename=None): - """Abstract water quality model. + """Abstract water quality model Parameters ---------- filename : str, optional - the file to use to populate the initial data + File to use to populate the initial data """ self.name: str = None if filename is None else os.path.splitext(os.path.split(filename)[1])[0] - """A name for the model, or the MSX model filename (no spaces allowed)""" + """Name for the model, or the MSX model filename (no spaces allowed)""" self.title: str = None - """The title line from the MSX file, must be a single line""" + """Title line from the MSX file, must be a single line""" self.description: str = None - """A longer description; note that multi-line descriptions may not be + """Longer description; note that multi-line descriptions may not be represented well in dictionary form""" self._orig_file: str = filename - """The protected original filename, if provided in the constructor""" + """Protected original filename, if provided in the constructor""" self._options = None - """The protected options data object""" + """Protected options data object""" self._rxn_system: ReactionSystemBase = None - """The protected reaction system object""" + """Protected reaction system object""" self._net_data: NetworkDataBase = None - """The protected network data object""" + """Protected network data object""" self._wn = None - """The protected water network object""" + """Protected water network object""" @abstractproperty def options(self): - """The model options structure. + """Model options structure Concrete classes should implement this with the appropriate typing and also implement a setter method. @@ -660,7 +660,7 @@ def options(self): @abstractproperty def reaction_system(self) -> ReactionSystemBase: - """The reaction variables defined for this model. + """Reaction variables defined for this model Concrete classes should implement this with the appropriate typing. """ @@ -668,7 +668,7 @@ def reaction_system(self) -> ReactionSystemBase: @abstractproperty def network_data(self) -> NetworkDataBase: - """The network-specific values added to this model. + """Network-specific values added to this model Concrete classes should implement this with the appropriate typing. """ @@ -676,27 +676,21 @@ def network_data(self) -> NetworkDataBase: @abstractmethod def to_dict(self) -> dict: - """Represent the model as a dictionary. - - Returns - ------- - dict - A dictionary representation of a water quality model - """ + """Represent the object as a dictionary""" raise NotImplementedError @abstractclassmethod def from_dict(self, data: dict) -> "QualityModelBase": - """Create a new model from a dictionary. + """Create a new model from a dictionary Parameters ---------- data : dict - A dictionary representation of a water quality model + Dictionary representation of the model Returns ------- QualityModelBase - the new concrete water quality model + New concrete model """ raise NotImplementedError diff --git a/wntr/msx/elements.py b/wntr/msx/elements.py index 82fa4236f..3499a59ef 100644 --- a/wntr/msx/elements.py +++ b/wntr/msx/elements.py @@ -1,11 +1,14 @@ -# -*- coding: utf-8 -*- - -"""Concrete implementations of MSX classes. +# 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, Literal, Tuple, Union +from typing import Any, Dict, Tuple, Union from wntr.epanet.util import ENcomment, NoteType from wntr.utils.disjoint_mapping import KeyExistsError @@ -25,7 +28,7 @@ class Species(VariableBase): - """A biological or chemical species that impacts water quality. + """Biological or chemical species """ def __init__( @@ -41,44 +44,46 @@ def __init__( _vars: ReactionSystemBase = None, _vals: VariableValuesBase = None, ) -> None: - """A biological or chemical species. + """Biological or chemical species Parameters ---------- name - The species name + Species name species_type - The species type + Species type units - The units of mass for this species, see :attr:`units` property. + Units of mass for this species, see :attr:`units` property. atol : float | None - The absolute tolerance when solving this species' equations, by default None + Absolute tolerance when solving this species' equations, by default + None rtol : float | None - The relative tolerance when solving this species' equations, by default 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 - the reaction system this species is a part of, by default None + Reaction system this species is a part of, by default None _vals - the initial quality values for this species, by default None + Initial quality values for this species, by default None Raises ------ KeyExistsError - if the name has already been used + If the name has already been used TypeError - if `atol` and `rtol` are not the same type + If `atol` and `rtol` are not the same type ValueError - if `atol` or `rtol` ≤ 0 + 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. + 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. """ super().__init__(name, note=note) if _vars is not None and not isinstance(_vars, ReactionSystemBase): @@ -94,12 +99,12 @@ def __init__( self._tolerances: Tuple[float, float] = None self.set_tolerances(atol, rtol) self.units: str = units - """The 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). + """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 - """The diffusivity of this species in water, if being used, by default None""" + """Diffusivity of this species in water, if being used, by default None""" self._vars: ReactionSystemBase = _vars self._vals: InitialQuality = _vals @@ -112,16 +117,16 @@ def set_tolerances(self, atol: float, rtol: float): Parameters ---------- atol - The absolute tolerance to use + Absolute tolerance to use rtol - The relative tolerance to use + Relative tolerance to use Raises ------ TypeError - if only one of `atol` or `rtol` is a float + If only one of `atol` or `rtol` is a float ValueError - if `atol` or `rtol` ≤ 0 + 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") @@ -138,41 +143,43 @@ def get_tolerances(self) -> Union[Tuple[float, float], None]: Returns ------- (atol, rtol) : (float, float) or None - absolute and relative tolerances, respectively, if they are set + Absolute and relative tolerances, respectively, if they are set """ return self._tolerances def clear_tolerances(self): - """Set both tolerances to None, reverting to the global options value.""" + """Set both tolerances to None, reverting to the global options value. + """ self._tolerances = None @property def atol(self) -> float: - """The absolute tolerance. Must be set using :meth:`set_tolerances`""" + """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: - """The relative tolerance. Must be set using :meth:`set_tolerances`""" + """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: - """The type of variable, :attr:`~wntr.msx.base.VariableType.SPECIES`.""" + """Type of variable, :attr:`~wntr.msx.base.VariableType.SPECIES`.""" return VariableType.SPECIES @property def species_type(self) -> SpeciesType: - """The type of species, either :attr:`~wntr.msx.base.SpeciesType.BULK` or :attr:`~wntr.msx.base.SpeciesType.WALL`""" + """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': - """If a specific network has been linked, then the initial quality values for the network""" + """Initial quality values for the network""" if self._vals is not None: return self._vals else: @@ -180,7 +187,7 @@ def initial_quality(self) -> 'InitialQuality': @property def pipe_reaction(self) -> 'Reaction': - """The pipe reaction definition""" + """Pipe reaction definition""" if self._vars is not None: return self._vars.pipe_reactions[self.name] else: @@ -188,14 +195,14 @@ def pipe_reaction(self) -> 'Reaction': @property def tank_reaction(self) -> 'Reaction': - """The tank reaction definition""" + """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") def to_dict(self) -> Dict[str, Any]: - """Create a dictionary representation of the object + """Dictionary representation of the object The species dictionary has the following format, as described using a json schema. @@ -249,23 +256,23 @@ def to_dict(self) -> Dict[str, Any]: class Constant(VariableBase): - """A constant coefficient for use in reaction expressions.""" + """Constant coefficient for use in expressions""" def __init__(self, name: str, value: float, *, units: str = None, note: Union[str, dict, ENcomment] = None, _vars: ReactionSystemBase = None) -> None: - """A variable representing a constant value. + """Constant coefficient for use in expressions Parameters ---------- name - The name of the variable. + Name of the variable. value - The constant value. + Constant value. units Units for the variable, by default None note Supplementary information regarding this variable, by default None _vars - the reaction system this constant is a part of, by default None + Reaction system this constant is a part of, by default None """ super().__init__(name, note=note) if _vars is not None and not isinstance(_vars, ReactionSystemBase): @@ -273,9 +280,9 @@ def __init__(self, name: str, value: float, *, units: str = None, note: Union[st if _vars is not None and name in _vars: raise KeyExistsError("This variable name is already taken") self.value: float = float(value) - """The value of the constant""" + """Value of the constant""" self.units: str = units - """The units of the constant""" + """Units of the constant""" self._vars: ReactionSystemBase = _vars def __call__(self, *, t=None) -> Any: @@ -283,10 +290,11 @@ def __call__(self, *, t=None) -> Any: @property def var_type(self) -> VariableType: - """The type of variable, :attr:`~wntr.msx.base.VariableType.CONSTANT`.""" + """Type of variable, :attr:`~wntr.msx.base.VariableType.CONSTANT`.""" return VariableType.CONSTANT 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 @@ -298,27 +306,27 @@ def to_dict(self) -> Dict[str, Any]: class Parameter(VariableBase): - """A coefficient that is parameterized by pipe/tank.""" + """Parameterized variable for use in expressions""" def __init__( self, name: str, global_value: float, *, units: str = None, note: Union[str, dict, ENcomment] = None, _vars: ReactionSystemBase = None, _vals: VariableValuesBase = None ) -> None: - """A parameterized variable for use in expressions. + """Parameterized variable for use in expressions Parameters ---------- name - The name of this parameter. + Name of this parameter. global_value - The global value for the parameter if otherwise unspecified. + Global value for the parameter if otherwise unspecified. units - The units for this parameter, by default None + Units for this parameter, by default None note Supplementary information regarding this variable, by default None _vars - the reaction system this parameter is a part of, by default None + Reaction system this parameter is a part of, by default None _vals - the netork-specific values for this parameter, by default None + Network-specific values for this parameter, by default None """ super().__init__(name, note=note) if _vars is not None and not isinstance(_vars, ReactionSystemBase): @@ -343,21 +351,22 @@ def __call__(self, *, pipe: str = None, tank: str = None) -> float: Parameters ---------- pipe - the name of a pipe to get the parameter value for, by default None + Name of a pipe to get the parameter value for, by default None tank - the name of a pipe to get the parameter value for, by default None + Name of a pipe to get the parameter value for, by default None Returns ------- float - the value at the specified pipe or tank, or the global value + Value at the specified pipe or tank, or the global value Raises ------ TypeError - if both pipe and tank are specified + If both pipe and tank are specified ValueError - if there is no ParameterValues object defined for and linked to this parameter + 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") @@ -371,10 +380,11 @@ def __call__(self, *, pipe: str = None, tank: str = None) -> float: @property def var_type(self) -> VariableType: - """The type of variable, :attr:`~wntr.msx.base.VariableType.PARAMETER`.""" + """Type of variable, :attr:`~wntr.msx.base.VariableType.PARAMETER`.""" return VariableType.PARAMETER 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 @@ -386,21 +396,23 @@ def to_dict(self) -> Dict[str, Any]: class Term(VariableBase): - """A named expression that can be used as a term in other expressions.""" + """Named expression (term) that can be used in expressions""" - def __init__(self, name: str, expression: str, *, note: Union[str, dict, ENcomment] = None, _vars: ReactionSystemBase = None) -> None: - """A named expression that can be used as a term in other expressions. + def __init__(self, name: str, expression: str, *, + note: Union[str, dict, ENcomment] = None, + _vars: ReactionSystemBase = None) -> None: + """Named expression (term) that can be used in expressions Parameters ---------- name - The variable name. + Variable name. expression - The mathematical expression to be aliased + Mathematical expression to be aliased note Supplementary information regarding this variable, by default None _vars - the reaction system this species is a part of, by default None + Reaction system this species is a part of, by default None """ super().__init__(name, note=note) if _vars is not None and not isinstance(_vars, ReactionSystemBase): @@ -408,15 +420,16 @@ def __init__(self, name: str, expression: str, *, note: Union[str, dict, ENcomme if _vars is not None and name in _vars: raise KeyExistsError("This variable name is already taken") self.expression: str = expression - """The expression that is aliased by this term""" + """Expression that is aliased by this term""" self._vars: ReactionSystemBase = _vars @property def var_type(self) -> VariableType: - """The type of variable, :attr:`~wntr.msx.base.VariableType.TERM`.""" + """Type of variable, :attr:`~wntr.msx.base.VariableType.TERM`.""" return VariableType.TERM 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() @@ -426,70 +439,71 @@ def to_dict(self) -> Dict[str, Any]: class ReservedName(VariableBase): - """An object representing a reserved name that should not be used by the user.""" + """Reserved name that should not be used""" def __init__(self, name: str, *, note: Union[str, dict, ENcomment] = None) -> None: - """An object representing a reserved name that should not be used by the user. + """Reserved name that should not be used Parameters ---------- name - The reserved name. + Reserved name. note Supplementary information regarding this variable, by default None Raises ------ KeyExistsError - if the name has already been registered + If the name has already been registered """ self.name: str = name self.note: Union[str, dict, ENcomment] = note @property def var_type(self) -> VariableType: - """The type of variable, :attr:`~wntr.msx.base.VariableType.RESERVED`.""" + """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))])) class HydraulicVariable(ReservedName): - """A variable representing instantaneous hydraulics data.""" + """Reserved name for hydraulic variables""" def __init__(self, name: str, units: str = None, *, note: Union[str, dict, ENcomment] = None) -> None: - """A variable representing instantaneous hydraulics data. + """Reserved name for hydraulic variables The user should not need to create any variables using this class, they - are created automatically by the MultispeciesModel object during initialization. + are created automatically by the MsxModel object during initialization. Parameters ---------- name - The name of the variable (predefined by MSX) + Name of the variable (predefined by MSX) units - The units for hydraulic variable, by default None + Units for hydraulic variable, by default None note Supplementary information regarding this variable, by default None """ super().__init__(name, note=note) self.units: str = units - """The hydraulic variable's units""" + """Hydraulic variable's units""" class MathFunction(ReservedName): - """A variable that is actually a mathematical function defined by MSX.""" + """Reserved name for math functions""" def __init__(self, name: str, func: callable, *, note: Union[str, dict, ENcomment] = None) -> None: - """A variable that is actually a mathematical function defined by MSX. + """Reserved name for math functions Parameters ---------- name - The function name + Function name func - The callable function + Callable function note Supplementary information regarding this variable, by default None """ @@ -503,7 +517,7 @@ def __call__(self, *args: Any, **kwds: Any) -> Any: class Reaction(ReactionBase): - """A water quality biochemical reaction dynamics definition for a specific species.""" + """Water quality reaction dynamics definition for a specific species""" def __init__( self, @@ -515,22 +529,24 @@ def __init__( note: Union[str, dict, ENcomment] = None, _vars: ReactionSystemBase = None, ) -> None: - """A water quality biochemical reaction dynamics definition for a specific species. + """Water quality reaction dynamics definition for a specific species Parameters ---------- species_name - The species (object or name) this reaction is applicable to. + Species (object or name) this reaction is applicable to. reaction_type - The type of reaction, either PIPE or TANK + Reaction type (location), from {PIPE, TANK} expression_type - The type of reaction dynamics being described by the expression: one of RATE, FORMULA, or EQUIL. + Expression type (left-hand-side) of the equation, from + {RATE, EQUIL, FORMULA} expression - The mathematical expression for the right-hand-side of the reaction equation. + Mathematical expression for the right-hand-side of the reaction + equation. note Supplementary information regarding this variable, by default None _vars - the reaction system this species is a part of, by default None + Reaction system this species is a part of, by default None """ super().__init__(species_name=species_name, note=note) if _vars is not None and not isinstance(_vars, ReactionSystemBase): @@ -546,20 +562,26 @@ def __init__( if not expression: raise TypeError("expression cannot be None") self.expression: str = expression - """The mathematical expression (right-hand-side)""" + """Mathematical expression (right-hand-side)""" self._vars: ReactionSystemBase = _vars @property def expression_type(self) -> ExpressionType: - """The 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`""" + """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: - """The reaction type (reaction location), either :attr:`~wntr.msx.base.ReactionType.PIPE` or :attr:`~wntr.msx.base.ReactionType.TANK`""" + """Reaction type (location), either + :attr:`~wntr.msx.base.ReactionType.PIPE` or + :attr:`~wntr.msx.base.ReactionType.TANK`""" return self.__rxn_type 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() @@ -569,22 +591,24 @@ def to_dict(self) -> dict: class InitialQuality(VariableValuesBase): - """A container for initial quality values for a species in a specific network.""" + """Initial quality values for a species in a specific network""" def __init__(self, global_value: float = 0.0, node_values: dict = None, link_values: dict = None): - """The initial quality values for a species. + """Initial quality values for a species in a specific network Parameters ---------- global_value - the global initial quality value, by default 0.0 + Global initial quality value, by default 0.0 node_values - any different initial quality values for specific nodes, by default None + Any different initial quality values for specific nodes, + by default None link_values - any different initial quality values for specific links, by default None + Any different initial quality values for specific links, + by default None """ self.global_value = global_value - """The global initial quality values for this species.""" + """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() @@ -595,35 +619,41 @@ def __repr__(self) -> str: @property def var_type(self) -> VariableType: - """The type of variable, :attr:`~wntr.msx.base.VariableType.SPECIES`, this object holds data for.""" + """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]: - """A mapping that overrides the global_value of the initial quality at specific nodes""" + """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]: - """A mapping that overrides the global_value of the initial quality in specific links""" + """Mapping that overrides the global_value of the initial quality in + specific links""" return self._link_values 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()) class ParameterValues(VariableValuesBase): - """A container for pipe and tank specific values of a parameter for a specific network.""" + """Pipe and tank specific values of a parameter for a specific network""" def __init__(self, *, pipe_values: dict = None, tank_values: dict = None) -> None: - """The non-global values for a parameter. + """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 + 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 + Any different values for this parameter for specific tanks, + by default 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() @@ -633,18 +663,22 @@ def __repr__(self) -> str: @property def var_type(self) -> VariableType: - """The type of variable, :attr:`~wntr.msx.base.VariableType.PARAMETER`, this object holds data for.""" + """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]: - """A mapping that overrides the global_value of a parameter for a specific pipe""" + """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]: - """A mapping that overrides the global_value of a parameter for a specific tank""" + """Mapping that overrides the global_value of a parameter for a + specific tank""" return self._tank_values 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()) diff --git a/wntr/msx/library.py b/wntr/msx/library.py index 76f00a173..62ce4d1ea 100644 --- a/wntr/msx/library.py +++ b/wntr/msx/library.py @@ -1,18 +1,19 @@ -# -*- coding: utf-8 -*- - -r"""A library of common MSX reactions. - +# coding: utf-8 +""" +The wntr.msx.library module includes a library of multi-species water +models .. rubric:: Environment Variable .. envvar:: WNTR_RXN_LIBRARY_PATH This environment variable, if set, will add additional folder(s) to the - path to search for quality model files, (files with an ".msx", ".yaml", - or ".json" file extension). + path to search for multi-species water quality model files, + (files with an ".msx", ".yaml", or ".json" file extension). Multiple folders should be separated using the "``;``" character. See :class:`~wntr.msx.library.ReactionLibrary` for more details. """ + from __future__ import annotations import json @@ -44,67 +45,49 @@ logger = logging.getLogger(__name__) -def cite_msx() -> dict: - """A citation generator for the EPANET-MSX user guide. - - Returns - ------- - str - Shang, F. and Rossman, L.A. and Uber, J.G. (2023) "EPANET-MSX 2.0 User Manual". (Cincinnati, OH: Water Infrastructure Division (CESER), U.S. Environmental Protection Agency). EPA/600/R-22/199. - """ - return 'Shang, F. and Rossman, L.A. and Uber, J.G. (2023) "EPANET-MSX 2.0 User Manual". (Cincinnati, OH: Water Infrastructure Division (CESER), U.S. Environmental Protection Agency). EPA/600/R-22/199.' - # return dict( - # entry_type="report", - # key="SRU23", - # fields=dict( - # title="EPANET-MSX 2.0 User Manual", - # year=2023, - # author="Shang, F. and Rossman, L.A. and Uber, J.G.", - # institution="Water Infrastructure Division (CESER), U.S. Environmental Protection Agency", - # location="Cincinnati, OH", - # number="EPA/600/R-22/199", - # ), - # ) - - class ReactionLibrary: - """A library of multispecies reaction definitions. + """Library of multi-species water quality models - This object can be accessed and treated like a dictionary, where keys are the model - names and the values are the model objects. + This object can be accessed and treated like a dictionary, where keys are + the model names and the values are the model objects. Paths are added/processed in the following order: 1. the builtin directory of reactions, - 2. any paths specified in the environment variable described below, with directories listed - first having the highest priority, - 3. any extra paths specified in the constructor, searched in the order provided. + 2. any paths specified in the environment variable described below, with + directories listed first having the highest priority, + 3. any extra paths specified in the constructor, searched in the order + provided. + + Once created, the library paths cannot be modified. However, a model can + be added to the library using :meth:`add_model_from_file` or + :meth:`add_models_from_dir`. - Once created, the library paths cannot be modified. However, a model can be added - to the library using :meth:`add_model_from_file` or :meth:`add_models_from_dir`. """ - def __init__(self, extra_paths: List[str] = None, include_builtins=True, include_envvar_paths=True, load=True) -> None: - """A library of multispecies reaction definitions. + def __init__(self, extra_paths: List[str] = None, include_builtins=True, + include_envvar_paths=True, load=True) -> None: + """Library of multi-species water quality models Parameters ---------- extra_paths : list of str, optional - user-specified list of reaction library directories, by default None + User-specified list of reaction library directories, by default + None include_builtins : bool, optional - load files built-in with wntr, by default True + Load files built-in with WNTR, by default True include_envvar_paths : bool, optional - load files from the paths specified in :envvar:`WNTR_RXN_LIBRARY_PATH`, by default True + Load files from the paths specified in + :envvar:`WNTR_RXN_LIBRARY_PATH`, by default True load : bool or str, optional - load the files immediately on creation, by default True. - + Load the files immediately on creation, by default True. If a string, then it will be passed as the `duplicates` argument to the load function. See :meth:`reset_and_reload` for details. Raises ------ TypeError - if `extra_paths` is not a list + If `extra_paths` is not a list """ if extra_paths is None: extra_paths = list() @@ -142,20 +125,20 @@ def __repr__(self) -> str: return "{}({})".format(self.__class__.__name__, repr(self.__library_paths)) def path_list(self) -> List[str]: - """Get the original list of paths used for this library. + """List of paths used to populate the library Returns ------- list of str - a copy of the paths used to **initially** populate this library + Copy of the paths used to **initially** populate the library """ return self.__library_paths.copy() def reset_and_reload(self, duplicates: str = "error") -> List[Tuple[str, str, Any]]: - """Load data from the configured directories into a library of models. + """Load data from the configured directories into a library of models - Note, this function is not recursive and does not 'walk' any of the library's - directories to look for subfolders. + Note, this function is not recursive and does not 'walk' any of the + library's directories to look for subfolders. The ``duplicates`` argument specifies how models that have the same name, or that have the same filename if a name isn't specified in the file, @@ -187,13 +170,13 @@ def reset_and_reload(self, duplicates: str = "error") -> List[Tuple[str, str, An Raises ------ TypeError - if `duplicates` is not a string + If `duplicates` is not a string ValueError - if `duplicates` is not a valid value + If `duplicates` is not a valid value IOError - if `path_to_folder` is not a directory + If `path_to_folder` is not a directory KeyError - if `duplicates` is ``"error"`` and two models have the same name + If `duplicates` is ``"error"`` and two models have the same name """ if duplicates and not isinstance(duplicates, str): raise TypeError("The `duplicates` argument must be None or a string") @@ -207,44 +190,43 @@ def reset_and_reload(self, duplicates: str = "error") -> List[Tuple[str, str, An return load_errors def add_models_from_dir(self, path_to_dir: str, duplicates: str = "error") -> List[Tuple[str, str, Union[MsxModel, Exception]]]: - """Load all valid model files in a folder. - - Note, this function is not recursive and does not 'walk' a directory tree. + """Load all valid model files in a folder - The ``duplicates`` argument specifies how models that have the same name, - or that have the same filename if a name isn't specified in the file, - are handled. **Warning**, if two files in the same directory have models - with the same name, there is no guarantee which will be read in first. + Note, this function is not recursive and does not 'walk' a directory + tree. Parameters ---------- path_to_dir : str - the path to the folder to search + Path to the folder to search duplicates : {"error", "skip", "replace"}, optional + Specifies how models that have the same name, or that have the same + filename if a name isn't specified in the file, are handled. + **Warning**, if two files in the same directory have models with + the same name, there is no guarantee which will be read in first, by default ``"error"`` - - - A value of of ``"error"`` raises an exception and stops execution. - - A value of ``"skip"`` will skip models with the same `name` as a model that already - exists in the library. - - A value of ``"replace"`` will replace any existing model with a model that is read - in that has the same `name`. + - A value of of ``"error"`` raises an exception and stops execution + - A value of ``"skip"`` will skip models with the same `name` as a + model that already exists in the library. + - A value of ``"replace"`` will replace any existing model with a + model that is read in that has the same `name`. Returns ------- (filename, reason, obj) : tuple[str, str, Any] - the file not read in, the cause of the problem, and the object that was skipped/overwritten - or the exception raised + File not read in, the cause of the problem, and the object that was + skipped/overwritten or the exception raised Raises ------ TypeError - if `duplicates` is not a string + If `duplicates` is not a string ValueError - if `duplicates` is not a valid value + If `duplicates` is not a valid value IOError - if `path_to_folder` is not a directory + If `path_to_folder` is not a directory KeyError - if `duplicates` is ``"error"`` and two models have the same name + If `duplicates` is ``"error"`` and two models have the same name """ if duplicates and not isinstance(duplicates, str): raise TypeError("The `duplicates` argument must be None or a string") @@ -309,7 +291,7 @@ def add_models_from_dir(self, path_to_dir: str, duplicates: str = "error") -> Li return load_errors def add_model_from_file(self, path_and_filename: str, name: str = None): - """Load a reaction model from a file and add it to the model. + """Load a model from a file and add it to the library Note, this **does not check** to see if a model exists with the same name, and it will automatically overwrite the existing model if one @@ -349,22 +331,22 @@ def add_model_from_file(self, path_and_filename: str, name: str = None): self.__data[new.name] = new def get_model(self, name: str) -> MsxModel: - """Get a reaction model from the library by model name. + """Get a model from the library by model name Parameters ---------- name : str - the name of the model + Name of the model Returns ------- MsxModel - the model object + Model object """ return self.__data[name] def model_name_list(self) -> List[str]: - """Get the names of all models in the library. + """Get the names of all models in the library Returns ------- diff --git a/wntr/msx/model.py b/wntr/msx/model.py index 4d9749c3d..49bf90058 100644 --- a/wntr/msx/model.py +++ b/wntr/msx/model.py @@ -1,13 +1,15 @@ -# -*- coding: utf-8 -*- - -"""The WNTR MSX model. +# coding: utf-8 +""" +The wntr.msx.model module includes methods to build a multi-species water +quality model. """ + from __future__ import annotations import logging import warnings -from typing import Any, Dict, Generator, List, NewType, Union -from wntr.epanet.util import ENcomment, NoteType +from typing import Dict, Generator, List, Union +from wntr.epanet.util import NoteType from wntr.utils.disjoint_mapping import KeyExistsError @@ -18,7 +20,6 @@ ReactionBase, NetworkDataBase, ReactionSystemBase, - VariableBase, ExpressionType, ReactionType, SpeciesType, @@ -34,7 +35,8 @@ class MsxReactionSystem(ReactionSystemBase): - """A registry for all the variables registered in the multispecies reactions model. + """Registry for all the variables registered in the multi-species reactions + model. This object can be used like a mapping. """ @@ -53,32 +55,32 @@ def __init__(self) -> None: @property def species(self) -> Dict[str, Species]: - """The dictionary view onto only species""" + """Dictionary view onto only species""" return self._species @property def constants(self) -> Dict[str, Constant]: - """The dictionary view onto only constants""" + """Dictionary view onto only constants""" return self._const @property def parameters(self) -> Dict[str, Parameter]: - """The dictionary view onto only parameters""" + """Dictionary view onto only parameters""" return self._param @property def terms(self) -> Dict[str, Term]: - """The dictionary view onto only named terms""" + """Dictionary view onto only named terms""" return self._term @property def pipe_reactions(self) -> Dict[str, Reaction]: - """The dictionary view onto pipe reactions""" + """Dictionary view onto pipe reactions""" return self._pipes @property def tank_reactions(self) -> Dict[str, Reaction]: - """The dictionary view onto tank reactions""" + """Dictionary view onto tank reactions""" return self._tanks def add_variable(self, variable: MsxVariable) -> None: @@ -90,14 +92,14 @@ def add_variable(self, variable: MsxVariable) -> None: Parameters ---------- variable - The variable to add. + Variable to add. Raises ------ TypeError - if `variable` is not an MsxVariable + If `variable` is not an MsxVariable KeyExistsError - if `variable` has a name that is already used in the registry + If `variable` has a name that is already used in the registry """ if not isinstance(variable, (Species, Constant, Parameter, Term, MathFunction, HydraulicVariable)): raise TypeError("Expected AVariable object") @@ -112,14 +114,14 @@ def add_reaction(self, reaction: Reaction) -> None: Parameters ---------- reaction : Reaction - a water quality reaction definition + Water quality reaction definition Raises ------ TypeError - if `reaction` is not a Reaction + If `reaction` is not a Reaction KeyError - if the `species_name` in the `reaction` does not exist in the model + If the `species_name` in the `reaction` does not exist in the model """ if not isinstance(reaction, Reaction): raise TypeError("Expected a Reaction object") @@ -129,17 +131,18 @@ def add_reaction(self, reaction: Reaction) -> None: def variables(self) -> Generator[tuple, None, None]: # FIXME: rename without "all_" for this - """A generator looping through all variables""" + """Generator looping through all variables""" for k, v in self._vars.items(): yield k, v.var_type.name.lower(), v def reactions(self) -> Generator[tuple, None, None]: - """A generator looping through all reactions""" + """Generator looping through all reactions""" for k2, v in self._rxns.items(): for k1, v1 in v.items(): yield k1, k2, v1 def to_dict(self) -> dict: + """Dictionary representation of the MsxModel.""" return dict( species=[v.to_dict() for v in self._species.values()], constants=[v.to_dict() for v in self._const.values()], @@ -151,38 +154,46 @@ def to_dict(self) -> dict: class MsxNetworkData(NetworkDataBase): - """A container for network-specific values associated with a multispecies water quality model.""" - def __init__(self, patterns: Dict[str, List[float]] = None, sources: Dict[str, Dict[str, dict]] = None, initial_quality: Dict[str, Union[dict,InitialQuality]] = None, parameter_values: Dict[str, Union[dict, ParameterValues]] = None) -> None: - """A container for network-specific values associated with a multispecies water quality model. + def __init__(self, patterns: Dict[str, List[float]] = None, + sources: Dict[str, Dict[str, dict]] = None, + initial_quality: Dict[str, Union[dict, InitialQuality]] = None, + parameter_values: Dict[str, Union[dict, ParameterValues]] = None) -> None: + """Network-specific values associated with a multi-species water + quality model - Data is copied from dictionaries passed in, so once created, the dictionaries passed are not connected - to this object. + Data is copied from dictionaries passed in, so once created, the + dictionaries passed are not connected to this object. Parameters ---------- patterns : dict, optional - patterns to use for sources + Patterns to use for sources sources : dict, optional - sources defined for the model + Sources defined for the model initial_quality : dict, optional - initial values for different species at different nodes, links, and the global value + Initial values for different species at different nodes, links, and + the global value parameter_values : dict, optional - parameter values for different pipes and tanks + Parameter values for different pipes and tanks Notes ----- ``patterns`` - A dictionary keyed by pattern name (str) with values being the multipliers (list of float) + Dictionary keyed by pattern name (str) with values being the + multipliers (list of float) ``sources`` - A dictionary keyed by species name (str) with values being dictionaries keyed by junction name (str) with values being the + Dictionary keyed by species name (str) with values being + dictionaries keyed by junction name (str) with values being the dictionary of settings for the source ``initial_quality`` - A dictionary keyed by species name (str) with values being either an :class:`~wntr.msx.elements.InitialQuality` object or - the appropriate dictionary representation thereof. + Dictionary keyed by species name (str) with values being either an + :class:`~wntr.msx.elements.InitialQuality` object or the + appropriate dictionary representation thereof. ``parameter_values`` - A dictionary keyed by parameter name (str) with values being either a :class:`~wntr.msx.elements.ParameterValues` object or - the appropriate dictionary representation thereof. + Dictionary keyed by parameter name (str) with values being either + a :class:`~wntr.msx.elements.ParameterValues` object or the + appropriate dictionary representation thereof. """ if sources is None: sources = dict() @@ -206,53 +217,55 @@ def __init__(self, patterns: Dict[str, List[float]] = None, sources: Dict[str, D @property def sources(self): - """A dictionary of sources, keyed by species name""" + """Dictionary of sources, keyed by species name""" return self._source_dict @property def initial_quality(self) -> Dict[str, InitialQuality]: - """A dictionary of initial quality values, keyed by species name""" + """Dictionary of initial quality values, keyed by species name""" return self._initial_quality_dict @property def patterns(self): - """A dictionary of patterns, specific for the water quality model, keyed by pattern name. + """Dictionary of patterns, specific for the water quality model, keyed + by pattern name. - .. note:: the WaterNetworkModel cannot see these patterns, so names can be reused, so be - careful. Likewise, this model cannot see the WaterNetworkModel patterns, so this could be - a source of some confusion. + .. note:: the WaterNetworkModel cannot see these patterns, so names can + be reused, so be careful. Likewise, this model cannot see the + WaterNetworkModel patterns, so this could be a source of some + confusion. """ return self._pattern_dict @property def parameter_values(self) -> Dict[str, ParameterValues]: - """A dictionary of parameter values, keyed by parameter name""" + """Dictionary of parameter values, keyed by parameter name""" return self._parameter_value_dict def add_pattern(self, name: str, multipliers: List[float]): - """Add a water-quality-model-specific pattern. + """Add a water quality model specific pattern. Arguments --------- name : str - The pattern name + Pattern name multipliers : list of float - The pattern multipliers + Pattern multipliers """ self._pattern_dict[name] = multipliers def init_new_species(self, species: Species): - """(Re)set the initial quality values for a species to a new container + """(Re)set the initial quality values for a species Arguments --------- species : Species - The species to (re)initialized. + Species to (re)initialized. Returns ------- InitialQuality - the new initial quality values container + New initial quality values """ self._initial_quality_dict[str(species)] = InitialQuality() if isinstance(species, Species): @@ -260,12 +273,12 @@ def init_new_species(self, species: Species): return self._initial_quality_dict[str(species)] def remove_species(self, species: Union[Species, str]): - """Remove a species from the network specific model. + """Remove a species from the network specific model Arguments --------- species : Species or str - a species to be removed from the network data + Species to be removed from the network data """ if isinstance(species, Species): species._vals = None @@ -275,17 +288,17 @@ def remove_species(self, species: Union[Species, str]): pass def init_new_parameter(self, param: Parameter): - """(Re)initialize parameter values for a parameter. + """(Re)initialize parameter values for a parameter Arguments --------- param : Parameter - a parameter to be (re)initialized with network data + Parameter to be (re)initialized with network data Returns ------- ParameterValues - the new network data for the specific parameter + New network data for the specific parameter """ self._parameter_value_dict[str(param)] = ParameterValues() if isinstance(param, Parameter): @@ -293,14 +306,14 @@ def init_new_parameter(self, param: Parameter): return self._parameter_value_dict[str(param)] def remove_parameter(self, param: Union[Parameter, str]): - """Remove values associated with a specific parameter. + """Remove values associated with a specific parameter Ignores non-parameters. Arguments --------- param : Parameter or str - the parameter or parameter name to be removed from the network data + Parameter or parameter name to be removed from the network data """ if isinstance(param, Parameter): param._vals = None @@ -321,15 +334,15 @@ def to_dict(self) -> dict: class MsxModel(QualityModelBase): - """A multispecies water quality model for use with WNTR EPANET-MSX simulator.""" + """Multi-species water quality model""" def __init__(self, msx_file_name=None) -> None: - """A full, multi-species water quality model. + """Multi-species water quality model Arguments --------- msx_file_name : str, optional - an MSX file to read in, by default None + MSX file to to load into the MsxModel object, by default None """ super().__init__(msx_file_name) self._references: List[Union[str, Dict[str, str]]] = list() @@ -363,47 +376,48 @@ def __repr__(self) -> str: @property def references(self) -> List[Union[str, Dict[str, str]]]: - """A list of strings or mappings that provide references for this model. + """List of strings or mappings that provide references for this model .. note:: - This property is a list, and should be modified using append/insert/remove. - Members of the list should be json seriealizable (i.e., strings or dicts of strings). + This property is a list, and should be modified using + append/insert/remove. Members of the list should be json + serializable (i.e., strings or dicts of strings). """ return self._references @property def reaction_system(self) -> MsxReactionSystem: - """The reaction variables defined for this model.""" + """Reaction variables defined for this model""" return self._rxn_system @property def network_data(self) -> MsxNetworkData: - """The network-specific values added to this model.""" + """Network-specific values added to this model""" return self._net_data @property def options(self) -> MsxSolverOptions: - """The MSX model options""" + """MSX model options""" return self._options @property def species_name_list(self) -> List[str]: - """all defined species names""" + """Get a list of species names""" return list(self.reaction_system.species.keys()) @property def constant_name_list(self) -> List[str]: - """all defined coefficient names""" + """Get a list of coefficient names""" return list(self.reaction_system.constants.keys()) @property def parameter_name_list(self) -> List[str]: - """all defined coefficient names""" + """Get a list of coefficient names""" return list(self.reaction_system.parameters.keys()) @property def term_name_list(self) -> List[str]: - """all defined function (MSX 'terms') names""" + """Get a list of function (MSX 'terms') names""" return list(self.reaction_system.terms.keys()) @options.setter @@ -411,7 +425,7 @@ def options(self, value: Union[dict, MsxSolverOptions]): if isinstance(value, dict): self._options = MsxSolverOptions.factory(value) elif not isinstance(value, MsxSolverOptions): - raise TypeError("Expected a MultispeciesOptions object, got {}".format(type(value))) + raise TypeError("Expected a MsxSolverOptions object, got {}".format(type(value))) else: self._options = value @@ -430,32 +444,32 @@ def add_species( Arguments --------- name : str - the species name + Species name species_type : SpeciesType - the type of species, either BULK or WALL + Type of species, either BULK or WALL units : str - the mass units for this species + Mass units for this species atol : float, optional unless rtol is not None - the absolute solver tolerance for this species, by default None + Absolute solver tolerance for this species, by default None rtol : float, optional unless atol is not None - the relative solver tolerance for this species, by default None + Relative solver tolerance for this species, by default None note : NoteType, optional keyword - supplementary information regarding this variable, by default None + Supplementary information regarding this variable, by default None (see also :class:`~wntr.epanet.util.ENcomment`) diffusivity : float, optional - diffusivity of this species in water + Diffusivity of this species in water Raises ------ KeyExistsError - if a variable with this name already exists + If a variable with this name already exists ValueError - if `atol` or `rtol` ≤ 0 + If `atol` or `rtol` ≤ 0 Returns ------- Species - the new species + New species """ if name in self._rxn_system: raise KeyExistsError("Variable named {} already exists in model as type {}".format(name, self._rxn_system._vars.get_groupname(name))) @@ -476,19 +490,19 @@ def add_species( return new def remove_species(self, variable_or_name): - """Remove a species from the model. + """Remove a species from the model Removes from both the reaction_system and the network_data. Parameters ---------- variable_or_name : Species or str - the species (or name of the species) to be removed + Species (or name of the species) to be removed Raises ------ KeyError - if `variable_or_name` is not a species in the model + If `variable_or_name` is not a species in the model """ name = str(variable_or_name) if name not in self.reaction_system.species: @@ -498,28 +512,28 @@ def remove_species(self, variable_or_name): self.reaction_system.__delitem__(name) def add_constant(self, name: str, value: float, units: str = None, note: NoteType = None) -> Constant: - """Add a constant coefficient to the model. + """Add a constant coefficient to the model Arguments --------- name : str - the name of the coefficient + Name of the coefficient value : float - the constant value of the coefficient + Constant value of the coefficient units : str, optional - the units for this coefficient, by default None + Units for this coefficient, by default None note : NoteType, optional - supplementary information regarding this variable, by default None + Supplementary information regarding this variable, by default None Raises ------ KeyExistsError - a variable with this name already exists + Variable with this name already exists Returns ------- Constant - the new constant coefficient + New constant coefficient """ if name in self._rxn_system: raise KeyExistsError("Variable named {} already exists in model as type {}".format(name, self._rxn_system._vars.get_groupname(name))) @@ -528,17 +542,17 @@ def add_constant(self, name: str, value: float, units: str = None, note: NoteTyp return new def remove_constant(self, variable_or_name): - """Remove a constant coefficient from the model. + """Remove a constant coefficient from the model Parameters ---------- variable_or_name : Constant or str - the constant (or name of the constant) to be removed + Constant (or name of the constant) to be removed Raises ------ KeyError - if `variable_or_name` is not a constant coefficient in the model + If `variable_or_name` is not a constant coefficient in the model """ name = str(variable_or_name) if name not in self.reaction_system.constants: @@ -547,16 +561,17 @@ def remove_constant(self, variable_or_name): self.reaction_system.__delitem__(name) def add_parameter(self, name: str, global_value: float, units: str = None, note: NoteType = None) -> Parameter: - """Add a parameterized coefficient to the model. + """Add a parameterized coefficient to the model Arguments --------- name : str - the name of the parameter + Name of the parameter global_value : float - the global value of the coefficient (can be overridden for specific pipes/tanks) + Global value of the coefficient (can be overridden for specific + pipes/tanks) units : str, optional - the units for the coefficient, by default None + Units for the coefficient, by default None note : NoteType, optional keyword Supplementary information regarding this variable, by default None (see also :class:`~wntr.epanet.util.ENcomment`). @@ -564,12 +579,12 @@ def add_parameter(self, name: str, global_value: float, units: str = None, note: Raises ------ KeyExistsError - if a variable with this name already exists + If a variable with this name already exists Returns ------- Parameter - the new parameterized coefficient + New parameterized coefficient """ if name in self._rxn_system: raise KeyExistsError("Variable named {} already exists in model as type {}".format(name, self._rxn_system._vars.get_groupname(name))) @@ -579,17 +594,17 @@ def add_parameter(self, name: str, global_value: float, units: str = None, note: return new def remove_parameter(self, variable_or_name): - """Remove a parameterized coefficient from the model. + """Remove a parameterized coefficient from the model Parameters ---------- variable_or_name : Parameter or str - the parameter (or name of the parameter) to be removed + Parameter (or name of the parameter) to be removed Raises ------ KeyError - if `variable_or_name` is not a parameter in the model + If `variable_or_name` is not a parameter in the model """ name = str(variable_or_name) if name not in self.reaction_system.parameters: @@ -599,14 +614,14 @@ def remove_parameter(self, variable_or_name): self.reaction_system.__delitem__(name) def add_term(self, name: str, expression: str, note: NoteType = None) -> Term: - """Add a named expression (term) to the model. + """Add a named expression (term) to the model Parameters ---------- name : str - the name of the functional term to be added + Name of the functional term to be added expression : str - the expression that the term defines + Expression that the term defines note : NoteType, optional keyword Supplementary information regarding this variable, by default None (see also :class:`~wntr.epanet.util.ENcomment`) @@ -619,7 +634,7 @@ def add_term(self, name: str, expression: str, note: NoteType = None) -> Term: Returns ------- Term - the new term + New term """ if name in self._rxn_system: raise KeyError("Variable named {} already exists in model as type {}".format(name, self._rxn_system._vars.get_groupname(name))) @@ -628,17 +643,17 @@ def add_term(self, name: str, expression: str, note: NoteType = None) -> Term: return new def remove_term(self, variable_or_name): - """Remove a named expression (term) from the model. + """Remove a named expression (term) from the model Parameters ---------- variable_or_name : Term or str - the term (or name of the term) to be deleted + Term (or name of the term) to be deleted Raises ------ KeyError - if `variable_or_name` is not a term in the model + If `variable_or_name` is not a term in the model """ name = str(variable_or_name) # FIXME: validate deletion @@ -647,25 +662,26 @@ def remove_term(self, variable_or_name): self.reaction_system.__delitem__(name) def add_reaction(self, species_name: Union[Species, str], reaction_type: ReactionType, expression_type: ExpressionType, expression: str, note: NoteType = None) -> ReactionBase: - """Add a reaction to a species in the model. + """Add a reaction to a species in the model - Note that all species need to have both a pipe and tank reaction defined - unless all species are bulk species and - the tank reactions are identical to the pipe reactions. However, it is not - recommended that users take this approach. + Note that all species need to have both a pipe and tank reaction + defined unless all species are bulk species and the tank reactions are + identical to the pipe reactions. However, it is not recommended that + users take this approach. Once added, access the reactions from the species' object. Arguments --------- species_name : Species or str - the species (or name of species) the reaction is being defined for + Species (or name of species) the reaction is being defined for reaction_type: ReactionType - where this reaction takes place, from {PIPE, TANK} + Reaction type (location), from {PIPE, TANK} expression_type : ExpressionType - the type (LHS) of the equation the expression belongs to, from {RATE, EQUIL, FORMULA} + Expression type (left-hand-side) of the equation, from {RATE, + EQUIL, FORMULA} expression : str - the expression defining the reaction + Expression defining the reaction note : NoteType, optional keyword Supplementary information regarding this reaction, by default None (see also :class:`~wntr.epanet.util.ENcomment`) @@ -673,12 +689,12 @@ def add_reaction(self, species_name: Union[Species, str], reaction_type: Reactio Raises ------ TypeError - if a variable that is not species is passed + If a variable that is not species is passed Returns ------- - MultispeciesReaction - the new reaction object + MsxReactionSystem + New reaction object """ species_name = str(species_name) species = self.reaction_system.species[species_name] @@ -697,20 +713,21 @@ def add_reaction(self, species_name: Union[Species, str], reaction_type: Reactio return new def remove_reaction(self, species_name: str, reaction_type: ReactionType) -> None: - """Remove a reaction at a specified location from a species. + """Remove a reaction at a specified location from a species Parameters ---------- species : Species or str - the species (or name of the species) of the reaction to remove + Species (or name of the species) of the reaction to remove reaction_type : ReactionType - the reaction type (location) of the reaction to remove + Reaction type (location) of the reaction to remove """ reaction_type = ReactionType.get(reaction_type, allow_none=False) species_name = str(species_name) del self.reaction_system.reactions[reaction_type.name.lower()][species_name] def to_dict(self) -> dict: + """Dictionary representation of the MsxModel""" from wntr import __version__ return { @@ -726,12 +743,12 @@ def to_dict(self) -> dict: @classmethod def from_dict(cls, data) -> "MsxModel": - """Create a new multispecies reaction model from a dictionary. + """Create a new multi-species water quality model from a dictionary Parameters ---------- data : dict - The model data + Model data """ from wntr import __version__ diff --git a/wntr/msx/options.py b/wntr/msx/options.py index e99645352..70bed47f2 100644 --- a/wntr/msx/options.py +++ b/wntr/msx/options.py @@ -1,5 +1,7 @@ # coding: utf-8 -"""Options for multispecies reaction models. +""" +The wntr.msx.options module includes options for multi-species water quality +models """ import logging @@ -12,7 +14,7 @@ class MsxReportOptions(_OptionsBase): """ - Options related to EPANET-MSX report outputs. + Report options """ def __init__( @@ -25,44 +27,39 @@ def __init__( links: Union[Literal["ALL"], List[str]] = None, ): """ - Options related to EPANET-MSX report outputs. + Report options Parameters ---------- report_filename : str - Provides the filename to use for outputting an EPANET report file, - by default this will be the prefix plus ".rpt". - + Filename for the EPANET-MSX report file, by default this will be + the prefix plus ".rpt". species : dict[str, bool] Output species concentrations - species_precision : dict[str, float] Output species concentrations with the specified precision - nodes : None, "ALL", or list - Output node information in report file. If a list of node names is provided, - EPANET only provides report information for those nodes. - + Output node information. If a list of node names is provided, + EPANET-MSX only provides report information for those nodes. links : None, "ALL", or list - Output link information in report file. If a list of link names is provided, - EPANET only provides report information for those links. - + Output link information. If a list of link names is provided, + EPANET-MSX only provides report information for those links. pagesize : str - Page size for EPANET report output + Page size for EPANET-MSX report output """ self.pagesize = pagesize - """The pagesize for the report""" + """Pagesize for the report""" self.report_filename = report_filename - """The prefix of the report filename (will add .rpt)""" + """Prefix of the report filename (will add .rpt)""" self.species = species if species is not None else dict() """Turn individual species outputs on and off, by default no species are output""" self.species_precision = species_precision if species_precision is not None else dict() - """Set the output precision for the concentration of a specific species""" + """Output precision for the concentration of a specific species""" self.nodes = nodes - """A list of nodes to print output for, or 'ALL' for all nodes, by default None""" + """List of nodes to print output for, or 'ALL' for all nodes, by default None""" self.links = links - """A list of links to print output for, or 'ALL' for all links, by default None""" + """List of links to print output for, or 'ALL' for all links, by default None""" def __setattr__(self, name, value): if name not in ["pagesize", "report_filename", "species", "nodes", "links", "species_precision"]: @@ -71,9 +68,7 @@ def __setattr__(self, name, value): class MsxSolverOptions(_OptionsBase): - """ - Multispecies quality model options. - """ + """Solver options""" def __init__( self, @@ -91,56 +86,73 @@ def __init__( report: MsxReportOptions = None, ): """ - Multispecies quality model options. + Solver options Parameters ---------- timestep : int >= 1 Water quality timestep (seconds), by default 60 (one minute). area_units : str, optional - The units of area to use in surface concentration forms, by default ``M2``. Valid values are ``FT2``, ``M2``, or ``CM2``. + Units of area to use in surface concentration forms, by default + ``M2``. Valid values are ``FT2``, ``M2``, or ``CM2``. rate_units : str, optional - The time units to use in all rate reactions, by default ``MIN``. Valid values are ``SEC``, ``MIN``, ``HR``, or ``DAY``. + Time units to use in all rate reactions, by default ``MIN``. Valid + values are ``SEC``, ``MIN``, ``HR``, or ``DAY``. solver : str, optional - The solver to use, by default ``RK5``. Options are ``RK5`` (5th order Runge-Kutta method), ``ROS2`` (2nd order Rosenbrock method), or ``EUL`` (Euler method). + Solver to use, by default ``RK5``. Options are ``RK5`` (5th order + Runge-Kutta method), ``ROS2`` (2nd order Rosenbrock method), or + ``EUL`` (Euler method). coupling : str, optional - Use coupling method for solution, by default ``NONE``. Valid options are ``FULL`` or ``NONE``. + Use coupling method for solution, by default ``NONE``. Valid + options are ``FULL`` or ``NONE``. atol : float, optional - Absolute concentration tolerance, by default 0.01 (regardless of species concentration units). + Absolute concentration tolerance, by default 0.01 (regardless of + species concentration units). rtol : float, optional Relative concentration tolerance, by default 0.001 (±0.1%). compiler : str, optional - Whether to use a compiler, by default ``NONE``. Valid options are ``VC``, ``GC``, or ``NONE`` + Whether to use a compiler, by default ``NONE``. Valid options are + ``VC``, ``GC``, or ``NONE`` segments : int, optional - Maximum number of segments per pipe (MSX 2.0 or newer only), by default 5000. + Maximum number of segments per pipe (MSX 2.0 or newer only), by + default 5000. peclet : int, optional - Peclet threshold for applying dispersion (MSX 2.0 or newer only), by default 1000. - report : ReportOptions or dict + Peclet threshold for applying dispersion (MSX 2.0 or newer only), + by default 1000. + report : MsxReportOptions or dict Options on how to report out results. """ self.timestep: int = timestep - """The timestep, in seconds, by default 360""" + """Timestep, in seconds, by default 360""" self.area_units: str = area_units - """The units used to express pipe wall surface area where, by default FT2. Valid values are FT2, M2, and CM2.""" + """Units used to express pipe wall surface area where, by default FT2. + Valid values are FT2, M2, and CM2.""" self.rate_units: str = rate_units - """The units in which all reaction rate terms are expressed, by default HR. Valid values are HR, MIN, SEC, and DAY.""" + """Units in which all reaction rate terms are expressed, by default HR. + Valid values are HR, MIN, SEC, and DAY.""" self.solver: str = solver - """The solver to use, by default EUL. Valid values are EUL, RK5, and ROS2.""" + """Solver to use, by default EUL. Valid values are EUL, RK5, and + ROS2.""" self.coupling: str = coupling - """Whether coupling should occur during solving, by default NONE. Valid values are NONE and FULL.""" + """Whether coupling should occur during solving, by default NONE. Valid + values are NONE and FULL.""" self.rtol: float = rtol - """The relative tolerance used during solvers ROS2 and RK5, by default 0.001 for all species. Can be overridden on a per-species basis.""" + """Relative tolerance used during solvers ROS2 and RK5, by default + 0.001 for all species. Can be overridden on a per-species basis.""" self.atol: float = atol - """The absolute tolerance used by the solvers, by default 0.01 for all species regardless of concentration units. Can be overridden on a per-species basis.""" + """Absolute tolerance used by the solvers, by default 0.01 for all + species regardless of concentration units. Can be overridden on a + per-species basis.""" self.compiler: str = compiler - """A compier to use if the equations should be compiled by EPANET-MSX, by default NONE. Valid options are VC, GC and NONE.""" + """Compiler to use if the equations should be compiled by EPANET-MSX, + by default NONE. Valid options are VC, GC and NONE.""" self.segments: int = segments - """The number of segments per-pipe to use, by default 5000.""" + """Number of segments per-pipe to use, by default 5000.""" self.peclet: int = peclet - """The threshold for applying dispersion, by default 1000.""" + """Threshold for applying dispersion, by default 1000.""" self.report: MsxReportOptions = MsxReportOptions.factory(report) - """The reporting output options.""" + """Reporting output options.""" def __setattr__(self, name, value): if name == "report": @@ -163,7 +175,7 @@ def __setattr__(self, name, value): except ValueError: raise ValueError("%s must be a number", name) elif name not in ["area_units", "rate_units", "solver", "coupling", "compiler"]: - raise ValueError("%s is not a valid member of MultispeciesOptions") + raise ValueError("%s is not a valid member of MsxSolverOptions") self.__dict__[name] = value def to_dict(self): diff --git a/wntr/network/base.py b/wntr/network/base.py index 20ddc94eb..7dcf3ae8b 100644 --- a/wntr/network/base.py +++ b/wntr/network/base.py @@ -245,7 +245,8 @@ def tag(self, tag): @property def initial_quality(self): - """float or dict: The initial quality (concentration) at the node, or a dict of species-->quality for multispecies quality""" + """float or dict: Initial quality (concentration) at the node, or + a dict of species-->quality for multi-species quality""" if not self._initial_quality: return 0.0 return self._initial_quality @@ -257,7 +258,7 @@ def initial_quality(self, value): @property def coordinates(self): - """tuple: The node coordinates, (x,y)""" + """tuple: Node coordinates, (x,y)""" return self._coordinates @coordinates.setter def coordinates(self, coordinates): diff --git a/wntr/tests/test_msx_elements.py b/wntr/tests/test_msx_elements.py index 39cc09188..e65f796f3 100644 --- a/wntr/tests/test_msx_elements.py +++ b/wntr/tests/test_msx_elements.py @@ -157,7 +157,7 @@ def test_Reaction(self): self.assertEqual(formula1.expression, "-Ka + Kb * Cl + T0") self.assertEqual(formula1.expression_type, wntr.msx.ExpressionType.FORMULA) - def test_WaterQualityReactionsModel_creation_specific_everything(self): + def test_MsxModel_creation_specific_everything(self): rxn_model1 = wntr.msx.MsxModel() bulk1 = wntr.msx.Species("Cl", 'b', "mg") wall1 = wntr.msx.Species("ClOH", 'w', "mg", 0.01, 0.0001, note="Testing stuff") diff --git a/wntr/utils/disjoint_mapping.py b/wntr/utils/disjoint_mapping.py index 27dc32c34..fd2572bf8 100644 --- a/wntr/utils/disjoint_mapping.py +++ b/wntr/utils/disjoint_mapping.py @@ -1,7 +1,8 @@ # coding: utf-8 -"""A set of utility classes that is similar to the 'registry' objects in the wntr.network -class, but more general, and therefore usable for other extensions, such as multispecies -reaction modeling. +""" +A set of utility classes that is similar to the 'registry' objects in the wntr.network +class, but more general, and therefore usable for other extensions, such as multi-species +water quality models. """ from collections.abc import MutableMapping @@ -10,13 +11,11 @@ class WrongGroupError(KeyError): """The key exists but is in a different disjoint group""" - pass class KeyExistsError(KeyError): - """The name already exists in the reaction model""" - + """The name already exists in the model""" pass