Resources Numerical accuracy Calculation policy

Numbers in the frame analysis report

The values printed in the analysis and design report tables, which are also what the CSV and PDF downloads contain.

How the precision is kept

The report reads the solver's 64-bit results and rounds them only when it writes the text of a cell. The same rounded text goes to the page, the CSV file and the PDF, so the downloads carry two or three decimals in kN, kN·m, mm and mrad, not the full values.

Open item. Offer the CSV download with the unrounded 64-bit values, since it is the file other programs read.

Calculations under this policy

10 calculations in 5 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.

frame/frame-report.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests bin/test-frame.mjs

A number as report text

64-bit

Writes a value with a fixed number of decimals.

text = v.toFixed(d); 0 when |v| < 0.5 × 10⁻ᵈ

The value is rounded once, by toFixed, at the moment the cell's text is made. That text is what the CSV and the PDF hold.

  • frame/frame-report.js
  • lines 22 to 26
  • CadCraft 0.8.2
function fmt(v, d) {
  if (!Number.isFinite(v)) return '';
  const s = (Math.abs(v) < 0.5 * 10 ** -d ? 0 : v).toFixed(d);
  return s === '-0' || /^-0\.0*$/.test(s) ? s.slice(1) : s;
}

Force in kN

64-bit

A force in newtons printed in kilonewtons.

kN = N ÷ 1000, 2 decimals; kN·m = N·mm ÷ 10⁶, 2 decimals

One division, then two decimals. A reaction is therefore exported to the nearest 10 N.

  • frame/frame-report.js
  • line 18
  • CadCraft 0.8.2
export const KN = (v) => fmt(v / 1000, 2);

Reactions, largest displacement and the envelope

64-bit

Gathers each set's reactions, the node that moves most, each member's extremes, and the largest value of each force over all sets.

displacement = √(ux² + uy² + uz²); envelope = the value with the largest magnitude over the sets

Values are copied from the solver's Float64Array without rounding. Member extremes are the largest of 41 station values.

  • frame/frame-report.js
  • lines 29 to 61
  • CadCraft 0.8.2
export function reportData(model, sol, { n = 40 } = {}) {
  const prep = sol.prep;
  const secs = new Map(model.sections.map((s) => [s.id, s]));
  const sets = [];
  for (const [key, R] of Object.entries(sol.results)) {
    const reactions = [];
    model.supports.forEach((s) => {
      const k = prep.idx.get(s.node);
      reactions.push({ node: s.node, R: Array.from(R.reactions.subarray(6 * k, 6 * k + 6)) });
    });
    let maxDisp = { value: 0, node: null };
    model.nodes.forEach((nd, k) => {
      const d = Math.hypot(R.u[6 * k], R.u[6 * k + 1], R.u[6 * k + 2]);
      if (d > maxDisp.value) maxDisp = { value: d, node: nd.id };
    });
    const members = model.members.map((m, k) => ({ id: m.id, ref: m.from?.ref || '', L: prep.mem[k].ax.L, section: secs.get(m.section)?.name || m.section, ext: memberExtremes(prep, R, k, n) }));
    sets.push({ key, name: R.name, kind: R.kind, R, reactions, sum: { applied: R.applied, reacted: R.reacted }, members, maxDisp });
  }
  // envelope: for each member and action the largest magnitude over every set, with its sign and the set it came from
  const envelope = model.members.map((m, k) => {
    const row = { id: m.id, ref: m.from?.ref || '' };
    for (const a of ['N', 'Vy', 'Vz', 'T', 'My', 'Mz']) {
      let best = { v: 0, set: '' };
      for (const s of sets) { const v = signedMax(s.members[k].ext[a]); if (Math.abs(v) > Math.abs(best.v)) best = { v, set: s.name }; }
      row[a] = best;
    }
    let d = { v: 0, set: '', ratio: Infinity };
    for (const s of sets) { const e = s.members[k].ext.defl; if (e.value > d.v) d = { v: e.value, set: s.name, ratio: e.ratio }; }
    row.defl = d;
    return row;
  });
  return { sets, envelope, counts: { nodes: model.nodes.length, members: model.members.length, supports: model.supports.length, cases: model.cases.length, combos: model.combos.length, dofs: sol.dofs }, warnings: sol.warnings || [] };
}

