// Run the browser precompute pipeline in node and compare its modal frequencies with a reference // file made by tools/make_modes_reference.py (same element size, --condense-rotations, --no-imperfections). // node tests/compare.ts [elementSize] import { readFileSync } from "node:fs"; import { parseModes } from "../src/modes.ts"; import { parseConfigYaml } from "../src/config/yaml.ts"; import { PRESETS } from "../src/config/presets.ts"; import { computeModes } from "../src/precompute/pipeline.ts"; const refPath = process.argv[2]; const es = Number(process.argv[3] ?? "0.010"); const ref = parseModes(readFileSync(refPath).buffer as ArrayBuffer); const { config } = parseConfigYaml(PRESETS[0].yaml); if (!config) throw new Error("preset did not parse"); config.imperfections.n_dents = 0; config.imperfections.rim_warp = 0; config.imperfections.shoulder_ovality = 0; config.imperfections.bell_offset = 0; const t0 = performance.now(); let lastPhase = ""; const data = computeModes(config, { elementSize: es, fmax: ref.meta.fmax }, (phase, frac) => { if (phase !== lastPhase) { lastPhase = phase; console.log(`[${((performance.now() - t0) / 1000).toFixed(1)} s] ${phase}`); } void frac; }); console.log(`pipeline: ${((performance.now() - t0) / 1000).toFixed(1)} s; nodes ${data.nNodes} (ref ${ref.nNodes}), modes ${data.nModes} (ref ${ref.nModes})`); const n = Math.min(data.nModes, ref.nModes); let worst = 0, worstIdx = 0; for (let m = 0; m < n; m++) { const rel = Math.abs(data.freq[m] - ref.freq[m]) / ref.freq[m]; if (rel > worst) { worst = rel; worstIdx = m; } } console.log(`frequency: max relative difference ${worst.toExponential(2)} at mode ${worstIdx} (${ref.freq[worstIdx].toFixed(1)} Hz)`); console.log("first 10 (browser | reference):", Array.from({ length: 10 }, (_, m) => `${data.freq[m].toFixed(2)}|${ref.freq[m].toFixed(2)}`).join(" ")); let sw = 0, si = 0; for (let m = 0; m < n; m++) { const rel = Math.abs(data.dampQ[m] - ref.dampQ[m]) / Math.max(ref.dampQ[m], 1e-12); if (rel > sw) { sw = rel; si = m; } } console.log(`sigma1 quadratic form: max relative difference ${sw.toExponential(2)} at mode ${si} (${data.dampQ[si].toFixed(3)} vs ${ref.dampQ[si].toFixed(3)})`); // gains: compare magnitudes (sign of a mode shape is arbitrary) let gw = 0, gi = 0; for (let m = 0; m < n; m++) { const a = Math.hypot(data.gainFront[2 * m], data.gainFront[2 * m + 1]), b = Math.hypot(ref.gainFront[2 * m], ref.gainFront[2 * m + 1]); const rel = Math.abs(a - b) / Math.max(b, 1e-30); if (rel > gw) { gw = rel; gi = m; } } console.log(`|gain_front|: max relative difference ${gw.toExponential(2)} at mode ${gi}`); // mode shape at one point (up to sign) const node = Math.floor(data.nNodes / 2); let pw = 0; for (let m = 0; m < Math.min(n, 200); m++) { const a = data.phiQ[node * data.nModes + m] * data.phiScale[m], b = ref.phiQ[node * ref.nModes + m] * ref.phiScale[m]; pw = Math.max(pw, Math.min(Math.abs(a - b), Math.abs(a + b)) / (Math.abs(b) + 1e-3 * Math.abs(a) + 1e-12)); } console.log(`mode shape at node ${node}, first 200 modes: max |diff|/|ref| ${pw.toExponential(2)} (degenerate pairs may mix)`);