The Water Decides
Told in the manner of Isaac Asimov’s science essays
Robert (Bob) Dickinson · SWMM Enablement
App version 1.1 · 17 October 2026
Engines: EPA SWMM 5.2.4 and OpenSWMM 6.0.0-alpha.4 (swmm6_rel 64d517f), both compiled to WebAssembly
Units: US customary (ft, s, cfs)
2. Ripples That Cannot Swim Upstream
13. What the Lab Cannot Tell You
A Note on Style
This is the second telling of the same story. The first, “Control Section Lab: How the App Works”, set out the facts in the usual engineering manner. This one sets out the same facts in the manner of Isaac Asimov’s science essays: plain words, one idea at a time, a little history where the history helps, and the conviction that anything worth knowing can be explained to anyone willing to follow along.
Only the telling has changed. The numbers, tables and figures are the same, and every number is still read by a script from the data built into the app, so the two documents cannot disagree. The source code excerpts are quoted exactly, and the OpenSWMM NOTICE is reproduced word for word, because some things should not be retold in anyone’s style.
Asimov himself, I hasten to add, had nothing to do with it. He died in 1992, a quarter of a century before web browsers learned to run WebAssembly. The imitation is the work of an AI model, and the section on the use of AI at the end says exactly what it did.
1. The Question
Stand on a footbridge over a quiet stream and drop a pebble into the water. A ring of ripples spreads out, and if you watch closely you will notice that the ring is lopsided. The downstream half races away. The upstream half crawls, because it is swimming against the current. But it does swim upstream, slowly, and in time it reaches water that the pebble never touched.
Now walk down to a steep stretch of the same stream, where the water is shallow and quick, and drop another pebble. This time the whole ring is swept downstream. No part of it gets upstream at all.
That small experiment holds the whole subject of this document. Where water is slow, news travels upstream: a weir, a pond or a narrowing downstream can be felt, through the water, far above it. Where water is fast, news cannot travel upstream, and what happens below is invisible to what happens above. Somewhere between the two there must be places where the water moves exactly as fast as a ripple can travel, and those places turn out to be the most important points in a channel. Engineers call them control sections, because they control: they fix the water level above them and the water level below them, and nothing on the far side can argue.
Which part of a channel becomes a control section is not obvious. It depends on the discharge, the shape of the channel and the water level at the far end, and when any of these change, the control can jump from one place to another. The Control Section Lab is a single web page built to ask that question of one channel, and then to ask a second question: do our computer models get the answer right?
The idea comes from the Control Section Lab at Vasslab, which this app rebuilds around a SWMM-style channel of eight conduits. To it, the app adds two things. First, real engines: EPA SWMM 5.2.4 and OpenSWMM 6, compiled to WebAssembly and run inside the page on the very same channel. Second, the documented rules of the InfoWorks ICM 1D solver. Beside them sits an exact reference solution, so that each engine can be compared, node by node, with what the hydraulics say should happen.
| Item | Detail |
| Delivery | A private claude.ai artifact, and a standalone index.html that runs from any static host (for example Netlify). No server, no sign-in, no network calls after load. |
| Page | One HTML file of about 6,472 KB. It carries both engines as base64 WebAssembly, the recorded runs, the source excerpts and the app code. |
| Engines | EPA SWMM 5.2.4 (843,203 bytes of wasm). OpenSWMM 6.0.0-alpha.4, branch swmm6_rel at commit 64d517f of 2026-10-16 (3,868,436 bytes of wasm), in two modes: dynamic wave (DW) and finite volume (FV). |
| Reference | Standard-step energy equation with mixed regime and jumps placed by specific force, written for this app. |
| ICM | Documented solver rules only. No ICM engine runs in the page. |
| Units | US customary throughout: ft, s, cfs. g = 32.2 ft/s² and the Manning constant 1.486, the same constants SWMM5 uses. |
| Version | App v1.1, built 2026-10-17. |
With the app you can find out:
- which of four candidate controls (a sill, a narrowing, a change of slope and the outfall) sets the water surface, and over which stretch of channel;
- at what tailwater level each control drowns;
- how closely SWMM 5.2.4, OpenSWMM 6 dynamic wave and OpenSWMM 6 finite volume reproduce that water surface, and whether each of them settles down;
- why the dynamic-wave engines miss certain controls, which turns out to be a story about a single term in one equation;
- what profile ICM’s documented settings imply for the same channel, as a prediction for someone to test in ICM;
- and, if you want to try it yourself, a ready-to-run .inp file and CSV files of every profile.
2. Ripples That Cannot Swim Upstream
Before we look at the app, it is worth spending a few pages on the three ideas it is built from. None of them is difficult, and all of them are old.
The speed of a ripple
A small wave in shallow water travels at a speed that depends on one thing only, the depth. Joseph-Louis Lagrange worked this out in 1781, and the result is pleasingly simple:
c = (g y)1/2
where g is the acceleration of gravity (32.2 ft/s²) and y is the depth. Take the approach channel of the lab at its default discharge of 600 cfs. It is 30 ft wide and flows 4.23 ft deep, so the water moves at 4.73 ft/s while a ripple moves at 11.67 ft/s. A ripple heading upstream therefore makes headway at 6.93 ft/s. News gets through.
Now take the chute, the steep conduit C6. There the same 600 cfs runs only 1.62 ft deep, so the water moves at 12.32 ft/s and a ripple at only 7.23 ft/s. A ripple heading upstream is carried backward at 5.09 ft/s. News does not get through.
The ratio of the two speeds is called the Froude number, after William Froude, the Victorian engineer who towed model ships through a long tank at Torquay in the 1870s to learn how real ships would behave. A ship makes waves, and how fast those waves can move compared with the ship decides how much they drag on it; Froude’s ratio of speeds turned out to apply to rivers just as well as to hulls.
Fr = V / (g y)1/2
Below 1 the flow is called subcritical, and it is slow and deep. Above 1 it is called supercritical, and it is fast and shallow. In the approach channel the Froude number is 0.41; on the chute it is 1.70.
Critical depth
Between the two lies Fr = 1, where the water moves exactly as fast as a ripple. For a given discharge in a given channel there is one depth at which this happens, the critical depth. In the 30-ft channel at 600 cfs it is 2.32 ft, and there the water and the ripple both move at 8.64 ft/s. In the 14-ft throat it is 3.85 ft, at 11.13 ft/s.
yc = ( Q2 / g b2 )1/3
There is a second way of seeing critical depth, and it is the one the reference solver uses. Water flowing in a channel carries energy above the bed, partly as depth and partly as speed. For a given discharge, that energy is smallest at critical depth: go deeper and the depth costs you, go shallower and the speed costs you. So when a section of channel has barely enough energy to pass the flow at all, it has no choice. It must run at critical depth, the cheapest way through. Jean-Baptiste Bélanger described this idea of critical flow in 1828, in the same work in which he gave engineers the step method for computing water surfaces, which is the method the reference solver still uses.
The rule the whole app turns on
Put the two ideas together and you get the rule:
Subcritical flow takes its orders from downstream. Supercritical flow takes its orders from upstream. A control section is where the two meet: the flow passes through critical depth, the water surface above it is set by it, and the water surface below it starts from it.
So to find the water surface in a channel, you must find its control sections first. That is precisely what the lab does, and precisely where, as we shall see, the computer models sometimes go astray.
The jump
One question remains. If fast water must become slow again, say at the foot of a steep chute where the channel flattens out, how does it do it? Not gently. It does it in a sudden, churning step called a hydraulic jump, in which the water surface leaps up and a good deal of energy is turned into turbulence and, eventually, heat.
Leonardo da Vinci sketched hydraulic jumps around 1504. Giorgio Bidone of Turin published the first experiments on them in 1820. Bélanger tried to explain them in 1828 by conserving energy and got the wrong answer, which is reassuring to those of us who also get things wrong the first time. About ten years later he saw the mistake: energy is precisely what a jump does not conserve. What it conserves is momentum. Measured per unit weight of water, the quantity that balances across a jump is the specific force:
M = Q2 / (g b y) + b y2 / 2
and the relation between the depths before and after a jump still carries Bélanger’s name. The reference solver places every jump where the specific force says it must go.
Ripple speed, minimum energy, conserved momentum: those three ideas are the whole of the physics in the app. Everything else is bookkeeping, though there is a fair amount of bookkeeping.
3. A Tour of the Premises
The app is a single page. Across the top runs a header with the app’s name, a small pill giving the version and build date, a Dark / Light / Auto switch for the colours, a ? button, a Make a GIF button and an Export menu. Below the header are five tabs.

Figure 1. The Lab tab at default settings, in the light theme. Sliders on the left, the long-section on the right, the control list and notes underneath.
| Tab | What it does |
| Lab | The reference solution. Pick a scenario or move the sliders; the long-section recomputes on every move and colours the water by the control that governs it. Overlays add critical depth, the energy line, the three engines and the ICM-rule line. |
| Engines | Runs SWMM5, SWMM6 DW and SWMM6 FV on the current channel and shows each run’s status, a node-by-node table against the reference, the flow classification from the engine’s own report, and the exact source lines behind each engine rule. Holds the dynamic-wave options. |
| ICM rules | Applies ICM’s documented defaults and draws the steady profile they imply. Explains why damped inertia erases controls. |
| Findings | Nine measured results, F1 to F9, recorded at build time, with a button that runs every case again in the browser. |
| About | Method, engine builds, verification, what the app does not show, the AI-use statement, sources, and credits and licenses. |
A newcomer is greeted by a short welcome dialog, which offers to skip or to take a tour. The tour has seven stops, each highlighting the part of the screen it talks about. Once dismissed, the dialog stays away in that browser (a small flag is kept in the browser’s local storage), and the ? button brings it back whenever it is wanted.

