Experimental Inner-Loop System Formulation
The inner loop comprises a PID controller, an experimentally identified plant, adaptive gain scaling, and negative feedback.
We call the experimentally identified plant $H(s,k)$. It maps the internal PID output $v$ to the measured response $y$, while $k$ identifies the current stiffness condition between 7k and 5k.
The PID and plant multiply to form the open-loop transfer function. The adaptive factor $\alpha\in[0.02,1]$ scales the controller’s influence; $\alpha=1.00$ means full PID gain.
Closing the negative-feedback loop gives the command → measurement response:
Read all three plots together.GM is the extra gain available when phase reaches $-180^\circ$. PM is the extra phase available when magnitude reaches 0 dB. The pole–zero map uses fixed six-pole, five-zero fits to the Bode data, so its order never changes. Open-loop poles can remain in the LHS even when a negative GM predicts closed-loop instability; switch to Closed to see the corresponding RHP poles in red. The identified family ends at 5k, and the 5k–1k range is a linear extrapolation. The live Bode curve turns from black to red; the light-gray 7k, $\alpha=1$ reference stays fixed.
Code connection · plant, PID, and nominal loop
112% H_dc_dm: response from the digital-controller output to measurement. 114[H_dc_dm, H_dc_dm_xfunc, H_dc_dm_fit_info] = ... 115 identify_experimental_system(dc, dm, fs, fit_config); 142digital_cntrlr = pid(P, I, D, 1/derivative_filter_N, dt, ... 143 'IFormula', 'ForwardEuler', 'DFormula', 'ForwardEuler'); 146PID_H_dc_dm = feedback(digital_cntrlr * H_dc_dm_d, 1); 160PID_H_dc_dm_loop = digital_cntrlr * H_dc_dm_d; 42C = pid(pv.P, pv.I, pv.D, 1/pv.derivative_filter_N, dt, ... 45cfg = sol_osi_simplified('config', dt, C*H_dc_dm, override.osi);
Short-Time Spectral Estimation of Interface Response
OSI updates at 0.125 s intervals using the most recent 0.5 s window of command and measured displacement data.
Let $u[n]$ be command displacement and $y[n]$ be measured displacement. A Hann window $w[n]$ reduces edge discontinuities before the FFT:
At time step $n$, the instantaneous input-power, output-power, and cross-power terms are
One window is noisy, so each spectrum is . An overbar denotes a smoothed power or cross-power estimate; $S$ is reserved for the feedback sensitivity function.
state.Suu, state.Syy, and state.Syu.
Recorded source. saved_results/tests/gpt5_6_tests/20260710_1845_plNew_authON_oscillations_unstableAtEnd.mat · command $d_d\equiv u$ · 12 Hz FFT bin · record time 24.625–29.625 s · 0.5 s Hann window updated every 0.125 s. The 41 circles are computed from the recorded command samples, not generated examples.
Code connection · window, FFT, and spectral memory
70'step_s', 0.125, ... 71'segment_s', 0.5, ... 106cfg.reference_keep = cfg.keep; 99cfg.window = 0.5-0.5*cos(2*pi*n/(cfg.segment_n-1)); 178Uall = fft(state.u.*cfg.window); 179Yall = fft(state.y.*cfg.window); 182pu = abs(U).^2; 183py = abs(Y).^2; 184cyu = Y.*conj(U); 198state.Suu = state.Suu+q*(pu-state.Suu); 199state.Syy = state.Syy+q*(py-state.Syy); 200state.Syu = state.Syu+q*(cyu-state.Syu);
Confidence-Weighted Coherent Identification of Closed-Loop Dynamics
The coherent branch estimates the closed-loop response from the command–measurement relationship. It then blends that estimate with the known healthy model.
asks: “At this frequency, how consistently does the input explain the output?”
Coherence alone is insufficient, so OSI gates it with input leverage for two reasons:
- Feedback-created coherence. At frequencies with high loop gain, measurement noise can drive the feedback loop and create high coherence between feedback-related signals even when the intended external excitation is absent. The resulting relationship is driven mainly by noise and can produce a misleading transfer-function estimate.
- Division by weak input power. Forming a transfer-function ratio requires division by the input spectrum. When $\bar p_{uu}$ is very small, numerical roundoff, measurement noise, spectral leakage, and ordinary estimation fluctuations can be greatly magnified.
The leverage factor therefore reduces confidence in frequency bins that do not contain meaningful input power. It cannot by itself distinguish strong feedback-correlated noise from intentional excitation.
First define the relative input power $p_{rel}(\omega)$: the power in the current frequency bin divided by the power in the strongest bin.
Next, $p_{floor}$ is a small positive floor ($10^{-3}$ here) that prevents tiny input values from receiving high trust. The input leverage $\ell(\omega)$ maps relative power to a number between 0 and 1:
Finally, $A$ is one activity score for the entire time window: $A=0$ means the command is quiet and $A=1$ means it is strongly driven. Stage 06 gives its exact RMS-based definition. The per-frequency confidence $c(\omega)$ combines activity, coherence, and leverage:
The spectra now provide the raw measured closed-loop estimate $\widehat T(\omega)$:
OSI also forms a model prior. It scales the healthy loop $L_0(\omega)$ by the currently applied adaptive gain $\alpha$, then converts that loop to its closed-loop response $T_{prior}(\omega)$. Here $\alpha$ is the applied a_cmd value: $\alpha=1.00$ means full PID gain, and it changes only if the separate adaptive-gain controller changes a_cmd. The estimator clamps the received value to $0.02\le\alpha\le1$:
The confidence $c(\omega)$ chooses how far the usable response $\widehat T_{use}(\omega)$ moves from the model prior toward the measured estimate. The final symbol $\widehat L(\omega)$ is the reconstructed open loop:
Code connection · confidence-weighted loop reconstruction
205gamma2 = min(1, abs(state.Syu).^2./max(state.Suu.*state.Syy, eps)); 206p_rel = state.Suu/max(max(state.Suu), eps); 207leverage = p_rel./(p_rel+cfg.power_floor); 208confidence = activity*gamma2.*leverage; 209That = state.Syu./max(state.Suu, eps); 210Tuse = confidence.*That+(1-confidence).*Tprior; 214Lhat = Tuse./den; 215b_st_curve = movmedian(chordal(Lhat, cfg.cap), 3);
Chordal-Distance Formulation of the Online Stability Indicator
At each frequency, OSI evaluates the separation between the estimated loop and the critical feedback condition; the minimum separation defines the reported margin.
The closed-loop denominator is $1+L$. Instability becomes possible when $L(j\omega)$ approaches $-1$, because then $1+L\to0$.
Define $b(\omega,\alpha,k)$ as the normalized distance from that critical condition at frequency $\omega$, adaptive gain $\alpha$, and plant condition $k$:
Let $\Omega$ be the usable 2–100 Hz band after excluding the 58–62 Hz line-noise bins. The operator $\operatorname{median}_3$ is a three-bin median filter that suppresses isolated spikes. The identification branch keeps its margin as a curve over $\Omega$:
Code connection · chordal curve and worst-frequency selection
110cfg.L0 = squeeze(freqresp(initial_loop_tf, 2*pi*cfg.freq_hz)); 112cfg.nominal_curve = chordal(cfg.L0, cfg.cap); 113cfg.nominal = min(cfg.nominal_curve.*cfg.keep+cfg.cap*(~cfg.keep)); 215b_st_curve = movmedian(chordal(Lhat, cfg.cap), 3); 216full_coherent = minMasked(movmedian(chordal(min(1/a_now, 4)*Lhat, ... 217 cfg.cap), 3), cfg.keep); 361function b = chordal(L, cap) 362b = min(real(abs(1+L)./sqrt(2*(1+abs(L).^2))), cap); 365function y = minMasked(x, mask) 366y = min(x(mask));
Residual Output-Energy Inference under Weak Excitation
When command excitation is insufficient for coherent identification, OSI evaluates changes in measured output spectra relative to a healthy reference.
Let $r=1,2,3$ index the first three non-overlapping 0.5 s reference windows, which are assumed to represent the healthy loop. For window $r$, $y_r[n]\equiv d_{m,r}[n]$ is its measured-displacement segment and $Y_r(\omega)$ is the FFT of that segment after applying the estimator window $w[n]$. The median reference power is denoted by $p_{yy,\mathrm{ref}}(\omega)$:
For every later window, the observed ratio $q_{obs}(\omega)$ divides the current smoothed output power by that fixed healthy-reference power:
OSI represents damage-like loop growth with a frequency-dependent scale $g(\omega)$. For each of 25 candidate values between 0.5 and 6, the modeled output power $p_{yy,\mathrm{model}}(\omega,g)$ is normalized by the same healthy-reference power used in $q_{obs}(\omega)$. The resulting modeled ratio is $q_{model}(\omega,g)$:
The remembered fitted curve is updated in log space. The same recurrence is used for damage-evidence accumulation and recovery, with a different update gain for each case:
The residual margin $b_{res}(\omega)$ is the chordal margin predicted by the frequency-dependent loop-growth hypothesis, kept as a curve just as $b_{st}(\omega)$ is. A three-bin moving median is applied across adjacent frequency bins:
Code connection · healthy reference and frequency-dependent $g$ fit (rho in MATLAB)
74'residual_s', 1.00, ... 83'rho_recover_s', 0.4, ... 86'reference_level_beta', 0.50, ... 105cfg.rho_fit_keep = cfg.keep; 106cfg.reference_keep = cfg.keep; 187state.q_ref_samples(:, state.q_ref_count) = py; 189state.q_ref = median(state.q_ref_samples, 2); 225qobs = state.Syy./max(state.q_ref, eps); 226state.reference_ratio = median(qobs(cfg.reference_keep)); 232loss = inf(numel(cfg.L0), numel(cfg.rho_grid)); 241loss(cfg.rho_fit_keep, k) = abs( ... 242 log(max(qobs(cfg.rho_fit_keep), eps))- ... 243 log(max(qmod(cfg.rho_fit_keep), eps))); 247rho_best(cfg.rho_fit_keep) = rho_values(kbest(cfg.rho_fit_keep)); 285b_rho_curve = movmedian(chordal(a_now*rho.*cfg.L0, cfg.cap), 3);
Per-Frequency Fusion of Stability Evidence
Neither branch is trusted everywhere. Both are kept as curves over frequency, every bin is charged for the evidence it actually has, and only then is one isolated low bin trimmed so the second-smallest bin governs.
The 0.5 s command RMS defines the binary activity $A$ directly. The approved threshold is $\eta_A=0.0575$, approximately $4.54\sigma_{\mathrm{noise},u}$ based on the clean quiet-record estimate:
Activity is no longer a regime switch that selects one scalar margin or the other. It enters only where it belongs: inside the confidence $c(\omega)$ of the identification branch, and in the fast drive-ratio detector of the residual branch.
Two independent kinds of evidence exist at every bin, and each one has to pay for the claim it makes. The first is the confidence $c(\omega)$ already built in Section 03: command energy and coherence say the loop is directly visible at this frequency, so the identified margin $b_{st}(\omega)$ is worth believing.
The second is a detection weight $d(\omega)$. The fitted loop growth $g(\omega)$ only means something once it clears the resolution of the $g$ fit itself; below the threshold $g_{det}=1.75$ the deviation carries little residual evidence. Where $g$ does clear it, the residual model $b_{res}(\omega)$ is worth believing to the extent the loop is not directly visible:
The two evidence weights never sum to more than one. Whatever is left over is the prior weight $w_{prior}$: the share of the bin that neither branch can account for, and which therefore falls back on the current authority-scaled commissioning prior $b_{prior}(\omega)$.
Carrying the unsupported share on the prior, rather than on the ceiling, is what makes the reported value an estimate of the loop margin instead of treating “unknown” as the largest possible margin.
Every frequency bin of every update lands somewhere on this square. Its position fixes the mixture completely, because the three weights are determined by $c$ and $d$ alone: $w_{st}=c$, $w_{res}=(1-c)d$, $w_{prior}=(1-c)(1-d)$. The colour is those three weights blended in linear light, so a corner shows its pure colour and the interior shows the true proportions.
After forming $b(\omega)$ at every retained frequency, the estimator trims the single smallest value and selects the second-smallest bin at $\omega_*$. That selected value is the scalar instantaneous margin $b_i[n]$:
For the configured sampling rate $f_s=2048\,\mathrm{Hz}$ and 0.5 s moving window, each FFT uses 1024 samples and has frequency spacing $\Delta f=f_s/1024=2\,\mathrm{Hz}$. The 2–100 Hz band therefore contains 50 bins; rejecting 58, 60, and 62 Hz leaves 47 retained bins. Thus $b_{(2)}$ is the second of 47 sorted margins, with empirical cumulative rank $2/47\approx4.3\%$—approximately the 4th percentile—and only the single lowest margin across the retained frequency bins is discarded.
The reported scalar margin is a first-order lag on $b_i[n]$, with $\eta_b$ the fraction of the new reading admitted at each update:
Recovery
Recovery uses the median observed power ratio over $\Omega$:
When $r_{\Omega}<1$, the remembered growth $g$ decays toward the healthy value $g=1$ using the recovery gain:
Each qualifying update applies the log-domain recurrence of Section 05 with the recovery correction toward $g=1$; otherwise the remembered value remains unchanged.
Code connection · recovery criterion, per-bin weights, and final margin smoothing
73'margin_s', 0.25, ... 80'eta_A', 0.0575, ... 81'hard_activity_gain', 0.80, ... 82'detector_max', 1.95, ... 83'rho_recover_s', 0.4, ... 84'rho_detect', 1.75, ... 86'reference_level_beta', 0.50, ... 162activity = double(rms(state.u) > cfg.eta_A); 163activation_weight = cfg.hard_activity_gain*activity; 260drive_ratio = 1+activation_weight*kick; 280forgetting = double(state.reference_ratio < 1); 281state.log_rho = state.log_rho*(1-cfg.recover_alpha*forgetting); 282rho = exp(state.log_rho)*drive_ratio; 297detected = min(max((rho-1)/(cfg.rho_detect-1), 0), 1); 298w_st = confidence; 299w_res = (1-confidence).*detected; 300no_evidence_curve = cfg.cap*ones(size(b_prior_curve)); 301if cfg.no_evidence_prior 302 no_evidence_curve = b_prior_curve; 303end 304b_bin = w_st.*b_st_curve+w_res.*b_rho_curve+ ... 305 (1-w_st-w_res).*no_evidence_curve; 307[b_sorted, order] = sort(b_bin(cfg.keep)); 308b_raw = b_sorted(2); 309governing_local = order(2); 314 activity, w_res(governing_bin), rho_best, full_coherent, cfg, state); 315info.direct_fraction = w_st(governing_bin); 316info.fallback_fraction = 1-w_st(governing_bin)-w_res(governing_bin); 317info.governing_freq_hz = cfg.freq_hz(governing_bin);
Separation of Stability Estimation and Adaptive-Gain Control
OSI estimates the stability margin $b$, while a separate adaptive-gain law determines the controller response.
a_cmd), evaluates the corresponding loop, and returns evidence for a separate adaptive-gain controller.The adaptive factor uniformly scales the inner open loop:
Because a positive scale changes magnitude but not phase, the $-180^\circ$ crossover frequency stays approximately fixed. The gain margin changes in closed form:
Let $GM_{target}$ be the desired gain margin. The scale required to meet that target is $\alpha_{required}(k)$:
The adaptive law keeps $\alpha$ between the permitted minimum $\alpha_{min}$ and 1. When damage raises loop gain and $b$ falls, reducing $\alpha$ can restore separation from the critical condition. Recovery should be slower and evidence-based so one optimistic window cannot rush the adaptive factor upward.
Code connection · applied adaptive gain enters the estimator
164a_now = min(max(state.a_cmd, 0.02), 1); 165Lprior = a_now*cfg.L0; 166Tprior = Lprior./(1+Lprior); 167b_prior_curve = chordal(Lprior, cfg.cap); 168b_prior = minMasked(b_prior_curve, cfg.keep); 3% T = feedback(a*C_pid*H_dc_dm, 1), with open loop 4% L(a) = a * C_pid * H_dc_dm. 8% GM_dB(a) = GM_dB(1) - 20*log10(a) 11% a_required = 10^((GM_dB(1) - target_gm_db)/20)
Integrated Mathematical Formulation of the OSI Estimator
The complete OSI update is summarized as an eight-stage computational sequence.
Correspondence Between Mathematical Formulation and Implementation
Read makeConfig as the mathematical constants, makeState as estimator memory, and stepEstimator as the eight-stage pipeline above. This formulation corresponds to research/sol_osi_simplified/sol_osi_simplified.m; it remains separate from the production SOL-OSI implementation.
The accepted simplified method keeps the complete V5 estimator and changes only the final reducer: after the per-frequency evidence mixture is formed, it selects the second-smallest retained value instead of the minimum. That one-bin trim prevents a single isolated residual from governing the full estimate while leaving all evidence weights, filters, detector settings, and authority logic unchanged.
In Fabby Bench 2, with synthetic target 0.55 and experimental target 0.30, the simplified method passes all seven gates. It scores 92.94 on Fabby 1, 62.48 on Fabby 2, 60.00 on detection, 87.73 on smoothness, and 100 on GDB recovery, for a composite of 80.63. Indicator jitter is 1.95, unstable time is zero, and tail authority is 0.503.
Fabby Bench 2 Comparison
The detection score counts the five known diagnostic-gain increases—three in the live experimental scenario and two in GDB—and awards one fifth for each event reflected in the reading or authority response within two seconds. The simplified method detects 3 of 5, so its detection score is $100(3/5)=60$.
The implemented estimator has the strongest raw detection score, but it fails the zero-unstable-time and end-hold gates. The simplified estimator passes every gate. Section 09 replays every test for these two maintained methods and tabulates the scorecard.
Fabby Bench 2: Implemented and Simplified
Every test of the benchmark, replayed for the implemented estimator and the accepted second-smallest-bin simplified estimator, with the reported margin and applied authority side by side.
The measured displacement is drawn once per implementation, because in every closed-loop test the authority factor scales the command that drives the plant: where the two traces separate, the implementations have driven the specimen differently. Dashed vertical marks give the plant blend used by the synthetic scenarios, where $\lambda=0$ is the identified 7k model and $\lambda=1$ the 5k model. Where a test provides ground truth, the true chordal margin is drawn dashed in each implementation's own colour, since it depends on the authority that implementation applied. The diagnostic gain, where the record carries one, is drawn against the right-hand axis of the authority panel. Click any legend entry to hide or restore that trace; the vertical scales follow what is left visible, so a single implementation can be read out of a crowded plot.
Scorecard
Read directly from the saved Fabby Bench 2 results, so this table cannot disagree with the benchmark that produced it. A raw composite is reported only when all seven gates pass; the failing implemented method is shown for reference and marked as gated out. Hover or tab onto any metric name for what it measures, whether high or low is the good direction, and which test in the dropdown above to look at.
| Metric | implemented | simplified |
|---|