Resources Numerical accuracy Calculation policy

Stress, displacement and error results on tetrahedra

Turns the solved displacements into stresses, principal stresses, strain, reactions, the reported maxima, the safety factor and the estimated error of the mesh.

How the precision is kept

Stresses are computed in 64-bit at the Gauss points, extrapolated to the nodes and averaged in Float64Array. The nodal von Mises stress, principal stresses, strain and displacement size are then stored in Float32Array, and the reported maximum von Mises stress, maximum displacement and minimum safety factor are read back from those 32-bit values. The largest and smallest principal stress, the element maximum, the reactions and the error percentage are taken from 64-bit values.

Open item. Take the maximum von Mises stress, the maximum displacement and the safety factor from the 64-bit values before they are stored in the 32-bit arrays.

Calculations under this policy

11 calculations in 2 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.

tetfea.js PartCraft 0.8.1 32-bit in part 10 calculations Reviewed

Tests tools/partcraft-test/test-tet.mjs

Principal stresses

64-bit

The largest, middle and smallest principal stress of a stress state.

s₁ = m + 2q cos φ, s₃ = m + 2q cos(φ + 2π ÷ 3), s₂ = 3m − s₁ − s₃; φ = acos(det ÷ 2q³) ÷ 3

Closed-form trigonometric solution on the deviatoric stress, so the mean is removed before the squares are formed. The acos argument is clamped to [−1, 1]; a state with deviatoric size below 1e-14 × |mean| returns three equal values. The middle value comes from the trace, so its error is the sum of the other two.

  • tetfea.js
  • lines 543 to 551
  • PartCraft 0.8.1
/** Principal stresses (descending) of a symmetric stress [xx, yy, zz, xy, yz, zx]. */
export function principal(s) {
  const [a, b, c, d, e, f] = s, m = (a + b + c) / 3, A = a - m, B = b - m, C = c - m;
  const q2 = (A * A + B * B + C * C + 2 * (d * d + e * e + f * f)) / 6, q = Math.sqrt(q2);
  if (q < 1e-14 * (Math.abs(m) + 1e-300)) return [m, m, m];
  const det = (A * (B * C - e * e) - d * (d * C - e * f) + f * (d * e - B * f)) / (q2 * q) / 2, phi = Math.acos(Math.max(-1, Math.min(1, det))) / 3;
  const s1 = m + 2 * q * Math.cos(phi), s3 = m + 2 * q * Math.cos(phi + 2 * Math.PI / 3);
  return [s1, 3 * m - s1 - s3, s3];
}

Equivalent strain

64-bit

The von Mises equivalent strain from the six engineering strains.

ε_eq = √(⅔ (a² + b² + c² + (γ_xy² + γ_yz² + γ_zx²) ÷ 2)), a, b, c = normal strains − mean

One 64-bit expression: the mean is subtracted from the normal strains, then a sum of squares, so the square root never sees a negative number.

  • tetfea.js
  • lines 552 to 553
  • PartCraft 0.8.1
/** Von Mises equivalent strain √(2/3 e:e) of engineering strains [xx, yy, zz, γxy, γyz, γzx]. */
export const eqStrain = (e) => { const m = (e[0] + e[1] + e[2]) / 3, a = e[0] - m, b = e[1] - m, c = e[2] - m; return Math.sqrt(2 / 3 * (a * a + b * b + c * c + (e[3] * e[3] + e[4] * e[4] + e[5] * e[5]) / 2)); };

Gauss points to corners

64-bit

The value at an element corner of the linear field that passes through the four Gauss-point values.

cᵢ = (gᵢ − 0.1381966… × Σg) ÷ (0.5854101… − 0.1381966…)

The two rule coordinates are 16-digit constants and their difference, about 0.447, is the only divisor, so no near-zero division occurs. The resulting weights are about 1.93 on the nearest Gauss point and −0.31 on each of the other three, so the corner value is an extrapolation and not an average.

  • tetfea.js
  • lines 564 to 566
  • PartCraft 0.8.1
const ex = (vals, i) => { // linear field from 4 Gauss values → value at corner i (c_i)
  let S = 0; for (let k = 0; k < 4; k++) S += vals[k]; return (vals[i] - GB * S) / (GA - GB);
};

Largest von Mises stress in an element

64-bit

The unaveraged maximum von Mises stress of each element and the largest over the mesh.

max over the 4 Gauss points and the 4 extrapolated corners

The mesh-wide maximum and its element are taken from the 64-bit value before that value is written into the 32-bit per-element array, so the reported element maximum is 64-bit.

  • tetfea.js
  • lines 579 to 587
  • PartCraft 0.8.1
let emax = 0;
for (let a = 0; a < 10; a++) {
  const [i, j] = a < 4 ? [a, a] : EDGES[a - 4], nd = el[a];
  for (let r = 0; r < 6; r++) { sigN[6 * nd + r] += (cs[6 * i + r] + cs[6 * j + r]) / 2; epsN[6 * nd + r] += (ce[6 * i + r] + ce[6 * j + r]) / 2; }
  cnt[nd]++;
  if (a < 4) emax = Math.max(emax, vonMises(cs.subarray(6 * a, 6 * a + 6)));
}
for (let p = 0; p < 4; p++) emax = Math.max(emax, vonMises(sg[p]));
vmElem[e] = emax; if (emax > elMax) { elMax = emax; elMaxAt = e; }

Nodal stress, strain and displacement, and their maxima

32-bit in part

The values the plot colors by and the maxima the result reports.

σ at a node = mean over its elements; |u| = √(u_x² + u_y² + u_z²); maximum over the nodes

The five nodal arrays are Float32Array. The maximum von Mises stress and the maximum displacement are compared and taken from the array after the value was stored, so both carry 32-bit precision. The largest first and smallest third principal stress are taken from the 64-bit values before storage.

  • tetfea.js
  • lines 589 to 599
  • PartCraft 0.8.1
const vm = new Float32Array(n), s1 = new Float32Array(n), s3 = new Float32Array(n), strain = new Float32Array(n), disp = new Float32Array(n);
let vMax = 0, vAt = -1, dMax = 0, dAt = -1, s1Max = -Infinity, s3Min = Infinity;
for (let a = 0; a < n; a++) {
  for (let r = 0; r < 6; r++) { sigN[6 * a + r] /= cnt[a] || 1; epsN[6 * a + r] /= cnt[a] || 1; }
  const s = sigN.subarray(6 * a, 6 * a + 6), pr = principal(s);
  vm[a] = vonMises(s); s1[a] = pr[0]; s3[a] = pr[2]; strain[a] = eqStrain(epsN.subarray(6 * a, 6 * a + 6));
  disp[a] = Math.hypot(u[3 * a], u[3 * a + 1], u[3 * a + 2]);
  if (vm[a] > vMax) { vMax = vm[a]; vAt = a; }
  if (disp[a] > dMax) { dMax = disp[a]; dAt = a; }
  if (pr[0] > s1Max) s1Max = pr[0]; if (pr[2] < s3Min) s3Min = pr[2];
}

Error indicator per element

64-bit

How far the averaged stress field is from each element's own stresses, in the energy norm.

e_K² = ∫ (σ* − σₕ)ᵀ C⁻¹ (σ* − σₕ) dV; U = ∫ σₕᵀ C⁻¹ σₕ dV

The squared error and the energy are accumulated in 64-bit and feed the reported percentage. The per-element indicator is stored as √max(0, s) in a Float32Array; that 32-bit copy is what the refinement step ranks elements by.

  • tetfea.js
  • lines 601 to 614
  • PartCraft 0.8.1
const err = new Float32Array(m);
let errSum = 0, energy = 0;
const Cinv = (s) => { const [a, b, c, d, e2, f2] = s; return ((a * a + b * b + c * c) - 2 * nu * (a * b + b * c + c * a) + 2 * (1 + nu) * (d * d + e2 * e2 + f2 * f2)) / E; };
for (let e = 0; e < m; e++) {
  let s = 0, w0 = 0;
  for (let a = 0; a < 10; a++) el[a] = model.tets10[10 * e + a];
  for (let p = 0; p < 4; p++) {
    const det = jac(X, el, SH4[p].dN, g), w = GP4[p][3] * det, N = SH4[p].N, sh = elemSig.subarray((4 * e + p) * 6, (4 * e + p) * 6 + 6), d = [0, 0, 0, 0, 0, 0];
    for (let a = 0; a < 10; a++) for (let r = 0; r < 6; r++) d[r] += N[a] * sigN[6 * el[a] + r];
    for (let r = 0; r < 6; r++) d[r] -= sh[r];
    s += w * Cinv(d); w0 += w * Cinv(sh);
  }
  err[e] = Math.sqrt(Math.max(0, s)); errSum += s; energy += w0;
}

Support reactions

64-bit

The total reaction force and the reaction of each support group.

r = K u − f at the supported nodes, kept along the supported directions; R = Σ r

K u is computed once with the unconstrained stiffness and f is subtracted per component; where the two are nearly equal the difference loses digits, and its accuracy is that of the iterative solve. Sums are 64-bit in support order.

  • tetfea.js
  • lines 616 to 623
  • PartCraft 0.8.1
const ku = symMul(model, K, u, new Float64Array(model.N));
const reaction = [0, 0, 0], nodeR = new Map();
for (let q = 0; q < cons.nodes.length; q++) {
  const d = 3 * cons.nodes[q], r = [ku[d] - f[d], ku[d + 1] - f[d + 1], ku[d + 2] - f[d + 2]], rr = [0, 0, 0];
  for (const dir of cons.dirs[q]) { const t = r[0] * dir[0] + r[1] * dir[1] + r[2] * dir[2]; for (let a = 0; a < 3; a++) rr[a] += t * dir[a]; }
  for (let a = 0; a < 3; a++) reaction[a] += rr[a];
  nodeR.set(cons.nodes[q], rr);
}

Reported maxima, safety factor and error percentage

32-bit in part

The numbers the study result states: error percentage, maximum stress and displacement, principal stress range and minimum safety factor.

η = 100 √(Σe² ÷ (U + Σe²)); safety = yield ÷ maximum von Mises

The error percentage is computed from the 64-bit sums. The maximum von Mises stress and maximum displacement are the values read back from the 32-bit nodal arrays, and the safety factor divides the yield stress by that 32-bit maximum.

  • tetfea.js
  • lines 630 to 635
  • PartCraft 0.8.1
errorPct: 100 * Math.sqrt(errSum / Math.max(1e-300, energy + errSum)), errSq: errSum, energy,   // energy = ∫σh:C⁻¹:σh (twice the strain energy)
maxVm: { value: vMax, node: vAt, at: at(vAt) },
maxVmElem: { value: elMax, at: elCentre(elMaxAt) },
maxDisp: { value: dMax, node: dAt, at: at(dAt), vec: dAt >= 0 ? [u[3 * dAt], u[3 * dAt + 1], u[3 * dAt + 2]] : null },
maxS1: s1Max, minS3: s3Min,
minSafety: vMax > 0 ? yieldStress / vMax : Infinity,

Error estimate of several fields

64-bit

Combines the error indicators of mode shapes, or of a buckling pre-stress and its modes, into one estimate.

e_K² = Σ_f e_K,f² ÷ U_f; η = 100 √(Σe² ÷ (n + Σe²))

The global percentage uses each field's 64-bit Σe² and energy. The per-element values are rebuilt in a Float64Array from the fields' 32-bit indicators, so they carry 32-bit input precision; they only rank elements for refinement.

  • tetfea.js
  • lines 764 to 782
  • PartCraft 0.8.1