Figure 2. The welcome dialog.
| Stop | Highlights | Says |
| 1 | Scenario card | Each preset sets the channel up to show one kind of control. |
| 2 | Discharge slider | The reference recomputes on every move. |
| 3 | Long-section | Plan view on top, profile in the middle, Froude number below. The water takes the colour of the control that sets it. |
| 4 | Outfall setting | Switch to FIXED and raise the stage: the chute crest drowns first, then the throat. |
| 5 | Overlays | Orange circles SWMM 5.2.4, red diamonds SWMM6 DW, green squares SWMM6 FV. |
| 6 | Tab bar | Engines, ICM rules and Findings. |
| 7 | ? button | Reopens the introduction and the tour. |
4. Eight Pipes Without Lids
The channel in the lab is made of eight rectangular open conduits, which is to say eight pipes with the lids taken off. Together they run 2,650 ft from the upstream node J1 to the outfall OUT1. A steady flow of Q cfs enters at J1 and leaves at OUT1. The bed falls gently, at 0.001, everywhere except on the chute C6. The inverts are built from the bottom up: the outfall sits at 100.00 ft, and each conduit adds its slope times its length. All conduits are 30 ft wide except the throat C4, and 20 ft deep, which is deep enough that they never fill.
| Conduit | From → to | Length ft | Width ft | Bed slope | Role | yc / yn at 600 cfs, ft |
| C1 | J1 → J2 | 400 | 30.0 | 0.0010 | approach | 2.32 / 4.23 |
| C2 | J2 → J3 | 300 | 30.0 | 0.0010 | rises to the sill at J3 | 2.32 / 4.23 |
| C3 | J3 → J4 | 300 | 30.0 | 0.0010 | falls from the sill | 2.32 / 4.23 |
| C4 | J4 → J5 | 150 | 14.0 | 0.0010 | the throat | 3.85 / 8.26 |
| C5 | J5 → J6 | 300 | 30.0 | 0.0010 | mild reach | 2.32 / 4.23 |
| C6 | J6 → J7 | 400 | 30.0 | 0.0200 | the chute (slope break at J6) | 2.32 / 1.62 |
| C7 | J7 → J8 | 400 | 30.0 | 0.0010 | mild reach | 2.32 / 4.23 |
| C8 | J8 → OUT1 | 400 | 30.0 | 0.0010 | tail reach to the outfall | 2.32 / 4.23 |
Defaults: Q 600 cfs, n 0.022, throat 14 ft, no sill, chute slope 0.02, FREE outfall. yc is critical depth and yn normal depth, the depth at which friction exactly balances the slope.
| Node | Station | Invert ft |
| J1 | 0+00 | 110.25 |
| J2 | 4+00 | 109.85 |
| J3 | 7+00 | 109.55 |
| J4 | 10+00 | 109.25 |
| J5 | 11+50 | 109.10 |
| J6 | 14+50 | 108.80 |
| J7 | 18+50 | 100.80 |
| J8 | 22+50 | 100.40 |
| OUT1 | 26+50 | 100.00 |
Four candidates
Along this channel four features compete for control, and each is a familiar thing in disguise.
- The sill at J3 is a low dam. Raise it high enough and the water must climb over it, passing through critical depth at the top.
- The throat C4 is a doorway. Anyone who has left a crowded theater knows that the pace is set by the door, not by the corridor. When the 150-ft narrowing cannot pass the flow with the energy available below it, it chokes: critical depth sits at its exit, J5, and the water backs up all the way to J1.
- The chute crest at J6 is the lip of a waterfall. If the chute is steep enough that its normal depth lies below critical depth, the water goes over the edge at critical depth and runs fast down the chute.
- The outfall OUT1 is the end of the line. It may be a free overfall, with critical depth at the brink; a normal-depth outfall; or a fixed tailwater level that tries to push its way upstream.
The sliders
| Control | Range | Default | What it changes |
| Discharge | 100 – 1,500 cfs, step 10 | 600 cfs | The steady inflow at J1. |
| Friction, Manning n | 0.012 – 0.060 | 0.022 | The roughness of every conduit. |
| Throat width, C4 | 6 – 30 ft, step 0.5 | 14 ft | The width of the doorway; 30 ft removes it. |
| Sill height at J3 | 0 – 4 ft, step 0.1 | none | Raises J3 above the graded bed. |
| Chute slope, C6 | 0.001 – 0.040 | 0.020 | The slope of the chute; 0.001 removes the break. |
| Outfall at OUT1 | FREE, NORMAL, FIXED | FREE | The downstream boundary. |
| Tailwater stage | 0 – 20 ft above the outfall invert, step 0.1 | 6 ft | The water level at OUT1, used only when the outfall is FIXED. |
Six scenarios
For those who would rather not twiddle sliders, six presets set the channel up to show one kind of control each. The last two columns are what the reference solver finds, computed by the document builder from the same code the page runs.
| Preset | Settings | Controls found by the reference | J1 depth ft |
| Competing controls | defaults | Throat exit (3.85 ft); Chute crest at J6 (2.32 ft); Free overfall at OUT1 (2.32 ft); 2 jumps | 5.47 |
| Tailwater 13 ft | FIXED, stage 13 ft | Throat exit (3.85 ft); Outfall water level (13.00 ft); 1 jump | 5.47 |
| Everything drowned | FIXED, stage 16 ft | Outfall water level (16.00 ft) | 6.28 |
| Sill only | throat 30 ft, chute 0.001, sill 2 ft | Sill crest at J3 (2.32 ft); Free overfall at OUT1 (2.32 ft); 1 jump | 5.14 |
| Hard choke | throat 10 ft, chute 0.001 | Throat exit (4.82 ft); Free overfall at OUT1 (2.32 ft); 1 jump | 6.99 |
| Supercritical sweep | n 0.014, Q 800 cfs | Throat exit (4.66 ft); Free overfall at OUT1 (2.81 ft); 1 jump | 6.11 |
5. A Matter of Energy
The reference solver is the yardstick against which the engines are measured, so it deserves a careful description. It is a steady, one-dimensional solution with a mixed regime, which is a mouthful meaning only that it allows both slow and fast flow in the same channel and finds where one turns into the other. The method is the one in Ven Te Chow’s Open-Channel Hydraulics of 1959 and in the mixed-flow option of the HEC-RAS Hydraulic Reference Manual, applied here to rectangles. It runs in a few thousandths of a second, so the lab can afford to rerun it every time a slider moves.
A short history of friction
Water flowing in a channel rubs against the bed and the walls and loses energy doing it, and engineers have been trying to put a number on that loss for a long time. Antoine de Chézy, studying canals for the city of Paris in the 1760s and 1770s, proposed that velocity goes as the square root of the hydraulic radius times the slope. Philippe Gauckler offered a better formula in 1867. Then, in 1889, an Irish engineer named Robert Manning presented essentially the same formula to the Institution of Civil Engineers of Ireland, and it was printed in 1891.
Manning did not care for his own formula. It was dimensionally untidy, and raising a number to the two-thirds power was a nuisance in the days before calculators, so he proposed a more complicated replacement. The profession ignored his objections and kept the simple version, which is how his name came to be attached to nearly every channel designed since. The lab uses it too, in US units:
Sf = ( n V / 1.486 R2/3 )2 V = Q / (b y) R = b y / (b + 2y)
The energy equation
Between two neighboring sections, 1 upstream and 2 downstream, a distance Δx apart, the energy above a fixed datum must balance, less what friction takes along the way:
z1 + y1 + V12/2g = z2 + y2 + V22/2g + Δx · (Sf1 + Sf2) / 2
Here z is the bed elevation, y the depth and V the velocity. The other formulas the solver needs we have already met: critical depth, the Froude number and the specific force. One more finds normal depth, the depth at which friction exactly balances the slope; the solver finds it by repeated halving (bisection):
Q = (1.486 / n) · b y · R2/3 · S01/2
and the head lost in a jump is
ΔEjump = (y2 − y1)3 / (4 y1 y2)
The Lab’s left-hand column carries the same equations in a small panel called “What the reference solves”, for anyone who wants them in view while working.
The bookkeeping
Each conduit of length L is cut into m = max(2, ⌈L / 2 ft⌉) equal steps, so the solver never looks at sections more than 2 ft apart. Every node gets two sections at the same station, one at the end of the conduit above it and one at the start of the conduit below it. When the width changes at a node, the solver crosses the change by conserving energy over a distance of zero, with no friction and no contraction or expansion loss. At default settings the channel has 1,333 sections. At J4 and J5, where the throat begins and ends, the two sections carry different depths; the app shows the downstream one and puts the upstream one in parentheses.
What happens at the outfall
The solver needs a depth to start from at the outfall. Here the reference and the engines part company in one small but telling way:
| Outfall | Reference | SWMM5 and the ICM rule |
| FREE | Critical depth at the brink: a free overfall. | min(yn, yc) of the outfall conduit. On a mild tail reach that is yc, the same as the reference; on a steep one it is yn. |
| NORMAL | max(yn, yc): normal depth, or an overfall if the tail reach is steep. | yn of the outfall conduit. |
| FIXED | The stage, if it is above critical depth; otherwise the overfall holds. | The stage, if it is above min(yn, yc); otherwise min(yn, yc). |
Two surveyors
The easiest way to picture the solver is as two surveyors walking the channel in opposite directions.
The first surveyor starts at the outfall with the boundary depth and walks upstream, section by section, carrying the subcritical answer: at each section she finds the depth above critical that satisfies the energy equation with the section just below. Usually there is such a depth. Sometimes there is not. If even critical depth, the cheapest possible way through, holds more energy than the water below requires, then no subcritical depth will do, and the flow must pass through critical depth right there. She writes down critical depth, marks the spot, and walks on. Her marks are the candidate controls.
The second surveyor starts at J1 and walks downstream, using the first surveyor’s numbers while the flow is slow. At the first mark he records a control and switches to the fast regime, stepping downstream by solving the energy equation for the depth below critical instead. If no such depth exists, the water has too little energy to stay fast; he holds it at critical depth and notes a choke.
At every fast step he compares two numbers: the specific force of his own supercritical depth, and the specific force of the first surveyor’s subcritical depth at the same section. While his is the larger, the fast water pushes on. At the first section where hers is the larger, the fast water can no longer hold back the slow water below, and a jump forms. He records the depths on either side, the Froude number going in and the head lost, and goes back to using her numbers. If he reaches the outfall on her numbers and she marked the outfall critical, the free overfall is recorded as a control too.
A confession about near-critical slopes
Honesty compels me to report that the step method has a weakness. On a slope where normal depth is very close to critical depth, it can flip between the slow and fast answers over a few feet, reporting a control and then, almost at once, a tiny jump. Such pairs are artefacts of the arithmetic, not hydraulics. The solver therefore throws away any control followed within 20 ft by a jump whose depths differ by less than 3 percent, and treats that stretch as slow. The rule, and the default values n 0.022 and chute slope 0.02, were chosen after such spurious pairs turned up during development.
Names and colours
Each control is named for where it sits: the throat exit, the sill crest at J3 (if within 30 ft of it), the chute crest at J6 (if within 40 ft), or the free overfall at OUT1. If the second surveyor ends on slow water and the outfall is not an overfall, the outfall itself is added as a boundary control, named “outfall water level” or “normal-depth outfall”.
Then every section is assigned the control that governs it, following the rule of chapter 2. A slow section is governed by the nearest control downstream of it; a fast section by the nearest control upstream of it. The long-section colours the water by its governing control (throat, sill, crest or outfall), and the Hydraulic control card lists each control with the stretches it sets.
Drowning
A control can lose its job. Raise the water downstream high enough and the slow water reaches back over the control, which then passes the flow without ever reaching critical depth. We say it is drowned. The Candidate controls table under the chart lists each feature that is present and says what became of it: it controls; it is drowned; it is swept, meaning fast water from a control upstream runs past it without a jump, so it never gets the chance; or, for the chute, it is mild at this discharge and never a candidate at all.
For the sill, the throat and the crest, the table also gives the drowning stage, the lowest FIXED tailwater (in feet above the outfall invert) at which the feature stops controlling. The solver finds it by bisection between 0.01 and 30 ft, 30 halvings, re-solving the whole channel each time on 4-ft sections for speed. At default settings the chute crest drowns at 11.97 ft and the throat at 14.65 ft.
An example
At the default 600 cfs, the 14-ft throat cannot pass the flow at the energy available below it. It chokes; critical depth, 3.85 ft, sits at its exit; and the backwater lifts J1 to 5.47 ft, 1.24 ft above the normal depth of the 30-ft channel. Below the throat the water runs fast for a few dozen feet and then jumps back to slow. The chute is steep (normal depth 1.62 ft, below critical depth 2.32 ft), so its crest is a second control. A second jump forms on the flat reach below the chute, and the free overfall at OUT1 controls the tail. Three controls, two jumps, one channel.
| Control | Station | Depth ft | Governs |
| Throat exit | 11+50 | 3.85 | subcritical 0+00 – 11+50; supercritical 11+50 – 11+88 |
| Chute crest at J6 | 14+50 | 2.32 | subcritical 11+90 – 14+50; supercritical 14+50 – 18+24 |
| Free overfall at OUT1 | 26+50 | 2.32 | subcritical 18+26 – 26+50 |
| Jump | y₁ ft | y₂ ft | Fr₁ | Head lost ft |
| 11+90 | 1.51 | 3.38 | 1.90 | 0.32 |
| 18+26 | 1.62 | 3.20 | 1.70 | 0.19 |

