Files
gps-denied-onboard/src/gps_denied_onboard/replay_input/interface.py
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

146 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""``replay_input/`` DTOs (AZ-405 / E-DEMO-REPLAY).
Frozen + slotted dataclasses per ADR-002 / module-layout.md so the
composition root and the coordinator can pass these by value without
fear of mutation downstream.
The DTOs come in two flavours:
- :class:`AutoSyncConfig` — operator-tunable thresholds for the
auto-sync algorithm. The composition root builds an instance from
``config.replay.auto_sync`` (owned by AZ-269 / AZ-270) and passes
it to :class:`ReplayInputAdapter`. Defaults match the contract
in :mod:`auto_sync` and the AC-1 / AC-2 / AC-3 thresholds.
- :class:`AutoSyncDecision` — the outcome of one auto-sync run. The
composition root attaches this to the FDR record so an operator can
audit how the offset was resolved.
- :class:`ReplayInputBundle` — the trio of strategies the composition
root consumes after :meth:`ReplayInputAdapter.open` returns. The
bundle also carries the resolved offset so the FDR write at the
start of the replay run can record provenance.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from gps_denied_onboard._types.fc import FcKind # noqa: F401 # for docstrings.
from gps_denied_onboard.clock import Clock
from gps_denied_onboard.components.c8_fc_adapter.tlog_replay_adapter import (
TlogReplayFcAdapter,
)
from gps_denied_onboard.frame_source.video_file import VideoFileFrameSource
__all__ = [
"AutoSyncConfig",
"AutoSyncDecision",
"ReplayInputBundle",
]
@dataclass(frozen=True, slots=True)
class AutoSyncConfig:
"""Operator-tunable thresholds for the AZ-405 auto-sync algorithm.
Defaults match the contract in
``_docs/02_document/contracts/replay/replay_protocol.md`` v2.0.0
and the AC-1 / AC-2 / AC-3 thresholds in the AZ-405 spec.
Attributes:
takeoff_accel_threshold_g: Sustained vertical-acceleration
magnitude (in g) above which a tlog sample is considered
part of a take-off pattern. Default 0.5 (AC-1).
takeoff_attitude_rate_threshold_rad_s: Sustained attitude-rate
magnitude (rad/s) above which an ``ATTITUDE`` pair is
considered part of a take-off pattern. Default 1.0.
sustained_seconds: Minimum duration both signals must persist
above their thresholds for a candidate to be accepted.
Default 0.5.
prescan_max_messages: Upper bound on tlog messages walked by
the take-off detector. ~30 s of telemetry at 200 Hz =
6000 messages, matching the AZ-399 pre-scan budget.
video_motion_threshold: Mean optical-flow magnitude (pixels)
above which a video frame pair is considered ``moving``.
Default 1.5 (calibrated for 720p footage).
video_motion_scan_seconds: Length of the leading video segment
inspected for the motion onset. Default 10.0 (AC-4 covers
an onset at frame 11 of a 60-frame fixture).
match_threshold_pct: AC-9 frame-window match-percentage
threshold (default 95.0). Configurable per
``config.replay.auto_sync_match_threshold_pct``.
match_window_ms: AC-9 per-frame matching tolerance in
milliseconds (default 100).
low_confidence_threshold: Combined-confidence cut-off below
which :meth:`ReplayInputAdapter.open` logs WARN and uses
the best-guess offset (AC-6). Default 0.80.
"""
takeoff_accel_threshold_g: float = 0.5
takeoff_attitude_rate_threshold_rad_s: float = 1.0
sustained_seconds: float = 0.5
prescan_max_messages: int = 6000
video_motion_threshold: float = 1.5
video_motion_scan_seconds: float = 10.0
match_threshold_pct: float = 95.0
match_window_ms: int = 100
low_confidence_threshold: float = 0.80
@dataclass(frozen=True, slots=True)
class AutoSyncDecision:
"""Outcome of one auto-sync run (AZ-405).
Attributes:
offset_ms: Resolved offset to be applied to tlog timestamps.
``offset_ms = tlog_takeoff_ns - video_motion_onset_ns``
converted to milliseconds.
tlog_takeoff_ns: Detected tlog take-off timestamp.
video_motion_onset_ns: Detected video motion-onset timestamp.
tlog_confidence: Take-off detector confidence in [0, 1].
video_confidence: Motion-onset detector confidence in [0, 1].
combined_confidence: Aggregated confidence in [0, 1]. Below
:attr:`AutoSyncConfig.low_confidence_threshold` the
coordinator logs WARN and proceeds (AC-6).
"""
offset_ms: int
tlog_takeoff_ns: int
video_motion_onset_ns: int
tlog_confidence: float
video_confidence: float
combined_confidence: float
@dataclass(frozen=True, slots=True)
class ReplayInputBundle:
"""Trio of strategies returned by :meth:`ReplayInputAdapter.open`.
The composition root wires the bundle into the same C1C7 + C13
pipeline as live (replay protocol Invariant 1 — the components
see only the standard :class:`FrameSource` / :class:`FcAdapter` /
:class:`Clock` interfaces past this point).
Attributes:
frame_source: :class:`VideoFileFrameSource` instance ready
for ``next_frame()`` calls.
fc_adapter: :class:`TlogReplayFcAdapter` instance with its
decode thread already started by :meth:`open`.
clock: :class:`TlogDerivedClock` (pace=ASAP) or
:class:`WallClock` (pace=REALTIME).
resolved_time_offset_ms: Offset applied to tlog timestamps.
Equals either the ``manual_time_offset_ms`` constructor
argument or :attr:`AutoSyncDecision.offset_ms`.
auto_sync_result: Auto-sync outcome; ``None`` when the
constructor received an explicit
``manual_time_offset_ms``.
"""
frame_source: "VideoFileFrameSource"
fc_adapter: "TlogReplayFcAdapter"
clock: "Clock"
resolved_time_offset_ms: int
auto_sync_result: AutoSyncDecision | None