Resources Numerical accuracy Calculation policy

Water supply and duct network sizing

Works out the flow, velocity, friction and pressure in each pipe or duct of a network, the size each needs, and the pressure left at every outlet.

How the precision is kept

The arithmetic is 64-bit and SI inside. The accuracy is set by the methods: the empirical Hazen–Williams formula with its printed 4-digit coefficient, rounded fitting loss coefficients, fluid properties with 3 to 4 digits, an iterated Colebrook friction factor, and a bisection for the duct diameter. Network nodes are formed by merging points within a tolerance of tens of millimeters. Defaults are labeled as examples in the code.

Open item. Write the network CSV at full precision instead of toFixed with 1 to 3 decimals.

Calculations under this policy

31 calculations in 4 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.

addons/piping/lib/pipenet.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests tools/addons-test/browser-mep.mjs

Pipe runs converted for the network

64-bit

Turns drawn pipe runs into polylines in millimeters with an elevation at every corner and an inside diameter per size.

mm = drawing units × k × 25.4; z = z₀ − slope × (plan distance) × k × 25.4

k is inches per drawing unit and 25.4 is exact. The plan distance to each corner is a running sum of Math.hypot lengths. Inside diameters are the table's inches times 25.4.

  • addons/piping/lib/pipenet.js
  • lines 22 to 45
  • CadCraft 0.8.2
export function engineRuns(pipes) {
  const runs = [], attrs = {}, ends = [], joints = [];
  for (const { id, data: d } of pipes) {
    if (!d?.pts || d.pts.length < 2 || !d.segs?.length) continue;
    const m = mmDraw(d), z0 = (Number(d.z) || 0) * m;
    const cum = [0]; for (let i = 1; i < d.pts.length; i++) cum.push(cum[i - 1] + Math.hypot(d.pts[i][0] - d.pts[i - 1][0], d.pts[i][1] - d.pts[i - 1][1]));
    const slope = Number(d.slope) || 0;   // a ratio: the run falls slope × (length along it), as pipe.js zAt
    const zAt = (i) => z0 - slope * cum[i] * m;
    let g = 0, from = 0;
    for (let i = 1; i <= d.segs.length; i++) {
      if (i < d.segs.length && sameSize(d.segs[i], d.segs[from])) continue;
      const verts = d.pts.slice(from, i + 1).map((p, j) => [p[0] * m, p[1] * m, zAt(from + j)]);
      const runId = `${id}.${g++}`, size = d.segs[from].size;
      const sizes = sizesFor(d.mat, d.cls).map((s) => pipeSize(d.mat, d.cls, s)).filter(Boolean).map((p) => ({ key: p.size, idMm: p.id * MM_IN }));
      const cur = pipeSize(d.mat, d.cls, size);
      runs.push({ id: runId, verts, valves: [] });
      attrs[runId] = { mat: d.mat, idMm: cur ? cur.id * MM_IN : 0, sizes, label: size };
      if (from === 0) ends.push({ run: runId, p: verts[0], tag: d.tag || String(id) });
      if (i === d.segs.length) ends.push({ run: runId, p: verts[verts.length - 1], tag: d.tag || String(id) });
      from = i;
    }
  }
  return { runs, attrs, ends, joints };
}

Source matched to a run end

64-bit

Finds the run end nearest the point picked as the supply source.

accepted when plan distance ≤ tol (60 mm by default)

The distance is measured in plan only, in millimeters, and the tolerance is absolute. The source then takes the coordinates of that run end.

  • addons/piping/lib/pipenet.js
  • lines 52 to 54
  • CadCraft 0.8.2
let near = null;
for (const e of ends) { const d = Math.hypot(e.p[0] - source.x * m, e.p[1] - source.y * m); if (!near || d < near.d) near = { d, e }; }
if (!near || near.d > tolMm) return { ok: false, why: 'source' };

Result rows in display units

64-bit

Converts each segment's flow to liters per second and its loss to kilopascals.

L/s = m³/s × 1000; kPa = Pa ÷ 1000

One multiplication or division by 1000 per value; the other values are passed on as computed.

  • addons/piping/lib/pipenet.js
  • line 88
  • CadCraft 0.8.2
const rows = result.segs.map((s) => ({ run: s.run, lengthM: s.length, fu: s.fu, lps: s.flow * 1000, size: s.size, V: s.V, rate: s.rate, dpKpa: s.dp / 1000, status: s.status, beyond: s.beyond }));

Network CSV export

64-bit

The per-run results written as CSV.

