Resources Numerical accuracy Calculation policy

Advanced simulation add-on: result summary and color bands

Lists the headline numbers of a nonlinear or contact result and cuts the deformed plot mesh into color bands with a legend.

How the precision is kept

The numbers come from the solver's result object; this file only combines and formats them. Values handed to the panel are rounded to 4 (in places 3) significant digits with toPrecision; the result object keeps the full values. Band limits are evenly spaced between the least and largest finite value.

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.

addons/advanced-sim/lib/results.js PartCraft 0.8.1 64-bit 3 calculations Reviewed

Tests tools/addons-test/browser-advanced-sim.mjs

Rounding for the panel

64-bit

Rounds a result to a few significant digits before the panel words it.

toPrecision(p), then back to a number: the value given to the panel is rounded to p significant digits (4 by default). The result object is not changed.

  • addons/advanced-sim/lib/results.js
  • line 25
  • PartCraft 0.8.1
const sig = (v, p = 4) => (Number.isFinite(v) ? +(+v).toPrecision(p) : v);

Summary rows

64-bit

The load reached, largest displacement and stress, plastic strain, contact force and penetration of a result.

load = √(F_x² + F_y² + F_z²) × λ; penetration % = 100 × penetration ÷ h

The load is Math.hypot of the three load totals times the load factor reached. Each number passes through sig, so the panel receives 4 significant digits (3 for plastic strain, penetration and time). The penetration percent divides by h only when h > 0.

  • addons/advanced-sim/lib/results.js
  • lines 27 to 48
  • PartCraft 0.8.1
export function summary(r) {
  const rows = [];
  if (!r) return { rows, steps: [], ok: false };
  const n = r.calculix ? null : r.nonlinear || null, c = n && n.contact ? n.contact : null;
  if (r.calculix) rows.push({ key: r.calculix.stress ? 'res.ccx' : 'res.ccx_disp', vars: { n: r.calculix.nodes } });
  if (n) {
    rows.push(n.converged ? { key: 'res.converged', vars: { steps: n.steps.length } } : { key: 'res.failed', vars: { at: Math.round(100 * (n.failedAt ?? 0)), done: Math.round(100 * (n.lambda ?? 0)) } });
    rows.push({ key: 'res.iterations', vars: { n: r.iterations } });
  }
  const lt = r.loadTotal || [0, 0, 0];
  rows.push({ key: 'res.load', vars: { f: sig(Math.hypot(...lt) * (n ? n.lambda ?? 1 : 1)) } });
  if (r.maxDisp) rows.push({ key: 'res.disp', vars: { d: sig(r.maxDisp.value) } });
  if (r.maxVm && !(r.calculix && !r.calculix.stress)) rows.push({ key: 'res.vm', vars: { s: sig(r.maxVm.value), y: sig(r.material && r.material.yield) } });
  if (n) rows.push(n.maxPlastic > 0 ? { key: 'res.plastic', vars: { e: sig(n.maxPlastic, 3), pct: sig(100 * n.maxPlastic, 3), Et: sig(n.Et) } } : { key: 'res.elastic', vars: {} });
  if (c) {
    rows.push({ key: 'res.contact', vars: { n: c.active, f: sig(c.normalForce) } });
    rows.push({ key: c.h > 0 && c.maxPenetration > PEN_LIMIT * c.h ? 'res.pen.over' : 'res.pen', vars: { p: sig(c.maxPenetration, 3), h: sig(c.h, 3), pct: sig(c.h > 0 ? 100 * c.maxPenetration / c.h : 0, 3) } });
  }
  if (r.summary) rows.push({ key: 'res.mesh', vars: { e: r.summary.elements, n: r.summary.nodes } });
  if (Number.isFinite(r.ms)) rows.push({ key: 'res.time', vars: { s: sig(r.ms / 1000, 3) } });
  return { rows, steps: n ? n.steps.map((s) => ({ lambda: s.lambda, iterations: s.iterations, plastic: s.plastic, residual: s.residual })) : [], ok: n ? !!n.converged : true };
}

Color bands and legend

64-bit

Splits the plot mesh into bands of equal value range and gives each band's limits.

band = floor((v − lo) ÷ (hi − lo) × n), clamped to 0 … n − 1; limits: lo + (hi − lo) × i ÷ n

