← Back to Write-ups

Double Pendulum • Part I

Teaching a Double Pendulum to Stand Up

By David Nash · · 17 min read (3,657 words)
Total Sparks: 0
Enjoyed this write-up? Charge it with sparks

The premise for this write up is pretty straightforward and well known. I'm going to be talking about an inverted double pendulum. It's simply a cart on a rail with two free-swinging links stacked on top. It only has one motor. The end goal for it is to have it throw itself upright and then stay there balancing. It sounds like a silly toy example problem, but it's actually the standard demo case for the common robotics problem of controlling an system.

This write up will cover a methodology for making these types of systems behave. While nothing I am talking about here will be novel or the result of any of my pure genius, I do think it's a nice introduction for those who are new to this topic as well as a nice refresher for those already familiar with it. I'm also pretty happy with the interactive experiments I've added. They give a much better way to feel how this stuff works rather than just reading about it.

Speaking of interactive examples, let's start with the first one. This one uses all the theory covered in this write up.

Fig. 1 Interactive simulator
Drag the cart, or swipe the cursor through a link, to interfere with the pendulum and make it lose balance. Watch it right itself after falling over.

The Deceptively Complicated Machine

Balancing a broom on your palm is a task most people are capable of doing. You watch the top, and when it tips one way you instinctively move your hand that way to get back under it.

If you then cut that broom in half and join the two pieces with a spinning joint the task undoubtedly becomes much harder. The top piece does whatever it likes, and the only way you can influence it is by maneuvering the bottom piece around underneath it. That is the premise of this machine (or ).

Fig. 2 The plant
x = 0θ₁θ₂xuthe only inputlink 11 m, 1 kglink 21 m, 1 kgfree hinge —no motorthings that can move3things you can push1underactuated by 2
Three things move independently — the cart along its rail, and each link about its hinge. Only the cart can be directly controlled with its motor.

The of the machine is captured by six numbers, the cart's position and velocity, and the angle and spin rate of each link. Angles are measured from straight up, so θ = 0 is balanced and θ = π is hanging straight down. The single input is a horizontal force on the cart.

To keep track of the system we can think back to the absolute basics of physics, F = ma. That simple equation works for a simple single body system and if we slightly adapt it, it will work for the double pendulum too. The main difference for the pendulum is that each symbol in the equation gets promoted from a single number to a small table, because now three things accelerate at once and push on each other. We also have to introduce new force terms to account for the fact that the links are swinging and gravity is pulling on them. The resulting equation looks like this:

q is the three positions (cart, link 1, link 2); q̈ is their accelerations.

We'll need to expand these terms for the calculations that the computer will do. That can be seen below.

Expanded into full vectors and 3×3 mass matrix for the cart (row 1) and two links (rows 2 and 3). Constants d₁…d₆ group link lengths and masses; τ_g are the gravity torques.

That became an alphabet soup real fast but don't let that discourage you! The underlying concepts are still quite simple here. is the one lonely motor, which is only in the first row because it can only push the cart along one dimension. is the whipping, the centrifugal and forces. is gravity, pulling the links downwards. And finally, is just the inertia represented as a table rather than a single mass, because shoving the cart also swings the links.

This equation will be the basis for the simulation, it's what is used to calculate how the system is working on every tick. The engineering task then becomes conceptually dead simple, figure out what to make so that the pendulum does what we want it to do.

The Balancing Act

Let's start with the good news: when the pendulum is nearly upright we can simplify it to a linear control problem. When the tilt angles stay small near the , the math simplifies and the curves behave almost like straight lines, since sin θ ≈ θ. Basically, when the pendulum is standing up, it acts more like a rigid stick. like this leaves a plain linear system, which lets us solve for the best possible motor force to apply using the Linear Quadratic Regulation (LQR) control algorithm.

LQR works with the concepts of penalties for things you don't want to happen. Being off-center is a penalty. Being tilted is a bigger penalty. Using force can be a penalty too, because a real motor has limits and you don't want your controller to be super violent. If we integrate (accumulate) those penalties continuously over all future time, we get a single total cost score, J, to minimize:

J is the total cost over all future time. Q sets how much you mind each kind of error; R sets how much you mind effort.

and are what we get to play with here to control the system. If we raise the controller becomes lazy and gentle, raise and it gets twitchy and aggressive. I bring even more good news now: because this is a linear system this problem has an exact solution, and we can find it by solving the . At first, the Riccati equation will feel like a blender of five different matrices (A, B, P, Q, R) with transposes and inverses thrown in but I will explain it so it makes more sense.

The algebraic Riccati equation. Known inputs: physics (A, B) and penalties (Q, R). We just need to solve for the future-cost matrix P.

This is where the sausage gets made. We feed in the machine's linearized physics with (how the plant drifts on its own) and (how the motor pushes the plant) along with the chosen penalties ( and ). Plugging in the knowns, we solve for matrix which captures the total future cost of being in any given state.

One way to read the on both sides of that middle term is as cause and effect. is the bill for future cost. The first sets how hard the motor should push right now. That push moves the machine and changes how much future penalty we'll have, and the second prices that change. Because how hard we push depends on , and the consequence of that push also scales with , the two multiply together into .

It isn't a strict derivation, but it's the intuition for if it spends a little motor effort now it won't have to pay for a big correction later. After we solve for , the gains needed for the controller are computed with a simple matrix multiplication:

K is calculated once offline from P. In the live controller, motor force u is simply -K times state x.

This little equation is all we need for balancing the pendulum near the top equilibrium. is six numbers, one per state variable, computed once from by plain matrix multiplication. From then on the controller just measures the six state values, multiplies each by its matching number in , adds them up, negates the total, and that's how hard it pushes. Conceptually, the entire balance controller is just a running at 600Hz (10 substeps per frame):

The balance controller equation. Six sensor measurements multiplied by six gain numbers and summed into one motor command.

How Far Can It Lean and Still Come Back?

LQR is optimal for linearized systems, but the machine is only roughly linear near the top equilibrium. The further it gets from upright, the worse the approximation becomes, until the controller is solving a problem that no longer describes reality. The set of states from which it can actually recover is its .

If the pendulum links lean in the same direction (like a single rigid stick) that's a standard tilt and catching it is like the simple broom example from earlier. If the two links go in opposite directions (buckling into a zig-zag) it becomes a lot more complicated to correct.

Fig. 3 Find the edge yourself
tilt type:
t = 0.0s
recoverable starting tilt sweeping…
34°
recovers falls
Drag the slider to test the starting lean, or toggle between tilting both links in the same direction versus opposite directions (folded). The strip below is swept live in your browser: green recovers, red falls.

Leaning both links in the same direction, it survives out to about 20°. But fold them in opposite directions and the drops to roughly 12°.

Planning the Swing

The linear balance controller only works when the links are within about 20° of upright. If the pendulum starts out hanging straight down at 180°, LQR can't be used to make it reach the balanced state at the top. That is because getting upright requires swinging backward first to build momentum, deliberately making your error bigger before making it smaller.

So now the objective has to be reframed. Instead of "what force should I apply right now?" it becomes "what is the whole sequence of forces, over the next X seconds, that I need to apply so that the pendulum ends up at upright equilibrium?" Generally, this is .

iLQR for Trajectory Optimization

Control engineers will learn through their schooling/textbooks a sort of flowchart that they can use to help them decide what controller they should use to solve a given control problem. I built an interactive controller picker if you want to see an actual flowchart for this purpose.

