ADRC — a guide for PID users ← Back to the simulator

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:

ÿ = f + b₀·u

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.

Why this is powerful: if you could measure 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.

r Σ + e feedback law (PD on estimates) u₀ = ωc²·e − 2ωc·ẑ₂ u₀ Σ + 1/b₀ u sat Plant ÿ = f + b₀·u y Extended State Observer (ESO) estimates ẑ₁ ≈ y, ẑ₂ ≈ ẏ, ẑ₃ ≈ total disturbance f u y ẑ₁ ẑ₂ ẑ₃
Top: the control path (PD law → cancel ẑ₃ → ÷b₀ → plant). Bottom: the observer reconstructs the output, its rate, and the disturbance from u and y.

① 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:

② State-feedback control law

A plain PD controller acting on the estimated states — what acceleration we'd like:

u₀ = ωc²·(r − ẑ₁) − 2·ωc·ẑ₂

③ Disturbance rejection + scaling

Cancel the estimated disturbance and divide by the input gain to get the actual command:

u = (u₀ − ẑ₃) / b₀

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:

5. Translating your PID intuition

PIDADRC
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 appearsCancels the disturbance, then light PD on the rest
Re-plumb all gains for each new plantBandwidths 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:

  1. Set b₀ from physics first. It's "how much ÿ you get per unit u." You only need it within a factor of ~2 — the ẑ₃ term absorbs the rest. Symptoms of a bad b₀: too large → sluggish; too small → twitchy / oscillatory.
  2. 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.
  3. 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 making u chatter. ωo is the main noise ↔ rejection trade-off.
SymptomLikely causeFix
Slow; won't reject a steady loadωo too low, or b₀ too highRaise ωo; lower b₀
Control signal chatters / noisyωo too high for the noiseLower ωo
Rings / overshootsωc too high, or b₀ too smallLower ωc; raise b₀
Sluggish everywhereωc too lowRaise ω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.

PlantOutputInputb₀Why
Carpositionacceleration1ẍ = u − …
Ball & beamball positionbeam angle≈7r̈ ≈ B·g·u, B·g = (5/7)·9.81
Servoshaft angletorque2θ̈ = u/J − …, 1/J = 1/0.5
Cart-polepole anglebase accel2θ̈ ≈ u·cosθ/L, 1/L = 1/0.5
Robustness demo: the car has a "Wrong b0 (×4)" preset. Try it — ADRC still works, just more sluggishly, because ẑ₃ quietly compensates for the gain you got wrong.

8. Try it in the simulator

→ Open the simulator