Deflection as a span ratio

64-bit

Prints a member's deflection as L over a whole number.

'L/' + round(L ÷ deflection)

The ratio is rounded to a whole number for the text; ratios of 10⁷ and above are left blank.

  • frame/frame-report.js
  • line 65
  • CadCraft 0.8.2
const ratio = (r) => (Number.isFinite(r) && r < 1e7 ? 'L/' + Math.round(r) : '');
frame/frame-design.js CadCraft 0.8.2 64-bit 2 calculations Reviewed

Tests bin/test-frame-design.mjs

Demand and capacity as text

64-bit

Prints a design check's demand and capacity in kN or kN·m.

kN = round(N ÷ 10) ÷ 100; kN·m = round(N·mm ÷ 10⁴) ÷ 100; ratios to 3 decimals

The value is rounded with Math.round and then written with toFixed. Only the text is rounded; the ratio that decides pass or fail is computed from the full values.

  • frame/frame-design.js
  • lines 334 to 339
  • CadCraft 0.8.2
function fmtF(v, unit) {
  if (!Number.isFinite(v)) return '';
  if (unit === 'N') return (Math.round(v / 10) / 100).toFixed(2) + ' kN';
  if (unit === 'N·mm') return (Math.round(v / 1e4) / 100).toFixed(2) + ' kN·m';
  return (Math.round(v * 1000) / 1000).toFixed(3);
}

Utilization as a percentage

64-bit

Prints a utilization ratio as a percentage with one decimal.

round(ratio × 1000) ÷ 10, 1 decimal

A ratio of 1.0004 prints as 100.0 %; the member's pass or fail state comes from the unrounded ratio, so the two can differ in the last printed digit.

  • frame/frame-design.js
  • line 298
  • CadCraft 0.8.2
const pc = (v) => (v === null || !Number.isFinite(v) ? '' : (Math.round(v * 1000) / 10).toFixed(1) + ' %');
frame/frame-concrete.js CadCraft 0.8.2 64-bit 2 calculations Reviewed

Tests tools/frame/test-concrete.mjs

A number in the concrete and seismic tables

64-bit

Writes a value with a chosen number of decimals.

round(v × 10ᵈ) ÷ 10ᵈ, then d decimals

Rounded with Math.round after a multiplication by a power of ten, then written with toFixed. Used for periods (3 decimals), Cs (4), forces in kN (0 or 1) and drifts in mm (1 or 2).

  • frame/frame-concrete.js
  • line 250
  • CadCraft 0.8.2
const f2 = (v, d = 2) => (Number.isFinite(v) ? (Math.round(v * 10 ** d) / 10 ** d).toFixed(d) : '');

Values in the shared problem list

64-bit

The found and required values attached to a failed concrete check or drift.

utilization rounded to 0.0001; drift rounded to 0.01 mm

The values stored with a problem are rounded with Math.round; whether there is a problem is decided from the unrounded ratio.

  • frame/frame-concrete.js
  • lines 256 to 286
  • CadCraft 0.8.2
