Resources Numerical accuracy Calculation policy

MEP check messages: units and rounding

Turns pipe, duct, cable carrier and lighting results into the warnings a person reads, with the required and found values.

How the precision is kept

The results arrive in SI units as 64-bit numbers. This file converts them for the message with exact unit factors and rounds what it prints and what it stores in each warning's required and found fields to one or two decimals. The sizing results themselves are not changed.

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/problems-mep.js BimCraft 0.8.1 64-bit 4 calculations Reviewed

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

Rounding to one and two decimals

64-bit

Rounds a value for a message or for a warning's stored fields.

f1(x) = round(10 x) ÷ 10; f2(x) = round(100 x) ÷ 100

The value is multiplied, rounded to a whole number and divided, so the kept value has one or two decimals. Math.round rounds halves toward plus infinity. Values that are not finite pass through unchanged.

  • mep/problems-mep.js
  • lines 11 to 12
  • BimCraft 0.8.1
const f1 = (x) => (Number.isFinite(x) ? Math.round(x * 10) / 10 : x);
const f2 = (x) => (Number.isFinite(x) ? Math.round(x * 100) / 100 : x);

Pipe flow and velocity in the message

64-bit

Shows a flow in gallons per minute or liters per second and a velocity in feet or meters per second.

gpm = q ÷ 0.003785411784 × 60; L/s = 1000 q; ft/s = v ÷ 0.3048

0.003785411784 m³ per US gallon and 0.3048 m per foot are the exact definitions. The division is done before the multiplication by 60, then the result is rounded to one decimal (two for L/s and m/s).

  • mep/problems-mep.js
  • lines 30 to 31
  • BimCraft 0.8.1
const gpm = (q) => f1(q / 0.003785411784 * 60), lps = (q) => f2(q * 1000), fl = (q) => (imperial ? gpm(q) + ' gpm' : lps(q) + ' L/s');
const vel = (v) => (imperial ? f1(v / 0.3048) + ' ft/s' : f2(v) + ' m/s');

Stored velocity and pressure values

64-bit

Writes the required and found velocity and the pressure left at a fixture into each pipe warning.

found = f2(V) m/s; required = f2(v_max) m/s; pressure = f1(kPa)

The stored velocity is rounded to two decimals and the pressures to one. The sizing decision was made upstream on the unrounded value; the stored pair is for the report.

  • mep/problems-mep.js
  • lines 27 to 46
  • BimCraft 0.8.1
/** analyzePipes() result → problems. `unit` { len: 'mm' | 'in', ... } is only for words; numbers are SI in the result. */
export function pipeProblems(res, { labelOf = (id) => id, guidOf = (id) => id, imperial = false } = {}) {
  const out = networkProblems('pipes', res, labelOf, guidOf);
  const gpm = (q) => f1(q / 0.003785411784 * 60), lps = (q) => f2(q * 1000), fl = (q) => (imperial ? gpm(q) + ' gpm' : lps(q) + ' L/s');
  const vel = (v) => (imperial ? f1(v / 0.3048) + ' ft/s' : f2(v) + ' m/s');
  for (const s of res.segs) {
    const about = [run(guidOf(s.run), labelOf(s.run))];
    if (s.status === 'small') out.push(P('pipes', 'pipe.small', 'warning', `${labelOf(s.run)} (${s.size}) carries ${fl(s.flow)} at ${vel(s.V)}; the velocity limit of ${vel(res.opts.vmax)}${res.opts.rateMax ? ' or the friction limit' : ''} needs ${s.need}.`,
      { part: s.run + '#' + s.id, about, vars: { run: s.run, flow: s.flow, need: s.need }, req: { op: '<=', value: f2(res.opts.vmax), unit: 'm/s' }, found: { value: f2(s.V), unit: 'm/s' }, fixes: [{ id: 'size', text: `Set the size to ${s.need}`, action: 'run-size', args: { run: s.run, size: s.need } }] }));
    else if (s.status === 'toobig') out.push(P('pipes', 'pipe.toobig', 'error', `${labelOf(s.run)} carries ${fl(s.flow)}: no size of its material keeps the velocity under ${vel(res.opts.vmax)}.`, { part: s.run + '#' + s.id, about, req: { op: '<=', value: f2(res.opts.vmax), unit: 'm/s' }, found: { value: f2(s.V), unit: 'm/s' } }));
    if (s.beyond) out.push(P('pipes', 'pipe.beyond', 'note', `${labelOf(s.run)}: ${f1(s.fu)} fixture units is beyond the last point of the demand curve; the curve's last slope was continued.`, { part: s.run + '#' + s.id, about }));
  }
  for (const l of res.loads) {
    if (l.reached === false) out.push(P('pipes', 'pipe.noroute', 'warning', `${l.label || l.key} is not joined to the source by pipe.`, { part: 'load:' + l.key, about: [] }));
    else if (l.short) out.push(P('pipes', 'pipe.pressure', 'warning', `${l.label || l.key}: ${f1(l.leftKpa)} kPa left at the fixture after ${f1(l.dpKpa)} kPa of loss; ${f1(res.opts.minKpa)} kPa is needed.`,
      { part: 'load:' + l.key, about: l.path.slice(0, 50).map((id) => run(guidOf(res.segs.find((s) => s.id === id)?.run), labelOf(res.segs.find((s) => s.id === id)?.run))), req: { op: '>=', value: f1(res.opts.minKpa), unit: 'kPa' }, found: { value: f1(l.leftKpa), unit: 'kPa' } }));
  }
  if (res.curve?.example) out.push(P('pipes', 'pipe.curve', 'note', 'The demand comes from the built-in example curve, not from a code table. Type the table of the code in force before relying on these sizes.', { part: 'curve' }));
  return out;
}

