The traced path of a double pendulum, fanning out into chaos

My First Program Was Chaos

The first program I ever wrote was a double pendulum simulation. Not hello world, not FizzBuzz — a chaotic mechanical system, integrated in real time, in a browser. That sounds far more impressive than it was. It was 2016, I had just discovered Daniel Shiffman's Coding Train, and Coding Challenge #93 was a double pendulum in p5.js. I typed along, character for character, the way you copy out a recipe in a language you don't speak.

The heart of that sketch was a pair of acceleration formulas that Shiffman — openly, cheerfully — pasted from myphysicslab. Two fractions so long they wrapped across several lines of code: sines of sums and differences of angles, squared velocities feeding back into one another, one denominator repeated under both like a chorus. I understood none of it. Not the thetas, not the omegas, not whether that denominator could ever reach zero and take the sketch down with it. The pendulum didn't care. It swung, it flipped, the second bob traced those looping, never-repeating ribbons, and I sat there completely hooked and slightly bothered.

The bothered part never went away. I have been meaning to come back to this program for ten years, and this post is me finally doing it. This time we derive everything: the equations of motion from one principle and two energies, the reason the naive simulation explodes and the two-line accident that fixes it, what chaos means once you attach an actual number to it, and — last — a map of every possible double pendulum at once, which turns out to be a fractal. Every figure below is a live simulation. Drag them. That's what they're for.

One pendulum first

Before bolting two pendulums together, it pays to be honest about one. A single pendulum: a point mass mm on a rigid, massless rod of length rr, swinging in a plane. One number pins down the entire configuration — the angle θ\theta from the vertical.

θrmg
Figure 1: The single pendulum. One coordinate, the angle θ\theta from the downward vertical, describes everything the system can do.

You could solve this the Newtonian way: draw the force diagram, decompose gravity along and across the rod, and carry the rod's tension around as a bookkeeping variable even though it does no work and exists purely to keep the mass on its circle. That works. It is also exactly the kind of accounting that turns miserable the moment a system grows joints. So instead we use the tool that was built for jointed things — the Lagrangian:

L=TVL = T - V

kinetic energy minus potential energy, written in whatever coordinates genuinely describe the configuration. For us, that's θ\theta. The equation of motion then comes from turning a single crank, the Euler–Lagrange equation:

ddt ⁣(Lθ˙)Lθ=0\frac{d}{dt}\!\left(\frac{\partial L}{\partial \dot{\theta}}\right) - \frac{\partial L}{\partial \theta} = 0

This is the condition for the trajectory to make the action — the time-integral of LL — stationary, and any mechanics text will tell that story properly. What matters for this post is the practical superpower: constraint forces never appear. The tension does no work, so it shows up in neither TT nor VV. And because we chose θ\theta as the coordinate, every motion we can even write down already keeps the mass on its circle. The constraint isn't enforced; it's built into the coordinate. Nothing to solve for, nothing to cancel.

Now the two energies. The bob moves at speed rθ˙r\dot{\theta}, and — measuring yy downward from the pivot, a convention that keeps signs tidy for the rest of this post — it hangs at depth rcosθr\cos\theta:

T=12mr2θ˙2,V=mgrcosθT = \tfrac{1}{2} m r^2 \dot{\theta}^2, \qquad V = -m g r \cos\theta

Feed these through the crank: L/θ˙=mr2θ˙\partial L/\partial \dot{\theta} = m r^2 \dot{\theta}, differentiate that in time, subtract L/θ=mgrsinθ\partial L/\partial \theta = -m g r \sin\theta:

ddt(mr2θ˙)+mgrsinθ=0\frac{d}{dt}\left(m r^2 \dot{\theta}\right) + m g r \sin\theta = 0

and divide through by mr2m r^2:

θ¨=grsinθ\ddot{\theta} = -\frac{g}{r} \sin\theta

Notice everything that didn't happen. No force diagram, no components, no tension. For one pendulum this saves five minutes. For two, it will save the whole derivation.

At this point every textbook plays the same trick: assume the swing is small, replace sinθ\sin\theta with θ\theta, and the equation collapses into a harmonic oscillator you can solve outright:

θ(t)θ0cos(ωt),ω=g/r,Tperiod=2πr/g\theta(t) \approx \theta_0 \cos(\omega t), \qquad \omega = \sqrt{g/r}, \qquad T_{\text{period}} = 2\pi\sqrt{r/g}