lo and hi are the least and largest finite value; a zero range is widened first, so the division is safe. A triangle's band comes from the mean of its three corner values. The shape scale is 5 % of the part's diagonal ÷ the largest displacement, rounded to 2 significant digits and never below 1.

  • addons/advanced-sim/lib/results.js
  • lines 55 to 88
  • PartCraft 0.8.1
export function bands(r, field = 'vm', { n = 8, scale = null, beside = true } = {}) {
  const pm = r && r.plotMesh, val = r && r.vertex && r.vertex[field], U = r && r.vertex && r.vertex.u;
  if (!pm || !val) return null;
  const P = pm.positions, I = pm.indices, nv = P.length / 3, nt = I.length / 3;
  let lo = Infinity, hi = -Infinity;
  for (let i = 0; i < val.length; i++) { const v = val[i]; if (Number.isFinite(v)) { if (v < lo) lo = v; if (v > hi) hi = v; } }
  if (!(hi >= lo)) return null;
  if (hi - lo < 1e-12 * Math.max(1, Math.abs(hi))) hi = lo + 1e-12 * Math.max(1, Math.abs(lo)) + 1e-30;
  // the size of the part, and the shape's scale: the largest displacement drawn as 5 % of the part's size unless given
  const mn = [Infinity, Infinity, Infinity], mx = [-Infinity, -Infinity, -Infinity];
  for (let i = 0; i < nv; i++) for (let k = 0; k < 3; k++) { const x = P[3 * i + k]; if (x < mn[k]) mn[k] = x; if (x > mx[k]) mx[k] = x; }
  const size = Math.hypot(mx[0] - mn[0], mx[1] - mn[1], mx[2] - mn[2]) || 1;
  let umax = 0; if (U) for (let i = 0; i < nv; i++) umax = Math.max(umax, Math.hypot(U[3 * i], U[3 * i + 1], U[3 * i + 2]));
  const k = scale !== null ? scale : (umax > 0 ? Math.max(1, +((0.05 * size) / umax).toPrecision(2)) : 1);
  const offset = beside ? [(mx[0] - mn[0]) * 1.25 + 0.1 * size, 0, 0] : [0, 0, 0];
  const band = (v) => Math.min(n - 1, Math.max(0, Math.floor((v - lo) / (hi - lo) * n)));
  const groups = Array.from({ length: n }, () => ({ pos: [], idx: [], map: new Map() }));
  for (let t = 0; t < nt; t++) {
    const a = I[3 * t], b = I[3 * t + 1], c = I[3 * t + 2];
    const g = groups[band((val[a] + val[b] + val[c]) / 3)];
    for (const q of [a, b, c]) {
      let id = g.map.get(q);
      if (id === undefined) {
        id = g.map.size; g.map.set(q, id);
        for (let d = 0; d < 3; d++) g.pos.push(P[3 * q + d] + (U ? k * U[3 * q + d] : 0) + offset[d]);
      }
      g.idx.push(id);
    }
  }
  const legend = groups.map((_, i) => ({ lo: lo + (hi - lo) * i / n, hi: lo + (hi - lo) * (i + 1) / n, color: rampColor((i + 0.5) / n) }));
  const bodies = [];
  groups.forEach((g, i) => { if (g.idx.length) bodies.push({ name: 'band ' + (i + 1), color: legend[i].color, mesh: { positions: g.pos, indices: g.idx } }); });
  return { bodies, legend, scale: k, offset, lo, hi, triangles: nt };
}

In detail

Method
summary: load = |load total| × load factor reached; penetration as a percent of the element size. bands: band index = floor((v − lo) ÷ (hi − lo) × n); a triangle takes the band of the mean of its three corner values.
Number types
64-bit arithmetic on the solver's arrays (their type is set by the worker that made them); band positions in plain arrays.
Tolerances
A range narrower than 1e-12 × max(1, |hi|) is widened by 1e-12 × max(1, |lo|) + 1e-30. Penetration is flagged above PEN_LIMIT × h (PEN_LIMIT is imported from nlstudy.js).
Limits
Band bodies pass through addon-bodies.js, which rounds their coordinates to 0.001 mm.

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