Skip to main content

swmmrs

swmmrs for Python

Native, typed Python bindings for the Rust port of the EPA Storm Water Management Model (SWMM) solver.

swmmrs gives each Python Simulation its own solver state, supports interactive stepping and runtime control, and exposes live model objects, immutable snapshots, and statistics without a separate C-library installation.

Status: Early-stage and not production-ready. The project is still focused on correctness and parity with upstream EPA SWMM. APIs and supported model features may change as the Rust port progresses.

Why swmmrs?

The main swmmrs project is a parity-first, line-by-line Rust port of EPA SWMM. It preserves upstream behavior before pursuing refactoring or performance work. The Python package puts a typed, lifecycle-aware interface over that native solver.

Key goals:

  • isolate all mutable solver state inside each Simulation;
  • make lifecycle and invalid operations explicit;
  • support deterministic interactive and batch execution;
  • expose Python-friendly, typed object views and immutable result records;
  • allow independent simulations to run concurrently;
  • borrow the familiar, productive Python workflow pioneered by PySWMM.

With gratitude to PySWMM

swmmrs owes a substantial debt to PySWMM. PySWMM showed what an approachable, interactive Python interface to SWMM could be and made workflows such as stepping through a simulation, inspecting model objects, applying real-time controls, and collecting results feel natural in Python. Its design, documentation, examples, and years of practical use are a major inspiration for this package.

Many of the most recognizable parts of the swmmrs API deliberately borrow from PySWMM: the Simulation context manager and iterator, object collections, node and link access, step-advance workflows, and the general shape of interactive control loops. Where swmmrs feels familiar to a PySWMM user, that is usually because PySWMM established a clear and effective pattern worth following.

Thank you to the PySWMM maintainers and contributors for building and sustaining such an important tool for the SWMM community. Their work significantly lowered the barrier to programmatic SWMM modeling and provided the practical API vocabulary that informed much of swmmrs. PySWMM remains the mature, established choice for Python users who need its broader feature set, ecosystem, callback system, or binary-output tools. Please visit the PySWMM documentation and PySWMM repository to learn more, support their work, and cite the PySWMM JOSS paper when appropriate.

This influence is not a compatibility promise. swmmrs embeds a Rust port of the solver and uses a different ownership and lifecycle model; it is largely not a drop-in replacement for PySWMM. Imports, classes, callbacks, supported properties, error behavior, and output capabilities can differ. Existing PySWMM applications should expect an intentional migration rather than a package-name substitution. swmmrs is an independent project and is not affiliated with or endorsed by the PySWMM project.

Features

  • Batch execution or step-by-step simulation
  • Context-manager cleanup and typed lifecycle states
  • Configurable simulation options, dates, and supported model properties
  • Live access to rain gages, subcatchments, nodes, links, and LIDs
  • Runtime inflow, rainfall, stage, pollutant, and link-setting controls
  • Configurable Python callback cadence with step_advance() and stride()
  • Immutable hydraulic, water-quality, and statistics snapshots
  • Hotstart input and checkpoint output
  • Independent concurrent simulation owners
  • Type annotations and a py.typed marker
  • No Python runtime dependencies

Requirements

  • Python 3.11 or newer
  • A supported native wheel

Released wheels are the supported installation artifact: they include the package-private native extension and do not require Rust, a compiler, or a solver checkout. The package version documented by this repository is 0.2.1.

[!NOTE] The native solver source is private. To request access to the swmm-rs organization, email admin@swmm.rs with your GitHub username and a short description of the contribution you want to make.

Install a released wheel

Install the published wheel into a clean virtual environment:

python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install swmmrs

For an authorized artifact-only installation, pass a downloaded wheel directly:

python -m pip install /path/to/swmmrs-0.2.1-cp311-abi3-manylinux_2_28_x86_64.whl

Quick start

Run a model to completion

Use execute() when Python does not need to inspect or modify the model during routing. It starts the run, advances to completion, ends it, writes the report when results are enabled, and closes the project.

from swmmrs import Simulation

simulation = Simulation("model.inp", "model.rpt", "model.out")
simulation.execute()

Inspect a model while it runs

Iteration starts the simulation on the first advance. Natural exhaustion leaves it in COMPLETE, where final statistics remain available; call end() before report(). The context manager finalizes an active or complete run and closes the project even if the body raises, but it does not generate the report for you.

from datetime import timedelta

from swmmrs import Simulation

records = []
with Simulation("model.inp", "model.rpt", "model.out") as simulation:
    node = simulation.nodes["J1"]
    conduit = simulation.links["C1"]
    simulation.step_advance(timedelta(minutes=5))

    for current_time in simulation:
        records.append((current_time, node.depth, conduit.flow))

    simulation.end()
    simulation.report()

step_advance() changes how often control returns to Python. It does not replace or enlarge SWMM's internal routing time step.

Apply a runtime control

Live object views can read current solver state and write supported runtime inputs. Values use the unit system configured by the model.

from datetime import timedelta

from swmmrs import Simulation

with Simulation("model.inp", "model.rpt", "model.out") as simulation:
    basin = simulation.nodes["BASIN"]
    gate = simulation.links["OUTLET_GATE"]
    simulation.step_advance(timedelta(minutes=5))

    for _ in simulation:
        if basin.depth >= 4.0:
            gate.target_setting = 1.0
        elif basin.depth <= 2.0:
            gate.target_setting = 0.0

    simulation.end()
    simulation.report()

Read finalized output

OutputReader exposes immutable, typed series from a finalized binary output. Family queries share the structured bulk reader and accept exact indexes or names plus optional start and end bounds:

from swmmrs.output import OutputName, OutputReader, PollutantAttribute

reader = OutputReader("model.out")
depth = reader.node_series("J1", "depth", start=0, end=12)
print(depth.selection, depth.times, depth.values)
pollutant = reader.node_series("J1", PollutantAttribute("TSS"))

SeriesSelection has the fields element_type, element, and attribute. Names match stored bytes exactly; use bytes or OutputName for non-UTF-8 names. Pollutants must use PollutantAttribute, so a pollutant named depth never silently replaces the explicit static node-depth attribute. Results expose tuples of times and values and raise stable OutputError categories for missing or ambiguous names and attributes.

read_bulk_series(..., low_memory=False) reads one complete payload per selected period by default. Set low_memory=True to read only selected adjacent cell runs with smaller scratch memory. The same keyword is available on all four family methods; both strategies return identical structured data.

Core concepts

One owner, one project generation

A Simulation owns one isolated native SWMM project at a time. Object collections and live views belong to that project generation. They remain valid across routing advances, but become stale after close() or after open() creates a replacement generation. Using a stale view raises StaleViewError.

Do not copy, deep-copy, or pickle a Simulation. Use a separate owner for each independent model or scenario.

Lifecycle

State Meaning Typical next operation
OPEN The input is parsed and configuration can be changed. Configure, start, iterate, execute, or close.
RUNNING Routing is active. Step, stride, iterate, inspect, control, or end.
COMPLETE Manual or iterator advancement reached the model end but finalization has not run. Read final statistics, then end.
ENDED Run finalization is complete and the project remains open. Report, reconfigure, restart, or close.
FAILED A native lifecycle operation failed. Preserve the error and close.
CLOSED Native state and project files are released. Open a fresh project generation.

Choose one advancement style per run:

  • for current_time in simulation for iterator-owned stepping;
  • start() plus step() or stride() for caller-owned stepping;
  • execute() for an unattended batch run.

Do not mix iterator-owned advancement with start(), step(), stride(), or execute().

The native Simulation Owner is authoritative for path resolution and retained metadata, collision checks, checkpoint metadata, iterator cadence/exhaustion, lifecycle cleanup, canonical collection identity, related-view construction, and final public exception classification. Python retains presentation of Path, datetime, and timedelta, uncached identity-only Live Views, and context-manager body-exception precedence.

Checkpoints and branches

At a manual-step boundary, save_checkpoint(path) publishes an immutable Simulation Checkpoint. Resume it into fresh output files, import only its physical continuation state into a compatible open model, or fork the live owner without replay:

simulation.save_checkpoint("split.json")
resumed = Simulation.resume("split.json", "resumed.rpt", "resumed.out")
branch = simulation.fork("branch.rpt", "branch.out")
receiver.load_checkpoint_state("split.json")

Resume and fork create independent owners and file handles. State Load keeps the receiver's dates, inputs, statistics, and outputs.

Model objects

Collections are available directly from a simulation:

node = simulation.nodes["J1"]
link = simulation.links["C1"]
subcatchment = simulation.subcatchments["S1"]
rain_gage = simulation.rain_gages["RG1"]

IDs are case-insensitive. Collections preserve configured project order and provide by_index() where index-based access is required. Invalid IDs use normal KeyError behavior; invalid indices raise IndexError.

Configure supported static properties while the simulation is OPEN or ENDED:

from datetime import timedelta

simulation.options.update(
    report_step=timedelta(minutes=15),
    allow_ponding=True,
)
simulation.nodes["J1"].settings.full_depth = 4.5
simulation.links["C1"].settings.roughness = 0.015
simulation.subcatchments["S1"].settings.width = 250.0

Inspect simulation.unit_system and simulation.flow_units before combining model values with external data. The API does not silently convert project units to SI.

Live values, snapshots, and statistics

Use a live scalar view for a small number of current values:

depth = simulation.nodes["J1"].depth
flow = simulation.links["C1"].flow

Use a snapshot for a coherent, immutable batch acquisition:

nodes = simulation.nodes.snapshot(["J1", "J2"])
for node_id, depth, inflow in zip(
    nodes.object_ids,
    nodes.depth,
    nodes.total_inflow,
    strict=True,
):
    print(node_id, depth, inflow)

Snapshots and statistics are solver-created, immutable native PyO3 records and remain usable after the simulation closes. They cannot be constructed or mutated by callers; copy selected fields into lists or dictionaries when an editable host representation is needed. Live views do not.

Statistics are available while RUNNING and after manual or iterator advancement reaches COMPLETE. Acquire final statistics before calling end():

simulation.start(save_results=False)
while simulation.step() is not None:
    pass

continuity = simulation.statistics
node_statistics = simulation.nodes.statistics()
simulation.end()
simulation.close()

save_results=False skips report-period binary results but still permits live reads, snapshots, quality snapshots, and statistics.

Output files

swmmrs can write SWMM .rpt and .out artifacts. OutputReader validates and queries a finalized binary .out file independently of a live simulation.

Collect live values or snapshots while Python must inspect or control the advancing model. For post-run report-period analysis, pass an explicit output_path, run with save_results=True, and open the completed artifact with OutputReader.

Input, report, and output paths must be distinct. If output_path is omitted, SWMM uses a scratch output artifact and simulation.output_path is None.

Concurrent simulations

Different Simulation owners can run concurrently, and native lifecycle operations release Python while they execute. Give every run distinct report and output paths. Operations on the same owner serialize; never use several threads to advance one simulation.

Also account for SWMM Dynamic Wave worker threads. For example, four concurrent models configured with THREADS 2 can use up to eight caller-inclusive solver threads.

Errors

All package-specific exceptions inherit from SwmmError:

  • ValidationError — invalid Python arguments, values, or paths;
  • LifecycleError — an operation is invalid in the current state;
  • StaleViewError — a collection or object view belongs to an old project generation;
  • SolverError — the native solver rejected an input or operation;
  • InternalSimulationError — an unexpected binding or solver failure.

SolverError exposes code, operation, and optional detail fields:

from swmmrs import Simulation, SolverError

try:
    Simulation("model.inp", "model.rpt", "model.out").execute()
except SolverError as error:
    print(error.code, error.operation, error.detail)
    raise

Package and solver versions

The Python distribution and embedded SWMM solver have separate identities:

import swmmrs

print(swmmrs.__version__)
print(swmmrs.solver_version)
print(swmmrs.solver_build_id)

Documentation

Licensing and third-party notices

The original Python interface source is licensed under Apache-2.0. Official wheels include the separately licensed native solver under the permissive swmmrs Binary Runtime License, which allows use, modification, and redistribution of compiled artifacts for any purpose without granting access to the private solver source. The native component incorporates EPA SWMM public-domain material and MIT-licensed Open Water Analytics contributions. See LICENSE and the bundled third-party notices for licensing and attribution.

Development

From python/:

uv sync --locked
uv run pytest
uv run ruff check src

Rust and native-extension development requires authorized access to the private solver submodule. From the repository root, initialize all dependencies and run the workspace tests:

cd ..
git submodule update --init --recursive
cargo test --workspace --exclude swmmrs-parallel --locked --all-features

To request maintainer access, email admin@swmm.rs with your GitHub username and a short description of the contribution you want to make.

Contributions should preserve upstream SWMM behavior first. Structural cleanup and performance changes come after parity can be demonstrated with tests.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

swmmrs-0.2.1-cp311-abi3-win_arm64.whl (3.1 MB view details)

Uploaded CPython 3.11+Windows ARM64

swmmrs-0.2.1-cp311-abi3-win_amd64.whl (3.2 MB view details)

Uploaded CPython 3.11+Windows x86-64

swmmrs-0.2.1-cp311-abi3-manylinux_2_28_x86_64.whl (3.0 MB view details)

Uploaded CPython 3.11+manylinux: glibc 2.28+ x86-64

swmmrs-0.2.1-cp311-abi3-manylinux_2_28_aarch64.whl (2.8 MB view details)

Uploaded CPython 3.11+manylinux: glibc 2.28+ ARM64

