Resources Numerical accuracy Calculation policy

Quantity totals and schedules from IFC properties

Adds up the quantities and number properties of IFC elements by group, lists rooms with their areas and volumes, and decides whether a property changed between two versions.

How the precision is kept

Property and quantity values are read from the file as 64-bit numbers, but the property index rounds each one to 6 decimals before it is kept. Totals add those values in element order and are then cut to 12 significant digits to hide binary noise. Two versions of a number count as the same within a relative 1e-9.

Open item. Keep property numbers as read, without the rounding to 6 decimals, and round only where a value is shown.

Calculations under this policy

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

ifc-props.js BimCraft 0.8.1 64-bit 1 calculation Reviewed

Tests bin/test-bimcraft.mjs

Reading a property value

64-bit

Turns an IFC property or quantity value into the number or text the index, the totals and the comparison use.

number kept = round(x × 10⁶) ÷ 10⁶

Every finite number is rounded to 6 decimals with Math.round before it is stored in the index, whatever its unit or size. This is the value that totals, schedules, the CSV exports and the model comparison then read.

  • ifc-props.js
  • lines 45 to 54
  • BimCraft 0.8.1
function plain(v) {
  if (v === null || v === undefined) return undefined;
  if (Array.isArray(v)) { const a = v.map(plain).filter((x) => x !== undefined); return a.length ? a.join(', ') : undefined; }
  let x = typeof v === 'object' && 'value' in v ? v.value : v;
  if (x === null || x === undefined || x === '') return undefined;
  if (typeof x === 'number') return Number.isFinite(x) ? Math.round(x * 1e6) / 1e6 : undefined;
  if (x === true || x === '.T.' || x === 'T') return 'Yes';
  if (x === false || x === '.F.' || x === 'F') return 'No';
  return String(x);
}
ifc-qto.js BimCraft 0.8.1 64-bit 3 calculations Reviewed

Tests bin/test-bimcraft.mjs

Rounding a total

64-bit

Cuts a sum to 12 significant digits so that binary noise such as 0.1 + 0.2 does not show.

tidy(n) = Number(n.toPrecision(12))

The total that is returned and exported is the sum rounded to 12 significant digits, three fewer than a 64-bit number holds.

  • ifc-qto.js
  • lines 33 to 34
  • BimCraft 0.8.1
/** Rounds away binary noise (0.1 + 0.2) without hiding real digits. */
export const tidy = (n) => (Number.isFinite(n) ? Number(n.toPrecision(12)) : n);

Totals by group

64-bit

Counts elements and sums the chosen number fields for each combination of group values and for the whole selection.

sum = Σ vᵢ over the elements with a number; counted = how many had one

Values are added one by one in element order, with no compensated summation, then passed through tidy. The group total and the grand total are accumulated separately from the same values.

  • ifc-qto.js
  • lines 36 to 55
  • BimCraft 0.8.1
export function totals(ix, { keys = null, groupBy = ['cls'], sum = [] } = {}) {
  const kb = ix.keyBase;
  const known = keys ? new Set(ix.ids) : null;
  const ids = keys ? [...new Set([...keys].map((k) => k - kb))].filter((id) => known.has(id)) : ix.ids;
  const gv = groupBy.slice(0, MAX_GROUP).map((f) => ix.values(f)), sv = sum.map((f) => ix.values(f));
  const rows = new Map();
  const total = { count: 0, sums: sum.map(() => 0), counted: sum.map(() => 0) };
  for (const id of ids) {
    const values = gv.map((m) => { const v = m.get(id); return v === undefined ? '' : v; });
    const k = JSON.stringify(values);
    let r = rows.get(k);
    if (!r) rows.set(k, (r = { values, count: 0, sums: sum.map(() => 0), counted: sum.map(() => 0) }));
    r.count++; total.count++;
    sv.forEach((m, i) => { const v = m.get(id); if (typeof v === 'number' && Number.isFinite(v)) { r.sums[i] += v; r.counted[i]++; total.sums[i] += v; total.counted[i]++; } });
  }
  const out = [...rows.values()].sort((a, b) => { for (let i = 0; i < a.values.length; i++) { const c = cmp(a.values[i], b.values[i]); if (c) return c; } return 0; });
  for (const r of out) r.sums = r.sums.map(tidy);
  total.sums = total.sums.map(tidy);
  return { groupBy: groupBy.slice(0, MAX_GROUP), sum: [...sum], rows: out, total };
}

Spaces schedule totals

64-bit

The total floor area and volume of the listed rooms.

Adds the rows' area and volume in sorted row order and rounds the result to 12 significant digits with tidy. Rows without a number add zero.

  • ifc-qto.js
  • lines 115 to 116
  • BimCraft 0.8.1
const sum = (f) => tidy(rows.reduce((s, r) => s + (typeof r[f] === 'number' ? r[f] : 0), 0));
return { rows, total: { count: rows.length, area: sum('area'), volume: sum('volume') } };
ifc-compare.js BimCraft 0.8.1 64-bit 1 calculation Reviewed

Tests bin/test-bimcraft.mjs

Is a property value the same in both versions

64-bit

Decides whether a number or text property changed between two versions of a model.

same when |a − b| ≤ 1e-9 × max(1, |a|, |b|)

Numbers compare with a tolerance that is relative above 1 and absolute (1e-9) below it; text compares exactly. Both values were already rounded to 6 decimals when the index read them.

  • ifc-compare.js
  • line 44
  • BimCraft 0.8.1
const sameVal = (a, b) => (typeof a === 'number' && typeof b === 'number' ? Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b)) : a === b);
ifc-model.js BimCraft 0.8.1 64-bit 1 calculation Reviewed

Tests bin/test-bimcraft.mjs

Number shown in Properties

64-bit

How a property or quantity value from the file is written out in the Properties panel.

shown = round(x × 10⁴) ÷ 10⁴

A whole number is shown as it is; any other number is rounded to 4 decimals with Math.round before it is turned into text. The value in the file is not changed by this.

  • ifc-model.js
  • lines 287 to 295
  • BimCraft 0.8.1
function fmt(v) {
  if (Array.isArray(v)) return v.map(fmt).join(', ');
  const x = val(v);
  if (x === null || x === undefined) return '';
  if (typeof x === 'number') return Number.isInteger(x) ? String(x) : String(Math.round(x * 10000) / 10000);
  if (x === true || x === '.T.' || x === 'T' || x === 'true') return 'Yes';
  if (x === false || x === '.F.' || x === 'F' || x === 'false') return 'No';
  return String(x);
}

In detail

Method
Plain sums per group and for the whole selection; an element with no number for a column adds nothing and is counted as left out.
Number types
64-bit numbers in JavaScript Maps and arrays.
Tolerances
Values rounded to 1e-6 (absolute) on reading. Totals rounded to 12 significant digits. Comparison: |a − b| ≤ 1e-9 × max(1, |a|, |b|).
Checked by
bin/test-bimcraft.mjs runs totals, totalsCsv and spaceSchedule (--qto) and compareModels (--compare) in Node.
Limits
A value smaller than 0.0000005 in its own unit becomes zero in the index. Totals carry 12 significant digits, not 15.

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