← Back to Write-ups

Double Pendulum • Part II

Bringing Reality into the Simulation

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

In Part I, I implemented a double pendulum balance controller using some nifty textbook solutions. One thing about textbooks though, they usually assume the ideal scenarios for everything. In this Part II, I explore adding a bit of reality into the problem and seeing how the system holds up to it.

For this, I will take that same optimal path that iLQR found with the same gains and run them against a cart with a slightly different mass, a rail with dry friction, and a control loop with delay and see what breaks.

The Simulation Simplification Problem

A simulator is able to provide the controller the exact , the instant it happens, and applies exactly the force asked for, to a whose numbers are precisely the ones the gains were solved against. A real double pendulum is nowhere near this generous. It might tell you where it was a few milliseconds ago, in encoder ticks, and to get the velocity you have to calculate it in a way which can amplify the noise you've already got.

If I were to actually build this double pendulum, these are problems I'd need to start caring about. But right now I am working while traveling, so I can't build a real test bench. However, I can modify my simulation to try to mimic the real world behavior a bit better. The modifications include friction which the model currently doesn't account for, a control loop with delay and a finite rate, imperfect joint readings with quantized angles down to encoder counts, and velocity derived from those encoders noisily.

To see how the system reacts to these new imperfections I programmatically tweaked all the params until the success rates dropped. Everything below is measured, over 16 randomized starting poses per point.

Break It Yourself

Below is the bench itself, live. You can use the sliders to tweak the settings. The controller and the stored plan are exactly the idealized ones from Part I. If you want to inject the most chaos, tweak the delay and watch the controller struggle.

Fig. 1 Add reality until it breaks
peak force 0 N
swinging up · t = 0.0s
the fixes
'A plausible bench' sets every dial to something reasonable. With those settings the plant fails. There are two options at the bottom to fix the failure, neither rescues it alone, but together they do.

What Has The Most Impact

If you played with the sliders in Fig. 1, you may have noticed that some knobs are far more unforgiving than others. A little bit of latency or rail friction is hard for the controller to manage. On the other hand, you can adjust the mass of link 2 quite a bit and the machine barely seems to care.

To compare these effects fairly I need a common scale since they have different units. For this, in the chart below I've normalized the reality effects into a safety factor. This is simply how much of that effect the baseline controller survives divided by what the natural values are. Let's assume my natural values here are perfectly representative of a real system. A value of 1.0x would mean it works in reality. Below 1.0x fails on real hardware. Anything above 1.0x is our working safety buffer.

Fig. 2 Baseline safety margin against each reality variable
1.0×1.3×dry frictionon the railnominal: 3 Nmax: 4 N2.7×loop delaysensing to drivenominal: 10 msmax: 27 ms3.1×encoder resolutionquantized countsnominal: 2048 counts/revmin: 663 counts/rev3.4×link-2 masserrornominal: 20%max: 67%
baseline (as designed) 1.0× hardware bench (failure threshold)
Values above 1.0× represent safety headroom over reality
How much headroom the unassisted controller has before failing. The 1.0x line is the failure threshold on a typical hardware bench; anything below it drops the pendulum.

Looking at the baseline numbers in Fig. 2, the vulnerabilities can be split into two types: wrong numbers and wrong assumptions.

One good thing here is that the system is quite tolerant to wrong numbers. Link-2 mass error sits comfortably at 3.4x headroom. Link 2 can be up to 67% heavier than whatever I used when solving the gains and the machine still stands up. Encoder resolution is similarly forgiving with a 3.1x safety margin. A standard 2048 counts/rev optical encoder gives us more than triple the resolution the controller needs before quantization noise starts to cause unrecoverable damage.

This seemed a bit too lenient to me at first, but I suppose it makes sense when I consider how the tracking gains work from Part I. The doesn't care why the machine is off course. A wrong mass or slightly quantized angle reading just shows up as a state error, and pulling the machine back toward the planned path is what the were designed to do. I didn't know I had built something with 3x the headroom it needed but that was nice to find out.