Values are rounded with toFixed on the way out: length 2 decimals (m), flow 3 (L/s), velocity 2 (m/s), friction rate 1 (Pa/m) and loss 2 (kPa).

  • addons/piping/lib/pipenet.js
  • line 94
  • CadCraft 0.8.2
export const csvRows = (a) => ['run,length_m,fixture_units,flow_lps,size,velocity_ms,rate_pa_m,loss_kpa,status', ...a.rows.map((r) => [r.run, r.lengthM.toFixed(2), r.fu, r.lps.toFixed(3), r.size, r.V.toFixed(2), r.rate.toFixed(1), r.dpKpa.toFixed(2), r.status].join(','))].join('\n') + '\n';
addons/piping/lib/mep/flow.js CadCraft 0.8.2 64-bit 11 calculations Reviewed

Tests tools/bimcraft-test/eg-mep-test.mjs

Physical constants for water

64-bit

Gravity, water properties and unit factors used by the supply calculation.

g is the standard 9.80665 and the gallon factor is the exact 0.003785411784 m³ divided by 60. Water density 998.2 kg/m³ and viscosity 1.004e-6 m²/s are 4-digit property values for 20 °C. KPA_PSI is rounded to 7 digits (6.894757).

  • addons/piping/lib/mep/flow.js
  • lines 17 to 20
  • CadCraft 0.8.2
export const G = 9.80665;
export const WATER = { rho: 998.2, nu: 1.004e-6 };            // 20 °C — a labelled default
export const GPM = 0.003785411784 / 60;                        // m³/s per US gpm
export const KPA_PSI = 6.894757;

Hazen–Williams friction loss

64-bit

Pressure lost to friction per meter of water pipe.

Δp/L = 10.67 × Q^1.852 ÷ (C^1.852 × D^4.871) × ρ g

The SI form with the printed coefficient 10.67 (4 digits) and exponents 1.852 and 4.871, evaluated in one expression. The formula is empirical, so its own fit, not the arithmetic, limits the result. A flow or diameter of zero or less returns 0.

  • addons/piping/lib/mep/flow.js
  • lines 24 to 28
  • CadCraft 0.8.2
/** Hazen–Williams friction loss, Pa/m, for Q (m³/s) in a pipe of inside diameter D (m). */
export function hazenWilliams(Q, D, C = 130) {
  if (!(Q > 0) || !(D > 0)) return 0;
  return (10.67 * Q ** 1.852 / (C ** 1.852 * D ** 4.871)) * WATER.rho * G;
}

Darcy–Weisbach friction loss for water

64-bit

Pressure lost to friction per meter of water pipe from the friction factor.

V = Q ÷ (πD² ÷ 4); Re = V D ÷ ν; Δp/L = (f ÷ D) × ρ V² ÷ 2

The friction factor comes from the iterated Colebrook equation in sizing.js; the default roughness is 1.5e-6 m. analyzePipes in this file does not call it: supply pipes are sized with Hazen–Williams.

  • addons/piping/lib/mep/flow.js
  • lines 29 to 34
  • CadCraft 0.8.2
/** Darcy–Weisbach friction loss for water, Pa/m (eps in m). */
export function darcyWater(Q, D, eps = 0.0000015) {
  if (!(Q > 0) || !(D > 0)) return 0;
  const V = Q / (Math.PI * D * D / 4), Re = V * D / WATER.nu;
  return (colebrook(Re, eps / D) / D) * WATER.rho * V * V / 2;
}

Velocity in a pipe

64-bit

The mean velocity of a flow in a round pipe.

V = Q ÷ (π D² ÷ 4)

One 64-bit expression with Math.PI. A diameter of zero or less returns 0 instead of dividing.

  • addons/piping/lib/mep/flow.js
  • line 35
  • CadCraft 0.8.2
export const velocity = (Q, D) => (D > 0 ? Q / (Math.PI * D * D / 4) : 0);

Demand from fixture units

64-bit

The design flow for a number of fixture units, read from a demand curve.

s = ln(y₁ ÷ y₀) ÷ ln(x₁ ÷ x₀); q = y₀ × (fu ÷ x₀)^s

Between curve points the demand is interpolated on log–log axes. Below the first point it is in proportion to the fixture units; above the last it continues the last exponent and the result is flagged as beyond the curve. The default curve is labeled in the code as an example, not a code table.

  • addons/piping/lib/mep/flow.js
  • lines 52 to 63
  • CadCraft 0.8.2
