OPM · Hydrology Course
ChaptersChapter 4

title: "Chapter 4 — Kinematic-Wave Routing" chapter: 4

Chapter 4 — Kinematic-Wave Routing

A flood wave arrives at the downstream gauge minutes before the water molecules that triggered it. Grasping why — and how to compute it — is the foundation of every 1-D flood routing model, including the OPM engine this course is built around.


4.1 — The Saint-Venant Equations

The complete physics of 1-D open-channel flow is captured by the Saint-Venant equations — one continuity law and one momentum law:

At+Qx=qlat(continuity)\frac{\partial A}{\partial t} + \frac{\partial Q}{\partial x} = q_\text{lat} \qquad \text{(continuity)} Qt(1)+(Q2/A)x(2)+gAhx(3)+gA(SfS0)=0(momentum)\underbrace{\frac{\partial Q}{\partial t}}_{\small(1)} + \underbrace{\frac{\partial (Q^2/A)}{\partial x}}_{\small(2)} + \underbrace{gA\frac{\partial h}{\partial x}}_{\small(3)} + gA(S_f - S_0) = 0 \qquad \text{(momentum)}

The kinematic wave drops terms ①②③ — local acceleration, convective acceleration, and pressure gradient — leaving only friction balanced by gravity: Sf=S0S_f = S_0. Combined with Manning's equation this yields the power law Q=αAβQ = \alpha A^\beta, which is the only equation OPM's kinematic router needs per cell.

Click each term to drop it and watch the simplification cascade. Press "Drop Inertia (①②③)" to jump straight to kinematic.

The Saint-Venant Equations

Click any term to understand it — then decide what to keep or drop

① Continuity Equation — Water Conservation

Q(x)Q(x+Δx)q_lat (rain / lateral)A(x,t)∂A/∂t← Δx →
Mass balance for the reach of length Δx: d/dt(A · Δx) = Q(x) − Q(x+Δx) + q_lat · Δx Divide both sides by Δx, take limit Δx → 0: ∂A/∂t = −∂Q/∂x + q_lat ∴ ∂A/∂t + ∂Q/∂x = q_lat ✓

Click any term to understand it (continuity always holds — nothing to drop here):


② Momentum Equation — Newton's 2nd Law for Water

Apply Newton's 2nd law (F = ma) to a water element of length Δx. Forces acting: hydrostatic pressure difference between faces, gravity down the slope, friction opposing flow. After expanding, dividing by ρ·A·Δx, and rearranging:

① ∂Q/∂t  +  ② ∂(Q²/A)/∂x  +  ③ gA·∂h/∂x  +  gA·(④ S_f  −  ⑤ S₀)  =  0
Local acceleration
~0.007×
+
Convective accel.
~F²×
+
Pressure gradient
~0.15×
+
Friction slope
~1.00×
🔒
Gravity (bed slope)
~1.00×
🔒
= 0
Full Dynamic Wave (Saint-Venant)

← Click any term to understand its physical meaning.

Kinematic Wave Derivation (from S_f = S₀)

Drop terms ①②③ to see the kinematic wave emerge.

After dropping ①②③ — remaining balance:

gA·S_f = gA·S₀   →   S_f = S₀

The flow is ALWAYS in local equilibrium: the channel instantaneously adjusts to uniform flow. This is the quasi-steady approximation.

Step 1

S_f = S₀

The flow is ALWAYS in local equilibrium: the channel instantaneously adjusts to uniform flow. This is the quasi-steady approximation.

Step 2

Q = (1/n)·A·R^(2/3)·S₀^(1/2)

Manning's equation applies at every cell, at every instant.

Step 3

R ≈ h = A/B

Wide rectangular channel: width >> depth, so hydraulic radius ≈ depth.

Step 4 — Result

Q = α · A^β

β = 5/3 > 1 always → wave celerity c = dQ/dA = (5/3)·u > u → wave outruns water.

α (n=0.04, S₀=0.001, B=10m)

0.1703

β (universal)

1.667

Simplification Hierarchy
DropKeepNameNotes
①②③④⑤Dynamic waveFull SVE; tides, dam-break
①②③④⑤Diffusive waveBackwater; flat rivers
①②③④⑤Kinematic waveS_f = S₀; used in OPM

The table on the right shows that dropping ① and ② gives the diffusive wave (keeps backwater); dropping all three gives the kinematic wave (bed-slope-only friction). Every term you drop is an assumption — revisit them in §4.7.


4.2 — Manning's Rating Curve & Wave Celerity

For a wide rectangular channel (BhB \gg h, so hydraulic radius RhR \approx h), Manning's equation integrates into the power law:

Q=S01/2nB2/3αAβ,β=53Q = \underbrace{\frac{S_0^{1/2}}{n B^{2/3}}}_{\alpha} \cdot A^{\,\beta}, \qquad \beta = \tfrac{5}{3}

The wave celerity cc — the speed at which a small disturbance propagates downstream — is the slope of the rating curve:

c=dQdA=βQA=53uc = \frac{dQ}{dA} = \beta \cdot \frac{Q}{A} = \tfrac{5}{3}\, u

Wave speed is always 5/3 times the flow velocity. The water moves at uu; the wave front races ahead at c=1.67uc = 1.67\, u.

Adjust the sliders below to explore any channel geometry. The animation at the bottom shows the water particles (blue) lagging behind the wave crest (amber) by exactly that 5/3 ratio. The full step-by-step derivation — plus the physical and geometric intuition for why the wave always outruns the water — is built into the widget below the sliders.

Manning's Rating Curve & Wave Celerity

c = dQ/dA = (5/3)u — the wave always outruns the water

0.040
0.0010
10 m
300 s
500 m
1.15 m
α = 0.1703 m^(1/3)/s
β = 1.667 (fixed)
hAQucCrStatus
0.505.02.490.4980.8300.498✓ Stable
0.808.05.450.6811.1350.681✓ Stable
1.0010.07.910.7911.3180.791✓ Stable
1.1511.59.980.8681.4460.868✓ Stable
1.5015.015.541.0361.7271.036✗ Unstable
2.0020.025.101.2552.0921.255✗ Unstable
2.5025.036.411.4562.4271.456✗ Unstable

h (m) | A (m²) | Q (m³/s) | u (m/s) | c=5u/3 | Cr=cΔt/Δx

Rating Curve Q(h) — tangent slope = c·B = dQ/dh

secant: Q/A = utangent: dQ/dA = c0.51.01.52.02.53.012.324.737.049.3Q (m³/s)h (m)
h = 1.15 mQ = 9.979 m³/su = 0.868 m/sc = 1.446 m/sCr = 0.868

Because the rating curve bends upward (it's convex — Q'' > 0 since β > 1), the tangent at any point is always steeper than the secant from the origin. That's a purely geometric proof that c > u for any convex rating curve, not just this one — the result generalizes to any power law Q = α·A^β with β > 1, not only wide rectangular channels.

Wave crest (amber) vs water particles (blue) — c = (5/3) u

wave
Wave: 0.0 mParticles: 0.0 mRatio: 1.67×
The flood WAVE arrives 5/3 × sooner than the water molecules that created it.
Key relations
Q = α · A^β,   α = √S₀ / (n · B^(2/3))
β = 5/3   (wide rectangular channel)
u = Q / A   (flow velocity)
c = dQ/dA = β · u = (5/3) u
Cr = c · Δt / Δx   (Courant number)
Stable explicit scheme: Cr ≤ 1

Why Is the Wave Faster Than the Water? — Deriving c = (5/3)u

Start: Manning's equation, valid for ANY channel shape:
Q = (1/n) · A · R^(2/3) · S₀^(1/2)
Step 1 — Specialize to a wide rectangular channel (B ≫ h):
A = B · h,   wetted perimeter P ≈ B (side walls negligible)  ⇒  R = A / P ≈ h

When the channel is much wider than it is deep, the two short side walls barely add to the wetted perimeter — so the hydraulic radius collapses to just the depth.

A rise in depth does two things at once: (1) there's simply more cross-sectional area to carry water — that's the factor of A; and (2) the water that's already there speeds up, because R^(2/3) grows too — a deeper channel has proportionally less wetted perimeter dragging on the flow, so friction's grip loosens and velocity rises. The two effects compound multiplicatively: A¹ from the extra area, A^(2/3) from the extra speed, giving Q ∝ A^(5/3). Because Q grows faster than A, the marginal rate dQ/dA exceeds the average rate Q/A — and a disturbance (the wave) travels at the marginal rate.

It's the same reason a wider highway lane doesn't just fit more cars — it also lets them drive faster. The marginal car entering downstream moves traffic more than the average flow rate would suggest.

At your current settings
A = 11.50,   Q = 9.98 m³/s
u = Q/A = 0.868 m/s
c = (5/3)u = 1.446 m/s

At the default settings (n=0.04, S₀=0.001, B=10 m, h=1.15 m): α=0.1703\alpha = 0.1703, u=0.87u = 0.87 m/s, c=1.45c = 1.45 m/s. The Courant column in the table turns red when C=cΔt/Δx>1C = c\,\Delta t/\Delta x > 1 — that is the stability boundary for explicit schemes.


4.3 — Explicit Finite Differences & the Courant Condition

The kinematic wave equation A/t+Q/x=qlat\partial A/\partial t + \partial Q/\partial x = q_\text{lat} is a partial differential equation — it links how AA changes in space and time simultaneously. For a real river, wave speed c=53uc = \tfrac{5}{3}u depends on depth, which changes as the flood evolves. No closed-form solution exists. We must solve it numerically: replace the continuous derivatives with finite ratios on a discrete grid, then march forward one step at a time.

The widget below builds this from absolute scratch — from "what is a finite difference?" to a 3-reach calculation you can verify with a calculator.

From Calculus to Computation

Build the explicit scheme from first principles — then walk through it by hand

Why Can't We Solve This Analytically?

The simple case (solvable)
If wave speed c is CONSTANT and q_lat = 0:

  ∂A/∂t + c·∂A/∂x = 0

Exact solution:
  A(x, t) = A₀(x − c·t)

The initial wave shape just SLIDES
downstream at speed c. No numerical
methods needed.
The real river (not solvable)
For real rivers:
  c = β·α·A^(β−1)  — it DEPENDS ON A.

As the flood rises, A grows → c grows
→ wave accelerates. Different parts
of the wave travel at different speeds.

No closed-form solution exists for
this nonlinear PDE. We must compute
it on a discrete grid.

The strategy: divide the river into short cells (Δx = 500 m) and time into short steps (Δt = 300 s), then march forward one step at a time using a simple algebraic update formula.

Replace the River with a Grid

Chop the channel into N = 8 cells of length Δx = 500 m each. Label them i = 0, 1, 2, …, 7. Then divide time into steps of Δt = 300 s. At every grid point (i, t) we store one number: A[i, t], the cross-sectional area at that location and instant.

🟩Bottom row (t = 0): initial condition. Every reach starts at baseflow A₀ = 4.38 m² (Q₀ = 2 m³/s).
🟨Left column (i = 0): boundary condition. The upstream hydrograph drives the simulation — we prescribe Q_in(t) here.
Everything else: UNKNOWN — computed left-to-right, bottom-to-top (forward in time).

Goal: fill in the entire table. Given what we know in the green and amber cells, can we systematically compute every white cell? Yes — that's what the explicit scheme does.

2?????8?????12?????10?????4.384.384.384.384.384.38i=0i=1i=2i=3i=4i=5→ distance downstream (x)t=4t=3t=2t=1t=0↑ time

What Is a Derivative on a Grid?

Time derivative: ∂A/∂t
∂A/∂t  ≈  (A[i, t+1] − A[i, t]) / Δt
A[i, t]known (now)A[i, t+1]UNKNOWN (next)Δt

Temperature at 8am: 15°C. At 9am: 18°C. Rate of change = (18−15)/1 hr = 3°C/hr. Same idea.

Space derivative: ∂Q/∂x
∂Q/∂x  ≈  (Q[i, t] − Q[i−1, t]) / Δx
Q[i−1, t]upstreamQ[i, t]current cellΔx

We look UPSTREAM (i−1), not downstream (i+1).

Why the upstream cell? (The upwind scheme)

Water flows from i−1 → i → i+1. Information travels WITH the flow — downstream. If we looked at Q[i+1,t], we'd be reading data from a cell the flood hasn't reached yet. Using Q[i−1,t] means we look “into the wind” — toward where the flow is coming from. This is the upwind approximation, and it's both physically correct and numerically stable.

Derive the Update Formula Step by Step

Start: the PDE with continuity and Manning power law:
∂A/∂t  +  ∂Q/∂x  =  q_lat    where Q = α·A^β
Step 1 — Substitute finite-difference approximations:
(A[i,t+1] − A[i,t]) / Δt  +  (Q[i,t] − Q[i−1,t]) / Δx  =  q_lat

Every ∂ (calculus) is replaced by Δ (arithmetic). No limits, no calculus — just subtraction.

Compute It Yourself — 3 Reaches, 5 Time Steps

Δx = 500 mΔt = 300 sΔt/Δx = 0.60α = 0.1703β = 1.667A₀ = 4.38 m²Q₀ = 2.00 m³/s
t01234
Q_in (m³/s)2101282
Cross-Sectional Area A (m²)
t \ i012
04.384.384.38
111.52??
212.85??
310.07??
44.38??
Discharge Q (m³/s)
t \ i012
02.002.002.00
110.00??
212.00??
38.00??
42.00??
Rows t=0 and column i=0 are already known.
Press "▶ Compute next cell" to start.

The Courant Condition — When C > 1, It Breaks

The explicit scheme advances information exactly one cell-width Δx per time step. But the flood wave travels c·Δt meters per step. If c·Δt > Δx — if the wave moves MORE than one cell per step — the scheme loses track of it.

Think of it like throwing a ball and trying to catch it: if it flies past you in less time than it takes your arm to reach, you can't catch it. The scheme “misses” the wave and produces oscillating, unphysical values.

stableC=0.87: wave reaches 87% of cell ✓unstableC=1.16: wave jumps past the cell ✗
C = c · Δt / Δx  ≤  1

c  = (5/3)·Q/A = wave celerity [m/s]
Δt = time step size [s]
Δx = cell width [m]

At defaults (Q=10, A=11.52):  c = 1.446 m/s
Max stable Δt = Δx / c = 500 / 1.446 = 345 s
Default Δt = 300 s → C = 0.868  ✓
Sub-section B — Watch three Δt choices race the same flood, side by side

Same upstream hydrograph (2, 8, 12, 8, 4, 2, 2, 2 m³/s), same 3 reaches, same formula — only Δt differs. Press Play and watch reach i=2 (the bottom cell in each strip, furthest downstream) as the flood arrives.

Too SmallΔt = 60 s → C = 0.17
iA (m²)Q (m³/s)
04.382.00
14.382.00
24.382.00
Peak Q so far (i=2): 0.00 m³/s
✓ stable
Just RightΔt = 300 s → C = 0.87
iA (m²)Q (m³/s)
04.382.00
14.382.00
24.382.00
Peak Q so far (i=2): 0.00 m³/s
✓ stable
Too BigΔt = 500 s → C = 1.45
iA (m²)Q (m³/s)
04.382.00
14.382.00
24.382.00
Peak Q so far (i=2): 0.00 m³/s
✓ stable
Step 0 of 8 · Stability boundary: Δt ≈ 346 s

At C > 1, the scheme subtracts more water from a cell than it contains — producing negative (unphysical) areas. The simulation diverges within a few steps. OPM fixes this with a flux limiter (see §4.4).

✓ You now understand the complete explicit scheme from first principles:
  • Finite differences replace ∂ with Δ
  • Upwind scheme uses upstream Q
  • Explicit means one unknown per step
  • Courant condition C ≤ 1 ensures stability

The interactive simulation below runs this same algorithm across 8 reaches and 12 time steps — every cell visible, every number computed.


The explicit update formula (derived in the widget above):

Ain+1=AinΔtΔx(QinQi1n)+ΔtqlatA_i^{n+1} = A_i^n - \frac{\Delta t}{\Delta x}\bigl(Q_i^n - Q_{i-1}^n\bigr) + \Delta t\, q_\text{lat}

then evaluate Qin+1=α(Ain+1)βQ_i^{n+1} = \alpha\,(A_i^{n+1})^\beta. The scheme is first-order accurate and conditionally stable: wave speed cc and time step Δt\Delta t must satisfy

C=cΔtΔx1C = \frac{c\,\Delta t}{\Delta x} \leq 1

Use the Step Through tab below to watch 12 time steps unfold across 8 reaches. Drag Δt\Delta t past 346 s to trigger instability. Toggle "Enable Flux Limiter" to see OPM's fix.

Explicit Finite Differences — Step by Step

Watch each reach update, track the Courant number, trigger instability

Q (m³/s)14001234567891011Q_in = 2.0 m³/sUpstream boundary Q_in(t)
Q_in = 2.0 m³/s
i=0
2.00
C=0.46
i=1
2.00
C=0.46
i=2
2.00
C=0.46
i=3
2.00
C=0.46
i=4
2.00
C=0.46
i=5
2.00
C=0.46
i=6
2.00
C=0.46
i=7
2.00
C=0.46
iA (m²)Q (m³/s)u (m/s)c (m/s)CQ_limActive?
04.3842.0000.4560.7600.4562.000
14.3842.0000.4560.7600.4562.000
24.3842.0000.4560.7600.4562.000
34.3842.0000.4560.7600.4562.000
44.3842.0000.4560.7600.4562.000
54.3842.0000.4560.7600.4562.000
64.3842.0000.4560.7600.4562.000
74.3842.0000.4560.7600.4562.000
t=0s: i=0: A 4.38→4.38 m² | Q 2.00→2.00 m³/s | C=0.46
t=0s: i=1: A 4.38→4.38 m² | Q 2.00→2.00 m³/s | C=0.46
t=0s: i=2: A 4.38→4.38 m² | Q 2.00→2.00 m³/s | C=0.46
t=0s: i=3: A 4.38→4.38 m² | Q 2.00→2.00 m³/s | C=0.46
t=0s: i=4: A 4.38→4.38 m² | Q 2.00→2.00 m³/s | C=0.46
t=0s: i=5: A 4.38→4.38 m² | Q 2.00→2.00 m³/s | C=0.46
t=0s: i=6: A 4.38→4.38 m² | Q 2.00→2.00 m³/s | C=0.46
t=0s: i=7: A 4.38→4.38 m² | Q 2.00→2.00 m³/s | C=0.46
t = 0 × 300s = 0s
Speed600ms
100s600s
Current step C_max
0.46✓ Stable
Courant number:
C = c · Δt/Δx
= 0.456 × 300 / 500
= 0.274
(mean celerity at this step)
Courant legend
C ≤ 0.8 — optimal
0.8 < C ≤ 1 — marginal
C > 1 — unstable
Channel: B=10m, S₀=0.001, n=0.04Grid: Δx=500m, 8 reachesα = 0.1703β = 5/3 (Manning power)Kinematic wave: ∂A/∂t + ∂Q/∂x = 0, Q = αA^β

Seeing the blow-up is not the same as understanding it — and instability is only half the story. The next section derives where the C1C \leq 1 limit actually comes from, and the quieter tax the grid charges even when you obey it.


4.4 — The Computer's White Lie: Numerical Diffusion & the Courant Number

§4.3 showed that the explicit scheme blows up past C=1C=1. This section shows why — and the subtler price you pay even when you stay safely below it. The headline: a pure kinematic wave should translate a flood without changing its shape, yet every grid quietly shrinks and smears the peak.

Where the wave equation comes from

Continuity A/t+Q/x=0\partial A/\partial t + \partial Q/\partial x = 0 closed with the rating curve Q=αAβQ=\alpha A^\beta (so Q/x=cA/x\partial Q/\partial x = c\,\partial A/\partial x with c=dQ/dAc = dQ/dA) collapses to a single advection equation:

At+cAx=0A(x,t)=A0(xct)\frac{\partial A}{\partial t} + c\,\frac{\partial A}{\partial x} = 0 \qquad\Longrightarrow\qquad A(x,t) = A_0(x - ct)

The exact solution is just the initial shape slid downstream at speed cc — sharp stays sharp, the peak is preserved forever. So any flattening your model produces is not physics; it is numerical, and the Courant number C=cΔt/ΔxC = c\,\Delta t/\Delta x controls exactly how much.

The flood peak the grid throws away

The true kinematic wave just slides downstream, shape intact. The upwind grid smears it. The Courant number C decides how much.

distance downstream (grid cell)Q
initial pulse true wave (PDE, no smear) numerical (upwind)
0.50
σ = 3.0 cells
peak kept54%
numerical diffusion ∝ (1−C)0.50
time steps to travel88

Δx (fixed)500 m
c (fixed)1.45 m/s
⇒ Δt = C·Δx/c172 s
The Δt trap. With the grid (Δx) and the wave (c) fixed, the C slider is the Δt knob. Drag C down — a smaller, "safer" time step — and the peak gets worse: more steps, each one averaging neighbours, so more smear. The sweet spot is C = 1 (exact, no smear), not C → 0. This is why OPM runs CFL_TARGET = 0.85 instead of a tiny step: a sharp, physical peak.

The update is a weighted average

Discretize with forward-time, backward-space (upwind — because the water arrives from upstream) and rearrange:

Qin+1QinΔt+cQinQi1nΔx=0Qin+1=CQi1n+(1C)Qin\frac{Q_i^{n+1}-Q_i^n}{\Delta t} + c\,\frac{Q_i^n - Q_{i-1}^n}{\Delta x}=0 \quad\Longrightarrow\quad \boxed{\,Q_i^{n+1} = C\,Q_{i-1}^n + (1-C)\,Q_i^n\,}

For 0C10\le C\le 1 both weights are positive and sum to one, so the new value is a weighted average of upstream and here — it can never escape the range it started in (bounded \Rightarrow stable). At C>1C>1 the weight (1C)(1-C) turns negative: no longer an average but an extrapolation, and the errors amplify every step.

Average, or extrapolation? The weight (1−C) decides

The new value is a blend of upstream and here. Two non-negative weights = a safe average. A negative weight = a runaway.

10.00
Qᵢ₋₁ⁿupstream
4.00
Qᵢⁿhere
7.60
Qᵢⁿ⁺¹result
inside the [4, 10] band → bounded average
Qᵢⁿ⁺¹ = 0.60·10 + (0.40)·4 = 7.60
weight on upstream: C = 0.60weight on here: (1−C) = 0.40
0.60
10
4
March a single spike 6 steps with this C — bounded decay, or runaway?
start
2.0
2.0
12.0
2.0
2.0
2.0
2.0
2.0
2.0
step 1
2.0
2.0
6.0
8.0
2.0
2.0
2.0
2.0
2.0
step 2
2.0
2.0
3.6
6.8
5.6
2.0
2.0
2.0
2.0
step 3
2.0
2.0
2.6
4.9
6.3
4.2
2.0
2.0
2.0
step 4
2.0
2.0
2.3
3.5
5.5
5.5
3.3
2.0
2.0
step 5
2.0
2.0
2.1
2.8
4.3
5.5
4.6
2.8
2.0
step 6
2.0
2.0
2.0
2.4
3.4
4.8
5.1
3.9
2.5
C ≤ 1: the spike stays trapped between baseflow and its starting height, translating downstream while gently flattening. Bounded = stable.

Telephone-game intuition. Pass a message cell to cell. Copy it exactly (C=1C=1) and it survives. Blend in a bit of your own value (C<1C<1) and it blurs a little each time — that blur is the diffusion. Exaggerate it (C>1C>1) and it explodes into nonsense.

What the computer is really solving

Taylor-expand that update around (xi,tn)(x_i,t_n), keep the leading error, then use the equation itself to trade the time error for a space error (Qtt=c2QxxQ_{tt}=c^2 Q_{xx}). The scheme turns out not to solve pure advection — it solves advection plus a diffusion term nobody wrote down:

Qt+cQx=α2Qx2,α=cΔx2(1C)\frac{\partial Q}{\partial t} + c\,\frac{\partial Q}{\partial x} = \alpha\,\frac{\partial^2 Q}{\partial x^2}, \qquad \alpha = \frac{c\,\Delta x}{2}\,(1 - C)

That αQxx\alpha\,Q_{xx} is the heat / diffusion operator — it spreads and flattens gradients. Because c,Δx>0c,\Delta x>0, the sign of this artificial diffusion is the sign of (1C)(1-C):

Courantα\alphawhat happens
C<1C<1α>0\alpha>0real diffusion → smears / damps the peak (stable, but dissipative)
C=1C=1α=0\alpha=0pure advection recovered → exact, no smear
C>1C>1α<0\alpha<0anti-diffusion → sharpens & amplifies → blows up

The equation the computer actually solves

Six lines of Taylor algebra reveal a hidden diffusion term — and the Courant number sets its sign.

Start from the upwind scheme

Forward in time, backward (upwind) in space:

  (Qᵢⁿ⁺¹ − Qᵢⁿ)/Δt  +  c·(Qᵢⁿ − Qᵢ₋₁ⁿ)/Δx  =  0
α explorer — pick a Courant number
0.60
α = (cΔx/2)(0.40) > 0 → diffusion, smears (stable)
α=0 (C=1)α>0 (C<1)α<0 (C>1)
a pulse after diffusing

Dashed grey = the original pulse (C=1, untouched). Solid = after the numerical diffusion α acts: spreads when C<1, blows up when C>1.

The counterintuitive trap. Since α=(cΔx/2)(1C)\alpha=(c\,\Delta x/2)(1-C), on a fixed grid shrinking Δt\Delta t pushes C0C\to 0 and drives α\alpha up toward its maximum cΔx/2c\,\Delta x/2. A smaller, "safer" time step makes the smearing worse, not better — the only cures are a finer Δx\Delta x or pushing C1C\to 1. This is exactly why OPM targets C=0.85C=0.85 (CFL_TARGET) instead of a tiny step, and why config.py warns that a static Δt0.9\Delta t\approx0.9 s "over-diffuses (damps/smears the peak)."

Grid speed vs. water speed

The upwind stencil reaches exactly one cell upstream per step, so information cannot travel faster than the grid celerity cgrid=Δx/Δtc_\text{grid}=\Delta x/\Delta t. The Courant number is the ratio of the two wave speeds — how many cells the flood crosses in one step:

C=cΔtΔx=cΔx/Δt=wave speedgrid speedC = \frac{c\,\Delta t}{\Delta x} = \frac{c}{\,\Delta x/\Delta t\,} = \frac{\text{wave speed}}{\text{grid speed}}

Trace the characteristic back from the node you are solving: the water there came from xicΔt=xiCΔxx_i - c\,\Delta t = x_i - C\,\Delta x upstream. Land on a node (C=1C=1) and the answer is exact; land between nodes (C<1C<1) and the scheme linearly interpolates a curved profile — cutting the corner, shaving the peak, which is the numerical diffusion you just derived; land outside the stencil (C>1C>1) and the formula reaches for data it does not have \Rightarrow instability.

Can the wave outrun the grid?

C = how many cells the wave crosses per step. Trace it back to where the water came from — and see why missing the node smears the peak.

space–time grid · trace the characteristic back
n+1ngrid speed (C=1, 1 cell/step)i−2i−1ii+1solve here (i, n+1)came from xᵢ − C·Δx
Departure lands between nodes → must interpolate → numerical diffusion.
why a miss smears it · interpolation vs truth
i−1i
true profile interpolation (the scheme) true value computed value
peak shortfall (true − computed) = -0.008 — that gap, every step, is the smear.
three different speeds
grid celerity Δx/Δt
1 cell / step (info limit)
wave front c
the flood signal
water u = 0.6 c
the molecules

C = c ÷ (Δx/Δt) = wave speed ÷ grid speed. Keep the wave from out-running the grid: C ≤ 1.

0.60

Why C>1C>1 erupts into oscillations

You watched a peak both collapse and spike when C>1C>1 — let's see why it flickers like that instead of just growing. The grid carries information one cell per step (the stencil reaches one cell upstream). When C>1C>1 the wave travels more than one cell per step, leaping past node i1i-1 into a cell the update cannot see; forced to rebuild that value from the only two nodes it has, the scheme stops interpolating and starts extrapolating, overshooting to the wrong side.

To see the runaway in its purest form, feed the update the jaggediest pattern a grid can hold — a sawtooth, where every cell is the mirror image of its neighbour, Qi1n=QinQ_{i-1}^n = -Q_i^n. The whole update collapses to a single multiply:

Qin+1=CQi1n+(1C)Qin=C(Qin)+(1C)Qin=(12C)QinQ_i^{n+1} = C\,Q_{i-1}^n + (1-C)\,Q_i^n = C(-Q_i^n) + (1-C)\,Q_i^n = (1-2C)\,Q_i^n

So each step the entire zigzag is just scaled by one number, g=12Cg = 1-2C:

CCg=12Cg = 1-2Ceach step the zigzag…meaning
0.50.500vanishes instantlydiffusion crushes the wiggles
111-1flips sign, same sizeneutral — the exact knife-edge
1.61.62.2-2.2flips sign and grows ×2.2oscillation + blow-up

A negative gg means every cell swaps sign each step — high \to low \to high — which is the oscillation; g>1|g|>1 means each swing is bigger than the last. That is why, once C>1C>1, a peak doesn't merely grow: it flickers up and down while exploding. Press ▶ below and compare the creeping, smooth spread at small Δt\Delta t with the growing sawtooth past C=1C=1.

When the wave outruns the grid

One tick, one cell — that's all the grid can follow. Watch what happens when the wave tries to cross more.

300 s
Courant
C = 0.87
① the real wave · stride = C cells per tick · grid can follow only 1
startthis tick: 0.87 cells
↳ stride < 1 cell: the wave is still inside the cell — the scheme interpolates between the two nodes it can see. Bounded, but a little smeared each tick.
② the numerical solution · one spike, marched 1 step
00.000.000.000.130.870.000.000.000.00
peak magnitude this step: 0.87 ( ×0.87 vs last step)blue = positive · red = negative / overshoot
Qᵢⁿ⁺¹ = 0.87·Q₍ᵢ₋₁₎ + (0.13)·Qᵢ
Jaggedest pattern (sawtooth, Q₍ᵢ₋₁₎ = −Qᵢ) is multiplied each step by
g = 1 − 2C = -0.74shrinks → wiggles die (diffusion)

4.5 — Adaptive Δt: Global vs. Local Time Stepping (and How to Keep It Physical)

§4.4 left us on a knife-edge: C>1C>1 explodes, C1C\ll1 over-smears, and only C1C\approx1 is both stable and sharp. But the wave speed c=53uc=\tfrac53 u is not a constant — it changes from cell to cell (steep headwater vs. flat valley) and from moment to moment (rising vs. falling limb). No single fixed Δt\Delta t can hold C1C\approx1 everywhere, for all time. The fix is to let the flow choose the step: adaptive time stepping.

How adaptive Δt works (what OPM does)

Each step, find the fastest wave anywhere on the wet grid and size Δt\Delta t so its Courant number lands on the target:

\Delta t^{n} = C_\text{target}\cdot\frac{\Delta x}{c_\max^{n}}, \qquad c_\max^{n} = \max_i c_i^{n}

OPM uses Ctarget=0.85C_\text{target}=0.85, with a floor/ceiling and a growth limiter (Δt may shrink instantly but grow by at most 1.5× per step, so a sudden drop in c_\max can't dump a volume pulse). In the real source it is one line — a max over the whole domain:

inv_dt     = c_1d / dx            # per-cell 1/Δt requirement
inv_dt_max = float(inv_dt.max())  # ← the single fastest cell wins
dt_new     = cfl_target / inv_dt_max

That max is the whole story: one Δt\Delta t for every cell, dictated by the one fastest cell. This is global (synchronous) adaptive stepping.

Global vs. Local

Global (what OPM uses)Local (per-cell, "LTS")
Δt ruleone \Delta t = C_\text{target}\,\Delta x/c_\maxeach cell its own Δti=CtargetΔx/ci\Delta t_i = C_\text{target}\,\Delta x/c_i
Courant reachedCtargetC_\text{target} only at the fastest cellCtargetC_\text{target} everywhere
Slow cellsrun at CCtargetC\ll C_\text{target} → over-diffusestay at CtargetC_\text{target} → minimal diffusion
Sync / conservationtrivial (all cells same time level)must match fluxes across Δt interfaces
Costwastes tiny steps on slow cellseach cell stepped only as often as needed

The hidden tax of going global

Under the one global Δt\Delta t, a cell's actual Courant number is

C_i = \frac{c_i\,\Delta t}{\Delta x} = C_\text{target}\cdot\frac{c_i}{c_\max}.

Only the single fastest cell runs at CtargetC_\text{target}; a flat valley cell with c_i\ll c_\max runs at Ci0C_i\approx0. Recall from §4.4 that the smear a peak collects crossing a reach of length LL is σ2=ΔxL(1C)\sigma^2 = \Delta x\,L\,(1-C) — it depends on (1C)(1-C) and geometry, not on how fast the wave is. So at the steep cell the factor is (1C)=0.15(1-C)=0.15, while at the flat cell it is 1\approx1:

The flat reaches over-damp the flood peak ~6× harder than the cell that set the timestep — purely because the global Δt\Delta t forced them far below their own stability limit. It is the grid attenuating the peak, not the physics.

This is most damning for the kinematic wave, whose true attenuation is zero — every bit of flat-reach peak loss is a grid artifact. For the diffusive wave there is real attenuation (Dphys=Q/2BS0D_\text{phys}=Q/2BS_0), but the global Δt\Delta t stacks uncalibrated numerical diffusion on top, and since DnumD_\text{num} and DphysD_\text{phys} occupy the same 2/x2\partial^2/\partial x^2 slot, the model can no longer tell you which fraction of a flat-valley hydrograph's attenuation is real. (Chapter 5 §5.5 puts OPM's true Δx100\Delta x\approx100 m numbers on this: the flat cell's CC is 164× smaller than the steep cell's, and its DnumD_\text{num} reaches 28%\approx28\% of DphysD_\text{phys}.)

One clock for the whole river — or one each?

A global Δt is set by the single fastest cell. Watch what that does to the slow, flat reaches.

one Δt = 45.6 s for all 6 reaches (from the steep cell)
S₀=0.05C=0.85(1−C)=0.15steep headwaterS₀=0.02C=0.54(1−C)=0.462× over-steppedS₀=0.008C=0.34(1−C)=0.663× over-steppedS₀=0.003C=0.21(1−C)=0.794× over-steppedS₀=0.001C=0.12(1−C)=0.887× over-steppedS₀=0.0004C=0.08(1−C)=0.92flat lowland11× over-steppedflow →
bar = numerical diffusion α=(cΔx/2)(1−C) per reach · green low / red high
peak after the 6 reaches
true (kinematic, 100%) global 71% local 90%
Global Δt — the hidden tax
  • flat reach runs at C=0.086.2× the numerical diffusion of the steep cell that set Δt
  • peak kept: 71% — yet the true kinematic peak is 100%, so 29% of the loss is a grid artifact, not physics
  • flat reach is integrated 11× more often than its accuracy needs
on hardware

✓ GPU-ideal. Every cell does the identical update at the identical time — perfect SIMT lockstep, one kernel. This is exactly why OPM uses it.

Keeping it physical without a rewrite: (1) local/block time stepping → uniform C; (2) a higher-order / TVD flux (Lax–Wendroff, MUSCL) kills the O(Δx)(1−C) term and keeps the GPU-friendly global Δt; (3) Muskingum–Cunge — tune the numerical diffusion to equal the physical D=Q/(2BS₀) so it stops being an artifact.

Can a GPU do these? (the architecture answer)

This is the real reason OPM went global. A global Δt\Delta t has every cell doing the identical update at the identical time — perfect SIMT lockstep, one kernel, coalesced memory, no divergence. It is the GPU's happy path (and fine on CPU).

Local stepping is the opposite. Different cells take different numbers of steps at different times, which on a GPU means warp divergence (threads in a warp branch apart), load imbalance (a fast cell's thread does 10× the work of a slow one in the same launch), irregular synchronization, and conservative flux matching across Δt\Delta t interfaces (atomic "reflux"). On a CPU none of that hurts — irregular scheduling and event queues are natural, and LTS is a classic CPU speed-up for heterogeneous hydraulics. The GPU-tractable compromise is block / class LTS: bin cells into a few power-of-two Δt\Delta t classes, give each class its own kernel, sub-cycle the fast classes, and reconcile fluxes at synchronization points — coarser than per-cell, but it keeps each kernel in lockstep.

How to keep it physical

Four routes, from cheapest-to-bolt-on to most general:

  1. Local / block time stepping — hold every cell at CCtargetC\approx C_\text{target} so the numerical diffusion is uniform and minimal; the flat reaches stop over-damping. Direct cure, GPU-costly (above), needs conservative inter-Δt coupling.
  2. Higher-order / TVD spatial flux (Lax–Wendroff, MUSCL + a limiter: minmod / van Leer / superbee) — cancels the leading O(Δx)(1C)O(\Delta x)(1-C) diffusion, leaving O(Δx2)O(\Delta x^2) error that barely depends on CC. So even a small-CC flat cell stops over-smearing — while keeping the GPU-friendly global Δt\Delta t. For a GPU code this is usually the best value.
  3. Muskingum–Cunge — make the numerical diffusion be the physics. Instead of fighting α\alpha, calibrate it: choose the weighting X=12 ⁣(1QBS0cΔx)X=\tfrac12\!\left(1-\dfrac{Q}{B S_0\,c\,\Delta x}\right) so the scheme's numerical diffusivity equals the hydraulic diffusivity D=Q/2BS0D=Q/2BS_0. Then the "artificial" diffusion is no longer artificial — it is exactly the attenuation a diffusive wave should have. The most physically honest answer.
  4. Implicit (Preissmann θ) — unconditionally stable, so Δt\Delta t is chosen for accuracy, not CFL; the fast cell no longer forces a tiny global step. But on OPM's branching DAG the confluences break the cheap bidiagonal solve (Chapter 5 §5.6), it needs a general sparse solver at 10410^410610^6 cells, and OPM's own implicit attempt was removed for divergence.

Verdict. Global explicit Δt\Delta t is the right GPU call, but it leaves the flat-reach diffusion on the table. The cheapest physics-preserving upgrade that stays on the GPU is a higher-order/TVD flux (route 2); the most physically honest is Muskingum–Cunge matched diffusion (route 3); the most general is block/class LTS (route 1) — none of which require the full implicit rewrite of route 4.


4.6 — Beating the CFL Limit: Implicit Schemes & Flux Limiters

When Δt\Delta t must be large (computational cost, large Δx\Delta x), two strategies break the CFL barrier:

Preissmann θ-implicit scheme — weight spatial differences at the new time level n+1n+1 by θ\theta and at nn by (1θ)(1-\theta):

Ain+1AinΔt+cˉiθ(Ain+1Ai1n+1)+(1θ)(AinAi1n)Δx=q\frac{A_i^{n+1} - A_i^n}{\Delta t} + \bar{c}_i \frac{\theta(A_i^{n+1} - A_{i-1}^{n+1}) + (1-\theta)(A_i^n - A_{i-1}^n)}{\Delta x} = q

This produces a lower-bidiagonal linear system solved by forward substitution (Thomas algorithm). Unconditionally stable for θ0.5\theta \geq 0.5; θ=0.6\theta = 0.6 is the Preissmann industry standard (HEC-RAS, ISIS, SWMM).

OPM flux limiter — no matrix, no extra storage. Simply cap each cell's outflow:

Qout=min ⁣(QManning,  AΔxΔt)Q_\text{out} = \min\!\left(Q_\text{Manning},\; \frac{A\,\Delta x}{\Delta t}\right)

Both keep the simulation stable at Δt=500\Delta t = 500 s (C ≈ 1.16). Explore the trade-offs — the matrix display in Tab 1 shows actual numerical values; the table in Tab 2 highlights exactly which cells hit the limiter. Tab 1 also walks through what "implicit" means and animates the Thomas-algorithm forward sweep row by row, using the same numbers shown in the matrix.

Beating the CFL Limit

Preissmann implicit scheme vs. OPM flux limiter — same C>1 problem, different solutions

Weighting parameter θ = 0.60Preissmann standard (★)
0 (explicit)0.5 (C-N)0.6 (standard)1 (implicit)
What does “implicit” actually mean?
Explicit
A1ⁿ⁺¹A2ⁿ⁺¹A3ⁿ⁺¹A1A2A3

Each future value depends only on past values — already known. Compute left→right, one at a time.

Implicit
A1ⁿ⁺¹A2ⁿ⁺¹A3ⁿ⁺¹A1A2A3

Each future value also depends on its neighbor's future value — none are known until ALL are solved together.

This is what “implicit” means: the unknowns are defined in terms of each other (implicitly), not directly from already-known data (explicitly). The trade-off: extra work per step — solving a coupled system instead of a single formula — buys you unconditional stability. Δt can be arbitrarily large without blowing up.

Analogy: explicit is a relay race — each runner starts only once the previous one has arrived. Implicit is a small group writing a joint statement — nobody finalizes their sentence until everyone's sentences are mutually consistent.

Why coupling prevents blow-up: an explicit step extrapolates forward from the past — if Δt is too large, the extrapolation overshoots, and that overshoot compounds every subsequent step (the same runaway oscillation you saw with the explicit scheme once C>1). An implicit step does not extrapolate; it solves for the one set of future values that satisfies every reach's equation simultaneously. There is no overshoot to compound — a larger Δt only costs accuracy (more numerical diffusion), never stability.

Matrix at time step:
Implicit system matrix (lower-bidiagonal) — step 2
rowA1A2A3A4A5A6RHS (d)Aⁿ⁺¹
10.003000000.0148.982
2−0.0010.00300000.0095.824
30−0.0010.0030000.0094.835
400−0.0010.003000.0094.525
5000−0.0010.00300.0094.428
60000−0.0010.0030.0094.398
bi = 1/Δt + θ·c̄/Δx−li = −θ·c̄/ΔxRHS disolution Aⁿ⁺¹
Thomas algorithm: forward sweep ↓ then back-substitute ↑

Nothing solved yet — row 1 only needs the boundary value (already known), so it can go first. Press “▶ Next row” to reveal each step of the sweep.

Outlet hydrograph (reach 6) — Δt=500 s, C≈1.16
0.069.2138.3207.5276.70369Q (m³/s)Time stepExplicit Δt=500s (unstable)Preissmann θ=0.60 Δt=500sExplicit Δt=300s (reference)
Seeing it on this hydrograph: at Δt=500 s the explicit line (gray) blows up — C≈1.16, the overshoot from the previous step compounding into the next. The Preissmann line (violet) tracks the Δt=300 s reference closely at the same Δt=500 s, because the coupled solve above never extrapolates — it just costs a touch of artificial diffusion, tunable via θ.
Explicit
C ≈ 1.16
Δt=500s, unstable
Preissmann
C ≈ 1.16 ✓
Δt=500s, stable (θ≥0.5)
Reference
C ≈ 0.70
Δt=300s, safe

4.7 — Kinematic vs. Diffusive Wave

The kinematic wave translates a flood pulse without changing its shape — peak discharge is perfectly preserved. Real floods don't behave that way: they flatten as they travel, and they back up behind anything that obstructs them. The diffusive wave fixes both by trading the bed slope S0S_0 for the water-surface slope, which lets the wave attenuate and feel backwater. A quick preview, dragging the blend weight θdiff\theta_\text{diff} from 0 (pure kinematic) to 1 (OPM's default, full diffusive):

Kinematic vs. Diffusive Wave

θ_diff controls attenuation — kinematic translates, diffusive spreads

8-Reach Channel — t = 0 / 11

i=0
2.0
i=1
2.0
i=2
2.0
i=3
2.0
i=4
2.0
i=5
2.0
i=6
2.0
i=7
2.0
Kinematicθ=0.0Diffusive
FastSlow
0 = kinematic0.5 = blend1 = diffusive
Input hydrograph: peak = 12 m³/s at t = 3

Outlet Hydrograph (reach i = 7)

04812036911Q (m³/s)time step2.32.31936.9t=0Kinematicθ=0.0Diffusive

Outlet Statistics

SchemePeak Q (m³/s)Peak at t=AttenuationMass (m³)
Kinematic2.3110%7293
θ=0.002.3110.0%7293
Diffusive1936.911-85388.7%588248

Routing Scheme Limitations

LimitationKinematicDiffusiveDynamic
No backwater effects✗ blindpartial
Hydrograph attenuation✗ zero✓ some✓ full
Flat reaches (S→0)✗ fails~ marginal
Computational costO(N)O(N)O(N log N)
Valid whenS₀ > 0.001S₀ > 0.0001always
In OPM config.py:
  ROUTING_SCHEME = 'kinematic'  →  θ_diff = 0
  ROUTING_SCHEME = 'diffusive'  →  uses DIFFUSION_THETA
  DIFFUSION_THETA = 1.0  (full diffusion by default)
LimitationKinematicDiffusiveDynamic
No backwater effects✗ blindpartial
Hydrograph attenuation✗ zero✓ some✓ full
Flat reaches (S00S_0 \to 0)✗ fails∼ marginal
Computational costO(N)O(N)O(N)O(N)O(NlogN)O(N \log N)
Valid whenS0>0.001S_0 > 0.001S0>104S_0 > 10^{-4}always

Chapter 5 is entirely about this — a real example of the kinematic wave giving the wrong answer, the diffusive equation derived from scratch, OPM's actual GSSHA-style production code walked through line by line, and what running it all under one global timestep costs on flat terrain.


4.8 — Explore Channel Routing

Try the four presets — Mountain Torrent gives small Courant numbers and sharp wave translation; Lowland River pushes Courant toward instability with the large Δt\Delta t needed for long low-slope channels. Click any reach to pin its time series on the right chart and watch peaks arrive with a lag of Δx(nreaches)/c\Delta x\,(n\,\text{reaches})/c.

Explore Channel Routing

Adjust parameters, pick presets, watch the flood wave propagate

i=0
2.0
0.46
i=1
2.0
0.46
i=2
2.0
0.46
i=3
2.0
0.46
i=4
2.0
0.46
i=5
2.0
0.46
i=6
2.0
0.46
i=7
2.0
0.46
Click reaches to pin their time series →
t=0 (0s)
speed500ms
Manning n0.040
Slope S₀0.0010
Width B (m)10 m
Rise steps3
Total steps12
Δt (s)300 s
θ_scheme (kin→diff)kinematic
Peak Q (m³/s)12 m³/s
Baseflow Q (m³/s)2 m³/s
Upstream forcing
130upstream hydrograph (m³/s)
Reach time series
03.06.09.012.0time step →i=0: 2.0i=7: 2.0
Live stats
α0.1703 m^(1/3)/s
β5/3 = 1.667 (fixed)
Wave celerity c0.760 m/s
Courant C_max0.456
Wave travel (0→7)3500m / c = 76.7 min
Upstream peak12 m³/s at t=3
Outlet peak4.1 m³/s
Peak attenuation~0% (kinematic)
Mass balance<0.001% error
C_max:0.456✓ Stable

4.9 — For Grad Students

Method of Characteristics (MOC) — the kinematic wave equation A/t+cA/x=q\partial A/\partial t + c\,\partial A/\partial x = q is hyperbolic; along the characteristic dx/dt=cdx/dt = c the solution reduces to an ODE. The CFL condition is geometric: the numerical grid must contain the characteristic within one cell per step.

Modified equation & von Neumann (§4.4 in full rigor) — the upwind scheme's leading truncation error is a diffusion term: it actually solves Qt+cQx=αQxxQ_t + cQ_x = \alpha Q_{xx} with α=cΔx2(1C)\alpha = \tfrac{c\,\Delta x}{2}(1-C). The Fourier (von Neumann) amplification factor G=(1C)+CeikΔxG = (1-C) + Ce^{-ik\Delta x} gives G2=12C(1C)(1coskΔx)|G|^2 = 1 - 2C(1-C)(1-\cos k\Delta x), so G1 k    0C1|G|\le 1\ \forall k \iff 0\le C\le 1 — the CFL bound, with the high-wavenumber damping being the spectral fingerprint of that same α\alpha.

Lax-Wendroff (2nd order) — adds a correction term 12c2(Δt/Δx)22A/x2\tfrac{1}{2}c^2(\Delta t/\Delta x)^2 \partial^2 A/\partial x^2 to the upwind scheme that cancels the αQxx\alpha Q_{xx} numerical diffusion to leading order, at the cost of mild dispersive overshoot near discontinuities.

Muskingum-Cunge — analytically equivalent to the linear kinematic finite-difference scheme; parameters K=Δx/cK = \Delta x/c (travel time) and X=12(1Q/BS0cΔx)X = \tfrac{1}{2}(1 - Q/B S_0 c \Delta x) (shape factor) match diffusive-wave attenuation exactly when chosen correctly.

OPM flux limiter derivationQoutAΔx/ΔtQ_\text{out} \leq A\,\Delta x/\Delta t is equivalent to capping the effective Courant number at 1 for each cell independently, introducing O((1C)Δx)O((1-C)\,\Delta x) numerical diffusion; the global mass balance closes to floating-point precision.

GPU vectorisation — topological sort converts the 2-D routing problem to a 1-D array operation; CuPy scatter_add propagates all cell discharges to downstream neighbours in a single vectorised kernel, achieving  ⁣250\sim\!250 steps/s on 10610^6 cells.

Adaptive Δt\Delta t (§4.5 in full) — OPM uses global adaptive stepping, Δt=CtargetΔx/cmax\Delta t = C_\text{target}\,\Delta x / c_\text{max} (Ctarget=0.85C_\text{target}=0.85) with a 1.5× growth limiter — recomputed each step from the fastest wet cell. Local (per-cell) stepping would hold CCtargetC\approx C_\text{target} everywhere and kill the flat-reach over-diffusion, but breaks SIMT lockstep; the GPU-tractable middle ground is block/class LTS (power-of-two Δt\Delta t bins). Physics-preserving alternatives without LTS: a TVD/higher-order flux (removes the O(Δx)(1C)O(\Delta x)(1-C) term, keeps global Δt\Delta t) or Muskingum–Cunge matched diffusion (Dnum ⁣ ⁣DphysD_\text{num}\!\equiv\!D_\text{phys}).

Complexity summary:

AlgorithmComplexityNotes
KW explicit (CPU)O(NT)O(N \cdot T)one pass per step
Preissmann implicitO(NT)O(N \cdot T)banded solve, larger Δt\Delta t
GPU explicitO(T)O(T) effectiveNN parallelised
Full SVE solverO(NlogNT)O(N \log N \cdot T)iterative Newton per step

Summary

ConceptFormulaKey point
Kinematic assumptionSf=S0S_f = S_0Drop ①②③ from momentum
Power lawQ=αAβQ = \alpha A^\beta, β=53\beta=\tfrac{5}{3}Manning + wide rectangular
Wave celerityc=βQ/A=53uc = \beta\,Q/A = \tfrac{5}{3}\,uWave always outruns water
Courant numberC=cΔt/Δx1C = c\,\Delta t/\Delta x \leq 1Stability limit for explicit
Upwind updateQin+1=CQi1n+(1C)QinQ_i^{n+1} = C\,Q_{i-1}^n + (1-C)\,Q_i^nWeighted average; (1C)<0(1-C)<0 at C>1C>1 → blow-up
Numerical diffusionα=cΔx2(1C)\alpha = \tfrac{c\,\Delta x}{2}(1-C)α>0\alpha>0 smears, =0=0 exact (C=1C{=}1), <0<0 explodes
The Δt\Delta t trapsmaller Δt\Delta t \Rightarrow smaller CC \Rightarrow larger α\alphaTiny step over-smears; aim C1C\to 1, not C0C\to 0
Global adaptive Δt\Delta t = C_\text{target}\Delta x/c_\max (one for all)GPU-ideal, but slow cells get C1C\ll1 → over-diffuse
Keeping it physicallocal/block LTS · TVD flux · Muskingum–Cungeuniform CC, or kill / calibrate α\alpha
Flux limiterQout=min(QM,V/Δt)Q_\text{out} = \min(Q_M,\,V/\Delta t)OPM's CFL-free strategy
Preissmann θ\thetaθ0.5\theta \geq 0.5 → unconditionally stableIndustry standard
AttenuationKinematic: 0%; Diffusive: >0%>0\%Controlled by θdiff\theta_\text{diff}