Skip to content

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 OnboardCEModel instance (F16NonlinearOnboardCE for 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_z is 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 — full AIDIConfig + n_state / n_control.
  • scaling_rls.npztheta, 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.npzu_prev, omega_prev, omega_dot_cached, last command, last G_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 (n_u,).

required
domega ndarray

Angular-rate-derivative increment, shape (n_y,).

required
G_nominal ndarray

Onboard CE matrix at the linearisation point, shape (n_y, n_u).

required

Returns:

Type Description
ndarray

The pre-update residual ε of shape (n_y,).

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:default_parameters).

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:numpy.linalg.pinv.

1e-08
cond_threshold float

When cond(G) exceeds this value, allocate returns Δu = 0 and emits a warning instead of inverting.

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 , shape (n_y, n_u).

required
nu_des ndarray

Virtual control vector, shape (n_y,).

required
omega_dot_meas ndarray

Measured angular acceleration, shape (n_y,).

required

Returns:

Type Description
ndarray

Control increment Δu of shape (n_u,). Zero when

ndarray

G_eff is too ill-conditioned to invert.

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 |ν_h| below which the axis is considered tracked (resets the saturation counter).

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 ν_h of shape (n_y,).

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.