A beautiful, load-bearing lie. The period doesn't depend on the amplitude — approximately — which is why pendulum clocks ruled timekeeping for three centuries. But it is strictly small print. Swing higher and the true period stretches. Push past the small-angle regime and the exact solution stops being expressible in elementary functions at all: you need elliptic integrals for one rod on one pivot. Remember that — it matters in a minute. Meanwhile, here is an honest pendulum next to its small-angle ghost:

measured period: — s · 2π√(r/g) = 2.01 s
Figure 2: A real pendulum against its own small-angle solution (the ghost). Drag the bob past 90° and let go: the ghost swings with the wrong, amplitude-blind period and falls a little further out of sync on every pass. The approximation is only good near the bottom.

Now bolt a pendulum to your pendulum

Hang a second rod and mass from the first bob. That is the entire modification. The configuration now needs two angles, θ1\theta_1 and θ2\theta_2, both measured from the vertical:

θ1r1m1(x1, y1)θ2r2m2(x2, y2)g
Figure 3: The double pendulum. Two coordinates now — θ1\theta_1 and θ2\theta_2, each from its own vertical — and the second pivot rides on the first bob.

You are welcome to attempt the Newtonian route here. The tension in the second rod acts on both masses, along a direction that changes with both angles, while the first rod's tension responds to all of it at once — it's a classic exam problem precisely because the bookkeeping is punishing. The Lagrangian recipe doesn't flinch: same three steps, two coordinates. First, positions, with yy still measured downward from the pivot:

x1=r1sinθ1,y1=r1cosθ1x_1 = r_1 \sin\theta_1, \qquad y_1 = r_1 \cos\theta_1x2=x1+r2sinθ2,y2=y1+r2cosθ2x_2 = x_1 + r_2 \sin\theta_2, \qquad y_2 = y_1 + r_2 \cos\theta_2

Bob 2 rides on bob 1: its position is bob 1's plus its own offset. That innocuous little sum is the whole story of this post. Differentiate in time, square, and add components to get speeds:

v12=r12θ˙12,v22=r12θ˙12+r22θ˙22+2r1r2θ˙1θ˙2cos(θ1θ2)v_1^2 = r_1^2 \dot{\theta}_1^2, \qquad v_2^2 = r_1^2 \dot{\theta}_1^2 + r_2^2 \dot{\theta}_2^2 + 2 r_1 r_2 \dot{\theta}_1 \dot{\theta}_2 \cos(\theta_1 - \theta_2)

There it is: the cross term. How much bob 2's motion feeds off bob 1's depends on cos(θ1θ2)\cos(\theta_1 - \theta_2), the alignment of the rods. Parallel rods add their velocities outright; perpendicular rods decouple; anti-parallel rods fight. Every ounce of trouble this system is about to cause traces back to that one cosine. The energies:

T=12m1v12+12m2v22,V=(m1+m2)gr1cosθ1m2gr2cosθ2T = \tfrac{1}{2} m_1 v_1^2 + \tfrac{1}{2} m_2 v_2^2, \qquad V = -(m_1 + m_2) g r_1 \cos\theta_1 - m_2 g r_2 \cos\theta_2

(Note the m1+m2m_1 + m_2 in the first potential term: rod 1 carries both masses, so tilting it lifts both.) Now the crank, turned twice — once with θ1\theta_1 as the coordinate, once with θ2\theta_2. This is ten minutes of careful product-rule work, because the cross term depends on both angles and both angular velocities, so d/dtd/dt keeps spawning sin(θ1θ2)\sin(\theta_1 - \theta_2) terms that then partially cancel against the ones from L/θ\partial L/\partial \theta. If you want every intermediate line, Diego Assencio's derivation (linked below) writes them all out; I checked mine against his. Grouped and tidied, with the shorthand Δ=θ1θ2\Delta = \theta_1 - \theta_2, the two Euler–Lagrange equations assemble into a single matrix statement:

[(m1+m2)r12m2r1r2cosΔm2r1r2cosΔm2r22][θ¨1θ¨2]=[m2r1r2θ˙22sinΔ(m1+m2)gr1sinθ1m2r1r2θ˙12sinΔm2gr2sinθ2],Δ=θ1θ2\begin{bmatrix} (m_1+m_2) r_1^2 & m_2 r_1 r_2 \cos\Delta \\ m_2 r_1 r_2 \cos\Delta & m_2 r_2^2 \end{bmatrix} \begin{bmatrix} \ddot{\theta}_1 \\ \ddot{\theta}_2 \end{bmatrix} = \begin{bmatrix} -m_2 r_1 r_2 \dot{\theta}_2^2 \sin\Delta - (m_1+m_2) g r_1 \sin\theta_1 \\ m_2 r_1 r_2 \dot{\theta}_1^2 \sin\Delta - m_2 g r_2 \sin\theta_2 \end{bmatrix}, \qquad \Delta = \theta_1 - \theta_2

Read it left to right: a mass matrix — how hard the configuration resists angular acceleration, and note that it depends on Δ\Delta, on where the pendulum currently is — times the accelerations, equals the applied torques: one centrifugal-flavored term from each bob's swing rate, plus gravity on each rod. It's Newton's second law wearing formal dress.

To simulate, we need the accelerations by themselves, which means solving that 2×2 system. Do it symbolically — Cramer's rule, substitute, expand — and out come two towering fractions, full of sin(θ12θ2)\sin(\theta_1 - 2\theta_2) and cos(2θ12θ2)\cos(2\theta_1 - 2\theta_2), with one denominator repeated under both. Those fractions are, character for character, the myphysicslab formulas I pasted into p5.js in 2016. The monsters were never monsters. They are this one tidy matrix equation, pre-solved by hand and flattened into ASCII. It took me ten years to find that out.

Code doesn't need the flattened form — it just solves the little linear system numerically at every step. The only conceivable danger is that division: what if the determinant hits zero? It can't:

detM=m2r12r22(m1+m2sin2Δ)>0\det M = m_2 r_1^2 r_2^2 \left(m_1 + m_2 \sin^2\Delta\right) > 0

Since sin2Δ0\sin^2\Delta \ge 0, the parenthesis is never smaller than m1m_1, which is positive because rod 1 carries an actual mass. (This is no accident — MM is the kinetic-energy metric, and kinetic energy is positive whenever anything moves.) The solve cannot blow up, for any angles, masses, or lengths. Which means we can safely hand it sliders:

Figure 4: The whole machine: the matrix equation above, solved and integrated live. Drag either bob to re-aim it, tune the masses, lengths and gravity, and watch the second bob draw. This is the sketch I copied in 2016, minus the mystery.

Making it not explode

The matrix equation tells us the accelerations right now. A simulation needs the state later. Bridging that gap is numerical integration — and remember, even the single pendulum had no elementary closed-form solution, so numbers were always our destiny. The only question is how to step forward in time without lying too much. Pack the state into one vector:

y=(θ1,θ2,ω1,ω2),y˙=f(y)y = (\theta_1, \theta_2, \omega_1, \omega_2), \qquad \dot{y} = f(y)

where the ω\omegas are angular velocities and ff is everything we just derived: the rates of the angles are the ω\omegas, and the rates of the ω\omegas come from the matrix solve. Geometrically, ff is a velocity field on the four-dimensional state space, and our pendulum's entire life is one streamline of it. An integrator hops along that streamline in steps of size hh. The obvious hop is explicit Euler — follow the tangent:

yn+1=yn+hf(yn)y_{n+1} = y_n + h\, f(y_n)

For a pendulum this is slow-acting poison, and the reason is geometric. Swinging motion loops around a closed orbit in state space — a level curve of the energy. The tangent to a loop points to the outside of the loop, so every Euler step lands slightly outside the orbit it left. Always outside, never inside: the errors don't cancel, they compound, and the trajectory spirals outward. For the small-angle oscillator you can prove each step multiplies the energy by 1+h2ω21 + h^2\omega^2 — geometric growth. Your pendulum gains energy from nothing, every swing, until it's whipping around its pivot like a propeller.

You can watch this exact failure in the original Coding Train video: left alone, the sketch's pendulum slowly swings itself higher than it was dropped from. And the fix that ended up in the code — the fix I faithfully copied without a second thought — was simply to update the velocity first and then move the position using the new velocity:

ωn+1=ωn+ha(θn,ωn),θn+1=θn+hωn+1\omega_{n+1} = \omega_n + h\, a(\theta_n, \omega_n), \qquad \theta_{n+1} = \theta_n + h\, \omega_{n+1}

