mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 17:21: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,145 @@
|
||||
"""``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 C1–C7 + 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
|
||||
Reference in New Issue
Block a user