/**
 * 0.4 P5: the error estimate of several fields at once (mode shapes, a buckling study's pre-stress and modes). Each field's
 * Zienkiewicz–Zhu indicators (tetPostprocess) are scaled to its own unit energy, so every field counts alike whatever its
 * amplitude: e_K² = Σ_f e_K,f² / U_f, Σe² = Σ_f Σe²_f / U_f, U = the number of fields, errorPct = 100·√(Σe² / (U + Σe²)) —
 * the RMS of the fields' own estimates for small errors. → { err, errSq, energy, errorPct, errorParts: [% per field] }
 */
export function combineEstimates(parts) {
  const m = parts[0].err.length, err = new Float64Array(m), pct = [];
  let errSq = 0;
  for (const q of parts) {
    const U = Math.max(q.energy, 1e-300);
    for (let e = 0; e < m; e++) err[e] += q.err[e] * q.err[e] / U;
    errSq += q.errSq / U;
    pct.push(q.errorPct);
  }
  for (let e = 0; e < m; e++) err[e] = Math.sqrt(err[e]);
  const energy = parts.length;
  return { err, errSq, energy, errorPct: 100 * Math.sqrt(errSq / (energy + errSq)), errorParts: pct };
}

Mesh volume and mass

64-bit

The volume and mass of the meshed part shown with the study.

volume = Σₑ Σₚ wₚ det Jₚ; mass in kg = volume × ρ (t/mm³) × 1000

The volume is a running 64-bit sum over all elements with the 4-point rule; the mass is two multiplications. The density passed in was already converted by tonnesPerMm3 in materials.js.

  • tetfea.js
  • lines 720 to 725
  • PartCraft 0.8.1
/** Summary of a meshed model for the UI. */
export function meshSummary(tm, t10, model, rho) {
  let vol = 0; const m = model.tets10.length / 10, g = new Float64Array(30), el = new Int32Array(10);
  for (let e = 0; e < m; e++) { for (let a = 0; a < 10; a++) el[a] = model.tets10[10 * e + a]; for (let p = 0; p < 4; p++) vol += GP4[p][3] * jac(model.X, el, SH4[p].dN, g); }
  return { mesher: 'tet', elements: m, nodes: model.n, dofs: model.N, h: tm.stats.h, volume: vol, mass: vol * rho * 1000, quality: tm.quality, background: tm.stats.background, curvedEdges: t10.stats.curvedEdges, straightKept: t10.stats.straightKept, snapped: tm.stats.snapped, steiner: tm.stats.steiner, exactFaces: tm.stats.exactFaces, tessFaces: tm.stats.tessFaces, tessTypes: tm.stats.tessTypes };
}
tet-elements.js PartCraft 0.8.1 64-bit 1 calculation Reviewed

Tests tools/partcraft-test/test-tet.mjs

Error estimate of a heat solution

64-bit

The estimated error of a steady temperature solution, per element and as a percentage.

e_K² = ∫ (q* − qₕ)·(q* − qₕ) ÷ k dV; η = 100 √(Σe² ÷ (U + Σe²))

Fluxes at the four Gauss points are extrapolated to the corners, averaged at the nodes and compared with the element's own, all in Float64Array; the percentage uses the 64-bit sums with the denominator floored at 1e-300. The per-element indicator is stored in a Float32Array, which the refinement step ranks elements by.

  • tet-elements.js
  • lines 145 to 187
  • PartCraft 0.8.1
/**
 * Zienkiewicz–Zhu error estimate of a heat solution on tets: the flux −k∇T at the 4 Gauss points, extrapolated linearly to
 * each element's corners (midsides: the mean of their two) and averaged at the nodes (q*); per element
 * e_K² = ∫ (q* − qh)·(q* − qh) / k dV (the energy norm of the heat problem), U = ∫ qh·qh / k dV, and the global
 * η = √(Σe²/(U + Σe²)) as a %. → { err (per element e_K), errSq, energy, errorPct }. k in solver units (W/(mm·K)).
 */
