Resources Numerical accuracy Calculation policy

Simulation workspace: plot values and the hover probe

Prepares the values a result plot colors by and reads the result under the pointer.

How the precision is kept

Solving happens in worker files; this file prepares plot values and the probe. The mode-shape magnitude is stored in a Float32Array and the probe's weights are computed from the drawn Float32Array positions, so the probed numbers carry 32-bit accuracy. Result tables print the solver's own numbers through sim-ui.js sig (4 significant figures).

Open item. Compute the probe's weights from the solver's own plot-mesh positions and keep the mode-shape magnitude in a Float64Array.

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.

ws-simulation.js PartCraft 0.8.1 32-bit in part 8 calculations Reviewed

Tests tools/partcraft-test/browser-simulation.mjs

Element size of a live analysis level

64-bit

The element size the coarse and refined live solves use.

h = base size × level factor

The base is the study's own size, or the automatic size worked out from the part's volume by sim.js; one 64-bit multiplication by the level's factor.

  • ws-simulation.js
  • lines 88 to 93
  • PartCraft 0.8.1
/** The element size a level uses: the study's own (or the automatic one), times the level's factor. Adaptive refinement is for the refined level only. */
export function liveStudy(st, regen, factor) {
  const tet = mesherOf(st) === 'tet', vol = regen.stats.volume;
  const base = st.h || (tet ? tetAutoSize(vol) : autoSize(vol, AUTO_ELEMENTS.simulation));
  return { ...st, h: base * factor, ...(factor > 1 ? { adaptive: null } : {}) };
}

Mode shape magnitude

32-bit in part

The size of the mode shape's displacement at each plot vertex.

|u| = √(u_x² + u_y² + u_z²)

Math.hypot of the three components, stored in a Float32Array. The probe interpolates from this array, so the relative amplitude it states has 32-bit accuracy.

  • ws-simulation.js
  • lines 197 to 198
  • PartCraft 0.8.1
const u = r.shapes[S.mode], nv = u.length / 3, mag = new Float32Array(nv);
for (let v = 0; v < nv; v++) mag[v] = Math.hypot(u[3 * v], u[3 * v + 1], u[3 * v + 2]);

Safety factor plot values

32-bit, display only

The safety factor at each plot vertex, for coloring.

safety = min(yield ÷ stress, 100)

Stored in a Float32Array that only colors the plot, on a fixed legend from 0 to 5. The probe computes its own safety factor from the interpolated stress.

  • ws-simulation.js
  • line 208
  • PartCraft 0.8.1
const y = r.material.yield, sf = Float32Array.from(v.vm, (s) => (s > 0 ? Math.min(y / s, 100) : 100));

Legend range

64-bit

The least and largest value of a plotted result, for the legend.

One pass of comparisons; the values are returned unchanged. When all values are equal the top is set to the bottom + 1 so the legend has a span.

  • ws-simulation.js
  • line 213
  • PartCraft 0.8.1
const range = (values, unit, title, u) => { let lo = Infinity, hi = -Infinity; for (const x of values) { if (x < lo) lo = x; if (x > hi) hi = x; } if (!(hi > lo)) hi = lo + 1; return { values, lo, hi, unit, title, u }; };

Result under the pointer

32-bit in part

The stress, displacement, strain and safety factor at the point under the pointer.

value = w₀v₀ + w₁v₁ + w₂v₂; safety = yield ÷ stress

The weights come from the drawn positions (a Float32Array, displaced when the plot is deformed) and the ray's hit point; each value is the weighted sum of the three corner entries of the result arrays. The safety factor divides only when the interpolated stress is above 0.

  • ws-simulation.js
  • lines 254 to 272
  • PartCraft 0.8.1
