Open Storm Water Management Model — Next-Generation Computational Engine


[
](LICENSE)

Overview
OpenSWMM Engine is a community-driven, open-source continuation of the EPA Storm Water Management Model (SWMM). SWMM is a dynamic hydrology–hydraulic–water quality simulation model used for single-event or long-term (continuous) simulation of runoff quantity and quality from primarily urban areas.
This project preserves and advances the rich legacy of SWMM by developing high-quality, QA/QC'd code while building an active community for sustainable maintenance and improvement. The community is actively working with organizations including ASCE's Environmental and Water Resources Institute (EWRI) and Water Environment Federation (WEF) to ensure the long-term sustainability of the SWMM codebase.
What's New in v6.0.0
OpenSWMM Engine v6.0.0 is a major modernization of the SWMM computational engine. Key improvements include:
Architecture & Performance
- Data-Oriented Design — Core data structures refactored to Structure of Arrays (SoA) layout for cache efficiency and SIMD-friendly computation.
- Reentrant Engine — Global state eliminated; all simulation state encapsulated in an opaque
SWMM_Engine handle, enabling multiple independent simulations in the same process.
- Plugin-Based I/O — Output and report writing abstracted through a plugin interface. Plugins receive read-only simulation snapshots on a dedicated I/O thread.
- C++20 Codebase — New engine written in modern C++20; legacy EPA SWMM 5.x solver preserved unmodified in
src/legacy/.
New C API
A comprehensive, domain-split C API replaces the monolithic legacy interface:
| Header | Domain |
openswmm_engine.h | Engine lifecycle, error codes, state machine |
openswmm_model.h | Model building, validation, serialization, options |
openswmm_nodes.h | Junctions, outfalls, storage, dividers |
openswmm_links.h | Conduits, pumps, orifices, weirs, outlets |
openswmm_subcatchments.h | Subcatchments, infiltration, coverage |
openswmm_gages.h | Rain gages (timeseries and file sources) |
openswmm_pollutants.h | Pollutant definitions and runtime injection |
openswmm_tables.h | Time series, curves, and patterns |
openswmm_inflows.h | External inflows, DWF, RDII |
openswmm_controls.h | Control rules and direct link actions |
openswmm_infrastructure.h | Transects, streets, inlets, LID controls |
openswmm_spatial.h | CRS, coordinates, polylines, polygons |
openswmm_quality.h | Landuse, buildup, washoff, treatment |
openswmm_massbalance.h | Continuity errors and cumulative flux totals |
openswmm_callbacks.h | Progress, warning, and step callbacks |
openswmm_hotstart.h | Hot start file save/load/modify |
openswmm_statistics.h | Node, link, and subcatchment statistics |
Additional Features
- Hot Start API — Save, load, modify, and query hot start files through a transparent C ABI.
- CRS Support — Coordinate reference system specification via OPTIONS for spatial data.
- User Flags — Custom USER_FLAGS section for user-defined metadata on objects.
- Plugin SDK — Header-only development kit for building output/report plugins.
- HEC-22 Inlet Analysis — Street inlet capture, grate and curb inlets (from SWMM 5.2).
- Variable Speed Pumps — Type5 pump curves with speed scaling.
- New Storage Shapes — Conical and pyramidal shapes with elliptical/rectangular bases.
Process Formulation Enhancements
The following physics-based improvements are being designed for backward-compatible integration. Each addresses a known simplification in the current SWMM formulation.
- Spatially Explicit Overland Flow & Groundwater — SWMM currently has one-way feedback between hydrology and hydraulics: flooded nodes pond over a user-specified area and are reintroduced locally. In reality, surcharge flows traverse the terrain and may re-enter the network at a downstream node. The new module couples a 2D overland flow grid with the 1D pipe network, enabling spatially explicit green infrastructure placement, terrain-routed surcharge, and lateral groundwater flow exchanges, etc.
- Dynamic Preissmann Slot — Standard SWMM uses a fixed-width slot for pressurized conduit transitions, which can cause numerical instability at the open-channel/pressure interface. A dynamic slot formulation smooths the transition with a geometry-dependent slot width, improving stability for rapidly filling/draining conduits.
- Spatially Explicit Inlets — Current inlets are attributes of conduits, limiting bypass routing and preventing backflow or series-inlet configurations. The redesign promotes inlets to mode-switching junction nodes that actively capture street flow when the gutter spread exceeds a threshold and revert to passive junctions otherwise.
- LID as Storage Nodes — Current LID units lack hydraulic feedback: no backpressure from the drainage network, no bidirectional flow, and no control-structure integration. The redesign maps LID layers (surface, media, gravel) onto an extended storage node using a reduced-physics kinematic Richards ODE — a simplified 1D formulation that captures gravity-driven percolation and capillary redistribution between discrete layers without the computational cost of a full 3D variably-saturated solver. This provides physically meaningful moisture dynamics while remaining compatible with SWMM's routing timestep, and enables elevation-mapped underdrain links with full network coupling.
Physics-Based Initial Abstraction Recovery — SWMM's seasonal RTK calibration for RDII confounds infrastructure leakage fraction with soil moisture state, requiring 12 monthly parameter sets. The new formulation tracks initial abstraction capacity as an exponential decay/recovery process. During dry periods, available capacity recovers with additive time-based and temperature-dependent rate components:
$$IA_{avail}(t+\Delta t) = IA_{max} - \bigl(IA_{max} - IA_{avail}(t)\bigr) \cdot e^{-k_{rec}(T)\,\Delta t}, \qquad k_{rec}(T) = k_0 + k_T \cdot e^{\,\theta\,(T - T_{ref})}$$
The base rate $k_0$ captures gravity drainage and capillary redistribution that occur regardless of temperature, while $k_T \cdot e^{\theta(T-T_{ref})}$ captures thermally-driven evapotranspiration. During storms, capacity is depleted: $IA_{avail}(t+\Delta t) = IA_{avail}(t) \cdot e^{-k_{dep}\,\Delta P}$. Recovery is suppressed when $T < T_{freeze}$, producing emergent seasonal variation (high winter RDII, low summer RDII) from a single RTK set per sewershed — no monthly parameter tables required.
Input File Extensions
The new engine extends the standard SWMM .inp format with several new sections while remaining fully backward-compatible with existing input files. Below is a complete .inp snippet demonstrating all new features:
;; EXTENSION OPTIONS — new and custom keys in [OPTIONS]
;; Standard SWMM options work as before. Any unrecognized key is stored
;; in an extension map accessible at runtime via the C/Python API.
[OPTIONS]
FLOW_UNITS CFS
INFILTRATION HORTON
FLOW_ROUTING DYNWAVE
START_DATE 01/01/2024
START_TIME 00:00:00
END_DATE 01/02/2024
END_TIME 00:00:00
ROUTING_STEP 00:00:30
REPORT_STEP 00:05:00
;; New built-in option (v6.0.0)
CRS EPSG:4326
;; Extension options — stored automatically, readable by plugins
MY_STABILITY_FACTOR 1.05
PLUGIN_LOG_LEVEL DEBUG
;; USER FLAGS — typed custom metadata on any model object
;; Define a schema of flag names with type and default value.
;; Supported types: BOOLEAN, INTEGER, REAL, STRING.
[USER_FLAGS]
;;Name Type Default Description
INSPECTED BOOLEAN NO 'Has the asset been field-inspected?'
PRIORITY INTEGER 0 'Maintenance priority (0=none, 1=low, 5=critical)'
ROUGHNESS_ADJ REAL 1.0 'Roughness calibration multiplier'
ASSET_ID STRING "" 'External asset management system ID'
;; Assign flag values to individual objects.
;; ObjectType can be NODE, LINK, SUBCATCHMENT, or GAGE.
[USER_FLAG_VALUES]
;;ObjectType ObjectName FlagName Value
NODE J1 INSPECTED YES
NODE J1 PRIORITY 3
NODE J2 ASSET_ID "AM-00412"
LINK C1 ROUGHNESS_ADJ 1.12
LINK C1 INSPECTED YES
LINK C2 PRIORITY 1
SUBCATCHMENT S1 INSPECTED NO
SUBCATCHMENT S1 ASSET_ID "AM-00501"
;; PLUGINS — load output/report plugins at runtime
;; Each line: <shared-library-path> [arg1 arg2 ...]
;; The first token is the path to a .so / .dylib / .dll.
;; Remaining tokens are passed to the plugin's initialize() method.
[PLUGINS]
;; Write time-series results to HDF5 instead of (or alongside) the .out file
./plugins/hdf5_output.so file="results.h5" compress=9
;; Write a custom CSV summary report after simulation
./plugins/csv_report.dylib file="summary.csv" delimiter=","
;; Multiple plugins can be loaded — they run concurrently on the I/O thread
./plugins/netcdf_output.so file="results.nc" variables="depth,flow"
How Each Feature Works
Extension Options — Any keyword the parser does not recognize in [OPTIONS] is stored in options.ext_options and can be read at runtime:
val = solver.get_option("MY_STABILITY_FACTOR")
char buf[256];
SWMM_ENGINE_API int swmm_options_get(SWMM_Engine engine, const char *key, char *buf, int buflen)
Retrieve a standard OPTIONS value as a string.
Definition openswmm_model_impl.cpp:118
User Flags — Flags attach structured metadata to objects for asset management, calibration tracking, or custom workflows. Plugins can read them during validate() or prepare() from the SimulationContext.
Plugins — Shared libraries loaded from the [PLUGINS] section implement one or both of:
| Interface | Purpose | Thread |
IOutputPlugin | Write time-series results at each reporting step | I/O thread |
IReportPlugin | Write summary statistics after simulation ends | Main thread |
Each plugin library exports a single entry point:
Describes a plugin component: metadata, capabilities, and factory methods.
Definition IPluginComponentInfo.hpp:57
The engine calls the plugin through a managed lifecycle:
initialize(args) → validate(ctx) → prepare(ctx) → update(snapshot)... → finalize(ctx)
└─ write_summary(ctx) [report plugins]
Output plugins receive a SimulationSnapshot — a read-only deep copy of simulation state safe to consume on the I/O thread while the main simulation advances.
Custom Sections — Plugins and embedders can also register handlers for entirely new .inp sections via the C++ SectionRegistry API:
engine.registry().register_custom("MY_CUSTOM_DATA",
[](SimulationContext& ctx, const std::vector<std::string>& lines) {
for (const auto& line : lines) {
}
});
This allows the .inp to contain:
[MY_CUSTOM_DATA]
;;ID Param1 Param2
OBJ_1 42.0 enabled
OBJ_2 17.5 disabled
The two built-in plugins (DefaultOutputPlugin for .out and DefaultReportPlugin for .rpt) are always available and require no [PLUGINS] entry. The Plugin SDK headers are in include/openswmm/plugin_sdk/.
Testing & Quality
- Google Test — All unit tests migrated from Boost.Test to Google Test 1.15.2.
- Comprehensive Test Suite — Legacy engine (73+ tests), legacy output (41 tests), and new engine unit tests.
- Multi-Platform CI — GitHub Actions pipelines for Windows, Linux, and macOS (Intel + ARM64).
- Doxygen API Docs — All 19 public C API headers fully documented with Doxygen conventions.
Project Structure
openswmm.engine/
├── include/openswmm/
│ ├── engine/ # New engine public C API headers (19 headers)
│ └── legacy/ # Legacy SWMM 5.x public headers
├── src/
│ ├── engine/ # New C++20 engine implementation
│ ├── legacy/engine/ # Original EPA SWMM 5.x solver (preserved unmodified)
│ ├── legacy/output/ # Original binary output reader
│ ├── plugin_sdk/ # Header-only plugin development kit
│ └── cli/ # Command-line interface
├── tests/
│ ├── unit/legacy/ # Legacy solver and output tests (Google Test)
│ ├── unit/engine/ # New engine unit tests
│ ├── regression/ # Regression tests (new vs. legacy)
│ └── benchmarks/ # Performance benchmarks (Google Benchmark)
├── python/ # Python bindings (Cython + scikit-build)
├── docs/ # Doxygen config and technical manuals
├── cmake/ # CMake helper modules
└── .github/workflows/ # CI/CD pipelines
Prerequisites
| Requirement | Version |
| CMake | 3.21 or higher |
| C compiler | C17 support (GCC 10+, Clang 12+, MSVC 19.29+) |
| C++ compiler | C++20 support (GCC 10+, Clang 14+, MSVC 19.29+) |
| vcpkg | 2025.02.14 (for test dependencies) |
| Python | 3.9–3.13 (optional, for bindings) |
| Ninja | Recommended for Linux/macOS builds |
Build Instructions
C/C++ Engine
# Clone the repository
git clone https://github.com/HydroCouple/openswmm.engine.git
cd openswmm.engine
# Bootstrap vcpkg (for test dependencies)
git clone https://github.com/microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh # or bootstrap-vcpkg.bat on Windows
# Configure and build using a platform preset
# Available presets: Windows, Windows-debug, Linux, Linux-debug, Darwin, Darwin-debug
export VCPKG_ROOT=$(pwd)/vcpkg
cmake --preset=<platform> -B build
cmake --build build --config Release
# Build with tests enabled
cmake --preset=<platform>-debug -B build-debug -DOPENSWMM_BUILD_TESTS=ON
cmake --build build-debug --config Debug
Running Tests
# Run all C++ unit tests
ctest --test-dir build-debug -C Debug --output-on-failure
# Run with verbose output
ctest --test-dir build-debug -C Debug --output-on-failure -V
Packaging
# Create distributable archives (ZIP + TGZ)
cmake --build build --target package
Python Bindings
cd python
# Install requirements
python -m pip install -r requirements.txt
# Build and install
python -m pip install .
# Run Python tests
python -m pytest -v tests
Python Usage
The openswmm package provides Cython bindings for the full C API. Domain objects are constructed by passing an active Solver instance.
Running a Simulation
from openswmm.engine import Solver, Nodes, Links, Subcatchments, Gages
with Solver("model.inp", "model.rpt", "model.out") as s:
nodes = Nodes(s)
links = Links(s)
subcatchments = Subcatchments(s)
gages = Gages(s)
while s.step():
depth = nodes.get_depth("J1")
flow = links.get_flow("C1")
runoff = subcatchments.get_runoff("S1")
all_depths = nodes.get_depths_bulk()
all_flows = links.get_flows_bulk()
nodes.set_lateral_inflow("J2", 0.5)
gages.set_rainfall(0, 25.4)
Advanced Forcing with Persistence
from openswmm.engine import Solver, Nodes, Forcing, ForcingMode, ForcingTarget
with Solver("model.inp", "model.rpt", "model.out") as s:
nodes = Nodes(s)
forcing = Forcing(s)
j1 = nodes.get_index("J1")
forcing.node_lat_inflow(j1, 1.5, ForcingMode.REPLACE, persist=True)
forcing.gage_rainfall(0, 10.0, ForcingMode.ADD, persist=True)
while s.step():
pass
forcing.clear(ForcingTarget.NODE, j1)
forcing.clear_all()
Programmatic Model Building (No .inp File)
from openswmm.engine import (
ModelBuilder, Nodes, Links,
NodeType, LinkType, XSectShape,
)
m = ModelBuilder()
m.add_node("J1", NodeType.JUNCTION)
m.add_node("J2", NodeType.JUNCTION)
m.add_node("OUT1", NodeType.OUTFALL)
m.add_link("C1", LinkType.CONDUIT)
m.add_link("C2", LinkType.CONDUIT)
m.set_node_invert(0, 10.0)
m.set_node_invert(1, 8.0)
m.set_node_invert(2, 5.0)
m.set_link_nodes(0, 0, 1)
m.set_link_nodes(1, 1, 2)
m.set_link_length(0, 400.0)
m.set_link_length(1, 400.0)
m.set_link_roughness(0, 0.013)
m.set_link_roughness(1, 0.013)
m.set_link_xsect(0, XSectShape.CIRCULAR, 1.0)
m.set_link_xsect(1, XSectShape.CIRCULAR, 1.0)
m.set_option("FLOW_UNITS", "CFS")
m.set_option("ROUTING_MODEL", "DYNWAVE")
m.set_option("START_DATE", "01/01/2024")
m.set_option("END_DATE", "01/02/2024")
m.validate()
m.finalize()
solver = m.to_solver()
solver.start()
while solver.step():
pass
solver.end()
m.write("generated_model.inp")
solver.destroy()
Reading Binary Output Files
from openswmm.engine import OutputReader, OutNodeVar, OutLinkVar
with OutputReader("model.out") as out:
print(f"Nodes: {out.get_node_count()}")
print(f"Links: {out.get_link_count()}")
print(f"Periods: {out.get_period_count()}")
print(f"Report step: {out.get_report_step()} sec")
for i in range(out.get_node_count()):
print(f" Node {i}: {out.get_node_id(i)}")
for t in range(out.get_period_count()):
depths = out.get_node_result(t, OutNodeVar.DEPTH)
flows = out.get_link_result(t, OutLinkVar.FLOW)
print(f" Period {t}: max depth = {depths.max():.3f}")
node_depths = out.get_node_series(
node_idx=0,
var=OutNodeVar.DEPTH,
start_period=0,
end_period=out.get_period_count() - 1,
)
Hot Start Save and Restore
from openswmm.engine import Solver, HotStart
with Solver("model.inp", "model.rpt", "model.out") as s:
for _ in range(100):
if not s.step():
break
HotStart.save(s, "checkpoint.hsf")
hs = HotStart.open("checkpoint.hsf")
hs.set_node_depth("J1", 2.5)
with Solver("model.inp", "model.rpt", "model2.out") as s2:
hs.apply(s2)
while s2.step():
pass
hs.close()
Mass Balance and Statistics
from openswmm.engine import Solver, MassBalance, Statistics, RunoffTotal
with Solver("model.inp", "model.rpt", "model.out") as s:
while s.step():
pass
mb = MassBalance(s)
stats = Statistics(s)
print(f"Runoff error: {mb.get_runoff_continuity_error():.4f}%")
print(f"Routing error: {mb.get_routing_continuity_error():.4f}%")
precip = mb.get_runoff_total(RunoffTotal.RAINFALL)
runoff = mb.get_runoff_total(RunoffTotal.RUNOFF)
print(f"Total precip: {precip:.2f}, Total runoff: {runoff:.2f}")
print(f"Node 0 max depth: {stats.node_max_depth(0):.3f}")
print(f"Link 0 max flow: {stats.link_max_flow(0):.3f}")
For the full API reference, see the documentation.
Libraries Built
| Target | Description |
openswmm_legacy_engine | Original EPA SWMM 5.x solver (shared library) |
openswmm_legacy_output | Original SWMM binary output reader (shared library) |
openswmm_engine | New refactored C++20 engine (shared library) |
openswmm_plugin_sdk | Header-only plugin SDK (INTERFACE library) |
openswmm_cli | Command-line executable |
Documentation
API documentation is auto-generated with Doxygen and deployed to GitHub Pages:
OpenSWMM Engine API Documentation
The documentation includes:
- Full C API reference with parameter descriptions and usage notes
- Technical reference manuals (Hydrology, Hydraulics, Water Quality)
- User manual with modeling capabilities and examples
- Architecture design decisions and implementation plan
Contributing
Contributions are welcome. Please:
- Fork the repository and create a feature branch.
- Ensure all tests pass (
ctest for C++, pytest for Python).
- Follow existing code style and naming conventions.
- Submit a pull request against the
develop branch.
License
This project is licensed under the MIT License — see [LICENSE](LICENSE) for details.
Copyright 2026 HydroCouple. Original EPA SWMM material is in the public domain under 17 USC § 105.
Acknowledgements
OpenSWMM builds on the foundational work of the EPA Storm Water Management Model, originally developed by Lewis A. Rossman at the U.S. EPA Office of Research and Development. See docs/authors.md for the complete list of authors and contributors.