Resources Numerical accuracy Calculation policy

Flow study: 32-bit steps in the mesh and the result fields

The largest speed, the pressure range, the values under the result probe and the surface pressure of a flow study, and the mesh the study is built from.

How the precision is kept

The part's mesh is copied into a Float32Array before the flow solver receives it, so opening hole areas, wall distances and the frontal area are computed from 32-bit coordinates. After the solve, velocity and pressure are stored per cell in Float32Array; the reported maximum speed and pressure range are taken from those arrays, and the probe reads them. Mass flow, pressure drop, force and coefficients do not pass through these arrays.

Open item. Take the maximum speed and the pressure range from the 64-bit state before the fields are stored, and hand the solver the mesh in a Float64Array.

Calculations under this policy

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

flow-study.js PartCraft 0.8.1 32-bit in part 1 calculation Reviewed

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

The mesh handed to the flow solver

32-bit in part

Builds the job for a flow study from the part and the study's settings.

The job's mesh is made with Float32Array.from(regen.mesh.positions). Everything the solver measures on the part (hole areas of openings, wall distances, the frontal area for drag and lift coefficients) is computed from those 32-bit coordinates. Face areas, normals and the bounding box are passed separately as 64-bit numbers.

  • flow-study.js
  • lines 139 to 169
  • PartCraft 0.8.1
/** Flow study + exact build → { job, problems, h, estimate }; check: no mesh copy. (experimental: accepted for old callers; no effect.) */
export function flowJob(st, regen, { check = false, experimental = false } = {}) {
  void experimental;
  const fl = st.flow || newFlow(), problems = [];
  if (!regen || !(regen.exact || regen.named) || !regen.mesh || !regen.faces || !regen.stats) return { job: null, problems: [t('study.flow.problem.need_exact')] };
  const gone = [], openings = fl.openings.map((o) => {
    const idx = o.faces.map((f) => findFaceIndex(regen.faces, f));
    if (idx.some((i) => i < 0)) gone.push(openingName(o));
    return { ...o, faces: idx.filter((i) => i >= 0) };
  });
  if (gone.length) problems.push(t('study.flow.problem.faces_gone', { names: gone.join(', ') }));
  if (fl.type === 'internal') {
    if (!openings.some((o) => o.role === 'inlet')) problems.push(t('study.flow.problem.no_inlet'));
    if (!openings.some((o) => o.role === 'outlet')) problems.push(t('study.flow.problem.no_outlet'));
    for (const o of openings) if (!o.faces.length) problems.push(t('study.flow.problem.no_faces', { name: openingName(o) }));
    const used = new Map();
    for (const o of openings) for (const f of o.faces) { if (used.has(f) && used.get(f) !== o.id) problems.push(t('study.flow.problem.face_twice', { name: openingName(o) })); used.set(f, o.id); }
  }
  const bbox = regen.stats.bbox, h = fl.h || autoCell(fl, bbox), est = cellEstimate(fl, bbox, h);
  if (est.cells > FLOW_LIMITS.gridCells) problems.push(t('study.flow.problem.grid_cells', { cells: est.cells.toLocaleString('en-US'), max: FLOW_LIMITS.gridCells.toLocaleString('en-US') }));
  const fluid = fluidOf(fl.fluid);
  const meshCopy = check ? null : { positions: Float32Array.from(regen.mesh.positions), indices: Uint32Array.from(regen.mesh.indices), triFace: regen.mesh.triFace ? Int32Array.from(regen.mesh.triFace) : null };
  const job = {
    type: fl.type, h, fluid: { name: fluid.name, density: fluid.density, viscosity: fluid.viscosity }, wall: fl.wall, model: fl.model || 'laminar', maxSteps: fl.maxSteps, tol: fl.tol,
    openings: openings.map((o) => ({ id: o.id, role: o.role, faces: o.faces, spec: o.spec, value: o.value, p: o.p })),
    faces: regen.faces.map((f) => ({ type: f.type, n: f.n ? f.n.slice() : null, o: f.o ? f.o.slice() : null, area: f.area })),
    ext: fl.type === 'external' ? { ...boxOf(fl.ext, bbox), speed: fl.ext.speed, dir: unitv(fl.ext.dir), lift: liftDir(fl.ext), refArea: fl.ext.refArea } : null,
    mesh: meshCopy, bbox: { min: bbox.min.slice(), max: bbox.max.slice() },
  };
  return { job, problems, h, estimate: est };
}
flow-worker.js PartCraft 0.8.1 32-bit in part 2 calculations Reviewed

Tests tools/partcraft-test/test-flow.mjs, tools/partcraft-test/test-flow-turb.mjs

Cell fields, maximum speed and pressure range

32-bit in part

Velocity and pressure of every cell in physical units, and the largest speed and lowest and highest pressure the results state.

v = u × c_u; p = p_ref + (ρ − 1) ÷ 3 × c_p; v_max = max |v|

The four arrays are Float32Array, and vmax, pmin and pmax are computed from values read back out of them. The reported maximum speed and pressure range therefore carry about 7 digits, not 15.

  • flow-worker.js
  • lines 134 to 143
  • PartCraft 0.8.1
const mask = new Uint8Array(ncell), vx = new Float32Array(ncell), vy = new Float32Array(ncell), vz = new Float32Array(ncell), p = new Float32Array(ncell), nut = turb ? new Float32Array(ncell) : null;
let vmax = 0, pmin = Infinity, pmax = -Infinity;
for (let e = 0; e < ncell; e++) { if (dom.kind[e] >= 1) mask[e] = 2; p[e] = NaN; }
for (let n = 0; n < lat.nf; n++) {
  const e = lat.cells[n];
  mask[e] = 1; vx[e] = S.ux[n] * cu; vy[e] = S.uy[n] * cu; vz[e] = S.uz[n] * cu;
  p[e] = pref + (S.rho[n] - 1) / 3 * cp;
  if (nut) nut[e] = S.nut[n] / sc.nu;
  vmax = Math.max(vmax, Math.hypot(vx[e], vy[e], vz[e])); pmin = Math.min(pmin, p[e]); pmax = Math.max(pmax, p[e]);
}

Pressure on the part's surface

32-bit in part

The static pressure at each vertex of the plot mesh, sampled a little way into the fluid.

p = Σ (wᵢ + 1e-6) pᵢ ÷ Σ (wᵢ + 1e-6) over the fluid cells around the sample point

The result is a Float32Array sampled from the 32-bit pressure field, and the probe shows it. Each trilinear weight has 1e-6 added, which pulls the value toward the plain mean of the cells by about that fraction. The sample point is tried at 0.75, 1.25, 0.35 and 1.75 cells along the vertex normal, so the value is near the surface, not on it.

  • flow-worker.js
  • lines 211 to 241
  • PartCraft 0.8.1
/** Static pressure at the part's surface: per vertex, sampled half a cell into the fluid along the vertex normal (trilinear over fluid cells; NaN where no fluid touches). */
export function surfacePressure(pm, F) {
  const P = pm.positions, I = pm.indices, nv = P.length / 3, nrm = new Float64Array(nv * 3), out = new Float32Array(nv);
  for (let t = 0; t < I.length; t += 3) {
    const a = 3 * I[t], b = 3 * I[t + 1], c = 3 * I[t + 2];
    const u = [P[b] - P[a], P[b + 1] - P[a + 1], P[b + 2] - P[a + 2]], w = [P[c] - P[a], P[c + 1] - P[a + 1], P[c + 2] - P[a + 2]];
    const n = [u[1] * w[2] - u[2] * w[1], u[2] * w[0] - u[0] * w[2], u[0] * w[1] - u[1] * w[0]];
    for (const v of [a, b, c]) { nrm[v] += n[0]; nrm[v + 1] += n[1]; nrm[v + 2] += n[2]; }
  }
  const { nx, ny, nz, h, origin, mask, p } = F;
  for (let v = 0; v < nv; v++) {
    const l = Math.hypot(nrm[3 * v], nrm[3 * v + 1], nrm[3 * v + 2]) || 1;
    let best = NaN;
    for (const off of [0.75, 1.25, 0.35, 1.75]) {
      const x = (P[3 * v] + off * h * nrm[3 * v] / l - origin[0]) / h - 0.5, y = (P[3 * v + 1] + off * h * nrm[3 * v + 1] / l - origin[1]) / h - 0.5, z = (P[3 * v + 2] + off * h * nrm[3 * v + 2] / l - origin[2]) / h - 0.5;
      const i = Math.floor(x), j = Math.floor(y), k = Math.floor(z), tx = x - i, ty = y - j, tz = z - k;
      let s = 0, ws = 0;
      for (let cn = 0; cn < 8; cn++) {
        const ii = i + (cn & 1), jj = j + ((cn >> 1) & 1), kk = k + ((cn >> 2) & 1);
        if (ii < 0 || jj < 0 || kk < 0 || ii >= nx || jj >= ny || kk >= nz) continue;
        const e = ii + nx * (jj + ny * kk);
        if (mask[e] !== 1) continue;
        const w = (cn & 1 ? tx : 1 - tx) * ((cn >> 1) & 1 ? ty : 1 - ty) * ((cn >> 2) & 1 ? tz : 1 - tz) + 1e-6;
        s += w * p[e]; ws += w;
      }
      if (ws > 0) { best = s / ws; break; }
    }
    out[v] = best;
  }
  return out;
}
flow-ui.js PartCraft 0.8.1 32-bit in part 2 calculations Reviewed

Tests tools/partcraft-test/browser-flow-turb.mjs

Result probe

32-bit in part

Shows the speed, velocity and pressure of the cell under the pointer, or the surface pressure.

speed = √(v_x² + v_y² + v_z²)

The values are read from the 32-bit field arrays and the 32-bit surface pressure. The cell is found with Math.floor of (point − origin) ÷ h, so the probe reports the cell's own value, not an interpolation.

  • flow-ui.js
  • lines 240 to 255
  • PartCraft 0.8.1
function probe(host, ev) {
  if (!F.probeEl || !fresh(host) || !ev) return;
  const objs = ['plane', 'surface'].map((k) => host.view.overlays && host.view.overlays.get(OV + k)).filter(Boolean);
  const hit = objs.length ? host.view.ray(ev.clientX, ev.clientY).intersectObjects(objs, false)[0] : null;
  if (!hit) { F.probeEl.textContent = t('study.flow.ui.probe.hint'); return; }
  const r = F.result, Fd = r.fields, p = hit.point.toArray(), at = p.map((v) => U.fmt(v, 2)).join(', ');
  if (hit.object.userData.flowPlot === 'plane') {
    const ijk = [0, 1, 2].map((a) => Math.min([Fd.nx, Fd.ny, Fd.nz][a] - 1, Math.max(0, Math.floor((p[a] - Fd.origin[a]) / Fd.h))));
    const e = ijk[0] + Fd.nx * (ijk[1] + Fd.ny * ijk[2]);
    if (Fd.mask[e] !== 1) { F.probeEl.textContent = t('study.flow.ui.probe.not_fluid', { at }); return; }
    F.probeEl.textContent = t('study.flow.ui.probe.cell', { speed: U.sig(Math.hypot(Fd.vx[e], Fd.vy[e], Fd.vz[e])), v: [Fd.vx[e], Fd.vy[e], Fd.vz[e]].map((v) => U.sig(v)).join(', '), p: U.sig(Fd.p[e]), at });
    return;
  }
  const v = U.valueAt(r.plotMesh, r.surfP, hit.faceIndex, p);
  F.probeEl.textContent = Number.isFinite(v) ? t('study.flow.ui.probe.surface', { p: U.sig(v), at }) : t('study.flow.ui.probe.dry', { at });
}

Lattice Mach number warning

32-bit in part

The fastest cell's speed as a share of the lattice speed of sound.

Ma = v_max × dt ÷ dx × √3

Uses vmax, which was read from the 32-bit velocity arrays. The value is only compared with 0.3 and shown in a warning.

  • flow-ui.js
  • line 526
  • PartCraft 0.8.1
const mach = r.vmax * r.lattice.dt / (r.mesh.h / 1000) * Math.sqrt(3);   // the fastest cell, in lattice speed of sound

In detail

Method
Unit conversion per cell, then a maximum and minimum over the stored values; trilinear sampling of the stored pressure for the surface.
Number types
Float32Array for the mesh copy, the cell fields and the surface pressure; the solver state itself is Float64Array.
Limits
These values carry about 7 significant digits. The surface pressure adds 1e-6 to every interpolation weight.

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