Error handling, edge cases & debugging#

Note

Engine: OpenSWMM 6 — refactored.

This page is a cross-cutting reference for the exception model, the EngineState rules that govern when each method is callable, and the patterns we recommend for robust scripts.

For the underlying lifecycle see Concepts & engine lifecycle.


The exception model in one paragraph#

Every Cython binding checks the C return code. Anything non-zero raises an EngineError, and the message is filled in by the C API — you don’t construct one yourself. Pure-Python checks (wrong-type argument, out-of-range index, missing id) raise TypeError / IndexError / KeyError before the C call dispatches, so they don’t carry an engine error code.

from openswmm.engine import Solver, Nodes, EngineError, EngineState

with Solver("model.inp", "model.rpt", "model.out") as s:
    nodes = Nodes(s)
    try:
        nodes.get_depth("does-not-exist")
    except EngineError as e:
        print(f"engine returned {e.code}: {e.message}")
    except KeyError as e:
        print(f"missing id: {e}")

EngineState reference (cheat-sheet)#

The Solver moves through these states in strict order:

State

Value

Meaning

CREATED

0

Engine handle allocated; no input parsed.

OPENED

1

.inp parsed; objects accessible for inspection / editing.

INITIALIZED

2

Initial conditions applied; arrays allocated.

RUNNING

3

start() called; routing loop active, step() callable.

PAUSED

4

Routing temporarily halted (reserved for future hot-swap support).

ENDED

5

end() called; cumulative results available.

REPORTED

6

report() called; summary written.

CLOSED

7

close() called; .rpt / .out flushed.

What you can do in each state#

You want to …

Required state(s)

See

Add objects (ModelBuilder)

pre-Solver

Programmatic model construction

Edit / delete / convert objects

OPENED

Model editing (deletion + type conversion)

Read object identity / topology

OPENED

Nodes, Links, Subcatchments

Read geometry (invert, length, …)

OPENED

Nodes, Links

Set geometry / parameters (invert, n, …)

OPENED

same

Apply initial conditions

INITIALIZED

Running a simulation — Solver

Read hydraulic state (depth, flow, …)

RUNNING or ENDED

Nodes, Links

One-shot per-step setters (set_depth, set_flow, …)

RUNNING

Nodes, Links

Persistent runtime forcing

RUNNING

Advanced forcing

Add / clear control rules

OPENED or RUNNING

Control rules

Read continuity / mass-balance

ENDED (final), RUNNING (partial)

Mass balance

Read accumulated statistics

RUNNING or ENDED

Statistics

Save a hot-start

RUNNING or ENDED

Hot start

Apply a hot-start

OPENED or INITIALIZED

Hot start

Persist edits to .inp

OPENED

Running a simulation — Solver (Solver.model_write())

A method called outside its state envelope raises EngineError. The most common codes:

Code

Name

Meaning

20

ERR_API_NOT_OPEN

Method needs OPENED; solver still CREATED.

21

ERR_API_NOT_STARTED

Called step() before start().

22

ERR_API_NOT_ENDED

Called report() before end().

23

ERR_API_INVALID_TYPE

Wrong object kind (e.g. setting a pump curve on a conduit).

The full enum is ErrorCode.


Defensive patterns#

Always use the context manager for the Solver#

with Solver("model.inp", "model.rpt", "model.out") as s:
    ...                # raises here are still cleaned up

The context manager runs end report close destroy even if your loop body raises. Skipping it leaks the engine handle on error.

Resolve names once, outside the loop#

j1 = nodes.get_index("J1")     # raises KeyError if not in model
while s.state == EngineState.RUNNING:
    if s.step() != 0:
        break
    d = nodes.get_depth(j1)    # no per-step name-lookup overhead

This catches typos at startup rather than after a long run.

Validate model state before running#

s.open()
if nodes.count() == 0:
    raise RuntimeError("model has no nodes")
if links.count() == 0:
    raise RuntimeError("model has no links")

# Programmatic edits: catch issues now, not at step()
s.initialize()

Verify continuity after the run#

from openswmm.engine import MassBalance

with Solver("model.inp", "model.rpt", "model.out") as s:
    while s.state == EngineState.RUNNING:
        if s.step() != 0:
            break
    mb = MassBalance(s)
    if abs(mb.get_routing_continuity_error()) > 2.0:
        raise RuntimeError(
            f"routing continuity {mb.get_routing_continuity_error():+.4f}% > 2%"
        )

Wrap third-party callbacks#

If you register progress / step callbacks that call back into your own Python code, isolate exceptions so a callback bug doesn’t tear down the run mid-way:

def _safe(fn):
    def wrapper(*a, **kw):
        try:
            return fn(*a, **kw)
        except Exception as e:
            import traceback; traceback.print_exc()
            # swallow — never propagate into the C engine
    return wrapper

run_with_callback("model.inp", "model.rpt", "model.out",
                  callback=_safe(my_progress_handler))

Edge cases & gotchas#

  • Bulk-array memory aliasing. *_bulk() methods return arrays that share memory with an internal scratch buffer. Read-once is fine; if you keep the array, .copy() it.

  • Index stability. Integer indices are stable for the lifetime of a single Solver but not across runs of different models. Always re-resolve via get_index() after Solver.open().

  • Float precision in `set_*` / `get_*` round-trips. The C engine stores most values in double precision; a few legacy code paths use single precision internally. set_x(v); get_x() == v is not guaranteed to be exact for those paths.

  • String ids are bytes-encoded UTF-8 on the C side. Non-ASCII ids work, but the C engine truncates at the first NUL byte and caps at ~80 chars.

  • Threading. One Solver per thread is supported; a single Solver is not thread-safe. See Concepts & engine lifecycle for the full contract.


Debugging tips#

Build with DEBUG=1#

DEBUG=1 pip install -e . --no-build-isolation

Produces unoptimised binaries with full debug symbols, suitable for lldb / gdb / IDE step-through into the C engine. See Installation for the rest of the env-var matrix.

Inspect the parsed model before stepping#

s = Solver("model.inp", "", "")
s.create()
s.open()
print(f"nodes: {Nodes(s).count()}, links: {Links(s).count()}")
# … check every domain class you care about …
s.destroy()         # without initializing / starting

This is the fastest way to confirm a parse without paying for the full simulation.


See also#