├── swmmio ├── reporting │ ├── __init__.py │ ├── utils.py │ ├── serialize.py │ ├── functions.py │ ├── basemaps │ │ ├── index.html │ │ ├── compare.html │ │ ├── mapbox_base.html │ │ └── mapbox_base_comparison.html │ └── batch.py ├── utils │ ├── __init__.py │ ├── error.py │ └── modify_model.py ├── vendor │ └── __init__.py ├── run_models │ ├── __init__.py │ ├── start_pool.py │ └── run.py ├── version_control │ ├── tests │ │ ├── __init__.py │ │ ├── validate.py │ │ └── compare_inp.py │ ├── meta_data_schema.json │ ├── __init__.py │ ├── utils.py │ └── version_control.py ├── graphics │ ├── fonts │ │ └── Verdana.ttf │ ├── __init__.py │ ├── utils.py │ ├── swmm_graphics.py │ └── drawing.py ├── tests │ ├── data │ │ ├── outfalls_modified_10.csv │ │ ├── df_test_coordinates.csv │ │ ├── model_blank_01.inp │ │ ├── test_build_instructions_01.txt │ │ ├── __init__.py │ │ ├── model_blank.inp │ │ ├── outfalls_issue.inp │ │ ├── outfalls_issue_free_first.inp │ │ ├── alt_test1.inp │ │ ├── baseline_test.inp │ │ ├── alt_test2.inp │ │ ├── alt_test3.inp │ │ ├── groundwater_model.inp │ │ └── Example6.inp │ ├── __init__.py │ ├── test_spatial.py │ ├── test_run_models.py │ ├── test_functions.py │ └── test_model_elements.py ├── damage │ ├── __init__.py │ └── parcels.py ├── defs │ ├── default.prj │ ├── __init__.py │ ├── config.py │ ├── sectionheaders.py │ ├── constants.py │ ├── inp_sections.yml │ └── section_headers.yml ├── __init__.py ├── examples.py ├── wrapper │ └── pyswmm_wrapper.py └── __main__.py ├── docs ├── changelog.md ├── _static │ └── img │ │ ├── default_draw.png │ │ ├── impact_of_option.png │ │ ├── swmm-zoom-graphic.png │ │ └── flooded_anno_example.png ├── reference │ ├── swmmio_inp.rst │ ├── swmmio_rpt.rst │ ├── swmmio_model.rst │ ├── utils │ │ ├── text.rst │ │ ├── spatial.rst │ │ ├── functions.rst │ │ ├── dataframes.rst │ │ ├── modify_model.rst │ │ └── index.rst │ ├── elements.rst │ ├── graphics │ │ ├── utils.rst │ │ ├── drawing.rst │ │ ├── swmm_graphics.rst │ │ └── index.rst │ ├── version_control │ │ ├── inp.rst │ │ ├── utils.rst │ │ ├── version_control.rst │ │ └── index.rst │ └── index.rst ├── usage │ └── index.md ├── requirements.txt ├── Makefile ├── make.bat ├── index.ipynb └── conf.py ├── pytest.ini ├── requirements.txt ├── tools └── update-authors.sh ├── MANIFEST.in ├── .mailmap ├── .gitignore ├── AUTHORS ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── RELEASE.md ├── .github └── workflows │ ├── documentation.yml │ └── python-app.yml └── setup.py /swmmio/reporting/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /swmmio/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /swmmio/vendor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /swmmio/run_models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /swmmio/version_control/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ```{include} ../CHANGELOG.md 4 | 5 | ``` -------------------------------------------------------------------------------- /docs/_static/img/default_draw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyswmm/swmmio/HEAD/docs/_static/img/default_draw.png -------------------------------------------------------------------------------- /swmmio/graphics/fonts/Verdana.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyswmm/swmmio/HEAD/swmmio/graphics/fonts/Verdana.ttf -------------------------------------------------------------------------------- /docs/_static/img/impact_of_option.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyswmm/swmmio/HEAD/docs/_static/img/impact_of_option.png -------------------------------------------------------------------------------- /swmmio/tests/data/outfalls_modified_10.csv: -------------------------------------------------------------------------------- 1 | Name,InvertElev,OutfallType,StageOrTimeseries,TideGate 2 | J4,10,FIXED,NO, 3 | -------------------------------------------------------------------------------- /docs/_static/img/swmm-zoom-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyswmm/swmmio/HEAD/docs/_static/img/swmm-zoom-graphic.png -------------------------------------------------------------------------------- /docs/_static/img/flooded_anno_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyswmm/swmmio/HEAD/docs/_static/img/flooded_anno_example.png -------------------------------------------------------------------------------- /docs/reference/swmmio_inp.rst: -------------------------------------------------------------------------------- 1 | swmmio.inp 2 | ======================== 3 | 4 | .. autoclass:: swmmio.core.inp 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/reference/swmmio_rpt.rst: -------------------------------------------------------------------------------- 1 | swmmio.rpt 2 | ======================== 3 | 4 | .. autoclass:: swmmio.core.rpt 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | uses_geopandas: marks tests that use Geopandas (deselect with '-m "not uses_geopandas"') 4 | serial 5 | -------------------------------------------------------------------------------- /docs/reference/swmmio_model.rst: -------------------------------------------------------------------------------- 1 | swmmio.Model 2 | ======================== 3 | 4 | .. autoclass:: swmmio.core.Model 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /swmmio/version_control/meta_data_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "Date of Build":0, 3 | "Baseline Model":0, 4 | "ID":0, 5 | "Parent Models":0, 6 | "Comments":0 7 | } 8 | -------------------------------------------------------------------------------- /docs/reference/utils/text.rst: -------------------------------------------------------------------------------- 1 | text 2 | ------------------------ 3 | 4 | .. automodule:: swmmio.utils.text 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/reference/elements.rst: -------------------------------------------------------------------------------- 1 | swmmio.elements 2 | ======================== 3 | 4 | .. automodule:: swmmio.elements 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/reference/utils/spatial.rst: -------------------------------------------------------------------------------- 1 | spatial 2 | --------------------------- 3 | 4 | .. automodule:: swmmio.utils.spatial 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/reference/graphics/utils.rst: -------------------------------------------------------------------------------- 1 | utils 2 | ---------------------------- 3 | 4 | .. automodule:: swmmio.graphics.utils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/reference/graphics/drawing.rst: -------------------------------------------------------------------------------- 1 | drawing 2 | ------------------------------ 3 | 4 | .. automodule:: swmmio.graphics.drawing 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/reference/utils/functions.rst: -------------------------------------------------------------------------------- 1 | functions 2 | ----------------------------- 3 | 4 | .. automodule:: swmmio.utils.functions 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/usage/index.md: -------------------------------------------------------------------------------- 1 | # User Guide 2 | 3 | ```{toctree} 4 | --- 5 | maxdepth: 2 6 | --- 7 | getting_started 8 | working_with_pyswmm 9 | visualizing_models 10 | making_art_with_swmm 11 | ``` -------------------------------------------------------------------------------- /docs/reference/utils/dataframes.rst: -------------------------------------------------------------------------------- 1 | dataframes 2 | ------------------------------ 3 | 4 | .. automodule:: swmmio.utils.dataframes 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/reference/version_control/inp.rst: -------------------------------------------------------------------------------- 1 | inp 2 | ---------------------------------- 3 | 4 | .. automodule:: swmmio.version_control.inp 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_design==0.5.0 2 | pydata-sphinx-theme==0.15.2 3 | sphinx==7.2.6 4 | sphinx_copybutton==0.5.2 5 | ipython==8.23.0 6 | myst-parser==3.0.1 7 | myst-nb==1.1.2 8 | numpydoc==1.8.0 9 | -------------------------------------------------------------------------------- /docs/reference/utils/modify_model.rst: -------------------------------------------------------------------------------- 1 | modify\_model 2 | --------------------------------- 3 | 4 | .. automodule:: swmmio.utils.modify_model 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/reference/version_control/utils.rst: -------------------------------------------------------------------------------- 1 | utils 2 | ------------------------------------ 3 | 4 | .. automodule:: swmmio.version_control.utils 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /swmmio/version_control/__init__.py: -------------------------------------------------------------------------------- 1 | from .inp import * 2 | from .version_control import * 3 | __all__ = ['BuildInstructions', 'INPSectionDiff', 'INPDiff', 'propagate_changes_from_baseline', 'create_combinations'] -------------------------------------------------------------------------------- /docs/reference/graphics/swmm_graphics.rst: -------------------------------------------------------------------------------- 1 | swmm\_graphics 2 | ------------------------------------- 3 | 4 | .. automodule:: swmmio.graphics.swmm_graphics 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/reference/version_control/version_control.rst: -------------------------------------------------------------------------------- 1 | version\_control 2 | ----------------------------------------------- 3 | 4 | .. automodule:: swmmio.version_control.version_control 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/reference/graphics/index.rst: -------------------------------------------------------------------------------- 1 | graphics 2 | ==================== 3 | 4 | .. only:: html 5 | 6 | :Release: |release| 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | swmm_graphics 12 | drawing 13 | utils 14 | 15 | .. only:: html -------------------------------------------------------------------------------- /docs/reference/version_control/index.rst: -------------------------------------------------------------------------------- 1 | version control 2 | ==================== 3 | 4 | .. only:: html 5 | 6 | :Release: |release| 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | inp 12 | version_control 13 | utils 14 | 15 | .. only:: html -------------------------------------------------------------------------------- /docs/reference/utils/index.rst: -------------------------------------------------------------------------------- 1 | utils 2 | ==================== 3 | 4 | .. only:: html 5 | 6 | :Release: |release| 7 | 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | dataframes 13 | functions 14 | spatial 15 | text 16 | modify_model 17 | 18 | .. only:: html -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Build dependencies 2 | pytest 3 | Pillow==10.3.0 4 | numpy>=1.16.4 5 | pandas>=0.24.2 6 | pyshp==2.3.1 7 | geojson==2.5.0 8 | networkx>=2.3 9 | pyyaml>=3.12 10 | requests==2.32.4 11 | typing_extensions==4.12.2 12 | 13 | # Run dependencies 14 | pyproj>=3.0.0 15 | geopandas 16 | matplotlib 17 | pyswmm>=1.2 -------------------------------------------------------------------------------- /swmmio/tests/data/df_test_coordinates.csv: -------------------------------------------------------------------------------- 1 | Name,X,Y 2 | J3,2748073.306,1117746.087 3 | 1,2746913.127,1118559.809 4 | 2,2747728.148,1118449.164 5 | 3,2747242.131,1118656.381 6 | 4,2747345.325,1118499.807 7 | 5,2747386.555,1118362.817 8 | J2,2747514.212,1118016.207 9 | J4,2748515.571,1117763.466 10 | J1,2747402.678,1118092.704 11 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | .. -*- coding: utf-8 -*- 2 | 3 | Reference 4 | ********* 5 | .. only:: html 6 | 7 | :Release: |release| 8 | 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | swmmio_model 14 | swmmio_inp 15 | swmmio_rpt 16 | elements 17 | utils/index 18 | graphics/index 19 | version_control/index 20 | 21 | .. only:: html -------------------------------------------------------------------------------- /swmmio/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright (c) 2018 Adam Erispaha 4 | # 5 | # Licensed under the terms of the BSD2 License 6 | # See LICENSE.txt for details 7 | # ----------------------------------------------------------------------------- 8 | """Main tests for the swmmio""" 9 | -------------------------------------------------------------------------------- /tools/update-authors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## 4 | ## This script will auto-generate the AUTHORS attribution file. 5 | ## If your name does not display correctly, then please 6 | ## update the .mailmap file in the root repo directory 7 | ## 8 | 9 | echo '# Contributing authors listed in alphabetical order:\n' > ../AUTHORS 10 | git log --reverse --format='%aN <%aE>' | sort -u >> ../AUTHORS 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include swmmio/run_models/inp_settings/*.txt 2 | include lib/windows/swmm5_22.exe 3 | include lib/linux/swmm5 4 | include swmmio/reporting/basemaps/*.html 5 | include swmmio/graphics/fonts/*.ttf 6 | include swmmio/defs/*.json 7 | include swmmio/defs/*.yml 8 | include swmmio/tests/data/*.inp 9 | include swmmio/tests/data/*.rpt 10 | include swmmio/tests/data/*.csv 11 | include swmmio/tests/data/*.txt -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Abhiram Mullapudi 2 | Adam Erispaha 3 | Adam Erispaha 4 | Adam Erispaha aerispaha 5 | Jackie Fortin-Flefil 6 | Assela Pathirana 7 | Bruce Rindahl -------------------------------------------------------------------------------- /swmmio/damage/__init__.py: -------------------------------------------------------------------------------- 1 | from swmmio.defs.constants import red, purple, lightblue, lightgreen 2 | 3 | FLOOD_IMPACT_CATEGORIES = { 4 | 'increased_flooding': { 5 | 'fill': red, 6 | }, 7 | 'new_flooding': { 8 | 'fill': purple, 9 | }, 10 | 'decreased_flooding': { 11 | 'fill': lightblue, 12 | }, 13 | 'eliminated_flooding': { 14 | 'fill': lightgreen, 15 | }, 16 | 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | private.py 3 | private/ 4 | # Setuptools distribution folder. 5 | /dist/ 6 | /build/ 7 | # Python egg metadata, regenerated from source files by setuptools. 8 | /*.egg-info 9 | notes/ 10 | .DS_Store 11 | notebooks/ 12 | __pycache__/ 13 | .cache/ 14 | .pytest_cache/ 15 | _build/ 16 | 17 | # IDE Stuff 18 | *.idea/ 19 | 20 | # SWMM files 21 | *.out 22 | #*.ini 23 | *.chi 24 | *.~inp 25 | *.thm 26 | 27 | # env 28 | venv/ 29 | 30 | # docs artifacts 31 | docs/_build/ 32 | docs/site/ -------------------------------------------------------------------------------- /swmmio/tests/data/model_blank_01.inp: -------------------------------------------------------------------------------- 1 | 2 | [TITLE] 3 | 4 | [OPTIONS] 5 | 6 | [FILES] 7 | 8 | [RAINGAGES] 9 | 10 | [LOSSES] 11 | 12 | [CONDUITS] 13 | 14 | [INFILTRATION] 15 | 16 | [JUNCTIONS] 17 | 18 | [DWF] 19 | 20 | [ORIFICES] 21 | 22 | [OUTFALLS] 23 | 24 | [PUMPS] 25 | 26 | [STORAGE] 27 | 28 | [SUBCATCHMENTS] 29 | 30 | [SUBAREAS] 31 | 32 | [WEIRS] 33 | 34 | [XSECTIONS] 35 | 36 | [COORDINATES] 37 | 38 | [VERTICES] 39 | 40 | [Polygons] 41 | 42 | [MAP] 43 | 44 | [REPORT] 45 | 46 | [TAGS] 47 | -------------------------------------------------------------------------------- /swmmio/defs/default.prj: -------------------------------------------------------------------------------- 1 | PROJCS["NAD_1983_StatePlane_Pennsylvania_South_FIPS_3702_Feet",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["False_Easting",1968500.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-77.75],PARAMETER["Standard_Parallel_1",39.93333333333333],PARAMETER["Standard_Parallel_2",40.96666666666667],PARAMETER["Latitude_Of_Origin",39.33333333333334],UNIT["Foot_US",0.3048006096012192]] -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Contributing authors listed below: 2 | 3 | Abhiram Mullapudi 4 | Adam Erispaha 5 | Assela Pathirana 6 | Bruce Rindahl 7 | Bryant E. McDonnell 8 | BuczynskiRafal 9 | David Irwin 10 | barneydobson 11 | Jackie Fortin-Flefil 12 | Jenn Wu 13 | Stijn Van Hoey 14 | algchyhao <35864573+algchyhao@users.noreply.github.com> 15 | chuwenhao123 <51478550+chuwenhao123@users.noreply.github.com> 16 | everett 17 | kaklise 18 | -------------------------------------------------------------------------------- /swmmio/tests/test_spatial.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import tempfile 3 | import os 4 | 5 | from swmmio.examples import philly 6 | 7 | 8 | class TestSpatialFunctions(unittest.TestCase): 9 | def setUp(self) -> None: 10 | self.test_dir = tempfile.gettempdir() 11 | 12 | def test_write_shapefile(self): 13 | with tempfile.TemporaryDirectory() as tmp_dir: 14 | 15 | philly.export_to_shapefile(tmp_dir) 16 | nodes_path = os.path.join(tmp_dir, f'{philly.name}_nodes.shp') 17 | links_path = os.path.join(tmp_dir, f'{philly.name}_conduits.shp') 18 | 19 | self.assertTrue(os.path.exists(nodes_path)) 20 | self.assertTrue(os.path.exists(links_path)) 21 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Required 2 | version: 2 3 | 4 | build: 5 | os: ubuntu-22.04 6 | tools: 7 | python: "3.10" 8 | 9 | # Build documentation in the docs/ directory with Sphinx 10 | sphinx: 11 | configuration: docs/conf.py 12 | 13 | # Build documentation with MkDocs 14 | #mkdocs: 15 | # configuration: mkdocs.yml 16 | 17 | # Optionally build your docs in additional formats such as PDF 18 | #formats: 19 | # - pdf 20 | 21 | # Optionally set the version of Python and requirements required to build your docs 22 | python: 23 | install: 24 | - requirements: docs/requirements.txt 25 | - requirements: requirements.txt 26 | - method: setuptools 27 | path: . 28 | 29 | submodules: 30 | include: all 31 | -------------------------------------------------------------------------------- /swmmio/__init__.py: -------------------------------------------------------------------------------- 1 | from swmmio.core import * 2 | from swmmio.elements import * 3 | from swmmio.version_control import * 4 | from swmmio.utils.dataframes import dataframe_from_bi, dataframe_from_rpt, dataframe_from_inp 5 | from swmmio.utils.functions import find_network_trace 6 | from swmmio.graphics.swmm_graphics import create_map, draw_model 7 | from swmmio.graphics.profiler import (build_profile_plot, add_hgl_plot, 8 | add_node_labels_plot, add_link_labels_plot) 9 | 10 | # import swmmio.core as swmmio 11 | '''Python SWMM Input/Output Tools''' 12 | 13 | 14 | VERSION_INFO = (0, 8, 3, 'dev0') 15 | __version__ = '.'.join(map(str, VERSION_INFO)) 16 | __author__ = 'Adam Erispaha' 17 | __copyright__ = 'Copyright (c) 2025' 18 | __licence__ = '' 19 | -------------------------------------------------------------------------------- /swmmio/examples.py: -------------------------------------------------------------------------------- 1 | from swmmio import Model 2 | from swmmio.tests.data import (MODEL_A_PATH, MODEL_EX_1, MODEL_FULL_FEATURES_XY, 3 | MODEL_FULL_FEATURES__NET_PATH, MODEL_FULL_FEATURES_XY_B, 4 | MODEL_GREEN_AMPT, MODEL_TEST_INLET_DRAINS, MODEL_GROUNDWATER, 5 | MODEL_PUMP_CONTROL) 6 | 7 | # example models 8 | philly = Model(MODEL_A_PATH, crs="+init=EPSG:2817") 9 | jersey = Model(MODEL_FULL_FEATURES_XY) 10 | jerzey = Model(MODEL_FULL_FEATURES_XY_B) 11 | spruce = Model(MODEL_FULL_FEATURES__NET_PATH) 12 | walnut = Model(MODEL_EX_1) 13 | green = Model(MODEL_GREEN_AMPT) 14 | streets = Model(MODEL_TEST_INLET_DRAINS) 15 | groundwater = Model(MODEL_GROUNDWATER) 16 | pump_control = Model(MODEL_PUMP_CONTROL) 17 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /swmmio/defs/__init__.py: -------------------------------------------------------------------------------- 1 | # Standard library imports 2 | import os 3 | import yaml 4 | from swmmio.defs.sectionheaders import normalize_inp_config 5 | 6 | _DEFS_PATH = os.path.abspath(os.path.dirname(__file__)) 7 | _HEADERS_YAML = os.path.join(_DEFS_PATH, 'section_headers.yml') 8 | _INP_SECTIONS_YAML = os.path.join(_DEFS_PATH, 'inp_sections.yml') 9 | 10 | with open(_INP_SECTIONS_YAML, 'r') as f: 11 | _inp_sections_conf_raw = yaml.safe_load(f) 12 | 13 | with open(_HEADERS_YAML, 'r') as f: 14 | HEADERS_YML = yaml.safe_load(f) 15 | 16 | INP_OBJECTS = normalize_inp_config(_inp_sections_conf_raw['inp_file_objects']) 17 | RPT_OBJECTS = normalize_inp_config(HEADERS_YML['rpt_sections']) 18 | SWMM5_VERSION = HEADERS_YML['swmm5_version'] 19 | INP_SECTION_TAGS = _inp_sections_conf_raw['inp_section_tags'] 20 | INFILTRATION_COLS = _inp_sections_conf_raw['infiltration_cols'] 21 | COMPOSITE_OBJECTS = HEADERS_YML['composite'] 22 | INFILTRATION_KEY = 'INFILTRATION' 23 | -------------------------------------------------------------------------------- /swmmio/tests/data/test_build_instructions_01.txt: -------------------------------------------------------------------------------- 1 | { 2 | "Parent Models": { 3 | "Baseline": { 4 | "C:\\PROJECTCODE\\swmmio\\swmmio\\tests\\data\\baseline_test.inp": "18-10-12 18:11" 5 | }, 6 | "Alternatives": { 7 | "C:\\PROJECTCODE\\swmmio\\swmmio\\tests\\data\\alt_test3.inp": "18-10-12 18:11" 8 | } 9 | }, 10 | "Log": { 11 | "test_version_id": "cool comments" 12 | } 13 | } 14 | ==================================================================================================== 15 | 16 | [JUNCTIONS] 17 | ;; InvertElev MaxDepth InitDepth SurchargeDepth PondedArea ; Comment Origin 18 | dummy_node1 -15.0 30.0 0 0 0 ; Altered alt_test3.inp 19 | dummy_node5 -6.96 15.0 0 0 73511 ; Altered alt_test3.inp 20 | 21 | [CONDUITS] 22 | ;; InletNode OutletNode Length ManningN InletOffset OutletOffset InitFlow MaxFlow ; Comment Origin 23 | pipe5 dummy_node6 dummy_node5 666 0.013000000000000001 0 0 0 0 ; Altered alt_test3.inp 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2023 Adam Erispaha, and swmmio developers (See AUTHORS) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /swmmio/defs/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # This is the swmmio project root 4 | ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 5 | 6 | # path to the Python executable used to run your version of Python 7 | PYTHON_EXE_PATH = "python"#os.path.join(os.__file__.split("lib/")[0],"bin","python") 8 | 9 | # feature class name of parcels in geodatabase 10 | PARCEL_FEATURES = r'PWD_PARCELS_SHEDS_PPORT' 11 | 12 | # name of the directories in which to store post processing data 13 | REPORT_DIR_NAME = r'Report' 14 | 15 | # path to the basemap file used to create custom basemaps 16 | FONT_PATH = os.path.join(ROOT_DIR, 'swmmio', 'graphics', 'fonts', 'Verdana.ttf') 17 | 18 | # path to the default geodatabase. used for some arcpy functions and parcel calcs 19 | GEODATABASE = r'C:\Data\ArcGIS\GDBs\LocalData.gdb' 20 | 21 | # path to the basemap file used to create custom basemaps 22 | BASEMAP_PATH = os.path.join(ROOT_DIR, 'swmmio', 'reporting', 'basemaps', 'index.html') 23 | BETTER_BASEMAP_PATH = os.path.join(ROOT_DIR, 'swmmio', 'reporting', 'basemaps', 'mapbox_base.html') 24 | 25 | # PySWMM Wrapper Path 26 | PYSWMM_WRAPPER_PATH = os.path.join(ROOT_DIR, 'swmmio', 'wrapper', 'pyswmm_wrapper.py') 27 | -------------------------------------------------------------------------------- /swmmio/reporting/utils.py: -------------------------------------------------------------------------------- 1 | from swmmio.defs.config import BASEMAP_PATH 2 | import shutil 3 | 4 | #THIS STUFF IS INCOMPLETE/ MAYBE BROKEN 5 | def insert_in_file(key, string, newfile, f=BASEMAP_PATH): 6 | 7 | #make a temp copy 8 | shutil.copyfile(newfile, ) 9 | 10 | #start writing that thing 11 | key = '{}{}{}'.format('{{', key, '}}') #Django style 12 | with open(f, 'r') as bm: 13 | with open(newfile, 'wb') as newmap: 14 | for line in bm: 15 | if key in line: 16 | newline = line.replace(key, string) 17 | newmap.write(newline) 18 | # newmap.write(geojson.dumps(FeatureCollection(geometries, crs=crs))) 19 | else: 20 | newmap.write(line) 21 | 22 | def insert_in_file_2(key, string, newfile): 23 | 24 | #start writing that thing 25 | key = '{}{}{}'.format('{{', key, '}}') #Django style 26 | print(key) 27 | with open(newfile, 'r') as newmap: 28 | for line in newmap: 29 | if key in line: 30 | newline = line.replace(key, string) 31 | newmap.write(newline) 32 | # newmap.write(geojson.dumps(FeatureCollection(geometries, crs=crs))) 33 | # else: 34 | # newmap.write(line) 35 | -------------------------------------------------------------------------------- /swmmio/version_control/tests/validate.py: -------------------------------------------------------------------------------- 1 | # METHODS USED TO VALIDATE INP FILES (e.g. ensure no duplicates exists) 2 | import swmmio.utils.functions 3 | import swmmio.utils.text 4 | from swmmio.utils.dataframes import dataframe_from_inp 5 | 6 | 7 | def search_for_duplicates(inp_path, verbose=False): 8 | """ 9 | scan an inp file and determine if any element IDs are duplicated in 10 | any section. Method: count the uniques and compare to total length 11 | """ 12 | headers = swmmio.utils.text.get_inp_sections_details(inp_path)['headers'] 13 | dups_found = False 14 | for header, cols, in headers.items(): 15 | if cols != 'blob': 16 | 17 | df = dataframe_from_inp(inp_path, section=header) 18 | elements = df.index 19 | n_unique = len(elements.unique()) #number of unique elements 20 | n_total = len(elements) #total number of elements 21 | if verbose: 22 | print('{} -> (uniques, total) -> ({}, {})'.format(header, n_unique , n_total)) 23 | 24 | if n_unique != n_total: 25 | dups = ', '.join(df[df.index.duplicated()].index.unique().tolist()) 26 | print('duplicate found in {}\nsection: {}\n{}'.format(inp_path, header, dups)) 27 | dups_found = True 28 | 29 | return dups_found 30 | -------------------------------------------------------------------------------- /swmmio/utils/error.py: -------------------------------------------------------------------------------- 1 | """ 2 | Error.py contains the long noted error strings to assist the users in quickly 3 | identifying what is wrong with their code. 4 | """ 5 | 6 | NO_TRACE_FOUND = \ 7 | """ 8 | 9 | No path has been discovered between your start and end nodes. 10 | If you are sure there IS a path, look over your links to see 11 | if any need to be reversed in your INP file. The network 12 | traverse features are one-directional. Alternately, you might have 13 | parallel loop items present in the include nodes/links. 14 | 15 | """ 16 | 17 | class NoTraceFound(Exception): 18 | """ 19 | Exception raised for impossible network trace. 20 | """ 21 | def __init__(self): 22 | self.message = NO_TRACE_FOUND 23 | super().__init__() 24 | 25 | 26 | class NodeNotInInputFile(Exception): 27 | """ 28 | Exception raised for incomplete simulation. 29 | """ 30 | def __init__(self, node): 31 | self.message = "Node {} is not present in INP file.".format(node) 32 | print(self.message) 33 | super().__init__(self.message) 34 | 35 | class LinkNotInInputFile(Exception): 36 | """ 37 | Exception raised for incomplete simulation. 38 | """ 39 | def __init__(self, link): 40 | self.message = "Link {} is not present in INP file.".format(link) 41 | print(self.message) 42 | super().__init__(self.message) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swmmio 2 | *v0.8.2 (2025/06/11)* 3 | 4 | _Programmatic pre and post processing for EPA Stormwater Management Model (SWMM)_ 5 | 6 | 7 | ![workflow status](https://github.com/aerispaha/swmmio/actions/workflows/python-app.yml/badge.svg) 8 | [![Documentation Status](https://readthedocs.org/projects/swmmio/badge/?version=latest)](https://swmmio.readthedocs.io/en/latest/?badge=latest) 9 | 10 | 11 | ![image](docs/_static/img/flooded_anno_example.png) 12 | 13 | 14 | ## Introduction 15 | `swmmio` is a Python tool for engineers and hydrologists who need to supercharge their ability to modify and analyze EPA SWMM models and results. Using a familiar Pandas interface, users can replace manual procesess that used to live in spreadsheets with scripts and automation. 16 | 17 | The core `swmmio.Model` object provides accessors to related elements in the INP and RPT. For example, `swmmio.Model.subcatchments` provides a DataFrame (or GeoDataFrame) joining data from the `[SUBCATCHMENTS]` and `[SUBAREAS]` tables in the model.inp file and, if available, the `Subcatchment Runoff Summary` from the model.rpt file. 18 | 19 | Additionally, `swmmio` provides a lower-level API for reading and writing (almost) all of the sections of the model.inp file which is useful for programmatically modifying EPA SWMM models. 20 | 21 | 22 | ## Installation 23 | ```bash 24 | pip install swmmio 25 | ``` 26 | 27 | For documentation and tutorials, see our [documentation](https://swmmio.readthedocs.io/). -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | ## To release a new version of **swmmio** on GitHub and PyPi: 4 | 5 | **1.)** Ensure you have the latest version from upstream and update your fork 6 | 7 | git pull upstream master 8 | git push origin master 9 | 10 | **2.)** Update [CHANGELOG.md](https://github.com/aerispaha/swmmio/blob/master/CHANGELOG.md), using loghub 11 | 12 | `loghub aerispaha/swmmio -m -u -ilr "complete" -ilg "feature" "New Features" -ilg "enhancement" "Enhancements" -ilg "bug" "Bugs fixed"` 13 | 14 | **3.)** Copy paste the text from [CHANGELOG.temp] to [CHANGELOG.md] 15 | 16 | **4.)** Update [`swmmio/__init__.py`](https://github.com/aerispaha/swmmio/blob/master/swmmio/__init__.py) (set release version, remove 'dev0') 17 | 18 | **5.)** Update the version number in [README.md](https://github.com/aerispaha/swmmio/blob/master/README.md) 19 | 20 | **6.)** Update the AUTHORS by running the [update_authors.sh](tools/update-authors.sh) 21 | 22 | **6.)** Commit changes 23 | 24 | git add . 25 | git commit -m "Set release version" 26 | 27 | **7.)** Add release tag and push to origin 28 | 29 | git tag -a vX.X.X -m 'Release version' 30 | git push --follow-tags 31 | 32 | **7.)** Update `__init__.py` (add 'dev0' and increment minor) 33 | 34 | **8.)** Commit changes 35 | 36 | git add . 37 | git commit -m "Restore dev version" 38 | 39 | **9.)** Push changes 40 | 41 | git push upstream master 42 | git push origin master -------------------------------------------------------------------------------- /swmmio/run_models/start_pool.py: -------------------------------------------------------------------------------- 1 | from swmmio.run_models import run 2 | from swmmio import Model 3 | from multiprocessing import Pool, cpu_count 4 | from datetime import datetime 5 | import os 6 | 7 | wd = os.getcwd() 8 | log_start_time = datetime.now().strftime("%y%m%d_%H%M") 9 | 10 | 11 | def run_swmm_engine(inp_folder): 12 | 13 | logfile = os.path.join(wd, 'log_'+log_start_time+'.txt') 14 | 15 | m = Model(inp_folder) 16 | if not m.rpt_is_valid(): 17 | # if the rpt is not valid i.e. not having current, usable data: run 18 | with open (logfile, 'a') as f: 19 | now = datetime.now().strftime("%y-%m-%d %H:%M") 20 | f.write('{}: started at {} '.format(m.inp.name, now)) 21 | # print 'running {}\n'.format(m.inp.name) 22 | run.run_hot_start_sequence(m.inp.path) 23 | now = datetime.now().strftime("%y-%m-%d %H:%M") 24 | f.write(', completed at {}\n'.format(now)) 25 | else: 26 | with open (logfile, 'a') as f: 27 | f.write('{}: skipped (up-to-date)\n'.format(m.inp.name)) 28 | 29 | 30 | def main(inp_paths, cores_left): 31 | 32 | """ 33 | called from the cli: 34 | swmmio -sp DIR1, DIR2, ... -cores_left=4 35 | """ 36 | 37 | # create multiprocessing Pool object using all cores less the -cores_left 38 | # beware of setting -cores_left=0, CPU usage will max the F out 39 | pool = Pool(cpu_count() - cores_left) 40 | 41 | # create a process pool with the run_swmm_engine() func on each directory 42 | res = pool.map(run_swmm_engine, inp_paths) 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: GH Pages Sphinx Docs 2 | on: [push, pull_request] 3 | 4 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 5 | permissions: 6 | contents: read 7 | pages: write 8 | id-token: write 9 | 10 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 11 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 12 | concurrency: 13 | group: "pages" 14 | cancel-in-progress: false 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repo 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Pages 24 | uses: actions/configure-pages@v3 25 | 26 | - name: Install Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.12" 30 | cache: "pip" 31 | 32 | - name: Install swmmio 33 | run: | 34 | pip install -r requirements.txt -r docs/requirements.txt 35 | pip install -e . 36 | 37 | - name: Sphinx build 38 | run: sphinx-build docs docs/build 39 | 40 | - name: Upload artifact 41 | uses: actions/upload-artifact@v4 42 | with: 43 | # Upload build 44 | path: './docs/build' 45 | 46 | deploy: 47 | if: startsWith(github.event.ref, 'refs/tags/v') 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | runs-on: ubuntu-latest 52 | needs: build 53 | steps: 54 | - name: Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v2 57 | -------------------------------------------------------------------------------- /swmmio/defs/sectionheaders.py: -------------------------------------------------------------------------------- 1 | # ================= 2 | # DEFINE INP HEADER THAT SHOULD BE REPLACED 3 | # ================= 4 | from collections import OrderedDict 5 | 6 | 7 | def parse_inp_section_config(raw_conf): 8 | """ 9 | normalize the config information in the YAML 10 | :return: 11 | >>> from swmmio.defs import INP_OBJECTS 12 | >>> parse_inp_section_config(INP_OBJECTS['LOSSES']) 13 | OrderedDict([('columns', ['Link', 'Inlet', 'Outlet', 'Average', 'Flap Gate', 'SeepageRate'])]) 14 | """ 15 | 16 | conf = OrderedDict() 17 | if isinstance(raw_conf, list): 18 | # has a simple list, assumed to be columns 19 | conf['columns'] = raw_conf 20 | elif isinstance(raw_conf, (dict, OrderedDict)): 21 | if 'keys' in raw_conf: 22 | # object is special case like OPTIONS 23 | conf.update(raw_conf) 24 | conf['columns'] = ['Key', 'Value'] 25 | else: 26 | conf.update(raw_conf) 27 | 28 | return conf 29 | 30 | 31 | def normalize_inp_config(inp_obects): 32 | """ 33 | Unpack the config details for each inp section and organize in a standard format. 34 | This allows the YAML file to be more short hand and human readable. 35 | :param inp_obects: 36 | :return: 37 | >>> from swmmio.defs import INP_OBJECTS 38 | >>> conf = normalize_inp_config(INP_OBJECTS) 39 | >>> print(conf['JUNCTIONS']) 40 | OrderedDict([('columns', ['Name', 'InvertElev', 'MaxDepth', 'InitDepth', 'SurchargeDepth', 'PondedArea'])]) 41 | """ 42 | normalized = OrderedDict() 43 | for sect, raw_conf in inp_obects.items(): 44 | conf = parse_inp_section_config(raw_conf) 45 | normalized[sect] = conf 46 | 47 | return normalized -------------------------------------------------------------------------------- /swmmio/tests/test_run_models.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import tempfile 4 | import unittest 5 | from unittest import mock 6 | 7 | import swmmio 8 | from swmmio.examples import philly, jerzey 9 | from swmmio.run_models.run import run_simple, run_hot_start_sequence 10 | 11 | 12 | class TestRunModels(unittest.TestCase): 13 | def setUp(self) -> None: 14 | pass 15 | 16 | def test_run_simple(self): 17 | with tempfile.TemporaryDirectory() as tmp_dir: 18 | 19 | # create copy of model in temp dir 20 | inp_path = os.path.join(tmp_dir, 'philly-example.inp') 21 | philly.inp.save(inp_path) 22 | 23 | # run the model 24 | return_code = run_simple(inp_path) 25 | self.assertEqual(return_code, 0) 26 | 27 | m = swmmio.Model(inp_path) 28 | self.assertTrue(m.rpt_is_valid()) 29 | 30 | def test_run_hot_start_sequence(self): 31 | with tempfile.TemporaryDirectory() as tmp_dir: 32 | 33 | # create copy of model in temp dir 34 | inp_path = os.path.join(tmp_dir, 'philly-example.inp') 35 | philly.inp.save(inp_path) 36 | 37 | # run the model 38 | return_code = run_hot_start_sequence(inp_path) 39 | self.assertEqual(return_code, 0) 40 | 41 | m = swmmio.Model(inp_path) 42 | self.assertTrue(m.rpt_is_valid()) 43 | 44 | @mock.patch('argparse.ArgumentParser.parse_args', 45 | return_value=argparse.Namespace( 46 | model_to_run=[jerzey.inp.path] 47 | )) 48 | def test_swmmio_run(self, mock_args): 49 | from swmmio import __main__ 50 | self.assertEqual(__main__.main(), 0) 51 | 52 | 53 | -------------------------------------------------------------------------------- /swmmio/defs/constants.py: -------------------------------------------------------------------------------- 1 | #COLOR CONSTANTS 2 | red = (250, 5, 5) 3 | blue = (5, 5, 250) 4 | lightblue = (184, 217, 242) 5 | shed_blue = (0,169,230) 6 | white = (250,250,240) 7 | black = (0,3,18) 8 | mediumgrey = (190, 190, 180) 9 | lightgrey = (235, 235, 225) 10 | grey = (100,95,97) 11 | park_green = (115, 178, 115) 12 | green = (115, 220, 115) 13 | green_bright= (23, 191, 23) 14 | lightgreen = (150,212,166) 15 | water_grey = (130, 130, 130) 16 | purple = (250, 0, 250) 17 | #plotly colors 18 | plt_orange = (255, 127, 14) 19 | plt_green = (44, 160, 44) 20 | plt_red = (214, 39, 40) 21 | plt_blue = (31, 119, 180) 22 | 23 | 24 | #BOUNDING BOX CONSTANTS (USED FOR S. PHILLY) 25 | sPhilaBox = ((2683629, 220000), (2700700, 231000)) 26 | sPhilaSq = ((2683629, 220000), (2700700, 237071)) 27 | sPhilaSm1 = ((2689018, 224343), (2691881, 226266)) 28 | sPhilaSm2 = ((2685990, 219185), (2692678, 223831)) 29 | sPhilaSm3 = ((2688842, 220590), (2689957, 221240)) 30 | sPhilaSm4 = ((2689615, 219776), (2691277, 220738)) 31 | sPhilaSm5 = ((2690303, 220581), (2690636, 220772)) 32 | sm6 = ((2692788, 225853), (2693684, 226477)) 33 | chris = ((2688798, 221573), (2702834, 230620)) 34 | nolibbox= ((2685646, 238860), (2713597, 258218)) 35 | mckean = ((2691080, 226162), (2692236, 226938)) 36 | d68d70 = ((2691647, 221073), (2702592, 227171)) 37 | d70 = ((2694096, 222741), (2697575, 225059)) 38 | ritner_moyamen =((2693433, 223967), (2694587, 224737)) 39 | morris_10th = ((2693740, 227260), (2694412, 227693)) 40 | morris_12th = ((2693257, 227422), (2693543, 227614)) 41 | study_area = ((2682005, 219180), (2701713, 235555)) 42 | dickenson_7th = ((2695378, 227948), (2695723, 228179)) 43 | packer_18th = ((2688448, 219932), (2691332, 221857)) 44 | moore_broad = ((2689315, 225537), (2695020, 228592)) 45 | oregon_front = ((2695959, 221033), (2701749, 224921)) 46 | mckean_2nd = ((2696719, 224600), (2699010, 226150)) 47 | -------------------------------------------------------------------------------- /swmmio/graphics/__init__.py: -------------------------------------------------------------------------------- 1 | from swmmio.defs.config import FONT_PATH 2 | from swmmio.defs.constants import park_green, water_grey, lightgrey, grey 3 | 4 | 5 | class _dotdict(dict): 6 | """dot.notation access to dictionary attributes""" 7 | __getattr__ = dict.get 8 | __setattr__ = dict.__setitem__ 9 | __delattr__ = dict.__delitem__ 10 | 11 | 12 | """ 13 | this allows module wide configuration of drawing methods. The dotdict allows for 14 | convenient access. 15 | Example: 16 | 17 | from swmmio import graphics 18 | from swmmio.graphics import swmm_graphics as sg 19 | 20 | #configure 21 | graphics.config.inlcude_parcels = True 22 | 23 | #draws the model with parcels 24 | sg.drawModel(swmmio.Model(/path/to/model), bbox=su.d68d70) 25 | 26 | """ 27 | _o = { 28 | 'include_basemap': False, 29 | 'include_parcels': False, 30 | 'basemap_shapefile_dir': r'C:\Data\ArcGIS\Shapefiles', 31 | 32 | # regular shapefile used for drawing parcels 33 | 'parcels_shapefile': r'C:\Data\ArcGIS\Shapefiles\pennsport_parcels.shp', 34 | 35 | # table resulting from one-to-many spatial join of parcels to sheds 36 | 'parcel_node_join_data': r'P:\02_Projects\SouthPhila\SE_SFR\MasterModels\CommonData\pennsport_sheds_parcels_join.csv', 37 | 38 | 'font_file': FONT_PATH, 39 | 'basemap_options': { 40 | 'gdb': r'C:\Data\ArcGIS\GDBs\LocalData.gdb', 41 | 'features': [ 42 | # this is an array so we can control the order of basemap layers 43 | { 44 | 'feature': 'PhiladelphiaParks', 45 | 'fill': park_green, 46 | 'cols': ["OBJECTID"] # , "SHAPE@"] 47 | }, 48 | { 49 | 'feature': 'HydroPolyTrim', 50 | 'fill': water_grey, 51 | 'cols': ["OBJECTID"] # , "SHAPE@"] 52 | }, 53 | { 54 | 'feature': 'Streets_Dissolved5_SPhilly', 55 | 'fill': lightgrey, 56 | 'fill_anno': grey, 57 | 'cols': ["OBJECTID", "ST_NAME"] # "SHAPE@", 58 | } 59 | ], 60 | } 61 | } 62 | 63 | config = _dotdict(_o) 64 | -------------------------------------------------------------------------------- /swmmio/version_control/tests/compare_inp.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def remove_comments_and_crlf(inp_path, comment_string=';', overwrite=False): 5 | tmpfilename = os.path.splitext(os.path.basename(inp_path))[0] + '_mod.inp' 6 | tmpfilepath = os.path.join(os.path.dirname(inp_path), tmpfilename) 7 | 8 | with open (inp_path) as oldf: 9 | with open(tmpfilepath, 'w') as newf: 10 | for line in oldf: 11 | 12 | if ';' in line: 13 | #remove the comments 14 | if line.strip()[0] == comment_string: 15 | #skip the whole line 16 | pass 17 | else: 18 | #write the line to the left of the comment 19 | non_comment_line = line.split(';')[0] 20 | newf.write(non_comment_line + '\n') 21 | elif line == '\n': 22 | pass 23 | else: 24 | newf.write(line) 25 | 26 | if overwrite: 27 | os.remove(inp_path) 28 | os.rename(tmpfilepath, inp_path) 29 | 30 | 31 | def line_by_line(path1, path2, outfile): 32 | """ 33 | given paths to two INP files, return a text file showing where differences 34 | occur in line-by-line fashion. If the order of elements do not match, this 35 | will be recorded as a difference. 36 | 37 | ignores any spaces in a file such that lines with more or less white space 38 | having the same non-whitespace will be considered equal. 39 | 40 | """ 41 | 42 | #outfile =r"P:\06_Tools\v_control\Testing\cleaned\linebyline.txt" 43 | with open(outfile, 'w') as diff_file: 44 | with open (path1) as f1: 45 | with open(path2) as f2: 46 | 47 | line1 = next(f1) 48 | line2 = next(f2) 49 | 50 | while line1 and line2: 51 | #replace all white space to check only actual content 52 | 53 | if line1.replace(" ", "") != line2.replace(" ", ""): 54 | diff_file.write(line1) 55 | 56 | line1 = next(f1) 57 | line2 = next(f2) 58 | -------------------------------------------------------------------------------- /swmmio/run_models/run.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | 4 | import pandas as pd 5 | 6 | from swmmio import Model 7 | from swmmio.defs.config import PYTHON_EXE_PATH, PYSWMM_WRAPPER_PATH 8 | 9 | 10 | def run_simple(inp_path, py_path=PYTHON_EXE_PATH, pyswmm_wrapper=PYSWMM_WRAPPER_PATH): 11 | """ 12 | run a model once as is. 13 | """ 14 | print('running {}'.format(inp_path)) 15 | # inp_path = model.inp.path 16 | rpt_path = os.path.splitext(inp_path)[0] + '.rpt' 17 | out_path = os.path.splitext(inp_path)[0] + '.out' 18 | 19 | # Pass Environment Info to Run 20 | env_definition = os.environ.copy() 21 | env_definition["PATH"] = "/usr/sbin:/sbin:" + env_definition["PATH"] 22 | 23 | subprocess.call([py_path, pyswmm_wrapper, inp_path, rpt_path, out_path], 24 | env=env_definition) 25 | return 0 26 | 27 | 28 | def run_hot_start_sequence(inp_path, py_path=PYTHON_EXE_PATH, pyswmm_wrapper=PYSWMM_WRAPPER_PATH): 29 | 30 | model = Model(inp_path) 31 | hotstart1 = os.path.join(model.inp.dir, model.inp.name + '_hot1.hsf') 32 | hotstart2 = os.path.join(model.inp.dir, model.inp.name + '_hot2.hsf') 33 | 34 | # create new model inp with params to save hotstart1 35 | print('create new model inp with params to save hotstart1') 36 | 37 | model.inp.report.loc[:, 'Status'] = 'NONE' 38 | model.inp.report.loc[['INPUT', 'CONTROLS'], 'Status'] = 'NO' 39 | model.inp.files = pd.DataFrame([f'SAVE HOTSTART "{hotstart1}"'], columns=['[FILES]']) 40 | model.inp.options.loc['IGNORE_RAINFALL', 'Value'] = 'YES' 41 | model.inp.save() 42 | 43 | run_simple(model.inp.path, py_path=py_path, pyswmm_wrapper=pyswmm_wrapper) 44 | 45 | # create new model inp with params to use hotstart1 and save hotstart2 46 | print('with params to use hotstart1 and save hotstart2') 47 | model.inp.files = pd.DataFrame([f'USE HOTSTART "{hotstart1}"', f'SAVE HOTSTART "{hotstart2}"'], columns=['[FILES]']) 48 | model.inp.save() 49 | 50 | run_simple(model.inp.path, py_path=py_path, pyswmm_wrapper=pyswmm_wrapper) 51 | 52 | # create new model inp with params to use hotstart2 and not save anything 53 | print('params to use hotstart2 and not save anything') 54 | 55 | model.inp.files = pd.DataFrame([f'USE HOTSTART "{hotstart2}"'], columns=['[FILES]']) 56 | model.inp.options.loc['IGNORE_RAINFALL', 'Value'] = 'NO' 57 | model.inp.save() 58 | 59 | return run_simple(model.inp.path, py_path=py_path, pyswmm_wrapper=pyswmm_wrapper) 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Standard library imports 2 | import os 3 | import ast 4 | 5 | # Third party imports 6 | from setuptools import find_packages, setup 7 | 8 | HERE = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | 11 | def get_version(module='swmmio'): 12 | """Get version.""" 13 | with open(os.path.join(HERE, module, '__init__.py'), 'r') as f: 14 | data = f.read() 15 | lines = data.split('\n') 16 | for line in lines: 17 | if line.startswith('VERSION_INFO'): 18 | version_tuple = ast.literal_eval(line.split('=')[-1].strip()) 19 | version = '.'.join(map(str, version_tuple)) 20 | break 21 | return version 22 | 23 | 24 | def get_description(): 25 | """Get long description.""" 26 | with open(os.path.join(HERE, 'README.md'), 'r') as f: 27 | data = f.read() 28 | return data 29 | 30 | 31 | AUTHOR_NAME = 'Adam Erispaha' 32 | AUTHOR_EMAIL = 'aerispaha@gmail.com' 33 | 34 | install_requires = [ 35 | 'Pillow>=6.2.0', 36 | 'numpy>=1.16.4', 37 | 'pandas>=0.24.2', 38 | 'pyshp>=2.1.0', 39 | 'geojson>=2.4.1', 40 | "networkx>=2.3,<2.8.1;python_version<'3.8'", 41 | "networkx>=2.3;python_version>='3.8'", 42 | 'pyyaml>=3.12', 43 | 'pyproj>=3.0.0', 44 | 'requests>=2.32.3', 45 | 'typing_extensions>=4.12.2', 46 | ] 47 | 48 | tests_require = [ 49 | 'pytest', 50 | ] 51 | 52 | setup(name='swmmio', 53 | version=get_version(), 54 | description='Tools for interacting with, editing, and visualizing EPA SWMM5 models', 55 | author=AUTHOR_NAME, 56 | url='https://github.com/aerispaha/swmmio', 57 | author_email=AUTHOR_EMAIL, 58 | packages=find_packages(exclude='tests'), 59 | entry_points={ 60 | "console_scripts": ['swmmio_run = swmmio.run_models.run:run_simple'] 61 | }, 62 | install_requires=install_requires, 63 | tests_require=tests_require, 64 | long_description=get_description(), 65 | long_description_content_type="text/markdown", 66 | include_package_data=True, 67 | platforms="OS Independent", 68 | license="MIT License", 69 | classifiers=[ 70 | "Development Status :: 3 - Alpha", 71 | "License :: OSI Approved :: MIT License", 72 | "Operating System :: OS Independent", 73 | "Programming Language :: Python :: 3.7", 74 | "Programming Language :: Python :: 3.8", 75 | "Programming Language :: Python :: 3.9", 76 | "Programming Language :: Python :: 3.10", 77 | ] 78 | ) 79 | -------------------------------------------------------------------------------- /docs/index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# swmmio \n", 8 | "_Programmatic pre and post processing for EPA Stormwater Management Model (SWMM)_\n", 9 | "\n", 10 | "![image](_static/img/flooded_anno_example.png)\n", 11 | "\n", 12 | "\n", 13 | "## Introduction\n", 14 | "`swmmio` is a Python tool for engineers and hydrologists who need to supercharge their ability to modify and analyze EPA SWMM models and results. Using a familiar Pandas interface, users can replace manual procesess that used to live in spreadsheets with scripts and automation.\n", 15 | "\n", 16 | "The core {py:class}`~swmmio.core.Model` object provides accessors to related elements in the INP and RPT. For example, the {py:obj}`Model.subcatchments ` property provides a {py:obj}`~pandas.DataFrame` (or GeoDataFrame) accessor joining data from the `[SUBCATCHMENTS]` and `[SUBAREAS]` tables in the model.inp file and, if available, the `Subcatchment Runoff Summary` from the model.rpt file. \n", 17 | "\n", 18 | "Additionally, `swmmio` provides a lower-level {py:class}`~swmmio.core.inp` API for reading and writing (almost) all of the sections of the model.inp file which is useful for programmatically modifying EPA SWMM models.\n", 19 | "\n", 20 | "\n", 21 | "## Installation\n", 22 | "```bash\n", 23 | "pip install swmmio\n", 24 | "``` \n", 25 | "\n", 26 | "For more examples and tutorials, see the [User Guide](usage/index.md) section." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "```{toctree}\n", 41 | "---\n", 42 | "maxdepth: 2\n", 43 | "hidden: \n", 44 | "---\n", 45 | "usage/index\n", 46 | "reference/index\n", 47 | "changelog\n", 48 | "```" 49 | ] 50 | } 51 | ], 52 | "metadata": { 53 | "kernelspec": { 54 | "display_name": "venv", 55 | "language": "python", 56 | "name": "python3" 57 | }, 58 | "language_info": { 59 | "codemirror_mode": { 60 | "name": "ipython", 61 | "version": 3 62 | }, 63 | "file_extension": ".py", 64 | "mimetype": "text/x-python", 65 | "name": "python", 66 | "nbconvert_exporter": "python", 67 | "pygments_lexer": "ipython3", 68 | "version": "3.11.4" 69 | } 70 | }, 71 | "nbformat": 4, 72 | "nbformat_minor": 2 73 | } 74 | -------------------------------------------------------------------------------- /swmmio/wrapper/pyswmm_wrapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright (c) 2022 Bryant E. McDonnell 4 | # 5 | # Licensed under the terms of the BSD2 License 6 | # See LICENSE.txt for details 7 | # ----------------------------------------------------------------------------- 8 | """ 9 | Function developed to execute a PySWMM Simulation on the command line. To run, 10 | execute the following on the command line: 11 | 12 | python --help # Produces a list of options 13 | 14 | python /pyswmm_wrapper.py /*.inp /*.rpt /*.out # optional args 15 | """ 16 | 17 | import argparse 18 | import pathlib 19 | import pyswmm 20 | 21 | 22 | def run_model(): 23 | """Run Model.""" 24 | # Argument Resolution 25 | parser = argparse.ArgumentParser() 26 | parser.add_argument('inp_file', type=pathlib.Path, 27 | help='Input File Path') 28 | parser.add_argument('rpt_file', type=pathlib.Path, nargs='?', 29 | help='Report File Path (Optional).') 30 | parser.add_argument('out_file', type=pathlib.Path, nargs='?', 31 | help='Output File Path (Optional).') 32 | 33 | report_prog_help = "--report-progress can be useful for longer model runs. The drawback "\ 34 | +"is that it slows the simulation down. Use an integer to specify how "\ 35 | +"frequent to interrup the simulation. This depends of the number of time "\ 36 | +"steps" 37 | parser.add_argument('--report_progress', default=False, type=int, 38 | help=report_prog_help) 39 | args = parser.parse_args() 40 | 41 | # File Naming -> Str Paths 42 | inp_file = str(args.inp_file) 43 | if args.rpt_file: 44 | rpt_file = str(args.rpt_file) 45 | else: 46 | rpt_file = args.rpt_file 47 | out_file = str(args.out_file) 48 | if args.out_file: 49 | out_file = str(args.out_file) 50 | else: 51 | out_file = args.out_file 52 | 53 | # Running the simulation without and with progress reporting. 54 | if args.report_progress == False: 55 | sim = pyswmm.Simulation(inp_file, rpt_file, out_file) 56 | sim.execute() 57 | else: 58 | with pyswmm.Simulation(inp_file, rpt_file, out_file) as sim: 59 | for ind, step in enumerate(sim): 60 | if ind % args.report_progress == 0: 61 | print(round(sim.percent_complete*1000)/10.0) 62 | 63 | return 0 64 | 65 | if __name__ in "__main__": 66 | run_model() 67 | -------------------------------------------------------------------------------- /swmmio/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from itertools import chain 4 | 5 | from swmmio.run_models.run import run_simple, run_hot_start_sequence 6 | from swmmio.run_models import start_pool 7 | 8 | 9 | def main(): 10 | # parse the arguments 11 | parser = argparse.ArgumentParser(description='Process some stuff') 12 | parser.add_argument('-r', '--run', dest='model_to_run', nargs="+") 13 | parser.add_argument('-rhs', '--run_hotstart', dest='hotstart_model_to_run', nargs="+") 14 | parser.add_argument('-sp', '--start_pool', dest='start_pool', nargs="+") 15 | parser.add_argument('-cores_left', '--cores_left', dest='cores_left', default=4, type=int) 16 | parser.add_argument('-pp', '--post_process', dest='post_process', nargs="+") 17 | 18 | args = parser.parse_args() 19 | wd = os.getcwd() # current directory script is being called from 20 | 21 | if args.model_to_run is not None: 22 | 23 | models_paths = [os.path.join(wd, f) for f in args.model_to_run] 24 | print('Adding models to queue:\n\t{}'.format('\n\t'.join(models_paths))) 25 | 26 | # run the models in series (one after the other) 27 | list(map(run_simple, models_paths)) 28 | # run_simple(args.model_to_run) 29 | 30 | elif args.hotstart_model_to_run is not None: 31 | models_paths = [os.path.join(wd, f) for f in args.hotstart_model_to_run] 32 | print('hotstart_model_to_run the model: {}'.format(args.hotstart_model_to_run)) 33 | # m = Model(args.hotstart_model_to_run) 34 | # run_hot_start_sequence(m)#args.hotstart_model_to_run) 35 | list(map(run_hot_start_sequence, models_paths)) 36 | 37 | elif args.start_pool is not None: 38 | 39 | models_dirs = [os.path.join(wd, f) for f in args.start_pool] 40 | print('Searching for models in:\n\t{}'.format('\n\t'.join(models_dirs))) 41 | # combine the segments and options (combinations) into one iterable 42 | inp_paths = [] 43 | for root, dirs, files in chain.from_iterable(os.walk(path) for path in models_dirs): 44 | for f in files: 45 | if f.endswith('.inp') and 'bk' not in root: 46 | # we've found a directory containing an inp 47 | inp_paths.append(os.path.join(root, f)) 48 | 49 | # call the main() function in start_pool.py 50 | start_pool.main(inp_paths, args.cores_left) 51 | 52 | print("swmmio has completed running {} models".format(len(inp_paths))) 53 | 54 | else: 55 | print('you need to pass in some args') 56 | 57 | return 0 58 | 59 | 60 | if __name__ == '__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: CI Tests 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | tags: 10 | - v* 11 | pull_request: 12 | branches: [ "master" ] 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | ci_matrix: 19 | name: Unit Tests 20 | strategy: 21 | matrix: 22 | python_version: [3.8, 3.9, "3.10", "3.11"] 23 | os: [ ubuntu-latest, windows-latest, macos-latest ] 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Set up Python ${{ matrix.python_version }} 28 | uses: actions/setup-python@v3 29 | with: 30 | python-version: ${{ matrix.python_version }} 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install flake8 pytest 35 | pip install -r requirements.txt 36 | - name: Lint with flake8 37 | run: | 38 | # stop the build if there are Python syntax errors or undefined names 39 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 40 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 41 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 42 | - name: Test with pytest 43 | run: | 44 | pytest -m "not uses_geopandas" 45 | python -m doctest swmmio/core.py 46 | 47 | release: 48 | name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI 49 | runs-on: ubuntu-latest 50 | needs: [ci_matrix] 51 | steps: 52 | - uses: actions/checkout@master 53 | - name: Set up Python 3.10 54 | uses: actions/setup-python@v3 55 | with: 56 | python-version: "3.10" 57 | - name: Build package 58 | run: | 59 | echo ${{ github.ref }} 60 | echo ${{ github.ref_type }} 61 | pip install wheel 62 | python setup.py bdist_wheel 63 | - name: Publish distribution 📦 to Test PyPI 64 | if: ${{ github.ref_type == 'tag'}} 65 | uses: pypa/gh-action-pypi-publish@release/v1 66 | with: 67 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 68 | repository_url: https://test.pypi.org/legacy/ 69 | - name: Publish distribution 📦 to PyPI 70 | if: ${{ github.ref_type == 'tag'}} 71 | uses: pypa/gh-action-pypi-publish@release/v1 72 | with: 73 | password: ${{ secrets.PYPI_API_TOKEN }} 74 | -------------------------------------------------------------------------------- /swmmio/tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright (c) 2018 Adam Erispaha 4 | # 5 | # Licensed under the terms of the BSD2 License 6 | # See LICENSE.txt for details 7 | # ----------------------------------------------------------------------------- 8 | """SWMM5 test models.""" 9 | 10 | # Standard library imports 11 | import os 12 | 13 | DATA_PATH = os.path.abspath(os.path.dirname(__file__)) 14 | 15 | # Test models paths 16 | MODEL_A_PATH = os.path.join(DATA_PATH, 'model_state_plane.inp') 17 | MODEL_FULL_FEATURES_PATH = os.path.join(DATA_PATH, 'model_full_features.inp') 18 | MODEL_FULL_FEATURES_XY = os.path.join( 19 | DATA_PATH, 'model_full_features_network_xy.inp') 20 | MODEL_FULL_FEATURES_XY_B = os.path.join(DATA_PATH, 'model_full_features_b.inp') 21 | MODEL_FULL_FEATURES__NET_PATH = os.path.join( 22 | DATA_PATH, 'model_full_features_network.inp') 23 | MODEL_FULL_FEATURES_INVALID = os.path.join(DATA_PATH, 'invalid_model.inp') 24 | MODEL_GREEN_AMPT = os.path.join(DATA_PATH, 'model_green_ampt.inp') 25 | MODEL_MOD_GREEN_AMPT = os.path.join(DATA_PATH, 'model_mod_green_ampt.inp') 26 | MODEL_CURVE_NUMBER = os.path.join(DATA_PATH, 'model_curve_num.inp') 27 | MODEL_MOD_HORTON = os.path.join(DATA_PATH, 'model_mod_horton.inp') 28 | MODEL_EX_1 = os.path.join(DATA_PATH, 'Example1.inp') 29 | MODEL_EX_1B = os.path.join(DATA_PATH, 'Example1b.inp') 30 | MODEL_EXAMPLE6 = os.path.join(DATA_PATH, 'Example6.inp') 31 | MODEL_EX_1_PARALLEL_LOOP = os.path.join(DATA_PATH, 'Example1_parallel_loop.inp') 32 | MODEL_INFILTRAION_PARSE_FAILURE = os.path.join(DATA_PATH, 'model-with-infiltration-parse-failure.inp') 33 | MODEL_EXTCNTRLMODEL = os.path.join(DATA_PATH, 'SWMMExtCntrlModel.inp') 34 | MODEL_TEST_INLET_DRAINS = os.path.join(DATA_PATH, 'test_inlet_drains.inp') 35 | MODEL_GROUNDWATER = os.path.join(DATA_PATH, 'groundwater_model.inp') 36 | 37 | # test rpt paths 38 | RPT_FULL_FEATURES = os.path.join(DATA_PATH, 'model_full_features_network.rpt') 39 | OWA_RPT_EXAMPLE = os.path.join(DATA_PATH, 'owa-rpt-example.rpt') 40 | 41 | # version control test models 42 | MODEL_XSECTION_BASELINE = os.path.join(DATA_PATH, 'baseline_test.inp') 43 | MODEL_XSECTION_ALT_01 = os.path.join(DATA_PATH, 'alt_test1.inp') 44 | MODEL_XSECTION_ALT_02 = os.path.join(DATA_PATH, 'alt_test2.inp') 45 | MODEL_XSECTION_ALT_03 = os.path.join(DATA_PATH, 'alt_test3.inp') 46 | MODEL_BLANK = os.path.join(DATA_PATH, 'model_blank_01.inp') 47 | BUILD_INSTR_01 = os.path.join(DATA_PATH, 'test_build_instructions_01.txt') 48 | 49 | df_test_coordinates_csv = os.path.join(DATA_PATH, 'df_test_coordinates.csv') 50 | OUTFALLS_MODIFIED = os.path.join(DATA_PATH, 'outfalls_modified_10.csv') 51 | 52 | # SWMM example model 53 | MODEL_PUMP_CONTROL = os.path.join(DATA_PATH, 'Pump_Control_Model.inp') 54 | -------------------------------------------------------------------------------- /swmmio/tests/data/model_blank.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | ;;Project Title/Notes 3 | 4 | [OPTIONS] 5 | ;;Option Value 6 | FLOW_UNITS CFS 7 | INFILTRATION HORTON 8 | FLOW_ROUTING KINWAVE 9 | LINK_OFFSETS DEPTH 10 | MIN_SLOPE 0 11 | ALLOW_PONDING NO 12 | SKIP_STEADY_STATE NO 13 | 14 | START_DATE 06/19/2019 15 | START_TIME 00:00:00 16 | REPORT_START_DATE 06/19/2019 17 | REPORT_START_TIME 00:00:00 18 | END_DATE 06/19/2019 19 | END_TIME 06:00:00 20 | SWEEP_START 1/1 21 | SWEEP_END 12/31 22 | DRY_DAYS 0 23 | REPORT_STEP 00:15:00 24 | WET_STEP 00:05:00 25 | DRY_STEP 01:00:00 26 | ROUTING_STEP 0:00:30 27 | 28 | INERTIAL_DAMPING PARTIAL 29 | NORMAL_FLOW_LIMITED BOTH 30 | FORCE_MAIN_EQUATION H-W 31 | VARIABLE_STEP 0.75 32 | LENGTHENING_STEP 0 33 | MIN_SURFAREA 0 34 | MAX_TRIALS 0 35 | HEAD_TOLERANCE 0 36 | SYS_FLOW_TOL 5 37 | LAT_FLOW_TOL 5 38 | MINIMUM_STEP 0.5 39 | THREADS 1 40 | 41 | [EVAPORATION] 42 | ;;Data Source Parameters 43 | ;;-------------- ---------------- 44 | CONSTANT 0.0 45 | DRY_ONLY NO 46 | 47 | [REPORT] 48 | ;;Reporting Options 49 | INPUT NO 50 | CONTROLS NO 51 | SUBCATCHMENTS ALL 52 | NODES ALL 53 | LINKS ALL 54 | 55 | [TAGS] 56 | 57 | [MAP] 58 | DIMENSIONS 0.000 0.000 10000.000 10000.000 59 | Units None 60 | 61 | [COORDINATES] 62 | ;;Node X-Coord Y-Coord 63 | ;;-------------- ------------------ ------------------ 64 | 65 | [VERTICES] 66 | ;;Link X-Coord Y-Coord 67 | ;;-------------- ------------------ ------------------ 68 | 69 | 70 | [TITLE] 71 | 72 | 73 | [FILE] 74 | 75 | [RAINGAGE] 76 | 77 | [TEMPERATURE] 78 | 79 | [SUBCATCHMENTS] 80 | 81 | [SUBAREAS] 82 | 83 | [INFILTRATION] 84 | 85 | [AQUIFERS] 86 | 87 | [GROUNDWATER] 88 | 89 | [SNOWPACK] 90 | 91 | [JUNCTIONS] 92 | 93 | [OUTFALLS] 94 | 95 | [STORAGE] 96 | 97 | [DIVIDERS] 98 | 99 | [CONDUITS] 100 | 101 | [PUMPS] 102 | 103 | [ORIFICES] 104 | 105 | [WEIRS] 106 | 107 | [OUTLETS] 108 | 109 | [XSECTIONS] 110 | 111 | [TRANSECTS] 112 | 113 | [LOSSES] 114 | 115 | [CONTROLS] 116 | 117 | [POLLUTANTS] 118 | 119 | [LANDUSE] 120 | 121 | [BUILDUP] 122 | 123 | [WASHOFF] 124 | 125 | [COVERAGE] 126 | 127 | [INFLOWS] 128 | 129 | [DWF] 130 | 131 | [PATTERNS] 132 | 133 | [RDII] 134 | 135 | [HYDROGRAPHS] 136 | 137 | [LOADING] 138 | 139 | [TREATMENT] 140 | 141 | [CURVES] 142 | 143 | [TIMESERIES] 144 | 145 | [POLYGON] 146 | 147 | [SYMBOLS] 148 | 149 | [LABELS] 150 | 151 | [BACKDROP] 152 | 153 | [PROFILES] 154 | 155 | [LID_CONTROLS] 156 | 157 | [LID_USAGE] 158 | 159 | [GW_FLOW] 160 | 161 | [GWF] 162 | 163 | [ADJUSTMENT] 164 | 165 | [EVEN] -------------------------------------------------------------------------------- /swmmio/utils/modify_model.py: -------------------------------------------------------------------------------- 1 | from swmmio.version_control.utils import write_inp_section 2 | import swmmio 3 | from swmmio.utils.text import get_inp_sections_details 4 | import os 5 | import tempfile 6 | import shutil 7 | 8 | 9 | def replace_inp_section(inp_path, modified_section_header, new_data): 10 | """ 11 | modify an existing inp file by passing in new data (Pandas Dataframe) 12 | and the section header that should be modified. This function will overwrite 13 | all data in the old section with the passed data 14 | 15 | :param inp_path: path to inp file to be changed 16 | :param modified_section_header: section for which data should be change 17 | :param new_data: pd.DataFrame of data to overwrite data in the modified section 18 | :return: swmmio.Model instantiated with modified inp file 19 | """ 20 | 21 | sections = get_inp_sections_details(inp_path) 22 | m = swmmio.Model(inp_path) 23 | with tempfile.TemporaryDirectory() as tempdir: 24 | with open(inp_path) as oldf: 25 | tmp_inp_path = os.path.join(tempdir, f'{m.inp.name}.inp') 26 | with open(tmp_inp_path, 'w') as new: 27 | 28 | # write each line as is from the original model until we find the 29 | # header of the section we wish to overwrite 30 | found_section = False 31 | found_next_section = False 32 | for line in oldf: 33 | if modified_section_header in line: 34 | # write the replacement data in the new file now 35 | write_inp_section(new, sections, 36 | modified_section_header, 37 | new_data, pad_top=False) 38 | 39 | found_section = True 40 | 41 | if (found_section and any((f"[{es}]") in line for es in sections.keys()) and modified_section_header not in line): 42 | found_next_section = True 43 | 44 | if found_next_section or not found_section: 45 | # write the lines from the original file 46 | # if we haven't found the section to modify. 47 | # if we have found the section and we've found the NEXT section 48 | # continue writing original file's lines 49 | new.write(line) 50 | 51 | if not found_section: 52 | # the header doesn't exist in the old model 53 | # so we should append it to the bottom of file 54 | write_inp_section(new, sections, 55 | modified_section_header, 56 | new_data) 57 | 58 | # rename files and remove old if we should overwrite 59 | os.remove(inp_path) 60 | shutil.copy2(tmp_inp_path, inp_path) 61 | 62 | return swmmio.Model(inp_path) 63 | -------------------------------------------------------------------------------- /swmmio/reporting/serialize.py: -------------------------------------------------------------------------------- 1 | # READ/WRITE REPORTS AS JSON 2 | import json 3 | import pandas as pd 4 | from pandas import json_normalize 5 | from swmmio.utils import spatial 6 | from swmmio.graphics import swmm_graphics as sg 7 | 8 | 9 | def decode_report(rpt_path): 10 | #read report from json into a dict 11 | with open(rpt_path, 'r') as f: 12 | read_rpt = json.loads(f.read()) 13 | 14 | #parse the geojson 15 | def df_clean(uncleandf): 16 | cleaned_cols = [x.split('.')[-1] for x in uncleandf.columns] 17 | uncleandf.columns = cleaned_cols 18 | clean_df = uncleandf.rename(columns={'coordinates':'coords'}).drop(['type'], axis=1) 19 | clean_df = clean_df.set_index(['Name']) 20 | return clean_df 21 | 22 | #parse conduit data into a dataframe 23 | conds_df = json_normalize(read_rpt['conduits']['features']) 24 | conds_df = df_clean(conds_df) 25 | 26 | #parse node data into a dataframe 27 | nodes_df = json_normalize(read_rpt['nodes']['features']) 28 | nodes_df = df_clean(nodes_df) 29 | 30 | #parse parcel data into a dataframe 31 | pars_df = json_normalize(read_rpt['parcels']['features']) 32 | pars_df = df_clean(pars_df) 33 | 34 | rpt_dict = {'conduits':conds_df, 'nodes':nodes_df, 'parcels':pars_df} 35 | rpt_dict.update() 36 | return {'conduits':conds_df, 'nodes':nodes_df, 'parcels':pars_df} 37 | 38 | def encode_report(rpt, rpt_path): 39 | 40 | rpt_dict = {} 41 | 42 | #write parcel json files 43 | parcels = spatial.read_shapefile(sg.config.parcels_shapefile) 44 | parcels = parcels[['PARCELID', 'coords']] #omit 'ADDRESS', 'OWNER1' 45 | flooded = rpt.alt_report.parcel_flooding #proposed flooding condition 46 | flooded = pd.merge(flooded, parcels, right_on='PARCELID', left_index=True) 47 | rpt_dict['parcels'] = spatial.write_geojson(flooded, geomtype='polygon') 48 | 49 | #non null delta category parcels 50 | delta_parcels = rpt.flood_comparison.loc[pd.notnull(rpt.flood_comparison.Category)] 51 | delta_parcels = pd.merge(delta_parcels, parcels, right_on='PARCELID', left_index=True) 52 | rpt_dict['delta_parcels'] = spatial.write_geojson(delta_parcels, geomtype='polygon') 53 | 54 | #encode conduit and nodes data into geojson 55 | # rpt_dict['conduits'] = spatial.write_geojson(rpt.alt_report.model.conduits()) 56 | rpt_dict['new_conduits'] = spatial.write_geojson(rpt.newconduits) 57 | # rpt_dict['nodes'] = spatial.write_geojson(rpt.model.nodes(), geomtype='point') 58 | 59 | #write summary stats 60 | rpt_dict.update(rpt.summary_dict) 61 | 62 | with open(rpt_path, 'w') as f: 63 | f.write(json.dumps(rpt_dict)) 64 | 65 | # #start writing that thing 66 | # with open(BETTER_BASEMAP_PATH, 'r') as bm: 67 | # filename = os.path.join(os.path.dirname(rpt_path), rpt.model.name + '.html') 68 | # with open(filename, 'wb') as newmap: 69 | # for line in bm: 70 | # if '//INSERT GEOJSON HERE ~~~~~' in line: 71 | # newmap.write('conduits = {};\n'.format(geojson.dumps(rpt_dict['conduits']))) 72 | # newmap.write('nodes = {};\n'.format(geojson.dumps(rpt_dict['nodes']))) 73 | # newmap.write('parcels = {};\n'.format(geojson.dumps(rpt_dict['parcels']))) 74 | # else: 75 | # newmap.write(line) 76 | -------------------------------------------------------------------------------- /swmmio/damage/parcels.py: -------------------------------------------------------------------------------- 1 | #PARCEL FLOOD DAMAGE CALCULATIONS 2 | #ASSUMES INPUT DATA IN ADDITION TO SWMM results 3 | 4 | 5 | import pandas as pd 6 | 7 | def flood_duration(node_flood_df, parcel_node_df=None, 8 | parcel_node_join_csv=None, threshold=0.08333): 9 | 10 | """ 11 | Given a dataframe with node flood duration and a csv table resulting 12 | from a one-to-many join of model shed drainage areas to parcels, this 13 | function returns a dataframe with flood data associated to each node. 14 | 15 | Assumptions: 16 | Flooding that occurs at any node in the SWMM model is applied to all 17 | parcels falling within that node's drainage area. Drainage areas are 18 | assumed to be generated using a Thiessen polygon method, or similar. 19 | """ 20 | 21 | #read the one-to-many parcels-nodes table into a Dataframe if csv provided 22 | if parcel_node_df is None: 23 | parcel_node_df = pd.read_csv(parcel_node_join_csv) 24 | 25 | useful_cols = ['PARCELID', 'OUTLET', 'SUBCATCH', 'ADDRESS'] 26 | parcel_node_df = parcel_node_df[useful_cols] 27 | 28 | #clean up the nodes df, using only a few columns 29 | useful_cols = ['HoursFlooded', 'TotalFloodVol', 'MaxHGL', 'MaxNodeDepth'] 30 | node_flood_df = node_flood_df[useful_cols] 31 | 32 | #join flood data to parcels by outlet, clean a bit more of the cols 33 | parcel_flood = pd.merge(parcel_node_df, node_flood_df, 34 | left_on='OUTLET', right_index=True) 35 | parcel_flood = parcel_flood[['PARCELID','HoursFlooded','TotalFloodVol']] 36 | 37 | #groupby parcel id to aggregate the duplicates, return the max of all dups 38 | parcel_flood_max = parcel_flood.groupby('PARCELID').max() 39 | 40 | #filter only parcels with flood duration above the threshold 41 | parcel_flood_max = parcel_flood_max.loc[parcel_flood_max.HoursFlooded>=threshold] 42 | return parcel_flood_max 43 | 44 | def compare_flood_duration(basedf,altdf,threshold=0.08333,delta_threshold=0.25): 45 | 46 | df = basedf.join(altdf, lsuffix='Baseline', rsuffix='Proposed', how='outer') 47 | df = df.fillna(0) #any NaN means no flooding observed 48 | delta = df.HoursFloodedProposed - df.HoursFloodedBaseline 49 | df = df.assign(DeltaHours=delta) 50 | 51 | def categorize(parcel): 52 | #rename for logic clarity 53 | existing_flood_duration = parcel.HoursFloodedBaseline 54 | proposed_flood_duration = parcel.HoursFloodedProposed 55 | flood_duration_delta = parcel.DeltaHours 56 | 57 | if ((existing_flood_duration >= threshold) 58 | and (proposed_flood_duration >= threshold)): 59 | #parcel used to and still floods, check how it changed: 60 | if flood_duration_delta > delta_threshold: 61 | #flood duration increased (more than delta_threhold) 62 | return 'increased_flooding' 63 | 64 | elif flood_duration_delta < - delta_threshold: 65 | #flooding duration decreased (more than delta_threhold) 66 | return 'decreased_flooding' 67 | 68 | elif (existing_flood_duration < threshold 69 | and proposed_flood_duration >= threshold 70 | and abs(flood_duration_delta) >= delta_threshold): 71 | #flooding occurs where it perviously did not 72 | return 'new_flooding' 73 | 74 | elif (existing_flood_duration >= threshold 75 | and proposed_flood_duration < threshold 76 | and abs(flood_duration_delta) >= delta_threshold): 77 | #parcel that previously flooded no longer does 78 | return 'eliminated_flooding' 79 | 80 | cats = df.apply(lambda row: categorize(row), axis=1) 81 | df = df.assign(Category=cats) 82 | 83 | return df 84 | -------------------------------------------------------------------------------- /swmmio/reporting/functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions related to comparing SWMM models 3 | """ 4 | import pandas as pd 5 | 6 | 7 | def conduits_cost_estimate(conduit_df, additional_costs=None): 8 | 9 | 10 | def calc_area(row): 11 | """calculate the cross-sectional area of a sewer segment. If the segment 12 | is multi-barrel, the area will reflect the total of all barrels""" 13 | 14 | if row.Shape == 'CIRCULAR': 15 | d = row.Geom1 16 | area = 3.1415 * (d * d) / 4 17 | return round((area * row.Barrels),2) 18 | 19 | if 'RECT' in row.Shape: 20 | #assume triangular bottom sections (geom3) deepens the excavated box 21 | return (row.Geom1 + row.Geom3) * float(row.Geom2) * row.Barrels 22 | if row.Shape =='EGG': 23 | #assume geom1 is the span 24 | return row.Geom1*1.5 * row.Barrels 25 | 26 | def get_unit_cost(row): 27 | cost_dict = {1.0:570, 28 | 1.5:570, 29 | 1.75:610, 30 | 2.0:680, 31 | 2.25:760, 32 | 2.5:860, 33 | 3.0:1020, 34 | 3.5:1200, 35 | 4.0:1400, 36 | 4.5:1550, 37 | 5.0:1700, 38 | 5.5:1960, 39 | 6.0:2260, 40 | 7.0:2600, 41 | 7.5:3000, 42 | } 43 | 44 | def round_to(n, precision): 45 | correction = 0.5 if n >= 0 else -0.5 46 | return int( n/precision+correction ) * precision 47 | 48 | def round_to_05(n): 49 | return round_to(n, 0.05) 50 | 51 | cleaned_geom = round_to_05(row.Geom1) 52 | RectBox_UnitCost = 80 53 | if row.Shape == 'CIRCULAR': 54 | try: 55 | val = cost_dict[cleaned_geom] 56 | except: 57 | val = 0 58 | return val 59 | if 'RECT' in row.Shape: 60 | """ 61 | assume any triangular bottom section adds to 62 | overall excavation box section 63 | """ 64 | val = round(RectBox_UnitCost*row.XArea,2) 65 | return val 66 | if row.Shape == 'EGG': 67 | """ 68 | We're going to treat this like a box" 69 | """ 70 | val = round(RectBox_UnitCost*row.XArea,2) 71 | return val 72 | 73 | 74 | def compute_conduit_cost(row): 75 | return row.UnitCostLF * row.Length 76 | 77 | def compute_volume(row): 78 | return row.XArea * row.Length 79 | 80 | def added_cost(row): 81 | #add in any additional cost data (from crossing water mains, etc) 82 | return row.CostEstimate + row.AdditionalCost 83 | 84 | conduit_df['XArea'] = conduit_df.apply (lambda r: calc_area (r), axis=1) 85 | conduit_df['UnitCostLF'] = conduit_df.apply(lambda r: get_unit_cost(r), axis=1) 86 | conduit_df['Volume'] = conduit_df.apply (lambda r:compute_volume (r), axis=1) 87 | conduit_df['CostEstimate'] = conduit_df.apply (lambda r:compute_conduit_cost (r),axis=1) 88 | 89 | if additional_costs: 90 | #read in the supplemental cost data from the csv 91 | addcosts = pd.read_csv(additional_costs, index_col=0) 92 | conduit_df = conduit_df.join(addcosts).fillna(0) 93 | conduit_df['TotalCostEstimate'] = conduit_df.apply (lambda r: 94 | added_cost(r), 95 | axis=1) 96 | else: 97 | # NOTE this lingo here is weak... 98 | #additional_costs not provided, rename the CostEstimate to TotalCostEstimate 99 | conduit_df = conduit_df.rename(columns={"CostEstimate": "TotalCostEstimate"}) 100 | 101 | return conduit_df 102 | -------------------------------------------------------------------------------- /swmmio/reporting/basemaps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{TITLE}} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 39 |
40 | 41 |
42 | 43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /swmmio/reporting/basemaps/compare.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 36 | 37 | 38 | 39 |
40 |
41 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /swmmio/reporting/basemaps/mapbox_base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | swmmio map 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 |
18 | 19 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /swmmio/tests/data/outfalls_issue.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | ;;Project Title/Notes 3 | 4 | [OPTIONS] 5 | ;;Option Value 6 | FLOW_UNITS CMS 7 | INFILTRATION HORTON 8 | FLOW_ROUTING DYNWAVE 9 | LINK_OFFSETS DEPTH 10 | MIN_SLOPE 0 11 | ALLOW_PONDING NO 12 | SKIP_STEADY_STATE NO 13 | 14 | START_DATE 01/01/2020 15 | START_TIME 00:00:00 16 | REPORT_START_DATE 01/01/2020 17 | REPORT_START_TIME 00:00:00 18 | END_DATE 01/01/2020 19 | END_TIME 02:00:00 20 | SWEEP_START 01/01 21 | SWEEP_END 12/31 22 | DRY_DAYS 0 23 | REPORT_STEP 00:01:00 24 | WET_STEP 00:05:00 25 | DRY_STEP 01:00:00 26 | ROUTING_STEP 0:00:10 27 | RULE_STEP 00:00:00 28 | 29 | INERTIAL_DAMPING PARTIAL 30 | NORMAL_FLOW_LIMITED BOTH 31 | FORCE_MAIN_EQUATION H-W 32 | VARIABLE_STEP 0.75 33 | LENGTHENING_STEP 0 34 | MIN_SURFAREA 1.167 35 | MAX_TRIALS 20 36 | HEAD_TOLERANCE 0.0015 37 | SYS_FLOW_TOL 5 38 | LAT_FLOW_TOL 5 39 | MINIMUM_STEP 0.5 40 | THREADS 1 41 | 42 | [EVAPORATION] 43 | ;;Data Source Parameters 44 | ;;-------------- ---------------- 45 | CONSTANT 0.0 46 | DRY_ONLY NO 47 | 48 | [JUNCTIONS] 49 | ;;Name Elevation MaxDepth InitDepth SurDepth Aponded 50 | ;;-------------- ---------- ---------- ---------- ---------- ---------- 51 | 3 1 0 0 0 0 52 | 4 1 0 0 0 0 53 | 54 | [OUTFALLS] 55 | ;;Name Elevation Type Stage Data Gated Route To 56 | ;;-------------- ---------- ---------- ---------------- -------- ---------------- 57 | StagedOutfall 0 TIMESERIES OutfallStage NO 58 | FreeOutfall 0 FREE NO 59 | 60 | [CONDUITS] 61 | ;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow 62 | ;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- 63 | 2 3 StagedOutfall 400 0.01 0 0 0 0 64 | 3 4 FreeOutfall 400 0.01 0 0 0 0 65 | 66 | [XSECTIONS] 67 | ;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert 68 | ;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- 69 | 2 CIRCULAR 1 0 0 0 1 70 | 3 CIRCULAR 1 0 0 0 1 71 | 72 | [INFLOWS] 73 | ;;Node Constituent Time Series Type Mfactor Sfactor Baseline Pattern 74 | ;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- 75 | 3 FLOW InflowTS FLOW 1.0 1.0 76 | 4 FLOW InflowTS FLOW 1.0 1.0 77 | 78 | [TIMESERIES] 79 | ;;Name Date Time Value 80 | ;;-------------- ---------- ---------- ---------- 81 | OutfallStage 01/01/2020 00:00 0.1 82 | OutfallStage 01/01/2020 01:00 0.5 83 | OutfallStage 01/01/2020 02:00 0.9 84 | ; 85 | InflowTS 01/01/2020 00:00 0.1 86 | InflowTS 01/01/2020 02:00 0.2 87 | 88 | [REPORT] 89 | ;;Reporting Options 90 | SUBCATCHMENTS ALL 91 | NODES ALL 92 | LINKS ALL 93 | 94 | [TAGS] 95 | 96 | [MAP] 97 | DIMENSIONS 0.000 0.000 10000.000 10000.000 98 | Units None 99 | 100 | [COORDINATES] 101 | ;;Node X-Coord Y-Coord 102 | ;;-------------- ------------------ ------------------ 103 | 3 3444.194 6843.292 104 | 4 1629.087 6820.744 105 | StagedOutfall 3466.742 5456.595 106 | FreeOutfall 1606.539 5434.047 107 | 108 | [VERTICES] 109 | ;;Link X-Coord Y-Coord 110 | ;;-------------- ------------------ ------------------ 111 | 112 | -------------------------------------------------------------------------------- /swmmio/tests/data/outfalls_issue_free_first.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | ;;Project Title/Notes 3 | 4 | [OPTIONS] 5 | ;;Option Value 6 | FLOW_UNITS CMS 7 | INFILTRATION HORTON 8 | FLOW_ROUTING DYNWAVE 9 | LINK_OFFSETS DEPTH 10 | MIN_SLOPE 0 11 | ALLOW_PONDING NO 12 | SKIP_STEADY_STATE NO 13 | 14 | START_DATE 01/01/2020 15 | START_TIME 00:00:00 16 | REPORT_START_DATE 01/01/2020 17 | REPORT_START_TIME 00:00:00 18 | END_DATE 01/01/2020 19 | END_TIME 02:00:00 20 | SWEEP_START 01/01 21 | SWEEP_END 12/31 22 | DRY_DAYS 0 23 | REPORT_STEP 00:01:00 24 | WET_STEP 00:05:00 25 | DRY_STEP 01:00:00 26 | ROUTING_STEP 0:00:10 27 | RULE_STEP 00:00:00 28 | 29 | INERTIAL_DAMPING PARTIAL 30 | NORMAL_FLOW_LIMITED BOTH 31 | FORCE_MAIN_EQUATION H-W 32 | VARIABLE_STEP 0.75 33 | LENGTHENING_STEP 0 34 | MIN_SURFAREA 1.167 35 | MAX_TRIALS 20 36 | HEAD_TOLERANCE 0.0015 37 | SYS_FLOW_TOL 5 38 | LAT_FLOW_TOL 5 39 | MINIMUM_STEP 0.5 40 | THREADS 1 41 | 42 | [EVAPORATION] 43 | ;;Data Source Parameters 44 | ;;-------------- ---------------- 45 | CONSTANT 0.0 46 | DRY_ONLY NO 47 | 48 | [JUNCTIONS] 49 | ;;Name Elevation MaxDepth InitDepth SurDepth Aponded 50 | ;;-------------- ---------- ---------- ---------- ---------- ---------- 51 | 3 1 0 0 0 0 52 | 4 1 0 0 0 0 53 | 54 | [OUTFALLS] 55 | ;;Name Elevation Type Stage Data Gated Route To 56 | ;;-------------- ---------- ---------- ---------------- -------- ---------------- 57 | FreeOutfall 0 FREE NO 58 | StagedOutfall 0 TIMESERIES OutfallStage NO 59 | 60 | [CONDUITS] 61 | ;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow 62 | ;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- 63 | 2 3 StagedOutfall 400 0.01 0 0 0 0 64 | 3 4 FreeOutfall 400 0.01 0 0 0 0 65 | 66 | [XSECTIONS] 67 | ;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert 68 | ;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- 69 | 2 CIRCULAR 1 0 0 0 1 70 | 3 CIRCULAR 1 0 0 0 1 71 | 72 | [INFLOWS] 73 | ;;Node Constituent Time Series Type Mfactor Sfactor Baseline Pattern 74 | ;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- 75 | 3 FLOW InflowTS FLOW 1.0 1.0 76 | 4 FLOW InflowTS FLOW 1.0 1.0 77 | 78 | [TIMESERIES] 79 | ;;Name Date Time Value 80 | ;;-------------- ---------- ---------- ---------- 81 | OutfallStage 01/01/2020 00:00 0.1 82 | OutfallStage 01/01/2020 01:00 0.5 83 | OutfallStage 01/01/2020 02:00 0.9 84 | ; 85 | InflowTS 01/01/2020 00:00 0.1 86 | InflowTS 01/01/2020 02:00 0.2 87 | 88 | [REPORT] 89 | ;;Reporting Options 90 | SUBCATCHMENTS ALL 91 | NODES ALL 92 | LINKS ALL 93 | 94 | [TAGS] 95 | 96 | [MAP] 97 | DIMENSIONS 0.000 0.000 10000.000 10000.000 98 | Units None 99 | 100 | [COORDINATES] 101 | ;;Node X-Coord Y-Coord 102 | ;;-------------- ------------------ ------------------ 103 | 3 3444.194 6843.292 104 | 4 1629.087 6820.744 105 | StagedOutfall 3466.742 5456.595 106 | FreeOutfall 1606.539 5434.047 107 | 108 | [VERTICES] 109 | ;;Link X-Coord Y-Coord 110 | ;;-------------- ------------------ ------------------ 111 | 112 | -------------------------------------------------------------------------------- /swmmio/graphics/utils.py: -------------------------------------------------------------------------------- 1 | # UTILITY/HELPER FUNCTIONS FOR DRAWING 2 | import pandas as pd 3 | import math 4 | import os 5 | from PIL import Image 6 | 7 | 8 | def save_image(img, img_path, antialias=True, auto_open=False): 9 | # get the size from the Image object 10 | imgSize = (img.getbbox()[2], img.getbbox()[3]) 11 | if antialias: 12 | size = (int(imgSize[0] * 0.5), int(imgSize[1] * 0.5)) 13 | img.thumbnail(size, Image.LANCZOS) 14 | 15 | img.save(img_path) 16 | if auto_open: 17 | os.startfile(img_path) 18 | 19 | 20 | def px_to_irl_coords(df, px_width=4096.0, bbox=None, shift_ratio=None): 21 | """ 22 | given a dataframe with element id (as index) and X1, Y1 columns (and 23 | optionally X2, Y2 columns), return a dataframe with the coords as pixel 24 | locations based on the targetImgW. 25 | """ 26 | 27 | df = df.loc[pd.notnull(df.coords)] 28 | 29 | if not bbox: 30 | xs = [xy[0] for verts in df.coords.tolist() for xy in verts] 31 | ys = [xy[1] for verts in df.coords.tolist() for xy in verts] 32 | xmin, ymin, xmax, ymax = (min(xs), min(ys), max(xs), max(ys)) 33 | bbox = [(xmin, ymin), (xmax, ymax)] 34 | 35 | else: 36 | df = clip_to_box(df, bbox) # clip if necessary 37 | xmin = float(bbox[0][0]) 38 | ymin = float(bbox[0][1]) 39 | 40 | # find the actual dimensions, use to find scale factor 41 | height = bbox[1][1] - bbox[0][1] 42 | width = bbox[1][0] - bbox[0][0] 43 | 44 | if not shift_ratio: 45 | # to scale down from coordinate to pixels 46 | shift_ratio = float(px_width / width) 47 | 48 | def shft_coords(row): 49 | # parse through coords (nodes, or link) and adjust for pixel space 50 | return [(int((xy[0] - xmin) * shift_ratio), 51 | int((height - xy[1] + ymin) * shift_ratio)) 52 | for xy in row.coords] 53 | 54 | # insert new column with the shifted coordinates 55 | draw_coords = df.apply(lambda row: shft_coords(row), axis=1) 56 | if not (draw_coords.empty and df.empty): 57 | df = df.assign(draw_coords=draw_coords) 58 | 59 | return df, bbox, int(height * shift_ratio), int(width * shift_ratio), shift_ratio 60 | 61 | 62 | def circle_bbox(coordinates, radius=5): 63 | """the bounding box of a circle given as centriod coordinate and radius""" 64 | 65 | x = coordinates[0] 66 | y = coordinates[1] 67 | r = radius 68 | 69 | return (x - r, y - r, x + r, y + r) 70 | 71 | 72 | def clip_to_box(df, bbox): 73 | """clip a dataframe with a coords column to a bounding box""" 74 | 75 | def any_xy_in_box(row, bbox): 76 | # because im confused with list comprehensions rn 77 | return any([point_in_box(bbox, pt) for pt in row]) 78 | 79 | coords = df.coords.tolist() 80 | result = [any_xy_in_box(p, bbox) for p in coords] 81 | return df.loc[result] 82 | 83 | 84 | def angle_bw_points(xy1, xy2): 85 | dx, dy = (xy2[0] - xy1[0]), (xy2[1] - xy1[1]) 86 | 87 | angle = (math.atan(float(dx) / float(dy)) * 180 / math.pi) 88 | if angle < 0: 89 | angle = 270 - angle 90 | else: 91 | angle = 90 - angle 92 | # angle in radians 93 | return angle 94 | 95 | 96 | def midpoint(xy1, xy2): 97 | dx, dy = (xy2[0] + xy1[0]), (xy2[1] + xy1[1]) 98 | midpt = (int(dx / 2), int(dy / 2.0)) 99 | 100 | # angle in radians 101 | return midpt 102 | 103 | 104 | def point_in_box(bbox, point): 105 | """check if a point falls with in a bounding box, bbox""" 106 | LB = bbox[0] 107 | RU = bbox[1] 108 | 109 | x = point[0] 110 | y = point[1] 111 | 112 | if x < LB[0] or x > RU[0]: 113 | return False 114 | elif y < LB[1] or y > RU[1]: 115 | return False 116 | else: 117 | return True 118 | 119 | 120 | def length_bw_coords(upstreamXY, downstreamXY): 121 | # return the distance (units based on input) between two points 122 | x1 = float(upstreamXY[0]) 123 | x2 = float(downstreamXY[0]) 124 | y1 = float(upstreamXY[1]) 125 | y2 = float(downstreamXY[1]) 126 | 127 | return math.hypot(x2 - x1, y2 - y1) 128 | 129 | 130 | def rotate_coord_about_point(xy, radians, origin=(0, 0)): 131 | """Rotate a point around a given origin 132 | https://gist.github.com/LyleScott/e36e08bfb23b1f87af68c9051f985302 133 | """ 134 | x, y = xy 135 | offset_x, offset_y = origin 136 | adjusted_x = (x - offset_x) 137 | adjusted_y = (y - offset_y) 138 | cos_rad = math.cos(radians) 139 | sin_rad = math.sin(radians) 140 | qx = offset_x + cos_rad * adjusted_x + sin_rad * adjusted_y 141 | qy = offset_y + -sin_rad * adjusted_x + cos_rad * adjusted_y 142 | 143 | return qx, qy 144 | -------------------------------------------------------------------------------- /swmmio/tests/data/alt_test1.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | ;;Project Title/Notes 3 | 4 | [OPTIONS] 5 | ;;Option Value 6 | FLOW_UNITS CFS 7 | INFILTRATION HORTON 8 | FLOW_ROUTING KINWAVE 9 | LINK_OFFSETS DEPTH 10 | MIN_SLOPE 0 11 | ALLOW_PONDING NO 12 | SKIP_STEADY_STATE NO 13 | 14 | START_DATE 03/05/2018 15 | START_TIME 00:00:00 16 | REPORT_START_DATE 03/05/2018 17 | REPORT_START_TIME 00:00:00 18 | END_DATE 03/05/2018 19 | END_TIME 06:00:00 20 | SWEEP_START 1/1 21 | SWEEP_END 12/31 22 | DRY_DAYS 0 23 | REPORT_STEP 00:15:00 24 | WET_STEP 00:05:00 25 | DRY_STEP 01:00:00 26 | ROUTING_STEP 0:00:30 27 | 28 | INERTIAL_DAMPING PARTIAL 29 | NORMAL_FLOW_LIMITED BOTH 30 | FORCE_MAIN_EQUATION H-W 31 | VARIABLE_STEP 0.75 32 | LENGTHENING_STEP 0 33 | MIN_SURFAREA 0 34 | MAX_TRIALS 0 35 | HEAD_TOLERANCE 0 36 | SYS_FLOW_TOL 5 37 | LAT_FLOW_TOL 5 38 | MINIMUM_STEP 0.5 39 | THREADS 1 40 | 41 | [EVAPORATION] 42 | ;;Data Source Parameters 43 | ;;-------------- ---------------- 44 | CONSTANT 0.0 45 | DRY_ONLY NO 46 | 47 | [JUNCTIONS] 48 | ;;Name Elevation MaxDepth InitDepth SurDepth Aponded 49 | ;;-------------- ---------- ---------- ---------- ---------- ---------- 50 | dummy_node1 -10.99 30 0 0 0 51 | dummy_node2 -9.24 20 0 0 0 52 | dummy_node3 -7.76 20 0 0 0 53 | dummy_node4 -6.98 12.59314 0 0 177885 54 | dummy_node5 -6.96 13.05439 0 0 73511 55 | dummy_node6 -6.8 13.27183 0 0 0 56 | 57 | [OUTFALLS] 58 | ;;Name Elevation Type Stage Data Gated Route To 59 | ;;-------------- ---------- ---------- ---------------- -------- ---------------- 60 | dummy_outfall -11 FREE YES 61 | 62 | [CONDUITS] 63 | ;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow 64 | ;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- 65 | outfall_pipe dummy_node1 dummy_outfall 200 0.013 0 0 0 0 66 | pipe1 dummy_node2 dummy_node1 1675 0.013 0 0 0 0 67 | pipe2 dummy_node3 dummy_node2 400 0.01 0 0 0 0 68 | pipe3 dummy_node4 dummy_node3 594 0.013 0 0 0 0 69 | pipe4 dummy_node5 dummy_node4 400 0.013 0 0 0 0 70 | pipe5 dummy_node6 dummy_node5 188 0.013 0 0 0 0 71 | 72 | [XSECTIONS] 73 | ;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert 74 | ;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- 75 | outfall_pipe RECT_CLOSED 7 14 0 0 1 76 | pipe1 RECT_TRIANGULAR 7 14 1.5 0 1 77 | pipe2 RECT_TRIANGULAR 7 14 1.5 0 1 78 | pipe3 CIRCULAR 8 0 0 0 1 79 | pipe4 CIRCULAR 6.5 0 0 0 1 80 | pipe5 RECT_TRIANGULAR 6.5 13 1.434756791 0 1 81 | 82 | [DWF] 83 | ;;Node Constituent Baseline Patterns 84 | ;;-------------- ---------------- ---------- ---------- 85 | dummy_node2 FLOW 0.000275704 "" 86 | dummy_node6 FLOW 0.008150676 "" 87 | 88 | [REPORT] 89 | ;;Reporting Options 90 | INPUT NO 91 | CONTROLS NO 92 | SUBCATCHMENTS ALL 93 | NODES ALL 94 | LINKS ALL 95 | 96 | [TAGS] 97 | 98 | [MAP] 99 | DIMENSIONS -13594.704 4798.556 5809.792 15410.113 100 | Units None 101 | 102 | [COORDINATES] 103 | ;;Node X-Coord Y-Coord 104 | ;;-------------- ------------------ ------------------ 105 | dummy_node1 2054.575 6051.364 106 | dummy_node2 -2937.400 7704.655 107 | dummy_node3 -4205.457 9695.024 108 | dummy_node4 -6163.724 12857.143 109 | dummy_node5 -9871.589 13723.917 110 | dummy_node6 -12712.681 14927.769 111 | dummy_outfall 4927.769 5280.899 112 | 113 | [VERTICES] 114 | ;;Link X-Coord Y-Coord 115 | ;;-------------- ------------------ ------------------ 116 | 117 | -------------------------------------------------------------------------------- /swmmio/tests/data/baseline_test.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | ;;Project Title/Notes 3 | 4 | [OPTIONS] 5 | ;;Option Value 6 | FLOW_UNITS CFS 7 | INFILTRATION HORTON 8 | FLOW_ROUTING KINWAVE 9 | LINK_OFFSETS DEPTH 10 | MIN_SLOPE 0 11 | ALLOW_PONDING NO 12 | SKIP_STEADY_STATE NO 13 | 14 | START_DATE 03/05/2018 15 | START_TIME 00:00:00 16 | REPORT_START_DATE 03/05/2018 17 | REPORT_START_TIME 00:00:00 18 | END_DATE 03/05/2018 19 | END_TIME 06:00:00 20 | SWEEP_START 1/1 21 | SWEEP_END 12/31 22 | DRY_DAYS 0 23 | REPORT_STEP 00:15:00 24 | WET_STEP 00:05:00 25 | DRY_STEP 01:00:00 26 | ROUTING_STEP 0:00:30 27 | 28 | INERTIAL_DAMPING PARTIAL 29 | NORMAL_FLOW_LIMITED BOTH 30 | FORCE_MAIN_EQUATION H-W 31 | VARIABLE_STEP 0.75 32 | LENGTHENING_STEP 0 33 | MIN_SURFAREA 0 34 | MAX_TRIALS 0 35 | HEAD_TOLERANCE 0 36 | SYS_FLOW_TOL 5 37 | LAT_FLOW_TOL 5 38 | MINIMUM_STEP 0.5 39 | THREADS 1 40 | 41 | [EVAPORATION] 42 | ;;Data Source Parameters 43 | ;;-------------- ---------------- 44 | CONSTANT 0.0 45 | DRY_ONLY NO 46 | 47 | [JUNCTIONS] 48 | ;;Name Elevation MaxDepth InitDepth SurDepth Aponded 49 | ;;-------------- ---------- ---------- ---------- ---------- ---------- 50 | dummy_node1 -10.99 30 0 0 0 51 | dummy_node2 -9.24 20 0 0 0 52 | dummy_node3 -7.76 20 0 0 0 53 | dummy_node4 -6.98 12.59314 0 0 177885 54 | dummy_node5 -6.96 13.05439 0 0 73511 55 | dummy_node6 -6.8 13.27183 0 0 0 56 | 57 | [OUTFALLS] 58 | ;;Name Elevation Type Stage Data Gated Route To 59 | ;;-------------- ---------- ---------- ---------------- -------- ---------------- 60 | dummy_outfall -11 FREE YES 61 | 62 | [CONDUITS] 63 | ;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow 64 | ;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- 65 | outfall_pipe dummy_node1 dummy_outfall 200 0.013 0 0 0 0 66 | pipe1 dummy_node2 dummy_node1 1675 0.013 0 0 0 0 67 | pipe2 dummy_node3 dummy_node2 400 0.01 0 0 0 0 68 | pipe3 dummy_node4 dummy_node3 594 0.013 0 0 0 0 69 | pipe4 dummy_node5 dummy_node4 400 0.013 0 0 0 0 70 | pipe5 dummy_node6 dummy_node5 188 0.013 0 0 0 0 71 | 72 | [XSECTIONS] 73 | ;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert 74 | ;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- 75 | outfall_pipe RECT_CLOSED 7 14 0 0 1 76 | pipe1 RECT_TRIANGULAR 7 14 1.5 0 1 77 | pipe2 RECT_TRIANGULAR 7 14 1.5 0 1 78 | pipe3 RECT_TRIANGULAR 7 14 1.5 0 1 79 | pipe4 CIRCULAR 6.5 0 0 0 1 80 | pipe5 RECT_TRIANGULAR 6.5 13 1.434756791 0 1 81 | 82 | [DWF] 83 | ;;Node Constituent Baseline Patterns 84 | ;;-------------- ---------------- ---------- ---------- 85 | dummy_node2 FLOW 0.000275704 "" "" "" 86 | dummy_node6 FLOW 0.008150676 "" "" "" 87 | 88 | [REPORT] 89 | ;;Reporting Options 90 | INPUT NO 91 | CONTROLS NO 92 | SUBCATCHMENTS ALL 93 | NODES ALL 94 | LINKS ALL 95 | 96 | [TAGS] 97 | 98 | [MAP] 99 | DIMENSIONS 0.000 0.000 10000.000 10000.000 100 | Units None 101 | 102 | [COORDINATES] 103 | ;;Node X-Coord Y-Coord 104 | ;;-------------- ------------------ ------------------ 105 | dummy_node1 2054.575 6051.364 106 | dummy_node2 -2937.400 7704.655 107 | dummy_node3 -4205.457 9695.024 108 | dummy_node4 -6163.724 12857.143 109 | dummy_node5 -9871.589 13723.917 110 | dummy_node6 -12712.681 14927.769 111 | dummy_outfall 4927.769 5280.899 112 | 113 | [VERTICES] 114 | ;;Link X-Coord Y-Coord 115 | ;;-------------- ------------------ ------------------ 116 | 117 | -------------------------------------------------------------------------------- /swmmio/tests/data/alt_test2.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | ;;Project Title/Notes 3 | 4 | [OPTIONS] 5 | ;;Option Value 6 | FLOW_UNITS CFS 7 | INFILTRATION HORTON 8 | FLOW_ROUTING KINWAVE 9 | LINK_OFFSETS DEPTH 10 | MIN_SLOPE 0 11 | ALLOW_PONDING NO 12 | SKIP_STEADY_STATE NO 13 | 14 | START_DATE 03/05/2018 15 | START_TIME 00:00:00 16 | REPORT_START_DATE 03/05/2018 17 | REPORT_START_TIME 00:00:00 18 | END_DATE 03/05/2018 19 | END_TIME 06:00:00 20 | SWEEP_START 1/1 21 | SWEEP_END 12/31 22 | DRY_DAYS 0 23 | REPORT_STEP 00:15:00 24 | WET_STEP 00:05:00 25 | DRY_STEP 01:00:00 26 | ROUTING_STEP 0:00:30 27 | 28 | INERTIAL_DAMPING PARTIAL 29 | NORMAL_FLOW_LIMITED BOTH 30 | FORCE_MAIN_EQUATION H-W 31 | VARIABLE_STEP 0.75 32 | LENGTHENING_STEP 0 33 | MIN_SURFAREA 0 34 | MAX_TRIALS 0 35 | HEAD_TOLERANCE 0 36 | SYS_FLOW_TOL 5 37 | LAT_FLOW_TOL 5 38 | MINIMUM_STEP 0.5 39 | THREADS 1 40 | 41 | [EVAPORATION] 42 | ;;Data Source Parameters 43 | ;;-------------- ---------------- 44 | CONSTANT 0.0 45 | DRY_ONLY NO 46 | 47 | [JUNCTIONS] 48 | ;;Name Elevation MaxDepth InitDepth SurDepth Aponded 49 | ;;-------------- ---------- ---------- ---------- ---------- ---------- 50 | dummy_node1 -10.99 30 0 0 0 51 | dummy_node2 -9.24 20 0 0 0 52 | dummy_node3 -7.76 20 0 0 0 53 | dummy_node4 -6.98 12.59314 0 0 177885 54 | dummy_node5 -6.96 13.05439 0 0 73511 55 | dummy_node6 -6.8 13.27183 0 0 0 56 | 57 | [OUTFALLS] 58 | ;;Name Elevation Type Stage Data Gated Route To 59 | ;;-------------- ---------- ---------- ---------------- -------- ---------------- 60 | dummy_outfall -11 FREE YES 61 | 62 | [CONDUITS] 63 | ;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow 64 | ;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- 65 | outfall_pipe dummy_node1 dummy_outfall 200 0.013 0 0 0 0 66 | pipe1 dummy_node2 dummy_node1 1675 0.013 0 0 0 0 67 | pipe2 dummy_node3 dummy_node2 400 0.01 0 0 0 0 68 | pipe3 dummy_node4 dummy_node3 594 0.013 0 0 0 0 69 | pipe4 dummy_node5 dummy_node4 400 0.013 0 0 0 0 70 | pipe5 dummy_node6 dummy_node5 188 0.013 0 0 0 0 71 | 72 | [XSECTIONS] 73 | ;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert 74 | ;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- 75 | outfall_pipe RECT_CLOSED 7 14 0 0 1 76 | pipe1 RECT_TRIANGULAR 7 14 1.5 0 1 77 | pipe2 RECT_TRIANGULAR 7 14 1.5 0 1 78 | pipe3 RECT_TRIANGULAR 7 14 1.5 0 1 79 | pipe4 RECT_TRIANGULAR 6.5 13 1.434756791 0 1 80 | pipe5 RECT_TRIANGULAR 6.5 13 1.434756791 0 1 81 | 82 | [DWF] 83 | ;;Node Constituent Baseline Patterns 84 | ;;-------------- ---------------- ---------- ---------- 85 | dummy_node2 FLOW 0.000275704 "" "" 86 | dummy_node6 FLOW 0.008150676 "" "" 87 | 88 | [REPORT] 89 | ;;Reporting Options 90 | INPUT NO 91 | CONTROLS NO 92 | SUBCATCHMENTS ALL 93 | NODES ALL 94 | LINKS ALL 95 | 96 | [TAGS] 97 | 98 | [MAP] 99 | DIMENSIONS -13594.704 4798.556 5809.792 15410.113 100 | Units None 101 | 102 | [COORDINATES] 103 | ;;Node X-Coord Y-Coord 104 | ;;-------------- ------------------ ------------------ 105 | dummy_node1 2054.575 6051.364 106 | dummy_node2 -2937.400 7704.655 107 | dummy_node3 -4205.457 9695.024 108 | dummy_node4 -6163.724 12857.143 109 | dummy_node5 -9871.589 13723.917 110 | dummy_node6 -12712.681 14927.769 111 | dummy_outfall 4927.769 5280.899 112 | 113 | [VERTICES] 114 | ;;Link X-Coord Y-Coord 115 | ;;-------------- ------------------ ------------------ 116 | 117 | -------------------------------------------------------------------------------- /swmmio/defs/inp_sections.yml: -------------------------------------------------------------------------------- 1 | 2 | infiltration_cols: 3 | CURVE_NUMBER: [Subcatchment, CurveNum, Conductivity (depreciated), DryTime, Param4, Param5] 4 | GREEN_AMPT: [Subcatchment, Suction, HydCon, IMDmax, Param4, Param5] 5 | HORTON: [Subcatchment, MaxRate, MinRate, Decay, DryTime, MaxInfil] 6 | MODIFIED_GREEN_AMPT: [Subcatchment, Suction, Ksat, IMD, Param4, Param5] 7 | MODIFIED_HORTON: [Subcatchment, MaxRate, MinRate, Decay, DryTime, MaxInfil] 8 | 9 | inp_file_objects: 10 | TITLE: 11 | columns: [blob] 12 | OPTIONS: 13 | keys: [FLOW_UNITS, INFILTRATION, FLOW_ROUTING, START_DATE, START_TIME, 14 | END_DATE, END_TIME, REPORT_START_DATE, REPORT_START_TIME, SWEEP_START, SWEEP_END, 15 | DRY_DAYS, WET_STEP, DRY_STEP, ROUTING_STEP, REPORT_STEP, RULE_STEP, ALLOW_PONDING, 16 | INERTIAL_DAMPING, SLOPE_WEIGHTING, VARIABLE_STEP, NORMAL_FLOW_LIMITED, LENGTHENING_STEP, 17 | MIN_SURFAREA, COMPATIBILITY, SKIP_STEADY_STATE, TEMPDIR, IGNORE_RAINFALL, FORCE_MAIN_EQUATION, 18 | LINK_OFFSETS, MIN_SLOPE, IGNORE_SNOWMELT, IGNORE_GROUNDWATER, IGNORE_ROUTING, IGNORE_QUALITY, 19 | MAX_TRIALS, HEAD_TOLERANCE, SYS_FLOW_TOL, LAT_FLOW_TOL, IGNORE_RDII, MINIMUM_STEP, 20 | THREADS, SURCHARGE_METHOD] 21 | FILES: 22 | columns: [Action, FileType, FileName] 23 | RAINGAGES: 24 | columns: [Name, RainType, TimeIntrvl, SnowCatch, DataSource, DataSourceName] 25 | EVAPORATION: 26 | keys: [Type, Parameters] 27 | LOSSES: 28 | # SeepageRage is new? 29 | columns: 30 | - Link 31 | - Inlet 32 | - Outlet 33 | - Average 34 | - Flap Gate 35 | - SeepageRate 36 | CONDUITS: [Name, InletNode, OutletNode, Length, Roughness, InOffset, OutOffset, InitFlow, MaxFlow] 37 | INFILTRATION: [Subcatchment, Suction, HydCon, IMDmax] 38 | AQUIFERS: [Name, Por, WP, FC, Ksat, Kslope, Tslope, ETu, ETs, Seep, Ebot, Egw, Umc, ETupat] 39 | GROUNDWATER: [Subcatchment, Aquifer, Node, Esurf, A1, B1, A2, B2, A3, Dsw, Egwt, Ebot, Wgr, Umc] 40 | JUNCTIONS: [Name, InvertElev, MaxDepth, InitDepth, SurchargeDepth, PondedArea] 41 | DWF: 42 | columns: [Node, Parameter, AverageValue, TimePatterns] 43 | RDII: [Node, UnitHydrograph, SewerArea] 44 | HYDROGRAPHS: [Hydrograph, RainGage/Month, Response, R, T, K, Dmax, Drecov, Dinit] 45 | LANDUSES: [Name, CleaningInterval, FractionAvailable, LastCleaned] 46 | BUILDUP: [LandUse, Pollutant, Function, Coeff1, Coeff2, Coeff3, Normalizer] 47 | WASHOFF: [LandUse, Pollutant, Function, Coeff1, Coeff2, CleaningEffic, BMPEffic] 48 | COVERAGES: [Subcatchment, LandUse, Percent] 49 | LOADINGS: [Subcatchment, Pollutant, Loading] 50 | ORIFICES: 51 | columns: [Name, InletNode, OutletNode, OrificeType, CrestHeight, DischCoeff, FlapGate, OpenCloseTime] 52 | OUTFALLS: [Name, InvertElev, OutfallType, StageOrTimeseries, TideGate, RouteTo] 53 | OUTLETS: [Name, InletNode, OutletNode, OutflowHeight, OutletType, Qcoeff/QTable, Qexpon, FlapGate] 54 | PUMPS: [Name, InletNode, OutletNode, PumpCurve, InitStatus, Depth, ShutoffDepth] 55 | STORAGE: [Name, InvertElev, MaxD, InitDepth, StorageCurve, Coefficient, Exponent, 56 | Constant, PondedArea, EvapFrac, SuctionHead, Conductivity, InitialDeficit] 57 | DIVIDERS: [Name, Elevation, Diverted Link, Type, Parameters] 58 | SUBCATCHMENTS: [Name, Raingage, Outlet, Area, PercImperv, Width, PercSlope, 59 | CurbLength, SnowPack] 60 | SUBAREAS: [Name, N-Imperv, N-Perv, S-Imperv, S-Perv, PctZero, RouteTo, PctRouted] 61 | WEIRS: [Name, InletNode, OutletNode, WeirType, CrestHeight, DischCoeff, FlapGate, EndCon, EndCoeff, 62 | Surcharge, RoadWidth, RoadSurf] 63 | XSECTIONS: [Link, Shape, Geom1, Geom2, Geom3, Geom4, Barrels, XX] 64 | POLLUTANTS: [Name, MassUnits, RainConcen, GWConcen, I&IConcen, DecayCoeff, SnowOnly, 65 | CoPollutName, CoPollutFraction, DWFConcen, InitConcen] 66 | INFLOWS: [Node, Constituent, Time Series, Type, Mfactor, Sfactor, Baseline, Pattern] 67 | LID_USAGE: [Subcatchment, LID_Process, Number, Area, Width, InitSat, FromImp, ToPerv, RptFile, DrainTo, FromPerv] 68 | TIMESERIES: [Name, Date, Time, Value] 69 | COORDINATES: [Name, X, Y] 70 | VERTICES: [Name, X, Y] 71 | Polygons: [Name, X, Y] 72 | POLYGONS: [Name, X, Y] 73 | MAP: [Param, x1, y1, x2, y2] 74 | REPORT: [Param, Status] 75 | TAGS: [ElementType, Name, Tag] 76 | ADJUSTMENTS: [blob] 77 | CURVES: [Name, Type, X-Value, Y-Value] 78 | STREETS: [Name, Tcrown, Hcurb, Sx, nRoad, a, W, Sides, Tback, Sback, nBack] 79 | INLETS: [Name, Type, Param1, Param2, Param3, Param4, Param5] 80 | INLET_USAGE: [Link, Inlet, Node, Number, "%Clogged", Qmax, aLocal, wLocal, Placement] 81 | PATTERNS: [Name, Type, Factors] 82 | CONTROLS: [blob] 83 | 84 | inp_section_tags: 85 | ['[TITLE', '[OPTION', '[FILE', '[RAINGAGES', '[TEMPERATURE', '[EVAP', 86 | '[SUBCATCHMENT', '[SUBAREA', '[INFIL', '[AQUIFER', '[GROUNDWATER', '[SNOWPACK', 87 | '[JUNC', '[OUTFALL', '[STORAGE', '[DIVIDER', '[CONDUIT', '[PUMP', '[ORIFICE', '[WEIR', 88 | '[OUTLET', '[XSECT', '[TRANSECT', '[LOSS', '[CONTROL', '[POLLUT', '[LANDUSE', '[BUILDUP', 89 | '[WASHOFF', '[COVERAGE', '[INFLOW', '[DWF', '[PATTERN', '[RDII', '[HYDROGRAPH', 90 | '[LOADING', '[TREATMENT', '[CURVE', '[TIMESERIES', '[REPORT', '[MAP', '[COORDINATE', 91 | '[VERTICES', '[POLYGON', '[Polygons', '[SYMBOL', '[LABEL', '[BACKDROP', '[TAG', '[PROFILE', '[LID_CONTROL', 92 | '[LID_USAGE', '[GW_FLOW', '[GWF', '[ADJUSTMENT', '[EVENT', '[STREETS', '[INLETS', '[INLET_USAGE',] 93 | 94 | -------------------------------------------------------------------------------- /swmmio/defs/section_headers.yml: -------------------------------------------------------------------------------- 1 | analysis_options: [FLOW_UNITS, INFILTRATION, FLOW_ROUTING, START_DATE, START_TIME, 2 | END_DATE, END_TIME, REPORT_START_DATE, REPORT_START_TIME, SWEEP_START, SWEEP_END, 3 | DRY_DAYS, WET_STEP, DRY_STEP, ROUTING_STEP, REPORT_STEP, RULE_STEP, ALLOW_PONDING, 4 | INERTIAL_DAMPING, SLOPE_WEIGHTING, VARIABLE_STEP, NORMAL_FLOW_LIMITED, LENGTHENING_STEP, 5 | MIN_SURFAREA, COMPATIBILITY, SKIP_STEADY_STATE, TEMPDIR, IGNORE_RAINFALL, FORCE_MAIN_EQUATION, 6 | LINK_OFFSETS, MIN_SLOPE, IGNORE_SNOWMELT, IGNORE_GROUNDWATER, IGNORE_ROUTING, IGNORE_QUALITY, 7 | MAX_TRIALS, HEAD_TOLERANCE, SYS_FLOW_TOL, LAT_FLOW_TOL, IGNORE_RDII, MINIMUM_STEP, 8 | THREADS, SURCHARGE_METHOD] 9 | conduits: 10 | columns: [Name, InletNode, OutletNode, Length, ManningN, InOffset, OutOffset, 11 | InitFlow, MaxFlow] 12 | inp_sections: ['[CONDUITS]', '[XSECTIONS]'] 13 | rpt_sections: [Link Flow Summary] 14 | 15 | rpt_sections: 16 | Cross Section Summary: 17 | - Name 18 | - Shape 19 | - DFull 20 | - AreaFull 21 | - HydRad 22 | - MaxW 23 | - NumBarrels 24 | - FullFlow 25 | Link Flow Summary: 26 | - Name 27 | - Type 28 | - MaxQ 29 | - MaxDay 30 | - MaxHr 31 | - MaxV 32 | - MaxQPerc 33 | - MaxDPerc 34 | Link Results: 35 | - Date 36 | - Time 37 | - FlowCFS 38 | - VelocityFPS 39 | - DepthFt 40 | - PercentFull 41 | Link Summary: 42 | - Name 43 | - FromNode 44 | - ToNode 45 | - Type 46 | - Length 47 | - SlopePerc 48 | - Roughness 49 | Node Depth Summary: 50 | - Name 51 | - Type_Node_Depth_Summary 52 | - AvgDepth 53 | - MaxNodeDepth 54 | - MaxHGL 55 | - MaxDay_Node_Depth_Summary 56 | - MaxHr_Node_Depth_Summary 57 | - MaxNodeDepthReported 58 | Node Flooding Summary: 59 | - Name 60 | - HoursFlooded 61 | - MaxQFlooding 62 | - MaxDay_Node_Flooding_Summary 63 | - MaxHr_Node_Flooding_Summary 64 | - TotalFloodVol 65 | - MaximumPondDepth 66 | Node Inflow Summary: 67 | - Name 68 | - Type 69 | - MaxLatInflow 70 | - MaxTotalInflow 71 | - MaxDay 72 | - MaxHr 73 | - LatInflowV 74 | - TotalInflowV 75 | - FlowBalErrorPerc 76 | - XXX 77 | Node Results: 78 | - Date 79 | - Time 80 | - InflowCFS 81 | - FloodingCFS 82 | - DepthFt 83 | - HeadFt 84 | - TSS 85 | - TP 86 | - TN 87 | Node Summary: 88 | - Name 89 | - Type_Node_Summary 90 | - InvertEl 91 | - MaxD 92 | - PondedA 93 | - ExternalInf 94 | Node Surcharge Summary: 95 | - Name 96 | - Type_Node_Surcharge_Summary 97 | - HourSurcharged 98 | - MaxHeightAboveCrown 99 | - MinDepthBelowRim 100 | Storage Volume Summary: 101 | - Name 102 | - AvgVolume 103 | - AvgPctFull 104 | - EvapPctLoss 105 | - ExfilPctLoss 106 | - MaxVolume 107 | - MaxPctFull 108 | - MaxDay 109 | - MaxFullHr 110 | - MaxOutflow 111 | Subcatchment Results: 112 | - Date 113 | - Time 114 | - PrecipInchPerHour 115 | - LossesInchPerHr 116 | - RunoffCFS 117 | Subcatchment Runoff Summary: 118 | - Name 119 | - TotalPrecip 120 | - TotalRunon 121 | - TotalEvap 122 | - TotalInfil 123 | - TotalRunoffIn 124 | - TotalRunoffMG 125 | - PeakRunoff 126 | - RunoffCoeff 127 | Subcatchment Summary: 128 | - Name 129 | - Area 130 | - Width 131 | - ImpervPerc 132 | - SlopePerc 133 | - RainGage 134 | - Outlet 135 | Pumping Summary: 136 | - PercentUtilized 137 | - NumberOfStartUps 138 | - MinFlowCFS 139 | - AvgFlowCFS 140 | - MaxFlowCFS 141 | - TotalVolume(MG) 142 | - PowerUsage(kW-hr) 143 | - PercentTimeOffPumpCurveLow 144 | - PercentTimeOffPumpCurveHigh 145 | 146 | swmm5_version: 147 | 1.13: 148 | rpt_sections: 149 | Subcatchment Runoff Summary: 150 | - Name 151 | - TotalPrecip 152 | - TotalRunon 153 | - TotalEvap 154 | - TotalInfil 155 | - ImpervRunoff 156 | - PervRunoff 157 | - TotalRunoffIn 158 | - TotalRunoffMG 159 | - PeakRunoff 160 | - RunoffCoeff 161 | 162 | composite: 163 | nodes: 164 | inp_sections: [junctions, outfalls, storage] 165 | rpt_sections: [Node Depth Summary, Node Flooding Summary, Node Inflow Summary] 166 | columns: [InvertElev, MaxDepth, InitDepth, SurchargeDepth, PondedArea, 167 | OutfallType, StageOrTimeseries, TideGate, MaxD, StorageCurve, 168 | Coefficient, Exponent, Constant, EvapFrac, SuctionHead, 169 | Conductivity, InitialDeficit, coords] 170 | junctions: 171 | columns: Name, Elevation, MaxDepth, InitDepth, SurDepth, Aponded 172 | inp_sections: [] 173 | orifices: 174 | inp_sections: [ORIFICES] 175 | rpt_sections: [Link Flow Summary] 176 | outfalls: [Name, Elevation, Type, Stage Data, Gated, Route To] 177 | links: 178 | inp_sections: [CONDUITS, WEIRS, ORIFICES, PUMPS, OUTLETS] 179 | join_sections: [XSECTIONS] 180 | rpt_sections: [Link Flow Summary] 181 | pumps: 182 | inp_sections: [pumps] 183 | rpt_sections: [Link Flow Summary] 184 | subcatchments: 185 | inp_sections: [subcatchments] 186 | join_sections: [subareas] 187 | rpt_sections: [Subcatchment Runoff Summary] 188 | geomtype: polygon 189 | -------------------------------------------------------------------------------- /swmmio/tests/test_functions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unittest 3 | from unittest.mock import patch, mock_open, MagicMock 4 | 5 | import swmmio 6 | from swmmio.tests.data import (MODEL_FULL_FEATURES__NET_PATH, 7 | OWA_RPT_EXAMPLE, RPT_FULL_FEATURES, 8 | MODEL_EX_1_PARALLEL_LOOP, MODEL_EX_1) 9 | from swmmio.utils.functions import format_inp_section_header, find_network_trace, check_if_url_and_download 10 | from swmmio.utils import error 11 | from swmmio.utils.text import get_rpt_metadata 12 | 13 | 14 | def test_format_inp_section_header(): 15 | 16 | header_string = '[CONDUITS]' 17 | header_string = format_inp_section_header(header_string) 18 | assert(header_string == '[CONDUITS]') 19 | 20 | header_string = '[conduits]' 21 | header_string = format_inp_section_header(header_string) 22 | assert (header_string == '[CONDUITS]') 23 | 24 | header_string = 'JUNCTIONS' 25 | header_string = format_inp_section_header(header_string) 26 | assert (header_string == '[JUNCTIONS]') 27 | 28 | header_string = 'pumps' 29 | header_string = format_inp_section_header(header_string) 30 | assert (header_string == '[PUMPS]') 31 | 32 | 33 | def test_get_rpt_metadata_owa_swmm(): 34 | meta = get_rpt_metadata(OWA_RPT_EXAMPLE) 35 | assert meta['swmm_version'] == {'major': 5, 'minor': 1, 'patch': 14} 36 | 37 | 38 | def test_get_rpt_metadata_epa_swmm(): 39 | meta = get_rpt_metadata(RPT_FULL_FEATURES) 40 | assert meta['swmm_version'] == {'major': 5, 'minor': 0, 'patch': 22} 41 | 42 | 43 | def test_model_to_networkx(): 44 | m = swmmio.Model(MODEL_FULL_FEATURES__NET_PATH) 45 | G = m.network 46 | 47 | assert (G['J2']['J3']['C2.1']['Length'] == 666) 48 | assert (G['J1']['J2']['C1:C2']['Length'] == 244.63) 49 | assert (round(G.nodes['J2']['InvertElev'], 3) == 13.0) 50 | 51 | links = m.links.dataframe 52 | assert(len(links) == len(G.edges())) 53 | 54 | 55 | def test_network_trace_loop(): 56 | m = swmmio.Model(MODEL_EX_1_PARALLEL_LOOP) 57 | start_node = "9" 58 | end_node = "18" 59 | path_selection = find_network_trace(m, start_node, end_node, 60 | include_nodes=[], 61 | include_links=["LOOP"]) 62 | correct_path = [('9', '10', '1'), 63 | ('10', '21', '6'), 64 | ('21', '24', 'LOOP'), 65 | ('24', '17', '16'), 66 | ('17', '18', '10')] 67 | assert (path_selection == correct_path) 68 | 69 | 70 | def test_network_trace_bad_link(): 71 | m = swmmio.Model(MODEL_EX_1) 72 | start_node = "9" 73 | end_node = "18" 74 | with pytest.raises(error.LinkNotInInputFile) as execinfo: 75 | path_selection = find_network_trace(m, start_node, end_node, 76 | include_links=["LOOP"]) 77 | 78 | 79 | def test_network_trace_bad_start_node(): 80 | m = swmmio.Model(MODEL_EX_1) 81 | start_node = "9000" 82 | end_node = "18" 83 | with pytest.raises(error.NodeNotInInputFile): 84 | path_selection = find_network_trace(m, start_node, end_node) 85 | 86 | 87 | def test_network_trace_bad_end_node(): 88 | m = swmmio.Model(MODEL_EX_1) 89 | start_node = "9" 90 | end_node = "18000" 91 | with pytest.raises(error.NodeNotInInputFile): 92 | path_selection = find_network_trace(m, start_node, end_node) 93 | 94 | 95 | def test_network_trace_bad_include_node(): 96 | m = swmmio.Model(MODEL_EX_1) 97 | start_node = "9" 98 | end_node = "18" 99 | with pytest.raises(error.NodeNotInInputFile): 100 | path_selection = find_network_trace(m, start_node, 101 | end_node, 102 | include_nodes=["1000"]) 103 | 104 | class TestCheckIfUrlAndDownload(unittest.TestCase): 105 | 106 | @patch('requests.get') 107 | @patch('tempfile.gettempdir') 108 | @patch('builtins.open', new_callable=mock_open) 109 | def test_download_file(self, mock_open, mock_gettempdir, mock_requests_get): 110 | # Mock the response from requests.get 111 | mock_response = MagicMock() 112 | mock_response.status_code = 200 113 | mock_response.content = b'Test content' 114 | mock_requests_get.return_value = mock_response 115 | 116 | # Mock the temporary directory 117 | mock_gettempdir.return_value = '/tmp' 118 | 119 | url = 'https://example.com/path/to/file.txt' 120 | expected_path = '/tmp/file.txt' 121 | 122 | result = check_if_url_and_download(url) 123 | 124 | # Check if the file was written correctly 125 | mock_open.assert_called_once_with(expected_path, 'wb') 126 | mock_open().write.assert_called_once_with(b'Test content') 127 | 128 | self.assertEqual(result, expected_path) 129 | 130 | @patch('requests.get') 131 | def test_download_file_failed(self, mock_requests_get): 132 | # Mock the response from requests.get 133 | mock_response = MagicMock() 134 | mock_response.status_code = 404 135 | mock_requests_get.return_value = mock_response 136 | 137 | url = 'https://example.com/path/to/file.txt' 138 | 139 | with self.assertRaises(Exception) as context: 140 | check_if_url_and_download(url) 141 | 142 | self.assertIn('Failed to download file: 404', str(context.exception)) 143 | 144 | def test_not_a_url(self): 145 | non_url_string = '/Users/bingo/models/not_a_url.inp' 146 | result = check_if_url_and_download(non_url_string) 147 | self.assertEqual(result, non_url_string) -------------------------------------------------------------------------------- /swmmio/tests/data/alt_test3.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | ;;Project Title/Notes 3 | 4 | [OPTIONS] 5 | ;;Option Value 6 | FLOW_UNITS CFS 7 | INFILTRATION HORTON 8 | FLOW_ROUTING KINWAVE 9 | LINK_OFFSETS DEPTH 10 | MIN_SLOPE 0 11 | ALLOW_PONDING NO 12 | SKIP_STEADY_STATE NO 13 | 14 | START_DATE 03/05/2018 15 | START_TIME 00:00:00 16 | REPORT_START_DATE 03/05/2018 17 | REPORT_START_TIME 00:00:00 18 | END_DATE 03/05/2018 19 | END_TIME 06:00:00 20 | SWEEP_START 1/1 21 | SWEEP_END 12/31 22 | DRY_DAYS 0 23 | REPORT_STEP 00:15:00 24 | WET_STEP 00:05:00 25 | DRY_STEP 01:00:00 26 | ROUTING_STEP 0:00:30 27 | 28 | INERTIAL_DAMPING PARTIAL 29 | NORMAL_FLOW_LIMITED BOTH 30 | FORCE_MAIN_EQUATION H-W 31 | VARIABLE_STEP 0.75 32 | LENGTHENING_STEP 0 33 | MIN_SURFAREA 0 34 | MAX_TRIALS 0 35 | HEAD_TOLERANCE 0 36 | SYS_FLOW_TOL 5 37 | LAT_FLOW_TOL 5 38 | MINIMUM_STEP 0.5 39 | THREADS 1 40 | 41 | [EVAPORATION] 42 | ;;Data Source Parameters 43 | ;;-------------- ---------------- 44 | CONSTANT 0.0 45 | DRY_ONLY NO 46 | 47 | [JUNCTIONS] 48 | ;;Name Elevation MaxDepth InitDepth SurDepth Aponded 49 | ;;-------------- ---------- ---------- ---------- ---------- ---------- 50 | dummy_node1 -15 30 0 0 0 51 | dummy_node2 -9.24 20 0 0 0 52 | dummy_node3 -7.76 20 0 0 0 53 | dummy_node4 -6.98 12.59314 0 0 177885 54 | dummy_node5 -6.96 15 0 0 73511 55 | dummy_node6 -6.8 13.27183 0 0 0 56 | 57 | [OUTFALLS] 58 | ;;Name Elevation Type Stage Data Gated Route To 59 | ;;-------------- ---------- ---------- ---------------- -------- ---------------- 60 | dummy_outfall -11 FREE YES 61 | 62 | [CONDUITS] 63 | ;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow 64 | ;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- 65 | outfall_pipe dummy_node1 dummy_outfall 200 0.013 0 0 0 0 66 | pipe1 dummy_node2 dummy_node1 1675 0.013 0 0 0 0 67 | pipe2 dummy_node3 dummy_node2 400 0.01 0 0 0 0 68 | pipe3 dummy_node4 dummy_node3 594 0.013 0 0 0 0 69 | pipe4 dummy_node5 dummy_node4 400 0.013 0 0 0 0 70 | pipe5 dummy_node6 dummy_node5 666 0.013 0 0 0 0 71 | 72 | [XSECTIONS] 73 | ;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert 74 | ;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- 75 | outfall_pipe RECT_CLOSED 7 14 0 0 1 76 | pipe1 RECT_TRIANGULAR 7 14 1.5 0 1 77 | pipe2 RECT_TRIANGULAR 7 14 1.5 0 1 78 | pipe3 RECT_TRIANGULAR 7 14 1.5 0 1 79 | pipe4 RECT_TRIANGULAR 6.5 13 1.434756791 0 1 80 | pipe5 RECT_TRIANGULAR 6.5 13 1.434756791 0 1 81 | 82 | [INFLOWS] 83 | ;;Node Constituent Time Series Type Mfactor Sfactor Baseline Pattern 84 | ;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- 85 | dummy_node2 FLOW my_time_series FLOW 1.0 1.0 0.0333 86 | dummy_node6 FLOW "" FLOW 1.0 1.0 0.6666 87 | 88 | [DWF] 89 | ;;Node Constituent Baseline Patterns 90 | ;;-------------- ---------------- ---------- ---------- 91 | dummy_node2 FLOW 0.000275 "" "" "" 92 | dummy_node6 FLOW 0.008150676 "" "" "" 93 | 94 | [TIMESERIES] 95 | ;;Name Date Time Value 96 | ;;-------------- ---------- ---------- ---------- 97 | ;cool 98 | my_time_series 0 0 99 | my_time_series 1 1 100 | my_time_series 2 2 101 | my_time_series 3 1 102 | my_time_series 4 0 103 | 104 | [REPORT] 105 | ;;Reporting Options 106 | INPUT NO 107 | CONTROLS NO 108 | SUBCATCHMENTS ALL 109 | NODES ALL 110 | LINKS ALL 111 | 112 | [TAGS] 113 | 114 | [MAP] 115 | DIMENSIONS -13594.704 4798.556 5809.792 15410.113 116 | Units None 117 | 118 | [COORDINATES] 119 | ;;Node X-Coord Y-Coord 120 | ;;-------------- ------------------ ------------------ 121 | dummy_node1 2054.575 6051.364 122 | dummy_node2 -2937.400 7704.655 123 | dummy_node3 -4205.457 9695.024 124 | dummy_node4 -6163.724 12857.143 125 | dummy_node5 -9871.589 13723.917 126 | dummy_node6 -12712.681 14927.769 127 | dummy_outfall 4927.769 5280.899 128 | 129 | [VERTICES] 130 | ;;Link X-Coord Y-Coord 131 | ;;-------------- ------------------ ------------------ 132 | 133 | -------------------------------------------------------------------------------- /swmmio/tests/data/groundwater_model.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | ;;Project Title/Notes 3 | A simple groundwater model. 4 | See Groundwater_Model.txt for more details. 5 | 6 | [OPTIONS] 7 | ;;Option Value 8 | FLOW_UNITS CFS 9 | INFILTRATION HORTON 10 | FLOW_ROUTING KINWAVE 11 | LINK_OFFSETS DEPTH 12 | MIN_SLOPE 0 13 | ALLOW_PONDING NO 14 | SKIP_STEADY_STATE NO 15 | 16 | START_DATE 09/13/2014 17 | START_TIME 00:00:00 18 | REPORT_START_DATE 09/13/2014 19 | REPORT_START_TIME 00:00:00 20 | END_DATE 09/15/2014 21 | END_TIME 00:00:00 22 | SWEEP_START 01/01 23 | SWEEP_END 12/31 24 | DRY_DAYS 0 25 | REPORT_STEP 00:15:00 26 | WET_STEP 00:05:00 27 | DRY_STEP 00:05:00 28 | ROUTING_STEP 0:00:30 29 | RULE_STEP 00:00:00 30 | 31 | INERTIAL_DAMPING PARTIAL 32 | NORMAL_FLOW_LIMITED BOTH 33 | FORCE_MAIN_EQUATION H-W 34 | VARIABLE_STEP 0.75 35 | LENGTHENING_STEP 0 36 | MIN_SURFAREA 12.557 37 | MAX_TRIALS 8 38 | HEAD_TOLERANCE 0.005 39 | SYS_FLOW_TOL 5 40 | LAT_FLOW_TOL 5 41 | MINIMUM_STEP 0.5 42 | THREADS 1 43 | 44 | [EVAPORATION] 45 | ;;Data Source Parameters 46 | ;;-------------- ---------------- 47 | CONSTANT 0.0 48 | DRY_ONLY NO 49 | 50 | [RAINGAGES] 51 | ;;Name Format Interval SCF Source 52 | ;;-------------- --------- ------ ------ ---------- 53 | 1 INTENSITY 0:15 1.0 TIMESERIES Rainfall 54 | 55 | [SUBCATCHMENTS] 56 | ;;Name Rain Gage Outlet Area %Imperv Width %Slope CurbLen SnowPack 57 | ;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- ---------------- 58 | 1 1 2 5 0 140 0.5 0 59 | 60 | [SUBAREAS] 61 | ;;Subcatchment N-Imperv N-Perv S-Imperv S-Perv PctZero RouteTo PctRouted 62 | ;;-------------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- 63 | 1 0.01 0.1 0.05 0.05 25 OUTLET 64 | 65 | [INFILTRATION] 66 | ;;Subcatchment Param1 Param2 Param3 Param4 Param5 67 | ;;-------------- ---------- ---------- ---------- ---------- ---------- 68 | 1 1.2 0.1 2 7 0 69 | 70 | [AQUIFERS] 71 | ;;Name Por WP FC Ksat Kslope Tslope ETu ETs Seep Ebot Egw Umc ETupat 72 | ;;-------------- ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ 73 | 1 0.5 0.15 0.30 0.1 12 15.0 0.35 14.0 0.002 0.0 3.5 0.40 74 | 75 | [GROUNDWATER] 76 | ;;Subcatchment Aquifer Node Esurf A1 B1 A2 B2 A3 Dsw Egwt Ebot Wgr Umc 77 | ;;-------------- ---------------- ---------------- ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ 78 | 1 1 2 6 0.1 1 0 0 0 0 4 79 | 80 | [OUTFALLS] 81 | ;;Name Elevation Type Stage Data Gated Route To 82 | ;;-------------- ---------- ---------- ---------------- -------- ---------------- 83 | 2 0 FREE NO 84 | 85 | [TIMESERIES] 86 | ;;Name Date Time Value 87 | ;;-------------- ---------- ---------- ---------- 88 | Rainfall 0:00 0.037 89 | Rainfall 0:15 0.111 90 | Rainfall 0:30 0.185 91 | Rainfall 0:45 0.259 92 | Rainfall 1:00 0.333 93 | Rainfall 1:15 0.407 94 | Rainfall 1:30 0.481 95 | Rainfall 1:45 0.556 96 | Rainfall 2:00 0.630 97 | Rainfall 2:15 0.644 98 | Rainfall 2:30 0.600 99 | Rainfall 2:45 0.556 100 | Rainfall 3:00 0.511 101 | Rainfall 3:15 0.467 102 | Rainfall 3:30 0.422 103 | Rainfall 3:45 0.378 104 | Rainfall 4:00 0.333 105 | Rainfall 4:15 0.289 106 | Rainfall 4:30 0.244 107 | Rainfall 4:45 0.200 108 | Rainfall 5:00 0.156 109 | Rainfall 5:15 0.111 110 | Rainfall 5:30 0.067 111 | Rainfall 5:45 0.022 112 | Rainfall 6:00 0 113 | 114 | [REPORT] 115 | ;;Reporting Options 116 | SUBCATCHMENTS ALL 117 | NODES ALL 118 | LINKS ALL 119 | 120 | [TAGS] 121 | 122 | [MAP] 123 | DIMENSIONS 0.000 0.000 10000.000 10000.000 124 | Units None 125 | 126 | [COORDINATES] 127 | ;;Node X-Coord Y-Coord 128 | ;;-------------- ------------------ ------------------ 129 | 2 4556.338 3352.113 130 | 131 | [VERTICES] 132 | ;;Link X-Coord Y-Coord 133 | ;;-------------- ------------------ ------------------ 134 | 135 | [Polygons] 136 | ;;Subcatchment X-Coord Y-Coord 137 | ;;-------------- ------------------ ------------------ 138 | 1 2964.789 4971.831 139 | 1 6134.511 4986.413 140 | 1 6134.511 7445.652 141 | 1 2964.789 7450.704 142 | 143 | [SYMBOLS] 144 | ;;Gage X-Coord Y-Coord 145 | ;;-------------- ------------------ ------------------ 146 | 1 4485.915 8197.183 147 | 148 | -------------------------------------------------------------------------------- /swmmio/tests/data/Example6.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | ;;Project Title/Notes 3 | Example 6 4 | Circular Culvert with Roadway Overtopping 5 | and Upstream Storage 6 | 7 | [OPTIONS] 8 | ;;Option Value 9 | FLOW_UNITS CFS 10 | INFILTRATION HORTON 11 | FLOW_ROUTING DYNWAVE 12 | LINK_OFFSETS DEPTH 13 | MIN_SLOPE 0 14 | ALLOW_PONDING NO 15 | SKIP_STEADY_STATE NO 16 | 17 | START_DATE 06/08/2015 18 | START_TIME 00:00:00 19 | REPORT_START_DATE 06/08/2015 20 | REPORT_START_TIME 00:00:00 21 | END_DATE 06/08/2015 22 | END_TIME 05:00:00 23 | SWEEP_START 01/01 24 | SWEEP_END 12/31 25 | DRY_DAYS 0 26 | REPORT_STEP 00:07:30 27 | WET_STEP 00:05:00 28 | DRY_STEP 01:00:00 29 | ROUTING_STEP 0:00:05 30 | 31 | INERTIAL_DAMPING PARTIAL 32 | NORMAL_FLOW_LIMITED BOTH 33 | FORCE_MAIN_EQUATION H-W 34 | VARIABLE_STEP 0.75 35 | LENGTHENING_STEP 0 36 | MIN_SURFAREA 12.557 37 | MAX_TRIALS 8 38 | HEAD_TOLERANCE 0.005 39 | SYS_FLOW_TOL 5 40 | LAT_FLOW_TOL 5 41 | ;MINIMUM_STEP 0.5 42 | THREADS 1 43 | 44 | [EVAPORATION] 45 | ;;Data Source Parameters 46 | ;;-------------- ---------------- 47 | CONSTANT 0.0 48 | DRY_ONLY NO 49 | 50 | [JUNCTIONS] 51 | ;;Name Elevation MaxDepth InitDepth SurDepth Aponded 52 | ;;-------------- ---------- ---------- ---------- ---------- ---------- 53 | Outlet 868 15 0 0 0 54 | 55 | [OUTFALLS] 56 | ;;Name Elevation Type Stage Data Gated Route To 57 | ;;-------------- ---------- ---------- ---------------- -------- ---------------- 58 | TailWater 858 FIXED 859.5 NO 59 | 60 | [STORAGE] 61 | ;;Name Elev. MaxDepth InitDepth Shape Curve Name/Params N/A Fevap Psi Ksat IMD 62 | ;;-------------- -------- ---------- ----------- ---------- ---------------------------- -------- -------- -------- -------- 63 | Inlet 878 9 0 TABULAR StorageCurve 0 0 64 | 65 | [CONDUITS] 66 | ;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow 67 | ;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- 68 | Culvert Inlet Outlet 200 0.014 0 0 0 0 69 | Channel Outlet TailWater 200 0.03 0 0 0 0 70 | 71 | [WEIRS] 72 | ;;Name From Node To Node Type CrestHt Qcoeff Gated EndCon EndCoeff Surcharge RoadWidth RoadSurf 73 | ;;-------------- ---------------- ---------------- ------------ ---------- ---------- -------- -------- ---------- ---------- ---------- ---------- 74 | Roadway Inlet Outlet ROADWAY 5 3.33 NO 0 0 NO 40 GRAVEL 75 | 76 | [XSECTIONS] 77 | ;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert 78 | ;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- 79 | Culvert CIRCULAR 3 0 0 0 2 4 80 | Channel TRAPEZOIDAL 9 10 2 2 1 81 | Roadway RECT_OPEN 50 200 0 0 82 | 83 | [INFLOWS] 84 | ;;Node Constituent Time Series Type Mfactor Sfactor Baseline Pattern 85 | ;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- 86 | Inlet FLOW Inflow FLOW 1.0 1.0 87 | 88 | [CURVES] 89 | ;;Name Type X-Value Y-Value 90 | ;;-------------- ---------- ---------- ---------- 91 | StorageCurve Storage 0 0 92 | StorageCurve 2 9583 93 | StorageCurve 4 33977 94 | StorageCurve 6 72310 95 | StorageCurve 8 136778 96 | 97 | [TIMESERIES] 98 | ;;Name Date Time Value 99 | ;;-------------- ---------- ---------- ---------- 100 | Inflow 0 0 101 | Inflow .125 9 102 | Inflow .25 10 103 | Inflow .375 11 104 | Inflow .5 13 105 | Inflow .625 17 106 | Inflow .75 28 107 | Inflow .875 40 108 | Inflow 1 80 109 | Inflow 1.125 136 110 | Inflow 1.25 190 111 | Inflow 1.375 220 112 | Inflow 1.5 220 113 | Inflow 1.625 201 114 | Inflow 1.75 170 115 | Inflow 1.875 140 116 | Inflow 2 120 117 | Inflow 2.125 98 118 | Inflow 2.25 82 119 | Inflow 2.375 70 120 | Inflow 2.5 60 121 | Inflow 2.625 53 122 | Inflow 2.75 47 123 | Inflow 2.875 41 124 | 125 | [REPORT] 126 | ;;Reporting Options 127 | INPUT NO 128 | CONTROLS NO 129 | SUBCATCHMENTS ALL 130 | NODES ALL 131 | LINKS ALL 132 | 133 | [TAGS] 134 | 135 | [MAP] 136 | DIMENSIONS -59.264 5535.422 7089.237 6044.959 137 | Units None 138 | 139 | [COORDINATES] 140 | ;;Node X-Coord Y-Coord 141 | ;;-------------- ------------------ ------------------ 142 | Outlet 4206.542 6357.994 143 | TailWater 6019.146 6168.726 144 | Inlet 1628.472 6476.537 145 | 146 | [VERTICES] 147 | ;;Link X-Coord Y-Coord 148 | ;;-------------- ------------------ ------------------ 149 | Roadway 2311.068 7166.633 150 | Roadway 3762.491 7151.463 151 | 152 | [LABELS] 153 | ;;X-Coord Y-Coord Label 154 | 1651.426 7755.664 "Circular Culvert with Roadway Overtopping and Upstream Storage" "" "Arial" 12 1 1 155 | -------------------------------------------------------------------------------- /swmmio/version_control/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import os 3 | import json 4 | import shutil 5 | 6 | from swmmio.utils.functions import format_inp_section_header 7 | 8 | 9 | def copy_rpts_hsf(from_dir: str, to_dir: str, search_dir: str): 10 | 11 | """ 12 | Walk through a directory and find all .rpt and hot start (.hsf) files and copy them to 13 | another location based on the relative path from the `to_dir`. 14 | 15 | Parameters 16 | ---------- 17 | from_dir : str 18 | The source directory from which the relative path is derived. 19 | to_dir : str 20 | The destination directory where the files will be copied. 21 | search_dir : str 22 | The directory to search for .rpt and .hsf files. 23 | 24 | Examples 25 | -------- 26 | >>> to_directory = r'P:\\02_Projects\\SouthPhila\\SE_SFR\\MasterModels' 27 | >>> from_dir = r'F:\\models\\SPhila\\MasterModels_170104' 28 | >>> search_dir = r'F:\\models\\SPhila\\MasterModels_170104\\Combinations' 29 | >>> copy_rpts_hsf(from_dir, to_dir, search_dir) 30 | 31 | Notes 32 | ----- 33 | This function is useful for copying model results written on a local drive to a network drive. 34 | """ 35 | 36 | # chain.from_iterable(os.walk(path) for path in paths): 37 | for path, dirs, files in os.walk(search_dir): 38 | for f in files: 39 | if '.rpt' in f: 40 | rpt_path = os.path.join(path, f) 41 | to_dir = path.replace(from_dir, to_dir) 42 | dest = os.path.join(to_dir, f) 43 | shutil.copyfile(src=rpt_path, dst=dest) 44 | 45 | if '.hsf' in f: 46 | hsf_path = os.path.join(path, f) 47 | to_dir = path.replace(from_dir, to_dir) 48 | dest = os.path.join(to_dir, f) 49 | shutil.copyfile(src=hsf_path, dst=dest) 50 | 51 | 52 | def write_inp_section(file_object, allheaders, sectionheader, section_data, pad_top=True, na_fill=''): 53 | """ 54 | given an open file object, list of header sections, the current 55 | section header, and the section data in a Pandas Dataframe format, this function writes 56 | the data to the file object. 57 | """ 58 | 59 | f = file_object 60 | add_str = '' 61 | sectionheader = format_inp_section_header(sectionheader) 62 | if not section_data.empty: 63 | if pad_top: 64 | f.write('\n\n' + sectionheader + '\n') # add SWMM-friendly header e.g. [DWF] 65 | else: 66 | f.write(sectionheader + '\n') 67 | if allheaders and (sectionheader in allheaders) and allheaders[sectionheader]['columns'] == ['blob']: 68 | # to left justify based on the longest string in the blob column 69 | formatter = '{{:<{}s}}'.format(section_data[sectionheader].str.len().max()).format 70 | add_str = section_data.fillna('').to_string( 71 | index_names=False, 72 | header=False, 73 | index=False, 74 | justify='left', 75 | formatters={sectionheader: formatter} 76 | ) 77 | 78 | else: 79 | # naming the columns to the index name so the it prints in-line with col headers 80 | f.write(';;') 81 | # to left justify on longest string in the Comment column 82 | # this is overly annoying, to deal with 'Objects' vs numbers to remove 83 | # two bytes added from the double semicolon header thing (to keep things lined up) 84 | objectformatter = {hedr: ' {{:<{}}}'.format(section_data[hedr].apply(str).str.len().max()).format 85 | for hedr in section_data.columns} 86 | numformatter = {hedr: ' {{:<{}}}'.format(section_data[hedr].apply(str).str.len().max()).format 87 | for hedr in section_data.columns if section_data[hedr].dtype != "O"} 88 | objectformatter.update(numformatter) 89 | add_str = section_data.infer_objects(copy=False).fillna(na_fill).to_string( 90 | index_names=False, 91 | header=True, 92 | justify='left', 93 | formatters=objectformatter # {'Comment':formatter} 94 | ) 95 | 96 | # Deliminate control string using keywords 97 | if sectionheader == '[CONTROLS]': 98 | for sep in [' IF ', ' THEN ', ' PRIORITY ', ' AND ', ' OR ', ' ELSE ']: 99 | add_str = add_str.replace(sep, '\n'+sep) 100 | 101 | # write the dataframe as a string 102 | f.write(add_str + '\n\n') 103 | 104 | 105 | def write_meta_data(file_object, metadicts): 106 | file_object.write(json.dumps(metadicts, indent=4)) 107 | file_object.write('\n' + '=' * 100 + '\n\n') 108 | 109 | 110 | def read_meta_data(filepath): 111 | s = '' 112 | with open(filepath) as file_object: 113 | for line in file_object: 114 | if '================' in line: 115 | break 116 | s += line.strip() 117 | 118 | return json.loads(s) 119 | 120 | 121 | def bi_is_current(build_instr_file): 122 | """ 123 | check if a given build instruction file has any parent models whose 124 | date modified does not match the date modified of the parent INP file 125 | """ 126 | 127 | meta = read_meta_data(build_instr_file) 128 | baseline = meta['Parent Models']['Baseline'] 129 | alternatives = meta['Parent Models']['Alternatives'] 130 | # parents = baseline.update(alternatives) 131 | # print meta['Parent Models']['Baseline'] 132 | # print alternatives 133 | for inp, revisiondate in baseline.items(): 134 | if modification_date(inp) != revisiondate: 135 | return False 136 | 137 | for inp, revisiondate in alternatives.items(): 138 | if modification_date(inp) != revisiondate: 139 | return False 140 | 141 | return True 142 | 143 | 144 | def bi_latest_parent_date_modified(vc_dir, parentname): 145 | """ 146 | given a path to a version control directory of build instructions and the 147 | name of the parent model, return the parent model's revision date 148 | """ 149 | newest_bi = newest_file(vc_dir) 150 | meta = read_meta_data(newest_bi) 151 | # with open (newest_bi) as f: 152 | 153 | return meta['Parent Models'][parentname] 154 | 155 | 156 | def newest_file(directory): 157 | """ 158 | return the newest file (most recent) in a given directory. Beware that 159 | people report the min / max to do different things per OS... 160 | """ 161 | files = os.listdir(directory) 162 | return max([os.path.join(directory, f) for f in files], key=os.path.getctime) 163 | 164 | 165 | def modification_date(filename, string=True): 166 | """ 167 | get modification datetime of a file 168 | credit: Christian Oudard 169 | 'stackoverflow.com/questions/237079/how-to-get-file-creation-modification- 170 | date-times-in-python' 171 | """ 172 | t = os.path.getmtime(filename) 173 | dt = datetime.fromtimestamp(t) 174 | if string: 175 | return dt.strftime("%y-%m-%d %H:%M") 176 | else: 177 | return dt # datetime.fromtimestamp(t) 178 | 179 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # from recommonmark.transform import AutoStructify 16 | # from m2r import MdInclude 17 | from datetime import datetime 18 | import os 19 | import sys 20 | import swmmio 21 | 22 | 23 | sys.path.insert(0, os.path.abspath('../../swmmio')) 24 | 25 | # -- Project information ----------------------------------------------------- 26 | 27 | project = 'swmmio' 28 | copyright = f'{datetime.now().year}, Adam Erispaha' 29 | author = 'Adam Erispaha' 30 | 31 | # The short X.Y version 32 | version = swmmio.__version__ 33 | # The full version, including alpha/beta/rc tags 34 | release = swmmio.__version__ 35 | 36 | 37 | # -- General configuration --------------------------------------------------- 38 | 39 | # If your documentation needs a minimal Sphinx version, state it here. 40 | # 41 | # needs_sphinx = '1.0' 42 | 43 | # Add any Sphinx extension module names here, as strings. They can be 44 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 45 | # ones. 46 | extensions = [ 47 | "sphinx.ext.autosummary", 48 | "sphinx.ext.autodoc", 49 | "IPython.sphinxext.ipython_console_highlighting", 50 | "IPython.sphinxext.ipython_directive", 51 | "sphinx.ext.intersphinx", 52 | "sphinx.ext.mathjax", 53 | "sphinx.ext.todo", 54 | "sphinx.ext.coverage", 55 | "sphinx.ext.viewcode", 56 | "sphinx_copybutton", 57 | "myst_nb", 58 | "numpydoc", 59 | ] 60 | 61 | myst_enable_extensions = ["colon_fence"] 62 | myst_heading_anchors = 3 63 | 64 | # sphinx-copybutton configurations 65 | copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " 66 | copybutton_prompt_is_regexp = True 67 | 68 | # Add any paths that contain templates here, relative to this directory. 69 | templates_path = ['_templates'] 70 | 71 | # The suffix(es) of source filenames. 72 | # You can specify multiple suffix as a list of string: 73 | # 74 | source_suffix = { 75 | '.rst': 'restructuredtext', 76 | '.md': 'myst-nb', 77 | ".ipynb": "myst-nb", 78 | ".myst": "myst-nb", 79 | } 80 | 81 | # The master toctree document. 82 | master_doc = 'index' 83 | 84 | # The language for content autogenerated by Sphinx. Refer to documentation 85 | # for a list of supported languages. 86 | # 87 | # This is also used if you do content translation via gettext catalogs. 88 | # Usually you set "language" from the command line for these cases. 89 | language = 'en' 90 | 91 | # List of patterns, relative to source directory, that match files and 92 | # directories to ignore when looking for source files. 93 | # This pattern also affects html_static_path and html_extra_path. 94 | exclude_patterns = [ 95 | # 'usage/visualizing_models.ipynb', 96 | # 'usage/making_art_with_swmm.ipynb' 97 | ] 98 | 99 | # nb_execution_mode = "cache" 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = 'sphinx' 103 | 104 | 105 | # -- Options for HTML output ------------------------------------------------- 106 | 107 | # The theme to use for HTML and HTML Help pages. See the documentation for 108 | # a list of builtin themes. 109 | # 110 | # html_theme = 'alabaster' 111 | html_theme = "pydata_sphinx_theme" 112 | 113 | # Theme options are theme-specific and customize the look and feel of a theme 114 | # further. For a list of options available for each theme, see the 115 | # documentation. 116 | # 117 | html_theme_options = { 118 | "github_url": "https://github.com/pyswmm/swmmio", 119 | "navbar_align": "content", 120 | "navigation_depth": 4, 121 | "collapse_navigation": True, 122 | } 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | html_static_path = ['_static'] 128 | 129 | # Custom sidebar templates, must be a dictionary that maps document names 130 | # to template names. 131 | # 132 | # The default sidebars (for documents that don't match any pattern) are 133 | # defined by theme itself. Builtin themes are using these templates by 134 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 135 | # 'searchbox.html']``. 136 | # 137 | # html_sidebars = {} 138 | 139 | 140 | # -- Options for HTMLHelp output --------------------------------------------- 141 | 142 | # Output file base name for HTML help builder. 143 | htmlhelp_basename = 'swmmiodoc' 144 | 145 | 146 | # -- Options for LaTeX output ------------------------------------------------ 147 | 148 | latex_elements = { 149 | # The paper size ('letterpaper' or 'a4paper'). 150 | # 151 | # 'papersize': 'letterpaper', 152 | 153 | # The font size ('10pt', '11pt' or '12pt'). 154 | # 155 | # 'pointsize': '10pt', 156 | 157 | # Additional stuff for the LaTeX preamble. 158 | # 159 | # 'preamble': '', 160 | 161 | # Latex figure (float) alignment 162 | # 163 | # 'figure_align': 'htbp', 164 | } 165 | 166 | # Grouping the document tree into LaTeX files. List of tuples 167 | # (source start file, target name, title, 168 | # author, documentclass [howto, manual, or own class]). 169 | latex_documents = [ 170 | (master_doc, 'swmmio.tex', 'swmmio Documentation', 171 | 'Adam Erispaha', 'manual'), 172 | ] 173 | 174 | 175 | # -- Options for manual page output ------------------------------------------ 176 | 177 | # One entry per manual page. List of tuples 178 | # (source start file, name, description, authors, manual section). 179 | man_pages = [ 180 | (master_doc, 'swmmio', 'swmmio Documentation', 181 | [author], 1) 182 | ] 183 | 184 | 185 | # -- Options for Texinfo output ---------------------------------------------- 186 | 187 | # Grouping the document tree into Texinfo files. List of tuples 188 | # (source start file, target name, title, author, 189 | # dir menu entry, description, category) 190 | texinfo_documents = [ 191 | (master_doc, 'swmmio', 'swmmio Documentation', 192 | author, 'swmmio', 'One line description of project.', 193 | 'Miscellaneous'), 194 | ] 195 | 196 | 197 | # -- Options for Epub output ------------------------------------------------- 198 | 199 | # Bibliographic Dublin Core info. 200 | epub_title = project 201 | 202 | # The unique identifier of the text. This can be a ISBN number 203 | # or the project homepage. 204 | # 205 | # epub_identifier = '' 206 | 207 | # A unique identification for the text. 208 | # 209 | # epub_uid = '' 210 | 211 | # A list of files that should not be packed into the epub file. 212 | epub_exclude_files = ['search.html'] 213 | 214 | 215 | numpydoc_class_members_toctree = False 216 | 217 | intersphinx_mapping = { 218 | "python": ("https://docs.python.org/3/", None), 219 | "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), 220 | "pyswmm": ("https://pyswmm.github.io/pyswmm/", None), 221 | } -------------------------------------------------------------------------------- /swmmio/tests/test_model_elements.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | import tempfile 5 | import pandas as pd 6 | import swmmio 7 | import swmmio.utils.functions 8 | import swmmio.utils.text 9 | from swmmio.tests.data import MODEL_FULL_FEATURES_XY, MODEL_FULL_FEATURES__NET_PATH, MODEL_A_PATH, MODEL_EX_1, MODEL_EX_1B, MODEL_GREEN_AMPT 10 | from swmmio import Model, dataframe_from_inp 11 | 12 | from swmmio.utils.dataframes import get_link_coords 13 | from swmmio.utils.text import get_rpt_sections_details, get_inp_sections_details 14 | 15 | 16 | @pytest.fixture 17 | def test_model_01(): 18 | return Model(MODEL_FULL_FEATURES_XY) 19 | 20 | 21 | @pytest.fixture 22 | def test_model_02(): 23 | return Model(MODEL_FULL_FEATURES__NET_PATH) 24 | 25 | 26 | def test_complete_headers(test_model_01): 27 | headers = swmmio.utils.text.get_inp_sections_details(test_model_01.inp.path) 28 | print (list(headers.keys())) 29 | sections_in_inp = [ 30 | 'TITLE', 'OPTIONS', 'EVAPORATION', 'RAINGAGES', 'SUBCATCHMENTS', 'SUBAREAS', 'INFILTRATION', 31 | 'JUNCTIONS', 'OUTFALLS', 'STORAGE', 'CONDUITS', 'PUMPS', 'WEIRS', 'XSECTIONS', 'INFLOWS', 32 | 'CURVES', 'TIMESERIES', 'REPORT', 'TAGS', 'MAP', 'COORDINATES', 'VERTICES', 'POLYGONS', 33 | 'SYMBOLS' 34 | ] 35 | assert (all(section in headers for section in sections_in_inp)) 36 | 37 | 38 | def test_complete_headers_rpt(test_model_02): 39 | 40 | headers = get_rpt_sections_details(test_model_02.rpt.path) 41 | sections_in_rpt = [ 42 | 'Link Flow Summary', 'Link Flow Summary', 'Subcatchment Summary', 43 | 'Cross Section Summary', 'Link Summary' 44 | ] 45 | 46 | assert(all(section in headers for section in sections_in_rpt)) 47 | assert headers['Link Summary']['columns'] == ['Name', 'FromNode', 'ToNode', 48 | 'Type', 'Length', 'SlopePerc', 49 | 'Roughness'] 50 | 51 | 52 | def test_get_set_curves(test_model_01): 53 | 54 | curves = test_model_01.inp.curves 55 | 56 | print (curves) 57 | 58 | 59 | @pytest.mark.uses_geopandas 60 | def test_dataframe_composite(test_model_02): 61 | m = test_model_02 62 | links = m.links 63 | 64 | feat = links.geojson[2] 65 | assert feat['properties']['Name'] == '1' 66 | assert feat['properties']['MaxQ'] == 2.54 67 | 68 | links_gdf = links.geodataframe 69 | assert links_gdf.index[2] == '1' 70 | assert links_gdf.loc['1', 'MaxQ'] == 2.54 71 | 72 | 73 | def test_model_section(): 74 | m = swmmio.Model(MODEL_FULL_FEATURES_XY) 75 | 76 | def pumps_old_method(model): 77 | """ 78 | collect all useful and available data related model pumps and 79 | organize in one dataframe. 80 | """ 81 | 82 | # check if this has been done already and return that data accordingly 83 | if model._pumps_df is not None: 84 | return model._pumps_df 85 | 86 | # parse out the main objects of this model 87 | inp = model.inp 88 | 89 | # create dataframes of relevant sections from the INP 90 | pumps_df = dataframe_from_inp(inp.path, "[PUMPS]") 91 | if pumps_df.empty: 92 | return pd.DataFrame() 93 | 94 | # add conduit coordinates 95 | xys = pumps_df.apply(lambda r: get_link_coords(r, inp.coordinates, inp.vertices), axis=1) 96 | df = pumps_df.assign(coords=xys.map(lambda x: x[0])) 97 | df.InletNode = df.InletNode.astype(str) 98 | df.OutletNode = df.OutletNode.astype(str) 99 | 100 | model._pumps_df = df 101 | 102 | return df 103 | 104 | pumps_old_method = pumps_old_method(m) 105 | pumps = m.pumps() 106 | 107 | assert(pumps_old_method.equals(pumps)) 108 | 109 | 110 | def test_subcatchment_composite(test_model_02): 111 | 112 | subs = test_model_02.subcatchments 113 | assert subs.dataframe.loc['S3', 'Outlet'] == 'j3' 114 | assert subs.dataframe['TotalRunoffIn'].sum() == pytest.approx(2.45, 0.001) 115 | 116 | 117 | def test_remove_model_section(): 118 | 119 | with tempfile.TemporaryDirectory() as tempdir: 120 | m1 = swmmio.Model(MODEL_A_PATH) 121 | 122 | # create a copy of the model without subcatchments 123 | # m1.inp.infiltration = m1.inp.infiltration.iloc[0:0] 124 | m1.inp.subcatchments = m1.inp.subcatchments.iloc[0:0] 125 | # m1.inp.subareas = m1.inp.subareas.iloc[0:0] 126 | # m1.inp.polygons = m1.inp.polygons.iloc[0:0] 127 | 128 | # save to temp location 129 | temp_inp = os.path.join(tempdir, f'{m1.inp.name}.inp') 130 | m1.inp.save(temp_inp) 131 | 132 | m2 = swmmio.Model(temp_inp) 133 | 134 | sects1 = get_inp_sections_details(m1.inp.path) 135 | sects2 = get_inp_sections_details(m2.inp.path) 136 | 137 | # confirm saving a copy retains all sections except those removed 138 | assert ['SUBCATCHMENTS'] == [x for x in sects1 if x not in sects2] 139 | 140 | # confirm subcatchments returns an empty df 141 | assert m2.subcatchments.dataframe.empty 142 | 143 | 144 | def test_example_1(): 145 | model = swmmio.Model(MODEL_EX_1) 146 | element_types = ['nodes', 'links', 'subcatchments'] 147 | elem_dict = {element: model.__getattribute__(element).geojson for element in element_types} 148 | swmm_version = model.rpt.swmm_version 149 | assert(swmm_version['major'] == 5) 150 | assert(swmm_version['minor'] == 1) 151 | assert(swmm_version['patch'] == 12) 152 | 153 | model_b = swmmio.Model(MODEL_EX_1B) 154 | swmm_version = model_b.rpt.swmm_version 155 | assert(swmm_version['patch'] == 13) 156 | elem_dict = {element: model_b.__getattribute__(element).geojson for element in element_types} 157 | 158 | subs = model.subcatchments.dataframe 159 | assert subs['TotalInfil'].sum() == pytest.approx(12.59, rel=0.001) 160 | assert subs['TotalRunoffMG'].sum() == pytest.approx(2.05, rel=0.001) 161 | 162 | # access lower level api 163 | peak_runoff = model.rpt.subcatchment_runoff_summary['PeakRunoff'] 164 | assert peak_runoff.values == pytest.approx([4.66, 4.52, 2.45, 2.45, 6.56, 1.5, 0.79, 1.33], rel=0.001) 165 | assert peak_runoff.values == pytest.approx(subs['PeakRunoff'].values, rel=0.001) 166 | 167 | def test_get_set_timeseries(test_model_02): 168 | 169 | ts = test_model_02.inp.timeseries 170 | assert(all(ts.columns == ['Date', 'Time', 'Value'])) 171 | assert(ts.loc['TS2'].Date == 'FILE') 172 | assert('"' in ts.loc['TS2'].Value) 173 | assert(ts.Value.isnull().sum() == 0) 174 | print (ts) 175 | 176 | 177 | def test_inp_tags_getter_and_setter(): 178 | common_data = {'ElementType': ['Subcatch'] * 4, 179 | 'Name': ['CA-1', 'CA-7', 'CA-8', 'CA-11'], 180 | 'Tag': ['CA'] * 4, 181 | } 182 | 183 | expected_output = pd.DataFrame(common_data) 184 | expected_output.set_index(['ElementType'], inplace=True) 185 | 186 | common_data['Tag'] = ['Modified'] * 4 187 | tags_to_set = pd.DataFrame(common_data) 188 | tags_to_set.set_index(['ElementType'], inplace=True) 189 | 190 | model = Model(MODEL_GREEN_AMPT) 191 | 192 | with tempfile.TemporaryDirectory() as tempdir: 193 | temp_inp_path = os.path.join(tempdir, f'{model.inp.name}.inp') 194 | model.inp.save(temp_inp_path) 195 | temp_model = Model(temp_inp_path) 196 | 197 | assert expected_output.equals(temp_model.inp.tags.sort_index()) 198 | 199 | temp_model.inp.tags["Tag"] = ["Modified"] * 4 200 | 201 | assert tags_to_set.equals(temp_model.inp.tags.sort_index()) 202 | -------------------------------------------------------------------------------- /swmmio/reporting/basemaps/mapbox_base_comparison.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 |
17 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /swmmio/version_control/version_control.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import os 3 | import itertools 4 | from datetime import datetime 5 | from swmmio import Model 6 | from swmmio.version_control import utils as vc_utils 7 | from swmmio.version_control import inp 8 | 9 | pd.options.display.max_colwidth = 200 10 | 11 | 12 | def propagate_changes_from_baseline(baseline_dir, alternatives_dir, combi_dir, 13 | version_id='', comments=''): 14 | """ 15 | if the baseline model has changes that need to be propagated to all models, 16 | iterate through each model and rebuild the INPs with the new baseline and 17 | existing build instructions. update the build instructions to reflect the 18 | revision date of the baseline. 19 | """ 20 | version_id += '_' + datetime.now().strftime("%y%m%d%H%M%S") 21 | 22 | # collect the directories of all models 23 | model_dirs = [] 24 | for alt in os.listdir(alternatives_dir): 25 | # print alt 26 | # iterate through each implementation level of each alternative 27 | for imp_level in os.listdir(os.path.join(alternatives_dir, alt)): 28 | # create or refresh the build instructions file for the alternatives 29 | model_dirs.append(os.path.join(alternatives_dir, alt, imp_level)) 30 | 31 | model_dirs += [os.path.join(combi_dir, x) for x in os.listdir(combi_dir)] 32 | # print model_dirs 33 | baseline = Model(baseline_dir) 34 | base_inp_path = baseline.inp.path 35 | 36 | for model_dir in model_dirs: 37 | model = Model(model_dir) 38 | vc_directory = os.path.join(model_dir, 'vc') 39 | latest_bi = vc_utils.newest_file(vc_directory) 40 | 41 | # update build instructions metadata and build the new inp 42 | bi = inp.BuildInstructions(latest_bi) 43 | bi.metadata['Parent Models']['Baseline'] = {base_inp_path: vc_utils.modification_date(base_inp_path)} 44 | bi.metadata['Log'].update({version_id: comments}) 45 | bi.save(vc_directory, version_id + '.txt') 46 | print('rebuilding {} with changes to baseline'.format(model.name)) 47 | bi.build(baseline_dir, model.inp.path) # overwrite old inp 48 | 49 | 50 | def create_combinations(baseline_dir, rsn_dir, combi_dir, version_id='', comments=''): 51 | """ 52 | Generate SWMM5 models of each logical combination of all implementation 53 | phases (IP) across all relief sewer networks (RSN). 54 | 55 | Parameters 56 | ---------- 57 | baseline_dir : str 58 | Path to directory containing the baseline SWMM5 model. 59 | rsn_dir : str 60 | Path to directory containing subdirectories for each RSN, which contain 61 | directories for each IP within the network. 62 | combi_dir : str 63 | Target directory in which child models will be created. 64 | version_id : str, optional 65 | Identifier for a given version (default is an empty string). 66 | comments : str, optional 67 | Comments tracked within build instructions log for each model scenario 68 | (default is an empty string). 69 | 70 | Notes 71 | ----- 72 | Calling `create_combinations` will update child models if parent models have 73 | """ 74 | 75 | base_inp_path = Model(baseline_dir).inp.path 76 | version_id += '_' + datetime.now().strftime("%y%m%d%H%M%S") 77 | 78 | # create a list of directories pointing to each IP in each RSN 79 | RSN_dirs = [os.path.join(rsn_dir, rsn) for rsn in os.listdir(rsn_dir)] 80 | IP_dirs = [os.path.join(d, ip) for d in RSN_dirs for ip in os.listdir(d)] 81 | 82 | # list of lists of each IP within each RSN, including a 'None' phase. 83 | IPs = [[None] + os.listdir(d) for d in RSN_dirs] 84 | 85 | # identify all scenarios (cartesian product of sets of IPs between each RSN) 86 | # then isolate child scenarios with atleast 2 parents (sets with one parent 87 | # are already modeled as IPs within the RSNs) 88 | all_scenarios = [[_f for _f in s if _f] for s in itertools.product(*IPs)] 89 | child_scenarios = [s for s in all_scenarios if len(s) > 1] 90 | 91 | # notify user of what was initially found 92 | str_IPs = '\n'.join([', '.join([_f for _f in i if _f]) for i in IPs]) 93 | print(('Found {} implementation phases among {} networks:\n{}\n' 94 | 'This yields {} combined scenarios ({} total)'.format(len(IP_dirs), 95 | len(RSN_dirs), str_IPs, len(child_scenarios), 96 | len(all_scenarios) - 1))) 97 | 98 | # ========================================================================== 99 | # UPDATE/CREATE THE PARENT MODEL BUILD INSTRUCTIONS 100 | # ========================================================================== 101 | for ip_dir in IP_dirs: 102 | ip_model = Model(ip_dir) 103 | vc_dir = os.path.join(ip_dir, 'vc') 104 | 105 | if not os.path.exists(vc_dir): 106 | print('creating new build instructions for {}'.format(ip_model.name)) 107 | inp.create_inp_build_instructions(base_inp_path, ip_model.inp.path, 108 | vc_dir, 109 | version_id, comments) 110 | else: 111 | # check if the alternative model was changed since last run of this tool 112 | # --> compare the modification date to the BI's modification date metadata 113 | latest_bi = vc_utils.newest_file(vc_dir) 114 | if not vc_utils.bi_is_current(latest_bi): 115 | # revision date of the alt doesn't match the newest build 116 | # instructions for this 'imp_level', so we should refresh it 117 | print('updating build instructions for {}'.format(ip_model.name)) 118 | inp.create_inp_build_instructions(base_inp_path, ip_model.inp.path, 119 | vc_dir, version_id, 120 | comments) 121 | 122 | # ========================================================================== 123 | # UPDATE/CREATE THE CHILD MODELS AND CHILD BUILD INSTRUCTIONS 124 | # ========================================================================== 125 | for scen in child_scenarios: 126 | newcombi = '_'.join(sorted(scen)) 127 | new_dir = os.path.join(combi_dir, newcombi) 128 | vc_dir = os.path.join(combi_dir, newcombi, 'vc') 129 | 130 | # parent model build instr files 131 | # BUG (this breaks with model IDs with more than 1 char) 132 | parent_vc_dirs = [os.path.join(rsn_dir, f[0], f, 'vc') for f in scen] 133 | latest_parent_bis = [vc_utils.newest_file(d) for d in parent_vc_dirs] 134 | build_instrcts = [inp.BuildInstructions(bi) for bi in latest_parent_bis] 135 | 136 | if not os.path.exists(new_dir): 137 | 138 | os.mkdir(new_dir) 139 | newinppath = os.path.join(new_dir, newcombi + '.inp') 140 | 141 | print('creating new child model: {}'.format(newcombi)) 142 | new_build_instructions = sum(build_instrcts) 143 | new_build_instructions.save(vc_dir, version_id + '.txt') 144 | new_build_instructions.build(baseline_dir, newinppath) 145 | 146 | else: 147 | # check if the alternative model was changed since last run 148 | # of this tool --> compare the modification date to the BI's 149 | # modification date meta data 150 | latest_bi = vc_utils.newest_file(os.path.join(new_dir, 'vc')) 151 | if not vc_utils.bi_is_current(latest_bi): 152 | # revision date of the alt doesn't match the newest build 153 | # instructions for this 'imp_level', so we should refresh it 154 | print('updating child build instructions for {}'.format(newcombi)) 155 | newinppath = os.path.join(new_dir, newcombi + '.inp') 156 | new_build_instructions = sum(build_instrcts) 157 | new_build_instructions.save(vc_dir, version_id + '.txt') 158 | new_build_instructions.build(baseline_dir, newinppath) 159 | -------------------------------------------------------------------------------- /swmmio/graphics/swmm_graphics.py: -------------------------------------------------------------------------------- 1 | # graphical functions for SWMM files 2 | import os 3 | import tempfile 4 | 5 | from PIL import Image, ImageDraw 6 | 7 | from swmmio.defs.config import BETTER_BASEMAP_PATH 8 | from swmmio.graphics import config 9 | from swmmio.defs.constants import white 10 | from swmmio.graphics.utils import px_to_irl_coords, save_image 11 | from swmmio.utils import spatial 12 | from swmmio.utils.spatial import centroid_and_bbox_from_coords 13 | from swmmio.graphics.drawing import (annotate_streets, annotate_title, annotate_details, annotate_timestamp, 14 | draw_conduit, draw_node) 15 | 16 | 17 | def _draw_basemap(draw, img, bbox, px_width, shift_ratio): 18 | """ 19 | given the shapefiles in config.basemap_options, render each layer 20 | on the model basemap. 21 | """ 22 | 23 | for f in config.basemap_options['features']: 24 | 25 | shp_path = os.path.join(config.basemap_shapefile_dir, f['feature']) 26 | df = spatial.read_shapefile(shp_path)[f['cols'] + ['coords']] 27 | df = px_to_irl_coords(df, bbox=bbox, shift_ratio=shift_ratio, 28 | px_width=px_width)[0] 29 | 30 | if 'ST_NAME' in df.columns: 31 | # this is a street, draw a polyline accordingly 32 | df.apply(lambda r: draw.line(r.draw_coords, fill=f['fill']), axis=1) 33 | annotate_streets(df, img, 'ST_NAME') 34 | else: 35 | df.apply(lambda r: draw.polygon(r.draw_coords, 36 | fill=f['fill']), axis=1) 37 | 38 | 39 | def draw_model(model=None, nodes=None, conduits=None, parcels=None, title=None, 40 | annotation=None, file_path=None, bbox=None, px_width=2048.0): 41 | """ 42 | Create a PNG rendering of the model and model results. 43 | 44 | Notes 45 | ----- 46 | A swmmio.Model object can be passed in independently, or Pandas Dataframes 47 | for the nodes and conduits of a model may be passed in. 48 | A dataframe containing parcel data can optionally be passed in. 49 | 50 | Parameters 51 | ---------- 52 | model : swmmio.Model, optional 53 | A swmmio.Model object. 54 | nodes : pandas.DataFrame, optional 55 | DataFrame for the nodes of a model. Required if model is not provided. 56 | conduits : pandas.DataFrame, optional 57 | DataFrame for the conduits of a model. Required if model is not provided. 58 | parcels : pandas.DataFrame, optional 59 | DataFrame containing parcel data. 60 | title : str, optional 61 | String to be written in the top left of the PNG. 62 | annotation : str, optional 63 | String to be written in the bottom left of the PNG. 64 | file_path : str, optional 65 | File path where PNG should be saved. If not specified, a PIL Image object is returned. 66 | bbox : tuple of tuple of float, optional 67 | Coordinates representing the bottom left and top right corner of a bounding box. 68 | The rendering will be clipped to this box. If not provided, the rendering will clip 69 | tightly to the model extents. Example: ((2691647, 221073), (2702592, 227171)). 70 | px_width : float, optional 71 | Width of the image in pixels. Default is 2048.0. 72 | 73 | Returns 74 | ------- 75 | PIL.Image.Image 76 | The rendered image. 77 | """ 78 | 79 | # gather the nodes and conduits data if a swmmio Model object was passed in 80 | if model is not None: 81 | nodes = model.nodes() 82 | conduits = model.links() 83 | 84 | # antialias X2 85 | xplier = 1 86 | xplier *= px_width / 1024 # scale the symbology sizes 87 | px_width = px_width * 2 88 | 89 | # compute draw coordinates, and the image dimensions (in px) 90 | conduits, bb, h, w, shift_ratio = px_to_irl_coords(conduits, bbox=bbox, px_width=px_width) 91 | nodes = px_to_irl_coords(nodes, bbox=bb, px_width=px_width)[0] 92 | 93 | # create the PIL image and draw objects 94 | img = Image.new('RGB', (w, h), white) 95 | draw = ImageDraw.Draw(img) 96 | 97 | # draw the basemap if required 98 | if config.include_basemap is True: 99 | _draw_basemap(draw, img, bb, px_width, shift_ratio) 100 | 101 | if parcels is not None: 102 | # expects dataframe with coords and draw color column 103 | par_px = px_to_irl_coords(parcels, bbox=bb, shift_ratio=shift_ratio, px_width=px_width)[0] 104 | par_px.apply(lambda r: draw.polygon(r.draw_coords, fill=r.draw_color), axis=1) 105 | 106 | # start the draw fest, mapping draw methods to each row in the dataframes 107 | conduits.apply(lambda row: draw_conduit(row, draw), axis=1) 108 | nodes.apply(lambda row: draw_node(row, draw), axis=1) 109 | 110 | # ADD ANNOTATION AS NECESSARY 111 | if title: 112 | annotate_title(title, draw) 113 | if annotation: 114 | annotate_details(annotation, draw) 115 | annotate_timestamp(draw) 116 | 117 | # SAVE IMAGE TO DISK 118 | if file_path: 119 | save_image(img, file_path) 120 | 121 | return img 122 | 123 | 124 | def create_map(model=None, filename=None, basemap=None, auto_open=False, links_geojson=None, nodes_geojson=None): 125 | """ 126 | Export model as a geojson object and create an HTML map. 127 | 128 | Parameters 129 | ---------- 130 | model : object, optional 131 | The model object to be exported. Must have a valid CRS (Coordinate Reference System). 132 | filename : str, optional 133 | The filename for the output HTML file. If None, a temporary file will be created. 134 | basemap : str, optional 135 | The path to the basemap file. If None, a default basemap path will be used. 136 | auto_open : bool, optional 137 | If True, the generated HTML file will be automatically opened in a web browser. 138 | links_geojson : str, optional 139 | A custon links GeoJSON string for writing to the HTML file. If none the default {geojson.dumps(model.links.geojson)} is used 140 | nodes_geojson : str, optional 141 | A custon nodes GeoJSON string for writing to the HTML file. If none the default {geojson.dumps(model.nodes.geojson)} is used 142 | 143 | Returns 144 | ------- 145 | str 146 | The content of the generated HTML file if `filename` is None, otherwise returns an empty string. 147 | 148 | Raises 149 | ------ 150 | ValueError 151 | If the model object does not have a valid CRS. 152 | 153 | Notes 154 | ----- 155 | The function reads a basemap file and inserts geojson data of the model's links and nodes into it. 156 | It also sets the map's center and bounding box based on the model's coordinates. 157 | """ 158 | 159 | import geojson 160 | 161 | basemap = BETTER_BASEMAP_PATH if basemap is None else basemap 162 | return_html = False if filename is not None else True 163 | if filename is None: 164 | filename = os.path.join(tempfile.gettempdir(), f'{model.name}.html') 165 | 166 | if model.crs: 167 | model.to_crs("EPSG:4326") 168 | else: 169 | raise ValueError('Model object must have a valid crs') 170 | 171 | # get map centroid and bbox 172 | c, bbox = centroid_and_bbox_from_coords(model.inp.coordinates) 173 | 174 | with open(basemap, 'r') as bm: 175 | with open(filename, 'w') as newmap: 176 | for line in bm: 177 | if 'INSERT GEOJSON HERE' in line: 178 | if links_geojson is None: 179 | newmap.write(f'conduits = {geojson.dumps(model.links.geojson)}\n') 180 | else: 181 | newmap.write(f'conduits = {links_geojson}\n') 182 | if nodes_geojson is None: 183 | newmap.write(f'nodes = {geojson.dumps(model.nodes.geojson)}\n') 184 | else: 185 | newmap.write(f'nodes = {nodes_geojson}\n') 186 | elif '// INSERT MAP CENTER HERE' in line: 187 | newmap.write('\tcenter:[{}, {}],\n'.format(c[0], c[1])) 188 | elif '// INSERT BBOX HERE' in line and bbox is not None: 189 | newmap.write(f'\tmap.fitBounds([[{bbox[0]}, {bbox[1]}], [{bbox[2]}, {bbox[3]}]]);\n') 190 | else: 191 | newmap.write(line) 192 | if return_html: 193 | with open(filename, 'r') as f: 194 | return f.read() 195 | -------------------------------------------------------------------------------- /swmmio/graphics/drawing.py: -------------------------------------------------------------------------------- 1 | from swmmio.defs.constants import red, purple, lightblue, lightgreen, black, lightgrey, grey 2 | from swmmio.defs.config import FONT_PATH 3 | from swmmio.graphics.utils import circle_bbox, length_bw_coords, angle_bw_points, midpoint 4 | from PIL import Image, ImageDraw, ImageFont, ImageOps 5 | from time import strftime 6 | import math 7 | import os 8 | 9 | 10 | # FUNCTIONS FOR COMPUTING THE VISUAL CHARACTERISTICS OF MODEL ELEMENTS 11 | def node_draw_size(node): 12 | """given a row of a nodes() dataframe, return the size it should be drawn""" 13 | 14 | if 'draw_size' in node.axes[0]: 15 | # if this value has already been calculated 16 | return node.draw_size 17 | 18 | radius = 0 # aka don't show this node by default 19 | if 'HoursFlooded' in node and node.HoursFlooded >= 0.083: 20 | radius = node.HoursFlooded * 3 21 | return radius 22 | 23 | 24 | def node_draw_color(node): 25 | """given a row of a nodes() dataframe, return the color it should be drawn""" 26 | 27 | if 'draw_color' in node.axes[0]: 28 | # if this value has already been calculated 29 | return node.draw_color 30 | 31 | color = '#d2d2e6' # (210, 210, 230) #default color 32 | if 'HoursFlooded' in node and node.HoursFlooded >= 0.083: 33 | color = red 34 | return color 35 | 36 | 37 | def conduit_draw_size(conduit): 38 | """return the draw size of a conduit""" 39 | 40 | if 'draw_size' in conduit.axes[0]: 41 | # if this value has already been calculated 42 | return conduit.draw_size 43 | 44 | draw_size = 1 45 | if 'MaxQPerc' in conduit and conduit.MaxQPerc >= 1: 46 | capacity = conduit.MaxQ / conduit.MaxQPerc 47 | stress = conduit.MaxQ / capacity 48 | fill = gradient_grey_red(conduit.MaxQ * 100, 0, capacity * 300) 49 | draw_size = int(round(math.pow(stress * 10, 0.8))) 50 | 51 | elif 'Geom1' in conduit and not math.isnan(conduit.Geom1): 52 | draw_size = conduit.Geom1 53 | 54 | return draw_size 55 | 56 | 57 | def conduit_draw_color(conduit): 58 | """return the draw color of a conduit""" 59 | 60 | if 'draw_color' in conduit.axes[0]: 61 | # if this value has already been calculated 62 | return conduit.draw_color 63 | 64 | fill = '#787882' # (120, 120, 130) 65 | if 'MaxQPerc' in conduit and conduit.MaxQPerc >= 1: 66 | capacity = conduit.MaxQ / conduit.MaxQPerc 67 | stress = conduit.MaxQ / capacity 68 | fill = gradient_grey_red(conduit.MaxQ * 100, 0, capacity * 300) 69 | return fill 70 | 71 | 72 | def parcel_draw_color(parcel, style='risk'): 73 | if style == 'risk': 74 | fill = gradient_color_red(parcel.HoursFlooded + 0.5, 0, 3) 75 | if style == 'delta': 76 | fill = lightgrey # default 77 | if parcel.Category == 'increased_flooding': 78 | # parcel previously flooded, now floods more 79 | fill = red 80 | 81 | if parcel.Category == 'new_flooding': 82 | # parcel previously did not flood, now floods in proposed conditions 83 | fill = purple 84 | 85 | if parcel.Category == 'decreased_flooding': 86 | # parcel flooding problem decreased 87 | fill = lightblue # du.lightgrey 88 | 89 | if parcel.Category == 'eliminated_flooding': 90 | # parcel flooding problem eliminated 91 | fill = lightgreen 92 | 93 | return fill 94 | 95 | 96 | # PIL DRAW METHODS APPLIED TO ImageDraw OBJECTS 97 | def draw_node(node, draw): 98 | """draw a node to the given PIL ImageDraw object""" 99 | color = node_draw_color(node) 100 | radius = node_draw_size(node) 101 | draw.ellipse(circle_bbox(node.draw_coords[0], radius), fill=color) 102 | 103 | 104 | def draw_conduit(conduit, draw): 105 | # default fill and size 106 | fill = conduit_draw_color(conduit) 107 | draw_size = int(conduit_draw_size(conduit)) 108 | xys = conduit.draw_coords 109 | 110 | # draw that thing 111 | draw.line(xys, fill=fill, width=draw_size) 112 | if length_bw_coords(xys[0], xys[-1]) > draw_size * 0.75: 113 | # if length is long enough, add circles on the ends to smooth em out 114 | # this check avoids circles being drawn for tiny pipe segs 115 | draw.ellipse(circle_bbox(xys[0], draw_size * 0.5), fill=fill) 116 | draw.ellipse(circle_bbox(xys[1], draw_size * 0.5), fill=fill) 117 | 118 | 119 | def draw_parcel_risk(parcel, draw): 120 | fill = gradient_color_red(parcel.HoursFlooded + 0.5, 0, 3) 121 | draw.polygon(parcel.draw_coords, fill=fill) 122 | 123 | 124 | def draw_parcel_risk_delta(parcel, draw): 125 | if parcel.Category == 'increased_flooding': 126 | # parcel previously flooded, now floods more 127 | fill = red 128 | 129 | if parcel.Category == 'new_flooding': 130 | # parcel previously did not flood, now floods in proposed conditions 131 | fill = purple 132 | 133 | if parcel.Category == 'decreased_flooding': 134 | # parcel flooding problem decreased 135 | fill = lightblue # du.lightgrey 136 | 137 | if parcel.Category == 'eliminated_flooding': 138 | # parcel flooding problem eliminated 139 | fill = lightgreen 140 | 141 | draw.polygon(parcel.draw_coords, fill=fill) 142 | 143 | 144 | def annotate_streets(df, img, text_col): 145 | # confirm font file location 146 | if not os.path.exists(FONT_PATH): 147 | print('Error loading default font. Check your FONT_PATH') 148 | return None 149 | 150 | unique_sts = df[text_col].unique() 151 | for street in unique_sts: 152 | draw_coords = df.loc[df.ST_NAME == street, 'draw_coords'].tolist()[0] 153 | coords = df.loc[df.ST_NAME == street, 'coords'].tolist()[0] 154 | font = ImageFont.truetype(FONT_PATH, int(25)) 155 | imgTxt = Image.new('L', font.getsize(street)) 156 | drawTxt = ImageDraw.Draw(imgTxt) 157 | drawTxt.text((0, 0), street, font=font, fill=(10, 10, 12)) 158 | angle = angle_bw_points(coords[0], coords[1]) 159 | texrot = imgTxt.rotate(angle, expand=1) 160 | mpt = midpoint(draw_coords[0], draw_coords[1]) 161 | img.paste(ImageOps.colorize(texrot, (0, 0, 0), (10, 10, 12)), mpt, texrot) 162 | 163 | 164 | def gradient_grey_red(x, xmin, xmax): 165 | range = xmax - xmin 166 | 167 | rMin = 100 168 | bgMax = 100 169 | rScale = (255 - rMin) / range 170 | bgScale = (bgMax) / range 171 | x = min(x, xmax) # limit any vals to the prescribed max 172 | 173 | # print "range = " + str(range) 174 | # print "scale = " + str(scale) 175 | r = int(round(x * rScale + rMin)) 176 | g = int(round(bgMax - x * bgScale)) 177 | b = int(round(bgMax - x * bgScale)) 178 | 179 | return (r, g, b) 180 | 181 | 182 | def line_size(q, exp=1): 183 | return int(round(math.pow(q, exp))) 184 | 185 | 186 | def gradient_color_red(x, xmin, xmax, startCol=lightgrey): 187 | range = xmax - xmin 188 | 189 | rMin = startCol[0] 190 | gMax = startCol[1] 191 | bMax = startCol[2] 192 | 193 | rScale = (255 - rMin) / range 194 | gScale = (gMax) / range 195 | bScale = (bMax) / range 196 | x = min(x, xmax) # limit any vals to the prescribed max 197 | 198 | # print "range = " + str(range) 199 | # print "scale = " + str(scale) 200 | r = int(round(x * rScale + rMin)) 201 | g = int(round(gMax - x * gScale)) 202 | b = int(round(bMax - x * bScale)) 203 | 204 | return (r, g, b) 205 | 206 | 207 | def annotate_title(title, draw): 208 | size = (draw.im.getbbox()[2], draw.im.getbbox()[3]) 209 | scale = 1 * size[0] / 2048 210 | fnt = ImageFont.truetype(FONT_PATH, int(40 * scale)) 211 | draw.text((10, 15), title, fill=black, font=fnt) 212 | 213 | 214 | def annotate_timestamp(draw): 215 | size = (draw.im.getbbox()[2], draw.im.getbbox()[3]) 216 | scale = 1 * size[0] / 2048 217 | fnt = ImageFont.truetype(FONT_PATH, int(20 * scale)) 218 | 219 | timestamp = strftime("%b-%d-%Y %H:%M:%S") 220 | txt_width = draw.textlength(timestamp, fnt) 221 | xy = (size[0] - txt_width - 10, 15) 222 | draw.text(xy, timestamp, fill=grey, font=fnt) 223 | 224 | 225 | def annotate_details(txt, draw): 226 | size = (draw.im.getbbox()[2], draw.im.getbbox()[3]) 227 | scale = 1 * size[0] / 2048 228 | fnt = ImageFont.truetype(FONT_PATH, int(20 * scale)) 229 | 230 | _, top, _, bottom = draw.textbbox((0, 0), txt, fnt) 231 | txt_height = top - bottom 232 | 233 | draw.text((10, size[1] - txt_height - 10), 234 | txt, fill=black, font=fnt) 235 | -------------------------------------------------------------------------------- /swmmio/reporting/batch.py: -------------------------------------------------------------------------------- 1 | from swmmio import Model 2 | from swmmio.reporting import reporting, serialize 3 | from swmmio.reporting import functions 4 | from swmmio.utils import spatial 5 | from swmmio.graphics import swmm_graphics as sg 6 | from time import strftime 7 | import os 8 | import shutil 9 | import math 10 | from itertools import chain 11 | from swmmio.defs.config import REPORT_DIR_NAME 12 | import pandas as pd 13 | 14 | 15 | def batch_reports(project_dir, results_file, 16 | additional_costs=None, join_data=None, 17 | report_dirname='Report_AllParcels'): 18 | 19 | #combine the segments and options (combinations) into one iterable 20 | SEGMENTS_DIR = os.path.join(project_dir, 'Segments') 21 | COMBOS_DIR = os.path.join(project_dir, 'Combinations') 22 | COMMON_DATA_DIR = os.path.join(project_dir, 'CommonData') 23 | ADMIN_DIR = os.path.join(project_dir, 'ProjectAdmin') 24 | BASELINE_DIR = os.path.join(project_dir, 'Baseline') 25 | 26 | #instantiate the true baseline flood report 27 | baseline_model = Model(BASELINE_DIR) 28 | pn_join_csv = os.path.join(COMMON_DATA_DIR,r'sphila_sheds_parcels_join.csv') 29 | parcel_node_join_df = pd.read_csv(pn_join_csv) 30 | parcel_shp_df = spatial.read_shapefile(sg.config.parcels_shapefile) 31 | baserpt = reporting.FloodReport(baseline_model, parcel_node_join_df) 32 | base_flood_vol = baserpt.flood_vol_mg 33 | 34 | paths = (SEGMENTS_DIR,COMBOS_DIR) 35 | #result file header 36 | cols = 'MODEL,COST,FLOOD_VOL_MG,PARCEL_FLOOD_HRS,FLOOD_VOL_REDUCED_MG,PARCEL_FLOOD_HRS_REDUCED,PARCEL_HRS_REDUCED_DELTA_THRESH' 37 | with open(results_file, 'a') as f: 38 | f.write(cols + '\n') 39 | 40 | for path, dirs, files in chain.from_iterable(os.walk(path) for path in paths): 41 | 42 | for f in files: 43 | if '.inp' in f: 44 | inp_path = os.path.join(path,f) 45 | alt = Model(inp_path) 46 | print('reporting on {}'.format(alt.name)) 47 | #generate the reports 48 | frpt = reporting.FloodReport(alt, parcel_node_join_df) 49 | impact_rpt = reporting.ComparisonReport(baserpt, frpt, 50 | additional_costs, 51 | join_data) 52 | 53 | #write to the log 54 | model_id = os.path.splitext(f)[0] 55 | with open(results_file, 'a') as f: 56 | 57 | stats = (model_id, impact_rpt.cost_estimate, 58 | frpt.flood_vol_mg, frpt.parcel_hrs_flooded, 59 | baserpt.flood_vol_mg - frpt.flood_vol_mg, 60 | baserpt.parcel_hrs_flooded - frpt.parcel_hrs_flooded, 61 | impact_rpt.parcel_hours_reduced, 62 | ) 63 | f.write('{},{},{},{},{},{},{}\n'.format(*stats)) 64 | 65 | 66 | 67 | report_dir = os.path.join(alt.inp.dir, report_dirname) 68 | if not os.path.exists(report_dir):os.mkdir(report_dir) 69 | 70 | #write the report files 71 | impact_rpt.write(report_dir) 72 | impact_rpt.generate_figures(report_dir, parcel_shp_df) 73 | serialize.encode_report(impact_rpt, os.path.join(report_dir, 'rpt.json')) 74 | 75 | 76 | def batch_cost_estimates(baseline_dir, segments_dir, options_dir, results_file, 77 | supplemental_cost_data=None, create_proj_reports=True): 78 | """ 79 | DEPRECIATED 80 | 81 | compute the cost estimate of each model/option in the segments and 82 | combinations directories. Resulsts will be printed in the results text file. 83 | """ 84 | #combine the segments and options (combinations) into one iterable 85 | paths = (segments_dir, options_dir) 86 | baseline = Model(baseline_dir) 87 | 88 | for path, dirs, files in chain.from_iterable(os.walk(path) for path in paths): 89 | 90 | for f in files: 91 | if '.inp' in f: 92 | inp_path = os.path.join(path,f) 93 | alt = Model(inp_path) 94 | 95 | #calculate the cost 96 | costsdf = functions.estimate_cost_of_new_conduits(baseline, alt, 97 | supplemental_cost_data) 98 | cost_estimate = costsdf.TotalCostEstimate.sum() / math.pow(10, 6) 99 | print('{}: ${}M'.format(alt.name, round(cost_estimate,1))) 100 | 101 | model_id = os.path.splitext(f)[0] 102 | with open(results_file, 'a') as res: 103 | res.write('{}, {}\n'.format(model_id, cost_estimate)) 104 | 105 | if create_proj_reports: 106 | #create a option-specific per segment costing csv file 107 | report_dir = os.path.join(alt.inp.dir, REPORT_DIR_NAME) 108 | fname = '{}_CostEstimate_{}.csv'.format(alt.name, strftime("%y%m%d")) 109 | cost_report_path = os.path.join(report_dir, fname) 110 | if not os.path.exists(report_dir):os.mkdir(report_dir) 111 | costsdf.to_csv(cost_report_path) 112 | 113 | 114 | def batch_post_process(options_dir, baseline_dir, log_dir, bbox=None, overwrite=False): 115 | """ 116 | DEPRECIATED 117 | 118 | batch process all models in a given directory, where child directories 119 | each model (with .inp and .rpt companions). A bbox should be passed to 120 | control where the grahics are focused. Specify whether reporting content 121 | should be overwritten if found. 122 | """ 123 | baseline = Model(baseline_dir) 124 | folders = os.listdir(options_dir) 125 | logfile = os.path.join(log_dir, 'logfile.txt') 126 | with open (logfile, 'a') as f: 127 | f.write('MODEL,NEW_SEWER_MILES,IMPROVED,ELIMINATED,WORSE,NEW\n') 128 | for folder in folders: 129 | #first check if there is already a Report directory and skip if required 130 | current_dir = os.path.join(options_dir, folder) 131 | report_dir = os.path.join(current_dir, REPORT_DIR_NAME) 132 | if not overwrite and os.path.exists(report_dir): 133 | print('skipping {}'.format(folder)) 134 | continue 135 | 136 | else: 137 | #generate the report 138 | current_model = Model(current_dir) 139 | print('Generating report for {}'.format(current_model.inp.name)) 140 | #reporting.generate_figures(baseline, current_model, bbox=bbox, imgDir=report_dir, verbose=True) 141 | report = reporting.Report(baseline, current_model) 142 | report.write(report_dir) 143 | 144 | #keep a summay log 145 | with open (logfile, 'a') as f: 146 | #'MODEL,NEW_SEWER_MILES,IMPROVED,ELIMINATED,WORSE,NEW' 147 | f.write('{},{},{},{},{},{}\n'.format( 148 | current_model.inp.name, 149 | report.sewer_miles_new, 150 | report.parcels_flooding_improved, 151 | report.parcels_eliminated_flooding, 152 | report.parcels_worse_flooding, 153 | report.parcels_new_flooding 154 | ) 155 | ) 156 | 157 | def gather_files_in_dirs(rootdir, targetdir, searchfilename, newfilesuffix='_Impact.png'): 158 | 159 | """ 160 | scan through a directory and copy files having a given file name into a 161 | taget directory. This is useful when wanting to collect the same report image 162 | within a SWMM model's Report directory (when there are many models). 163 | 164 | This expects the Parent ... > M01_R01_W02 > Report > '03 Impact of Option.png' 168 | 169 | would be copied as follows: 170 | 171 | targetdir > 'M01_R01_W02_Impact.png' 172 | 173 | """ 174 | 175 | 176 | for root, dirs, files in os.walk(rootdir): 177 | 178 | for f in files: 179 | #if the file name = the searched file name, copy it to the target dirs 180 | #for example searchfilename = '03 Impact of Option.png' 181 | if os.path.basename(f) == searchfilename: 182 | current_dir = os.path.dirname(os.path.join(root, f)) 183 | model_id = os.path.basename(os.path.dirname(current_dir)) 184 | 185 | newf = os.path.join(targetdir, model_id + newfilesuffix) 186 | shutil.copyfile(os.path.join(root, f), newf) 187 | --------------------------------------------------------------------------------