Skip to content

Opt-in mag-offset outage pin: hold heading through a GNSS gap with a trusted mag - #64

Merged
georgesleen merged 10 commits into
mainfrom
dev/georgesleen/no-gps-coast-hardening
Jul 26, 2026
Merged

Opt-in mag-offset outage pin: hold heading through a GNSS gap with a trusted mag#64
georgesleen merged 10 commits into
mainfrom
dev/georgesleen/no-gps-coast-hardening

Conversation

@georgesleen

Copy link
Copy Markdown
Collaborator

What this is

An audit of the no-GPS heading coast (drift when GNSS lock is lost) plus one
opt-in hardening lever, validated in the sim. Requested with ocean-imu as a
reference and the option to swap the filter; see the verdict below on why no
swap. Nothing changes by default: the new knob is unset out of the box, so
shipped behaviour is identical (host 67, sim 190, pico build all green).

Audit findings

  1. The filter already fails safe. The rudder only steers on heading while
    heading_valid (sigma <= 5 deg, rudder_task.cpp). In sim a GNSS outage
    pushes heading sigma past 5 deg within ~16 s, mag on or off, so the rudder
    stops steering on fused heading ~16 s into any outage. The large errors that
    build over a long outage happen entirely after heading is already flagged
    invalid; max error while still valid is < 1 deg. There is no "steering on a
    silently-wrong heading" hazard in sim.

  2. As tuned, the mag does not extend usable coast. t_valid is ~16 s for every
    case (clean/moderate/indoor, mag-on/off). The loose q_offset (1.0/step =
    100 deg^2/s) that correctly absorbs mag wander also floats the offset so fast
    that even a perfect mag cannot pin heading during an outage. The mag's benefit
    (re-bounding error to ~60 deg vs ~180 at 20 min) lands only after heading is
    long since invalid.

  3. So the real lever is usable coast time, and it is parameterized exactly by
    the unknown: how good the boat's mag is.

ocean-imu verdict (no swap)

No filter to swap in. It is Eigen, 18-21 state, wave-aware Kalman targeting
ESP32-S3; its roll/pitch/heave work is redundant with the MTi-3's onboard marine
AHRS (we consume its fused quaternion), its QMEKF was already declined, and an
18-21 state Eigen filter at 100 Hz is not viable on the RP2040 (M0+, no FPU,
already stack-tight). Its heading-without-GPS still rests on a magnetometer, same
as our mag_offset path, so no architecture beats the physics: no GPS and no
trusted mag means heading drifts with gyro bias. The one transferable idea,
regime-aware measurement trust, is applied below.

The change: opt-in outage offset-pin

New optional mti_yaw.q_offset_outage_deg2. Once GNSS has been absent past
MAG_OUTAGE_GRACE (3 s), the mag-offset random walk switches to this value.
Pinning it small freezes the offset at its last GNSS-anchored value, so the mag
observes heading directly and holds it. Unset (default) keeps q_offset_deg2
and today's fail-safe behaviour.

Before vs after, static heading, 0.2 deg/s turn-on Z bias, 300 s outage:

   mag       variant   peak  drift_dps final_sig valid_err t_valid
   clean    baseline  13.06   0.0452    54.44     0.54      16
   clean    pin1e-4    0.87   0.0006     2.66     0.87     300   held <1 deg, stays valid
   moderate pin1e-4    9.84   0.0053     2.66     9.84     300
   indoor   pin1e-4   13.01   0.0158     2.66    13.01     300   confidently wrong: do NOT enable

Clean mag: heading held < 1 deg for the whole 5 min outage and stays valid, vs
losing heading at 16 s. Dirty mag: the pin makes the filter confidently wrong
(valid_err ~10-13 deg), which is why it is opt-in and must be gated on a
characterized-clean mag. docs/tuning.md explains the tradeoff and how to
characterize the mag.

Tests

  • Host test_fusion (+2): pinned offset holds heading and confidence through a
    clean-mag outage while the loose default decays past the steer-on bound; the
    grace delays the switch.
  • Sim test_outage_hold.py (new): same, end to end through the bound C++ filter.
  • Full suites green; pico firmware builds unchanged (RAM 7.1%).

Recommendation

Before enabling the pin, characterize the boat's magnetometer: log MTi mag-yaw
vs GNSS heading over a straight run and measure the residual after the offset
converges. A steady residual is a clean mag (enable the pin); a wandering one is
not (leave it off, fail safe). Worth also capturing a real long-outage bench run
to check the sim's ~16 s fail-safe against hardware.

Notes

The 3-axis bias, gates, sigma caps, and q_offset were left as settled decisions;
this adds an orthogonal, default-off knob rather than re-tuning them.

georgesleen and others added 10 commits July 23, 2026 23:03
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@georgesleen

Copy link
Copy Markdown
Collaborator Author

Update: grew from the pin into the magnetometer-tuning toolchain

Since the original description, this branch became the full offline loop for
diagnosing the no-GPS coast, driven by the finding that the coast is mag-bound
(soft iron suspected) and the mag is uncharacterized on the boat.

Added:

  • ReplaySource (sim/plrs_sim/replay.py): drives the real C++ filter from a
    captured I/G telemetry log, so a recorded session tunes what ships.
  • GNSS-referenced mag analyzer (sim/plrs_sim/magcal.py): fits mag-yaw vs
    GNSS heading into 0/1/2-per-rev (declination / hard iron / soft iron) and
    prints a plain-language verdict on whether the outage pin is safe.
  • analyze CLI subcommand + guided menu: python -m plrs_sim analyze <capture>, or run with no args for a newcomer-friendly menu.
  • Soft-iron 2/rev term in the sim mag model, so a synthetic capture can
    stand in for the boat before hardware exists.
  • pico_rawlog firmware build: drops the telemetry throttle for a full-rate
    capture (faithful replay needs the IMU at the predict rate).
  • docs/magnetometer.md: the capture-to-decision runbook, including
    no-drive capture options (trailer circles, passive swing at a mooring).

Adversarial Opus review run on the branch: EKF core correct, unset-pin
byte-identical confirmed, FFI consistent. One should-fix actioned (mag fit now
centers on the circular mean so a large constant offset near +-180 cannot split
the least-squares fit) plus a grace-boundary test.

Tests: native 177, sim 206, both pico + pico_rawlog firmware build.

Note: the pin stays default-OFF. The intended path is characterize the mag
(analyzer) -> if clean/calibrated, enable the pin -> replay to confirm.

@georgesleen
georgesleen merged commit 1f008d6 into main Jul 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant