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 .inp input file.

  • rpt — path for the human-readable .rpt report. Empty string to skip.

  • out — path for the binary .out results 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

Solver.create()

Allocate the engine handle. Implicit on open().

Solver.open()

Parse the .inp and load plugins.

Solver.initialize()

Allocate state arrays; apply initial conditions.

Solver.start()

Start routing. save_results=True writes .out.

Solver.step()

Advance one routing step. Returns the C error code (0 on success). Poll state for completion.

Solver.stride()

Advance n_steps steps in one call. Returns the C error code.

Solver.end()

Stop routing; finalize cumulative outputs.

Solver.report()

Write the human-readable .rpt summary.

Solver.close()

Flush .rpt / .out and close files.

Solver.destroy()

Free the engine handle. Always call this.

Solver.__enter__() / __exit__

Context-manager wrapper for the full lifecycle.

Inspection / time#

Property / method

Returns

Solver.state

Current EngineState (int).

Solver.handle

Opaque engine handle (mostly for plugin authors).

Solver.elapsed

Elapsed simulation time in days.

get_start_time()

Simulation start time (decimal days since 1899-12-30).

get_end_time()

Simulation end time (same epoch).

get_current_time()

Current simulation time (same epoch).

get_routing_step()

Current routing time step in seconds.

Solver.start_datetime

Simulation start as a datetime.datetime.

Solver.end_datetime

Simulation end as a datetime.datetime.

Solver.report_start_datetime

Report start as a datetime.datetime.

get_option() / set_option()

Read or update [OPTIONS] entries (e.g. FLOW_UNITS).

get_option_ext() / set_option_ext()

Read or update extension options unknown to base SWMM.

get_crs()

Coordinate reference system string (EPSG / PROJ / WKT).

Event windows, steady-state, and callbacks#

Method

What it does

Solver.events_count() / events_get() / events_set()

Inspect or update the event-window list (start/end pairs).

Solver.events_add() / events_remove() / events_clear()

Programmatically grow or trim the event-window list.

Solver.get_steady_state_skip() / set_steady_state_skip()

Enable/disable the steady-state skip optimisation.

Solver.set_step_begin_callback()

Register a callable invoked before each routing step.

Solver.set_step_end_callback()

Register a callable invoked after each routing step.

Solver.set_warning_callback()

Register a callable invoked when the engine raises a warning.

Module-level helpers#

Function

What it does

openswmm.engine.run()

One-call helper that opens, runs, reports, and closes a model.

openswmm.engine.run_with_callback()

Same as run() but invokes a progress callback each step.

Save / serialise#

Method

Action

model_write()

Write the current model state back out as .inp.


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

create

any (no-op if already created)

Idempotent.

open

CREATED

Raises if the file fails to parse.

initialize

OPENED

Allocates per-element state arrays.

start

INITIALIZED

Transitions to RUNNING. save_results=False skips .out writes.

step

RUNNING

Returns the C error code (0 on success). Engine transitions to ENDED when the end-time is reached; poll state to detect completion.

stride

RUNNING

As step, but advances n_steps timesteps in one call.

end

RUNNING or ENDED

Idempotent; second call is a no-op.

report

ENDED

Writes the .rpt summary.

close

any (after ENDED for a sensible report)

Flushes .out and .rpt.

destroy

any

Frees the engine handle.

Calling a method out of order raises EngineError. Common codes you’ll see:

Code

Name

Meaning

20

ERR_API_NOT_OPEN

You called a method that needs OPENED while still CREATED.

21

ERR_API_NOT_STARTED

You called step() before start().

22

ERR_API_NOT_ENDED

You called report() before end().

23

ERR_API_INVALID_TYPE

Wrong object type for the operation (e.g. setting an outfall parameter on a junction).

For the full list see ErrorCode.


See also#