Figure 3. The long-section at default settings, with SWMM5 (circles) and SWMM6 FV (squares) overlaid. Plan view on top, profile in the middle, Froude number below, shaded where Fr > 1.
The chart has three bands, and each answers a question. The plan view asks how wide the channel is, and shows the throat and the chute. The profile asks where the water is: it shows the bed, the water coloured by its governing control, a triangle at each control, a tick and label at each jump, and, if wanted, the critical-depth line (dotted) and the energy line (dash-dot). The Froude panel asks how fast, and shades every stretch where the water outruns its ripples. Hover anywhere and the app gives station, bed, depth, water surface, Froude number and governing control.
Below the chart, the What to notice card says in words what the chart shows, and the Key depths card lists the discharge, the critical and normal depths in the channel, the throat and the chute, the water surface at J1, the peak Froude number, the supercritical length and the number of jumps.
The most instructive thing to do with the lab is to raise the tailwater and watch the controls drown in turn. At FIXED 13 ft the chute crest is gone (it drowned at 12.0 ft), and the outfall water level governs everything below the throat. But the throat still chokes, and J1 does not move an inch. Only above 14.6 ft does the throat drown too, and then, at last, the tailwater reaches J1.

Figure 4. The “Tailwater 13 ft” preset: the crest is drowned, the throat still controls.
6. Engines in a Bottle
So much for what the water ought to do. Now for what the computer models say it does.
SWMM has a long history. The EPA released the first version in 1971, and the program has been rewritten several times since; SWMM 5.2.4 is the EPA release this app carries, and OpenSWMM 6 is the successor now being built, still in alpha. Both are written in C or C++ and are meant to run on a desktop computer. To run them inside a web page takes a trick, and the trick is called WebAssembly.
WebAssembly is a way of shipping compiled programs to a web browser, which the major browsers began running in 2017. A program in C or C++ is compiled not for an Intel or ARM processor but for an imaginary one, and the browser translates that, as it loads, into instructions for the real processor underneath. The result runs at something close to native speed, and it runs inside the browser’s sandbox, where it can do no harm.
There is one catch. A program like SWMM expects an operating system: files to open, a clock to read. A browser offers neither. So the page also carries a small stand-in, a WASI layer (browser_wasi_shim 0.4.2), which gives each engine an imaginary disk held in memory. The engine reads its .inp file from that disk and writes its .rpt and .out files to it, exactly as it would on a desktop, and never knows the difference. The engines are, in short, ships in bottles: the real thing, built small and sealed in glass.
How a run goes
- The app writes the .inp text for the current channel and engine options.
- It creates a fresh engine with a fresh in-memory disk holding that file. Nothing carries over from one run to the next.
- It starts the engine and steps it to the end: three hours of steady inflow, a routing step of 2 s, about 5,400 steps in all.
- From 2.5 hours on, after every step, it notes the smallest and largest depth at each node and flow in each link. This last half hour is how the app decides whether the run has settled.
- At the last step it reads every node depth and link flow, closes the engine, and reads the engine’s own report for the flow classification and the continuity error.
A run begins 110 ms after the last change to the inputs, and the engines run one after another. Each result is filed under the exact .inp text that produced it, and it is drawn only while that text still matches the current inputs, so an old answer can never be shown against new settings.
Settling deserves a word. The engines simulate time; they do not solve for the steady state directly. Given a steady inflow, a well-behaved model settles down to a steady answer, but a model may also wander, or rock back and forth forever. The app is strict about telling these apart:
| Verdict | Rule (last half hour of the run) |
| Steady | Every node depth moved less than 0.01 ft, and every link flow less than 0.5% of Q. |
| Nearly steady | Every node depth moved less than 0.1 ft, and every link flow less than 5% of Q. |
| Not steady | Anything more. The node table and chart then show the range, not just the final value. |
The input file
One builder writes the input file for all three engines, and it is the same file the Export menu offers. Junctions carry the inverts of chapter 4, a maximum depth of 25 ft and no ponding. Conduits are RECT_OPEN, 20 ft high, with one Manning n. The inflow is a constant baseline FLOW at J1. The main options are:
| Option | Value | Why |
| FLOW_UNITS | CFS | US customary. |
| FLOW_ROUTING | DYNWAVE, or FV for SWMM6 FV | FV is an OpenSWMM 6 option; SWMM 5.2.4 does not know it. |
| ROUTING_STEP / VARIABLE_STEP | 2 s / 0.75 | Typical dynamic-wave settings; MINIMUM_STEP 0.5 s. |
| INERTIAL_DAMPING | PARTIAL (NONE, PARTIAL, FULL) | The SWMM5 default; set on the Engines tab. |
| NORMAL_FLOW_LIMITED | BOTH (SLOPE, FROUDE, BOTH, NONE) | The SWMM5 default; set on the Engines tab. |
| END_TIME | 03:00:00 | Long enough for this channel to settle where it can. |
| MIN_SURFAREA, HEAD_TOLERANCE, MAX_TRIALS | 12.566, 0.0015, 8 | SWMM5 defaults written out. |
| LINK_OFFSETS, MIN_SLOPE | DEPTH, 0 | No offsets are used; no slope floor. |
| THREADS | 1 | One thread in the browser. |
| FV_CELL_LENGTH | only when the FV slider is above 0 | Cell length in ft for the FV solver; otherwise four cells per conduit. |
Two options change the network itself. Split cuts every conduit into 2, 4 or 8 equal pieces, adding junctions between them, to test the common belief that shorter conduits give better answers. Throat losses write a [LOSSES] section, with Kentry on the first piece of the throat and Kexit on the last. Appendix A gives the whole default file.
EPA SWMM 5.2.4
The SWMM5 engine is the unmodified solver from tag v5.2.4 of the EPA’s repository, compiled with clang to wasm32-wasi as a library with no main program. Only two functions needed stand-ins, because the browser has no temporary folder: mkstemp and realpath. The page drives the engine through its own step-by-step interface (swmm_open, swmm_start, swmm_step, swmm_end, swmm_close) and reads depths and flows with swmm_getValue. It does not call swmm_report, so its report matches the one the command-line program writes.
The engine is 843,203 bytes, with SHA-256 fingerprint 50346aaef8848143…. When the page loads, it fingerprints the bytes it carries and shows the result on the Findings tab, so anyone can see that the engine has not been tampered with.
OpenSWMM 6
OpenSWMM 6 is the C++ engine that the OpenSWMM team at HydroCouple, led by Caleb Buahin, is building as SWMM5’s successor. The page carries the swmm6_rel branch at commit 64d517f of 2026-10-16, which calls itself 6.0.0-alpha.4. It is a pre-release that changes from week to week, so the app pins this one commit and no other.
OpenSWMM 6 throws and catches C++ exceptions as part of its ordinary work, and WebAssembly only recently learned to do that. The engine was compiled with wasi-sdk-34 (clang 23.1.0), wasm32-wasip1, -fwasm-exceptions and the standard exception-handling instructions (-fwasm-exceptions -mllvm -wasm-use-legacy-eh=false, linked with -lunwind), which current Chrome, Firefox and Safari all run. GeoPackage, HDF5, the 2D module and the GPU plugin were switched off, leaving a build with no outside libraries at all.
Five source files needed small changes to build for a browser that has one thread and cannot load plug-ins. None of the changes touches the hydraulics, and each changed file carries a dated note saying so, written on an existing comment line so that no line numbers move:
| File | Change |
| src/engine/CMakeLists.txt | Treat WASI like Emscripten: static archive, no threads, no OpenMP, no libdl. |
| src/engine/output/IOThread.cpp | Extend the engine’s own Emscripten guard to WASI, so output is written on the calling thread. |
| src/engine/plugins/PluginFactory.cpp | Extend the Emscripten guard to WASI, so no plug-in library is opened. |
| src/engine/hydraulics/fv/NetworkSolverFactory.cpp | Under WASI the GPU-plugin loader returns no library. |
| src/engine/output/openswmm_output_impl.cpp | Add #include <new> for std::nothrow, which wasi-sdk’s libc++ does not supply indirectly. |
The page drives OpenSWMM 6 in the same order as the engine’s own command-line program: create, open, initialize, start, step to the end, then end, report, close and destroy. Depths and flows are read with swmm_node_get_depth and swmm_link_get_flow, and the version shown in the app is read from the report the engine writes.
The engine is 3,868,436 bytes, SHA-256 f46bea530af76cdf….
Two ways to solve the same equations
In 1871 Adhémar Barré de Saint-Venant wrote down the two equations, one for mass and one for momentum, that still govern unsteady flow in channels. Every engine in this app, and ICM as well, solves some form of them. They differ in how.
SWMM6 DW, for FLOW_ROUTING DYNWAVE, is OpenSWMM 6’s port of SWMM5’s dynamic wave. Its source code marks the correspondence line by line with comments that read PARITY, and on this channel it gives the same node depths as SWMM 5.2.4 to the last digit printed (finding F8).
SWMM6 FV, for FLOW_ROUTING FV, is a finite-volume solver, and it belongs to a different family. In 1959 Sergei Godunov, in Moscow, showed how to compute flows with shocks by treating each cell as a little tank and asking, at each face between cells, what would happen if the two sides were suddenly allowed to meet. In 1983 Amiram Harten, Peter Lax and Bram van Leer gave a cheap and robust approximation to that question, the HLL flux. OpenSWMM 6’s FV solver uses HLL fluxes with hydrostatic reconstruction on several cells per conduit (four by default, or whatever length FV_CELL_LENGTH sets), with semi-implicit friction and local time stepping. It has no special treatment of fast or slow flow and no normal-flow limit. As we shall see, that makes it the most interesting engine on a channel built to have control sections. The app reads its depths at the nodes only, not inside the conduits.
When the browser says no
Some viewers will not run WebAssembly at all. For them the page keeps 27 runs recorded when it was built: the six presets and three extra cases (the null control, FIXED 9 ft with the normal-flow limit off, and conduits split in four), each with all three engines and the default options. Each recorded run is filed under a fingerprint of its exact .inp text, so it appears only for exactly those inputs, and it is labelled “recorded” so no one mistakes it for a live run.
The Engines tab

