mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 19:51:13 +00:00
[AZ-405] Replay — replay_input/ coordinator + IMU take-off auto-sync
Adds the Layer-4 cross-cutting `replay_input/` module per ADR-011: ReplayInputAdapter converges (video, tlog) into the standard FrameSource + FcAdapter + Clock surfaces the airborne composition root consumes. Owns time-alignment between video frames and tlog IMU/attitude ticks (manual via --time-offset-ms or auto via the AZ-405 IMU-take-off detector + Farneback motion-onset detector). Auto-sync algorithm (auto_sync.py): - Tlog take-off detector: sustained vertical-accel excess > 0.5 g for >= 0.5 s + sustained attitude-rate magnitude > 1 rad/s. - Video motion-onset detector: dense Farneback flow magnitude > 1.5 px sustained >= 0.5 s (deterministic per AC-10). - compute_offset combines the two; confidence = min(tlog, video). - validate_offset_or_fail implements the AC-9 95 % frame-window match validator with configurable threshold + window. ReplayInputAdapter.open() ordering (AC-13): 1. Load tlog samples + fail-fast on missing RAW_IMU/SCALED_IMU2 or ATTITUDE BEFORE any video read. 2. Resolve offset (auto-sync OR manual override; manual bypasses the detectors entirely per AC-8). 3. Run AC-9 validator on resolved offset; raise auto-sync hard-fail for AC-7 (CLI exit 2 mapping). 4. Build single Clock instance per pace (TlogDerived/ASAP, Wall/REAL). 5. Construct VideoFileFrameSource and TlogReplayFcAdapter with the resolved offset baked in (replay protocol Invariant 8). Structured log + FDR records on auto-sync detected / low-confidence / AC-8 hard-fail kinds. Idempotent close (AC-12). Tests: 25 unit tests across tests/unit/replay_input/ covering all 13 ACs (kernel-level synthetic fixtures for AC-1..AC-10; coordinator- level OpenCV synthetic videos + faked pymavlink for AC-6..AC-13). Contract update: replay_protocol.md v2.0.0 added fdr_client to the ReplayInputAdapter __init__ signature (was missing in the prose; the task spec already listed it in the allowed-imports section). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# Batch 60 — Cycle 1 Report
|
||||
|
||||
**Date**: 2026-05-14
|
||||
**Tasks**: AZ-405 (`replay_input/` Layer-4 coordinator + auto-sync video↔tlog via IMU take-off detection)
|
||||
**Verdict**: COMPLETE — PASS_WITH_WARNINGS
|
||||
|
||||
## Summary
|
||||
|
||||
Closed the AZ-405 gap in the replay subsystem by landing the `replay_input/` cross-cutting coordinator (Layer 4) and the auto-sync algorithm. After this batch, AZ-401 (composition root branch) has every strategy + every coordinator surface it needs to pivot `compose_root(config)` on `config.mode`.
|
||||
|
||||
The new module follows ADR-011 ("replay is a configuration of the airborne binary"). `ReplayInputAdapter.open()` performs strict ordering so AC-13 holds:
|
||||
|
||||
1. Tlog message-type pre-validation runs FIRST so a tlog missing `RAW_IMU` / `SCALED_IMU2` / `ATTITUDE` raises `ReplayInputAdapterError("tlog missing required message types: [...]")` before any video read.
|
||||
2. If `manual_time_offset_ms is None`, the auto-sync detectors run; otherwise the manual offset is adopted directly (AC-8 — verified via call-count assertion that the detectors are NOT invoked).
|
||||
3. The resolved offset is fed through the AC-9 frame-window match validator; a hard-fail raises `"auto-sync hard-fail: …"` so the shared main maps it to CLI exit code 2 (AC-7).
|
||||
4. The single `Clock` instance is constructed: `TlogDerivedClock` for `pace=ASAP`, `WallClock` for `pace=REALTIME`. Invariant 2.
|
||||
5. `VideoFileFrameSource` is built first; if construction fails the FC adapter is never opened. The FC adapter's own pre-scan runs as a defensive second sanity check during `open()`.
|
||||
6. `ReplayInputBundle(frame_source, fc_adapter, clock, resolved_time_offset_ms, auto_sync_result)` is returned.
|
||||
|
||||
`auto_sync.py` is split into pure compute kernels (`_compute_tlog_takeoff_from_samples`, `_compute_video_onset_from_samples`, `compute_offset`, `validate_offset_or_fail`) and disk-reading wrappers (`_load_tlog_samples`, `_read_video_frames`, `_compute_flow_magnitudes`). Tests target the kernels with synthetic fixtures; the wrappers are exercised end-to-end through the coordinator with `tlog_source_factory` / `video_frames_factory` / `video_timestamps_factory` injection points (mirrors the AZ-399 `source_factory` precedent).
|
||||
|
||||
The take-off detector uses the body-frame proper-acceleration excess above the 1 g hover baseline (`abs(total_g - 1.0) > 0.5 g sustained ≥ 0.5 s`) plus a sustained attitude-rate magnitude (`> 1.0 rad/s sustained ≥ 0.5 s`). When both signals fire we take the earlier onset (thrust precedes the body-rate spike on a vertical climb) and `confidence = min(accel_ratio, attitude_ratio)`. When only one signal fires we discount confidence by 0.6 so `combined_confidence` reliably trips the WARN-and-proceed regime (AC-6). When neither fires we fall through to `confidence = 0.0` and let the AC-9 validator decide whether the run is salvageable.
|
||||
|
||||
The video motion-onset detector uses `cv2.calcOpticalFlowFarneback` (dense flow, deterministic given identical input frames per AC-10) rather than pyramidal LK. Mean magnitude per pair is compared against `video_motion_threshold` (default 1.5 px) sustained for `sustained_seconds` (default 0.5 s).
|
||||
|
||||
The contract `_docs/02_document/contracts/replay/replay_protocol.md` v2.0.0 was updated in-batch to add `fdr_client: FdrClient` to the `ReplayInputAdapter.__init__` signature — the v2.0.0 prose was missing it (the AZ-405 task spec had it correctly listed in the Constraints section, so no implementation drift). Captured as F1 Medium/Spec-Gap in the batch review and resolved by the contract update.
|
||||
|
||||
## Files added / modified
|
||||
|
||||
### Added (7)
|
||||
|
||||
- `src/gps_denied_onboard/replay_input/__init__.py` — Public API re-exports (`ReplayInputAdapter`, `ReplayInputBundle`, `AutoSyncDecision`, `AutoSyncConfig`, `ReplayInputAdapterError`).
|
||||
- `src/gps_denied_onboard/replay_input/errors.py` — `ReplayInputAdapterError(RuntimeError)` taxonomy.
|
||||
- `src/gps_denied_onboard/replay_input/interface.py` — `AutoSyncConfig`, `AutoSyncDecision`, `ReplayInputBundle` (frozen + slots).
|
||||
- `src/gps_denied_onboard/replay_input/auto_sync.py` — `detect_tlog_takeoff` + `detect_video_motion_onset` wrappers; `_compute_tlog_takeoff_from_samples` + `_compute_video_onset_from_samples` pure kernels; `compute_offset`; `validate_offset_or_fail` AC-9 validator; `TlogSamples` DTO; `_find_sustained_event` sliding-window helper; `_wrap_pi`; `_load_tlog_samples` + `_read_video_frames` + `_compute_flow_magnitudes` disk readers.
|
||||
- `src/gps_denied_onboard/replay_input/tlog_video_adapter.py` — `ReplayInputAdapter` class (`open()` + idempotent `close()`); structured `replay.input.opened_manual_offset` / `replay.auto_sync.detected` / `replay.auto_sync.low_confidence` / `replay.auto_sync.ac8_validation_failed` log + FDR mirror.
|
||||
- `tests/unit/replay_input/__init__.py` — empty marker.
|
||||
- `tests/unit/replay_input/test_az405_auto_sync.py` — 14 tests covering AC-1..AC-10 (auto-sync kernels + offset compute + AC-9 validator + R-DEMO-3 kernel-side).
|
||||
- `tests/unit/replay_input/test_az405_replay_input_adapter.py` — 11 tests covering AC-6..AC-13 (coordinator-side) + manual override bypass + clock-strategy-by-pace + idempotent close.
|
||||
|
||||
### Modified (1)
|
||||
|
||||
- `_docs/02_document/contracts/replay/replay_protocol.md` — added `fdr_client: FdrClient` to the `ReplayInputAdapter.__init__` signature with a one-line rationale comment (was missing in v2.0.0).
|
||||
|
||||
## Task Results
|
||||
|
||||
| Task | Status | Files Modified | Focused tests | AC Coverage | Issues |
|
||||
|--------|--------|-------------------------------------------------------------|---------------|---------------|--------|
|
||||
| AZ-405 | Done | 5 added under `src/`; 2 added under `tests/unit/replay_input/`; 1 contract clarification | 25/25 pass | 13/13 covered | None |
|
||||
|
||||
## AC Test Coverage: 13/13 covered
|
||||
|
||||
| AC | Test | Status |
|
||||
|----|------|--------|
|
||||
| AC-1 | `test_ac1_tlog_takeoff_detector_positive_within_50ms_and_high_confidence` | Covered |
|
||||
| AC-2 | `test_ac2_tlog_takeoff_detector_low_amplitude_vibration_low_confidence` | Covered |
|
||||
| AC-3 | `test_ac3_tlog_takeoff_detector_hand_launch_warn_regime` | Covered |
|
||||
| AC-4 | `test_ac4_video_motion_onset_detected_within_one_frame` | Covered |
|
||||
| AC-5 | `test_ac5_combined_offset_within_200ms_of_ground_truth` | Covered |
|
||||
| AC-6 | `test_ac6_low_confidence_warn_and_proceed_does_not_raise` (+ `test_ac6_combined_confidence_takes_minimum_of_inputs`) | Covered |
|
||||
| AC-7 | `test_ac7_validator_hard_fail_returns_2_for_offset_outside_window` (kernel) + `test_ac7_ac8_validator_hard_fail_raises_on_open` (coordinator) | Covered |
|
||||
| AC-8 | `test_ac8_manual_override_bypasses_auto_detect` | Covered |
|
||||
| AC-9 | `test_ac9_validator_passes_for_well_matched_offset` + `test_ac9_threshold_configurable` | Covered |
|
||||
| AC-10 | `test_ac10_confidence_score_deterministic_across_two_runs` + `test_ac10_video_onset_deterministic_across_two_runs` | Covered |
|
||||
| AC-11 | `test_ac11_open_returns_complete_bundle_with_correct_strategies` + `_pace_realtime_yields_wall_clock` + `_pace_asap_yields_tlog_derived_clock` + `_resolved_offset_matches_auto_sync_result` | Covered |
|
||||
| AC-12 | `test_ac12_close_is_idempotent` + `test_close_without_open_does_not_raise` | Covered |
|
||||
| AC-13 | `test_ac13_missing_imu_messages_fails_fast_before_video_read` + `_missing_attitude_messages_fails_fast` | Covered |
|
||||
|
||||
## Code Review Verdict: PASS_WITH_WARNINGS
|
||||
|
||||
See `_docs/03_implementation/reviews/batch_60_review.md`. Three findings — Medium ×1, Low ×2 — none blocking:
|
||||
|
||||
1. **F1 Medium / Spec-Gap** — Replay protocol contract v2.0.0 prose was missing `fdr_client` from the `ReplayInputAdapter.__init__` signature. Resolved in-batch by updating the contract.
|
||||
2. **F2 Low / Maintainability** — Confidence aggregator is a `min()` only (no agreement bonus). Acceptable today; AC-1 bar is "≥ 0.85" with both signals strong → `min()` returns 1.0.
|
||||
3. **F3 Low / Maintainability** — Three test-only injection kwargs on the production constructor. Mirrors the AZ-399 `source_factory` precedent.
|
||||
|
||||
No Critical / High / Architecture findings. Auto-fix not required.
|
||||
|
||||
## Cumulative Code Review Verdict (batches 58-60): PASS_WITH_WARNINGS
|
||||
|
||||
See `_docs/03_implementation/cumulative_review_batches_58-60_cycle1_report.md`. Five findings — Medium ×1 (resolved in-batch), Low ×4 (3 carry-forward from prior cumulative reviews + 1 new). No Architecture findings, no new cyclic dependencies, all cross-component imports respect Public API surfaces.
|
||||
|
||||
## Auto-Fix Attempts: 0
|
||||
|
||||
## Stuck Agents: None
|
||||
|
||||
## Tests Run
|
||||
|
||||
- Focused suite (`tests/unit/replay_input/`): **25 passed**.
|
||||
- Replay-adjacent regression (`tests/unit/c8_fc_adapter/`, `tests/unit/frame_source/`, sampled): no regressions.
|
||||
- Full repo suite: deferred to Step 16 (Final Test Run) per the implement skill's "exactly once at end of implementation phase" cadence.
|
||||
|
||||
## Next Batch
|
||||
|
||||
The replay track is now nine-tenths wired:
|
||||
|
||||
- ✅ `Clock` Protocol (AZ-398, batch 57)
|
||||
- ✅ `FrameSource` + `VideoFileFrameSource` (AZ-398, batch 57)
|
||||
- ✅ `TlogReplayFcAdapter` (AZ-399, batch 59)
|
||||
- ✅ `ReplaySink` + `JsonlReplaySink` + `MavlinkTransport` cut-out (AZ-400, batch 59)
|
||||
- ✅ `replay_input/` coordinator + auto-sync (AZ-405, this batch)
|
||||
- ⏳ `compose_root(config)` mode-aware branch (AZ-401)
|
||||
- ⏳ `gps-denied-replay` CLI (AZ-402)
|
||||
- ⏳ E2E replay fixture (AZ-404)
|
||||
- (cancelled) `gps-denied-replay-cli` Dockerfile + SBOM diff (AZ-403 — replaced by ADR-011 single-image design)
|
||||
|
||||
Next eligible batch: AZ-401 alone (the only remaining task whose dependencies are now all satisfied; AZ-402 depends on AZ-401, AZ-404 depends on AZ-401+AZ-402). The C5 orthorectifier track (AZ-389) remains independently eligible and could be batched alongside if scope permits.
|
||||
Reference in New Issue
Block a user