Beam step 5: λ vs E scatter over notes
4 changed files+31−6
src/beam/README.mdmodified+3−3View file
@@ -1,10 +1,10 @@
11 # beam
22
3-Struck beam with bending stiffness, step 3 of "Listening for chaos" (after double-pendulum and fput-chain). Work in progress: step 1 (physics, animation, sped-up note with spectrogram), step 2 (Lyapunov exponent), step 3 (mode bars and mode histories), step 4 (ghost).
3+Struck beam with bending stiffness, step 3 of "Listening for chaos" (after double-pendulum and fput-chain). Work in progress: step 1 (physics, animation, sped-up note with spectrogram), step 2 (Lyapunov exponent), step 3 (mode bars and mode histories), step 4 (ghost), step 5 (λ vs E).
44
55 Current state: N masses (2 to 64, default 8) on vertical rails, moving sideways only, with tension k, bending κ and a stretching nonlinearity k3 (below). Ends (a select next to Masses; changing it returns the beam to rest): clamped–clamped (default), pinned–pinned, or cantilever (clamped at the left, free at the right). Strike gives energy E (log slider, 10 µJ to 100 J, default 2.51 J) as velocity, on one mass alone (default mass 2) or in the shape of one normal mode. The card draws the beam from the side, displacements scaled to fit (scale given), with rails as faint lines, a hatched wall for a clamp and a triangle for a pin, plus the time, energy and relative energy error. The Ghost chip (as in fput-chain; controls in `../common/ghost.ts`) adds a second beam started from the same state with every mass moved by 10^k / sqrt(N) m (total offset 10^k m, k = -12 to -1, default -10), run in lockstep without renormalization and drawn in translucent orange underneath; restarted on Strike, on "Restart ghost", when switched on or when the offset changes; a readout gives the separation (root sum of squared displacement differences, m) and the time since the ghost started. Default strike: the separation stays near 1e-10 m for ~10 s, then grows to 1.7e-8 m by 39.5 s, e^5.1, matching λ since the strike (0.13 /s × 39.5 s). A "Real time" card on the right has chips: Mode bars (on; the current harmonic energy of each mode relative to the strike, log scale over 12 decades, with the number of modes sharing the energy, 1 / sum (E_n / sum E)^2, and the mode frequency range; `../common/modebars.ts`), Mode history (off; each mode's share of the harmonic energy over the last 300 s as a heat map, mode 1 at the bottom; `LiveModeHistory` in `../common/modehistory.ts`), and charts (`../common/traces.ts`): Lyapunov (on; the largest Lyapunov exponent over the last 10 s and since the strike, last 60 s shown, the first 5 s after a strike left out of the y range) Mode energies (off; log, last 60 s, values below 1e-12 of the strike left out), Energy error (off) and Separation (log, last 60 s; offered only with the ghost on). The exponent is the two-trajectory method of `../common/lyapunov.ts` with the beam's shadow in `lyapunov.ts`: a hidden second beam 1e-8 away, displacements in units of A / sqrt(N) and momenta in units of m omega_1 A / sqrt(N), A the mode-1 amplitude at the nonlinear scale. Speed 0.1x to 20x real time; Pause or space. Parameters (m, k, κ, k3, a, τ, stretching form) fold out with derived values (mode frequencies, ratios to mode 1, the nonlinear scale, integrator step); Apply returns the beam to rest.
66
7-"Sped up · listen" (as in fput-chain): each Strike renders the same strike in a worker (`render-worker.ts`), sped up so the lowest mode sounds at the Pitch (5 to 1000 Hz, default 100), and plays it; the page starts muted. A damped note is the whole decay to -60 dB. A red warning shows when the top mode is above half the sample rate (it would alias). Signals (`signals.ts`): velocity (default), displacement or acceleration of mass j (default 2), or the sum of accelerations (the net force of the supports). Replay, Save WAV (named e.g. beam-clamped-local-N8-mass2-2.51J-v2-100Hz.wav), spectrogram over the whole note with dashed lines at the mode pitches. Chips: Spectrogram (on), Modes (off; the note's mode-energy shares over the whole note, one column per 256 samples, from the worker; `ModeHistory` in `../common/modehistory.ts`), Lyapunov (on; the note's exponent over the last 10 s of physical time, from a shadow run in the worker; the first 5 s of physical time left out of the y range; the shadow runs only while this chart is shown, as it doubles the cost) and Energy (log). They span the whole note and share one cursor (playhead while playing; hover, click to pin). The render loop is shared with fput-chain (`../common/note-render.ts`). The status line gives the render time (default note 1.0 to 1.4 s with the shadow, 0.6 s without).
7+"Sped up · listen" (as in fput-chain): each Strike renders the same strike in a worker (`render-worker.ts`), sped up so the lowest mode sounds at the Pitch (5 to 1000 Hz, default 100), and plays it; the page starts muted. A damped note is the whole decay to -60 dB. A red warning shows when the top mode is above half the sample rate (it would alias). Signals (`signals.ts`): velocity (default), displacement or acceleration of mass j (default 2), or the sum of accelerations (the net force of the supports). Replay, Save WAV (named e.g. beam-clamped-local-N8-mass2-2.51J-v2-100Hz.wav), spectrogram over the whole note with dashed lines at the mode pitches. Chips: Spectrogram (on), Modes (off; the note's mode-energy shares over the whole note, one column per 256 samples, from the worker; `ModeHistory` in `../common/modehistory.ts`), Lyapunov (on; the note's exponent over the last 10 s of physical time, from a shadow run in the worker; the first 5 s of physical time left out of the y range; the shadow runs only while this chart or λ vs E is shown, as it doubles the cost) Energy (log) and λ vs E (off; `../common/scatter.ts` as in fput-chain: one point per 256 samples, accumulated over notes, log energy axis, colored by time into the note, earlier notes faded, hover for values, Clear; not cleared when parameters change). All but λ vs E span the whole note and share one cursor (playhead while playing; hover, click to pin). The render loop is shared with fput-chain (`../common/note-render.ts`). The status line gives the render time (default note 1.0 to 1.4 s with the shadow, 0.6 s without).
88
99 Live site: https://chaos-apps--listening-for-chaos.vault1-sites.magland.org/beam/ (part of the chaos-apps site).
1010
@@ -18,7 +18,7 @@ Live site: https://chaos-apps--listening-for-chaos.vault1-sites.magland.org/beam
1818 - Nonlinear scale: the energy of mode 1 alone at the amplitude where its stretching energy equals its linear energy (N = 8, local: clamped 176 mJ, pinned 8.7 mJ, cantilever 1.1 mJ).
1919 - Damping: drag proportional to mass, every mode's amplitude decays as exp(-t / τ); applied exactly in half steps around each Verlet step, dissipated energy in W.
2020 - Integrator: velocity Verlet, step 0.03 / omega_eff, omega_eff^2 = (Kmax + 12 k3 d^2) / m, Kmax the top eigenvalue of the stiffness matrix and d = (4 E / k3)^(1/4) (Kirchhoff: the k3 term over sqrt(nS)). Relative energy error <= 8e-5 over 200 mode-1 periods, all ends and both forms, 10 mJ to 100 J.
21-- Default note (clamped, local, 2.51 J on mass 2, τ 540 s, 100 Hz): λ ~0.27 /s at the strike (λ T1 ~1.3), falling into the noise of the 10 s window (±0.05 /s) by ~1.6 s of sound (E ~0.14 J), as the spectrogram resolves into lines. Kirchhoff at the same strike: ~0.04 /s, then noise. A 50 mJ strike: noise throughout. Cantilever: ~0.2 /s for the first ~0.3 s of its 1.3 s note. On Modes, the shares shuffle while λ is up and freeze into steady stripes as it falls (~2 s of sound); on Mode bars about 6 to 7 of the 8 modes share the energy early on.
21+- Default note (clamped, local, 2.51 J on mass 2, τ 540 s, 100 Hz): λ ~0.27 /s at the strike (λ T1 ~1.3), falling into the noise of the 10 s window (±0.05 /s) by ~1.6 s of sound (E ~0.14 J), as the spectrogram resolves into lines. Kirchhoff at the same strike: ~0.04 /s, then noise. A 50 mJ strike: noise throughout. Cantilever: ~0.2 /s for the first ~0.3 s of its 1.3 s note. On Modes, the shares shuffle while λ is up and freeze into steady stripes as it falls (~2 s of sound); on Mode bars about 6 to 7 of the 8 modes share the energy early on. λ vs E over a 2.51 J and a 25 J note (clamped, local): λ in the noise around 0 at low energy, above it from roughly 0.3 J, ~1.4 /s at 25 J (25 J note: 7.5 s, computed in 2.1 s).
2222 - First look (offline, N = 8, hammer on mass 2, no damping, two-trajectory Lyapunov exponent times the mode-1 period T1): local stretching is chaotic from ~1 J (clamped 0.57 at 1 J, 3.3 at 10 J; pinned 1.8 at 1 J; cantilever 1.2 at 0.1 J). Kirchhoff is much less chaotic at the same energy (clamped 0.04 at 1 J, 0.24 at 10 J; pinned ~0 up to 1 J, 0.09 at 10 J).
2323
2424 Code: `beam.ts` (physics, modes, strikes), `main.ts` (page), `signals.ts`, `render-worker.ts`, `lyapunov.ts` (shadow); shared: `../common/note-render.ts`, `../common/modebars.ts`, `../common/modehistory.ts`, `../common/lyapunov.ts`, `../common/traces.ts`, `../common/cursor.ts`, `../common/sound.ts`, `../common/spectrogram.ts`, `../common/wav.ts`, `../common/mute.ts`, `../common/ghost.ts`, `../common/format.ts`, `../common/canvas.ts`, `../common/help.ts`.
src/beam/index.htmlmodified+5−1View file
@@ -119,6 +119,10 @@
119119 <figure id="spec-fig"><div id="spectrogram"></div></figure>
120120 <figure id="modes-fig" hidden><div id="mode-history"></div></figure>
121121 <div id="sound-traces"></div>
122+ <figure id="scatter-fig" hidden>
123+ <div id="scatter"></div>
124+ <div class="row scatter-actions"><button id="scatter-clear" type="button">Clear</button></div>
125+ </figure>
122126 </details>
123127 </main>
124128 <dialog id="help">
@@ -145,7 +149,7 @@
145149 <template id="help-sound">
146150 <h2>Sped up · listen</h2>
147151 <p>A copy of the same strike, run fast enough to be heard: Pitch sets where the lowest normal mode lands, and the whole decay plays in a few seconds. While this panel is open each Strike plays a note; Replay plays the last one again with the current settings.</p>
148- <p>Save WAV saves the last note as heard (16-bit mono, normalized). Signal chooses what is heard. The charts span the whole note: the spectrogram, with dashed lines at the normal-mode pitches, the Lyapunov exponent over the last 10 s of the beam's own time, the energy, and Modes, each mode's share of the energy as a heat map (mode 1 at the bottom). A warning shows when the top mode is above half the sample rate and would alias.</p>
152+ <p>Save WAV saves the last note as heard (16-bit mono, normalized). Signal chooses what is heard. The charts span the whole note: the spectrogram, with dashed lines at the normal-mode pitches, the Lyapunov exponent over the last 10 s of the beam's own time, the energy, and Modes, each mode's share of the energy as a heat map (mode 1 at the bottom). λ vs E collects the exponent against the energy over notes, earlier notes faded (Clear starts over). A warning shows when the top mode is above half the sample rate and would alias.</p>
149153 </template>
150154 <script type="module" src="./main.ts"></script>
151155 </body>
src/beam/main.tsmodified+22−2View file
@@ -14,6 +14,7 @@ import { drawModeBars } from "../common/modebars";
1414 import { LiveModeHistory, ModeHistory } from "../common/modehistory";
1515 import { setupMute } from "../common/mute";
1616 import { DIAG_EVERY, END_RATIO, LYAP_WINDOW, UNDAMPED_LENGTH } from "../common/note-render";
17+import { ScatterPlot } from "../common/scatter";
1718 import { Player } from "../common/sound";
1819 import { NoteSpectrogram } from "../common/spectrogram";
1920 import { TracePanel } from "../common/traces";
@@ -309,6 +310,22 @@ const soundTraces = new TracePanel(
309310 ],
310311 10, 960, soundCursor,
311312 );
313+// Lambda against energy, accumulated over notes (chip after the trace chips).
314+const scatter = new ScatterPlot(960);
315+document.getElementById("scatter")!.appendChild(scatter.canvas);
316+const scatterFig = document.getElementById("scatter-fig")!;
317+{
318+ const box = document.createElement("input");
319+ box.type = "checkbox";
320+ box.addEventListener("change", () => (scatterFig.hidden = !box.checked));
321+ const text = document.createElement("span");
322+ text.textContent = "λ vs E";
323+ const chip = document.createElement("label");
324+ chip.className = "chip";
325+ chip.append(box, text);
326+ document.getElementById("sound-chips")!.appendChild(chip);
327+}
328+document.getElementById("scatter-clear")!.addEventListener("click", () => scatter.clear());
312329 let spec: NoteSpectrogram | null = null;
313330 let note: {
314331 x: Float32Array; energy: Float32Array; lambda: Float32Array; modeEnergy: Float32Array; N: number; rate: number;
@@ -329,6 +346,7 @@ function feedDisplays(upTo: number) {
329346 noteEnergy = note.energy[fed / DIAG_EVERY];
330347 noteLambda = note.lambda[fed / DIAG_EVERY];
331348 soundTraces.sample(fed / note.rate);
349+ scatter.add(noteEnergy, noteLambda, fed / note.rate);
332350 const k = fed / DIAG_EVERY;
333351 modeHistory.push((fed + DIAG_EVERY) / note.rate, note.modeEnergy.subarray(k * note.N, (k + 1) * note.N));
334352 }
@@ -356,8 +374,8 @@ function renderNote() {
356374 id: ++requestId, P, state: lastStrike, speedup: speedup(), sampleRate: ctx.sampleRate,
357375 maxLength: 2 * predictedNoteLength(), signal: sig.key, j: j - 1,
358376 // The note's Lyapunov shadow doubles the computation, so it runs only
359- // while its chart is shown.
360- lyapunov: soundTraces.shownLabels().includes("Lyapunov"),
377+ // when a display needs it (Lyapunov chart or λ vs E).
378+ lyapunov: soundTraces.shownLabels().includes("Lyapunov") || !scatterFig.hidden,
361379 };
362380 noteInfo = `${sig.label}${sig.perMass ? ` ${j}` : ""}`;
363381 noteFile =
@@ -389,6 +407,7 @@ worker.onmessage = (e: MessageEvent<RenderResult>) => {
389407 document.getElementById("spectrogram")!.replaceChildren(spec.canvas);
390408 soundTraces.clear();
391409 soundTraces.setFixed({ t0: 0, span });
410+ scatter.newNote(span);
392411 const secs = (performance.now() - renderStart) / 1000;
393412 soundStatus.textContent = `${noteInfo} · ${fmtDuration(span)} · computed in ${secs.toFixed(1)} s`;
394413 player.play(x);
@@ -592,6 +611,7 @@ function frame(now: number) {
592611 if (!specFig.hidden) spec?.draw();
593612 if (!modesFig.hidden) modeHistory.draw();
594613 soundTraces.draw();
614+ if (!scatterFig.hidden) scatter.draw();
595615 }
596616 requestAnimationFrame(frame);
597617 }
src/beam/style.cssmodified+1−0View file
@@ -12,6 +12,7 @@ canvas { display: block; width: 100%; }
1212 #toggles { display: contents; }
1313 #mode-history canvas, #live-modes canvas { width: 100%; height: auto; border-radius: 4px; }
1414 #sound-chips { margin-left: auto; }
15+#scatter canvas { width: 100%; height: auto; }
1516 input[type="number"] { width: 4.5em; font-family: var(--mono); }
1617 #energy-value { min-width: 4.5em; text-align: right; }
1718 details.panel { margin-top: 16px; border: 1px solid var(--line); padding: 16px; }