Resources Numerical accuracy Calculation policy

Sanitary drain sizing by fixture units

Adds up the fixture units each drain carries, finds which run flows into which, and picks the smallest pipe size the plumbing tables allow.

How the precision is kept

The fixture units are whole numbers from the fixture table, summed down the network, so the totals are exact. The capacity of a size is read from the tables of NBR 8160 or the International Plumbing Code 2021 as the file's header cites them, at the listed slope at or below the run's slope. The table, not a formula, sets the result.

Calculations under this policy

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

mep/drainage.js BimCraft 0.8.1 64-bit 4 calculations Reviewed

Tests bin/test-bimcraft-drainage.mjs

Fixture units a size carries

Exact

The fixture units a drain of a nominal size carries at a slope, as a branch or as a building drain.

column = last listed slope ≤ slope + 10⁻⁹; capacity = table[size][column]

The value is a whole number read from the table. The slope comparison adds 1e-9 so a slope computed as exactly a listed value is not missed through rounding; the imperial slopes are written as fractions such as 1/8/12.

  • mep/drainage.js
  • lines 40 to 47
  • BimCraft 0.8.1
/** Fixture units a size carries: a branch from the branch table (else the main table), a building drain the other way round. */
export function capacity(std, nominal, slope, role = 'branch') {
  const T = DRAIN_TABLES[std];
  if (!T) return 0;
  const col = T.slopes.reduce((c, s, i) => (slope + 1e-9 >= s ? i : c), -1);
  const main = col >= 0 ? T.main[nominal]?.[col] || 0 : 0, branch = T.branch[nominal] || 0;
  return role === 'main' ? main || branch : branch || main;
}

Smallest size that carries the load

Exact

The first size, smallest first, that carries the fixture units and is not below the least size allowed.

A comparison of whole-number capacities with the whole-number load; the floor on the size allows 1e-9.

  • mep/drainage.js
  • lines 49 to 52
  • BimCraft 0.8.1
export function smallestSize(std, sizes, units, { slope = 0, role = 'branch', floor = 0 } = {}) {
  for (const s of sizes) if (s.nominal + 1e-9 >= floor && capacity(std, s.nominal, slope, role) >= units) return s;
  return null;
}

Distance from a point to a run

64-bit

The shortest distance from a run's end or a fixture's connector to another run's center line.

t = clamp(((p − a) · d) ÷ |d|², 0, 1); distance = |p − a − d t|

The nearest point on each leg is found with a clamped parameter and the distance is one Math.hypot; a zero-length leg uses t = 0.

  • mep/drainage.js
  • lines 56 to 65
  • BimCraft 0.8.1
/** Distance from p to a polyline. */
function offLine(verts, p) {
  let best = Infinity;
  for (let i = 0; i + 1 < verts.length; i++) {
    const a = verts[i], d = sub(verts[i + 1], a), L2 = dot(d, d);
    const t = L2 ? Math.max(0, Math.min(1, dot(sub(p, a), d) / L2)) : 0;
    best = Math.min(best, Math.hypot(p[0] - a[0] - d[0] * t, p[1] - a[1] - d[1] * t, p[2] - a[2] - d[2] * t));
  }
  return best;
}

Fixture units carried by each run

Exact

Which run each run and fixture drains into, and the total fixture units and least size of every run.

total = own fixtures + Σ totals of the runs flowing in

Connections are decided by distance against the larger of 5 mm and half the outside diameter. The sums are of whole fixture units, so they are exact; a run reached again round a loop adds nothing the second time.

  • mep/drainage.js
  • lines 67 to 103
  • BimCraft 0.8.1
export function drainNetwork(runs, fixtures) {
  const tol = (r) => Math.max(5, (r.od || 0) / 2);
  const out = new Map(runs.map((r) => [r.guid, { uhc: 0, dfu: 0, minDn: 0, minIn: 0, up: [], down: null, fixtures: [] }]));
  // where each run flows: the other run its last point lies on (the nearest)
  for (const r of runs) {
    const end = r.verts[r.verts.length - 1];
    let best = null;
    for (const o of runs) {
      if (o === r) continue;
      const d = offLine(o.verts, end);
      if (d <= Math.max(tol(r), tol(o)) && (!best || d < best.d)) best = { o, d };
    }
    if (best) { out.get(r.guid).down = best.o.guid; out.get(best.o.guid).up.push(r.guid); }
  }
  // each fixture drains into the run passing through its connector
  const own = new Map(runs.map((r) => [r.guid, []]));
  for (const f of fixtures) {
    if (!FIXTURES[f.kind]) continue;
    let best = null;
    for (const r of runs) { const d = offLine(r.verts, f.p); if (d <= Math.max(tol(r), (f.size || 0) / 2) && (!best || d < best.d)) best = { r, d }; }
    if (best) own.get(best.r.guid).push(f);
  }
  // totals: a run's own fixtures and everything flowing into it (a loop is counted once)
  const done = new Set(), busy = new Set(), NONE = { uhc: 0, dfu: 0, minDn: 0, minIn: 0 };
  const total = (g) => {
    const n = out.get(g);
    if (busy.has(g)) return NONE;   // back round a loop: already being counted
    if (done.has(g)) return n;
    busy.add(g);
    for (const f of own.get(g)) { const k = FIXTURES[f.kind]; n.uhc += k.uhc; n.dfu += k.dfu; n.minDn = Math.max(n.minDn, k.dnMin); n.minIn = Math.max(n.minIn, k.trapIn); n.fixtures.push(f.id); }
    for (const u of n.up) { const m = total(u); n.uhc += m.uhc; n.dfu += m.dfu; n.minDn = Math.max(n.minDn, m.minDn); n.minIn = Math.max(n.minIn, m.minIn); }
    busy.delete(g); done.add(g);
    return n;
  };
  for (const r of runs) total(r.guid);
  return out;
}

In detail

Method
Table lookup by nominal size and slope column; network by the nearest run within a tolerance of each run's last point; totals by a walk upstream that counts a loop once.
Number types
64-bit numbers; fixture units and capacities are whole numbers.
Tolerances
A slope matches a table column with a slack of 1e-9. A run or fixture connects within the larger of 5 mm and half the pipe's outside diameter (absolute, world millimeters). A size meets a floor with a slack of 1e-9.
Limits
Capacities are not interpolated between slopes: the next lower listed slope is used. A size missing from the branch table is looked up in the building-drain table, and the other way round.

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