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 other domain class
(Nodes, Links, Forcing, …) takes a Solver in
its constructor.
Class signature#
class Solver:
def __init__(self, inp: str = "", rpt: str = "", out: str = "") -> None: ...
inp— path to the SWMM.inpinput file.rpt— path for the human-readable.rptreport. Empty string to skip.out— path for the binary.outresults file. Empty string to skip.
The C engine handle is allocated lazily on the first call to
open(); until then, only state, handle, and the lifecycle
helpers are valid.
Lifecycle methods#
Method |
What it does |
|---|---|
|
Allocate the engine handle. Implicit on |
|
Parse the |
|
Allocate state arrays; apply initial conditions. |
|
Start routing. |
|
Advance one routing step. Returns the C error code ( |
|
Advance |
|
Stop routing; finalize cumulative outputs. |
|
Write the human-readable |
|
Flush |
|
Free the engine handle. Always call this. |
|
Context-manager wrapper for the full lifecycle. |
Inspection / time#
Property / method |
Returns |
|---|---|
|
Current |
|
Opaque engine handle (mostly for plugin authors). |
|
Elapsed simulation time in days. |
|
Simulation start time (decimal days since 1899-12-30). |
|
Simulation end time (same epoch). |
|
Current simulation time (same epoch). |
|
Current routing time step in seconds. |
|
Simulation start as a |
|
Simulation end as a |
|
Report start as a |
|
Read or update |
|
Read or update extension options unknown to base SWMM. |
|
Coordinate reference system string (EPSG / PROJ / WKT). |
Event windows, steady-state, and callbacks#
Method |
What it does |
|---|---|
|
Inspect or update the event-window list (start/end pairs). |
|
Programmatically grow or trim the event-window list. |
|
Enable/disable the steady-state skip optimisation. |
|
Register a callable invoked before each routing step. |
|
Register a callable invoked after each routing step. |
|
Register a callable invoked when the engine raises a warning. |
Module-level helpers#
Function |
What it does |
|---|---|
|
One-call helper that opens, runs, reports, and closes a model. |
|
Same as |
Save / serialise#
Method |
Action |
|---|---|
|
Write the current model state back out as |
End-to-end example#
from openswmm.engine import Solver, EngineState
with Solver("site_drainage.inp", "site_drainage.rpt", "site_drainage.out") as s:
print(f"Routing step: {s.get_routing_step():.1f}s")
print(f"Sim window: day {s.get_start_time():.4f} → day {s.get_end_time():.4f}")
steps = 0
while s.state == EngineState.RUNNING:
rc = s.step()
if rc != 0:
break
steps += 1
if steps % 240 == 0: # every ~hour at 15s step
print(f" t = {s.elapsed*24:5.2f} h")
print(f"Done — {steps} routing steps.")
The context manager guarantees end() → report() → close() → destroy()
runs even if the loop body raises.
Manual lifecycle (when you need to inspect the parsed model before initialisation):
from openswmm.engine import Solver, Nodes, EngineState
s = Solver("model.inp", "model.rpt", "model.out")
s.create()
s.open() # state == OPENED
nodes = Nodes(s)
print(f"Model has {nodes.count()} nodes")
for i in range(nodes.count()):
print(f" {nodes.get_id(i)} (type={nodes.get_type(i)})")
s.initialize()
s.start(save_results=True) # state == RUNNING
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
s.end()
s.report()
s.close()
s.destroy() # release engine handle
Common recipes#
Report progress every wall-clock second#
import time
from openswmm.engine import Solver, EngineState
with Solver("model.inp", "model.rpt", "model.out") as s:
last = time.monotonic()
total = s.get_end_time() - s.get_start_time()
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
now = time.monotonic()
if now - last >= 1.0:
pct = 100.0 * s.elapsed / total
print(f"{pct:5.1f}% ({s.elapsed:.4f} d / {total:.4f} d)")
last = now
Run multiple scenarios in parallel#
from concurrent.futures import ProcessPoolExecutor
from openswmm.engine import Solver, EngineState
def run_one(inp_path):
rpt = inp_path.replace(".inp", ".rpt")
out = inp_path.replace(".inp", ".out")
with Solver(inp_path, rpt, out) as s:
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
return out
inputs = ["scenario_a.inp", "scenario_b.inp", "scenario_c.inp"]
with ProcessPoolExecutor(max_workers=4) as pool:
for out_file in pool.map(run_one, inputs):
print("done:", out_file)
Each child process has its own engine handle; no global state to collide.
Stop simulation early on a custom condition#
from openswmm.engine import Solver, Nodes, EngineState
with Solver("model.inp", "model.rpt", "model.out") as s:
nodes = Nodes(s)
flooded = nodes.get_index("J1")
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
if nodes.get_depth(flooded) > 5.0:
print(f"Flood threshold hit at t={s.elapsed:.4f} d")
break # context manager still runs end/report/close
Skip writing the binary .out#
# Pass empty string for `out`
with Solver("model.inp", "model.rpt", "") as s:
...
This is faster (no per-step output writes) and saves disk; you trade
away the ability to use OutputReader afterwards.
Skip writing the report too#
with Solver("model.inp", "", "") as s:
...
Useful in tight Monte-Carlo loops or as part of a CI smoke test.
Save the modified model back to disk#
After you’ve used ModelEditor or ModelBuilder to
mutate the model, persist the result:
s.model_write("modified.inp")
The output is a fully-valid SWMM .inp file (round-trippable
through any SWMM reader).
Bulk arrays#
The Solver itself does not expose a bulk-array surface — those live on
the domain classes (Nodes, Links, etc.). The Solver
does, however, expose scalar time accessors that you’ll often
combine with bulk reads:
import numpy as np
from openswmm.engine import Solver, Nodes, EngineState
times, depths = [], []
with Solver("model.inp", "model.rpt", "") as s:
nodes = Nodes(s)
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
times.append(s.elapsed) # days since start
depths.append(nodes.get_depths_bulk().copy()) # (n_nodes,)
times = np.array(times) # shape (T,)
depths = np.stack(depths) # shape (T, n_nodes)
EngineState requirements & exceptions#
Method |
Required state |
Notes |
|---|---|---|
|
any (no-op if already created) |
Idempotent. |
|
|
Raises if the file fails to parse. |
|
|
Allocates per-element state arrays. |
|
|
Transitions to |
|
|
Returns the C error code ( |
|
|
As |
|
|
Idempotent; second call is a no-op. |
|
|
Writes the |
|
any (after |
Flushes |
|
any |
Frees the engine handle. |
Calling a method out of order raises EngineError. Common
codes you’ll see:
Code |
Name |
Meaning |
|---|---|---|
|
|
You called a method that needs |
|
|
You called |
|
|
You called |
|
|
Wrong object type for the operation (e.g. setting an outfall parameter on a junction). |
For the full list see ErrorCode.
See also#
Concepts & engine lifecycle — the broader engine-state and exception model.
Error handling, edge cases & debugging — programmatic error handling patterns.
Output reader (binary .out file) — post-process the
.outfile after the run.