Error handling, edge cases & debugging#
Note
Engine: OpenSWMM 6 — refactored.
This page is a cross-cutting reference for the EngineError
hierarchy, the EngineState rules that govern when each method
is callable, and the idioms we recommend for robust scripts.
For the underlying lifecycle see Concepts & engine lifecycle.
The exception model in one paragraph#
Every binding checks the C return code. A non-zero code raises an
EngineError — but which EngineError depends on the
underlying SWMM_ERR_*. Each subclass also inherits from a
standard library exception so existing except IndexError: /
except ValueError: handlers do the right thing without any
engine-specific imports.
from openswmm.engine import Solver
with Solver("model.inp") as s:
try:
depth = s.nodes["DOES_NOT_EXIST"].depth
except KeyError as e: # also an EngineError subclass
print(f"missing node: {e}")
try:
depth = s.nodes[10_000].depth
except IndexError as e: # likewise
print(f"out of range: {e}")
The EngineError hierarchy#
The raise_for_code() dispatcher maps
every documented SWMM_ERR_* to the corresponding subclass:
Subclass |
Also inherits from |
Raised when … |
|---|---|---|
|
Engine handle is |
|
|
Integer index is out of range. |
|
|
Argument fails range / shape / domain check. |
|
|
Method called in the wrong |
|
|
Hot start file is missing, corrupt, or schema-mismatched. |
|
|
A loaded plugin returned a failure or refused to initialise. |
|
|
Any of input / report / output file I/O failures. |
|
|
Input file has a syntactically invalid token or section. |
|
|
Solver divergence, NaN propagation, instability. |
|
|
Coordinate reference system string is missing or invalid. |
|
|
Object can’t be deleted / converted because other objects still reference it. |
|
|
Base class. Catches everything above; also raised directly for
|
Every instance carries three attributes:
.code— the raw integer (SWMM_ERR_*value)..code_enum— the same value as anErrorCodeenum member, orErrorCode.INTERNALif the code is unrecognised..message— human-readable description filled fromswmm_error_message()when not given explicitly.
from openswmm.engine import EngineError, ErrorCode
try:
s.nodes[-1].depth = 0.0
except EngineError as e:
print(e.code, e.code_enum, e.message)
# 8 ErrorCode.BADINDEX 'index out of range'
EngineState reference#
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 |
|
|
5 |
Inside the routing loop, |
|
6 |
|
|
7 |
|
|
8 |
Programmatic construction in progress (no |
Calling a method in the wrong state raises LifecycleError
(which is also a RuntimeError):
from openswmm.engine import Solver, LifecycleError
s = Solver("model.inp")
try:
s.step() # not started yet
except LifecycleError as e:
print(f"can't step in state {s.state.name}: {e}")
Recommended patterns#
Catch the broadest sensible base#
If you don’t care which specific failure occurred, catch
EngineError:
try:
s.run()
except EngineError as e:
log.error("simulation failed: %s (code=%s)", e.message, e.code_enum)
Catch the stdlib base when interop matters#
When the engine call is part of a larger pipeline that uses standard exceptions, the stdlib base classes give you uniform handlers:
def lookup(coll, key):
try:
return coll[key]
except (IndexError, KeyError):
return None
Use code_enum for decision logic#
When you do need to distinguish failures, compare against
ErrorCode — never against raw integers:
from openswmm.engine import ErrorCode
try:
s.hotstart.apply(other)
except EngineError as e:
if e.code_enum is ErrorCode.HOTSTART:
log.warning("hotstart skipped: %s", e.message)
else:
raise
Common pitfalls#
Mutating during iteration#
Adding or deleting objects through solver.nodes / solver.links
invalidates any wrapper objects currently held by Python code via a
generation counter — accessing a stale wrapper raises a
LifecycleError with a message naming the operation that
invalidated it. Re-look up by id after a structural change.
Reading state too early#
Node.depth / Link.flow / etc. only have meaningful values once
the engine is at EngineState.RUNNING (or later). Accessing them
before start() raises LifecycleError.
Confusing KeyError and BadIndexError#
coll["NO_SUCH_ID"]→KeyError(str didn’t match any object).coll[10_000]→BadIndexError(also catchable asIndexError).coll[None]→TypeError.
This split lets you write try / except KeyError: for the
“check-if-name-exists” idiom without accidentally swallowing
out-of-range integer accesses.
See also#
Concepts & engine lifecycle — engine lifecycle and the
EngineStatemachine.SWMM DateTime and Python datetime interop — the SWMM DateTime double and Python
datetimeinterop.API Reference — generated reference for every symbol surfaced above.