The more serious issue is wrong assumptions since those are missed by the model entirely. The textbook controller has two assumptions that aren't true with physical hardware.

The first assumption is that sensor data describes the current instant. In reality, collecting sensor data takes time, so there is a delay. At just 27 ms of round-trip latency (a 2.7x margin), the controller completely loses balance. That latency budget is super easy to blow past, especially with hobby grade components. Feedback-based control only works when you react to where the machine is now. If it is reacting to where the machine was 30 ms ago the correction can show up in the wrong direction and add instability.

The second assumption is that there is no friction in the system. Real linear rails have dry friction ( ). This is our strictest margin at only 1x. Stiction stays roughly constant and always opposes the cart's motion. This makes the cart harder to move, stalls the motion, and causes the balance controller to overcorrect back and forth endlessly.

Key takeaway for me here is that the model can be inaccurate with its captured terms and still be fine (for the most part), but if the model is missing a term entirely, you stand little chance against it.

Accounting For Delay

Luckily, the data-collection delay is not that complicated to solve for. In the real world you can measure it once with an oscilloscope and store that value. Then, in your control system, you offset the model forward by that value and feed back on the predicted state instead of the current state. This is essentially just looking ahead with our controller so by the time the correction force arrives it's at the intended moment rather than behind. Imagine it like a quarterback throwing to his receiver, you have to lead the target.

This is what the prediction step looks like mathematically:

τ is loop delay; x_meas is the stale sensor reading; f is the physics model; K(t) and x_plan are Part I's tracking gains and planned trajectory. In classical control, this look-ahead strategy is the foundation of a Smith predictor.

This barely changes the controller from Part I. The only difference is that we swap the raw sensor measurement for the forward-predicted state before computing the correction.

Fig. 3 Delay tolerance, before and after
raw feedback27 ms max tolerated delaywith prediction119 ms max tolerated delay
Forward prediction extends allowable loop delay from 27 ms to 119 ms.

This simple prediction step takes our tolerable delay from 27 ms to 119 ms, which is a 4x gain. It only costs one extra model rollout per tick (around 20 microseconds on my macbook m3 pro), which is a tiny fraction of the 2ms compute budget. This type of delay tolerance means you could easily run the control loop over a tethered PC instead of having to run it on a microcontroller, which is a huge convenience for development.

Modeling Friction

The fix for friction uses the same philosophy I used for fixing the delay. Include it in the model! Measure the rail stiction once on the bench (or guess, in my case), and put that force directly into the commanded motor force as :

F̂_c is the estimated rail stiction magnitude; sgn(ẋ) cancels the drag in the instantaneous direction of motion.

Just like the delay fix, this is a tiny change to the controller. The feedback gains and planned trajectory stay identical; we only add the friction cancellation term directly onto the motor output.

Fig. 4 Friction tolerance, before and after
raw feedback4 N max tolerated frictionwith feed-forward10 N max tolerated friction
Feed-forward stiction compensation pushes allowable rail drag from 4 N to 10 N.

Adding feed-forward compensation pushes our tolerable stiction from 4 N to 10 N, more than doubling our margin and giving us plenty of headroom above the ~3 N typical stiction of a linear rail.

One detail to watch out for: be slightly conservative on your friction estimate. Canceling less friction than the rail has is no biggie, but canceling more creates positive feedback on velocity. It's a negative damper that instead of slowing motion down, actively injects energy and destabilizes the cart (like buttering the rail, but in a bad, not delicious way).

Putting It Together: The Side-by-Side Payoff

With both new variables captured in my model, we can revisit the safety margin from Fig. 2.

Fig. 5 Safety margins before and after the robustness fixes
1.0×10×15×1.3×3.2×dry frictionon the railnominal: 3 Nmax: 4 N 10 N2.7×11.9×loop delaysensing to drivenominal: 10 msmax: 27 ms 119 ms3.1×3.1×encoder resolutionquantized countsnominal: 2048 counts/revmin: 663 counts/rev (as-is)3.4×3.4×link-2 masserrornominal: 20%max: 67% (as-is)
as designed with robustness package 1.0× hardware bench (failure threshold)
Values above 1.0× represent safety headroom over reality
Comparing the stock baseline (gray) to the controller with delay prediction and friction feed-forward enabled (orange). Headroom against loop delay jumps by 4x, and dry friction margin more than doubles.