Figure 5. The Engines tab at default settings. SWMM5 (circles) hides under SWMM6 DW (diamonds) because their depths are identical; SWMM6 FV (squares) stands apart. The dashed line is the steady profile of SWMM5’s damped equation (chapter 8).
| Part | What it shows or does |
| Engine selector | SWMM5 5.2.4, SWMM6 DW or SWMM6 FV: the engine whose run, report and .inp the tab details. Overlay chips choose which engines are drawn. |
| SWMM6 FV cell length | 0 to 100 ft in steps of 5; 0 leaves the engine’s default of four cells per conduit. |
| Dynamic-wave options | Normal-flow criterion (SLOPE, FROUDE, BOTH, NONE), inertial damping (NONE, PARTIAL, FULL), conduit split (1, 2, 4, 8), throat entry loss Kentry (0 to 6) and exit loss Kexit (0 to 2). They apply to SWMM5 and SWMM6 DW. |
| Fit Kentry | Finds, by bisection with real engine runs, the entry loss on C4 that brings SWMM5’s J1 depth to the reference (finding F7). |
| The run | Engine and version, live or recorded, steps, run time, continuity error, conduits, and the steadiness verdict. |
| Node table | Station, invert, reference depth and every engine’s depth with its difference from the reference, and the range where a run did not settle. |
| Flow classification | From the selected engine’s own report: the share of time each conduit spent slow, fast, critical at either end, or normal-flow limited. |
| The rules in the code | The exact source lines behind each rule (chapter 7). |