export function heatErrorZZ(model, T, k) {
  const m = model.tets10.length / 10, n = model.n, X = new Float64Array(30), qg = new Float64Array(12), qN = new Float64Array(3 * n), cnt = new Float64Array(n);
  const elQ = new Float64Array(m * 12);
  const exq = (vals, i) => { let S = 0; for (let p = 0; p < 4; p++) S += vals[p]; return (vals[i] - GB * S) / (GA - GB); };
  const tmp = new Float64Array(4), cq = new Float64Array(12);
  const elemX = (e) => { for (let a = 0; a < 10; a++) { const nd = model.tets10[10 * e + a]; X[3 * a] = model.X[3 * nd]; X[3 * a + 1] = model.X[3 * nd + 1]; X[3 * a + 2] = model.X[3 * nd + 2]; } };
  for (let e = 0; e < m; e++) {
    elemX(e);
    for (let p = 0; p < 4; p++) {
      jac(X, ID10, SH4[p].dN, g30);
      let gx = 0, gy = 0, gz = 0; for (let a = 0; a < 10; a++) { const t = T[model.tets10[10 * e + a]]; gx += g30[3 * a] * t; gy += g30[3 * a + 1] * t; gz += g30[3 * a + 2] * t; }
      qg[3 * p] = -k * gx; qg[3 * p + 1] = -k * gy; qg[3 * p + 2] = -k * gz;
    }
    elQ.set(qg, 12 * e);
    for (let c = 0; c < 3; c++) { for (let p = 0; p < 4; p++) tmp[p] = qg[3 * p + c]; for (let i = 0; i < 4; i++) cq[3 * i + c] = exq(tmp, i); }
    for (let a = 0; a < 10; a++) {
      const [i, j] = a < 4 ? [a, a] : EDGES10[a - 4], nd = model.tets10[10 * e + a];
      for (let c = 0; c < 3; c++) qN[3 * nd + c] += (cq[3 * i + c] + cq[3 * j + c]) / 2;
      cnt[nd]++;
    }
  }
  for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) qN[3 * i + c] /= cnt[i] || 1;
  const err = new Float32Array(m);
  let errSq = 0, energy = 0;
  for (let e = 0; e < m; e++) {
    elemX(e);
    let s = 0;
    for (let p = 0; p < 4; p++) {
      const w = GP4[p][3] * jac(X, ID10, SH4[p].dN, g30), N = SH4[p].N;
      let d0 = 0, d1 = 0, d2 = 0; for (let a = 0; a < 10; a++) { const nd = model.tets10[10 * e + a]; d0 += N[a] * qN[3 * nd]; d1 += N[a] * qN[3 * nd + 1]; d2 += N[a] * qN[3 * nd + 2]; }
      const q0 = elQ[12 * e + 3 * p], q1 = elQ[12 * e + 3 * p + 1], q2 = elQ[12 * e + 3 * p + 2];
      s += w * ((d0 - q0) ** 2 + (d1 - q1) ** 2 + (d2 - q2) ** 2) / k; energy += w * (q0 * q0 + q1 * q1 + q2 * q2) / k;
    }
    err[e] = Math.sqrt(s); errSq += s;
  }
  return { err, errSq, energy, errorPct: 100 * Math.sqrt(errSq / Math.max(1e-300, energy + errSq)) };
}

In detail

Method
σ = E D B u at 4 Gauss points; linear extrapolation to the corners, midsides as the mean of their two corners, average at each node. Principal stresses by the trigonometric solution of the cubic. Zienkiewicz–Zhu error indicator in the energy norm, per element and as a global percentage. Reactions from K u − f in the supported directions.
Number types
64-bit numbers and Float64Array for stresses, strains, reactions and the error sums. Float32Array for the nodal von Mises, principal stress, strain and displacement fields, the per-element maximum and the per-element error indicator.
Tolerances
Principal stresses: when the deviatoric size is below 1e-14 × |mean stress| three equal values are returned; the acos argument is clamped to [−1, 1]. Error percentage: the denominator is floored at 1e-300.
Limits
The error percentage is an estimate, not a bound; the tetadapt.js header says it can read low on a coarse mesh and grows without end at singular corners. Stresses at a node are an average over the elements around it.

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