# Direct binary-tlog GPS-truth extractor

**Task**: AZ-697_tlog_ground_truth_extractor
**Name**: Direct binary-tlog GPS-truth extractor (replaces data_imu.csv middle-man)
**Description**: New `tlog_ground_truth.py` module that streams `GLOBAL_POSITION_INT` (or falls back to `GPS_RAW_INT`) from a binary ArduPilot tlog into a typed `TlogGroundTruth` DTO. Production helper (not test-only).
**Complexity**: 3 points
**Dependencies**: None
**Component**: replay_input (cross-cutting validation helper)
**Tracker**: AZ-697
**Epic**: AZ-696

## Problem

Cycle-1 AC-3 (≤ 100 m horizontal error for 80 % of ticks) was permanently
`@xfail` partly because the test fed the SUT a tlog synthesized from
`_docs/00_problem/input_data/flight_derkachi/data_imu.csv`, and read
ground truth from the same CSV — comparing the estimator to itself.

A real binary `derkachi.tlog` (5.8 MB ArduPilot tlog, MAVLink v2) was
committed on 2026-05-20. The remaining gap is a direct extractor that
reads `GLOBAL_POSITION_INT` (or `GPS_RAW_INT`) from the binary and
returns a typed DTO suitable for the AC-3 comparison helper.

## Outcome

- A new production module `src/gps_denied_onboard/replay_input/tlog_ground_truth.py`
  exposes `load_tlog_ground_truth(path: Path) -> TlogGroundTruth`.
- The existing AC-3 comparison helpers (`l2_horizontal_m`,
  `match_percentage`) move from `tests/e2e/replay/_helpers.py` into
  `src/gps_denied_onboard/helpers/` so they are production code, not
  test-only.
- The replay-test conftest uses the new extractor when the real tlog is
  present; CSV path remains as a synth-tlog fallback.

## Scope

### Included
- New `TlogGroundTruth` dataclass (frozen + slotted) with per-record
  `ts_ns`, `lat_deg`, `lon_deg`, `alt_m`, `hdg_deg`, `vx_m_s`, `vy_m_s`,
  `vz_m_s` fields.
- `load_tlog_ground_truth(path)` — lazy `pymavlink.mavutil` open
  mirroring `replay_input/auto_sync.py::_open_tlog`.
- Move `l2_horizontal_m` + `match_percentage` from test helpers to
  `src/gps_denied_onboard/helpers/gps_compare.py`.
- Wire `tests/e2e/replay/conftest.py` to consume the new path when
  `derkachi.tlog` exists.
- Unit tests under `tests/unit/replay_input/test_tlog_ground_truth.py`
  using a synthetic tlog (extend `tests/e2e/replay/_tlog_synth.py`).

### Excluded
- Tlog trimming for mid-flight slices — AZ-698 (T2).
- Accuracy report writing — AZ-699 (T3).
- Map visualization — AZ-700 (T4).

## Acceptance Criteria

**AC-1: Happy path on real tlog**
Given the committed `derkachi.tlog`
When `load_tlog_ground_truth(derkachi.tlog)` runs
Then it returns `TlogGroundTruth` with `len(records) > 100` and lat ≈ 50.08, lon ≈ 36.11

**AC-2: Empty GPS gracefully**
Given a tlog with no `GLOBAL_POSITION_INT` / `GPS_RAW_INT` messages
When the extractor runs
Then it returns `TlogGroundTruth(records=())` and logs WARN (does NOT raise)

**AC-3: Fallback precedence**
Given a tlog containing only `GPS_RAW_INT` (no `GLOBAL_POSITION_INT`)
When the extractor runs
Then it returns records sourced from `GPS_RAW_INT`

**AC-4: Type safety**
When `mypy --strict src/gps_denied_onboard/replay_input/tlog_ground_truth.py` runs
Then it reports zero errors

**AC-5: Comparison helpers in production**
Given the moved `l2_horizontal_m` + `match_percentage`
When imported from `gps_denied_onboard.helpers.gps_compare`
Then they behave identically to the prior test-helpers location (snapshot test)

## Non-Functional Requirements

**Performance**
- `load_tlog_ground_truth(derkachi.tlog)` (5.8 MB, ~60 s of GPS at 5 Hz) returns in < 2 s on Tier-1 hardware.

**Reliability**
- Lazy pymavlink import; missing dep raises `ReplayInputAdapterError` per project convention.

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-1 | Real derkachi.tlog parse | Non-empty TlogGroundTruth with Derkachi geofence lat/lon |
| AC-2 | Tlog with no GPS messages | Empty records tuple + WARN log |
| AC-3 | GPS_RAW_INT fallback | Records sourced from GPS_RAW_INT when GLOBAL_POSITION_INT absent |
| AC-3 | Mixed GLOBAL_POSITION_INT + GPS_RAW_INT | GLOBAL_POSITION_INT wins per AC-3 |
| AC-4 | mypy --strict | Zero errors |
| AC-5 | Helper move snapshot | Same numeric output as prior test-helpers location |

## Blackbox Tests

| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|--------|------------------------|-------------|-------------------|----------------|
| AC-1 | derkachi.tlog (real) | Load full tlog | ≥ 100 records, Derkachi geofence | Perf < 2s |

## Constraints

- pymavlink is already a project dep (used by C8); MUST be lazy-imported (auto_sync.py pattern).
- New module MUST follow the project's frozen + slotted dataclass convention.
- File ownership goes in `_docs/02_document/module-layout.md` per AZ-696 epic layout (no contract — internal helper).

## Risks & Mitigation

**Risk 1: MAVLink unit-conversion bugs**
- *Risk*: Mavlink encodes lat/lon as int × 1e7. Forgetting the divide ships records off by 7 orders of magnitude.
- *Mitigation*: AC-1 asserts Derkachi geofence values; unit test snapshots a known fixture.

**Risk 2: pymavlink import flakiness on Jetson**
- *Risk*: pymavlink occasionally fails to import on aarch64.
- *Mitigation*: Lazy import + raise `ReplayInputAdapterError` (existing pattern).
