Implementation¶
Code organisation¶
flowchart LR
main["main.c<br/>options, output"] --> sim["simulation.c<br/>owns grid and buffers"]
sim --> int["integrator.c<br/>Euler, SSP-RK2, SSP-RK3"]
sim --> cases["cases.c<br/>topography, initial data,<br/>boundaries, exact solutions"]
int --> scheme["scheme.c<br/>flux balance L(U)"]
scheme --> kin["kinetic.c<br/>Perthame-Simeoni"]
scheme --> rus["rusanov.c<br/>hydrostatic reconstruction"]
kin --> quad["quadrature.c<br/>tanh-sinh"]
main --> out["output.c<br/>snapshots, errors"]
| File | Responsibility |
|---|---|
grid.c |
uniform grid, cell centres, state arrays \(h\), \(hu\), \(Z\) |
cases.c |
test cases: bottom, initial state, boundary conditions, exact solutions |
scheme.c |
the spatial operator: one pass over the interfaces, flux balance per cell |
kinetic.c |
interface fluxes of the kinetic scheme with reflections |
rusanov.c |
interface fluxes of the reference scheme |
quadrature.c |
tanh-sinh quadrature for \(II^q_3\) |
integrator.c |
explicit time integrators in Shu-Osher form |
simulation.c |
one run: creation, time stepping, destruction |
output.c |
snapshot files, error norms |
main.c |
command-line options, summary |
Schemes, integrators and cases are each a table of entries looked up by name, so adding one does not touch the rest of the code (see Contributing).
Grid¶
The domain \([x_{\min}, x_{\max}]\) is divided into \(n\) cells of width \(\Delta x = (x_{\max} - x_{\min})/n\). Interior cells are \(i = 1, \dots, n\), with centres \(x_i = x_{\min} + (i - \tfrac12)\Delta x\); cells \(0\) and \(n + 1\) are ghost cells filled by the boundary conditions.
The bottom is stored as cell averages, \(Z_i = \frac{1}{\Delta x}\int_{C_i} Z(x)\,dx\), as in Section 3.1 of the paper. Each topography is defined by its primitive, so the averages are exact.
Sign convention for the bottom jump¶
The jump is always the one seen from the cell that owns the flux:
A positive jump is a step up for particles leaving cell \(i\). This is the convention the proof of the lake-at-rest property uses (\(\Delta Z_{i-1/2} = h_i - h_{i-1}\) at rest).
Interface fluxes¶
Both schemes implement the same interface: given the states of the two cells on each
side of an interface, return the flux seen by the left cell (\(F^-\)) and the flux
seen by the right cell (\(F^+\)). scheme_flux_balance visits each of the \(n + 1\)
interfaces once and forms
For the kinetic scheme, only \(F^-\) is coded. \(F^+\) is obtained by the mirror symmetry \(x \to -x\): the right cell becomes the "own" cell of a mirrored problem in which velocities change sign. The mass flux changes sign under the mirror and the momentum flux (a second moment in \(\xi\)) does not:
This halves the code and guarantees that left and right fluxes are computed consistently.
The integral II3q¶
\(II^q_3\) has no elementary primitive. When the bottom steps down towards the own cell (\(K^+ < 0\)), the radicand \((\omega + Fr)^2 + K^+\) vanishes exactly at the upper bound \(\beta = -\sqrt{(K^+)_-} - Fr\), and \(s(\omega) = \sqrt{1 - \omega^2}\) vanishes at the lower bound \(-1\). The integrand therefore has square-root singularities at both ends.
The paper suggests a repeated midpoint rule. Because of the singularities it converges only like \(N^{-3/2}\), and since the lake-at-rest property relies on exact integrals, the quadrature error shows up directly as spurious currents:
| Quadrature of \(II^q_3\) | max \(\lvert hu \rvert\) after 1.5 s on the lake at rest |
|---|---|
| midpoint, \(N = 100\) | \(1.8 \times 10^{-3}\) |
| midpoint, \(N = 1000\) | \(5.2 \times 10^{-5}\) |
| midpoint, \(N = 10\,000\) | \(\approx 2 \times 10^{-6}\) |
| tanh-sinh, 113 nodes | \(1.4 \times 10^{-14}\) |
The midpoint figures were measured on the 2013 code in double precision with π corrected (1000 cells); the last line is the current code.
The code uses a tanh-sinh (double exponential) rule, which converges exponentially even with algebraic endpoint singularities. The integrand receives the distances to both endpoints, computed without cancellation, and the radicand is expanded around \(\beta\) so that its exact zero is preserved. Against a 30-digit reference the relative error is below \(2 \times 10^{-15}\).
A sign in the paper¶
The closed form of \(II^h_3\) printed in the paper ends with \(-\tfrac12 Fr\,\beta\sqrt{1-\beta^2}\). Integrating \(\sqrt{1 - \omega^2}\) over \([-1, \beta]\) gives \(+\tfrac12 Fr\,\beta\sqrt{1-\beta^2}\), which is what the code uses and what matches the flux definition (3.5) numerically.
Dry cells¶
Cells with \(h \le 10^{-10}\) are treated as dry: zero velocity, zero Froude number, and no contribution to the fluxes. Both schemes keep \(h \ge 0\) under their CFL condition; after each stage the code only removes round-off level negative values and the momentum of cells that just dried out.
Time integration¶
The semi-discrete system \(dU/dt = L(U)\) is advanced by explicit integrators written in Shu-Osher form,
| Integrator | \((a_k, b_k)\) | Order |
|---|---|---|
euler |
\((0, 1)\) | 1 |
ssp-rk2 |
\((0, 1),\ (\tfrac12, \tfrac12)\) | 2 |
ssp-rk3 |
\((0, 1),\ (\tfrac34, \tfrac14),\ (\tfrac13, \tfrac23)\) | 3 |
With \(a_k, b_k \ge 0\) and \(a_k + b_k = 1\), every stage is a convex combination of forward Euler steps (strong-stability-preserving methods with coefficient 1). Positivity, the lake at rest and conservation therefore carry over from Euler under the same CFL condition. The time step comes from the CFL condition (3.9) with a default CFL number of 0.95.
Forward Euler is the scheme of the paper and remains the default: with a first-order space discretisation it is also the most accurate at large CFL numbers (see Validation).
The Rusanov reference scheme¶
The reference scheme combines the Rusanov (local Lax-Friedrichs) flux with the hydrostatic reconstruction of Audusse et al. (2004). At each interface the heights are reconstructed above the highest of the two bottoms,
the Rusanov flux \(\mathcal{F}\) is evaluated on the reconstructed states, and each side adds the pressure correction
It is positive and preserves the lake at rest, dry areas included. Its time step uses the wave speed \(\lvert u\rvert + \sqrt{gh}\) with a default CFL number of 0.9.
Boundary conditions¶
| Kind | Ghost cells | Used by |
|---|---|---|
| free (transmissive) | copy of the neighbouring cell | lakes at rest, dam breaks |
| wall | copy, with \(hu\) reversed | sloshing |
| periodic | copy of the opposite end | periodic-pulse |
| inflow / outflow | left: imposed discharge \(q\), height copied; right: imposed height \(h_{\text{out}}\) while the outflow is subcritical, copy once it is supercritical | flows over the bump |
The outflow condition follows the characteristics: at a subcritical outlet one wave enters the domain and the downstream height must be imposed; at a supercritical outlet none does and imposing it would be wrong. With a plain copy instead, any subcritical steady state is compatible with the boundary and the transcritical flow never develops: water piles up downstream.
The bottom of the ghost cells follows the same rule, so that a lake at rest stays at rest across the boundary.
History¶
The 2013 code had the same scheme but several bugs, found and fixed in 2026:
a value of π truncated to 3.1415 (it broke the lake-at-rest property by mixing with
acos), an error norm multiplied by \(\Delta x\), a Rusanov scheme without topography
that produced NaN on dry beds, three different cell-position formulas, and periodic
boundaries one step behind. The pull requests on GitHub give the details and the
measurements.