Beam: draw the rest shape and the beam at rest shape plus displacement
2 changed files+15−11
src/beam/README.mdmodified+2−2View file
@@ -2,7 +2,7 @@
22
33 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).
44
5-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, rise h, 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.
5+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, τ, 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
77 "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; frequency axis logarithmic by default, from a round number at or below half the lowest mode pitch, with a "log f" chip to switch to linear from 0; each pixel row shows the loudest FFT bin in its band; mode lines each drawn only while at least 6 px from their neighbours: at the default note the six lowest are drawn, modes 7 and 8 are 2 px apart; on the old linear axis to 10 kHz the modes filled the bottom eighth), 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
@@ -14,7 +14,7 @@ Live site: https://chaos-apps--listening-for-chaos.vault1-sites.magland.org/beam
1414 - 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.
1515 - 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.
1616 - 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.
17-- Curvature (toward gongs and cymbals): rise h (m, default 0 = straight) of a stress-free rest shape w0, a half sine between two supports, 1 - cos(pi x / 2L) for a cantilever (flat at the clamp). Stretching is measured from it: d^2 becomes e = d^2 + 2 D0 d (D0 the spring's rest offset) in both forms; bending is unchanged (linear). This adds linear stiffness and a quadratic nonlinearity (softening) to the quartic one (hardening). The animation still draws the displacement u from the rest shape, not w0 + u. The nonlinear scale in the derived values counts only the quartic part. Checked (Node): h = 0 bit-identical to before (all ends, both forms, k = 0 and 10, 50000 damped steps); forces = -grad V to 5e-11 and the linear stiffness = the Hessian at rest to 7e-9 for h = 0.05 and 0.3; energy error <= 7e-5 over 200 T1 up to 100 J.
17+- Curvature (toward gongs and cymbals): rise h (m, default 0 = straight) of a stress-free rest shape w0, a half sine between two supports, 1 - cos(pi x / 2L) for a cantilever (flat at the clamp). Stretching is measured from it: d^2 becomes e = d^2 + 2 D0 d (D0 the spring's rest offset) in both forms; bending is unchanged (linear). This adds linear stiffness and a quadratic nonlinearity (softening) to the quartic one (hardening). The animation draws the beam at w0 + u, with the rest shape as the faint line through the rails, both scaled to fit the largest |w0 + u| (so at rest the arch fills the upper half; small vibrations on a large arch look small). The nonlinear scale in the derived values counts only the quartic part. Checked (Node): h = 0 bit-identical to before (all ends, both forms, k = 0 and 10, 50000 damped steps); forces = -grad V to 5e-11 and the linear stiffness = the Hessian at rest to 7e-9 for h = 0.05 and 0.3; energy error <= 7e-5 over 200 T1 up to 100 J.
1818 - Pitch glide (mode-1 frequency from zero crossings, clamped, local, τ 540 s, cents from the linear mode 1): straight beam hardening, pitch falls as the note decays (mode-1 strike 2.51 J: +1237 c at 2 J, +352 at 0.1 J, +25 at 4 mJ). Curvature flips it: h = 0.1 m hardening at high energy, softening below ~0.1 J (pitch falls, then rises slightly); h = 0.2 m +302 c at 2 J, -232 at 0.5 J, -27 at 20 mJ (mode 2 at 1.98 x mode 1, near 2:1); h = 0.3 m softening throughout (-358 c at 2 J, -49 at 0.1 J), pitch rises as the note decays. Kirchhoff is similar. Default mass-2 strike, h = 0.3: lowest line rises from ~78 to 100 Hz over ~3 s of sound (17 s note, x219: mode 1 is 0.457 Hz, so the same τ covers fewer periods per second of sound), with steady difference tones (e.g. mode 2 - mode 1 at 79 Hz) from the quadratic term.
1919 - Normal modes of the linear beam from the stiffness matrix (assembled from the force routine) by Jacobi; mode energies as in fput-chain. Checked: pinned frequencies equal the closed form omega_n^2 = (4 k s^2 + 16 κ s^4) / m, s = sin(pi n / (2 (N + 1))), to 1e-14; N = 32 ratios clamped 2.75, 5.35, 8.78 (beam theory 2.76, 5.40, 8.93), cantilever 6.26, 17.48, 34.09 (6.27, 17.55, 34.39). At N = 8: clamped 1, 2.59, 4.68, 6.97, top 13.2; pinned 1, 3.88, 8.29, top 32.2; cantilever 1, 6.13, 16.4, top 75.
2020 - 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).
src/beam/main.tsmodified+13−9View file
@@ -1,7 +1,7 @@
11 import "../common/style.css";
22 import "./style.css";
33 import {
4- Modes, Verlet, copyState, energy, newState, nonlinearScale, stepFor, strikeMass, strikeMode,
4+ Modes, Verlet, copyState, energy, newState, nonlinearScale, restShape, stepFor, strikeMass, strikeMode,
55 type Ends, type Params, type State, type Stretch,
66 } from "./beam";
77 import { fit, indexColor } from "../common/canvas";
@@ -51,7 +51,7 @@ let H: number; // integrator step (s), set at each strike from its energy (see s
5151 let E0 = 0; // energy of the last strike
5252 let t = 0;
5353 let running = false;
54-let ampl = 0; // recent largest |u|, for scaling the drawing
54+let ampl = 0; // recent largest height |w0 + u| (rest shape plus displacement), for scaling the drawing
5555 let lastStrike: State | null = null; // state right after the last strike (for the sound)
5656 let Ek = new Float64Array(0); // mode energies at the last sample or draw
5757
@@ -475,8 +475,9 @@ const TEXT = "#23262b", MUTED = "#7a7d82", LINE = "#e3e2dc";
475475 const MONO = "12px ui-monospace, SFMono-Regular, Menlo, monospace";
476476
477477 // The beam seen from the side: masses on their rails (faint vertical lines),
478-// the beam through them and the supports, displacements scaled to fit (the
479-// scale is given). Supports: a clamp is a hatched wall, a pin a triangle; a
478+// the beam through them and the supports, at height w0 + u (rest shape plus
479+// displacement), scaled to fit (the scale is given); the rest shape is the
480+// faint line through the rails (straight when the rise is 0). Supports: a clamp is a hatched wall, a pin a triangle; a
480481 // free end has none. The ghost, if on, is drawn the same way in translucent
481482 // orange, underneath.
482483 const GHOST = "rgba(214, 120, 40, 0.45)";
@@ -491,7 +492,9 @@ function drawBeam() {
491492 const top = 34, bottom = Ht - 14, y0 = (top + bottom) / 2, half = 0.45 * (bottom - top);
492493 const scale = ampl > 0 ? half / ampl : 0; // px per m
493494
494- // Rails and the rest line.
495+ const w0 = Array.from({ length: N }, (_, j) => restShape(P, j));
496+
497+ // Rails and the rest shape.
495498 ctx.strokeStyle = LINE;
496499 ctx.lineWidth = 1;
497500 ctx.beginPath();
@@ -500,7 +503,8 @@ function drawBeam() {
500503 ctx.lineTo(x(j), bottom);
501504 }
502505 ctx.moveTo(m, y0);
503- ctx.lineTo(free ? x(N - 1) : W - m, y0);
506+ for (let j = 0; j < N; j++) ctx.lineTo(x(j), y0 - scale * w0[j]);
507+ if (!free) ctx.lineTo(W - m, y0);
504508 ctx.stroke();
505509
506510 // Supports.
@@ -539,7 +543,7 @@ function drawBeam() {
539543 // The beam and its masses.
540544 const r = Math.max(2.5, Math.min(6, dx / 5));
541545 const drawState = (st: State, ghostly: boolean) => {
542- const y = (j: number) => y0 - scale * st.u[j];
546+ const y = (j: number) => y0 - scale * (w0[j] + st.u[j]);
543547 ctx.strokeStyle = ghostly ? GHOST : TEXT;
544548 ctx.lineWidth = 2.5;
545549 ctx.lineJoin = "round";
@@ -567,7 +571,7 @@ function drawBeam() {
567571 ctx.textAlign = "right";
568572 ctx.fillText(`energy error ${Math.abs(relErr()).toExponential(1)}`, W - 6, 16);
569573 ctx.font = "11px system-ui, sans-serif";
570- if (ampl > 0) ctx.fillText(`displacement scaled to fit, full height ±${fmtLength(ampl / 0.9)}`, W - 6, Ht - 2);
574+ if (ampl > 0) ctx.fillText(`${P.rise ? "shape" : "displacement"} scaled to fit, full height ±${fmtLength(ampl / 0.9)}`, W - 6, Ht - 2);
571575 ctx.textAlign = "left";
572576 }
573577
@@ -597,7 +601,7 @@ function frame(now: number) {
597601 }
598602 }
599603 let umax = 0;
600- for (const u of s.u) umax = Math.max(umax, Math.abs(u));
604+ for (let j = 0; j < P.N; j++) umax = Math.max(umax, Math.abs(restShape(P, j) + s.u[j]));
601605 ampl = Math.max(umax, 0.998 * ampl);
602606 drawBeam();
603607 ghostUI.show(ghost ? ghostSeparation() : null, t - ghostT0);