export function problemsOf({ rc, drift, drawn = (id) => id, label = () => '', t }) {
  const out = [];
  const src = { name: 'detailcraft.frame' };
  const seen = new Set();
  if (rc?.ok) for (const m of rc.members) {
    const id = drawn(m.id);
    if (m.status === 'fail') {
      const key = 'rc.fail|' + id;
      if (seen.has(key)) continue;
      seen.add(key);
      out.push({ id: 'rc.fail:' + id, code: 'rc.fail', severity: 'warning', source: src, about: [{ kind: 'member', id, label: label(id) }],
        msg: { key: 'frame.rc.prob.fail', vars: { util: Math.round(m.util * 1000) / 10, check: m.check.id, clause: m.check.clause } },
        text: t('frame.rc.prob.fail', { util: f2(m.util * 100, 1), check: t('frame.rc.check.' + m.check.id), clause: m.check.clause }),
        req: { op: '<=', value: 1, unit: 'ratio' }, found: { value: Math.round(m.util * 1e4) / 1e4, unit: 'ratio' } });
    } else if (m.status === 'partial') {
      const key = 'rc.partial|' + id;
      if (seen.has(key)) continue;
      seen.add(key);
      out.push({ id: 'rc.partial:' + id, code: 'rc.partial', severity: 'note', source: src, about: [{ kind: 'member', id, label: label(id) }],
        msg: { key: 'frame.rc.prob.partial', vars: { why: m.partial[0] } }, text: t('frame.rc.prob.partial', { why: t('frame.rc.why.' + m.partial[0]) }) });
    }
  }
  for (const d of drift || []) {
    if (d.ratio <= 1) continue;
    out.push({ id: 'drift:' + d.set + ':' + d.id, code: 'seismic.drift', severity: 'warning', source: src, about: [],
      msg: { key: 'frame.eq.prob.drift', vars: { floor: d.name, ratio: Math.round(d.ratio * 1000) / 10 } },
      text: t('frame.eq.prob.drift', { floor: d.name, set: d.setName, design: f2(d.design, 1), allowed: f2(d.allowed, 1), ratio: f2(d.ratio * 100, 1) }),
      req: { op: '<=', value: Math.round(d.allowed * 100) / 100, unit: 'mm' }, found: { value: Math.round(d.design * 100) / 100, unit: 'mm' } });
  }
  return out;
}
frame/plate-draw.js CadCraft 0.8.2 64-bit 1 calculation Reviewed

Tests tools/frame/test-plates-app.mjs

Slab and wall rows of the report

64-bit

For each slab or wall: its largest deflection, its largest positive and negative moments, twisting moment and shear.

max and min of the plates' center values; Q = √(Qx² + Qy²); w = largest node displacement; kN·m/m = N·mm/mm ÷ 1000, 2 decimals

Moments and shears are the plate center values, not the smoothed node values the colored view shows. They are written with toFixed(2) after one division by 1000.

  • frame/plate-draw.js
  • lines 61 to 82
  • CadCraft 0.8.2
export function plateTables(model, sol, key, t) {
  const R = sol?.results?.[key], pl = sol?.prep?.shell?.pl;
  if (!R || !pl?.length) return [];
  const groups = new Map();
  model.plates.forEach((p, k) => { const g = p.from?.ref || p.id.split('.p')[0]; if (!groups.has(g)) groups.set(g, []); groups.get(g).push(k); });
  const kNm = (v) => (v / 1e3).toFixed(2), kN = (v) => (v / 1e3).toFixed(2);
  const rows = [];
  for (const [g, list] of groups) {
    let w = 0, mx = [0, 0], my = [0, 0], mxy = 0, q = 0;
    for (const k of list) {
      const c = plateCentre(sol.prep, R, k);
      mx = [Math.max(mx[0], c[3]), Math.min(mx[1], c[3])]; my = [Math.max(my[0], c[4]), Math.min(my[1], c[4])];
      mxy = Math.max(mxy, Math.abs(c[5])); q = Math.max(q, Math.hypot(c[6], c[7]));
      for (const nd of pl[k].nodes) w = Math.max(w, Math.hypot(R.u[6 * nd], R.u[6 * nd + 1], R.u[6 * nd + 2]));
    }
    rows.push([g, String(list.length), (model.plates[list[0]].t).toFixed(0), w.toFixed(2), kNm(mx[0]), kNm(mx[1]), kNm(my[0]), kNm(my[1]), kNm(mxy), kN(q)]);
  }
  /* i18n-ignore-start: column heads that are symbols / units */
  return [{ title: t('frame.pl.title'), head: [t('frame.pl.element'), t('frame.pl.n'), 't (mm)', 'w (mm)', 'Mx+ (kN·m/m)', 'Mx− (kN·m/m)', 'My+ (kN·m/m)', 'My− (kN·m/m)', '|Mxy| (kN·m/m)', 'Q (kN/m)'], numeric: [1, 2, 3, 4, 5, 6, 7, 8, 9], rows },
    { title: t('frame.pl.about'), head: [t('frame.rep.item'), t('frame.rep.value')], numeric: [], rows: [[t('frame.pl.method'), t('frame.pl.method-text')], [t('frame.pl.limits'), t('frame.pl.limits-text')]] }];
  /* i18n-ignore-end */
}
frame/slab-design.js CadCraft 0.8.2 64-bit 1 calculation Reviewed

