Resources Numerical accuracy Calculation policy

Conduit and tray fill, cable schedule

Works out how full a conduit or cable tray is, picks the smallest conduit that holds a set of cables, routes cables through the carriers and lists their lengths.

How the precision is kept

Areas and fill percentages are 64-bit expressions with Math.PI ÷ 4. Conduit inside diameters come from a table in the file given to 0.1 mm, and the fill limits are defaults the person replaces with the code in force, as the file's header says. The cable schedule rounds each length to 0.1 m.

Open item. Keep the unrounded cable length in the schedule row and round to 0.1 m only where it is shown.

Calculations under this policy

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

addons/electrical/lib/mep/carrier.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

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

Cross-section of the cables

64-bit

The total cross-sectional area of a set of cables.

area = Σ (π ÷ 4) × od² × n

Math.PI ÷ 4 is computed once. Each cable's count is rounded to a whole number of at least 1. A plain sum in list order.

  • addons/electrical/lib/mep/carrier.js
  • line 35
  • CadCraft 0.8.2
const cableArea = (cables) => cables.reduce((a, c) => a + PI4 * (c.odMm || 0) ** 2 * Math.max(1, Math.round(c.n || 1)), 0);

Conduit fill

64-bit

The percentage of a conduit's inside area the cables take, and whether it is within the limit.

pct = 100 × used ÷ (π ÷ 4 × id²); free = area × limit ÷ 100 − used

π ÷ 4 appears in both the cable area and the conduit area and is not cancelled, which costs nothing measurable. The comparison with the limit allows 1e-9. A zero inside diameter gives 0 % instead of a division by zero.

  • addons/electrical/lib/mep/carrier.js
  • lines 39 to 44
  • CadCraft 0.8.2
export function conduitFill({ idMm, cables, limits = FILL_DEFAULTS.conduit }) {
  const count = cableCount(cables), area = PI4 * idMm * idMm, used = cableArea(cables), limit = limitFor(count, limits);
  const pct = area > 0 ? 100 * used / area : 0;
  const biggest = cables.reduce((m, c) => Math.max(m, c.odMm || 0), 0);
  return { areaMm2: area, usedMm2: used, pct, count, limitPct: limit, ok: count === 0 || (pct <= limit + 1e-9 && biggest <= idMm), free: area * limit / 100 - used, tooWide: biggest > idMm };
}

Smallest conduit that holds the cables

64-bit

The first trade size, going up, whose fill is within the limit.

Runs conduitFill for each size in trade order with the inside diameter from the table in this file, which is given to 0.1 mm. Returns null when no listed size passes.

  • addons/electrical/lib/mep/carrier.js
  • lines 45 to 49
  • CadCraft 0.8.2
export function conduitFor(type, cables, limits = FILL_DEFAULTS.conduit) {
  const T = CONDUIT_TYPES[type] || CONDUIT_TYPES.emt;
  for (const size of conduitSizes(type)) { const d = T.sizes[size]; if (conduitFill({ idMm: d.id, cables, limits }).ok) return { size, ...d }; }
  return null;
}

Cable tray fill

64-bit

The fill of a tray by area, or by the sum of cable diameters when the cables lie in one layer.

area mode: pct = 100 × used ÷ (width × depth); layer mode: pct = 100 × Σ (od × n) ÷ width

Both modes are one division guarded against a zero width or area. The pass tests allow 1e-9 on the limit, the width and the depth.

  • addons/electrical/lib/mep/carrier.js
  • lines 51 to 64
  • CadCraft 0.8.2
/**
 * Tray fill. mode 'area': the cables' cross-sections as a percentage of width × usable depth against `maxPct`; mode 'layer': the cables lie in
 * one layer, so the sum of their diameters must fit the width (the single-layer rule for large cables), and the layer height is the biggest cable.
 */
export function trayFill({ widthMm, depthMm, cables, mode = 'area', maxPct = FILL_DEFAULTS.tray }) {
  const used = cableArea(cables), area = widthMm * depthMm, count = cableCount(cables);
  const biggest = cables.reduce((m, c) => Math.max(m, c.odMm || 0), 0);
  if (mode === 'layer') {
    const sumOd = cables.reduce((s, c) => s + (c.odMm || 0) * Math.max(1, Math.round(c.n || 1)), 0);
    return { mode, count, usedMm2: used, sumOdMm: sumOd, widthMm, pct: widthMm > 0 ? 100 * sumOd / widthMm : 0, limitPct: 100, layerMm: biggest, ok: sumOd <= widthMm + 1e-9 && biggest <= depthMm + 1e-9, tooDeep: biggest > depthMm };
  }
  const pct = area > 0 ? 100 * used / area : 0;
  return { mode, count, usedMm2: used, areaMm2: area, pct, limitPct: maxPct, ok: pct <= maxPct + 1e-9 && biggest <= depthMm + 1e-9, tooDeep: biggest > depthMm, free: area * maxPct / 100 - used };
}

