Aircraft: N720AK, RV-10
Date: 2026-06-02
Duration: 51.4 min
Log rate: 416 Hz
Rows: 1,282,972
Log integrity: zero drops (drops=0 dbg_drops=0 paused_drops=0 throughout the .dbg)
Algorithm: EKFQ (confirmed by finite values in ekf* columns — under Madgwick these are NaN)
This is the first real-flight test of the six EKFQ-diagnostic states landed in PR #678 (gyro biases bp/bq/br, vertical-accel bias b_az, sideslip β, yaw). The goal was to find out whether the now-exposed bias states are physically meaningful enough to use in a downstream P2-style "bias-corrected gyro rates on the wire" cleanup.
Companion analysis: β vs lateral-G as a slip-skid ball signal — same flight, different question.
| IAS | 0 → 127 kt |
| Palt | 4900 → 9650 ft |
| Pitch | -13.9° → +15.6° |
| Roll | -56.1° → +48.1° |
Regime breakdown:
| Regime | Samples | Duration |
|---|---|---|
| Ground (IAS < 20) | 521,643 | 20.9 min |
| Taxi (5 ≤ IAS < 30) | 490,387 | 19.6 min |
| Climb (IAS ≥ 60, VSI > 300) | 239,554 | 9.6 min |
| Cruise (IAS ≥ 80, |VSI| < 200) | 120,885 | 4.8 min |
| Descend (IAS ≥ 60, VSI < -300) | 262,235 | 10.5 min |
Converges within ~30 s of takeoff to ≈ -0.3 m/s² (≈ -0.03 g). Steady-state cruise: mean -0.44 m/s², std 0.20 m/s². That's a plausible accelerometer bias for a MEMS IMU at flight temperature. No tuning issue. This is the only bias state in the filter that behaves like an actual bias.
Stays within ±2° in coordinated flight, settles around -0.23° in cruise. Spikes visibly during the obvious roll maneuvers (31-35 min). Gated off below TAS = 12 m/s as expected. Looks correct.
Walks through ±180° (atan2 range). Final 60 s average: 107.8° ± 6.9°. There's no magnetometer in OnSpeed, so this is a relative-yaw signal, not compass heading. As designed.
| State | Range across flight | Behavior |
|---|---|---|
bp (roll-rate bias) |
[-0.0005, +0.0004] deg/s | Effectively nailed at the zero-mean prior |
bq (pitch-rate bias) |
[-1.7?, +1.72] deg/s | Spikes ±1 deg/s during turns/maneuvers |
br (yaw-rate bias) |
[-1.1?, +1.14] deg/s | Similar to bq |
Tracing the EKFQ correct step (EKFQ.cpp:548-610):
ax_pred = -2g(q1q3 - q0q2) + tasDot— nobpcouplingay_pred = -2g(q2q3 + q0q1) + tas·r_cwherer_c = yawRate - br— couplesbraz_pred = -g·R22 - tas·q_cwhereq_c = pitchRate - bq— couplesbq
bp is structurally unobservable. Roll rate produces no centripetal accel observable in body ay/az under coordinated flight, so the filter has no information channel to estimate bp. The zero-mean prior is the only thing acting on it. This isn't a bug — it's a consequence of "accelerometer residuals after centripetal compensation are the only update source."
This is the headline finding.
Look at bq in the overview plot. It's near-zero on the ground, then slams ±1 deg/s during turns/pull-ups, then returns toward zero between maneuvers within seconds. That's not bias behavior. Real MEMS gyro bias drift is on a minutes-to-hours timescale.
The cumulative ∫bq dt over the flight is -25° while pitch is centered at 0. If bq were a real gyro bias, attitude would have drifted by 25° relative to truth — but obviously it didn't (the pre/post-flight pitch numbers are within 0.4° of each other).
The likely cause: q_bias = 0.0806 (rad/s)²/s is ~5 orders of magnitude too large for actual gyro bias dynamics.
- Real MEMS gyros: bias drift < 0.1 deg/s per minute →
q_bias≈ 3e-6 (rad/s)²/s would be physically appropriate - This config allows bp/bq/br to drift by
sqrt(0.0806) ≈ 0.28 rad/s ≈ 16 deg/sper second of process noise - The zero-mean prior (
r_bias_prior = 3.6e-4 (rad/s)², std ≈ 1.1 deg/s) pulls back to zero, but the Q is so loose that the filter happily uses bq as a fast attitude-correction lever
This is the Optuna substrate's fault — it was tuned for "minimize cruise-AOA loss vs VN-300 truth" and likely found that a fast-bq let the filter respond faster to pitch maneuvers. But the resulting "bias" doesn't mean anything physical.
| Measurement | Value |
|---|---|
| Pre-takeoff ground pitch (123k samples, IAS ≤ 5.5) | -0.5392° |
| Post-landing ground pitch (3.5k samples, IAS ≤ 5.5) | -0.1067° |
| Net drift | +0.4325° |
| Duration | ~44 min |
| Drift rate | +0.010°/min |
That's half of flight 274's reported +0.019°/min. Either PR #663 (exact-exp quaternion propagation) cut it in half, or this is run-to-run variation, or pre/post taxiway slopes differ subtly. More flights needed to separate signal from noise.
| Finding | What it means | Action |
|---|---|---|
bq/br Q-noise too high — they act as residual sinks, not biases |
Optuna found a perverse local optimum. Filter resolves attitude correctly but the bias states don't carry physical meaning. | Re-tune q_bias against this (and future) flights using a loss that constrains bias-drift bandwidth, not just cruise-AOA loss. |
bp is unobservable |
Roll-rate bias structurally can't be estimated from accel residuals alone in this filter. | Accept it. Could add a slow-mode observability path if it ever matters. |
| Pitch drift ~0.010°/min, halved from flight 274 | PR #663 plausibly helps; or this is run-to-run noise. | Need more flights to separate signal from noise. |
b_az converges cleanly to ~-0.3 m/s² |
The vertical-accel bias is the only bias state that works as intended. | No action — confirms the EKF infrastructure is correct, just q_bias for gyros is wrong. |
| Zero log drops at 416 Hz over 51 min | The lock-free snapshot + universal writer architecture is solid at the experimental log rate. | Confirms PR #647 / #670 / #671 etc. are production-ready. |
- Don't ship P2 (bias-corrected gyro rates on the wire) yet. The
bp/bq/brnumbers aren't physically meaningful. P2 would replace the 30-sample RunningMean withrate − bias, but ifbiasis a fast residual sink rather than a real bias, the corrected rate would be worse than the running mean. Block P2 on q_bias re-tune. b_azIS usable. P3 (subtract fromEarthVerticalGlog column) and the b_az portion of P8 (bias-correctedgOnsetFilterinput) are both fine to proceed with — though as noted earlier they're trivially derivable from existing columns, so probably not worth a firmware PR.- Re-run Optuna substrate with this log (and ideally another) using a loss that includes a
bqsmoothness / bandwidth penalty. Goal: bias states that look bias-shaped, not residual-sink-shaped. Track the trade-off vs cruise-AOA loss. - Collect more 416 Hz logs with EKFQ-diag on before any bias-correction PRs. We need to see whether
bq's bad behavior is reproducible across flights and whether it scales with maneuver intensity.
Looking at EKFQ on actual origin/master (post-PR #663 exact-exp map, post-#678 diagnostics):
- Quaternion integration is the exact exponential map, not first-order Euler.
q ← q ⊗ exp(½ω·dt)with sinc Taylor expansion in the small-angle limit. Unit-preserving by construction. The renormalise after the step is just float-roundoff shedding. Not a source of drift. - The H matrix on the accel-z row has
H[BQ] = +tasand the accel-y row hasH[BR] = -tas, both correct centripetal-comp couplings.H[BP]legitimately doesn't appear in any accel measurement — that's the structural unobservability finding above, not a bug. - The β dynamics gate on
tas > tas_min_mps = 12 m/sis correctly preventing pitot-noise-driven β estimation at low speed.
- Coupling between
bqand quaternion via the predict step's F matrix. The F-row for Q0..Q3 includesqb_q*_b{p,q,r} = ∓half_dt · q*entries (coupling each quaternion component to each gyro bias). At 416 Hzdtis small, so any single-frame coupling is small. But when bq is allowed to swing ±1 deg/s by an overly looseq_bias, that swing feeds back into the quaternion's covariance prediction and may amplify quaternion-bq cross-correlations. Diagnostic to add: plotP[Q0][BQ](or any quat-bq covariance term) over time — if it grows unbounded or oscillates, that's a sign the cross-coupling is excited by the q_bias setting. - The TAS feed to the centripetal terms is
tas_ · compFadeIn_(faded by the IAS-gate rising-edge ramp). When IAS just becomes alive, TAS ramps from 0 to its true value over ~0.5 s. During that ramp, the+tas·q_cterm in az_pred is artificially small, so the filter under-uses bq for that ~0.5 s. Probably fine — it's a brief transient — but worth noting that bq's behavior near IAS-alive transitions may look different from steady-state. - The bq prior is applied alongside the accel measurements in the same batch. The Cholesky update jointly solves for all states given all observations. With
r_bias_prior = 3.6e-4 (rad/s)²(std ≈ 1.1 deg/s), the prior is weak; the accel measurements dominate when their residual is large. Hence: when az-residual is big, bq swings to absorb it; when the maneuver stops, the prior slowly pulls it back. This is the smoking-gun mechanism of "bq behaves like a residual sink not a bias." Not a code bug; a tuning consequence.
Not a bug hunt — more a modeling adequacy question. The bq term as currently structured can absorb any az-residual that the quaternion attitude doesn't already explain. In steady level flight that's noise (and bq stays small). In a sustained pull-up or descent, az ≠ -g·R22, and the residual gets split between attitude correction (the four quat H entries) and a "fake bias" correction (the BQ H entry). The filter has no way to know which is which without longer time-scale evidence — and q_bias being huge tells it "bias can change fast, so use BQ liberally."
A real fix isn't a code change; it's either:
- Add a magnetometer so attitude has an independent reference and the filter no longer needs to use BQ as a slack variable. (Not happening soon — no hardware.)
- Re-tune
q_biasMUCH tighter so BQ can only change on minutes-timescale. Optuna will then have to find another way to fit cruise-AOA — possibly by relaxingr_azor by accepting larger residuals during pull-ups. The trade-off may degrade the AOA estimate during maneuvers. - Add a maneuver-detection gate that disables BQ updates when pitch-rate or load-factor exceeds a threshold. Keeps BQ as a slow-drift estimator; doesn't let it absorb maneuver-driven residuals.
Option 2 is the cheap experiment. Re-running Optuna with bounded q_bias (e.g., 1e-6 to 1e-4 instead of the current 1e-8 to 1e-2 search range) would tell us whether the cruise-AOA degradation is meaningful.
The Optuna substrate (ekfq_pipeline/tune_ekf.py) runs against a recorded log replayed through host_main. If the training log was 208 Hz and this flight is 416 Hz, the per-frame Q is q_bias × dt, halved relative to training. The per-second integrated Q is rate-invariant in principle, but the per-frame interplay between predict and the joint Cholesky correct is not — at 2× rate, the filter gets 2× more accel updates per second to slam BQ around. So 416 Hz operation would make BQ swing twice as fast as it did at the training rate, amplifying the existing tuning weakness rather than causing it.
Action: re-tune Optuna at 416 Hz before drawing conclusions about whether the bq residual-sink behavior is reducible to per-frame Q. If the same Optuna run at 416 Hz also lands on q_bias ≈ 0.08, the cause is the objective function (cruise-AOA loss rewarding fast-bq) not the rate. If it lands much smaller, the cause is per-frame Q scaling, and we get a cleaner fix.
Pay attention to:
- Gyro bias panel (3rd row): bp is invisible (stuck near zero); bq and br swing wildly during the in-flight portion (~18-50 min) and return to zero on the ground.
- b_az panel (4th row): starts with a transient spike at takeoff, then settles around -0.3 m/s² for the rest of the flight.
- β panel (5th row): zero on the ground (TAS gate), small ±2° excursions in flight.
- yaw panel (6th row): wraps at ±180° because of atan2. Not a compass — no magnetometer in OnSpeed.
This is the engine-warm-up + taxi phase. IAS < 12 m/s, so β-dynamics are gated off and stay at exactly zero. b_az converges quickly (~30 s) to its steady-state value. Gyro biases are all clamped near zero because there's no flight maneuvering to drive the centripetal terms.
Top panel: pitch (blue, left axis) vs ∫bq dt (red, right axis). The integrated bias would, if bq were a real gyro bias, predict 25° of pitch drift over the flight — but pitch obviously didn't drift 25°. This is the visual evidence that bq is functioning as a fast residual sink, not a slow bias estimator.
Bottom panel: raw bq, ±1.5 deg/s during maneuvering. The spikiness and rapid return to zero between maneuvers is the give-away that this isn't a bias state in the conventional sense — it's a degree of freedom the filter is using to absorb az-residuals.