/** Demand (m³/s) for `fu` fixture units: log–log interpolation; below the first point in proportion; above the last by the last slope (`beyond` = true). */
export function demandFor(fu, curve = DEFAULT_CURVE) {
  const c = cleanCurve(curve) || DEFAULT_CURVE, k = c.unit === 'lps' ? 0.001 : GPM, P = c.points;
  if (!(fu > 0)) return { q: 0, beyond: false };
  if (fu <= P[0][0]) return { q: P[0][1] * k * (fu / P[0][0]), beyond: false };
  let i = P.findIndex((p) => p[0] >= fu);
  const beyond = i < 0;
  if (beyond) i = P.length - 1;
  const [x0, y0] = P[i - 1], [x1, y1] = P[i];
  const s = Math.log(y1 / y0) / Math.log(x1 / x0);
  return { q: y0 * (fu / x0) ** s * k, beyond };
}

Loss in fittings

64-bit

Pressure lost in a segment's bends, valves and the junction it leaves.

Δp = (Σ K) × ρ V² ÷ 2; bend K = K₉₀ × min(1.5, angle ÷ 90)

The K values are added first and multiplied once by ρV² ÷ 2. Bends of 15° or less add nothing. The K tables are rounded published figures, labeled as defaults.

  • addons/piping/lib/mep/flow.js
  • lines 72 to 79
  • CadCraft 0.8.2
/** Pressure (Pa) lost in the fittings of one segment: its bends, valves and the junction it leaves (a tee branch when it is not the biggest child). */
function fittingLoss(seg, V, rho, K, branchOf, round) {
  let k = 0;
  for (const a of seg.bends || []) if (a > 15) k += bendK(round ? (K.elbow90_round ?? K.elbow90) : K.elbow90, a);
  for (const v of seg.valves || []) k += K[v] ?? 0;
  if (branchOf) k += branchOf === 'branch' ? K.tee_branch : K.tee_run;
  return k * rho * V * V / 2;
}

Flow, size and loss of one pipe segment

64-bit

Velocity, friction rate, the smallest size that passes, and the total pressure loss of a segment.

Δp = rate × L + fittings + ρ g Δz

The needed size is the first table size whose velocity is within the limit + 1e-9 and, when a rate limit is set, whose friction rate is within it + 1e-12. Friction, fittings and static head are added in that order; a rise counts as a loss.

  • addons/piping/lib/mep/flow.js
  • lines 110 to 115
  • CadCraft 0.8.2
const Q = flow.get(id), C = o.C?.[a.mat] ?? HW_C[a.mat] ?? HW_C.default, D = (a.idMm || 0) / 1000;
const V = velocity(Q, D), rate = hazenWilliams(Q, D, C);
let need = null;
for (const z of a.sizes || []) { const d = z.idMm / 1000; if (velocity(Q, d) <= o.vmax + 1e-9 && (!o.rateMax || hazenWilliams(Q, d, C) / 1000 <= o.rateMax + 1e-12)) { need = z; break; } }
const fit = D > 0 ? fittingLoss(s, V, WATER.rho, o.K, jn.get(id) || null, true) : 0;
const dp = rate * s.length + fit + WATER.rho * G * s.dz;     // friction + fittings + static (up is a cost)

Pressure lost to each outlet

64-bit

The pressure drop from the source to an outlet and what is left of the supply pressure.

Δp = Σ segments on the path; left = available − Δp ÷ 1000

Segment losses are added in order from the source and divided by 1000 for kilopascals. The outlet is marked short when what is left is below the typed minimum, compared without a tolerance.

  • addons/piping/lib/mep/flow.js
  • lines 122 to 126
  • CadCraft 0.8.2
const path = pathTo(tree, l.node);
if (!path.length && l.node !== source) { out.push({ ...l, reached: false }); continue; }
let dp = 0; for (const id of path) dp += segs.get(id).dp;
const left = o.availKpa != null ? o.availKpa - dp / 1000 : null;
out.push({ ...l, reached: true, path, dpKpa: dp / 1000, leftKpa: left, short: o.minKpa != null && left != null ? left < o.minKpa : false });

Flow, size and loss of one duct segment

64-bit

Velocity, friction rate, equivalent diameter, needed diameter and pressure loss of a duct segment.

by velocity: D = √(4Q ÷ (π v_max)); Δp = rate × L + fittings

Flow is passed to sectionLoss in L/s and millimeters and comes back in SI. A segment is marked small when its equivalent diameter is under 98 % of the needed one, and fast above 102 % of the velocity limit or 110 % of the target rate. Static head is left out.

  • addons/piping/lib/mep/flow.js
  • lines 148 to 156
  • CadCraft 0.8.2
