Resources Numerical accuracy Calculation policy

Sanitary sewer design flows and pipe sizes

Works out the peak sanitary flow in each pipe of a gravity network from people and area, checks capacity and velocity, and picks pipe sizes.

How the precision is kept

Flows are 64-bit products of population, per-person flow and a peaking factor, plus infiltration. The peaking factor is an empirical formula (Harmon or Babbitt) held between two limits, so the method sets the accuracy. The gallon to cubic foot factor is typed to 9 significant digits. Capacity, normal depth and part-full area come from hydro.js.

Open item. Write the gallon factor as 231 ÷ 1728 so it is exact to 64-bit.

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.

sewer.js CivilCraft 0.8.1 64-bit 6 calculations Reviewed

Tests bin/test-civilcraft-sewer.mjs

US gallon in cubic feet

64-bit

The factor that turns gallons into cubic feet in feet projects.

1 gal = 0.133680556 ft³

A typed constant with 9 significant digits. A US gallon is 231 cubic inches, so the exact factor is 231 ÷ 1728 = 0.1336805555…; flows in feet projects therefore carry 9 digits from this factor, not 15.

  • sewer.js
  • line 30
  • CivilCraft 0.8.1
export const GAL_FT3 = 0.133680556;          // one US gallon in cubic feet

Peaking factor

64-bit

The ratio of peak to average sanitary flow for a population.

Harmon: 1 + 14 ÷ (4 + √P); Babbitt: 5 ÷ P^0.2; P in thousands; held between pfMin and pfMax

One 64-bit expression; the division is safe because zero population returns pfMax before the formula. The formulas are empirical, so their own fit, not the arithmetic, limits the result.

  • sewer.js
  • lines 57 to 62
  • CivilCraft 0.8.1
/** The peaking factor for a population (people, not thousands), held between pfMin and pfMax. Zero people: pfMax. */
export function peakingFactor(pop, sw) {
  const P = Math.max(0, pop) / 1000;
  const f = !(P > 0) ? sw.pfMax : sw.peak === 'babbitt' ? 5 / Math.pow(P, 0.2) : 1 + 14 / (4 + Math.sqrt(P));
  return Math.min(sw.pfMax, Math.max(sw.pfMin, f));
}

Average flow from people

64-bit

People times per-person daily flow, in project flow units.

m: P × L/day ÷ 1000 ÷ 86400; ft: P × gal/day × 0.133680556 ÷ 86400

Multiplications first, then the divisions by 1000 and 86400, which are exact whole numbers. The feet branch uses the rounded gallon factor.

  • sewer.js
  • lines 64 to 65
  • CivilCraft 0.8.1
/** Per person a day and infiltration per area → project flow units (m³/s | cfs). */
const domFlow = (pop, sw, units) => (units === 'm' ? (pop * sw.perCap) / 1000 / 86400 : (pop * sw.perCap * GAL_FT3) / 86400);

Infiltration flow

64-bit

Tributary area times the infiltration allowance, in project flow units.

m: ha × L/s per ha ÷ 1000; ft: ac × gal/day per ac × 0.133680556 ÷ 86400

One 64-bit product and exact divisors; the feet branch uses the rounded gallon factor.

  • sewer.js
  • line 66
  • CivilCraft 0.8.1
const infFlow = (area, sw, units) => (units === 'm' ? (area * sw.infil) / 1000 : (area * sw.infil * GAL_FT3) / 86400);

Design flow, slope and velocity per pipe

64-bit

For each pipe in flow order: the people and area above it, the peak flow, the slope, the full capacity, the normal depth and the velocity.

S = (inv up − inv down) ÷ length; Q = q_dom × pf + q_inf; ratio = Q ÷ Q_full; V = Q ÷ A(yn)

People and area are added down the network in flow order in 64-bit. The slope divides by the plan length from Math.hypot and is zero when the length is zero. The over-capacity test adds an absolute 1e-12 to the limit. Percentages in the problem messages are rounded for the text only.

  • sewer.js
  • lines 74 to 105
  • CivilCraft 0.8.1
