diff --git a/MANIFEST.in b/MANIFEST.in index 7157c882b..0f6c9b053 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,5 +8,5 @@ include wntr/sim/aml/numpy.i include wntr/sim/network_isolation/network_isolation* include wntr/sim/network_isolation/numpy.i include wntr/tests/networks_for_testing/*.inp -include wntr/msx/_library_data/*.json -include wntr/msx/_library_data/*.msx +include wntr/library/msx/*.json +include wntr/library/msx/*.msx diff --git a/documentation/advancedsim.rst b/documentation/advancedsim.rst index cb295bdd3..2235aa640 100644 --- a/documentation/advancedsim.rst +++ b/documentation/advancedsim.rst @@ -252,7 +252,9 @@ The solution for :math:`u` and :math:`v` is then returned and printed to four si Building MSX models ------------------- -The following two examples illustrate how to build :class:`~wntr.msx.model.MsxModel` objects in WNTR +The following two examples illustrate how to build :class:`~wntr.msx.model.MsxModel` objects in WNTR. +There is also a jupyter notebook in the examples/demos source directory called multisource-cl-decay.ipynb +that demonstrates this process with Net3. .. _msx_example1_lead: @@ -260,7 +262,7 @@ Plumbosolvency of lead ^^^^^^^^^^^^^^^^^^^^^^ The following example builds the plumbosolvency of lead model -described in [BWMS20]_. The model represents plumbosolvency +described in :cite:p:`bwms20`. The model represents plumbosolvency in lead pipes within a dwelling. The MSX model is built without a specific water network model in mind. @@ -332,7 +334,7 @@ Type Name Value Units --------------- --------------- --------------- --------------------------------- ------------------------ 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? +parameter :math:`F` 0 `flag` is pipe made of lead? =============== =============== =============== ================================= ======================== These are added to the MsxModel using the using the @@ -344,7 +346,18 @@ methods. >>> 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") + >>> msx.add_parameter("F", global_value=0, note="determines which pipes are made of lead") + +If the value of one of these needs to be modified, then it can be accessed and modified as an object +in the same manner as other WNTR objects. + +.. doctest:: + + >>> M = msx.reaction_system.constants['M'] + >>> M.value = 0.118 + >>> M + Constant(name='M', value=0.118, units='ug * m^(-2) * s^(-1)', note='Desorption rate (ug/m^2/s)') + Note that all models must include both pipe and tank reactions. Since the model only has reactions within @@ -374,16 +387,23 @@ method. .. doctest:: >>> msx.add_reaction("PB2", "pipe", "RATE", expression="F * Av * M * (E - PB2) / E") + + +If the species is saved as an object, as was done above, then it can be passed instead of the species name. + +.. doctest:: + >>> msx.add_reaction(PB2, "tank", "rate", expression="0") + 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]_. +adsorption/desorption, as given in section 3 of the EPANET-MSX user manual :cite:p:`shang2023`. 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]_. +in :cite:p:`shang2023`. This is a simplified model, taken from :cite:p:`gscl94`. .. math:: diff --git a/documentation/references.bib b/documentation/references.bib index e07d3450c..13f609914 100644 --- a/documentation/references.bib +++ b/documentation/references.bib @@ -39,6 +39,18 @@ @misc{bieni19 year = "2019" } +@article{bwms20, + author="Burkhardt, J. B. and Woo, J. and Mason, J. and Shang, F. and Triantafyllidou, S. and Schock, M.R., and Lytle, D., and Murray, R." , + year = 2020, + title="Framework for Modeling Lead in Premise Plumbing Systems Using EPANET", + journal="Journal of Water Resources Planning and Management", + volume=146, + number=12, + doi="10.1061/(asce)wr.1943-5452.0001304", + eprint="33627937", + eprintclass="PMID" +} + @book{crlo02, author = "Crowl, D.A. and Louvar, J.F.", address = "Upper Saddle River, NJ", @@ -79,6 +91,15 @@ @misc{gacl18 year = "2018" } +@article{gscl94, + author = "Gu, B. and Schmitt, J. and Chen, Z. and Liang, L. and McCarthy, J.F.", + title = "Adsorption and desorption of natural organic matter on iron oxide: mechanisms and models", + year = "1994", + journal = "Environmental Science and Technology", + volume = "28", + pages = "38--46" +} + @inproceedings{hass08, author = "Hagberg, Aric A. and Schult, Daniel A. and Swart, Pieter J.", booktitle = "Proceedings of the 7th {Python} in Science Conference ({SciPy2008}), August 19-24, Pasadena, CA, USA", diff --git a/documentation/resultsobject.rst b/documentation/resultsobject.rst index 7ba089176..20c8e0c45 100644 --- a/documentation/resultsobject.rst +++ b/documentation/resultsobject.rst @@ -206,3 +206,19 @@ For example, DataFrames can be saved to Excel files using: .. note:: The Pandas method ``to_excel`` requires the Python package **openpyxl** :cite:p:`gacl18`, which is an optional dependency of WNTR. + + +Water quality results +--------------------- +Water quality metrics are stored under the 'quality' key of the node and link results +if the EpanetSimulator is used. The units of the quality results depend on the quality +parameter that is used (see :ref:`_water_quality_simulation`) and can be the age, +concentration, or the fraction of water that belongs to a tracer. If the parameter +is set to 'NONE', then the quality results will be zero. + +The quality of a link is equal to the average across the length of the link. The quality +at a node is the instantaneous value. + +When using the EPANET-MSX water quality model, each species is given its own key in the +node and link results objects, and the 'quality' results still references the EPANET +water quality results. diff --git a/documentation/waterquality.rst b/documentation/waterquality.rst index 8b8c6ee37..697c83f60 100644 --- a/documentation/waterquality.rst +++ b/documentation/waterquality.rst @@ -18,10 +18,10 @@ Water quality simulation Water quality simulations can only be run using the EpanetSimulator. This includes the ability to run -EPANET 2.00.12 Programmer's Toolkit [Ross00]_ or -EPANET 2.2.0 Programmer's Toolkit [RWTS20]_ for single species, water age, and tracer analysis. +EPANET 2.00.12 Programmer's Toolkit :cite:p:`ross00` or +EPANET 2.2.0 Programmer's Toolkit :cite:p:`rwts20` for single species, water age, and tracer analysis. -WNTR also includes the ability to run EPANET-MSX 2.0 [SRU23]_, see :ref:`msx_water_quality` for more information. +WNTR also includes the ability to run EPANET-MSX 2.0 :cite:p:`shang2023`, see :ref:`msx_water_quality` for more information. After defining water quality options and sources (described in the :ref:`wq_options` and :ref:`sources` sections below), a hydraulic and water quality simulation using the EpanetSimulator is run using the following code: diff --git a/documentation/waterquality_msx.rst b/documentation/waterquality_msx.rst index 3ffcf6145..bbb4826a9 100644 --- a/documentation/waterquality_msx.rst +++ b/documentation/waterquality_msx.rst @@ -18,13 +18,13 @@ Multi-species water quality simulation ======================================= -The EpanetSimulator can use EPANET-MSX 2.0 [SRU23]_ to run +The EpanetSimulator can use EPANET-MSX 2.0 :cite:p:`shang2023` to run multi-species water quality simulations. Additional multi-species simulation options are discussed in :ref:`advanced_simulation`. A multi-species analysis is run if a :class:`~wntr.msx.model.MsxModel` is added to the :class:`~wntr.network.model.WaterNetworkModel`, as shown below. -In this example, the MsxModel is created from a MSX file (see [SRU23]_ for more information on file format). +In this example, the MsxModel is created from a MSX file (see :cite:p:`shang2023` for more information on file format). .. doctest:: @@ -80,9 +80,9 @@ WNTR also contains a library of MSX models that are accessed through the :class:`~wntr.msx.library.ReactionLibrary`. This includes the following models: -* `Arsenic oxidation/adsorption `_ [SRU23]_ +* `Arsenic oxidation/adsorption `_ :cite:p:`shang2023` * `Batch chloramine decay `_ -* `Lead plumbosolvency `_ [BWMS20]_ +* `Lead plumbosolvency `_ :cite:p:`bwms20` * `Nicotine/chlorine reaction `_ * `Nicotine/chlorine reaction with reactive intermediate `_ diff --git a/examples/data/Net3_arsenic.msx b/examples/data/Net3_arsenic.msx index bb01238ae..51c37cafd 100644 --- a/examples/data/Net3_arsenic.msx +++ b/examples/data/Net3_arsenic.msx @@ -48,6 +48,7 @@ Arsenic Oxidation/Adsorption Example ;Initial conditions (= 0 if not specified here) NODE River AS3 10.0 NODE River NH2CL 2.5 + NODE Lake NH2CL 2.5 [REPORT] NODES All ;Report results for nodes C and D diff --git a/examples/demos/multisource-cl-decay.ipynb b/examples/demos/multisource-cl-decay.ipynb index 5e9e106da..da1115d55 100644 --- a/examples/demos/multisource-cl-decay.ipynb +++ b/examples/demos/multisource-cl-decay.ipynb @@ -86,7 +86,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Species(name='CL2', species_type=, units='MG', atol=None, rtol=None, note='Free Chlorine')\n" + "Species(name='CL2', species_type='BULK', units='MG', atol=None, rtol=None, note='Free Chlorine')\n" ] } ], @@ -124,7 +124,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Reaction(species_name='CL2', expression_type=, expression='-(k1*T1 + k2*(1-T1))*CL2')\n" + "Reaction(species_name='CL2', expression_type='RATE', expression='-(k1*T1 + k2*(1-T1))*CL2')\n" ] } ], @@ -330,12 +330,281 @@ "plt.ylabel('Concentraion [mg/L]')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Save the model\n", + "The model is now saved in two formats: the EPANET-MSX style format as Net3.msx and then as a JSON file, Net3-msx.json.\n", + "We also can save the model as in a library format; this strips the JSON file of any network-specific information so that it only contains the species, constants, and reaction dynamics." + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "from wntr.msx import io as msxio\n", + "msxio.write_msxfile(wn.msx, 'Net3.msx')\n", + "msxio.write_json(wn.msx, 'Net3-msx.json')\n", + "msxio.write_json(wn.msx, 'multisource-cl.json', as_library=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can look at the file that was written out, and we can read in the two JSON files to see how the library format has stripped out the network data." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "; WNTR-reactions MSX file generated 2024-05-20 07:00:10.969400\n", + "\n", + "[TITLE]\n", + " Multisource Chlorine Decay\n", + "\n", + "[OPTIONS]\n", + " AREA_UNITS FT2\n", + " RATE_UNITS DAY\n", + " SOLVER RK5\n", + " COUPLING NONE\n", + " TIMESTEP 300\n", + " ATOL 0.0001\n", + " RTOL 0.0001\n", + " COMPILER NONE\n", + " SEGMENTS 5000\n", + " PECLET 1000\n", + "\n", + "[SPECIES]\n", + " BULK T1 MG ; Source 1 Tracer\n", + " BULK CL2 MG ; Free Chlorine\n", + "\n", + "[COEFFICIENTS]\n", + " CONSTANT k1 16.900000000000002\n", + " CONSTANT k2 17.7 \n", + "\n", + "[TERMS]\n", + "\n", + "[PIPES]\n", + " RATE T1 0 \n", + " RATE CL2 -(k1*T1 + k2*(1-T1))*CL2 \n", + "\n", + "[TANKS]\n", + "\n", + "[DIFFUSIVITY]\n", + "\n", + "[PARAMETERS]\n", + "\n", + "[PATTERNS]\n", + "\n", + "[REPORT]\n", + "\n", + "[QUALITY]\n", + " NODE River T1 1.0\n", + " NODE River CL2 1.2\n", + " NODE Lake CL2 1.2\n", + "\n", + "[SOURCES]\n", + "\n", + "\n" + ] + } + ], + "source": [ + "with open('Net3.msx', 'r') as fin:\n", + " print(fin.read())" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "With network data:\n", + "{'initial_quality': {'CL2': {'global_value': 0.0,\n", + " 'link_values': {},\n", + " 'node_values': {'Lake': 1.2, 'River': 1.2}},\n", + " 'T1': {'global_value': 0.0,\n", + " 'link_values': {},\n", + " 'node_values': {'River': 1.0}}},\n", + " 'parameter_values': {},\n", + " 'patterns': {},\n", + " 'sources': {}}\n", + "As a library:\n", + "{'initial_quality': {}, 'parameter_values': {}, 'patterns': {}, 'sources': {}}\n" + ] + } + ], + "source": [ + "import json\n", + "with_net: dict = None\n", + "without_net: dict = None\n", + "\n", + "with open('Net3-msx.json', 'r') as fin:\n", + " with_net = json.load(fin)\n", + "with open('multisource-cl.json', 'r') as fin:\n", + " without_net = json.load(fin)\n", + "\n", + "print('With network data:')\n", + "pprint(with_net['network_data'])\n", + "\n", + "print('As a library:')\n", + "pprint(without_net['network_data'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using the MSX WNTR library\n", + "WNTR now includes a library functionality that allows a user to access certain objects by name.\n", + "The MSX integration includes adding a library of certain reaction models that are described in\n", + "the EPANET-MSX user manual. This section demonstrates how to use the model that was just saved\n", + "in the library." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['arsenic_chloramine',\n", + " 'batch_chloramine_decay',\n", + " 'lead_ppm',\n", + " 'msx.schema',\n", + " 'nicotine',\n", + " 'nicotine_ri',\n", + " 'ms-cl-tmp',\n", + " 'Multisource-Cl',\n", + " 'Net3-msx',\n", + " 'Net3',\n", + " 'Net3_multisource-cl',\n", + " 'temp.check',\n", + " 'temp']" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from wntr.library.msx import MsxLibrary\n", + "my_library = MsxLibrary(extra_paths=['.']) # load files from the current directory\n", + "my_library.model_name_list()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that this has pulled in a lot more files than might be expected. This is because it is getting the default models (the first five models) followed by all the models in the current directory ('.'). It grabs any file that has the extension .json or .msx, which means that it has pulled in \"temp\", since this demo has run the EpanetSimulator several times, Net3, Net3-msx, and multisource-cl, because they were just created.\n", + "\n", + "The models are accessed by name. To see how they are different, compare the initial quality for the \"Net3\" model (which came from the .msx file created above) and the \"multisource-cl\" model (created with as_library=True)." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'T1': InitialQuality(global_value=0.0, node_values=<1 entries>, link_values=<0 entries>),\n", + " 'CL2': InitialQuality(global_value=0.0, node_values=<2 entries>, link_values=<0 entries>)}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_library.get_model('Net3').network_data.initial_quality" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'T1': InitialQuality(global_value=0.0, node_values=<0 entries>, link_values=<0 entries>),\n", + " 'CL2': InitialQuality(global_value=0.0, node_values=<0 entries>, link_values=<0 entries>)}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_library.get_model('Multisource-Cl').network_data.initial_quality" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, examine a model that comes from the built-in data." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AS3 \t Species(name='AS3', species_type='BULK', units='UG', atol=None, rtol=None, note='Dissolved arsenite')\n", + "AS5 \t Species(name='AS5', species_type='BULK', units='UG', atol=None, rtol=None, note='Dissolved arsenate')\n", + "AStot \t Species(name='AStot', species_type='BULK', units='UG', atol=None, rtol=None, note='Total dissolved arsenic')\n", + "AS5s \t Species(name='AS5s', species_type='WALL', units='UG', atol=None, rtol=None, note='Adsorbed arsenate')\n", + "NH2CL \t Species(name='NH2CL', species_type='BULK', units='MG', atol=None, rtol=None, note='Monochloramine')\n", + "Ka \t Constant(name='Ka', value=10.0, units='1 / (MG * HR)', note='Arsenite oxidation rate coefficient')\n", + "Kb \t Constant(name='Kb', value=0.1, units='1 / HR', note='Monochloramine decay rate coefficient')\n", + "K1 \t Constant(name='K1', value=5.0, units='M^3 / (UG * HR)', note='Arsenate adsorption coefficient')\n", + "K2 \t Constant(name='K2', value=1.0, units='1 / HR', note='Arsenate desorption coefficient')\n", + "Smax \t Constant(name='Smax', value=50.0, units='UG / M^2', note='Arsenate adsorption limit')\n", + "Ks \t Term(name='Ks', expression='K1/K2', note='Equil. adsorption coeff.')\n", + "AS3 \t Reaction(species_name='AS3', expression_type='RATE', expression='-Ka*AS3*NH2CL', note='Arsenite oxidation')\n", + "AS5 \t Reaction(species_name='AS5', expression_type='RATE', expression='Ka*AS3*NH2CL - Av*(K1*(Smax-AS5s)*AS5 - K2*AS5s)', note='Arsenate production less adsorption')\n", + "NH2CL \t Reaction(species_name='NH2CL', expression_type='RATE', expression='-Kb*NH2CL', note='Monochloramine decay')\n", + "AS5s \t Reaction(species_name='AS5s', expression_type='EQUIL', expression='Ks*Smax*AS5/(1+Ks*AS5) - AS5s', note='Arsenate adsorption')\n", + "AStot \t Reaction(species_name='AStot', expression_type='FORMULA', expression='AS3 + AS5', note='Total arsenic')\n", + "AS3 \t Reaction(species_name='AS3', expression_type='RATE', expression='-Ka*AS3*NH2CL', note='Arsenite oxidation')\n", + "AS5 \t Reaction(species_name='AS5', expression_type='RATE', expression='Ka*AS3*NH2CL', note='Arsenate production')\n", + "NH2CL \t Reaction(species_name='NH2CL', expression_type='RATE', expression='-Kb*NH2CL', note='Monochloramine decay')\n", + "AStot \t Reaction(species_name='AStot', expression_type='FORMULA', expression='AS3 + AS5', note='Total arsenic')\n" + ] + } + ], + "source": [ + "arsenic = my_library.get_model('arsenic_chloramine')\n", + "for key,value in arsenic.reaction_system.variables(): print(key, '\\t', repr(value))\n", + "for key,value in arsenic.reaction_system.reactions(): print(key, '\\t', repr(value))" + ] } ], "metadata": { diff --git a/wntr/epanet/msx/io.py b/wntr/epanet/msx/io.py index 5ac252e09..47e35d5c6 100644 --- a/wntr/epanet/msx/io.py +++ b/wntr/epanet/msx/io.py @@ -847,7 +847,7 @@ def _write_report(self, fout): fout.write("\n") -def MsxBinFile(filename, wn: wntr.network.WaterNetworkModel, res = None): +def MsxBinFile(filename, wn, res = None): duration = int(wn.options.time.duration) if res is None: from wntr.sim.results import SimulationResults diff --git a/wntr/library/msx/__init__.py b/wntr/library/msx/__init__.py new file mode 100644 index 000000000..403cb8c23 --- /dev/null +++ b/wntr/library/msx/__init__.py @@ -0,0 +1 @@ +from ._msxlibrary import MsxLibrary diff --git a/wntr/msx/library.py b/wntr/library/msx/_msxlibrary.py similarity index 98% rename from wntr/msx/library.py rename to wntr/library/msx/_msxlibrary.py index 62ce4d1ea..117be0c82 100644 --- a/wntr/msx/library.py +++ b/wntr/library/msx/_msxlibrary.py @@ -5,7 +5,7 @@ .. rubric:: Environment Variable -.. envvar:: WNTR_RXN_LIBRARY_PATH +.. envvar:: WNTR_LIBRARY_PATH This environment variable, if set, will add additional folder(s) to the path to search for multi-species water quality model files, @@ -23,8 +23,8 @@ from pkg_resources import resource_filename -from .base import ExpressionType, ReactionType, SpeciesType -from .model import MsxModel +from wntr.msx.base import ExpressionType, ReactionType, SpeciesType +from wntr.msx.model import MsxModel try: import yaml @@ -45,7 +45,7 @@ logger = logging.getLogger(__name__) -class ReactionLibrary: +class MsxLibrary: """Library of multi-species water quality models This object can be accessed and treated like a dictionary, where keys are @@ -78,7 +78,7 @@ def __init__(self, extra_paths: List[str] = None, include_builtins=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 + :envvar:`WNTR_LIBRARY_PATH`, by default True load : bool or str, optional Load the files immediately on creation, by default True. If a string, then it will be passed as the `duplicates` argument @@ -99,12 +99,12 @@ def __init__(self, extra_paths: List[str] = None, include_builtins=True, self.__data = dict() if include_builtins: - default_path = os.path.abspath(resource_filename(__name__, "_library_data")) + default_path = os.path.abspath(resource_filename(__name__, '.')) if default_path not in self.__library_paths: self.__library_paths.append(default_path) if include_envvar_paths: - environ_path = os.environ.get("WNTR_RXN_LIBRARY_PATH", None) + environ_path = os.environ.get("WNTR_LIBRARY_PATH", None) if environ_path: lib_folders = environ_path.split(";") for folder in lib_folders: diff --git a/wntr/msx/_library_data/arsenic_chloramine.json b/wntr/library/msx/arsenic_chloramine.json similarity index 100% rename from wntr/msx/_library_data/arsenic_chloramine.json rename to wntr/library/msx/arsenic_chloramine.json diff --git a/wntr/msx/_library_data/batch_chloramine_decay.json b/wntr/library/msx/batch_chloramine_decay.json similarity index 100% rename from wntr/msx/_library_data/batch_chloramine_decay.json rename to wntr/library/msx/batch_chloramine_decay.json diff --git a/wntr/msx/_library_data/lead_ppm.json b/wntr/library/msx/lead_ppm.json similarity index 100% rename from wntr/msx/_library_data/lead_ppm.json rename to wntr/library/msx/lead_ppm.json diff --git a/wntr/msx/_library_data/nicotine.json b/wntr/library/msx/nicotine.json similarity index 100% rename from wntr/msx/_library_data/nicotine.json rename to wntr/library/msx/nicotine.json diff --git a/wntr/msx/_library_data/nicotine_ri.json b/wntr/library/msx/nicotine_ri.json similarity index 100% rename from wntr/msx/_library_data/nicotine_ri.json rename to wntr/library/msx/nicotine_ri.json diff --git a/wntr/msx/__init__.py b/wntr/msx/__init__.py index d563c9a97..3daaa7e4b 100644 --- a/wntr/msx/__init__.py +++ b/wntr/msx/__init__.py @@ -8,6 +8,5 @@ from .elements import Species, Constant, Parameter, Term, Reaction, HydraulicVariable, MathFunction from .model import MsxModel from .options import MsxSolverOptions -from .library import ReactionLibrary -from . import base, elements, library, model, options +from . import base, elements, model, options, io diff --git a/wntr/msx/_library_data/msx.schema.json b/wntr/msx/_library_data/msx.schema.json deleted file mode 100644 index 24feef0e2..000000000 --- a/wntr/msx/_library_data/msx.schema.json +++ /dev/null @@ -1,873 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "wntr-version": { - "type": "string" - }, - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": [ - "null", - "string" - ] - }, - "references": { - "type": "array", - "items": { - "type": "string" - } - }, - "reaction_system": { - "type": "object", - "properties": { - "species": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "species_type": { - "type": "string" - }, - "units": { - "type": "string" - }, - "atol": { - "type": "null" - }, - "rtol": { - "type": "null" - }, - "note": { - "type": "string" - } - }, - "required": [ - "atol", - "name", - "note", - "rtol", - "species_type", - "units" - ] - } - }, - "constants": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "number" - }, - "units": { - "type": "string" - }, - "note": { - "type": "string" - } - }, - "required": [ - "name", - "note", - "units", - "value" - ] - } - }, - "parameters": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "global_value": { - "type": "number" - }, - "note": { - "type": "string" - } - }, - "required": [ - "global_value", - "name" - ] - } - }, - "terms": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "expression": { - "type": "string" - }, - "note": { - "type": "string" - } - }, - "required": [ - "expression", - "name" - ] - } - }, - "pipe_reactions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "species_name": { - "type": "string" - }, - "expression_type": { - "type": "string" - }, - "expression": { - "type": "string" - }, - "note": { - "type": "string" - } - }, - "required": [ - "expression", - "expression_type", - "species_name" - ] - } - }, - "tank_reactions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "species_name": { - "type": "string" - }, - "expression_type": { - "type": "string" - }, - "expression": { - "type": "string" - }, - "note": { - "type": "string" - } - }, - "required": [ - "expression", - "expression_type", - "species_name" - ] - } - } - }, - "required": [ - "constants", - "parameters", - "pipe_reactions", - "species", - "tank_reactions", - "terms" - ] - }, - "network_data": { - "type": "object", - "properties": { - "initial_quality": { - "type": "object", - "properties": { - "AS3": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "AS5": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "AStot": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "AS5s": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "NH2CL": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "HOCL": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "NH3": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "NHCL2": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "I": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "OCL": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "NH4": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "ALK": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "H": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "OH": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "CO3": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "HCO3": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "H2CO3": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "chloramine": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "PB2": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "Nx": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - }, - "NX2": { - "type": "object", - "properties": { - "global_value": { - "type": "number" - }, - "node_values": { - "type": "object" - }, - "link_values": { - "type": "object" - } - }, - "required": [ - "global_value", - "link_values", - "node_values" - ] - } - } - }, - "parameter_values": { - "type": "object", - "properties": { - "k1": { - "type": "object", - "properties": { - "pipe_values": { - "type": "object" - }, - "tank_values": { - "type": "object" - } - }, - "required": [ - "pipe_values", - "tank_values" - ] - }, - "k2": { - "type": "object", - "properties": { - "pipe_values": { - "type": "object" - }, - "tank_values": { - "type": "object" - } - }, - "required": [ - "pipe_values", - "tank_values" - ] - }, - "k3": { - "type": "object", - "properties": { - "pipe_values": { - "type": "object" - }, - "tank_values": { - "type": "object" - } - }, - "required": [ - "pipe_values", - "tank_values" - ] - }, - "k4": { - "type": "object", - "properties": { - "pipe_values": { - "type": "object" - }, - "tank_values": { - "type": "object" - } - }, - "required": [ - "pipe_values", - "tank_values" - ] - }, - "k6": { - "type": "object", - "properties": { - "pipe_values": { - "type": "object" - }, - "tank_values": { - "type": "object" - } - }, - "required": [ - "pipe_values", - "tank_values" - ] - }, - "k7": { - "type": "object", - "properties": { - "pipe_values": { - "type": "object" - }, - "tank_values": { - "type": "object" - } - }, - "required": [ - "pipe_values", - "tank_values" - ] - }, - "k8": { - "type": "object", - "properties": { - "pipe_values": { - "type": "object" - }, - "tank_values": { - "type": "object" - } - }, - "required": [ - "pipe_values", - "tank_values" - ] - }, - "k9": { - "type": "object", - "properties": { - "pipe_values": { - "type": "object" - }, - "tank_values": { - "type": "object" - } - }, - "required": [ - "pipe_values", - "tank_values" - ] - }, - "k10": { - "type": "object", - "properties": { - "pipe_values": { - "type": "object" - }, - "tank_values": { - "type": "object" - } - }, - "required": [ - "pipe_values", - "tank_values" - ] - }, - "F": { - "type": "object", - "properties": { - "pipe_values": { - "type": "object" - }, - "tank_values": { - "type": "object" - } - }, - "required": [ - "pipe_values", - "tank_values" - ] - } - } - }, - "sources": { - "type": "object" - }, - "patterns": { - "type": "object" - } - }, - "required": [ - "initial_quality", - "parameter_values", - "patterns", - "sources" - ] - }, - "options": { - "type": "object", - "properties": { - "timestep": { - "type": "integer" - }, - "area_units": { - "type": "string" - }, - "rate_units": { - "type": "string" - }, - "solver": { - "type": "string" - }, - "coupling": { - "type": "string" - }, - "rtol": { - "type": "number" - }, - "atol": { - "type": "number" - }, - "compiler": { - "type": "string" - }, - "segments": { - "type": "integer" - }, - "peclet": { - "type": "integer" - }, - "report": { - "type": "object", - "properties": { - "pagesize": { - "type": "null" - }, - "report_filename": { - "type": "null" - }, - "species": { - "type": "object", - "properties": { - "PB2": { - "type": "string" - } - } - }, - "species_precision": { - "type": "object", - "properties": { - "PB2": { - "type": "integer" - } - } - }, - "nodes": { - "type": [ - "null", - "string" - ] - }, - "links": { - "type": [ - "null", - "string" - ] - } - }, - "required": [ - "links", - "nodes", - "pagesize", - "report_filename", - "species", - "species_precision" - ] - } - }, - "required": [ - "area_units", - "atol", - "compiler", - "coupling", - "peclet", - "rate_units", - "report", - "rtol", - "segments", - "solver", - "timestep" - ] - } - }, - "required": [ - "description", - "name", - "network_data", - "options", - "reaction_system", - "references", - "title", - "wntr-version" - ] -} diff --git a/wntr/msx/base.py b/wntr/msx/base.py index bee6b727d..74d30a6e8 100644 --- a/wntr/msx/base.py +++ b/wntr/msx/base.py @@ -209,6 +209,9 @@ class VariableType(Enum): C = CONST = CONSTANT R = RES = RESERVED + def __repr__(self): + return repr(self.name) + @add_get(abbrev=True) class SpeciesType(Enum): @@ -238,6 +241,9 @@ class SpeciesType(Enum): B = BULK W = WALL + def __repr__(self): + return repr(self.name) + @add_get(abbrev=True) class ReactionType(Enum): @@ -266,6 +272,9 @@ class ReactionType(Enum): P = PIPE T = TANK + def __repr__(self): + return repr(self.name) + @add_get(abbrev=True) class ExpressionType(Enum): @@ -303,6 +312,9 @@ class ExpressionType(Enum): R = RATE F = FORMULA + def __repr__(self): + return repr(self.name) + class ReactionBase(ABC): """Water quality reaction class @@ -374,7 +386,8 @@ def species_name(self) -> str: """Name of the species that has a reaction being defined.""" return self._species_name - @abstractproperty + @property + @abstractmethod def reaction_type(self) -> Enum: """Reaction type (reaction location).""" raise NotImplementedError @@ -450,15 +463,15 @@ def __init__(self, name: str, *, note: NoteType = None) -> None: """Optional note regarding the variable (see :class:`~wntr.epanet.util.NoteType`) """ - @abstractproperty + @property + @abstractmethod def var_type(self) -> Enum: """Type of reaction variable""" raise NotImplementedError - @abstractmethod def to_dict(self) -> Dict[str, Any]: """Represent the object as a dictionary""" - raise NotImplementedError + return dict(name=self.name) def __str__(self) -> str: """Return the name of the variable""" @@ -573,7 +586,8 @@ class VariableValuesBase(ABC): and methods documented here must be defined by a subclass. """ - @abstractproperty + @property + @abstractmethod def var_type(self) -> Enum: """Type of variable this object holds data for.""" raise NotImplementedError @@ -650,7 +664,8 @@ def __init__(self, filename=None): self._wn = None """Protected water network object""" - @abstractproperty + @property + @abstractmethod def options(self): """Model options structure @@ -659,7 +674,8 @@ def options(self): """ raise NotImplementedError - @abstractproperty + @property + @abstractmethod def reaction_system(self) -> ReactionSystemBase: """Reaction variables defined for this model @@ -667,7 +683,8 @@ def reaction_system(self) -> ReactionSystemBase: """ raise NotImplementedError - @abstractproperty + @property + @abstractmethod def network_data(self) -> NetworkDataBase: """Network-specific values added to this model @@ -680,7 +697,8 @@ def to_dict(self) -> dict: """Represent the object as a dictionary""" raise NotImplementedError - @abstractclassmethod + @classmethod + @abstractmethod def from_dict(self, data: dict) -> "QualityModelBase": """Create a new model from a dictionary diff --git a/wntr/msx/elements.py b/wntr/msx/elements.py index 3499a59ef..0c923df9d 100644 --- a/wntr/msx/elements.py +++ b/wntr/msx/elements.py @@ -464,9 +464,9 @@ def var_type(self) -> VariableType: """Type of variable, :attr:`~wntr.msx.base.VariableType.RESERVED`.""" return VariableType.RESERVED - def to_dict(self) -> Dict[str, Any]: - """Dictionary representation of the object""" - return "{}({})".format(self.__class__.__name__, ", ".join(["name={}".format(repr(self.name)), "note={}".format(repr(self.note))])) + # 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): diff --git a/wntr/msx/io.py b/wntr/msx/io.py new file mode 100644 index 000000000..900f21e92 --- /dev/null +++ b/wntr/msx/io.py @@ -0,0 +1,123 @@ +""" +The wntr.msx.io module includes functions that convert the MSX reaction +model to other data formats, create an MSX model from a file, and write +the MSX model to a file. +""" +import logging +import json + + +logger = logging.getLogger(__name__) + +def to_dict(msx) -> dict: + """ + Convert a MsxModel into a dictionary + + Parameters + ---------- + msx : MsxModel + The MSX reaction model. + + Returns + ------- + dict + Dictionary representation of the MsxModel + """ + return msx.to_dict() + +def from_dict(d: dict): + """ + Create or append a MsxModel from a dictionary + + Parameters + ---------- + d : dict + Dictionary representation of the water network model. + + Returns + ------- + MsxModel + + """ + from wntr.msx.model import MsxModel + return MsxModel.from_dict(d) + +def write_json(msx, path_or_buf, as_library=False, **kw_json): + """ + Write the MSX model to a JSON file + + Parameters + ---------- + msx : MsxModel + The model to output. + path_or_buf : str or IO stream + Name of the file or file pointer. + as_library : bool, optional + Strip out network-specific elements if True, by default False. + kw_json : keyword arguments + Arguments to pass directly to :meth:`json.dump`. + """ + d = to_dict(msx) + if as_library: + d.get('network_data', {}).get('initial_quality',{}).clear() + d.get('network_data', {}).get('parameter_values',{}).clear() + d.get('network_data', {}).get('sources',{}).clear() + d.get('network_data', {}).get('patterns',{}).clear() + d.get('options', {}).get('report',{}).setdefault('nodes', None) + d.get('options', {}).get('report',{}).setdefault('links', None) + if isinstance(path_or_buf, str): + with open(path_or_buf, "w") as fout: + json.dump(d, fout, **kw_json) + else: + json.dump(d, path_or_buf, **kw_json) + +def read_json(path_or_buf, **kw_json): + """ + Create or append a WaterNetworkModel from a JSON file + + Parameters + ---------- + f : str + Name of the file or file pointer. + kw_json : keyword arguments + Keyword arguments to pass to `json.load`. + + Returns + ------- + MsxModel + + """ + if isinstance(path_or_buf, str): + with open(path_or_buf, "r") as fin: + d = json.load(fin, **kw_json) + else: + d = json.load(path_or_buf, **kw_json) + return from_dict(d) + +def write_msxfile(msx, filename): + """ + Write an EPANET-MSX input file (.msx) + + Parameters + ---------- + msx : MsxModel + The model to write + filename : str + The filename to use for output + """ + from wntr.epanet.msx.io import MsxFile + MsxFile.write(filename, msx) + +def read_msxfile(filename, append=None): + """ + Read in an EPANET-MSX input file (.msx) + + Parameters + ---------- + filename : str + The filename to read in. + append : MsxModel + An existing model to add data into, by default None. + """ + from wntr.epanet.msx.io import MsxFile + return MsxFile.read(filename, append) diff --git a/wntr/msx/model.py b/wntr/msx/model.py index be46e2201..bf3b9b3ed 100644 --- a/wntr/msx/model.py +++ b/wntr/msx/model.py @@ -133,13 +133,14 @@ def variables(self) -> Generator[tuple, None, None]: # FIXME: rename without "all_" for this """Generator looping through all variables""" for k, v in self._vars.items(): - yield k, v.var_type.name.lower(), v + if v.var_type.name.lower() not in ['reserved']: + yield k, v def reactions(self) -> Generator[tuple, None, None]: """Generator looping through all reactions""" for k2, v in self._rxns.items(): for k1, v1 in v.items(): - yield k1, k2, v1 + yield k1, v1 def to_dict(self) -> dict: """Dictionary representation of the MsxModel.""" @@ -724,14 +725,14 @@ def remove_reaction(self, species_name: str, reaction_type: ReactionType) -> Non """ 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] + del self.reaction_system._rxns[reaction_type.name.lower()][species_name] def to_dict(self) -> dict: """Dictionary representation of the MsxModel""" from wntr import __version__ return { - "wntr-version": "{}".format(__version__), + "version": "wntr-{}".format(__version__), "name": self.name, "title": self.title, "description": self.description if self.description is None or "\n" not in self.description else self.description.splitlines(), @@ -752,8 +753,8 @@ def from_dict(cls, data) -> "MsxModel": """ from wntr import __version__ - ver = data.get("wntr-version", None) - if ver != __version__: + ver = data.get("version", None) + if ver != 'wntr-{}'.format(__version__): logger.warn("Importing from a file created by a different version of wntr, compatibility not guaranteed") # warnings.warn("Importing from a file created by a different version of wntr, compatibility not guaranteed") new = cls()