// Heuristic (m, n) classification of a mode shape: m nodal diameters and n nodal circles, read // off the discrete shape by counting sign changes around a ring and along a ray. On a hammered, // warped cymbal the azimuthal patterns are only approximately sinusoidal and high modes mix, so // the label is a description, not a mode number; it is computed lazily for the modes the user // looks at. import type { ModesData } from "../modes.ts"; export interface ModeLabel { diameters: number; circles: number } /** Count sign changes of ``vals`` (ordered), ignoring excursions below ``rel`` of the peak. */ function signChanges(vals: number[], rel = 0.15): number { let peak = 0; for (const v of vals) peak = Math.max(peak, Math.abs(v)); if (peak === 0) return 0; let last = 0, changes = 0; for (const v of vals) { if (Math.abs(v) < rel * peak) continue; const s = Math.sign(v); if (last !== 0 && s !== last) changes++; last = s; } return changes; } export function classifyMode(modes: ModesData, m: number): ModeLabel { const { nNodes, xy, radius } = modes, Nm = modes.nModes; const phi = (n: number) => modes.phiQ[n * Nm + m] * modes.phiScale[m]; // ring: nodes with 0.62 R < r < 0.85 R, ordered by angle; sign changes around it = 2 m const ring: { a: number; v: number }[] = []; // ray: nodes within a narrow wedge about the antinode's angle, ordered by radius let antinodeAngle = 0, best = 0; for (let n = 0; n < nNodes; n++) { const v = Math.abs(phi(n)); if (v > best) { best = v; antinodeAngle = Math.atan2(xy[2 * n + 1], xy[2 * n]); } } const ray: { r: number; v: number }[] = []; for (let n = 0; n < nNodes; n++) { const x = xy[2 * n], y = xy[2 * n + 1], r = Math.hypot(x, y); if (r > 0.62 * radius && r < 0.85 * radius) ring.push({ a: Math.atan2(y, x), v: phi(n) }); let da = Math.atan2(y, x) - antinodeAngle; da = Math.atan2(Math.sin(da), Math.cos(da)); if (Math.abs(da) < Math.PI / 14) ray.push({ r, v: phi(n) }); } ring.sort((p, q) => p.a - q.a); ray.sort((p, q) => p.r - q.r); // around the closed ring the count is even for a clean pattern; halve and round const diameters = Math.round(signChanges(ring.map((p) => p.v)) / 2); const circles = signChanges(ray.map((p) => p.v)); return { diameters, circles }; }