swmmrs-0.2.1-cp311-abi3-macosx_11_0_arm64.whl (2.7 MB view details)

Uploaded CPython 3.11+macOS 11.0+ ARM64

swmmrs-0.2.1-cp311-abi3-macosx_10_13_x86_64.whl (2.9 MB view details)

Uploaded CPython 3.11+macOS 10.13+ x86-64

File details

Details for the file swmmrs-0.2.1-cp311-abi3-win_arm64.whl.

File metadata

  • Download URL: swmmrs-0.2.1-cp311-abi3-win_arm64.whl
  • Upload date:
  • Size: 3.1 MB
  • Tags: CPython 3.11+, Windows ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for swmmrs-0.2.1-cp311-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 3fee9cd55b851c3513a900ecfbf7c358fbbbe68eecc07ebb1ff143d07a5ff837
MD5 a4de4da3b2555c0c961dddbe4faf7f83
BLAKE2b-256 b5a5baad22f83c9f50e3f0495a53046974083eca7c2a7a91d40ebf6cf2492082

See more details on using hashes here.

Provenance

The following attestation bundles were made for swmmrs-0.2.1-cp311-abi3-win_arm64.whl:

Publisher: release.yml on swmm-rs/swmmrs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file swmmrs-0.2.1-cp311-abi3-win_amd64.whl.

File metadata

  • Download URL: swmmrs-0.2.1-cp311-abi3-win_amd64.whl
  • Upload date:
  • Size: 3.2 MB
  • Tags: CPython 3.11+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for swmmrs-0.2.1-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 c168860ac15c58364154ad1ba32446fb9cfac88435db862fde538991fb56970f
MD5 a1b0147a1f8c480d58e309419246677f
BLAKE2b-256 d6a107e157dbbf8f6cc798abc543a437f8568921bf9b482ff2bd23c98f44f240

See more details on using hashes here.

Provenance

The following attestation bundles were made for swmmrs-0.2.1-cp311-abi3-win_amd64.whl:

Publisher: release.yml on swmm-rs/swmmrs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file swmmrs-0.2.1-cp311-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for swmmrs-0.2.1-cp311-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 d168b3593cea3398b92d6e8eff840cd17a4a35781c13a22b8a27262a3b6767c7
MD5 84b3e0f788ea1d000db15556336047f1
BLAKE2b-256 850cd9164e1372bbaad1dd018cebe0dc5eb2d15865fd12658ceee4549782ddd9

See more details on using hashes here.

Provenance

The following attestation bundles were made for swmmrs-0.2.1-cp311-abi3-manylinux_2_28_x86_64.whl:

Publisher: release.yml on swmm-rs/swmmrs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file swmmrs-0.2.1-cp311-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for swmmrs-0.2.1-cp311-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 2845a210c5bc2bf4ccc991fe11552a046173743ffd0dc0fb818490dd01b547b5
MD5 63b8b489aa2b237f9398e59ccc13a414
BLAKE2b-256 252a4f900352e479108ef9a26a8c866796fb891fcd567b638c04125347fe1277

See more details on using hashes here.

Provenance

The following attestation bundles were made for swmmrs-0.2.1-cp311-abi3-manylinux_2_28_aarch64.whl:

Publisher: release.yml on swmm-rs/swmmrs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file swmmrs-0.2.1-cp311-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for swmmrs-0.2.1-cp311-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a904ad9adca999e92b0d22d7c4d1484c25ee5587d1f9ec9cc3d2f0d784172563
MD5 65cae71f45ad39838f1a1ee234dc931e
BLAKE2b-256 7cb334912d965068677fdf26165e34499d9417cde590b2df8682dee01f618146

See more details on using hashes here.

Provenance

The following attestation bundles were made for swmmrs-0.2.1-cp311-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on swmm-rs/swmmrs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file swmmrs-0.2.1-cp311-abi3-macosx_10_13_x86_64.whl.

File metadata

File hashes

Hashes for swmmrs-0.2.1-cp311-abi3-macosx_10_13_x86_64.whl
Algorithm Hash digest
SHA256 44c12d7aee814156e4804bcab574b9644b98965ff2605432830e3d0c0dffc50b
MD5 f0443d5d3656c08a05cb86e1e5750818
BLAKE2b-256 2b439d559299f07b9a63fd089851a25822f04b5c79c74e3e85dfdf0cbcbfbd57

See more details on using hashes here.

Provenance

The following attestation bundles were made for swmmrs-0.2.1-cp311-abi3-macosx_10_13_x86_64.whl:

Publisher: release.yml on swmm-rs/swmmrs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.1 This release

6 files

0.1.0

1 file

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page