Repository: pyswmm/swmm-python (language: python)

Issue:
Dev docs
Working on swmm-toolkit docs


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
new file mode 100644
index 00000000..621a8835
--- /dev/null
+++ b/.github/workflows/python-package.yml
@@ -0,0 +1,48 @@
+name: Build Wheels
+
+on: [push, pull_request]
+
+jobs:
+  build_wheels:
+    name: Build wheels on ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+        fail-fast: false
+        matrix:
+          os: [windows-2016, ubuntu-16.04, macos-10.15]
+          py: [3.6, 3.7, 3.8]
+
+    steps:
+      - name: Checkout repo
+        uses: actions/checkout@v2
+        with:
+          submodules: true
+
+      - name: Install Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.py }}
+
+      - name: Install build requirements
+        if: ${{ contains(matrix.os, 'windows') }}
+        run: choco install swig
+
+      - if: ${{ contains(matrix.os, 'ubuntu') }}
+        run: sudo apt-get install swig
+
+      - if: ${{ contains(matrix.os, 'macos') }}
+        run: brew install libomp swig
+
+      - name: Build wheel
+        run: pip wheel --wheel-dir=./dist ./swmm-toolkit
+
+      - name: Test wheel
+        run: |
+          pip install pytest numpy
+          pip install --no-index --find-links=./dist swmm-toolkit
+          pytest
+
+      - name: Upload artifacts
+        uses: actions/upload-artifact@v2
+        with:
+          path: ./dist/swmm_toolkit*.whl
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..db9855c9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+#
+#  .gitignore for swmm-python repo
+#
+
+!.git*
+
+.vscode
+
+.DS_Store
+
+*.pyc
+
+_skbuild/
+_cmake_test_compile
+
+dist/
+MANIFEST
+.pytest_cache/
+__pycache__/
+*.egg-info/
+
+swmm-toolkit/docs/_build
+
+swmm-toolkit/tests/data/*
+!test_*.*
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..123ee6f0
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,4 @@
+[submodule "swmm-toolkit/swmm-solver"]
+    path = swmm-toolkit/swmm-solver
+    url = https://github.com/OpenWaterAnalytics/Stormwater-Management-Model.git
+    branch = swig
\ No newline at end of file
diff --git a/README.md b/README.md
index 15faf8e8..70f85249 100644
--- a/README.md
+++ b/README.md
@@ -1,1 +1,10 @@
-# swmm-python
\ No newline at end of file
+# swmm-python
+
+
+## Contents
+nrtest-swmm  - Plugin for performing numerical regression testing on swmm-solver.
+
+
+swmm-toolkit - SWIG based wrappers for the swmm-solver and swmm-output libraries.
+
+![Build Wheels](https://github.com/SWMM-Project/swmm-python/workflows/Build%20Wheels/badge.svg)
diff --git a/swmm-toolkit/AUTHORS b/swmm-toolkit/AUTHORS
new file mode 100644
index 00000000..296b9e22
--- /dev/null
+++ b/swmm-toolkit/AUTHORS
@@ -0,0 +1,5 @@
+# Authors ordered by first contribution
+
+Michael Tryby <tryby.michael@epa.gov> (public domain)
+Jennifer Wu <jennifer.wu@xyleminc.com>
+Caleb Buahin <caleb.buahin@xyleminc.com>
diff --git a/swmm-toolkit/CMakeLists.txt b/swmm-toolkit/CMakeLists.txt
new file mode 100644
index 00000000..0484c53e
--- /dev/null
+++ b/swmm-toolkit/CMakeLists.txt
@@ -0,0 +1,43 @@
+#
+# CMakeLists.txt - CMake configuration file for swmm-toolkit python package
+#
+# Created:   Feb 6, 2020
+# Modified   May 7, 2020
+#
+#  Author:     See AUTHORS
+#
+
+
+cmake_minimum_required (VERSION 3.17)
+
+project(swmm-toolkit)
+
+
+set(Python_EXECUTABLE ${PYTHON_EXECUTABLE})
+set(Python_INCLUDE_DIRS ${PYTHON_INCLUDE_DIR})
+set(Python_LIBRARY ${PYTHON_LIBRARY})
+find_package (Python ${PYTHON_VERSION_STRING} COMPONENTS Interpreter Development)
+
+
+find_package(SWIG REQUIRED)
+cmake_policy(SET CMP0078 NEW)
+cmake_policy(SET CMP0086 NEW)
+include(${SWIG_USE_FILE})
+
+
+add_subdirectory(swmm-solver)
+
+
+add_subdirectory(src/swmm/toolkit)
+
+
+# Relocate external libs into install package
+if(APPLE)
+    install(CODE "execute_process(
+        COMMAND
+            ${PROJECT_SOURCE_DIR}/tools/relocate.zsh ${EXTERN_LIB_PATH}
+        WORKING_DIRECTORY
+            ${CMAKE_INSTALL_PREFIX}
+        )"
+    )
+endif()
diff --git a/swmm-toolkit/LICENSE.md b/swmm-toolkit/LICENSE.md
new file mode 100644
index 00000000..a26d9b01
--- /dev/null
+++ b/swmm-toolkit/LICENSE.md
@@ -0,0 +1,160 @@
+
+# Copyright Notice
+
+This project contains material prepared by the United States Government for
+which domestic copyright protection is not available under 17 USC § 105.
+
+
+No Copyright Restrictions 2020 U.S. Federal Government
+
+
+# License Notice
+
+This project is distributed under the Creative Commons Zero (CC0) Universal
+version 1.0 (see below). swmm-toolkit also includes third-party software
+libraries which have separate licensing policies.
+
+
+
+# CC0 1.0 Universal
+
+## Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator and
+subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+
+Certain owners wish to permanently relinquish those rights to a Work for the
+purpose of contributing to a commons of creative, cultural and scientific
+works ("Commons") that the public can reliably and without fear of later
+claims of infringement build upon, modify, incorporate in other works, reuse
+and redistribute as freely as possible in any form whatsoever and for any
+purposes, including without limitation commercial purposes. These owners may
+contribute to the Commons to promote the ideal of a free culture and the
+further production of creative, cultural and scientific works, or to gain
+reputation or greater distribution for their Work in part through the use and
+efforts of others.
+
+
+For these and/or other purposes and motivations, and without any expectation
+of additional consideration or compensation, the person associating CC0 with a
+Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
+and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
+and publicly distribute the Work under its terms, with knowledge of his or her
+Copyright and Related Rights in the Work and the meaning and intended legal
+effect of CC0 on those rights.
+
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not limited
+to, the following:
+
+
+  i. the right to reproduce, adapt, distribute, perform, display, communicate,
+  and translate a Work;
+
+
+  ii. moral rights retained by the original author(s) and/or performer(s);
+
+
+  iii. publicity and privacy rights pertaining to a person's image or likeness
+  depicted in a Work;
+
+
+  iv. rights protecting against unfair competition in regards to a Work,
+  subject to the limitations in paragraph 4(a), below;
+
+
+  v. rights protecting the extraction, dissemination, use and reuse of data in
+  a Work;
+
+
+  vi. database rights (such as those arising under Directive 96/9/EC of the
+  European Parliament and of the Council of 11 March 1996 on the legal
+  protection of databases, and under any national implementation thereof,
+  including any amended or successor version of such directive); and
+
+
+  vii. other similar, equivalent or corresponding rights throughout the world
+  based on applicable law or treaty, and any national implementations thereof.
+
+
+2. Waiver. To the greatest extent permitted by, but not in contravention of,
+applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
+unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
+and Related Rights and associated claims and causes of action, whether now
+known or unknown (including existing as well as future claims and causes of
+action), in the Work (i) in all territories worldwide, (ii) for the maximum
+duration provided by applicable law or treaty (including future time
+extensions), (iii) in any current or future medium and for any number of
+copies, and (iv) for any purpose whatsoever, including without limitation
+commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
+the Waiver for the benefit of each member of the public at large and to the
+detriment of Affirmer's heirs and successors, fully intending that such Waiver
+shall not be subject to revocation, rescission, cancellation, termination, or
+any other legal or equitable action to disrupt the quiet enjoyment of the Work
+by the public as contemplated by Affirmer's express Statement of Purpose.
+
+
+3. Public License Fallback. Should any part of the Waiver for any reason be
+judged legally invalid or ineffective under applicable law, then the Waiver
+shall be preserved to the maximum extent permitted taking into account
+Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
+is so judged Affirmer hereby grants to each affected person a royalty-free,
+non transferable, non sublicensable, non exclusive, irrevocable and
+unconditional license to exercise Affirmer's Copyright and Related Rights in
+the Work (i) in all territories worldwide, (ii) for the maximum duration
+provided by applicable law or treaty (including future time extensions), (iii)
+in any current or future medium and for any number of copies, and (iv) for any
+purpose whatsoever, including without limitation commercial, advertising or
+promotional purposes (the "License"). The License shall be deemed effective as
+of the date CC0 was applied by Affirmer to the Work. Should any part of the
+License for any reason be judged legally invalid or ineffective under
+applicable law, such partial invalidity or ineffectiveness shall not
+invalidate the remainder of the License, and in such case Affirmer hereby
+affirms that he or she will not (i) exercise any of his or her remaining
+Copyright and Related Rights in the Work or (ii) assert any associated claims
+and causes of action with respect to the Work, in either case contrary to
+Affirmer's express Statement of Purpose.
+
+
+4. Limitations and Disclaimers.
+
+
+  a. No trademark or patent rights held by Affirmer are waived, abandoned,
+  surrendered, licensed or otherwise affected by this document.
+
+
+  b. Affirmer offers the Work as-is and makes no representations or warranties
+  of any kind concerning the Work, express, implied, statutory or otherwise,
+  including without limitation warranties of title, merchantability, fitness
+  for a particular purpose, non infringement, or the absence of latent or
+  other defects, accuracy, or the present or absence of errors, whether or not
+  discoverable, all to the greatest extent permissible under applicable law.
+
+
+  c. Affirmer disclaims responsibility for clearing rights of other persons
+  that may apply to the Work or any use thereof, including without limitation
+  any person's Copyright and Related Rights in the Work. Further, Affirmer
+  disclaims responsibility for obtaining any necessary consents, permissions
+  or other rights required for any use of the Work.
+
+
+  d. Affirmer understands and acknowledges that Creative Commons is not a
+  party to this document and has no duty or obligation with respect to this
+  CC0 or use of the Work.
+
+
+For more information, please see
+    <http://creativecommons.org/publicdomain/zero/1.0/>
+
+
+
+# Third-Party Libraries
+
+## OWA SWMM
+
+## OpenMP
diff --git a/swmm-toolkit/README.md b/swmm-toolkit/README.md
new file mode 100644
index 00000000..760f7d1d
--- /dev/null
+++ b/swmm-toolkit/README.md
@@ -0,0 +1,52 @@
+# swmm-toolkit
+
+`swmm-toolkit` contains SWIG generated Python wrappers for the swmm-solver and swmm-output libraries. 
+
+
+## Features
+
+`swmm-toolkit` makes SWMM a fully fledged Python extension with:  
+
+ - Python integration at the speed of C
+ - Full access to library APIs
+ - Pythonic naming, enums, exceptions, and return value handling 
+
+
+## Installation
+
+
+## Build 
+1. Initialize submodule
+```cmd
+git submodule init
+git submodule update 
+```
+2. Create virtual environment
+3. `pip install` from requirements.txt
+4. `python setup.py build`
+ 
+## Troubleshoot
+
+Steps to try if `python setup.py build` fails 
+
+### Establish working swmm-solver build
+1. Create an environment variable `BOOST_ROOT_1_67_0` and set the variable to boost install location
+2. Confirm swmm-solver directory is initialized
+3. Confirm swmm-solver build is working 
+
+```
+cd swmm-solver 
+mkdir buildprod
+cd buildprod
+cmake -G "Visual Studio 14 2015 Win64" ..
+cmake --build . --config Release --target install 
+```
+
+## Basic Usage
+
+Run a SWMM simulation. 
+```
+from swmm.toolkit import solver
+
+solver.run('input_file.inp', 'report_file.rpt', 'output_file.out')
+```
diff --git a/swmm-toolkit/docs/Makefile b/swmm-toolkit/docs/Makefile
new file mode 100644
index 00000000..d4bb2cbb
--- /dev/null
+++ b/swmm-toolkit/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?=
+SPHINXBUILD   ?= sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/swmm-toolkit/docs/apidoc/swmm.rst b/swmm-toolkit/docs/apidoc/swmm.rst
new file mode 100644
index 00000000..ebd21aad
--- /dev/null
+++ b/swmm-toolkit/docs/apidoc/swmm.rst
@@ -0,0 +1,20 @@
+
+
+
+.. _api_documentation:
+
+API documentation
+===================
+
+.. automodule:: swmm
+    :members:
+    :no-undoc-members:
+    :show-inheritance:
+
+Subpackages
+-----------
+
+.. toctree::
+    :maxdepth: 1
+
+    swmm.toolkit
diff --git a/swmm-toolkit/docs/apidoc/swmm.toolkit.output.rst b/swmm-toolkit/docs/apidoc/swmm.toolkit.output.rst
new file mode 100644
index 00000000..b2fdafa0
--- /dev/null
+++ b/swmm-toolkit/docs/apidoc/swmm.toolkit.output.rst
@@ -0,0 +1,9 @@
+
+
+swmm.toolkit.output module
+==========================
+
+.. automodule:: swmm.toolkit.output
+    :members:
+    :no-undoc-members:
+    :show-inheritance:
diff --git a/swmm-toolkit/docs/apidoc/swmm.toolkit.rst b/swmm-toolkit/docs/apidoc/swmm.toolkit.rst
new file mode 100644
index 00000000..13a799b2
--- /dev/null
+++ b/swmm-toolkit/docs/apidoc/swmm.toolkit.rst
@@ -0,0 +1,18 @@
+
+
+swmm.toolkit package
+====================
+
+.. automodule:: swmm.toolkit
+    :members:
+    :no-undoc-members:
+    :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+
+   swmm.toolkit.output
+   swmm.toolkit.shared_enum
+   swmm.toolkit.solver
diff --git a/swmm-toolkit/docs/apidoc/swmm.toolkit.shared_enum.rst b/swmm-toolkit/docs/apidoc/swmm.toolkit.shared_enum.rst
new file mode 100644
index 00000000..ec70d3a8
--- /dev/null
+++ b/swmm-toolkit/docs/apidoc/swmm.toolkit.shared_enum.rst
@@ -0,0 +1,8 @@
+
+swmm.toolkit.shared_enum module
+===============================
+
+.. automodule:: swmm.toolkit.shared_enum
+    :members:
+    :no-undoc-members:
+    :show-inheritance:
\ No newline at end of file
diff --git a/swmm-toolkit/docs/apidoc/swmm.toolkit.solver.rst b/swmm-toolkit/docs/apidoc/swmm.toolkit.solver.rst
new file mode 100644
index 00000000..6db4b0eb
--- /dev/null
+++ b/swmm-toolkit/docs/apidoc/swmm.toolkit.solver.rst
@@ -0,0 +1,20 @@
+
+
+swmm.toolkit.solver module
+==========================
+
+
+.. automodule:: swmm.toolkit.solver
+    :members:
+    :no-undoc-members:
+    :show-inheritance:
+
+
+.. autoclass:: swmm.toolkit.solver.NodeStats
+.. autoclass:: swmm.toolkit.solver.StorageStats
+.. autoclass:: swmm.toolkit.solver.OutfallStats
+.. autoclass:: swmm.toolkit.solver.LinkStats
+.. autoclass:: swmm.toolkit.solver.PumpStats
+.. autoclass:: swmm.toolkit.solver.SubcatchStats
+.. autoclass:: swmm.toolkit.solver.RoutingTotals
+.. autoclass:: swmm.toolkit.solver.RunoffTotals
diff --git a/swmm-toolkit/docs/conf.py b/swmm-toolkit/docs/conf.py
new file mode 100644
index 00000000..f6ff3055
--- /dev/null
+++ b/swmm-toolkit/docs/conf.py
@@ -0,0 +1,70 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../_skbuild/macosx-10.10-x86_64-3.7/cmake-install/src'))
+
+# -- Project information -----------------------------------------------------
+
+project = 'swmm.toolkit'
+copyright = 'See LICENSE'
+author = 'See AUTHORS'
+
+# The full version, including alpha/beta/rc tags
+release = '0.5.0'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.viewcode',
+    'sphinx.ext.autosummary',
+    'numpydoc',
+    'sphinx.ext.napoleon',
+    'sphinx.ext.intersphinx'
+]
+
+napoleon_use_rtype = False
+viewcode_follow_imported_members = True
+numpydoc_show_class_members = True
+numpydoc_show_inherited_class_members = False
+numpydoc_class_members_toctree = False
+autodoc_member_order = 'bysource'
+autoclass_content = 'init'
+numfig=True
+numfig_format = {'figure':  'Figure %s', 'table': 'Table %s', 'code-block': 'Listing %s'}
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
diff --git a/swmm-toolkit/docs/index.rst b/swmm-toolkit/docs/index.rst
new file mode 100644
index 00000000..37fa809f
--- /dev/null
+++ b/swmm-toolkit/docs/index.rst
@@ -0,0 +1,22 @@
+.. swmm.toolkit documentation master file, created by
+   sphinx-quickstart on Mon Jun 15 13:57:22 2020.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+
+Welcome to swmm.toolkit's documentation!
+========================================
+
+.. toctree::
+   :maxdepth: 1
+   :caption: Contents:
+
+   API documentation <apidoc/swmm>
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/swmm-toolkit/docs/make.bat b/swmm-toolkit/docs/make.bat
new file mode 100644
index 00000000..922152e9
--- /dev/null
+++ b/swmm-toolkit/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.http://sphinx-doc.org/
+	exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/swmm-toolkit/pyproject.toml b/swmm-toolkit/pyproject.toml
new file mode 100644
index 00000000..6d0263b7
--- /dev/null
+++ b/swmm-toolkit/pyproject.toml
@@ -0,0 +1,6 @@
+
+
+
+[build-system]
+requires = ["setuptools", "wheel", "scikit-build", "cmake"]
+build-backend = "setuptools.build_meta"
diff --git a/swmm-toolkit/setup.py b/swmm-toolkit/setup.py
new file mode 100644
index 00000000..11e2b294
--- /dev/null
+++ b/swmm-toolkit/setup.py
@@ -0,0 +1,120 @@
+#
+# setup.py - Setup script for swmm-toolkit python package
+#
+# Created:    Jul 2, 2018
+# Updated:    May 7, 2020
+#
+#  Author:    See AUTHORS
+#
+# Suggested Usage:
+#   python setup.py build
+#   python setup.py bdist_wheel
+#   python setup.py sdist
+#   python setup.py clean
+#
+
+import platform
+import subprocess
+import pathlib
+
+from skbuild import setup
+from setuptools import Command
+
+
+# Determine platform
+platform_system = platform.system()
+
+
+class CleanCommand(Command):
+    ''' Cleans project tree '''
+    user_options = []
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        if platform_system == "Windows":
+            cmd = ['del' '/Q', 'tests\\data\\temp_*.*' '&&' \
+            'rd' '/s/q', '_cmake_test_compile', '_skbuild', 'dist', '.pytest_cache', \
+            'src\\swmm\\toolkit\\swmm_toolkit.egg-info', 'tests\\__pycache__']
+            exe = "C:\\Windows\\System32\\cmd.exe"
+
+        elif platform_system == "Linux":
+            cmd = ["rm -vrf _skbuild/ dist/ **/build .pytest_cache/ **/__pycache__  \
+            **/*.egg-info **/data/temp_*.* **/data/en* **/.DS_Store MANIFEST"]
+            exe = "/bin/bash"
+
+        elif platform_system == "Darwin":
+            cmd = ['setopt extended_glob nullglob; rm -vrf _skbuild dist **/build .pytest_cache \
+            **/__pycache__ **/*.egg-info **/data/(^test_*).* **/data/en* **/.DS_Store MANIFEST']
+            exe = '/bin/zsh'
+
+        p = subprocess.Popen(cmd, shell=True, executable=exe)
+        p.wait()
+
+
+# Set Platform specific cmake args here
+if platform_system == "Windows":
+    cmake_args = ["-GVisual Studio 15 2017 Win64"]
+
+elif platform_system == "Darwin":
+    cmake_args = ["-GXcode","-DCMAKE_OSX_DEPLOYMENT_TARGET:STRING=10.10"]
+
+else:
+    cmake_args = ["-GUnix Makefiles"]
+
+
+# Filters cmake manifest for wheel build
+def exclude_files(cmake_manifest):
+    print("INFO: processing cmake manifest")
+    exclude_pats = ('runswmm', '.exe', '.cmake', '.h', '.lib')
+    return list(filter(lambda name: not (name.endswith(exclude_pats)), cmake_manifest))
+
+
+
+# Get the long description from the README file
+here = pathlib.Path(__file__).parent.resolve()
+long_description = (here / 'README.md').read_text(encoding='utf-8')
+
+
+setup(
+    name = "swmm-toolkit",
+    version = "0.8.0",
+
+    packages = ["swmm.toolkit"],
+    package_dir = {"": "src"},
+
+    zip_safe = False,
+    install_requires = ["aenum"],
+
+    cmdclass = {"clean": CleanCommand},
+    cmake_args = cmake_args,
+    cmake_process_manifest_hook = exclude_files,
+
+
+    description='OWA SWMM Python Toolkit',
+    long_description=long_description,
+    long_description_content_type='text/markdown',
+    url='https://github.com/OpenWaterAnalytics/swmm-python',
+    
+    author='See AUTHORS',
+    maintainer_email='tryby.michael@epa.gov',
+    license='CC0',
+
+    keywords="swmm5, swmm, stormwater, hydraulics, hydrology, ",
+    classifiers=[
+        "Topic :: Scientific/Engineering",
+        "Operating System :: Microsoft :: Windows",
+        "Operating System :: POSIX :: Linux",
+        "Operating System :: MacOS",
+        "License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: C",
+        "Development Status :: 4 - Beta",
+    ]
+)
diff --git a/swmm-toolkit/src/swmm/toolkit/CMakeLists.txt b/swmm-toolkit/src/swmm/toolkit/CMakeLists.txt
new file mode 100644
index 00000000..d1752e72
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/CMakeLists.txt
@@ -0,0 +1,155 @@
+#
+# CMakeLists.txt - CMake configuration file for swmm.toolkit python package
+#
+# Created:   Feb 6, 2020
+# Updated:   May 6, 2020
+#
+#  Author:   See AUTHORS
+#
+
+
+set(CMAKE_SWIG_FLAGS -py3)
+
+# Determine filename suffix for python extension
+if(WIN32)
+    set(LIB_EXT "pyd")
+else()
+    set(LIB_EXT "so")
+endif()
+
+set(Python_EXTENSION ".${Python_SOABI}.${LIB_EXT}")
+
+
+# Location of package at runtime on MacOS/Linux
+if(APPLE)
+    set(LIB_ROOT "@loader_path")
+else()
+    set(LIB_ROOT "$ORIGIN")
+endif()
+
+set(PACKAGE_RPATH "${LIB_ROOT}/../../../..;${LIB_ROOT}/../../../../extern;${LIB_ROOT}/../../../lib")
+
+
+#############################################################
+####################    OUTPUT TARGET    ####################
+#############################################################
+
+set_property(SOURCE output.i
+    PROPERTY
+        USE_TARGET_INCLUDE_DIRECTORIES ON
+)
+
+swig_add_library(output
+    TYPE
+        MODULE
+    LANGUAGE
+        python
+    SOURCES
+        output.i
+)
+
+target_link_options(output
+    PUBLIC
+        $<$<BOOL:${APPLE}>:-undefineddynamic_lookup>
+)
+
+target_include_directories(output
+    PUBLIC
+        ${Python_INCLUDE_DIRS}
+)
+
+target_link_libraries(output
+    PUBLIC
+        $<$<BOOL:$<C_COMPILER_ID:MSVC>>:Python::Module>
+        swmm-output
+)
+
+set_target_properties(output
+    PROPERTIES
+        SUFFIX ${Python_EXTENSION}
+        SWIG_COMPILE_DEFINITIONS EXPORT_OUT_API
+        MACOSX_RPATH TRUE
+        SKIP_BUILD_RPATH FALSE
+        BUILD_WITH_INSTALL_RPATH FALSE
+        INSTALL_RPATH "${PACKAGE_RPATH}"
+        INSTALL_RPATH_USE_LINK_PATH TRUE
+)
+
+install(
+    FILES
+        "__init__.py"
+        "shared_enum.py"
+        "output_metadata.py"
+        "${CMAKE_CURRENT_BINARY_DIR}/output.py"
+    DESTINATION
+        "src/swmm/toolkit"
+)
+
+install(
+    TARGETS
+        output
+    DESTINATION
+        "src/swmm/toolkit"
+)
+
+#############################################################
+####################    SOLVER TARGET    ####################
+#############################################################
+
+set_property(SOURCE solver.i
+    PROPERTY
+        USE_TARGET_INCLUDE_DIRECTORIES ON
+)
+
+swig_add_library(solver
+    TYPE
+        MODULE
+    LANGUAGE
+        python
+    SOURCES
+        solver.i
+)
+
+target_link_options(solver
+    PUBLIC
+        $<$<BOOL:${APPLE}>:-undefineddynamic_lookup>
+)
+
+target_include_directories(solver
+    PUBLIC
+        ${Python_INCLUDE_DIRS}
+)
+
+target_link_libraries(solver
+    PUBLIC
+        $<$<BOOL:$<C_COMPILER_ID:MSVC>>:Python::Module>
+        swmm5
+)
+
+set_target_properties(solver
+    PROPERTIES
+        SUFFIX ${Python_EXTENSION}
+        MACOSX_RPATH TRUE
+        SKIP_BUILD_RPATH FALSE
+        BUILD_WITH_INSTALL_RPATH FALSE
+        INSTALL_RPATH "${PACKAGE_RPATH}"
+        INSTALL_RPATH_USE_LINK_PATH TRUE
+)
+
+
+# Stages extension module for package build
+install(
+    FILES
+        "${CMAKE_CURRENT_BINARY_DIR}/solver.py"
+    DESTINATION
+        "src/swmm/toolkit"
+)
+
+
+# Stages extension library for package build
+install(
+    TARGETS
+        solver
+    DESTINATION
+        "src/swmm/toolkit"
+)
diff --git a/swmm-toolkit/src/swmm/toolkit/__init__.py b/swmm-toolkit/src/swmm/toolkit/__init__.py
new file mode 100644
index 00000000..ab6cb94b
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/__init__.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+
+#
+#  __init__.py - SWMM toolkit package
+#
+#  Created:    Aug 9, 2018
+#  Updated:    Dec 22, 2020
+#
+#  Author:     See AUTHORS
+#
+
+'''
+A low level pythonic API for the swmm-output and swmm-solver dlls using SWIG.
+'''
+
+
+__author__ = "See AUTHORS"
+__copyright__ = "None"
+__credits__ = "Colleen Barr, Sam Hatchett"
+__license__ = "CC0 1.0 Universal"
+
+__version__ = "0.8.0"
+__date__ = "December 22, 2020"
+
+__maintainer__ = "Michael Tryby"
+__email__ = "tryby.michael@epa.gov"
+__status__  = "Beta"
+
+
+import os
+import platform
+
+
+# Adds directory containing swmm libraries to path
+if platform.system() == "Windows":
+    libdir = os.path.join(os.path.dirname(__file__), "../../../../bin")
+
+    if hasattr(os, 'add_dll_directory'):
+        os.add_dll_directory(libdir)
+    else:
+        os.environ["PATH"] = libdir + ";" + os.environ["PATH"]
diff --git a/swmm-toolkit/src/swmm/toolkit/output.i b/swmm-toolkit/src/swmm/toolkit/output.i
new file mode 100644
index 00000000..95fd21d8
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/output.i
@@ -0,0 +1,155 @@
+/*
+ *  output.i - SWIG interface description file for swmm-output
+ *
+ *  Created:    November 3, 2017
+ *  Updated:    February 21, 2020
+ *
+ *  Author:     See AUTHORS
+ *
+*/
+
+
+%include "typemaps.i"
+%include "cstring.i"
+
+
+/* Docstrings for module */
+%include "output_docs.i"
+
+%module(package="swmm.toolkit", docstring=OUTPUT_MODULE_DOCS) output
+%{
+#define SWIG_FILE_WITH_INIT
+
+#include "swmm_output_enums.h"
+#include "swmm_output.h"
+%}
+
+/* RENAME FUNCTIONS PYTHON STYLE */
+//%rename("%(regex:/^\w+_([a-zA-Z]+)/\L\\1/)s") "";
+%include "output_rename.i"
+
+
+/* MARK FUNCTIONS FOR ALLOCATING AND DEALLOCATING HANDLES */
+%newobject SMO_init;
+%delobject SMO_close;
+
+
+/* TYPEMAPS FOR HANDLE POINTER */
+/* Used for functions that output a new opaque pointer */
+%typemap(in,numinputs=0) SMO_Handle *p_handle (SMO_Handle temp) {
+    $1 = &temp;
+}
+/* used for functions that take in an opaque pointer (or NULL)
+and return a (possibly) different pointer */
+%typemap(argout) SMO_Handle *p_handle {
+    %append_output(SWIG_NewPointerObj(*$1, SWIGTYPE_p_Handle, SWIG_POINTER_NEW));
+}
+
+
+/* TYPEMAP FOR IGNORING INT ERROR CODE RETURN VALUE */
+%typemap(out) int {
+    $result = Py_None;
+    Py_INCREF($result);
+}
+
+
+/* APPLY MACROS FOR OUTPUT VARIABLES */
+%apply double *OUTPUT {
+    double *date
+}
+
+%apply int *OUTPUT {
+    int *version,
+    int *time
+}
+
+%cstring_output_allocate_size(char **elementName, int *size, SMO_freeMemory(*$1));
+
+
+/* TYPEMAPS FOR MEMORY MANAGEMNET OF FLOAT ARRAYS */
+%typemap(in, numinputs=0)float **float_out (float *temp), int *int_dim (int temp){
+   $1 = &temp;
+}
+%typemap(argout) (float **float_out, int *int_dim) {
+    if (*$1) {
+      PyObject *o = PyList_New(*$2);
+      float* temp = *$1;
+      for(int i=0; i<*$2; i++) {
+        PyList_SetItem(o, i, PyFloat_FromDouble((double)temp[i]));
+      }
+      $result = SWIG_Python_AppendOutput($result, o);
+      SMO_freeMemory(*$1);
+    }
+}
+
+
+/* TYPEMAPS FOR MEMORY MANAGEMENT OF INT ARRAYS */
+%typemap(in, numinputs=0)int **int_out (int *temp), int *int_dim (int temp){
+    $1 = &temp;
+}
+%typemap(argout) (int **int_out, int *int_dim) {
+    if (*$1) {
+        int *temp = *$1;
+        PyObject *o = PyList_New(*$2);
+        for(int i=0; i<*$2; i++) {
+            PyList_SetItem(o, i, PyInt_FromLong((long)temp[i]));
+        }
+        $result = SWIG_Python_AppendOutput($result, o);
+        SMO_freeMemory(*$1);
+    }
+}
+
+
+/* TYPEMAP FOR ENUMERATED TYPE INPUT ARGUMENTS */
+%typemap(in) EnumTypeIn {
+    int value = 0;
+    if (PyObject_HasAttrString($input, "value")) {
+        PyObject *o = PyObject_GetAttrString($input, "value");
+        SWIG_AsVal_int(o, &value);
+    }
+    $1 = ($1_basetype)(value);
+}
+%apply EnumTypeIn {
+    SMO_unit,
+    SMO_elementType,
+    SMO_time,
+    SMO_subcatchAttribute,
+    SMO_nodeAttribute,
+    SMO_linkAttribute,
+    SMO_systemAttribute
+}
+
+
+/* INSERTS CUSTOM EXCEPTION HANDLING IN WRAPPER */
+%exception SMO_init
+{
+    $function
+}
+
+%exception SMO_close
+{
+    $function
+}
+
+%exception
+{
+    char *err_msg;
+    SMO_clearError(arg1);
+    $function
+    if (SMO_checkError(arg1, &err_msg))
+    {
+        PyErr_SetString(PyExc_Exception, err_msg);
+        SMO_freeMemory((void *)err_msg);
+        SWIG_fail;
+    }
+}
+
+/* INSERT EXCEPTION HANDLING FOR THESE FUNCTIONS */
+
+%ignore SMO_freeMemory;
+%ignore SMO_clearError;
+%ignore SMO_checkError;
+
+%include "swmm_output.h"
+
+%exception;
diff --git a/swmm-toolkit/src/swmm/toolkit/output_docs.i b/swmm-toolkit/src/swmm/toolkit/output_docs.i
new file mode 100644
index 00000000..b2893419
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/output_docs.i
@@ -0,0 +1,440 @@
+/*
+ *  output_docs.i - Documentation for swmm-output
+ *
+ *  Created:    12/10/2020
+ *  
+ *  Author:     See AUTHORS
+ *
+*/
+
+
+%define OUTPUT_MODULE_DOCS
+"Output Module
+"
+%enddef
+
+
+%feature("autodoc", 
+"Initialize pointer for output handle.
+
+Returns
+-------
+p_handle: SMO_Handle *
+    A SWMM output handle
+"
+) SMO_init;
+
+
+%feature("autodoc",
+"Open binary output file and reads prologue and epilogue.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+path: char const *
+    The name of the binary output file to be opened.
+"
+) SMO_open;
+
+
+%feature("autodoc",
+"Close binary output file and perform cleanup. 
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+"
+) SMO_close;
+
+
+%feature("autodoc",
+"Get the SWMM version that produced the binary output file. 
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+
+Returns
+-------
+version: int *
+    The SWMM version number found in the output file prologue.
+
+"
+) SMO_getVersion;
+
+
+%feature("autodoc",
+"Project element counts.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+
+Returns
+-------
+elementCount:int **
+    Array of element count values
+length:int *
+    Array size
+"
+) SMO_getProjectSize;
+
+
+%feature("autodoc",
+"Project unit flags for unit_system, flow, and pollutants.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+
+Returns
+-------
+unitFlag: int **
+    Array of unit flag values
+length: int *
+    Array length
+"
+) SMO_getUnits;
+
+
+%feature("autodoc",
+"Analysis start date.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+
+Returns
+-------
+date: double *
+    SWMM simulation start date (encoded)
+"
+) SMO_getStartDate;
+
+
+%feature("autodoc",
+"Analysis step size and number of periods.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+code: SMO_time
+    A time parameter code (see :ref: SMO_Time)
+
+Returns
+-------
+time: int *
+    Time value
+"
+) SMO_getTimes;
+
+
+%feature("autodoc",
+"Given an element index returns the element name.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+type: SMO_elementType
+    The type of the element being queried
+elementIndex: int
+    The index of the element being queried
+
+Returns
+-------
+name: char **
+    Element name array
+length: int *
+    Length of array
+"
+) SMO_getElementName;
+
+
+%feature("autodoc",
+"Get time series results for particular attribute. Specify series start and 
+length using timeIndex and length respectively.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+subcatchIndex: int
+    The subcatchment index for time series
+attr: SMO_subcatchAttribute
+    Code for the subcatch attribute to report (see :ref: SMO_subcatchAttribute)
+startPeriod: int
+    Time period of series start
+endPeriod: int
+    Time period of series end
+
+Returns
+-------
+outValueArray: float **
+    Array of time series values
+length: int *
+    Length of array
+"
+) SMO_getSubcatchSeries;
+
+
+%feature("autodoc",
+"Get time series results for particular attribute. Specify series start and 
+length using timeIndex and length respectively.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+nodeIndex: int
+    The node index for time series
+attr: SMO_nodeAttribute
+    Code for the node attribute to report (see :ref: SMO_nodeAttribute)
+startPeriod: int
+    Time period of series start
+endPeriod: int
+    Time period of series end
+
+Returns
+-------
+outValueArray: float **
+    Array of time series values
+length: int *
+    Length of array
+"
+) SMO_getNodeSeries;
+
+
+%feature("autodoc",
+"Get time series results for particular attribute. Specify series start and 
+length using timeIndex and length respectively.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+linkIndex: int
+    The link index for time series
+attr: SMO_linkAttribute
+    Code for the link attribute to report (see :ref: SMO_linkAttribute)
+startPeriod: int
+    Time period of series start
+endPeriod: int
+    Time period of series end
+
+Returns
+-------
+outValueArray: float **
+    Array of time series values
+length: int *
+    Length of array
+"
+) SMO_getLinkSeries;
+
+
+%feature("autodoc",
+"Get time series results for particular attribute. Specify series start and 
+length using timeIndex and length respectively.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+attr: SMO_systemAttribute
+    Code for the system attribute to report (see :ref: SMO_systemAttribute)
+startPeriod: int
+    Time period of series start
+endPeriod: int
+    Time period of series end
+
+Returns
+-------
+outValueArray: float **
+    Array of time series values
+length: int *
+    Length of array
+"
+) SMO_getSystemSeries;
+
+
+%feature("autodoc",
+"For all subcatchments at given time, get a particular attribute.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+timeIndex: int
+    The time period index for reporting results
+attr: SMO_subcatchAttribute
+    Code for the subcatchment attribute to report (see :ref: SMO_subcatchAttribute)
+
+Returns
+-------
+outValueArray: float **
+    The array of subcatchment attribute values
+length: int *
+    Length of array
+"
+) SMO_getSubcatchAttribute;
+
+
+%feature("autodoc",
+"For all nodes at given time, get a particular attribute.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+timeIndex: int
+    The time period index for reporting results
+attr: SMO_nodeAttribute
+    Code for the node attribute to report (see :ref: SMO_nodeAttribute)
+
+Returns
+-------
+outValueArray: float **
+    The array of node attribute values
+length: int *
+    Length of array
+"
+) SMO_getNodeAttribute;
+
+
+%feature("autodoc",
+"For all links at given time, get a particular attribute.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+timeIndex: int
+    The time period index for reporting results
+attr: SMO_linkAttribute
+    Code for the link attribute to report (see :ref: SMO_linkAttribute)
+
+Returns
+-------
+outValueArray: float **
+    The array of link attribute values
+length: int *
+    Length of array
+"
+) SMO_getLinkAttribute;
+
+
+%feature("autodoc",
+"For the system at given time, get a particular attribute.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+timeIndex: int
+    The time period index for reporting results
+attr: SMO_systemAttribute
+    Code for the system attribute to report (see :ref: SMO_systemAttribute)
+
+Returns
+-------
+outValueArray: float **
+    The array of system attribute values
+length: int *
+    Length of array
+"
+) SMO_getSystemAttribute;
+
+
+%feature("autodoc",
+"For a subcatchment at given time, get all attributes.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+timeIndex: int
+    The time period index for reporting results
+subcatchIndex: int
+    The subcatchment index for reporting results
+
+Returns
+-------
+outValueArray: float **
+    The array of subcatchment result values
+length: int *
+    Length of array
+"
+) SMO_getSubcatchResult;
+
+
+%feature("autodoc",
+"For a node at given time, get all attributes.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+timeIndex: int
+    The time period index for reporting results
+nodeIndex: int
+    The node index for reporting results
+
+Returns
+-------
+outValueArray: float **
+    The array of node result values
+length: int *
+    Length of array
+"
+) SMO_getNodeResult;
+
+
+%feature("autodoc",
+"For a link at given time, get all attributes.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+timeIndex: int
+    The time period index for reporting results
+linkIndex: int
+    The link index for reporting results
+
+Returns
+-------
+outValueArray: float **
+    The array of link result values
+length: int *
+    Length of array
+"
+) SMO_getLinkResult;
+
+
+%feature("autodoc",
+"For the system at given time, get all attributes.
+
+Parameters
+----------
+p_handle: SMO_Handle
+    A SWMM output handle
+timeIndex: int
+    The time period index for reporting results
+dummyIndex: int
+    No-op for API consistency
+
+Returns
+-------
+outValueArray: float **
+    The array of system result values
+length: int *
+    Length of array
+"
+) SMO_getSystemResult;
diff --git a/swmm-toolkit/src/swmm/toolkit/output_metadata.py b/swmm-toolkit/src/swmm/toolkit/output_metadata.py
new file mode 100644
index 00000000..82576fe6
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/output_metadata.py
@@ -0,0 +1,245 @@
+#
+#  output_metadata.py -
+#
+#  Created:  February 20, 2020
+#  Updated:
+#
+#  Author:   See AUTHORS
+#
+
+
+from itertools import islice
+
+from aenum import extend_enum
+
+from swmm.toolkit import output, shared_enum
+
+
+class OutputMetadata:
+    '''
+    Simple attribute name and unit lookup.
+    '''
+    _unit_labels_us_ = {
+        shared_enum.BaseUnits.RAIN_INT:   "in/hr",
+        shared_enum.BaseUnits.SNOW_DEPTH: "in",
+        shared_enum.BaseUnits.EVAP_RATE:  "in/day",
+        shared_enum.BaseUnits.INFIL_RATE: "in/hr",
+        shared_enum.BaseUnits.ELEV:       "ft",
+        shared_enum.BaseUnits.PERCENT:    "%",
+        shared_enum.BaseUnits.HEAD:       "ft",
+        shared_enum.BaseUnits.VOLUME:     "cu ft",
+        shared_enum.BaseUnits.VELOCITY:   "ft/sec",
+        shared_enum.BaseUnits.TEMP:       "deg F",
+        shared_enum.BaseUnits.UNITLESS:   "unitless",
+        shared_enum.BaseUnits.NONE:       "",
+
+        shared_enum.FlowUnits.CFS:          "cu ft/sec",
+        shared_enum.FlowUnits.GPM:          "gal/min",
+        shared_enum.FlowUnits.MGD:          "M gal/day",
+    }
+
+    _unit_labels_si_ = {
+        shared_enum.BaseUnits.RAIN_INT:   "mm/hr",
+        shared_enum.BaseUnits.SNOW_DEPTH: "mm",
+        shared_enum.BaseUnits.EVAP_RATE:  "mm/day",
+        shared_enum.BaseUnits.INFIL_RATE: "mm/hr",
+        shared_enum.BaseUnits.ELEV:       "m",
+        shared_enum.BaseUnits.PERCENT:    "%",
+        shared_enum.BaseUnits.HEAD:       "m",
+        shared_enum.BaseUnits.VOLUME:     "cu m",
+        shared_enum.BaseUnits.VELOCITY:   "m/sec",
+        shared_enum.BaseUnits.TEMP:       "deg C",
+        shared_enum.BaseUnits.UNITLESS:   "unitless",
+        shared_enum.BaseUnits.NONE:       "",
+
+        shared_enum.FlowUnits.CMS:          "cu m/sec",
+        shared_enum.FlowUnits.LPS:          "L/sec",
+        shared_enum.FlowUnits.MLD:          "M L/day",
+    }
+
+    _unit_labels_quality_ = {
+        shared_enum.ConcUnits.MG:           "mg/L",
+        shared_enum.ConcUnits.UG:           "ug/L",
+        shared_enum.ConcUnits.COUNT:        "Count/L",
+        shared_enum.ConcUnits.NONE:         ""
+    }
+
+
+    def _build_pollut_metadata(self, output_handle):
+        '''
+        Builds metadata for pollutant attributes at runtime.
+        '''
+        # Get number of pollutants
+        n = output.get_proj_size(output_handle)[shared_enum.ElementType.POLLUT]
+
+        if n > 0:
+
+            pollut_name = []
+            pollut_units = []
+
+            # Get pollutant names
+            for i in range(0, n):
+                pollut_name.append(output.get_elem_name(
+                    output_handle, shared_enum.ElementType.POLLUT, i))
+            # Get pollutant units
+            for u in output.get_units(output_handle)[2:]:
+                pollut_units.append(shared_enum.ConcUnits(u))
+
+            # Create dictionary keys
+            for i in range(1, n):
+                symbolic_name = 'POLLUT_CONC_' + str(i)
+                extend_enum(shared_enum.SubcatchAttribute, symbolic_name, 8 + i)
+                extend_enum(shared_enum.NodeAttribute, symbolic_name, 6 + i)
+                extend_enum(shared_enum.LinkAttribute, symbolic_name, 5 + i)
+
+            # Update metadata dictionary with pollutant metadata
+            for i, attr in enumerate(islice(shared_enum.SubcatchAttribute, 8, None)):
+                self._metadata[attr] = (pollut_name[i], self._unit_labels[pollut_units[i]])
+
+            for i, attr in enumerate(islice(shared_enum.NodeAttribute, 6, None)):
+                self._metadata[attr] = (pollut_name[i], self._unit_labels[pollut_units[i]])
+
+            for i, attr in enumerate(islice(shared_enum.LinkAttribute, 5, None)):
+                self._metadata[attr] = (pollut_name[i], self._unit_labels[pollut_units[i]])
+
+
+    def __init__(self, output_handle):
+        # Get units from binary output file
+        self.units = output.get_units(output_handle)
+
+        # Determine prevailing unit system
+        self._unit_system = shared_enum.UnitSystem(self.units[0])
+        if self._unit_system == shared_enum.UnitSystem.US:
+            self._unit_labels = type(self)._unit_labels_us_
+        else:
+            self._unit_labels = type(self)._unit_labels_si_
+
+        self._unit_labels.update(type(self)._unit_labels_quality_)
+
+        # Set user flow units
+        self._flow = shared_enum.FlowUnits(self.units[1])
+
+        self._metadata = {
+            shared_enum.SubcatchAttribute.RAINFALL:
+                ("Rainfall", self._unit_labels[shared_enum.BaseUnits.RAIN_INT]),
+            shared_enum.SubcatchAttribute.SNOW_DEPTH:
+                ("Snow Depth", self._unit_labels[shared_enum.BaseUnits.SNOW_DEPTH]),
+            shared_enum.SubcatchAttribute.EVAP_LOSS:
+                ("Evaporation Loss", self._unit_labels[shared_enum.BaseUnits.EVAP_RATE]),
+            shared_enum.SubcatchAttribute.INFIL_LOSS:
+                ("Infiltration Loss", self._unit_labels[shared_enum.BaseUnits.INFIL_RATE]),
+            shared_enum.SubcatchAttribute.RUNOFF_RATE:
+                ("Runoff Rate", self._unit_labels[self._flow]),
+            shared_enum.SubcatchAttribute.GW_OUTFLOW_RATE:
+                ("Groundwater Flow Rate", self._unit_labels[self._flow]),
+            shared_enum.SubcatchAttribute.GW_TABLE_ELEV:
+                ("Groundwater Elevation", self._unit_labels[shared_enum.BaseUnits.ELEV]),
+            shared_enum.SubcatchAttribute.SOIL_MOISTURE:
+                ("Soil Moisture", self._unit_labels[shared_enum.BaseUnits.PERCENT]),
+            shared_enum.SubcatchAttribute.POLLUT_CONC_0:
+                ("Pollutant Concentration", self._unit_labels[shared_enum.BaseUnits.NONE]),
+
+            shared_enum.NodeAttribute.INVERT_DEPTH:
+                ("Invert Depth", self._unit_labels[shared_enum.BaseUnits.ELEV]),
+            shared_enum.NodeAttribute.HYDRAULIC_HEAD:
+                ("Hydraulic Head", self._unit_labels[shared_enum.BaseUnits.HEAD]),
+            shared_enum.NodeAttribute.PONDED_VOLUME:
+                ("Ponded Volume", self._unit_labels[shared_enum.BaseUnits.VOLUME]),
+            shared_enum.NodeAttribute.LATERAL_INFLOW:
+                ("Lateral Inflow", self._unit_labels[self._flow]),
+            shared_enum.NodeAttribute.TOTAL_INFLOW:
+                ("Total Inflow", self._unit_labels[self._flow]),
+            shared_enum.NodeAttribute.FLOODING_LOSSES:
+                ("Flooding Loss", self._unit_labels[self._flow]),
+            shared_enum.NodeAttribute.POLLUT_CONC_0:
+                ("Pollutant Concentration", self._unit_labels[shared_enum.BaseUnits.NONE]),
+
+            shared_enum.LinkAttribute.FLOW_RATE:
+                ("Flow Rate", self._unit_labels[self._flow]),
+            shared_enum.LinkAttribute.FLOW_DEPTH:
+                ("Flow Depth", self._unit_labels[shared_enum.BaseUnits.ELEV]),
+            shared_enum.LinkAttribute.FLOW_VELOCITY:
+                ("Flow Velocity", self._unit_labels[shared_enum.BaseUnits.VELOCITY]),
+            shared_enum.LinkAttribute.FLOW_VOLUME:
+                ("Flow Volume", self._unit_labels[shared_enum.BaseUnits.VOLUME]),
+            shared_enum.LinkAttribute.CAPACITY:
+                ("Capacity", self._unit_labels[shared_enum.BaseUnits.PERCENT]),
+            shared_enum.LinkAttribute.POLLUT_CONC_0:
+                ("Pollutant Concentration", self._unit_labels[shared_enum.BaseUnits.NONE]),
+
+            shared_enum.SystemAttribute.AIR_TEMP:
+                ("Temperature", self._unit_labels[shared_enum.BaseUnits.TEMP]),
+            shared_enum.SystemAttribute.RAINFALL:
+                ("Rainfall", self._unit_labels[shared_enum.BaseUnits.RAIN_INT]),
+            shared_enum.SystemAttribute.SNOW_DEPTH:
+                ("Snow Depth", self._unit_labels[shared_enum.BaseUnits.SNOW_DEPTH]),
+            shared_enum.SystemAttribute.EVAP_INFIL_LOSS:
+                ("Evap and Infil Losses", self._unit_labels[shared_enum.BaseUnits.INFIL_RATE]),
+            shared_enum.SystemAttribute.RUNOFF_FLOW:
+                ("Runoff Flow Rate", self._unit_labels[self._flow]),
+            shared_enum.SystemAttribute.DRY_WEATHER_INFLOW:
+                ("Dry Weather Inflow", self._unit_labels[self._flow]),
+            shared_enum.SystemAttribute.GW_INFLOW:
+                ("Groundwater Inflow", self._unit_labels[self._flow]),
+            shared_enum.SystemAttribute.RDII_INFLOW:
+                ("RDII Inflow", self._unit_labels[self._flow]),
+            shared_enum.SystemAttribute.DIRECT_INFLOW:
+                ("Direct Inflow", self._unit_labels[self._flow]),
+            shared_enum.SystemAttribute.TOTAL_LATERAL_INFLOW:
+                ("Total Lateral Inflow", self._unit_labels[self._flow]),
+            shared_enum.SystemAttribute.FLOOD_LOSSES:
+                ("Flood Losses", self._unit_labels[self._flow]),
+            shared_enum.SystemAttribute.OUTFALL_FLOWS:
+                ("Outfall Flow", self._unit_labels[self._flow]),
+            shared_enum.SystemAttribute.VOLUME_STORED:
+                ("Volume Stored", self._unit_labels[shared_enum.BaseUnits.VOLUME]),
+            shared_enum.SystemAttribute.EVAP_RATE:
+                ("Evaporation Rate", self._unit_labels[shared_enum.BaseUnits.EVAP_RATE])
+        }
+
+        self._build_pollut_metadata(output_handle)
+
+
+    def get_attribute_metadata(self, attribute):
+        '''
+        Takes an attribute enum and returns the name and units in a tuple.
+        '''
+        return self._metadata[attribute]
+
+
+# Units of Measurement
+#
+# Units                  US Customary                           SI Metric
+#    AREA_SUBCATCH          acres                 ac               hectares            ha
+#    AREA_STOR              square feet           sq ft            square meters       sq m
+#    AREA_POND              square feet           sq ft            square meters       sq m
+#    CAP_SUC                inches                in               millimeters         mm
+#    CONC                   milligrams/liter      mg/L             milligrams/liter    mg/L
+#                           micrograms/liter      ug/L             micrograms/liter    ug/L
+#                           counts/liter          Count/L          counts/liter        Count/L
+#    INFIL_DECAY            1/hours               1/hrs            1/hours             1/hrs
+#    POLLUT_DECAY           1/days                1/days           1/days              1/days
+#    DEPRES_STOR            inches                in               millimeters         mm
+#    DEPTH                  feet                  ft               meters              m
+#    DIAM                   feet                  ft               meters              m
+#    DISC_COEFF_ORIF        dimensionless         dimless          dimensionless       dimless
+#    DISC_COEFF_WEIR        CFS/foot^n            CFS/ft^n         CMS/meter^n         CMS/m^n
+#    ELEV                   feet                  ft               meters              m
+#    EVAP_RATE              inches/day            in/day           millimeters/day     mm/day
+#    FLOW_RATE              cubic feet/sec        CFS              cubic meter/sec     CMS
+#                           gallons/minute        GPM              liter/sec           LPS
+#                           million gallons/day   MGD              million liter/day   MLD
+#    HEAD                   feet                  ft               meters              m
+#    HYD_CONDUCT            inches/hour           in/hr            millimeters/hour    mm/hr
+#    INFIL_RATE             inches/hour           in/hr            millimeters/hour    mm/hr
+#    LEN                    feet                  ft               meters              m
+#    MANN_N                 seconds/meter^1/3     sec/m^1/3        seconds/meter^1/3   sec/m^1/3
+#    POLLUT_BUILDUP         mass/length           mass/len         mass/length         mass/len
+#                           mass/acre             mass/ac          mass/hectare        mass/ha
+#    RAIN_INTENSITY         inches/hour           in/hr            millimeters/hour    mm/hr
+#    RAIN_VOLUME            inches                in               millimeters         mm
+#    SLOPE_SUBCATCH         percent               percent          percent             percent
+#    SLOPE_XSEC             rise/run              rise/run         rise/run            rise/run
+#    STREET_CLEAN_INT       days                  days             days                days
+#    VOLUME                 cubic feet            cu ft            cubic meters        cu m
+#    WIDTH                  feet                  ft               meters              m
diff --git a/swmm-toolkit/src/swmm/toolkit/output_rename.i b/swmm-toolkit/src/swmm/toolkit/output_rename.i
new file mode 100644
index 00000000..b3566514
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/output_rename.i
@@ -0,0 +1,36 @@
+/*
+ *  output_rename.i - Rename mapping for swmm-output
+ *
+ *  Created:    2/12/2020
+ *
+ *  Author:     See AUTHORS
+ *
+*/
+
+
+// RENAME FUNCTIONS ACCORDING TO PEP8
+%rename(init)                   SMO_init;
+%rename(open)                   SMO_open;
+%rename(close)                  SMO_close;
+%rename(get_version)            SMO_getVersion;
+%rename(get_proj_size)          SMO_getProjectSize;
+
+%rename(get_units)              SMO_getUnits;
+%rename(get_start_date)         SMO_getStartDate;
+%rename(get_times)              SMO_getTimes;
+%rename(get_elem_name)          SMO_getElementName;
+
+%rename(get_subcatch_series)    SMO_getSubcatchSeries;
+%rename(get_node_series)        SMO_getNodeSeries;
+%rename(get_link_series)        SMO_getLinkSeries;
+%rename(get_system_series)      SMO_getSystemSeries;
+
+%rename(get_subcatch_attribute) SMO_getSubcatchAttribute;
+%rename(get_node_attribute)     SMO_getNodeAttribute;
+%rename(get_link_attribute)     SMO_getLinkAttribute;
+%rename(get_system_attribute)   SMO_getSystemAttribute;
+
+%rename(get_subcatch_result)    SMO_getSubcatchResult;
+%rename(get_node_result)        SMO_getNodeResult;
+%rename(get_link_result)        SMO_getLinkResult;
+%rename(get_system_result)      SMO_getSystemResult;
diff --git a/swmm-toolkit/src/swmm/toolkit/shared_enum.py b/swmm-toolkit/src/swmm/toolkit/shared_enum.py
new file mode 100644
index 00000000..089cc039
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/shared_enum.py
@@ -0,0 +1,731 @@
+#
+#  shared_enum.py -
+#
+#  Created:   February 21, 2020
+#  Updated:
+#
+#  Author:    See AUTHORS
+#
+
+from aenum import Enum, IntEnum
+
+
+class UnitSystem(Enum, start=0):
+    """SWMM Unit System enum class. 
+
+    .. rubric:: Enum Members
+
+    ================ ====================
+    :attr:`~US`      US Traditional units
+    :attr:`~SI`      SI (metric) units
+    ================ ====================
+    """
+    US
+    SI
+
+
+class FlowUnits(Enum, start=0):
+    """SWMM Flow Units enum class.
+
+    .. rubric:: Enum Members
+
+    ================ =======================
+    :attr:`~CFS`     cubic feet per second
+    :attr:`~GPM`     gallons per minute
+    :attr:`~MGD`     million gallons per day
+    :attr:`~CMS`     cubic meters per second
+    :attr:`~LPS`     liters per second
+    :attr:`~MLD`     million liters per day
+    ================ =======================
+    """
+    CFS
+    GPM
+    MGD
+    CMS
+    LPS
+    MLD
+
+
+class ConcUnits(Enum, start=0):
+    """SWMM Concentration Units enum class.
+
+    .. rubric:: Enum Members
+
+    ================ ====================
+    :attr:`~MG`      Milligrams per liter
+    :attr:`~UG`      Micrograms per liter
+    :attr:`~COUNT`   Counts per liter
+    :attr:`~NONE`    None
+    ================ ====================
+    """
+    MG,
+    UG,
+    COUNT
+    NONE
+
+
+class BaseUnits(Enum, start = 1):
+    """Base Units enum class
+
+    .. rubric:: Enum Members
+
+    =================== ==================
+    :attr:`~RAIN_INT`   rainfall intensity
+    :attr:`~SNOW_DEPTH` rainfall depth  
+    :attr:`~EVAP_RATE`  evaporation rate
+    :attr:`~INFIL_RATE` infiltration rate
+    :attr:`~FLOW_RATE`  flow rate
+    :attr:`~ELEV`       elevation
+    :attr:`~PERCENT`    percent
+    :attr:`~CONCEN`     concentration
+    :attr:`~HEAD`       head
+    :attr:`~VOLUME`     volume
+    :attr:`~VELOCITY`   velocity
+    :attr:`~TEMP`       temperature
+    :attr:`~UNITLESS`   unitless quantity
+    :attr:`~NONE`       none
+    =================== ==================
+    """
+    RAIN_INT
+    SNOW_DEPTH
+    EVAP_RATE
+    INFIL_RATE
+    FLOW_RATE
+    ELEV
+    PERCENT
+    CONCEN
+    HEAD
+    VOLUME
+    VELOCITY
+    TEMP
+    UNITLESS
+    NONE
+
+
+class ObjectType(Enum, start=0):
+    """SWMM Object Types enum class.
+
+    .. rubric:: Enum Members
+
+    ==================== ===============================
+    :attr:`~GAGE`        rain gage 
+    :attr:`~SUBCATCH`    subcatchment
+    :attr:`~NODE`        conveyance system node
+    :attr:`~LINK`        conveyance system link
+    :attr:`~POLLUT`      pollutant
+    :attr:`~LANDUSE`     land use category
+    :attr:`~TIMEPATTERN` dry weather flow time pattern
+    :attr:`~CURVE`       generic table of values
+    :attr:`~TSERIES`     generic time series of values
+    :attr:`~CONTROL`     conveyance system control rules
+    :attr:`~TRANSECT`    irregular channel cross-section
+    :attr:`~AQUIFER`     groundwater aquifer
+    :attr:`~UNITHYD`     RDII unit hydrograph
+    :attr:`~SNOWMELT`    snowmelt parameter set
+    :attr:`~SHAPE`       custom conduit shape
+    :attr:`~LID`         LID treatment unit
+    ==================== ===============================
+    """
+    GAGE
+    SUBCATCH
+    NODE
+    LINK
+    POLLUT
+    LANDUSE
+    TIMEPATTERN
+    CURVE
+    TSERIES
+    CONTROL
+    TRANSECT
+    AQUIFER
+    UNITHYD
+    SNOWMELT
+    SHAPE
+    LID
+
+
+class NodeType(Enum, start=0):
+    """Node Sub-Types enum class.
+
+    :attr:`~JUNCTION`
+    :attr:`~OUTFALL`
+    :attr:`~STORAGE`
+    :attr:`~DIVIDER`
+    """
+    JUNCTION
+    OUTFALL
+    STORAGE
+    DIVIDER
+
+
+class LinkType(Enum, start=0):
+    """Link Sub-Types enum class.
+
+    :attr:`~CONDUIT`
+    :attr:`~PUMP`
+    :attr:`~ORIFICE`
+    :attr:`~WEIR`
+    :attr:`~OUTLET`
+    """
+    CONDUIT
+    PUMP
+    ORIFICE
+    WEIR
+    OUTLET
+
+
+class LinkDirection(Enum):
+    """Link Direction enum class.
+
+    :attr:`~UPSTREAM_TO_DOWNSTREAM`
+    :attr:`~DOWNSTREAM_TO_UPSTREAM`
+    """
+    UPSTREAM_TO_DOWNSTREAM = 1
+    DOWNSTREAM_TO_UPSTREAM = -1
+    
+
+class ElementType(IntEnum, start = 0):
+    """SWMM Element Types enum class.
+
+    :attr:`~SUBCATCH`
+    :attr:`~NODE`
+    :attr:`~LINK`
+    :attr:`~SYSTEM`
+    :attr:`~POLLUT`
+    """
+    SUBCATCH
+    NODE
+    LINK
+    SYSTEM
+    POLLUT
+
+
+class Time(Enum, start = 0):
+    """Time enum class.
+
+    :attr:`~REPORT_STEP`
+    :attr:`~NUM_PERIODS`
+    """
+    REPORT_STEP
+    NUM_PERIODS
+
+
+class SubcatchAttribute(Enum, start = 0):
+    """Subcatchment Attributes enum class.
+
+    .. rubric:: Enum Members
+
+    ========================= ===============================
+    :attr:`~RAINFALL`         rainfall intensity
+    :attr:`~SNOW_DEPTH`       snow depth
+    :attr:`~EVAP_LOSS`        evaporation loss 
+    :attr:`~INFIL_LOSS`       infiltration loss
+    :attr:`~RUNOFF_RATE`      runoff flow rate
+    :attr:`~GW_OUTFLOW_RATE`  groundwater flow rate to node
+    :attr:`~GW_TABLE_ELEV`    elevation of saturated gw table
+    :attr:`~SOIL_MOISTURE`    soil moisture
+    :attr:`~POLLUT_CONC_0`    pollutant washoff concentration
+    ========================= ===============================
+    """    
+    RAINFALL
+    SNOW_DEPTH
+    EVAP_LOSS
+    INFIL_LOSS
+    RUNOFF_RATE
+    GW_OUTFLOW_RATE
+    GW_TABLE_ELEV
+    SOIL_MOISTURE
+    POLLUT_CONC_0
+
+
+class NodeAttribute(Enum, start = 0):
+    """Node Attribute enum class.
+
+    .. rubric:: Enum Members
+
+    ========================= ===============================
+    :attr:`~INVERT_DEPTH`     water depth above invert
+    :attr:`~HYDRAULIC_HEAD`   hydraulic head
+    :attr:`~PONDED_VOLUME`    volume stored and ponded
+    :attr:`~LATERAL_INFLOW`   lateral inflow rate
+    :attr:`~TOTAL_INFLOW`     total inflow rate
+    :attr:`~FLOODING_LOSSES`  overflow rate
+    :attr:`~POLLUT_CONC_0`    concentration of each pollutant
+    ========================= ===============================
+    """
+    INVERT_DEPTH
+    HYDRAULIC_HEAD
+    PONDED_VOLUME
+    LATERAL_INFLOW
+    TOTAL_INFLOW
+    FLOODING_LOSSES
+    POLLUT_CONC_0
+
+
+class LinkAttribute(Enum, start = 0):
+    """Link Attribute enum class.
+
+    .. rubric:: Enum Members
+
+    ====================== ===============================
+    :attr:`~FLOW_RATE`     flow rate
+    :attr:`~FLOW_DEPTH`    flow depth
+    :attr:`~FLOW_VELOCITY` flow velocity
+    :attr:`~FLOW_VOLUME`   link volume
+    :attr:`~CAPACITY`      ratio of area to full area
+    :attr:`~POLLUT_CONC_0` concentration of each pollutant
+    ====================== ===============================
+    """
+    FLOW_RATE
+    FLOW_DEPTH
+    FLOW_VELOCITY
+    FLOW_VOLUME
+    CAPACITY
+    POLLUT_CONC_0
+
+
+class SystemAttribute(Enum, start = 0):
+    """System Attribute enum class.
+
+    .. rubric:: Enum Members
+
+    ============================= ====================
+    :attr:`~AIR_TEMP`             air temperature
+    :attr:`~RAINFALL`             rainfall intensity
+    :attr:`~SNOW_DEPTH`           snow depth
+    :attr:`~EVAP_INFIL_LOSS`      infiltration
+    :attr:`~RUNOFF_FLOW`          runoff
+    :attr:`~DRY_WEATHER_INFLOW`   dry weather inflow
+    :attr:`~GW_INFLOW`            ground water inflow
+    :attr:`~RDII_INFLOW`          RDII inflow
+    :attr:`~DIRECT_INFLOW`        external inflow
+    :attr:`~TOTAL_LATERAL_INFLOW` total lateral inflow
+    :attr:`~FLOOD_LOSSES`         flooding outflow
+    :attr:`~OUTFALL_FLOWS`        outfall outflow
+    :attr:`~VOLUME_STORED`        storage volume
+    :attr:`~EVAP_RATE`            evaporation
+    ============================= ====================
+    """
+    AIR_TEMP
+    RAINFALL
+    SNOW_DEPTH
+    EVAP_INFIL_LOSS
+    RUNOFF_FLOW
+    DRY_WEATHER_INFLOW
+    GW_INFLOW
+    RDII_INFLOW
+    DIRECT_INFLOW
+    TOTAL_LATERAL_INFLOW
+    FLOOD_LOSSES
+    OUTFALL_FLOWS
+    VOLUME_STORED
+    EVAP_RATE
+
+
+#
+# Solver Toolkit API Enums
+#    
+class TimeProperty(Enum, start=0):
+    """Time Property enum class.
+
+    :attr:`~START_DATE`
+    :attr:`~END_DATE`
+    :attr:`~REPORT_DATE`
+    """
+    START_DATE
+    END_DATE
+    REPORT_DATE
+
+
+class UnitProperty(Enum, start=0):
+    """Unit Property enum class.
+
+    :attr:`~SYSTEM_UNIT`
+    :attr:`~FLOW_UNIT`
+    """
+    SYSTEM_UNIT
+    FLOW_UNIT
+
+
+class SimOption(Enum, start=0):
+    """Simulation Option enum class.
+
+    :attr:`~ALLOW_POND`
+    :attr:`~SKIP_STEADY`
+    :attr:`~IGNORE_RAIN`
+    :attr:`~IGNORE_RDII`
+    :attr:`~IGNORE_SNOW`
+    :attr:`~IGNORE_GW`
+    :attr:`~IGNORE_ROUTE`
+    :attr:`~IGNORE_ROUTE_QUALITY`
+    """
+    ALLOW_POND
+    SKIP_STEADY
+    IGNORE_RAIN
+    IGNORE_RDII
+    IGNORE_SNOW
+    IGNORE_GW
+    IGNORE_ROUTE
+    IGNORE_ROUTE_QUALITY
+
+
+class SimSetting(Enum, start=0):
+    """Simulation Settings enum class.
+
+    :attr:`~ROUTE_STEP`
+    :attr:`~MIN_ROUTE_STEP`
+    :attr:`~LENGTH_STEP`
+    :attr:`~START_DRY_DAYS`
+    :attr:`~COURANT_FACTOR`
+    :attr:`~MIN_SURFACE_AREA`
+    :attr:`~MIN_SLOPE`
+    :attr:`~RUNOFF_ERROR`
+    :attr:`~GW_ERROR`
+    :attr:`~FLOW_ERROR`
+    :attr:`~QUALITY_ERROR`
+    :attr:`~HEAD_TOLERANCE`
+    :attr:`~SYSTEM_FLOW_TOLERANCE`
+    :attr:`~LATERAL_FLOW_TOLERANCE`
+    """
+    ROUTE_STEP
+    MIN_ROUTE_STEP
+    LENGTH_STEP
+    START_DRY_DAYS
+    COURANT_FACTOR
+    MIN_SURFACE_AREA
+    MIN_SLOPE
+    RUNOFF_ERROR
+    GW_ERROR
+    FLOW_ERROR
+    QUALITY_ERROR
+    HEAD_TOLERANCE
+    SYSTEM_FLOW_TOLERANCE
+    LATERAL_FLOW_TOLERANCE
+
+
+class NodeProperty(Enum, start=0):
+    """Node Property enum class.
+
+    :attr:`~INVERT_ELEVATION`
+    :attr:`~FULL_DEPTH`
+    :attr:`~SURCHARGE_DEPTH`
+    :attr:`~POND_AREA`
+    :attr:`~INITIAL_DEPTH`
+    """
+    INVERT_ELEVATION
+    FULL_DEPTH
+    SURCHARGE_DEPTH
+    POND_AREA
+    INITIAL_DEPTH
+
+
+class NodeResult(Enum, start=0):
+    """Node Result enum class.
+
+    :attr:`~TOTAL_INFLOW`
+    :attr:`~TOTAL_OUTFLOW`
+    :attr:`~LOSSES`
+    :attr:`~VOLUME`
+    :attr:`~FLOOD`
+    :attr:`~DEPTH`
+    :attr:`~HEAD`
+    :attr:`~LATERAL_INFLOW`
+    """
+    TOTAL_INFLOW
+    TOTAL_OUTFLOW
+    LOSSES
+    VOLUME
+    FLOOD
+    DEPTH
+    HEAD
+    LATERAL_INFLOW
+
+
+class NodePollutant(Enum, start=0):
+    """Node Pollutant enum class.
+
+    :attr:`~QUALITY`
+    """
+    QUALITY
+
+
+class LinkProperty(Enum, start=0):
+    """Link Property enum class.
+
+    :attr:`~OFFSET_1`
+    :attr:`~OFFSET_2`
+    :attr:`~INITIAL_FLOW`
+    :attr:`~FLOW_LIMIT`
+    :attr:`~INLET_LOSS`
+    :attr:`~OUTLET_LOSS`
+    :attr:`~AVERAGE_LOSS`
+    """
+    OFFSET_1
+    OFFSET_2
+    INITIAL_FLOW
+    FLOW_LIMIT
+    INLET_LOSS
+    OUTLET_LOSS
+    AVERAGE_LOSS
+
+
+class LinkResult(Enum, start=0):
+    """Link Result enum class.
+
+    :attr:`~FLOW`
+    :attr:`~DEPTH`
+    :attr:`~VOLUME`
+    :attr:`~US_SURFACE_AREA`
+    :attr:`~DS_SURFACE_AREA`
+    :attr:`~SETTING`
+    :attr:`~TARGET_SETTING`
+    :attr:`~FROUDE`
+    """
+    FLOW
+    DEPTH
+    VOLUME
+    US_SURFACE_AREA
+    DS_SURFACE_AREA
+    SETTING
+    TARGET_SETTING
+    FROUDE
+
+
+class LinkPollutant(Enum, start=0):
+    """Link Pollutant enum class.
+
+    :attr:`~QUALITY`
+    :attr:`~TOTAL_LOAD`
+    """
+    QUALITY
+    TOTAL_LOAD
+
+    
+class SubcatchProperty(Enum, start=0):
+    """Subcatchment Property enum class.
+
+    :attr:`~WIDTH`
+    :attr:`~AREA`
+    :attr:`~IMPERVIOUS_FRACTION`
+    :attr:`~SLOPE`
+    :attr:`~CURB_LENGTH`
+    """
+    WIDTH
+    AREA
+    IMPERVIOUS_FRACTION
+    SLOPE
+    CURB_LENGTH
+
+
+class SubcatchResult(Enum, start=0):
+    """Subcatchment Result enum class.
+
+    :attr:`~RAIN`
+    :attr:`~EVAPORATION`
+    :attr:`~INFILTRATION`
+    :attr:`~RUNON`
+    :attr:`~RUNOFF`
+    :attr:`~SNOW`
+    """
+    RAIN
+    EVAPORATION
+    INFILTRATION
+    RUNON
+    RUNOFF
+    SNOW
+
+
+class SubcatchPollutant(Enum, start=0):
+    """Subcatchment Pollutant enum class.
+
+    :attr:`~BUILD_UP`
+    :attr:`~CONCENTRATION`
+    :attr:`~QUALITY`
+    :attr:`~TOTAL_LOAD`
+    """
+    BUILD_UP
+    CONCENTRATION
+    QUALITY
+    TOTAL_LOAD
+
+    
+class LidUsageProperty(Enum, start=0):
+    """LID Usage Property enum class.
+
+    :attr:`~UNIT_AREA`
+    :attr:`~TOP_WIDTH`
+    :attr:`~BOTTOM_WIDTH`
+    :attr:`~INITIAL_SATURATION`
+    :attr:`~FROM_IMPERVIOUS`
+    :attr:`~FROM_PERVIOUS`
+    """
+    UNIT_AREA
+    TOP_WIDTH
+    BOTTOM_WIDTH
+    INITIAL_SATURATION
+    FROM_IMPERVIOUS
+    FROM_PERVIOUS
+
+
+class LidUsageOption(Enum, start=0):
+    """LID Usage Option enum class.
+
+    :attr:`~INDEX`
+    :attr:`~NUMBER`
+    :attr:`~TO_PERV`
+    :attr:`~DRAIN_SUBCATCH`
+    :attr:`~DRAIN_NODE`
+    """
+    INDEX
+    NUMBER
+    TO_PERV
+    DRAIN_SUBCATCH
+    DRAIN_NODE
+
+
+class LidLayer(Enum, start=0):
+    """LID Layer enum class.
+
+    :attr:`~SURFACE`
+    :attr:`~SOIL`
+    :attr:`~STORAGE`
+    :attr:`~PAVEMENT`
+    :attr:`~DRAIN`
+    :attr:`~DRAIN_MAT`
+    """
+    SURFACE
+    SOIL
+    STORAGE
+    PAVEMENT
+    DRAIN
+    DRAIN_MAT
+
+    
+class LidLayerProperty(Enum, start=0):
+    """LID Layer Property enum class.
+
+    :attr:`~THICKNESS`
+    :attr:`~VOID_FRACTION`
+    :attr:`~ROUGHNESS`
+    :attr:`~SURFACE_SLOPE`
+    :attr:`~SIDE_SLOP`
+    :attr:`~ALPHA`
+    :attr:`~POROSITY`
+    :attr:`~FIELD_CAPACITY`
+    :attr:`~WILTING_POINT`
+    :attr:`~SUCTION_HEAD`
+    :attr:`~K_SATURATION`
+    :attr:`~K_SLOPE`
+    :attr:`~CLOG_FACTOR`
+    :attr:`~IMPERVIOUS_FRACTION`
+    :attr:`~DRAIN_COEFFICIENT`
+    :attr:`~DRAIN_EXPONENT`
+    :attr:`~DRAIN_OFFSET`
+    :attr:`~DRAIN_DELAY`
+    :attr:`~DRAIN_HEAD_OPEN`
+    :attr:`~DRAIN_HEAD_CLOSE`
+    :attr:`~DRAIN_CURVE`
+    :attr:`~DRAIN_REGEN_DAYS`
+    :attr:`~DRAIN_REGEN_DEGREE`
+    """
+    THICKNESS
+    VOID_FRACTION
+    ROUGHNESS
+    SURFACE_SLOPE
+    SIDE_SLOP
+    ALPHA
+    POROSITY
+    FIELD_CAPACITY
+    WILTING_POINT
+    SUCTION_HEAD
+    K_SATURATION
+    K_SLOPE
+    CLOG_FACTOR
+    IMPERVIOUS_FRACTION
+    DRAIN_COEFFICIENT
+    DRAIN_EXPONENT
+    DRAIN_OFFSET
+    DRAIN_DELAY
+    DRAIN_HEAD_OPEN
+    DRAIN_HEAD_CLOSE
+    DRAIN_CURVE
+    DRAIN_REGEN_DAYS
+    DRAIN_REGEN_DEGREE
+
+
+class LidResult(Enum, start=0):
+    """LID Result enum class.
+
+    :attr:`~INFLOW`
+    :attr:`~EVAPORATION`
+    :attr:`~INFILTRATION`
+    :attr:`~SURFACE_FLOW`
+    :attr:`~DRAIN_FLOW`
+    :attr:`~INITIAL_VOLUME`
+    :attr:`~FINAL_VOLUME`
+    :attr:`~SURFACE_DEPTH`
+    :attr:`~PAVEMENT_DEPTH`
+    :attr:`~SOIL_MOISTURE`
+    :attr:`~STORAGE_DEPTH`
+    :attr:`~DRY_TIME`
+    :attr:`~OLD_DRAIN_FLOW`
+    :attr:`~NEW_DRAIN_FLOW`
+    :attr:`~PERVIOUS_AREA`
+    :attr:`~FLOW_TO_PERVIOUS_AREA`
+    :attr:`~EVAPORATE_RATE`
+    :attr:`~NATIVE_INFILTRATION`
+    :attr:`~SURFACE_INFLOW`
+    :attr:`~SURFACE_INFILTRATION`
+    :attr:`~SURFACE_EVAPORATION`
+    :attr:`~SURFACE_OUTFLOW`
+    :attr:`~PAVEMENT_EVAPORATION`
+    :attr:`~PAVEMENT_PERCOLATION`
+    :attr:`~SOIL_EVAPORATION`
+    :attr:`~SOIL_PERCOLATION`
+    :attr:`~STORAGE_INFLOW`
+    :attr:`~STORAGE_EXFILTRATION`
+    :attr:`~STORAGE_EVAPORATION`
+    :attr:`~STORAGE_DRAIN`
+    """
+    INFLOW
+    EVAPORATION
+    INFILTRATION
+    SURFACE_FLOW
+    DRAIN_FLOW
+    INITIAL_VOLUME
+    FINAL_VOLUME
+    SURFACE_DEPTH
+    PAVEMENT_DEPTH
+    SOIL_MOISTURE
+    STORAGE_DEPTH
+    DRY_TIME
+    OLD_DRAIN_FLOW
+    NEW_DRAIN_FLOW
+    PERVIOUS_AREA
+    FLOW_TO_PERVIOUS_AREA
+    EVAPORATE_RATE
+    NATIVE_INFILTRATION
+    SURFACE_INFLOW
+    SURFACE_INFILTRATION
+    SURFACE_EVAPORATION
+    SURFACE_OUTFLOW
+    PAVEMENT_EVAPORATION
+    PAVEMENT_PERCOLATION
+    SOIL_EVAPORATION
+    SOIL_PERCOLATION
+    STORAGE_INFLOW
+    STORAGE_EXFILTRATION
+    STORAGE_EVAPORATION
+    STORAGE_DRAIN
+
+    
+class RainResult(Enum, start=0):
+    """Rain Result enum class.
+
+    :attr:`~TOTAL`
+    :attr:`~RAINFALL`
+    :attr:`~SNOWFALL`
+    """
+    TOTAL
+    RAINFALL
+    SNOWFALL
diff --git a/swmm-toolkit/src/swmm/toolkit/solver.i b/swmm-toolkit/src/swmm/toolkit/solver.i
new file mode 100644
index 00000000..e3af2d87
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/solver.i
@@ -0,0 +1,215 @@
+/*
+ *  solver.i - SWIG interface description file for swmm-solver
+ *
+ *  Created:    7/2/2018
+ *  Updated:    8/17/2020
+ *
+ *  Author:     See AUTHORS
+ * 
+*/
+
+
+%include "typemaps.i"
+%include "cstring.i"
+
+
+/* Docstrings for module */
+%include "solver_docs.i"
+
+%module(package="swmm.toolkit", docstring=SOLVER_MODULE_DOCS) solver
+%{
+#define SWIG_FILE_WITH_INIT
+
+#include "swmm5.h"
+#include "toolkit.h"
+#include "toolkit_enums.h"
+#include "toolkit_structs.h"
+%}
+
+/* RENAME FUNCTIONS PYTHON STYLE */
+%include "solver_rename.i"
+
+%include "stats_typemaps.i";
+
+
+
+/* TYPEMAP FOR IGNORING INT ERROR CODE RETURN VALUE */
+%typemap(out) int {
+    $result = Py_None;
+    Py_INCREF($result);
+}
+
+
+%apply int *OUTPUT {
+    int *index,
+    int *value,
+    int *count, 
+    int *node1, 
+    int *node2,
+    int *out_index,
+    int *condition,
+    int *year,
+    int *month,
+    int *day,
+    int *hour,
+    int *minute,
+    int *second
+}
+
+
+%apply double *OUTPUT {
+    double *elapsedTime,
+    double *value,
+    double *result
+}
+
+
+%apply float *OUTPUT {
+    float *runoffErr,
+    float *flowErr, 
+    float *qualErr
+}
+
+
+%apply signed char *OUTPUT {
+    signed char *value
+}
+
+
+%apply char *OUTPUT {
+    char *condition
+}
+
+
+%cstring_output_allocate(char **OUTCHAR, swmm_freeMemory(*$1));
+
+%apply char **OUTCHAR {
+    char **id,
+    char **major, 
+    char **minor, 
+    char **patch
+}
+
+
+/* TYPEMAPS FOR MEMORY MANAGEMNET OF DOUBLE ARRAYS */
+%typemap(in, numinputs=0) double **double_out (double *temp) {
+    $1=&temp;
+}
+%typemap(in, numinputs=0) int *int_dim (int temp) {
+    $1=&temp;
+}
+%typemap(argout) (double **double_out, int *int_dim) {
+    if (*$1) {
+      PyObject *o = PyList_New(*$2);
+      for(int i=0; i<*$2; i++) {
+        PyList_SetItem(o, i, PyFloat_FromDouble(temp$argnum[i]));
+      }
+      $result = SWIG_Python_AppendOutput($result, o);
+      swmm_freeMemory(*$1);
+    }
+}
+
+%apply double **double_out {
+    double **pollutArray
+}
+%apply int *int_dim {
+    int *length
+}
+%apply (double **double_out, int *int_dim) {
+    (double **pollutArray, int *length)
+}
+
+
+/* TYPEMAP FOR ENUMERATED TYPE INPUT ARGUMENTS */
+%typemap(in) EnumTypeIn {
+    int value = 0;
+    if (PyObject_HasAttrString($input, "value")) {
+        PyObject *o = PyObject_GetAttrString($input, "value");
+        SWIG_AsVal_int(o, &value);
+    }
+    else if (PyLong_Check($input)) {
+        SWIG_AsVal_int($input, &value);
+    }
+    $1 = ($1_basetype)(value);
+}
+%apply EnumTypeIn {
+    SM_ObjectType,
+    SM_NodeType,
+    SM_LinkType,
+    SM_TimePropety,
+    SM_Units,
+    SM_SimOption,
+    SM_SimSetting,
+    SM_NodeProperty,
+    SM_LinkProperty,
+    SM_SubcProperty,
+    SM_NodeResult,
+    SM_NodePollut,
+    SM_LinkResult,
+    SM_LinkPollut,
+    SM_SubcResult,
+    SM_SubcPollut,
+    SM_GagePrecip,
+    SM_LidLayer,
+    SM_LidLayerProperty,
+    SM_LidUProperty,
+    SM_LidUOptions,
+    SM_LidResult
+}
+
+
+/* TYPEMAPS FOR ENUMERATED TYPE OUTPUT ARGUMENTS */
+%typemap(in, numinputs=0) EnumTypeOut * (int temp) {
+    $1 = ($1_type)&temp;
+}
+%typemap(argout) EnumTypeOut * {
+    char *temp = "$1_basetype";
+    PyObject *module = PyImport_ImportModule("swmm.toolkit.shared_enum");
+    PyObject *function = PyDict_GetItemString(PyModule_GetDict(module), (temp + 3));
+    if (PyCallable_Check(function)) {
+        PyObject *enum_out = PyObject_CallFunction(function, "i", *$1);
+        %append_output(enum_out);
+    }
+}
+%apply EnumTypeOut * {
+    SM_ObjectType *,
+    SM_NodeType *,
+    SM_LinkType *
+}
+
+
+/* INSERTS CUSTOM EXCEPTION HANDLING IN WRAPPER */
+%exception
+{
+    $function
+    if (result > 0) {
+        char* errorMsg = NULL;
+        int errorCode = 0;
+        errorCode = swmm_getAPIError(result, &errorMsg);
+        if (errorCode == 0) {
+            PyErr_SetString(PyExc_Exception, errorMsg);
+        }
+        swmm_freeMemory(errorMsg);
+        SWIG_fail;
+    }
+}
+
+// CANONICAL API
+%ignore swmm_getError;
+%ignore swmm_getWarnings;
+%ignore swmm_IsOpenFlag;
+%ignore swmm_IsStartedFlag;
+
+%include "swmm5.h"
+
+
+// TOOLKIT API
+%ignore swmm_run_cb;
+%ignore swmm_getAPIError;
+%ignore swmm_getObjectIndex;
+%ignore swmm_freeMemory;
+
+%include "toolkit.h"
+
+
+%exception;
diff --git a/swmm-toolkit/src/swmm/toolkit/solver_docs.i b/swmm-toolkit/src/swmm/toolkit/solver_docs.i
new file mode 100644
index 00000000..4e130e20
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/solver_docs.i
@@ -0,0 +1,1258 @@
+/*
+ *  solver_docs.i - Documentation for swmm-solver
+ *
+ *  Created:    12/10/2020
+ *  
+ *  Author:     See AUTHORS
+ *
+*/
+
+
+%define SOLVER_MODULE_DOCS
+"Solver Module
+"
+%enddef
+
+
+// PUBLIC STRUCTS
+%feature("autodoc", 
+"Node Statistics
+
+Attributes
+----------
+avgDepth: double
+    average node depth (level)
+maxDepth: double
+    max node depth (level) (from routing step)
+maxDepthDate: DateTime
+    date of maximum depth
+maxRptDepth: double
+    max node depth (level) (from reporting step)
+volFlooded: double
+    total volume flooded (volume)
+timeFlooded: double
+    total time flooded
+timeSurcharged: double
+    total time surcharged
+timeCourantCritical: double
+    total time courant critical
+totLatFlow: double
+    total lateral inflow (volume)
+maxLatFlow: double
+    maximum lateral inflow (flowrate)
+maxInflow: double
+    maximum total inflow (flowrate)
+maxOverflow: double
+    maximum flooding (flowrate)
+maxPondedVol: double
+    maximum ponded volume (volume)
+maxInflowDate: DateTime
+    date of maximum inflow
+maxOverflowDate: DateTime
+    date of maximum overflow
+" 
+) SM_NodeStats();
+
+
+%feature("autodoc", 
+"Storage Statistics
+
+Attributes
+----------
+initVol: double
+    initial volume (volume)
+avgVol: double
+    average volume (volume) (from routing step)
+maxVol: double
+    maximum volume (volume) (from routing step)
+maxFlow: double
+    maximum total inflow (flowrate) (from routing step)
+evapLosses: double
+    evaporation losses (volume)
+exfilLosses: double
+    exfiltration losses (volume)
+maxVolDate: DateTime
+    date of maximum volume
+" 
+) SM_StorageStats();
+
+
+%feature("autodoc", 
+"Outfall Statistics
+
+Attributes
+----------
+avgFlow: double
+    average flow (flowrate)
+maxFlow: double
+    maximum flow (flowrate) (from routing step)
+totalLoad: double *
+    total pollutant load (mass)
+totalPeriods: double
+    total simulation steps (from routing step)
+" 
+) SM_OutfallStats(int);
+
+
+%feature("autodoc", 
+"Link Statistics
+
+.. rubric:: 'Flow Classes'
+
+===========   ===========================
+Flow Class    Description                
+===========   ===========================
+DRY           dry conduit
+UP_DRY        upstream end is dry
+DN_DRY        downstream end is dry
+SUBCRITICAL   sub-critical flow
+SUPCRITICAL   super-critical flow
+UP_CRITICAL   free-fall at upstream end
+DN_CRITICAL   free-fall at downstream end
+===========   ===========================
+
+Attributes
+----------
+maxFlow: double
+    maximum flow (flowrate) (from routing step)
+maxFlowDate: double
+    date of maximum flowrate
+maxVeloc: double
+    maximum velocity (from routing step)
+maxDepth: double
+    maximum depth (level)
+timeNormalFlow: double
+    time in normal flow
+timeInletControl: double
+    time under inlet control
+timeSurcharged: double
+    time surcharged
+timeFullUpstream: double
+    time full upstream
+timeFullDnstream: double
+    time full downstream
+timeFullFlow: double
+    time full flow
+timeCapacityLimited: double
+    time capacity limited
+timeInFlowClass: double[7]
+    time in flow class (See: 'Flow Classes')
+timeCourantCritical: double
+    time courant critical
+flowTurns: double
+    number of flow turns
+flowTurnSign: double
+    number of flow turns sign
+" 
+) SM_LinkStats();
+
+
+%feature("autodoc", 
+"Pump Statistics
+
+Attributes
+----------
+utilized: double
+    time utilized
+minFlow: double
+    minimum flowrate
+avgFlow: double
+    average flowrate
+maxFlow: double
+    maximum flowrate
+volume: double
+    total pumping volume (volume)
+energy: double
+    total energy demand
+offCurveLow: double
+    hysteresis low (off depth wrt curve)
+offCurveHigh: double
+    hysteresis high (on depth wrt curve)
+startUps: int
+    number of start ups
+totalPeriods: int
+    total simulation steps (from routing step)
+" 
+) SM_PumpStats();
+
+
+%feature("autodoc", 
+"Subcatchment Statistics
+
+Attributes
+----------
+precip: double
+    total precipication (length)
+runon: double
+    total runon (volume)
+evap: double
+    total evaporation (volume)
+infil: double
+    total infiltration (volume)
+runoff: double
+    total runoff (volume)
+maxFlow: double
+    maximum runoff rate (flowrate)
+" 
+) SM_SubcatchStats();
+
+
+%feature("autodoc", 
+"System Flow Routing Totals
+
+Attributes
+----------
+dwInflow: double
+    dry weather inflow
+wwInflow: double
+    wet weather inflow
+gwInflow: double
+    groundwater inflow
+iiInflow: double
+    RDII inflow
+exInflow: double
+    direct inflow
+flooding: double
+    internal flooding
+outflow: double
+    external outflow
+evapLoss: double
+    evaporation loss
+seepLoss: double
+    seepage loss
+reacted: double
+    reaction losses
+initStorage: double
+    initial storage volume
+finalStorage: double
+    final storage volume
+pctError: double
+    continuity error
+" 
+) SM_RoutingTotals();
+
+
+%feature("autodoc", 
+"System Runoff Totals
+
+Attributes
+----------
+rainfall: double
+    rainfall total (depth)
+evap: double
+    evaporation loss (volume)
+infil: double
+    infiltration loss (volume)
+runoff: double
+    runoff volume (volume)
+drains: double
+    LID drains (volume)
+runon: double
+    runon from outfalls (volume)
+initStorage: double
+    inital surface storage (depth)
+finalStorage: double
+    final surface storage (depth)
+initSnowCover: double
+    initial snow cover (depth)
+finalSnowCover: double
+    final snow cover (depth)
+snowRemoved: double
+    snow removal (depth)
+pctError: double
+    continuity error (%)
+" 
+) SM_RunoffTotals();
+
+
+// CANONICAL API
+%feature("autodoc", 
+"Opens SWMM input file, reads in network data, runs, and closes
+
+Parameters
+----------
+f1: char const *
+    pointer to name of input file (must exist)
+f2: char const *
+    pointer to name of report file (to be created)
+f3: char const *
+    pointer to name of binary output file (to be created)
+"
+) swmm_run;
+
+
+%feature("autodoc",
+"Opens SWMM input file & reads in network data
+
+Parameters
+----------
+f1: char const *
+    pointer to name of input file (must exist)
+f2: char const *
+    pointer to name of report file (to be created)
+f3: char const *
+    pointer to name of binary output file (to be created)
+"
+) swmm_open;
+
+
+%feature("autodoc",
+"Start SWMM simulation
+
+Parameters
+----------
+saveFlag: int
+    TRUE or FALSE to save timeseries to report file
+"
+) swmm_start;
+
+
+%feature("autodoc",
+"Step SWMM simulation forward
+
+Returns
+-------
+elapsedTime: double
+    elapsed simulation time [milliseconds]
+"
+) swmm_step;
+
+
+%feature("autodoc",
+"End SWMM simulation   
+"
+) swmm_end;
+
+
+%feature("autodoc",
+"Write text report file
+"
+) swmm_report;
+
+
+%feature("autodoc",
+"Get routing errors
+
+Returns
+-------
+runoffErr: float *
+    Runoff routing error
+flowErr: float * 
+    Flow routing error
+qualErr: float * 
+    Quality routing error
+"
+) swmm_getMassBalErr;
+
+
+%feature("autodoc",
+"Frees all memory and files used by SWMM
+"
+) swmm_close;
+
+
+%feature("autodoc",
+"Get Legacy SWMM version number
+
+Returns
+-------
+version: int
+    Version number
+"
+) swmm_getVersion;
+
+
+// TOOLKIT API
+%feature("autodoc",
+"Get full semantic version number info
+
+Returns
+-------
+major: char ** 
+    sematic version major number
+minor: char ** 
+    sematic version minor number
+patch: char ** 
+    sematic version patch number
+"
+) swmm_getVersionInfo;
+
+
+%feature("autodoc",
+"Finds the index of an object given its ID.
+
+Parameters
+----------
+type: SM_ObjectType
+    type An object type (see :ref: SM_ObjectType)
+id: char *
+    id The object ID
+
+Returns
+-------    
+index: int *
+    The objects index
+"
+) swmm_project_findObject;
+
+
+%feature("autodoc", 
+"Gets Object ID
+
+Parameters
+----------
+type: SM_ObjectType
+    type Option code (see :ref: SM_ObjectType)
+index: int
+    index of the Object
+
+Returns
+-------
+id: char **
+    The string ID of object.
+"
+) swmm_getObjectId;
+
+
+%feature("autodoc", 
+"Gets Object Count
+
+Parameters
+----------
+type: SM_ObjectType
+    Option code (see :ref: SM_ObjectType)
+
+Returns
+-------
+count: int *
+    Option value
+"
+) swmm_countObjects;
+
+
+%feature("autodoc", 
+"Get the simulation datetime information
+
+Parameters
+----------
+type: SM_TimePropety
+    The property type code (See :ref: SM_TimePropety)
+
+Returns
+-------
+year: int *
+    The year
+month: int *
+    The month
+day: int *
+    The day
+hour: int *
+    The hour
+minute: int *
+    The minute
+second: int *
+    The seconds
+"
+) swmm_getSimulationDateTime;
+
+
+%feature("autodoc", 
+"Get the current simulation datetime information.
+
+Returns
+-------
+year: int *
+    The year
+month: int *
+    The month
+day: int *
+    The day
+hour: int *
+    The hour
+minute: int *
+    The minute
+second: int *
+    The seconds
+"
+) swmm_getCurrentDateTime;
+
+
+%feature("autodoc", 
+"Set simulation datetime information.
+
+Parameters
+----------
+type: SM_TimePropety 
+    type The property type code (See :ref: SM_TimePropety)
+year: int
+    The year
+month: int
+    The month
+day: int
+    The day
+hour: int
+    The hour
+minute: int
+    The minute
+second: int
+    The seconds
+"
+) swmm_setSimulationDateTime;
+
+
+%feature("autodoc", 
+"Gets Simulation Analysis Setting
+
+Parameters
+----------
+type: SM_SimOption
+    Option code (see :ref: SM_SimOption)
+
+Returns
+-------
+value: int *
+    Option value
+"
+) swmm_getSimulationAnalysisSetting;
+
+
+%feature("autodoc", 
+"Gets Simulation Analysis Setting
+
+Parameters
+----------
+type: SM_SimSetting
+    Option code (see :ref: SM_SimSetting)
+
+Returns
+-------
+value: double *
+    Option value
+"
+) swmm_getSimulationParam;
+
+
+%feature("autodoc",
+"Gets Simulation Unit
+
+Parameters
+----------
+type: SM_Units
+    Option code (see :ref: SM_Units)
+
+Returns
+-------
+value: int *
+    Option value
+"
+) swmm_getSimulationUnit;
+
+
+%feature("autodoc", 
+"Get the type of node with specified index.
+
+Parameters
+----------
+index: int
+    The index of a node
+
+Returns
+-------
+Ntype: SM_NodeType *
+    The type code for the node (:ref: SM_NodeType). 
+    id must be pre-allocated by the caller.
+"
+) swmm_getNodeType;
+
+
+%feature("autodoc", 
+"Get a property value for specified node.
+
+Parameters
+----------
+index: int
+    The index of a node
+param: SM_NodeProperty
+    The property type code (See :ref: SM_NodeProperty)
+
+Returns
+-------
+value: double *
+    The value of the node's property
+"
+) swmm_getNodeParam;
+
+
+%feature("autodoc", 
+"Set a property value for specified node.
+
+Parameters
+----------
+index: int
+    The index of a node
+param: SM_NodeProperty
+    The property type code (See :ref: SM_NodeProperty)
+value: double
+    The new value of the node's property
+"
+) swmm_setNodeParam;
+
+
+%feature("autodoc", 
+"Get a result value for specified node.
+
+Parameters
+----------
+index: int
+    The index of a node
+type: SM_NodeResult
+    The property type code (See :ref: SM_NodeResult)
+
+Returns
+-------
+result: double *
+    The result of the node's property
+"
+) swmm_getNodeResult;
+
+
+%feature("autodoc", 
+"Gets pollutant values for a specified node.
+
+Parameters
+----------
+index: int
+    The index of a node
+type: SM_NodePollut
+    The property type code (see :ref: SM_NodePollut)
+
+Returns
+-------
+PollutArray: double *
+    result array
+"
+) swmm_getNodePollut;
+
+
+%feature("autodoc", 
+"Get the cumulative inflow for a node.
+
+Parameters
+----------
+index: int
+    The index of a node
+
+Returns
+-------
+value: double *
+    The total inflow.
+"
+) swmm_getNodeTotalInflow;
+
+
+%feature("autodoc", 
+"Set an inflow rate to a node. The inflow rate is held constant until the 
+caller changes it.
+
+Parameters
+----------
+index: int
+    The node index.
+flowrate: double
+    The new node inflow rate.
+"
+) swmm_setNodeInflow;
+
+
+%feature("autodoc", 
+"Get a node statistics.
+
+Parameters
+----------
+index: int
+    The index of a node
+
+Returns
+-------
+nodeStats: SM_NodeStats * 
+    The Node Stats struct (see :ref: SM_NodeStats) pre-allocated by the caller.
+"
+) swmm_getNodeStats;
+
+
+%feature("autodoc", 
+"Get a storage statistics.
+
+Parameters
+----------
+index: int
+    The index of a storage node
+
+Returns
+-------
+storageStats: SM_StorageStats * 
+    The storage Stats struct (see :ref: SM_StorageStats) pre-allocated by the caller.
+"
+) swmm_getStorageStats;
+
+
+%feature("autodoc", 
+"Set outfall stage.
+
+Parameters
+----------
+index: int
+    The outfall node index.
+stage: double
+    The outfall node stage (head).
+"
+) swmm_setOutfallStage;
+
+
+%feature("autodoc", 
+"Get outfall statistics.
+
+Parameters
+----------
+index: int
+    The index of a outfall node
+
+Returns
+-------
+outfallStats: SM_OutfallStats * 
+    The outfall Stats struct (see :ref: SM_OutfallStats).
+    pre-allocated by the caller. Caller is also responsible for freeing the
+    SM_OutfallStats structure using swmm_freeOutfallStats(). This frees any
+    pollutants array.
+"
+) swmm_getOutfallStats;
+
+
+%feature("autodoc", 
+"Get the type of link with specified index.
+
+Parameters
+----------
+index: int
+    The index of a link
+
+Returns
+-------
+Ltype: SM_LinkType *
+    The type code for the link (:ref: SM_LinkType).
+"
+) swmm_getLinkType;
+
+
+%feature("autodoc", 
+"Get the link Connection Node Indeces. If the conduit has a negative slope, 
+the dynamic wave solver will automatically reverse the nodes. 
+
+Parameters
+----------
+index: int
+    The index of a link
+
+Returns
+-------
+Node1: int *
+    The upstream node index.
+Node2: int *
+    The downstream node index.
+"
+) swmm_getLinkConnections;
+
+
+%feature("autodoc", 
+"Get the link flow direction
+
+Parameters
+----------
+index: int
+    The index of a link
+
+Returns
+-------
+value: char *
+    The link flow direction.
+"
+) swmm_getLinkDirection;
+
+
+%feature("autodoc", 
+"Get a property value for specified link.
+
+Parameters
+----------
+index: int
+    The index of a link
+param: SM_LinkProperty
+    The property type code (See :ref: SM_LinkProperty)
+
+Returns
+-------
+value: double *
+    The value of the link's property
+"
+) swmm_getLinkParam;
+
+
+%feature("autodoc", 
+"Set a property value for specified link.
+
+Parameters
+----------
+index: int
+    The index of a link
+param: SM_LinkProperty
+    The property type code (See :ref: SM_LinkProperty)
+value: double
+    The new value of the link's property
+"
+) swmm_setLinkParam;
+
+
+%feature("autodoc", 
+"Get a result value for specified link.
+
+Parameters
+----------
+index: int
+    The index of a link
+type: SM_LinkResult
+    The property type code (See :ref: SM_LinkResult)
+
+Returns
+-------
+result: double *
+    The result of the link's property
+"
+) swmm_getLinkResult;
+
+
+%feature("autodoc", 
+"Gets pollutant values for a specified link.
+
+Parameters
+----------
+index: int
+    The index of a link
+type: SM_LinkPollut
+    The property type code (see :ref: SM_LinkPollut)
+
+Returns
+-------
+PollutArray: double *
+    result array
+"
+) swmm_getLinkPollut;
+
+
+%feature("autodoc", 
+"Set a link setting (pump, orifice, or weir). Setting for an orifice and a 
+weir should be [0, 1]. A setting for a pump can range from [0, inf). However, 
+if a pump is set to 1, it will pump at its maximum curve setting.
+
+Parameters
+----------
+index: int
+    The link index.
+setting: double
+    The new setting for the link.
+"
+) swmm_setLinkSetting;
+
+
+%feature("autodoc", 
+"Get link statistics.
+
+Parameters
+----------
+index: int
+    The link index.
+
+Returns
+-------
+linkStats: SM_LinkStats *
+    The link Stats struct (see :ref: SM_LinkStats). pre-allocated by the caller.
+"
+) swmm_getLinkStats;
+
+
+%feature("autodoc", 
+"Get pump statistics.
+
+Parameters
+----------
+index: int
+    The index of a pump
+
+Returns
+-------
+pumpStats: SM_PumpStats *
+    The link Stats struct (see :ref: SM_PumpStats). pre-allocated by the caller.
+"
+) swmm_getPumpStats;
+
+
+%feature("autodoc", 
+"Get the Subcatchment connection. Subcatchments can load to a node, another 
+subcatchment, or itself.
+
+Parameters
+----------
+index: int
+    The index of a Subcatchment
+
+Returns
+-------
+type: SM_ObjectType
+    The type of object loading (See :ref: SM_ObjectType)
+out_index: int *
+    The object index
+"
+) swmm_getSubcatchOutConnection;
+
+
+%feature("autodoc", 
+"Get a property value for specified subcatchment.
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+param: SM_SubcProperty
+    The property type code (See :ref: SM_SubcProperty)
+
+Returns
+-------
+value: double *
+    The value of the subcatchment's property
+"
+) swmm_getSubcatchParam;
+
+
+%feature("autodoc", 
+"Set a property value for specified subcatchment.
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+param: SM_SubcProperty
+    The property type code (See :ref: SM_SubcProperty)
+value: double
+    The new value of the subcatchment's property
+"
+) swmm_setSubcatchParam;
+
+
+%feature("autodoc", 
+"Get a result value for specified subcatchment.
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+type: SM_SubcResult
+    The property type code (See :ref: SM_SubcResult)
+
+Returns
+-------
+result: double *
+    The result of the subcatchment's property
+"
+) swmm_getSubcatchResult;
+
+
+%feature("autodoc", 
+"Gets pollutant values for a specified subcatchment.
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+type: SM_SubcPollut
+    The property type code (see :ref: SM_SubcPollut)
+
+Returns
+-------
+PollutArray: double **
+    result array
+"
+) swmm_getSubcatchPollut;
+
+
+%feature("autodoc", 
+"Get subcatchment statistics.
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+
+Returns
+-------
+subcatchStats: SM_SubcatchStats *
+    The link Stats struct (see :ref: SM_SubcatchStats).
+    pre-allocated by the caller. Caller is also responsible for freeing the
+    SM_SubcatchStats structure using swmm_freeSubcatchStats(). This frees any
+    pollutants array.
+"
+) swmm_getSubcatchStats;
+
+
+%feature("autodoc", 
+"Get system routing totals.
+
+Returns
+-------
+routingTot: SM_RoutingTotals *
+    The system Routing Stats struct (see :ref: SM_RoutingTotals).
+    pre-allocated by the caller.
+"
+) swmm_getSystemRoutingTotals;
+
+
+%feature("autodoc", 
+"Get system runoff totals.
+
+Returns
+-------
+runoffTot: SM_RunoffTotals *
+    The system Runoff Stats struct (see :ref: SM_RunoffTotals).
+    pre-allocated by the caller.
+"
+) swmm_getSystemRunoffTotals;
+
+
+%feature("autodoc", 
+"Get the number of lid units on a subcatchment.
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+
+Returns
+-------
+value: int *
+    The number of lid units on a subcatchment
+"
+) swmm_getLidUCount;
+
+
+%feature("autodoc", 
+"Get a property value for a specified lid unit on a specified subcatchment
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+lidIndex: int
+    The index of specified lid unit
+param: SM_LidUProperty
+    The property type code (See :ref: SM_LidUProperty)
+
+Returns
+-------
+value: double
+    The value of the lid unit's property
+"
+) swmm_getLidUParam;
+
+
+%feature("autodoc", 
+"Set a property value for a specified lid unit on a specified subcatchment
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+lidIndex: int
+    The index of specified lid unit
+param: SM_LidUProperty
+    The property type code (See :ref: SM_LidUProperty)
+value: double
+    The new value of the lid unit's property
+"
+) swmm_setLidUParam;
+
+
+%feature("autodoc", 
+"Get the lid option for a specified lid unit on a specified subcatchment
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+lidIndex: int
+    The index of specified lid unit
+param: SM_LidUOptions
+    The lid option type code (See :ref: SM_LidUOptions)
+
+Returns
+-------
+value: int *
+    The value of the option for the lid unit
+"
+) swmm_getLidUOption;
+
+
+%feature("autodoc", 
+"Set the lid option for a specified lid unit on a specified subcatchment
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+lidIndex: int
+    The index of specified lid unit
+param: SM_LidUOptions
+    The lid option type code (See :ref: SM_LidUOptions)
+value: int
+    The new value of the option for the lid unit
+"
+) swmm_setLidUOption;
+
+
+%feature("autodoc", 
+"Get the lid unit water balance simulated value at current time
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+lidIndex: int
+    The index of specified lid unit
+layerIndex: SM_LidLayer
+    The index of specified lid layer (See :ref: SM_LidLayer)
+
+Returns
+-------
+result: double * 
+    The result for the specified lid unit
+"
+) swmm_getLidUFluxRates;
+
+
+%feature("autodoc", 
+"Get the lid unit of a specified subcatchment result at current time
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+lidIndex: int
+    The index of specified lid unit
+type: SM_LidResult
+    The result type code (See :ref: SM_LidResult)
+
+Returns
+-------
+result: double *
+    The result for the specified lid unit
+"
+) swmm_getLidUResult;
+
+
+%feature("autodoc", 
+"Get the lid control surface immediate overflow condition
+
+Parameters
+----------
+lidControlIndex: int
+    The index of specified lid control
+
+Returns
+-------
+condition: int *
+    The value of surface immediate overflow condition
+"
+) swmm_getLidCOverflow;
+
+
+%feature("autodoc", 
+"Get a property value for specified lid control
+
+Parameters
+----------
+lidControlIndex: int
+    The index of specified lid control
+layerIndex: SM_LidLayer
+    The index of specified lid layer (See :ref: SM_LidLayer)
+param: SM_LidLayerProperty
+    The property type code (See :ref: SM_LidLayerProperty)
+
+Returns
+-------
+value: double 
+    The value of lid control's property
+"
+) swmm_getLidCParam;
+
+
+%feature("autodoc", 
+"Set a property value for specified lid control
+
+Parameters
+----------
+lidControlIndex: int
+    The index of specified lid control
+layerIndex: SM_LidLayer
+    The index of specified lid layer (See :ref: SM_LidLayer)
+param: SM_LidLayerProperty
+    The property type code (See :ref: SM_LidLayerProperty)
+value: double
+    The new value for the lid control's property
+"
+) swmm_setLidCParam;
+
+
+%feature("autodoc", 
+"Get the lid group of a specified subcatchment result at current time
+
+Parameters
+----------
+index: int
+    The index of a subcatchment
+type: SM_LidResult
+    The result type code (See :ref: SM_LidResult)
+
+Returns
+-------
+result: double *
+    The result for the specified lid group
+"
+) swmm_getLidGResult;
+
+
+%feature("autodoc", 
+"Get precipitation rates for a gage.
+
+Parameters
+----------
+index: int
+    The index of gage
+type: SM_GagePrecip
+    The property type code (see :ref: SM_GagePrecip)
+
+Returns
+-------
+GageArray: double *
+    precipitation rate
+"
+) swmm_getGagePrecip;
+
+
+%feature("autodoc", 
+"Set a total precipitation intensity to the gage.
+
+Parameters
+----------
+index: int
+    The gage index.
+total_precip: double
+    The new total precipitation intensity.
+"
+) swmm_setGagePrecip;
diff --git a/swmm-toolkit/src/swmm/toolkit/solver_rename.i b/swmm-toolkit/src/swmm/toolkit/solver_rename.i
new file mode 100644
index 00000000..648f1d9a
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/solver_rename.i
@@ -0,0 +1,102 @@
+/*
+ *  solver_rename.i - Rename mapping for swmm-solver
+ *
+ *  Created:    2/26/2020
+ *  
+ *  Author:     See AUTHORS
+ *
+ *  Purpose: Renames functions according to PEP8
+*/
+
+
+// RENAME STATS STRUCTS
+%rename (NodeStats)                         SM_NodeStats;
+%rename (StorageStats)                      SM_StorageStats;
+%rename (OutfallStats)                      SM_OutfallStats;
+%rename (LinkStats)                         SM_LinkStats;
+%rename (PumpStats)                         SM_PumpStats;
+%rename (SubcatchStats)                     SM_SubcatchStats;
+%rename (RoutingTotals)                     SM_RoutingTotals;
+%rename (RunoffTotals)                      SM_RunoffTotals;
+
+
+// RENAME CANONICAL API AS NECESSARY
+%rename(swmm_get_mass_balance)              swmm_getMassBalErr;
+%rename(swmm_get_version)                   swmm_getVersion;
+
+
+// RENAME NEW API
+%rename(project_get_index)                  swmm_project_findObject;
+%rename(project_get_id)                     swmm_getObjectId;
+%rename(project_get_count)                  swmm_countObjects;
+
+
+%rename(simulation_get_datetime)            swmm_getSimulationDateTime;
+%rename(simulation_get_current_datetime)    swmm_getCurrentDateTime;
+%rename(simulation_set_datetime)            swmm_setSimulationDateTime;
+%rename(simulation_get_setting)             swmm_getSimulationAnalysisSetting;
+%rename(simulation_get_parameter)           swmm_getSimulationParam;
+%rename(simulation_get_unit)                swmm_getSimulationUnit;
+
+
+%rename(node_get_type)                      swmm_getNodeType;
+%rename(node_get_parameter)                 swmm_getNodeParam;
+%rename(node_set_parameter)                 swmm_setNodeParam;
+%rename(node_get_result)                    swmm_getNodeResult;
+%rename(node_get_pollutant)                 swmm_getNodePollut;
+%rename(node_get_total_inflow)              swmm_getNodeTotalInflow;
+%rename(node_set_total_inflow)              swmm_setNodeInflow;
+%rename(node_get_stats)                     swmm_getNodeStats;
+
+
+%rename(storage_get_stats)                  swmm_getStorageStats;
+
+
+%rename(outfall_set_stage)                  swmm_setOutfallStage;
+%rename(outfall_get_stats)                  swmm_getOutfallStats;
+
+
+%rename(link_get_type)                      swmm_getLinkType;
+%rename(link_get_connections)               swmm_getLinkConnections;
+%rename(link_get_direction)                 swmm_getLinkDirection;
+%rename(link_get_parameter)                 swmm_getLinkParam;
+%rename(link_set_parameter)                 swmm_setLinkParam;
+%rename(link_get_result)                    swmm_getLinkResult;
+%rename(link_get_pollutant)                 swmm_getLinkPollut;
+%rename(link_set_target_setting)            swmm_setLinkSetting;
+%rename(link_get_stats)                     swmm_getLinkStats;
+
+
+%rename(pump_get_stats)                     swmm_getPumpStats;
+
+
+%rename(subcatch_get_connection)            swmm_getSubcatchOutConnection;
+%rename(subcatch_get_parameter)             swmm_getSubcatchParam;
+%rename(subcatch_set_parameter)             swmm_setSubcatchParam;
+%rename(subcatch_get_result)                swmm_getSubcatchResult;
+%rename(subcatch_get_pollutant)             swmm_getSubcatchPollut;
+%rename(subcatch_get_stats)                 swmm_getSubcatchStats;
+
+
+%rename(system_get_routing_totals)          swmm_getSystemRoutingTotals;
+%rename(system_get_runoff_totals)           swmm_getSystemRunoffTotals;
+
+
+%rename(lid_usage_get_count)                swmm_getLidUCount;
+%rename(lid_usage_get_parameter)            swmm_getLidUParam;
+%rename(lid_usage_set_parameter)            swmm_setLidUParam;
+%rename(lid_usage_get_option)               swmm_getLidUOption;
+%rename(lid_usage_set_option)               swmm_setLidUOption;
+%rename(lid_usage_get_flux_rate)            swmm_getLidUFluxRates;
+%rename(lid_usage_get_result)               swmm_getLidUResult;
+%rename(lid_control_get_overflow)           swmm_getLidCOverflow;
+%rename(lid_control_get_parameter)          swmm_getLidCParam;
+%rename(lid_control_set_parameter)          swmm_setLidCParam;
+%rename(lid_group_get_result)               swmm_getLidGResult;
+
+
+%rename(raingage_get_precipitation)         swmm_getGagePrecip;
+%rename(raingage_set_precipitation)         swmm_setGagePrecip;
+
+
+%rename(swmm_version_info)                  swmm_getVersionInfo;
diff --git a/swmm-toolkit/src/swmm/toolkit/stats_typemaps.i b/swmm-toolkit/src/swmm/toolkit/stats_typemaps.i
new file mode 100644
index 00000000..c744cefc
--- /dev/null
+++ b/swmm-toolkit/src/swmm/toolkit/stats_typemaps.i
@@ -0,0 +1,102 @@
+/*
+ *  stats_typemaps.i - SWIG interface description file for swmm structs
+ *
+ *  Created:    11/16/2020
+ *  Updated:
+ *
+ *  Author:     See AUTHORS
+ * 
+*/
+
+
+%define %statsmaps(Type)
+%typemap(in, numinputs=0) Type *out_stats (Type *temp) {
+    temp = (Type *)calloc(1, sizeof(Type));
+    $1 = temp;
+}
+%typemap(argout) Type *out_stats {
+    %append_output(SWIG_NewPointerObj(SWIG_as_voidptr(temp$argnum), $1_descriptor, 0));
+}
+%enddef
+
+%statsmaps(SM_NodeStats);
+%apply SM_NodeStats *out_stats {
+    SM_NodeStats *nodeStats
+}
+
+%statsmaps(SM_StorageStats);
+%apply SM_StorageStats *out_stats {
+    SM_StorageStats *storageStats
+}
+
+
+/* PROVIDE CUSTOM CONSTRUCTOR/DECONSTRUCTOR */
+%nodefaultctor SM_OutfallStats;
+
+%extend SM_OutfallStats {
+    SM_OutfallStats(int num_pollut) {
+        SM_OutfallStats *s = (SM_OutfallStats *)calloc(1, sizeof(SM_OutfallStats));
+        if (num_pollut > 0)
+            s->totalLoad = (double *)calloc(num_pollut, sizeof(double));
+        return s;
+    }
+    ~SM_OutfallStats(SM_OutfallStats *self) {
+        if (self != NULL)
+            free(self->totalLoad);
+        free(self);
+    }
+    double get_totalLoad(int index) {
+        return self->totalLoad[index];
+    }
+}
+
+%typemap(in, numinputs=0) SM_OutfallStats *out_outfall_stats (SM_OutfallStats *temp) {
+    int num_pollut;
+    swmm_countObjects(SM_POLLUT, &num_pollut);
+    temp = new_SM_OutfallStats(num_pollut);
+    $1 = temp;
+}
+%typemap(argout) SM_OutfallStats *out_outfall_stats {
+    %append_output(SWIG_NewPointerObj(SWIG_as_voidptr(temp$argnum), $1_descriptor, 0));
+}
+
+%apply SM_OutfallStats *out_outfall_stats {
+    SM_OutfallStats *outfallStats
+}
+
+
+
+%statsmaps(SM_LinkStats);
+%apply SM_LinkStats *out_stats {
+    SM_LinkStats *linkStats
+}
+
+
+%statsmaps(SM_PumpStats);
+%apply SM_PumpStats *out_stats {
+    SM_PumpStats *pumpStats
+}
+
+
+%statsmaps(SM_SubcatchStats);
+%apply SM_SubcatchStats *out_stats {
+    SM_SubcatchStats *subcatchStats
+}
+
+
+%statsmaps(SM_RoutingTotals);
+%apply SM_RoutingTotals *out_stats {
+    SM_RoutingTotals *routingTotals
+}
+
+
+%statsmaps(SM_RunoffTotals);
+%apply SM_RunoffTotals *out_stats {
+    SM_RunoffTotals *runoffTotals
+}
+
+
+/* WRAP PUBLIC STRUCTURES AND GENERATE GETTERS */
+%immutable;
+%include "toolkit_structs.h"
+%noimmutable;
diff --git a/swmm-toolkit/swmm-solver b/swmm-toolkit/swmm-solver
new file mode 160000
index 00000000..b3bcc3b8
--- /dev/null
+++ b/swmm-toolkit/swmm-solver
@@ -0,0 +1,1 @@
+Subproject commit b3bcc3b8ae23b0cb985225cace674bbee0f9370e
diff --git a/swmm-toolkit/tools/relocate.zsh b/swmm-toolkit/tools/relocate.zsh
new file mode 100755
index 00000000..0cb95f4f
--- /dev/null
+++ b/swmm-toolkit/tools/relocate.zsh
@@ -0,0 +1,36 @@
+#!/usr/bin/env zsh
+
+#
+#  relocate.zsh - Relocate lib to install/extern for MacOS
+#
+#  Date created: May 7, 2020
+#
+#  Author:       See AUTHORS
+#
+#  Arguments:
+#    1 - Path to library
+#
+
+echo "INFO: Relocating ... $1"
+
+if [[ -z "$1" ]]; then
+    echo "ERROR: Required argument is missing"; return 1
+fi
+
+chmod 755 extern/$1:t
+
+
+# Grabs current path for lib
+IFS=$' '
+TOKEN=($( otool -l extern/$1:t | grep LC_ID_DYLIB -A2 | grep name ))
+LIB_PATH=${TOKEN[2]}
+
+
+# Changes load path for lib in _solver.so
+install_name_tool -change ${LIB_PATH} @rpath/$1:t src/swmm/toolkit/_solver*.so
+
+# Changes load path for lib in libswmm5
+install_name_tool -change ${LIB_PATH} @rpath/$1:t lib/libswmm5.dylib
+
+# Changes id on relocated lib
+install_name_tool -id @rpath/$1:t extern/$1:t