const w = (a.wMm || 0) / 1000, h = (a.shape === 'round' ? a.wMm : a.hMm || 0) / 1000;
let V = 0, rate = 0, de = 0, area = 0;
if (w > 0 && h > 0 && Q > 0) { const L = sectionLoss({ q: Q * 1000, shape: a.shape || 'rect', w: w * 1000, h: h * 1000, u: 'mm', mat }); V = L.V; rate = L.rate; de = L.de / 1000; area = section(a.shape || 'rect', w, h).area; }
const fit = V > 0 ? fittingLoss(s, V, AIR.rho, { ...o.C, elbow90: o.C.elbow90, tee_run: o.C.tee_run, tee_branch: o.C.tee_branch }, jn.get(id) || null, a.shape === 'round') : 0;
// needed size: the equal-friction round diameter, or the diameter at the velocity limit — in round equivalents
let needDe = null;
if (Q > 0) needDe = o.method === 'velocity' ? Math.sqrt(4 * Q / (Math.PI * o.vmax)) : diameterAtRate(Q, o.rate, ROUGHNESS[mat] ?? ROUGHNESS.galv);
const aspect = a.shape === 'round' || !(w && h) ? 1 : Math.max(w, h) / Math.min(w, h);
const dp = rate * s.length + fit + 0;     // ducts: static head cancels in air at building heights (stack effect is ignored, said in the help)

Critical path, damper pressures and fan pressure

64-bit

The duct path with the largest loss, the pressure each other terminal's damper must take up, and the fan's required pressure.

damper = Δp_critical − Δp_terminal; fan = Δp_critical + equipment loss

Path losses are sums of segment losses in order. The damper pressure is a difference of two such sums, so for a terminal almost as far as the critical one it is a small difference of large numbers.

  • addons/piping/lib/mep/flow.js
  • lines 167 to 169
  • CadCraft 0.8.2
const crit = res.filter((r) => r.reached).reduce((m, r) => (!m || r.dp > m.dp ? r : m), null);
for (const r of res) if (r.reached && crit) r.damperPa = crit.dp - r.dp;    // pressure the balancing damper must take up
return { tree, segs: [...segs.values()], terminals: res, critical: crit, fanPa: crit ? crit.dp + (o.equipDp || 0) : 0, cycles: tree.cycles, unreached: tree.unreached, opts: o };

Duct diameter for a friction rate

64-bit

The round diameter that gives a target friction rate at a flow.

Bisection on ln D between 10 mm and 5 m with a fixed 100 halvings and no tolerance test; the upper bound is returned. It returns null when a 5 m duct still exceeds the rate.

  • addons/piping/lib/mep/flow.js
  • lines 171 to 177
  • CadCraft 0.8.2
/** The round diameter (m) giving `rate` Pa/m at Q — sizing.js bisection. */
function diameterAtRate(Q, rate, eps) {
  let lo = Math.log(0.01), hi = Math.log(5);
  if (roundLoss(Q, 5, eps).rate > rate) return null;
  for (let i = 0; i < 100; i++) { const m = (lo + hi) / 2; if (roundLoss(Q, Math.exp(m), eps).rate > rate) lo = m; else hi = m; }
  return Math.exp(hi);
}
addons/piping/lib/mep/network.js CadCraft 0.8.2 64-bit 6 calculations Reviewed

Tests tools/bimcraft-test/eg-mep-test.mjs

Foot of a point on a run

64-bit

The nearest point of a 3D polyline to a point, as a distance along the run and a distance away.

u = clamp((p − a)·v ÷ |v|², 0, 1); s = Σ earlier lengths + |v| u

The parameter is clamped to [0, 1] and a zero-length segment gives 0. The distance along the run adds the earlier segment lengths in order.

  • addons/piping/lib/mep/network.js
  • lines 22 to 34
  • CadCraft 0.8.2
/** The foot of p on the polyline: { s (arc length mm), d (distance mm) }. */
function project(verts, p) {
  let best = { s: 0, d: Infinity }, acc = 0;
  for (let i = 0; i + 1 < verts.length; i++) {
    const a = verts[i], v = sub(verts[i + 1], a), L2 = dot(v, v), L = Math.sqrt(L2);
    let u = L2 > 0 ? dot(sub(p, a), v) / L2 : 0;
    u = Math.max(0, Math.min(1, u));
    const q = [a[0] + v[0] * u, a[1] + v[1] * u, (a[2] || 0) + v[2] * u], d = dist(p, q);
    if (d < best.d) best = { s: acc + L * u, d };
    acc += L;
  }
  return best;
}

