fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 1// Web Worker: runs the numbl script, computes linear modes, and simulates.
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 3import { growthRate, relDiff } from "./sim/analysis";
185968eRevert "Dome example: chaos that keeps its pitch..."Jeremy Magland 4import { computeModes, isPlanar, modalDamping } from "./sim/modes";
5import { numblEig, runScript } from "./sim/runScript";
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 6import { simulate } from "./sim/solver";
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 7import { runTwin } from "./sim/twin";
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 8import type { BeadSystem, SimResult } from "./sim/types";
10const helperSources = import.meta.glob("../matlab/helpers/*.m", { query: "?raw", import: "default", eager: true }) as Record<string, string>;
11const helpers = Object.entries(helperSources).map(([path, source]) => ({ name: path.split("/").pop()!, source }));
185968eRevert "Dome example: chaos that keeps its pitch..."Jeremy Magland 13const MAX_MODE_BEADS = 1500;
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 14const MAX_TWIN_BYTES = 400e6;
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 15
16export interface RunPayload {
17 sys: {
18 n: number;
19 pos0: Float64Array;
20 pinned: Uint8Array;
21 si: Int32Array;
22 sj: Int32Array;
23 nDashpots: number;
24 listenBeads: Int32Array;
25 hitBeads: Int32Array;
26 method: string;
27 duration: number;
28 };
29 result: SimResult;
30 modeFreqs: Float64Array | null;
185968eRevert "Dome example: chaos that keeps its pitch..."Jeremy Magland 31 modeDecay: Float64Array | null; // small-amplitude amplitude decay rate of each mode (1/s)
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 32 notes: string[];
33 check: { dt: number; windows: { t: number; rel: number }[] } | null;
34 // the same run with every hit scaled by 1e-3, its output scaled back up
35 linear: { audio: Float32Array; windows: { t: number; rel: number }[] } | null;
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 36 // the same run with every hit scaled by (1 + eps): separation of the bead
37 // positions (max over coordinates, relative to the largest displacement)
38 twin: { eps: number; audio: Float32Array; times: Float64Array; sep: Float64Array; rate: number | null } | null;
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 39}
41export interface RunRequest {
42 type: "run";
43 script: string;
44 dtCheck: boolean;
45 linear: boolean;
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 46 twin: boolean;
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 47}
49export type FromWorker =
50 | { type: "log"; text: string }
51 | { type: "stage"; text: string }
52 | { type: "progress"; t: number; duration: number; label: string }
53 | { type: "result"; payload: RunPayload }
54 | { type: "error"; message: string };
56const post = (m: FromWorker, transfer: Transferable[] = []) => (self as unknown as Worker).postMessage(m, transfer);
58const windowsOf = (duration: number) =>
59 [0.01, 0.03, 0.1, 0.3, 1, duration].filter((t, i, a) => t <= duration && a.indexOf(t) === i);
61self.onmessage = (ev: MessageEvent<RunRequest>) => {
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 62 const { script, dtCheck, linear, twin } = ev.data;
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 63 try {
64 post({ type: "stage", text: "Running script" });
65 const { sys, warnings } = runScript(script, helpers, text => post({ type: "log", text }));
66 const notes = [...warnings];
68 let modes = null;
185968eRevert "Dome example: chaos that keeps its pitch..."Jeremy Magland 69 const nFree = sys.pinned.reduce((a, p) => a + (p ? 0 : 1), 0);
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 70 if (sys.modes) {
185968eRevert "Dome example: chaos that keeps its pitch..."Jeremy Magland 71 if (!isPlanar(sys)) notes.push("Linear modes skipped: the beads do not rest in a plane z = const.");
72 else if (nFree > MAX_MODE_BEADS) notes.push(`Linear modes skipped: ${nFree} free beads (limit ${MAX_MODE_BEADS}).`);
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 73 else {
185968eRevert "Dome example: chaos that keeps its pitch..."Jeremy Magland 74 post({ type: "stage", text: `Computing linear modes (${nFree} free beads)` });
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 75 modes = computeModes(sys, numblEig);
76 }
77 }
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 79 // positions at every frame, kept for the perturbed twin
80 const saved: Float64Array[] = [];
81 const savedBytes = Math.round(sys.duration * sys.frameRate + 1) * 3 * sys.n * 8;
82 const keep = twin && savedBytes <= MAX_TWIN_BYTES;
83 if (twin && !keep) notes.push(`Perturbed twin skipped: it would store ${(savedBytes / 1e6).toFixed(0)} MB of positions (limit ${MAX_TWIN_BYTES / 1e6} MB). Lower sys.frameRate or sys.duration.`);
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 85 post({ type: "stage", text: "Simulating" });
86 const result = simulate(sys, {
87 modes,
88 onProgress: t => post({ type: "progress", t, duration: sys.duration, label: "Simulating" }),
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 89 onFrame: keep ? (_f, _t, x) => saved.push(x.slice()) : undefined,
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 90 });
92 let checkOut: RunPayload["check"] = null;
93 if (dtCheck && !result.aborted) {
94 post({ type: "stage", text: "Simulating again with dt/2" });
95 const half: BeadSystem = { ...sys, dt: result.dt / 2 };
96 const r2 = simulate(half, {
97 maxAnimBytes: 0,
98 onProgress: t => post({ type: "progress", t, duration: sys.duration, label: "Simulating with dt/2" }),
99 });
100 const fs = result.sampleRate;
101 const windows = windowsOf(sys.duration).map(t => ({ t, rel: relDiff(r2.audio, result.audio, Math.round(t * fs)) }));
102 checkOut = { dt: r2.dt, windows };
103 }
105 let linearOut: RunPayload["linear"] = null;
106 if (linear && !result.aborted) {
107 post({ type: "stage", text: "Simulating the small-strike reference" });
108 const s = 1e-3;
109 const small: BeadSystem = {
110 ...sys,
111 dt: result.dt,
112 hits: sys.hits.map(h => ({
113 ...h,
114 velocity: [h.velocity[0] * s, h.velocity[1] * s, h.velocity[2] * s],
115 force: [h.force[0] * s, h.force[1] * s, h.force[2] * s],
116 })),
117 };
118 const r3 = simulate(small, {
119 maxAnimBytes: 0,
120 onProgress: t => post({ type: "progress", t, duration: sys.duration, label: "Small-strike reference" }),
121 });
122 const audio = r3.audio.map(v => v / s);
123 const fs = result.sampleRate;
124 const windows = windowsOf(sys.duration).map(t => ({ t, rel: relDiff(audio, result.audio, Math.round(t * fs)) }));
125 linearOut = { audio, windows };
126 }
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 128 let twinOut: RunPayload["twin"] = null;
129 if (keep && !result.aborted) {
130 post({ type: "stage", text: "Simulating the perturbed twin" });
131 const eps = 1e-9;
132 const { audio, sep } = runTwin(sys, saved, result.dt, eps, t =>
133 post({ type: "progress", t, duration: sys.duration, label: "Perturbed twin" })
134 );
135 const times = result.frameTimes.slice(0, saved.length);
136 twinOut = { eps, audio, times, sep, rate: growthRate(times, sep, eps) };
137 }
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 139 const hitBeads = Int32Array.from(new Set(sys.hits.flatMap(h => Array.from(h.beads))));
140 const payload: RunPayload = {
141 sys: {
142 n: sys.n,
143 pos0: sys.pos0,
144 pinned: sys.pinned,
145 si: sys.si,
146 sj: sys.sj,
147 nDashpots: sys.di.length,
148 listenBeads: sys.listenBeads,
149 hitBeads,
150 method: sys.method,
151 duration: sys.duration,
152 },
153 result,
154 modeFreqs: modes ? modes.freqs : null,
0330c41Damping by decay times: bp_decay helper, per-mode decay-time plot, examples convertedJeremy Magland 155 modeDecay: modes ? modalDamping(sys, modes) : null,
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 156 notes,
157 check: checkOut,
158 linear: linearOut,
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 159 twin: twinOut,
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 160 };
161 const transfer: Transferable[] = [result.audio.buffer, ...result.animFrames.map(f => f.buffer)];
162 if (result.modeEnergy) transfer.push(result.modeEnergy.buffer);
163 if (linearOut) transfer.push(linearOut.audio.buffer);
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 164 if (twinOut) transfer.push(twinOut.audio.buffer, twinOut.sep.buffer);
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 165 post({ type: "result", payload }, transfer);
166 } catch (e) {
167 const err = e as { message?: string; line?: number; file?: string };
168 const where = err.line ? ` (line ${err.line})` : "";
169 post({ type: "error", message: `${err.message ?? String(e)}${where}` });
170 }
171};