Test patch:
diff --git a/nrtest-swmm/main.py b/nrtest-swmm/main.py
new file mode 100644
index 00000000..69f24fe0
--- /dev/null
+++ b/nrtest-swmm/main.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+
+#
+#  main.py
+#
+#  Author:     Michael E. Tryby
+#              US EPA - ORD/NRMRL
+#
+'''
+Provides entry point main. Useful for development and testing purposes.
+'''
+
+import cStringIO
+
+import time
+
+import header_detail_footer as hdf
+import numpy as np
+
+import nrtest_swmm.output_reader as ordr
+
+def result_compare(path_test, path_ref, comp_args):
+
+    isclose = True
+    close = 0
+    notclose = 0
+    equal = 0
+    total = 0
+    output = cStringIO.StringIO()
+    eps =  np.finfo(float).eps
+
+    start = time.time()
+
+    test_reader = ordr.output_generator(path_test)
+    ref_reader = ordr.output_generator(path_ref)
+
+    for test, ref in _zip(test_reader, ref_reader):
+        total += 1
+        if total%100000 == 0:
+        print(total)
+
+        if len(test) != len(ref):
+            raise ValueError('Inconsistent lengths')
+
+        # Skip results if they are zero or equal
+        if np.array_equal(test, ref):
+            equal += 1
+            continue
+        else:
+            try:
+                np.testing.assert_allclose(test, ref, comp_args[0], comp_args[1])
+                close += 1
+
+            except AssertionError as ae:
+                notclose += 1
+                output.write(str(ae))
+                output.write('\n\n')
+                continue
+
+    stop = time.time()
+
+    print(output.getvalue())
+    output.close()
+
+    print('equal: %d  close: %d  notclose: %d  total: %d  in %f (sec)\n' %
+          (equal, close, notclose, total, (stop - start)))
+
+    if notclose > 0:
+        print('%d differences found\n' % notclose)
+        isclose = False
+
+    return isclose
+
+def array_zero(test, ref):
+    if not test.any() and not ref.any():
+        return True
+    return False
+
+def report_compare(path_test, path_ref, (comp_args)):
+    '''
+    Compares results in two report files ignoring contents of header and footer.
+    '''
+    with open(path_test ,'r') as ftest, open(path_ref, 'r') as fref:
+        for (test_line, ref_line) in zip(hdf.parse(ftest, 4, 4)[1],
+                                         hdf.parse(fref, 4, 4)[1]):
+            if test_line != ref_line:
+                return False
+
+    return True
+
+
+import logging
+from os import listdir
+from os.path import exists, isfile, isdir, join
+
+from nrtest.testsuite import TestSuite
+from nrtest.compare import compare_testsuite, validate_testsuite
+from nrtest.execute import execute_testsuite
+
+def nrtest_compare(path_test, path_ref, rtol, atol):
+
+    ts_new = TestSuite.read_benchmark(path_test)
+    ts_old = TestSuite.read_benchmark(path_ref)
+
+    if not validate_testsuite(ts_new) or not validate_testsuite(ts_old):
+        exit(1)
+
+    try:
+        logging.info('Found %i tests' % len(ts_new.tests))
+        compatible = compare_testsuite(ts_new, ts_old, rtol, atol)
+    except KeyboardInterrupt:
+        logging.warning('Process interrupted by user')
+        compatible = False
+    else:
+        logging.info('Finished')
+
+    # Non-zero exit code indicates failure
+    exit(not compatible)
+
+def nrtest_execute(app_path, test_path, output_path):
+
+
+    for p in test_path + [app_path]:
+        if not exists(p):
+            logging.error('Could not find path: "%s"' % p)
+
+    test_dirs = [p for p in test_path if isdir(p)]
+    test_files = [p for p in test_path if isfile(p)]
+    test_files += [join(d, p) for d in test_dirs for p in listdir(d)
+                   if p.endswith('.json')]
+
+    test_files = list(set(test_files))  # remove duplicates
+
+    ts = TestSuite.read_config(app_path, test_files, output_path)
+
+    if not validate_testsuite(ts):
+        exit(1)
+
+    try:
+        logging.info('Found %i tests' % len(test_files))
+        success = execute_testsuite(ts)
+        ts.write_manifest()
+    except KeyboardInterrupt:
+        logging.warning('Process interrupted by user')
+        success = False
+    else:
+        logging.info('Finished')
+
+    # Non-zero exit code indicates failure
+    exit(not success)
diff --git a/nrtest-swmm/nrtest_swmm/__init__.py b/nrtest-swmm/nrtest_swmm/__init__.py
new file mode 100644
index 00000000..3ba91075
--- /dev/null
+++ b/nrtest-swmm/nrtest_swmm/__init__.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+
+#
+#  __init__.py - nrtest_swmm module
+#
+#  Author:     Michael E. Tryby
+#              US EPA - ORD/NRMRL
+#
+
+'''
+Numerical regression testing (nrtest) plugin for comparing SWMM binary results
+files and SWMM text based report files.
+'''
+
+# third party imports
+import header_detail_footer as hdf
+import numpy as np
+
+# project imports
+import nrtest_swmm.output_reader as ordr
+
+
+__author__ = "Michael E. Tryby"
+__copyright__ = "None"
+__credits__ = "Colleen Barr, Maurizio Cingi, Mark Gray, David Hall, Bryant McDonnell"
+__license__ = "CC0 1.0 Universal"
+
+__version__ = "0.6.0"
+__date__ = "May 8, 2020"
+
+__maintainer__ = "Michael E. Tryby"
+__email__ = "tryby.michael@epa.gov"
+__status  = "Development"
+
+
+def swmm_allclose_compare(path_test, path_ref, rtol, atol):
+    '''
+    Compares results in two SWMM binary files. Using the comparison criteria
+    described in the numpy assert_allclose documentation.
+
+        (test_value - ref_value) <= atol + rtol * abs(ref_value)
+
+    Returns true if all of the results in the two binary files meet the
+    comparison criteria; otherwise, an AssertionError is thrown.
+
+    Numpy allclose is quite expensive to evaluate. Test and reference results
+    are checked to see if they are equal before being compared using the
+    allclose criteria. This reduces comparison times significantly.
+
+    Arguments:
+        path_test - path to result file being tested
+        path_ref  - path to reference result file
+        rtol - relative tolerance
+        atol - absolute tolerance
+
+    Returns:
+        True or raises an error
+
+    Raises:
+        ValueError()
+        AssertionError()
+        ...
+    '''
+
+    for (test, ref) in zip(ordr.output_generator(path_test),
+                           ordr.output_generator(path_ref)):
+
+        if len(test[0]) != len(ref[0]):
+            raise ValueError('Inconsistent lengths')
+
+        # Skip over results if they are equal
+        if (np.array_equal(test[0], ref[0])):
+            continue
+
+        else:
+            np.testing.assert_allclose(test[0], ref[0], rtol, atol)
+
+    return True
+
+#def swmm_better_compare():
+#    '''
+#    If for some reason you don't like numpy.testing.assert_allclose() add a
+#    better function here. Be sure to add the entry point to the setup file so
+#    nrtest can find it at runtime.
+#    '''
+#    pass
+
+def swmm_report_compare(path_test, path_ref, rtol, atol):
+    '''
+    Compares results in two report files ignoring contents of header and footer.
+
+    Arguments:
+        path_test - path to result file being tested
+        path_ref  - path to reference result file
+        rtol - ignored
+        atol - ignored
+
+    Returns:
+        True or False
+
+    Raises:
+        HeaderError()
+        FooterError()
+        RunTimeError()
+        ...
+    '''
+
+    HEADER = 4
+    FOOTER = 4
+
+    with open(path_test ,'r') as ftest, open(path_ref, 'r') as fref:
+
+        for (test_line, ref_line) in zip(hdf.parse(ftest, HEADER, FOOTER)[1],
+                                         hdf.parse(fref, HEADER, FOOTER)[1]):
+
+            if test_line != ref_line:
+                return False
+
+    return True
diff --git a/nrtest-swmm/nrtest_swmm/output_reader.py b/nrtest-swmm/nrtest_swmm/output_reader.py
new file mode 100644
index 00000000..7b5f9666
--- /dev/null
+++ b/nrtest-swmm/nrtest_swmm/output_reader.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+
+#
+#  output_reader.py
+#
+#  Date Created: 11/14/2017
+#  Date Modified: 10/17/2019
+#
+#  Author:       Michael E. Tryby
+#                US EPA - ORD/NRMRL
+#
+'''
+The module output_reader provides the class used to implement the output
+generator.
+'''
+
+# system import
+from itertools import islice
+
+# project import
+from swmm.toolkit import output, output_enum
+
+
+def output_generator(path_ref):
+    '''
+    The output_generator is designed to iterate over a swmm binary file and
+    yield element results. It is useful for comparing contents of binary files
+    for numerical regression testing.
+
+    The generator yields a list containing the SWMM element result.
+
+    Arguments:
+        path_ref - path to result file
+
+    Raises:
+        SWMM_OutputReaderError()
+        ...
+    '''
+
+    with OutputReader(path_ref) as reader:
+
+        for period_index in range(0, reader.report_periods()):
+            for element_type in islice(output_enum.ElementType, 4):
+                for element_index in range(0, reader.element_count(element_type)):
+
+                    yield (reader.element_result(element_type, period_index, element_index),
+                        (element_type, period_index, element_index))
+
+
+class OutputReader():
+    '''
+    Provides minimal interface needed to implement the SWMM output generator.
+    '''
+
+    def __init__(self, filename):
+        self.filepath = filename
+        self.handle = None
+        self.count = None
+        self.get_element_result = {
+            output_enum.ElementType.SUBCATCH: output.get_subcatch_result,
+            output_enum.ElementType.NODE: output.get_node_result,
+            output_enum.ElementType.LINK: output.get_link_result,
+            output_enum.ElementType.SYSTEM: output.get_system_result
+        }
+
+    def __enter__(self):
+        self.handle = output.init()
+        output.open(self.handle, self.filepath)
+        self.count = output.get_proj_size(self.handle)
+        return self
+
+    def __exit__(self, type, value, traceback):
+        output.close(self.handle)
+
+    def report_periods(self):
+        return output.get_times(self.handle, output_enum.Time.NUM_PERIODS)
+
+    def element_count(self, element_type):
+        return self.count[element_type]
+
+    def element_result(self, element_type, time_index, element_index):
+        return self.get_element_result[element_type](self.handle, time_index, element_index)
diff --git a/nrtest-swmm/scripts/report-diff b/nrtest-swmm/scripts/report-diff
new file mode 100644
index 00000000..054def11
--- /dev/null
+++ b/nrtest-swmm/scripts/report-diff
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+#
+#  binary_diff.py
+#
+#  Date Created: July 11, 2018
+#  Date Modified: October 17, 2019
+#
+#  Author:     Michael E. Tryby
+#              US EPA - ORD/NRMRL
+#
+
+# system imports
+import argparse
+
+# third party imports
+import numpy as np
+
+# project imports
+import nrtest_swmm.output_reader as ordr
+
+
+def _binary_diff(path_test, path_ref, min_cdd):
+    for (test, ref) in zip(ordr.output_generator(path_test),
+                           ordr.output_generator(path_ref)):
+
+        if len(test[0]) != len(ref[0]):
+            raise ValueError('Inconsistent lengths')
+
+        # Skip over arrays that are equal
+        if np.array_equal(test[0], ref[0]):
+            continue
+        else:
+            lre = _log_relative_error(test[0], ref[0])
+            idx = np.unravel_index(np.argmin(lre), lre.shape)
+
+            if lre[idx] < min_cdd:
+                _print_diff(idx, lre, test, ref)
+
+    return
+
+
+def _log_relative_error(q, c):
+    '''
+    Computes log relative error, a measure of numerical accuracy.
+
+    Single precision machine epsilon is between 2^-24 and 2^-23.
+
+    Reference:
+    McCullough, B. D. "Assessing the Reliability of Statistical Software: Part I."
+    The American Statistician, vol. 52, no. 4, 1998, pp. 358-366.
+    '''
+    diff = np.subtract(q, c)
+    tmp_c = np.copy(c)
+
+    # If ref value is small compute absolute error
+    tmp_c[np.fabs(tmp_c) < 1.0e-6] = 1.0
+    re = np.fabs(diff)/np.fabs(tmp_c)
+
+    # If re is tiny set lre to number of digits
+    re[re < 1.0e-7] = 1.0e-7
+    # If re is very large set lre to zero
+    re[re > 2.0] = 1.0
+
+    lre = np.negative(np.log10(re))
+
+    # If lre is negative set to zero
+    lre[lre < 0.0] = 0.0
+
+    return lre
+
+
+def _print_diff(idx, lre, test, ref):
+
+    idx_val = (idx[0], ref[1])
+    test_val = (test[0][idx[0]])
+    ref_val = (ref[0][idx[0]])
+    diff_val = (test_val - ref_val)
+    lre_val = (lre[idx[0]])
+
+    print("Idx: %s\nSut: %e  Ref: %e  Diff: %e  LRE: %.2f\n"
+          % (idx_val, test_val, ref_val, diff_val, lre_val))
+
+
+def report(args):
+    _binary_diff(args.test, args.ref, args.mincdd)
+
+
+if __name__ == '__main__':
+
+    parser = argparse.ArgumentParser(description='SWMM binary output difference reporting')
+    parser.set_defaults(func=report)
+
+    parser.add_argument('-t', '--test', default=None,
+                        help='Path to test benchmark')
+    parser.add_argument('-r', '--ref', default=None,
+                        help='Path to reference benchmark')
+    parser.add_argument('-mc', '--mincdd', type=int, default=3,
+                        help='Minimum correct decimal digits')
+
+    args = parser.parse_args()
+    args.func(args)
diff --git a/nrtest-swmm/setup.py b/nrtest-swmm/setup.py
new file mode 100644
index 00000000..7d2338d0
--- /dev/null
+++ b/nrtest-swmm/setup.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+#
+#  setup.py
+#
+#  Modified on October 17, 2019
+#
+#  Author:     Michael E. Tryby
+#              US EPA - ORD/NRMRL
+#
+#  Usage:
+#    \>python setup.py bdist_wheel
+#
+''' Setup up script for nrtest_swmm package. '''
+
+
+from setuptools import setup
+
+
+entry_points = {
+    'nrtest.compare': [
+        'swmm allclose = nrtest_swmm:swmm_allclose_compare',
+        'swmm report = nrtest_swmm:swmm_report_compare',
+        # Add the entry point for new comparison functions here
+    ]
+}
+
+setup(
+    name='nrtest-swmm',
+    version='0.6.0',
+    description="SWMM extension for nrtest",
+
+    author="Michael E. Tryby",
+    author_email='tryby.michael@epa.gov',
+    url='https://github.com/USEPA',
+
+    packages=['nrtest_swmm',],
+    scripts=['scripts/report-diff',],
+    entry_points=entry_points,
+
+    install_requires=[
+        'header_detail_footer>=2.3',
+        'nrtest>=0.2.0',
+        'numpy>=1.7.0',
+        'swmm-toolkit',
+    ],
+
+    keywords='nrtest_swmm',
+    classifiers=[
+        'Programming Language :: Python :: 3'
+    ],
+)
diff --git a/swmm-toolkit/tests/data/test_Example1.inp b/swmm-toolkit/tests/data/test_Example1.inp
new file mode 100644
index 00000000..102c2e07
--- /dev/null
+++ b/swmm-toolkit/tests/data/test_Example1.inp
@@ -0,0 +1,350 @@
+[TITLE]
+;;Project Title/Notes
+Example 1
+
+[OPTIONS]
+;;Option             Value
+FLOW_UNITS           CFS
+INFILTRATION         HORTON
+FLOW_ROUTING         KINWAVE
+LINK_OFFSETS         DEPTH
+MIN_SLOPE            0
+ALLOW_PONDING        NO
+SKIP_STEADY_STATE    NO
+
+START_DATE           01/01/1998
+START_TIME           00:00:00
+REPORT_START_DATE    01/01/1998
+REPORT_START_TIME    00:00:00
+END_DATE             01/02/1998
+END_TIME             12:00:00
+SWEEP_START          1/1
+SWEEP_END            12/31
+DRY_DAYS             5
+REPORT_STEP          01:00:00
+WET_STEP             00:15:00
+DRY_STEP             01:00:00
+ROUTING_STEP         0:01:00 
+
+INERTIAL_DAMPING     PARTIAL
+NORMAL_FLOW_LIMITED  BOTH
+FORCE_MAIN_EQUATION  H-W
+VARIABLE_STEP        0.75
+LENGTHENING_STEP     0
+MIN_SURFAREA         0
+MAX_TRIALS           0
+HEAD_TOLERANCE       0
+SYS_FLOW_TOL         5
+LAT_FLOW_TOL         5
+MINIMUM_STEP         0.5
+THREADS              1
+
+[EVAPORATION]
+;;Data Source    Parameters
+;;-------------- ----------------
+CONSTANT         0.0
+DRY_ONLY         NO
+
+[RAINGAGES]
+;;Name           Format    Interval SCF      Source    
+;;-------------- --------- ------ ------ ----------
+RG1              INTENSITY 1:00     1.0      TIMESERIES TS1             
+
+[SUBCATCHMENTS]
+;;Name           Rain Gage        Outlet           Area     %Imperv  Width    %Slope   CurbLen  SnowPack        
+;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- ----------------
+2                RG1              10               10       50       500      0.01     0                        
+3                RG1              13               5        50       500      0.01     0                        
+1                RG1              9                10       50       500      0.01     0                        
+4                RG1              22               5        50       500      0.01     0                        
+5                RG1              15               15       50       500      0.01     0                        
+6                RG1              23               12       10       500      0.01     0                        
+7                RG1              19               4        10       500      0.01     0                        
+8                RG1              18               10       10       500      0.01     0                        
+
+[SUBAREAS]
+;;Subcatchment   N-Imperv   N-Perv     S-Imperv   S-Perv     PctZero    RouteTo    PctRouted 
+;;-------------- ---------- ---------- ---------- ---------- ---------- ---------- ----------
+2                0.001      0.10       0.05       0.05       25         OUTLET    
+3                0.001      0.10       0.05       0.05       25         OUTLET    
+1                0.001      0.10       0.05       0.05       25         OUTLET    
+4                0.001      0.10       0.05       0.05       25         OUTLET    
+5                0.001      0.10       0.05       0.05       25         OUTLET    
+6                0.001      0.10       0.05       0.05       25         OUTLET    
+7                0.001      0.10       0.05       0.05       25         OUTLET    
+8                0.001      0.10       0.05       0.05       25         OUTLET    
+
+[INFILTRATION]
+;;Subcatchment   MaxRate    MinRate    Decay      DryTime    MaxInfil  
+;;-------------- ---------- ---------- ---------- ---------- ----------
+2                0.7        0.3        4.14       0.50       0         
+3                0.7        0.3        4.14       0.50       0         
+1                0.35       0.25       4.14       0.50       0         
+4                0.7        0.3        4.14       0.50       0         
+5                0.7        0.3        4.14       0.50       0         
+6                0.7        0.3        4.14       0.50       0         
+7                0.7        0.3        4.14       0.50       0         
+8                0.7        0.3        4.14       0.50       0         
+
+[JUNCTIONS]
+;;Name           Elevation  MaxDepth   InitDepth  SurDepth   Aponded   
+;;-------------- ---------- ---------- ---------- ---------- ----------
+9                1000       3          0          0          0         
+10               995        3          0          0          0         
+13               995        3          0          0          0         
+14               990        3          0          0          0         
+15               987        3          0          0          0         
+16               985        3          0          0          0         
+17               980        3          0          0          0         
+19               1010       3          0          0          0         
+20               1005       3          0          0          0         
+21               990        3          0          0          0         
+22               987        3          0          0          0         
+23               990        3          0          0          0         
+24               984        3          0          0          0         
+
+[OUTFALLS]
+;;Name           Elevation  Type       Stage Data       Gated    Route To        
+;;-------------- ---------- ---------- ---------------- -------- ----------------
+18               975        FREE                        NO                       
+
+[STORAGE]
+;;               Invert   Max.     Init.    Storage    Curve                               Evap.   
+;;Name           Elev.    Depth    Depth    Curve      Params                              Frac.    Infiltration parameters
+;;-------------- -------- -------- -------- ---------- -------- -------- -------- -------- -------- -----------------------
+SU1              0        1        0        FUNCTIONAL 1000     0        0        0        0       
+SU2              0        5        0        FUNCTIONAL 1000     0        0        0        0       
+
+[CONDUITS]
+;;Name           From Node        To Node          Length     Roughness  InOffset   OutOffset  InitFlow   MaxFlow   
+;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ----------
+1                9                10               400        0.01       0          0          0          0         
+4                19               20               200        0.01       0          0          0          0         
+5                20               21               200        0.01       0          0          0          0         
+6                10               21               400        0.01       0          1          0          0         
+7                21               22               300        0.01       1          1          0          0         
+8                22               16               300        0.01       0          0          0          0         
+10               17               18               400        0.01       0          0          0          0         
+11               13               14               400        0.01       0          0          0          0         
+12               14               15               400        0.01       0          0          0          0         
+13               15               16               400        0.01       0          0          0          0         
+14               23               24               400        0.01       0          0          0          0         
+15               16               24               100        0.01       0          0          0          0         
+16               24               17               400        0.01       0          0          0          0         
+
+[PUMPS]
+;;               Inlet            Outlet           Pump             Init.  Startup  Shutoff 
+;;Name           Node             Node             Curve            Status Depth    Depth   
+;;-------------- ---------------- ---------------- ---------------- ------ -------- --------
+P1               SU1              SU2              Curve1           ON     0        0       
+
+[XSECTIONS]
+;;Link           Shape        Geom1            Geom2      Geom3      Geom4      Barrels    Culvert   
+;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ----------
+1                CIRCULAR     1.5              0          0          0          1                    
+4                CIRCULAR     1                0          0          0          1                    
+5                CIRCULAR     1                0          0          0          1                    
+6                CIRCULAR     1                0          0          0          1                    
+7                CIRCULAR     2                0          0          0          1                    
+8                CIRCULAR     2                0          0          0          1                    
+10               CIRCULAR     2                0          0          0          1                    
+11               CIRCULAR     1.5              0          0          0          1                    
+12               CIRCULAR     1.5              0          0          0          1                    
+13               CIRCULAR     1.5              0          0          0          1                    
+14               CIRCULAR     1                0          0          0          1                    
+15               CIRCULAR     2                0          0          0          1                    
+16               CIRCULAR     2                0          0          0          1                    
+
+[POLLUTANTS]
+;;Name           Units  Crain      Cgw        Crdii      Kdecay     SnowOnly   Co-Pollutant     Co-Frac    Cdwf       Cinit     
+;;-------------- ------ ---------- ---------- ---------- ---------- ---------- ---------------- ---------- ---------- ----------
+TSS              MG/L   0.0        0.0        0          0.0        NO         *                0.0        0          0         
+Lead             UG/L   0.0        0.0        0          0.0        NO         TSS              0.2        0          0         
+
+[LANDUSES]
+;;               Sweeping   Fraction   Last      
+;;Name           Interval   Available  Swept     
+;;-------------- ---------- ---------- ----------
+Residential                                      
+Undeveloped                                      
+
+[COVERAGES]
+;;Subcatchment   Land Use         Percent   
+;;-------------- ---------------- ----------
+2                Residential      50.00     
+2                Undeveloped      50.00     
+3                Residential      100.00    
+1                Residential      100.00    
+4                Residential      50.00     
+4                Undeveloped      50.00     
+5                Residential      100.00    
+6                Undeveloped      100.00    
+7                Undeveloped      100.00    
+8                Undeveloped      100.00    
+
+[LOADINGS]
+;;Subcatchment   Pollutant        Buildup   
+;;-------------- ---------------- ----------
+
+[BUILDUP]
+;;Land Use       Pollutant        Function   Coeff1     Coeff2     Coeff3     Per Unit  
+;;-------------- ---------------- ---------- ---------- ---------- ---------- ----------
+Residential      TSS              SAT        50         0          2          AREA      
+Residential      Lead             NONE       0          0          0          AREA      
+Undeveloped      TSS              SAT        100        0          3          AREA      
+Undeveloped      Lead             NONE       0          0          0          AREA      
+
+[WASHOFF]
+;;Land Use       Pollutant        Function   Coeff1     Coeff2     SweepRmvl  BmpRmvl   
+;;-------------- ---------------- ---------- ---------- ---------- ---------- ----------
+Residential      TSS              EXP        0.1        1          0          0         
+Residential      Lead             EMC        0          0          0          0         
+Undeveloped      TSS              EXP        0.1        0.7        0          0         
+Undeveloped      Lead             EMC        0          0          0          0         
+
+[CURVES]
+;;Name           Type       X-Value    Y-Value   
+;;-------------- ---------- ---------- ----------
+Curve1           Pump3      1          0         
+Curve1                      10         1         
+
+[TIMESERIES]
+;;Name           Date       Time       Value     
+;;-------------- ---------- ---------- ----------
+;RAINFALL
+TS1                         0:00       0.0       
+TS1                         1:00       0.25      
+TS1                         2:00       0.5       
+TS1                         3:00       0.8       
+TS1                         4:00       0.4       
+TS1                         5:00       0.1       
+TS1                         6:00       0.0       
+TS1                         27:00      0.0       
+TS1                         28:00      0.4       
+TS1                         29:00      0.2       
+TS1                         30:00      0.0       
+
+[REPORT]
+;;Reporting Options
+INPUT      NO
+CONTROLS   NO
+SUBCATCHMENTS ALL
+NODES ALL
+LINKS ALL
+
+[TAGS]
+
+[MAP]
+DIMENSIONS 0.000 0.000 10000.000 10000.000
+Units      None
+
+[COORDINATES]
+;;Node           X-Coord            Y-Coord           
+;;-------------- ------------------ ------------------
+9                4042.110           9600.000          
+10               4105.260           6947.370          
+13               2336.840           4357.890          
+14               3157.890           4294.740          
+15               3221.050           3242.110          
+16               4821.050           3326.320          
+17               6252.630           2147.370          
+19               7768.420           6736.840          
+20               5957.890           6589.470          
+21               4926.320           6105.260          
+22               4421.050           4715.790          
+23               6484.210           3978.950          
+24               5389.470           3031.580          
+18               6631.580           505.260           
+SU1              6876.91            50.471          
+
+[VERTICES]
+;;Link           X-Coord            Y-Coord           
+;;-------------- ------------------ ------------------
+10               6673.680           1368.420          
+
+[Polygons]
+;;Subcatchment   X-Coord            Y-Coord           
+;;-------------- ------------------ ------------------
+2                7600.000           9663.160          
+2                7705.260           6736.840          
+2                5915.790           6694.740          
+2                4926.320           6294.740          
+2                4189.470           7200.000          
+2                4126.320           9621.050          
+3                2357.890           6021.050          
+3                2400.000           4336.840          
+3                3031.580           4252.630          
+3                2989.470           3389.470          
+3                315.790            3410.530          
+3                294.740            6000.000          
+1                3936.840           6905.260          
+1                3494.740           6252.630          
+1                273.680            6336.840          
+1                252.630            8526.320          
+1                463.160            9200.000          
+1                1157.890           9726.320          
+1                4000.000           9705.260          
+4                3473.680           6105.260          
+4                3915.790           6421.050          
+4                4168.420           6694.740          
+4                4463.160           6463.160          
+4                4821.050           6063.160          
+4                4400.000           5263.160          
+4                4357.890           4442.110          
+4                4547.370           3705.260          
+4                4000.000           3431.580          
+4                3326.320           3368.420          
+4                3242.110           3536.840          
+4                3136.840           5157.890          
+4                2589.470           5178.950          
+4                2589.470           6063.160          
+4                3284.210           6063.160          
+4                3705.260           6231.580          
+4                4126.320           6715.790          
+5                2568.420           3200.000          
+5                4905.260           3136.840          
+5                5221.050           2842.110          
+5                5747.370           2421.050          
+5                6463.160           1578.950          
+5                6610.530           968.420           
+5                6589.470           505.260           
+5                1305.260           484.210           
+5                968.420            336.840           
+5                315.790            778.950           
+5                315.790            3115.790          
+6                9052.630           4147.370          
+6                7894.740           4189.470          
+6                6442.110           4105.260          
+6                5915.790           3642.110          
+6                5326.320           3221.050          
+6                4631.580           4231.580          
+6                4568.420           5010.530          
+6                4884.210           5768.420          
+6                5368.420           6294.740          
+6                6042.110           6568.420          
+6                8968.420           6526.320          
+7                8736.840           9642.110          
+7                9010.530           9389.470          
+7                9010.530           8631.580          
+7                9052.630           6778.950          
+7                7789.470           6800.000          
+7                7726.320           9642.110          
+8                9073.680           2063.160          
+8                9052.630           778.950           
+8                8505.260           336.840           
+8                7431.580           315.790           
+8                7410.530           484.210           
+8                6842.110           505.260           
+8                6842.110           589.470           
+8                6821.050           1178.950          
+8                6547.370           1831.580          
+8                6147.370           2378.950          
+8                5600.000           3073.680          
+8                6589.470           3894.740          
+8                8863.160           3978.950          
+
+[SYMBOLS]
+;;Gage           X-Coord            Y-Coord           
+;;-------------- ------------------ ------------------
+RG1              10084.210          8210.530          
+
diff --git a/swmm-toolkit/tests/data/test_Example1.out b/swmm-toolkit/tests/data/test_Example1.out
new file mode 100644
index 00000000..78b15a8b
Binary files /dev/null and b/swmm-toolkit/tests/data/test_Example1.out differ
diff --git a/swmm-toolkit/tests/data/test_Example2.inp b/swmm-toolkit/tests/data/test_Example2.inp
new file mode 100644
index 00000000..3429785b
--- /dev/null
+++ b/swmm-toolkit/tests/data/test_Example2.inp
@@ -0,0 +1,165 @@
+[TITLE]
+;;Project Title/Notes
+ Example 5
+ A simple groundwater example.
+
+[OPTIONS]
+;;Option             Value
+FLOW_UNITS           CFS
+INFILTRATION         HORTON
+FLOW_ROUTING         KINWAVE
+LINK_OFFSETS         DEPTH
+MIN_SLOPE            0
+ALLOW_PONDING        NO
+SKIP_STEADY_STATE    NO
+
+START_DATE           09/13/2014
+START_TIME           00:00:00
+REPORT_START_DATE    09/13/2014
+REPORT_START_TIME    00:00:00
+END_DATE             09/15/2014
+END_TIME             00:00:00
+SWEEP_START          01/01
+SWEEP_END            12/31
+DRY_DAYS             0
+REPORT_STEP          01:00:00
+WET_STEP             00:30:00
+DRY_STEP             01:00:00
+ROUTING_STEP         0:00:30
+RULE_STEP            00:00:00
+
+INERTIAL_DAMPING     PARTIAL
+NORMAL_FLOW_LIMITED  BOTH
+FORCE_MAIN_EQUATION  H-W
+VARIABLE_STEP        0.75
+LENGTHENING_STEP     0
+MIN_SURFAREA         12.557
+MAX_TRIALS           8
+HEAD_TOLERANCE       0.005
+SYS_FLOW_TOL         5
+LAT_FLOW_TOL         5
+MINIMUM_STEP         0.5
+THREADS              1
+
+[EVAPORATION]
+;;Data Source    Parameters
+;;-------------- ----------------
+CONSTANT         0.0
+DRY_ONLY         NO
+
+[RAINGAGES]
+;;Name           Format    Interval SCF      Source
+;;-------------- --------- ------ ------ ----------
+1                INTENSITY 0:15     1.0      TIMESERIES Rainfall
+
+[SUBCATCHMENTS]
+;;Name           Rain Gage        Outlet           Area     %Imperv  Width    %Slope   CurbLen  SnowPack
+;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- ----------------
+wBC              1                2                5        50       100      1        0
+woBC             1                2                5        50       100      1        0
+
+[SUBAREAS]
+;;Subcatchment   N-Imperv   N-Perv     S-Imperv   S-Perv     PctZero    RouteTo    PctRouted
+;;-------------- ---------- ---------- ---------- ---------- ---------- ---------- ----------
+wBC              0.01       0.1        0.05       0.05       25         OUTLET
+woBC             0.01       0.1        0.05       0.05       25         OUTLET
+
+[INFILTRATION]
+;;Subcatchment   MaxRate    MinRate    Decay      DryTime    MaxInfil
+;;-------------- ---------- ---------- ---------- ---------- ----------
+wBC              1.2        0.1        2          7          0
+woBC             1.2        0.1        2          7          0
+
+[LID_CONTROLS]
+;;Name           Type/Layer Parameters
+;;-------------- ---------- ----------
+BC               BC
+BC               SURFACE    6          .25        0.1        1.0        5
+BC               SOIL       12         0.5        0.2        0.1        0.5        10.0       3.5
+BC               STORAGE    12         0.75       0.5        0
+BC               DRAIN      0          0.5        6          6          0          0
+
+[LID_USAGE]
+;;Subcatchment   LID Process      Number  Area       Width      InitSat    FromImp    ToPerv     RptFile                  DrainTo          FromPerv
+;;-------------- ---------------- ------- ---------- ---------- ---------- ---------- ---------- ------------------------ ---------------- ----------
+wBC              BC               100     50         10         0          25         1          "feng.txt"               *                0
+
+[OUTFALLS]
+;;Name           Elevation  Type       Stage Data       Gated    Route To
+;;-------------- ---------- ---------- ---------------- -------- ----------------
+2                0          FREE                        NO
+
+[TIMESERIES]
+;;Name           Date       Time       Value
+;;-------------- ---------- ---------- ----------
+Rainfall                    0:00       0.037
+Rainfall                    0:15       0.111
+Rainfall                    0:30       0.185
+Rainfall                    0:45       0.259
+Rainfall                    1:00       0.333
+Rainfall                    1:15       0.407
+Rainfall                    1:30       0.481
+Rainfall                    1:45       0.556
+Rainfall                    2:00       0.630
+Rainfall                    2:15       0.644
+Rainfall                    2:30       0.600
+Rainfall                    2:45       0.556
+Rainfall                    3:00       0.511
+Rainfall                    3:15       0.467
+Rainfall                    3:30       0.422
+Rainfall                    3:45       0.378
+Rainfall                    4:00       0.333
+Rainfall                    4:15       0.289
+Rainfall                    4:30       0.244
+Rainfall                    4:45       0.200
+Rainfall                    5:00       0.156
+Rainfall                    5:15       0.111
+Rainfall                    5:30       0.067
+Rainfall                    5:45       0.022
+Rainfall                    6:00       0
+
+[REPORT]
+;;Reporting Options
+INPUT      YES
+CONTROLS   YES
+SUBCATCHMENTS ALL
+NODES ALL
+LINKS ALL
+
+[TAGS]
+
+[MAP]
+DIMENSIONS 2806.303 3109.860 6292.997 8439.437
+Units      None
+
+[COORDINATES]
+;;Node           X-Coord            Y-Coord
+;;-------------- ------------------ ------------------
+2                4556.338           3352.113
+
+[VERTICES]
+;;Link           X-Coord            Y-Coord
+;;-------------- ------------------ ------------------
+
+[Polygons]
+;;Subcatchment   X-Coord            Y-Coord
+;;-------------- ------------------ ------------------
+wBC              2964.789           4971.831
+wBC              6134.511           4986.413
+wBC              6134.511           7445.652
+wBC              2964.789           7450.704
+woBC             2957.539           7437.452
+woBC             2957.539           7437.452
+woBC             2957.539           7437.452
+woBC             3000.569           4960.151
+woBC             3000.569           4960.151
+woBC             1445.340           5236.773
+woBC             480.238            5464.218
+woBC             369.590            6558.410
+woBC             443.355            7425.158
+woBC             2963.686           7437.452
+
+[SYMBOLS]
+;;Gage           X-Coord            Y-Coord
+;;-------------- ------------------ ------------------
+1                4485.915           8197.183
diff --git a/swmm-toolkit/tests/test_metadata.py b/swmm-toolkit/tests/test_metadata.py
new file mode 100644
index 00000000..25df8839
--- /dev/null
+++ b/swmm-toolkit/tests/test_metadata.py
@@ -0,0 +1,99 @@
+#
+#   test_metadata.py
+#
+#   Created:  Apr 8, 2020
+#   Modified: Apr 20, 2020
+#
+#   Author:    See AUTHORS
+#
+#   Unit testing for SWMM output metadata using pytest.
+#
+
+
+import os
+
+import pytest
+
+from swmm.toolkit import output, shared_enum, output_metadata
+
+
+DATA_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
+OUTPUT_FILE_EXAMPLE1 = os.path.join(DATA_PATH, 'test_Example1.out')
+
+
+@pytest.fixture()
+def handle(request):
+    _handle = output.init()
+    output.open(_handle, OUTPUT_FILE_EXAMPLE1)
+
+    def close():
+        output.close(_handle)
+
+    request.addfinalizer(close)
+    return _handle
+
+
+def test_outputmetadata(handle):
+
+    om = output_metadata.OutputMetadata(handle)
+
+    ref = {
+        shared_enum.SubcatchAttribute.RAINFALL:           ("Rainfall",                "in/hr"),
+        shared_enum.SubcatchAttribute.SNOW_DEPTH:         ("Snow Depth",              "in"),
+        shared_enum.SubcatchAttribute.EVAP_LOSS:          ("Evaporation Loss",        "in/day"),
+        shared_enum.SubcatchAttribute.INFIL_LOSS:         ("Infiltration Loss",       "in/hr"),
+        shared_enum.SubcatchAttribute.RUNOFF_RATE:        ("Runoff Rate",             "cu ft/sec"),
+        shared_enum.SubcatchAttribute.GW_OUTFLOW_RATE:    ("Groundwater Flow Rate",   "cu ft/sec"),
+        shared_enum.SubcatchAttribute.GW_TABLE_ELEV:      ("Groundwater Elevation",   "ft"),
+        shared_enum.SubcatchAttribute.SOIL_MOISTURE:      ("Soil Moisture",           "%"),
+        shared_enum.SubcatchAttribute.POLLUT_CONC_0:      ("TSS",                     "mg/L"),
+        shared_enum.SubcatchAttribute.POLLUT_CONC_1:      ("Lead",                    "ug/L"),
+
+        shared_enum.NodeAttribute.INVERT_DEPTH:           ("Invert Depth",            "ft"),
+        shared_enum.NodeAttribute.HYDRAULIC_HEAD:         ("Hydraulic Head",          "ft"),
+        shared_enum.NodeAttribute.PONDED_VOLUME:          ("Ponded Volume",           "cu ft"),
+        shared_enum.NodeAttribute.LATERAL_INFLOW:         ("Lateral Inflow",          "cu ft/sec"),
+        shared_enum.NodeAttribute.TOTAL_INFLOW:           ("Total Inflow",            "cu ft/sec"),
+        shared_enum.NodeAttribute.FLOODING_LOSSES:        ("Flooding Loss",           "cu ft/sec"),
+        shared_enum.NodeAttribute.POLLUT_CONC_0:          ("TSS",                     "mg/L"),
+        shared_enum.NodeAttribute.POLLUT_CONC_1:          ("Lead",                    "ug/L"),
+
+        shared_enum.LinkAttribute.FLOW_RATE:              ("Flow Rate",               "cu ft/sec"),
+        shared_enum.LinkAttribute.FLOW_DEPTH:             ("Flow Depth",              "ft"),
+        shared_enum.LinkAttribute.FLOW_VELOCITY:          ("Flow Velocity",           "ft/sec"),
+        shared_enum.LinkAttribute.FLOW_VOLUME:            ("Flow Volume",             "cu ft"),
+        shared_enum.LinkAttribute.CAPACITY:               ("Capacity",                "%"),
+        shared_enum.LinkAttribute.POLLUT_CONC_0:          ("TSS",                     "mg/L"),
+        shared_enum.LinkAttribute.POLLUT_CONC_1:          ("Lead",                    "ug/L"),
+
+        shared_enum.SystemAttribute.AIR_TEMP:             ("Temperature",             "deg F"),
+        shared_enum.SystemAttribute.RAINFALL:             ("Rainfall",                "in/hr"),
+        shared_enum.SystemAttribute.SNOW_DEPTH:           ("Snow Depth",              "in"),
+        shared_enum.SystemAttribute.EVAP_INFIL_LOSS:      ("Evap and Infil Losses",   "in/hr"),
+        shared_enum.SystemAttribute.RUNOFF_FLOW:          ("Runoff Flow Rate",        "cu ft/sec"),
+        shared_enum.SystemAttribute.DRY_WEATHER_INFLOW:   ("Dry Weather Inflow",      "cu ft/sec"),
+        shared_enum.SystemAttribute.GW_INFLOW:            ("Groundwater Inflow",      "cu ft/sec"),
+        shared_enum.SystemAttribute.RDII_INFLOW:          ("RDII Inflow",             "cu ft/sec"),
+        shared_enum.SystemAttribute.DIRECT_INFLOW:        ("Direct Inflow",           "cu ft/sec"),
+        shared_enum.SystemAttribute.TOTAL_LATERAL_INFLOW: ("Total Lateral Inflow",    "cu ft/sec"),
+        shared_enum.SystemAttribute.FLOOD_LOSSES:         ("Flood Losses",            "cu ft/sec"),
+        shared_enum.SystemAttribute.OUTFALL_FLOWS:        ("Outfall Flow",            "cu ft/sec"),
+        shared_enum.SystemAttribute.VOLUME_STORED:        ("Volume Stored",           "cu ft"),
+        shared_enum.SystemAttribute.EVAP_RATE:            ("Evaporation Rate",        "in/day")
+    }
+
+    for attr in shared_enum.SubcatchAttribute:
+        temp = om.get_attribute_metadata(attr)
+        assert temp == ref[attr]
+
+    for attr in shared_enum.NodeAttribute:
+        temp = om.get_attribute_metadata(attr)
+        assert temp == ref[attr]
+
+    for attr in shared_enum.LinkAttribute:
+        temp = om.get_attribute_metadata(attr)
+        assert temp == ref[attr]
+
+    for attr in shared_enum.SystemAttribute:
+        temp = om.get_attribute_metadata(attr)
+        assert temp == ref[attr]
diff --git a/swmm-toolkit/tests/test_output.py b/swmm-toolkit/tests/test_output.py
new file mode 100644
index 00000000..9e3224f8
--- /dev/null
+++ b/swmm-toolkit/tests/test_output.py
@@ -0,0 +1,196 @@
+#
+#   test_output.py
+#
+#   Created: Aug 7, 2018
+#   Updated: Apr 20, 2020
+#
+#   Author:  See AUTHORS
+#
+#   Unit testing for SWMM Output API using pytest.
+#
+
+
+import os
+
+import pytest
+import numpy as np
+
+from swmm.toolkit import output, shared_enum, output_metadata
+
+
+DATA_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
+OUTPUT_FILE_EXAMPLE1 = os.path.join(DATA_PATH, 'test_Example1.out')
+
+
+def test_openclose():
+    _handle = output.init()
+    output.open(_handle, OUTPUT_FILE_EXAMPLE1)
+    output.close(_handle)
+
+
+def test_opencloseopenclose():
+    _handle = output.init()
+    output.open(_handle, OUTPUT_FILE_EXAMPLE1)
+    output.close(_handle)
+    _handle2 = output.init()
+    output.open(_handle2, OUTPUT_FILE_EXAMPLE1)
+    output.close(_handle2)
+
+
+@pytest.fixture()
+def handle(request):
+    _handle = output.init()
+    output.open(_handle, OUTPUT_FILE_EXAMPLE1)
+
+    def close():
+        output.close(_handle)
+
+    request.addfinalizer(close)
+    return _handle
+
+
+def test_getversion(handle):
+
+  assert output.get_version(handle) == 51000
+
+
+def test_getprojectsize(handle):
+
+   assert output.get_proj_size(handle) == [8, 14, 13, 1, 2]
+
+
+# def test_getflowunits(handle):
+#
+#    assert output.getunits(handle)[0] == output.FlowUnits.CFS.value
+#
+#
+# def test_getpollutantunits(handle):
+#
+#    assert output.getunits(handle)[2:] == [output.ConcUnits.MG.value, output.ConcUnits.UG.value]
+
+
+def test_getstartdate(handle):
+
+    assert output.get_start_date(handle) == 35796
+
+
+def test_gettimes(handle):
+
+    assert output.get_times(handle, shared_enum.Time.REPORT_STEP) == 3600
+    assert output.get_times(handle, shared_enum.Time.NUM_PERIODS) == 36
+
+
+def test_getelementname(handle):
+
+    assert output.get_elem_name(handle, shared_enum.ElementType.NODE, 1) == "10"
+
+
+def test_getsubcatchseries(handle):
+
+    ref_array = np.array([0.0,
+                          1.2438242,
+                          2.5639679,
+                          4.524055,
+                          2.5115132,
+                          0.69808137,
+                          0.040894926,
+                          0.011605669,
+                          0.00509294,
+                          0.0027438672])
+
+    test_array = output.get_subcatch_series(handle, 1, shared_enum.SubcatchAttribute.RUNOFF_RATE, 0, 10)
+
+    assert len(test_array) == 10
+    assert np.allclose(test_array, ref_array)
+
+
+def test_getsubcatchattribute(handle):
+
+    ref_array = np.array([0.125,
+                          0.125,
+                          0.125,
+                          0.125,
+                          0.125,
+                          0.225,
+                          0.225,
+                          0.225])
+
+    test_array = output.get_subcatch_attribute(handle, 1, shared_enum.SubcatchAttribute.INFIL_LOSS)
+
+    assert len(test_array) == 8
+    assert np.allclose(test_array, ref_array)
+
+
+def test_getsubcatchresult(handle):
+
+    ref_array = np.array([0.5,
+                          0.0,
+                          0.0,
+                          0.125,
+                          1.2438242,
+                          0.0,
+                          0.0,
+                          0.0,
+                          33.481991,
+                          6.6963983])
+
+    test_array = output.get_subcatch_result(handle, 1, 1)
+
+    assert len(test_array) == 10
+    assert np.allclose(test_array, ref_array)
+
+
+def test_getnoderesult(handle):
+
+    ref_array = np.array([0.296234,
+                          995.296204,
+                          0.0,
+                          1.302650,
+                          1.302650,
+                          0.0,
+                          15.361463,
+                          3.072293])
+
+    test_array = output.get_node_result(handle, 2, 2)
+
+    assert len(test_array) == 8
+    assert np.allclose(test_array, ref_array)
+
+
+def test_getlinkresult(handle):
+
+    ref_array = np.array([4.631762,
+                          1.0,
+                          5.8973422,
+                          314.15927,
+                          1.0,
+                          19.070757,
+                          3.8141515])
+
+    test_array = output.get_link_result(handle, 3, 3)
+
+    assert len(test_array) == 7
+    assert np.allclose(test_array, ref_array)
+
+
+def test_getsystemresult(handle):
+
+    ref_array = np.array([70.0,
+                          0.1,
+                          0.0,
+                          0.19042271,
+                          14.172027,
+                          0.0,
+                          0.0,
+                          0.0,
+                          0.0,
+                          14.172027,
+                          0.55517411,
+                          13.622702,
+                          2913.0793,
+                          0.0])
+
+    test_array = output.get_system_result(handle, 4, 4)
+
+    assert len(test_array) == 14
+    assert np.allclose(test_array, ref_array)
diff --git a/swmm-toolkit/tests/test_solver.py b/swmm-toolkit/tests/test_solver.py
new file mode 100644
index 00000000..ae6a23e9
--- /dev/null
+++ b/swmm-toolkit/tests/test_solver.py
@@ -0,0 +1,661 @@
+#
+#  test_toolkit.py
+#
+#  Created:    Aug  8, 2018
+#  Updated:    Apr 20, 2020
+#
+#  Author:     See AUTHORS
+#
+
+
+import os
+
+import pytest
+
+from swmm.toolkit import solver, shared_enum
+
+DATA_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
+INPUT_FILE_EXAMPLE_1 = os.path.join(DATA_PATH, 'test_Example1.inp')
+REPORT_FILE_TEST_1 = os.path.join(DATA_PATH, 'temp_Example.rpt')
+OUTPUT_FILE_TEST_1 = os.path.join(DATA_PATH, 'temp_Example.out')
+INPUT_FILE_EXAMPLE_2 = os.path.join(DATA_PATH, 'test_Example2.inp')
+REPORT_FILE_TEST_2 = os.path.join(DATA_PATH, 'temp_Example2.rpt')
+OUTPUT_FILE_TEST_2 = os.path.join(DATA_PATH, 'temp_Example2.out')
+INPUT_FILE_FAIL = os.path.join(DATA_PATH, 'temp_nodata.inp')
+
+
+def test_run():
+    solver.swmm_run(INPUT_FILE_EXAMPLE_1, REPORT_FILE_TEST_1, OUTPUT_FILE_TEST_1)
+
+
+def test_openclose():
+    solver.swmm_open(INPUT_FILE_EXAMPLE_1, REPORT_FILE_TEST_1, OUTPUT_FILE_TEST_1)
+    solver.swmm_close()
+
+
+def test_errorhandling():
+    with pytest.raises(Exception):
+        solver.swmm_open(INPUT_FILE_FAIL, REPORT_FILE_TEST_1, OUTPUT_FILE_TEST_1)
+
+
+@pytest.fixture()
+def handle(request):
+    solver.swmm_open(INPUT_FILE_EXAMPLE_1, REPORT_FILE_TEST_1, OUTPUT_FILE_TEST_1)
+
+    def close():
+        solver.swmm_close()
+
+    request.addfinalizer(close)
+
+
+@pytest.fixture()
+def run_sim(request):
+    solver.swmm_open(INPUT_FILE_EXAMPLE_1, REPORT_FILE_TEST_1, OUTPUT_FILE_TEST_1)
+    solver.swmm_start(0)
+
+    def close():
+        solver.swmm_end()
+        solver.swmm_close()
+
+    request.addfinalizer(close)
+
+
+@pytest.fixture()
+def lid_handle(request):
+    solver.swmm_open(INPUT_FILE_EXAMPLE_2, REPORT_FILE_TEST_2, OUTPUT_FILE_TEST_2)
+
+
+    def close():
+        solver.swmm_close()
+
+    request.addfinalizer(close)
+
+
+@pytest.fixture()
+def run_lid_sim(request):
+    solver.swmm_open(INPUT_FILE_EXAMPLE_2, REPORT_FILE_TEST_2, OUTPUT_FILE_TEST_2)
+    solver.swmm_start(0)
+
+    def close():
+        solver.swmm_end()
+        solver.swmm_close()
+
+    request.addfinalizer(close)
+
+
+def test_step(handle):
+    solver.swmm_start(0)
+
+    while True:
+        time = solver.swmm_step()
+        if time == 0:
+            break
+
+    solver.swmm_end()
+    solver.swmm_report()
+
+
+def test_version(handle):
+    major, minor, patch = solver.swmm_version_info()
+    print(major, minor, patch)
+    assert major == '5'
+    
+
+def test_simulation_unit(handle):
+    simulation_system_unit_option = solver.simulation_get_unit(shared_enum.UnitProperty.SYSTEM_UNIT)
+    simulation_flow_unit_option = solver.simulation_get_unit(shared_enum.UnitProperty.FLOW_UNIT)
+    assert simulation_system_unit_option == shared_enum.UnitSystem.US.value
+    assert simulation_flow_unit_option == shared_enum.FlowUnits.CFS.value
+
+
+def test_object_find(handle):
+    rg_index = solver.project_get_index(shared_enum.ObjectType.GAGE, 'RG1')
+    sub_index_0 = solver.project_get_index(shared_enum.ObjectType.SUBCATCH, '2')
+    sub_index_1 = solver.project_get_index(shared_enum.ObjectType.SUBCATCH, '3')
+    node_index = solver.project_get_index(shared_enum.ObjectType.NODE, '24')
+    link_index = solver.project_get_index(shared_enum.ObjectType.LINK, '16')
+    assert rg_index == 0
+    assert sub_index_0 == 0
+    assert sub_index_1 == 1
+    assert node_index == 12
+    assert link_index == 12
+
+
+def test_error_message_handling(handle):
+    with pytest.raises(Exception) as exc_info:
+        solver.project_get_index(shared_enum.ObjectType.SUBCATCH, 'sloth')
+
+    assert 'API Key Error: Object index out of Bounds' in str(exc_info.value)
+
+
+def test_object_count(handle):
+    rg_count = solver.project_get_count(shared_enum.ObjectType.GAGE)
+    sub_count = solver.project_get_count(shared_enum.ObjectType.SUBCATCH)
+    assert rg_count == 1
+    assert sub_count == 8
+
+
+#def test_api_error_message(handle):
+#    error_message_501 = solver.error_get_message(501)
+#    assert error_message_501 == '\n API Key Error: Object Type Outside Bonds'
+
+
+def test_simulation_datetime(handle):
+    from datetime import datetime
+
+    year, month, day, hour, minute, second = solver.simulation_get_datetime(shared_enum.TimeProperty.START_DATE)
+    assert datetime(1998, 1, 1) == datetime(year, month, day, hour, minute, second)
+    year, month, day, hour, minute, second = solver.simulation_get_datetime(shared_enum.TimeProperty.END_DATE)
+    assert datetime(1998, 1, 2, 12) == datetime(year, month, day, hour, minute, second)
+    year, month, day, hour, minute, second = solver.simulation_get_datetime(shared_enum.TimeProperty.REPORT_DATE)
+    assert datetime(1998, 1, 1) == datetime(year, month, day, hour, minute, second)
+
+    solver.simulation_set_datetime(shared_enum.TimeProperty.START_DATE, 2020, 1, 1, 12, 59, 59)
+    year, month, day, hour, minute, second = solver.simulation_get_datetime(shared_enum.TimeProperty.START_DATE)
+    assert datetime(2020, 1, 1, 12, 59, 59) == datetime(year, month, day, hour, minute, second)
+
+
+def test_simulation_get_current_datetime(run_sim):
+    from datetime import datetime
+    current_datetime = datetime(*solver.simulation_get_current_datetime())
+    assert current_datetime == datetime(1998, 1, 1)
+
+
+def test_simulation_analysis_setting(handle):
+    allow_ponding = solver.simulation_get_setting(0)
+    skip_steady = solver.simulation_get_setting(1)
+    ignore_rain = solver.simulation_get_setting(2)
+    ignore_gw = solver.simulation_get_setting(5)
+    assert allow_ponding == 0
+    assert skip_steady == 0
+    assert ignore_rain == 0
+    assert ignore_gw == 1
+
+
+def test_simulation_parameter(handle):
+    route_step = solver.simulation_get_parameter(0)
+    min_route_step = solver.simulation_get_parameter(1)
+    min_slope = solver.simulation_get_parameter(6)
+    head_tolerance = solver.simulation_get_parameter(11)
+    assert route_step == 60
+    assert min_route_step == 0.5
+    assert min_slope == 0
+    assert head_tolerance ==  0
+
+
+def test_object_get_index(handle):
+    with pytest.raises(Exception) as exc_info:
+        node_index = solver.project_get_index(shared_enum.ObjectType.NODE, '6')
+        sub_index = solver.project_get_index(shared_enum.ObjectType.SUBCATCH, '20')
+
+#    assert node_index == -1
+#    assert sub_index == -1
+
+    node_index = solver.project_get_index(shared_enum.ObjectType.NODE, '20')
+    sub_index = solver.project_get_index(shared_enum.ObjectType.SUBCATCH, '6')
+
+    assert node_index == 8
+    assert sub_index == 5
+
+
+def test_object_get_id(handle):
+    node_name = solver.project_get_id(shared_enum.ObjectType.NODE, 8)
+    sub_name = solver.project_get_id(shared_enum.ObjectType.SUBCATCH, 5)
+    assert node_name == '20'
+    assert sub_name == '6'
+
+
+def test_link_get_type(handle):
+    link_type = solver.link_get_type(2)
+    assert link_type == shared_enum.LinkType.CONDUIT
+
+
+def test_link_get_connections(handle):
+    us_node, ds_node = solver.link_get_connections(4)
+    assert us_node == 9
+    assert ds_node == 10
+
+    us_node, ds_node = solver.link_get_connections(0)
+    assert us_node == 0
+    assert ds_node == 1
+
+
+def test_link_get_direction(handle):
+    for index in range(0, 13):
+        link_direction = solver.link_get_direction(index)
+        assert link_direction == shared_enum.LinkDirection.UPSTREAM_TO_DOWNSTREAM.value
+
+
+def test_link_param(handle):
+    offset_1 = solver.link_get_parameter(0, shared_enum.LinkProperty.OFFSET_1)
+    offset_2 = solver.link_get_parameter(0, shared_enum.LinkProperty.OFFSET_2)
+    initial_flow = solver.link_get_parameter(0, shared_enum.LinkProperty.INITIAL_FLOW)
+    flow_limit = solver.link_get_parameter(0, shared_enum.LinkProperty.FLOW_LIMIT)
+    inlet_loss = solver.link_get_parameter(0, shared_enum.LinkProperty.INLET_LOSS)
+    outlet_loss = solver.link_get_parameter(0, shared_enum.LinkProperty.OUTLET_LOSS)
+    average_loss = solver.link_get_parameter(0, shared_enum.LinkProperty.AVERAGE_LOSS)
+
+    assert offset_1 == 0
+    assert offset_2 == 0
+    assert initial_flow == 0
+    assert flow_limit == 0
+    assert inlet_loss == 0
+    assert outlet_loss == 0
+    assert average_loss == 0
+
+    solver.link_set_parameter(0, shared_enum.LinkProperty.OFFSET_1, 0.2)
+    solver.link_set_parameter(0, shared_enum.LinkProperty.OFFSET_2, 0.1)
+    solver.link_set_parameter(0, shared_enum.LinkProperty.INITIAL_FLOW, 0.1)
+    solver.link_set_parameter(0, shared_enum.LinkProperty.FLOW_LIMIT, 1)
+    solver.link_set_parameter(0, shared_enum.LinkProperty.INLET_LOSS, 0.5)
+    solver.link_set_parameter(0, shared_enum.LinkProperty.OUTLET_LOSS, 1.0)
+    solver.link_set_parameter(0, shared_enum.LinkProperty.AVERAGE_LOSS, 0.2)
+
+    offset_1 = solver.link_get_parameter(0, shared_enum.LinkProperty.OFFSET_1)
+    offset_2 = solver.link_get_parameter(0, shared_enum.LinkProperty.OFFSET_2)
+    initial_flow = solver.link_get_parameter(0, shared_enum.LinkProperty.INITIAL_FLOW)
+    flow_limit = solver.link_get_parameter(0, shared_enum.LinkProperty.FLOW_LIMIT)
+    inlet_loss = solver.link_get_parameter(0, shared_enum.LinkProperty.INLET_LOSS)
+    outlet_loss = solver.link_get_parameter(0, shared_enum.LinkProperty.OUTLET_LOSS)
+    average_loss = solver.link_get_parameter(0, shared_enum.LinkProperty.AVERAGE_LOSS)
+
+    assert offset_1 == 0.2
+    assert offset_2 == 0.1
+    assert initial_flow == 0.1
+    assert flow_limit == 1
+    assert inlet_loss == 0.5
+    assert outlet_loss == 1.0
+    assert average_loss == 0.2
+
+
+def test_link_get_result(run_sim):
+    flow = solver.link_get_result(0, shared_enum.LinkResult.FLOW)
+    depth = solver.link_get_result(0, shared_enum.LinkResult.DEPTH)
+    assert flow == 0.0
+    assert depth == 0.0
+    for i in range(0, 100):
+        solver.swmm_step()
+    flow = solver.link_get_result(0, shared_enum.LinkResult.FLOW)
+    depth = solver.link_get_result(0, shared_enum.LinkResult.DEPTH)
+    assert flow == pytest.approx(1.054, 0.1)
+    assert depth == pytest.approx(0.268, 0.1)
+
+
+def test_link_get_pollutant(run_sim):
+    pollut = solver.link_get_pollutant(0, shared_enum.NodePollutant.QUALITY)
+    tss, lead = tuple(pollut)
+    assert tss == 0.0
+    assert lead == 0.0
+
+    for i in range(0, 250):
+        solver.swmm_step()
+    pollut = solver.link_get_pollutant(0, shared_enum.NodePollutant.QUALITY)
+
+    tss, lead = tuple(pollut)
+
+    assert tss == pytest.approx(14.7179, 0.1)
+    assert lead == pytest.approx(2.9435, 0.1)
+
+
+def test_link_set_target_setting(run_sim):
+    target_setting = solver.link_get_result(0, shared_enum.LinkResult.TARGET_SETTING)
+    assert target_setting == 1.0
+    solver.link_set_target_setting(0, 0.5)
+    target_setting = solver.link_get_result(0, shared_enum.LinkResult.TARGET_SETTING)
+    assert target_setting == 0.5
+
+    for i in range(0, 250):
+        solver.swmm_step()
+
+    target_setting = solver.link_get_result(0, shared_enum.LinkResult.TARGET_SETTING)
+    target_setting == 0.5
+
+
+def test_link_get_stats(run_sim):
+    while True:
+        time = solver.swmm_step()
+        if time == 0:
+            break
+
+    link_stats = solver.link_get_stats(0)
+
+
+def test_pump_get_stats(run_sim):
+    index = solver.project_get_index(shared_enum.ObjectType.LINK, 'P1')
+    while True:
+        time = solver.swmm_step()
+        if time == 0:
+            break
+
+    stats = solver.pump_get_stats(index)
+
+
+def test_node_get_type(handle):
+    node_type = solver.node_get_type(8)
+    assert node_type == shared_enum.NodeType.JUNCTION
+    node_index = solver.project_get_index(shared_enum.ObjectType.NODE, '18')
+    node_type = solver.node_get_type(node_index)
+    assert node_type == shared_enum.NodeType.OUTFALL
+
+
+def test_node_param(handle):
+    invert_elevation = solver.node_get_parameter(0, shared_enum.NodeProperty.INVERT_ELEVATION)
+    full_depth = solver.node_get_parameter(0, shared_enum.NodeProperty.FULL_DEPTH)
+    surcharge_depth = solver.node_get_parameter(0, shared_enum.NodeProperty.SURCHARGE_DEPTH)
+    pond_area = solver.node_get_parameter(0, shared_enum.NodeProperty.POND_AREA)
+    initial_depth = solver.node_get_parameter(0, shared_enum.NodeProperty.INITIAL_DEPTH)
+
+    assert invert_elevation == 1000
+    assert full_depth == 3
+    assert surcharge_depth == 0
+    assert pond_area == 0
+    assert initial_depth == 0
+
+    solver.node_set_parameter(0, shared_enum.NodeProperty.INVERT_ELEVATION, 90)
+    solver.node_set_parameter(0, shared_enum.NodeProperty.FULL_DEPTH, 10)
+    solver.node_set_parameter(0, shared_enum.NodeProperty.SURCHARGE_DEPTH, 100)
+    solver.node_set_parameter(0, shared_enum.NodeProperty.POND_AREA, 100)
+    solver.node_set_parameter(0, shared_enum.NodeProperty.INITIAL_DEPTH, 1)
+
+    invert_elevation = solver.node_get_parameter(0, shared_enum.NodeProperty.INVERT_ELEVATION)
+    full_depth = solver.node_get_parameter(0, shared_enum.NodeProperty.FULL_DEPTH)
+    surcharge_depth = solver.node_get_parameter(0, shared_enum.NodeProperty.SURCHARGE_DEPTH)
+    pond_area = solver.node_get_parameter(0, shared_enum.NodeProperty.POND_AREA)
+    initial_depth = solver.node_get_parameter(0, shared_enum.NodeProperty.INITIAL_DEPTH)
+
+    assert invert_elevation == 90
+    assert full_depth == 10
+    assert surcharge_depth == 100
+    assert pond_area == 100
+    assert initial_depth == 1
+
+
+def test_node_get_result(run_sim):
+    total_inflow = solver.node_get_result(0, shared_enum.NodeResult.TOTAL_INFLOW)
+    total_outflow = solver.node_get_result(0, shared_enum.NodeResult.TOTAL_OUTFLOW)
+    assert total_inflow == 0.0
+    assert total_outflow == 0.0
+    for i in range(0, 100):
+        solver.swmm_step()
+    total_inflow = solver.node_get_result(0, shared_enum.NodeResult.TOTAL_INFLOW)
+    total_outflow = solver.node_get_result(0, shared_enum.NodeResult.TOTAL_OUTFLOW)
+    assert total_inflow == pytest.approx(1.07, 0.1)
+    assert total_outflow == pytest.approx(1.07, 0.1)
+
+
+def test_node_get_pollutant(run_sim):
+    pollut = solver.node_get_pollutant(0, shared_enum.NodePollutant.QUALITY)
+    tss, lead = tuple(pollut)
+    assert tss == 0.0
+    assert lead == 0.0
+
+    for i in range(0, 250):
+        solver.swmm_step()
+    pollut = solver.node_get_pollutant(0, shared_enum.NodePollutant.QUALITY)
+
+    tss, lead = tuple(pollut)
+    assert tss == pytest.approx(14.7179, 0.1)
+    assert lead == pytest.approx(2.9435, 0.1)
+
+
+def test_node_total_inflow(run_sim):
+    total_inflow = solver.node_get_total_inflow(0)
+    assert total_inflow == 0.0
+
+    for i in range(0, 550):
+        solver.swmm_step()
+
+    total_inflow = solver.node_get_total_inflow(0)
+    assert total_inflow == pytest.approx(42639.814, 0.1)
+
+    solver.node_set_total_inflow(0, 1.0)
+
+    for i in range(0, 550):
+        solver.swmm_step()
+
+    total_inflow = solver.node_get_total_inflow(0)
+    assert total_inflow == pytest.approx(75644.471, 0.1)
+
+
+def test_outfall_set_stage(run_sim):
+    head = solver.node_get_result(13, shared_enum.NodeResult.HEAD)
+    head == 975
+
+    for i in range(0, 100):
+        solver.swmm_step()
+
+    head = solver.node_get_result(13, shared_enum.NodeResult.HEAD)
+    assert head == pytest.approx(975.519, 0.1)
+
+    solver.outfall_set_stage(13, 1000)
+    solver.swmm_step()
+
+
+def test_subcatch_param(handle):
+    width = solver.subcatch_get_parameter(0, shared_enum.SubcatchProperty.WIDTH)
+    area = solver.subcatch_get_parameter(0, shared_enum.SubcatchProperty.AREA)
+    impervious_fraction = solver.subcatch_get_parameter(0, shared_enum.SubcatchProperty.IMPERVIOUS_FRACTION)
+    slope = solver.subcatch_get_parameter(0, shared_enum.SubcatchProperty.SLOPE)
+    curb_length = solver.subcatch_get_parameter(0, shared_enum.SubcatchProperty.CURB_LENGTH)
+
+    assert width == 500
+    assert area == 10
+    assert impervious_fraction == 50/100
+    assert slope == 0.01/100
+    assert curb_length == 0
+
+    solver.subcatch_set_parameter(0, shared_enum.SubcatchProperty.WIDTH, 100)
+    solver.subcatch_set_parameter(0, shared_enum.SubcatchProperty.AREA, 50)
+    #solver.subcatch_set_parameter(0, shared_enum.SubcatchProperty.IMPERVIOUS_FRACTION, 100)
+    solver.subcatch_set_parameter(0, shared_enum.SubcatchProperty.SLOPE, 0.1)
+    solver.subcatch_set_parameter(0, shared_enum.SubcatchProperty.CURB_LENGTH, 10)
+
+    width = solver.subcatch_get_parameter(0, shared_enum.SubcatchProperty.WIDTH)
+    area = solver.subcatch_get_parameter(0, shared_enum.SubcatchProperty.AREA)
+    #impervious_fraction = solver.subcatch_get_parameter(0, shared_enum.SubcatchProperty.IMPERVIOUS_FRACTION)
+    slope = solver.subcatch_get_parameter(0, shared_enum.SubcatchProperty.SLOPE)
+    curb_length = solver.subcatch_get_parameter(0, shared_enum.SubcatchProperty.CURB_LENGTH)
+
+    assert width == 100
+    assert area == 50
+    #assert impervious_fraction == 100/100
+    assert slope == 0.1
+    assert curb_length == 10
+
+
+def test_subcatch_get_connection(handle):
+    object_type, object_index = solver.subcatch_get_connection(0)
+    assert object_type == shared_enum.ObjectType.NODE
+    assert object_index == 1
+
+    object_type, object_index = solver.subcatch_get_connection(1)
+    assert object_type == shared_enum.ObjectType.NODE
+    assert object_index == 2
+    object_type, object_index = solver.subcatch_get_connection(2)
+    assert object_type == shared_enum.ObjectType.NODE
+    assert object_index == 0
+    object_type, object_index = solver.subcatch_get_connection(3)
+    assert object_type == shared_enum.ObjectType.NODE
+    assert object_index == 10
+
+
+def test_subcatch_get_result(run_sim):
+    rain = solver.subcatch_get_result(0, shared_enum.SubcatchResult.RAIN)
+    evaporation = solver.subcatch_get_result(0, shared_enum.SubcatchResult.EVAPORATION)
+    assert rain == 0.0
+    assert evaporation == 0.0
+    for i in range(0, 100):
+        solver.swmm_step()
+    rain = solver.subcatch_get_result(0, shared_enum.SubcatchResult.RAIN)
+    evaporation = solver.subcatch_get_result(0, shared_enum.SubcatchResult.EVAPORATION)
+    assert rain == pytest.approx(0.25, 0.1)
+    assert evaporation == pytest.approx(0.0, 0.1)
+
+
+def test_subcatch_get_pollutant(run_sim):
+    pollut = solver.subcatch_get_pollutant(0, shared_enum.SubcatchPollutant.BUILD_UP)
+    tss, lead = tuple(pollut)
+    assert tss == 0.0
+    assert lead == 0.0
+
+    for i in range(0, 250):
+        solver.swmm_step()
+    pollut = solver.subcatch_get_pollutant(0, shared_enum.SubcatchPollutant.BUILD_UP)
+
+    tss, lead = tuple(pollut)
+    assert tss == pytest.approx(44.246, 0.1)
+    assert lead == 0.0
+
+
+def test_subcatch_get_stats(run_sim):
+    while True:
+        time = solver.swmm_step()
+        if time == 0:
+            break
+
+    stats = solver.subcatch_get_stats(0)
+
+
+def test_lid_usage_get_count(lid_handle):
+    subcatch_usage_1 = solver.lid_usage_get_count(0)
+    subcatch_usage_2 = solver.lid_usage_get_count(1)
+
+    assert subcatch_usage_1 == 1
+    assert subcatch_usage_2 == 0
+
+
+def test_lid_usage_parameter(lid_handle):
+    unit_area = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.UNIT_AREA)
+    top_width = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.TOP_WIDTH)
+    #bottom_width = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.BOTTOM_WIDTH)
+    initial_saturation = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.INITIAL_SATURATION)
+    from_impervious = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.FROM_IMPERVIOUS)
+    from_pervious = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.FROM_PERVIOUS)
+
+    assert unit_area == 50
+    assert top_width == 10
+    #assert bottom_width == pytest.approx(0)
+    assert initial_saturation == 0
+    assert from_impervious == 25
+    assert from_pervious == 0
+
+    solver.lid_usage_set_parameter(0, 0, shared_enum.LidUsageProperty.UNIT_AREA, 100)
+    solver.lid_usage_set_parameter(0, 0, shared_enum.LidUsageProperty.TOP_WIDTH, 20)
+    #solver.lid_usage_set_parameter(0, 0, shared_enum.LidUsageProperty.BOTTOM_WIDTH, 20)
+    solver.lid_usage_set_parameter(0, 0, shared_enum.LidUsageProperty.INITIAL_SATURATION, 10)
+    solver.lid_usage_set_parameter(0, 0, shared_enum.LidUsageProperty.FROM_IMPERVIOUS, 50)
+    solver.lid_usage_set_parameter(0, 0, shared_enum.LidUsageProperty.FROM_PERVIOUS, 10)
+
+    unit_area = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.UNIT_AREA)
+    top_width = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.TOP_WIDTH)
+    #bottom_width = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.BOTTOM_WIDTH)
+    initial_saturation = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.INITIAL_SATURATION)
+    from_impervious = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.FROM_IMPERVIOUS)
+    from_pervious = solver.lid_usage_get_parameter(0, 0, shared_enum.LidUsageProperty.FROM_PERVIOUS)
+
+    assert unit_area == 100
+    assert top_width == 20
+    #assert bottom_width == 0
+    assert initial_saturation == 10
+    assert from_impervious == 50
+    assert from_pervious == 10
+
+
+def test_lid_usage_option(lid_handle):
+    index = solver.lid_usage_get_option(0, 0, shared_enum.LidUsageOption.INDEX)
+    number = solver.lid_usage_get_option(0, 0, shared_enum.LidUsageOption.NUMBER)
+    to_perv = solver.lid_usage_get_option(0, 0, shared_enum.LidUsageOption.TO_PERV)
+    drain_subcatch = solver.lid_usage_get_option(0, 0, shared_enum.LidUsageOption.DRAIN_SUBCATCH)
+    drain_node = solver.lid_usage_get_option(0, 0, shared_enum.LidUsageOption.DRAIN_NODE)
+
+    assert index == 0
+    assert number == 100
+    assert to_perv == 1
+    assert drain_subcatch == -1
+    assert drain_node == 0
+
+    solver.lid_usage_set_option(0, 0, shared_enum.LidUsageOption.NUMBER, 200)
+
+    number = solver.lid_usage_get_option(0, 0, shared_enum.LidUsageOption.NUMBER)
+    assert number == 200
+
+
+def test_lid_usage_get_flux_rate(run_lid_sim):
+    while True:
+        time = solver.swmm_step()
+        if time == 0:
+            break
+
+    rate = solver.lid_usage_get_flux_rate(0, 0, shared_enum.LidLayer.SURFACE)
+
+
+def test_lid_usage_get_result(run_lid_sim):
+    while True:
+        time = solver.swmm_step()
+        if time == 0:
+            break
+
+    result = solver.lid_usage_get_result(0, 0, shared_enum.LidResult.SURFACE_OUTFLOW)
+
+
+def test_lid_group_get_result(run_lid_sim):
+    index = solver.project_get_index(shared_enum.ObjectType.SUBCATCH, 'wBC')
+    while True:
+        time = solver.swmm_step()
+        if time == 0:
+            break
+
+    result = solver.lid_group_get_result(index, shared_enum.LidResult.OLD_DRAIN_FLOW)
+
+
+def test_lid_control_get_overflow(lid_handle):
+    overflow_option = solver.lid_control_get_overflow(0)
+    # BC surface is greater than zero
+    assert overflow_option == 0
+
+
+def test_lid_control_parameter(lid_handle):
+    alpha = solver.lid_control_get_parameter(0, shared_enum.LidLayer.SURFACE, shared_enum.LidLayerProperty.ALPHA)
+    surface_thickness = solver.lid_control_get_parameter(0, shared_enum.LidLayer.SURFACE, shared_enum.LidLayerProperty.THICKNESS)
+    soil_thickness = solver.lid_control_get_parameter(0, shared_enum.LidLayer.SOIL, shared_enum.LidLayerProperty.THICKNESS)
+    storage_thickness = solver.lid_control_get_parameter(0, shared_enum.LidLayer.STORAGE, shared_enum.LidLayerProperty.THICKNESS)
+
+    assert alpha == 1.49
+    assert surface_thickness == 6
+    assert soil_thickness == 12
+    assert storage_thickness == 12
+
+    solver.lid_control_set_parameter(0, shared_enum.LidLayer.SURFACE, shared_enum.LidLayerProperty.THICKNESS, 12)
+    surface_thickness = solver.lid_control_get_parameter(0, shared_enum.LidLayer.SURFACE, shared_enum.LidLayerProperty.THICKNESS)
+    assert surface_thickness == 12
+
+
+def test_gage_precipitation(run_sim):
+    total = solver.raingage_get_precipitation(0, shared_enum.RainResult.TOTAL)
+    snowfall = solver.raingage_get_precipitation(0, shared_enum.RainResult.SNOWFALL)
+    rainfall = solver.raingage_get_precipitation(0, shared_enum.RainResult.RAINFALL)
+    assert total == 0.0
+    assert snowfall == 0.0
+    assert rainfall == 0.0
+
+    for i in range(0, 250):
+        solver.swmm_step()
+
+    total = solver.raingage_get_precipitation(0, shared_enum.RainResult.TOTAL)
+    snowfall = solver.raingage_get_precipitation(0, shared_enum.RainResult.SNOWFALL)
+    rainfall = solver.raingage_get_precipitation(0, shared_enum.RainResult.RAINFALL)
+    assert total == 0.4
+    assert snowfall == 0.0
+    assert rainfall == 0.4
+
+    solver.raingage_set_precipitation(0, 1.0)
+
+    for i in range(0, 250):
+        solver.swmm_step()
+
+    total = solver.raingage_get_precipitation(0, shared_enum.RainResult.TOTAL)
+    snowfall = solver.raingage_get_precipitation(0, shared_enum.RainResult.SNOWFALL)
+    rainfall = solver.raingage_get_precipitation(0, shared_enum.RainResult.RAINFALL)
+    assert total == 1.0
+    assert snowfall == 0.0
+    assert rainfall == 1.0
diff --git a/swmm-toolkit/tests/test_stats.py b/swmm-toolkit/tests/test_stats.py
new file mode 100644
index 00000000..d161c089
--- /dev/null
+++ b/swmm-toolkit/tests/test_stats.py
@@ -0,0 +1,79 @@
+#
+#  test_stats.py
+#
+#  Created:    Nov  16, 2020
+#  Updated:
+#
+#  Author:     See AUTHORS
+#
+
+import os
+
+import pytest
+
+from swmm.toolkit import solver, shared_enum
+
+
+DATA_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
+INPUT_FILE_EXAMPLE_1 = os.path.join(DATA_PATH, 'test_Example1.inp')
+REPORT_FILE_TEST_1 = os.path.join(DATA_PATH, 'temp_Example.rpt')
+OUTPUT_FILE_TEST_1 = os.path.join(DATA_PATH, 'temp_Example.out')
+
+
+@pytest.fixture()
+def before_end(request):
+    solver.swmm_open(INPUT_FILE_EXAMPLE_1, REPORT_FILE_TEST_1, OUTPUT_FILE_TEST_1)
+    solver.swmm_start(0)
+    
+    while True:
+        time = solver.swmm_step()
+        if time == 0:
+            break
+    
+    def close():
+        solver.swmm_end()
+        solver.swmm_close()
+
+    request.addfinalizer(close)
+
+
+def test_node_get_stats(before_end):
+    while True:
+        time = solver.swmm_step()
+        if time == 0:
+            break
+
+    node_stats = solver.node_get_stats(5)
+    assert node_stats.maxDepth  == pytest.approx(1.15, 0.1)
+
+
+def test_storage_get_stats(before_end):
+    node_index = solver.project_get_index(shared_enum.ObjectType.NODE, 'SU1')
+    while True:
+        time = solver.swmm_step()
+        if time == 0:
+            break
+
+    stor_stats = solver.storage_get_stats(node_index)
+    assert stor_stats.avgVol == pytest.approx(0, 0.1)
+
+
+def test_outfall_stats(before_end):
+    id = '18'
+
+    index = solver.project_get_index(shared_enum.ObjectType.NODE, id)
+
+    stats = solver.outfall_get_stats(index)
+    assert stats.totalLoad != None
+
+
+def test_get_routing_totals(before_end):
+
+    stats = solver.system_get_routing_totals()
+    assert stats != None
+
+
+def test_get_runoff_totals(before_end):
+
+    stats = solver.system_get_runoff_totals()
+    assert stats != None