Migrating from SWMM 5 to v6#
If you have existing Python code calling the legacy SWMM 5 solver
(openswmm.legacy.engine or any other SWMM 5 binding), this page
shows the v6.0 equivalent of every common pattern.
The legacy path continues to work — the SWMM 5 solver is preserved
verbatim under openswmm.legacy.engine and your existing code
imports unchanged. Migrate at your pace, module by module.
Why migrate?#
The v6.0 engine is the future of OpenSWMM. Compared to SWMM 5, it gives you:
Reentrancy. Multiple independent simulations in the same process — useful for ensembles, parameter sweeps, optimisers.
Domain-split API. Instead of one
getValue(SUBCATCH, idx, attr)god-method, you callSubcatchments(s).get_runoff(idx)— IDE auto-complete, type checking, and discoverability all work.Bulk numpy accessors.
Nodes(s).get_depths_bulk()returns a contiguousnp.ndarrayin one C call instead of a Python loop.Programmatic model construction. Build a model in Python via
ModelBuilder, no.inpfile required.In-place editing. Add, delete, or convert objects via
ModelEditor.Plugin SDK. Bring your own input format (GeoPackage, HDF5, …) and report writer.
New physics. Semi-implicit node continuity, Anderson acceleration on Picard, dynamic Preissmann slot (in-progress).
Side-by-side translation#
Run a model start to finish#
from openswmm.legacy.engine import Solver
with Solver("model.inp", "model.rpt", "model.out") as s:
while True:
s.step()
if s.elapsed >= s.duration:
break
from openswmm.engine import Solver, EngineState
with Solver("model.inp", "model.rpt", "model.out") as s:
while s.step(): # returns False at end-of-sim
pass
In v6,
Solver.step()returns a bool:Truewhile there is more time to simulate,Falsewhen the simulation reaches its end time. No need to trackelapsedagainstdurationyourself.
Read a node depth at every step#
from openswmm.legacy.engine import Solver, SWMMObjects, SWMMNodeProperties
with Solver("model.inp", "model.rpt", "model.out") as s:
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
d = s.getValue(SWMMObjects.NODE,
s.getObjectIndex(SWMMObjects.NODE, "J1"),
SWMMNodeProperties.DEPTH)
from openswmm.engine import Solver, Nodes
with Solver("model.inp", "model.rpt", "model.out") as s:
nodes = Nodes(s)
j1 = nodes.get_index("J1") # resolve once
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
d = nodes.get_depth(j1)
Domain class
Nodes, not enum-drivengetValue.String → integer index resolution happens once, outside the loop.
Inject a lateral inflow#
j1 = s.getObjectIndex(SWMMObjects.NODE, "J1")
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
s.setValue(SWMMObjects.NODE, j1,
SWMMNodeProperties.LATERAL_INFLOW, 1.5)
nodes = Nodes(s)
j1 = nodes.get_index("J1")
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
nodes.set_lateral_inflow(j1, 1.5)
from openswmm.engine import Forcing, ForcingMode
forcing = Forcing(s)
j1 = nodes.get_index("J1")
forcing.node_lat_inflow(j1, 1.5, ForcingMode.REPLACE, persist=True)
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
forcing.clear_all()
The SWMM 5
setValueis one-shot (overwritten by the engine on the next step). v6.0Nodes.set_lateral_inflow()is the same one-shot semantic — same code shape.For overrides that survive across steps without re-applying every loop iteration, use the new
ForcingAPI (no SWMM 5 equivalent).
Read every node’s depth at once#
n = s.getCount(SWMMObjects.NODE)
depths = [
s.getValue(SWMMObjects.NODE, i, SWMMNodeProperties.DEPTH)
for i in range(n)
] # Python loop crosses C boundary n times
depths = nodes.get_depths_bulk() # one C call, returns np.ndarray
The
*_bulkfamily is dramatically faster for read-many patterns (model, post-process, plot).
Run multiple scenarios in parallel#
# Not safe — SWMM 5 is not reentrant.
# Multiple Solver instances in one process share state.
from concurrent.futures import ThreadPoolExecutor
from openswmm.engine import Solver
def run(inp):
with Solver(inp, inp.replace(".inp", ".rpt"),
inp.replace(".inp", ".out")) as s:
while s.state == EngineState.RUNNING:
if s.step() != 0:
break
inputs = ["a.inp", "b.inp", "c.inp"]
with ThreadPoolExecutor(max_workers=4) as pool:
list(pool.map(run, inputs))
v6 is reentrant: one thread per Solver, multiple Solvers per process is fully supported.
SWMM 5’s global state means you must drop to multiprocessing.
Build a model from scratch#
Not possible without writing an .inp text file by hand and
feeding it to Solver(inp_path, …).
from openswmm.engine import (
ModelBuilder, NodeType, LinkType, XSectShape
)
m = ModelBuilder()
m.add_node("J1", NodeType.JUNCTION)
m.add_node("OUT1", NodeType.OUTFALL)
m.add_link("C1", LinkType.CONDUIT)
m.set_link_nodes(0, 0, 1)
m.set_link_length(0, 300.0)
m.set_link_roughness(0, 0.013)
m.set_link_xsect(0, XSectShape.CIRCULAR, 1.0)
m.validate()
m.finalize()
solver = m.to_solver()
solver.start()
while solver.state == EngineState.RUNNING:
if solver.step() != 0:
break
solver.end()
solver.destroy()
Read a binary .out file#
from openswmm.legacy.output import Output, ElementType, NodeAttribute
out = Output("model.out")
depth = out.getNodeSeries("J1", NodeAttribute.DEPTH)
from openswmm.engine import OutputReader, OutNodeVar
reader = OutputReader("model.out")
depth = reader.node_series("J1", OutNodeVar.DEPTH)
Both APIs read the same on-disk format (the v6 engine writes a binary
.outthat’s compatible with the legacy reader).The new
OutputReaderadds bulk*_arraymethods for vectorised reads of every node / link.
Concept-mapping cheat sheet#
SWMM 5 / legacy |
OpenSWMM 6 equivalent |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
n/a |
|
n/a |
|
n/a |
|
n/a |
|
|
|
Compatibility notes#
The legacy
SolverandOutputclasses remain exposed unchanged atopenswmm.legacy.engineandopenswmm.legacy.output. Importingopenswmmre-exports them at the top level for code that pre-dates the namespace split.The two engines share the binary
.outformat, so a v6 run can be post-processed with the legacyOutputreader and vice-versa.The two engines do not share the
.inpextension keys. v6 introduces several new sections (e.g.[OPTIONS] CRS,[USER_FLAGS], semi-implicit / Anderson knobs) that the legacy parser will warn about and ignore — your file will still run on legacy with degraded behaviour.
Where to next?#
Walk through Quickstart with v6 idioms.
Pick the domain pages that matter most to your existing code: Nodes, Links, Subcatchments, Output reader (binary .out file).
If you’re dynamically modifying
.inpfiles via string manipulation today, replace that with Programmatic model construction and Model editing (deletion + type conversion).