Running a simulation — Solver#
Note
Engine: OpenSWMM 6 — refactored. This is
openswmm.engine.Solver. The legacy SWMM 5 solver of the
same name lives at openswmm.legacy.engine.Solver — see
Legacy SWMM 5 Solver for its (different) API.
The Solver class is the entry point. It owns the SWMM engine
handle, parses the input file, drives the simulation forward in time,
and writes report and binary-output files. Every domain accessor —
nodes, links, subcatchments, gages, controls, output, etc. — hangs off
the Solver as a lazy attribute (solver.nodes, solver.links, …).
Class signature#
class Solver:
def __init__(
self,
inp: str | os.PathLike = "",
rpt: str | os.PathLike | None = None,
out: str | os.PathLike | None = None,
*,
plugin_lib: str | os.PathLike | None = None,
) -> None: ...
Every file argument accepts pathlib.Path or any
os.PathLike. The C engine handle is allocated lazily on the
first call to open(); until then, only state,
handle, and the lifecycle helpers are valid.
Quickstart#
from datetime import timedelta
from pathlib import Path
from openswmm.engine import Solver
with Solver(Path("model.inp"), Path("model.rpt"), Path("model.out")) as s:
print(s.start_datetime, s.routing_step) # datetime, timedelta
for elapsed in s.steps(): # iterates until END
if elapsed >= timedelta(hours=24):
break
# Inside the `with` block we are RUNNING. On exit the context
# manager runs end → report → close → destroy automatically.
That’s the entire happy path. The remaining sections cover the manual lifecycle, the property surface, and the view objects.
Lifecycle#
Method |
What it does |
|---|---|
|
Allocate the engine handle. Called implicitly by |
|
Parse the |
|
Allocate state arrays; apply initial conditions. |
|
Begin routing. |
|
Advance one routing step. Returns a |
|
Advance |
|
Iterator that yields per-step |
|
Stride forward until a target |
|
Finalize the simulation. Cumulative stats become available. |
|
Write the summary report to the |
|
Close all files. Idempotent. |
|
Free the C engine handle. Idempotent. |
Every lifecycle method raises EngineError (or a subclass —
see Error handling, edge cases & debugging) on failure. None of them return an integer
status code.
Manual lifecycle (no context manager):
s = Solver("model.inp", "model.rpt", "model.out")
s.open() # state == OPENED — inspect / edit the model here
s.initialize() # state == INITIALIZED
s.start() # state == STARTED → RUNNING after first step
for _ in s.steps():
pass
s.end() # state == ENDED — mass balance available
s.report()
s.close() # state == CLOSED
s.destroy()
Use Solver.run() for a one-call run-to-completion when there’s
nothing to do between steps; or the free function
openswmm.engine.run() if you have no Solver yet.
Typed property surface#
Every value that has a natural Python type now returns it:
Property |
Type |
Notes |
|---|---|---|
|
|
|
|
Elapsed simulation time after the last |
|
|
The active routing step duration. |
|
|
Read/write. Microsecond round-trip ≤ 1 µs. |
|
|
Read/write. |
|
|
Read/write. |
|
|
Updated each step; equals |
|
|
|
CRS string from |
|
|
|
|
|
Read/write; toggles |
|
|
Raw engine pointer for advanced interop. |
|
|
Monotonic counter bumped on structural mutations (add / delete / rename). Wrapper objects (P2+) check this to detect staleness. |
See SWMM DateTime and Python datetime interop for the SWMM DateTime ↔ Python datetime rules
behind the date properties.
Iteration helpers#
Solver.steps() — per-step iteration#
with Solver("model.inp") as s:
for elapsed in s.steps():
# elapsed is a datetime.timedelta after the most recent step
if s.nodes["J1"].depth > 5.0: # depth wrapper lands in P2
break
The iterator terminates automatically when the engine reports zero
elapsed time. A bare for x in solver is intentionally not provided —
the steps() method makes the intent explicit.
Solver.until(target) — advance to a moment#
from datetime import timedelta
with Solver("model.inp") as s:
s.until(timedelta(hours=12)) # elapsed-time target
# ... read state at t = 12 h ...
s.until(s.end_datetime) # finish the run
target may be a datetime (absolute moment) or a
timedelta (duration from start). The return value is
the actual elapsed timedelta reached — it can be
less than the requested target if the simulation ended first.
View objects#
solver.options#
A MutableMapping view over the [OPTIONS] block, plus typed
shortcuts:
solver.options["FLOW_UNITS"] = "CMS" # string-keyed access
solver.options.start_datetime # datetime
solver.options.routing_step # timedelta
Iteration yields every key the C API confirms is present:
for k in solver.options:
print(k, "=", solver.options[k])
solver.options.ext is a parallel sub-mapping for the
[OPTIONS_EXT] block.
solver.userflags#
Typed user-flag access:
solver.userflags["MY_FLAG"] = True # picks the bool setter
solver.userflags["MAX_PATHS"] = 4 # picks the int setter
solver.userflags["TOLERANCE"] = 1e-6 # picks the real setter
Reading returns a Python bool, int, or float depending on
which C getter responds first. Unknown flag names raise KeyError.
solver.events#
A MutableSequence over the [EVENTS] block:
from datetime import datetime
from openswmm.engine._solver import Event
solver.events.append(Event(
start=datetime(2024, 1, 1, 0, 0),
end=datetime(2024, 1, 1, 12, 0),
))
for ev in solver.events:
print(ev.start, ev.end)
del solver.events[0]
solver.events.clear()
insert is supported (the C API only allows append, so it’s emulated
by clearing and rebuilding).
Domain-collection accessors#
Every domain hangs off the Solver as a lazy attribute, materialised on first access and cached:
Attribute |
What it is |
|---|---|
|
Node collection (see Nodes). |
|
Link collection (see Links). |
|
Subcatchment collection (see Subcatchments). |
|
Rain-gage collection (see Rain gages). |
|
Pollutant collection (see Pollutants). |
|
Time series, curves, patterns (see Tables (time series, curves, patterns)). |
|
External / DWF / RDII inflows (see External inflows). |
|
Control rules and direct actions (see Control rules). |
|
Runtime forcing (see Advanced forcing). |
|
Transects, streets, inlets, LIDs (see Infrastructure (transects, streets, inlets, LIDs)). |
|
Coordinates, CRS, vertices, polygons (see Spatial (CRS, coordinates, geometry)). |
|
Landuse, buildup, washoff, treatment (see Water quality (landuse, buildup, washoff, treatment)). |
|
Cumulative simulation statistics (see Statistics). |
|
Continuity errors and flux totals (see Mass balance). |
|
In-place model editing (see Model editing (deletion + type conversion)). |
Callbacks#
Three callback registration points:
def on_step_begin(sim_time, dt):
... # inject forcings, modify boundary conditions
def on_step_end(sim_time, dt):
... # read state for real-time monitoring
def on_warning(code, message):
log.warning("engine: %s (%s)", message, code)
solver.set_step_begin_callback(on_step_begin)
solver.set_step_end_callback(on_step_end)
solver.set_warning_callback(on_warning)
Pass None to unregister any of them.
Convenience module-level helpers#
from openswmm.engine import run, run_with_callback
run("model.inp", "model.rpt", "model.out") # one-call run-to-completion
def progress(frac):
print(f"{frac:.0%}")
run_with_callback("model.inp", callback=progress)
Both raise EngineError on failure.
See also#
Concepts & engine lifecycle — engine lifecycle and the
EngineStatemachine.Error handling, edge cases & debugging — exception hierarchy and recommended idioms.
SWMM DateTime and Python datetime interop — SWMM DateTime ↔ Python
datetimeconversion.Nodes, Links, Subcatchments, Output reader (binary .out file) — the most-used domain accessors.