Resources Numerical accuracy Calculation policy

River model: stations, cut sections and reach lengths

Builds the river model a water surface profile is computed from: river stations, cross-sections cut from the ground, bank positions, reach lengths, flows along the reach and the unit constants.

How the precision is kept

Stations, offsets and lengths are 64-bit closed-form results from the plan geometry. Two things are rounded before they are stored: the river station of a cut section and each section point's offset are rounded to 6 decimals, and a guessed boundary slope is rounded to 3 significant digits. The US-unit gravity and Manning constant are typed rounded values.

Open item. Derive the US-unit gravity and Manning constant from the exact SI values (9.80665 ÷ 0.3048 and 1 ÷ 0.3048^(1/3)) instead of the typed 32.174 and 1.486, and store cut-section stations and offsets unrounded.

Calculations under this policy

7 calculations in 2 source files. 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.

river.js CivilCraft 0.8.1 64-bit 6 calculations Reviewed

Tests bin/test-civilcraft-river.mjs, bin/test-civilcraft-river3.mjs

Gravity and Manning constant per unit system

64-bit

The gravity and the Manning equation constant every river calculation uses.

m: k = 1, g = 9.80665; ft: k = 1.486, g = 32.174

9.80665 m/s² is the defined standard value. The two US-unit values are typed rounded numbers: 9.80665 ÷ 0.3048 is 32.17404855…, and 1 ÷ 0.3048^(1/3) is 1.48592…, so results in feet carry these constants to 5 and 4 significant digits.

  • river.js
  • line 51
  • CivilCraft 0.8.1
export const UNIT_CONST = { m: { k: 1, g: 9.80665 }, ft: { k: 1.486, g: 32.174 } };

River station of a point

64-bit

The station of a point, measured along the centerline from its downstream end, and its offset to the side.

rs = L − d

One 64-bit subtraction of the distance along the centerline from its total length; both come from terrain-section.js.

  • river.js
  • lines 199 to 202
  • CivilCraft 0.8.1
export function riverStation(centreline, p) {
  const L = lineLength(centreline), at = locateOn(centreline, p);
  return at ? { rs: L - at.d, off: at.off } : null;
}

Cross-section from a cut line

64-bit

Turns a line drawn across the river into a cross-section: station, ground points by offset, and first bank positions.

rs = round₆(L − d_crossing); o = round₆(offset); banks = lowest point ± bank distance, clipped to the section

The station and every point offset are rounded to 6 decimals with toFixed(6) before they are stored in the model; elevations are stored unrounded. At large station values 6 decimals is fewer than 15 significant digits.

  • river.js
  • lines 204 to 234
  • CivilCraft 0.8.1
export function sectionFromLine(surface, centreline, cut, opts = {}) {
  if (!Array.isArray(cut) || cut.length < 2) return null;
  const units = opts.units === 'ft' ? 'ft' : 'm', d = { ...RIVER_DEFAULTS(units), ...opts };
  const L = lineLength(centreline);
  let ln = cut.map((p) => ({ x: p.x, y: p.y }));
  let hit = crossing(ln, centreline);
  if (!hit) return null;
  // left to right looking downstream: the first point must be to the LEFT of the centreline
  const first = locateOn(centreline, ln[0]);
  if (first && first.off > 0) { ln = ln.reverse(); hit = crossing(ln, centreline); }
  const sec = cutSection(surface, ln, { origin: hit.da });
  if (!sec.runs.length) return null;
  const pts = flatten(sec.runs.length > 1 ? [sec.runs.reduce((a, b) => (b.length > a.length ? b : a))] : sec.runs);
  const lo = pts[0].o, hi = pts[pts.length - 1].o, low = lowest([pts]);
  const banks = [Math.max(lo, low.o - d.bank), Math.min(hi, low.o + d.bank)];
  return {
    id: opts.id || 'xs',
    rs: Number((L - hit.db).toFixed(6)),
    name: '',
    line: ln,
    lineAt0: hit.da,
    pts: pts.map((p) => ({ o: Number(p.o.toFixed(6)), z: p.z })),
    banks,
    n: [{ o: lo, n: d.nOver }, { o: banks[0], n: d.n }, { o: banks[1], n: d.nOver }],
    reach: { lob: null, ch: null, rob: null },
    cc: d.cc,
    ce: d.ce,
    alpha: null,
    gaps: sec.runs.length > 1,
  };
}

Sections at a spacing along the centerline

64-bit

Cuts square cross-sections every set distance along the centerline and at both ends.

dₖ = k × spacing while k × spacing < L − 1e-9; rs = round₆(L − dₖ)

Distances are k × spacing, a single multiplication per section, so they do not accumulate error. The stored station is rounded to 6 decimals. The 1e-9 end test is an absolute length.

  • river.js
  • lines 236 to 249
  • CivilCraft 0.8.1
export function sectionsAlong(surface, centreline, opts = {}) {
  const units = opts.units === 'ft' ? 'ft' : 'm', d = { ...RIVER_DEFAULTS(units), ...opts };
  const L = lineLength(centreline);
  if (!(L > 0) || !(d.spacing > 0)) return [];
  const ds = [];
  for (let k = 0; k * d.spacing < L - 1e-9 && ds.length < MAX_SECTIONS - 1; k++) ds.push(k * d.spacing);
  ds.push(L);
  const out = [];
  for (const at of ds) {
    const s = sectionFromLine(surface, centreline, squareAcross(centreline, at, d.left, d.right), { ...d, id: 'xs' + (out.length + 1) });
    if (s) { s.rs = Number((L - at).toFixed(6)); out.push(s); }
  }
  return out;
}

Reach lengths between sections

64-bit

The channel and overbank distances from each section to the next one downstream.

channel = rsᵢ − rsᵢ₋₁; overbank = √(Δx² + Δy²) between the middles of the overbank parts of the two cut lines

The channel length is a difference of two stations, which were rounded to 6 decimals when the sections were cut. Overbank lengths use Math.hypot on plan coordinates and fall back to the channel length when a section has no cut line.

  • river.js
  • lines 263 to 276
  • CivilCraft 0.8.1
export function setReachLengths(model) {
  const s = ordered(model);
  for (let i = 0; i < s.length; i++) {
    const me = s[i], dn = s[i - 1];
    if (!dn) { me.reach = { lob: 0, ch: 0, rob: 0 }; continue; }
    const ch = me.rs - dn.rs;
    const side = (k) => {
      const a = overbankMid(me, k), b = overbankMid(dn, k);
      return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : ch;
    };
    me.reach = { lob: side('lob'), ch, rob: side('rob') };
  }
  return model;
}

Flow at a station

64-bit

The flow that applies at a station, from the profile's flow and its flow changes.

Picks the nearest flow change at or upstream of the station; the comparison allows 1e-9 (absolute) so a change placed at a section's own station applies there. The flow value is copied, not computed.

  • river.js
  • lines 278 to 283
  • CivilCraft 0.8.1
export function flowAt(profile, rs) {
  let q = profile.q, best = Infinity;
  // the nearest change at or upstream of rs holds
  for (const c of profile.changes || []) if (c.rs >= rs - 1e-9 && c.rs < best) { best = c.rs; q = c.q; }
  return q;
}
river-ui.js CivilCraft 0.8.1 64-bit 1 calculation Reviewed

Tests None found by the review

First guess of the boundary slope

64-bit

The slope offered for a normal-depth boundary when one is first chosen, from the lowest points of the end sections.

S = (z_min upstream − z_min downstream) ÷ (rs upstream − rs downstream), to 3 significant digits; 0.001 when S ≤ 1e-5

The slope is rounded to 3 significant digits with toPrecision(3) and that rounded number is stored in the model as the boundary slope. It is a starting value that the person can replace.

  • river-ui.js
  • lines 81 to 87
  • CivilCraft 0.8.1
function slopeGuess(model) {
  const s = ordered(model);
  if (s.length < 2) return 0.001;
  const z = (x) => Math.min(...x.pts.map((p) => p.z));
  const v = (z(s[s.length - 1]) - z(s[0])) / (s[s.length - 1].rs - s[0].rs);
  return v > 1e-5 ? Number(v.toPrecision(3)) : 0.001;
}

In detail

Method
Station = centerline length minus the distance along it; overbank reach length = plan distance between the middles of the overbank parts of two cut lines; channel length = station difference.
Number types
64-bit numbers.
Tolerances
Section spacing loop stops 1e-9 before the end of the centerline; a flow change applies from 1e-9 downstream of its station; numbers read from a file must be finite and below 1e9 in size. Both 1e-9 values are absolute.
Limits
Stations and offsets of cut sections are kept to 6 decimals. Gravity is 9.80665 in meters (exact by definition) and 32.174 in feet (rounded); the Manning constant in feet is 1.486 (rounded).

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach