8354482cymmodes: linear modes of a cymsim cymbal driven in the full nonlinear model, with the energy of every mode over timeJeremy Magland 1// Collects the modal snapshots of a run and turns them into per-mode energies.
2//
3// A snapshot holds q_m and qdot_m for every mode (projections of w and w-dot onto the
4// mass-weighted shapes). The energy of a mode is E_m = (qdot_m^2 + omega_m^2 q_m^2) / 2, exact
5// for the linearised, condensed model; in the nonlinear run it is the standard measure of how
6// much of the transverse motion lives in mode m. The sum over modes misses the in-plane and
7// rotational kinetic energy and the part of the strain energy that the linear basis does not
8// represent, so it is compared against, not equated to, the simulator's energy diagnostics.
9import type { ChunkResult } from "../model/simulator.ts";
11export class ModalTracker {
12 readonly nModes: number;
13 readonly omega: Float64Array; // (Nm,) [rad/s] at the current material
14 readonly times: number[] = []; // [s]
15 readonly energies: Float32Array[] = []; // per snapshot, (Nm,) [J]
16 readonly totals: number[] = []; // sum over modes per snapshot [J]
17 maxEnergy = 0;
18 drivenMode: number;
20 constructor(freq: Float64Array, drivenMode: number) {
21 this.nModes = freq.length;
22 this.omega = Float64Array.from(freq, (f) => 2 * Math.PI * f);
23 this.drivenMode = drivenMode;
24 }
26 addChunk(res: ChunkResult, fs: number) {
27 const Nm = this.nModes;
28 for (let s = 0; s < res.proj.length; s++) {
29 const row = res.proj[s], E = new Float32Array(Nm);
30 let total = 0;
31 for (let m = 0; m < Nm; m++) {
32 const q = row[m], v = row[Nm + m];
33 const e = 0.5 * (v * v + this.omega[m] * this.omega[m] * q * q);
34 E[m] = e; total += e;
35 if (e > this.maxEnergy) this.maxEnergy = e;
36 }
37 this.times.push(res.projSamples[s] / fs);
38 this.energies.push(E);
39 this.totals.push(total);
40 }
41 }
43 get length() { return this.times.length; }
44 get duration() { return this.times.length ? this.times[this.times.length - 1] : 0; }
46 /** Index of the snapshot at or before ``t``. */
47 indexAtTime(t: number): number {
48 const ts = this.times;
49 let lo = 0, hi = ts.length - 1;
50 if (hi < 0) return -1;
51 while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (ts[mid] <= t) lo = mid; else hi = mid - 1; }
52 return lo;
53 }
55 /** Energy of every mode except the driven one, at snapshot ``s``. */
56 otherEnergy(s: number): number {
57 const E = this.energies[s];
58 let sum = 0;
59 for (let m = 0; m < this.nModes; m++) if (m !== this.drivenMode) sum += E[m];
60 return sum;
61 }
63 /** The ``k`` most energetic modes at snapshot ``s``, as (mode, energy) sorted descending. */
64 topModes(s: number, k: number): { mode: number; energy: number }[] {
65 const E = this.energies[s];
66 const idx = Array.from(E.keys()).sort((a, b) => E[b] - E[a]).slice(0, k);
67 return idx.map((mode) => ({ mode, energy: E[mode] }));
68 }
69}