Both of our tight margin failure points are now safely clear of the 1.0x cliff edge because we no longer make assumptions about them. As you can see we gained no margin from the mass error or encoder resolution, but those were already well above the cliff edge to begin with and accounted for in the model.

Additional Considerations

Software tweaks can make a controller much more forgiving, but they cannot fix hard hardware limits. Control math won't help if your motor can't deliver enough force to move the system effectively or if your is too coarse to measure velocity at the required granularity. These are physical limits, and I can't help you tune your way around them. With these software robustness changes I've been increasing the margin against the things I got wrong about the system. These changes don't give me abilities the machine never had in the first place, unfortunately.

The other thing I want to mention is that you can go as crazy with this as you want to. I only covered adding two real-world effects to the model, but you could add more. You could add rail deflection, air resistance, joint backlash, link flex, or any other real-world effect you can measure. There is a point of diminishing returns here for sure though.

Since I am an engineer and not a control theorist, my preference would be to make it just good enough to test on a real machine without destroying itself. From there, I'd work with the real machine as much as possible to refine the model since it's way easier to make it work there in an imperfect environment than to perfectly model all those imperfections and assumptions.

The Build Order

If I were putting this on a real bench next week I'd probably do it in this order:

  1. Measure the loop delay first. Send a square wave into the motor, put a scope on the encoder, count the milliseconds. It gives us critical margin on one of our tightest failure points.
  2. Measure the friction. Push on the cart, ramping the force until the cart breaks free and mark the number. Feed it into the predictor's model at around 90% of the true value. Remember going low is better here so you don't introduce unwanted velocity.
  3. Skip careful system identification for now. The funnel absorbs plenty of measurement error on its own. Weigh the parts and measure the lengths with standard equipment, nothing fancy. If it misbehaves, the cause is far more likely to be one of the two items above than model inaccuracies.
  4. Test the brake before the swing up. The brake is Part I's second controller, the one that resets the machine to hanging after a fall. It runs about a stable equilibrium, so a bug there should be less significant than a full blown crash. Plus, you probably want the part of the controller that is good at taking energy out of the system dialed in before you pump a ton of energy into it.

I mentioned earlier that I am traveling so I can't build this real setup. That doesn't mean I can't dream about building it though! So, if you want to see my silly tangent I went on where I try to spec out parts for building this in reality (which admittedly is a bit ridiculous since this pendulum in the write up has 1m long links) then you can expand the section below to read what I came up with.

Hardware Daydream What I'd build with a $2,000 Budget
The Challenge: If I had a $2,000 budget to build this thing, what specific motor, motor driver, and encoder resolution would I choose? More importantly, what microcontroller or compute would I use to guarantee the loop rate and minimize round-trip latency?

With that budget, I have plenty of great options. In the simulation, the double pendulum setup was intentionally oversized to have nice numbers. It had a 1 kg cart with two 1-meter links (1 kg each) operating along a 2-meter rail. Dealing with a system that large means substantial forces and torques whenever the pendulum swings wildly, but it is completely buildable within budget.

For compute, I'd go with a Teensy 4.1 from PJRC ($31.50). Its 600 MHz ARM Cortex-M7, native CAN-FD, and high-speed SPI make light work of the LQR gain calculations in under 100 microseconds. It is affordable, easy to program, and well-supported in the embedded robotics community.

For the rail, I need high rigidity to handle the bending moments when the pendulum flails. A 2.02-meter HIWIN HGH20 linear guide with a square carriage block costs about $287. Over a 2-meter span, rubber or fiberglass belts stretch under dynamic load, so I would pair it with a steel-core polyurethane belt (HTD-5M, ~$21) at 5 mm pitch for high torque transfer without elasticity, driven by two 20T pulleys ($15) with an outer radius of 15.35 mm.