The double pendulum is a classic problem, so the standard choices for how to control it are well known. To save you the hassle let me just tell you, near the top we use LQR (as we've already discussed), and when we get out of that 20° range we use a trajectory optimization method called . The idea behind iLQR is simple. Take some guess at a trajectory, however bad, linearize the physics along that guess, solve that easy version, take the new, better guess, then linearize along it and do it again.

So now that we know how we will plan the trajectory, it's time for another LQR variant to be introduced: . A trajectory on its own is just a plan. Without feedback, the smallest nudge will ruin that plan. To actually follow the path, you need gains that adjust at every point in time along the swing. This is what TVLQR is used for here.

You can compute TVLQR along any trajectory but pairing TVLQR with iLQR has a big benefit: we get the TVLQR gain schedule basically for free. Let me explain. Every time iLQR takes an iteration, it runs a from the end of the trajectory back to the start. At each step along the trajectory (one every 20 ms), that backward pass solves for the optimal control adjustment:

One backward sweep, two outputs. k is an open-loop nudge that makes the trajectory better; K is a feedback gain that says what to do when you are not where you expected to be.

It computes two things at every step. First is , which is the change in planned motor force at time t to move the swing closer to the optimal path. Second is , which is the feedback gain that says how to correct if we are not where we expected to be.

Once iLQR converges, the adjustments shrink to zero since the converged plan can't be improved any further. However, the feedback matrices stick around. The backward pass found them by linearizing the physics and solving a Riccati recursion along the trajectory, which is exactly what TVLQR does, so the matrices from the final iteration are the TVLQR gains. That's how it's "free", we get the path to follow as well as the gains to correct against deviations from that path all in one go. Pretty neat, huh?

Fig. 4 One Iteration of iLQR
1 · ROLL OUTrun the current forcesthrough the real physicsgives the trajectory x(t)2 · BACKWARD PASSlinearise about x(t),solve that exactly,end to startgives a correction k(t)and the gains K(t)3 · LINE SEARCHtry the full step, thensmaller ones, until cost fallsgives better forces u(t)repeat until the cost stops falling — typically a few hundred timesK(t) is a by-product here —it becomes the tracker.Each pass is cheap: cost grows linearly with the number of time steps,not as the cube of them.
An iLQR iteration loop demonstrating the forward and backward passes.

In an ideal world, we’d simply take those nudges, apply them to the motor inputs, and roll out a new, improved trajectory. But there’s a catch (why is there always a catch 😔). Just like the 20° balancing window earlier, this linearized model is only a local approximation. If the solver takes too big of a step it might escape the region where the linearization is valid and result in an overall worse action for achieving the balancing goal.

To keep the loop in Fig. 4 from flying off the rails, there are two safeguards. First, during the backward pass, the step is . This adds a damping term, just like in , so when the simplified physics causes the solver to take an optimization step that looks untrustworthy, the solver takes a baby step downhill instead of a giant leap down the hill.

Second, the solver runs a backtracking line search (Step 3 in the loop). It tests the full 100% correction first. If this actually reduces the total cost in the nonlinear simulation, great! If not, it tries 80%, then 60%, and so on until it finds a step that improves things. With those two guardrails in place, the solver guarantees non-increasing cost on each iteration without blowing up.

To actually put this into practice for the double pendulum we need to solve the balancing problem in four stages. In stage one, we set a very loose penalty on missing the upright target, just looking for any rough motion that gets the linkage swinging high. Once it discovers the gross swing, we crank up the target penalty in stages, forcing the solver to clean up the trajectory and stick the landing. If we demand perfection too early, the solver doesn't like the penalties and it won't be brave enough to commit to a big swing that it'll need to get the links upright.

Fig. 5 Cost falling over 566 iterations
1.93254792931578580113226340453566cost (log scale)accepted iteration
Hover for values
stage 1 starts at iter 2stage 2 starts at iter 386stage 3 starts at iter 468stage 4 starts at iter 514

Shaded and unshaded regions are the four continuation stages. Every point is scored against the same final cost function, so the curve measures progress rather than the jump that happens whenever the terminal weight changes.

Starting from random noise. The four regions are a continuation schedule: we raise the penalty on missing the target in stages, because demanding perfection immediately tends to stall in a local minimum where the linkage flails without ever committing to a swing. Loosen the requirement, find the gross motion, then tighten.

What It Found

Once iteration 566 wraps up, the resulting plan is tidy. I want to remind you here that the optimizer was not specifically told to pump. It was just given a model of the physics, a starting pose, a target pose, and a mild penalty for using excessive force, and it worked out the wind-up on its own because that's the cheapest way to satisfy the constraints.

Fig. 6 The solved plan
0.00s
θ₁
180°
θ₂
180°
cart
0.00 m
force
-9.6 N
Energy climbs from hanging to upright
-20-10010200.00.81.62.43.24.0hanginguprightjoulestime (s)
Hover for values
…on a peak of about 30 N
-30-18-75170.00.81.62.43.24.0newtonstime (s)
Hover for values
Scrub through it. The cart backs away, swings the linkage out, reverses hard through the middle, and arrives with both links vertical and almost no residual spin. Peak force is about 30 N, spread over four seconds, close enough to the weight of the entire machine (cart plus both 1 m, 1 kg links, 3 kg total) that one hand could just about tip the whole thing over sideways.

The energy numbers in the graph make it clear what the swing-up is actually for. Hanging at rest, the machine sits at . Balanced at rest, it needs , same magnitude, opposite sign, which I hope is obvious. The swing-up is moving 39.24 joules into the system through its motor force.

Putting the Plan into Action

Now that we have the planned trajectory from Fig. 6, it’s tempting to think the hard work is done. But we can't just press play and replay that sequence of forces.

Because the double pendulum is exponentially unstable, open-loop replay just won't work. Any little discrepancy doubles and doubles again very quickly. In almost every conceivable scenario this makes the cart add chaos instead of achieving equilibrium.

Fig. 7 The same plan, with and without eyes
t = 0.00s · swing-up
plan replayed blind off by 0°
plan + tracking gains off by 0°

At 0° both succeed — the plan is exact. Nudge the start even a couple of degrees and the blind replay drifts off, because nothing is watching. Same plan on the right, plus K(t).

Both panels run the identical planned force sequence. The right panel adds the TVLQR feedback corrections from the backward pass. At 0° disturbance, both reach the top; bump the start by just a couple of degrees and blind replay immediately fails.

This is where those "free" TVLQR feedback gains I discussed earlier add value. Instead of treating the trajectory as a rigid script, the controller uses it as a moving reference and nudges the motor with those feedback gains whenever the machine drifts off course.

One Plan, Many Starting Poses

This trajectory following approach might bring about a question for you (it did for me). If there's only one stored trajectory, solved from one specific starting pose, how does that single plan cover all of the possible poses the machine might be in when you start it?

The controller doesn't solve this problem by having more plans. It works as-is because the correction term actively drags nearby states onto the plan. The nominal trajectory acts like a magnetic line, and the gains are what pull nearby states toward it. You might also hear this described as a "funnel" in control theory textbooks but for me the magnet analogy is more intuitive.

Fig. 8 Two dozen different starts, one plan
24 randomised starts · worst final error 0.00°
0.02.34.56.89.00.00.20.40.60.81.0degrees off the plantime along the swing-up (s)
Hover for values
Each line is a separate run from a randomly perturbed hanging pose, plotted as its angular distance from the single stored plan. With the gains on, the family is pulled toward zero and pinned there; switch them off and the same starts fan out and leave the top of the chart.

This "magnetism" has a limit, just like an actual magnet. The runs above are perturbed by up to about 7° in each link and a quarter of a radian per second in each spin rate. Push much beyond that and the correction the gains require exceeds what the motor can deliver and the swing-up fails.

The Hand-Over

Now I've told you about the two controllers needed to solve this balancing problem. The TVLQR controller that tracks the swing-up trajectory that the iLQR optimization found, and the LQR controller that balances the pendulum near upright. I'll now explain how to switch between them smoothly.

If you simply hard switch from one to the other somewhere near the top, their control commands will almost certainly disagree. That then causes a sudden jolt and can be enough to knock it straight back down. In control theory, switching modes without causing sudden spikes in actuator effort is known as a . Often this requires delicate blending filters or threshold tuning, but for this problem, the textbook solution simply uses boundary conditions.

To compute the swing-up gains, the backward pass starts at the finish line (t = 4s) and solves backwards to the start. But before it can take its first step, it needs an answer to a simple question: if the pendulum arrives at the top tilted by even a fraction of a degree, how much penalty should that carry?

After the swing-up is completed, the linear balance controller takes over. Back in The Balancing Act section, the Riccati equation gave us a matrix, , which is the price sheet for how much future effort it costs to balance out any tilt with that controller. This is the lazy (or brilliant?) solution. We simply re-use that exact matrix as the starting penalty at t = 4s for the backpass math. Because both calculations use the same cost and physics at the finish line, once the recursion is substepped, the swing-up’s final gain numbers match the balance controller's numbers.

There’s no switching logic or mode-blending needed. At four seconds, the swing-up controller naturally morphs into the balance controller. You can see this in Fig. 9: while the solid lines (the TVLQR swing-up gains) swing wildly early on, as the clock approaches four seconds they settle directly onto the dashed lines (the steady-state LQR balance controller).

Fig. 9 The schedule lands on the balance gains
TVLQR θ₁ TVLQR θ₂ LQR balance θ₁ LQR balance θ₂
-450-1431644717780.00.81.62.43.24.0LQR balance θ₁LQR balance θ₂feedback gaintime along the swing-up (s)
Hover for values
Feedback gains on each link's angle throughout the 4-second swing-up. The TVLQR gains swing dramatically, even reversing sign to let the links tip and build momentum. As the trajectory approaches upright the gains converge.

Coming Back Down

If you knock the pendulum over, it's got to have a way to get back up. But its reset has to be actively controlled, because a pendulum that's just lost its balance can swing around pretty chaotically. Energy enters and leaves the machine at a rate equal to force times cart velocity, u·v. To remove energy, use a force that opposes the cart's motion, u = −k·v, which drains energy at k·v².

Crank k up too high and the cart locks in place: v drops to zero, and energy stops leaving the links. The cart needs to stay movable to act as a damper so the pendulum can actually dump its energy into it.

Teaching an Old Dog The Same Trick

So how do the experts solve this? If we use Russ Tedrake's Underactuated Robotics notes, you've already learned the answer. We just use LQR again.

LQR is not only for precarious balancing acts at the top. LQR works around any equilibrium fixed point where the forces cancel. Hanging straight down (θ = ±π) is an equilibrium just like balancing upright (θ = 0). All the math from before is the same, we just solve the Riccati equation linearized about the hanging pose.

The exact same Riccati equation from The Balancing Act section, solved with hanging physics and a flipped penalty matrix.

The equation and solver are identical to the balancing case, but the tuning is flipped. For balancing upright, we penalized link tilt aggressively (Q_θ = 150) and tolerated cart drift. For braking, gravity stabilizes the angles for free. We dial the angle penalties down to 0.5 and crank the cart position penalty up to 50. That keeps the cart centered on the rail while letting it "dance" with the swing, timing horizontal moves to dump kinetic energy out of the links as fast as possible.

The Numbers Breakdown

Because nerds like me love numbers, let's go through the numbers of this writeup. I ran 50 iLQR solves (five horizons, five random starting guesses, two noise levels) and kept the best one which took 70 seconds in total. The winning iLQR solve took 566 iterations, each one linearizing the physics along 200 knots, taking 1.8 seconds on my Macbook M3 Pro. It produced 2,606 numbers and occupies 21 KB as a static table. Everything you've been seeing run live in the figures above is just evaluating , which is 6 multiplications and an interpolation on every physics step. Because the heavy part was done offline, the live controller can run at 600Hz (10 substeps per frame). I can't render the animations at 600Hz but I can hit a smooth 60fps with ease.

A lot of real robots split the work the same way, heavy planning offline or at a few hertz, light feedback in a tight loop. The browser is running the part a real machine's control loop would actually run.

Appendix: Gotchas That Got Me

I wanted to just add a few notes here about the things I got wrong when I was implementing this code and learning the theory. These are some of the practical things that you learn by doing, not just reading about "how to solve it". Maybe my mistakes will save you some time if you try to implement this, or something similar, yourself.

  • TVLQR tracking gains along the planned iLQR path need the timestep folded in. When computing the TVLQR feedback gains along the nominal iLQR trajectory, continuous-time cost weights need the timestep multiplied in (Q·Δt). I forgot this the first time at 50 Hz (Δt = 0.02s), which left the gains 50× too small and made the controller look completely dead. On top of that, a 20 ms knot interval was too coarse for how fast the double pendulum falls, distorting the gains by ~20%. the backward Riccati pass inside each knot fixed it.
  • Anything periodic needs its errors . and −π are the exact same physical pose, but 6.28 apart numerically. I lost an embarrassing amount of time to one unwrapped comparison buried three functions deep, which caused the controller to spin a link 360° to fix an error that didn't exist. The fix was a simple wrapping helper, wrapPi(θ), that maps all angular errors back into (−π, π] before they ever hit a gain multiplication.
  • Validate twice: once in the offline solver, once in the live sim. The offline optimizer runs in double precision with a fine RK4 integrator. The browser runs in a 60 fps requestAnimationFrame loop against gains rounded to three decimals. At first, taking a single Euler step per animation frame accumulated enough numerical drift that the live sim missed the balance catch entirely. I fixed this by substepping the browser physics (taking 10 small integration steps per frame, running physics at 600 Hz) and adding an automated validation script that verifies the exact rounded numbers under browser dynamics before shipping.

Have feedback or thoughts?

Found a typo, have a question on the derivations, or want to discuss this topic?

Send Feedback →
Control TheoryTrajectory OptimizationiLQRLQR