Active-Adaptive Incremental Nonlinear Dynamic Inversion (AA-INDI)¶
AA-INDI is a fault-tolerant flight controller built on top of Incremental Nonlinear Dynamic Inversion (INDI). It combines a classical INDI control law with online Variable-Forgetting-Factor RLS identification of the control-effectiveness matrix so that the controller adapts quickly to actuator faults, and a lightweight sensor-filter surrogate that mimics the OTSEKF-HOSM branch of the reference paper. See also the nonlinear F-16 model: NonlinearLongitudinalF16.
Reference: Sun et al., "Active Incremental Nonlinear Dynamic Inversion for Sensor and Actuator Fault Diagnosis and Fault-Tolerant Flight Control", TU Delft Aerospace, research.tudelft.nl.
Key ideas¶
- INDI control law: the applied control increment \(\Delta u = G^+ \cdot (\nu_{\text{des}} - \dot{\omega}_{\text{meas}})\) requires only the control-effectiveness matrix \(G\), not the full nonlinear dynamics \(f\). This eliminates model-uncertainty sensitivity.
- Reference model: a second-order filter shapes the commanded angular rate into a smooth desired rate and its derivative \(\nu_{\text{des}} = \dot{\omega}_{\text{ref}}\).
- VFF-RLS: the forgetting factor \(\lambda_k\) contracts toward a lower bound when the prediction residual grows (fast adaptation during faults/manoeuvres) and relaxes toward the upper bound in quiet operation (noise rejection).
- Sensor-filter surrogate: a low-pass differentiator produces \(\dot{\omega}\) from raw \(\omega\), and a residual-based bias estimator yields a coarse IMU bias that the agent subtracts from measurements — a minimal stand-in for the paper's OTSEKF-HOSM stack.
Differences from related methods¶
| Aspect | INDI | Adaptive INDI | AA-INDI |
|---|---|---|---|
| Control-effectiveness \(G\) | Offline / fixed | Online (basic RLS) | Online VFF-RLS |
| Sensor fault handling | None | None | Bias estimator (OTSEKF-HOSM surrogate) |
| Reaction to abrupt faults | Poor | Moderate | Fast (λ contracts under large residuals) |
| Noise rejection in nominal flight | Good | Moderate | Good (λ relaxes to max) |
AA-INDI components¶
| Component | Role | Implementation |
|---|---|---|
| VFFRLSEstimator | Online identification of \(G = \partial \dot{\omega}/\partial u\) with variable forgetting | tensoraerospace.agent.aa_indi.VFFRLSEstimator |
| LowPassDerivative | Causal differentiator (HOSM surrogate) | tensoraerospace.agent.aa_indi.LowPassDerivative |
| BiasEstimator | Exponential-forgetting IMU-bias estimator | tensoraerospace.agent.aa_indi.BiasEstimator |
| Reference model | 2nd-order filter for \(\nu_{\text{des}}\) | Inline in AAINDIAgent |
| AAINDIAgent | Orchestrates INDI law, estimators, filter | tensoraerospace.agent.aa_indi.AAINDIAgent |
Algorithm¶
On each control tick \(k\), given the measurement \(\omega_k\) and command \(r_k\):
- Measurement conditioning. Subtract the current bias estimate (if enabled): \(\omega_k^c = \omega_k - \hat{b}\). The low-pass differentiator yields \(\dot{\omega}_k^{\text{meas}}\) (advanced inside
learn()to avoid double-stepping). - Reference model. Second-order filter:
- INDI law.
with \(\Delta u\) first rate-limited to \(\pm\dot{u}_{\max} \cdot dt\). 4. VFF-RLS update. From \((\Delta u_k, \Delta \dot{\omega}_k)\):
followed by the usual RLS gain / covariance recursion with forgetting factor \(\lambda_k\). 5. Bias update. Exponential moving average of the residual between \(\omega\) and its reintegration from \(\dot{\omega}\).
Quick start¶
import numpy as np
from tensoraerospace.agent.aa_indi import AAINDIAgent, AAINDIConfig
# Onboard model snapshot of the control-effectiveness matrix at design trim.
G_init = np.array([[-2.0, 0.1, 0.0],
[0.05, -1.5, 0.2],
[0.0, 0.05, -0.9]])
cfg = AAINDIConfig(
dt=0.01,
ref_wn=5.0,
ref_zeta=0.7,
u_magnitude_limit=25.0,
u_rate_limit=200.0,
vff_forgetting_min=0.9,
vff_forgetting_max=0.999,
vff_eps_sensitivity=2.0,
sensor_cutoff_hz=50.0,
enable_bias_correction=True,
G_init=G_init,
seed=0,
)
agent = AAINDIAgent(n_state=3, n_control=3, config=cfg)
omega = np.zeros(3)
ref = np.array([0.2, -0.1, 0.05]) # rad/s targets for roll/pitch/yaw rates
for k in range(500):
u = agent.predict(omega, ref, k)
# Plant step (placeholder — plug your environment here)
omega = omega + cfg.dt * (G_init @ u)
metrics = agent.learn(omega, ref, k)
Warm-start G_init matters
INDI needs a reasonable \(G\) on the first few ticks — with the default random init, the pseudo-inverse explodes and the actuator saturates before VFF-RLS has converged. Provide G_init from a linearised on-board model.
Hyperparameters¶
Reference model¶
| Parameter | Default | Description |
|---|---|---|
ref_wn |
10.0 | Natural frequency of the reference filter (rad/s). Higher → faster tracking, larger Δu. |
ref_zeta |
0.7 | Damping ratio. 0.7 gives a critically-damped-ish response. |
Actuator bounds¶
| Parameter | Default | Description |
|---|---|---|
dt |
0.01 | Control step (s) |
u_magnitude_limit |
25.0 | Hard magnitude clamp per channel (same units as env action) |
u_rate_limit |
60.0 | Max Δu per second per channel |
pinv_rcond |
1e-6 | Cutoff for np.linalg.pinv(G) |
G_init |
None | Warm-start of shape (n_state, n_control) |
VFF-RLS¶
| Parameter | Default | Description |
|---|---|---|
vff_forgetting_min |
0.7 | Lower bound on λ — fast-adaptation regime |
vff_forgetting_max |
0.999 | Upper bound on λ — noise-rejection regime |
vff_eps_sensitivity |
1.0 | Residual norm at which λ drops ~1/e |
vff_cov_init |
1e2 | Initial covariance scale |
Sensor filter¶
| Parameter | Default | Description |
|---|---|---|
sensor_cutoff_hz |
10.0 | Low-pass cutoff of the differentiator |
bias_forgetting |
0.99 | EMA retention of the bias estimator |
enable_bias_correction |
True | Subtract bias estimate from ω before forming the INDI residual |
Supported environments¶
- Any Gymnasium env whose observation vector contains measurable angular rates (e.g.
[alpha, wz]inNonlinearLongitudinalF16-v0after light shaping, or a full[p, q, r]vector from a 6-DoF plant).
Persistence¶
Same API as the other adaptive-critic agents:
run_dir = agent.save("./checkpoints") # creates <date>_AAINDIAgent/
restored = AAINDIAgent.from_pretrained(run_dir)
agent.publish_to_hub("me/my-aaindi", folder_path=run_dir, access_token="hf_...")
Saved artefacts:
config.json— fullAAINDIConfig+n_state/n_control.vff_rls.npz— RLSθ, covarianceP, last forgetting factorλ, update counter.bias_state.npz— exponential bias estimate.deriv_state.npz— low-pass differentiator state.loop_state.npz— reference-model state, PI integrator, last applied control, cachedω̇. Persisting these means a mid-episode save resumes bit-identically on reload (essential whenref_error_kp/ref_error_kiare non-zero).
API reference¶
AAINDIAgent(n_state, n_control, config=None)
¶
Active-Adaptive INDI control agent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_state
|
int
|
Dimension of the controlled angular state vector |
required |
n_control
|
int
|
Number of control channels |
required |
config
|
AAINDIConfig | None
|
:class: |
None
|
reset()
¶
Clear the per-episode rolling state (keeps learned G estimate).
predict(omega, reference, time_step=0, *, deterministic=True)
¶
Compute the commanded control for the current step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
omega
|
ndarray
|
Measured angular state ω (shape |
required |
reference
|
ndarray
|
Commanded angular rate ω_cmd. Accepted shapes are
|
required |
time_step
|
int
|
Current step index; used to slice a time-varying reference. |
0
|
deterministic
|
bool
|
Unused — kept for API parity with stochastic agents. |
True
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Control command |
learn(next_omega, reference, time_step=0)
¶
Update the online estimators from the newly observed state.
Must be called once per environment step, after :meth:predict
and the corresponding env.step(u) call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
next_omega
|
ndarray
|
Angular state measured at |
required |
reference
|
ndarray
|
Commanded reference (unused at learn time — accepted only for API parity with other agents). |
required |
time_step
|
int
|
Same index passed to :meth: |
0
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
Dict of scalar metrics: prediction residual norm, current |
dict[str, float]
|
forgetting factor, norm of the identified G, and current |
dict[str, float]
|
bias estimate norm. |
get_param_env()
¶
Build a JSON-serialisable config dict for :meth:save.
save(path=None)
¶
Write the agent to a directory.
Files produced
config.json— agent/config metadata.vff_rls.npz— RLS parameter matrixtheta, covarianceP, last forgetting factor, update counter.bias_state.npz— current bias estimate.deriv_state.npz— low-pass differentiator internal state.loop_state.npz— reference-model state, PI integrator, last applied control, cached ω̇. Persisting these means a saved agent resumes bit-identically on reload mid-episode (essential whenref_error_kp/ref_error_kiare non-zero).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Union[str, Path, None]
|
Base directory ( |
None
|
Returns:
| Type | Description |
|---|---|
str
|
Absolute path to the created run directory. |
from_pretrained(repo_name, access_token=None, version=None)
classmethod
¶
Load an agent from a local directory or Hugging Face Hub.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repo_name
|
str
|
Local folder path, or |
required |
access_token
|
Optional[str]
|
Hub access token for private repos. |
None
|
version
|
Optional[str]
|
Hub revision / branch / tag. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
AAINDIAgent |
'AAINDIAgent'
|
Reconstructed agent. |
publish_to_hub(repo_name, folder_path, access_token=None)
¶
Upload a :meth:save directory to the Hugging Face Hub.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repo_name
|
str
|
Target repository id, e.g. |
required |
folder_path
|
Union[str, Path]
|
Local folder produced by :meth: |
required |
access_token
|
Optional[str]
|
Hub access token. |
None
|
AAINDIConfig(dt=0.01, ref_wn=10.0, ref_zeta=0.7, u_magnitude_limit=25.0, u_rate_limit=60.0, vff_forgetting_min=0.7, vff_forgetting_max=0.999, vff_eps_sensitivity=1.0, vff_cov_init=100.0, sensor_cutoff_hz=10.0, bias_forgetting=0.99, enable_bias_correction=True, pinv_rcond=1e-06, G_init=None, ref_error_kp=0.0, ref_error_ki=0.0, seed=None, history=dict())
dataclass
¶
Hyper-parameters for :class:AAINDIAgent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dt
|
float
|
Simulation / control step [s]. |
0.01
|
ref_wn
|
float
|
Reference-model natural frequency [rad/s]. Higher values track aggressive reference changes faster at the cost of larger control increments. |
10.0
|
ref_zeta
|
float
|
Reference-model damping ratio. Default |
0.7
|
u_magnitude_limit
|
float
|
Hard magnitude clamp on the control output (per-channel). Matches the actuator envelope of the plant. |
25.0
|
u_rate_limit
|
float
|
Maximum Δu per step (per-channel). Limits how far the incremental law can move the actuator in one control tick. |
60.0
|
vff_forgetting_min
|
float
|
Lower bound on the VFF-RLS forgetting factor. Smaller values react to faults sooner. |
0.7
|
vff_forgetting_max
|
float
|
Upper bound on the VFF-RLS forgetting factor. Larger values tune how aggressively old data is kept for noise rejection. |
0.999
|
vff_eps_sensitivity
|
float
|
Residual norm at which the forgetting
factor has dropped to |
1.0
|
vff_cov_init
|
float
|
Initial covariance scale for the RLS. |
100.0
|
sensor_cutoff_hz
|
float
|
Cut-off of the low-pass differentiator used to produce ω̇_meas from raw ω. |
10.0
|
bias_forgetting
|
float
|
Exponential-forgetting parameter of the bias estimator (closer to 1 → slower, smoother tracking). |
0.99
|
enable_bias_correction
|
bool
|
When True, subtract the estimated IMU bias from the angular-rate measurement before forming the INDI residual. |
True
|
pinv_rcond
|
float
|
Cut-off for the pseudo-inverse of |
1e-06
|
G_init
|
Optional[ndarray]
|
Optional warm-start for the control-effectiveness
matrix, shape |
None
|
ref_error_kp
|
float
|
Proportional feedback gain that injects the
reference-model tracking error |
0.0
|
ref_error_ki
|
float
|
Integral feedback gain on the reference tracking
error. Eliminates residual offset from persistent
modelling error / constant disturbances (e.g. a stuck
trim). Start at |
0.0
|
seed
|
int | None
|
Optional RNG seed. |
None
|
VFFRLSEstimator(n_y, n_u, forgetting_min=0.7, forgetting_max=0.999, eps_sensitivity=1.0, cov_init=100.0, theta_init_scale=0.001, seed=None)
¶
VFF-RLS identifier for the control-effectiveness matrix G.
Internally stores the parameter matrix theta of shape
(n_u, n_y) such that y ≈ θᵀ · φ where φ = Δu (length
n_u) and y = Δω̇ (length n_y). Under that convention
G = θᵀ, which is the usual row-action convention for INDI.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_y
|
int
|
Dimension of the output Δω̇. |
required |
n_u
|
int
|
Dimension of the control increment Δu. |
required |
forgetting_min
|
float
|
Lower bound on λ — reached under strong innovations (fast adaptation). |
0.7
|
forgetting_max
|
float
|
Upper bound on λ — reached under quiescent operation (noise rejection). |
0.999
|
eps_sensitivity
|
float
|
Scale of the residual norm at which λ falls off significantly. Smaller values make the forgetting factor more reactive to transients. |
1.0
|
cov_init
|
float
|
Initial scale of the covariance matrix
|
100.0
|
theta_init_scale
|
float
|
Standard deviation of a zero-mean Gaussian used
to randomly initialise |
0.001
|
seed
|
int | None
|
RNG seed for the initial |
None
|
G
property
¶
Return the control-effectiveness matrix of shape (n_y, n_u).
reset_covariance()
¶
Restore P to its initial large-variance state.
update(du, dy)
¶
Run one VFF-RLS step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
du
|
ndarray
|
Control increment |
required |
dy
|
ndarray
|
Measured output increment |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
The prediction residual |
ndarray
|
update. |
predict(du)
¶
Predict Δω̇ from a candidate control increment Δu.
LowPassDerivative(n, dt, cutoff_hz=10.0)
¶
Causal finite-difference differentiator with a low-pass filter.
Computes ω̇_t from a sequence of ω_t readings using the first-order
backward difference (ω_t − ω_{t-1}) / dt followed by an
exponential filter with cut-off set by cutoff_hz. The filter is
a discrete first-order IIR with α = dt · 2π · cutoff.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Dimension of the input signal. |
required |
dt
|
float
|
Sampling period [s]. |
required |
cutoff_hz
|
float
|
Low-pass cut-off frequency [Hz]. Values in 5–20 Hz are typical for sub-sonic flight envelopes. |
10.0
|
BiasEstimator(n, forgetting=0.99)
¶
Exponential-forgetting mean of an innovation signal.
Used by :mod:aa_indi to produce a scalar IMU bias estimate b̂
from the residual between the raw measurement and the
reintegrated-from-derivative one. When an actual bias appears, the
residual has a non-zero mean and b̂ tracks it with a time
constant of roughly dt / (1 − lambda).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Dimension of the innovation. |
required |
forgetting
|
float
|
Exponential-moving-average retention ( |
0.99
|
update(innovation)
¶
Update the bias estimate with a new innovation sample.
Sources¶
- Sun et al. "Active Incremental Nonlinear Dynamic Inversion for Sensor and Actuator Fault Diagnosis and Fault-Tolerant Flight Control", TU Delft Aerospace, research.tudelft.nl.
- Smeur, Chu, de Croon. "Adaptive Incremental Nonlinear Dynamic Inversion for Attitude Control of Micro Air Vehicles", J. Guid. Control Dyn., 2016.
- Fortescue, Kershenbaum, Ydstie. "Implementation of Self-Tuning Regulators with Variable Forgetting Factors", Automatica, 1981.