With peak cart force from the simulation around 30 N, I can calculate the motor torque requirement directly:

With a 36.9 mm pitch diameter pulley, achieving max cart speed (3.09 m/s) requires 1,600 RPM.

Using P = (T × RPM) / 9550, peak mechanical power comes out to around 770 W. A brushless drone motor like the T-Motor U7 KV490 ($150) delivers 1 kW at 75% throttle (25 V, 40 A) while weighing only 300 g, keeping cart inertia near my 1 kg target. Since peak output is only needed during violent catches, steady-state thermal load is low. (An alternative is the ODrive D5065 270KV at 420 g).

To drive the motor, I'd pair it with an ODrive S1 ($150), supporting 80 A peak, native CAN-FD, and an onboard brake chopper to dissipate voltage spikes during aggressive pendulum decel. For feedback, two CUI AMT21 absolute RS485 encoders ($100 total) provide 14-bit resolution (16,384 CPR). With my chosen pulley radius, this resolves cart motion down to ~0.01 mm per count, basically eliminating velocity quantization noise.

For the pendulum links, link 1 sees peak static torque when fully horizontal:

Dynamic swings can double this to ~30 N·m. Keeping tip deflection under 5 mm requires I ≥ 20,000 mm⁴.

To resist that bending moment without flailing, we can use two 1-meter 30 mm OD × 26 mm ID carbon fiber tubes ($120). They easily satisfy the stiffness requirement while weighing only ~300 g each, leaving plenty of mass margin for dual flanged ball bearings, 8 mm ground steel shafts, clamping shaft collars (~$30), and custom SendCutSend (or similar) laser brackets (~$50).

Bill of Materials (Full 2m Scale Bench) (Budget: $2,000)
SubsystemComponent SelectedKey SpecificationEst. Cost
MicrocontrollerTeensy 4.1600 MHz M7, CAN-FD, SPI$31.50
Linear RailHIWIN HGH202.02 m rail + square block~$287.00
Belt DriveHTD-5M Steel Core2 m zero-stretch polyurethane~$21.00
Pulleys2x 20T HTD-5Mr = 15.35 mm outer radius~$15.00
Brushless MotorT-Motor U7 KV4901 kW peak, 300 g mass$150.00
Motor DriverODrive S180 A peak, CAN-FD, brake chopper$150.00
Encoders2x CUI AMT2114-bit absolute RS485 (16,384 CPR)$100.00
Carbon Links2x Carbon Tubes (1 m)30mm OD × 26mm ID, ~300 g each$120.00
Joint HardwareBearings, Shafts & CollarsFlanged bearings, 8mm steel shafts~$30.00
Custom BracketsSendCutSend CNC PlatesMotor mount & link pivots~$50.00
Misc / FastenersWiring, 6S LiPo, HardwareBolts, connectors, battery~$100–$200
Estimated Total (including misc)~$1,050–$1,150
Microsecond Latency Budget (1 kHz Control Loop) < 0.35 ms total delay
t = 0.0 ms (SPI Poll) t ≈ 0.3 ms (Current Total) t = 1.0 ms (Next Tick)
t = 0.0 ms ~0.05 ms
SPI Angle Query

Teensy 4.1 polls joint encoders over high-speed SPI.

t = 0.1 ms ~0.10 ms
Gain Math & Filter

600 MHz M7 differences angles, filters velocity, and evaluates TVLQR.

t = 0.2 ms ~0.10 ms
CAN-FD Dispatch

Torque command fired over high-speed CAN-FD to ODrive S1.

t = 0.3 ms Immediate
Current Injection

ODrive injects phase current into low-friction T-Motor windings.

Total round-trip delay is well under our 1 ms budget. At that speed my RK4 forward-prediction code becomes a redundancy.

Scaling note: If a 2-meter rail is too big for your workspace, everything scales down linearly. At 25% scale (0.5 m rail, 0.25 m links), motor torque drops to just 0.115 N·m and bending moment drops to 7.5 N·m.

Have feedback or thoughts?

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

Send Feedback →
Sim-to-RealRobustnessControl Theory