L1 Adaptive Control (gnc-autonomy/control/l1-adaptive-control)
Use when the task is designing and simulating an L1 adaptive control
law, after Cao and Hovakimyan (L1 Adaptive Control Theory, SIAM 2010,
scalar chapter-2 sigma-only specialization), for a first-order plant
whose coefficient a_p is unknown to the controller and that is hit by
an unknown constant matched disturbance. The controller closes the loop
with a state predictor run from the design model, a projection-based
adaptation law on the prediction error that estimates the lumped
matched uncertainty inside a bounded interval, a low-pass filter
C(s) = omega_c/(s + omega_c) on the adaptive signal, and the control u
= k_g*r - nu against a reference model that sets the command response.
This leaf implements the closed loop in pure Python, stdlib only,
deterministic, discrete-time Euler integration, and reports the
tracking-error and prediction-error histories, the sigma_hat and
filtered-signal histories, the projection engagement, the convergence
verdict and the certified transient-bound check. It pairs with
gnc-autonomy/control/adaptive-control (the architecture that replaces
the state predictor with a gradient update of two gains on the tracking
error) and with gnc-autonomy/control/control-allocation for spreading
the scalar command across redundant effectors; the fixed-gain design
leaves of the control pack cover plants whose coefficient is known.
Scope: single input, deterministic, no noise, constant matched
disturbance, sign of the control effectiveness known positive, a_p
unknown inside a bounded prior.
Domain quick reference
- Reference model: xm_dot = a_mxm + b_mr with a_m < 0; the model
settles at b_mr/(-a_m) = 1 for the unit command. Euler step: xm_next
= xm + dt(a_mxm + b_mr).
- Truth plant (simulator only): x_dot = a_px + bu + bd, a_p = -0.5
unknown to the controller, d = 1.0 the unknown constant matched
disturbance. In the design frame the plant reads x_dot = a_mx +
b*(u + sigma_true) with the lumped uncertainty sigma_true(x) = (a_p -
a_m)x/b + d = 0.25x + 1.0.
- State predictor: xh_dot = a_mxh + b(u + sigma_hat), run from the
same initial state as the plant. Prediction error: xt = xh - x.
- Projection-based adaptation law on the prediction error: sigma_hat_dot
= gammaProj(sigma_hat, -xt), projection onto [-sigma_b, sigma_b].
Discrete clamped Euler step: raw = sigma_hat - dtgamma*xt, then
clamp to [-sigma_b, sigma_b], so |sigma_hat| <= sigma_b every step.
- Low-pass filter on the adaptive signal, C(s) = omega_c/(s + omega_c):
nu_dot = omega_c*(sigma_hat - nu); Euler step nu_next = nu +
dtomega_c(sigma_hat - nu).
- Control law: u = k_gr - nu with k_g = -a_m/b = 0.5, the unfiltered
feedforward for unit DC tracking minus the filtered cancellation
estimate. The equilibrium control is negative: u = -0.75 holds the
plant at x = 1 against the disturbance.
- Histories are seeded at index 0 with the initial values and have
length steps + 1: history[k] is the value at time k*dt (after k
advances). Per step the loop samples the prediction error, updates
sigma_hat, advances the filter on the updated estimate, forms the
control, advances plant, predictor and reference model, then appends.
- L1 admissibility of the scenario: ||C(s)(s - a_m)^-1b||_L1 =
b/|a_m| = 2.0 for every first-order C(s) with unit DC gain, so the
scalar L1-norm condition with Lipschitz constant L = (a_p - a_m)/b
reduces to (a_p - a_m)/|a_m| < 1: 0.5 at the worked plant, 0.75 at
the prior corner a_p = -0.25. An open-loop-unstable mismatch
violates the condition and the estimator resonator destabilizes the
loop; the worked scenario keeps a_p < 0.
- Reference-loop stability identity at perfect adaptation: the
linearized (x, nu) pair has trace a_p - omega_c = -10.5 and
determinant -a_momega_c = 10.0 (stable for every omega_c > 0 when
a_p < 0). The discrete prediction-error pair contracts at |lambda| =
sqrt(1 + a_mdt) = 0.9949874 per step for every adaptation rate.
- ARP4754A frames the development assurance context for the airborne
function that hosts the adaptive loop; the relations above are
standard L1 adaptive control methodology, summary-only.
Workflow
- Fix the loop data: design pole a_m (negative), control effectiveness
b, reference gain b_m, command r, initial state x0, disturbance d,
step dt, adaptation rate gamma, filter bandwidth omega_c, projection
bound sigma_b, prior bounds a_lo..a_hi on the unknown plant
coefficient, and the run length in steps.
- Sanity-check the pieces with single steps before the full run:
projection boundary behavior and adaptive_update clamping of the
projection-based-adaptation-law, filter_step (the low-pass filter
C(s) = omega_c/(s + omega_c)), plant_step, predictor_step,
reference_step and control_output (u = k_g*r - nu).
- Form the design-frame lumped uncertainty with sigma_true(x)
(simulator bookkeeping only, never an input to the controller) to
see what the estimator must cancel, and confirm L1 admissibility:
the product (a_hi - a_m)/|a_m| below 1 and the reference-loop
stability signs trace = a_p - omega_c < 0, determinant
-a_m*omega_c > 0.
- Run the closed loop with simulate(...): read the x, xh, xm state
histories, the sigma_hat and nu (filtered-signal) histories, the u
history, pred_err = xh - x and track_err = x - xm, proj_active,
max_abs_track, max_abs_pred, max_abs_sigma, tail_abs_pred and
tail_sigma_drift.
- Check the transient-bound verdict of the tracking error: max_abs_track
must sit below the certified scenario bound E_BOUND = 0.75 (the
guaranteed-transient-response check of the run).
- Read the convergence verdict with convergence_report(res):
converged True when the tail |xt| stays below 1e-4, the tail
sigma_hat drift below 1e-6, and the final sigma_hat within 0.05 of
sigma_ideal(x_final), the ideal cancellation value at the settled
state (assessment target, never an input to the controller).
- Cross-check the marches against the closed forms:
reference_closed_form against the xm history, filter_closed_form
against a constant-input filter march, no_adaptation_closed_form
against a gamma-0 run (the plant goes to the wrong equilibrium x =
6 without adaptation).
- Confirm the deterministic checks: rerun for bitwise-identical
histories, reject every non-physical input with ValueError, and run
the contract test scripts/test_l1_adaptive_control.py.
Worked example
Scenario: a_m = -1.0, b = 2.0, b_m = 1.0, r = 1.0, x0 = 0.0, dt = 0.01
s. The plant coefficient a_p = -0.5 is unknown to the controller (prior
[-1.0, -0.25]: the plant is less damped than the design model assumes)
and an unknown constant matched disturbance d = 1.0 acts on it, so the
estimator must cancel sigma_true(x) = 0.25x + 1.0. Adaptation rate
gamma = 10.0, filter omega_c = 10.0 rad/s (C(s) = 10/(s + 10)),
projection bound sigma_b = 2.0, k_g = 0.5, run for 6000 steps (60 s).
The plant and the reference model both settle at x = 1, sigma_ideal =
1.25, u = -0.75. Real module outputs (stdlib math, exit 0):
- Transient: the plant races ahead of the reference model while
sigma_hat ramps; the tracking error peaks at max |e| = 0.5607716796
at k = 43 (t = 0.43 s, x = 0.9116690512 vs xm = 0.3508973716), the
control swings to its most negative u = -1.4394547881 at k = 82 to
arrest the overshoot, the plant overshoots to x = 1.1374400036 at
k = 200 (xm = 0.8660203251, e = +0.2714196785), and the prediction
error peaks at max |xt| = 0.4359531903 just after t = 0.36 s. The
projection clamps sigma_hat to the sigma_b = 2.0 bound on exactly 7
consecutive steps, k = 67..73, and max |sigma_hat| over the run is
2.0000000000 (the bound value).
- Settling: x stays within 1e-3 of 1 permanently from k = 2211 and the
prediction error stays below 1e-4 permanently from k = 3064. Samples
(k, x, xm, e): k = 100 (0.4904796820, 0.6339676587, -1.435e-01),
k = 300 (0.9913721769, 0.9509591059, +4.041e-02), k = 1000
(0.9818672877, 0.9999568288, -1.809e-02), k = 3000 (0.9998807895,
1.0000000000, -1.192e-04), k = 5000 (0.9999993843, 1.0000000000,
-6.157e-07).
- Final state: x_final = 0.9999999587, xm_final = 1.0000000000 (the
reference settles at 1), e_final = -4.13e-08, xt_final = 4.10e-08,
sigma_hat_final = 1.2499999832 against sigma_true(x_final) =
1.2499999897, nu_final = 1.2500000187, u_final = -0.7500000187 with
the equilibrium residual a_px + bu + bd = -1.68e-08: the control
holds the plant at x = 1 against the disturbance, exactly like u =
-0.75.
- Verdict: converged True on all three criteria: tail (last 1000
steps, t = 50 to 60 s) max |xt| = 5.604e-07 below the 1e-4
threshold, tail sigma_hat drift = 5.604e-08 below 1e-6, and sigma_dev
= |sigma_hat_final - sigma_ideal| = 6.47e-09 below 0.05. Transient
verdict: max |e| = 0.5607716796 below the certified bound E_BOUND =
0.75 (margin 1.34x).
- Guard runs: with gamma = 0 the plant ignores the disturbance estimate
and converges to the wrong equilibrium x = 6.0 (e(6000) = 5.0,
march matches 6*(1 - 0.995^k) to 1e-9); with the slow filter
omega_c = 0.5 rad/s the loop still converges but the transient bound
degrades to max |e| = 1.555849 (2.77x the omega_c = 10 run).
- Adaptation-rate study: max |track error| = 0.5607716796 at
gamma = 10, 0.3349860564 at 40, 0.2049952822 at 160, 0.1595401164 at
1000 (monotone decrease: the faster the estimator, the tighter the
transient bound), with tail |xt| = 5.60e-07, 6.33e-09, 3.33e-11 and
1.00e-12 respectively (every rate converges). Determinism: a second
identical run is bitwise identical in x, sigma_hat and u.
Verification
- Confirm the projection unit behavior: interior directions pass,
inward directions at a bound pass, outward directions at a bound
return 0.0; adaptive_update(1.9, -0.2, 10.0, 0.1) returns exactly
SIGMA_B = 2.0 and adaptive_update(0.0, -0.2, 10.0, 0.1) returns 0.2.
- Confirm the worked scenario: x_final = 0.9999999587, sigma_hat_final
= 1.2499999832, nu_final = 1.2500000187, u_final = -0.7500000187,
final errors below 1e-3, equilibrium residual below 1e-6, proj_active
= 7 with the clamp steps exactly k = 67..73, and max_abs_track =
0.5607716796 below E_BOUND = 0.75.
- Confirm convergence_report returns converged True with the tail
criteria from the verdict above.
- Confirm the closed forms: the xm history matches 1 - 0.99^k within
1e-12, a constant-input filter march matches 1 - 0.9^k within 1e-12,
and a gamma-0 plant march matches 6*(1 - 0.995^k) within 1e-9.
- Confirm the reference-loop identity trace = -10.5, det = 10.0 and the
L1-norm integral of the filtered impulse response equals b/|a_m| =
2.0 within 1e-4, with both admissibility products below 1.
- Confirm every non-physical input raises ValueError: a_m >= 0, b == 0,
dt <= 0, gamma < 0, omega_c <= 0, sigma_b <= 0, steps < 2, non-finite
values across the module.
- Run the contract test offline: python3
scripts/test_l1_adaptive_control.py (34 tests, deterministic).
Pitfalls
- Reading the transient verdict from the wrong metric: the certified
bound E_BOUND = 0.75 applies to max |e| over the whole run
(0.5607716796), not to the tail; the tail metrics feed the separate
convergence verdict, and the two must not be conflated.
- Running too few steps: x settles within 1e-3 of 1 only from about
k = 2211 and the prediction error clears 1e-4 only from about
k = 3065, so a short run reads a premature verdict.
- Zero adaptation: with gamma = 0 the plant converges to the wrong
equilibrium x = 6.0 with tracking error 5.0; the projection-based
adaptation law is what cancels the lumped disturbance.
- Slow filter bandwidth: omega_c sets the achievable transient bound;
at omega_c = 0.5 rad/s max |e| = 1.555849 is 2.77x the omega_c = 10
value, so a very slow filter defeats the transient guarantee.
- Open-loop-unstable mismatch: an a_p >= 0 plant violates the scalar
L1-norm admissibility condition (a_p - a_m)/|a_m| < 1 and the
estimator resonator destabilizes the loop; keep the worked scenario
inside the stable prior.
- sigma_ideal and sigma_true are assessment/simulator values only:
feeding the true lumped uncertainty back into the controller defeats
the adaptive purpose (the controller sees only the plant state and
the prediction error).
- Updating order: the filter must advance on the freshly updated
sigma_hat before the control is formed; the documented Euler ordering
is what drives the discrete prediction-error pair contraction at
|lambda| = 0.9949874 per step.
Related leaves
- gnc-autonomy/control/adaptive-control: the architecture that replaces
the state predictor and the projection law with a gradient update of
two adaptive gains driven by the tracking error of a first-order
plant, the closest sibling in the pack.
- gnc-autonomy/control/control-allocation: distributing the scalar
command across redundant effectors once the loop closes.
- gnc-autonomy/control/state-space-analysis: eigenvalue and
controllability analysis of plants with known parameters, the
fixed-gain counterpart to online adaptation.
- gnc-autonomy/control/observer-design: reconstructing unmeasured
states when the adaptive loop cannot measure its state directly.
- gnc-autonomy/control/python-control-design: scripted fixed-gain loop
design for known plants, the alternative when no online estimation
is needed.
Behavior contract (gate 3)
Run the deterministic contract test (stdlib unittest, offline):
python3 scripts/test_l1_adaptive_control.py
The test covers the worked example contract (finals x_final,
sigma_hat_final, nu_final, u_final and sigma_ideal within 1e-6
relative, final errors below 1e-3, equilibrium residual below 1e-6,
transient metrics max_abs_track = 0.5607716796 below E_BOUND = 0.75
and max_abs_pred = 0.4359531903, proj_active = 7 with clamp steps
k = 67..73, history seeding and lengths), the convergence verdict with
its tail criteria, the settling milestones from k = 2211 and k = 3065,
the reference-model, filter and no-adaptation closed-form identities,
the slow-filter tradeoff at omega_c = 0.5, the adaptation-rate study
over (10, 40, 160, 1000), the L1-norm admissibility integral and the
reference-loop stability identity, the projection unit behavior with
the exact bound anchors, bitwise determinism, module purity (no RNG,
stdlib only) and ValueError rejection of non-physical and non-finite
inputs across the module. All numeric asserts are tolerance based and
pass under both /usr/bin/python3 and the pyenv 3.13 hook interpreter.
Compliance
- Standards referenced, not reproduced: ARP4754A frames development
assurance for the aircraft functions that host adaptive loops; the L1
equations above are standard adaptive control methodology after Cao
and Hovakimyan (SIAM 2010), summary-only per standards-map.yaml,
reference-only: true.
- compliance: STANDARDS-REF, gated: false.
1---2name: l1-adaptive-control3description: Use when you must design and simulate an l1-adaptive-control law for a first-order plant with an unknown coefficient: run the state-predictor from the design model, drive the projection-based-adaptation-law with the prediction error, pass the adaptive signal through the low-pass filter omega_c/(s + omega_c) and form the control as the feedforward minus the filtered estimate. Produces the tracking-error and prediction-error time histories, the sigma_hat and filtered-signal histories, the projection engagement, the convergence verdict and the certified transient-bound check that gate an L1 adaptive control assessment. Trigger: l1-adaptive-control, state-predictor, low-pass-filtered-adaptation, projection-based-adaptation-law, guaranteed-transient-response.4license: Apache-2.05---67# L1 Adaptive Control (gnc-autonomy/control/l1-adaptive-control)89Use when the task is designing and simulating an L1 adaptive control10law, after Cao and Hovakimyan (L1 Adaptive Control Theory, SIAM 2010,11scalar chapter-2 sigma-only specialization), for a first-order plant12whose coefficient a_p is unknown to the controller and that is hit by13an unknown constant matched disturbance. The controller closes the loop14with a state predictor run from the design model, a projection-based15adaptation law on the prediction error that estimates the lumped16matched uncertainty inside a bounded interval, a low-pass filter17C(s) = omega_c/(s + omega_c) on the adaptive signal, and the control u18= k_g*r - nu against a reference model that sets the command response.19This leaf implements the closed loop in pure Python, stdlib only,20deterministic, discrete-time Euler integration, and reports the21tracking-error and prediction-error histories, the sigma_hat and22filtered-signal histories, the projection engagement, the convergence23verdict and the certified transient-bound check. It pairs with24gnc-autonomy/control/adaptive-control (the architecture that replaces25the state predictor with a gradient update of two gains on the tracking26error) and with gnc-autonomy/control/control-allocation for spreading27the scalar command across redundant effectors; the fixed-gain design28leaves of the control pack cover plants whose coefficient is known.29Scope: single input, deterministic, no noise, constant matched30disturbance, sign of the control effectiveness known positive, a_p31unknown inside a bounded prior.3233## Domain quick reference3435- Reference model: xm_dot = a_m*xm + b_m*r with a_m < 0; the model36 settles at b_m*r/(-a_m) = 1 for the unit command. Euler step: xm_next37 = xm + dt*(a_m*xm + b_m*r).38- Truth plant (simulator only): x_dot = a_p*x + b*u + b*d, a_p = -0.539 unknown to the controller, d = 1.0 the unknown constant matched40 disturbance. In the design frame the plant reads x_dot = a_m*x +41 b*(u + sigma_true) with the lumped uncertainty sigma_true(x) = (a_p -42 a_m)*x/b + d = 0.25*x + 1.0.43- State predictor: xh_dot = a_m*xh + b*(u + sigma_hat), run from the44 same initial state as the plant. Prediction error: xt = xh - x.45- Projection-based adaptation law on the prediction error: sigma_hat_dot46 = gamma*Proj(sigma_hat, -xt), projection onto [-sigma_b, sigma_b].47 Discrete clamped Euler step: raw = sigma_hat - dt*gamma*xt, then48 clamp to [-sigma_b, sigma_b], so |sigma_hat| <= sigma_b every step.49- Low-pass filter on the adaptive signal, C(s) = omega_c/(s + omega_c):50 nu_dot = omega_c*(sigma_hat - nu); Euler step nu_next = nu +51 dt*omega_c*(sigma_hat - nu).52- Control law: u = k_g*r - nu with k_g = -a_m/b = 0.5, the unfiltered53 feedforward for unit DC tracking minus the filtered cancellation54 estimate. The equilibrium control is negative: u* = -0.75 holds the55 plant at x = 1 against the disturbance.56- Histories are seeded at index 0 with the initial values and have57 length steps + 1: history[k] is the value at time k*dt (after k58 advances). Per step the loop samples the prediction error, updates59 sigma_hat, advances the filter on the updated estimate, forms the60 control, advances plant, predictor and reference model, then appends.61- L1 admissibility of the scenario: ||C(s)*(s - a_m)^-1*b||_L1 =62 b/|a_m| = 2.0 for every first-order C(s) with unit DC gain, so the63 scalar L1-norm condition with Lipschitz constant L = (a_p - a_m)/b64 reduces to (a_p - a_m)/|a_m| < 1: 0.5 at the worked plant, 0.75 at65 the prior corner a_p = -0.25. An open-loop-unstable mismatch66 violates the condition and the estimator resonator destabilizes the67 loop; the worked scenario keeps a_p < 0.68- Reference-loop stability identity at perfect adaptation: the69 linearized (x, nu) pair has trace a_p - omega_c = -10.5 and70 determinant -a_m*omega_c = 10.0 (stable for every omega_c > 0 when71 a_p < 0). The discrete prediction-error pair contracts at |lambda| =72 sqrt(1 + a_m*dt) = 0.9949874 per step for every adaptation rate.73- ARP4754A frames the development assurance context for the airborne74 function that hosts the adaptive loop; the relations above are75 standard L1 adaptive control methodology, summary-only.7677## Workflow78791. Fix the loop data: design pole a_m (negative), control effectiveness80 b, reference gain b_m, command r, initial state x0, disturbance d,81 step dt, adaptation rate gamma, filter bandwidth omega_c, projection82 bound sigma_b, prior bounds a_lo..a_hi on the unknown plant83 coefficient, and the run length in steps.842. Sanity-check the pieces with single steps before the full run:85 projection boundary behavior and adaptive_update clamping of the86 projection-based-adaptation-law, filter_step (the low-pass filter87 C(s) = omega_c/(s + omega_c)), plant_step, predictor_step,88 reference_step and control_output (u = k_g*r - nu).893. Form the design-frame lumped uncertainty with sigma_true(x)90 (simulator bookkeeping only, never an input to the controller) to91 see what the estimator must cancel, and confirm L1 admissibility:92 the product (a_hi - a_m)/|a_m| below 1 and the reference-loop93 stability signs trace = a_p - omega_c < 0, determinant94 -a_m*omega_c > 0.954. Run the closed loop with simulate(...): read the x, xh, xm state96 histories, the sigma_hat and nu (filtered-signal) histories, the u97 history, pred_err = xh - x and track_err = x - xm, proj_active,98 max_abs_track, max_abs_pred, max_abs_sigma, tail_abs_pred and99 tail_sigma_drift.1005. Check the transient-bound verdict of the tracking error: max_abs_track101 must sit below the certified scenario bound E_BOUND = 0.75 (the102 guaranteed-transient-response check of the run).1036. Read the convergence verdict with convergence_report(res):104 converged True when the tail |xt| stays below 1e-4, the tail105 sigma_hat drift below 1e-6, and the final sigma_hat within 0.05 of106 sigma_ideal(x_final), the ideal cancellation value at the settled107 state (assessment target, never an input to the controller).1087. Cross-check the marches against the closed forms:109 reference_closed_form against the xm history, filter_closed_form110 against a constant-input filter march, no_adaptation_closed_form111 against a gamma-0 run (the plant goes to the wrong equilibrium x =112 6 without adaptation).1138. Confirm the deterministic checks: rerun for bitwise-identical114 histories, reject every non-physical input with ValueError, and run115 the contract test scripts/test_l1_adaptive_control.py.116117## Worked example118119Scenario: a_m = -1.0, b = 2.0, b_m = 1.0, r = 1.0, x0 = 0.0, dt = 0.01120s. The plant coefficient a_p = -0.5 is unknown to the controller (prior121[-1.0, -0.25]: the plant is less damped than the design model assumes)122and an unknown constant matched disturbance d = 1.0 acts on it, so the123estimator must cancel sigma_true(x) = 0.25*x + 1.0. Adaptation rate124gamma = 10.0, filter omega_c = 10.0 rad/s (C(s) = 10/(s + 10)),125projection bound sigma_b = 2.0, k_g = 0.5, run for 6000 steps (60 s).126The plant and the reference model both settle at x = 1, sigma_ideal =1271.25, u* = -0.75. Real module outputs (stdlib math, exit 0):128129- Transient: the plant races ahead of the reference model while130 sigma_hat ramps; the tracking error peaks at max |e| = 0.5607716796131 at k = 43 (t = 0.43 s, x = 0.9116690512 vs xm = 0.3508973716), the132 control swings to its most negative u = -1.4394547881 at k = 82 to133 arrest the overshoot, the plant overshoots to x = 1.1374400036 at134 k = 200 (xm = 0.8660203251, e = +0.2714196785), and the prediction135 error peaks at max |xt| = 0.4359531903 just after t = 0.36 s. The136 projection clamps sigma_hat to the sigma_b = 2.0 bound on exactly 7137 consecutive steps, k = 67..73, and max |sigma_hat| over the run is138 2.0000000000 (the bound value).139- Settling: x stays within 1e-3 of 1 permanently from k = 2211 and the140 prediction error stays below 1e-4 permanently from k = 3064. Samples141 (k, x, xm, e): k = 100 (0.4904796820, 0.6339676587, -1.435e-01),142 k = 300 (0.9913721769, 0.9509591059, +4.041e-02), k = 1000143 (0.9818672877, 0.9999568288, -1.809e-02), k = 3000 (0.9998807895,144 1.0000000000, -1.192e-04), k = 5000 (0.9999993843, 1.0000000000,145 -6.157e-07).146- Final state: x_final = 0.9999999587, xm_final = 1.0000000000 (the147 reference settles at 1), e_final = -4.13e-08, xt_final = 4.10e-08,148 sigma_hat_final = 1.2499999832 against sigma_true(x_final) =149 1.2499999897, nu_final = 1.2500000187, u_final = -0.7500000187 with150 the equilibrium residual a_p*x + b*u + b*d = -1.68e-08: the control151 holds the plant at x = 1 against the disturbance, exactly like u* =152 -0.75.153- Verdict: converged True on all three criteria: tail (last 1000154 steps, t = 50 to 60 s) max |xt| = 5.604e-07 below the 1e-4155 threshold, tail sigma_hat drift = 5.604e-08 below 1e-6, and sigma_dev156 = |sigma_hat_final - sigma_ideal| = 6.47e-09 below 0.05. Transient157 verdict: max |e| = 0.5607716796 below the certified bound E_BOUND =158 0.75 (margin 1.34x).159- Guard runs: with gamma = 0 the plant ignores the disturbance estimate160 and converges to the wrong equilibrium x = 6.0 (e(6000) = 5.0,161 march matches 6*(1 - 0.995^k) to 1e-9); with the slow filter162 omega_c = 0.5 rad/s the loop still converges but the transient bound163 degrades to max |e| = 1.555849 (2.77x the omega_c = 10 run).164- Adaptation-rate study: max |track error| = 0.5607716796 at165 gamma = 10, 0.3349860564 at 40, 0.2049952822 at 160, 0.1595401164 at166 1000 (monotone decrease: the faster the estimator, the tighter the167 transient bound), with tail |xt| = 5.60e-07, 6.33e-09, 3.33e-11 and168 1.00e-12 respectively (every rate converges). Determinism: a second169 identical run is bitwise identical in x, sigma_hat and u.170171## Verification172173- Confirm the projection unit behavior: interior directions pass,174 inward directions at a bound pass, outward directions at a bound175 return 0.0; adaptive_update(1.9, -0.2, 10.0, 0.1) returns exactly176 SIGMA_B = 2.0 and adaptive_update(0.0, -0.2, 10.0, 0.1) returns 0.2.177- Confirm the worked scenario: x_final = 0.9999999587, sigma_hat_final178 = 1.2499999832, nu_final = 1.2500000187, u_final = -0.7500000187,179 final errors below 1e-3, equilibrium residual below 1e-6, proj_active180 = 7 with the clamp steps exactly k = 67..73, and max_abs_track =181 0.5607716796 below E_BOUND = 0.75.182- Confirm convergence_report returns converged True with the tail183 criteria from the verdict above.184- Confirm the closed forms: the xm history matches 1 - 0.99^k within185 1e-12, a constant-input filter march matches 1 - 0.9^k within 1e-12,186 and a gamma-0 plant march matches 6*(1 - 0.995^k) within 1e-9.187- Confirm the reference-loop identity trace = -10.5, det = 10.0 and the188 L1-norm integral of the filtered impulse response equals b/|a_m| =189 2.0 within 1e-4, with both admissibility products below 1.190- Confirm every non-physical input raises ValueError: a_m >= 0, b == 0,191 dt <= 0, gamma < 0, omega_c <= 0, sigma_b <= 0, steps < 2, non-finite192 values across the module.193- Run the contract test offline: python3194 scripts/test_l1_adaptive_control.py (34 tests, deterministic).195196## Pitfalls197198- Reading the transient verdict from the wrong metric: the certified199 bound E_BOUND = 0.75 applies to max |e| over the whole run200 (0.5607716796), not to the tail; the tail metrics feed the separate201 convergence verdict, and the two must not be conflated.202- Running too few steps: x settles within 1e-3 of 1 only from about203 k = 2211 and the prediction error clears 1e-4 only from about204 k = 3065, so a short run reads a premature verdict.205- Zero adaptation: with gamma = 0 the plant converges to the wrong206 equilibrium x = 6.0 with tracking error 5.0; the projection-based207 adaptation law is what cancels the lumped disturbance.208- Slow filter bandwidth: omega_c sets the achievable transient bound;209 at omega_c = 0.5 rad/s max |e| = 1.555849 is 2.77x the omega_c = 10210 value, so a very slow filter defeats the transient guarantee.211- Open-loop-unstable mismatch: an a_p >= 0 plant violates the scalar212 L1-norm admissibility condition (a_p - a_m)/|a_m| < 1 and the213 estimator resonator destabilizes the loop; keep the worked scenario214 inside the stable prior.215- sigma_ideal and sigma_true are assessment/simulator values only:216 feeding the true lumped uncertainty back into the controller defeats217 the adaptive purpose (the controller sees only the plant state and218 the prediction error).219- Updating order: the filter must advance on the freshly updated220 sigma_hat before the control is formed; the documented Euler ordering221 is what drives the discrete prediction-error pair contraction at222 |lambda| = 0.9949874 per step.223224## Related leaves225226- gnc-autonomy/control/adaptive-control: the architecture that replaces227 the state predictor and the projection law with a gradient update of228 two adaptive gains driven by the tracking error of a first-order229 plant, the closest sibling in the pack.230- gnc-autonomy/control/control-allocation: distributing the scalar231 command across redundant effectors once the loop closes.232- gnc-autonomy/control/state-space-analysis: eigenvalue and233 controllability analysis of plants with known parameters, the234 fixed-gain counterpart to online adaptation.235- gnc-autonomy/control/observer-design: reconstructing unmeasured236 states when the adaptive loop cannot measure its state directly.237- gnc-autonomy/control/python-control-design: scripted fixed-gain loop238 design for known plants, the alternative when no online estimation239 is needed.240241## Behavior contract (gate 3)242243Run the deterministic contract test (stdlib unittest, offline):244245 python3 scripts/test_l1_adaptive_control.py246247The test covers the worked example contract (finals x_final,248sigma_hat_final, nu_final, u_final and sigma_ideal within 1e-6249relative, final errors below 1e-3, equilibrium residual below 1e-6,250transient metrics max_abs_track = 0.5607716796 below E_BOUND = 0.75251and max_abs_pred = 0.4359531903, proj_active = 7 with clamp steps252k = 67..73, history seeding and lengths), the convergence verdict with253its tail criteria, the settling milestones from k = 2211 and k = 3065,254the reference-model, filter and no-adaptation closed-form identities,255the slow-filter tradeoff at omega_c = 0.5, the adaptation-rate study256over (10, 40, 160, 1000), the L1-norm admissibility integral and the257reference-loop stability identity, the projection unit behavior with258the exact bound anchors, bitwise determinism, module purity (no RNG,259stdlib only) and ValueError rejection of non-physical and non-finite260inputs across the module. All numeric asserts are tolerance based and261pass under both /usr/bin/python3 and the pyenv 3.13 hook interpreter.262263## Compliance264265- Standards referenced, not reproduced: ARP4754A frames development266 assurance for the aircraft functions that host adaptive loops; the L1267 equations above are standard adaptive control methodology after Cao268 and Hovakimyan (SIAM 2010), summary-only per standards-map.yaml,269 reference-only: true.270- compliance: STANDARDS-REF, gated: false.