Tests tools/frame/test-slab.mjs

Values in a slab problem

64-bit

The required and provided steel, or the shear and its strength, attached to a failed slab check.

steel rounded to 0.1 mm²/m; shear to 0.1 kN/m

A check fails when its ratio exceeds 1 + 1e-9, decided from the unrounded ratio. The values shown with it are rounded with Math.round.

  • frame/slab-design.js
  • lines 99 to 116
  • CadCraft 0.8.2
/** Failed slab checks as shared problems (rule 29). `t(key, vars)` words them; ids do not depend on the language. */
export function slabProblems(res, { t = (k) => k, name = 'slab' } = {}) {
  if (!res?.ok) return [];
  const out = [], fmt = (v) => String(Math.round(v * 10) / 10);
  for (const r of res.rows) if (r.ratio > 1 + 1e-9) {
    out.push({ id: 'concrete.slab:' + r.key, code: 'concrete.slab-flexure', severity: 'error', about: [], source: { kind: 'engine', name: 'detailcraft.frame' }, clause: r.clause,
      msg: { key: 'frame.slab.prob.flexure', vars: { layer: r.layer, need: fmt(r.need), prov: fmt(r.prov), bar: r.bar, s: r.s, plate: r.plate, set: r.set } },
      text: t('frame.slab.prob.flexure', { layer: r.layer, need: fmt(r.need), prov: fmt(r.prov), bar: r.bar, s: r.s, plate: r.plate, set: r.set }),
      req: { op: '<=', value: Math.round(r.prov * 10) / 10, unit: 'mm²/m' }, found: { value: Math.round(r.need * 10) / 10, unit: 'mm²/m' } });
  }
  for (const c of res.checks) if (c.ratio > 1 + 1e-9) {
    out.push({ id: 'concrete.slab:' + c.id, code: 'concrete.slab-shear', severity: 'error', about: [], source: { kind: 'engine', name: 'detailcraft.frame' }, clause: c.clause,
      msg: { key: 'frame.slab.prob.shear', vars: { Vu: fmt(c.demand / 1000), phiVc: fmt(c.capacity / 1000), plate: c.plate, set: c.set } },
      text: t('frame.slab.prob.shear', { Vu: fmt(c.demand / 1000), phiVc: fmt(c.capacity / 1000), plate: c.plate, set: c.set }),
      req: { op: '<=', value: Math.round(c.capacity / 100) / 10, unit: 'kN/m' }, found: { value: Math.round(c.demand / 100) / 10, unit: 'kN/m' } });
  }
  return out;
}

In detail

Method
Unit change by one division (N to kN, N·mm to kN·m, rad to mrad), then fixed decimals.
Number types
64-bit numbers until the text is written.
Tolerances
A value smaller than half of the last printed decimal is written as 0, so no cell reads −0.00.
Limits
Forces 0.01 kN, moments 0.01 kN·m, displacements 0.01 mm, rotations 0.001 mrad, utilization 0.1 %. Section properties are printed in cm units to 1 or 2 decimals.

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