Summary
ActiveDisturbanceRejectionControl injects the observer gains with forward Euler. For Order = 1 the ESO error dynamics leave the unit circle at wo * Ts ~ 0.83. Nothing in the constructor validates this, and the resulting divergence is silent until the state becomes NaN.
doc/robust_control/ActiveDisturbanceRejection.md documents an accuracy guideline:
Euler discretization accuracy. The forward-Euler ESO introduces phase lag proportional to wo * Ts. Keeping wo * Ts << 1 (e.g. wo * Ts <= 0.1) maintains accuracy [...]
but not the stability boundary, and the same document recommends:
A practical rule of thumb is wo in [3 wc, 10 wc].
At a 1 kHz outer loop with wc = 2*pi*30 ~ 188.5 rad/s, the documented rule of thumb gives wo in [565, 1885] rad/s, i.e. wo * Ts in [0.57, 1.89]. Everything from ratio 5 upward is unstable. Following the library's own guidance produces a divergent observer.
Error dynamics
From Compute() with Order = 1, L = [2 wo, wo^2], and h = wo * Ts, the estimation error propagates as
[e; df][k+1] = [[1 - 2h - h^2, Ts],
[-h^2 / Ts, 1]] [e; df][k]
with characteristic polynomial z^2 - (2 - 2h - h^2) z + (1 - 2h). Jury's criteria give stability only for h < (-4 + sqrt(32)) / 2 ~ 0.828.
Measured spectral radius:
wo * Ts |
spectral radius |
| 0.20 |
0.872 |
| 0.40 |
0.785 |
| 0.60 |
0.718 |
| 0.80 |
0.904 |
| 0.83 |
1.005 |
| 0.90 |
1.250 |
| 0.94 |
1.404 |
| 1.20 |
2.419 |
Reproduction
#include "numerical/robust_control/ActiveDisturbanceRejection.hpp"
#include <cstdio>
// Mechanical plant w[k+1] = 0.999 w[k] + 0.5 u[k], b0 = Kt/J = 500, Ts = 1 ms
int main()
{
for (float wo : { 942.48f, 400.0f }) // 5*wc (documented ratio) and a clamped value
{
robust_control::ActiveDisturbanceRejectionControl<float, 1> adrc{ wo, 188.5f, 500.0f, 0.001f };
float speed{ 0.0f };
for (int step = 0; step != 4000; ++step)
speed = 0.999f * speed + 0.5f * adrc.Compute(20.0f, speed);
std::printf("wo=%7.2f (wo*Ts=%.3f) -> final speed %g rad/s (reference 20)\n",
wo, wo * 0.001f, speed);
}
}
Output:
wo= 942.48 (wo*Ts=0.942) -> final speed nan rad/s (reference 20)
wo= 400.00 (wo*Ts=0.400) -> final speed 20 rad/s (reference 20)
No actuator limit is involved here — the command stays around 7.5 A against a 10 A envelope. The divergence is entirely the observer.
Suggested direction
- Document the hard stability limit alongside the existing accuracy guidance, and note that it interacts with the
[3 wc, 10 wc] rule of thumb — the two are incompatible at common outer-loop rates.
- Validate
wo * Ts in the constructor: either really_assert it, or clamp with a documented bound.
- Longer term, discretize the ESO exactly (ZOH or pole-matched), which the doc already lists under future work as "Discrete ESO".
The boundary above is derived for Order = 1. Higher orders have their own, tighter limits — Order = 2 carries a wo^3 gain — so the guard should be order-aware rather than a single constant.
Found while building an ADRC speed controller. Worked around downstream by clamping wo <= 0.4 / Ts, which keeps both poles real and damped.
Summary
ActiveDisturbanceRejectionControlinjects the observer gains with forward Euler. ForOrder = 1the ESO error dynamics leave the unit circle atwo * Ts ~ 0.83. Nothing in the constructor validates this, and the resulting divergence is silent until the state becomes NaN.doc/robust_control/ActiveDisturbanceRejection.mddocuments an accuracy guideline:but not the stability boundary, and the same document recommends:
At a 1 kHz outer loop with
wc = 2*pi*30 ~ 188.5 rad/s, the documented rule of thumb giveswoin[565, 1885]rad/s, i.e.wo * Tsin[0.57, 1.89]. Everything from ratio 5 upward is unstable. Following the library's own guidance produces a divergent observer.Error dynamics
From
Compute()withOrder = 1,L = [2 wo, wo^2], andh = wo * Ts, the estimation error propagates aswith characteristic polynomial
z^2 - (2 - 2h - h^2) z + (1 - 2h). Jury's criteria give stability only forh < (-4 + sqrt(32)) / 2 ~ 0.828.Measured spectral radius:
wo * TsReproduction
Output:
No actuator limit is involved here — the command stays around 7.5 A against a 10 A envelope. The divergence is entirely the observer.
Suggested direction
[3 wc, 10 wc]rule of thumb — the two are incompatible at common outer-loop rates.wo * Tsin the constructor: eitherreally_assertit, or clamp with a documented bound.The boundary above is derived for
Order = 1. Higher orders have their own, tighter limits —Order = 2carries awo^3gain — so the guard should be order-aware rather than a single constant.Found while building an ADRC speed controller. Worked around downstream by clamping
wo <= 0.4 / Ts, which keeps both poles real and damped.