Scene 1 of 11
Numbers read from the source are real. Values tagged example or illustrative on the stage only make the diagram move.
OpenSWMM Engine 6.0.0-alpha.4 · HydroCouple/openswmm.engine · swmm6_rel @ 93421ef · 20 Oct 2026
Scene 1 of 11
Numbers read from the source are real. Values tagged example or illustrative on the stage only make the diagram move.
| # | Part | Phase | Source line |
|---|
About
This page animates the OpenSWMM Engine, the C++20 successor to EPA SWMM 5 that Caleb Buahin and the OpenSWMM team are building in the HydroCouple/openswmm.engine repository. Eleven scenes follow one run in the order the code executes it: the lifecycle of an engine handle, input becoming arrays, the time step, runoff on its own clock, the routing step, the dynamic wave loop, finite-volume cells, the 2D surface, the aquifer beneath it, and output.
Every default, keyword and line number was read from the swmm6_rel branch at commit 93421ef. The build script re-opens each cited line and checks its text: 89 of 89 passed. The engine was not compiled or run for this page; behavioral figures come from the authors' comments in the source and say so.
This page was built with Claude, Anthropic's AI model, in a Claude session on 20 October 2026, at the request of Robert (Bob) Dickinson, who asked for an animation of how SWMM6 works based on Caleb Buahin's GitHub repository. Bob chose the subject, and responsibility for publishing the page is his.
What Claude did. It read the source at commit 93421ef, chose what to show, and wrote the drawing code for all eleven scenes, the narration, the Doc essay and its figures, the GIF encoder, the build script that re-checks every cited line, and the Word version of the essay.
How it was checked. Mechanically: 89 cited lines re-read against the source, option keywords confirmed in the parsers, line counts made by script, and the GIF encoder decoded by Pillow with no pixel mismatches over 345,600 pixels. No person has yet reviewed the narration against the source line by line, and nobody from the OpenSWMM team has reviewed it.
Doc
A walk through the OpenSWMM Engine, in the order it runs
The same essay ships as a Word file, how-swmm6-works.docx, sent alongside this page.
A good engineer does not trust a machine until the cover has come off it. You want to see which shaft turns which gear, where the heat goes, and what happens when the load comes on. A computer program deserves the same treatment, and it rarely gets it.
For most of its life SWMM has had one answer to the question of how it really computes something: read the code. The program came out of an EPA contract in 1971. Its dynamic wave solver descends from EXTRAN, the Extended Transport block that joined SWMM in the 1970s. Lewis Rossman rewrote the whole program in C as SWMM 5. Generations of modelers learned it the hard way, one routine at a time.
Now there is a second machine in the shop. The OpenSWMM Engine lives in the HydroCouple/openswmm.engine repository on GitHub. Caleb Buahin and the OpenSWMM team are building it in C++20; it stands at version 6.0.0-alpha.4, with the EPA-lineage engine kept alongside it in the same repository as a 5.3.0-beta.3 wrapper. The new engine adds a finite-volume pipe solver, a two-dimensional surface and an aquifer under that surface.
This page takes the new machine apart in the order it runs. Every default, keyword and line number here was read off the swmm6_rel branch at commit 93421ef, pushed on October 20, 2026. When the film cites a line, the line is real, and the link takes you to it.
The Film tab is eleven short scenes, a little over two minutes end to end. Each scene is one assembly of the machine, drawn as a working diagram: the two engines, the run’s lifecycle, input becoming arrays, picking the time step, runoff on its own clock, the routing step in order, the dynamic wave loop, finite-volume cells, the 2D surface, the aquifer under it, and closing the books. Under each scene sit the narration and the source lines it describes. Numbers taken from the source are real. Numbers that only make a diagram move, such as a residual shrinking pass by pass, are tagged example or illustrative on the stage.
Before you take a machine apart, weigh it. The weight tells you where the work went.
The legacy engine in src/legacy is 82 files and 60,304 lines of C. The new engine in src/engine is 596 files and 231,919 lines of C++, 3.8 times the size.
Look where the mass sits. The engine shell, its C API and the checkpoint machinery run to 49,268 lines. The 2D surface and the aquifer under it take 48,235. Hydraulics is 35,791, and 13,368 of that is the finite-volume solver alone. Together the mesh, the aquifer and the finite-volume solver are 27 percent of the new engine. The old program never had any of that machinery, and it is where much of the new code has gone.
The engine is a state machine, and it is strict about it. A handle starts out CREATED. swmm_engine_open parses the input and leaves it OPENED. initialize applies the initial conditions, and start sets it RUNNING. Then you call swmm_engine_step as many times as it takes. Each call advances one routing step and reports the elapsed time in days. When the clock passes the end date, step marks the run ENDED by itself. report writes the summary and close releases everything.
Call a function out of turn and you get an error code back instead of a crash. There is a BUILDING state for a model put together through the API with no input file at all, and an ERROR_STATE that tells you to ask swmm_engine_last_error what happened.
Here is why a modeler should care. The step belongs to the caller. Between any two steps a program can read a depth, push an inflow, change a pump setting or save a checkpoint. The state lives in the handle instead of in global variables, so two models can run in one program without fouling each other. The public C API is split into 42 headers, one per subject: nodes, links, gages, the 2D surface, hot starts and so on.
An input file is a stack of bracketed sections. The engine keeps a SectionRegistry, a table that maps each section name to a handler, the code that knows how to read that one section. OptionsHandler takes [OPTIONS], NodesHandler the junctions, outfalls and storage units, LinksHandler the conduits, pumps and regulators. The 2D model registers its own: [2D_OPTIONS], [2D_VERTICES], [2D_TRIANGLES], [2D_QUADS] and the rest. A plugin can register a section of its own, and the model need not come from text at all: there are GeoPackage and HDF5 input plugins.
The handlers do not build one object per junction. They fill columns. Every node’s invert elevation goes in one array, every node’s depth in another, every link’s flow in a third. The name for this is structure-of-arrays. It sounds like clerk’s work, and it is. It also means that when a solver sweeps ten thousand conduits, the numbers it needs next lie side by side in memory, where the processor can reach them fast.
step() runs 419 lines, 1834 to 2252 of SWMMEngine.cpp. Strip off the guard rails and it does eight things, always in this order:
The step length is the smallest of several limits. ROUTING_STEP, 20 s by default, is the ceiling. With VARIABLE_STEP set, 0.75 by default, the router works out a Courant-limited step from the conduits. RULE_STEP trims the step so it lands exactly on the next control-rule boundary, and the end of the run clamps the last step. Between storm events the engine can jump straight to the next runoff or report time.
One limit is missing, and on purpose. The 2D surface never shortens the pipe step. The source says so in a comment: the 2D marcher owns its own stability by subcycling. Each part of the machine keeps its own governor.
Here is a design decision from the old program that the new engine keeps to the letter. Runoff does not run every routing step. It runs on its own clock: WET_STEP, 300 s by default while it rains, and DRY_STEP, 3,600 s when it does not. Inside each runoff step the order is rain gages, climate, snowmelt, subcatchment runoff and infiltration, surface water quality, LID units and groundwater.
Routing comes around far more often. At each routing step the engine does not recompute runoff. It interpolates between the last two runoff results:
The comment above that code gives the reason in one sentence: computing runoff at every 5-second routing step produces peak rates 15 to 18 percent higher, with the same volume and sharper peaks. That figure is the source’s own, and this page did not re-run it. Two clocks with a straight line between them cost less than one fast clock, and they keep the new engine’s hydrographs in step with SWMM 5’s.
The routing phase works in a fixed order: pump targets, control rules, inflows (dry weather, direct, RDII and interface files), 2D outfall preparation, LID storage nodes, saving the old hydraulic state, 2D coupling conductances, exfiltration, and then the router. After the router come the 2D surface and water quality. The order is engineering. Controls act before the inflows are summed. Exfiltration rates are fixed before the router runs, so the router sees them. The surface moves only after the pipes have settled.
The router dispatches to one of four solvers: STEADY, KINWAVE, DYNWAVE or FV. Dynamic wave is the one most of us lean on, and its heart is a Picard iteration: guess the heads, compute the flows, use the flows to correct the heads, and go around again until the answer stops moving.
Each pass computes conduit geometry, solves momentum in every conduit, scatters the conduit flows onto their nodes, evaluates pumps, orifices, weirs and outlets, sets the outfall depths from the current flows, and updates every node depth. Then comes the test. HEAD_TOLERANCE is 0.005 ft unless you set it, and MAX_TRIALS is 8. The node update defaults to NODE_CONTINUITY EXPLICIT, the classic two-branch form, with SEMI_IMPLICIT available. ANDERSON_ACCEL, which blends earlier iterates to speed convergence, defaults to off.
The threading is worth a look. The whole loop runs inside one OpenMP team, forked once per routing step instead of once per phase per pass. Every parallel loop writes only its own elements, and on that basis the source states the results are bit-identical at any thread count. That is a strong claim, the kind you test before you lean on it. This page reports it and did not measure it.
Dynamic wave carries one flow per conduit, computed at its middle. The finite-volume router, FLOW_ROUTING FV, cuts every conduit into cells and solves the Saint-Venant equations in conservation form, one cell at a time. At each face between two cells an approximate Riemann solver, HLLC by default, decides how much water and momentum cross. The number of cells is max(FV_MIN_CELLS, ceil(L / FV_CELL_LENGTH)). The floor is 4, and there is no length target unless you set one.
Why four and not one? The authors measured it on Example1 and left the numbers in the source, which is exactly where numbers like that belong.
One cell per conduit gives a mean peak-flow deviation from dynamic wave of 37.1 percent. Four cells give 15.3 percent at 2.2 times the run time of one; eight give 7.6 percent at 5.4 times. The comment calls four the knee and says one cell was a known-wrong setting, not a cheap approximation. Keep in mind that deviation from dynamic wave is not error, because dynamic wave is not the truth either. The trend is what tells you when a mesh is too coarse.
The FV router substeps on its own to fill each routing step, at FV_CFL 0.5. Local time stepping, FV_LTS, on by default, lets short conduits take small steps while long ones take big ones, so the shortest pipe no longer sets the pace for the whole network. The October 20 push added a native implicit, backward-Euler FV prototype with its own GMRES linear solver. It has no input keyword yet, and its commit message names a known failure that holds back its promotion.
Turn on the 2D surface and the ground becomes a mesh of triangles; quads are accepted as well. The default momentum equation is LOCAL_INERTIAL, the de Almeida and Bates face-flow update, with FULL_SWE and DIFFUSIVE_WAVE on the shelf. A coupled junction trades water with the cells around it through an orifice law by default. COUPLING_LAW WEIR_ORIFICE switches to a regime-aware law with free and submerged weir and orifice forms.
The surface runs at its own stability limit, CFL_NUMBER 0.7, and it advances in sync batches covering several routing steps, because a routing step of a second or so is shorter than one of its local-time-step cycles. Water exchanged during a batch flows back to the pipes as a uniform rate across the batch. BACKEND AUTO hands a big mesh to a Kokkos plugin for OpenMP, CUDA, HIP or SYCL, and keeps a small one on the CPU.
The October 20 push fixed an outfall bug worth knowing about. A flap-gated outfall under a flooded surface used to discharge as if its outlet were free, because the code skipped the 2D tailwater whenever the gate was present and the surface stood above the free stage. Now the tailwater applies to every coupled outfall, and the gate does the one thing a flap gate does: it stops reverse flow.
Below the mesh the engine can carry a two-zone aquifer, an unsaturated zone over a saturated one. It has no time loop of its own. The surface marcher calls four hooks. assignTiers gives each cell its time-step tier. fireGwFaces moves water sideways between cells by Darcy’s law. fireGwCells gathers every flow into a cell and updates it. settle flushes the accumulators before any re-tiering. Each exchange is booked as plus-or-minus volume at the pace of the part that produced it and collected at the pace of the part that owns it. That is the same bookkeeping that lets the surface conserve mass across tiers, borrowed instead of derived a second time.
Seepage out of a conduit goes to the aquifer cells the pipe crosses, split by length. Three closures describe the unsaturated zone: CLOSED_FORM, ENSLAVED, and SIGMA, an explicit column of layers. AUTO, the default, picks one at start-up. In the 25 commits of October 19 and 20, wells, flow edges, head boundaries, equilibrium and SIGMA chemistry, solute transport and water age were all moved onto one shared, conservative, second-order Runge-Kutta step.
When routing is done, the engine closes the books. Each routing mass-balance term is booked by the trapezoid rule: half of last step’s rate times dt, plus half of this step’s. Final storage is computed, the clock advances, and a snapshot of the result arrays goes to the output side.
Output runs on its own thread. The main thread posts each snapshot into a bounded queue and goes straight back to computing. The IO thread takes the snapshots off the queue and hands them to every output plugin: the binary .out file, the .rpt report, GeoPackage, and a CF/UGRID HDF5 file for 2D results. Hot starts and checkpoints are written last, after the step has been accepted. Then the crank turns again.
Every default, option keyword and line number in the film and in this essay was read from the source at commit 93421ef. The script that built this page opens each cited file again and confirms the cited line still contains the text it is cited for: 89 of 89 passed. The option keywords were confirmed against the parsers that read them, OptionsHandler.cpp and SectionHandlers2D.cpp. The line counts were made by a script over every .c, .h, .cpp and .hpp file under the two source trees. The GIF recorder’s encoder was checked by decoding a test file with Pillow: 345,600 pixels, no mismatches.
One check was not made. The engine was not compiled or run for this page. Statements about behavior, such as the 15 to 18 percent peak figure, the cell-count table and bit-identical threading, come from the authors’ comments in the source and are labeled that way.
A film about a machine is not the machine. Know where this one stops.
OPENSWMM_HAS_2D.This page was built with Claude, Anthropic’s AI model, in a Claude session on October 20, 2026, at the request of Robert (Bob) Dickinson, who asked for an animation of how SWMM6 works based on Caleb Buahin’s GitHub repository. Bob chose the subject, and responsibility for publishing the page is his.
Claude read the source at commit 93421ef, chose what to show, and wrote the film’s drawing code, the narration, this essay, the GIF encoder, the build script that re-checks every cited line, and the Word version of this essay. The checks listed under What was checked are mechanical. No person has yet reviewed the narration or this essay against the source line by line, and nobody from the OpenSWMM team has reviewed it.
An eleven-scene film of the OpenSWMM Engine (SWMM6), drawn from its source at commit 93421ef.
Renders the frames straight from the film, adds a caption bar, and encodes an animated GIF in this tab.