Adaptive Incremental Dynamic Inversion (AIDI)¶
AIDI is a fault-tolerant flight controller built on Incremental Nonlinear Dynamic Inversion. It adapts the control-effectiveness matrix online via a per-row VFF-RLS that estimates a multiplicative scaling \(\Theta\) over a known onboard \(G_{\text{nominal}}\). The result is model-agnostic and recovers tracking quickly when a control surface loses authority. See also the nonlinear F-16 angular model: NonlinearAngularF16.
Reference: Ul Haq, Atmaca & van Kampen, "Adaptive Incremental Dynamic Inversion for Fault-tolerant Flight Control of a Flying Wing", AIAA SciTech 2026, 10.2514/6.2026-1744.
Key ideas¶
- INDI inner law: \(\Delta u = \tilde{G}^{+} \cdot (\nu_{\text{des}} - \dot{\omega}{\text{meas}})\), where \(\tilde{G} = \Theta \odot G\). Only the linearised onboard CE is needed; the rest is absorbed by \(\Theta\).}
- Information-content VFF: \(\lambda_i = 1 - (1 - \phi_i^{\top} K_i)\, \varepsilon_i^2 / \Sigma_0\) with \(\Sigma_0 = \sigma_0^2 N_0\). Per-paper Eq. 26-27.
- Cross-axis consistency check: column-mean averaging across rows when per-row updates agree, useful when surfaces are redundantly mapped to the same axes (Flying-V style). Default
consistency_threshold = 10⇒ effectively off; tighten only on truly redundant plants. - Pseudo-control hedging (PCH): the gap \(\nu_{\text{des}} - \dot{\omega}_{\text{meas}}\) is fed back to the reference models so they freeze under saturation.
- Onboard CE protocol: \(G_{\text{nominal}}(x, u)\) is queried each tick from an
OnboardCEModelinstance (F16NonlinearOnboardCEfor the F-16,LinearOnboardCE(B)for any plant with a known linearisation).
Architecture¶
┌────────────────────┐
C*_cmd, φ_cmd, │ Outer-loop blocks │
β_cmd, V_cmd ───► │ (C*, roll, β, │
│ speed, linear) │
└────────┬───────────┘
│ ω_des
PCH ◄── ω̇_meas ─┐ ▼
│ ┌──────────────────┐
│ │ Linear controller │ ν
│ └──────┬───────────┘
│ ▼
│ ┌──────────────────┐ G_nominal(x, u)
│ │ Inner AIDI law │ ◄── OnboardCEModel
│ │ Δu = G̃⁺·(ν−ω̇) │
│ └──────┬───────────┘
│ ▼ Δu
│ ┌──────────────────┐
│ │ Rate / mag clamp │
│ └──────┬───────────┘
│ ▼ u
│ env.step
│ ▼ ω
│ ┌──────────────────┐
└─◄ │ ω̇ from LP-deriv │
└──────┬───────────┘
▼
┌──────────────────┐
│ ScalingRLS: │
│ Θ ← Θ + ΔΘ │
│ info-content VFF │
│ consistency-chk │
└──────────────────┘
Components¶
| Component | Role | Implementation |
|---|---|---|
ScalingRLS |
Per-row VFF-RLS over Θ; observability mask + covariance trace bound | tensoraerospace.agent.aidi.ScalingRLS |
OnboardCEModel |
Protocol returning \(G_{\text{nominal}}(x, u)\) | tensoraerospace.agent.aidi.OnboardCEModel |
LinearOnboardCE |
Constant-matrix CE | tensoraerospace.agent.aidi.LinearOnboardCE |
F16NonlinearOnboardCE |
FD adapter over the F-16 angular ODE; remaps (wx, wy, wz) to (p, q, r) |
tensoraerospace.agent.aidi.F16NonlinearOnboardCE |
MoorePenroseAllocator |
Pseudo-inverse with conditioning guard | tensoraerospace.agent.aidi.MoorePenroseAllocator |
PseudoControlHedge |
Hedge signal with per-axis freeze counter | tensoraerospace.agent.aidi.PseudoControlHedge |
CStarController, RollReferenceModel, SideslipCompensator, SpeedController, LinearController |
Outer-loop blocks | tensoraerospace.agent.aidi.ref_models |
AIDIAgent / AIDIConfig |
Orchestrator + persistence | tensoraerospace.agent.aidi.AIDIAgent |
Quick start (F-16)¶
import math, numpy as np
from tensoraerospace.agent.aidi import AIDIAgent, AIDIConfig, F16NonlinearOnboardCE
from tensoraerospace.aerospacemodel.f16.nonlinear.angular.params import default_parameters
agent = AIDIAgent(
n_state=3, n_control=3,
onboard_ce=F16NonlinearOnboardCE(default_parameters(), perturb=1e-3),
config=AIDIConfig(dt=0.01, seed=0),
)
# obs['omega'] in (p, q, r) — F-16 env stores wy=r and wz=q, so re-order:
# omega = (obs[2], obs[4], obs[3])
obs = {"omega": np.zeros(3), "alpha": 0.05, "beta": 0.0,
"theta": 0.0, "phi": 0.0, "V": 200.0, "state": np.zeros(14)}
ref = {"C_star": 1.0, "phi_cmd": 0.0, "beta_cmd": 0.0, "V_cmd": 200.0}
u_rad = agent.predict(obs, references=ref, time_step=0)
# env.step(np.rad2deg(u_rad)) → next_obs
metrics = agent.learn(next_obs, references=ref, time_step=0)
The agent keeps the same save/load/Hugging-Face round-trip API as aa_indi/et_dhp/im_gdhp.
Worked example¶
example/reinforcement_learning/incremental_adp/example_aidi_damage_f16.ipynb — a full fault-recovery walkthrough on the nonlinear F-16: trim, baseline, 25 % stab efficiency loss at t = 5 s, side-by-side adaptive vs frozen-Θ runs.
Benchmark CLI¶
python -m tensoraerospace.scripts.benchmark_aidi \
--env f16_nonlinear_angular \
--baselines frozen \
--scenarios nominal,stab_50,stab_25,stab_lost,rudder_lost \
--episodes 5 --steps 1500 \
--out report.md --csv report.csv
Produces a Markdown table + CSV of per-axis RMSE — Table 8 of the paper, but on the F-16.
Hyperparameters¶
Inner-loop / actuator bounds¶
| Parameter | Default | Description |
|---|---|---|
dt |
0.01 | Control step (s) |
u_magnitude_limit |
radians(25) |
Magnitude clamp (same units as OnboardCEModel's u) |
u_rate_limit |
radians(60) |
Max Δu per second |
pinv_rcond |
1e-6 | Cutoff for np.linalg.pinv(G) |
cond_threshold |
1e12 | Falls back to Δu = 0 when cond(G) exceeds this |
sensor_cutoff_hz |
15.0 | Low-pass cutoff for ω̇ |
Scaling-RLS¶
| Parameter | Default | Description |
|---|---|---|
rls_lambda_min |
0.7 | Forgetting-factor lower bound (fast adaptation) |
rls_lambda_max |
0.999 | Forgetting-factor upper bound (noise rejection) |
rls_sigma0 |
1e-3 | Sensor-noise std σ₀ used in Σ₀ = σ₀²·N₀ |
rls_memory_length |
100 | Nominal memory length N₀ (samples) |
rls_cov_init |
1.0 | Initial scale of P_i |
rls_consistency_threshold |
10.0 | Cross-axis consistency check (≤ 1e-6 for redundant plants) |
PCH¶
| Parameter | Default | Description |
|---|---|---|
pch_freeze_after |
30 | Saturation ticks before reference rate is hard-frozen |
pch_gap_tol |
1e-3 | |ν_h| below which the axis is considered tracked |
Outer loop¶
| Parameter | Default | Description |
|---|---|---|
cstar_kp / cstar_ki |
1.5 / 0.5 | C\* PI gains |
cstar_V_co |
122.6 | C\* crossover speed (m/s) |
roll_omega_n / roll_zeta |
2.5 / 0.7 | Roll reference 2nd-order |
sideslip_kp / sideslip_ki |
1.5 / 0.1 | Sideslip PI |
speed_*, speed_enabled |
0 / False | Auto-throttle (off by default) |
Supported environments¶
- Any Gymnasium env that exposes \((p, q, r)\) plus \(\alpha, \beta, \theta, \phi, V\). Optional
n_zis reconstructed from \((\alpha, \dot{\alpha}, q, V, \theta, \phi)\) when missing. - F-16 nonlinear angular env wired through
F16NonlinearOnboardCE(axis remap built in). - Any plant with a constant linearised CE: pass
LinearOnboardCE(B).
Persistence¶
run_dir = agent.save("./checkpoints") # creates <date>_AIDIAgent/
restored = AIDIAgent.from_pretrained(run_dir, onboard_ce=F16NonlinearOnboardCE(...))
agent.publish_to_hub("me/my-aidi", folder_path=run_dir, access_token="hf_...")
Saved artefacts:
config.json— fullAIDIConfig+n_state/n_control.scaling_rls.npz—theta,P,last_lambda,last_residual,num_updates.outer_state.npz— C\*/sideslip/speed integrators + roll-ref state.pch_state.npz— hedge, saturation counter, freeze flags.deriv_state.npz— low-pass differentiator state.loop_state.npz—u_prev,omega_prev,omega_dot_cached, last command, lastG_nominal, step counter.
API reference¶
AIDIAgent(n_state, n_control, onboard_ce, config=None)
¶
Adaptive Incremental Dynamic Inversion control agent.
reset()
¶
Clear per-episode rolling state — keeps Θ and P (lifelong adaptation).
AIDIConfig(dt=0.01, u_magnitude_limit=math.radians(25.0), u_rate_limit=math.radians(60.0), pinv_rcond=1e-06, cond_threshold=1000000000000.0, sensor_cutoff_hz=15.0, rls_lambda_min=0.7, rls_lambda_max=0.999, rls_sigma0=0.001, rls_memory_length=100, rls_cov_init=1.0, rls_consistency_threshold=10.0, pch_freeze_after=30, pch_gap_tol=0.001, cstar_kp=1.5, cstar_ki=0.5, cstar_V_co=122.6, cstar_i_clip=5.0, roll_omega_n=2.5, roll_zeta=0.7, sideslip_kp=1.5, sideslip_ki=0.1, sideslip_i_clip=5.0, speed_kp=0.0, speed_ki=0.0, speed_kd=0.0, speed_enabled=False, rate_kp=(0.0, 0.0, 0.0), seed=None, history=dict())
dataclass
¶
Hyper-parameters for :class:AIDIAgent.
ScalingRLS(n_y, n_u, lambda_min=0.7, lambda_max=0.999, sigma0=0.001, memory_length=100, cov_init=1.0, consistency_threshold=10.0, observability_floor=1e-08, cov_trace_bound=None, seed=None)
¶
Recursive identifier of the multiplicative scaling matrix Θ.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_y
|
int
|
Number of rate axes (rows of Θ). |
required |
n_u
|
int
|
Number of control surfaces (columns of Θ). |
required |
lambda_min
|
float
|
Lower bound on the variable forgetting factor — the estimator falls toward this value when the residual is large (fast adaptation during faults). |
0.7
|
lambda_max
|
float
|
Upper bound on the variable forgetting factor — the estimator returns toward this value during quiescent operation. |
0.999
|
sigma0
|
float
|
Sensor-noise standard deviation σ₀ used in the information-content VFF (Eq. 27 of the paper, Σ₀ = σ₀²·N₀). |
0.001
|
memory_length
|
int
|
Nominal memory length N₀ in samples. |
100
|
cov_init
|
float
|
Initial scale of the per-row covariance matrices. |
1.0
|
consistency_threshold
|
float
|
Per-paper relative threshold for the cross-axis consistency check; updates that deviate by more than this from the column mean are replaced by the mean. |
10.0
|
seed
|
int | None
|
Reserved for future stochastic variants — currently unused. |
None
|
sigma_total
property
¶
Information-content denominator Σ₀ = σ₀²·N₀.
update(du, domega, G_nominal)
¶
Run one RLS step using (Δu, Δω̇, G_nominal).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
du
|
ndarray
|
Control increment, shape |
required |
domega
|
ndarray
|
Angular-rate-derivative increment, shape |
required |
G_nominal
|
ndarray
|
Onboard CE matrix at the linearisation point, shape
|
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The pre-update residual ε of shape |
OnboardCEModel
¶
Bases: Protocol
Duck-typed onboard CE provider.
F16NonlinearOnboardCE(params=None, perturb=0.001)
¶
Finite-difference adapter over the F-16 6-DoF angular ODE.
The F-16 angular ODE applies aero moments through the actuator
positions held in the state vector (indices 8 = stab, 10 = ail,
12 = dir); the control input u only feeds the second-order
actuator dynamics. INDI's control-effectiveness is therefore the
gain from actuator deflection to angular acceleration, with
time-scale separation handing the actuator dynamics over to the
inner loop's increment law (Δu ≡ Δ(deflection) on the airframe
time-scale).
Axis-ordering note: this F-16 codebase stores the body rates in the
order (wx, wy, wz) = (p, r, q) — i.e. wy is yaw rate and
wz is pitch rate. The adapter remaps so the returned matrix
rows correspond to the conventional (p, q, r) order expected by
the AIDI outer loop (CStar/roll/sideslip).
We compute G_ij = ∂ω̇_i/∂(deflection_j) by central differencing
f16_ode_6dof around the operating point: perturb state[8/10/12]
in turn, read rows 2/4/3 of the ODE output (= p, q, r). The
returned matrix is in the basis (p, q, r) × (stab, ail, dir).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
params
|
'F16AngularParameters | None'
|
F-16 parameter set (defaults to
:func: |
None
|
perturb
|
float
|
Half-width of the central-difference perturbation (radians). |
0.001
|
MoorePenroseAllocator(rcond=1e-08, cond_threshold=100000000.0)
¶
Minimum-norm control allocation via :func:numpy.linalg.pinv.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rcond
|
float
|
Cut-off for small singular values, passed to
:func: |
1e-08
|
cond_threshold
|
float
|
When |
100000000.0
|
allocate(G_eff, nu_des, omega_dot_meas)
¶
Compute Δu = G⁺ · (ν_des − ω̇_meas).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
G_eff
|
ndarray
|
Scaled control-effectiveness matrix |
required |
nu_des
|
ndarray
|
Virtual control vector, shape |
required |
omega_dot_meas
|
ndarray
|
Measured angular acceleration, shape |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Control increment |
ndarray
|
|
PseudoControlHedge(n_y, freeze_after=20, gap_tol=1e-06)
¶
PCH state machine, one entry per rate axis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_y
|
int
|
Number of rate axes. |
required |
freeze_after
|
int
|
Number of consecutive saturated ticks before the corresponding reference rate is hard-frozen. |
20
|
gap_tol
|
float
|
Magnitude of |
1e-06
|
update(nu_des_prev, omega_dot_meas)
¶
Compute hedge and update the freeze counters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nu_des_prev
|
ndarray
|
Virtual control demanded on the previous tick. |
required |
omega_dot_meas
|
ndarray
|
Measured angular acceleration this tick. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Hedge vector |
Sources¶
- Ul Haq, Atmaca, van Kampen. "Adaptive Incremental Dynamic Inversion for Fault-tolerant Flight Control of a Flying Wing", AIAA SciTech 2026, 10.2514/6.2026-1744.
- Atmaca, van Kampen. "Fault Tolerant Control for the Flying-V Using Adaptive Incremental Nonlinear Dynamic Inversion", AIAA SciTech 2025, 10.2514/6.2025-0081.
- Fortescue, Kershenbaum, Ydstie. "Implementation of Self-Tuning Regulators with Variable Forgetting Factors", Automatica, 1981.