/ proof-of-concept / cymmodes
Sign in
proof-of-concept / cymmodes
cymmodes / src / precompute / worker.ts
77 lines · 4.5 KBCodeBlameHistory
8354482cymmodes: linear modes of a cymsim cymbal driven in the full nonlinear model, with the energy of every mode over timeJeremy Magland 1// Web Worker entry: the computations that are too slow for the main thread. "modes" is the full
2// eigensolve (seconds to minutes); "gains" is the listener sum, redone whenever the listener or
3// the material changes (a fraction of a second); "complement" applies the static condensation to
4// a transverse field, giving the in-plane displacements and rotations that make a mode shape a
5// complete initial condition for the five-DOF simulator. The condensation operator (a banded
6// Cholesky factor and the sparse coupling block) is built on first use and kept while the
7// structural configuration stays the same.
8import type { Listener, SimulationConfig } from "../config/schema.ts";
9import { cloneConfig } from "../config/yaml.ts";
10import { CymbalProfile } from "../model/geometry.ts";
11import { diskMesh } from "../model/mesh.ts";
12import { buildShell } from "../model/shell.ts";
13import { parseModes, serializeModes, type ModesData } from "../modes.ts";
14import { buildCondensationOperator, type CondensationOperator } from "./assemble.ts";
15import { computeGains } from "./gains.ts";
16import { computeModes, structuralKey, type PrecomputeOptions } from "./pipeline.ts";
18export type PrecomputeRequest =
19 | { type: "modes"; token: number; config: SimulationConfig; options: PrecomputeOptions }
20 | { type: "gains"; token: number; buffer?: ArrayBuffer; listener: Listener; freq: Float64Array; phiFactor: number }
21 | { type: "complement"; token: number; config: SimulationConfig; options: PrecomputeOptions; w: Float64Array };
23export type PrecomputeReply =
24 | { type: "progress"; token: number; phase: string; frac: number }
25 | { type: "modes"; token: number; buffer: ArrayBuffer; seconds: number }
26 | { type: "gains"; token: number; front: Float32Array; back: Float32Array; seconds: number }
27 | { type: "complement"; token: number; o: Float64Array; seconds: number }
28 | { type: "error"; token: number; message: string };
30/** The worker keeps the modes it was last given so that repeated gain requests, which happen
31 * while a listener slider is being dragged, do not re-send a 12 MB buffer every time. */
32let held: { buffer: ArrayBuffer; data: ModesData } | null = null;
33/** The condensation operator of the last structural configuration. */
34let condensation: { key: string; op: CondensationOperator } | null = null;
36self.onmessage = (ev: MessageEvent<PrecomputeRequest>) => {
37 const msg = ev.data;
38 const post = (reply: PrecomputeReply, transfer?: Transferable[]) => (self as unknown as Worker).postMessage(reply, transfer ?? []);
39 const t0 = performance.now();
40 let lastPost = 0;
41 const progress = (token: number) => (phase: string, frac: number) => {
42 const t = performance.now();
43 if (t - lastPost > 100 || frac >= 1) {
44 lastPost = t;
45 post({ type: "progress", token, phase, frac });
46 }
47 };
48 try {
49 if (msg.type === "modes") {
50 const data = computeModes(msg.config, msg.options, progress(msg.token));
51 const buffer = serializeModes(data);
52 held = { buffer, data };
53 condensation = null; // the shape changed; rebuild on the next complement request
54 post({ type: "modes", token: msg.token, buffer: buffer.slice(0), seconds: (performance.now() - t0) / 1000 });
55 } else if (msg.type === "complement") {
56 const key = JSON.stringify(structuralKey(msg.config, msg.options));
57 if (!condensation || condensation.key !== key) {
58 const cfg = cloneConfig(msg.config);
59 cfg.mesh.element_size = msg.options.elementSize;
60 const profile = new CymbalProfile(cfg.profile, cfg.imperfections);
61 const mesh = diskMesh(cfg.profile.radius, cfg.mesh.element_size, cfg.mesh.smoothing);
62 const shell = buildShell(mesh, profile, cfg.material, cfg.numerics);
63 condensation = { key, op: buildCondensationOperator(mesh, shell, progress(msg.token)) };
64 }
65 const o = condensation.op.complement(msg.w);
66 post({ type: "complement", token: msg.token, o, seconds: (performance.now() - t0) / 1000 }, [o.buffer]);
67 } else {
68 if (msg.buffer) held = { buffer: msg.buffer, data: parseModes(msg.buffer) };
69 if (!held) throw new Error("no modes in the worker: send the buffer with the first gains request");
70 const g = computeGains(held.data, msg.listener, msg.freq, msg.phiFactor);
71 post({ type: "gains", token: msg.token, front: g.front, back: g.back, seconds: (performance.now() - t0) / 1000 },
72 [g.front.buffer, g.back.buffer]);
73 }
74 } catch (err) {
75 post({ type: "error", token: msg.token, message: (err as Error).message ?? String(err) });
76 }
77};
↑↓ move↵openescclose