Point at a distance along a run

64-bit

The 3D point a given distance along a polyline.

Linear interpolation inside the segment that contains the distance, with the fraction clamped to [0, 1]. A distance past the end returns the last point.

  • addons/piping/lib/mep/network.js
  • lines 35 to 44
  • CadCraft 0.8.2
/** The point at arc length s of the polyline. */
function at(verts, s) {
  let acc = 0;
  for (let i = 0; i + 1 < verts.length; i++) {
    const L = dist(verts[i], verts[i + 1]);
    if (s <= acc + L || i + 2 === verts.length) { const u = L > 0 ? Math.max(0, Math.min(1, (s - acc) / L)) : 0; const a = verts[i], b = verts[i + 1]; return [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u, (a[2] || 0) + ((b[2] || 0) - (a[2] || 0)) * u]; }
    acc += L;
  }
  return verts[0].slice();
}

Bend angle at a corner

64-bit

The angle a run turns through at a corner, in degrees.

angle = acos(u·v ÷ (|u| |v|)) × 180 ÷ π

The cosine is clamped to [−1, 1] before acos. Near a straight run the cosine is close to 1, where acos resolves the angle only to about 1.5e-8 radian; the loss calculation ignores bends of 15° or less.

  • addons/piping/lib/mep/network.js
  • lines 45 to 49
  • CadCraft 0.8.2
const turn = (a, b, c) => {
  const u = sub(b, a), v = sub(c, b), lu = Math.hypot(...u), lv = Math.hypot(...v);
  if (!lu || !lv) return 0;
  return Math.acos(Math.max(-1, Math.min(1, dot(u, v) / (lu * lv)))) * 180 / Math.PI;
};

Cut points and nodes

64-bit

Cuts runs where another run ends on them or a fixture connects, and merges nearby cut points into nodes.

cut when distance ≤ tol; merged when distance ≤ tol

The tolerance is absolute: 40 mm by default, 60 mm from the piping add-on. Every pair of cut points is compared, and a merged node takes the position of one of its points, so a node can sit up to the tolerance from the other points merged into it.

  • addons/piping/lib/mep/network.js
  • lines 59 to 67
  • CadCraft 0.8.2
const hit = (xi, p, tag) => { const pr = project(runInfo[xi].verts, p); if (pr.d <= tol) { cuts[xi].push({ s: pr.s, p: at(runInfo[xi].verts, pr.s), tag }); return true; } return false; };
// a run's end lying on another run's line cuts the other run (a tee)
runInfo.forEach((x, i) => { for (const e of [x.verts[0], x.verts[x.verts.length - 1]]) runInfo.forEach((y, j) => { if (i !== j) hit(j, e, null); }); });
for (const pt of points) runInfo.forEach((x, i) => hit(i, pt.p, pt.key));
// nodes: cluster every cut point within tol (a small union-find)
const all = [];
cuts.forEach((c, xi) => c.forEach((k) => all.push({ xi, k })));
const parent = all.map((_, i) => i), find = (i) => { while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; } return i; };
for (let i = 0; i < all.length; i++) for (let j = i + 1; j < all.length; j++) if (dist(all[i].k.p, all[j].k.p) <= tol) parent[find(j)] = find(i);

Segment length, rise and bends

64-bit

The length in meters, the rise and the bend angles of each segment between two nodes.

length = Σ |vᵢ₊₁ − vᵢ| ÷ 1000; Δz = (z_b − z_a) ÷ 1000

Length is the sum of 3D distances through the cut points and the corners between them. A corner closer than half the tolerance (along the run) to either cut point is left out, with its bend.

  • addons/piping/lib/mep/network.js
  • lines 83 to 93
  • CadCraft 0.8.2
for (let k = 0; k + 1 < uniq.length; k++) {
  const A = uniq[k], B = uniq[k + 1];
  if (A.node === B.node) continue;
  const verts = [A.k.p];
  const bends = [];
  for (const cr of corners) if (cr.s > A.k.s + tol * 0.5 && cr.s < B.k.s - tol * 0.5) { verts.push(cr.b); bends.push(turn(cr.a, cr.b, cr.c)); }
  verts.push(B.k.p);
  let len = 0; for (let i = 1; i < verts.length; i++) len += dist(verts[i - 1], verts[i]);
  const valves = (x.r.valves || []).filter((v) => v.s >= A.k.s && v.s < B.k.s).map((v) => v.kind);
  segs.push({ id: x.r.id + ':' + k, run: x.r.id, a: A.node, b: B.node, verts, length: len / 1000, dz: ((B.k.p[2] || 0) - (A.k.p[2] || 0)) / 1000, bends, valves });
}

