Skip to content

ADRC forward-Euler ESO diverges for wo * Ts > 0.83, with no guard or documented limit #275

Description

@gabrielsantosphilips

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

  1. 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.
  2. Validate wo * Ts in the constructor: either really_assert it, or clamp with a documented bound.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions