Resources Numerical accuracy Calculation policy

Assistant answers: fire ratings and room areas

Answers two questions a connected assistant asks of the open model: which walls have a given fire rating, and which rooms are under or over a given area.

How the precision is kept

Fire ratings are read from text into whole minutes. Room areas are compared at full 64-bit precision in square meters; a limit given in square feet is multiplied by 0.09290304, the exact value of 0.3048². The areas returned are rounded to two decimals in both units after the comparison.

Calculations under this policy

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

ask-tools.js BimCraft 0.8.1 64-bit 3 calculations Reviewed

Tests tools/bimcraft-test/ask-test.mjs

Fire rating text as minutes

64-bit

A rating written as '2HR', '120 min', 'REI 120' or a bare number, as minutes.

minutes = round(hours × 60)

parseFloat reads the number after a decimal comma is turned into a point, and hours × 60 is rounded to a whole minute. Class forms give the minutes directly as an integer.

  • ask-tools.js
  • lines 33 to 51
  • BimCraft 0.8.1
/**
 * A FireRating value as minutes, and how it was read. Files write it as '2HR', '2 hr', '2 hours', '120', '120 min', 'REI 120',
 * 'EI60', '1-hour', 'R90' or just a number. A bare number is hours when it is 4 or less (nobody rates a wall 4 minutes), else minutes;
 * the answer says `assumed` so the assistant can say so. null when it does not read as a rating at all.
 */
export function parseFireRating(raw) {
  if (raw === null || raw === undefined || raw === '') return null;
  const s = String(raw).trim().toLowerCase().replace(/[–—]/g, '-');
  if (!s || /^(none|n\/?a|no|0|-)$/.test(s)) return /^0$/.test(s) ? { minutes: 0, how: 'unit-less zero' } : null;
  let m = s.match(/(\d+(?:[.,]\d+)?)\s*[- ]?\s*(hours?|hrs?|h)\b/);
  if (m) return { minutes: Math.round(parseFloat(m[1].replace(',', '.')) * 60), how: 'hours' };
  m = s.match(/(\d+(?:[.,]\d+)?)\s*[- ]?\s*(minutes?|mins?|m)\b/);
  if (m) return { minutes: Math.round(parseFloat(m[1].replace(',', '.'))), how: 'minutes' };
  m = s.match(/\b(?:rei|ei|re|r|e|i)\s*[- ]?(\d{1,3})\b/);   // European classes: R 90, EI 60, REI 120 are minutes
  if (m) return { minutes: parseInt(m[1], 10), how: 'class (minutes)' };
  m = s.match(/^(\d+(?:[.,]\d+)?)$/);
  if (m) { const n = parseFloat(m[1].replace(',', '.')); return n <= 4 ? { minutes: Math.round(n * 60), how: 'bare number read as hours', assumed: true } : { minutes: Math.round(n), how: 'bare number read as minutes', assumed: true }; }
  return null;
}

Matching walls to the rating asked for

64-bit

Which walls are rated exactly, or at least, the time asked for.

want = minutes, or hours × 60; match: minutes ≥ want, or minutes = want

The parsed rating is a whole number of minutes; the value asked for is hours × 60 with no rounding. For an exact match the two are compared with ===.

  • ask-tools.js
  • lines 98 to 114
  • BimCraft 0.8.1
const want = a.minutes !== undefined ? a.minutes : a.hours !== undefined ? a.hours * 60 : null;
if (want === null) throw bad('Give hours (for example 2) or minutes (for example 120).', ['Call bim_walls_find {hours: 2}.']);
const fields = fireRatingFields(ix);
if (!fields.length) throw new ToolError('not_found', 'No element in this model has a FireRating property.', { next: ['Ask the person where the ratings are kept, then call bim_elements_find {rules: [{field: "p:<Set>.<Property>", op: "is", value: "<rating>"}]}.'] });
const rules = [{ field: 'cls', op: 'is', value: a.class || 'IfcWall' }];
if (a.storey) rules.push({ field: 'storey', op: 'is', value: a.storey });
const walls = find(ix, { rules });
const hit = [], unreadable = [];
let unrated = 0;
for (const k of walls) {
  const id = k - ix.keyBase;
  let raw = null;
  for (const f of fields) { const v = ix.value(f.id, id); if (v !== undefined && v !== '') { raw = v; break; } }
  if (raw === null) { unrated++; continue; }
  const p = parseFireRating(raw);
  if (!p) { unreadable.push({ id: k, name: ix.value('name', id) ?? null, value: String(raw).slice(0, 60) }); continue; }
  if (a.match === 'at_least' ? p.minutes >= want : p.minutes === want) hit.push({ key: k, value: String(raw).slice(0, 60), minutes: p.minutes, assumed: !!p.assumed });

Rooms under or over an area

64-bit

The rooms smaller or larger than a limit, with each area in square meters and square feet.

limit in m² = limit in sq ft × 0.09290304; area in sq ft = area ÷ 0.09290304

The comparison uses the unrounded area in square meters with strict less-than and greater-than. The areas and limits returned are rounded to two decimals with Math.round(n × 100) ÷ 100, after the comparison.

  • ask-tools.js
  • lines 154 to 166
  • BimCraft 0.8.1
const k = a.unit === 'sqft' ? SQFT : 1;
const lo = a.over !== undefined ? a.over * k : -Infinity, hi = a.under !== undefined ? a.under * k : Infinity;
const rows = roomAreas(ix, m).filter((r) => (!a.storey || r.storey === a.storey));
const noArea = rows.filter((r) => typeof r.area !== 'number');
const hit = rows.filter((r) => typeof r.area === 'number' && r.area < hi && r.area > lo).sort((x, y) => x.area - y.area);
const pg = page(hit, a), shown = show(a, hit.map((r) => r.key));
const notes = [];
if (noArea.length) notes.push(noArea.length + ' room(s) have no area and are not in the answer.');
if (hit.some((r) => r.areaFrom === 'shape')) notes.push('Some areas are measured from the room shape, not stated in the file.');
return {
  model: app.name(), rooms: rows.length, total: hit.length, returned: pg.returned, next: pg.next, selected: shown, noArea: noArea.length, notes,
  limits: { under_m2: hi === Infinity ? null : r2(hi), over_m2: lo === -Infinity ? null : r2(lo), unit: a.unit || 'm2' },
  rooms_found: pg.rows.map((r) => ({ id: r.key, number: r.number, name: r.name, storey: r.storey, area_m2: r2(r.area), area_sqft: r2(r.area / SQFT), areaFrom: r.areaFrom })),

In detail

Method
Text patterns for hours, minutes and class forms (REI 120, EI 60); a bare number of 4 or less is read as hours, a larger one as minutes, and the answer says so. Room areas come from spaceSchedule in ifc-qto.js.
Number types
64-bit numbers; parsed minutes are whole numbers after Math.round.
Limits
The rating asked for in hours is multiplied by 60 without rounding and an exact match uses ===. The usual ratings (0.5, 0.75, 1, 1.5, 2, 3 and 4 hours) are exact; 2.05 hours gives 122.99999999999999 and would not match a wall rated 123 minutes.

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