Files
gps-denied-onboard/_docs/03_implementation/batch_60_cycle1_report.md
T
Oleksandr Bezdieniezhnykh 8149083cac [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>
2026-05-14 09:50:51 +03:00

9.5 KiB
Raw Blame History

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.pyReplayInputAdapterError(RuntimeError) taxonomy.
  • src/gps_denied_onboard/replay_input/interface.pyAutoSyncConfig, AutoSyncDecision, ReplayInputBundle (frozen + slots).
  • src/gps_denied_onboard/replay_input/auto_sync.pydetect_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.pyReplayInputAdapter 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.