Double Pendulum
Twenty-four double pendulums, let go together from the same angles, each a billionth of a radian further round than the last. For ten seconds they swing as one. Then they open into a fan, and then into a tangle: a difference too small to measure grows until it is the whole picture.
About this piece
Twenty-four double pendulums hang from the same pivot. Each is two point masses on two weightless rods, one metre long each in the model, under ordinary gravity (9.81 m/s²). All twenty-four are let go at the same moment from the same angles — the first release is the upper rod at 120° and the lower at −10° from straight down — except that each pendulum’s lower rod starts one billionth of a radian (10−9) further round than its neighbour’s. On a one-metre rod that is a nanometre of arc: no ruler, camera or screen could show it.
For the first ten seconds or so you see one pendulum, because all twenty-four are drawn on top of each other. Then the stack starts to come apart. Measured from the model itself, for that first release the lower bobs of the two outermost pendulums are less than 1% of a rod length apart at 12 s, 9% at 16 s, and more than a full rod length apart by 18 s. From there each one goes its own way. That is the sensitive dependence on initial conditions the double pendulum is the standard classroom example of: the Wikipedia article on it puts it as “the double pendulum undergoes chaotic motion, and clearly shows a sensitive dependence on initial conditions.”
The set runs for 45 seconds, fades, and is let go again from the next of five common starting angles. Measured the same way, the fan opens (outermost bobs 5% of a rod apart) between 10 and 15 seconds after release and has become a tangle (a full rod apart) between 14 and 18 seconds, depending on the angles:
- 120°, −10° — fan at 15.2 s, tangle at 18.0 s
- 150°, 120° — 10.1 s and 14.9 s
- 130°, −60° — 12.0 s and 14.2 s
- 170°, 0° — 10.8 s and 13.9 s
- 110°, 160° — 11.6 s and 14.0 s
Clicking the animation skips to the next release.
The equations it runs
Call the rod angles θ₁ (upper) and θ₂ (lower), measured from straight down, with angular velocities ω₁, ω₂. For two masses m on rods of length l, the kinetic minus potential energy — the Lagrangian, in the general form given in SciPython’s derivation with m₁ = m₂ and l₁ = l₂ — is:
L = m l² ω₁² + ½ m l² ω₂²
+ m l² ω₁ ω₂ cos(θ₁ − θ₂)
+ 2 m g l cos θ₁ + m g l cos θ₂
Putting that through the Euler–Lagrange equations and solving for the two angular accelerations gives, with Δ = θ₁ − θ₂:
θ₁″ = [ −3g sin θ₁ − g sin(θ₁ − 2θ₂)
− 2 sin Δ (ω₂² l + ω₁² l cos Δ) ]
/ [ l (3 − cos 2Δ) ]
θ₂″ = 2 sin Δ (2ω₁² l + 2g cos θ₁
+ ω₂² l cos Δ)
/ [ l (3 − cos 2Δ) ]
The mass cancels out, which is why nothing on the screen depends on how heavy the bobs are. These are exactly the equations on myPhysicsLab’s double pendulum page with m₁ = m₂ and L₁ = L₂ substituted; that page reaches them by Newton’s laws on a free-body diagram rather than from the Lagrangian, and the two routes agree. Nowhere in them is there a random number: the picture is fully determined by the starting angles, and still unpredictable in practice.
Why small swings do not do this
The same page makes the other half of the point: “for large motions it is a chaotic system, but for small motions it is a simple linear system.” Near the bottom the double pendulum has two normal modes. The frequency equation for small oscillations of two point masses on light rods is equation 17.5.9 in Tatum’s Classical Mechanics on LibreTexts:
m₁ l₁ l₂ ω⁴ − (m₁ + m₂) g (l₁ + l₂) ω² + (m₁ + m₂) g² = 0
With equal masses and equal lengths it becomes l²ω⁴ − 4glω² + 2g² = 0, so ω² = (g/l)(2 ∓ √2). For the one-metre rods here the slow mode, in which the lower rod swings with the upper at √2 times its angle, has a period of 2.62 s; the fast mode, in which the two rods swing against each other, 1.09 s. In that linear regime a billionth of a radian stays a billionth of a radian, and twenty-four pendulums would stay one line forever. The chaos needs large angles, which is why every release here starts at 110° or more on the upper rod.
What this is not, and the two plates it sits nearest
This is the gallery’s first mechanical system: real masses, real gravity, and a clock in seconds. Two plates are its close relatives.
- Strange Attractor is also chaos, but it is a map, not a machine: one point jumped from place to place by the Peter de Jong formula, millions of times, until its dust shows the shape of the attractor. It draws where one orbit goes. This plate draws what happens to many copies that start almost together.
- Kuramoto Sync is also a crowd of oscillators, but it runs the other way: coupled lights with different natural rhythms pull each other into step. Here the twenty-four are not coupled at all, start in step, and fall out of it on their own.
Reading the plate: what to watch for
- The colour is the nudge. The pendulums are coloured in the order they were nudged: the first three quarters run through the cobalt inks and the last quarter warms to coral. While they are together you only see the top one, which is coral; when the fan opens it opens as a ribbon from blue to orange, because neighbours in colour are neighbours in starting angle.
- The fan before the tangle. For a second or two the pendulums spread out in colour order like the ribs of a fan. That is the difference still growing smoothly. Once it is as large as the swing itself the order is lost and the colours scatter.
How it is drawn
The equations are integrated with the classical fourth-order Runge–Kutta method at a fixed step of 1/480 of a second, and the step has nothing to do with the frame rate. Each frame hands the model the wall-clock time since the last one (capped at a tenth of a second, so a tab returning from the background does not lurch); the model counts how many whole 1/480 s steps that time is worth and takes exactly those. So a 30 fps phone and a 144 fps monitor compute the same trajectory: the test suite drives the plate’s own step function at both rates and requires the two states at 10 s to agree to within 10−9. That matters more here than anywhere else in the gallery — a step tied to the frame time would make every screen see a different piece.
Energy is the accuracy check. Released from 120° and −10°, the total energy per kilogram drifts by at most 1.4×10−6 J over 60 simulated seconds, against a potential-energy scale of gl = 9.81 J per kilogram. Each frame costs 24 pendulums × 8 steps × 4 evaluations of the equations above at 60 fps.
Rods are drawn in the page’s own text ink at low opacity, so twenty-four rods stacked on each other read as one solid line and a spread fan reads as fine hatching. Each lower bob leaves a 1.2-second tail that fades out behind it. Not one colour is named in the drawing code: the background, the text ink and the four ink slots are handed in from the shared palette, which is why the gallery tile, this page and a copy of the embed on someone else’s page follow the same light and dark settings.
Under prefers-reduced-motion no animation loop runs. Instead the first release is computed 17 seconds ahead and drawn once, fan half open, so the still shows the idea rather than one pendulum.
Honest limits
It is an ideal pendulum. Point masses, rods with no mass, a frictionless pivot and no air. A real double pendulum loses energy and slows down; this one never does. It is also a flat, two-dimensional one.
After the tangle, the paths are not predictions. The computer carries about sixteen significant digits, so every step is rounded by around 10−16. The same growth that turns 10−9 into a full rod length in under twenty seconds turns rounding error into visible error not long afterwards. Past that point each pendulum is still moving in a way the equations allow — the energy check above holds — but it is not the exact path that its starting angles would give with infinite precision.
RK4 does not conserve energy exactly. Its error is small and measured above, but it is not zero, and a method designed to conserve energy (a symplectic integrator) would behave differently over hours. Over one 45-second cycle it is far below anything you could see.
The timings are for these five releases. The 10–15 second fan comes from the size of the nudge and from how chaotic each starting angle is. A smaller nudge would hold the line longer, a larger one would open the fan sooner, and gentler starting angles would not open it at all.
Nothing here is adjustable. The angles, the nudge and the cycle length are constants in one file. This gallery ships finished compositions rather than controls.
Draw your own
The equations above are short enough to run yourself. The snippet below is this plate in 30 lines of HTML and plain JavaScript with no library, and with the plate’s own model: the same two equations of motion, the same classical fourth-order Runge–Kutta step at the same fixed 1/480 of a second, one-metre rods, g = 9.81 m/s², no friction, and the same 24 pendulums let go from 120° and −10°, each lower rod one billionth of a radian further round than its neighbour’s. The 1.2-second tails, the rods in the text ink and the cobalt-to-coral ramp in nudge order are drawn the way the plate draws them. What it leaves out is everything that exists for the gallery rather than for the physics: the 45-second cycle, its fades and the other four starting angles (this one swings until you reload), click-to-skip, the dots on the upper bobs and the pivot, resizing with the window, and sharp drawing on high-density screens (it draws in CSS pixels).
- State. Each pendulum is four numbers, [t1, t2, w1, w2]: the upper and lower rod angles from straight down, in radians, and their angular velocities in radians per second. set holds 24 of them, and bob() turns one into the position of its lower bob in rod lengths, which is all the drawing needs. The masses never appear, because with two equal masses they cancel out of the equations.
- Equations and integrator. deriv() is the pair of equations from “The equations it runs”, term for term as the plate’s own code writes them, plus a DAMP term the plate does not have, set to 0. rk4() is the classical Runge–Kutta step: four calls to deriv(), weighted 1, 2, 2, 1. Measured on this code from the 120°, −10° release, the total energy stays within 1.3×10−8 of its starting value over 10 simulated seconds, counting energy from the pendulum hanging at rest (29.6 J per kilogram at release).
- Step and speed. The step is 1 / RATE = 1/480 s whatever the frame rate: each frame turns the clock time since the last one (capped at 0.1 s) into whole steps, so a 60 fps screen takes 8 per frame, and 24 pendulums × 8 steps × 4 calls is 768 evaluations of the equations per frame. One simulated second of all 24 took about 6.5 ms in Node on the machine that built this page. Two neighbours, started 10−9 rad apart, are 1.2×10−9 rad apart at 1 s, 1.5×10−6 at 7 s, 3.3×10−4 at 12 s and 0.49 rad at 20 s; the outermost pair’s bobs are 5% of a rod apart at 15.2 s and a full rod apart at 18.0 s, the plate’s own timings for this release.
- Colour. The ground, the rod ink (the site’s text colour) and the three inks of the ramp are this site’s palette, light or dark to match your device. The first three quarters of the pendulums run from cobalt to the deeper cobalt and the last quarter from there to coral, with the plate’s opacities for tail, rod and bob (0.95, 0.34 and 1 on dark; 1, 0.42 and 1 on light). Reduced-motion visitors get one still frame, the release computed 17 s ahead and drawn once, fan half open, like the plate’s.
Three edits on line 6 show what the piece is made of. Set NUDGE to 1e-6, a thousand times the plate’s, and the fan opens at 7.3 s instead of 15.2 s; at 1e-12 it holds until 24.7 s. Each factor of a thousand in the nudge buys only eight or nine seconds, which is what sensitive dependence means in practice. Set START to [20, 20] and nothing opens at all: in 60 simulated seconds the outermost bobs never get more than 6×10−8 of a rod apart, the small-swing linear regime described above. Set DAMP to 0.1 and every swing loses energy: half of it is gone after 7.8 s and 0.3% is left after a minute. That damping is a plain drag on each angular velocity, an assumption of this snippet rather than a model of a real pivot’s friction, and with it on the energy check above no longer applies. The equations are as given on myPhysicsLab’s double pendulum page, which writes them as four first-order equations “exactly the form needed to plug in to the Runge-Kutta method”; Wikipedia’s article on the double pendulum says the motion can only be solved numerically, “using the Runge Kutta method or similar techniques”, and that it “clearly shows a sensitive dependence on initial conditions”. Save the snippet as an .html file and open it.
<canvas id="pendulum" style="position:fixed; inset:0"></canvas>
<script>
const dark = matchMedia('(prefers-color-scheme: dark)').matches, still = matchMedia('(prefers-reduced-motion: reduce)').matches;
const [ground, rodInk, cobalt, deep, coral, LOOK] = dark ? ['11,13,18', '236,231,217', '150,180,255', '120,158,255', '255,120,84', [0.95, 0.34, 1]]
: ['231,226,213', '25,27,34', '40,72,205', '52,88,214', '190,68,28', [1, 0.42, 1]]; // LOOK: tail, rod, bob opacity
const G = 9.81, L = 1, DAMP = 0, RATE = 480, N = 24, NUDGE = 1e-9, START = [120, -10], STILL = 17; // m/s², m, 1/s, steps/s, rad, deg, s
function deriv([t1, t2, w1, w2]) { // the plate's equations of motion; with equal masses the mass cancels
const d = t1 - t2, sd = Math.sin(d), cd = Math.cos(d), den = L * (3 - Math.cos(2 * d));
return [w1, w2, (-3 * G * Math.sin(t1) - G * Math.sin(t1 - 2 * t2) - 2 * sd * (w2 * w2 * L + w1 * w1 * L * cd)) / den - DAMP * w1,
(2 * sd * (2 * w1 * w1 * L + 2 * G * Math.cos(t1) + w2 * w2 * L * cd)) / den - DAMP * w2]; }
function rk4(s, h) { // one classical Runge-Kutta step of h seconds on the state [t1, t2, w1, w2]
const at = (k, f) => s.map((v, i) => v + f * h * k[i]), k1 = deriv(s), k2 = deriv(at(k1, 0.5)), k3 = deriv(at(k2, 0.5)), k4 = deriv(at(k3, 1));
return s.map((v, i) => v + h / 6 * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i])); }
let set = Array.from({ length: N }, (_, i) => [START[0] * Math.PI / 180, START[1] * Math.PI / 180 + (i - (N - 1) / 2) * NUDGE, 0, 0]), steps = 0, t = 0, last = performance.now();
const q = (i) => i / (N - 1), mix = (a, b, f) => a.split(',').map((v, j) => Math.round(+v + (b.split(',')[j] - v) * f)).join(',');
const ramp = set.map((_, i) => q(i) < 0.75 ? mix(cobalt, deep, q(i) / 0.75) : mix(deep, coral, (q(i) - 0.75) / 0.25)); // 3/4 cobalt, last 1/4 to coral
const bob = ([t1, t2]) => [Math.sin(t1) + Math.sin(t2), Math.cos(t1) + Math.cos(t2)], tails = set.map((s) => [bob(s)]); // lower bob, in rod lengths
function advance(to) { for (; steps < to; steps++) { set = set.map((s) => rk4(s, 1 / RATE)); // fixed 1/480 s steps, a tail point every 8
if ((steps + 1) % 8 === 0) set.forEach((s, i) => tails[i].push(bob(s)) > 72 && tails[i].shift()); } } // 72 points: 1.2 s of track
const cv = document.getElementById('pendulum'), ctx = cv.getContext('2d'); cv.width = innerWidth; cv.height = innerHeight;
function draw() { const S = 0.235 * Math.min(cv.width, cv.height); ctx.resetTransform(); ctx.fillStyle = 'rgb(' + ground + ')'; ctx.fillRect(0, 0, cv.width, cv.height);
ctx.setTransform(S, 0, 0, S, cv.width / 2, cv.height / 2); ctx.lineWidth = 1.6 / S; ctx.lineCap = 'round'; // pivot in the centre, 1 unit = 1 rod
tails.forEach((tl, i) => tl.forEach((p, j) => { if (!j) return; ctx.strokeStyle = 'rgba(' + ramp[i] + ',' + LOOK[0] * (j / (tl.length - 1)) ** 2 + ')';
ctx.beginPath(); ctx.moveTo(...tl[j - 1]); ctx.lineTo(...p); ctx.stroke(); })); // each tail fades out behind its bob
ctx.lineWidth = 1 / S; ctx.strokeStyle = 'rgba(' + rodInk + ',' + LOOK[1] + ')'; ctx.beginPath(); // rods in the text ink, then the bobs
set.forEach((s) => { ctx.moveTo(0, 0); ctx.lineTo(Math.sin(s[0]), Math.cos(s[0])); ctx.lineTo(...bob(s)); }); ctx.stroke();
set.forEach((s, i) => { ctx.fillStyle = 'rgba(' + ramp[i] + ',' + LOOK[2] + ')'; ctx.beginPath(); ctx.arc(...bob(s), 2.4 / S, 0, 2 * Math.PI); ctx.fill(); }); }
const frame = (now) => { t += Math.min(0.1, Math.max(0, now - last) / 1000); last = now; advance(Math.floor(t * RATE + 1e-6)); draw(); requestAnimationFrame(frame); };
if (still) { advance(STILL * RATE); draw(); } else requestAnimationFrame(frame); // reduced motion: one still, 17 s in; else steps follow the clock
</script>
Curious how the loop and canvas fit together? Read how it works →
More from the gallery
- Plate 31 Game of Life Two rules on a soup of cells: births in coral, survivors in cobalt, the dead as fading ghosts.
- Plate 32 Three-Body Problem Three suns chasing round a figure eight, then a Pythagorean dance that throws one out.
- Plate 01 Quantum Fibonacci Golden-angle phyllotaxis blooming from the seed outward.
- Plate 02 Bitcoin Matrix A quiet rain of ₿ and hex sliding down the glass.
- Plate 03 Cosmic Circles Soft orbs drifting through slow orbital rounds.
- Plate 04 Spiral Waves Archimedean arms turning, so the bands seem to stream inward.