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

Solver.create()

Allocate the engine handle. Called implicitly by open().

Solver.open()

Parse the .inp and load plugins. Raises on failure.

Solver.initialize()

Allocate state arrays; apply initial conditions.

Solver.start()

Begin routing. save_results=True writes .out.

Solver.step()

Advance one routing step. Returns a timedelta; timedelta(0) means the simulation has ended.

Solver.stride()

Advance n_steps routing steps in one call.

Solver.steps()

Iterator that yields per-step timedelta until the simulation ends.

Solver.until()

Stride forward until a target datetime / timedelta is reached.

Solver.end()

Finalize the simulation. Cumulative stats become available.

Solver.report()

Write the summary report to the .rpt file.

Solver.close()

Close all files. Idempotent.

Solver.destroy()

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

state

EngineState

IntEnum; compares cleanly with bare integers.

elapsed

timedelta

Elapsed simulation time after the last step().

routing_step

timedelta

The active routing step duration.

start_datetime

datetime

Read/write. Microsecond round-trip ≤ 1 µs.

end_datetime

datetime

Read/write.

report_start_datetime

datetime

Read/write.

current_datetime

datetime

Updated each step; equals start_datetime + elapsed.

crs

str

CRS string from [OPTIONS] (or raises CRSError).

is_between_events

bool

True when the current time falls outside any [EVENTS] window.

steady_state_skip

bool

Read/write; toggles SKIP_STEADY_STATE.

handle

int

Raw engine pointer for advanced interop.

generation

int

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

solver.nodes

Node collection (see Nodes).

solver.links

Link collection (see Links).

solver.subcatchments

Subcatchment collection (see Subcatchments).

solver.gages

Rain-gage collection (see Rain gages).

solver.pollutants

Pollutant collection (see Pollutants).

solver.tables

Time series, curves, patterns (see Tables (time series, curves, patterns)).

solver.inflows

External / DWF / RDII inflows (see External inflows).

solver.controls

Control rules and direct actions (see Control rules).

solver.forcing

Runtime forcing (see Advanced forcing).

solver.infrastructure

Transects, streets, inlets, LIDs (see Infrastructure (transects, streets, inlets, LIDs)).

solver.spatial

Coordinates, CRS, vertices, polygons (see Spatial (CRS, coordinates, geometry)).

solver.quality

Landuse, buildup, washoff, treatment (see Water quality (landuse, buildup, washoff, treatment)).

solver.statistics

Cumulative simulation statistics (see Statistics).

solver.mass_balance

Continuity errors and flux totals (see Mass balance).

solver.editor

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#