Repository navigation
ADRC forward-Euler ESO diverges for wo * Ts > 0.83, with no guard or documented limit #275
Description
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. Keepingwo * 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
woin[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 * Tsin the constructor: eitherreally_assertit, 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.
Activity