Double Pendulum — how it works

Two rods, two bobs, one pivot, and the most famous demonstration of deterministic chaos.

The state is four numbers

The two angles

θ₁ and θ₂ are the angles each rod makes with straight down; ω₁ and ω₂ are how fast those angles are changing. Everything else — where the bobs are, how much energy the system has, what it will do next — follows from those four and the fixed hardware (rod lengths L₁, L₂, masses m₁, m₂, gravity g).

The forces, as the notebook drew them

Forces on the upper bob Forces on the lower bob

Gravity pulls each bob straight down. Each rod's tension resolves that pull into the direction its bob is actually free to move. The difficulty is the coupling: the lower rod pulls back on the upper bob, so neither rod can be solved on its own — write down one and the other appears in it.

Doing that exactly gives two coupled second-order equations for ω̇₁ and ω̇₂ that share one denominator:

den = 2m₁ + m₂ − m₂·cos(2θ₁ − 2θ₂)

No small-angle approximation. sin θ ≈ θ would make the system linear, solvable in closed form, and completely wrong: it is precisely the nonlinearity that produces the chaos this plugin exists to show. The exact form is carried over from the notebook verbatim (PendulumMath.Derivatives).

Integration: RK4, and why the drift number is there

There is no closed-form solution, so the state is stepped forward numerically with classical Runge–Kutta 4. Every run reports its energy drift, because that is the one honest check on whether the picture can be believed at all: total mechanical energy is exactly constant in the real system, so any drift is the integrator's error and nothing else.

🚨 Drift is measured against (m₁+m₂)·g·(L₁+L₂), never against the run's own initial energy. Total energy is defined only up to a constant, and for the most natural release of all — both rods horizontal, at rest — it is exactly zero. Dividing by it turns a flawless integrator into an infinite error. The first draft of these tests did precisely that and reported a drift of 2.5×10⁵ for a run whose true drift was 1.1×10⁻¹¹.

Motion without scripts

Controls.Html does not execute injected script, so the animation is data: the trajectory is integrated once, sampled to ~240 frames, and handed to the browser as SVG SMIL keyframes. Deterministic, identical for every viewer, and nothing is mutated to show it — the honest answer to the notebook's Task.Run + PeriodicTimer mutation of a live scope.

Languages

Every explanation on the Live and Play pages exists in English and German, in PendulumTexts — one pure table per language. The page picks its language from ?lang=de first, then the viewer's profile locale, then English; a 🌐 line above the animation switches. Release NAMES stay English: they are also the ?release= URL parameter.

Numbers are always formatted invariant-culture, in both languages — a German page rendering 1,8 beside SVG coordinates that use 1.8 reads as two different documents, and a decimal comma inside a markdown table cell is indistinguishable from a thousands separator. A test pins it.

One control

There used to be four routes — a gallery, a playground, a walkthrough and its chrome-free twin — and moving between them meant leaving the pendulum you were watching. There is now one screen:

┌──────────────────────────────────────────────┐
│      the pendulum running, trail growing     │   ← behind the bob, not ahead of it
├──────────────────────────────────────────────┤
│ rods 1/1  masses 1/1  g 9.8  90°/90°  50s    │
│                    [ Run ]  [ 🎲 Surprise ]  │
├──────────────────────────────────────────────┤
│ ‹The physics› ‹Examples› ‹Try yourself›      │
└──────────────────────────────────────────────┘
Area What it is
Live (default) The whole control. ?release=Chaotic opens on a named release; ?size=900 enlarges; ?lang=de picks the language.
Play The same control. The old playground route, kept alive so no link, bookmark or embed 404s.
Tests The physics suite, executed on the mesh.

Three things about it are deliberate:

The pendulum carries no name of its own: PendulumContent.NameOf recognises it by its setup, so a release loaded from the picker is called Chaotic and gets Chaotic's note, and the moment a number is nudged off it, it quietly stops being Chaotic.

Reading the phase portrait

θ₁ against ω₁, one dot per step, dark to bright over time. This is the view that turns "it looks messy" into something checkable:

Run Gentle swing and then Chaotic and compare the two portraits. That difference is the whole subject.

The five releases

Release What it shows
Gentle swing Under ~20° the system is not chaotic: two coupled harmonic oscillators, energy sloshing on a regular beat. The control case.
Chaotic Both rods horizontal, from rest — the canonical demonstration. A billionth of a radian rewrites the trace within seconds.
Over the top Released almost inverted; the lower bob loops the loop, never the same way twice. The hardest case for the integrator, so watch the drift here.
Heavy lower bob m₂ = 4·m₁ — a heavy tail dominates the coupling and drags the upper rod instead of following it.
Long lower rod L₂ = 1.8·L₁ — reach and period both grow with √L, so slower motion over a much larger area.

Authoring a node

Content is the setup plus the release point plus the integration settings:

{ "nodeType": "DoublePendulum/Pendulum",
  "content": { "$type": "PendulumContent", "preset": "Chaotic" } }

Naming a preset fills in the rest; explicit numbers are the other way to say it. Either way the node pins that release and the page opens on it — and a node that authors nothing rolls one instead, which is what DoublePendulum/Live does. A pinned node is rendered from its OWN content rather than through the settings model, so an authored omega1/omega2 survives: the form offers where you let go, not how hard you throw, and routing a spun release through it would silently zero the spin.

There is no body field any more: the note under a release is the Examples tab's, keyed off the preset, so a per-node one would describe whichever release the page happened to roll. PendulumContent.Validated() guards lengths, masses and the step so an admissible setup can never divide by zero, and the playground clamps every typed field on top of that (PendulumSettings.ToContent) — the generated number field does not enforce [Range], so a pasted NaN arrives exactly as typed.

Original notebook: Double Pendulum, Systemorph Cloud. The force and angle diagrams are the originals. Rebuilt for MeshWeaver in 2026.