Beam: Masses refines the same beam (parameters for 8 masses, scaled with the spacing), so the low modes, decay times and note length stay put as N grows
4 changed files+40−12
src/beam/README.mdmodified+2−1View file
@@ -1,6 +1,6 @@
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), step 5 (λ vs E), curvature (rise h of a curved rest shape), frequency-dependent loss (τ top).
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), curvature (rise h of a curved rest shape), frequency-dependent loss (τ top), Masses as resolution (refine).
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, at rest shape plus displacement, scaled to fit (scale given; the rest shape is a faint line), 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, rise h, a, τ, τ top, 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
@@ -11,6 +11,7 @@ Live site: https://chaos-apps--listening-for-chaos.vault1-sites.magland.org/beam
1111 ## Model
1212
1313 - SI units. Defaults m = 1 kg, k = 0, κ = 20 N/m, k3 = 4000 N/m^3, a = 0.5 m (drawing only), τ = 540 s, local stretching, clamped ends.
14+- Resolution: the Parameters values are for the beam with 8 masses (REF_N); Masses (2 to 64) refines the same beam (`refine` in `beam.ts`): same length, rise, drag, strike energies; with spacing ratio r = span(8) / span(N) (span N + 1 spacings between two supports, N for a cantilever) m x r, k / r, κ / r^3, k3 / r^3, a x r, and the loss dashpots / r (τ top is converted so a wave of given length decays alike at any N). N = 8 returns the parameters unchanged (checked, all ends). The derived values show the scaled constants when N is not 8. Before this (to 2026-10-09) more masses made the beam longer at fixed spacing: mode 1 fell as ~1/N^2 (0.211 Hz at N = 8, 0.015 Hz at 32), so at fixed τ the note shrank (7.9 s at N = 8, 0.6 s at 32, 100 Hz pitch). Now (clamped, local, τ 540, τ top 50): f1 0.207 / 0.211 / 0.206 / 0.202 / 0.200 Hz at N = 4 / 8 / 16 / 32 / 64; f2/f1 2.24 / 2.59 / 2.71 / 2.75 / 2.754 and f3/f1 3.37 / 4.68 / 5.20 / 5.35 / 5.39 (beam theory 2.757, 5.404); pinned 3.998, 8.986 at 64 (4, 9); cantilever 6.265, 17.53 (6.27, 17.55); mode-1 decay ~375 s, mode 3 ~122 s; nonlinear scale 176 / 167 / 161 / 158 mJ at 8 / 16 / 32 / 64; default note 7.9 / 7.7 / 7.5 s. Cost: the step falls ~1/N^2 (0.58 ms at 8, 0.087 ms at 32 for 2.51 J), so the default note (with the Lyapunov shadow) takes ~1 s to compute at N = 8, 8.2 s at 16 and 55 s at 32 in the pane. The strike and listening masses are indices, so at larger N "mass 2" is a narrower blow nearer the end.
1415 - Masses on rails: only sideways motion u_j, so every axial force is taken by the rails and tension and stretching are defined at a free end too.
1516 - Potential: springs between neighbours (and to each support), k d^2 / 2 with d = u_j - u_{j-1}; bending κ c^2 / 2 at each mass with both neighbours, c = u_{j-1} - 2 u_j + u_{j+1} (supports at u = 0). Pinned support: no extra term (ghost u_{-2} = -u_0 gives c = 0 there). Clamped: a joint at the support with ghost u_{-2} = u_0, c = 2 u_0, energy 2 κ u_0^2. Free end: no spring or joint beyond the last mass.
1617 - Stretching: "local", k3 d^4 / 4 per spring (FPUT); or "Kirchhoff", k3 S^2 / (4 nS) with S the sum of d^2 over the nS springs, which acts as one tension k3 S / nS shared by all springs. They agree when all springs are stretched alike.
src/beam/beam.tsmodified+19−0View file
@@ -57,6 +57,25 @@ export interface Params {
5757 tauTop: number; // s, amplitude decay time of the top of the band from the loss alone; 0 = off
5858 }
5959
60+// Resolution: the page's parameters describe the beam at REF_N masses;
61+// refine() gives the same beam (same length, rise, strike energies, decay
62+// times) with N masses. The spacing becomes a r, with r the ratio of the
63+// spans in spacings (N + 1 between two supports, N for a cantilever), and
64+// each constant follows its continuum origin: m = rho A a (x r), k = T / a
65+// (/ r), kappa = EI / a^3 (/ r^3), k3 (stretching, EA / (2 a^3), / r^3), the
66+// loss dashpots c = eta / a (/ r, so a wave of given length decays at the
67+// same rate at any N). Drag (rate per unit mass) and the rise do not change.
68+// At N = REF_N the parameters come back unchanged.
69+export const REF_N = 8;
70+export function refine(base: Params, N: number): Params {
71+ const span = (n: number) => (base.ends === "cantilever" ? n : n + 1);
72+ const r = span(base.N) / span(N);
73+ const P = { ...base, N, m: base.m * r, k: base.k / r, kappa: base.kappa / r ** 3, k3: base.k3 / r ** 3, a: base.a * r };
74+ // tauTop is set at the top of the band, which moves with N: keep c / r.
75+ if (base.tauTop > 0) P.tauTop = base.tauTop * (laplacianMax(base) / laplacianMax(P)) * r * r;
76+ return P;
77+}
78+
6079 // Drag rate gamma (1/s).
6180 export const dragRate = (P: Params) => (P.tau > 0 ? 2 / P.tau : 0);
6281 export const isDamped = (P: Params) => P.tau > 0 || P.tauTop > 0;
src/beam/index.htmlmodified+2−1View file
@@ -140,7 +140,8 @@
140140 </template>
141141 <template id="help-params">
142142 <h2>Parameters</h2>
143- <p>Mass, tension k, bending κ (either may be 0, not both), stretching k3, the rise h of a curved rest shape (0 for straight), the rest spacing a (drawing only), the decay time τ of the amplitude (drag on the masses, all modes alike; 0 for none), and τ top, the decay time a frequency-dependent loss (dashpots alongside the springs) gives the highest modes, less for lower ones (0 for none); the derived values list each mode's decay time. Apply restarts the beam at rest.</p>
143+ <p>Mass, tension k, bending κ (either may be 0, not both), stretching k3, the rise h of a curved rest shape (0 for straight), the rest spacing a (drawing only), the decay time τ of the amplitude (drag on the masses, all modes alike; 0 for none), and τ top, the decay time a frequency-dependent loss (dashpots alongside the springs) gives the highest modes (at 8 masses), less for lower ones (0 for none); the derived values list each mode's decay time. Apply restarts the beam at rest.</p>
144+ <p>The values are for the beam with 8 masses. Masses refines the same beam: same length, rise, strike energies and drag, with each mass and spring constant scaled for the finer spacing (mass with the spacing, tension and loss inversely, bending and stretching as its inverse cube), so the low modes stay put and more masses add higher ones. The derived values give the scaled constants. τ top refers to the highest mode at 8 masses.</p>
144145 <p>Stretching "local" stiffens each spring by its own stretch, as in the FPUT chain. "Kirchhoff" gives all springs one shared tension set by the total stretch, as for a beam whose ends are held apart. The two agree when every spring is stretched alike. The nonlinear scale below is the energy of mode 1 at which its stretching energy equals its linear energy.</p>
145146 </template>
146147 <template id="help-realtime">
src/beam/main.tsmodified+17−10View file
@@ -1,7 +1,7 @@
11 import "../common/style.css";
22 import "./style.css";
33 import {
4- Modes, Verlet, copyState, decayTimes, energy, isDamped, newState, nonlinearScale, restShape, stepFor, strikeMass, strikeMode,
4+ Modes, REF_N, Verlet, copyState, decayTimes, energy, isDamped, newState, nonlinearScale, refine, restShape, stepFor, strikeMass, strikeMode,
55 type Ends, type Params, type State, type Stretch,
66 } from "./beam";
77 import { fit, indexColor } from "../common/canvas";
@@ -39,7 +39,10 @@ const pauseButton = document.getElementById("pause") as HTMLButtonElement;
3939 // clamped at both ends, with the FPUT chain's stretching coefficient.
4040 // tau: amplitude decay time from drag; 540 s makes the default strike ring
4141 // down in about 8 s of sound at the default pitch.
42-let P: Params = { N: 8, m: 1, k: 0, kappa: 20, k3: 4000, stretch: "local", ends: "clamped", rise: 0, a: 0.5, tau: 540, tauTop: 0 };
42+// base: the page's parameters, for the beam at REF_N masses; P: the same beam
43+// refined to the chosen number of masses (see refine).
44+let base: Params = { N: REF_N, m: 1, k: 0, kappa: 20, k3: 4000, stretch: "local", ends: "clamped", rise: 0, a: 0.5, tau: 540, tauTop: 0 };
45+let P: Params = base;
4346 const SAMPLE_DT = 0.01; // s between plotted samples
4447
4548 // The beam. It sits at rest until the first Strike; changing parameters puts
@@ -213,7 +216,7 @@ const paramInputs = {
213216 const stretchSelect = document.getElementById("p-stretch") as HTMLSelectElement;
214217 const paramStatus = document.getElementById("param-status")!;
215218 function showParams() {
216- for (const [key, el] of Object.entries(paramInputs)) el.value = String(P[key as keyof typeof paramInputs]);
219+ for (const [key, el] of Object.entries(paramInputs)) el.value = String(base[key as keyof typeof paramInputs]);
217220 stretchSelect.value = P.stretch;
218221 endsSelect.value = P.ends;
219222 showDerived();
@@ -231,7 +234,9 @@ function showDerived() {
231234 const hz = Array.from(modes.omega, (w) => w / (2 * Math.PI));
232235 const list = P.N <= 8 ? hz.map((f) => f.toPrecision(3)).join(", ") : `${hz[0].toPrecision(3)} … ${hz[P.N - 1].toPrecision(3)}`;
233236 const ratios = hz.slice(1, 6).map((f) => (f / hz[0]).toFixed(2)).join(", ");
237+ const g = (x: number) => (x === 0 ? "0" : x.toPrecision(4));
234238 document.getElementById("params")!.textContent =
239+ (P.N === REF_N ? "" : `${`at ${P.N} masses`.padEnd(18)}m ${g(P.m)} kg, k ${g(P.k)}, κ ${g(P.kappa)} N/m, k3 ${g(P.k3)} N/m³, a ${g(P.a)} m\n`) +
235240 `normal modes ${list} Hz\n` +
236241 `ratios to mode 1 ${ratios}${P.N > 6 ? ", …" : ""}; top ${(hz[P.N - 1] / hz[0]).toFixed(1)}\n` +
237242 `nonlinear scale ${fmtEnergy(nonlinearScale(P, modes))} (mode 1: stretching = linear energy)\n` +
@@ -429,10 +434,12 @@ worker.onmessage = (e: MessageEvent<RenderResult>) => {
429434 };
430435 replayButton.addEventListener("click", renderNote);
431436
432-// Set new parameters: everything back to rest.
433-function setParams(next: Params) {
437+// Set new parameters (for REF_N masses) and the number of masses:
438+// everything back to rest.
439+function setParams(next: Params, N: number) {
434440 const shown = traces ? traces.shownLabels() : ["Lyapunov"];
435- P = next;
441+ base = next;
442+ P = refine(base, N);
436443 modes = new Modes(P);
437444 H = stepFor(P, modes, 0);
438445 s = newState(P.N);
@@ -461,13 +468,13 @@ function setParams(next: Params) {
461468 showPitch();
462469 showParams();
463470 }
464-setParams(P);
471+setParams(base, REF_N);
465472
466473 nInput.addEventListener("change", () => {
467474 const N = Math.min(64, Math.max(2, Math.round(Number(nInput.value) || P.N)));
468- setParams({ ...P, N });
475+ setParams(base, N);
469476 });
470-endsSelect.addEventListener("change", () => setParams({ ...P, ends: endsSelect.value as Ends }));
477+endsSelect.addEventListener("change", () => setParams({ ...base, ends: endsSelect.value as Ends }, P.N));
471478 document.getElementById("param-apply")!.addEventListener("click", () => {
472479 const v = Object.fromEntries(Object.entries(paramInputs).map(([key, el]) => [key, Number(el.value)]));
473480 const ok = Object.entries(v).every(
@@ -478,7 +485,7 @@ document.getElementById("param-apply")!.addEventListener("click", () => {
478485 return;
479486 }
480487 paramStatus.textContent = "";
481- setParams({ ...P, ...v, stretch: stretchSelect.value as Stretch });
488+ setParams({ ...base, ...v, stretch: stretchSelect.value as Stretch }, P.N);
482489 });
483490
484491 const TEXT = "#23262b", MUTED = "#7a7d82", LINE = "#e3e2dc";