8354482cymmodes: linear modes of a cymsim cymbal driven in the full nonlinear model, with the energy of every mode over timeJeremy Magland 1// Heuristic (m, n) classification of a mode shape: m nodal diameters and n nodal circles, read
2// off the discrete shape by counting sign changes around a ring and along a ray. On a hammered,
3// warped cymbal the azimuthal patterns are only approximately sinusoidal and high modes mix, so
4// the label is a description, not a mode number; it is computed lazily for the modes the user
5// looks at.
6import type { ModesData } from "../modes.ts";
8export interface ModeLabel { diameters: number; circles: number }
10/** Count sign changes of ``vals`` (ordered), ignoring excursions below ``rel`` of the peak. */
11function signChanges(vals: number[], rel = 0.15): number {
12 let peak = 0;
13 for (const v of vals) peak = Math.max(peak, Math.abs(v));
14 if (peak === 0) return 0;
15 let last = 0, changes = 0;
16 for (const v of vals) {
17 if (Math.abs(v) < rel * peak) continue;
18 const s = Math.sign(v);
19 if (last !== 0 && s !== last) changes++;
20 last = s;
21 }
22 return changes;
23}
25export function classifyMode(modes: ModesData, m: number): ModeLabel {
26 const { nNodes, xy, radius } = modes, Nm = modes.nModes;
27 const phi = (n: number) => modes.phiQ[n * Nm + m] * modes.phiScale[m];
28 // ring: nodes with 0.62 R < r < 0.85 R, ordered by angle; sign changes around it = 2 m
29 const ring: { a: number; v: number }[] = [];
30 // ray: nodes within a narrow wedge about the antinode's angle, ordered by radius
31 let antinodeAngle = 0, best = 0;
32 for (let n = 0; n < nNodes; n++) {
33 const v = Math.abs(phi(n));
34 if (v > best) { best = v; antinodeAngle = Math.atan2(xy[2 * n + 1], xy[2 * n]); }
35 }
36 const ray: { r: number; v: number }[] = [];
37 for (let n = 0; n < nNodes; n++) {
38 const x = xy[2 * n], y = xy[2 * n + 1], r = Math.hypot(x, y);
39 if (r > 0.62 * radius && r < 0.85 * radius) ring.push({ a: Math.atan2(y, x), v: phi(n) });
40 let da = Math.atan2(y, x) - antinodeAngle;
41 da = Math.atan2(Math.sin(da), Math.cos(da));
42 if (Math.abs(da) < Math.PI / 14) ray.push({ r, v: phi(n) });
43 }
44 ring.sort((p, q) => p.a - q.a);
45 ray.sort((p, q) => p.r - q.r);
46 // around the closed ring the count is even for a clean pattern; halve and round
47 const diameters = Math.round(signChanges(ring.map((p) => p.v)) / 2);
48 const circles = signChanges(ray.map((p) => p.v));
49 return { diameters, circles };
50}