Loads summed toward the source

64-bit

The total load (fixture units or flow) carried by each segment.

sum(segment) = load at its far node + Σ sums of its children

Segments are visited from the far ends toward the source, so each total is built from its children's totals by plain addition.

  • addons/piping/lib/mep/network.js
  • lines 121 to 130
  • CadCraft 0.8.2
export function accumulate(tree, loadAt) {
  const sum = new Map();
  for (let i = tree.order.length - 1; i >= 0; i--) {
    const s = tree.segOf.get(tree.order[i]);
    let v = loadAt.get(s.b) || 0;
    for (const c of tree.children.get(s.b) || []) v += sum.get(c) || 0;
    sum.set(s.id, v);
  }
  return sum;
}
addons/piping/lib/mep/sizing.js CadCraft 0.8.2 64-bit 10 calculations Reviewed

Tests tools/bimcraft-test/eg-mep-test.mjs

Air properties and unit factors

64-bit

Standard-air properties, duct roughness values and the factors for cfm, L/s, inches of water, feet per minute, inches and millimeters.

The cubic foot (0.028316846592 m³), 0.00508 m/s per ft/min, 0.0254 and 0.001 are exact. IN_WG = 249.08891 Pa is the conventional inch of water (9.80665 × 25.4). Air density 1.204 kg/m³ and viscosity 1.51e-5 m²/s are handbook values with 3 to 4 digits.

  • addons/piping/lib/mep/sizing.js
  • lines 23 to 30
  • CadCraft 0.8.2
export const AIR = { rho: 1.204, nu: 1.51e-5 };          // kg/m³, m²/s — ASHRAE standard air
export const ROUGHNESS = { smooth: 0.03e-3, galv: 0.09e-3, board: 0.9e-3, flex: 3.0e-3 };   // m
export const CFM = 0.028316846592 / 60;                  // m³/s per cfm (1 ft³ = 0.028316846592 m³)
export const LPS = 0.001;                                 // m³/s per L/s
export const IN_WG = 249.08891;                           // Pa per inch of water (conventional, 4 °C)
export const PER_100FT = IN_WG / 30.48;                   // Pa/m per in. w.g./100 ft  (≈ 8.1722)
export const FPM = 0.00508;                               // m/s per ft/min
export const IN = 0.0254, MM = 0.001;

Colebrook friction factor

64-bit

The Darcy friction factor for a Reynolds number and relative roughness.

1 ÷ √f = −2 log₁₀(ε ÷ (3.7 D) + 2.51 ÷ (Re √f))

Fixed-point iteration from the Swamee–Jain estimate, at most 60 passes, stopping when f changes by less than 1e-14 (absolute). Below Re 2300 it returns 64 ÷ Re, so the factor jumps at 2300.

  • addons/piping/lib/mep/sizing.js
  • lines 35 to 47
  • CadCraft 0.8.2
/** Colebrook friction factor (iterated from the Swamee–Jain estimate; laminar 64/Re below Re 2300). */
export function colebrook(Re, rel) {
  if (!(Re > 0)) return NaN;
  if (Re < 2300) return 64 / Re;
  let f = 0.25 / Math.log10(rel / 3.7 + 5.74 / Re ** 0.9) ** 2;
  for (let i = 0; i < 60; i++) {
    const x = -2 * Math.log10(rel / 3.7 + 2.51 / (Re * Math.sqrt(f)));
    const g = 1 / (x * x);
    if (Math.abs(g - f) < 1e-14) { f = g; break; }
    f = g;
  }
  return f;
}

Altshul–Tsal friction factor

64-bit

An explicit friction factor kept as a cross-check of the iterated one.

f′ = 0.11 (ε ÷ D + 68 ÷ Re)^0.25; f = f′ if f′ ≥ 0.018, else 0.85 f′ + 0.0028

One expression with a fourth root. The file's comment calls it a cross-check, and the sizing functions in this file do not call it.

  • addons/piping/lib/mep/sizing.js
  • lines 48 to 52
  • CadCraft 0.8.2
/** Altshul–Tsal (ASHRAE's explicit form) — a cross-check, not used for sizing. */
export function altshulTsal(Re, rel) {
  const f1 = 0.11 * (rel + 68 / Re) ** 0.25;
  return f1 >= 0.018 ? f1 : 0.85 * f1 + 0.0028;
}

Friction loss of a round duct

64-bit

