Skip to content

feat: flight-event detection v2 + barometer (validated on 53 real flights) - #121

Merged
fetzu merged 4 commits into
mainfrom
feat/flight-event-detection-v2
Aug 2, 2026
Merged

feat: flight-event detection v2 + barometer (validated on 53 real flights)#121
fetzu merged 4 commits into
mainfrom
feat/flight-event-detection-v2

Conversation

@fetzu

@fetzu fetzu commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Flight-event detection v2 + barometer

Implements the redesigned flight-event detector validated against 53 real flights and the club's billing export (analysis: the 2026-08-02 replay study; prototype + scoring harness in CLAUDE/review/flight-events/, not committed). Closes out recommendations R1, R3, R4 and R5 of that study; R2 (post-flight reconciliation) follows in a separate PR.

What

Detector rewrite (FlightEventDetector.swift) — a faithful port of the validated detector_v2.py:

  • States GROUND → CLIMBOUT → AIRBORNE → APPROACH → ROLLOUT replace the old idle/zone/lowApproach/touchdown FSM.
  • Takeoff is a first-class event: ground → acceleration through Vr → climb. The 60 s suppression window anchors at the real liftoff (not the Line-Up tap, which expired during hold-shorts and produced phantom go-arounds on departure — M2), and "has flown" means 300 ft of climb, not 30 kt of takeoff roll.
  • Calibrated altitude: GPS altitude bias measured while parked, sticky fixed-wing-only anchor (heliports and closed strips flapped "AGL" by ±440 ft — M4), bias refreshed from flat ground samples at every confirmed touch.
  • Wheels-on = flat and low, not slow: raw-speed dip below max(Vso−5, 24) OR flat at <15 ft corrected AGL. Median-of-3 raw speed replaces the 15 s mean that hid every quick touch-and-go (M1: v1 turned 25 circuit landings into 1–3 landings + phantom GA-fests).
  • One classification point at climb-away: stopped ≥10 s → full stop (stamped at touchdown), ground evidence → touch-and-go, neither → go-around. Go-arounds detect up to 400 ft/1.5 nm and stamp at the lowest point (D4). Stop-and-gos count as full stops (D1) and are labeled "STOP & GO" in the flight-log detail.
  • End-of-flight flush: a rollout in progress when recording stops is still a landing — 6 of the 53 corpus flights lost their only landing without this.

Barometer (BarometricAltitudeService.swift, decision D3 — in detection now):

  • CMAltimeter relative altitude at ~1 Hz, 3 s median against cabin-pressure transients. Relative only — re-zeroed at every detected ground contact; never used as MSL (weather drift ≈ 28 ft/h).
  • The detector prefers it (when fresh <3 s) for the flat/plateau and descend-climb tests; GPS otherwise. On devices without a barometer (and the simulator) everything runs GPS-only, exactly as validated.
  • GPSPoint gains an optional baroAltitude (raw relative meters) — Codable backward compatible, export additive, Watch/Companion wire contracts untouched.

