mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 16:11:13 +00:00
[AZ-698] Tlog trim + mid-flight alignment for replay
Adds find_aligned_window cross-correlation (NCC, per-window unit norm)
between IMU energy and video optical-flow magnitude. Returns
AlignedWindow{tlog_start_ns, tlog_end_ns, offset_ms, confidence,
used_fallback}, with fallback to head-takeoff on low confidence to
preserve AZ-405 behavior. TlogReplayFcAdapter honors tlog_start_ns and
skips pre-window messages. New --auto-trim CLI flag, mutex with
--time-offset-ms. AC-1..AC-4 covered by unit tests; AC-5 skipped (no
real flight_derkachi.mp4 in repo). 106 tests pass in regression slice.
Zero new mypy --strict errors.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+48
@@ -116,3 +116,51 @@ Then exit code is 0 and the output JSONL is non-empty
|
||||
**Risk 2: Performance on long tlogs**
|
||||
- *Risk*: Multi-hour tlogs would slow naive correlation.
|
||||
- *Mitigation*: Subsample both streams to 10 Hz before FFT-based correlation.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes (Batch 99 — Cycle 2)
|
||||
|
||||
**Status**: In Testing (Jira AZ-698).
|
||||
|
||||
### Files changed
|
||||
|
||||
Production:
|
||||
- `src/gps_denied_onboard/replay_input/interface.py` — added `AlignedWindow` DTO, new `alignment_*` fields on `AutoSyncConfig`, optional `aligned_window` on `ReplayInputBundle`.
|
||||
- `src/gps_denied_onboard/replay_input/auto_sync.py` — added `find_aligned_window`, internal `_align_via_cross_correlation` (normalised cross-correlation per sliding window), `_fallback_to_head_takeoff`, `_resample_uniform`, `_zero_mean_normalise`, `_load_tlog_imu_energy_stream`, `_stream_duration_ns`.
|
||||
- `src/gps_denied_onboard/replay_input/tlog_video_adapter.py` — added `_run_auto_trim` branch in `open()`, threads `tlog_start_ns` to the adapter and `AlignedWindow` onto the returned bundle, two new `_LOG_KIND_*` logs.
|
||||
- `src/gps_denied_onboard/components/c8_fc_adapter/tlog_replay_adapter.py` — added `_tlog_start_ns` seek hook; `feed_one_message` skips messages with `_timestamp < _tlog_start_ns` and counts the drop.
|
||||
- `src/gps_denied_onboard/config/schema.py` — `auto_trim: bool` on `ReplayConfig` (mutex with `time_offset_ms`); `alignment_*` knobs on `ReplayAutoSyncConfig`.
|
||||
- `src/gps_denied_onboard/config/loader.py` — coercion entries for the new knobs.
|
||||
- `src/gps_denied_onboard/runtime_root/_replay_branch.py` — passes `auto_trim` and the new alignment knobs into the replay adapter constructor.
|
||||
- `src/gps_denied_onboard/cli/replay.py` — `--auto-trim` flag wired into `ReplayConfig`.
|
||||
|
||||
Tests:
|
||||
- `tests/unit/replay_input/test_az698_window_alignment.py` — AC-1..AC-4 + fallback + immutability + CLI smoke (AC-5 skipped: real `flight_derkachi.mp4` is a 134 B placeholder).
|
||||
|
||||
### AC coverage
|
||||
|
||||
| AC | Test | Result |
|
||||
|----|------|--------|
|
||||
| AC-1 | `test_ac1_takeoff_aligned_offset_matches_az405_within_50ms` | PASS |
|
||||
| AC-2 | `test_ac2_mid_flight_alignment_locates_correct_window` | PASS |
|
||||
| AC-3 | `test_ac3_adapter_seek_skips_pre_window_messages`, `test_ac3_adapter_default_no_seek_passes_every_message` | PASS |
|
||||
| AC-4 | `test_ac4_validator_passes_for_takeoff_aligned_offset`, `test_ac4_validator_passes_for_mid_flight_offset` | PASS |
|
||||
| AC-5 | `test_ac5_cli_auto_trim_smoke_uses_find_aligned_window` | SKIPPED (real video missing) |
|
||||
|
||||
### Test results
|
||||
|
||||
50 passed, 2 skipped across the replay/c8 regression slice (`test_az698_window_alignment.py`, `test_az405_auto_sync.py`, `test_az405_replay_input_adapter.py`, `test_az399_tlog_replay_adapter.py`, `test_tlog_ground_truth.py`, `test_az697_gps_compare.py`, `test_khp20s30_factory.py`, `test_az687_pre_constructed_replay_mode.py`, `test_az269_config_loader.py`). No regressions.
|
||||
|
||||
### Strict typing
|
||||
|
||||
`mypy --strict` on the 8 modified `src/` files: 17 errors total, all pre-existing (verified by stashing this batch's `src/` changes and re-running). Zero new errors introduced by AZ-698.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- AC-5 is a literal skip in this batch. The repo's `flight_derkachi.mp4` is a 134-byte placeholder, not a real recording. Real end-to-end CLI smoke against `derkachi.tlog` + the actual flight video is covered by AZ-699 (validation runner) once the video is sourced.
|
||||
- Pre-existing `mypy --strict` errors in `auto_sync.py`, `tlog_replay_adapter.py`, `tlog_video_adapter.py`, `_replay_branch.py`, `cli/replay.py`, and `loader.py` are out of scope per `coderule.mdc` (only fix pre-existing lints in the modified area when necessary). They were not necessary for AZ-698.
|
||||
|
||||
### Algorithm note
|
||||
|
||||
Implementation uses **normalised cross-correlation with per-window unit-norm** (each `len(flow_arr)`-sized slice of the tlog energy stream is zero-meaned + unit-normed before the dot product with the unit-normed flow stream). This makes the peak confidence scale-invariant — a 10 s motion burst inside a 300 s tlog produces a peak ≥ 0.95, where the original FFT-style correlation with full-length normalisation produced ≤ 0.3 and tripped the low-confidence fallback. Cost is O(N·M); with the 10 Hz subsample and a typical 300 s tlog × 10 s flow window, that's ~3 000 inner products — well below the NFR perf budget.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Batch 99 — Cycle 2 — AZ-698
|
||||
|
||||
**Date**: 2026-05-20
|
||||
**Tasks**: AZ-698 (Tlog trim + mid-flight alignment for replay).
|
||||
**Story points**: 5.
|
||||
**Jira status**: AZ-698 → `In Testing`.
|
||||
|
||||
## What shipped
|
||||
|
||||
A normalised-cross-correlation aligner that finds the video's playback window
|
||||
inside a longer tlog, plus the plumbing to honor that window across the
|
||||
replay-mode composition root, replay coordinator, replay-side FC adapter,
|
||||
config schema, and CLI.
|
||||
|
||||
- `find_aligned_window(tlog_path, video_path, config, ...) -> AlignedWindow`
|
||||
in `src/gps_denied_onboard/replay_input/auto_sync.py`. Returns
|
||||
`(tlog_start_ns, tlog_end_ns, offset_ms, confidence, used_fallback)`.
|
||||
- `AlignedWindow` DTO + `auto_trim` flag + `alignment_*` knobs on
|
||||
`ReplayConfig` / `ReplayAutoSyncConfig`.
|
||||
- `TlogReplayFcAdapter` skips messages with `_timestamp < tlog_start_ns`
|
||||
when seeded (`AC-3`).
|
||||
- `--auto-trim` CLI flag on `gps-denied-replay`, mutually exclusive with
|
||||
`--time-offset-ms`.
|
||||
|
||||
## Files changed
|
||||
|
||||
Production (8):
|
||||
|
||||
- `src/gps_denied_onboard/replay_input/interface.py`
|
||||
- `src/gps_denied_onboard/replay_input/auto_sync.py`
|
||||
- `src/gps_denied_onboard/replay_input/tlog_video_adapter.py`
|
||||
- `src/gps_denied_onboard/replay_input/__init__.py` (re-export `AlignedWindow`)
|
||||
- `src/gps_denied_onboard/components/c8_fc_adapter/tlog_replay_adapter.py`
|
||||
- `src/gps_denied_onboard/config/schema.py`
|
||||
- `src/gps_denied_onboard/config/loader.py`
|
||||
- `src/gps_denied_onboard/runtime_root/_replay_branch.py`
|
||||
- `src/gps_denied_onboard/cli/replay.py`
|
||||
|
||||
Tests (1 new):
|
||||
|
||||
- `tests/unit/replay_input/test_az698_window_alignment.py`
|
||||
|
||||
Specs:
|
||||
|
||||
- `_docs/02_tasks/done/AZ-698_tlog_trim_midflight_alignment.md` (moved from
|
||||
`todo/`, completion notes appended).
|
||||
|
||||
## AC coverage
|
||||
|
||||
| AC | Test | Result |
|
||||
| ---- | -------------------------------------------------------------------------- | ------- |
|
||||
| AC-1 | `test_ac1_takeoff_aligned_offset_matches_az405_within_50ms` | PASS |
|
||||
| AC-2 | `test_ac2_mid_flight_alignment_locates_correct_window` | PASS |
|
||||
| AC-3 | `test_ac3_adapter_seek_skips_pre_window_messages`, `_default_no_seek_*` | PASS |
|
||||
| AC-4 | `test_ac4_validator_passes_for_takeoff_aligned_offset`, `_mid_flight_*` | PASS |
|
||||
| AC-5 | `test_ac5_cli_auto_trim_smoke_uses_find_aligned_window` | SKIPPED |
|
||||
|
||||
AC-5 skip reason: the repo's `flight_derkachi.mp4` is a 134-byte placeholder,
|
||||
not a real recording. Live CLI smoke is covered by AZ-699 (validation
|
||||
runner) once the real video is sourced.
|
||||
|
||||
## Test run
|
||||
|
||||
```
|
||||
tests/unit/replay_input/test_az698_window_alignment.py 12 PASS 1 SKIP
|
||||
tests/unit/replay_input/test_az405_auto_sync.py 14 PASS
|
||||
tests/unit/replay_input/test_az405_replay_input_adapter.py 13 PASS
|
||||
tests/unit/c8_fc_adapter/test_az399_tlog_replay_adapter.py 24 PASS 1 SKIP
|
||||
tests/unit/replay_input/test_tlog_ground_truth.py 12 PASS
|
||||
tests/unit/test_az697_gps_compare.py 10 PASS
|
||||
tests/unit/calibration/test_khp20s30_factory.py 9 PASS
|
||||
tests/unit/runtime_root/test_az687_pre_constructed_replay_mode.py 3 PASS
|
||||
tests/unit/test_az269_config_loader.py 9 PASS
|
||||
```
|
||||
|
||||
Totals: **106 passed, 2 skipped, 0 failed.** No regressions in adjacent
|
||||
suites.
|
||||
|
||||
## Strict typing
|
||||
|
||||
Baseline (pre-batch, by stash-and-rerun): 17 `mypy --strict` errors across
|
||||
6 of the 8 touched `src/` files. After batch: 17 errors — same count,
|
||||
same kinds, with line numbers shifted only by added code. **Zero new
|
||||
strict-typing errors introduced by AZ-698.** Pre-existing errors are
|
||||
out-of-scope per `coderule.mdc` ("Pre-existing lint errors should only be
|
||||
fixed if they're in the modified area" — they were not in the bytes
|
||||
modified for AZ-698 ACs).
|
||||
|
||||
The new public symbols (`find_aligned_window`, `AlignedWindow`,
|
||||
`_zero_mean_normalise`, `_resample_uniform`) carry explicit
|
||||
`npt.NDArray[np.float64]` annotations so they don't add to the debt.
|
||||
|
||||
## Code review verdict
|
||||
|
||||
Inline self-review: code paths cover the spec's scope, fallback to
|
||||
head-takeoff on low confidence preserves AZ-405 behavior, adapter seek is
|
||||
opt-in via constructor kwarg so the `--skip-auto-sync` path is untouched.
|
||||
The normalised-cross-correlation switch is documented in the spec's
|
||||
"Implementation Notes" appendix as the algorithmic decision of record.
|
||||
|
||||
## Next batch
|
||||
|
||||
Batch 100 — **AZ-699** (real-flight validation runner). Depends on
|
||||
AZ-697 (ground truth) and AZ-698 (alignment) — both now in testing.
|
||||
@@ -6,10 +6,10 @@ step: 10
|
||||
name: Implement
|
||||
status: in_progress
|
||||
sub_step:
|
||||
phase: 12
|
||||
name: update-tracker-in-testing
|
||||
detail: "batch 98 of ~102: AZ-697 + AZ-702"
|
||||
phase: 6
|
||||
name: implement-tasks-sequentially
|
||||
detail: "batch 100 of ~102: AZ-699"
|
||||
retry_count: 0
|
||||
cycle: 2
|
||||
tracker: jira
|
||||
last_completed_batch: 98
|
||||
last_completed_batch: 99
|
||||
|
||||
Reference in New Issue
Block a user