Same arithmetic, same cost, two lines reordered. It looks like an off-by-one bug you decided to keep. It is actually a different algorithm with its own name — semi-implicit Euler, also sold as symplectic Euler — and a shockingly deep property: for systems whose energy separates cleanly into kinetic-of-velocity plus potential-of-position, like our single pendulum, it exactly conserves a slightly-perturbed "shadow" energy that sits within O(h)O(h) of the true one. It cannot drift. Its energy error just oscillates in a bounded band, forever. By swapping two lines of JavaScript, Shiffman reached — presumably by accident — for one of the deepest results in numerical analysis.

Now the honesty footnote, because this is the detail everyone flattens: that ironclad guarantee is for separable systems, and the double pendulum is not one. The cross term ties kinetic energy to the angles — the mass matrix depends on θ\theta — and with that, the strict symplectic warranty is void. What survives in practice is still remarkable: semi-implicit Euler's energy error creeps — slowly, first-order slowly — instead of staying in a fixed band, but it never runs away exponentially the way explicit Euler's does. Downgraded from "bounded forever" to "drifts politely." You can see exactly that downgrade in the demo below.

The third contender is the workhorse: classical fourth-order Runge–Kutta. Instead of committing to the slope at the start of the step, sample it four times — once at the start, twice at trial midpoints, once at a trial endpoint — and take a weighted blend:

yn+1=yn+h6(k1+2k2+2k3+k4)y_{n+1} = y_n + \tfrac{h}{6}\left(k_1 + 2k_2 + 2k_3 + k_4\right)

where k1,,k4k_1, \dots, k_4 are those four slope samples, each evaluated where the previous one pointed. Fourth-order accuracy means halving hh cuts the error sixteen-fold. RK4 knows nothing about energy — run it for geological time and it too will drift — but at the step sizes used here its drift is negligible, which is why every other simulation on this page runs on it. Here are all three on the same pendulum, at the same time step:

explicit Eulersymplectic EulerRK4
Figure 5: One initial condition, three integrators, one shared Δt\Delta t. The chart tracks energy error: explicit Euler (red) leaves almost immediately, semi-implicit Euler (teal) creeps, RK4 (dark blue) hugs zero. Raise Δt\Delta t to make everyone worse.

The part where it becomes chaos

Everything so far — the derivation, the integrators, the energy audit — is the orderly half of the story. Here is the disorderly half, the half that kept a copied p5.js sketch lodged in my head for a decade. The equations above are perfectly deterministic: no randomness, no hidden inputs. Given the state exactly, the entire future is fixed. And the system is still unpredictable — in a precise, quantifiable, non-hand-wavy sense.

Run two copies of the pendulum, identical except for a nudge δ(0)\delta(0) in one angle — the kind of nudge a rounding error in the seventh decimal place would give you. Track the distance between them in state space. In the systems physics students are raised on, that distance grows politely: linearly, maybe quadratically. Here, it grows like this:

δ(t)δ(0)eλt\lVert \delta(t) \rVert \approx \lVert \delta(0) \rVert\, e^{\lambda t}

Exponentially — with λ\lambda, the Lyapunov exponent, as the system's built-in error-amplification rate: 1/λ1/\lambda is the time it takes an uncertainty to grow by a factor of ee. On a log scale, exponential growth is a straight line, and you can watch that line draw itself in the demo below — a steady climb at slope λ\lambda, until the separation saturates because two pendulums can only get so far apart.

The consequence for prediction is brutal, and worth spelling out. Say you measure the initial state to some precision, and your forecast counts as good while the error stays under some tolerance. Then your horizon is

thorizon1λln ⁣toleranceprecisiont_{\text{horizon}} \sim \frac{1}{\lambda} \ln\!\frac{\text{tolerance}}{\text{precision}}

The logarithm is the villain. Measure a thousand times more precisely and the horizon grows by ln10007\ln 1000 \approx 7 units of 1/λ1/\lambda — a fixed few seconds for this pendulum, not a thousand times longer. Every additional digit of precision buys the same flat, modest increment of future. Determinism survives; prediction doesn't.

None of this is a pendulum quirk. Edward Lorenz met the same mathematics in the early 1960s in a stripped-down weather model, when a run restarted from printout values — rounded to three decimal places — invented an entirely different month of weather. That same logarithm is why forecasts stall out around two weeks no matter how good the satellites get: the atmosphere's 1/λ1/\lambda is a few days, and better data only ever buys logarithmically more horizon.