Friction rate, velocity, Reynolds number and friction factor for an airflow in a round duct.

V = Q ÷ (πD² ÷ 4); Re = V D ÷ ν; rate = (f ÷ D) × ρ V² ÷ 2

Area, velocity, Reynolds number and friction factor are computed in sequence in 64-bit. There is no guard for a zero diameter; callers pass positive sizes.

  • addons/piping/lib/mep/sizing.js
  • lines 54 to 58
  • CadCraft 0.8.2
/** Friction loss of a round duct: Pa/m for airflow Q (m³/s) in diameter D (m), roughness eps (m). Also V, Re, f. */
export function roundLoss(Q, D, eps = ROUGHNESS.galv) {
  const A = (Math.PI * D * D) / 4, V = Q / A, Re = reynolds(V, D), f = colebrook(Re, eps / D);
  return { rate: (f / D) * (AIR.rho * V * V) / 2, V, Re, f };
}

Round diameter for a friction rate

64-bit

The round duct diameter that gives a target friction rate at an airflow.

Bisection on ln D between 10 mm and 5 m with a fixed 200 halvings and no tolerance test; the upper bound is returned. NaN is returned for a flow or rate of zero or less, or when a 5 m duct still exceeds the rate.

  • addons/piping/lib/mep/sizing.js
  • lines 60 to 70
  • CadCraft 0.8.2
/** The round diameter (m) giving friction rate `rate` (Pa/m) at Q (m³/s): bisection on log D, 10 mm … 5 m. */
export function diameterFor(Q, rate, eps = ROUGHNESS.galv) {
  if (!(Q > 0) || !(rate > 0)) return NaN;
  let lo = Math.log(0.01), hi = Math.log(5);
  if (roundLoss(Q, 5, eps).rate > rate) return NaN;   // more than a 5 m duct carries at that rate
  for (let i = 0; i < 200; i++) {
    const mid = (lo + hi) / 2;
    if (roundLoss(Q, Math.exp(mid), eps).rate > rate) lo = mid; else hi = mid;
  }
  return Math.exp(hi);
}

Round equivalent of a rectangular duct

64-bit

The round diameter with the same friction and flow as a rectangle a × b.

De = 1.30 (a b)^0.625 ÷ (a + b)^0.25

Huebscher's formula with its printed coefficient and exponents, in one 64-bit expression.

  • addons/piping/lib/mep/sizing.js
  • lines 72 to 73
  • CadCraft 0.8.2
/** Huebscher's circular equivalent of a rectangle a × b (any one length unit). */
export const deRect = (a, b) => (1.3 * (a * b) ** 0.625) / (a + b) ** 0.25;

Flat-oval area, perimeter and round equivalent

64-bit

The area and perimeter of a flat-oval duct and its equivalent round diameter.

A = πb² ÷ 4 + b(a − b); P = πb + 2(a − b); De = 1.55 A^0.625 ÷ P^0.25

Closed forms with Math.PI and the printed coefficient 1.55. When the two axes differ by 1e-12 or less (absolute) the minor axis is returned as the diameter.

  • addons/piping/lib/mep/sizing.js
  • lines 75 to 77
  • CadCraft 0.8.2
export const ovalArea = (a, b) => (Math.PI * b * b) / 4 + b * (a - b);
export const ovalPerimeter = (a, b) => Math.PI * b + 2 * (a - b);
export const deOval = (a, b) => (a - b <= 1e-12 ? b : (1.55 * ovalArea(a, b) ** 0.625) / ovalPerimeter(a, b) ** 0.25);

Area and perimeter of a section

64-bit

The cross-section area, perimeter and equivalent diameter of a rectangular, round or oval size.

rect: A = w h, P = 2(w + h); round: A = πw² ÷ 4, P = πw

Closed forms in the unit of the size given; no conversion happens here.

  • addons/piping/lib/mep/sizing.js
  • lines 79 to 84
  • CadCraft 0.8.2
/** Cross-section area and perimeter of a size (same length unit as the size): rect w×h, round d (= w), oval w×h. */
export function section(shape, w, h) {
  if (shape === 'round') return { area: (Math.PI * w * w) / 4, perimeter: Math.PI * w, de: w };
  if (shape === 'oval') return { area: ovalArea(w, h), perimeter: ovalPerimeter(w, h), de: deOval(w, h) };
  return { area: w * h, perimeter: 2 * (w + h), de: deRect(w, h) };
}

Duct size by equal friction

64-bit

The stocked duct size for an airflow and a target friction rate, with its velocity and actual rate.

Q = q × (cfm or L/s factor); size = first stocked size ≥ exact diameter

The exact diameter from the bisection is rounded up to a stocked size, accepting a size up to 1e-9 (inches or millimeters) under the requirement. Rectangular and oval widths grow in steps of 2 in or 50 mm. The rate reported is recomputed for the chosen size.

  • addons/piping/lib/mep/sizing.js
  • lines 92 to 122
  • CadCraft 0.8.2
/**
 * Size a section by equal friction. q = airflow in cfm ('in') or L/s ('mm'); rate in in. w.g./100 ft or Pa/m; shape and,
 * for rect/oval, the fixed side `h` (height, or the oval's minor axis) in in or mm. → { shape, w, h, d (exact round
 * diameter), de, V (fpm or m/s), rate (actual, typed units), aspect, note } in the typed units; null when it cannot be sized.
 */
export function sizeSection({ q, rate, shape = 'round', h = null, u = 'in', mat = 'galv', maxAspect = 4 }) {
  const Q = q * (u === 'in' ? CFM : LPS), R = rate * (u === 'in' ? PER_100FT : 1), unit = u === 'in' ? IN : MM;
  const eps = ROUGHNESS[mat] ?? ROUGHNESS.galv;
  const D = diameterFor(Q, R, eps);
  if (!Number.isFinite(D)) return null;
  const dReq = D / unit;
  let w, hh = h, de;
  if (shape === 'round') {
    const list = u === 'in' ? ROUND_IN : ROUND_MM;
    w = list.find((x) => x >= dReq - 1e-9);
    if (!w) return null;
    hh = w; de = w;
  } else {
    const { step, min } = STEP[u];
    if (!(hh > 0)) return null;
    const deOf = shape === 'oval' ? (a) => deOval(a, hh) : (a) => deRect(a, hh);
    w = shape === 'oval' ? Math.max(hh, min) : min;
    while (deOf(w) < dReq - 1e-9) { w += step; if (w > 400 * step) return null; }
    de = deOf(w);
  }
  const s = section(shape, w, hh);
  const V = Q / (s.area * unit * unit);
  const actual = roundLoss(Q, de * unit, eps).rate / (u === 'in' ? PER_100FT : 1);
  const aspect = shape === 'round' ? 1 : Math.max(w, hh) / Math.min(w, hh);
  return { shape, w, h: hh, d: dReq, de, V: u === 'in' ? V / FPM : V, rate: actual, aspect, note: aspect > maxAspect ? 'aspect' : null };
}

Friction rate and velocity of a drawn section

64-bit

The friction rate and velocity of a duct section as drawn, in the units typed.

The friction rate uses the section's equivalent round diameter; the velocity uses its true area. Results are converted back with the same factors used on the way in.

  • addons/piping/lib/mep/sizing.js
  • lines 124 to 131
  • CadCraft 0.8.2
/** Friction rate and velocity of a section as drawn (typed units): rate in in. w.g./100 ft or Pa/m, V in fpm or m/s. */
export function sectionLoss({ q, shape, w, h, u = 'in', mat = 'galv' }) {
  const Q = q * (u === 'in' ? CFM : LPS), unit = u === 'in' ? IN : MM;
  const s = section(shape, w, h);
  const r = roundLoss(Q, s.de * unit, ROUGHNESS[mat] ?? ROUGHNESS.galv);
  const V = Q / (s.area * unit * unit);
  return { rate: r.rate / (u === 'in' ? PER_100FT : 1), V: u === 'in' ? V / FPM : V, Re: r.Re, f: r.f, de: s.de };
}

In detail

Method
Hazen–Williams for supply pipes; Darcy–Weisbach with the Colebrook equation for ducts; fitting losses by K × ρV² ÷ 2; demand from fixture units by log–log interpolation of a curve (Hunter's method, per the file's comments); equal-friction duct sizing by bisection and rounding up to a stocked size; a tree from the source with loads summed toward it.
Number types
64-bit numbers throughout.
Tolerances
Colebrook: stops when f changes by less than 1e-14, at most 60 passes. Diameter bisection: fixed 100 or 200 halvings on ln D between 10 mm and 5 m. Node merge: 40 mm by default, 60 mm from the piping add-on, absolute. Size choice: velocity limit + 1e-9, rate limit + 1e-12, stocked size − 1e-9.
Limits
Laminar factor 64 ÷ Re below Re 2300 gives a jump at 2300. Bends of 15° or less add no loss. Static head is left out for ducts. The network CSV rounds its values to 1 to 3 decimals.

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