/** The result under the pointer: a ray on the plot overlay, the value interpolated in its triangle. */
function probe(host, ev) {
  if (!S.probeEl || !fresh(host) || !ev) return;
  const ov = host.view.overlays && host.view.overlays.get('simulation-plot');
  const h = ov ? host.view.ray(ev.clientX, ev.clientY).intersectObject(ov, false)[0] : null;
  if (!h) { S.probeEl.textContent = t('study.ws.probe_hint'); return; }
  const mesh = S.result.plotMesh, tri = h.faceIndex, g = ov.geometry.attributes.position, I = mesh.indices, P = mesh.positions;
  const a = [0, 1, 2].map((k) => I[3 * tri + k]);
  const w = bary(a.map((i) => [g.getX(i), g.getY(i), g.getZ(i)]), h.point.toArray());
  const val = (arr) => w[0] * arr[a[0]] + w[1] * arr[a[1]] + w[2] * arr[a[2]];
  const p = [0, 1, 2].map((c) => w[0] * P[3 * a[0] + c] + w[1] * P[3 * a[1] + c] + w[2] * P[3 * a[2] + c]);
  const r = S.result, at = t('study.ws.probe_at', { xyz: p.map((v) => U.fmt(v, 2)).join(', ') });
  if (TH.handles(r)) { S.probeEl.textContent = TH.probeText(r, S, val, at); return; }
  if (r.kind === 'modal') { S.probeEl.textContent = t('study.ws.probe_modal', { a: U.fmt(val(field(host).values), 3), at }); return; }
  const vm = val(r.vertex.vm), d = val(r.vertex.disp);
  const sf = vm > 0 ? U.sig(r.material.yield / vm) : t('study.ws.over_100');
  S.probeEl.textContent = r.vertex.s1 ? t('study.ws.probe_static_principal', { vm: U.sig(vm), s1: U.sig(val(r.vertex.s1)), s3: U.sig(val(r.vertex.s3)), e: U.sig(val(r.vertex.strain)), d: U.sig(d), sf, at })
    : t('study.ws.probe_static', { vm: U.sig(vm), d: U.sig(d), sf, at });
}

Barycentric weights

64-bit

The three weights of a point inside a triangle.

w₁ = (d₁₁d₂₀ − d₀₁d₂₁) ÷ (d₀₀d₁₁ − d₀₁²); w₂ = (d₀₀d₂₁ − d₀₁d₂₀) ÷ (d₀₀d₁₁ − d₀₁²); w₀ = 1 − w₁ − w₂

Closed form from five dot products. The determinant d₀₀d₁₁ − d₀₁² subtracts nearly equal numbers for a sliver triangle; a determinant of exactly zero is replaced by 1.

  • ws-simulation.js
  • lines 273 to 279
  • PartCraft 0.8.1
function bary(a, p) {
  const e0 = a[1].map((c, k) => c - a[0][k]), e1 = a[2].map((c, k) => c - a[0][k]), e2 = p.map((c, k) => c - a[0][k]);
  const dot = (x, y) => x[0] * y[0] + x[1] * y[1] + x[2] * y[2];
  const d00 = dot(e0, e0), d01 = dot(e0, e1), d11 = dot(e1, e1), d20 = dot(e2, e0), d21 = dot(e2, e1), den = d00 * d11 - d01 * d01 || 1;
  const w1 = (d11 * d20 - d01 * d21) / den, w2 = (d00 * d21 - d01 * d20) / den;
  return [1 - w1 - w2, w1, w2];
}

Deformed plot positions

32-bit, display only

The drawn position of each plot vertex when the deformed shape is shown.

drawn position = position + scale × displacement

Written into a Float32Array for the drawn geometry only.

  • ws-simulation.js
  • lines 230 to 231
  • PartCraft 0.8.1
const P = mesh.positions, pos = def ? new Float32Array(P.length) : Float32Array.from(P);
if (def) for (let i = 0; i < P.length; i++) pos[i] = P[i] + S.scale * f.u[i];

Automatic deformation scale

32-bit, display only

The factor the displacements are multiplied by so the deformed shape can be seen.

raw = 0.08 × box diagonal ÷ largest |u|, rounded up to 1, 2, 5 or 10 × 10ᵉ

The result is rounded with toPrecision(2) and is 1 when there is no displacement. It only scales the drawn shape.

  • ws-simulation.js
  • lines 214 to 220
  • PartCraft 0.8.1
function autoScale(host, f) {
  const rg = host.regen, bb = rg.stats.bbox, diag = Math.hypot(bb.max[0] - bb.min[0], bb.max[1] - bb.min[1], bb.max[2] - bb.min[2]);
  let m = 0; for (let i = 0; i < f.u.length; i += 3) m = Math.max(m, Math.hypot(f.u[i], f.u[i + 1], f.u[i + 2]));
  if (!(m > 0)) return 1;
  const raw = 0.08 * diag / m, e = Math.floor(Math.log10(raw)), b = raw / 10 ** e;
  return +(([1, 2, 5, 10].find((s) => s >= b) || 10) * 10 ** e).toPrecision(2);
}

In detail

Method
Probe: ray cast on the plot overlay, barycentric weights of the hit point in its triangle, weighted sum of the three corner values. Legend range: least and largest value in one pass.
Number types
Float32Array for plot positions, mode-shape magnitude and safety-factor colors; result arrays as the solver returned them; 64-bit arithmetic.
Tolerances
bary replaces a determinant of exactly zero by 1. The live coarse level uses 4 times the study's element size.
Limits
Probe positions are printed to 2 decimals and values to 4 significant figures.

Approach: 32-bit step, flagged

One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.

Every policy that takes this approach