Duct velocity, friction rate and aspect ratio

64-bit

Writes a duct's flow, velocity, needed round size and aspect ratio into its warning.

fpm = round(V ÷ 0.00508); L/s = f1(1000 q); needed size = round(mm)

0.00508 m/s per foot per minute is exact (0.3048 ÷ 60). The needed equivalent diameter is rounded to a whole millimeter in the text. The required velocity and friction rate are stored unrounded; the found ones are rounded to two decimals.

  • mep/problems-mep.js
  • lines 48 to 61
  • BimCraft 0.8.1
/** analyzeDucts() result → problems. */
export function ductProblems(res, { labelOf = (id) => id, guidOf = (id) => id, imperial = false } = {}) {
  const out = networkProblems('ducts', res, labelOf, guidOf);
  const vel = (v) => (imperial ? Math.round(v / 0.00508) + ' fpm' : f1(v) + ' m/s');
  for (const s of res.segs) {
    const about = [run(guidOf(s.run), labelOf(s.run))];
    if (s.status === 'small' || s.status === 'fast') out.push(P('ducts', 'duct.' + s.status, 'warning', `${labelOf(s.run)} (${s.size}) carries ${f1(s.flow * 1000)} L/s at ${vel(s.V)}; ${res.opts.method === 'velocity' ? 'the velocity limit' : 'the friction rate'} needs a round equivalent of at least ${Math.round(s.needDeMm)} mm.`,
      { part: s.run + '#' + s.id, about, req: res.opts.method === 'velocity' ? { op: '<=', value: res.opts.vmax, unit: 'm/s' } : { op: '<=', value: res.opts.rate, unit: 'Pa/m' }, found: res.opts.method === 'velocity' ? { value: f2(s.V), unit: 'm/s' } : { value: f2(s.rate), unit: 'Pa/m' } }));
    if (s.aspectHigh) out.push(P('ducts', 'duct.aspect', 'note', `${labelOf(s.run)} (${s.size}) is ${f1(s.aspect)}:1; ${res.opts.maxAspect}:1 is the usual limit.`, { part: s.run + '#' + s.id, about, req: { op: '<=', value: res.opts.maxAspect, unit: ':1' }, found: { value: f1(s.aspect), unit: ':1' } }));
  }
  for (const t of res.terminals) if (t.reached === false) out.push(P('ducts', 'duct.noroute', 'warning', `${t.label || t.key} is not joined to the source by duct.`, { part: 'term:' + t.key }));
  out.push(P('ducts', 'duct.defaults', 'note', 'Fitting losses use rounded default coefficients (elbows, tees, transitions), not a fitting database. Replace them with your project table for a final fan selection.', { part: 'defaults' }));
  return out;
}

In detail

Method
Unit factors: 1 US gallon = 0.003785411784 m³, 1 ft = 0.3048 m, 1 ft/min = 0.00508 m/s. Rounding by Math.round(x × 10) ÷ 10 and Math.round(x × 100) ÷ 100.
Number types
64-bit numbers.
Limits
A found value stored in a warning is the rounded one, so a value just over a limit can be stored equal to the limit (for example 2.004 m/s against 2.00).

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