Resources Numerical accuracy Calculation policy

Gravity pipe network: slopes, cover and automatic inverts

Works out each pipe's length, slope and least cover, each structure's rim and sump, and sets inverts that keep the minimum slope, cover and drop.

How the precision is kept

Lengths and slopes are closed form in 64-bit. Cover is the least of samples taken along the pipe, about every 2 units and at most 200 intervals, so a low spot between samples can be missed. Automatic inverts are rounded down to 2 decimals before they are stored, by design, so a pipe is never shallower than computed.

Calculations under this policy

3 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.

pipes.js CivilCraft 0.8.1 64-bit 3 calculations Reviewed

Tests bin/test-civilcraft.mjs

Pipe slope, cover and structure levels

64-bit

Length, slope and least cover of each pipe; rim and sump of each structure; and the list of rule problems.

slope = (inv_up − inv_dn) ÷ L; crown(t) = inv_up + (inv_dn − inv_up)·t + D; cover = min over samples (ground − crown); sump = lowest invert − sump depth

Sample positions are k ÷ n along the pipe from whole numbers. Diameters in inches or millimeters are divided by the exact 12 or 1000. Comparisons with the rules allow an absolute 1e-9. The cover and invert values in the problem messages are rounded to 2 decimals for the text only.

  • pipes.js
  • lines 36 to 98
  • CivilCraft 0.8.1
export function analyzeNetwork(net, surface, units = 'ft') {
  const problems = [], structs = new Map(), pipes = new Map();
  const D = (dia) => (Number(dia) || 0) / DIA_UNIT[units === 'm' ? 'm' : 'ft'].perUnit;
  const R = net.rules || {};
  for (const s of net.structures) {
    const g = surface ? surface.elevationAt(s.x, s.y, 1e-6) : null;
    const rim = Number.isFinite(s.rim) ? s.rim : g;
    structs.set(s.id, { s, rim, rimFrom: Number.isFinite(s.rim) ? 'set' : g !== null ? 'surface' : null, ins: [], outs: [], sump: null });
    if (rim === null) problems.push({ id: s.id, kind: 'structure', msg: t('pipes.p.offsurf', { name: s.name }) });
  }
  for (const p of net.pipes) {
    const a = structs.get(p.from), b = structs.get(p.to);
    if (!a || !b || p.from === p.to) { problems.push({ id: p.id, kind: 'pipe', msg: t('pipes.p.unconnected', { name: p.name }) }); continue; }
    a.outs.push(p.id); b.ins.push(p.id);
    const len = Math.hypot(b.s.x - a.s.x, b.s.y - a.s.y);
    const info = { p, len, slope: null, cover: null, coverAt: null, ground: [] };
    pipes.set(p.id, info);
    if (!Number.isFinite(p.invUp) || !Number.isFinite(p.invDn)) { problems.push({ id: p.id, kind: 'pipe', msg: t('pipes.p.noinv', { name: p.name }) }); continue; }
    info.slope = len > 0 ? (p.invUp - p.invDn) / len : 0;
    if (info.slope <= 0) problems.push({ id: p.id, kind: 'pipe', msg: t(info.slope < 0 ? 'pipes.p.uphill' : 'pipes.p.flat', { name: p.name, slope: pctTxt(info.slope) }) });
    else if (R.minSlope > 0 && info.slope < R.minSlope - 1e-9) problems.push({ id: p.id, kind: 'pipe', msg: t('pipes.p.flatter', { name: p.name, slope: pctTxt(info.slope), min: pctTxt(R.minSlope) }) });
    // cover along the pipe
    const n = Math.max(2, Math.min(200, Math.ceil(len / 2)));
    let minC = Infinity, at = null, off = 0;
    for (let k = 0; k <= n; k++) {
      const t = k / n, x = a.s.x + (b.s.x - a.s.x) * t, y = a.s.y + (b.s.y - a.s.y) * t;
      let g = surface ? surface.elevationAt(x, y, 1e-6) : null;
      if (k === 0 && a.rimFrom === 'set') g = a.rim;
      if (k === n && b.rimFrom === 'set') g = b.rim;
      const crown = p.invUp + (p.invDn - p.invUp) * t + D(p.dia);
      info.ground.push({ t, g, crown });
      if (g === null) { off++; continue; }
      if (g - crown < minC) { minC = g - crown; at = t; }
    }
    if (Number.isFinite(minC)) { info.cover = minC; info.coverAt = at; }
    if (off) problems.push({ id: p.id, kind: 'pipe', msg: t('pipes.p.partoff', { name: p.name }) });
    if (Number.isFinite(minC) && R.minCover > 0 && minC < R.minCover - 1e-9) problems.push({ id: p.id, kind: 'pipe', msg: at <= 0 ? t('pipes.p.cover.up', { name: p.name, cover: minC.toFixed(2), min: R.minCover }) : at >= 1 ? t('pipes.p.cover.down', { name: p.name, cover: minC.toFixed(2), min: R.minCover }) : t('pipes.p.cover.along', { name: p.name, cover: minC.toFixed(2), at: Math.round(at * len), min: R.minCover }) });
  }
  // structures: drops, sumps, splits
  for (const [id, st] of structs) {
    const inv = [];
    for (const pid of st.ins) { const p = pipes.get(pid)?.p; if (Number.isFinite(p?.invDn)) inv.push(p.invDn); }
    for (const pid of st.outs) { const p = pipes.get(pid)?.p; if (Number.isFinite(p?.invUp)) inv.push(p.invUp); }
    st.sump = inv.length ? Math.min(...inv) - (Number(st.s.sumpDepth) || 0) : null;
    if (st.outs.length > 1) problems.push({ id, kind: 'structure', msg: t('pipes.s.outs', { name: st.s.name, n: st.outs.length }) });
    for (const o of st.outs) {
      const po = pipes.get(o)?.p;
      if (!Number.isFinite(po?.invUp)) continue;
      for (const i of st.ins) {
        const pi = pipes.get(i)?.p;
        if (!Number.isFinite(pi?.invDn)) continue;
        if (po.invUp > pi.invDn - (R.minDrop || 0) + 1e-9) problems.push({ id, kind: 'structure', msg: po.invUp > pi.invDn ? t('pipes.s.drop.above', { name: st.s.name, out: po.name, outInv: po.invUp.toFixed(2), in: pi.name, inInv: pi.invDn.toFixed(2) }) : t('pipes.s.drop.less', { name: st.s.name, out: po.name, outInv: po.invUp.toFixed(2), drop: R.minDrop, in: pi.name, inInv: pi.invDn.toFixed(2) }) });
      }
    }
    if (st.rim !== null) for (const pid of [...st.ins, ...st.outs]) {
      const p = pipes.get(pid)?.p; if (!p) continue;
      const inv = st.ins.includes(pid) ? p.invDn : p.invUp;
      if (Number.isFinite(inv) && inv + D(p.dia) > st.rim + 1e-9) problems.push({ id, kind: 'structure', msg: t('pipes.s.crown', { pipe: p.name, name: st.s.name }) });
    }
  }
  if (hasCycle(net)) problems.push({ id: null, kind: 'network', msg: t('pipes.loop') });
  return { structs, pipes, problems };
}

Automatic inverts

64-bit

Sets each pipe's upstream and downstream inverts from the ground, the minimum cover, slope and drop.

up = min(ground − cover − D, lowest incoming invert − drop); dn = min(up − s·L, min over samples (up + (ground − cover − D − up) ÷ t))

The downstream invert is the lowest line through the upstream invert that stays under ground − cover − D at every sample. Both inverts are then rounded down to 0.01 and stored; if rounding made the pipe flatter than the minimum slope the downstream invert is set again from the rounded upstream one.

  • pipes.js
  • lines 132 to 166
  • CivilCraft 0.8.1
export function autoInverts(net, surface, units = 'ft', { only = null } = {}) {
  const order = flowOrder(net);
  if (!order) return { pipes: net.pipes, problems: [t('pipes.loop.inv')] };
  const R = net.rules || {}, c = Number(R.minCover) || 0, s = Number(R.minSlope) || 0, drop = Number(R.minDrop) || 0;
  const per = DIA_UNIT[units === 'm' ? 'm' : 'ft'].perUnit;
  const byId = new Map(net.structures.map((x) => [x.id, x]));
  const ground = (x, y, st) => (st && Number.isFinite(st.rim) ? st.rim : surface ? surface.elevationAt(x, y, 1e-6) : null);
  const res = new Map(net.pipes.map((p) => [p.id, { ...p }]));
  const problems = [];
  for (const p0 of order) {
    const p = res.get(p0.id);
    if (only && !only.has(p.id)) continue;
    const a = byId.get(p.from), b = byId.get(p.to);
    if (!a || !b) continue;
    const Dm = (Number(p.dia) || 0) / per, len = Math.hypot(b.x - a.x, b.y - a.y);
    const ga = ground(a.x, a.y, a);
    const incoming = net.pipes.filter((q) => q.to === p.from).map((q) => res.get(q.id).invDn).filter(Number.isFinite);
    let up = ga === null ? null : ga - c - Dm;
    if (incoming.length) { const v = Math.min(...incoming) - drop; up = up === null ? v : Math.min(up, v); }
    if (up === null) { problems.push(t('pipes.noground', { name: p.name, at: a.name })); continue; }
    // the lowest downstream invert that keeps the slope ≥ minimum and the cover ≥ minimum all along
    let dn = up - s * len;
    const n = Math.max(2, Math.min(200, Math.ceil(len / 2)));
    for (let k = 1; k <= n; k++) {
      const t = k / n, x = a.x + (b.x - a.x) * t, y = a.y + (b.y - a.y) * t;
      const g = k === n ? ground(b.x, b.y, b) : ground(x, y, null);
      if (g === null) continue;
      const lim = up + (g - c - Dm - up) / t;   // invert line under (ground − cover − D) at t
      if (lim < dn) dn = lim;
    }
    p.invUp = round2(up); p.invDn = round2(Math.min(dn, up - s * len));
    if (p.invDn > p.invUp - s * len + 1e-9) p.invDn = round2(p.invUp - s * len);
  }
  return { pipes: net.pipes.map((p) => res.get(p.id)), problems };
}

Rounding an invert down

64-bit

Rounds a computed invert down to two decimals.

⌊v × 100 + 1e-7⌋ ÷ 100

The stored invert is the computed value rounded down to 0.01, a deliberate design rounding. The 1e-7 added before the floor keeps a value that is a hundredth already, but held a hair under it in 64-bit, from dropping a whole hundredth.

  • pipes.js
  • line 168
  • CivilCraft 0.8.1
const round2 = (v) => Math.floor(v * 100 + 1e-7) / 100;   // round down: never shallower than computed

In detail

Method
Slope = (invert up − invert down) ÷ plan length; cover = ground − (invert + inside diameter) at samples along the pipe; automatic inverts from minimum cover, slope and drop.
Number types
64-bit numbers.
Tolerances
Rule checks allow 1e-9 (absolute). Samples: n = max(2, min(200, ⌈length ÷ 2⌉)). Stored automatic inverts: floor to 0.01 with a 1e-7 nudge in hundredths.
Limits
Diameters are inside diameters; wall thickness is not counted. Ground between samples is not checked. Pipe length is the plan distance between structure centers. The default rules are placeholders, as the file header states.

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