ae4a407cymplay: design and play a cymsim cymbal in the browserJeremy Magland 1// Run the browser precompute pipeline in node and compare its modal frequencies with a reference
2// file made by tools/make_modes_reference.py (same element size, --condense-rotations, --no-imperfections).
3// node tests/compare.ts <reference.bin> [elementSize]
4import { readFileSync } from "node:fs";
5import { parseModes } from "../src/modes.ts";
6import { parseConfigYaml } from "../src/config/yaml.ts";
7import { PRESETS } from "../src/config/presets.ts";
8import { computeModes } from "../src/precompute/pipeline.ts";
10const refPath = process.argv[2];
11const es = Number(process.argv[3] ?? "0.010");
12const ref = parseModes(readFileSync(refPath).buffer as ArrayBuffer);
13const { config } = parseConfigYaml(PRESETS[0].yaml);
14if (!config) throw new Error("preset did not parse");
15config.imperfections.n_dents = 0; config.imperfections.rim_warp = 0;
16config.imperfections.shoulder_ovality = 0; config.imperfections.bell_offset = 0;
17const t0 = performance.now();
18let lastPhase = "";
19const data = computeModes(config, { elementSize: es, fmax: ref.meta.fmax }, (phase, frac) => {
20 if (phase !== lastPhase) { lastPhase = phase; console.log(`[${((performance.now() - t0) / 1000).toFixed(1)} s] ${phase}`); }
21 void frac;
22});
23console.log(`pipeline: ${((performance.now() - t0) / 1000).toFixed(1)} s; nodes ${data.nNodes} (ref ${ref.nNodes}), modes ${data.nModes} (ref ${ref.nModes})`);
24const n = Math.min(data.nModes, ref.nModes);
25let worst = 0, worstIdx = 0;
26for (let m = 0; m < n; m++) {
27 const rel = Math.abs(data.freq[m] - ref.freq[m]) / ref.freq[m];
28 if (rel > worst) { worst = rel; worstIdx = m; }
29}
30console.log(`frequency: max relative difference ${worst.toExponential(2)} at mode ${worstIdx} (${ref.freq[worstIdx].toFixed(1)} Hz)`);
31console.log("first 10 (browser | reference):", Array.from({ length: 10 }, (_, m) => `${data.freq[m].toFixed(2)}|${ref.freq[m].toFixed(2)}`).join(" "));
32let sw = 0, si = 0;
33for (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; } }
34console.log(`sigma1 quadratic form: max relative difference ${sw.toExponential(2)} at mode ${si} (${data.dampQ[si].toFixed(3)} vs ${ref.dampQ[si].toFixed(3)})`);
35// gains: compare magnitudes (sign of a mode shape is arbitrary)
36let gw = 0, gi = 0;
37for (let m = 0; m < n; m++) {
38 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]);
39 const rel = Math.abs(a - b) / Math.max(b, 1e-30);
40 if (rel > gw) { gw = rel; gi = m; }
41}
42console.log(`|gain_front|: max relative difference ${gw.toExponential(2)} at mode ${gi}`);
43// mode shape at one point (up to sign)
44const node = Math.floor(data.nNodes / 2);
45let pw = 0;
46for (let m = 0; m < Math.min(n, 200); m++) {
47 const a = data.phiQ[node * data.nModes + m] * data.phiScale[m], b = ref.phiQ[node * ref.nModes + m] * ref.phiScale[m];
48 pw = Math.max(pw, Math.min(Math.abs(a - b), Math.abs(a + b)) / (Math.abs(b) + 1e-3 * Math.abs(a) + 1e-12));
49}
50console.log(`mode shape at node ${node}, first 200 modes: max |diff|/|ref| ${pw.toExponential(2)} (degenerate pairs may mix)`);