Resources Numerical accuracy Calculation policy

Storm hydrographs routed through the pipe network

Carries each catchment's runoff over time down the pipes and reports the peak flow, its time and the attenuation at every pipe and structure.

How the precision is kept

Hydrographs are plain arrays of 64-bit numbers on a fixed time step (1 minute by default, at most 4000 steps). The method sets the accuracy: a uniform time-area runoff curve, Muskingum-Cunge routing with the wave celerity fixed at 1.5 times the flow velocity, and linear interpolation between time steps. Peaks are the largest sampled values, so a peak between steps is not resolved. The CSV export rounds minutes to 3 decimals and flows to 6.

Open item. Use 43560 ÷ 43200 for the acre-inch per hour factor instead of 1 in the catchment hydrograph.

Calculations under this policy

6 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

storm-route.js CivilCraft 0.8.1 64-bit 6 calculations Reviewed

Tests bin/test-civilcraft-storm-route.mjs

Catchment runoff hydrograph

64-bit

The runoff from one catchment at each time step for a constant storm.

q(t) = k·C·A·(depth(t) − depth(t − w)) ÷ w; depth(τ) = i·clamp(τ, 0, duration); k = 1 ÷ 360 (m), 1 (ft)

Times are n × dt, a single multiplication, so they do not drift. The unit factor for feet projects is 1; one acre-inch per hour is 43560 ÷ 43200 = 1.00833… ft³/s, so flows in feet projects are about 0.8 % lower than the exact conversion gives. The metric factor 1 ÷ 360 is exact.

  • storm-route.js
  • lines 36 to 45
  • CivilCraft 0.8.1
/** One catchment's runoff hydrograph at times 0, dt, 2dt…: C·A converted to flow units. Flow per (area × intensity): ft³/s per ac·in/h (≈1), m³/s per ha·mm/h (1/360). */
export function catchmentHydrograph({ CA, tc, i, duration, dt, steps, units }) {
  const k = units === 'm' ? 1 / 360 : 1;
  const q = new Array(steps).fill(0), w = Math.max(tc, dt);
  for (let n = 0; n < steps; n++) {
    const t1 = n * dt;
    q[n] = k * CA * (rainDepth(i, duration, t1) - rainDepth(i, duration, t1 - w)) / w;
  }
  return q;
}

Muskingum coefficients

64-bit

The three weights of the routing equation for a reach.

d = K(1 − X) + Δt ÷ 2; c₀ = (−KX + Δt ÷ 2) ÷ d; c₁ = (KX + Δt ÷ 2) ÷ d; c₂ = (K(1 − X) − Δt ÷ 2) ÷ d

Three 64-bit quotients that sum to 1 up to rounding. A negative c₀ or c₂ sets the lag flag instead of being used.

  • storm-route.js
  • lines 47 to 52
  • CivilCraft 0.8.1
/** Muskingum coefficients for K, X, dt (minutes). `lag` = the step is too coarse to attenuate: C2 would go negative. */
export function muskingum(K, X, dt) {
  const d = K * (1 - X) + 0.5 * dt;
  const c0 = (-K * X + 0.5 * dt) / d, c1 = (K * X + 0.5 * dt) / d, c2 = (K * (1 - X) - 0.5 * dt) / d;
  return { c0, c1, c2, lag: c0 < 0 || c2 < 0 };
}

Routing one reach

64-bit

The outflow hydrograph of a pipe from its inflow hydrograph.

O₂ = max(0, c₀·I₂ + c₁·I₁ + c₂·O₁) on a step of max(Δt, 2KX); values between steps by linear interpolation

A 64-bit recurrence, one step from the last. When the routing step is longer than the time step the inflow is read by linear interpolation and the outflow is interpolated back, which smooths peaks slightly. The outflow is floored at zero.

  • storm-route.js
  • lines 54 to 75
  • CivilCraft 0.8.1
/** Route one series through a reach. Muskingum needs 2·K·X ≤ step ≤ 2·K·(1 − X) for its three coefficients to stay positive, so the reach is run on a grid of
 *  max(dt, 2·K·X) minutes (the inflow read off by linear interpolation, the outflow interpolated back to dt); if even that is longer than 2·K·(1 − X) the reach is too short
 *  to attenuate on this grid and the flow passes after a plain lag of K minutes (linear interpolation, peak and volume kept). `step` = the grid used. */
export function routeReach(inflow, K, X, dt) {
  const n = inflow.length, out = new Array(n).fill(0);
  const d = Math.max(dt, 2 * K * X), co = muskingum(K, X, d);
  const at = (a, u) => { if (u <= 0) return a[0]; const j = Math.floor(u), f = u - j; return j >= a.length - 1 ? a[a.length - 1] : a[j] * (1 - f) + a[j + 1] * f; };
  if (co.lag) {
    const s = K / dt;
    for (let j = 0; j < n; j++) { const f = j - s, i = Math.floor(f), b = f - i; out[j] = f < 0 ? 0 : (inflow[i] ?? 0) * (1 - b) + (inflow[i + 1] ?? inflow[i] ?? 0) * b; }
    return { out, lag: true, step: dt };
  }
  const m = Math.ceil(((n - 1) * dt) / d) + 1, g = new Array(m);
  let qi = at(inflow, 0), prevIn = qi, prevOut = qi;
  g[0] = qi;
  for (let k = 1; k < m; k++) {
    qi = at(inflow, (k * d) / dt);
    prevOut = Math.max(0, co.c0 * qi + co.c1 * prevIn + co.c2 * prevOut); prevIn = qi; g[k] = prevOut;
  }
  for (let j = 0; j < n; j++) out[j] = at(g, (j * dt) / d);
  return { out, lag: false, step: d };
}