Figure 6: Twenty pendulums released together, adjacent starts 10710^{-7} radians apart — far below anything a pixel could show. The chart tracks the separation between the first and last on a log scale: the straight-line climb is eλte^{\lambda t} in person.

Every possible pendulum at once

Figure 6 asks about one pendulum and its nearest neighbors. The natural escalation is to ask about all of them. Fix the setup — equal masses and equal lengths, m1=m2=m,  r1=r2=lm_1 = m_2 = m,\; r_1 = r_2 = l, released from rest — so that a starting condition is nothing but a pair of angles (θ1,θ2)(\theta_1, \theta_2). Then ask every start the same question, Jeremy Heyl's question: how long until your second bob flips over the top? Color each pixel of the angle–angle plane by its answer, and you get the map in Figure 7 — my favorite picture in this entire subject.

Before simulating tens of thousands of pendulums, though, theory gets one more word, and it's a good one. Released from rest, the kinetic energy is zero, so the total energy is the starting potential:

E=mgl(2cosθ1+cosθ2)E = -mgl\,(2\cos\theta_1 + \cos\theta_2)

(the 2 because rod 1 carries both masses). To flip, the pendulum must at some instant pass through a configuration with the second bob over the top, θ2=π\theta_2 = \pi. Among all such configurations, the cheapest has bob 1 hanging straight down — θ1=0\theta_1 = 0 — with potential mgl(21)=mgl-mgl\,(2 - 1) = -mgl. Energy is conserved and kinetic energy can't go negative, so if E<mglE < -mgl, every flipped configuration is out of reach. Forever. Unpacking the inequality:

Vflipmin=mglno flip possible while 2cosθ1+cosθ2>1V_{\text{flip}}^{\min} = -mgl \quad\Rightarrow\quad \text{no flip possible while } 2\cos\theta_1 + \cos\theta_2 > 1

Four lines of energy bookkeeping just proved a theorem about eternity: a pendulum released with 2cosθ1+cosθ2>12\cos\theta_1 + \cos\theta_2 > 1 — both rods hanging low — will never flip, no matter how long you wait. (You'll often see this quoted as 3cosθ1+cosθ2>23\cos\theta_1 + \cos\theta_2 > 2; that is the identical argument run for a compound pendulum built from uniform bars — Heyl's version — where the distributed mass shifts the coefficients.)

On the map below, that inequality is the black curve, and the pale region inside it is painted without simulating a single step. Outside it, chaos speaks: solid basins where the pendulum flips almost immediately, and — hugging the boundary — filigree where neighboring pixels flip after wildly different times, structure inside structure as far down as you care to zoom. That's Figure 6 restated as geography: near the boundary, "arbitrarily close starts, arbitrarily different fates" stops being a warning label and becomes a texture.

The theorem is also a computational free lunch. A pixel that never flips is the most expensive pixel there is — it burns the entire budget, thirty simulated seconds at 120 RK4 steps per second, just to answer "never" — and the inequality hands us about thirty percent of the map, precisely the pixels that would have cost the most, for free. Theory does the heavy lifting; the worker threads mop up.

θ2\theta_2 \uparrow
θ1\theta_1 \rightarrow
Figure 7: The flip-time map, computed live in your browser. Each pixel is a pendulum released from rest at (θ1,θ2)(\theta_1, \theta_2), colored by how long its second bob takes to flip; light gray pixels never flipped within the budget, and the near-white region inside the black curve provably never will. Click anywhere on the map to fly that pixel's pendulum.

Here's the part that gets me, ten years later. The 2016 sketch and this entire page run on the same hundred-ish lines of physics. The matrix solve driving every figure above is the myphysicslab formulas, un-flattened; the integrators are the same handful of additions and multiplications, ordered with intent. Nothing became more powerful. What changed is that every line is now load-bearing: I know why there's a cos(θ1θ2)\cos(\theta_1 - \theta_2) in the kinetic energy, why the denominator that worried me can never reach zero, why swapping two updates tamed the energy drift, and why that trick's warranty quietly expired the moment we attached the second rod. The incantation became an argument.

If you have a formula like that — one you've been copying since before you could read it, someone else's variable names fossilized inside it — I can report that going back for it is worth the trip. Mine took ten years and turned into the five toys on this page. Scroll back up and drag the pendulums around one more time. That's still the whole point.

References