Figure 6. The run card for SWMM6 FV at default settings.
At default settings, the node table reads as follows:
| Node | Reference ft | SWMM5 ft | SWMM6 DW ft | SWMM6 FV ft |
| J1 | 5.47 | 4.12 (−1.35) | 4.12 (−1.35) | 5.47 (−0.00) |
| J2 | 5.71 | 4.07 (−1.65) | 4.07 (−1.65) | 5.71 (−0.00) |
| J3 | 5.91 | 4.01 (−1.90) | 4.01 (−1.90) | 5.91 (+0.00) |
| J4 | 5.25 (6.12) | 3.94 (−1.31) | 3.94 (−1.31) | 6.28 (+1.03) |
| J5 | 1.16 (3.85) | 2.75 (+1.59) | 2.75 (+1.59) | 2.20 (+1.04) |
| J6 | 2.32 | 1.62 (−0.69) | 1.62 (−0.69) | 2.59 (+0.27) |
| J7 | 3.82 | 3.73 (−0.09) | 3.73 (−0.09) | 3.09 (−0.73) |
| J8 | 3.55 | 3.34 (−0.21) | 3.34 (−0.21) | 3.55 (−0.00) |
| OUT1 | 2.32 | 2.32 (−0.00) | 2.32 (−0.00) | 2.32 (−0.00) |
Each engine’s difference from the reference follows its value in parentheses. At J4 and J5 the reference has a depth on each side of the width change; the upstream one is in parentheses.
Look down the J1 row. The two dynamic-wave engines agree with each other exactly and fall 1.35 ft short of the reference. The finite-volume engine lands within 0.001 ft. The rest of this document is, in a sense, an explanation of that one row.
7. The Fine Print
Lawyers say the devil is in the fine print. Programmers know he lives in the if statements. The Engines tab quotes, exactly as written, the source lines that decide where an engine can hold a control, from EPA SWMM 5.2.4 and from an unaltered copy of OpenSWMM 6 swmm6_rel at the pinned commit, with the deciding lines in bold. They are reproduced here because they are the cause of most of the findings in chapter 10.
EPA SWMM 5.2.4
node.c, lines 1430–1443: FREE and NORMAL outfalls
1430 switch ( Outfall[i].type )
1431 {
1432 case FREE_OUTFALL:
1433 if ( z > 0.0 ) Node[j].newDepth = 0.0;
1434 else Node[j].newDepth = MIN(yNorm, yCrit);
1435 return;
1436
1437 case NORMAL_OUTFALL:
1438 if ( z > 0.0 ) Node[j].newDepth = 0.0;
1439 else Node[j].newDepth = yNorm;
1440 return;
1441
1442 case FIXED_OUTFALL:
1443 stage = Outfall[i].fixedStage;
link_setOutfallDepth passes the outfall conduit’s normal and critical depth for the current flow. A FREE outfall takes the smaller; z is the conduit’s offset above the outfall invert.
node.c, lines 1463–1491: FIXED, TIDAL and TIMESERIES outfalls
1463 // — now determine depth at node given outfall stage elev.
1464
1465 // — let critical flow depth be min. of critical & normal depth
1466 yCrit = MIN(yCrit, yNorm);
1467
1468 // — if elev. of critical depth is below outfall stage elev. then
1469 // the outfall stage determines node depth
1470 if ( yCrit + z + Node[j].invertElev < stage )
1471 {
1472 yNew = stage – Node[j].invertElev;
1473 }
1474
1475 // — otherwise if the outfall conduit lies above the outfall invert
1476 else if ( z > 0.0 )
1477 {
1478 // — if the outfall stage lies below the bottom of the outfall
1479 // conduit then the result is distance from node invert to stage
1480 if ( stage < Node[j].invertElev + z )
1481 yNew = MAX(0.0, (stage – Node[j].invertElev));
1482
1483 // — otherwise stage lies between bottom of conduit and critical
1484 // depth in conduit so result is elev. of critical depth
1485 else yNew = z + yCrit;
1486 }
1487
1488 // — and for case where there is no conduit offset and outfall stage
1489 // lies below critical depth, then node depth = critical depth
1490 else yNew = yCrit;
1491 Node[j].newDepth = yNew;
The stage is used only when it lies above min(yc, yn) in the outfall conduit; below that the free level holds.
dwflow.c, lines 186–194: Inertial damping factor
186 // — compute Froude No.
187 Link[j].froude = link_getFroude(j, v, yMid);
188 if ( Link[j].flowClass == SUBCRITICAL &&
189 Link[j].froude > 1.0 ) Link[j].flowClass = SUPCRITICAL;
190
191 // — find inertial damping factor (sigma)
192 if ( Link[j].froude <= 0.5 ) sigma = 1.0;
193 else if ( Link[j].froude >= 1.0 ) sigma = 0.0;
194 else sigma = 2.0 * (1.0 – Link[j].froude);
Computed from the conduit’s midpoint Froude number every iteration, then forced to 1 or 0 by INERTIAL_DAMPING NONE or FULL. The inertial terms dq3 and dq4 are multiplied by sigma.
dwflow.c, lines 245–258: When the normal-flow limit is tested
245 // — check if any flow limitation applies
246 Link[j].inletControl = FALSE;
247 Link[j].normalFlow = FALSE;
248 if ( q > 0.0 )
249 {
250 // — check for inlet controlled culvert flow
251 if ( xsect->culvertCode > 0 && !isFull )
252 q = culvert_getInflow(j, q, h1);
253
254 // — check for normal flow limitation based on surface slope & Fr
255 else if (NormalFlowLtd != NEITHER && y1 < Link[j].xsect.yFull &&
256 ( Link[j].flowClass == SUBCRITICAL ||
257 Link[j].flowClass == SUPCRITICAL ))
258 q = checkNormalFlow(j, q, y1, y2, a1, r1);
Only for positive flow, a conduit below full depth, and a flow class of subcritical or supercritical.
dwflow.c, lines 658–685: checkNormalFlow
658 // — check if water surface slope < conduit slope
659 if ( NormalFlowLtd == SLOPE || NormalFlowLtd == BOTH || hasOutfall )
660 {
661 if ( y1 < y2) check = TRUE;
662 }
663
664 // — check if Fr >= 1.0 at upstream end of conduit
665 if ( !check && (NormalFlowLtd == FROUDE || NormalFlowLtd == BOTH) &&
666 !hasOutfall )
667 {
668 if ( y1 > FUDGE && y2 > FUDGE )
669 {
670 f1 = link_getFroude(j, q/a1, y1);
671 if ( f1 >= 1.0 ) check = TRUE;
672 }
673 }
674
675 // — check if normal flow < dynamic flow
676 if ( check )
677 {
678 qNorm = Conduit[k].beta * a1 * pow(r1, 2./3.);
679 if ( qNorm < q )
680 {
681 Link[j].normalFlow = TRUE;
682 return qNorm;
683 }
684 }
685 return q;
If the water surface is flatter than the bed (y1 < y2) or the upstream Froude number reaches 1, the flow is capped at Manning flow for the upstream area and hydraulic radius. Next to an outfall only the slope test applies.
dwflow.c, lines 351–369: Free fall at an outlet offset (DN_CRITICAL)
351 // — case of normal direction flow
352 else
353 {
354 // — downstream end at smaller of critical and normal depth
355 // if downstream flow depth below this and a downstream
356 // conduit offset exists
357 if ( z2 > 0.0 )
358 {
359 *yN = link_getYnorm(j, fabs(q));
360 *yC = link_getYcrit(j, fabs(q));
361 ycMin = MIN(*yN, *yC);
362 ycMax = MAX(*yN, *yC);
363 if ( y2 < ycMin ) flowClass = DN_CRITICAL;
364 else if ( y2 < ycMax )
365 {
366 if ( ycMax – ycMin < FUDGE ) *fasnh = 0.0;
367 else *fasnh = (ycMax – y2) / (ycMax – ycMin);
368 }
369 }
The one internal critical-depth rule: a conduit with an outlet offset gets min(yc, yn) at its downstream end when the water below is lower. Plain conduits without offsets, as in this reach, never trigger it.
project.c, lines 849–850: Defaults
849 InertDamping = PARTIAL_DAMPING; // Partial inertial damping
850 NormalFlowLtd = BOTH; // Default normal flow limitation
PARTIAL damping and BOTH are what a model gets when [OPTIONS] says nothing.
OpenSWMM 6 (swmm6_rel)
Outfall.cpp (SWMM6), lines 283–292: OpenSWMM 6: FREE and NORMAL outfalls
283 switch (ost.bc_type) {
284 case OutfallType::FREE:
285 // Legacy: depth = z + MIN(yNorm, yCrit)
286 depth = z + std::min(yNorm, yCrit);
287 break;
288
289 case OutfallType::NORMAL:
290 // Legacy: depth = z + yNorm
291 depth = z + yNorm;
292 break;
The swmm6_rel port of the same rule, with the legacy line quoted in its comment.
DynamicWave.cpp (SWMM6), lines 2836–2842: OpenSWMM 6: inertial damping factor
2836 // Reclassify SUBCRITICAL → SUPERCRITICAL
2837 FlowClass fc = links.flow_class[uj];
2838 if (fc == FlowClass::SUBCRITICAL && fr > 1.0)
2839 links.flow_class[uj] = FlowClass::SUPERCRITICAL;
2840
2841 // Branchless sigma
2842 sig = std::max(0.0, std::min(1.0, 2.0 * (1.0 – fr)));
The same 2(1 − Fr) ramp, written branch-free.
DynamicWave.cpp (SWMM6), lines 2631–2673: OpenSWMM 6: normal-flow limit
2631 bool slope_check = (nfl == 0 || nfl == 2 || hasOutfall) &&
2632 (depth1_[uj] < depth2_[uj]);
2633 bool froude_check = false;
2634 if (!slope_check && (nfl == 1 || nfl == 2) && !hasOutfall) {
2635 if (depth1_[uj] > FUDGE && depth2_[uj] > FUDGE) {
2636 // PARITY dwflow.c checkNormalFlow → link_getFroude
2637 // (link.c:860-873): the hydraulic depth uses the RAW
2638 // top width xsect_getWofY(y1) — NOT the STEP-B
2639 // CrownCutoff-capped width1_ (which overstates Froude
2640 // for a nearly-full pipe and spuriously fires the
2641 // normal-flow limit) — and a CLOSED conduit within
2642 // FUDGE of full has Froude 0 (no limit check).
2643 const double y1d = depth1_[uj];
2644 const bool closed_nearfull =
2645 !tile_is_open_[uci] && (yf – y1d <= FUDGE);
2646 if (!closed_nearfull) {
2647 double v1 = q / area1_[uj];
2648 XSectParams xs = buildXSP(ctx, uj);
2649 double w1 = xsect::getWofY(xs, y1d);
2650 // Legacy divides A/W with no zero guard; y1 < yf
2651 // and not near-full keeps W finite here.
2652 double dh1 = area1_[uj] / w1;
2653 double f1 = std::fabs(v1) /
2654 std::sqrt(constants::GRAVITY * dh1);
2655 froude_check = (f1 >= 1.0);
2656 }
2657 }
2658 }
2659 if (slope_check || froude_check) {
2660 // PARITY dwflow.c:675: qNorm = beta*a1*pow(r1,2./3.) — (beta*a1)
2661 // grouped first, libm std::pow (NOT cbrt(x*x)), raw upstream
2662 // hyd radius (no FUDGE clamp). §6 fast-mode: cbrt(x*x) (opt-in).
2663 #ifdef SWMM_FAST_MANNING_POW
2664 double qNorm = tile_beta_[uci] * area1_[uj]
2665 * fastmath::pow2_3(hrad1_[uj]);
2666 #else
2667 double qNorm = tile_beta_[uci] * area1_[uj]
2668 * std::pow(hrad1_[uj], 2.0 / 3.0);
2669 #endif
2670 if (qNorm < q) {
2671 q = qNorm;
2672 CD.normal_flow_limited[ucr] = uint8_t{1};
2673 }
A line-by-line port of checkNormalFlow, marked PARITY in the source.
8. The Equation That Forgets
Why do the dynamic-wave engines miss the choke at the throat and the critical depth at the crest? The answer is a single factor in a single equation, and it was put there on purpose.
For steady flow in a channel of constant shape, the momentum equation reduces to the equation of gradually varied flow, which tells how the depth changes with distance:
dy/dx = (S0 − Sf) / (1 − Fr2)
Look at the bottom of the fraction. At Fr = 1 it becomes zero, and the slope of the water surface becomes infinite. This is not a flaw; it is the whole point. That zero is the mathematical form of the rippled pebble of chapter 1. It is what forces the water surface to pass through critical depth at a control, and nowhere else, as it changes from slow to fast.
Now, dynamic-wave models have a practical difficulty near Fr = 1: the inertial terms, which carry the information about speed, make the computation unstable there. So both SWMM5 and ICM multiply those terms by a damping factor, σ, which shrinks from 1 to 0 as the Froude number approaches 1. The equation becomes
dy/dx = (S0 − Sf) / (1 − σ Fr2)
and the zero is gone. In SWMM5, σ is 1 up to Fr 0.5, falls as 2(1 − Fr) between 0.5 and 1, and is 0 above that (dwflow.c, lines 191 to 194); the bottom of the fraction never falls below about 0.70. Under ICM’s documented defaults, the inertial terms are phased out between Froude numbers of 0.8 and 1.0. The documentation does not say what shape the phase-out takes, so the app assumes a straight line, and then the bottom never falls below 0.36.
An equation without that zero cannot make a control on its own. It is rather like a road from which someone has removed the one sign that says “stop here”. The traffic still gets through; it simply never stops where it should. Controls can then come only from explicit rules written into the engine: the outfall, a free fall at an offset drop, and the normal-flow limit. A narrowing with no offset, like our throat, matches none of these.

Figure 7. Left: the damping factor σ against the Froude number (solid: SWMM5; dashed: ICM defaults, straight-line phase-out assumed). Right: the denominator 1 − σFr²; the black curve, with full inertia, is the only one that reaches zero, at Fr = 1.
Testing the explanation
An explanation is only as good as the test it survives. So the app solves the damped equation itself, to see whether it reproduces the engines’ answers. Starting from the engine’s own outfall depth (min(yn, yc) for a FREE outfall), it integrates the equation upstream through each conduit with the fourth-order method that Carl Runge and Martin Kutta devised around 1900, in steps of about 2 ft, carrying one water level across each node as the engines do. With SWMM5’s σ this is the dashed line on the Engines chart; with ICM’s it is the ICM-rule line.
At default settings the damped SWMM5 equation gives 1.623 ft at J6, against the engine’s 1.623 ft and the reference’s 2.316 ft; and 4.28 ft at J1, against the engine’s 4.12 ft and the reference’s 5.47 ft. The explanation passes. The crest falls to normal depth, and most of the throat’s backwater vanishes, because the equation says so, not because anyone made a coding error.
9. Rules Without an Engine
No ICM engine runs in the page. The ICM line is the steady profile that ICM’s documented rules imply for this channel: a free outfall at min(yn, yc), inertia phased out between the Lower and Upper Froude numbers (straight line assumed), one water level per node. It is a prediction, waiting for someone to test it: export the .inp, import it into an InfoWorks network in ICM, and run with defaults.
InfoWorks ICM solves the Saint-Venant equations with a four-point implicit scheme of the kind Alexandre Preissmann published in 1961. Its source code is not public, so the ICM tab works from what ICM’s documentation says: the help page on Simulation Parameters, an Autodesk support article on outfall boundaries, and a published note by Mel Meng.
| Setting | Default | In the app |
| Lower Froude number | 0.8 | Slider, 0.50 to 0.98. Inertia starts being phased out above it. |
| Upper Froude number | 1.0 | Slider, 0.60 to 1.50. Inertia is dropped above it. The help warns that values above 1.0 may cause instabilities. |
| Time weighting θ | 0.65 | Shown for reference; a steady profile does not use it. |
| Space step | 20 × min(width, height), limited to 0.5 – 100 m | Sets the computational nodes per conduit. |
| Minimum nodes per conduit | 5 | As documented. |
| Free outfall | min(yn, yc) of the outfall conduit | The starting depth of the ICM line. |
| Boundary level below the free level | The free outfall level controls | Applied to FIXED stages. |
Unlike SWMM, ICM carries depths at computational nodes inside each conduit. For this channel the space-step rule gives:
| Conduit | Length ft | Space step ft | Computational nodes |
| C1 | 400 | 328 | 5 |
| C2 | 300 | 328 | 5 |
| C3 | 300 | 328 | 5 |
| C4 | 150 | 280 | 5 |
| C5 | 300 | 328 | 5 |
| C6 | 400 | 328 | 5 |
| C7 | 400 | 328 | 5 |
| C8 | 400 | 328 | 5 |
Every conduit ends up with the minimum of five. Twenty times the width gives 600 ft for the 30-ft conduits, cut back to the 100-m (328-ft) limit, and 280 ft for the 14-ft throat. Those steps would put at most three nodes in any conduit, so the five-node minimum wins everywhere. The dots on the ICM line mark these nodes.

Figure 8. The ICM tab at default settings: the ICM-rule line (violet, dots at computational nodes) and SWMM5’s damped equation (dashed) against the reference. At J1, ICM rules give 4.44 ft and the reference 5.47 ft.
More nodes do not rescue the ICM line from the problem of chapter 8. The nodes resolve the throat well enough, but near Fr = 1 the inertia is damped there just as in SWMM5, and the line passes the throat without choking. Whether ICM itself behaves this way is exactly the question an ICM run would answer.
Rule by rule
The tab ends with a side-by-side table of how SWMM5, ICM and the physics handle each situation that matters here:
| Rule | SWMM5 5.2.4 | ICM InfoWorks | Physics (reference) |
| Free outfall depth | min(yn, yc) of the outfall conduit; zero if the conduit sits on an offset (node.c 1432–1435). | The smaller of the normal and critical depth at the end of the pipe (Autodesk support article). | Critical depth at the brink when the approach is subcritical; no influence when it is supercritical. |
| Fixed or level-series outfall below the free level | Stage used only if above min(yc, yn); otherwise the free level holds (node.c 1466–1473). | If the boundary level is lower than the free outfall level, the free level controls (Mel Meng). | Same: a pool below critical depth cannot be felt upstream. |
| Inertia near Fr = 1 | σ = 1 to Fr 0.5, then 2(1 − Fr), 0 from Fr 1; INERTIAL_DAMPING can force 1 or 0 (dwflow.c 191–194). | Phased out between Lower Froude 0.8 and Upper Froude 1.0 (ICM help, Simulation Parameters). | Full inertia; critical depth where 1 − Fr² vanishes. |
| Where depth is carried | One depth per node; one momentum equation per conduit. | Computational nodes inside each conduit: 20 × min(width, height), 0.5–100 m, at least 5. | Continuous. |
| Supercritical conduits | Normal-flow limit: flow capped at Manning flow for the upstream depth when the surface is flatter than the bed (SLOPE) or Fr ≥ 1 upstream (FROUDE) (dwflow.c 255–258, 658–683). | Inertia dropped above the Upper Froude number. No normal-flow limit is named on the parameters page checked. | Supercritical profile marched down from the upstream control. |
| Free fall at a drop | DN_CRITICAL: a conduit with an outlet offset gets min(yc, yn) at its downstream end when the water below is lower (dwflow.c 354–368). | Not found in the documentation checked. | Critical depth at the brink. |
| Choke at a narrower section | No rule. The narrower conduit carries its own momentum equation only. | No rule found in the documentation checked. Computational nodes resolve the throat, but inertia is damped near Fr 1. | Critical depth at the throat exit when the energy downstream is too low to pass the flow. |
10. What the Machines Said
The Findings tab sets down nine measured results on this channel. Each was recorded when the page was built, and the button marked “Re-run all cases in this browser” runs all of them again, about 70 engine runs in all, and replaces the recorded numbers with fresh ones. No figure on that tab can quietly go out of date. Each finding also has an “Open this case” button that loads its scenario and engine options. F1 to F7 concern SWMM 5.2.4; F8 and F9 bring in OpenSWMM 6.
A word on method before we begin. When two programs disagree, it is tempting to blame whichever one we trust less. The findings are arranged to avoid that temptation: the first one checks that the engine and the reference agree when they ought to, so that the disagreements that follow cannot be blamed on units, geometry or friction.
F1. When the tailwater controls everything, SWMM5 and the reference agree
The case: Prismatic reach (no throat, no sill, no chute). FIXED stage 9 ft, and NORMAL outfall.
The largest difference between SWMM5 and the reference over the nine nodes is 0.0003 ft with a FIXED 9-ft tailwater and 0.0000 ft with a NORMAL outfall, and both runs are steady. This is the null control: with no control section in the channel, the engine reproduces the reference backwater almost exactly. Whatever goes wrong later, it is not the friction, the geometry or the units.
F2. SWMM5 does not see the choke at the throat
The case: Defaults: Q 600 cfs, n 0.022, throat 14 ft, chute S 0.02, FREE outfall, NORMAL_FLOW_LIMITED BOTH, PARTIAL damping.
The reference chokes at the throat exit and backs water up to 5.47 ft at J1. SWMM5 gives 4.12 ft, and the shortfall grows to 1.90 ft at J3. The engine’s own report classes C4 as supercritical for 98% of the run. The narrow conduit passes the flow as though it were a steep pipe, and no backwater forms.
| Node | Reference ft | SWMM5 ft | Difference ft |
| J1 | 5.47 | 4.12 | −1.35 |
| J2 | 5.71 | 4.07 | −1.65 |
| J3 | 5.91 | 4.01 | −1.90 |
| J4 | 5.25 | 3.94 | −1.31 |
For the modeler: a narrowing modeled as a narrower conduit does not choke in dynamic wave. If the choke matters, it needs an explicit device such as an orifice or a weir, or a calibrated loss (F7).
F3. At the chute crest SWMM5 carries normal depth, not critical depth
The case: Defaults. C6 is the 0.02 chute; J6 is its crest.
The reference puts critical depth, 2.316 ft, at J6. SWMM5 with BOTH gives 1.623 ft, which is the chute’s normal depth (1.623 ft); C6 is normal-flow limited for 97% of the run. With NONE, J6 is 1.25 ft. Neither setting gives critical depth at the crest, so the drawdown above the chute is too deep.
F4. With the normal-flow limit off and the chute toe drowned, SWMM5 never settles
The case: Defaults with FIXED tailwater 9 ft (a jump sits inside the chute). Last half hour of a 3 h run.
With NORMAL_FLOW_LIMITED set to NONE, the flow in C6 swings between -160 and 1,415 cfs in the last half hour of a 600-cfs run, a swing of 1,575 cfs. Note that the minimum is negative: for part of each swing the model has the water in the chute running backward, uphill. Smaller routing steps did not cure it (0.5 s was tried during the build). With BOTH, C6 holds within 6.6 cfs, though the channel as a whole still moves: C4 swings 143 cfs. The limiter is what keeps a conduit with a jump inside it from rocking; it does not make this channel steady.
F5. Shorter conduits do not converge here; the run stops settling
The case: Defaults, each conduit split into 1, 2, 4 or 8 equal pieces.
Splitting long conduits into shorter ones is the usual cure for a coarse model, and it is natural to expect the answer to converge as the pieces get shorter. Here it does not. Splitting moves J1 toward the reference (5.47 ft), but only the unsplit run settles. Over the last half hour J1 wanders 0.85 ft with 4 pieces and 1.31 ft with 8.
| Pieces | Verdict | J1 ft (range) | Largest node range ft | Steps |
| 1 | steady | 4.12 | 0.000 | 5,401 |
| 2 | not steady | 4.75 | 0.382 | 5,402 |
| 4 | not steady | 4.58 – 5.43 | 1.677 | 8,727 |
| 8 | not steady | 4.69 – 6.00 | 1.828 | 17,089 |
F6. The damped momentum equation explains both misses
The case: Defaults. Steady profile of the momentum equation with SWMM5’s damping factor, one water level per node.
This is the test of chapter 8. The damped equation, solved on its own, gives 1.623 ft at J6 (engine 1.623, reference 2.316) and 4.28 ft at J1 (engine 4.12, reference 5.47). Damping removes the critical point from the equation; the crest falls to normal depth, and most of the throat’s backwater disappears. The misses belong to the equation the engine solves, not to a mistake in the code.
F7. An entry loss on the throat can stand in for the choke at J1
The case: Defaults at 300, 600 and 900 cfs. Kentry on C4 found by bisection so SWMM5 matches the reference depth at J1.
If the engine will not choke, can a loss coefficient stand in for the choke? It can, at one discharge. An entry loss of K = 2.67 on C4 brings J1 to the reference at 600 cfs. But the coefficient that matches runs from 2.25 at 300 cfs to 2.89 at 900 cfs, so a single calibrated K carries an error to every other flow. A choke is not a loss, and only pretends to be one at the flow it was calibrated for.
| Q cfs | Reference J1 ft | SWMM5, K = 0 | Matching K | J1 with K = 2.67 |
| 300 | 3.32 | 2.71 | 2.25 | 3.45 (+0.13) |
| 600 | 5.47 | 4.12 | 2.67 | 5.47 (+0.00) |
| 900 | 7.28 | 6.15 | 2.89 | 7.09 (−0.19) |
F8. OpenSWMM 6 dynamic wave against SWMM 5.2.4
The case: The same .inp files run in OpenSWMM 6 (swmm6_rel, FLOW_ROUTING DYNWAVE) and in EPA SWMM 5.2.4.
| Case | Max |SWMM6 − SWMM5| ft | J1 SWMM6 / SWMM5 ft | J6 SWMM6 / SWMM5 ft | Settled? SWMM6 / SWMM5 |
| Null control (prismatic, FIXED 9 ft) | 0.0000 | 6.73 / 6.73 | 7.93 / 7.93 | steady / steady |
| Defaults (throat, chute, FREE) | 0.0000 | 4.12 / 4.12 | 1.62 / 1.62 | steady / steady |
| FIXED 9 ft, normal-flow limit NONE | 1.4249 | 4.22 / 4.12 | 1.62 / 0.41 | not steady / not steady |
| Defaults, conduits split in 4 | 0.0000 | 4.65 / 4.65 | 1.68 / 1.68 | not steady / not steady |
On 3 of 4 cases the new engine’s dynamic wave gives node depths identical to SWMM 5.2.4 at every node, including the split-conduit run, which rocks in exactly the same way in both. The exception is “FIXED 9 ft, normal-flow limit NONE”, and it is only an apparent exception: neither engine settles there, so the end-of-run snapshots catch two different moments of the same oscillation, which differ by up to 1.42 ft. SWMM6’s dynamic wave is a faithful port of SWMM5’s, and a faithful port inherits the original’s blind spots along with its strengths: the misses of F2 to F6 belong to it too.
F9. OpenSWMM 6 finite volume on the choke and the crest
The case: FLOW_ROUTING FV, the swmm6_rel Godunov/HLL solver with several cells per conduit, on the same reach.
| Case | J1 FV / reference ft | J6 FV / reference ft | Max |FV − reference| ft | Settled? | Continuity % |
| Null control (prismatic, FIXED 9 ft) | 6.72 / 6.73 | 7.92 / 7.93 | 0.012 | steady | -0.003 |
| Defaults, default mesh (4 cells per conduit) | 5.47 / 5.47 | 2.59 / 2.32 | 1.038 | steady | -0.002 |
| Defaults, FV_CELL_LENGTH 25 ft | 5.64 / 5.47 | 2.55 / 2.32 | 1.237 | steady | -0.002 |
| FIXED 9 ft (jump in the chute) | 5.47 / 5.47 | 2.59 / 2.32 | 1.038 | steady | -0.002 |
Here, at last, is an engine that sees the throat. With its default mesh the finite-volume solver backs water up to 5.468 ft at J1, within 0.001 ft of the reference 5.468 ft, where both dynamic-wave engines stop at 4.12 ft. At the chute crest it carries 2.59 ft. That is above critical depth (2.32 ft), because a node value stands for the cells around the brink, but well clear of the chute’s normal depth (1.62 ft) where dynamic wave puts it. With 25-ft cells the crest comes down to 2.55 ft.
The same refinement lifts J1 to 5.64 ft, 0.17 ft above the reference, which is a reminder that a reference is a model too. The reference crosses the abrupt width change at J4 by conserving energy with no loss; the FV solver crosses it by its own fluxes at the cell faces; and the finer mesh resolves the difference between the two. With a FIXED 9-ft tailwater the FV run settles, where SWMM5 does not (F4), and J1 stays at 5.468 ft, as it must while the throat controls. The null control agrees with the reference within 0.012 ft, and FV routing continuity is -0.002%, against -0.225% for dynamic wave.
For the modeler: on this channel the OpenSWMM 6 finite-volume solver is the only engine in the app that finds the choke and holds the crest near critical depth without a calibrated loss. That is one channel and one pre-release commit. It is a reason to test FV on real models with narrowings and slope breaks, and not yet a general law.
11. Odds and Ends
Taking things home
The Export menu offers four files. Each opens in a dialog showing its contents, with Copy and Download buttons.

Figure 9. The Export menu.
| Item | File | Contents |
| SWMM .inp | control-section-lab.inp, or control-section-lab-swmm6-fv.inp when SWMM6 FV is selected | The channel with the options of the selected engine. The DW file runs in EPA SWMM 5.2.4 and OpenSWMM 6 and imports into an ICM network; the FV file needs OpenSWMM 6. |
| Reference profile | reference-profile.csv | Every section of the reference: station, x, conduit, width, bed, depth, water surface, energy line, critical depth, Froude number, regime and governing control. |
| Engines vs reference at nodes | engines-vs-reference.csv | Each node’s reference depth (both sides) and, for every engine with a current run, its depth, difference and last-half-hour minimum and maximum. |
| ICM-rule profile | icm-rule-profile.csv | The steady profile implied by ICM’s documented rules: x, bed, depth, water surface, Froude number and σ. |
Inside the claude.ai viewer a file can be saved only through the viewer’s own download prompt, which accepts a fixed list of file types; an .inp file is therefore offered as .inp.txt, and the dialog says so. Copy always works. On an ordinary web host, Download is an ordinary download with the real file name.
Moving pictures
Make a GIF records a captioned film of the Lab chart on the default channel: one frame with the free outfall, then the outfall set to FIXED and the tailwater raised from 2 ft to 17 ft in half-foot steps, so that the controls drown one after another. Every frame is solved by the reference solver as it is drawn, and its caption names the controls in that frame. The page paints each frame onto a canvas, chooses one palette of 256 colours from three sample frames, and writes a looping GIF89a file with its own LZW compressor; no library or server is involved. The result is 32 frames, about half a megabyte, in the current colour theme.

Figure 10. The GIF dialog after recording, in the light theme.
Day and night
The app opens in a dark theme. The Dark / Light / Auto switch changes every colour, the charts included, and Auto follows the computer’s own setting. The choice is remembered in that browser. Colours are defined once as named tokens, with a separate set for the light theme, and the chart colours (control colours, engine markers, the ICM line) come in a dark and a light version, each chosen to be readable on its own background. The figures in this document use the light theme, which prints better.
Small screens
Below 900 pixels wide the two columns stack, chart first and sliders after, and the header shrinks. The chart redraws to fit, and wide tables scroll inside their own cards rather than widening the page.

Figure 11. The Lab on a 390-pixel phone screen.
12. How Do We Know It Works?
It is one thing to build a program and another to know it does what it claims. The app was checked in four ways, three of them by test suites that run against the shipped HTML file itself. They pull the physics code and the engine code out of the page and run those exact bytes, so what was tested is what ships.
| Suite | Checks | What it establishes |
| Physics (test_core.js) | 29 passed | An M2 drawdown agrees with an independent Runge–Kutta integration 1,000 ft upstream; the throat and apron jumps satisfy Bélanger’s sequent-depth relation within the 2-ft grid; a tailwater below critical depth gives the same profile as a free overfall; an M1 backwater agrees with the continuous solution within 0.005 ft; with full inertia the damped integrator reproduces the standard step; neither damping band lets 1 − σFr² reach zero; σ(0.75) = 0.5 as in dwflow.c; drowning stages are found and bracketed. |
| SWMM5 engine (test_engine.js) | 33 passed | The browser SWMM 5.2.4 and a native gcc build of the same source run the same models; the binary .out files are identical byte for byte and the .rpt files identical apart from wall-clock lines. Node order, mass balance at steady state (outflow within 0.5% of inflow), report parsing and the known behaviour of F4 are checked. |
| SWMM6 engine (test_engine6.js) | 24 passed | The browser OpenSWMM 6 and a native g++ build of the same swmm6_rel source run six models, three dynamic wave and three finite volume. All six .out files are identical byte for byte. Four .rpt files are identical apart from wall-clock and path lines; on two FV models the .rpt differs only in the clock time at which an already steady node first reached its maximum, a last-digit difference between the two math libraries that never reaches the .out. |
| Browser pass (headless Chromium) | no errors | The built page loads, both engines show their fingerprints and run live, all three engines appear in the node table, the FV cell-length slider reruns the engine, the exports produce the expected files, and the Findings rerun completes with the recorded wording. |
A byte-for-byte match deserves a moment’s admiration. It means that a program compiled for an imaginary processor and run inside a web browser produced exactly the same binary output, down to the last bit of the last number, as the same program compiled for a real processor and run on a desktop. There is no room for a hidden difference in that.
The findings are a check of their own: anyone can press the rerun button and compare the fresh numbers with the recorded ones.
What has not been checked: no person has yet compared the ICM-rule line with an ICM run; the ICM descriptions rest on ICM’s documentation, not its code; and the OpenSWMM team has not reviewed the build changes.
13. What the Lab Cannot Tell You
Every model is a simplification, and a model that does not say where it stops is a model that will be used where it should not be. This one does not show:
- unsteady flow; every case is a steady inflow run to the end of three hours;
- three-dimensional, curved-streamline or air-entrained flow at the throat, the sill and the jumps;
- contraction and expansion losses in the reference, which crosses width changes with no loss;
- the real length of a hydraulic jump; jumps are placed at a single section;
- ICM’s own Preissmann discretization, base flow and conveyance handling;
- SWMM models that use offsets, weirs or orifices to represent controls instead of plain conduits;
- depths inside conduits from the FV solver; the app reads node depths only;
- any OpenSWMM 6 build other than the one pinned here, a pre-release that changes from week to week.
14. The Parts List
The page is assembled by a build script from parts that are tested separately. The physics, the engine drivers and the findings are plain JavaScript files that run unchanged in Node, for the tests and the build, and in the browser.
| File | Role |
| csl_core.js | Physics: channel, reference solver, candidate report, drowning stage, damping bands, damped integrator, ICM computational nodes, .inp builder, report parsers. No screen code. |
| csl_engine.js | Drives SWMM 5.2.4 through its step interface, in a fresh WASI instance per run. |
| csl_engine6.js | Drives OpenSWMM 6 through its handle interface, in a fresh WASI instance per run. |
| csl_findings.js | Runs findings F1 to F9, for the build to record and the page to rerun. |
| app.js, app.src.html | The interface: tabs, charts (hand-drawn SVG), tables, tour, exports, GIF encoder, theme. |
| wasi_shim.iife.js | browser_wasi_shim 0.4.2, bundled. |
| swmm524.wasm, openswmm6.wasm | The two engines. |
| build.js | Runs both engines on 9 cases × 3 engine modes for the recorded runs, runs the findings, copies the source excerpts, and writes control-section-lab.html. |
| test_core.js, test_engine.js, test_engine6.js | The three test suites of chapter 12. |
| Engine | Bytes | SHA-256 |
| EPA SWMM 5.2.4 | 843,203 | 50346aaef8848143f42e082fdde36d741116ce5eff68f2680e6fcd1ac1fef17b |
| OpenSWMM 6.0.0-alpha.4 | 3,868,436 | f46bea530af76cdfe57d5de32118e305665b78f266c88e4ed7ac0960e5e41a07 |
15. Further Reading
Hydraulics
Chow, V. T. (1959). Open-Channel Hydraulics. McGraw-Hill.
Sturm, T. W. (2001). Open Channel Hydraulics. McGraw-Hill.
U.S. Army Corps of Engineers. HEC-RAS Hydraulic Reference Manual: mixed flow regime calculations.
History
Bélanger, J.-B. (1828). Essai sur la solution numérique de quelques problèmes relatifs au mouvement permanent des eaux courantes. Carilian-Goeury, Paris.
Chanson, H. (2009). Development of the Bélanger equation and backwater equation by Jean-Baptiste Bélanger (1828). Journal of Hydraulic Engineering 135(3), 159–163.
Manning, R. (1891). On the flow of water in open channels and pipes. Transactions of the Institution of Civil Engineers of Ireland 20, 161–207.
Ponce, V. M. History of the Manning formula. San Diego State University.
Barré de Saint-Venant, A. J. C. (1871). Théorie du mouvement non permanent des eaux. Comptes Rendus de l’Académie des Sciences 73, 147–154 and 237–240.
Godunov, S. K. (1959). A difference method for numerical calculation of discontinuous solutions of the equations of hydrodynamics. Matematicheskii Sbornik 47(89), 271–306.
Harten, A., Lax, P. D., and van Leer, B. (1983). On upstream differencing and Godunov-type schemes for hyperbolic conservation laws. SIAM Review 25(1), 35–61.
Preissmann, A. (1961). Propagation des intumescences dans les canaux et rivières. First Congress of the French Association for Computation, Grenoble, 433–442.
Wikipedia. Hydraulic jump (Leonardo’s sketches; Bidone’s 1820 experiments).
Software and documentation
Vasslab. Control Section Lab.
U.S. EPA. Storm Water Management Model 5.2.4 source (node.c, link.c, dwflow.c, project.c).
HydroCouple. OpenSWMM engine, swmm6_rel branch, commit 64d517f1f238361e42488ac239078761ba74f81a.
Autodesk. How to set outfall boundary condition in InfoWorks ICM.
Innovyze / Autodesk. InfoWorks ICM help: Simulation Parameters.
Meng, M. How to set outfall boundary in InfoWorks ICM.
WebAssembly. wasi-sdk.
bjorn3. browser_wasi_shim, version 0.4.2.
16. Credits and Licenses
EPA SWMM 5.2.4 was written by the U.S. Environmental Protection Agency and is in the public domain.
OpenSWMM 6 is the work of the OpenSWMM team at HydroCouple: Caleb Buahin (lead developer and maintainer) and Corinne Wiesner-Friedman (developer, documentation and technical review). The engine is Copyright 2026 HydroCouple Developers and licensed under the Apache License, Version 2.0, on the swmm6_rel branch (the main branch carries a different license). The app redistributes it in compiled form, built from commit 64d517f with the five changed files listed in chapter 6, so it carries the license conditions: the About tab reproduces the upstream NOTICE word for word and the full Apache-2.0 text, the page source opens with an attribution comment, and the changed files are marked. The USEPA and HydroCouple do not endorse the app.
Other bundled code: browser_wasi_shim 0.4.2 by bjorn3 and contributors (MIT or Apache-2.0). Both engines include runtime code from wasi-sdk: wasi-libc, whose parts carry permissive licenses (CC0 and Apache-2.0 for its own code, BSD-2-Clause for code derived from cloudlibc, MIT and others for code derived from musl), and, in OpenSWMM 6, LLVM’s libc++, libc++abi and libunwind (Apache-2.0 with LLVM exception).
The OpenSWMM NOTICE at the pinned commit, verbatim:
OpenSWMM Engine
Copyright 2026 HydroCouple Developers
This product includes software developed by the OpenSWMM authors
(https://github.com/HydroCouple/openswmm.engine).
Licensed under the Apache License, Version 2.0. See the LICENSE file for the
full license text.
——————————————————————————–
PUBLIC DOMAIN MATERIAL
——————————————————————————–
Portions of this product are derived from the Storm Water Management Model
(SWMM) developed and released by the United States Environmental Protection
Agency (USEPA) over the course of several decades. That material was prepared
by or for the United States Government and resides in the public domain:
domestic copyright protection is not available for it under 17 USC § 105, and
no copyright is claimed over it by the OpenSWMM authors.
The Apache License, Version 2.0 applies to the original contributions of the
OpenSWMM authors. It does not extend copyright to, and places no restriction
on, the public domain material described above. Recipients retain all rights
they already hold in that material independently of this License.
Files derived in whole or in part from USEPA SWMM sources are, where
practical, marked as such in their file headers. The EPA SWMM 5.x sources are
available from https://www.epa.gov/water-research/storm-water-management-model-swmm.
The USEPA does not endorse this product, and the names “EPA”, “USEPA”, and
“SWMM” are used solely to describe the origin of the public domain material.
——————————————————————————–
THIRD-PARTY COMPONENTS
——————————————————————————–
This product depends on and, in some distribution formats, bundles third-party
libraries that are licensed separately by their respective copyright holders.
Dependencies are declared in vcpkg.json. Each such component remains subject to
its own license terms, which accompany the component in its own distribution.
17. Use of AI
Isaac Asimov spent half a century writing about robots, so a document written in his manner ought to be especially clear about what its robot did.
The app and both versions of this document were made with Claude, Anthropic’s AI model, in a Claude session in October 2026, under the direction of Robert (Bob) Dickinson. He chose the subject and the Vasslab lab to follow, asked for InfoWorks ICM and the OpenSWMM 6 release engine to be added, asked for the first document, and asked for this retelling in the manner of Isaac Asimov. Responsibility for the work is his.
Claude did most of the work. It read the SWMM 5.2.4 and OpenSWMM 6 sources and the ICM documentation; wrote the reference solver, the damped-equation integrator and the .inp builder; compiled SWMM 5.2.4 and OpenSWMM 6 to WebAssembly and made the five-file build changes for OpenSWMM 6; wrote the code that drives both engines; ran the engine cases behind the findings; wrote the interface, its charts and its text, including the credits and license section; wrote the test suites; and wrote both documents. For this one it rewrote the prose in imitation of Asimov’s essay style and added the historical asides, drawing on the secondary sources under History in chapter 15. No text of Asimov’s is quoted. The numbers are read by a script from the build data embedded in the shipped page, and the figures are screenshots of that page.
The checks are mechanical: the physics tests, the byte comparison of each WebAssembly engine against a native build of the same source, a headless browser pass, and the in-browser rerun of every finding. No person has yet compared the ICM line with an ICM run, the OpenSWMM team has not reviewed the build changes, and no person has checked the historical asides against the primary sources.
Appendix A. The Default .inp
For the reader who wants to see the thing itself: the file the app writes for the default channel with the default engine options (Export → SWMM .inp, with SWMM5 selected).
[TITLE]
;;Control Section Lab – Q 600 cfs, n 0.022, throat 14 ft, sill 0 ft, chute S 0.02, outfall FREE
Control Section Lab reach
[OPTIONS]
FLOW_UNITS CFS
INFILTRATION HORTON
FLOW_ROUTING DYNWAVE
LINK_OFFSETS DEPTH
MIN_SLOPE 0
ALLOW_PONDING NO
SKIP_STEADY_STATE NO
START_DATE 01/01/2026
START_TIME 00:00:00
REPORT_START_DATE 01/01/2026
REPORT_START_TIME 00:00:00
END_DATE 01/01/2026
END_TIME 03:00:00
SWEEP_START 01/01
SWEEP_END 12/31
DRY_DAYS 0
REPORT_STEP 00:05:00
WET_STEP 00:05:00
DRY_STEP 00:05:00
ROUTING_STEP 2
RULE_STEP 00:00:00
INERTIAL_DAMPING PARTIAL
NORMAL_FLOW_LIMITED BOTH
FORCE_MAIN_EQUATION H-W
VARIABLE_STEP 0.75
LENGTHENING_STEP 0
MIN_SURFAREA 12.566
MAX_TRIALS 8
HEAD_TOLERANCE 0.0015
SYS_FLOW_TOL 5
LAT_FLOW_TOL 5
MINIMUM_STEP 0.5
THREADS 1
[JUNCTIONS]
;;Name Elevation MaxDepth InitDepth SurDepth Aponded
J1 110.25 25 0 0 0
J2 109.85 25 0 0 0
J3 109.55 25 0 0 0
J4 109.25 25 0 0 0
J5 109.1 25 0 0 0
J6 108.8 25 0 0 0
J7 100.8 25 0 0 0
J8 100.4 25 0 0 0
[OUTFALLS]
;;Name Elevation Type Stage Data Gated Route To
OUT1 100 FREE NO
[CONDUITS]
;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow
C1 J1 J2 400 0.022 0 0 0 0
C2 J2 J3 300 0.022 0 0 0 0
C3 J3 J4 300 0.022 0 0 0 0
C4 J4 J5 150 0.022 0 0 0 0
C5 J5 J6 300 0.022 0 0 0 0
C6 J6 J7 400 0.022 0 0 0 0
C7 J7 J8 400 0.022 0 0 0 0
C8 J8 OUT1 400 0.022 0 0 0 0
[XSECTIONS]
;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels
C1 RECT_OPEN 20 30 0 0 1
C2 RECT_OPEN 20 30 0 0 1
C3 RECT_OPEN 20 30 0 0 1
C4 RECT_OPEN 20 14 0 0 1
C5 RECT_OPEN 20 30 0 0 1
C6 RECT_OPEN 20 30 0 0 1
C7 RECT_OPEN 20 30 0 0 1
C8 RECT_OPEN 20 30 0 0 1
[INFLOWS]
;;Node Constituent Time Series Type Mfactor Sfactor Baseline Pattern
J1 FLOW “” FLOW 1.0 1.0 600
[REPORT]
INPUT NO
CONTROLS NO
SUBCATCHMENTS NONE
NODES ALL
LINKS ALL
[COORDINATES]
;;Node X-Coord Y-Coord
J1 0 0
J2 400 0
J3 700 0
J4 1000 0
J5 1150 0
J6 1450 0
J7 1850 0
J8 2250 0
OUT1 2650 0