mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 11:01:13 +00:00
8149083cac
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>
146 lines
5.9 KiB
Python
146 lines
5.9 KiB
Python
"""``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
|