Shortest way through the carriers

64-bit

The shortest path between two nodes of the tray and conduit network, and its length.

Dijkstra: d = dist[u] + segment length × 1000

Each segment's length is multiplied by 1000 as it is added, so the path length is a running sum of products. Equal-length alternatives are decided by rounding and visiting order.

  • addons/electrical/lib/mep/carrier.js
  • lines 67 to 85
  • CadCraft 0.8.2
/**
 * Shortest path (by length) between two nodes of the carriers' graph. graph = graphFromRuns(...) over the tray/conduit runs only.
 * → { segs: [seg], length (mm) } or null.
 */
function shortest(graph, from, to) {
  if (from === to) return { segs: [], length: 0 };
  const adj = new Map();
  for (const s of graph.segs) for (const [a, b] of [[s.a, s.b], [s.b, s.a]]) { if (!adj.has(a)) adj.set(a, []); adj.get(a).push({ to: b, s }); }
  const dist = new Map([[from, 0]]), prev = new Map(), todo = new Set([from]);
  while (todo.size) {
    let u = null; for (const n of todo) if (u === null || dist.get(n) < dist.get(u)) u = n;
    todo.delete(u);
    if (u === to) break;
    for (const { to: v, s } of adj.get(u) || []) { const d = dist.get(u) + s.length * 1000; if (d < (dist.get(v) ?? Infinity)) { dist.set(v, d); prev.set(v, { u, s }); todo.add(v); } }
  }
  if (!dist.has(to)) return null;
  const segs = []; for (let n = to; prev.has(n); n = prev.get(n).u) segs.unshift(prev.get(n).s);
  return { segs, length: dist.get(to) };
}

Cable length through each run

64-bit

For each cable, the runs it passes through and its length in each.

length in a run = Σ segment length × 1000 over that run's segments on the path

A cable with runs fixed by the person takes every segment of those runs; its total is the sum of the per-run sums. Otherwise the total is the path length from the search, and the per-run figures are summed separately from the same segments.

  • addons/electrical/lib/mep/carrier.js
  • lines 86 to 107
  • CadCraft 0.8.2
/**
 * routeCables(graph, cables, nodeOf) — cable = { tag, from: key, to: key } (keys the graph was given as points); `fixedRuns` on a cable ([run ids])
 * overrides the search (the person said which runs it lies in). → Map tag → { found, lengthMm, runs: [{ run, mm }], segs }
 */
export function routeCables(graph, cables, nodeOf = graph.nodeOf) {
  const out = new Map();
  for (const c of cables) {
    if (c.fixedRuns?.length) {
      const segs = graph.segs.filter((s) => c.fixedRuns.includes(s.run));
      const runs = c.fixedRuns.map((r) => ({ run: r, mm: segs.filter((s) => s.run === r).reduce((a, s) => a + s.length * 1000, 0) }));
      out.set(c.tag, { found: true, lengthMm: runs.reduce((a, r) => a + r.mm, 0), runs, segs: segs.map((s) => s.id) });
      continue;
    }
    const a = nodeOf[c.from], b = nodeOf[c.to];
    const p = a != null && b != null ? shortest(graph, a, b) : null;
    if (!p) { out.set(c.tag, { found: false, lengthMm: 0, runs: [], segs: [], why: a == null || b == null ? 'ends' : 'gap' }); continue; }
    const byRun = new Map();
    for (const s of p.segs) byRun.set(s.run, (byRun.get(s.run) || 0) + s.length * 1000);
    out.set(c.tag, { found: true, lengthMm: p.length, runs: [...byRun].map(([run, mm]) => ({ run, mm })), segs: p.segs.map((s) => s.id) });
  }
  return out;
}

Cable schedule length

64-bit

The length of each cable in the schedule, with slack and an allowance at each end.

length m = (route mm × (1 + slack) + 2 × ends mm) ÷ 1000, rounded to 0.1

The value stored in the row and written to the CSV is Math.round(len × 10) ÷ 10, so it is kept to 0.1 m. The defaults are 10 % slack and 1000 mm per end.

  • addons/electrical/lib/mep/carrier.js
  • lines 128 to 136
  • CadCraft 0.8.2
