Gathers the results of a contact study between parts and of a 2-D magnet study: maximum stress, displacement and flux density, contact force, and the per-vertex and per-element fields.
How the precision is kept
The solvers this file calls (nonlinear.js, contact.js, magnet.js) work in 64-bit, and the reported maxima are taken from their 64-bit arrays. The fields handed back to the page are then copied into Float32Array: the per-vertex stress, plastic strain and displacement of a contact study, and the flux density and potential of a magnet study. The probe readout and the field exports read those 32-bit copies.
Open item. Return the magnet study's B and A and the per-vertex result arrays as Float64Array (or keep a 64-bit copy for the probe and the exports) so that only the copy sent to the graphics card is 32-bit.
Calculations under this policy
10 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.
phys-worker.jsPartCraft 0.8.1 32-bit in part10 calculationsReviewed
Converts a material's density from kg/m³ to tonnes/mm³, the unit the solver uses.
ρ (t/mm³) = ρ (kg/m³) × 1e-12
The factor 1e-12 is the correct power of ten but has no exact binary form, so the product is rounded once in the last place. A yield of zero or less is replaced by Infinity (never yields).
Gives each node the mean of the values of the elements that share it.
value at a node = Σ element values ÷ number of elements at the node
Sums and counts are Float64Array; the terms are added in element order. A node with no element keeps zero, so the division is guarded.
phys-worker.js
lines 41 to 47
PartCraft 0.8.1
/** Nodal averages of per-element values (each element's value goes to its ten nodes). */
function nodalFromElements(model, perElem) {
const sum = new Float64Array(model.n), cnt = new Float64Array(model.n), m = model.tets10.length / 10;
for (let e = 0; e < m; e++) for (let a = 0; a < 10; a++) { const n = model.tets10[10 * e + a]; sum[n] += perElem[e]; cnt[n]++; }
for (let i = 0; i < model.n; i++) if (cnt[i]) sum[i] /= cnt[i];
return sum;
}
Maximum stress, plastic strain and displacement
64-bit
The maxima the study reports, and where they occur.
|u| = √(u_x² + u_y² + u_z²); maximum over the elements (stress, plastic strain) and over the nodes (displacement)
The maxima are searched in the 64-bit element and node arrays, before anything is copied to 32-bit. The stress maximum is the largest element value, not the nodal average that is plotted. An element's position is the sum of its ten nodes' coordinates, each divided by 10.
phys-worker.js
lines 54 to 65
PartCraft 0.8.1
const dispN = new Float64Array(model.n);
for (let i = 0; i < model.n; i++) dispN[i] = Math.hypot(r.u[3 * i], r.u[3 * i + 1], r.u[3 * i + 2]);
const vertex = { vm: at(pm, vmN), plastic: at(pm, apN), disp: at(pm, dispN), u: at(pm, r.u, 3) };
ctx.transfer && ctx.transfer(...Object.values(vertex).map((a) => a.buffer));
const pos = (e) => { const c = [0, 0, 0]; for (let a = 0; a < 10; a++) for (let k = 0; k < 3; k++) c[k] += model.X[3 * model.tets10[10 * e + a] + k] / 10; return c; };
const ev = argmax(ex.vm), ep = argmax(ex.plasticStrain), nd = argmax(dispN);
let plasticElements = 0; for (let e = 0; e < ex.plasticStrain.length; e++) if (ex.plasticStrain[e] > 0) plasticElements++;
return {
kind: 'nonlinear', mesher: 'tet', converged: r.converged, failedAt: r.failedAt ?? null, lambda: r.lambda, summary,
steps: r.steps.map((s) => ({ lambda: s.lambda, iterations: s.iterations, plastic: s.plastic, residual: s.residual })),
maxVm: { value: ex.vm[ev], at: pos(ev) }, maxPlastic: { value: ex.plasticStrain[ep], at: pos(ep) }, plasticElements,
maxDisp: { value: dispN[nd], at: [model.X[3 * nd], model.X[3 * nd + 1], model.X[3 * nd + 2]] },
Per-vertex result arrays
32-bit in part
Copies the nodal stress, plastic strain, displacement size and displacement vector to the vertices of the plot mesh.
The copy is a Float32Array, so each value is rounded to about 7 significant digits. The same arrays are returned as the result's vertex fields. Code that reads a result's vertex fields sees these 32-bit values: the probe in ws-simulation.js interpolates vertex.vm and vertex.disp, and bands in addons/advanced-sim/lib/results.js takes its lowest and highest value from them.
phys-worker.js
line 36
PartCraft 0.8.1
const at = (pm, values, comps = 1) => { const out = new Float32Array(pm.node.length * comps); pm.node.forEach((n, v) => { for (let c = 0; c < comps; c++) out[comps * v + c] = values[comps * n + c]; }); return out; };
Placing a part's mesh in the assembly
64-bit
Moves each mesh node of a placed part to the assembly frame with the instance's 4 × 4 matrix.
x′ = M · (x, y, z, 1)
Each coordinate is three products and a translation in 64-bit (applyMat in vec.js), written back into the mesh's node array. The matrix is used as given; the code comment says rigid placements only, and nothing here checks that.
h_min is the smallest element size over all the parts, or 1 when none is finite. The penalty method lets the faces overlap slightly; the overlap is reported as maxPenetration. The formula itself is autoPenalty in contact.js.
phys-worker.js
lines 88 to 90
PartCraft 0.8.1
const hMin = Math.min(...meshes.map((m) => m.tm.stats.h || Infinity), ...jobs.map((p) => p.h || Infinity));
const pairs = (job.pairs || []).map((pr) => ({ type: pr.type === 'bonded' ? 'bonded' : 'frictionless', slave: trisOf(pr.slave), master: trisOf(pr.master),
kn: pr.kn || CT.autoPenalty(Math.min(mats[pr.slave.part].E, mats[pr.master.part].E), Number.isFinite(hMin) ? hMin : 1, pr.factor > 0 ? pr.factor : pr.type === 'bonded' ? 1e3 : 100), ...(pr.kt ? { kt: pr.kt } : {}) })); // factors 1e3 / 100: the penalty's own give is ~1e-3 / 1e-2 of the element size's share (contact.js header); gap-closing loads also need small steps, below
Total contact force and peak pressure
64-bit
Adds the force at every contact point and finds the highest contact pressure.
F = Σ forceᵢ; p_max = max pressureᵢ
A plain 64-bit sum over the contact report in report order, with no compensation.
phys-worker.js
line 110
PartCraft 0.8.1
let normalForce = 0, maxPressure = 0; for (const q of rep) { normalForce += q.force; if (q.pressure > maxPressure) maxPressure = q.pressure; }
Maximum flux density, forces and region areas
64-bit
The largest flux density of a magnet study and where it is, the force on each requested body, and the area of each region.
B_max = max over the elements of |B|; region area = Σ element areas
The maximum is searched in the solver's own array before the 32-bit copies are made. Region areas are a plain sum of the solver's element areas in element order. The forces come from maxwell in magnet.js.
phys-worker.js
lines 130 to 136
PartCraft 0.8.1
const G = r.geom, e = argmax(r.Bmag);
const forces = (job.contours || []).map((c) => {
const inside = c.box ? (x, y) => x > c.box[0] && x < c.box[2] && y > c.box[1] && y < c.box[3] : (x, y) => (c.radius !== undefined ? Math.hypot(x - c.at[0], y - c.at[1]) < c.radius : false);
return { id: c.id, ...MG.maxwell(mesh, r, inside) };
});
const area = {}; for (let i = 0; i < mesh.region.length; i++) area[mesh.region[i]] = (area[mesh.region[i]] || 0) + G.area[i];
const out = { kind: 'magnet', stats: mesh.stats, iterations: r.iterations, Bmax: { value: r.Bmag[e], at: [G.cx[e], G.cy[e]] }, areas: area, forces,
Flux density and potential fields
32-bit in part
The flux density of every element and the magnetic vector potential of every node, as returned to the page.
B.x, B.y, B.mag and A are converted with Float32Array.from, so each value keeps about 7 significant digits. The Magnetics add-on writes these arrays to its CSV and VTK exports with 8 significant digits and averages B.mag per region (addons/magnetics/lib/results.js).
64-bit throughout; the accuracy is that of the numbers printed in the file. The stress maximum is taken over the file's stress records directly. The read is refused when the file's node count differs from the mesh's.
phys-worker.js
lines 150 to 163
PartCraft 0.8.1
/** The results file (.frd) the person's own CalculiX wrote for that deck, read back on the rebuilt mesh: the displacements (and the von Mises of S when it holds one). */
export function contactRead(job) {
const { model, jobs } = prepareContact(job);
const frd = CX.parseFrd(String(job.frd || ''));
if (frd.nodes.size !== model.n) throw new Error(`The results file has ${frd.nodes.size} nodes and this study's mesh has ${model.n}: the mesh is not the one the deck was written from. Write the deck again from this study and run that.`); // i18n-ignore
const u = CX.frdDisplacements(frd, model);
if (!u) throw new Error('The results file has no displacements (DISP). Ask CalculiX for U in the step\'s output.'); // i18n-ignore
let nd = 0, dmax = -1;
for (let i = 0; i < model.n; i++) { const d = Math.hypot(u[3 * i], u[3 * i + 1], u[3 * i + 2]); if (d > dmax) { dmax = d; nd = i; } }
const stress = [...frd.blocks].reverse().find((b) => b.name === 'STRESS');
let vm = null;
if (stress) { vm = 0; for (const [, v] of stress.values) if (v.length >= 6) { const [a, b, c, d, e, f] = v; vm = Math.max(vm, Math.sqrt(0.5 * ((a - b) ** 2 + (b - c) ** 2 + (c - a) ** 2) + 3 * (d * d + e * e + f * f))); } }
return { kind: 'calculix', calculix: { nodes: frd.nodes.size, stress: !!stress }, parts: jobs.length, maxDisp: { value: dmax, at: [model.X[3 * nd], model.X[3 * nd + 1], model.X[3 * nd + 2]] }, maxVm: vm === null ? null : { value: vm } };
}
In detail
Method
Per-element maxima averaged to nodes; displacement size by Math.hypot; von Mises from six stress components for a results file read back; contact penalty from the smaller Young's modulus, the smallest element size and a factor (100 for frictionless contact, 1000 for bonded contact by default); 10 load steps by default.
Number types
Float64Array for nodal sums, displacement sizes and all solver arrays; Float32Array for the per-vertex plot arrays (vm, plastic, disp, u) and for the magnet result's B.x, B.y, B.mag and A.
Limits
Values read from the 32-bit arrays carry about 7 significant digits. Gravity loads are refused in a contact study.
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.