Travel time and weighting of each pipe

64-bit

The routing constants K and X of each pipe from its length, slope and rational peak flow.

c = 1.5 × max(V, 0.1); K = L ÷ c ÷ 60; X = clamp(½(1 − Q ÷ (T·S·c·L)), 0, ½)

64-bit; pipe length from Math.hypot of the structure coordinates. Pipes with no fall or no length are skipped before the divisions. The celerity ratio 1.5 and the floors on velocity and top width are fixed choices in the code.

  • storm-route.js
  • lines 97 to 103
  • CivilCraft 0.8.1
const len = Math.hypot(b.x - a.x, b.y - a.y);
if (!(f.S > 0) || !(len > 0)) { problems.push({ id: p.id, kind: 'pipe', msg: t('rt.flat', { name: p.name }) }); continue; }
const full = manningFull(D, f.S, h.n, units), yn = normalDepth(f.Q, D, f.S, h.n, units);
const A = yn === null ? full.A : partFull(yn, D).A, V = A > 0 ? f.Q / A : full.V, Tw = yn === null ? D : Math.max(partFull(yn, D).T, 0.1 * D);
const c = CELERITY_RATIO * Math.max(V, 0.1), K = len / c / 60;      // minutes
const X = Math.min(0.5, Math.max(0, 0.5 * (1 - f.Q / (Tw * f.S * c * len))));
reach.set(p.id, { len, S: f.S, cap: full.Q, K, X, c, V });

Summing flows and reading peaks

64-bit

Adds the hydrographs arriving at each structure and reads each pipe's peak in, peak out, attenuation and share of capacity.

att = max(0, 1 − peak out ÷ peak in); ratio = peak out ÷ capacity; over when peak out > capacity × (1 + 1e-9)

Hydrographs are added step by step in flow order. A peak is the largest value on the time grid and its time is the step index × dt, so both are resolved to the time step. The over-capacity tolerance is relative.

  • storm-route.js
  • lines 116 to 127
  • CivilCraft 0.8.1
const pipes = new Map();
for (const p of ord) {
  const r = reach.get(p.id), inflow = into.get(p.from);
  if (!r) continue;
  const { out, lag } = routeReach(inflow, r.K, r.X, dt);
  const a = peakOf(inflow, dt), b = peakOf(out, dt), dn = into.get(p.to);
  for (let j = 0; j < steps; j++) dn[j] += out[j];
  const over = b.peak > r.cap * (1 + 1e-9);
  pipes.set(p.id, { peakIn: a.peak, peakOut: b.peak, tPeakOut: b.tPeak, lag, att: a.peak > 0 ? Math.max(0, 1 - b.peak / a.peak) : 0, cap: r.cap, ratio: r.cap > 0 ? b.peak / r.cap : 0, K: r.K, X: r.X, c: r.c, V: r.V, over });
  if (over) problems.push({ id: p.id, kind: 'pipe', msg: t('rt.over', { name: p.name }) });
}
const structs = new Map(); for (const [id, q] of into) structs.set(id, { ...peakOf(q, dt), q });

Hydrograph as CSV

64-bit

Writes a structure's hydrograph as minutes and flow.

minutes to 3 decimals; flow to 6 decimals

The exported values are rounded with toFixed(3) and toFixed(6) and trailing zeros are dropped.

  • storm-route.js
  • lines 132 to 137
  • CivilCraft 0.8.1
/** The outlet's hydrograph (or any structure's) as CSV text: minutes, flow. */
export function hydrographCsv(r, id, units = 'ft') {
  const s = r.structs.get(id);
  if (!s) return '';
  return ['minutes,flow ' + (units === 'm' ? 'm3/s' : 'cfs'), ...r.t.map((m, j) => `${+m.toFixed(3)},${+s.q[j].toFixed(6)}`)].join('\r\n') + '\r\n';
}

In detail

Method
Runoff: rain averaged over the last tc minutes times C·A. Routing: Muskingum-Cunge with K = L ÷ c and X = ½(1 − Q ÷ (T·S·c·L)) held in 0 to ½, as the file header states; a reach too short for the step passes the flow after a plain lag of K.
Number types
64-bit numbers in plain arrays.
Tolerances
A pipe is flagged over capacity when the routed peak exceeds capacity × (1 + 1e-9). Velocity is floored at 0.1 and top width at 0.1 × diameter when the routing constants are set.
Limits
No surcharge, backwater, storage in structures or flooding. The celerity ratio 1.5 is a fixed choice. The metric flow factor is 1 ÷ 360 (exact); the US factor is taken as 1, where the exact value of 1 acre-inch per hour is 1.00833… ft³/s.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach