Sets inlet speeds from the given velocity or flow rate, and turns the lattice result into mass flow, pressure drop, force, drag and lift coefficients and Reynolds number.
How the precision is kept
These numbers are computed in 64-bit from the solver's Float64Array state, not from the 32-bit plot fields. Their accuracy is set by the lattice method and by when the run is stopped: the residual tolerance plus a 1e-4 relative change of the pressure drop or drag between checks. Pressure is proportional to density minus one, a subtraction of nearly equal numbers.
Calculations under this policy
8 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.
Each inlet's flow rate, mean speed and the speed set on its lattice links.
Q = v × A (or the given Q); U = Q ÷ A; link speed = Q × (A_face ÷ A) ÷ (lattice link area × dx²); D = √(4A ÷ π)
64-bit. The flow is set on the opening's true area in mm² × 1e-6, and spread over the staircase of lattice links so the total matches whatever the staircase. An inlet with no lattice links raises an error instead of dividing by zero.
flow-worker.js
lines 50 to 58
PartCraft 0.8.1
const opening = internal ? job.openings.map((o, oi) => {
const gs = dom.openingGroups[oi], A = gs.reduce((s, g) => s + area[g], 0), At = gs.reduce((s, g) => s + groups[g].area, 0);
const rec = { id: o.id, role: o.role, groups: gs, modes: dom.faceMode[oi], area: At * 1e-6, cellArea: A * dx * dx };
if (o.role === 'inlet') {
if (!(A > 0) || gs.some((g) => !(area[g] > 0))) throw new Error(t('study.flow.err.inlet_dry', { n: o.id.replace(/\D/g, '') }));
rec.Q = o.spec === 'velocity' ? o.value * At * 1e-6 : o.value;
rec.U = rec.Q / (At * 1e-6);
rec.gU = gs.map((g) => rec.Q * (groups[g].area / At) / (area[g] * dx * dx)); // speed on the lattice links of each face
rec.Dc = Math.sqrt(4 * At * 1e-6 / Math.PI) / dx;
Speed and pressure scales
64-bit
The factors between lattice and physical speed and pressure, and the density set at each outlet.
64-bit closed form. The reference pressure is the lowest outlet pressure; a spread of outlet densities above 5 percent is refused.
flow-worker.js
lines 80 to 90
PartCraft 0.8.1
const cu = sc.cu, cp = rhoP * cu * cu; // m/s and Pa per lattice unit
// set speeds and pressures on the links
const ulat = new Float64Array(ng), rhow = new Float64Array(ng).fill(1);
let pref = 0;
if (internal) {
const outs = opening.filter((o) => o.role === 'outlet');
pref = Math.min(...outs.map((o) => o.p));
for (const o of opening) o.groups.forEach((g, gi) => { if (o.role === 'inlet') ulat[g] = o.gU[gi] / cu; else rhow[g] = 1 + 3 * (o.p - pref) / cp; });
const dr = Math.max(...rhow) - 1;
if (dr > 0.05) throw new Error(t('study.flow.err.outlet_spread', { pct: (100 * dr).toFixed(0), pa: (0.05 * cp / 3).toPrecision(2) }));
} else ulat[1] = Uref / cu;
Second steady-state test
64-bit
Watches the pressure drop or the drag force between checks before the run is accepted as steady.
|q − q_prev| ÷ max(|q|, 1e-30) < 1e-4 on two checks in a row
The watched quantity is a 64-bit mean density difference (internal) or force along the flow (external). The run stops when this relative change is under 1e-4 twice running and the velocity residual is under its tolerance, so the reported pressure drop or drag may still have been changing by up to 1e-4 of its value per check.
flow-worker.js
lines 103 to 120
PartCraft 0.8.1
const watch = () => {
const st = stats();
if (!internal) return { q: dotv(st.force, job.ext.dir), st };
let pin = 0, wi = 0, pout = 0, wo = 0;
for (let k = 0; k < L.n; k++) { const g = L.g[k]; if (!g) continue; const r = S.rho[L.cell[k]]; if (groups[g].type === 'velocity') { pin += r; wi++; } else if (groups[g].type === 'pressure') { pout += r; wo++; } }
return { q: (wi ? pin / wi : 0) - (wo ? pout / wo : 0), st };
};
let last = null, steady = 0, lastSent = 0;
const qTol = 1e-4;
const minSteps = Math.min(Math.floor(job.maxSteps / 2), Math.ceil(Lc / Math.max(sc.u, 1e-6)), 20000); // at least one pass through
const res0 = { v: null };
const r = run(S, {
maxSteps: job.maxSteps, tol: job.tol, every: 100, minSteps, accept: () => steady >= 2, ramp: turb && internal ? Math.min(Math.floor(job.maxSteps / 10), Math.ceil(0.5 * Lc / Math.max(sc.u, 1e-6))) : 0,
onCheck: ({ step, res }) => {
const w = watch();
const dq = last === null ? Infinity : Math.abs(w.q - last) / Math.max(Math.abs(w.q), 1e-30);
last = w.q;
steady = dq < qTol ? steady + 1 : 0;
Mass flow, mean pressure and pressure drop
64-bit
The mass and volume flow through each opening, its mean pressure, and the pressure drop between inlets and outlets.
ṁ = lattice mass × ρ dx³ ÷ dt; p = p_ref + (mean ρ − 1) ÷ 3 × c_p; Δp = flow-weighted mean inlet p − outlet p
Computed from the 64-bit density state. The pressure is proportional to ρ − 1, where ρ stays within a few percent of 1, so its absolute rounding error is about 1e-16 × c_p ÷ 3 and a small pressure difference keeps fewer relative digits.
flow-worker.js
lines 144 to 160
PartCraft 0.8.1
const mdot = (lm) => lm * rhoP * dx * dx * dx / sc.dt; // lattice mass per step → kg/s
// per opening: mass flow (+ in), mean static pressure over the fluid cells next to it
const meanP = (gs) => { let s = 0, w = 0; const set = new Set(gs); for (let k = 0; k < L.n; k++) if (set.has(L.g[k])) { s += S.rho[L.cell[k]]; w++; } return w ? pref + (s / w - 1) / 3 * cp : NaN; };
let massIn = 0, massOut = 0;
const openings = [];
if (internal) for (const o of opening) {
const m = o.groups.reduce((s, g) => s + mdot(st.mass[g]), 0);
if (m > 0) massIn += m; else massOut -= m;
openings.push({ id: o.id, role: o.role, mass: m, flow: m / rhoP, p: meanP(o.groups), area: o.area, cellArea: o.cellArea, U: o.U ?? null, Q: o.Q ?? null, modes: o.modes, setP: o.p ?? null });
} else {
const mi = mdot(st.mass[1]), mo = mdot(st.mass[2]);
massIn = Math.max(0, mi) + Math.max(0, mo); massOut = Math.max(0, -mi) + Math.max(0, -mo);
openings.push({ id: 'inflow', role: 'inflow', mass: mi, flow: mi / rhoP, p: meanP([1]) }, { id: 'outflow', role: 'outflow', mass: mo, flow: mo / rhoP, p: meanP([2]) });
}
const wallLeak = mdot(st.mass[0]);
const flowW = (role) => { let s = 0, w = 0; for (const o of openings) if (o.role === role && Number.isFinite(o.p)) { const wt = Math.abs(o.mass) || 1e-30; s += o.p * wt; w += wt; } return w ? s / w : NaN; };
const dp = internal ? flowW('inlet') - flowW('outlet') : openings[0].p - openings[1].p;
Force, drag and lift coefficients
64-bit
The force on the part in newtons and, for external flow, drag, lift and their coefficients.
F = lattice force × ρ dx⁴ ÷ dt²; C_d = F · d ÷ (½ ρ U² A)
64-bit. For turbulent runs the force is a momentum balance over the openings or box faces, a running sum of pressure and momentum terms of both signs. The reference area A is the given one or the rasterized frontal area in mm² × 1e-6.
flow-worker.js
lines 161 to 181
PartCraft 0.8.1
const Fscale = rhoP * dx ** 4 / (sc.dt * sc.dt);
let force = st.force.map((v) => v * Fscale);
// turbulent flow: the wall links' momentum exchange is not trustworthy with slip terms on them (it counts the slip's
// momentum as force), so the force on the part comes from a momentum balance instead: −∮(p n + ρu(u·n))dA over the
// openings (internal) or the box faces (external: inflow, outflow, frictionless slip sides); steady flow assumed
let forceBy = 'walls';
if (turb) {
const f = [0, 0, 0];
for (let k = 0; k < L.n; k++) {
const q = L.dir[k]; if (q > 6 || !L.g[k] || (L.kind[k] !== 3 && L.kind[k] !== 4 && L.kind[k] !== 5)) continue;
const n = L.cell[k], nv = [-CXq[q], -CYq[q], -CZq[q]], pr = (S.rho[n] - 1) / 3 * cp, u = [S.ux[n] * cu, S.uy[n] * cu, S.uz[n] * cu], un = u[0] * nv[0] + u[1] * nv[1] + u[2] * nv[2];
for (let a = 0; a < 3; a++) f[a] -= (pr * nv[a] + rhoP * u[a] * un) * dx * dx;
}
force = f; forceBy = internal ? 'openings' : 'box';
}
let ext = null;
if (!internal) {
const d = job.ext.dir, l = job.ext.lift, q = 0.5 * rhoP * Uref * Uref, A = Aref * 1e-6;
const drag = dotv(force, d), lift = dotv(force, l);
ext = { drag, lift, cd: drag / (q * A), cl: lift / (q * A), refArea: Aref, refAuto: !job.ext.refArea, lref: Lref, dir: d, liftDir: l, q };
}
Reynolds number
64-bit
The Reynolds number of the study.
Re = U √(4A ÷ π) ÷ ν (internal); Re = U L ÷ ν (external)
One 64-bit expression. The length is the inlet's equivalent diameter from its area, or the part's largest size across the flow in mm ÷ 1000.
The turbulence level given to inflows in a turbulent run.
ν_t = max(3ν, √1.5 × 0.09^0.25 × I × u × ℓ), I = 5 % (internal) or 1 % (external)
A 64-bit expression built on assumed values: intensity 5 or 1 percent and a length scale of 0.07 or 0.01 of the reference size. It is an input assumption of the experimental turbulence model, not a measured quantity.
flow-worker.js
lines 97 to 98
PartCraft 0.8.1
const I = internal ? 0.05 : 0.01, ell = internal ? 0.07 * Dc : 0.01 * Dc, nti = Math.max(3 * sc.nu, Math.sqrt(1.5) * 0.09 ** 0.25 * I * sc.u * ell);
return attachSA(S, { nu: sc.nu, nuIn: nti });
The share of the inflow that does not leave through the outlets.
balance = (ṁ_in − ṁ_out) ÷ ṁ_in
A subtraction of two nearly equal 64-bit mass flows; the result is a small number whose own relative accuracy is limited by that cancellation. Zero inflow gives 0 instead of a division by zero.
Unit conversion by the lattice scales dx and dt; pressure p = p_ref + (ρ − 1) ÷ 3 × ρ_fluid (dx ÷ dt)²; flow-weighted mean pressures; momentum exchange or, for turbulent runs, a momentum balance over the openings or box faces.
Number types
64-bit numbers and Float64Array.
Tolerances
Steady: residual under the study's tolerance and the watched quantity changing by under 1e-4 relative on two checks in a row. Outlet pressures may not differ by more than 5 percent in lattice density.
Limits
The drag and lift coefficients divide by the reference area, which is a pixel count unless the study gives one.
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.