feat: flight-event detection v2 + barometer (validated on 53 real flights) - #121
Merged
Conversation
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>
Owner
Author
|
Verification summary (what/how/why is in the PR body):
Reconciliation (R2) follows in #122, stacked on this branch. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 validateddetector_v2.py:Barometer (
BarometricAltitudeService.swift, decision D3 — in detection now):GPSPointgains an optionalbaroAltitude(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):
Physical timestamps —
backDatedStopTime()'s one-minute guess is retired:notifyManualEventfirst, which now returns the detector's touchdown time when a rollout is in progress, so a LANDED tap while vacating logs the real touchdown.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:
testFullCorpusMatchesPythonReferee, auto-skips off this machine)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
Δaltterm 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 theFlightEventConfirmationOverlaymodifier in the same file changed, to pass the event's physical timestamp intorecord(at:)— without it the touchdown-timestamped FS could never reach the logbook.recordFullStop(at:)now also updateslandingTime(the latest full stop is the flight's landing time). The final-FS-wins semantics keep the end state identical for normal flights.startFlightleak 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