export function sewerFlows(net, units = 'ft') {
  const sw = cleanSewer(net?.sewer, units), problems = [];
  const structs = new Map((net?.structures || []).map((s) => { const l = cleanSewLoad(s.sew); return [s.id, { P: l?.pop || 0, area: l?.area || 0 }]; }));
  const pipes = new Map();
  const ord = flowOrder(net || { pipes: [] });
  if (!ord) { problems.push({ id: null, kind: 'network', msg: t('hy.loop') }); return { structs, pipes, problems, sewer: sw }; }
  const byId = new Map((net?.structures || []).map((x) => [x.id, x]));
  for (const p of ord) {
    const up = structs.get(p.from), dn = structs.get(p.to);
    if (!up || !dn) continue;
    const a = byId.get(p.from), b = byId.get(p.to), len = Math.hypot(b.x - a.x, b.y - a.y);
    const S = fin(p.invUp) && fin(p.invDn) && len > 0 ? (p.invUp - p.invDn) / len : 0;
    const pf = peakingFactor(up.P, sw), qDom = domFlow(up.P, sw, units) * pf, qInf = infFlow(up.area, sw, units), Q = qDom + qInf;
    const D = diaOf(p, units), full = manningFull(D, S, sw.n, units);
    const r = { P: up.P, area: up.area, pf, qDom, qInf, Q, S, len, cap: full.Q, vFull: full.V, ratio: full.Q > 0 ? Q / full.Q : null, yn: null, V: null };
    const name = p.name || p.id;
    if (!(S > 0)) problems.push({ id: p.id, kind: 'pipe', msg: t('san.p.flat', { name }) });
    else if (Q > 0) {
      r.yn = normalDepth(Q, D, S, sw.n, units);
      if (r.yn === null) problems.push({ id: p.id, kind: 'pipe', msg: t('san.p.over', { name, pct: Math.round(r.ratio * 100), max: Math.round(sw.maxFull * 100) }) });
      else {
        r.V = Q / partFull(r.yn, D).A;
        if (Q > sw.maxFull * full.Q + 1e-12) problems.push({ id: p.id, kind: 'pipe', msg: t('san.p.over', { name, pct: Math.round(r.ratio * 100), max: Math.round(sw.maxFull * 100) }) });
        if (r.V < sw.vMin) problems.push({ id: p.id, kind: 'pipe', msg: t('san.p.slow', { name, v: r.V.toFixed(2), min: sw.vMin }) });
        if (r.V > sw.vMax) problems.push({ id: p.id, kind: 'pipe', msg: t('san.p.fast', { name, v: r.V.toFixed(2), max: sw.vMax }) });
      }
    }
    pipes.set(p.id, r);
    dn.P += up.P; dn.area += up.area;
  }
  return { structs, pipes, problems, sewer: sw };
}

Pipe size for the design flow

64-bit

The smallest listed pipe size whose allowed share of full capacity carries the design flow.

first size with Q_full(size) × maxFull ≥ Q

A 64-bit comparison per size with no tolerance; the result is one of the listed nominal sizes, or the largest with ok false.

  • sewer.js
  • lines 107 to 116
  • CivilCraft 0.8.1
export function sizeSewers(net, flows, units = 'ft', { sizes = SEWER_SIZES[units === 'm' ? 'm' : 'ft'] } = {}) {
  const per = DIA_UNIT[units === 'm' ? 'm' : 'ft'].perUnit, sw = flows.sewer || cleanSewer(net.sewer, units), out = [];
  for (const p of net.pipes) {
    const f = flows.pipes.get(p.id);
    if (!f || !(f.S > 0)) { out.push({ id: p.id, dia: p.dia, ok: false }); continue; }
    const d = sizes.find((x) => manningFull(x / per, f.S, sw.n, units).Q * sw.maxFull >= f.Q);
    out.push(d === undefined ? { id: p.id, dia: sizes[sizes.length - 1], ok: false } : { id: p.id, dia: d, ok: true });
  }
  return out;
}

In detail

Method
Harmon 1 + 14 ÷ (4 + √P) or Babbitt 5 ÷ P^0.2 with P in thousands; Q = P × per-person flow × peaking factor + infiltration; Manning full-pipe capacity and normal depth from hydro.js.
Number types
64-bit numbers.
Tolerances
A pipe is over its limit when Q > maxFull × capacity + 1e-12 (absolute, in flow units).
Limits
The peaking formulas are empirical. One US gallon is taken as 0.133680556 ft³; the exact value is 231 ÷ 1728 = 0.13368055… (repeating 5). The file header calls the default design values placeholders, not a standard.

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