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
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 |
|---|---|---|
|
1 |
Engine handle allocated; no input parsed. |
|
2 |
|
|
3 |
Initial conditions applied; arrays allocated. |
|
4 |
Routing started; no time has elapsed. |
|
5 |
At least one step has been taken. |
|
6 |
|
|
7 |
|
What you can do in each state#
You want to … |
Required state(s) |
See |
|---|---|---|
Add objects ( |
pre-Solver |
|
Edit / delete / convert objects |
|
|
Read object identity / topology |
|
|
Read geometry (invert, length, …) |
|
|
Set geometry / parameters (invert, n, …) |
|
same |
Apply initial conditions |
|
|
Read hydraulic state (depth, flow, …) |
|
|
One-shot per-step setters ( |
|
|
Persistent runtime forcing |
|
|
Add / clear control rules |
|
|
Read continuity / mass-balance |
|
|
Read accumulated statistics |
|
|
Save a hot-start |
|
|
Apply a hot-start |
|
|
Persist edits to |
|
Running a simulation — Solver
( |
A method called outside its state envelope raises
EngineError. The most common codes:
Code |
Name |
Meaning |
|---|---|---|
|
|
Method needs |
|
|
Called |
|
|
Called |
|
|
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.step():
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.step():
pass
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()afterSolver.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() == vis 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.
Print the report file#
with Solver("model.inp", "model.rpt", "model.out") as s:
while s.step():
pass
print(open("model.rpt").read())
The .rpt file contains the engine’s own warnings / errors and
continuity summary — read it before debugging the Python side.
See also#
Concepts & engine lifecycle — the conceptual model behind the rules above.
Running a simulation — Solver — the methods whose state requirements this page cross-references.
Mass balance — continuity diagnostics worth gating CI on.