Active Disturbance Rejection Control
If you know PID, you already know 80% of what you need. This explains the other 20% — and why ADRC often does better with less tuning.
On this page: the shift from PID · the model · the three pieces · the math · PID ↔ ADRC · tuning · b₀ per plant · try it
1. The idea, in one shift of mindset
A PID controller is fundamentally reactive: a disturbance pushes the output off target, an error appears, and PID responds to that error. The integral term eventually cancels steady offsets, but only after they've built up.
ADRC turns this around: it estimates the disturbance as it happens and cancels it directly, leaving only a simple, well-behaved system for a basic feedback law to steer.
The clever part is how broadly "disturbance" is defined. It isn't just an external push — it's everything the controller doesn't explicitly model: friction, drag, gravity, coupling between axes, the parts of the true nonlinear plant you ignored, and the external load. All of it gets bundled into a single signal called the total disturbance, estimated live, and subtracted from the control.
2. The model: every plant is a double integrator + "everything else"
Whatever the real plant is, ADRC writes it in one canonical form:
y— the output you control (position, angle, …)u— the control signal you commandb₀— the input gain: how stronglyuacceleratesyf— the total disturbance: literally everything else, lumped together
Example — the car in the simulator: ẍ = u − (b/m)·ẋ. Here b₀ = 1 and the
drag term −(b/m)·ẋ is simply part of f. You never have to know the mass or
drag coefficient — ADRC measures their net effect and cancels it.
f, control would be trivial — set
u = (desired ÿ − f) / b₀ and the messy plant collapses into a clean double integrator
you steer with a textbook gain. ADRC's whole job is to estimate f well enough
to do exactly that.
3. The three pieces
Read the diagram left-to-right along the top — that's the control path, the same shape as a PID loop. The block underneath is the observer; its three estimates (ẑ₁, ẑ₂, ẑ₃) feed back up into the control path.
① Extended State Observer (ESO) — the heart of ADRC
A small model that runs in parallel with the real plant and, from only the command u
and the measurement y, continuously estimates three things:
ẑ₁ ≈ y— the output (a smooth, de-noised version of the measurement)ẑ₂ ≈ ẏ— its rate of change (a clean, model-based derivative)ẑ₃ ≈ f— the total disturbance (the "extended" state — the magic one)
② State-feedback control law
A plain PD controller acting on the estimated states — what acceleration we'd like:
③ Disturbance rejection + scaling
Cancel the estimated disturbance and divide by the input gain to get the actual command:
The − ẑ₃ is the move that makes it "active disturbance rejection": the controller
subtracts off whatever the observer believes is pushing the plant — drag, gravity, a shove —
before the error has a chance to grow.
4. The math, exactly as implemented
Discrete update each timestep dt, for a second-order plant (input → output
through two integrations — true of every plant in the simulator). See
src/engine/adrcController.js.
# Extended State Observer (gains place its 3 poles at −ωo): e = ẑ₁ − y ẑ₁ += dt · ( ẑ₂ − 3·ωo·e ) ẑ₂ += dt · ( ẑ₃ − 3·ωo²·e + b₀·u ) ẑ₃ += dt · ( − ωo³·e ) # Control law (gains place the closed loop at (s + ωc)²): u₀ = ωc²·(r − ẑ₁) − 2·ωc·ẑ₂ u = (u₀ − ẑ₃) / b₀ then clamp to [u_min, u_max]
Three implementation details that matter:
- The observer is driven by the actual, saturated
u— feeding it the clamped command is ADRC's built-in anti-windup. ẑ₁is seeded to the first measurement so there's no startup jump.- For angular plants (servo, pendulum) the errors are wrapped to (−180°, 180°], so the ±180° seam is a small error, not a near-full-circle one.
5. Translating your PID intuition
| PID | ADRC |
|---|---|
Kp, Ki, Kd — 3 gains, re-tuned per plant | ωc, ωo, b₀ — 2 bandwidths + 1 physical gain |
| Integral term accumulates to kill steady-state error | ẑ₃ (disturbance estimate) does this — and sooner |
| Derivative of a noisy measurement | ẑ₂ is a filtered, model-based rate |
| Reacts to error after it appears | Cancels the disturbance, then light PD on the rest |
| Re-plumb all gains for each new plant | Bandwidths transfer; mostly just set b₀ |
The integral action you'd reach for in PID to reject a constant load is replaced by the live
ẑ₃ estimate. That's why, on the simulator's cart-pole, an ADRC nudge recovers with a
much smaller swing than PID: it cancels the shove instead of waiting for the error to accumulate.
6. How to tune it
Only three knobs, and a reliable order to set them:
-
Set
b₀from physics first. It's "how muchÿyou get per unitu." You only need it within a factor of ~2 — theẑ₃term absorbs the rest. Symptoms of a badb₀: too large → sluggish; too small → twitchy / oscillatory. -
Set
ωc(controller bandwidth) for the response speed you want. Biggerωc→ faster (rough rule: settling time ≈ 4–6 ⁄ ωc). Push until the control effort saturates or motion gets too aggressive, then ease back. -
Set
ωo(observer bandwidth) to about 3–10×ωc. The observer must out-run the loop so its disturbance estimate stays ahead. Raise it for crisper rejection; lower it if measurement noise starts makinguchatter.ωois the main noise ↔ rejection trade-off.
| Symptom | Likely cause | Fix |
|---|---|---|
| Slow; won't reject a steady load | ωo too low, or b₀ too high | Raise ωo; lower b₀ |
| Control signal chatters / noisy | ωo too high for the noise | Lower ωo |
| Rings / overshoots | ωc too high, or b₀ too small | Lower ωc; raise b₀ |
| Sluggish everywhere | ωc too low | Raise ωc (then ωo) |
7. b₀ per plant — the one number that changes
b₀ = ∂ÿ/∂u, read straight off each plant's equation of motion. The bandwidths
ωc, ωo are similar across plants; b₀ is the plant-specific one, which is
why each model ships its own default.
| Plant | Output | Input | b₀ | Why |
|---|---|---|---|---|
| Car | position | acceleration | 1 | ẍ = u − … |
| Ball & beam | ball position | beam angle | ≈7 | r̈ ≈ B·g·u, B·g = (5/7)·9.81 |
| Servo | shaft angle | torque | 2 | θ̈ = u/J − …, 1/J = 1/0.5 |
| Cart-pole | pole angle | base accel | 2 | θ̈ ≈ u·cosθ/L, 1/L = 1/0.5 |
ẑ₃ quietly compensates for the gain you got wrong.
8. Try it in the simulator
- Watch
ẑ₃. Hover the block diagram for tooltips. Apply a disturbance and watch the liveẑ₃ (dist)value track it — that's ADRC "seeing" the load. - Read the control-split chart. The disturbance-rejection trace (
−ẑ₃/b₀) shows how much ofuis fighting the disturbance vs the feedback trace doing setpoint tracking. Clean step → feedback dominates; under load → rejection carries it. - Sweep
ωo. On any plant, raise it and watch disturbance rejection sharpen (and noise creep intou); lower it and watch it soften. - Cart-pole. ADRC folds the unstable gravity term into
fand cancels it — raiseωoand the balance visibly stiffens. Click left/right to nudge it.