/ proof-of-concept / bead-percussion
Sign in
proof-of-concept / bead-percussion
bead-percussion / src / worker.ts
203 lines · 8.7 KBCodeBlameHistory
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";
7import { simulate, type SimOptions } from "./sim/solver";
11const helperSources = import.meta.glob("../matlab/helpers/*.m", { query: "?raw", import: "default", eager: true }) as Record<string, string>;
12const helpers = Object.entries(helperSources).map(([path, source]) => ({ name: path.split("/").pop()!, source }));
15const MAX_FULL_DOF = 1200; // curved nets: all three components of every free bead
18export interface RunPayload {
19 sys: {
20 n: number;
21 pos0: Float64Array;
22 pinned: Uint8Array;
23 si: Int32Array;
24 sj: Int32Array;
25 nDashpots: number;
31 hitBeads: Int32Array;
32 method: string;
33 duration: number;
34 };
35 result: SimResult;
36 modeFreqs: Float64Array | null;
185968eRevert "Dome example: chaos that keeps its pitch..."Jeremy Magland 38 modeDecay: Float64Array | null; // small-amplitude amplitude decay rate of each mode (1/s)
40 check: { dt: number; windows: { t: number; rel: number }[] } | null;
41 // the same run with every hit scaled by 1e-3, its output scaled back up
42 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 43 // the same run with every hit scaled by (1 + eps): separation of the bead
44 // positions (max over coordinates, relative to the largest displacement)
45 twin: { eps: number; audio: Float32Array; times: Float64Array; sep: Float64Array; rate: number | null } | null;
48export interface RunRequest {
49 type: "run";
50 script: string;
51 dtCheck: boolean;
52 linear: boolean;
55 gpu: boolean;
58export type FromWorker =
59 | { type: "log"; text: string }
60 | { type: "stage"; text: string }
61 | { type: "progress"; t: number; duration: number; label: string }
62 | { type: "result"; payload: RunPayload }
63 | { type: "error"; message: string };
65const post = (m: FromWorker, transfer: Transferable[] = []) => (self as unknown as Worker).postMessage(m, transfer);
67const windowsOf = (duration: number) =>
68 [0.01, 0.03, 0.1, 0.3, 1, duration].filter((t, i, a) => t <= duration && a.indexOf(t) === i);
71 const { script, dtCheck, linear, twin: twinWanted, modes: modesToggle, gpu } = ev.data;
72 // the GPU path for the main run and the checks; the CPU otherwise
73 const sim = (s: BeadSystem, o: SimOptions) => (gpu ? simulateGPU(s, o) : Promise.resolve(simulate(s, o)));
75 post({ type: "stage", text: "Running script" });
76 const { sys, warnings } = runScript(script, helpers, text => post({ type: "log", text }));
77 const notes = [...warnings];
79 let modes = null;
185968eRevert "Dome example: chaos that keeps its pitch..."Jeremy Magland 80 const nFree = sys.pinned.reduce((a, p) => a + (p ? 0 : 1), 0);
83 if (planar && nFree > MAX_MODE_BEADS) notes.push(`Linear modes skipped: ${nFree} free beads (limit ${MAX_MODE_BEADS} for a flat net).`);
84 else if (!planar && 3 * nFree > MAX_FULL_DOF) {
85 notes.push(
86 `Linear modes skipped: the net is curved, so its modes need all ${3 * nFree} coordinates of the free beads (limit ${MAX_FULL_DOF}, about ${MAX_FULL_DOF / 3} free beads).`
87 );
88 } else {
89 post({ type: "stage", text: planar ? `Computing linear modes (${nFree} free beads)` : `Computing 3D linear modes (${3 * nFree} coordinates)` });
90 modes = computeModes(sys, numblEig, planar ? "transverse" : "full");
94 notes.push(
74b1dcdFree edges: bending triplets (sys.bends) with damping, lattice triplets from bp_hexdisk/bp_chain, bp_decay for bending, bp_anchor soft supports, zero-mode warning; plate and bar examplesJeremy Magland 96 "Pin more beads (a cluster rather than one bead), or tie the object to pinned anchor beads with soft springs."
97 );
98 }
100 }
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 102 // positions at every frame, kept for the perturbed twin
103 const saved: Float64Array[] = [];
104 const savedBytes = Math.round(sys.duration * sys.frameRate + 1) * 3 * sys.n * 8;
106 notes.push("Perturbed twin skipped: a perturbation of one part in 1e9 is below single precision; turn off WebGPU to run it.");
107 }
108 const twin = twinWanted && !gpu;
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 109 const keep = twin && savedBytes <= MAX_TWIN_BYTES;
110 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.`);
113 const result = await sim(sys, {
115 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 116 onFrame: keep ? (_f, _t, x) => saved.push(x.slice()) : undefined,
119 let checkOut: RunPayload["check"] = null;
120 if (dtCheck && !result.aborted) {
121 post({ type: "stage", text: "Simulating again with dt/2" });
122 const half: BeadSystem = { ...sys, dt: result.dt / 2 };
125 onProgress: t => post({ type: "progress", t, duration: sys.duration, label: "Simulating with dt/2" }),
126 });
127 const fs = result.sampleRate;
128 const windows = windowsOf(sys.duration).map(t => ({ t, rel: relDiff(r2.audio, result.audio, Math.round(t * fs)) }));
129 checkOut = { dt: r2.dt, windows };
130 }
132 let linearOut: RunPayload["linear"] = null;
133 if (linear && !result.aborted) {
134 post({ type: "stage", text: "Simulating the small-strike reference" });
135 const s = 1e-3;
136 const small: BeadSystem = {
137 ...sys,
138 dt: result.dt,
139 hits: sys.hits.map(h => ({
140 ...h,
141 velocity: [h.velocity[0] * s, h.velocity[1] * s, h.velocity[2] * s],
142 force: [h.force[0] * s, h.force[1] * s, h.force[2] * s],
143 })),
144 };
147 onProgress: t => post({ type: "progress", t, duration: sys.duration, label: "Small-strike reference" }),
148 });
149 const audio = r3.audio.map(v => v / s);
150 const fs = result.sampleRate;
151 const windows = windowsOf(sys.duration).map(t => ({ t, rel: relDiff(audio, result.audio, Math.round(t * fs)) }));
152 linearOut = { audio, windows };
153 }
156 if (keep && !result.aborted) {
157 post({ type: "stage", text: "Simulating the perturbed twin" });
158 const eps = 1e-9;
159 const { audio, sep } = runTwin(sys, saved, result.dt, eps, t =>
160 post({ type: "progress", t, duration: sys.duration, label: "Perturbed twin" })
161 );
162 const times = result.frameTimes.slice(0, saved.length);
163 twinOut = { eps, audio, times, sep, rate: growthRate(times, sep, eps) };
164 }
fc06d37Bead-and-spring percussion simulator: numbl-scripted setup, Verlet/Yoshida solver with exact damping, linear modes, browser appJeremy Magland 166 const hitBeads = Int32Array.from(new Set(sys.hits.flatMap(h => Array.from(h.beads))));
167 const payload: RunPayload = {
168 sys: {
169 n: sys.n,
170 pos0: sys.pos0,
171 pinned: sys.pinned,
172 si: sys.si,
173 sj: sys.sj,
180 hitBeads,
181 method: sys.method,
182 duration: sys.duration,
183 },
184 result,
185 modeFreqs: modes ? modes.freqs : null,
0330c41Damping by decay times: bp_decay helper, per-mode decay-time plot, examples convertedJeremy Magland 187 modeDecay: modes ? modalDamping(sys, modes) : null,
189 check: checkOut,
190 linear: linearOut,
193 const transfer: Transferable[] = [result.audio.buffer, ...result.animFrames.map(f => f.buffer)];
194 if (result.modeEnergy) transfer.push(result.modeEnergy.buffer);
195 if (linearOut) transfer.push(linearOut.audio.buffer);
95f2216Minimal chaos example and perturbed-twin chaos test with separation plot and growth-rate fitJeremy Magland 196 if (twinOut) transfer.push(twinOut.audio.buffer, twinOut.sep.buffer);
198 } catch (e) {
199 const err = e as { message?: string; line?: number; file?: string };
200 const where = err.line ? ` (line ${err.line})` : "";
201 post({ type: "error", message: `${err.message ?? String(e)}${where}` });
202 }
203};
↑↓ move↵openescclose