/** Rows of the cable schedule. `slack` (fraction, default 0.1) and `endsMm` (per end, default 1000) are typed allowances, shown as a column of their own. */
export function scheduleRows(cables, routes, { slack = 0.1, endsMm = 1000 } = {}) {
  return cables.map((c) => {
    const r = routes.get(c.tag) || { found: false, runs: [], lengthMm: 0 };
    const len = r.found ? (r.lengthMm * (1 + slack) + 2 * endsMm) / 1000 : 0;
    return { tag: c.tag, circuit: c.circuit || '', type: c.type || '', size: c.size || '', odMm: c.odMm || 0, n: c.n || 1, from: c.from || '', to: c.to || '', lengthM: Math.round(len * 10) / 10,
      carriers: r.runs.map((x) => x.run).join(' '), note: r.found ? '' : (c.fixedRuns?.length ? '' : r.why === 'ends' ? 'end not on a carrier' : 'no continuous route') };
  });
}

Cable list read from CSV

64-bit

Reads cable tags, outside diameters and counts from a CSV file.

The outside diameter and count are read with Number(), so they keep the decimals in the file. A diameter must be above 0 and below 200 mm and a count a whole number from 1 to 1000; other rows are reported as errors.

  • addons/electrical/lib/mep/carrier.js
  • lines 140 to 162
  • CadCraft 0.8.2
/** A cable list typed or exported from the Electrical add-on: tag, circuit, type, size, od (mm), n, from, to. Unknown columns ignored; a bad number is a row error. */
export function parseCablesCsv(text) {
  const rows = [], errors = [];
  const lines = String(text || '').split(/\r?\n/).filter((l) => l.trim());
  if (!lines.length) return { cables: rows, errors };
  const split = (l) => { const out = []; let cur = '', q = false; for (let i = 0; i < l.length; i++) { const ch = l[i]; if (q) { if (ch === '"' && l[i + 1] === '"') { cur += '"'; i++; } else if (ch === '"') q = false; else cur += ch; } else if (ch === '"') q = true; else if (ch === ',') { out.push(cur); cur = ''; } else cur += ch; } out.push(cur); return out; };
  const head = split(lines[0]).map((h) => h.trim().toLowerCase().replace(/[^a-z]/g, ''));
  const col = (n) => head.indexOf(n);
  const ix = { tag: col('tag'), circuit: col('circuit'), type: col('type'), size: col('size'), od: Math.max(col('od'), col('odmm')), n: Math.max(col('n'), col('count')), from: col('from'), to: col('to') };
  if (ix.tag < 0 || ix.od < 0) return { cables: rows, errors: [{ line: 1, key: 'columns' }] };
  const seen = new Set();
  for (let i = 1; i < lines.length && i <= 5000; i++) {
    const f = split(lines[i]), g = (k) => (ix[k] >= 0 ? (f[ix[k]] ?? '').trim() : '');
    const od = Number(g('od')), n = g('n') === '' ? 1 : Number(g('n'));
    if (!g('tag')) { errors.push({ line: i + 1, key: 'tag' }); continue; }
    if (seen.has(g('tag'))) { errors.push({ line: i + 1, key: 'duplicate', tag: g('tag') }); continue; }
    if (!(od > 0 && od < 200)) { errors.push({ line: i + 1, key: 'od', tag: g('tag') }); continue; }
    if (!(Number.isInteger(n) && n >= 1 && n <= 1000)) { errors.push({ line: i + 1, key: 'n', tag: g('tag') }); continue; }
    seen.add(g('tag'));
    rows.push({ tag: g('tag').slice(0, 40), circuit: g('circuit').slice(0, 40), type: g('type').slice(0, 60), size: g('size').slice(0, 30), odMm: od, n, from: g('from').slice(0, 60), to: g('to').slice(0, 60) });
  }
  return { cables: rows, errors };
}

In detail

Method
Fill % = 100 × Σ (π ÷ 4 × od² × n) ÷ (π ÷ 4 × id²), compared with the limit for 1, 2 or more cables; tray fill against width × depth, or the sum of diameters against the width in single-layer mode; cable route by Dijkstra over the carrier segments; schedule length = route × (1 + slack) + 2 × end allowance.
Number types
64-bit numbers; cable counts rounded to whole numbers of at least 1.
Tolerances
A fill passes when it is within 1e-9 percentage points of the limit; single-layer and depth tests allow 1e-9 mm.
Checked by
tools/bimcraft-test/eg-mep-test.mjs runs BimCraft's mep/carrier.js, which was byte-identical to this file at the review. It checks which limit applies to 1, 2 and more cables, that a cable wider than the conduit is flagged, tray fill by area and by layer, and the schedule rows.
Limits
Conduit sizes are the 19 table entries for EMT and PVC Schedule 40. The default limits (53, 31, 40 and 50 %) are labelled as defaults for review.

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