/ listening-for-chaos / chaos-apps
Sign in
listening-for-chaos / chaos-apps
Click to strike from the Poincare section; deploy site
- 'Click to strike' option: clicking the section strikes the pendulum from that exact state at the section's energy - Vite config with relative base; workflow builds and deploys the site on every push to main
Jeremy Magland <jeremy.magland@gmail.com> committed commit ecb951f864a0 parent 092e715 Browse files
7 changed files+85−9
src/double-pendulum/.github/workflows/pages.ymladded+41−0View file
@@ -0,0 +1,41 @@
1+name: Deploy site
2+
3+on:
4+ push:
5+ branches: [main]
6+ workflow_dispatch:
7+
8+permissions:
9+ contents: read
10+ pages: write
11+ id-token: write
12+
13+concurrency:
14+ group: pages
15+ cancel-in-progress: true
16+
17+jobs:
18+ build:
19+ runs-on: ubuntu-latest
20+ steps:
21+ - uses: actions/checkout@v4
22+ - uses: actions/setup-node@v4
23+ with:
24+ node-version: 22
25+ cache: npm
26+ - run: npm ci
27+ - run: npm run build
28+ - uses: actions/configure-pages@v5
29+ - uses: actions/upload-pages-artifact@v3
30+ with:
31+ path: dist
32+
33+ deploy:
34+ needs: build
35+ runs-on: ubuntu-latest
36+ environment:
37+ name: github-pages
38+ url: ${{ steps.deployment.outputs.page_url }}
39+ steps:
40+ - id: deployment
41+ uses: actions/deploy-pages@v4
src/double-pendulum/README.mdmodified+3−1View file
@@ -2,7 +2,9 @@
22
33 Interactive double pendulum, step 1 of "Listening for chaos". Work in progress.
44
5-Current state: the pendulum hangs at rest until Strike, which starts it from hanging rest with the energy on the slider (default 50 J), plus a random relative jitter of up to 1e-9 in each angle and momentum (so chaotic strikes differ from one another while regular ones sound alike); Pause freezes the animation. The Ghost toggle adds a second pendulum (translucent orange) started from the same state with the outer arm's angle offset by 10^-12 to 10^-1 rad (default 10^-10), restarted on Strike or Reset, with a readout of the angular separation and the time since the ghost started: it grows exponentially when the motion is chaotic and stays tiny when it is regular. Charts "Separation" (ghost separation, log scale, last 30 s) and "Lyapunov" (largest Lyapunov exponent over a 10 s window and since the strike, last 30 s). Parameters (masses, lengths, g, decay time, presets) fold out underneath. Toggle chips choose the plots: a spectrogram of the selected signal (frequency axis auto-scaled, dashed lines at the normal modes) and live traces of energies, energy balance error, angles, momenta and bob positions. The Poincare section panel (closed by default) has its own energy slider and uses the same masses and lengths, without damping: each time the inner arm swings down through vertical moving right, the outer arm's angle and momentum are plotted. 42 orbits start on a grid inside the allowed region (outlined) and leave 300 dots each, computed in a Web Worker and drawn as they arrive; regular orbits trace closed curves, chaotic ones scatter. Hover to pick out an orbit (with its Lyapunov exponent), click inside the outline to start a new orbit there, and optionally color orbits by Lyapunov exponent (log scale, blue regular to red chaotic). The Sound panel at the bottom starts closed; opening it switches the sound engine on (closing it switches it off). While it is open, every Strike plays a note: a separate, sped-up copy of the motion from the same strike; a damped note ends once it has decayed by 60 dB. The mute button (speaker icon) starts muted: notes are still computed and shown, just silent. The panel also has "Sound of" (the signal, also used by the animation spectrogram), volume, pitch (where the small-amplitude lower mode lands, 30 to 1000 Hz, default 100 Hz), and optional whole-note displays that do not scroll (strike at the left edge, end of the note at the right): a spectrogram of the sound, the largest Lyapunov exponent of the motion being heard (10 s window, physical time), its energy (from 0), and a scatter of lambda against energy accumulated over notes (colored by time into the note, earlier notes faded, Clear button). Spectrogram on by default. Hovering over any chart (animation or sound) shows a time cursor and the values there; a click pins it. Charts in the same card share the cursor. The time axis spans the predicted note length ((tau/2) ln 1e6 of physical time, divided by the speedup), is extended if the note runs longer, and is fitted to the note when it ends.
5+Current state: the pendulum hangs at rest until Strike, which starts it from hanging rest with the energy on the slider (default 50 J), plus a random relative jitter of up to 1e-9 in each angle and momentum (so chaotic strikes differ from one another while regular ones sound alike); Pause freezes the animation. The Ghost toggle adds a second pendulum (translucent orange) started from the same state with the outer arm's angle offset by 10^-12 to 10^-1 rad (default 10^-10), restarted on Strike or Reset, with a readout of the angular separation and the time since the ghost started: it grows exponentially when the motion is chaotic and stays tiny when it is regular. Charts "Separation" (ghost separation, log scale, last 30 s) and "Lyapunov" (largest Lyapunov exponent over a 10 s window and since the strike, last 30 s). Parameters (masses, lengths, g, decay time, presets) fold out underneath. Toggle chips choose the plots: a spectrogram of the selected signal (frequency axis auto-scaled, dashed lines at the normal modes) and live traces of energies, energy balance error, angles, momenta and bob positions. The Poincare section panel (closed by default) has its own energy slider and uses the same masses and lengths, without damping: each time the inner arm swings down through vertical moving right, the outer arm's angle and momentum are plotted. 42 orbits start on a grid inside the allowed region (outlined) and leave 300 dots each, computed in a Web Worker and drawn as they arrive; regular orbits trace closed curves, chaotic ones scatter. Hover to pick out an orbit (with its Lyapunov exponent), click inside the outline to start a new orbit there (with "Click to strike" on, this also strikes the pendulum above from that exact state, at the section's energy, without jitter), and optionally color orbits by Lyapunov exponent (log scale, blue regular to red chaotic). The Sound panel at the bottom starts closed; opening it switches the sound engine on (closing it switches it off). While it is open, every Strike plays a note: a separate, sped-up copy of the motion from the same strike; a damped note ends once it has decayed by 60 dB. The mute button (speaker icon) starts muted: notes are still computed and shown, just silent. The panel also has "Sound of" (the signal, also used by the animation spectrogram), volume, pitch (where the small-amplitude lower mode lands, 30 to 1000 Hz, default 100 Hz), and optional whole-note displays that do not scroll (strike at the left edge, end of the note at the right): a spectrogram of the sound, the largest Lyapunov exponent of the motion being heard (10 s window, physical time), its energy (from 0), and a scatter of lambda against energy accumulated over notes (colored by time into the note, earlier notes faded, Clear button). Spectrogram on by default. Hovering over any chart (animation or sound) shows a time cursor and the values there; a click pins it. Charts in the same card share the cursor. The time axis spans the predicted note length ((tau/2) ln 1e6 of physical time, divided by the speedup), is extended if the note runs longer, and is fitted to the note when it ends.
6+
7+Live site: https://double-pendulum--listening-for-chaos.vault1-sites.magland.org/ (built and deployed by `.github/workflows/pages.yml` on every push to main).
68
79 ```bash
810 npm install
src/double-pendulum/index.htmlmodified+1−0View file
@@ -204,6 +204,7 @@
204204 <input id="pc-energy" type="range" value="20" min="0" max="120" step="0.1" aria-label="Section energy" />
205205 <span id="pc-energy-value" class="num">20.0 J</span>
206206 <label class="chip"><input id="pc-lambda" type="checkbox" /><span>Color by λ</span></label>
207+ <label class="chip" title="Clicking a point also strikes the pendulum from that exact state, at this energy"><input id="pc-strike" type="checkbox" /><span>Click to strike</span></label>
207208 <span id="pc-status" class="muted"></span>
208209 </div>
209210 <figure>
src/double-pendulum/main.tsmodified+18−2View file
@@ -405,6 +405,22 @@ function showEnergy() {
405405 document.getElementById("controls")!.addEventListener("submit", (e) => {
406406 e.preventDefault();
407407 sim.strike(sliderEnergy(), STRIKE_JITTER);
408+ afterStrike();
409+});
410+
411+// Strike from an exact state with energy E (from the Poincare section): the
412+// strike energy is set to E; no jitter.
413+function strikeFrom(y: Float64Array, E: number) {
414+ energyInput.value = String(E);
415+ const s = Float64Array.from(y);
416+ s[4] = 0;
417+ sim.setState(s, energy(P, s), E);
418+ sim.t = 0;
419+ afterStrike();
420+}
421+
422+// Restart everything that follows a strike.
423+function afterStrike() {
408424 resetGhost();
409425 lyap.start(sim);
410426 setRunning(true);
@@ -412,7 +428,7 @@ document.getElementById("controls")!.addEventListener("submit", (e) => {
412428 traces.clear();
413429 spectrogram.clear();
414430 if (audio.enabled) startNote();
415-});
431+}
416432
417433 // Parameter form. Presets keep m1 = m2 = 1 kg and l1 = 1 m and vary l2. With
418434 // equal masses the squared mode ratio is (L + 1 + s) / (L + 1 - s), where
@@ -449,7 +465,7 @@ document.getElementById("param-form")!.addEventListener("submit", (e) => {
449465 });
450466
451467 // Poincare section panel (own energy; same parameters, no damping).
452-const poincare = new PoincarePanel(P);
468+const poincare = new PoincarePanel(P, strikeFrom);
453469
454470 // Apply the parameters in the form: the pendulum returns to rest (waiting for
455471 // a Strike) and the strike energy keeps its fraction of the both-inverted
src/double-pendulum/poincare.tsmodified+13−5View file
@@ -1,7 +1,8 @@
11 // Poincare section panel: its own energy slider (same parameters as the
22 // pendulum above, no damping), the allowed region's outline, orbits seeded on
3-// a grid and computed in a worker, click to add an orbit, hover to highlight
4-// one, and coloring per orbit or by Lyapunov exponent.
3+// a grid and computed in a worker, click to add an orbit (or, with "Click to
4+// strike" on, also strike the pendulum above from that exact state), hover to
5+// highlight one, and coloring per orbit or by Lyapunov exponent.
56
67 import { topEnergy, type Params } from "./pendulum";
78 import { p2Max, sectionState, seedGrid, thetaMax } from "./poincare-core";
@@ -42,12 +43,16 @@ export class PoincarePanel {
4243 private energyValue: HTMLElement;
4344 private status: HTMLElement;
4445 private debounce = 0;
46+ private strikeBox: HTMLInputElement;
47+ private onStrike: (y: Float64Array, E: number) => void;
4548 // Axes (data ranges), set from E.
4649 private thMax = 1;
4750 private pMax = 1;
4851
49- constructor(P: Params) {
52+ constructor(P: Params, onStrike: (y: Float64Array, E: number) => void) {
5053 this.P = P;
54+ this.onStrike = onStrike;
55+ this.strikeBox = document.getElementById("pc-strike") as HTMLInputElement;
5156 this.energyInput = document.getElementById("pc-energy") as HTMLInputElement;
5257 this.energyValue = document.getElementById("pc-energy-value")!;
5358 this.status = document.getElementById("pc-status")!;
@@ -172,11 +177,14 @@ export class PoincarePanel {
172177 this.changed = true;
173178 }
174179
175- // Add an orbit starting at the clicked point, if it is reachable.
180+ // Add an orbit starting at the clicked point, if it is reachable, and with
181+ // "Click to strike" on, strike the pendulum from that state.
176182 private onClick(e: MouseEvent): void {
177183 const [x, y] = this.mouse(e);
178184 const [th2, p2] = this.fromPx(x, y);
179- if (!this.worker || !sectionState(this.P, this.E, th2, p2)) return;
185+ const state = sectionState(this.P, this.E, th2, p2);
186+ if (!this.worker || !state) return;
187+ if (this.strikeBox.checked) this.onStrike(state, this.E);
180188 this.orbits.push({ points: [], lambda: NaN, done: false, clicked: true });
181189 this.finished = false;
182190 this.send({ type: "add", id: this.runId, seed: [th2, p2] });
src/double-pendulum/tsconfig.jsonmodified+1−1View file
@@ -8,5 +8,5 @@
88 "noEmit": true,
99 "skipLibCheck": true
1010 },
11- "include": ["src"]
11+ "include": ["src", "vite.config.ts"]
1212 }
src/double-pendulum/vite.config.tsadded+8−0View file
@@ -0,0 +1,8 @@
1+import { defineConfig } from "vite";
2+
3+// Relative asset paths, so the build works wherever the site is served
4+// (an origin root or a /<collection>/<repo>/site/ path); ES module workers.
5+export default defineConfig({
6+ base: "./",
7+ worker: { format: "es" },
8+});
↑↓ move↵openescclose