Nodes#
Note
Engine: OpenSWMM 6 — refactored. This page documents the
openswmm.engine.Nodes collection and the
openswmm.engine._nodes.Node wrapper objects it produces.
Legacy SWMM 5 users access nodes through the enum-driven
getValue / setValue API on
openswmm.legacy.engine.Solver — see Legacy SWMM 5 Solver.
Junctions, outfalls, storage units, and dividers — every point where
flow enters, leaves, or is stored in the network. The Nodes
collection hangs off the Solver as a lazy attribute:
from openswmm.engine import Solver
with Solver("model.inp") as s:
s.nodes # → Nodes (the collection)
s.nodes["J1"] # → Node (a wrapper for one node)
s.nodes[0] # → Node (same shape; by integer index)
Reference: openswmm_nodes.h.
Quickstart#
Five idioms cover most use cases.
# 1. Indexing — both ``int`` and ``str`` work.
j1 = s.nodes["J1"]
same = s.nodes[0] # same Node identity (different wrapper)
assert j1 == same
# 2. Iteration.
for node in s.nodes:
print(node.id, node.invert_elev)
# 3. Per-object property access.
print(s.nodes["J1"].depth)
s.nodes["J1"].lateral_inflow = 0.5
# 4. Bulk vectorised access.
depths = s.nodes.depths # numpy float64 array
s.nodes.depths = depths * 1.05
# 5. Type-specific sub-views.
s.nodes["OUT1"].outfall.type = OutfallType.FIXED
s.nodes["S1"].storage.functional = (0.0, 0.0, 100.0)
s.nodes["J1"].stats.max_depth # cumulative stats
Collection: Nodes#
Operation |
What it does |
|---|---|
|
Current node count. |
|
Returns a |
|
Yields a fresh |
|
Membership by |
|
String → int. Raises |
|
Int → string. Raises |
|
Append a node ( |
|
Remove the most recently added node. |
|
Rename in place. Invalidates any |
Bulk numpy properties#
Every per-node array is exposed as a property whose getter returns a
fresh float64 array of shape (n_nodes,) and whose setter (if
present) accepts the same shape:
Property |
Mode |
What it carries |
|---|---|---|
|
read/write |
Water depth above invert. |
|
read-only |
Hydraulic head. |
|
read-only |
Total inflow. |
|
read-only |
Flooding / overflow rate. |
|
read-only |
Stored volume. |
|
read-only |
Total outflow. |
|
read-only |
Evaporation + seepage losses. |
|
read-only |
Per-node lateral inflows. |
|
read-only |
String ids as a |
For lateral-inflow forcing, use the explicit method:
s.nodes.set_lateral_inflows(arr) # float64, shape (n_nodes,)
For per-pollutant concentrations, use:
concs = s.nodes.qualities("TSS") # or by pollutant index
Wrapper: Node#
The wrapper is the natural place to read and write a single node. All properties round-trip directly through the C API — no Python-side caching:
Property |
Type |
Mode |
Meaning |
|---|---|---|---|
|
|
read-only |
Node identifier. |
|
|
read-only |
Position in the engine’s node array. |
|
|
read-only |
JUNCTION / OUTFALL / STORAGE / DIVIDER. |
|
|
read/write |
Invert elevation. |
|
|
read/write |
|
|
|
read/write |
|
|
|
read/write |
|
|
|
read/write |
|
|
|
read-only |
Top-of-crown elevation derived from connected links. |
|
|
read-only |
Storage volume at |
|
|
read-only |
Number of links incident to the node. |
|
|
read/write |
Runtime depth above invert. |
|
|
read-only |
Runtime hydraulic head. |
|
|
read-only |
|
|
|
read/write |
One-shot lateral inflow for the next step. |
|
|
read-only |
|
|
|
read-only |
Total inflow. |
|
|
read-only |
|
|
|
read-only |
Methods:
Method |
What it does |
|---|---|
|
One-shot head boundary for the next step. |
|
Concentration of |
|
Inject a mass flux. |
|
Storage-curve inverse lookup. |
Type-specific sub-views#
Each node type exposes a small sub-namespace. Accessing the wrong one
raises AttributeError — the wrong sub-view is not silently
returned:
# OUTFALL nodes only.
out = s.nodes["OUT1"]
out.outfall.type = OutfallType.FIXED
out.outfall.set_stage(123.4)
out.outfall.flap_gate = True
out.outfall.route_to # subcatchment index, or -1
# STORAGE nodes only.
sto = s.nodes["S1"]
sto.storage.curve # curve index, or -1 if functional
sto.storage.functional = (a, b, c) # ax² + bx + c
sto.storage.seep_rate = 0.01
sto.storage.exfil_params = (suction, ksat, imd)
# DIVIDER nodes only.
div = s.nodes["D1"]
div.divider.type # integer divider type code
Statistics sub-view#
Every node carries a .stats sub-view exposing the cumulative
flooding/overflow statistics aggregated by the engine:
j1 = s.nodes["J1"]
print(j1.stats.max_depth)
print(j1.stats.max_overflow)
print(j1.stats.vol_flooded) # cubic feet (US) / m³ (SI)
print(j1.stats.time_flooded) # seconds; divide by 3600 for hours
These values are only meaningful after Solver.end() (or
equivalently, after the simulation loop terminates).
Staleness & generation counter#
Adding, removing, renaming, or converting nodes invalidates every
Node wrapper minted before the change. Touching a stale
wrapper raises StaleObjectError (which is also a
LifecycleError and therefore a RuntimeError):
j1 = s.nodes["J1"]
s.nodes.rename(0, "J1_RENAMED")
j1.depth # raises StaleObjectError
Recovery is to re-look-up by the new id (or by index):
j1 = s.nodes["J1_RENAMED"]
j1.depth # OK
Per-property reads/writes are cheap, so don’t try to cache wrappers across mutations. The collection itself never goes stale — it always addresses the current state of the model.
Equality, hashing, repr#
a == bisTruewhenaandbwrap the same(solver, index)pair, regardless of when each wrapper was minted.hash(node)is consistent with equality, soNodeis usable as a dict key / set element within a single Solver.repr(node)includes the captured id and integer index, e.g.<Node id='J1' index=0>.
See also#
Running a simulation — Solver — where
s.nodescomes from.Links — link wrappers follow the same pattern.
Output reader (binary .out file) — node time-series after the run.
Error handling, edge cases & debugging — every exception type referenced on this page.