Block times (EASA FCL.010, measured against 34 corpus flights + the club's entries):

  • Block off backdates to the first moving fix of the movement run (was +7 s median late).
  • Block on = the start of the final stillness run — no longer overwritten with "now" on every stationary sample (was +55 s median, +159 s worst ≈ +1 min of logged block time per flight). One noisy parked sample no longer restarts the clock; two consecutive moving samples do.

Physical timestampsbackDatedStopTime()'s one-minute guess is retired:

  • Confirmed detector events record at their physical time (FS at touchdown, GA at the approach minimum).
  • Manual buttons route through notifyManualEvent first, which now returns the detector's touchdown time when a rollout is in progress, so a LANDED tap while vacating logs the real touchdown.
  • Physically impossible duplicates (two landings <60 s apart) are refused at entry — the corpus contains real double-fire pairs 7–35 s apart.

Misc (R5): detector airport feed filtered to AirportType.fixedWing; invalid GPS speed (−1) is skipped, never read as 0 kt (an invalid fix on final used to be an instant touchdown candidate); unresolved-checklist speeds (Vso/Vr 0) fall back to validated defaults instead of collapsing every threshold.

Validation

The Python harness is the referee; the Swift port is pinned to it:

Check Result
Full corpus, Swift vs Python referee (53 flights) sequence-for-sequence match, timestamps within tolerance (testFullCorpusMatchesPythonReferee, auto-skips off this machine)
Labeled flights (Tier A+C) 19/20 exact (single miss = the known GPS-physical LEAM case)
Club billing export (49 matched flights) 43/49 exact landing counts — residual is the documented 0–30 ft deliberate-low-work band
Takeoff / landing times vs club median +0.9 / −0.1 min
Committed regression suite 17 real-flight fixtures asserting the full event sequence (type + time ±90 s) per flight, plus unit trajectories (baro reclassification, flush, dedupe, block-time backdating)
Full test suite 484+ tests green on iPad Air 11-inch (M4)
Simulator app runs the new stack live (GPS feed → recording → detector → HUD/FREQ verified at LSZQ); barometer inert on sim as designed

Fixtures are downsampled (field-stripped, ~1 m coordinate rounding) from the pilot's own flights; regenerate with CLAUDE/review/flight-events/make_fixtures.py (never committed).

Notes for review

  • The prototype's ROLLOUT Δalt term self-compares (history updates before the state dispatch) and is identically zero — the effective, validated behaviour is "slow AND below 15 ft corrected AGL", and the port preserves exactly that, with a comment. Changing it means re-validating against the harness.
  • EventConfirmationView (the hold-to-confirm UI) is untouched; only the FlightEventConfirmationOverlay modifier in the same file changed, to pass the event's physical timestamp into record(at:) — without it the touchdown-timestamped FS could never reach the logbook.
  • recordFullStop(at:) now also updates landingTime (the latest full stop is the flight's landing time). The final-FS-wins semantics keep the end state identical for normal flights.
  • Known pre-existing issue surfaced during verification (not addressed here, flagged separately): tests that startFlight leak an active-flight checkpoint into the simulator's shared container, so the real app restores a phantom flight after a test run.

🤖 Generated with Claude Code

fetzu and others added 4 commits August 2, 2026 18:39
Rewrite FlightEventDetector as a faithful Swift port of the prototype
validated against 53 real flights (CLAUDE/review/flight-events/detector_v2.py:
19/20 labeled flights exact, 43/49 exact landing counts vs the club's billing
export — v1 scored 14/20 and 0/5 on circuit sessions).

The five v2 rules replace the old idle/zone/lowApproach/touchdown FSM:
- Takeoff is a first-class event (ground → acceleration through Vr → climb),
  anchoring the 60 s suppression window at the real liftoff and defining
  "has flown" as 300 ft of climb, not 30 kt of taxi roll (fixes M2 phantom
  go-arounds on departure).
- GPS altitude bias is calibrated while parked, behind a sticky fixed-wing-only
  anchor (fixes the ±440 ft AGL flapping of M4), refreshed at every touch.
- Wheels-on evidence = raw dip below max(Vso−5, 24) OR flat at <15 ft corrected
  AGL — median-of-3 raw speed, no 15 s mean (fixes M1: circuits turned into
  GA-fests because no T&G ever reached the old 10 kt bar).
- Touches classify once, at climb-away: stopped ≥10 s → full stop (stamped at
  TOUCHDOWN), ground evidence → touch-and-go, neither → go-around.
- Go-around = descended into the <400 ft / 1.5 nm window then climbed 150 ft
  off the minimum, stamped at the lowest point (decision D4; fixes M3 —
  pattern-altitude go-arounds were invisible above 100 ft).

Plus: end-of-flight flush (a rollout in progress when recording stops is still
a landing — 6 of 53 corpus flights lost their only landing without it), invalid
GPS speed (−1) skipped instead of read as 0 kt, manual-event dedupe window,
barometric vertical reference seam (consumed when fresh, GPS otherwise), and an
emittedEvents record for the fixture tests and the reconciliation pass.

Public API kept: pending events, configure(), setTakeoffTime(),
processLocation(), notifyManualEvent() (now returns the physical timestamp),
reset(), and the PR-34 clock seam. Stop-and-gos are full stops (decision D1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…time backdating

Barometer (decision D3 — in detection now, not record-only):
- BarometricAltitudeService wraps CMAltimeter: RELATIVE altitude only (weather
  drift ≈28 ft/h makes absolute pressure altitude meaningless), ~1 Hz, 3 s
  median against cabin-pressure transients. Inert without a barometer/simulator.
- The detector re-zeroes the baro reference at every detected ground contact
  and prefers it (when fresh <3 s) for the flat/plateau and descend-climb
  tests — ±1 ft @1 Hz vs GPS's ±10–30 ft at 5–12 s.
- GPSPoint gains an optional baroAltitude (raw relative meters) — Codable stays
  backward compatible, flight JSON export is additive, Watch/Companion wire
  contracts untouched.

Physical timestamps (retires backDatedStopTime's 1-minute guess):
- Confirmed detector events record at their PHYSICAL time: full stops at
  touchdown, go-arounds at the approach's lowest point (D4).
- Manual buttons route through notifyManualEvent first, which returns the
  detector's touchdown time when a rollout is in progress — a LANDED tap while
  vacating now logs the real touchdown.
- recordGoAround/TouchAndGo/FullStop/Landing take `at:` and refuse physically
  impossible duplicates (<60 s apart — the corpus contains real double-fire
  pairs 7–35 s apart).

Block times (EASA FCL.010, measured on 34 corpus flights):
- Block off backdates to the first moving fix of the movement run (was +7 s
  median late).
- Block on = the START of the final stillness run — no longer overwritten with
  "now" on every stationary sample (was +55 s median / +159 s worst ≈ +1 min
  of logged block time per flight). Two consecutive moving samples break a
  run; one noisy parked sample does not.

Also: the detector's airport feed is fixed-wing filtered (M4), the end-of-flight
flush is applied on stopTracking() before the flight is finalized, and the
flight log labels non-final full stops "STOP & GO" (D1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
17 real flights committed as downsampled fixtures (stripped to detection-
relevant fields, coordinates rounded to ~1 m): the 10 pilot-labeled flights,
the strong-wind circuit sessions, and the ground-effect / slow-flight
ambiguity-band cases (Vol 2.3, Vol 4.1, 20260519). Each fixture carries the
airports near its track and the exact event sequence the authoritative Python
detector produced on that data (regenerate with
CLAUDE/review/flight-events/make_fixtures.py).

Tests assert the full event SEQUENCE per flight — types in order, timestamps
within ±90 s — not just counts, plus the detected takeoff times. A dev-machine
referee test replays all 53 corpus flights against the Python expectations
(auto-skips where the local corpus is absent); it passes sequence-for-sequence,
which transitively pins the validated scores: 19/20 labeled flights and 43/49
exact landing counts vs the club's billing export.

Unit-level scripted trajectories cover: FS stamped at touchdown, end-of-flight
flush, baro flat-run reclassification (GA→TG when the barometer shows ground
contact GPS bias hides), manual-event dedupe, the physical-timestamp return of
notifyManualEvent, aborted-takeoff non-events, and block off/on backdating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… unresolved

An unresolved checklist (no speeds, stall 0) collapsed every threshold — with
Vr = 0, taxiing at 6 kt reads as a takeoff roll. Surfaced live on the simulator
when a restored flight carried an unresolved checklist; the old detector had the
same collapse (touchdown < 5 kt), now guarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fetzu fetzu self-assigned this Aug 2, 2026
@fetzu fetzu added the enhancement New feature or request label Aug 2, 2026
@fetzu fetzu added this to the v4.4.0 milestone Aug 2, 2026
@fetzu

fetzu commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Verification summary (what/how/why is in the PR body):

  • Referee handshake: the Swift port reproduces the Python detector_v2.py event sequences on all 53 corpus flights (testFullCorpusMatchesPythonReferee), which transitively pins the validated scores — 19/20 labeled flights, 43/49 exact landing counts vs the club billing export, takeoff median +0.9 min. No thresholds were tuned in the port; the one behavioural addition beyond the prototype is the unresolved-checklist speed fallback (last commit), which the referee corpus cannot exercise (all corpus flights have real speeds).
  • Suite: 491 tests green on iPad Air 11-inch (M4), including the 17 committed real-flight fixtures asserting full event sequences ±90 s.
  • Simulator: app runs the new stack live — GPS feed → recording → fixed-wing airport query → detector → HUD/FREQ verified at LSZQ; BarometricAltitudeService inert on the simulator as designed; END FLIGHT exercises the flush path cleanly.
  • Found in passing (pre-existing, flagged as a separate task, not fixed here): tests that call startFlight leak an active-flight checkpoint into the simulator container, so launching the real app after a test run restores a phantom flight.

Reconciliation (R2) follows in #122, stacked on this branch.

@fetzu
fetzu merged commit 76336a5 into main Aug 2, 2026
1 check passed
@fetzu
fetzu deleted the feat/flight-event-detection-v2 branch August 2, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant