diff --git a/.gitignore b/.gitignore index 49a569569..681f47bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ docker/ *.html *.pickle *.xlsx +*.out +*.rpt temp* diff --git a/documentation/attention.rst b/documentation/attention.rst index 5f18bb2eb..a536e0053 100644 --- a/documentation/attention.rst +++ b/documentation/attention.rst @@ -1,4 +1,8 @@ .. attention:: Version 1.2.0 is now available. See `release notes `_ - for more information. \ No newline at end of file + for more information. + +.. attention:: + WNTR now includes capabilities to model stormwater and wastewater systems. + See :ref:`stormwater` for more information. diff --git a/documentation/figures/Site-Post.jpg b/documentation/figures/Site-Post.jpg new file mode 100644 index 000000000..816384098 Binary files /dev/null and b/documentation/figures/Site-Post.jpg differ diff --git a/documentation/figures/plot_Site_Drainage_Model.png b/documentation/figures/plot_Site_Drainage_Model.png new file mode 100644 index 000000000..4e03596ad Binary files /dev/null and b/documentation/figures/plot_Site_Drainage_Model.png differ diff --git a/documentation/figures/plot_basic_stormwater_network.png b/documentation/figures/plot_basic_stormwater_network.png new file mode 100644 index 000000000..26f75819c Binary files /dev/null and b/documentation/figures/plot_basic_stormwater_network.png differ diff --git a/documentation/figures/timeseries_plot.png b/documentation/figures/timeseries_plot.png new file mode 100644 index 000000000..e27df8dc6 Binary files /dev/null and b/documentation/figures/timeseries_plot.png differ diff --git a/documentation/gis.rst b/documentation/gis.rst index e57d6886d..181d9f733 100644 --- a/documentation/gis.rst +++ b/documentation/gis.rst @@ -10,6 +10,8 @@ >>> import pandas as pd >>> pd.options.display.expand_frame_repr = False >>> pd.options.display.precision = 3 + >>> pd.options.display.max_colwidth = 50 + >>> pd.options.display.max_columns = 10 >>> try: ... import geopandas as gpd ... except ModuleNotFoundError: diff --git a/documentation/index.rst b/documentation/index.rst index d27271b7a..9bacc0bf3 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -30,8 +30,10 @@ Key Features in WNTR :hidden: userguide + installation wntr-api users + S-WNTR .. include:: citing_wntr.rst diff --git a/documentation/index_latex.rst b/documentation/index_latex.rst index a54756660..3371ba7b7 100644 --- a/documentation/index_latex.rst +++ b/documentation/index_latex.rst @@ -32,6 +32,7 @@ WNTR documentation graphics gis advancedsim + stormwater license users developers diff --git a/documentation/installation.rst b/documentation/installation.rst index d32013c43..4ae987d83 100644 --- a/documentation/installation.rst +++ b/documentation/installation.rst @@ -276,8 +276,10 @@ The following Python packages are optional: https://rtree.readthedocs.io/ * openpyxl :cite:p:`gacl18`: used to read/write to Microsoft® Excel® spreadsheets, https://openpyxl.readthedocs.io +* pyswmm :cite:p:`pyswmm`: used to run SWMM hydraulic simulations, https://github.com/pyswmm/pyswmm +* swmmio :cite:p:`swmmio`: used to access and modify SWMM INP files, https://github.com/pyswmm/swmmio -All of these packages **except geopandas** are included in the Anaconda Python distribution. +All of these packages **except geopandas, pyswmm, and swmmio** are included in the Anaconda Python distribution. To install optional dependencies, run:: pip install -r requirements.txt diff --git a/documentation/overview.rst b/documentation/overview.rst index 9103e2bc9..9d7410a30 100644 --- a/documentation/overview.rst +++ b/documentation/overview.rst @@ -2,17 +2,17 @@ \setcounter{secnumdepth}{1} \clearpage - -.. _wntr_overview: - -Overview -======== .. raw:: latex \pagenumbering{arabic} \setcounter{page}{1} - + +.. _wntr_overview: + +Overview +======== + Drinking water systems face multiple challenges, including aging infrastructure, water quality concerns, uncertainty in supply and demand, natural disasters, environmental emergencies, and cyber and terrorist attacks. @@ -49,7 +49,12 @@ and the associated analysis results. Information on EPANET can be found at https **WNTR is compatible with EPANET 2.00.12** :cite:p:`ross00` **and EPANET 2.2** :cite:p:`rwts20`. In addition, users should have experience using Python, including the installation of additional Python packages. General information on Python can be found at https://www.python.org/. - +.. note:: + In addition to resilience analysis for drinking water systems, WNTR now includes + functionality to simulate and analyze resilience of stormwater and wastewater systems. + This builds on **EPA's Storm Water Management Model (SWMM)** :cite:p:`ross22`. + See :ref:`stormwater` for more information. + WNTR can be installed through the United States Environmental Protection Agency (US EPA) GitHub organization at https://github.com/USEPA/WNTR. An integrated development environment (IDE), like Spyder, is recommended for users and developers. diff --git a/documentation/references.bib b/documentation/references.bib index 40f09982c..09a1f394f 100644 --- a/documentation/references.bib +++ b/documentation/references.bib @@ -166,6 +166,38 @@ @article{lwfz17 year = "2017" } +@article{niemi2019automated, + title={Automated urban rainfall--runoff model generation with detailed land cover and flow routing}, + author={Niemi, Tero J and Kokkonen, Teemu and Sillanp{\"a}{\"a}, Nora and Set{\"a}l{\"a}, Heikki and Koivusalo, Harri}, + journal={Journal of Hydrologic Engineering}, + volume={24}, + number={5}, + pages={04019011}, + year={2019}, + publisher={American Society of Civil Engineers}, + doi = "10.1061/(ASCE)HE.1943-5584.0001784", +} + +@misc{pcswmm, + author = "Computational Hydraulics International (CHI)", + howpublished = "Software", + title = "PCSWMM", + url = "https://www.pcswmm.com/", + urldate = "2024-01-16", + year = "2024" +} + +@article{pyswmm, + author = "McDonnell, Bryant E and Ratliff, Katherine and Tryby, Michael E. and Jia Xin Wu, Jennifer and Mullapudi, Abhiram", + doi = "10.21105/joss.02292", + journal = "Journal of Open Source Software", + number = "5", + pages = "2292", + title = "PySWMM: The Python Interface to Stormwater Management Model (SWMM)", + volume = "52", + year = "2020" +} + @book{mcki13, author = "McKinney, Wes", publisher = "O'Reilly Media, Inc.", @@ -211,6 +243,16 @@ @techreport{ross00 pagetotal = "200" } +@techreport{ross22, + author = "Rossman, L. A. and Simon, Michelle", + address = "Cincinnati, OH", + institution = "U.S. Environmental Protection Agency", + number = "EPA/600/R--22/030", + title = "Storm Water Management Model User's Manual Version 5.2", + year = "2022", + pagetotal = "423" +} + @misc{rtree, author = "Toblerity", howpublished = "Software", @@ -250,6 +292,15 @@ @misc{sphc16 optcomment = "modified from below preferred citation, per Plotly" } +@misc{swmmio, + author = "Erispaha, Adam and swmmio developers", + howpublished = "Software", + title = "swmmio", + url = "https://github.com/pyswmm/swmmio", + urldate = "2024-01-24", + year = "2024" +} + @article{todi00, author = "Todini, Ezio", doi = "10.1016/S1462-0758(00)00049-2", diff --git a/documentation/stormwater.rst b/documentation/stormwater.rst new file mode 100644 index 000000000..76cf34f04 --- /dev/null +++ b/documentation/stormwater.rst @@ -0,0 +1,702 @@ + +.. role:: red + +.. raw:: latex + + \clearpage + +.. doctest:: + :hide: + + >>> import matplotlib.pylab as plt + >>> import wntr.stormwater as swntr + >>> try: + ... swn = swntr.network.StormWaterNetworkModel('../examples/networks/Site_Drainage_Model.inp') + ... except: + ... swn = swntr.network.StormWaterNetworkModel('examples/networks/Site_Drainage_Model.inp') + >>> try: + ... swnP = swntr.network.StormWaterNetworkModel('../examples/networks/Pump_Control_Model.inp') + ... except: + ... swnP = swntr.network.StormWaterNetworkModel('examples/networks/Pump_Control_Model.inp') + >>> try: + ... backdrop_img = plt.imread('../figures/Site-Post.jpg') + ... except: + ... backdrop_img = plt.imread('documentation/figures/Site-Post.jpg') + +.. _stormwater: + +Stormwater/wastewater analysis using S-WNTR +=========================================== + + +.. note:: + Stormwater and wastewater resilience analysis capabilities in WNTR are new + and should be considered beta software. + Feedback can be posted at https://github.com/USEPA/WNTR/issues. + +Overview +-------- + +The following section describes capabilities in WNTR to +quantify the resilience of stormwater and wastewater systems. +This capability resides in the :class:`~wntr.stormwater` subpackage of WNTR and +is referred to as S-WNTR (pronounced "S-winter"). +**S-WNTR is intended to +leverage existing stormwater and wastewater software within a framework that +facilitates the use of WNTR capabilities for resilience analysis.** +For that reason, some familiarity with WNTR is recommended before using S-WNTR. +Drinking water functionality in WNTR is cross referenced in +the documentation below to provide additional background. + +S-WNTR uses EPA's `Storm Water Management Model (SWMM) `_ :cite:p:`ross22` +through the use of two open-source Python packages managed by the `pyswmm organization `_. +This includes: + +* **pyswmm** :cite:p:`pyswmm`: used to run SWMM hydraulic simulations, https://github.com/pyswmm/pyswmm +* **swmmio** :cite:p:`swmmio`: used to access and modify SWMM INP files, https://github.com/pyswmm/swmmio + +A subset of WNTR classes/methods/functions that were developed for drinking water +resilience analysis are imported into the stormwater subpackage to provide capabilities for +stormwater and wastewater resilience analysis. + +S-WNTR is intended to be used a standalone package. +In the examples below, the stormwater subpackage is imported as "swntr". + +.. doctest:: + + >>> import wntr.stormwater as swntr + +S-WNTR includes the following modules: + +.. _table-wntr-stormwater-modules: +.. table:: WNTR Stormwater Modules + + ================================================= ============================================================================================================================================================================================================================================================================= + Module Description + ================================================= ============================================================================================================================================================================================================================================================================= + :class:`~wntr.stormwater.gis` Contains methods to integrate geospatial data into the model and analysis. + :class:`~wntr.stormwater.graphics` Contains methods to generate network and fragility curve graphics. + :class:`~wntr.stormwater.io` Contains methods to read and write stormwater network models and translate models to other formats. + :class:`~wntr.stormwater.metrics` Contains methods to compute resilience, including topographic and hydraulic metrics. + :class:`~wntr.stormwater.network` Contains methods to define stormwater network models. + :class:`~wntr.stormwater.scenario` Contains methods to define fragility/survival curves. + :class:`~wntr.stormwater.sim` Contains methods to simulate hydraulics. + ================================================= ============================================================================================================================================================================================================================================================================= + +Installation +------------ + +Follow WNTR's :ref:`installation` instructions to install S-WNTR. + +S-WNTR requires the following dependencies (included in the `requirements file `_): + +* numpy +* scipy +* networkx +* pandas +* matplotlib +* setuptools +* geopandas +* pyswmm +* swmmio + +Units +----- + +While WNTR uses SI units for all drinking water models and analysis (see :ref:`units`), +**stormwater and wastewater models are not converted to SI units** when loaded into S-WNTR. +Therefore, any additional data used in analysis or computation should adhere the units of the model. + +.. dropdown:: **SWMM unit conventions** + + For reference, :numref:`table-swmm-units` includes SWMM unit conventions :cite:p:`ross22`. + + .. _table-swmm-units: + .. csv-table:: SWMM INP File Unit Conventions + :file: tables/swmm_units.csv + :widths: 30, 30, 30 + :header-rows: 1 + + +Stormwater network model +------------------------ + +A stormwater network model can be created directly from SWMM INP files. +The model is stored in a +:class:`~wntr.stormwater.network.StormWaterNetworkModel` object. + +.. doctest:: + + >>> swn = swntr.network.StormWaterNetworkModel('networks/Site_Drainage_Model.inp') # doctest: +SKIP + >>> swnP = swntr.network.StormWaterNetworkModel('networks/Pump_Control_Model.inp') # doctest: +SKIP + +.. note:: + The stormwater examples in this documentation all use **Site_Drainage_Model.inp** to build the StormWaterNetworkModel, named ``swn``. + Examples that involve pumps use **Pump_Control_Model.inp** to build the StormWaterNetworkModel, named ``swnP``. + Both model files are distributed with SWMM :cite:p:`ross22`. + +.. doctest:: + :hide: + + >>> fig, ax = plt.subplots() + >>> f = ax.imshow(backdrop_img[::-1], origin='lower', alpha=0.5) + >>> f = ax.set_xlim(0, 1423) + >>> f = ax.set_ylim(0, 1475) + >>> f = swntr.graphics.plot_network(swn, link_labels=True, ax=ax) + >>> plt.tight_layout() + >>> plt.savefig('plot_Site_Drainage_Model.png', dpi=300) + +.. _fig-swmm-network: +.. figure:: figures/plot_Site_Drainage_Model.png + :width: 640 + :alt: Network + + Stormwater network model from Site_Drainage_Model.inp. + +.. dropdown:: **Model attributes** + + The StormWaterNetworkModel includes the following DataFrames which store model attributes + (and correspond to sections of SWMM INP files). + + * ``swn.junctions`` + * ``swn.outfalls`` + * ``swn.storage`` + * ``swn.conduits`` + * ``swn.weirs`` + * ``swn.orifices`` + * ``swn.pumps`` + * ``swn.controls`` + * ``swn.subcatchments`` + * ``swn.raingages`` + * ``swn.options`` + * ``swn.report`` + + A full list of SWMM INP file sections that are supported by S-WNTR are stored in ``swn.section_names``. + + Model attributes are stored in Pandas DataFrames or Series. + For example, ``swn.junctions`` contains the following information: + + .. doctest:: + + >>> swn.junctions # doctest: +SKIP + InvertElev MaxDepth InitDepth SurchargeDepth PondedArea + Name + J1 4973.0 0 0 0 0 + J2 4969.0 0 0 0 0 + J3 4973.0 0 0 0 0 + J4 4971.0 0 0 0 0 + J5 4969.8 0 0 0 0 + J6 4969.0 0 0 0 0 + J7 4971.5 0 0 0 0 + J8 4966.5 0 0 0 0 + J9 4964.8 0 0 0 0 + J10 4963.8 0 0 0 0 + J11 4963.0 0 0 0 0 + + + The DataFrames and Series can be modified by the user and the + updated model is used in hydraulic simulation and analysis. + + The StormWaterNetworkModel object also includes methods to return a list of + junction names, conduits names, etc. + + .. doctest:: + + >>> swn.conduit_name_list + ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9', 'C10', 'C11'] + + Additional properties on the StormWaterNetworkModel include: + + * ``swn.conduit_cross_section`` + * ``swn.conduit_volume`` + + .. note:: + :class:`~wntr.stormwater.network.StormWaterNetworkModel` uses ``swmmio.Model`` to + read and write the SWMM INP file. + swimmio stores this information in Pandas and GeoPandas data formats. + +.. dropdown:: **Class methods** + + + In addition to modifying StormWaterNetworkModel DataFrames directly, the following class + methods are also available to help modify models. + + * :class:`~wntr.stormwater.network.StormWaterNetworkModel.add_composite_patterns`: + Combine multiple dry weather flows into a single composite base value and pattern + and update the model (updates ``swn.dwf`` and ``swn.patterns``) + * :class:`~wntr.stormwater.network.StormWaterNetworkModel.add_pump_outage_control`: + Add a pump outage control to the model (updates ``swn.controls``) + * :class:`~wntr.stormwater.network.StormWaterNetworkModel.add_datetime_indexed_timeseries`: + Add timeseries to the model from a datetime indexed DataFrame (updates ``swn.timeseries``) + * :class:`~wntr.stormwater.network.StormWaterNetworkModel.add_datetime_indexed_patterns`: + Add patterns to the model from a datetime indexed DataFrame (updates ``swn.patterns``) + * :class:`~wntr.stormwater.network.StormWaterNetworkModel.anonymize_coordinates`: + Anonymize model coordinates (using a spring layout) and remove vertices and polygons + to anonymize the model (updates ``swn.coordinates``, ``swn.vertices``, and ``swn.polygons``) + + The following class methods convert timeseries and patterns to datetime index DataFrames. + + * :class:`~wntr.stormwater.network.StormWaterNetworkModel.timeseries_to_datetime_format`: + Convert SWMM formatted timeseries DataFrame to a datetime indexed DataFrame + * :class:`~wntr.stormwater.network.StormWaterNetworkModel.patterns_to_datetime_format`: + Convert SWMM formatted patterns DataFrame to a datetime indexed DataFrame + +.. dropdown:: **Model I/O** + + S-WNTR includes the following functions to read/write files and transform + the StormWaterNetworkModel to other data formats. + + * :class:`~wntr.stormwater.io.read_inpfile`: Create a StormWaterNetworkModel object from a SWMM INP file + * :class:`~wntr.stormwater.io.write_inpfile`: Write a SWMM INP file from a StormWaterNetworkModel + * :class:`~wntr.stormwater.io.to_graph`: Convert a StormWaterNetworkModel object into a NetworkX graph object + * :class:`~wntr.stormwater.io.to_gis`: Convert a StormWaterNetworkModel object into a WaterNetworkGIS object + * :class:`~wntr.stormwater.io.write_geojson`: Write GeoJSON files from a StormWaterNetworkModel + + Additional methods are available for reading hydraulic simulation results files. + See :ref:`stormwater_simulation` for more information. + + * :class:`~wntr.stormwater.io.read_outfile`: Read the SWMM binary output file into Pandas DataFrames + * :class:`~wntr.stormwater.io.read_rptfile`: Read the SWMM summary report file into Pandas DataFrames + +.. _stormwater_simulation: + +Hydraulic simulation +-------------------- + +Hydraulic simulations are run using the +:class:`~wntr.stormwater.sim.SWMMSimulator` class. Simulation results are stored in a series of +Pandas DataFrames, as described in the following section. + +.. doctest:: + + >>> sim = swntr.sim.SWMMSimulator(swn) + >>> results = sim.run_sim() + +.. note:: + :class:`~wntr.stormwater.sim.SWMMSimulator` uses ``swmmio`` and ``pyswmm`` to run the full + duration of the SWMM simulation. pyswmm can be used directly for stepwise simulation. + +.. dropdown:: **Overland flow** + + Overland flow is an important aspect of resilience analysis for stormwater and wastewater systems. + While SWMM quantifies ponded volume and flooding loss, which account for flood impacts + at the discharge node, SWMM does not support 2D overland flow. + Open source and commercial software tools like GisToSWMM5 :cite:p:`niemi2019automated` + and PCSWMM :cite:p:`pcswmm` are able to generate 2D overland + meshes that can be stored in SWMM INP files and run using SWMM. + + To include overland flow in S-WNTR, + the user should first modify their INP file to include 2D overland conduits. + +.. dropdown:: **Simulation results** + + Simulation results are stored in a + :class:`~wntr.stormwater.sim.ResultsObject` organized in **node**, **link**, **subcatchment**, and **report** sections. + Each section contains a + DataFrames storing a timeseries of + simulation results or summary information. + See drinking water documentation on :ref:`simulation_results` for more information on the format of simulation results in WNTR. + + The S-WNTR :class:`~wntr.stormwater.sim.ResultsObject` includes the following sections: + + **results.node** includes the following timeseries for junctions, outfall, and storage nodes: + + * Invert depth + * Hydraulic head + * Ponded volume + * Lateral inflow + * Total inflow + * Flooding loss + * Pollution concentration + + **results.link** results include the following timeseries for conduits, weirs, orifices, and pumps: + + * Flow rate + * Flow depth + * Flow velocity + * Capacity + * Pollution concentration + + **results.subcatchment** results include the following timeseries: + + * Rainfall + * Snow depth + * Evaporation loss + * Infill loss + * Runoff rate + * Groundwater outflow rate + * Groundwater table elevation + * Soil moisture + * Pollution concentration + + **results.report** results include the following summary information: + + * Node summary + * Node depth summary + * Node inflow summary + * Node surcharge summary + * Node flooding summary + * Storage volume summary + + * Link summary + * Link flow summary + * Link pollutant load summary + * Conduit surcharge summary + * Pumping summary + + * Subcatchment summary + * Subcatchment runoff summary + * Subcatchment washoff summary + + The following example lists node attributes (Note that attribute names use all caps with an underscore between words) + + .. doctest:: + + >>> print(results.node.keys()) + dict_keys(['INVERT_DEPTH', 'HYDRAULIC_HEAD', 'PONDED_VOLUME', 'LATERAL_INFLOW', 'TOTAL_INFLOW', 'FLOODING_LOSSES', 'POLLUT_CONC_0']) + + The following example extracts the 'C0' conduit capacity from simulation results. + + .. doctest:: + + >>> conduit_capacity = results.link['CAPACITY'].loc[:, 'C1'] + + Simulation timeseries can also be extracted directly from a SWMM binary output file + using the function :class:`~wntr.stormwater.io.read_outfile` and + a report summary can be extracted directly from a SWMM report file + using the function :class:`~wntr.stormwater.io.read_rptfile`, as shown in the example below. + The ``file_prefix`` is used to name the output files. + The default file prefix is "temp". + + .. doctest:: + + >>> sim = swntr.sim.SWMMSimulator(swn) + >>> results = sim.run_sim(file_prefix='base') # creates base.bin and base.rpt + + >>> timeseries_results = swntr.io.read_outfile('base.out') + >>> summary_report = swntr.io.read_rptfile('base.rpt') + +Disaster scenarios +------------------ +Disaster scenarios can be defined based on a **specific threat** +or **threat agnostic** analysis. +For example, a specific landslide threat can be quantified using +GIS data to define landslide potential and fragility curves +to define the probability a conduit is damaged as a function of displacement. +Threat agnostic impacts can be quantified using criticality analysis, +where the impact of individual component failures is evaluated. + +.. dropdown:: **Modeling damage** + + To model disaster scenarios, attributes and controls in the + :class:`~wntr.stormwater.network.StormWaterNetworkModel` are modified to + reflect the damage state. + Several damage scenarios can be used to quantify resilience of the + stormwater/wastewater systems, this includes: + + * **Long term power outages**: Power outages impact pumps and lift stations. + The method :class:`~wntr.stormwater.network.StormWaterNetworkModel.add_pump_outage_control` + adds a control to the model which turns a pump off and on at user specified start and end times, respectively. + By default, the control priority is set to 4 (highest) to override other controls. + + .. doctest:: + + >>> # The following example uses swnP + >>> start_time = 4.5 # hours + >>> end_time = 12 # hours + >>> control = swnP.add_pump_outage_control('PUMP1', start_time, end_time) + + Note that controls can be viewed and modified using ``swn.controls`` which stores controls as + a Pandas DataFrame (one row per control). + + .. doctest:: + + >>> print(swnP.controls.loc['RULE PUMP1_outage', 'Control']) + IF SIMULATION TIME > 4.5 AND SIMULATION TIME < 12 THEN PUMP PUMP1 status = OFF ELSE PUMP PUMP1 status = ON PRIORITY 4 + + * **Conduit blockage or collapse**: Conduit blockage or collapse impacts the flowrate at the conduit. + The flowrate in a conduit can be constrained by modifying conduit properties as follows: + + * Decrease max flow. Note that a max flow value of 0 means that the flowrate is unconstrained (no upper bound). + * Increate roughness + * Decrease cross sectional area + + .. doctest:: + + >>> swn.conduits.loc['C1', "MaxFlow"] = 1e-6 + >>> swn.conduits.loc['C1', "Roughness"] = 0.999 + >>> swn.xsections.loc['C1', "Geom1"] = 0.00125 + + * **Extreme rainfall events**: Increased runoff impacts combined stormwater/wastewater systems. + The methods :class:`~wntr.stormwater.network.StormWaterNetworkModel.timeseries_to_datetime_format` can be used to + convert ``swn.timeseries`` into a datetime Pandas DataFrame. This format is easy to modify or import from other data sources. + The method :class:`~wntr.stormwater.network.StormWaterNetworkModel.add_datetime_indexed_timeseries` can then be used to + add timeseries formatted as datetime Pandas DataFrames to the model. This facilitates greater flexibility in the way timeseries are modified. + + The following example creates a new timeseries that is a combination of a 100 and 10 year rainfall event, + adds the new timeseries to the model, and then updates the data source of the raingage. + + .. doctest:: + + >>> swn.timeseries_name_list + ['2-yr', '10-yr', '100-yr'] + >>> ts = swn.timeseries_to_datetime_format() + >>> ts['New'] = ts['100-yr'] + ts['10-yr'].shift(periods=12, fill_value=0) + >>> ax = ts.plot() + + >>> timeseries = swn.add_datetime_indexed_timeseries(ts[['New']]) + >>> swn.timeseries_name_list + ['2-yr', '10-yr', '100-yr', 'New'] + >>> swn.raingages['DataSourceName'] = 'New' + + .. doctest:: + :hide: + + >>> plt.tight_layout() + >>> plt.savefig('timeseries_plot.png', dpi=300) + + .. _fig-fragility: + .. figure:: figures/timeseries_plot.png + :width: 640 + :alt: Timeseries plot + + Timeseries plot + +.. dropdown:: **Geospatial capabilities** + + Site and hazard specific GIS data can be used to define disaster scenarios + through the use of geospatial capabilities which allow the user to identify + components which intersect areas impacted by disruptive events. + Furthermore, GIS data can be used to characterize community impact based on the + location of critical facilities and vulnerable populations. + + Example GIS data that can help inform disaster scenarios includes: + + * Hazard maps + * Elevation data + * Census data + * Social vulnerability data + * Location of critical facilities and emergency services + + S-WNTR includes a :class:`~wntr.stormwater.gis` module which + facilitates the use of GIS data in geospatial operations, like + :class:`~wntr.stormwater.gis.snap` and :class:`~wntr.stormwater.gis.intersect`. + + The :class:`~wntr.stormwater.network.StormWaterNetworkModel` can be converted into a + :class:`~wntr.stormwater.gis.WaterNetworkGIS` object, as shown below. + + .. doctest:: + + >>> swn_gis = swn.to_gis() + + The user can also write geojson files, using the function :class:`~wntr.stormwater.io.write_geojson`. + + See drinking water documentation on :ref:`geospatial` for more information. + +.. dropdown:: **Fragility curves** + + Fragility curves are used within disaster scenarios to define the probability that a + component fails for a specific environmental change. For example, fragility curves can define the + probability of conduit collapse as a function of peak ground acceleration from an earthquake, or the + probability of damage to a pump station as a function of flood stage. + + :numref:`fig-fragility2` illustrates the fragility curve as a function of peak ground acceleration. + For example, if the peak ground acceleration is 0.3 at + a specific pipe, the probability of exceeding a Major damage state is 0.16 and the probability + of exceeding the Minor damage state is 0.80. + + .. _fig-fragility2: + .. figure:: figures/fragility_curve.png + :width: 640 + :alt: Fragility curve + + Example fragility curve. + + See drinking water documentation on :ref:`fragility_curves` for more information. + +.. dropdown:: **Criticality analysis** + + In cases where a specific disaster scenario is not included in the analysis, + a series of simulations can be used to perform N-k contingency analysis, + where N is the number of elements and k elements fail. + N-1 contingency analysis is commonly called criticality analysis :cite:p:`wawc06` + and uses a series of simulations to impart damage to one component at a time. + In stormwater and wastewater systems, the analysis can include the following: + + * Conduit criticality + * Pump criticality + + See drinking water documentation on :ref:`criticality` for more information. + +Resilience metrics +------------------ + +Resilience of stormwater and wastewater distribution systems depends on many factors, including the +design, maintenance, and operations of that system. For that reason, the WNTR stormwater module +includes several metrics to help quantify resilience. +Additional metrics could also be added at a later date. + +.. dropdown:: **Topographic metrics** + + Topographic metrics, based on graph theory, can be used to assess the connectivity + of stormwater and wastewater systems. Many metrics can be computed directly using NetworkX. + See drinking water documentation on :ref:`topographic_metrics` for more information. + + The StormWaterNetworkModel can be converted to a NetworkX graph as shown below: + + .. doctest:: + + >>> G = swn.to_graph() + + .. note:: + The :class:`~wntr.stormwater.network.StormWaterNetworkModel.to_graph` method uses ``swmmio.Model`` to + create the NetworkX graph object. The WNTR methods includes additional options to add node and link weight, and + modify the direction of links according to the sign of the link weight (generally flow direction). + + The graph can be used in NetworkX functions to compute network topographic metrics. + Example topographic metrics include: + + * Node degree + * Betweenness centrality + * Shortest path length + * Segmentation groups + * and many more + + The following example uses NetworkX to compute node degree. + + .. doctest:: + + >>> import networkx as nx + + >>> G = swn.to_graph() + >>> node_degree = nx.degree(G) + +.. dropdown:: **Travel paths** + + Since stormwater and wastewater systems typically operate in a unidirectional mode (flow in one direction), + it is possible to identify assets that are upstream and downstream from other assets. This calculation helps identify + travel time along flow paths and capacity limitations along those paths. + + Travel path metrics include: + + * Upstream edges or nodes from a starting node + * Downstream edges or nodes from a starting node + * Shortest path edges or nodes between two nodes + + The following example identifies upstream edges from a single node. + + .. doctest:: + + >>> average_flowrate = results.link['FLOW_RATE'].mean() + >>> G_flow = swn.to_graph(link_weight=average_flowrate, modify_direction=True) + >>> upstream_edges = swntr.metrics.upstream_edges(G_flow, 'J8') + +.. dropdown:: **Travel time** + + Travel time along an individual conduit is simply computed as the conduit length divided by the conduit velocity. + + .. doctest:: + + >>> length = length = swn.links['Length'] + >>> average_velocity = results.link['FLOW_VELOCITY'].mean() + >>> travel_time = swntr.metrics.conduit_travel_time(length, average_velocity) # in seconds + + If velocites are stable, the travel time along a path can be computed as the sum of travel times along that path. + + .. doctest:: + + >>> path_edges = swntr.metrics.shortest_path_edges(G_flow, 'J1', 'J9') + >>> path_travel_time = travel_time[path_edges].sum() # in seconds + +.. dropdown:: **Time to reach capacity** + + The time for an individual conduit to reach a specified capacity can be approximated by knowing the conduit available volume and average flowrate. + This assumes that the flowrate is blocked at the outflow of each conduit. + This rough approximation overly simplifies dynamics from blocked flow, but can be useful to identify areas with marginal reserve. + + .. doctest:: + + >>> flow_units = swnP.options.loc['FLOW_UNITS', 'Value'] + >>> volume = swn.conduit_volume + >>> average_capacity = results.link['CAPACITY'].mean() + >>> available_volume = swntr.metrics.conduit_available_volume(volume, average_capacity, threshold=1) + >>> time_to_capacity = swntr.metrics.conduit_time_to_capacity(available_volume, average_flowrate, flow_units=flow_units) + + To compute the time to reach capacity along a path, the total available volume and max flowrate are used in the calculation. + Again, this overly simplifies dynamics from blocked flow, but can be useful to identify response time for upstream assets. + + .. doctest:: + + >>> path_average_capacity = average_capacity[path_edges] + >>> path_average_flowrate = average_flowrate[path_edges] + >>> path_time_to_capacity = swntr.metrics.conduit_time_to_capacity(path_average_capacity, path_average_flowrate, flow_units=flow_units, connected=True) + +.. dropdown:: **Pump power and energy use** + + Pump flowrate and headloss can be used to compute power and energy use as a function of time. + + The following example uses pump flowrate and headloss to compute pump power and energy. + + .. doctest:: + + >>> # The following example uses swnP + >>> flow_units = swnP.options.loc['FLOW_UNITS', 'Value'] + >>> sim = swntr.sim.SWMMSimulator(swnP) + >>> results = sim.run_sim() + + >>> pump_flowrate = results.link['FLOW_RATE'].loc[:, swn.pump_name_list] + >>> node_head = results.node['HYDRAULIC_HEAD'] + >>> pump_headloss = swntr.metrics.headloss(node_head, swn, swn.pump_name_list) + >>> pump_power = swntr.metrics.pump_power(pump_flowrate, pump_headloss, flow_units) + >>> pump_energy = swntr.metrics.pump_energy(pump_flowrate, pump_headloss, flow_units) + +Graphics +-------- + +Network attributes, simulation results, and resilience metrics can be plotted in several +ways to better understand system characteristics. + +* Basic network graphics can be generated using the function :class:`~wntr.stormwater.graphics.plot_network`. +* Time series graphics can be generated using options available in Matplotlib and Pandas. +* Fragility curves can be plotted using the function :class:`~wntr.stormwater.graphics.plot_fragility_curve`. + +See drinking water documentation on :ref:`graphics` for more information on graphics capabilities in WNTR. + +The following example creates a network plot with invert elevation. + +.. doctest:: + :hide: + + >>> fig = plt.figure() + +.. doctest:: + + >>> ax = swntr.graphics.plot_network(swn, node_attribute='InvertElev', + ... node_colorbar_label='Invert Elevation') + +.. doctest:: + :hide: + + >>> plt.tight_layout() + >>> plt.savefig('plot_basic_stormwater_network.png', dpi=300) + +.. _fig-network-2: +.. figure:: figures/plot_basic_stormwater_network.png + :width: 640 + :alt: Network + + Basic stormwater network graphic. + +.. + .. _stormwater_examples: + + Examples + -------- + + ``[TODO: Add examples, or link to Jupyter notebooks]`` + + * Upstream and downstream assets + * Time to reach capacity + * Conduit criticality + * Power outages + * Extreme rainfall diff --git a/documentation/tables/swmm_units.csv b/documentation/tables/swmm_units.csv new file mode 100644 index 000000000..ca69f293d --- /dev/null +++ b/documentation/tables/swmm_units.csv @@ -0,0 +1,39 @@ +PARAMETER,US CUSTOMARY,SI METRIC +Area (Subcatchment),acres,hectares +Area (Storage Unit),square feet,square meters +Area (Ponding),square feet,square meters +Capillary Suction,inches,millimeters +Concentration,"* mg/L (milligrams/liter) +* ug/L (micrograms/liter) +* #/L (counts/liter)","* mg/L +* ug/L +* #/L" +Decay Constant (Infiltration),1/hours,1/hours +Decay Constant (Pollutants),1/days,1/days +Depression Storage,inches,millimeters +Depth,feet,meters +Diameter,feet,meters +Discharge Coefficient (Orifice),dimensionless,dimensionless +Discharge Coefficient (Weir),CFS/foot,CMS/meter +Elevation,feet,meters +Evaporation,inches/day,millimeters/day +Flow,"* CFS (cubic feet / second) +* GPM (gallons / minute) +* MGD (million gallons/day)","* CMS (cubic meters/second) +* LPS (liters/second) +* MLD (million liters/day)" +Head,feet,meters +Hydraulic Conductivity,inches/hour,millimeters/hour +Infiltration Rate,inches/hour,millimeters/hour +Length,feet,meters +Manning's Coefficient (n),seconds/meter1/3,seconds/meter1/3 +Pollutant Buildup,"* mass/length +* mass/acre","* mass/length +* mass/hectare" +Rainfall Intensity,inches/hour,millimeters/hour +Rainfall Volume,inches,millimeters +Slope (Subcatchments),percent,percent +Slope (Cross Section),rise/run,rise/run +Street Cleaning Interval,days,days +Volume,cubic feet,cubic meters +Width,feet,meters diff --git a/documentation/units.rst b/documentation/units.rst index fbfc172f6..470d65ffc 100644 --- a/documentation/units.rst +++ b/documentation/units.rst @@ -2,6 +2,8 @@ \clearpage +.. _units: + Units ====================================== diff --git a/documentation/userguide.rst b/documentation/userguide.rst index f38baf977..135e59147 100644 --- a/documentation/userguide.rst +++ b/documentation/userguide.rst @@ -7,6 +7,20 @@ User Guide The Water Network Tool for Resilience (WNTR) is an EPANET compatible Python package designed to simulate and analyze resilience of water distribution networks. + +.. grid:: 2 + + .. grid-item-card:: + + A majority of this user guide is dedicated to the user + of WNTR for resilience analysis of **drinking water systems**. + See :ref:`wntr_overview` for more information. + + .. grid-item-card:: + + A subset of WNTR's functionality has been developed for + **stormwater and wastewater systems**. + See :ref:`stormwater` for more information. US EPA Disclaimer ----------------- diff --git a/documentation/wntr-api.rst b/documentation/wntr-api.rst index f3fc7c4c1..7d2bd86c9 100644 --- a/documentation/wntr-api.rst +++ b/documentation/wntr-api.rst @@ -24,4 +24,5 @@ API documentation wntr.network wntr.scenario wntr.sim + wntr.stormwater wntr.utils diff --git a/examples/networks/Pump_Control_Model.inp b/examples/networks/Pump_Control_Model.inp new file mode 100644 index 000000000..ce61eacd7 --- /dev/null +++ b/examples/networks/Pump_Control_Model.inp @@ -0,0 +1,306 @@ +[TITLE] +;;Project Title/Notes +A pump control model. +See Pump_Control_Model.txt for more details. + +[OPTIONS] +;;Option Value +FLOW_UNITS CFS +INFILTRATION HORTON +FLOW_ROUTING DYNWAVE +LINK_OFFSETS DEPTH +MIN_SLOPE 0 +ALLOW_PONDING NO +SKIP_STEADY_STATE NO + +START_DATE 01/01/2001 +START_TIME 00:00:00 +REPORT_START_DATE 01/01/2001 +REPORT_START_TIME 00:00:00 +END_DATE 01/02/2001 +END_TIME 00:00:00 +SWEEP_START 01/01 +SWEEP_END 12/31 +DRY_DAYS 0 +REPORT_STEP 00:00:20 +WET_STEP 00:15:00 +DRY_STEP 01:00:00 +ROUTING_STEP 0:00:20 +RULE_STEP 00:00:00 + +INERTIAL_DAMPING PARTIAL +NORMAL_FLOW_LIMITED BOTH +FORCE_MAIN_EQUATION H-W +VARIABLE_STEP 0.00 +LENGTHENING_STEP 0 +MIN_SURFAREA 12.566 +MAX_TRIALS 8 +HEAD_TOLERANCE 0.005 +SYS_FLOW_TOL 5 +LAT_FLOW_TOL 5 +MINIMUM_STEP 0.5 +THREADS 1 + +[EVAPORATION] +;;Data Source Parameters +;;-------------- ---------------- +CONSTANT 0.0 +DRY_ONLY NO + +[JUNCTIONS] +;;Name Elevation MaxDepth InitDepth SurDepth Aponded +;;-------------- ---------- ---------- ---------- ---------- ---------- +KRO3001 556.19 10 0 0 0 +KRO6015 585.98 8 0 0 0 +KRO6016 584.14 9 0 0 0 +KRO6017 582.01 14 0 0 0 +KRO1002 594.89 3 0 0 0 +KRO1003 594.73 5 0 0 0 +KRO1004 588.09 8 0 0 0 +KRO1005 579.40 16 0 0 0 +KRO1006 602.48 7 0 0 0 +KRO1007 596.76 15 0 0 0 +KRO1008 593.58 12 0 0 0 +KRO1009 590.56 15 0 0 0 +KRO1010 584.82 11 0 0 0 +KRO1012 584.25 10 0 0 0 +KRO1013 591.58 14 0 0 0 +KRO1014 592.41 6 0 0 0 +KRO1015 586.69 10 0 0 0 +KRO2001 576.29 8 0 0 0 +KRO4004 587.39 11 0 0 0 +KRO4008 583.48 10 0 0 0 +KRO4009 581.68 8 0 0 0 +KRO4010 579.88 12 0 0 0 +KRO4011 578.43 10 0 0 0 +KRO4012 564.71 10 0 0 0 +KRO4013 567.88 12 0 0 0 +KRO4014 573.18 14 0 0 0 +KRO4015 563.71 10 0 0 0 +KRO4016 563.24 10 0 0 0 +KRO4017 558.36 10 0 0 0 +KRO4018 556.02 9 0 0 0 +KRO4019 552.42 10 0 0 0 + +[OUTFALLS] +;;Name Elevation Type Stage Data Gated Route To +;;-------------- ---------- ---------- ---------------- -------- ---------------- +KRO2005 574.32 FREE NO +PSO 548.36 FREE NO + +[STORAGE] +;;Name Elev. MaxDepth InitDepth Shape Curve Type/Params SurDepth Fevap Psi Ksat IMD +;;-------------- -------- ---------- ----------- ---------- ---------------------------- --------- -------- -------- -------- +;4' diameter wet well +SU1 544.74 17 0 CYLINDRICAL 6 6 0 0 0 + +[CONDUITS] +;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow +;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- +KRO3001-KRO3002 KRO3001 SU1 176.7171053 0.013 0 5 0 0 +;Pump Station +SU1-PSO SU1 PSO 65.78947368 0.013 6 0 0 0 +KRO1002-KRO1003 KRO1002 KRO1003 27.23684211 0.013 0 0 0 0 +KRO1003-KRO1008 KRO1003 KRO1008 197.7236842 0.013 0 0 0 0 +KRO1004-KRO6016 KRO1004 KRO6016 197.4144737 0.013 0 0 0 0 +KRO1005-KRO2001 KRO1005 KRO2001 134.3092105 0.013 0 0 0 0 +KRO1006-KRO1007 KRO1006 KRO1007 162.5921053 0.013 0 0 0 0 +KRO1007-KRO1008 KRO1007 KRO1008 165.8881579 0.013 0 0 0 0 +KRO1008-KRO1009 KRO1008 KRO1009 157.3223684 0.013 0 0 0 0 +KRO1009-KRO1010 KRO1009 KRO1010 210.9539474 0.013 0 0 0 0 +KRO1010-KRO2001 KRO1010 KRO2001 190.1842105 0.013 0 0 0 0 +KRO1012-KRO4009 KRO1012 KRO4009 128.2631579 0.013 0 0 0 0 +KRO1013-KRO1009 KRO1013 KRO1009 170.1052632 0.013 0 0 0 0 +KRO1014-KRO1013 KRO1014 KRO1013 500 0.013 0 0 0 0 +KRO1015-KRO4014 KRO1015 KRO4014 151.4342105 0.013 0 0 0 0 +KRO2001-KRO2005 KRO2001 KRO2005 243.7631579 0.013 0 0 0 0 +KRO4004-KRO4008 KRO4004 KRO4008 140 0.013 0 0 0 0 +KRO4008-KRO4011 KRO4008 KRO4011 78.72368421 0.013 0 0 0 0 +KRO4009-KRO4010 KRO4009 KRO4010 180.0526316 0.013 0 0 0 0 +KRO4010-KRO4011 KRO4010 KRO4011 144.5328947 0.013 0 0 0 0 +KRO4011-KRO4012 KRO4011 KRO4012 198.0723684 0.013 0 0 0 0 +KRO4012-KRO3001 KRO4012 KRO3001 129.0526316 0.013 0 0.5 0 0 +KRO4013-KRO4017 KRO4013 KRO4017 176.5394737 0.013 0 0 0 0 +KRO4014-KRO4015 KRO4014 KRO4015 138.7039474 0.013 0 0 0 0 +KRO4015-KRO4016 KRO4015 KRO4016 16.95394737 0.013 0 0 0 0 +KRO4016-KRO4017 KRO4016 KRO4017 108.7697368 0.013 0 0 0 0 +KRO4017-KRO4018 KRO4017 KRO4018 101.5592105 0.013 0 0 0 0 +KRO4018-KRO4019 KRO4018 KRO4019 159.8486842 0.013 0 0 0 0 +KRO4019-KRO3002 KRO4019 SU1 87.57894737 0.013 0 5 0 0 +KRO6015-KRO6016 KRO6015 KRO6016 91.85526316 0.013 0 0 0 0 +KRO6016-KRO6017 KRO6016 KRO6017 211.9736842 0.013 0 0 0 0 +KRO6017-KRO1005 KRO6017 KRO1005 178.8092105 0.013 0 0 0 0 + +[PUMPS] +;;Name From Node To Node Pump Curve Status Sartup Shutoff +;;-------------- ---------------- ---------------- ---------------- ------ -------- -------- +PUMP1 SU1 KRO1014 PUMP_CURVE1 ON 0 0 + +[XSECTIONS] +;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert +;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- +KRO3001-KRO3002 CIRCULAR 1 0 0 0 1 +SU1-PSO CIRCULAR 1 0 0 0 1 +KRO1002-KRO1003 CIRCULAR 1 0 0 0 1 +KRO1003-KRO1008 CIRCULAR 1 0 0 0 1 +KRO1004-KRO6016 CIRCULAR 1 0 0 0 1 +KRO1005-KRO2001 CIRCULAR 1 0 0 0 1 +KRO1006-KRO1007 CIRCULAR 1 0 0 0 1 +KRO1007-KRO1008 CIRCULAR 1 0 0 0 1 +KRO1008-KRO1009 CIRCULAR 1 0 0 0 1 +KRO1009-KRO1010 CIRCULAR 1 0 0 0 1 +KRO1010-KRO2001 CIRCULAR 1 0 0 0 1 +KRO1012-KRO4009 CIRCULAR 1 0 0 0 1 +KRO1013-KRO1009 CIRCULAR 1 0 0 0 1 +KRO1014-KRO1013 CIRCULAR 1 0 0 0 1 +KRO1015-KRO4014 CIRCULAR 1 0 0 0 1 +KRO2001-KRO2005 CIRCULAR 1 0 0 0 1 +KRO4004-KRO4008 CIRCULAR 1 0 0 0 1 +KRO4008-KRO4011 CIRCULAR 1 0 0 0 1 +KRO4009-KRO4010 CIRCULAR 1 0 0 0 1 +KRO4010-KRO4011 CIRCULAR 1 0 0 0 1 +KRO4011-KRO4012 CIRCULAR 1 0 0 0 1 +KRO4012-KRO3001 CIRCULAR 1 0 0 0 1 +KRO4013-KRO4017 CIRCULAR 1 0 0 0 1 +KRO4014-KRO4015 CIRCULAR 1 0 0 0 1 +KRO4015-KRO4016 CIRCULAR 1 0 0 0 1 +KRO4016-KRO4017 CIRCULAR 1 0 0 0 1 +KRO4017-KRO4018 CIRCULAR 1 0 0 0 1 +KRO4018-KRO4019 CIRCULAR 1 0 0 0 1 +KRO4019-KRO3002 CIRCULAR 1 0 0 0 1 +KRO6015-KRO6016 CIRCULAR 1 0 0 0 1 +KRO6016-KRO6017 CIRCULAR 1 0 0 0 1 +KRO6017-KRO1005 CIRCULAR 1 0 0 0 1 + +[CONTROLS] +RULE PUMP1A +IF NODE SU1 DEPTH >= 4 +THEN PUMP PUMP1 status = ON +PRIORITY 1 + +RULE PUMP1B +IF NODE SU1 DEPTH < 1 +THEN PUMP PUMP1 status = OFF +PRIORITY 1 + + + + + + +[DWF] +;;Node Constituent Baseline Patterns +;;-------------- ---------------- ---------- ---------- +KRO3001 FLOW 1 "" "" "DWF" +KRO6015 FLOW 1 "" "" "DWF" +KRO6016 FLOW 1 "" "" "DWF" +KRO6017 FLOW 1 "" "" "DWF" +KRO1002 FLOW 1 "" "" "DWF" +KRO1003 FLOW 1 "" "" "DWF" +KRO1004 FLOW 1 "" "" "DWF" +KRO1005 FLOW 1 "" "" "DWF" +KRO1006 FLOW 1 "" "" "DWF" +KRO1007 FLOW 1 "" "" "DWF" +KRO1008 FLOW 1 "" "" "DWF" +KRO1009 FLOW 1 "" "" "DWF" +KRO1010 FLOW 1 "" "" "DWF" +KRO1012 FLOW 1 "" "" "DWF" +KRO1013 FLOW 1 "" "" "DWF" +KRO1015 FLOW 1 "" "" "DWF" +KRO2001 FLOW 1 "" "" "DWF" +KRO4004 FLOW 1 "" "" "DWF" +KRO4008 FLOW 1 "" "" "DWF" +KRO4009 FLOW 1 "" "" "DWF" +KRO4010 FLOW 1 "" "" "DWF" +KRO4011 FLOW 1 "" "" "DWF" +KRO4012 FLOW 1 "" "" "DWF" +KRO4013 FLOW 1 "" "" "DWF" +KRO4014 FLOW 1 "" "" "DWF" +KRO4015 FLOW 1 "" "" "DWF" +KRO4017 FLOW 1 "" "" "DWF" +KRO4018 FLOW 1 "" "" "DWF" +KRO4019 FLOW 1 "" "" "DWF" +SU1 FLOW 1 "" "" "DWF" + +[CURVES] +;;Name Type X-Value Y-Value +;;-------------- ---------- ---------- ---------- +PUMP_CURVE1 Pump4 0 0 +PUMP_CURVE1 1 0.2 +PUMP_CURVE1 2 0.4 +PUMP_CURVE1 3 0.6 +PUMP_CURVE1 4 0.9 +PUMP_CURVE1 17 0.9 + +[PATTERNS] +;;Name Type Multipliers +;;-------------- ---------- ----------- +DWF HOURLY .0151 .01373 .01812 .01098 .01098 .01922 +DWF .02773 .03789 .03515 .03982 .02059 .02471 +DWF .03021 .03789 .03350 .03158 .03954 .02114 +DWF .02801 .03680 .02911 .02334 .02499 .02718 + +[REPORT] +;;Reporting Options +CONTROLS YES +SUBCATCHMENTS ALL +NODES ALL +LINKS ALL + +[TAGS] + +[MAP] +DIMENSIONS 1361856.362 428552.651 1363638.769 431248.506 +Units None + +[COORDINATES] +;;Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +KRO3001 1362408.250 431113.810 +KRO6015 1362748.630 428675.190 +KRO6016 1362767.000 428813.590 +KRO6017 1363087.250 428778.190 +KRO1002 1362361.780 429189.370 +KRO1003 1362367.000 429230.440 +KRO1004 1362860.250 429098.810 +KRO1005 1363142.000 429044.410 +KRO1006 1361937.380 429699.810 +KRO1007 1362171.000 429619.190 +KRO1008 1362406.250 429528.410 +KRO1009 1362629.000 429441.410 +KRO1010 1362927.630 429324.590 +KRO1012 1362287.630 429934.410 +KRO1013 1362657.380 429698.410 +KRO1014 1362656.752 430800.636 +KRO1015 1362725.750 429953.810 +KRO2001 1363203.750 429239.000 +KRO4004 1362024.630 430653.190 +KRO4008 1362236.380 430632.000 +KRO4009 1362307.000 430128.410 +KRO4010 1362333.380 430400.810 +KRO4011 1362355.380 430619.410 +KRO4012 1362385.250 430919.000 +KRO4013 1362511.750 430605.810 +KRO4014 1362751.190 430182.590 +KRO4015 1362774.000 430392.190 +KRO4016 1362761.250 430414.590 +KRO4017 1362778.750 430579.000 +KRO4018 1362796.000 430732.410 +KRO4019 1362704.750 430957.590 +KRO2005 1363557.750 429129.590 +PSO 1362683.696 431125.967 +SU1 1362652.040 431078.910 + +[VERTICES] +;;Link X-Coord Y-Coord +;;-------------- ------------------ ------------------ + +[Polygons] +;;Storage Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +SU1 1362652.040 431078.910 + +[LABELS] +;;X-Coord Y-Coord Label +1362744.750 431134.090 "Regulator Point" PSO "Arial" 10 0 0 + diff --git a/examples/networks/Site_Drainage_Model.inp b/examples/networks/Site_Drainage_Model.inp new file mode 100644 index 000000000..d630a93a0 --- /dev/null +++ b/examples/networks/Site_Drainage_Model.inp @@ -0,0 +1,498 @@ +[TITLE] +;;Project Title/Notes +A site surface drainage model. +See Site_Drainage_Model.txt for more details. + +[OPTIONS] +;;Option Value +FLOW_UNITS CFS +INFILTRATION HORTON +FLOW_ROUTING DYNWAVE +LINK_OFFSETS DEPTH +MIN_SLOPE 0 +ALLOW_PONDING NO +SKIP_STEADY_STATE NO + +START_DATE 01/01/1998 +START_TIME 00:00:00 +REPORT_START_DATE 01/01/1998 +REPORT_START_TIME 00:00:00 +END_DATE 01/01/1998 +END_TIME 06:00:00 +SWEEP_START 01/01 +SWEEP_END 12/31 +DRY_DAYS 5 +REPORT_STEP 00:01:00 +WET_STEP 00:01:00 +DRY_STEP 01:00:00 +ROUTING_STEP 0:00:15 +RULE_STEP 00:00:00 + +INERTIAL_DAMPING PARTIAL +NORMAL_FLOW_LIMITED SLOPE +FORCE_MAIN_EQUATION H-W +VARIABLE_STEP 0.75 +LENGTHENING_STEP 0 +MIN_SURFAREA 12.566 +MAX_TRIALS 8 +HEAD_TOLERANCE 0.005 +SYS_FLOW_TOL 5 +LAT_FLOW_TOL 5 +MINIMUM_STEP 0.5 +THREADS 1 + +[EVAPORATION] +;;Data Source Parameters +;;-------------- ---------------- +CONSTANT 0.0 +DRY_ONLY NO + +[RAINGAGES] +;;Name Format Interval SCF Source +;;-------------- --------- ------ ------ ---------- +RainGage INTENSITY 0:05 1.0 TIMESERIES 2-yr + +[SUBCATCHMENTS] +;;Name Rain Gage Outlet Area %Imperv Width %Slope CurbLen SnowPack +;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- ---------------- +S1 RainGage J1 4.55 56.8 1587 2 1680 +S2 RainGage J2 4.74 63.0 1653 2 1680 +S3 RainGage J3 3.74 39.5 1456 3.1 930 +S4 RainGage J7 6.79 49.9 2331 3.1 2250 +S5 RainGage J10 4.79 87.7 1670 2 2480 +S6 RainGage J11 1.98 95.0 690 2 1100 +S7 RainGage J10 2.33 0.0 907 3.1 565 + +[SUBAREAS] +;;Subcatchment N-Imperv N-Perv S-Imperv S-Perv PctZero RouteTo PctRouted +;;-------------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- +S1 0.015 0.24 0.06 0.3 25 OUTLET +S2 0.015 0.24 0.06 0.3 25 OUTLET +S3 0.015 0.24 0.06 0.3 25 OUTLET +S4 0.015 0.24 0.06 0.3 25 OUTLET +S5 0.015 0.24 0.06 0.3 25 OUTLET +S6 0.015 0.24 0.06 0.3 25 OUTLET +S7 0.015 0.24 0.06 0.3 25 OUTLET + +[INFILTRATION] +;;Subcatchment Param1 Param2 Param3 Param4 Param5 +;;-------------- ---------- ---------- ---------- ---------- ---------- +S1 4.5 0.2 6.5 7 0 +S2 4.5 0.2 6.5 7 0 +S3 4.5 0.2 6.5 7 0 +S4 4.5 0.2 6.5 7 0 +S5 4.5 0.2 6.5 7 0 +S6 4.5 0.2 6.5 7 0 +S7 4.5 0.2 6.5 7 0 + +[JUNCTIONS] +;;Name Elevation MaxDepth InitDepth SurDepth Aponded +;;-------------- ---------- ---------- ---------- ---------- ---------- +J1 4973 0 0 0 0 +J2 4969 0 0 0 0 +J3 4973 0 0 0 0 +J4 4971 0 0 0 0 +J5 4969.8 0 0 0 0 +J6 4969 0 0 0 0 +J7 4971.5 0 0 0 0 +J8 4966.5 0 0 0 0 +J9 4964.8 0 0 0 0 +J10 4963.8 0 0 0 0 +J11 4963 0 0 0 0 + +[OUTFALLS] +;;Name Elevation Type Stage Data Gated Route To +;;-------------- ---------- ---------- ---------------- -------- ---------------- +O1 4962 FREE NO + +[CONDUITS] +;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow +;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- +C1 J1 J5 185.00 0.05 0 0 0 0 +C2 J2 J11 526.00 0.016 0 4 0 0 +C3 J3 J4 109.00 0.016 0 0 0 0 +C4 J4 J5 133.00 0.05 0 0 0 0 +C5 J5 J6 207.00 0.05 0 0 0 0 +C6 J7 J6 140.00 0.05 0 0 0 0 +C7 J6 J8 95.00 0.016 0 0 0 0 +C8 J8 J9 166.00 0.05 0 0 0 0 +C9 J9 J10 320.00 0.05 0 0 0 0 +C10 J10 J11 145.00 0.05 0 0 0 0 +C11 J11 O1 89.00 0.016 0 0 0 0 + +[XSECTIONS] +;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert +;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- +C1 TRAPEZOIDAL 3 5 5 5 1 +C2 TRAPEZOIDAL 1 0 0.0001 25 1 +C3 CIRCULAR 2.25 0 0 0 1 +C4 TRAPEZOIDAL 3 5 5 5 1 +C5 TRAPEZOIDAL 3 5 5 5 1 +C6 TRAPEZOIDAL 3 5 5 5 1 +C7 CIRCULAR 3.5 0 0 0 1 +C8 TRAPEZOIDAL 3 5 5 5 1 +C9 TRAPEZOIDAL 3 5 5 5 1 +C10 TRAPEZOIDAL 3 5 5 5 1 +C11 CIRCULAR 4.75 0 0 0 1 + +[POLLUTANTS] +;;Name Units Crain Cgw Crdii Kdecay SnowOnly Co-Pollutant Co-Frac Cdwf Cinit +;;-------------- ------ ---------- ---------- ---------- ---------- ---------- ---------------- ---------- ---------- ---------- +TSS MG/L 0.0 0.0 0.0 0.0 NO * 0.0 0 0 + +[LANDUSES] +;; Sweeping Fraction Last +;;Name Interval Available Swept +;;-------------- ---------- ---------- ---------- +Residential_1 0 0 0 +Residential_2 0 0 0 +Commercial 0 0 0 +Undeveloped 0 0 0 + +[COVERAGES] +;;Subcatchment Land Use Percent +;;-------------- ---------------- ---------- +S1 Residential_1 100 +S2 Residential_1 27 +S2 Residential_2 73 +S3 Residential_1 27 +S3 Residential_2 32 +S4 Residential_1 9 +S4 Residential_2 30 +S4 Commercial 26 +S5 Commercial 98 +S6 Commercial 100 + +[LOADINGS] +;;Subcatchment Pollutant Buildup +;;-------------- ---------------- ---------- + +[BUILDUP] +;;Land Use Pollutant Function Coeff1 Coeff2 Coeff3 Per Unit +;;-------------- ---------------- ---------- ---------- ---------- ---------- ---------- +Residential_1 TSS EXP 0.11 0.5 0.0 CURB +Residential_2 TSS EXP 0.13 0.5 0.0 CURB +Commercial TSS EXP 0.15 0.2 0.0 CURB +Undeveloped TSS NONE 0.0 0.0 0.0 AREA + +[WASHOFF] +;;Land Use Pollutant Function Coeff1 Coeff2 SweepRmvl BmpRmvl +;;-------------- ---------------- ---------- ---------- ---------- ---------- ---------- +Residential_1 TSS EXP 2 1.8 0.0 0.0 +Residential_2 TSS EXP 4 2.2 0.0 0.0 +Commercial TSS EXP 4 2.2 0.0 0.0 +Undeveloped TSS RC 500 2 0.0 0.0 + +[TIMESERIES] +;;Name Date Time Value +;;-------------- ---------- ---------- ---------- +2-yr 0:00 0.29 +2-yr 0:05 0.33 +2-yr 0:10 0.38 +2-yr 0:15 0.64 +2-yr 0:20 0.81 +2-yr 0:25 1.57 +2-yr 0:30 2.85 +2-yr 0:35 1.18 +2-yr 0:40 0.71 +2-yr 0:45 0.42 +2-yr 0:50 0.35 +2-yr 0:55 0.3 +2-yr 1:00 0.2 +2-yr 1:05 0.19 +2-yr 1:10 0.18 +2-yr 1:15 0.17 +2-yr 1:20 0.17 +2-yr 1:25 0.16 +2-yr 1:30 0.15 +2-yr 1:35 0.15 +2-yr 1:40 0.14 +2-yr 1:45 0.14 +2-yr 1:50 0.13 +2-yr 1:55 0.13 +; +10-yr 0:00 0.49 +10-yr 0:05 0.56 +10-yr 0:10 0.65 +10-yr 0:15 1.09 +10-yr 0:20 1.39 +10-yr 0:25 2.69 +10-yr 0:30 4.87 +10-yr 0:35 2.02 +10-yr 0:40 1.21 +10-yr 0:45 0.71 +10-yr 0:50 0.6 +10-yr 0:55 0.52 +10-yr 1:00 0.39 +10-yr 1:05 0.37 +10-yr 1:10 0.35 +10-yr 1:15 0.34 +10-yr 1:20 0.32 +10-yr 1:25 0.31 +10-yr 1:30 0.3 +10-yr 1:35 0.29 +10-yr 1:40 0.28 +10-yr 1:45 0.27 +10-yr 1:50 0.26 +10-yr 1:55 0.25 +; +100-yr 0:00 1 +100-yr 0:05 1.14 +100-yr 0:10 1.33 +100-yr 0:15 2.23 +100-yr 0:20 2.84 +100-yr 0:25 5.49 +100-yr 0:30 9.95 +100-yr 0:35 4.12 +100-yr 0:40 2.48 +100-yr 0:45 1.46 +100-yr 0:50 1.22 +100-yr 0:55 1.06 +100-yr 1:00 1 +100-yr 1:05 0.95 +100-yr 1:10 0.91 +100-yr 1:15 0.87 +100-yr 1:20 0.84 +100-yr 1:25 0.81 +100-yr 1:30 0.78 +100-yr 1:35 0.75 +100-yr 1:40 0.73 +100-yr 1:45 0.71 +100-yr 1:50 0.69 +100-yr 1:55 0.67 + +[REPORT] +;;Reporting Options +SUBCATCHMENTS ALL +NODES ALL +LINKS ALL + +[TAGS] +Link C1 Swale +Link C2 Gutter +Link C3 Culvert +Link C4 Swale +Link C5 Swale +Link C6 Swale +Link C7 Culvert +Link C8 Swale +Link C9 Swale +Link C10 Swale +Link C11 Culvert + +[MAP] +DIMENSIONS -255.206 -70.199 1490.833 1514.231 +Units Feet + +[COORDINATES] +;;Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +J1 648.532 1043.713 +J2 1221.281 1005.760 +J3 405.287 905.702 +J4 505.345 862.573 +J5 631.281 859.123 +J6 822.772 819.444 +J7 831.398 709.035 +J8 915.930 840.146 +J9 1072.918 867.749 +J10 1254.058 640.029 +J11 1334.823 497.305 +O1 1411.468 477.401 + +[VERTICES] +;;Link X-Coord Y-Coord +;;-------------- ------------------ ------------------ +C2 1321.339 774.591 +C2 1374.819 652.105 +C2 1366.193 588.275 +C4 541.573 841.871 +C5 672.684 850.497 +C5 712.363 829.795 +C5 743.415 805.643 +C5 805.520 812.544 +C6 791.719 734.912 +C6 798.620 784.942 +C8 965.959 838.421 +C8 995.287 831.520 +C8 1038.415 850.497 +C9 1102.246 867.749 +C9 1131.573 852.222 +C9 1147.099 829.795 +C9 1162.626 809.094 +C9 1198.854 779.766 +C9 1219.556 757.339 +C9 1233.357 721.111 +C9 1238.532 715.936 +C9 1235.082 674.532 +C9 1247.158 646.930 + +[Polygons] +;;Subcatchment X-Coord Y-Coord +;;-------------- ------------------ ------------------ +S1 282.657 1334.810 +S1 111.700 1101.604 +S1 172.525 1062.743 +S1 231.660 1027.262 +S1 306.002 990.092 +S1 370.206 959.679 +S1 409.066 946.163 +S1 444.547 936.025 +S1 493.545 924.198 +S1 532.405 915.750 +S1 569.576 907.302 +S1 610.125 897.165 +S1 655.744 897.165 +S1 684.338 1318.700 +S1 651.043 1321.922 +S1 596.269 1332.662 +S1 551.160 1346.624 +S1 495.312 1367.030 +S1 455.573 1384.214 +S1 410.465 1409.991 +S1 386.836 1427.175 +S1 363.208 1442.211 +S2 678.967 1238.149 +S2 673.584 1152.903 +S2 655.744 897.165 +S2 758.808 893.786 +S2 817.943 895.475 +S2 880.458 898.855 +S2 921.007 905.613 +S2 978.453 920.819 +S2 1042.657 937.715 +S2 1103.482 959.679 +S2 1159.238 985.023 +S2 1225.131 1010.367 +S2 1109.646 1274.665 +S2 1052.723 1400.325 +S2 985.061 1370.252 +S2 924.916 1348.772 +S2 861.549 1331.588 +S2 815.367 1325.144 +S2 762.740 1319.774 +S2 719.780 1316.552 +S2 684.338 1317.626 +S3 109.199 1103.258 +S3 141.754 1081.555 +S3 190.586 1051.713 +S3 247.557 1019.158 +S3 304.528 989.317 +S3 354.716 964.900 +S3 398.123 949.980 +S3 490.166 922.509 +S3 477.743 883.275 +S3 501.993 816.065 +S3 556.059 778.895 +S3 488.476 679.210 +S3 422.582 729.897 +S3 282.348 557.560 +S3 179.734 633.927 +S3 153.962 651.561 +S3 107.843 693.610 +S3 71.218 742.443 +S3 48.159 785.849 +S3 31.881 837.394 +S3 29.168 886.226 +S3 31.881 933.702 +S3 38.664 967.613 +S3 50.872 1001.525 +S3 65.793 1035.436 +S3 87.496 1070.704 +S3 109.199 1103.258 +S4 282.348 559.250 +S4 420.893 729.897 +S4 488.476 680.899 +S4 556.828 779.067 +S4 501.213 814.335 +S4 479.468 885.000 +S4 491.718 922.851 +S4 616.511 898.434 +S4 668.056 897.078 +S4 783.355 895.722 +S4 815.909 898.434 +S4 857.959 899.791 +S4 890.595 897.165 +S4 968.316 915.750 +S4 1042.657 937.715 +S4 1074.759 849.857 +S4 1054.484 773.826 +S4 1020.692 702.864 +S4 963.247 623.454 +S4 689.536 256.816 +S5 1301.482 474.258 +S5 1271.677 445.380 +S5 1232.340 393.835 +S5 1241.835 384.340 +S5 1222.844 366.706 +S5 1233.696 355.854 +S5 1026.159 66.931 +S5 1008.525 56.079 +S5 708.750 275.824 +S5 1023.446 704.462 +S5 1150.644 618.812 +S5 1251.203 640.809 +S5 1328.193 519.824 +S6 1334.478 519.824 +S6 1306.266 488.956 +S6 1293.380 474.205 +S6 1232.340 393.835 +S6 1241.835 381.627 +S6 1222.844 365.350 +S6 1232.340 353.142 +S6 1027.516 65.574 +S6 1012.595 56.079 +S6 707.393 273.111 +S6 688.403 254.121 +S6 739.948 218.853 +S6 788.780 159.169 +S6 806.414 106.268 +S6 813.197 1.821 +S6 994.961 12.673 +S6 1228.270 27.594 +S6 1222.844 115.763 +S6 1228.270 167.308 +S6 1241.835 229.705 +S6 1255.399 254.121 +S6 1279.815 302.953 +S6 1309.657 354.498 +S6 1335.430 401.974 +S6 1359.846 448.093 +S6 1370.616 475.830 +S6 1381.615 491.542 +S7 1122.467 968.970 +S7 1174.012 987.282 +S7 1225.557 1005.594 +S7 1377.480 675.977 +S7 1391.044 642.065 +S7 1396.470 598.659 +S7 1381.615 491.542 +S7 1331.336 519.824 +S7 1249.632 640.809 +S7 1150.644 617.241 +S7 1020.733 704.462 +S7 1054.645 772.285 +S7 1076.796 848.212 +S7 1056.370 900.062 +S7 1040.658 937.772 + +[SYMBOLS] +;;Gage X-Coord Y-Coord +;;-------------- ------------------ ------------------ +RainGage -175.841 1212.778 + +[LABELS] +;;X-Coord Y-Coord Label +218.836 1149.697 "S1" "" "Arial" 14 0 0 +721.146 997.569 "S2" "" "Arial" 14 0 0 +236.058 742.108 "S3" "" "Arial" 14 0 0 +663.739 446.463 "S4" "" "Arial" 14 0 0 +979.477 236.928 "S5" "" "Arial" 14 0 0 +853.182 96.281 "S6" "" "Arial" 14 0 0 +1094.291 753.590 "S7" "" "Arial" 14 0 0 +1375.584 437.852 "Outfall" "" "Arial" 10 1 0 + + +[BACKDROP] +FILE "Site-Post.jpg" +DIMENSIONS 0.000 0.000 1423.000 1475.000 diff --git a/examples/stormwater_criticality.py b/examples/stormwater_criticality.py new file mode 100644 index 000000000..12d07a288 --- /dev/null +++ b/examples/stormwater_criticality.py @@ -0,0 +1,21 @@ +# Conduit criticality analysis +import matplotlib.pylab as plt +import wntr.stormwater as swntr + +inp_file = 'networks/Site_Drainage_Model.inp' +swn = swntr.network.StormWaterNetworkModel(inp_file) + +results = {} +fig, ax = plt.subplots() + +for conduit_name in swn.conduit_name_list: + print(conduit_name) + swn = swntr.network.StormWaterNetworkModel(inp_file) + swn.conduits.loc[conduit_name, "MaxFlow"] = 0.00001 + sim = swntr.sim.SWMMSimulator(swn) + results[conduit_name] = sim.run_sim(conduit_name) + + flowrate = results[conduit_name].link['FLOW_VELOCITY'] + flowrate.mean(axis=1).plot(ax=ax, label=conduit_name) + +plt.legend() diff --git a/requirements.txt b/requirements.txt index da4347a42..9852147b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,8 @@ openpyxl geopandas<1.0 fiona<1.10 rtree +pyswmm +swmmio # Documentation sphinx diff --git a/wntr/__init__.py b/wntr/__init__.py index 2028f34c3..0a94a3f8a 100644 --- a/wntr/__init__.py +++ b/wntr/__init__.py @@ -7,6 +7,7 @@ from wntr import graphics from wntr import gis from wntr import utils +from wntr import stormwater __version__ = '1.2.0' diff --git a/wntr/stormwater/__init__.py b/wntr/stormwater/__init__.py new file mode 100644 index 000000000..b4285018c --- /dev/null +++ b/wntr/stormwater/__init__.py @@ -0,0 +1,13 @@ +""" +The wntr.stormwater package contains stormwater and wastewater +resilience functionality +""" +from wntr.stormwater.gis import * +from wntr.stormwater.graphics import * +from wntr.stormwater.io import * +from wntr.stormwater.metrics import * +from wntr.stormwater.network import * +from wntr.stormwater.sim import * +from wntr.stormwater.scenario import * + +warnings.filterwarnings('ignore', module='swmmio') diff --git a/wntr/stormwater/gis.py b/wntr/stormwater/gis.py new file mode 100644 index 000000000..b3692b863 --- /dev/null +++ b/wntr/stormwater/gis.py @@ -0,0 +1,265 @@ +""" +The wntr.stormwater.gis module contains methods to +integrate geospatial data into stormwater models and analysis. +""" +import pandas as pd + +try: + from shapely.geometry import LineString, Point, Polygon + has_shapely = True +except ModuleNotFoundError: + has_shapely = False + +try: + import geopandas as gpd + has_geopandas = True +except ModuleNotFoundError: + has_geopandas = False + +from wntr.gis import snap, intersect + + +class StormWaterNetworkGIS: + """ + Storm Water network GIS class + + Contains methods to create GeoDataFrames from StormWaterNetworkModel. + The ability to a create StormWaterNetworkModel from GeoDataFrames is + not implemented. + + Parameters + ---------- + gis_data : dict, optional + Dictionary of GeoDataFrames containing data to populate an instance + of StormWaterNetworkGIS. Valid dictionary keys are + 'junctions', 'outfalls','storage', + 'conduits', 'weirs', 'orifices', 'pumps', and 'subcatchments' + + Raises + ------ + ModuleNotFoundError + if missing either shapely or geopandas + """ + + def __init__(self, gis_data=None) -> None: + + if not has_shapely or not has_geopandas: + raise ModuleNotFoundError('shapley and geopandas are required') + + self.junctions = gpd.GeoDataFrame() + self.outfalls = gpd.GeoDataFrame() + self.storage = gpd.GeoDataFrame() + self.conduits = gpd.GeoDataFrame() + self.weirs = gpd.GeoDataFrame() + self.orifices = gpd.GeoDataFrame() + self.pumps = gpd.GeoDataFrame() + self.subcatchments = gpd.GeoDataFrame() + + self._gdf_name_list = ["junctions", "outfalls", "storage", + "conduits", "weirs", "orifices", "pumps", + "subcatchments"] + + if isinstance(gis_data, dict): + for name in self._gdf_name_list: + gdf = getattr(self, name) + if name in gis_data.keys(): + assert isinstance(gis_data[name], gpd.GeoDataFrame) + gdf = gis_data[name] + + def _create_gis(self, swn, crs: str = None) -> None: + """ + Create GIS data from a water network model. + + This method is used by wntr.network.io.to_gis + + Note: patterns, curves, rules, controls, sources, and options are not + saved to the GIS data + + Parameters + ---------- + wn : WaterNetworkModel + Water network model + crs : str, optional + Coordinate reference system, by default None + """ + + # Nodes + geom = [(i, Point(coord['X'], coord['Y'])) for i, coord in swn.coordinates.iterrows()] + geom = pd.Series(dict(geom)) + self.junctions = gpd.GeoDataFrame(swn.junctions, geometry=geom[swn.junction_name_list], crs=crs) + self.outfalls = gpd.GeoDataFrame(swn.outfalls, geometry=geom[swn.outfall_name_list], crs=crs) + self.storage = gpd.GeoDataFrame(swn.storage, geometry=geom[swn.storage_name_list], crs=crs) + + # Links + link_inlet_pt = swn.coordinates.loc[swn.links['InletNode'].values] + link_inlet_pt.index = swn.links.index + link_outlet_pt = swn.coordinates.loc[swn.links['OutletNode'].values] + link_outlet_pt.index = swn.links.index + geom = {} + for link_name in swn.links.index: + vertices = [] + inlet_coord = link_inlet_pt.loc[link_name,:] + outlet_coord = link_outlet_pt.loc[link_name,:] + vertices.append((inlet_coord['X'], inlet_coord['Y'])) + vertices.extend(swn.vertices.loc[swn.vertices.index == link_name,:].values) + vertices.append((outlet_coord['X'], outlet_coord['Y'])) + geom[link_name] = LineString(vertices) + geom = pd.Series(geom) + + self.conduits = gpd.GeoDataFrame(swn.conduits, geometry=geom[swn.conduit_name_list], crs=crs) + self.weirs = gpd.GeoDataFrame(swn.weirs, geometry=geom[swn.weir_name_list], crs=crs) + self.orifices = gpd.GeoDataFrame(swn.orifices, geometry=geom[swn.orifice_name_list], crs=crs) + self.pumps = gpd.GeoDataFrame(swn.pumps, geometry=geom[swn.pump_name_list], crs=crs) + + # Subcatchments + geom = {} + for subcatch_name in swn.subcatchments.index: + vertices = swn.polygons.loc[swn.polygons.index == subcatch_name,:].values + geom[subcatch_name] = Polygon(vertices) + geom = pd.Series(geom) + self.subcatchments = gpd.GeoDataFrame(swn.subcatchments, geometry=geom, crs=crs) + """ + import swmmio + # create gis from an updated swmmio model + # This is very slow for large models + # Models without certain features (subcatchments) fail + filename = 'temp.inp' + swn._swmmio_model.inp.save(filename) + m = swmmio.Model(filename) + + self.junctions = m.nodes.geodataframe.loc[swn.junction_name_list,:] + self.outfalls = m.nodes.geodataframe.loc[swn.outfall_name_list,:] + self.storage = m.nodes.geodataframe.loc[swn.storage_name_list,:] + + self.conduits = m.links.geodataframe.loc[swn.conduit_name_list,:] + self.weirs = m.links.geodataframe.loc[swn.weir_name_list,:] + self.orifices = m.links.geodataframe.loc[swn.orifice_name_list,:] + self.pumps = m.links.geodataframe.loc[swn.pump_name_list,:] + + self.subcatchments = m.subcatchments.geodataframe + + if crs is not None: + self.set_crs(crs, allow_override=True) + """ + def _create_swn(self, append=None): + raise NotImplementedError + + def to_crs(self, crs): + """ + Transform CRS of the junctions, tanks, reservoirs, pipes, pumps, + and valves GeoDataFrames. + + Calls geopandas.GeoDataFrame.to_crs on each GeoDataFrame. + + Parameters + ---------- + crs : str + Coordinate reference system + """ + for name in self._gdf_name_list: + gdf = getattr(self, name) + if 'geometry' in gdf.columns: + gdf = gdf.to_crs(crs, inplace=True) + + def set_crs(self, crs, allow_override=False): + """ + Set CRS of the junctions, tanks, reservoirs, pipes, pumps, + and valves GeoDataFrames. + + Calls geopandas.GeoDataFrame.set_crs on each GeoDataFrame. + + Parameters + ---------- + crs : str + Coordinate reference system + allow_override : bool (optional) + Allow override of existing coordinate reference system + """ + + for name in self._gdf_name_list: + gdf = getattr(self, name) + if 'geometry' in gdf.columns: + gdf = gdf.set_crs(crs, inplace=True, + allow_override=allow_override) + + def add_node_attributes(self, values, name): + raise NotImplementedError + + def add_link_attributes(self, values, name): + raise NotImplementedError + + def _read(self, files, index_col='index'): + + for name in self._gdf_name_list: + gdf = getattr(self, name) + if name in files.keys(): + data = gpd.read_file(files[name]).set_index(index_col) + gdf = pd.concat([gdf, data]) + + def read_geojson(self, files, index_col='index'): + """ + Append information from GeoJSON files to a WaterNetworkGIS object + + Parameters + ---------- + files : dictionary + Dictionary of GeoJSON filenames, where the keys are in the set + ('junction', 'tanks', 'reservoirs', 'pipes', 'pumps', 'valves') and + values are the corresponding GeoJSON filename + index_col : str, optional + Column that contains the element name + """ + self._read(files, index_col) + + def read_shapefile(self, files, index_col='index'): + raise NotImplementedError + + def _write(self, prefix: str, driver="GeoJSON") -> None: + """ + Write the WaterNetworkGIS object to GIS files + + One file will be created for each type of network element (junctions, + pipes, etc.) if those elements exists in the network + + Parameters + ---------- + prefix : str + Filename prefix, will have the element type (junctions, + pipes, etc.) appended + driver : str, optional + GeoPandas driver. Use "GeoJSON" for GeoJSON files, use :code:`None` + for Esri Shapefile folders, by default "GeoJSON" + + """ + + if driver is None or driver == "": + extension = "" + else: + extension = "." + driver.lower() + + for name in self._gdf_name_list: + gdf = getattr(self, name) + if len(gdf) > 0: + filename = prefix + "_" + name + extension + gdf.to_file(filename, driver=driver) + + def write_geojson(self, prefix: str): + """ + Write the WaterNetworkGIS object to a set of GeoJSON files, one file + for each network element. + + Parameters + ---------- + prefix : str + File prefix + """ + self._write(prefix=prefix, driver="GeoJSON") + + def write_shapefile(self, prefix: str): + raise NotImplementedError + + def _valid_names(self, complete_list=True, truncate_names=None): + raise NotImplementedError + + def _shapefile_field_name_map(self): + raise NotImplementedError diff --git a/wntr/stormwater/graphics.py b/wntr/stormwater/graphics.py new file mode 100644 index 000000000..4c14d4834 --- /dev/null +++ b/wntr/stormwater/graphics.py @@ -0,0 +1,181 @@ +""" +The wntr.stormwater.graphics module contains methods to +generate graphics. +""" +import logging +import networkx as nx +import matplotlib.pylab as plt +import pandas as pd + +try: + from shapely.geometry import LineString, Point, Polygon + has_shapely = True +except ModuleNotFoundError: + has_shapely = False + +try: + import geopandas as gpd + has_geopandas = True +except ModuleNotFoundError: + has_geopandas = False + +from wntr.graphics.network import plot_network as _plot_network +from wntr.graphics.curve import plot_fragility_curve +from wntr.graphics.color import custom_colormap, random_colormap + +logger = logging.getLogger(__name__) + + +def plot_network(swn, node_attribute=None, link_attribute=None, subcatchment_attribute=None, title=None, + node_size=20, node_range=[None,None], node_alpha=1, node_cmap=None, node_labels=False, + link_width=1, link_range=[None,None], link_alpha=1, link_cmap=None, link_labels=False, + subcatchment_width=1, subcatchment_range=[None,None], subcatchment_alpha=0.5, subcatchment_cmap=None, + add_colorbar=True, node_colorbar_label='Node', link_colorbar_label='Link', + directed=False, ax=None, filename=None): + """ + Plot network graphic + + Parameters + ---------- + wn : wntr WaterNetworkModel + A WaterNetworkModel object + + node_attribute : None, str, list, pd.Series, or dict, optional + + - If node_attribute is a string, then a node attribute dictionary is + created using node_attribute = wn.query_node_attribute(str) + - If node_attribute is a list, then each node in the list is given a + value of 1. + - If node_attribute is a pd.Series, then it should be in the format + {nodeid: x} where nodeid is a string and x is a float. + - If node_attribute is a dict, then it should be in the format + {nodeid: x} where nodeid is a string and x is a float + + link_attribute : None, str, list, pd.Series, or dict, optional + + - If link_attribute is a string, then a link attribute dictionary is + created using edge_attribute = wn.query_link_attribute(str) + - If link_attribute is a list, then each link in the list is given a + value of 1. + - If link_attribute is a pd.Series, then it should be in the format + {linkid: x} where linkid is a string and x is a float. + - If link_attribute is a dict, then it should be in the format + {linkid: x} where linkid is a string and x is a float. + + subcatchment_attribute : None, str + Name of the subcatchment attribute to plot (only supports string, + which must be a column name in swn.subcatchments) + + title: str, optional + Plot title + + node_size: int, optional + Node size + + node_range: list, optional + Node color range ([None,None] indicates autoscale) + + node_alpha: int, optional + Node transparency + + node_cmap: matplotlib.pyplot.cm colormap or list of named colors, optional + Node colormap + + node_labels: bool, optional + If True, the graph will include each node labelled with its name. + + link_width: int, optional + Link width + + link_range : list, optional + Link color range ([None,None] indicates autoscale) + + link_alpha : int, optional + Link transparency + + link_cmap: matplotlib.pyplot.cm colormap or list of named colors, optional + Link colormap + + link_labels: bool, optional + If True, the graph will include each link labelled with its name. + + subcatchment_width: int, optional + Subcatchment width + + subcatchment_range: list, optional + Subcatchment color range ([None,None] indicates autoscale) + + subcatchment_alpha : int, optional + Subcatchment transparency + + subcatchment_cmap: matplotlib.pyplot.cm colormap or list of named colors, optional + Subcatchment colormap + + add_colorbar: bool, optional + Add colorbar + + node_colorbar_label: str, optional + Node colorbar label + + link_colorbar_label: str, optional + Link colorbar label + + directed: bool, optional + If True, plot the directed graph + + ax: matplotlib axes object, optional + Axes for plotting (None indicates that a new figure with a single + axes will be used) + + show_plot: bool, optional + If True, show plot with plt.show() + + filename : str, optional + Filename used to save the figure + + Returns + ------- + ax : matplotlib axes object + """ + + if ax is None: # create a new figure + plt.figure(facecolor="w", edgecolor="k") + ax = plt.gca() + + if isinstance(node_attribute, str): + node_attribute = swn.nodes[node_attribute] + if isinstance(link_attribute, str): + link_attribute = swn.links[link_attribute] + + if (swn.subcatchments.shape[0] > 0) and (swn.polygons.shape[0] > 0): + if not has_shapely or not has_geopandas: + raise ModuleNotFoundError('shapley and geopandas are required') + geom = {} + for subcatch_name in swn.subcatchments.index: + vertices = swn.polygons.loc[swn.polygons.index == subcatch_name,:].values + geom[subcatch_name] = Polygon(vertices) + geom = pd.Series(geom) + subcatchments = gpd.GeoDataFrame(swn.subcatchments, geometry=geom) + + if subcatchment_attribute is None: + subcatchments.boundary.plot(color='darkblue', + linewidth=subcatchment_width, + vmin=subcatchment_range[0], + vmax=subcatchment_range[1], + alpha=subcatchment_alpha, + cmap=subcatchment_cmap, + ax=ax) + else: + subcatchments.plot(column=subcatchment_attribute, + linewidth=subcatchment_width, + vmin=subcatchment_range[0], + vmax=subcatchment_range[1], + alpha=subcatchment_alpha, + cmap=subcatchment_cmap, + ax=ax) + + _plot_network(swn, node_attribute, link_attribute, title, + node_size, node_range, node_alpha, node_cmap, node_labels, + link_width, link_range, link_alpha, link_cmap, link_labels, + add_colorbar, node_colorbar_label, link_colorbar_label, + directed, ax=ax, filename=filename) diff --git a/wntr/stormwater/io.py b/wntr/stormwater/io.py new file mode 100644 index 000000000..494117206 --- /dev/null +++ b/wntr/stormwater/io.py @@ -0,0 +1,263 @@ +""" +The wntr.stormwater.io module contains methods to +read and write stormwater and wastewater network models. +""" +import logging +import pandas as pd +import networkx as nx + +try: + import swmmio + has_swmmio = True +except ModuleNotFoundError: + swmmio = None + has_swmmio = False + +try: + import pyswmm + import swmm.toolkit + has_pyswmm = True +except ModuleNotFoundError: + pyswmm = None + has_pyswmm = False + +from wntr.sim import SimulationResults +from wntr.stormwater.gis import StormWaterNetworkGIS +import wntr.stormwater + +logger = logging.getLogger(__name__) + + +def to_graph(swn, node_weight=None, link_weight=None, modify_direction=False): + """ + Convert a StormWaterNetworkModel into a NetworkX MultiDiGraph + + Parameters + ---------- + swn : StormWaterNetworkModel + Storm water network model + node_weight : dict or pandas Series (optional) + Node weights + link_weight : dict or pandas Series (optional) + Link weights + modify_direction : bool (optional) + If True, then if the link weight is negative, the link start and + end node are switched and the abs(weight) is assigned to the link + (this is useful when weighting graphs by flowrate). If False, link + direction and weight are not changed. + + Returns + ------- + NetworkX MultiDiGraph + + """ + # Note, this function could use "G = swn._swmmio_model.network" but that would + # require an additional write/read of the inp file to capture model + # updates + + G = nx.MultiDiGraph() + + for name in swn.node_name_list: + node = swn.get_node(name) + G.add_node(name) + coords = (swn.coordinates.loc[name, 'X'], swn.coordinates.loc[name, 'Y']) + nx.set_node_attributes(G, name="pos", values={name: coords}) + nx.set_node_attributes(G, name="type", values={name: node.node_type}) + + if node_weight is not None: + try: # weight nodes + value = node_weight[name] + nx.set_node_attributes(G, name="weight", values={name: value}) + except: + pass + + for name in swn.link_name_list: + link = swn.get_link(name) + start_node = link.start_node_name + end_node = link.end_node_name + G.add_edge(start_node, end_node, key=name) + nx.set_edge_attributes(G, name="type", values={(start_node, end_node, name): link.link_type}) + + if link_weight is not None: + try: # weight links + value = link_weight[name] + if modify_direction and value < 0: # change the direction of the link and value + G.remove_edge(start_node, end_node, name) + G.add_edge(end_node, start_node, name) + nx.set_edge_attributes(G, name="type", values={(end_node, start_node, name): link.link_type}) + nx.set_edge_attributes(G, name="weight", values={(end_node, start_node, name): -value}) + else: + nx.set_edge_attributes(G, name="weight", values={(start_node, end_node, name): value}) + except: + pass + + return G + +def to_gis(swn, crs=None): + """ + Convert a StormWaterNetworkModel into GeoDataFrames + + Parameters + ---------- + swn : WaterNetworkModel + Water network model + crs : str, optional + Coordinate reference system, by default None + + Returns + ------- + StormWaterNetworkGIS object that contains GeoDataFrames + + """ + # Create geodataframes + gis_data = StormWaterNetworkGIS() + gis_data._create_gis(swn, crs) + return gis_data + +def write_inpfile(swn, filename): + """ + Write the StormWaterNetworkModel to an EPANET INP file + + Parameters + ---------- + swn : WaterNetworkModel + Water network model + filename : string + Name of the inp file + + """ + for sec in swn.section_names: + df = getattr(swn, sec) + setattr(swn._swmmio_model.inp, sec, df) + + swn._swmmio_model.inp.save(filename) + +def read_inpfile(filename): + """ + Create a StormWaterNetworkModel from an SWMM INP file + + Parameters + ---------- + filename : string + Name of the inp file + + Returns + ------- + StormWaterNetworkModel + + """ + swn = wntr.stormwater.network.StormWaterNetworkModel(filename) + + return swn + +def read_rptfile(filename): + """ + Read a SWMM summary report file + + Parameters + ---------- + filename : string + Name of the SWMM summary report file + + Returns + ------- + dict + + """ + report = {} + + rpt_sections = swmmio.utils.text.get_rpt_sections_details(filename) + + for section in rpt_sections: + try: + data = swmmio.utils.dataframes.dataframe_from_rpt(filename, section) + if data.shape[0] > 0: + report[section] = data + except: + pass + + return report + +def read_outfile(filename): + """ + Read a SWMM binary output file + + Parameters + ---------- + filename : string + Name of the SWMM binary output file + + Returns + ------- + SimulationResults + + """ + results = SimulationResults() + + # Node results = INVERT_DEPTH, HYDRAULIC_HEAD, PONDED_VOLUME, + # LATERAL_INFLOW, TOTAL_INFLOW, FLOODING_LOSSES, POLLUT_CONC_0 + results.node = {} + + # Link results = FLOW_RATE, FLOW_DEPTH, FLOW_VELOCITY, FLOW_VOLUME, + # CAPACITY, POLLUT_CONC_0 + results.link = {} + + # Subcatchment results = RAINFALL, SNOW_DEPTH, EVAP_LOSS, INFIL_LOSS, + # RUNOFF_RATE, GW_OUTFLOW_RATE, GW_TABLE_ELEV, SOIL_MOISTURE, + # POLLUT_CONC_0 + results.subcatch = {} + + # System results = AIR_TEMP, RAINFALL, SNOW_DEPTH, EVAP_INFIL_LOSS, + # RUNOFF_FLOW, DRY_WEATHER_INFLOW, GW_INFLOW, RDII_INFLOW, DIRECT_INFLOW, + # TOTAL_LATERAL_INFLOW, FLOOD_LOSSES, OUTFALL_FLOWS, VOLUME_STORED, + # EVAP_RATE + results.system = {} + + with pyswmm.Output(filename) as out: + times = out.times + + for attribute in swmm.toolkit.shared_enum.NodeAttribute: + temp = {} + for node_name in out.nodes.keys(): + ts = out.node_series(node_name, attribute) + temp[node_name] = ts.values() + results.node[attribute.name] = pd.DataFrame(data=temp, index=times) + + for attribute in swmm.toolkit.shared_enum.LinkAttribute: + temp = {} + for link_name in out.links.keys(): + ts = out.link_series(link_name, attribute) + temp[link_name] = ts.values() + results.link[attribute.name] = pd.DataFrame(data=temp, index=times) + + for attribute in swmm.toolkit.shared_enum.SubcatchAttribute: + temp = {} + for subcatch_name in out.subcatchments.keys(): + ts = out.subcatch_series(subcatch_name, attribute) + temp[subcatch_name] = ts.values() + results.subcatch[attribute.name] = pd.DataFrame(data=temp, index=times) + + for attribute in swmm.toolkit.shared_enum.SystemAttribute: + ts = out.system_series(attribute) + temp[attribute] = ts.values() + results.system = pd.DataFrame(data=temp, index=times) + + return results + +def write_geojson(swn, prefix: str, crs=None): + """ + Write the StormWaterNetworkModel to a set of GeoJSON files, one file for each + network element. + + Parameters + ---------- + swn : wntr StormWaterNetworkModel + Storm water network model + prefix : str + File prefix + crs : str, optional + Coordinate reference system, by default None + + """ + swn_gis = swn.to_gis(crs) + swn_gis.write_geojson(prefix=prefix) diff --git a/wntr/stormwater/metrics.py b/wntr/stormwater/metrics.py new file mode 100644 index 000000000..35e88739d --- /dev/null +++ b/wntr/stormwater/metrics.py @@ -0,0 +1,410 @@ +""" +The wntr.stormwater.metrics module includes methods to compute +topographic and hydraulic metrics. +""" +import networkx as nx +import pandas as pd + +from wntr.metrics.topographic import * +from wntr.stormwater.network import StormWaterNetworkModel + +def headloss(head, swn, link_names=None): + """ + Headloss across links [ft or m] + + Parameters + ------------ + head : pandas DataFrame + Head values at nodes, from simulation results + (index = times, columns = node names) + swn : wntr StormWaterNetworkModel + Stormwater network model, used to extract start and end + nodes from links. + link_names : list of strings (optional, default = swn.link_name_list) + List of link names + + Returns + -------- + pandas DataFrame with headloss in feet (US Customary units) or meters + (SI Units) (index = times, columns = link names) + + """ + assert isinstance(head, pd.DataFrame) + assert isinstance(swn, StormWaterNetworkModel) + assert isinstance(link_names, (type(None), list)) + + if link_names is None: + link_names = swn.link_name_list + + time = head.index + headloss = pd.DataFrame(data=None, index=time, columns=link_names) + + for name in link_names: + link = swn.get_link(name) + start_node = link.start_node_name + end_node = link.end_node_name + start_head = head.loc[:, start_node] + end_head = head.loc[:, end_node] + headloss.loc[:, name] = end_head - start_head + + return headloss + +def pump_power(flowrate, headloss, flow_units, efficiency=100): + """ + Pump power [kW] + + Parameters + ------------ + flowrate : pandas DataFrame + Pump flowrate, from simulation results + (index = times, columns = pump names) + headloss : pandas DataFrame + Pump headloss, from simulation results (see `headloss` function) + (index = times, columns = pump names) + flow_units : str + Stormwater network model flow units + efficiency : float (optional, default = 100) + Pump efficiency + + Returns + -------- + pandas DataFrame with pump power in kW + (index = times, columns = pump names) + + """ + assert isinstance(flowrate, pd.DataFrame) + assert isinstance(headloss, pd.DataFrame) + assert isinstance(flow_units, str) + assert flow_units in ['CFS', 'GPM', 'MGD', 'CMS', 'LPS', 'MLD'] + assert isinstance(efficiency, (int, float)) + assert 0 <= efficiency <= 100 + + # Convert headloss to meters + if flow_units in ['CFS', 'GPM ', 'MGD']: + headloss = headloss*0.3048 + + # Convert all flow units to CMS + if flow_units == 'CFS': + flowrate = flowrate*(0.3048**3) + elif flow_units == 'GPM': + flowrate = flowrate*15850.3 + elif flow_units == 'MGD': + flowrate = flowrate*(15850.3/(24*60*1e6)) + elif flow_units == 'LPS': + flowrate = flowrate*0.001 + elif flow_units == 'MLD': + flowrate = flowrate*(0.001/(24*3600*1e6)) + + power_W = 1000.0 * 9.81 * headloss * flowrate / (efficiency/100) # Watts = J/s + power_kW = power_W/1000 + + return power_kW + +def pump_energy(flowrate, headloss, flow_units, efficiency=100): + """ + Pump energy use [kW-hr] + + Parameters + ------------ + flowrate : pandas DataFrame + Pump flowrate, from simulation results + (index = times, columns = pump names) + headloss : pandas DataFrame + Pump headloss, from simulation results (see `headloss` function) + (index = times, columns = pump names) + flow_units : str + Stormwater network model flow units + efficiency : float (optional, default = 100) + Pump efficiency + + Returns + -------- + pandas DataFrame with pump energy in kW-hr + (index = times, columns = pump names) + + """ + assert isinstance(flowrate, pd.DataFrame) + assert isinstance(headloss, pd.DataFrame) + assert isinstance(flow_units, str) + assert flow_units in ['CFS', 'GPM', 'MGD', 'CMS', 'LPS', 'MLD'] + assert isinstance(efficiency, (int, float)) + assert 0 <= efficiency <= 100 + + power_kW = pump_power(flowrate, headloss, flow_units, efficiency) + time_delta = flowrate.index[1] - flowrate.index[0] + time_hrs = time_delta.seconds/3600 + + energy_kW_hr = power_kW * time_hrs # kW*hr + return energy_kW_hr + +def conduit_available_volume(volume, capacity, threshold=1): + """ + Conduit available volume, up to a capacity threshold [ft^3 or m^3]. + A returned value of NaN indicates that the conduit has exceeded the + capacity threshold. + + Parameters + ------------ + volume : pandas Series + Conduit volume, see `swn.conduit_volume (index = conduit names) + capacity : pandas DataFrame + Conduit capacity, from simulation results + (index = times, columns = conduit names) + threshold : float (optional, default = 1) + Capacity threshold + + Returns + ------- + pandas DataFrame with conduit available volume in ft^3 or m^3 + (index = times, columns = conduit names) + + """ + assert isinstance(volume, pd.Series) + assert isinstance(capacity, (pd.Series, pd.DataFrame)) + assert isinstance(threshold, (int, float)) + assert 0 <= threshold <= 1 + + available_volume = volume*(threshold - capacity) + available_volume[available_volume < 0] = None + + return available_volume + +def conduit_travel_time(length, velocity): + """ + Conduit travel time [s], computed as length divided by velocity + + Parameters + ---------- + length : pandas Series + Conduit length + velocity : pandas DataFrame + Conduit velocity, from simulation results + (index = times, columns = conduit names) + + Returns + ------- + pandas DataFrame with conduit travel time + (index = times, columns = conduit names) + + """ + assert isinstance(length, pd.Series) + assert isinstance(velocity, (pd.Series, pd.DataFrame)) + + travel_time = length/velocity + + return travel_time + + +def conduit_time_to_capacity(volume, flowrate, flow_units, connected=False): + """ + Conduit time to capacity [s] based on a steady state flowrate, computed as + volume divided by flowrate or total volume / max flowrate, + depending on the connected input parameter. + + This function can also use steady state available volume in place of + volume to consider conduits that are not empty, see + `conduit_available_volume` function. + + Parameters + ------------ + volume : pandas Series + Conduit volume (or steady state available volume) in ft^3 or m^3, + (index = conduit names) + flowrate : pandas Series + Steady state conduit flowrate, from simulation results + (index = conduit names) + flow_units : str + Stormwater network model flow units + connected : bool (default = False) + If connected = False, each conduit is treated individually and + time_to_capacity = available_volume/flowrate + If connected = True, the collection of conduits is considered an + isolated connected system and + time_to_capacity = sum(available_volume)/max(flowrate) + + Returns + ------- + pandas Series (if connected = False) or single value + (if connected = True) with the time to capacity + + """ + assert isinstance(volume, pd.Series) + assert isinstance(flowrate, pd.Series) + assert isinstance(flow_units, str) + assert flow_units in ['CFS', 'GPM', 'MGD', 'CMS', 'LPS', 'MLD'] + assert isinstance(connected, bool) + + # Convert flowrate + if flow_units == 'GPM': # convert to CFS + cflowrate = flowrate*(1/7.48052)*(1/60) + elif flow_units == 'MGD': # convert to CFS + cflowrate = (flowrate*1E6)*(1/7.48052)*(1/86400) + elif flow_units == 'LPS': # convert to CMS + cflowrate = flowrate*(1/1000) + elif flow_units == 'MLD': # convert to CMS + cflowrate = (flowrate*1E6)*(1/1000)*(1/86400) + else: + cflowrate = flowrate # CFS or CMS + + if connected: + time_to_capacity = volume.sum()/cflowrate.max() + else: + time_to_capacity = volume/cflowrate + + return time_to_capacity + +def shortest_path_nodes(G, source_node, target_node): + """ + Nodes along the shortest path from a source to target node + + Parameters + ------------ + G : networkX MultiDiGraph + Graph, directed using steady state flow direction + source_node : str + Source node name + target_node : str + Target node name + + Returns + -------- + List of node names in the path (including the source and target node) + + """ + assert isinstance(G, nx.MultiDiGraph) + assert isinstance(source_node, str) + assert isinstance(target_node, str) + + assert nx.has_path(G, source_node, target_node), \ + "No path between " + source_node + " and " + target_node + + node_list = nx.shortest_path(G, source_node, target_node) + return node_list + +def shortest_path_edges(G, source_node, target_node): + """ + Edges along the shortest path from a source to target node + + Parameters + ------------ + G : networkX MultiDiGraph + Graph, directed using steady state flow direction + source_node : str + Source node name + target_node : str + Target node name + + Returns + -------- + List of edge names in the path + + """ + assert isinstance(G, nx.MultiDiGraph) + assert isinstance(source_node, str) + assert isinstance(target_node, str) + + node_list = shortest_path_nodes(G, source_node, target_node) + edge_list = [set(G[u][v]) for u,v in zip(node_list, node_list[1:])] + edge_list = list(set().union(*edge_list)) + return edge_list + +def upstream_nodes(G, source_node): + """ + Upstream nodes from a source node + + Parameters + ------------ + G : networkX MultiDiGraph + Graph, directed using steady state flow direction + source_node : str + Source node name + + Returns + -------- + List of upstream node names (including the source node) + + """ + assert isinstance(G, nx.MultiDiGraph) + assert isinstance(source_node, str) + + nodes = list(nx.traversal.bfs_tree(G, source_node, reverse=True)) + return nodes + +def upstream_edges(G, source_node): + """ + Upstream edges from a source node + + Parameters + ------------ + G : networkX MultiDiGraph + Graph, directed using steady state flow direction + source_node : str + Source node name + + Returns + -------- + List of upstream edge names + + """ + assert isinstance(G, nx.MultiDiGraph) + assert isinstance(source_node, str) + + # NOTE: 'edge_bfs' yields edges even if they extend back to an already + # explored node while 'bfs_edges' yields the edges of the tree that results + # from a breadth-first-search (BFS) so no edges are reported if they extend + # to already explored nodes. AND Extracting edge names from u,v is slower + # edge_uv = list(nx.traversal.bfs_edges(G, node, reverse=False)) + # uG = G.to_undirected() + # edges = [] + # for u,v in edge_uv: + # edges.extend(list(uG[u][v].keys())) + edge_uvko = list(nx.traversal.edge_bfs(G, source_node, + orientation='reverse')) + edges = [k for u,v,k,orentation in edge_uvko] + return edges + +def downstream_nodes(G, source_node): + """ + Downstream nodes from a source node + + Parameters + ------------ + G : networkX MultiDiGraph + Graph, directed using steady state flow direction + source_node : str + Source node name + + Returns + -------- + List of downstream node names (including the source node) + + """ + assert isinstance(G, nx.MultiDiGraph) + assert isinstance(source_node, str) + + nodes = list(nx.traversal.bfs_tree(G, source_node, reverse=False)) + return nodes + +def downstream_edges(G, source_node): + """ + Downstream edges from a source node + + Parameters + ------------ + G : networkX MultiDiGraph + Graph, directed using steady state flow direction + source_node : str + Source node name + + Returns + -------- + List of downstream edge names + + """ + assert isinstance(G, nx.MultiDiGraph) + assert isinstance(source_node, str) + + edge_uvko = list(nx.traversal.edge_bfs(G, source_node, + orientation='original')) + edges = [k for u,v,k,orentation in edge_uvko] + return edges diff --git a/wntr/stormwater/network.py b/wntr/stormwater/network.py new file mode 100644 index 000000000..b0cbc7d05 --- /dev/null +++ b/wntr/stormwater/network.py @@ -0,0 +1,847 @@ +""" +The wntr.stormwater.network module includes methods to define +stormwater/wastewater network models. +""" +import logging +import random +import numpy as np +import pandas as pd +import networkx as nx +import warnings +import os + +try: + import swmmio + has_swmmio = True +except ModuleNotFoundError: + swmmio = None + has_swmmio = False + +from wntr.stormwater.io import to_graph, to_gis + +logger = logging.getLogger(__name__) + + +class StormWaterNetworkModel(object): + """ + Storm water network model class. + + Unlike the WaterNetworkModel, this class has no iterator methods, + add/remove methods, and no component registries. + + Parameters + ------------------- + inp_file_name: string + Directory and filename of SWMM inp file to load into the + StormWaterNetworkModel object. + + """ + + def __init__(self, inp_file_name): + + if not has_swmmio: + raise ModuleNotFoundError('swmmio is required') + + from swmmio.defs import INP_OBJECTS + from swmmio.utils.text import get_inp_sections_details + import shutil + + headers = get_inp_sections_details(inp_file_name, include_brackets=False) + missing_headers = set(INP_OBJECTS.keys()) - set(headers.keys()) + appended_text = "" + for missing_header in missing_headers: + appended_text = appended_text + "[" + missing_header + "]\n\n" + + shutil.copyfile(inp_file_name, 'temp.inp') + with open("temp.inp", "a") as f: + f.write(appended_text) + + self._swmmio_model = swmmio.Model("temp.inp", include_rpt=False) + + # See https://github.com/pyswmm/swmmio/issues/57 for a list of supported INP file sections + + # Attributes of StormWaterNetworkModel link to attributes + # in swmmio.Model.inp, which contains dataframes from an INP file. + # The swmmio.Model.inp object also includes a .save method to + # write a new INP file. + + # Nodes = Junctions, outfall, and storage nodes + # Links = Conduits, weirs, orifices, and pumps + + # Sections that are commented out are not currently supported by swntr + + self.section_names = [ + # Options + 'options', + 'report', + 'files', + + # Climate + 'raingages', + 'evaporation', + 'subcatchments', + 'subareas', + 'infiltration', + + # Hydraulics + 'junctions', + 'outfalls', + 'storage', + 'conduits', + 'pumps', + 'orifices', + 'weirs', + 'xsections', + 'streets', + 'inlets', + 'inlet_usage', + 'controls', + + # Quality + 'pollutants', + 'landuses', + 'coverages', + 'buildup', + 'washoff', + 'inflows', + 'dwf', + + # Curves, timeseries, patterns + 'curves', + 'timeseries', + 'patterns', + + # Map + 'polygons', + 'coordinates', + 'vertices', + 'tags', + + # Not included or empty in INP test files (see "untested" in tests) + 'hydrographs', + 'loadings', + 'groundwater', + 'aquifers', + 'losses', + 'dividers', + 'lid_usage', + 'rdii', + + # Not supported by swmmio model.inp + #'title', + #'temperature', + #'adjustments', + #'lid_controls', + #'gwf', + #'snowpacks', + #'outlets', + #'transects', + #'treatment', + #'map', + #'labels', + #'symbols', + #'backdrop', + ] + for sec in self.section_names: + df = getattr(self._swmmio_model.inp, sec) + setattr(self, sec, df.copy()) + + # Reset inp file path and remove temp file + self._swmmio_model.inp.path = inp_file_name + os.remove("temp.inp") + + def describe(self): + """ + Describe number of components in the network model + + Returns + ------- + A pandas Series with component counts + """ + d = {} + for sec in self.section_names: + df = getattr(self, sec) + d[sec] = df.shape[0] + + return pd.Series(d) + + @property + def nodes(self): + """Nodes database (read only)""" + df_list = [self.junctions, + self.outfalls, + self.storage] + df = pd.concat([df for df in df_list if not df.empty]) + + df.loc[df.index.isin(self.junction_name_list), 'Type'] = 'Junction' + df.loc[df.index.isin(self.outfall_name_list), 'Type'] = 'Outfalls' + df.loc[df.index.isin(self.storage_name_list), 'Type'] = 'Storage' + + return df + + @property + def links(self): + """Links database (read only)""" + df_list = [self.conduits, + self.weirs, + self.orifices, + self.pumps] + df = pd.concat([df for df in df_list if not df.empty]) + + df.loc[df.index.isin(self.conduit_name_list), 'Type'] = 'Conduit' + df.loc[df.index.isin(self.weir_name_list), 'Type'] = 'Weir' + df.loc[df.index.isin(self.orifice_name_list), 'Type'] = 'Orifice' + df.loc[df.index.isin(self.pump_name_list), 'Type'] = 'Pump' + + return df + + @property + def node_name_list(self): + """List of node names""" + return list(self.nodes.index) + + @property + def junction_name_list(self): + """List of junction names""" + return list(self.junctions.index) + + @property + def outfall_name_list(self): + """List of outfall names""" + return list(self.outfalls.index) + + @property + def storage_name_list(self): + """List of storage names""" + return list(self.storage.index) + + @property + def link_name_list(self): + """List of link names""" + return list(self.links.index) + + @property + def conduit_name_list(self): + """List of conduit names""" + return list(self.conduits.index) + + @property + def weir_name_list(self): + """List of weir names""" + return list(self.weirs.index) + + @property + def orifice_name_list(self): + """List of orifice names""" + return list(self.orifices.index) + + @property + def pump_name_list(self): + """List of pump names""" + return list(self.pumps.index) + + @property + def subcatchment_name_list(self): + """List of subcatchment names""" + return list(self.subcatchments.index) + + @property + def raingage_name_list(self): + """List of raingage names""" + return list(self.raingages.index) + + @property + def timeseries_name_list(self): + """List of timeseries names""" + return list(self.timeseries.index.unique()) + + @property + def controls_name_list(self): + """List of control names""" + return list(self.controls.index) + + def get_node(self, name): + """ + Get a specific node + + Parameters + ---------- + name : str + The node name + + Returns + ------- + Node + + """ + return Node(name, self.nodes.loc[name,:]) + + def get_link(self, name): + """ + Get a specific link + + Parameters + ---------- + name : str + The link name + + Returns + ------- + Link + + """ + return Link(name, self.links.loc[name,:]) + + @property + def conduit_cross_section(self): + """ + Cross section area of each conduit, according to the geometric + parameters stored in xsections + + Returns + ------- + pandas Series with conduit cross section + + """ + cross_section = {} + + for link_name in self.conduit_name_list: + + geom1 = self.xsections.loc[link_name, 'Geom1'] + geom2 = self.xsections.loc[link_name, 'Geom2'] + geom3 = self.xsections.loc[link_name, 'Geom3'] + geom4 = self.xsections.loc[link_name, 'Geom4'] + shape = self.xsections.loc[link_name, 'Shape'] + + if shape == "CIRCULAR": + d = geom1 # diameter + r = d/2 # radius + + area = np.pi*(r**2) + + elif shape == "FILLED_CIRCULAR": + d = geom1 # diameter + r = d/2 # radius + h = geom2 # height + + circular_area = np.pi*(r**2) + filled_area = 0.5*(r**2)*(2*np.arccos((r-h)/r))-np.sin((2*np.arccos((r-h)/r))) + area = circular_area - filled_area + + elif shape == "RECT_CLOSED": + h = geom1 # height + b = geom2 # base (width) + + area = h*b + + elif shape == "RECT_OPEN": + h = geom1 # height + b = geom2 # base (width) + + area = h*b + + elif shape == "TRAPEZOIDAL": + h = geom1 # height + b = geom2 # width + p1 = geom3 # pitch, side 1 + p2 = geom4 # pitch, side 2 + + area = 0.5*h*(h + (b + np.sqrt((p1**2) - h**2) + np.sqrt((p2**2) - h**2))) + + elif shape == "TRIANGULAR": + area = (0.5*geom1*geom2) + + elif shape == "HORIZ_ELLIPSE": + area = np.pi*(geom1*0.5)*(geom2*0.5) + + elif shape == "VERT_ELLIPSE": + area = np.pi*(geom1*0.5)*(geom2*0.5) + + elif shape == "PARABOLIC": + area = (2/3)*geom1*geom2 + + elif shape == "RECT_TRIANGULAR": + area = (geom1*geom2)+(0.5*geom3*geom2) + + else: # ARCH, POWER, CUSTOM + area = None + warnings.warn(shape + ' not yet implemented') + + cross_section[link_name] = area + + return pd.Series(cross_section) + + @property + def conduit_volume(self): + """ + Volume of each conduit, according to the geometric + parameters stored in xsections and length + + Returns + ------- + pandas Series with conduit volume + + """ + cross_section = self.conduit_cross_section + length =self.conduits['Length'] + volume = cross_section*length + + return volume + + def to_gis(self, crs=None): + """ + Convert a StormWaterNetworkModel into GeoDataFrames + + Parameters + ---------- + crs : str, optional + Coordinate reference system, by default None + + """ + return to_gis(self, crs) + + def to_graph(self, node_weight=None, link_weight=None, modify_direction=False): + """ + Convert a StormWaterNetworkModel into a networkx MultiDiGraph + + Returns + -------- + networkx MultiDiGraph + + """ + return to_graph(self, node_weight, link_weight, modify_direction) + + def timeseries_to_datetime_format(self): + """ + Reformat "Date, Time, Value" timeseries to a datetime indexed DataFrame + + Returns + ------- + pandas DataFrame, with one column per timeseries + + """ + assert set(self.timeseries.columns) == set(['Date', 'Time', 'Value']) + + start_date_value = self.options.loc['START_DATE','Value'] + + ts = {} + for name, timeseries in self.timeseries.groupby('Name'): + timeseries.loc[timeseries['Date'].isna(), 'Date'] = start_date_value + datestr = timeseries['Date'] + ' ' + timeseries['Time'] + datetime = pd.DatetimeIndex(datestr) + value = timeseries['Value'].astype(float).values + ts[name] = pd.Series(data=value, index=datetime) + + ts = pd.DataFrame(ts) + + return ts + + def patterns_to_datetime_format(self, names=None): + """ + Reformat "Type, Factor1, Factor2, ..." pattern to a datetime + indexed DataFrame + + Parameters + ---------- + names : list of str (optional, defaults to all patterns) + List of pattern names + + Returns + ------- + pandas DataFrame, with one column per pattern + + """ + if names is None: + patterns = self.patterns.copy() + else: + patterns = self.patterns.loc[names,:] + + # Check to make sure there is one type of pattern + # (MONTHLY, DAILY, HOURLY, WEEKEND) + """ + The MONTHLY format is used to set monthly pattern factors for dry + weather flow constituents. + The DAILY format is used to set dry weather pattern factors for each + day of the week, where Sunday is day 1. + The HOURLY format is used to set dry weather factors for each hour + of the day starting from midnight. + If these factors are different for weekend days than for weekday days + then the WEEKEND format can be used to specify hourly adjustment + factors just for weekends. + """ + assert len(patterns['Type'].unique()) == 1 + + mask = patterns.columns.str.contains('Factor') + factor_cols = self.patterns.columns[mask] + factors = patterns[factor_cols].T + + if patterns.iloc[0]['Type'] == 'MONTHLY': + #factors.index = pd.to_timedelta(np.arange(12), unit="MS") # month start? + raise NotImplementedError + elif patterns.iloc[0]['Type'] == 'DAILY': + factors.index = pd.to_timedelta(np.arange(7), unit="D") + elif (patterns.iloc[0]['Type'] == 'HOURLY') or (patterns.iloc[0]['Type'] == 'WEEKEND'): + factors.index = pd.to_timedelta(np.arange(24), unit="H") + + return factors + + def add_datetime_indexed_timeseries(self, ts, update_model=True): + """ + Add datetime indexed timeseries data to the model + + Note, if you have timeseries data in "Date, Time, Value" format, + you can update/modify the swn.timeseries DataFrame directly. + + Parameters + ---------- + ts : pandas DataFrame + Datetime indexed timeseries data + update_model : Bool (optional, default = True) + Flag indicating if the timeseries are added to the + model. If False, the timeseries are converted to swn format, but + not used to update the model. + + Returns + ------- + pandas DataFrame with timeseries in swn model format + + """ + assert isinstance(ts, pd.DataFrame) + assert isinstance(update_model, bool) + + timeseries = ts.unstack().reset_index() + timeseries = timeseries.set_index('level_1') + timeseries['Date'] = timeseries.index.strftime('%m/%d/%Y') + timeseries['Time'] = timeseries.index.strftime('%H:%M') + timeseries = timeseries.set_index('level_0') + timeseries.index.name = 'Name' + timeseries.rename(columns={0: 'Value'}, inplace=True) + timeseries = timeseries[['Date', 'Time', 'Value']] + + if update_model: + self.timeseries = pd.concat([self.timeseries, timeseries], axis=0) + + return timeseries + + def add_datetime_indexed_patterns(self, ts, update_model=True): + """ + Add datetime indexed pattern data to the model + + Note, if you have pattern data in "Type, Factor1, Factor2, ..." format, + you can update/modify the swn.patterns DataFrame directly. + + Parameters + ---------- + ts : pandas DataFrame + Datetime indexed patterns data + update_model : Bool (optional, default = True) + Flag indicating if the patterns are added to the + model. If False, the patterns are converted to swn format, but + not used to update the model. + + Returns + ------- + pandas DataFrame with patterns in swn model format + + """ + assert isinstance(ts, pd.DataFrame) + assert isinstance(update_model, bool) + + t_delta = ts.index.diff()[1] + if t_delta == pd.Timedelta('0 days 01:00:00'): + type_str = 'HOURLY' + elif t_delta == pd.Timedelta('1 days 00:00:00'): + type_str = 'DAILY' + else: # Monthly + raise NotImplementedError + + ts.index = ['Factor'+str(i+1) for i in range(ts.shape[0])] + patterns = ts.T + patterns['Type'] = type_str + + if update_model: + self.patterns = pd.concat([self.patterns, patterns], axis=0) + + return patterns + + def add_pump_outage_control(self, pump_name, start_time, end_time=None, + priority=4, control_suffix="_outage", + update_model=True): + """ + Add a pump outage rule to the stormwater network model. + + Parameters + ---------- + pump_name : str + Name of the pump + start_time : int + The time at which the outage starts in decimal hours + end_time : int + The time at which the outage stops in decimal hours + priority : int + The outage rule priority, default = 4 (highest priority) + control_suffix : str (optional, default = "_outage") + Control name suffix, appended to the pump name + update_model : Bool (optional, default = True) + Flag indicating if the control is added to the + model. If False, the control string is returned, but + not used to update the model. + """ + assert pump_name in self.pump_name_list + + rule_name = 'RULE ' + pump_name + control_suffix + + rule = "IF SIMULATION TIME > " + str(start_time) + " " + if end_time is not None: + rule = rule + "AND SIMULATION TIME < "+str(end_time) + " " + rule = rule + "THEN PUMP "+pump_name+" status = OFF " + rule = rule + "ELSE PUMP "+pump_name+" status = ON " + rule = rule + "PRIORITY "+str(priority) + + if update_model: + self.controls.loc[rule_name, 'Control'] = rule + + return rule + + def add_composite_patterns(self, data, pattern_suffix="_composite", + update_model=True): + """ + Add composite dry weather flow (DWF) and composite patterns to the + model, computed from data that contains multiple base values and + pattern names per node. + + Composite DWFs override existing DWF (see `swn.dwf`). + + Composite pattern values (Factor1, Factor2, ...) + have a mean value of 1 and are appended to existing patterns. Patterns + are named + pattern_suffix (see `swn.patterns`). + + Parameters + ---------- + data : pd.DataFrame + Data containing multiple base and pattern names per node. + DataFrame columns = ['Node', 'Base', 'Pattern'] where + the Node column contains node names, + the Base column contains base values for mean flow, and + the Pattern column contains pattern names that already exist in the + model (swn). + pattern_suffix : string (optional, default = "_composite") + Pattern name suffix, appended to the node name + update_model : Bool (optional, default = True) + Flag indicating if the composite DWF and Patterns are added to the + model. If False, the composite base and patterns are returned, but + not used to update the model. + + Returns + ------- + pd.DataFrame with one base and pattern value per node. + + """ + mask = self.patterns.columns.str.contains('Factor') + factor_cols = self.patterns.columns[mask] + + assert self.patterns.loc[:,factor_cols].isna().sum().sum() == 0, \ + "Composite patterns is not implemented for variable pattern length" + + nodes = data.index.unique() + cols = ['AverageValue'] + cols.extend(list(factor_cols)) + composite = pd.DataFrame(index=nodes, columns=cols) + + for name, group in data.groupby('Node'): + pattern_names = group['Pattern'] + + # Check to make sure there is one type of pattern + # (MONTHLY, DAILY, HOURLY, WEEKEND) + pattern_types = self.patterns.loc[pattern_names, 'Type'] + assert len(pattern_types.unique()) == 1 + + factors = self.patterns.loc[pattern_names, factor_cols].values + base = group['AverageValue'].values + + # Scale each factor by its base value + scaled = np.multiply(factors, np.expand_dims(base, axis=1)) + composite_factors = scaled.sum(axis=0) + + # Assign the composite base to the mean value + composite_base = np.round(composite_factors.mean(), 6) + + # Normalize the composite factos by the mean value + if composite_base > 0: + composite_factors = composite_factors/composite_base + composite_factors = np.around(composite_factors, 6) + + composite.loc[name, 'AverageValue'] = composite_base + composite.loc[name, factor_cols] = composite_factors + + if update_model: + data = composite.copy() + data['TimePatterns'] = data.index + pattern_suffix + + # Override DWF with new composite values + composite_dwf = data[['AverageValue', 'TimePatterns']] + composite_dwf.loc[:,'Parameter'] = 'FLOW' + composite_dwf = composite_dwf[['Parameter', 'AverageValue', 'TimePatterns']] + self.dwf.loc[composite_dwf.index, :] = composite_dwf + + # Concat Patterns with new composite values + composite_patterns = data[factor_cols] + composite_patterns.index = data['TimePatterns'] + composite_patterns.loc[:,'Type'] = 'HOURLY' + concat_patterns = pd.concat([self.patterns, composite_patterns]) + self.patterns = concat_patterns + + return composite + + + def anonymize_coordinates(self, seed=None, dot_graphviz=False, update_model=True): + """ + Anonymize coordinates using a networkX spring layout or graphviz dot layout + + Parameters + ---------- + seed : int + Random seed used to set the spring layout + dot_graphviz : Bool (optional, default = False) + Flag indicating if dot_graphviz is used to anonymize coordinates. + If False, spring layout is used. + update_model : Bool (optional, default = True) + Flag indicating if the model coordinates are updated and vertices + and polygons are removed. If False, the coordinates are returned, + but not used to update the model. + + returns + ------- + pandas DataFrame with coordinates + + """ + G = self.to_graph() + + if dot_graphviz: + pos = nx.nx_agraph.pygraphviz_layout(G, prog="dot", args="-Gmclimit=100") + else: + pos = nx.spring_layout(G, seed=seed) + coordinates = pd.DataFrame(pos).T + coordinates.rename(columns={0: 'X', 1: 'Y'}, inplace=True) + + if update_model: + self.coordinates = coordinates + self.vertices.drop(self.vertices.index, inplace=True) + self.polygons.drop(self.polygons.index, inplace=True) + + return coordinates + + +class Node(object): + """ + Base class for nodes. + """ + + def __init__(self, name, data): + + # Set the node name + self._name = name + self._node_type = data['Type'] + + @property + def name(self): + """str: The name of the node (read only)""" + return self._name + + @property + def node_type(self): + """str: The node type (read only)""" + return self._node_type + +class Link(object): + """ + Base class for links. + """ + + def __init__(self, name, data): + + self._link_name = name + self._start_node_name = data['InletNode'] + self._end_node_name = data['OutletNode'] + self._link_type = data['Type'] + + @property + def name(self): + """str: The link name (read only)""" + return self._link_name + + @property + def start_node_name(self): + """str: The name of the start node (read only)""" + return self._start_node_name + + @property + def end_node_name(self): + """str: The name of the end node (read only)""" + return self._end_node_name + + @property + def link_type(self): + """str: The link type (read only)""" + return self._link_type + + +def generate_valve_layer(swn, placement_type='strategic', n=1, seed=None): + """ + Generate valve layer data, which can be used in valve segmentation analysis. + + Parameters + ----------- + swn : wntr StrormWaterNetworkModel + A StormWaterNetworkModel object + + placement_type : string + Options include 'strategic' and 'random'. + + - If 'strategic', n is the number of pipes from each node that do not + contain a valve. In this case, n is generally 0, 1 or 2 + (i.e. N, N-1, N-2 valve placement). + - If 'random', then n randomly placed valves are used to define the + valve layer. + + n : int + + - If 'strategic', n is the number of pipes from each node that do not + contain a valve. + - If 'random', n is the number of number of randomly placed valves. + + seed : int or None + Random seed + + Returns + --------- + valve_layer : pandas DataFrame + Valve layer, defined by node and link pairs (for example, valve 0 is + on link A and protects node B). The valve_layer DataFrame is indexed by + valve number, with columns named 'node' and 'link'. + + """ + if seed is not None: + np.random.seed(seed) + random.seed(seed) + valve_layer = [] + if placement_type=='random': + s = swn.conduits['InletNode'] + all_valves = list(tuple(zip(s.index,s))) + s = swn.conduits['OutletNode'] + all_valves.extend(tuple(zip(s.index,s))) + for valve_tuple in random.sample(all_valves, n): + pipe_name, node_name = valve_tuple + valve_layer.append([pipe_name, node_name]) + + elif placement_type == 'strategic': + s = pd.concat([swn.conduits['InletNode'], swn.conduits['OutletNode']]) + s = s.to_frame('Node') + for node_name, group in s.groupby('Node'): + links = list(group.index) + for l in np.random.choice(links, max(len(links)-n,0), replace=False): + valve_layer.append([l, node_name]) + + valve_layer = pd.DataFrame(valve_layer, columns=['link', 'node']) + + return valve_layer diff --git a/wntr/stormwater/scenario.py b/wntr/stormwater/scenario.py new file mode 100644 index 000000000..c0f76a088 --- /dev/null +++ b/wntr/stormwater/scenario.py @@ -0,0 +1,5 @@ +""" +The wntr.stormwater.scenario module includes methods to define +fragility/survival curves. +""" +from wntr.scenario import FragilityCurve diff --git a/wntr/stormwater/sim.py b/wntr/stormwater/sim.py new file mode 100644 index 000000000..a1ddfe1c8 --- /dev/null +++ b/wntr/stormwater/sim.py @@ -0,0 +1,80 @@ +""" +The wntr.stormwater.sim module includes methods to simulate +hydraulics. +""" +import os +import subprocess + +try: + import pyswmm + import swmmio + has_swmmio = True +except ModuleNotFoundError: + pyswmm = None + swmmio = None + has_swmmio = False + +from wntr.stormwater.io import write_inpfile, read_outfile, read_rptfile + +os.environ["CONDA_DLL_SEARCH_MODIFICATION_ENABLE"] = "1" +# See https://github.com/OpenWaterAnalytics/pyswmm/issues/298 + + +class SWMMSimulator(object): + """ + SWMM simulator class. + """ + def __init__(self, swn): + self._swn = swn + + def run_sim(self, file_prefix='temp', full_results=True): + """ + Run a SWMM simulation + + Parameters + ---------- + file_prefix : str + Default prefix is "temp". Output files (.out and .rpt) use this prefix + full_results: bool (optional) + If full_results is True, the binary output file and report summary + file are used to extract results. If False, results are only + extracted from report summary file. Default = True. + + Returns + ------- + Simulation results from the binary .out file (default) or summary .rpt file + """ + if not has_swmmio: + raise ModuleNotFoundError('swmmio is required') + + temp_inpfile = file_prefix + '.inp' + if os.path.isfile(temp_inpfile): + os.remove(temp_inpfile) + + temp_outfile = file_prefix + '.out' + if os.path.isfile(temp_outfile): + os.remove(temp_outfile) + + temp_rptfile = file_prefix + '.rpt' + if os.path.isfile(temp_rptfile): + os.remove(temp_rptfile) + + write_inpfile(self._swn, temp_inpfile) + + with pyswmm.Simulation(temp_inpfile) as sim: + for step in sim: + pass + sim.report() + + # The simulation can also be run with swmmio + #p = subprocess.run("python -m swmmio --run " + temp_inpfile) + + if full_results: + results = read_outfile(temp_outfile) + report_summary = read_rptfile(temp_rptfile) + results.report = report_summary + else: + results = SimulationResults() + results.report = report_summary + + return results diff --git a/wntr/tests/networks_for_testing/SWMM_examples/Culvert_Model.inp b/wntr/tests/networks_for_testing/SWMM_examples/Culvert_Model.inp new file mode 100644 index 000000000..2cba9e3e6 --- /dev/null +++ b/wntr/tests/networks_for_testing/SWMM_examples/Culvert_Model.inp @@ -0,0 +1,158 @@ +[TITLE] +;;Project Title/Notes +A culvert model with roadway overtopping. +See Culvert_Model.txt for more details. + +[OPTIONS] +;;Option Value +FLOW_UNITS CFS +INFILTRATION HORTON +FLOW_ROUTING DYNWAVE +LINK_OFFSETS DEPTH +MIN_SLOPE 0 +ALLOW_PONDING NO +SKIP_STEADY_STATE NO + +START_DATE 06/08/2015 +START_TIME 00:00:00 +REPORT_START_DATE 06/08/2015 +REPORT_START_TIME 00:00:00 +END_DATE 06/08/2015 +END_TIME 05:00:00 +SWEEP_START 01/01 +SWEEP_END 12/31 +DRY_DAYS 0 +REPORT_STEP 00:07:30 +WET_STEP 00:05:00 +DRY_STEP 01:00:00 +ROUTING_STEP 0:00:05 +RULE_STEP 00:00:00 + +INERTIAL_DAMPING PARTIAL +NORMAL_FLOW_LIMITED BOTH +FORCE_MAIN_EQUATION H-W +VARIABLE_STEP 0.75 +LENGTHENING_STEP 0 +MIN_SURFAREA 12.557 +MAX_TRIALS 8 +HEAD_TOLERANCE 0.005 +SYS_FLOW_TOL 5 +LAT_FLOW_TOL 5 +MINIMUM_STEP 0.5 +THREADS 1 + +[EVAPORATION] +;;Data Source Parameters +;;-------------- ---------------- +CONSTANT 0.0 +DRY_ONLY NO + +[JUNCTIONS] +;;Name Elevation MaxDepth InitDepth SurDepth Aponded +;;-------------- ---------- ---------- ---------- ---------- ---------- +Outlet 868 0 0 0 0 + +[OUTFALLS] +;;Name Elevation Type Stage Data Gated Route To +;;-------------- ---------- ---------- ---------------- -------- ---------------- +TailWater 858 FIXED 859.5 NO + +[STORAGE] +;;Name Elev. MaxDepth InitDepth Shape Curve Type/Params SurDepth Fevap Psi Ksat IMD +;;-------------- -------- ---------- ----------- ---------- ---------------------------- --------- -------- -------- -------- +Inlet 878 9 0 TABULAR StorageCurve 0 0 + +[CONDUITS] +;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow +;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- +Culvert Inlet Outlet 200 0.014 0 0 0 0 +Channel Outlet TailWater 200 0.03 0 0 0 0 + +[WEIRS] +;;Name From Node To Node Type CrestHt Qcoeff Gated EndCon EndCoeff Surcharge RoadWidth RoadSurf Coeff. Curve +;;-------------- ---------------- ---------------- ------------ ---------- ---------- -------- -------- ---------- ---------- ---------- ---------- ---------------- +Roadway Inlet Outlet ROADWAY 9 3.33 NO 0 0 NO 40 GRAVEL + +[XSECTIONS] +;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert +;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- +Culvert CIRCULAR 3 0 0 0 2 4 +Channel TRAPEZOIDAL 9 10 2 2 1 +Roadway RECT_OPEN 50 200 0 0 + +[INFLOWS] +;;Node Constituent Time Series Type Mfactor Sfactor Baseline Pattern +;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- +Inlet FLOW Inflow FLOW 1.0 1.0 + +[CURVES] +;;Name Type X-Value Y-Value +;;-------------- ---------- ---------- ---------- +StorageCurve Storage 0 0 +StorageCurve 2 9583 +StorageCurve 4 33977 +StorageCurve 6 72310 +StorageCurve 8 136778 + +[TIMESERIES] +;;Name Date Time Value +;;-------------- ---------- ---------- ---------- +Inflow 0 0 +Inflow .125 9 +Inflow .25 10 +Inflow .375 11 +Inflow .5 13 +Inflow .625 17 +Inflow .75 28 +Inflow .875 40 +Inflow 1 80 +Inflow 1.125 136 +Inflow 1.25 190 +Inflow 1.375 220 +Inflow 1.5 220 +Inflow 1.625 201 +Inflow 1.75 170 +Inflow 1.875 140 +Inflow 2 120 +Inflow 2.125 98 +Inflow 2.25 82 +Inflow 2.375 70 +Inflow 2.5 60 +Inflow 2.625 53 +Inflow 2.75 47 +Inflow 2.875 41 + +[REPORT] +;;Reporting Options +SUBCATCHMENTS ALL +NODES ALL +LINKS ALL + +[TAGS] + +[MAP] +DIMENSIONS -59.264 5535.422 7089.237 6491.928 +Units None + +[COORDINATES] +;;Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +Outlet 4206.542 6357.994 +TailWater 6019.146 6168.726 +Inlet 1628.472 6476.537 + +[VERTICES] +;;Link X-Coord Y-Coord +;;-------------- ------------------ ------------------ +Roadway 2311.068 7166.633 +Roadway 3762.491 7151.463 + +[Polygons] +;;Storage Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +Inlet 1628.472 6476.537 + +[LABELS] +;;X-Coord Y-Coord Label +1651.426 7755.664 "Circular Culvert with Roadway Overtopping and Upstream Storage" "" "Arial" 12 1 1 + diff --git a/wntr/tests/networks_for_testing/SWMM_examples/Detention_Pond_Model.inp b/wntr/tests/networks_for_testing/SWMM_examples/Detention_Pond_Model.inp new file mode 100644 index 000000000..2cc3ef954 --- /dev/null +++ b/wntr/tests/networks_for_testing/SWMM_examples/Detention_Pond_Model.inp @@ -0,0 +1,501 @@ +[TITLE] +;;Project Title/Notes +A detention pond model. +See Detention_Pond_Model.txt for more details. + +[OPTIONS] +;;Option Value +FLOW_UNITS CFS +INFILTRATION HORTON +FLOW_ROUTING KINWAVE +LINK_OFFSETS DEPTH +MIN_SLOPE 0 +ALLOW_PONDING NO +SKIP_STEADY_STATE NO + +START_DATE 01/01/2007 +START_TIME 00:00:00 +REPORT_START_DATE 01/01/2007 +REPORT_START_TIME 00:00:00 +END_DATE 01/01/2007 +END_TIME 12:00:00 +SWEEP_START 01/01 +SWEEP_END 12/31 +DRY_DAYS 0 +REPORT_STEP 00:05:00 +WET_STEP 00:01:00 +DRY_STEP 01:00:00 +ROUTING_STEP 0:00:15 +RULE_STEP 00:00:00 + +INERTIAL_DAMPING FULL +NORMAL_FLOW_LIMITED BOTH +FORCE_MAIN_EQUATION H-W +VARIABLE_STEP 0.75 +LENGTHENING_STEP 0 +MIN_SURFAREA 12.557 +MAX_TRIALS 8 +HEAD_TOLERANCE 0.005 +SYS_FLOW_TOL 5 +LAT_FLOW_TOL 5 +MINIMUM_STEP 0.5 +THREADS 1 + +[EVAPORATION] +;;Data Source Parameters +;;-------------- ---------------- +CONSTANT 0.0 +DRY_ONLY NO + +[RAINGAGES] +;;Name Format Interval SCF Source +;;-------------- --------- ------ ------ ---------- +RainGage INTENSITY 0:05 1.0 TIMESERIES 2-yr + +[SUBCATCHMENTS] +;;Name Rain Gage Outlet Area %Imperv Width %Slope CurbLen SnowPack +;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- ---------------- +S1 RainGage J1 4.55 56.8 1587 2 0 +S2 RainGage J2 4.74 63.0 1653 2 0 +S3 RainGage J3 3.74 39.5 1456 3.1 0 +S4 RainGage J7 6.79 49.9 2331 3.1 0 +S5 RainGage J10 4.79 87.7 1670 2 0 +S6 RainGage J11 1.98 95.0 690 2 0 +S7 RainGage J10 2.33 0.0 907 3.1 0 +PreDevelop RainGage PreDevelop 28.94 5 2521 0.5 0 + +[SUBAREAS] +;;Subcatchment N-Imperv N-Perv S-Imperv S-Perv PctZero RouteTo PctRouted +;;-------------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- +S1 0.015 0.24 0.06 0.3 25 OUTLET +S2 0.015 0.24 0.06 0.3 25 OUTLET +S3 0.015 0.24 0.06 0.3 25 OUTLET +S4 0.015 0.24 0.06 0.3 25 OUTLET +S5 0.015 0.24 0.06 0.3 25 OUTLET +S6 0.015 0.24 0.06 0.3 25 OUTLET +S7 0.015 0.24 0.06 0.3 25 OUTLET +PreDevelop 0.015 0.24 0.06 0.3 25 OUTLET + +[INFILTRATION] +;;Subcatchment Param1 Param2 Param3 Param4 Param5 +;;-------------- ---------- ---------- ---------- ---------- ---------- +S1 4.5 0.2 6.5 7 0 +S2 4.5 0.2 6.5 7 0 +S3 4.5 0.2 6.5 7 0 +S4 4.5 0.2 6.5 7 0 +S5 4.5 0.2 6.5 7 0 +S6 4.5 0.2 6.5 7 0 +S7 4.5 0.2 6.5 7 0 +PreDevelop 4.5 0.2 6.5 7 0 + +[JUNCTIONS] +;;Name Elevation MaxDepth InitDepth SurDepth Aponded +;;-------------- ---------- ---------- ---------- ---------- ---------- +J1 4973 0 0 0 0 +J2 4969 0 0 0 0 +J3 4973 0 0 0 0 +J4 4971 0 0 0 0 +J5 4969.8 0 0 0 0 +J6 4969 0 0 0 0 +J7 4971.5 0 0 0 0 +J8 4966.5 0 0 0 0 +J9 4964.8 0 0 0 0 +J10 4963.8 0 0 0 0 +J11 4963 0 0 0 0 +J_out 4956 0 0 0 0 + +[OUTFALLS] +;;Name Elevation Type Stage Data Gated Route To +;;-------------- ---------- ---------- ---------------- -------- ---------------- +O2 4954 FREE NO + +[STORAGE] +;;Name Elev. MaxDepth InitDepth Shape Curve Type/Params SurDepth Fevap Psi Ksat IMD +;;-------------- -------- ---------- ----------- ---------- ---------------------------- --------- -------- -------- -------- +SU1 4956 10 0 PYRAMIDAL 180 90 2 0 0 + +[CONDUITS] +;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow +;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- +C1 J1 J5 185.00 0.05 0 0 0 0 +C2 J2 J11 526.00 0.016 0 4 0 0 +C3 J3 J4 109.00 0.016 0 0 0 0 +C4 J4 J5 133.00 0.05 0 0 0 0 +C5 J5 J6 207.00 0.05 0 0 0 0 +C6 J7 J6 140.00 0.05 0 0 0 0 +C7 J6 J8 95.00 0.016 0 0 0 0 +C8 J8 J9 166.00 0.05 0 0 0 0 +C9 J9 J10 320.00 0.05 0 0 0 0 +C10 J10 J11 145.00 0.05 0 0 0 0 +C11 J11 SU1 150 0.016 0 1 0 0 +C_out J_out O2 100 0.01 0 0 0 0 + +[ORIFICES] +;;Name From Node To Node Type Offset Qcoeff Gated CloseTime +;;-------------- ---------------- ---------------- ------------ ---------- ---------- -------- ---------- +O1 SU1 J_out SIDE 0 0.65 NO 0 + +[WEIRS] +;;Name From Node To Node Type CrestHt Qcoeff Gated EndCon EndCoeff Surcharge RoadWidth RoadSurf Coeff. Curve +;;-------------- ---------------- ---------------- ------------ ---------- ---------- -------- -------- ---------- ---------- ---------- ---------- ---------------- +W1 SU1 J_out TRANSVERSE 8 3.33 NO 0 0 NO + +[XSECTIONS] +;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert +;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- +C1 TRAPEZOIDAL 3 5 5 5 1 +C2 TRAPEZOIDAL 1 0 0.0001 25 1 +C3 CIRCULAR 2.25 0 0 0 1 +C4 TRAPEZOIDAL 3 5 5 5 1 +C5 TRAPEZOIDAL 3 5 5 5 1 +C6 TRAPEZOIDAL 3 5 5 5 1 +C7 CIRCULAR 3.5 0 0 0 1 +C8 TRAPEZOIDAL 3 5 5 5 1 +C9 TRAPEZOIDAL 3 5 5 5 1 +C10 TRAPEZOIDAL 3 5 5 5 1 +C11 CIRCULAR 4.75 0 0 0 1 +C_out CIRCULAR 4.75 0 0 0 1 +O1 CIRCULAR 0.83 0 0 0 +W1 RECT_OPEN 2 5 0 0 + +[CURVES] +;;Name Type X-Value Y-Value +;;-------------- ---------- ---------- ---------- +;WQCV +SU1 Storage 0 21780 +SU1 4 21780 + +[TIMESERIES] +;;Name Date Time Value +;;-------------- ---------- ---------- ---------- +2-yr 0:00 0.29 +2-yr 0:05 0.33 +2-yr 0:10 0.38 +2-yr 0:15 0.64 +2-yr 0:20 0.81 +2-yr 0:25 1.57 +2-yr 0:30 2.85 +2-yr 0:35 1.18 +2-yr 0:40 0.71 +2-yr 0:45 0.42 +2-yr 0:50 0.35 +2-yr 0:55 0.3 +2-yr 1:00 0.2 +2-yr 1:05 0.19 +2-yr 1:10 0.18 +2-yr 1:15 0.17 +2-yr 1:20 0.17 +2-yr 1:25 0.16 +2-yr 1:30 0.15 +2-yr 1:35 0.15 +2-yr 1:40 0.14 +2-yr 1:45 0.14 +2-yr 1:50 0.13 +2-yr 1:55 0.13 +; +10-yr 0:00 0.49 +10-yr 0:05 0.56 +10-yr 0:10 0.65 +10-yr 0:15 1.09 +10-yr 0:20 1.39 +10-yr 0:25 2.69 +10-yr 0:30 4.87 +10-yr 0:35 2.02 +10-yr 0:40 1.21 +10-yr 0:45 0.71 +10-yr 0:50 0.6 +10-yr 0:55 0.52 +10-yr 1:00 0.39 +10-yr 1:05 0.37 +10-yr 1:10 0.35 +10-yr 1:15 0.34 +10-yr 1:20 0.32 +10-yr 1:25 0.31 +10-yr 1:30 0.3 +10-yr 1:35 0.29 +10-yr 1:40 0.28 +10-yr 1:45 0.27 +10-yr 1:50 0.26 +10-yr 1:55 0.25 +; +100-yr 0:00 1 +100-yr 0:05 1.14 +100-yr 0:10 1.33 +100-yr 0:15 2.23 +100-yr 0:20 2.84 +100-yr 0:25 5.49 +100-yr 0:30 9.95 +100-yr 0:35 4.12 +100-yr 0:40 2.48 +100-yr 0:45 1.46 +100-yr 0:50 1.22 +100-yr 0:55 1.06 +100-yr 1:00 1 +100-yr 1:05 0.95 +100-yr 1:10 0.91 +100-yr 1:15 0.87 +100-yr 1:20 0.84 +100-yr 1:25 0.81 +100-yr 1:30 0.78 +100-yr 1:35 0.75 +100-yr 1:40 0.73 +100-yr 1:45 0.71 +100-yr 1:50 0.69 +100-yr 1:55 0.67 + +[REPORT] +;;Reporting Options +SUBCATCHMENTS ALL +NODES ALL +LINKS ALL + +[TAGS] +Link C1 Swale +Link C2 Gutter +Link C3 Culvert +Link C4 Swale +Link C5 Swale +Link C6 Swale +Link C7 Culvert +Link C8 Swale +Link C9 Swale +Link C10 Swale +Link C11 Culvert + +[MAP] +DIMENSIONS -483.280 -70.199 1654.873 1514.231 +Units Feet + +[COORDINATES] +;;Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +J1 648.532 1043.713 +J2 1221.281 1005.760 +J3 405.287 905.702 +J4 505.345 862.573 +J5 631.281 859.123 +J6 822.772 819.444 +J7 831.398 709.035 +J8 915.930 840.146 +J9 1072.918 867.749 +J10 1254.058 640.029 +J11 1333.415 519.269 +J_out 1557.684 372.632 +O2 1554.593 204.586 +SU1 1471.427 458.889 + +[VERTICES] +;;Link X-Coord Y-Coord +;;-------------- ------------------ ------------------ +C2 1321.339 774.591 +C2 1374.819 652.105 +C2 1366.193 588.275 +C4 541.573 841.871 +C5 672.684 850.497 +C5 712.363 829.795 +C5 743.415 805.643 +C5 805.520 812.544 +C6 791.719 734.912 +C6 798.620 784.942 +C8 965.959 838.421 +C8 995.287 831.520 +C8 1038.415 850.497 +C9 1102.246 867.749 +C9 1131.573 852.222 +C9 1147.099 829.795 +C9 1162.626 809.094 +C9 1198.854 779.766 +C9 1219.556 757.339 +C9 1233.357 721.111 +C9 1238.532 715.936 +C9 1235.082 674.532 +C9 1247.158 646.930 +W1 1554.107 490.499 +W1 1596.150 427.435 + +[Polygons] +;;Subcatchment X-Coord Y-Coord +;;-------------- ------------------ ------------------ +S1 282.657 1334.810 +S1 111.700 1101.604 +S1 172.525 1062.743 +S1 231.660 1027.262 +S1 306.002 990.092 +S1 370.206 959.679 +S1 409.066 946.163 +S1 444.547 936.025 +S1 493.545 924.198 +S1 532.405 915.750 +S1 569.576 907.302 +S1 610.125 897.165 +S1 655.744 897.165 +S1 684.338 1318.700 +S1 651.043 1321.922 +S1 596.269 1332.662 +S1 551.160 1346.624 +S1 495.312 1367.030 +S1 455.573 1384.214 +S1 410.465 1409.991 +S1 386.836 1427.175 +S1 363.208 1442.211 +S2 678.967 1238.149 +S2 673.584 1152.903 +S2 655.744 897.165 +S2 758.808 893.786 +S2 817.943 895.475 +S2 880.458 898.855 +S2 921.007 905.613 +S2 978.453 920.819 +S2 1042.657 937.715 +S2 1103.482 959.679 +S2 1159.238 985.023 +S2 1225.131 1010.367 +S2 1109.646 1274.665 +S2 1052.723 1400.325 +S2 985.061 1370.252 +S2 924.916 1348.772 +S2 861.549 1331.588 +S2 815.367 1325.144 +S2 762.740 1319.774 +S2 719.780 1316.552 +S2 684.338 1317.626 +S3 109.199 1103.258 +S3 141.754 1081.555 +S3 190.586 1051.713 +S3 247.557 1019.158 +S3 304.528 989.317 +S3 354.716 964.900 +S3 398.123 949.980 +S3 490.166 922.509 +S3 472.567 876.374 +S3 501.993 816.065 +S3 556.059 778.895 +S3 488.476 679.210 +S3 422.582 729.897 +S3 282.348 557.560 +S3 179.734 633.927 +S3 153.962 651.561 +S3 107.843 693.610 +S3 71.218 742.443 +S3 48.159 785.849 +S3 31.881 837.394 +S3 29.168 886.226 +S3 31.881 933.702 +S3 38.664 967.613 +S3 50.872 1001.525 +S3 65.793 1035.436 +S3 87.496 1070.704 +S3 109.199 1103.258 +S4 282.348 559.250 +S4 420.893 729.897 +S4 488.476 680.899 +S4 556.828 779.067 +S4 501.213 814.335 +S4 472.567 879.825 +S4 491.718 922.851 +S4 616.511 898.434 +S4 668.056 897.078 +S4 783.355 895.722 +S4 815.909 898.434 +S4 857.959 899.791 +S4 890.595 897.165 +S4 968.316 915.750 +S4 1042.657 937.715 +S4 1074.759 849.857 +S4 1054.484 773.826 +S4 1020.692 702.864 +S4 963.247 623.454 +S4 689.536 256.816 +S5 1301.482 474.258 +S5 1271.677 445.380 +S5 1232.340 393.835 +S5 1241.835 384.340 +S5 1222.844 366.706 +S5 1233.696 355.854 +S5 1026.159 66.931 +S5 1008.525 56.079 +S5 708.750 275.824 +S5 1023.446 704.462 +S5 1150.644 618.812 +S5 1251.203 640.809 +S5 1328.193 519.824 +S6 1334.478 519.824 +S6 1306.266 488.956 +S6 1293.380 474.205 +S6 1232.340 393.835 +S6 1241.835 381.627 +S6 1222.844 365.350 +S6 1232.340 353.142 +S6 1027.516 65.574 +S6 1012.595 56.079 +S6 707.393 273.111 +S6 688.403 254.121 +S6 739.948 218.853 +S6 788.780 159.169 +S6 806.414 106.268 +S6 813.197 1.821 +S6 994.961 12.673 +S6 1228.270 27.594 +S6 1222.844 115.763 +S6 1228.270 167.308 +S6 1241.835 229.705 +S6 1255.399 254.121 +S6 1279.815 302.953 +S6 1309.657 354.498 +S6 1335.430 401.974 +S6 1359.846 448.093 +S6 1370.616 475.830 +S6 1381.615 491.542 +S7 1122.467 968.970 +S7 1174.012 987.282 +S7 1225.557 1005.594 +S7 1377.480 675.977 +S7 1391.044 642.065 +S7 1396.470 598.659 +S7 1381.615 491.542 +S7 1331.336 519.824 +S7 1249.632 640.809 +S7 1150.644 617.241 +S7 1020.733 704.462 +S7 1054.645 772.285 +S7 1076.796 848.212 +S7 1056.370 900.062 +S7 1040.658 937.772 +PreDevelop -122.020 556.971 +PreDevelop -147.955 510.994 +PreDevelop -155.029 469.733 +PreDevelop -228.120 467.375 +PreDevelop -272.918 502.742 +PreDevelop -300.622 518.068 +PreDevelop -386.091 587.622 +PreDevelop -369.587 665.429 +PreDevelop -308.285 736.163 +PreDevelop -246.982 710.227 +PreDevelop -178.606 712.585 +PreDevelop -129.093 597.054 + +;;Storage Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +SU1 1471.427 458.889 + +[SYMBOLS] +;;Gage X-Coord Y-Coord +;;-------------- ------------------ ------------------ +RainGage -148.485 1207.602 + +[LABELS] +;;X-Coord Y-Coord Label +146.515 1129.971 "S1" "" "Arial" 14 0 0 +684.760 957.456 "S2" "" "Arial" 14 0 0 +243.123 624.503 "S3" "" "Arial" 14 0 0 +655.433 332.953 "S4" "" "Arial" 14 0 0 +986.661 125.936 "S5" "" "Arial" 14 0 0 +827.947 56.930 "S6" "" "Arial" 14 0 0 +1050.491 733.187 "S7" "" "Arial" 14 0 0 +-383.734 441.440 "Pre-Development Site" "" "Arial" 14 0 0 +1375.172 399.000 "Orifice" "" "Arial" 14 0 0 +1601.519 521.604 "Weir" "" "Arial" 14 0 0 +-324.789 380.138 "(not to scale)" "" "Arial" 14 0 0 +1436.475 547.540 "Pond" "" "Arial" 14 0 0 +439.409 1459.694 "Post-Development Site" "" "Arial" 14 0 0 + diff --git a/wntr/tests/networks_for_testing/SWMM_examples/Groundwater_Model.inp b/wntr/tests/networks_for_testing/SWMM_examples/Groundwater_Model.inp new file mode 100644 index 000000000..75b161cf1 --- /dev/null +++ b/wntr/tests/networks_for_testing/SWMM_examples/Groundwater_Model.inp @@ -0,0 +1,147 @@ +[TITLE] +;;Project Title/Notes +A simple groundwater model. +See Groundwater_Model.txt for more details. + +[OPTIONS] +;;Option Value +FLOW_UNITS CFS +INFILTRATION HORTON +FLOW_ROUTING KINWAVE +LINK_OFFSETS DEPTH +MIN_SLOPE 0 +ALLOW_PONDING NO +SKIP_STEADY_STATE NO + +START_DATE 09/13/2014 +START_TIME 00:00:00 +REPORT_START_DATE 09/13/2014 +REPORT_START_TIME 00:00:00 +END_DATE 09/15/2014 +END_TIME 00:00:00 +SWEEP_START 01/01 +SWEEP_END 12/31 +DRY_DAYS 0 +REPORT_STEP 00:15:00 +WET_STEP 00:05:00 +DRY_STEP 00:05:00 +ROUTING_STEP 0:00:30 +RULE_STEP 00:00:00 + +INERTIAL_DAMPING PARTIAL +NORMAL_FLOW_LIMITED BOTH +FORCE_MAIN_EQUATION H-W +VARIABLE_STEP 0.75 +LENGTHENING_STEP 0 +MIN_SURFAREA 12.557 +MAX_TRIALS 8 +HEAD_TOLERANCE 0.005 +SYS_FLOW_TOL 5 +LAT_FLOW_TOL 5 +MINIMUM_STEP 0.5 +THREADS 1 + +[EVAPORATION] +;;Data Source Parameters +;;-------------- ---------------- +CONSTANT 0.0 +DRY_ONLY NO + +[RAINGAGES] +;;Name Format Interval SCF Source +;;-------------- --------- ------ ------ ---------- +1 INTENSITY 0:15 1.0 TIMESERIES Rainfall + +[SUBCATCHMENTS] +;;Name Rain Gage Outlet Area %Imperv Width %Slope CurbLen SnowPack +;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- ---------------- +1 1 2 5 0 140 0.5 0 + +[SUBAREAS] +;;Subcatchment N-Imperv N-Perv S-Imperv S-Perv PctZero RouteTo PctRouted +;;-------------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- +1 0.01 0.1 0.05 0.05 25 OUTLET + +[INFILTRATION] +;;Subcatchment Param1 Param2 Param3 Param4 Param5 +;;-------------- ---------- ---------- ---------- ---------- ---------- +1 1.2 0.1 2 7 0 + +[AQUIFERS] +;;Name Por WP FC Ksat Kslope Tslope ETu ETs Seep Ebot Egw Umc ETupat +;;-------------- ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ +1 0.5 0.15 0.30 0.1 12 15.0 0.35 14.0 0.002 0.0 3.5 0.40 + +[GROUNDWATER] +;;Subcatchment Aquifer Node Esurf A1 B1 A2 B2 A3 Dsw Egwt Ebot Wgr Umc +;;-------------- ---------------- ---------------- ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ +1 1 2 6 0.1 1 0 0 0 0 4 + +[OUTFALLS] +;;Name Elevation Type Stage Data Gated Route To +;;-------------- ---------- ---------- ---------------- -------- ---------------- +2 0 FREE NO + +[TIMESERIES] +;;Name Date Time Value +;;-------------- ---------- ---------- ---------- +Rainfall 0:00 0.037 +Rainfall 0:15 0.111 +Rainfall 0:30 0.185 +Rainfall 0:45 0.259 +Rainfall 1:00 0.333 +Rainfall 1:15 0.407 +Rainfall 1:30 0.481 +Rainfall 1:45 0.556 +Rainfall 2:00 0.630 +Rainfall 2:15 0.644 +Rainfall 2:30 0.600 +Rainfall 2:45 0.556 +Rainfall 3:00 0.511 +Rainfall 3:15 0.467 +Rainfall 3:30 0.422 +Rainfall 3:45 0.378 +Rainfall 4:00 0.333 +Rainfall 4:15 0.289 +Rainfall 4:30 0.244 +Rainfall 4:45 0.200 +Rainfall 5:00 0.156 +Rainfall 5:15 0.111 +Rainfall 5:30 0.067 +Rainfall 5:45 0.022 +Rainfall 6:00 0 + +[REPORT] +;;Reporting Options +SUBCATCHMENTS ALL +NODES ALL +LINKS ALL + +[TAGS] + +[MAP] +DIMENSIONS 0.000 0.000 10000.000 10000.000 +Units None + +[COORDINATES] +;;Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +2 4556.338 3352.113 + +[VERTICES] +;;Link X-Coord Y-Coord +;;-------------- ------------------ ------------------ + +[Polygons] +;;Subcatchment X-Coord Y-Coord +;;-------------- ------------------ ------------------ +1 2964.789 4971.831 +1 6134.511 4986.413 +1 6134.511 7445.652 +1 2964.789 7450.704 + +[SYMBOLS] +;;Gage X-Coord Y-Coord +;;-------------- ------------------ ------------------ +1 4485.915 8197.183 + diff --git a/wntr/tests/networks_for_testing/SWMM_examples/Inlet_Drains_Model.inp b/wntr/tests/networks_for_testing/SWMM_examples/Inlet_Drains_Model.inp new file mode 100644 index 000000000..f7de000c6 --- /dev/null +++ b/wntr/tests/networks_for_testing/SWMM_examples/Inlet_Drains_Model.inp @@ -0,0 +1,502 @@ +[TITLE] +;;Project Title/Notes +A dual drainage model with street inlets. +See Inlet_Drains_Model.txt for more details. + +[OPTIONS] +;;Option Value +FLOW_UNITS CFS +INFILTRATION HORTON +FLOW_ROUTING DYNWAVE +LINK_OFFSETS DEPTH +MIN_SLOPE 0 +ALLOW_PONDING NO +SKIP_STEADY_STATE NO + +START_DATE 01/01/2007 +START_TIME 00:00:00 +REPORT_START_DATE 01/01/2007 +REPORT_START_TIME 00:00:00 +END_DATE 01/01/2007 +END_TIME 06:00:00 +SWEEP_START 01/01 +SWEEP_END 12/31 +DRY_DAYS 0 +REPORT_STEP 00:01:00 +WET_STEP 00:01:00 +DRY_STEP 01:00:00 +ROUTING_STEP 0:00:15 +RULE_STEP 00:00:00 + +INERTIAL_DAMPING PARTIAL +NORMAL_FLOW_LIMITED SLOPE +FORCE_MAIN_EQUATION H-W +VARIABLE_STEP 0.75 +LENGTHENING_STEP 0 +MIN_SURFAREA 12.566 +MAX_TRIALS 8 +HEAD_TOLERANCE 0.005 +SYS_FLOW_TOL 5 +LAT_FLOW_TOL 5 +MINIMUM_STEP 0.5 +THREADS 1 + +[EVAPORATION] +;;Data Source Parameters +;;-------------- ---------------- +CONSTANT 0.0 +DRY_ONLY NO + +[RAINGAGES] +;;Name Format Interval SCF Source +;;-------------- --------- ------ ------ ---------- +RainGage INTENSITY 0:05 1.0 TIMESERIES 2-yr + +[SUBCATCHMENTS] +;;Name Rain Gage Outlet Area %Imperv Width %Slope CurbLen SnowPack +;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- ---------------- +S1 RainGage Aux1 4.55 56.8 1587 2 0 +S2 RainGage Aux4 4.74 63.0 1653 2 0 +S3 RainGage Aux3 3.74 39.5 1456 3.1 0 +S4 RainGage J7 6.79 49.9 2331 3.1 0 +S5 RainGage J10 4.79 87.7 1670 2 0 +S6 RainGage J11 1.98 95.0 690 2 0 +S7 RainGage J10 2.33 0.0 907 3.1 0 + +[SUBAREAS] +;;Subcatchment N-Imperv N-Perv S-Imperv S-Perv PctZero RouteTo PctRouted +;;-------------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- +S1 0.015 0.24 0.06 0.3 25 OUTLET +S2 0.015 0.24 0.06 0.3 25 OUTLET +S3 0.015 0.24 0.06 0.3 25 OUTLET +S4 0.015 0.24 0.06 0.3 25 OUTLET +S5 0.015 0.24 0.06 0.3 25 OUTLET +S6 0.015 0.24 0.06 0.3 25 OUTLET +S7 0.015 0.24 0.06 0.3 25 OUTLET + +[INFILTRATION] +;;Subcatchment Param1 Param2 Param3 Param4 Param5 +;;-------------- ---------- ---------- ---------- ---------- ---------- +S1 4.5 0.2 6.5 7 0 +S2 4.5 0.2 6.5 7 0 +S3 4.5 0.2 6.5 7 0 +S4 4.5 0.2 6.5 7 0 +S5 4.5 0.2 6.5 7 0 +S6 4.5 0.2 6.5 7 0 +S7 4.5 0.2 6.5 7 0 + +[JUNCTIONS] +;;Name Elevation MaxDepth InitDepth SurDepth Aponded +;;-------------- ---------- ---------- ---------- ---------- ---------- +Aux1 4975 0 0 0 0 +Aux2 4973 0 0 0 0 +Aux3 4968.5 0 0 0 0 +Aux4 4971.8 0 0 0 0 +Aux5 4970.7 0 0 0 0 +Aux6 4969 0 0 0 0 +Aux7 4963 0 0 0 0 +J1 4969 4 0 0 0 +J10 4957.8 0 0 0 0 +J11 4957 0 0 0 0 +J2 4965 4 0 0 0 +J2a 4966.7 4 0 0 0 +J3 4973 0 0 0 0 +J4 4965 0 0 0 0 +J5 4965.8 0 0 0 0 +J6 4969 0 0 0 0 +J7 4963.5 0 0 0 0 +J8 4966.5 0 0 0 0 +J9 4964.8 0 0 0 0 + +[OUTFALLS] +;;Name Elevation Type Stage Data Gated Route To +;;-------------- ---------- ---------- ---------------- -------- ---------------- +O1 4956 FREE NO + +[CONDUITS] +;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow +;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- +Street1 Aux1 Aux2 377.31 0.016 0 0 0 0 +Street2 Aux2 Aux4 286.06 0.016 0 0 0 0 +Street3 Aux4 Aux5 239.41 0.016 0 0 0 0 +Street4 Aux5 Aux6 157.48 0.016 0 0 0 0 +Street5 Aux6 Aux7 526.0 0.016 0 0 0 0 +C3 J3 J4 109.0 0.016 0 6 0 0 +C4 J4 J5 133.0 0.05 6 4 0 0 +C5 J5 J6 207.0 0.05 4 0 0 0 +C6 J7 J6 140.0 0.05 8 0 0 0 +C7 J6 J8 95.0 0.016 0 0 0 0 +C8 J8 J9 166.0 0.05 0 0 0 0 +C9 J9 J10 320.0 0.05 0 6 0 0 +C10 J10 J11 145.0 0.05 6 6 0 0 +C11 J11 O1 89.0 0.016 0 0 0 0 +C_Aux3 Aux3 J3 444.75 0.05 6 0 0 0 +P1 J1 J5 185.39 0.016 0 0 0 0 +P2 J2a J2 157.48 0.016 0 0 0 0 +P3 J2 J11 529.22 0.016 0 0 0 0 +P4 Aux3 J4 567.19 0.016 0 0 0 0 +P5 J5 J4 125.98 0.016 0 0 0 0 +P6 J4 J7 360.39 0.016 0 0 0 0 +P7 J7 J10 507.76 0.016 0 0 0 0 +P8 J10 J11 144.50 0.016 0 0 0 0 + +[XSECTIONS] +;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert +;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- +Street1 STREET FullStreet +Street2 STREET FullStreet +Street3 STREET FullStreet +Street4 STREET HalfStreet +Street5 STREET HalfStreet +C3 CIRCULAR 2.25 0 0 0 1 +C4 TRAPEZOIDAL 3 5 5 5 1 +C5 TRAPEZOIDAL 3 5 5 5 1 +C6 TRAPEZOIDAL 3 5 5 5 1 +C7 CIRCULAR 3.5 0 0 0 1 +C8 TRAPEZOIDAL 3 5 5 5 1 +C9 TRAPEZOIDAL 3 5 5 5 1 +C10 TRAPEZOIDAL 3 5 5 5 1 +C11 CIRCULAR 4.75 0 0 0 1 +C_Aux3 TRAPEZOIDAL 3 5 5 5 1 +P1 CIRCULAR 1.33 0 0 0 1 +P2 CIRCULAR 1.5 0 0 0 1 +P3 CIRCULAR 1.5 0 0 0 1 +P4 CIRCULAR 1.67 0 0 0 1 +P5 CIRCULAR 1.83 0 0 0 1 +P6 CIRCULAR 2 0 0 0 1 +P7 CIRCULAR 2 0 0 0 1 +P8 CIRCULAR 3.17 0 0 0 1 + +[STREETS] +;;Name Tcrown Hcurb Sx nRoad a W Sides Tback Sback nBack +;;-------------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- +HalfStreet 20 0.5 4 0.016 0 0 1 20 4 0.016 +FullStreet 20 0.5 4 0.016 0 0 2 20 4 0.016 + +[INLETS] +;;Name Type Parameters: +;;-------------- ---------------- ----------- +ComboInlet GRATE 2 2 P_BAR-50 +ComboInlet CURB 2 0.5 HORIZONTAL + +[INLET_USAGE] +;;Conduit Inlet Node Number %Clogged Qmax aLocal wLocal Placement +;;-------------- ---------------- ---------------- --------- --------- --------- --------- --------- --------- --------- +Street1 ComboInlet J1 1 0 0 0 0 +Street3 ComboInlet J2a 1 0 0 0 0 +Street4 ComboInlet J2 1 0 0 0 0 +Street5 ComboInlet J11 2 0 0 0 0 + +[TIMESERIES] +;;Name Date Time Value +;;-------------- ---------- ---------- ---------- +2-yr 0:00 0.29 +2-yr 0:05 0.33 +2-yr 0:10 0.38 +2-yr 0:15 0.64 +2-yr 0:20 0.81 +2-yr 0:25 1.57 +2-yr 0:30 2.85 +2-yr 0:35 1.18 +2-yr 0:40 0.71 +2-yr 0:45 0.42 +2-yr 0:50 0.35 +2-yr 0:55 0.3 +2-yr 1:00 0.2 +2-yr 1:05 0.19 +2-yr 1:10 0.18 +2-yr 1:15 0.17 +2-yr 1:20 0.17 +2-yr 1:25 0.16 +2-yr 1:30 0.15 +2-yr 1:35 0.15 +2-yr 1:40 0.14 +2-yr 1:45 0.14 +2-yr 1:50 0.13 +2-yr 1:55 0.13 +; +10-yr 0:00 0.49 +10-yr 0:05 0.56 +10-yr 0:10 0.65 +10-yr 0:15 1.09 +10-yr 0:20 1.39 +10-yr 0:25 2.69 +10-yr 0:30 4.87 +10-yr 0:35 2.02 +10-yr 0:40 1.21 +10-yr 0:45 0.71 +10-yr 0:50 0.6 +10-yr 0:55 0.52 +10-yr 1:00 0.39 +10-yr 1:05 0.37 +10-yr 1:10 0.35 +10-yr 1:15 0.34 +10-yr 1:20 0.32 +10-yr 1:25 0.31 +10-yr 1:30 0.3 +10-yr 1:35 0.29 +10-yr 1:40 0.28 +10-yr 1:45 0.27 +10-yr 1:50 0.26 +10-yr 1:55 0.25 + +[REPORT] +;;Reporting Options +INPUT YES +SUBCATCHMENTS ALL +NODES ALL +LINKS ALL + +[TAGS] +Link Street1 Full_Street +Link Street2 Full_Street +Link Street3 Full_Street +Link Street4 Half_Street +Link Street5 Half_Street +Link C3 Culvert +Link C4 Swale +Link C5 Swale +Link C6 Swale +Link C7 Culvert +Link C8 Swale +Link C9 Swale +Link C10 Swale +Link C11 Culvert +Link C_Aux3 Swale + +[MAP] +DIMENSIONS -255.206 -70.199 1490.833 1514.231 +Units Feet + +[COORDINATES] +;;Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +Aux1 293.152 1161.023 +Aux2 653.046 1052.180 +Aux3 122.363 696.959 +Aux4 914.142 1058.025 +Aux5 1175.238 1120.376 +Aux6 1260.972 956.704 +Aux7 1381.777 514.399 +J1 647.200 1022.952 +J10 1254.058 640.029 +J11 1270.714 491.017 +J2 1218.105 950.859 +J2a 1159.651 1069.716 +J3 405.287 905.702 +J4 505.345 862.573 +J5 631.281 859.123 +J6 803.079 869.022 +J7 831.398 709.035 +J8 915.930 840.146 +J9 1072.918 867.749 +O1 1411.468 477.401 + +[VERTICES] +;;Link X-Coord Y-Coord +;;-------------- ------------------ ------------------ +Street1 382.860 1112.719 +Street1 514.703 1061.922 +Street2 806.976 1042.437 +Street3 1062.227 1091.149 +Street4 1210.311 1061.922 +Street5 1338.911 781.341 +Street5 1387.623 662.483 +Street5 1393.468 586.493 +C4 559.710 846.393 +C5 672.684 850.497 +C5 712.363 829.795 +C5 743.415 805.643 +C5 768.006 833.950 +C6 791.719 734.912 +C6 798.620 784.942 +C8 965.959 838.421 +C8 995.287 831.520 +C8 1038.415 850.497 +C9 1102.246 867.749 +C9 1131.573 852.222 +C9 1147.099 829.795 +C9 1162.626 809.094 +C9 1198.854 779.766 +C9 1219.556 757.339 +C9 1233.357 721.111 +C9 1238.532 715.936 +C9 1235.082 674.532 +C9 1247.158 646.930 +C_Aux3 163.003 699.992 +C_Aux3 208.620 726.287 +C_Aux3 239.673 793.567 +C_Aux3 251.749 876.374 +C_Aux3 291.427 912.602 +C_Aux3 355.257 929.854 +P3 1331.117 691.711 +P3 1329.168 590.390 +P4 275.901 572.749 +P4 472.567 824.620 +P7 886.602 800.468 + +[Polygons] +;;Subcatchment X-Coord Y-Coord +;;-------------- ------------------ ------------------ +S1 282.657 1334.810 +S1 111.700 1101.604 +S1 172.525 1062.743 +S1 231.660 1027.262 +S1 306.002 990.092 +S1 370.206 959.679 +S1 409.066 946.163 +S1 444.547 936.025 +S1 493.545 924.198 +S1 532.405 915.750 +S1 569.576 907.302 +S1 610.125 897.165 +S1 655.744 897.165 +S1 684.338 1318.700 +S1 651.043 1321.922 +S1 596.269 1332.662 +S1 551.160 1346.624 +S1 495.312 1367.030 +S1 455.573 1384.214 +S1 410.465 1409.991 +S1 386.836 1427.175 +S1 363.208 1442.211 +S2 678.967 1238.149 +S2 673.584 1152.903 +S2 655.744 897.165 +S2 758.808 893.786 +S2 817.943 895.475 +S2 880.458 898.855 +S2 921.007 905.613 +S2 978.453 920.819 +S2 1042.657 937.715 +S2 1103.482 959.679 +S2 1159.238 985.023 +S2 1225.131 1010.367 +S2 1109.646 1274.665 +S2 1052.723 1400.325 +S2 985.061 1370.252 +S2 924.916 1348.772 +S2 861.549 1331.588 +S2 815.367 1325.144 +S2 762.740 1319.774 +S2 719.780 1316.552 +S2 684.338 1317.626 +S3 109.199 1103.258 +S3 141.754 1081.555 +S3 190.586 1051.713 +S3 247.557 1019.158 +S3 304.528 989.317 +S3 354.716 964.900 +S3 398.123 949.980 +S3 490.166 922.509 +S3 477.743 883.275 +S3 501.993 816.065 +S3 556.059 778.895 +S3 488.476 679.210 +S3 422.582 729.897 +S3 282.348 557.560 +S3 179.734 633.927 +S3 153.962 651.561 +S3 107.843 693.610 +S3 71.218 742.443 +S3 48.159 785.849 +S3 31.881 837.394 +S3 29.168 886.226 +S3 31.881 933.702 +S3 38.664 967.613 +S3 50.872 1001.525 +S3 65.793 1035.436 +S3 87.496 1070.704 +S3 109.199 1103.258 +S4 282.348 559.250 +S4 420.893 729.897 +S4 488.476 680.899 +S4 556.828 779.067 +S4 501.213 814.335 +S4 479.468 885.000 +S4 491.718 922.851 +S4 616.511 898.434 +S4 668.056 897.078 +S4 783.355 895.722 +S4 815.909 898.434 +S4 857.959 899.791 +S4 890.595 897.165 +S4 968.316 915.750 +S4 1042.657 937.715 +S4 1074.759 849.857 +S4 1054.484 773.826 +S4 1020.692 702.864 +S4 963.247 623.454 +S4 689.536 256.816 +S5 1301.482 474.258 +S5 1271.677 445.380 +S5 1232.340 393.835 +S5 1241.835 384.340 +S5 1222.844 366.706 +S5 1233.696 355.854 +S5 1026.159 66.931 +S5 1008.525 56.079 +S5 708.750 275.824 +S5 1023.446 704.462 +S5 1150.644 618.812 +S5 1251.203 640.809 +S5 1328.193 519.824 +S6 1334.478 519.824 +S6 1306.266 488.956 +S6 1293.380 474.205 +S6 1232.340 393.835 +S6 1241.835 381.627 +S6 1222.844 365.350 +S6 1232.340 353.142 +S6 1027.516 65.574 +S6 1012.595 56.079 +S6 707.393 273.111 +S6 688.403 254.121 +S6 739.948 218.853 +S6 788.780 159.169 +S6 806.414 106.268 +S6 813.197 1.821 +S6 994.961 12.673 +S6 1228.270 27.594 +S6 1222.844 115.763 +S6 1228.270 167.308 +S6 1241.835 229.705 +S6 1255.399 254.121 +S6 1279.815 302.953 +S6 1309.657 354.498 +S6 1335.430 401.974 +S6 1359.846 448.093 +S6 1370.616 475.830 +S6 1381.615 491.542 +S7 1122.467 968.970 +S7 1174.012 987.282 +S7 1225.557 1005.594 +S7 1377.480 675.977 +S7 1391.044 642.065 +S7 1396.470 598.659 +S7 1381.615 491.542 +S7 1331.336 519.824 +S7 1249.632 640.809 +S7 1150.644 617.241 +S7 1020.733 704.462 +S7 1054.645 772.285 +S7 1076.796 848.212 +S7 1056.370 900.062 +S7 1040.658 937.772 + +[SYMBOLS] +;;Gage X-Coord Y-Coord +;;-------------- ------------------ ------------------ +RainGage -175.841 1212.778 + +[LABELS] +;;X-Coord Y-Coord Label +145.274 1129.896 "S1" "" "Arial" 14 0 0 +758.404 969.723 "S2" "" "Arial" 14 0 0 +247.369 666.226 "S3" "" "Arial" 14 0 0 +628.971 458.688 "S4" "" "Arial" 14 0 0 +952.552 257.845 "S5" "" "Arial" 14 0 0 +827.947 56.930 "S6" "" "Arial" 14 0 0 +1073.058 780.037 "S7" "" "Arial" 14 0 0 +1385.481 454.225 "Outfall" "" "Arial" 10 1 0 + diff --git a/wntr/tests/networks_for_testing/SWMM_examples/LID_Model.inp b/wntr/tests/networks_for_testing/SWMM_examples/LID_Model.inp new file mode 100644 index 000000000..cb95e5e67 --- /dev/null +++ b/wntr/tests/networks_for_testing/SWMM_examples/LID_Model.inp @@ -0,0 +1,390 @@ +[TITLE] +;;Project Title/Notes +A Low Impact Development model. +See LID_Model.txt for more details. + +[OPTIONS] +;;Option Value +FLOW_UNITS CFS +INFILTRATION GREEN_AMPT +FLOW_ROUTING KINWAVE +LINK_OFFSETS DEPTH +MIN_SLOPE 0 +ALLOW_PONDING NO +SKIP_STEADY_STATE NO + +START_DATE 01/01/2007 +START_TIME 00:00:00 +REPORT_START_DATE 01/01/2007 +REPORT_START_TIME 00:00:00 +END_DATE 01/01/2007 +END_TIME 12:00:00 +SWEEP_START 01/01 +SWEEP_END 12/31 +DRY_DAYS 0 +REPORT_STEP 00:01:00 +WET_STEP 00:01:00 +DRY_STEP 01:00:00 +ROUTING_STEP 0:01:00 +RULE_STEP 00:00:00 + +INERTIAL_DAMPING PARTIAL +NORMAL_FLOW_LIMITED SLOPE +FORCE_MAIN_EQUATION H-W +VARIABLE_STEP 0.75 +LENGTHENING_STEP 0 +MIN_SURFAREA 0 +MAX_TRIALS 0 +HEAD_TOLERANCE 0 +SYS_FLOW_TOL 5 +LAT_FLOW_TOL 5 +MINIMUM_STEP 0.5 +THREADS 1 + +[EVAPORATION] +;;Data Source Parameters +;;-------------- ---------------- +CONSTANT 0.2 +DRY_ONLY NO + +[RAINGAGES] +;;Name Format Interval SCF Source +;;-------------- --------- ------ ------ ---------- +RainGage INTENSITY 0:05 1.0 TIMESERIES 2-yr + +[SUBCATCHMENTS] +;;Name Rain Gage Outlet Area %Imperv Width %Slope CurbLen SnowPack +;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- ---------------- +S1 RainGage Swale4 4.55 56.8 1587 2 0 +S2 RainGage O1 4.74 63.0 1653 2 0 +S3 RainGage Swale3 3.70 39.5 1456 3.1 0 +S4 RainGage Swale4 6.82 49.9 2331 3.1 0 +S5 RainGage O1 6.60 87.7 1670 2 0 +S6 RainGage Swale6 2.58 0.0 907 3.1 0 +Swale3 RainGage Swale4 0.33 0 50 0.5 0 +Swale4 RainGage Swale6 0.50 0 50 0.5 0 +Swale6 RainGage O1 0.41 0 50 0.5 0 + +[SUBAREAS] +;;Subcatchment N-Imperv N-Perv S-Imperv S-Perv PctZero RouteTo PctRouted +;;-------------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- +S1 0.015 0.24 0.06 0.3 25 OUTLET +S2 0.015 0.24 0.06 0.3 25 OUTLET +S3 0.015 0.24 0.06 0.3 25 OUTLET +S4 0.015 0.24 0.06 0.3 25 OUTLET +S5 0.015 0.24 0.06 0.3 25 OUTLET +S6 0.015 0.24 0.06 0.3 25 OUTLET +Swale3 0.01 0.2 0.05 0.05 25 OUTLET +Swale4 0.01 0.2 0.05 0.05 25 OUTLET +Swale6 0.01 0.2 0.05 0.05 25 OUTLET + +[INFILTRATION] +;;Subcatchment Param1 Param2 Param3 Param4 Param5 +;;-------------- ---------- ---------- ---------- ---------- ---------- +S1 3.5 0.2 0.2 7 0 +S2 3.5 0.2 0.2 7 0 +S3 3.5 0.2 0.2 7 0 +S4 3.5 0.2 0.2 7 0 +S5 3.5 0.2 0.2 7 0 +S6 3.5 0.2 0.2 7 0 +Swale3 3.5 0.2 0.2 7 0 +Swale4 3.5 0.2 0.2 7 0 +Swale6 3.5 0.2 0.2 7 0 + +[LID_CONTROLS] +;;Name Type/Layer Parameters +;;-------------- ---------- ---------- +GreenRoof BC +GreenRoof SURFACE 0.0 0.0 0.1 1.0 5 +GreenRoof SOIL 3 0.5 0.2 0.1 0.5 10.0 3.5 +GreenRoof STORAGE 1 0.75 0 0 NO +GreenRoof DRAIN 1 0.5 0 6 0 0 + +PorousPave PP +PorousPave SURFACE 0.0 0.0 0.02 2 5 +PorousPave PAVEMENT 6 0.15 0 100 0 0 0 +PorousPave STORAGE 12 0.75 0.2 0 NO +PorousPave DRAIN 0 0.5 0 6 0 0 + +Planters BC +Planters SURFACE 6 0.0 0.0 0.0 5 +Planters SOIL 12 0.5 0.2 0.1 0.5 10.0 3.5 +Planters STORAGE 12 0.5 0.2 0 NO +Planters DRAIN 0 1 0.5 6 0 0 + +InfilTrench IT +InfilTrench SURFACE 0.0 0.0 0.24 0.4 5 +InfilTrench STORAGE 36 0.40 0.2 0 NO +InfilTrench DRAIN 0 0.5 0 6 0 0 + +RainBarrels RB +RainBarrels STORAGE 48 1 0 0 NO +RainBarrels DRAIN 1 0.5 0 6 0 0 + +Swale VS +Swale SURFACE 36 0.0 0.24 1.0 5 + +[LID_USAGE] +;;Subcatchment LID Process Number Area Width InitSat FromImp ToPerv RptFile DrainTo FromPerv +;;-------------- ---------------- ------- ---------- ---------- ---------- ---------- ---------- ------------------------ ---------------- ---------- +S1 InfilTrench 4 532 133 0 40 0 * * 0 +S1 RainBarrels 32 5 0 0 17 1 * * 0 +S4 Planters 30 500 0 0 80 0 * * 0 +S5 PorousPave 1 232872 683 0 0 0 * * 0 +S5 GreenRoof 1 18400 136 0 0 0 * * 0 +Swale3 Swale 1 14374.80 45 0 0 0 * * 0 +Swale4 Swale 1 21780.00 45 0 0 0 * * 0 +Swale6 Swale 1 17859.60 45 0 0 0 * * 0 + +[OUTFALLS] +;;Name Elevation Type Stage Data Gated Route To +;;-------------- ---------- ---------- ---------------- -------- ---------------- +O1 4962 FREE NO + +[TIMESERIES] +;;Name Date Time Value +;;-------------- ---------- ---------- ---------- +2-yr 0:00 0.29 +2-yr 0:05 0.33 +2-yr 0:10 0.38 +2-yr 0:15 0.64 +2-yr 0:20 0.81 +2-yr 0:25 1.57 +2-yr 0:30 2.85 +2-yr 0:35 1.18 +2-yr 0:40 0.71 +2-yr 0:45 0.42 +2-yr 0:50 0.35 +2-yr 0:55 0.3 +2-yr 1:00 0.2 +2-yr 1:05 0.19 +2-yr 1:10 0.18 +2-yr 1:15 0.17 +2-yr 1:20 0.17 +2-yr 1:25 0.16 +2-yr 1:30 0.15 +2-yr 1:35 0.15 +2-yr 1:40 0.14 +2-yr 1:45 0.14 +2-yr 1:50 0.13 +2-yr 1:55 0.13 +; +10-yr 0:00 0.49 +10-yr 0:05 0.56 +10-yr 0:10 0.65 +10-yr 0:15 1.09 +10-yr 0:20 1.39 +10-yr 0:25 2.69 +10-yr 0:30 4.87 +10-yr 0:35 2.02 +10-yr 0:40 1.21 +10-yr 0:45 0.71 +10-yr 0:50 0.6 +10-yr 0:55 0.52 +10-yr 1:00 0.39 +10-yr 1:05 0.37 +10-yr 1:10 0.35 +10-yr 1:15 0.34 +10-yr 1:20 0.32 +10-yr 1:25 0.31 +10-yr 1:30 0.3 +10-yr 1:35 0.29 +10-yr 1:40 0.28 +10-yr 1:45 0.27 +10-yr 1:50 0.26 +10-yr 1:55 0.25 +; +100-yr 0:00 1 +100-yr 0:05 1.14 +100-yr 0:10 1.33 +100-yr 0:15 2.23 +100-yr 0:20 2.84 +100-yr 0:25 5.49 +100-yr 0:30 9.95 +100-yr 0:35 4.12 +100-yr 0:40 2.48 +100-yr 0:45 1.46 +100-yr 0:50 1.22 +100-yr 0:55 1.06 +100-yr 1:00 1 +100-yr 1:05 0.95 +100-yr 1:10 0.91 +100-yr 1:15 0.87 +100-yr 1:20 0.84 +100-yr 1:25 0.81 +100-yr 1:30 0.78 +100-yr 1:35 0.75 +100-yr 1:40 0.73 +100-yr 1:45 0.71 +100-yr 1:50 0.69 +100-yr 1:55 0.67 + +[REPORT] +;;Reporting Options +INPUT YES +SUBCATCHMENTS ALL +NODES ALL +LINKS ALL + +[TAGS] + +[MAP] +DIMENSIONS -39.947 -29.165 1480.583 1512.277 +Units Feet + +[COORDINATES] +;;Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +O1 1411.468 477.401 + +[VERTICES] +;;Link X-Coord Y-Coord +;;-------------- ------------------ ------------------ + +[Polygons] +;;Subcatchment X-Coord Y-Coord +;;-------------- ------------------ ------------------ +S1 282.657 1334.810 +S1 111.700 1101.604 +S1 172.525 1062.743 +S1 231.660 1027.262 +S1 306.002 990.092 +S1 370.206 959.679 +S1 409.066 946.163 +S1 444.547 936.025 +S1 493.545 924.198 +S1 532.405 915.750 +S1 569.576 907.302 +S1 610.125 897.165 +S1 655.744 897.165 +S1 684.338 1318.700 +S1 651.043 1321.922 +S1 596.269 1332.662 +S1 551.160 1346.624 +S1 495.312 1367.030 +S1 455.573 1384.214 +S1 410.465 1409.991 +S1 386.836 1427.175 +S1 363.208 1442.211 +S2 678.967 1238.149 +S2 673.584 1152.903 +S2 655.744 897.165 +S2 758.808 893.786 +S2 817.943 895.475 +S2 880.458 898.855 +S2 921.007 905.613 +S2 978.453 920.819 +S2 1042.657 937.715 +S2 1103.482 959.679 +S2 1159.238 985.023 +S2 1225.131 1010.367 +S2 1109.646 1274.665 +S2 1052.723 1400.325 +S2 985.061 1370.252 +S2 924.916 1348.772 +S2 861.549 1331.588 +S2 815.367 1325.144 +S2 762.740 1319.774 +S2 719.780 1316.552 +S2 684.338 1317.626 +S3 109.199 1103.258 +S3 141.754 1081.555 +S3 190.586 1051.713 +S3 247.557 1019.158 +S3 304.528 989.317 +S3 354.716 964.900 +S3 398.123 949.980 +S3 490.166 922.509 +S3 474.292 876.374 +S3 501.993 816.065 +S3 556.059 778.895 +S3 488.476 679.210 +S3 422.582 729.897 +S3 282.348 557.560 +S3 179.734 633.927 +S3 153.962 651.561 +S3 107.843 693.610 +S3 71.218 742.443 +S3 48.159 785.849 +S3 31.881 837.394 +S3 29.168 886.226 +S3 31.881 933.702 +S3 38.664 967.613 +S3 50.872 1001.525 +S3 65.793 1035.436 +S3 87.496 1070.704 +S3 109.199 1103.258 +S4 282.348 559.250 +S4 420.893 729.897 +S4 488.476 680.899 +S4 556.828 779.067 +S4 501.213 814.335 +S4 474.292 876.374 +S4 491.718 922.851 +S4 616.511 898.434 +S4 668.056 897.078 +S4 783.355 895.722 +S4 815.909 898.434 +S4 857.959 899.791 +S4 890.595 897.165 +S4 968.316 915.750 +S4 1042.657 937.715 +S4 1074.759 849.857 +S4 1054.484 773.826 +S4 1020.692 702.864 +S4 963.247 623.454 +S4 689.536 256.816 +S5 1397.873 478.033 +S5 1408.099 429.463 +S5 1372.310 393.674 +S5 1336.522 339.991 +S5 1287.951 268.414 +S5 1247.050 168.718 +S5 1226.600 51.127 +S5 1027.206 46.014 +S5 807.362 40.901 +S5 789.468 109.922 +S5 740.898 186.612 +S5 694.884 258.189 +S5 1023.446 704.462 +S5 1150.644 618.812 +S5 1224.043 562.392 +S5 1313.515 503.596 +S6 1122.467 968.970 +S6 1174.012 987.282 +S6 1225.557 1005.594 +S6 1377.480 675.977 +S6 1391.044 642.065 +S6 1396.470 598.659 +S6 1395.317 480.589 +S6 1316.071 503.596 +S6 1216.374 570.061 +S6 1150.644 617.241 +S6 1020.733 704.462 +S6 1054.645 772.285 +S6 1076.796 848.212 +S6 1056.370 900.062 +S6 1040.658 937.772 +Swale3 283.315 920.277 +Swale3 616.781 831.709 +Swale3 633.132 871.873 +Swale3 290.984 958.622 +Swale4 617.343 831.761 +Swale4 1113.210 830.993 +Swale4 1116.207 879.551 +Swale4 633.558 871.873 +Swale6 1113.809 831.592 +Swale6 1177.955 787.230 +Swale6 1213.337 741.244 +Swale6 1366.957 486.918 +Swale6 1392.560 504.840 +Swale6 1298.681 680.649 +Swale6 1238.940 778.795 +Swale6 1133.967 876.941 +Swale6 1116.207 879.551 + +[SYMBOLS] +;;Gage X-Coord Y-Coord +;;-------------- ------------------ ------------------ +RainGage 95.211 1382.022 + diff --git a/wntr/tests/networks_for_testing/SWMM_examples/README.md b/wntr/tests/networks_for_testing/SWMM_examples/README.md new file mode 100644 index 000000000..0d07453b2 --- /dev/null +++ b/wntr/tests/networks_for_testing/SWMM_examples/README.md @@ -0,0 +1 @@ +INP files distributed with SWMM 5.2 \ No newline at end of file diff --git a/wntr/tests/test_stormwater.py b/wntr/tests/test_stormwater.py new file mode 100644 index 000000000..84873909a --- /dev/null +++ b/wntr/tests/test_stormwater.py @@ -0,0 +1,653 @@ +import unittest +import warnings +import os +from os.path import abspath, dirname, join, isfile +from pandas.testing import assert_frame_equal +import numpy as np +import networkx as nx +import pandas as pd +import matplotlib.pylab as plt +import subprocess + +try: + import swmmio + warnings.filterwarnings('ignore', module='swmmio') + has_swmmio = True +except ModuleNotFoundError: + swmmio = None + has_swmmio = False +try: + import pyswmm +except ModuleNotFoundError: + pyswmm = None + +import wntr.stormwater as swntr + + +testdir = dirname(abspath(str(__file__))) +test_datadir = join(testdir, "networks_for_testing") +ex_datadir = join(testdir, "..", "..", "examples", "networks") + + +@unittest.skipIf(not has_swmmio, + "Cannot test SWNTR capabilities: swmmio is missing") +class TestStormWaterModel(unittest.TestCase): + + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + def test_conduit_cross_section_and_volume(self): + inpfile = join(test_datadir, "SWMM_examples", "Detention_Pond_Model.inp") + swn = swntr.network.StormWaterNetworkModel(inpfile) + + conduit_cross_section = swn.conduit_cross_section + conduit_volume = swn.conduit_volume + + link_name = 'C3' + assert swn.xsections.loc[link_name, 'Shape'] == 'CIRCULAR' + + diameter = swn.xsections.loc[link_name, 'Geom1'] + cross_section = np.pi*(diameter/2)**2 + self.assertAlmostEqual(conduit_cross_section[link_name], cross_section, 4) + + volume = cross_section*swn.conduits.loc[link_name, 'Length'] + self.assertAlmostEqual(conduit_volume[link_name], volume, 4) + + def test_timeseries_to_datetime_format(self): + inpfile = join(test_datadir, "SWMM_examples", "Detention_Pond_Model.inp") + swn = swntr.network.StormWaterNetworkModel(inpfile) + + # 3 24-hour timeseries + assert swn.timeseries.shape == (24*3, 3) + + ts = swn.timeseries_to_datetime_format() + assert set(ts.columns) == set(['2-yr', '10-yr', '100-yr']) + assert ts.shape == (24, 3) + + ts['New1'] = 1 + ts['New2'] = 2 + timeseries = swn.add_datetime_indexed_timeseries(ts[['New1', 'New2']]) + + # 5 24-hour timeseries + assert swn.timeseries.shape == (24*5, 3) + + def test_patterns_to_datetime_format(self): + inpfile = join(ex_datadir, "Pump_Control_Model.inp") + swn = swntr.network.StormWaterNetworkModel(inpfile) + + # 1 24-hour pattern + assert swn.patterns.shape == (1, 25) + + ts = swn.patterns_to_datetime_format() + assert set(ts.columns) == set(['DWF']) + assert ts.shape == (24, 1) + + ts['New1'] = 1 + ts['New2'] = 2 + pattern = swn.add_datetime_indexed_patterns(ts[['New1', 'New2']]) + + # 3 24-hour pattern + assert swn.patterns.shape == (3, 25) + + def test_to_graph(self): + inpfile = join(ex_datadir, "Pump_Control_Model.inp") + swn = swntr.network.StormWaterNetworkModel(inpfile) + + G = swn.to_graph() + + assert isinstance(G, nx.MultiDiGraph) + assert G.number_of_nodes() == swn.junctions.shape[0] + \ + swn.outfalls.shape[0] + \ + swn.storage.shape[0] + assert G.number_of_edges() == swn.conduits.shape[0] + \ + swn.weirs.shape[0] + \ + swn.orifices.shape[0] + \ + swn.pumps.shape[0] + + def test_add_composite_pattern(self): + inpfile = join(ex_datadir, "Pump_Control_Model.inp") + swn = swntr.network.StormWaterNetworkModel(inpfile) + + swn.patterns.loc['Pat1',:] = ['HOURLY', 0.8, 0.9, 1.0, 0.9, 0.8, 1.1, + 1.2, 1.3, 1.4, 1.0, 0.9, 0.8, + 0.7, 0.8, 0.9, 0.8, 1.1, 1.2, + 1.3, 1.4, 1.2, 1.0, 0.9, 0.7] + dwf_data = pd.DataFrame([['KRO3001', 1, 'DWF'], + ['KRO3001', 0.03, 'Pat1']], + columns=['Node', 'AverageValue', 'Pattern']) + dwf_data.set_index('Node', inplace=True) + + composite = swn.add_composite_patterns(dwf_data) + + composite_pattern = composite.copy() + del composite_pattern['AverageValue'] + pattern_mean = composite_pattern.T.mean() + + self.assertEqual(composite.index, ['KRO3001']) + self.assertAlmostEqual(pattern_mean['KRO3001'], 1, 4) + + self.assertEqual(set(swn.patterns.index), + set(['DWF', 'Pat1', 'KRO3001_composite'])) + self.assertAlmostEqual(swn.dwf.loc['KRO3001', 'AverageValue'], + composite.loc['KRO3001', 'AverageValue'], 4) + + +@unittest.skipIf(not has_swmmio, + "Cannot test SWNTR capabilities: swmmio is missing") +class TestStormWaterSim(unittest.TestCase): + + @classmethod + def setUpClass(self): + inpfile = join(ex_datadir, "Site_Drainage_Model.inp") + swn = swntr.network.StormWaterNetworkModel(inpfile) + self.supported_sections = set(swn.section_names) + self.tested_sections = set() + self.tested_rpt_sections = set() + + @classmethod + def tearDownClass(self): + untested_sections = self.supported_sections - self.tested_sections + print('untested sections', untested_sections) + #print('rpt sections', self.tested_rpt_sections) + + def test_simulation(self): + # Run swmm using + # 1. direct use of pyswmm, stepwise simulation + # 2. direct use of swmmio cmd + # 3. swmmio cmd with INP file read/write + # 4. swntr with INP file read/write + inpfiles = [ + # SWMMIO INP test files + swmmio.tests.data.MODEL_FULL_FEATURES_PATH, + #swmmio.tests.data.MODEL_CURVE_NUMBER, # pyswmm fails + #swmmio.tests.data.MODEL_MOD_HORTON, # pyswmm fails + swmmio.tests.data.MODEL_GREEN_AMPT, + + # SWMM INP example files + #'Culvert.inp', # pyswmm fails + 'Detention_Pond_Model.inp', + #'Groundwater_Model.inp', # pyswmm fails (rpt file does not finish writing). swmmio results in empty link results + 'Inlet_Drains_Model.inp', + #'LID_Model.inp', # pyswmm fails (rpt file does not finish writing). swmmio results in empty link results + 'Pump_Control_Model.inp', + 'Site_Drainage_Model.inp', + ] + + for inpfile_name in inpfiles: + print(inpfile_name) + + if inpfile_name in ['Pump_Control_Model.inp', 'Site_Drainage_Model.inp']: + inpfile = join(ex_datadir, inpfile_name) + else: + inpfile = join(test_datadir, "SWMM_examples", inpfile_name) + print(inpfile) + rootname = inpfile.split('.inp')[0] + outfile = join(test_datadir, rootname+'.out') + + temp_inpfile = 'temp.inp' + temp_outfile = 'temp.out' + + # Direct use of INP file with pyswmm + print(" run pyswmm") + if isfile(outfile): + os.remove(outfile) + with pyswmm.Simulation(inpfile) as sim: + for step in sim: + pass + sim.report() + + results_pyswmm = swntr.io.read_outfile(outfile) + + # swmmio with saved INP file + # No model sections are flagged for rewrite + # print(" run swmmio") + # if isfile(temp_inpfile): + # os.remove(temp_inpfile) + # if isfile(temp_outfile): + # os.remove(temp_outfile) + # swmmio_model = swmmio.Model(inpfile) + # swmmio_model.inp.save(temp_inpfile) + # p = subprocess.run("python -m swmmio --run " + temp_inpfile) + # results_swmmio = swntr.io.read_outfile(temp_outfile) + + # swntr + # All model sections are flagged for rewrite + print(" run swntr") + if isfile(temp_inpfile): + os.remove(temp_inpfile) + if isfile(temp_outfile): + os.remove(temp_outfile) + swn = swntr.network.StormWaterNetworkModel(inpfile) + sim = swntr.sim.SWMMSimulator(swn) + results_swntr = sim.run_sim() + + for sec in swn.section_names: + df = getattr(swn, sec) + if df.shape[0] > 0: + self.tested_sections.add(sec) + + for sec in results_swntr.report.keys(): + self.tested_rpt_sections.add(sec) + + # Compare direct methods to swmmio and swntr, node total inflow + #assert_frame_equal(results_pyswmm.node['TOTAL_INFLOW'], + # results_swmmio.node['TOTAL_INFLOW']) + assert_frame_equal(results_pyswmm.node['TOTAL_INFLOW'], + results_swntr.node['TOTAL_INFLOW']) + + # Compare direct methods to swmmio and swntr, link capacity + #assert_frame_equal(results_pyswmm.link['CAPACITY'], + # results_swmmio.link['CAPACITY']) + assert_frame_equal(results_pyswmm.link['CAPACITY'], + results_swntr.link['CAPACITY']) + + def test_report_summary(self): + inpfile = join(ex_datadir, "Site_Drainage_Model.inp") + swn = swntr.network.StormWaterNetworkModel(inpfile) + sim = swntr.sim.SWMMSimulator(swn) + results = sim.run_sim() + report = results.report + assert 'Node Depth Summary' in report.keys() + assert 'MaxNodeDepth' in report['Node Depth Summary'].columns + assert set(report['Node Depth Summary'].index) == set(swn.node_name_list) + + +@unittest.skipIf(not has_swmmio, + "Cannot test SWNTR capabilities: swmmio is missing") +class TestStormWaterScenarios(unittest.TestCase): + + def test_conduit_reduced_flow(self): + conduit_name = 'C1' + max_flow1 = 0.001 + + inpfile = join(ex_datadir, "Site_Drainage_Model.inp") + swn1 = swntr.network.StormWaterNetworkModel(inpfile) + swn1.conduits.loc[conduit_name, "MaxFlow"] = max_flow1 + + # Test ability to modify INP file + inpfile = join(testdir, "temp_Site_Drainage_Model.inp") + swntr.io.write_inpfile(swn1, inpfile) + swn2 = swntr.network.StormWaterNetworkModel(inpfile) + max_flow2 = swn2.conduits.loc[conduit_name, "MaxFlow"] + assert max_flow1 == max_flow2 + + # Test simulation results + sim = swntr.sim.SWMMSimulator(swn1) + results_swntr = sim.run_sim() + + average_flow_rate = results_swntr.link['FLOW_RATE'].loc[:, conduit_name].mean() + self.assertAlmostEqual(average_flow_rate, max_flow1, 4) + + def test_pump_outage(self): + pump_name = 'PUMP1' + start_time = 4.5 + end_time = 12 + + inpfile = join(ex_datadir, "Pump_Control_Model.inp") + swn1 = swntr.network.StormWaterNetworkModel(inpfile) + assert swn1.controls.shape[0] == 2 + swn1.add_pump_outage_control(pump_name, start_time, end_time) # Outage times in decimal hours + assert swn1.controls.shape[0] == 3 + + # Test ability to modify INP file + inpfile = join(testdir, "temp_Pump_Control_Model.inp") + swntr.io.write_inpfile(swn1, inpfile) + swn2 = swntr.network.StormWaterNetworkModel(inpfile) + assert swn2.controls.shape[0] == 3 + control_name = 'RULE ' + pump_name + '_outage' + assert control_name in swn2.controls.index + + # Test simulation results + sim = swntr.sim.SWMMSimulator(swn1) + results_swntr = sim.run_sim() + + # Pump flowrate over the entire simulation is not 0 + flow_rate = results_swntr.link['FLOW_RATE'].loc[:, pump_name] + assert flow_rate.mean() != 0 + + # Pump flowrate during the outage is 0 + start_datetime = flow_rate.index[0] + + pd.Timedelta(str(start_time) + " hours") + end_datetime = flow_rate.index[0] + + pd.Timedelta(str(end_time-0.001) + " hours") + flow_rate_outage = results_swntr.link['FLOW_RATE'].loc[start_datetime:end_datetime, pump_name] + self.assertAlmostEqual(flow_rate_outage.mean(), 0, 4) + + +@unittest.skipIf(not has_swmmio, + "Cannot test SWNTR capabilities: swmmio is missing") +class TestStormWaterMetrics(unittest.TestCase): + + @classmethod + def setUpClass(self): + inpfile = join(ex_datadir, "Site_Drainage_Model.inp") + self.swn = swntr.network.StormWaterNetworkModel(inpfile) + sim = swntr.sim.SWMMSimulator(self.swn) + self.results = sim.run_sim() + flowrate = self.results.link['FLOW_RATE'].mean() + self.G = self.swn.to_graph(link_weight=flowrate, modify_direction=True) + + @classmethod + def tearDownClass(self): + pass + + def test_upstream_nodes(self): + nodes = swntr.metrics.upstream_nodes(self.G, 'J8') + solution = ['J1', 'J3', 'J4', 'J5', 'J6', 'J7', 'J8'] + assert len(nodes) == len(solution) + assert set(nodes) == set(solution) + + def test_upstream_edges(self): + edges = swntr.metrics.upstream_edges(self.G, 'J8') + solution = ['C1', 'C3', 'C4', 'C5', 'C6', 'C7'] + assert len(edges) == len(solution) + assert set(edges) == set(solution) + + def test_downstream_nodes(self): + nodes = swntr.metrics.downstream_nodes(self.G, 'J5') + solution = ['J5', 'J6', 'J8', 'J9', 'J10', 'J11', 'O1'] + assert len(nodes) == len(solution) + assert set(nodes) == set(solution) + + def test_downstream_edges(self): + edges = swntr.metrics.downstream_edges(self.G, 'J5') + solution = ['C5', 'C7', 'C8', 'C9', 'C10', 'C11'] + assert len(edges) == len(solution) + assert set(edges) == set(solution) + + def test_shortest_path_nodes(self): + nodes = swntr.metrics.shortest_path_nodes(self.G, 'J4', 'J8') + solution = ['J4', 'J5', 'J6', 'J8'] + assert len(nodes) == len(solution) + assert set(nodes) == set(solution) + + def test_shortest_path_edges(self): + edges = swntr.metrics.shortest_path_edges(self.G, 'J4', 'J8') + solution = ['C4', 'C5', 'C7'] + assert len(edges) == len(solution) + assert set(edges) == set(solution) + + def test_pump_headloss_power_energy(self): + inpfile = join(ex_datadir, "Pump_Control_Model.inp") + swn = swntr.network.StormWaterNetworkModel(inpfile) + flow_units = swn.options.loc['FLOW_UNITS', 'Value'] + + sim = swntr.sim.SWMMSimulator(swn) + results = sim.run_sim() + + pump_flowrate = results.link['FLOW_RATE'].loc[:, swn.pump_name_list] + head = results.node['HYDRAULIC_HEAD'] + + pump_headloss = swntr.metrics.headloss(head, swn, swn.pump_name_list) + pump_power = swntr.metrics.pump_power(pump_flowrate, pump_headloss, flow_units) + pump_energy = swntr.metrics.pump_energy(pump_flowrate, pump_headloss, flow_units) + + pump_name = swn.pump_name_list[0] + from_metrics = pump_energy[pump_name].sum() + from_rpt = results.report['Pumping Summary'].loc[pump_name,'PowerUsage(kW-hr)'] + + self.assertAlmostEqual(from_metrics, from_rpt, 1) + + def test_conduit_available_volume(self): + # Network values + volume = self.swn.conduit_volume + capacity = self.results.link['CAPACITY'] + available_volume = swntr.metrics.conduit_available_volume(volume, capacity) + fraction = available_volume/volume + del fraction['C2'] # no volume + assert (fraction <= 1).all().all() + + # Simplified values + volume = pd.Series([1000], index=['C1']) + capacity = pd.DataFrame([1, 0.75, 0.5, 0.25, 0], index=[0,1,2,3,4], columns=['C1']) + available_volume = swntr.metrics.conduit_available_volume(volume, capacity) + assert (available_volume['C1'].values == [0,250,500,750,1000]).all() + + available_volume = swntr.metrics.conduit_available_volume(volume, capacity, 0.5) + # remove nan + temp = available_volume['C1'] + assert temp.isna().sum() == 2 + temp = temp[~temp.isna()] + assert (temp.values == [0,250,500]).all() + + def test_conduit_travel_time(self): + # Network values + length = self.swn.conduits['Length'] + velocity = self.results.link['FLOW_VELOCITY'] + + travel_time = swntr.metrics.conduit_travel_time(length, velocity) + + assert travel_time.shape == velocity.shape + + # Simplified values + length = pd.Series([1000], index=['C1']) + velocity = pd.DataFrame([1, 2, 5, 10, 50], + index=[0,1,2,3,4], + columns=['C1']) + + travel_time = swntr.metrics.conduit_travel_time(length, velocity) + + assert (travel_time['C1'].values == [1000,500,200,100,20]).all() + + def test_conduit_time_to_capacity(self): + available_volume = pd.Series([0, 250, 500, 750, 1000], + index=['C1', 'C2', 'C3', 'C4', 'C5']) + flowrate = pd.Series([1, 2, 5, 10, 50], + index=['C1', 'C2', 'C3', 'C4', 'C5']) + flow_units = 'CFS' # no conversion + + # When connected = True, time_to_capacity = available_volume/flowrate + # One value per timestep + time_to_capacity = swntr.metrics.conduit_time_to_capacity(available_volume, + flowrate, + flow_units, + connected=False) + assert (time_to_capacity.values == [0,125,100,75,20]).all() + + # When connected = True, time_to_capacity = sum(available_volume)/max(flowrate) + time_to_capacity = swntr.metrics.conduit_time_to_capacity(available_volume, + flowrate, + flow_units, + connected=True) + assert time_to_capacity == 50 + + def test_shortest_path_metrics(self): + swn = self.swn + G = self.G + results = self.results + + length = swn.conduits.loc[:,'Length'] + cross_section = swn.conduit_cross_section + volume = swn.conduit_volume + flow_units = swn.options.loc['FLOW_UNITS', 'Value'] + + flowrate = results.link['FLOW_RATE'].mean() + capacity = results.link['CAPACITY'].mean() + available_volume = swntr.metrics.conduit_available_volume(volume, capacity) + time_to_capacity = swntr.metrics.conduit_time_to_capacity(available_volume, flowrate, flow_units) + + # Shortest path + source_node = 'J4' + target_node = 'J8' + edge_list = swntr.metrics.shortest_path_edges(G, source_node, target_node) + + # Shortest path metrics + total_length = length[edge_list].sum() + total_volume = volume[edge_list].sum() + total_available_volume = available_volume[edge_list].sum() + + time_to_capacity = time_to_capacity[edge_list] + total_time_to_capacity = swntr.metrics.conduit_time_to_capacity(available_volume[edge_list], + flowrate[edge_list], + flow_units, + connected=True) + + self.assertAlmostEqual(total_length, 435.0, 1) + self.assertAlmostEqual(total_volume, 9074.0, 1) + self.assertAlmostEqual(total_available_volume, 8958.8, 1) + self.assertAlmostEqual(total_time_to_capacity, 7675.9, 1) + + +@unittest.skipIf(not has_swmmio, + "Cannot test SWNTR capabilities: swmmio is missing") +class TestStormWaterGIS(unittest.TestCase): + + @classmethod + def setUpClass(self): + inpfile = join(ex_datadir, "Site_Drainage_Model.inp") + self.swn = swntr.network.StormWaterNetworkModel(inpfile) + + @classmethod + def tearDownClass(self): + pass + + def test_create_gis_object(self): + swn_gis = self.swn.to_gis() + + fig, ax = plt.subplots() + swn_gis.subcatchments.boundary.plot(ax=ax) + swn_gis.junctions.plot(column="InvertElev", ax=ax) + swn_gis.conduits.plot(column="MaxFlow", ax=ax) + + assert swn_gis.subcatchments.shape == (7,8) + assert 'geometry' in swn_gis.subcatchments.columns + assert swn_gis.junctions.shape == (11,6) + assert 'geometry' in swn_gis.junctions.columns + assert swn_gis.conduits.shape == (11,9) + assert 'geometry' in swn_gis.conduits.columns + + def test_write_geojson(self): + valid_components = ["junctions", "outfalls", "conduits", "subcatchments"] + for name in valid_components: + filename = abspath(join(testdir, "temp_"+name+".geojson")) + if isfile(filename): + os.remove(filename) + + file_root = abspath(join(testdir, "temp")) + swntr.io.write_geojson(self.swn, file_root) + + for name in valid_components: + filename = file_root+"_"+name+".geojson" + self.assertTrue(isfile(filename)) + + +@unittest.skipIf(not has_swmmio, + "Cannot test SWNTR capabilities: swmmio is missing") +class TestStormWaterGraphics(unittest.TestCase): + + @classmethod + def setUpClass(self): + inpfile = join(ex_datadir, "Site_Drainage_Model.inp") + self.swn = swntr.network.StormWaterNetworkModel(inpfile) + + @classmethod + def tearDownClass(self): + pass + + def test_plot_network1(self): + # Basic network plot + filename = abspath(join(testdir, "plot_network1_swmm.png")) + if isfile(filename): + os.remove(filename) + + plt.figure() + swntr.graphics.plot_network(self.swn) + plt.savefig(filename, format="png") + plt.close() + + self.assertTrue(isfile(filename)) + + def test_plot_network2(self): + # Node and link attributes + filename = abspath(join(testdir, "plot_network2_swmm.png")) + if isfile(filename): + os.remove(filename) + + plt.figure() + swntr.graphics.plot_network(self.swn, + node_attribute="InvertElev", + link_attribute="Length", + subcatchment_attribute='PercImperv') + plt.savefig(filename, format="png") + plt.close() + + self.assertTrue(isfile(filename)) + + def test_plot_network3(self): + # List of node and link names + filename = abspath(join(testdir, "plot_network3_swmm.png")) + if isfile(filename): + os.remove(filename) + + plt.figure() + swntr.graphics.plot_network(self.swn, + node_attribute=["J1", "J4"], + link_attribute=["C3", "C5"], + link_labels=True) + plt.savefig(filename, format="png") + plt.close() + + self.assertTrue(isfile(filename)) + + def test_plot_network4(self): + # Dictionary of attributes + filename = abspath(join(testdir, "plot_network4_swmm.png")) + if isfile(filename): + os.remove(filename) + + plt.figure() + swntr.graphics.plot_network(self.swn, + node_attribute={"J1": 5, "J4": 10}, + link_attribute={"C3": 3, "C5": 9}, + node_labels=True) + plt.savefig(filename, format="png") + plt.close() + + self.assertTrue(isfile(filename)) + + def test_plot_network5(self): + # Series, range and title + filename = abspath(join(testdir, "plot_network5_swmm.png")) + if isfile(filename): + os.remove(filename) + + G = self.swn.to_graph() + node_degree = pd.Series(dict(nx.degree(G))) + + plt.figure() + swntr.graphics.plot_network(self.swn, + node_attribute=node_degree, + node_range=[1, 4], + title="Node degree") + plt.savefig(filename, format="png") + plt.close() + + self.assertTrue(isfile(filename)) + + def test_plot_network6(self): + # Anonymize coordinates + filename = abspath(join(testdir, "plot_network6_swmm.png")) + if isfile(filename): + os.remove(filename) + + coordinates = self.swn.anonymize_coordinates(seed=12345) + + G = self.swn.to_graph() + node_degree = pd.Series(dict(nx.degree(G))) + + plt.figure() + swntr.graphics.plot_network(self.swn, + node_attribute=node_degree, + node_range=[1, 4], + title="Anonymized coordinates") + plt.savefig(filename, format="png") + plt.close() + + self.assertTrue(isfile(filename)) + + +if __name__ == "__main__": + unittest.main()