Skip to content

feat(autotune): robustness tab - #57

Merged
bresch merged 10 commits into
masterfrom
feat/autotune-robustness-tab
Oct 5, 2026
Merged

bresch merged 10 commits into
masterfrom
feat/autotune-robustness-tab

Conversation

@bresch

@bresch bresch commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a Robustness tab next to the existing plots, which move to a General tab. The new tab has three plots, all computed from the identified model and the current gains.

image
  • Nyquist (top left) of the loop gain. It shows the gain, phase and modulus margins, and a shaded disc where the modulus margin is below 0.5 (|S| above 6 dB). Two new margins are added:
    • Delay margin: the extra loop delay that destabilizes the loop, in ms and in samples. It is the minimum over all gain crossovers, not only the one the phase margin comes from.
    • Disk margin: stability against simultaneous gain and phase changes. It is also drawn as the exclusion disk that the Nyquist curve touches.
  • Root locus in Kp (bottom left). The closed-loop poles in the z-plane as Kp is scaled with I and D fixed; this is an exact root locus because the PID is in ideal form. It marks the current poles and the poles at the stability limit (Kp × gain margin), on a grid of constant damping ratio and natural frequency.
  • Sensitivity functions (right): the gang of four (S, T, PS, CS) plus setpoint → control (C_r·S). The peak of |S| is checked against the same 6 dB limit.

When the closed loop is unstable, all three plots show "unstable closed loop" instead of margins, which are meaningless in that case.

Extended presets with initial gains

Initial gains can be set from parameters. When the reference signal is also set, the identification can provide a closed-loop identification fit quality
image

Gains can now be tuned in standard/ideal or parallel form
image

Refactor

The closed loop now lives in a new loop_model.py, as a two-degree-of-freedom controller u = C_r r − C_y y around the plant. The time simulation, the loop gain, the margins and the sensitivities all come from this single block diagram. It reproduces the previous step/disturbance responses and margins to machine precision.

Testing

  • New test_loop_model.py (28 tests, no Qt needed). It checks:
    • the loop gain for every P/D placement;
    • that the closed-loop poles are the roots of 1 + L;
    • stability detection, S + T = 1, and max |S| = 1 / modulus margin;
    • that scaling the loop by the gain margin puts a pole on the unit circle;
    • that adding ⌊delay margin⌋ samples of delay keeps the loop stable and one more destabilizes it;
    • that the Nyquist curve touches the disk-margin exclusion disk.
  • The full pipeline runs on the three sample logs in logs/ with no warnings.
  • Checked stable and unstable renders, and small window sizes. The layout no longer collapses in small windows; the in-axes legends and texts are excluded from matplotlib's constrained layout.

Move the existing plots into a "General" tab and add a
"Robustness" tab, meant to hold several robustness plots. It starts with
a Nyquist plot of the loop gain showing the gain, phase and modulus
margins, and the region where the modulus margin is below 0.5 (peak
sensitivity above 6 dB).
Build the loop once in a LoopModel, as a two-degree-of-freedom
controller u = C_r r - C_y y + sign * disturbance around the plant. The
time simulation, the loop gain used for the margins and the upcoming
sensitivity plots all derive from the same block diagram instead of
separate hand-written formulas that must be kept in sync.

No change in behavior: the step and disturbance responses and the
margins match the previous implementation to machine precision. The
model is covered by unit tests independent of the GUI.
Gain, phase and modulus margins only describe robustness of a stable
closed loop; for an unstable one they are still computed and could be
read as comfortable. Check the roots of 1 + L and show "unstable closed
loop" instead of the margins in the Bode and Nyquist plots.
Plot the gang of four (S, T, PS, CS) next to the Nyquist plot, plus the
setpoint-to-control response CSF, which differs from CS because the
controller has two degrees of freedom (feedforward, P and D on the
feedback). CS shows how the controller, in particular the D gain and
filter, amplifies measurement noise into the control signal, PS how
well load disturbances are rejected.

The peak of |S| is the inverse of the modulus margin and is checked
against the same 6dB limit.
Show how the closed-loop poles move in the z-plane when Kp is scaled
with I and D fixed. As the PID is in ideal form Kp * (1 + I + D), this
is exactly a root locus of the loop gain. The current poles are marked,
together with the poles at the stability limit, where Kp reaches the
gain margin and the locus leaves the unit circle.

A grid of constant damping ratio and natural frequency helps reading
the poles in terms of the specifications of a continuous-time design.
The delay margin is the additional loop delay that destabilizes the
loop, the minimum of phase margin / crossover frequency over all gain
crossovers. It is shown in ms and in samples, the unit of the identified
delay, and is required to be at least one sample so that the loop
survives the delay being underestimated by one.

The balanced disk margin guarantees stability for simultaneous gain and
phase variations, unlike the classical margins that each assume the
other one is untouched. It is also drawn on the Nyquist plot as the
exclusion disk that the loop gain must avoid.
Nyquist on the top left, root locus on the bottom left and the
sensitivity functions over the full height on the right, which suits
the two square z-plane plots better than a single row and gives the
frequency plot the most room. Use matplotlib's constrained layout to
keep the space around the plots small while leaving room for the
labels.

The texts and legends drawn inside the axes are excluded from the
layout: otherwise, in a small window, they overflow the axes, the
layout makes more room for them, and the axes shrink until the layout
collapses.

Stack the Nyquist margin lines in a box so that their spacing follows
the font size instead of the axes height, which no longer fits five
lines in the smaller axes.
Use constrained layout for the general tab as for the robustness tab,
instead of the fixed and generous spacing between plots. The plots use
more of the window and titles no longer overlap the labels of the plot
above in smaller windows.
@bresch
bresch force-pushed the feat/autotune-robustness-tab branch from 4291f11 to fe06b00 Compare October 5, 2026 07:53
@bresch
bresch force-pushed the feat/autotune-robustness-tab branch from fe06b00 to ebf0e2b Compare October 5, 2026 07:54
Some controllers run outside PX4 and keep their gains in AOS settings
instead of PX4 parameters. List the numeric AOS settings logged in the
aos_settings info message next to the parameters, so a preset can read
the flown gains from them.
@bresch
bresch merged commit 26fcd34 into master Oct 5, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant