Resources Numerical accuracy Calculation policy

Automatic mesh refinement of tetrahedral studies

Decides which elements to refine after a solve, how small to make them, refines the mesh and stops when the estimated error meets the target.

How the precision is kept

Coordinates are Float64Array and all sizes are 64-bit. The loop is driven by the estimated error percentage of each solve, which is an estimate and not a bound, so the final accuracy is set by the estimate, the target and the pass and element limits. The per-element indicators it ranks by are 32-bit copies made in tetfea.js and tet-elements.js.

Calculations under this policy

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

tetadapt.js PartCraft 0.8.1 64-bit 5 calculations Reviewed

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

Longest edge of a tetrahedron

64-bit

The length of the longest of an element's six edges.

L = √max((Δx)² + (Δy)² + (Δz)²)

The maximum is taken over squared lengths and one square root follows, so the comparison between edges involves no rounding from the root.

  • tetadapt.js
  • lines 31 to 36
  • PartCraft 0.8.1
/** Longest edge of tet e of T (flat 4s) over coordinates A. */
function longestEdge(A, T, e) {
  let l = 0;
  for (const [i, j] of PAIRS) { const a = 3 * T[4 * e + i], b = 3 * T[4 * e + j]; l = Math.max(l, (A[a] - A[b]) ** 2 + (A[a + 1] - A[b + 1]) ** 2 + (A[a + 2] - A[b + 2]) ** 2); }
  return Math.sqrt(l);
}

Which elements to refine and to what size

64-bit

Marks the elements with the largest error and gives each a new target edge length.

ē = (target ÷ 100) √((U + Σe²) ÷ m); h_new = h × clamp((e_K ÷ ē)^(−1/2), 0.5, 0.7)

Elements are sorted by their indicator and their squares added until the sum reaches half the total or 30 % of the elements are marked. The ratio is guarded by a 1e-300 floor on ē; the size factor is clamped, so an indicator's exact value matters only for its rank. The indicators read here are 32-bit for a single field.

  • tetadapt.js
  • lines 98 to 114
  • PartCraft 0.8.1
/**
 * Which elements to refine and to what size → { target: Float64Array (Infinity = leave), marked, share (of Σe² marked) }.
 * err: e_K per element; energy U; errSq Σe²; tm: the linear mesh (element sizes); opts { target %, theta, maxFraction }.
 */
export function markForRefinement(tm, err, energy, errSq, { target = 5, theta = 0.5, maxFraction = 0.3, p = 2 } = {}) {
  const m = err.length, order = Array.from({ length: m }, (_, e) => e).sort((a, b) => err[b] - err[a]);
  const goal = theta * errSq, cap = Math.max(1, Math.floor(maxFraction * m)), out = new Float64Array(m).fill(Infinity);
  const ebar = target / 100 * Math.sqrt((energy + errSq) / m);
  let sum = 0, marked = 0;
  for (const e of order) {
    if (sum >= goal || marked >= cap) break;
    sum += err[e] * err[e]; marked++;
    const xi = err[e] / Math.max(ebar, 1e-300), f = Math.max(0.5, Math.min(0.7, xi ** (-1 / p)));
    out[e] = f * longestEdge(tm.nodes, tm.tets, e);
  }
  return { target: out, marked, share: errSq > 0 ? sum / errSq : 0 };
}

New boundary vertices onto the exact faces

64-bit

Moves a vertex created on the boundary onto the true surface of its faces, unless that would damage the elements around it.

accept when every tet keeps volume > 0 and min dihedral ≥ min(10°, before)

A move shorter than 1e-14 × the bounding-box diagonal is skipped. The vertex is moved, the tets around it are tested with tetVolume and tetQuality, and it is put back on the chord when any test fails; both outcomes are counted.

  • tetadapt.js
  • lines 80 to 85
  • PartCraft 0.8.1
const p = pts.get(v), q = projectToFaces(p, [...fs], faces, bvh, mesh.triFace), els = around.get(v) || [];
if (Math.hypot(q[0] - p[0], q[1] - p[1], q[2] - p[2]) < 1e-14 * bvh.diag) continue;
const before = Math.min(...els.map((e) => tetQuality(pts.a, quad(e)).minDih));
pts.a[3 * v] = q[0]; pts.a[3 * v + 1] = q[1]; pts.a[3 * v + 2] = q[2];
if (els.every((e) => tetVolume(pts.a, ...quad(e)) > 0 && tetQuality(pts.a, quad(e)).minDih >= Math.min(10, before))) projected++;
else { pts.a[3 * v] = p[0]; pts.a[3 * v + 1] = p[1]; pts.a[3 * v + 2] = p[2]; kept++; }

Volume of the refined mesh

64-bit

The volume of the refined linear mesh, kept in its statistics.

V = Σₑ volume of tet e

A running 64-bit sum of signed tet volumes in element order over a Float64Array copy of the coordinates.

  • tetadapt.js
  • line 91
  • PartCraft 0.8.1
let vol = 0; for (let e = 0; e < m; e++) vol += tetVolume(nodes, tets[4 * e], tets[4 * e + 1], tets[4 * e + 2], tets[4 * e + 3]);

When the refinement stops

64-bit

Compares each pass's estimated error with the target and decides whether to refine again.

stop when η ≤ target, or passes used, or elements > limit

The comparison uses the solver's 64-bit error percentage directly. The percentage written in the message is formatted with two decimals; the table row keeps the unrounded value.

  • tetadapt.js
  • lines 137 to 138
  • PartCraft 0.8.1
if (r.errorPct <= target) { stop = 'target'; message = t('study.adapt.msg.target', { err: r.errorPct.toFixed(2), target, n: i + 1 }); break; }
if (i + 1 >= passes) { stop = 'passes'; message = t('study.adapt.msg.passes', { n: passes, err: r.errorPct.toFixed(2), target }); break; }

In detail

Method
Dörfler marking: the elements with the largest indicators until their squares add up to θ = 0.5 of the total, at most 30 % of the elements. New size h × (ē ÷ e_K)^(1/2) clamped to 0.5 to 0.7 of the longest edge, with ē = target × √((U + Σe²) ÷ m). Conforming longest-edge bisection from tetmesh.js; new boundary vertices projected onto the exact faces when no tet around them inverts or drops below the smaller of 10° and its angle before.
Number types
64-bit numbers; Float64Array for coordinates and target sizes; Int32Array for elements.
Tolerances
An element is bisected while its longest edge exceeds 1.0001 × its target. A projection shorter than 1e-14 × the part's bounding-box diagonal is skipped. Default target 5 %, 1 to 10 passes (default 5), 150,000 elements; up to 9 bisection rounds per pass.
Limits
Every pass is a full re-solve. The loop ends on the target, the pass limit, the element limit or a failed pass, and reports which.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach