mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 23:31:13 +00:00
06f655d8fb
Adds JsonSidecarWarmStartHintStore (atomic JSON + SHA-256 sidecar via AZ-280) inside c1_vio, plus the cross-strategy WarmStartWiredStrategy wrapper + prime_warm_start_from_disk / prime_warm_start_from_fc hooks at runtime_root. AC-7 post-reset covariance inflation and AC-8 "no fake confidence" baseline floor are enforced at the wiring layer so no strategy module needed edits. Adds three c1_vio config fields (warm_start_store_dir, warm_start_save_period_frames, post_reset_covariance_inflation_factor) and registers the new FDR kind vio.warm_start. 34 unit tests cover all 10 ACs + 3 NFRs. Verdict PASS_WITH_WARNINGS — see _docs/03_implementation/reviews/batch_56_review.md for the four non-blocking documentation findings (F1 cold-start log kind shorthand, F2 strategy-frame pose semantics, F3 dev-hardware perf smoke, F4 runtime_root importing c1-internal _facade_spine for shared FDR conventions). Closes AZ-335; depends on AZ-528 (batch 55). Co-authored-by: Cursor <cursoragent@cursor.com>
933 lines
32 KiB
Python
933 lines
32 KiB
Python
"""AZ-335 — C1 warm-start hint persistence + F8 reboot recovery wiring tests.
|
||
|
||
Covers all 10 acceptance criteria from
|
||
``_docs/02_tasks/todo/AZ-335_c1_warm_start_recovery.md`` plus three
|
||
non-functional requirements (perf-save, perf-load, no-crash). Tests
|
||
target both the c1-internal :class:`JsonSidecarWarmStartHintStore`
|
||
and the runtime-root :class:`WarmStartWiredStrategy` + prime hooks.
|
||
|
||
The wiring tests construct a deliberately minimal scriptable
|
||
:class:`_FakeVioStrategy` (kept local — the c1_vio strategy backends
|
||
already exercise the strategy-internal Protocol shape exhaustively;
|
||
this file's job is to verify the **wiring** layer behaves correctly
|
||
when wrapped around any strategy). The store tests use the real
|
||
:class:`Sha256Sidecar` (atomicwrites) on tmp_path — no fakes here
|
||
because the AC-1/AC-2/AC-10 contracts ARE about the on-disk
|
||
behaviour itself.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import time
|
||
from dataclasses import dataclass
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
from typing import Any, Literal
|
||
|
||
import gtsam
|
||
import numpy as np
|
||
import pytest
|
||
|
||
from gps_denied_onboard._types.nav import (
|
||
FeatureQuality,
|
||
ImuBias,
|
||
ImuWindow,
|
||
NavCameraFrame,
|
||
VioHealth,
|
||
VioOutput,
|
||
VioState,
|
||
WarmStartPose,
|
||
)
|
||
from gps_denied_onboard._types.calibration import CameraCalibration
|
||
from gps_denied_onboard.components.c1_vio.warm_start_store import (
|
||
HINT_FILENAME,
|
||
HINT_SCHEMA_VERSION,
|
||
JsonSidecarWarmStartHintStore,
|
||
LoadedWarmStartHint,
|
||
WarmStartFcSource,
|
||
WarmStartHintStore,
|
||
)
|
||
from gps_denied_onboard.fdr_client.fakes import FakeFdrSink
|
||
from gps_denied_onboard.helpers.sha256_sidecar import SIDECAR_SUFFIX
|
||
from gps_denied_onboard.runtime_root.warm_start_wiring import (
|
||
WARM_START_PRODUCER_ID,
|
||
WarmStartWiredStrategy,
|
||
prime_warm_start_from_disk,
|
||
prime_warm_start_from_fc,
|
||
)
|
||
|
||
|
||
_DEFAULT_CALIBRATION_ID = "adti26"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Shared builders.
|
||
|
||
|
||
def _make_pose(yaw_deg: float = 0.0, x: float = 1.0, y: float = 2.0, z: float = 3.0) -> gtsam.Pose3:
|
||
"""A concrete SE(3) pose with a deterministic, non-identity rotation."""
|
||
yaw = np.deg2rad(yaw_deg)
|
||
R = np.array(
|
||
[
|
||
[np.cos(yaw), -np.sin(yaw), 0.0],
|
||
[np.sin(yaw), np.cos(yaw), 0.0],
|
||
[0.0, 0.0, 1.0],
|
||
],
|
||
dtype=np.float64,
|
||
)
|
||
T = np.eye(4, dtype=np.float64)
|
||
T[:3, :3] = R
|
||
T[:3, 3] = [x, y, z]
|
||
return gtsam.Pose3(T)
|
||
|
||
|
||
def _make_hint(
|
||
*,
|
||
yaw_deg: float = 5.0,
|
||
velocity: tuple[float, float, float] = (1.0, 2.0, 3.0),
|
||
accel_bias: tuple[float, float, float] = (0.01, -0.02, 0.03),
|
||
gyro_bias: tuple[float, float, float] = (0.001, 0.002, -0.003),
|
||
captured_at_ns: int = 1_700_000_000_000,
|
||
) -> WarmStartPose:
|
||
return WarmStartPose(
|
||
body_T_world=_make_pose(yaw_deg=yaw_deg),
|
||
velocity_b=velocity,
|
||
bias=ImuBias(accel_bias=accel_bias, gyro_bias=gyro_bias),
|
||
captured_at_ns=captured_at_ns,
|
||
)
|
||
|
||
|
||
def _make_calibration() -> CameraCalibration:
|
||
return CameraCalibration(
|
||
camera_id="cam0",
|
||
intrinsics_3x3=np.eye(3, dtype=np.float64),
|
||
distortion=np.zeros(4, dtype=np.float64),
|
||
body_to_camera_se3=_make_pose(),
|
||
acquisition_method="checker_board",
|
||
)
|
||
|
||
|
||
def _make_imu_window() -> ImuWindow:
|
||
return ImuWindow(samples=tuple(), ts_start_ns=0, ts_end_ns=0)
|
||
|
||
|
||
def _make_frame(frame_id: int = 1) -> NavCameraFrame:
|
||
return NavCameraFrame(
|
||
frame_id=frame_id,
|
||
timestamp=datetime(2026, 5, 14, 0, 0, 0, tzinfo=timezone.utc),
|
||
image=np.zeros((10, 10), dtype=np.uint8),
|
||
camera_calibration_id="cam0",
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Local scriptable VioStrategy fake — wiring tests only.
|
||
|
||
|
||
@dataclass
|
||
class _ResetCall:
|
||
"""One captured ``reset_to_warm_start`` invocation on the fake strategy."""
|
||
|
||
hint: WarmStartPose
|
||
|
||
|
||
class _FakeVioStrategy:
|
||
"""Scriptable minimal :class:`VioStrategy` for AZ-335 wiring tests.
|
||
|
||
Returns a deterministic per-call :class:`VioOutput` whose
|
||
``pose_covariance_6x6`` is the value most recently set via
|
||
:meth:`set_emit_covariance` (default ``np.eye(6) * 0.01``).
|
||
Each :meth:`reset_to_warm_start` invocation is captured in
|
||
:attr:`reset_calls` so wiring tests can assert single-call,
|
||
correct-hint, no-call semantics.
|
||
"""
|
||
|
||
def __init__(self, *, label: Literal["okvis2", "vins_mono", "klt_ransac"] = "klt_ransac") -> None:
|
||
self._label = label
|
||
self._next_cov = np.eye(6, dtype=np.float64) * 0.01
|
||
self._next_bias = ImuBias(accel_bias=(0.0, 0.0, 0.0), gyro_bias=(0.0, 0.0, 0.0))
|
||
self._frame_counter = 0
|
||
self.reset_calls: list[_ResetCall] = []
|
||
self._raise_on_reset: Exception | None = None
|
||
|
||
def set_emit_covariance(self, cov: np.ndarray) -> None:
|
||
self._next_cov = np.asarray(cov, dtype=np.float64)
|
||
|
||
def set_emit_bias(self, bias: ImuBias) -> None:
|
||
self._next_bias = bias
|
||
|
||
def script_reset_failure(self, exc: Exception) -> None:
|
||
self._raise_on_reset = exc
|
||
|
||
def process_frame(
|
||
self,
|
||
frame: NavCameraFrame,
|
||
imu: ImuWindow,
|
||
calibration: CameraCalibration,
|
||
) -> VioOutput:
|
||
self._frame_counter += 1
|
||
return VioOutput(
|
||
frame_id=f"frame-{self._frame_counter}",
|
||
relative_pose_T=_make_pose(),
|
||
pose_covariance_6x6=self._next_cov.copy(),
|
||
imu_bias=self._next_bias,
|
||
feature_quality=FeatureQuality(
|
||
tracked=80, new=2, lost=1, mean_parallax=5.0, mre_px=0.8
|
||
),
|
||
emitted_at_ns=1_700_000_000_000 + self._frame_counter,
|
||
)
|
||
|
||
def reset_to_warm_start(self, hint: WarmStartPose) -> None:
|
||
if self._raise_on_reset is not None:
|
||
raise self._raise_on_reset
|
||
self.reset_calls.append(_ResetCall(hint=hint))
|
||
|
||
def health_snapshot(self) -> VioHealth:
|
||
return VioHealth(state=VioState.TRACKING, consecutive_lost=0, bias_norm=0.0)
|
||
|
||
def current_strategy_label(self) -> Literal["okvis2", "vins_mono", "klt_ransac"]:
|
||
return self._label
|
||
|
||
|
||
class _FakeFcSource:
|
||
"""Scriptable :class:`WarmStartFcSource` for F2 takeoff tests."""
|
||
|
||
def __init__(
|
||
self,
|
||
*,
|
||
hint: WarmStartPose | None = None,
|
||
raise_with: Exception | None = None,
|
||
calibration_id: str = _DEFAULT_CALIBRATION_ID,
|
||
) -> None:
|
||
self._hint = hint
|
||
self._raise_with = raise_with
|
||
self._calibration_id = calibration_id
|
||
self.fetch_call_count = 0
|
||
|
||
def fetch_warm_start_pose(self) -> WarmStartPose | None:
|
||
self.fetch_call_count += 1
|
||
if self._raise_with is not None:
|
||
raise self._raise_with
|
||
return self._hint
|
||
|
||
def calibration_id(self) -> str:
|
||
return self._calibration_id
|
||
|
||
|
||
def _make_wired(
|
||
inner: _FakeVioStrategy,
|
||
store: WarmStartHintStore,
|
||
*,
|
||
warm_start_max_frames: int = 5,
|
||
inflation_factor: float = 2.0,
|
||
save_period: int = 5,
|
||
) -> WarmStartWiredStrategy:
|
||
return WarmStartWiredStrategy(
|
||
inner=inner,
|
||
store=store,
|
||
warm_start_max_frames=warm_start_max_frames,
|
||
post_reset_covariance_inflation_factor=inflation_factor,
|
||
warm_start_save_period_frames=save_period,
|
||
)
|
||
|
||
|
||
def _drive_frames(wired: WarmStartWiredStrategy, n: int) -> list[VioOutput]:
|
||
return [
|
||
wired.process_frame(_make_frame(i), _make_imu_window(), _make_calibration())
|
||
for i in range(1, n + 1)
|
||
]
|
||
|
||
|
||
# ===========================================================================
|
||
# Store tests — AC-1, AC-2, AC-9, AC-10, NFR-perf-save, NFR-perf-load,
|
||
# Risk-2 calibration-mismatch.
|
||
|
||
|
||
class TestStoreAc1RoundTrip:
|
||
def test_save_then_load_returns_deep_equal_hint(self, tmp_path: Path) -> None:
|
||
# Arrange
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
hint = _make_hint()
|
||
|
||
# Act
|
||
store.save(hint, pre_reboot_covariance_norm=0.123)
|
||
loaded = store.load()
|
||
|
||
# Assert
|
||
assert loaded is not None
|
||
assert isinstance(loaded, LoadedWarmStartHint)
|
||
assert loaded.calibration_id == _DEFAULT_CALIBRATION_ID
|
||
assert loaded.pre_reboot_covariance_norm == pytest.approx(0.123)
|
||
np.testing.assert_array_almost_equal(
|
||
loaded.pose.body_T_world.matrix(), hint.body_T_world.matrix()
|
||
)
|
||
assert loaded.pose.velocity_b == hint.velocity_b
|
||
assert loaded.pose.bias == hint.bias
|
||
assert loaded.pose.captured_at_ns == hint.captured_at_ns
|
||
|
||
def test_save_creates_payload_and_sidecar_files(self, tmp_path: Path) -> None:
|
||
# Arrange
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
hint = _make_hint()
|
||
|
||
# Act
|
||
store.save(hint, pre_reboot_covariance_norm=0.5)
|
||
|
||
# Assert
|
||
assert (tmp_path / HINT_FILENAME).exists()
|
||
assert (tmp_path / (HINT_FILENAME + SIDECAR_SUFFIX)).exists()
|
||
assert store.payload_path == tmp_path / HINT_FILENAME
|
||
|
||
def test_save_creates_missing_parent_directory(self, tmp_path: Path) -> None:
|
||
# Arrange
|
||
nested = tmp_path / "nested" / "dirs" / "warm_start"
|
||
store = JsonSidecarWarmStartHintStore(nested, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
hint = _make_hint()
|
||
|
||
# Act
|
||
store.save(hint, pre_reboot_covariance_norm=0.0)
|
||
|
||
# Assert
|
||
assert (nested / HINT_FILENAME).exists()
|
||
|
||
|
||
class TestStoreAc2Corrupted:
|
||
def _seed_valid_then_flip_one_byte(
|
||
self, tmp_path: Path
|
||
) -> tuple[JsonSidecarWarmStartHintStore, Path]:
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
store.save(_make_hint(), pre_reboot_covariance_norm=0.1)
|
||
payload_path = tmp_path / HINT_FILENAME
|
||
original = payload_path.read_bytes()
|
||
# Flip one byte mid-payload to trigger sha256 mismatch but keep
|
||
# the file structurally present and the sidecar untouched.
|
||
idx = len(original) // 2
|
||
corrupted = original[:idx] + bytes([(original[idx] + 1) % 256]) + original[idx + 1 :]
|
||
payload_path.write_bytes(corrupted)
|
||
return store, payload_path
|
||
|
||
def test_corrupted_payload_returns_none(self, tmp_path: Path, caplog: Any) -> None:
|
||
# Arrange
|
||
store, _ = self._seed_valid_then_flip_one_byte(tmp_path)
|
||
|
||
# Act
|
||
with caplog.at_level(logging.WARNING):
|
||
loaded = store.load()
|
||
|
||
# Assert
|
||
assert loaded is None
|
||
warn_records = [
|
||
r for r in caplog.records if getattr(r, "kind", "") == "c1.warm_start.corrupted"
|
||
]
|
||
assert len(warn_records) == 1
|
||
assert warn_records[0].levelname == "WARNING"
|
||
|
||
def test_corrupted_file_is_not_silently_deleted(self, tmp_path: Path) -> None:
|
||
# Arrange + Act
|
||
store, payload_path = self._seed_valid_then_flip_one_byte(tmp_path)
|
||
_ = store.load()
|
||
|
||
# Assert
|
||
assert payload_path.exists(), "AC-2: operator may want to forensically inspect"
|
||
|
||
def test_structurally_invalid_json_returns_none_with_warn(
|
||
self, tmp_path: Path, caplog: Any
|
||
) -> None:
|
||
# Arrange — write a payload with the WRONG schema version and rebuild the sidecar
|
||
# so sha256 verifies but envelope deserialisation rejects.
|
||
from gps_denied_onboard.helpers.sha256_sidecar import Sha256Sidecar
|
||
|
||
bad_payload = b'{"version": 999, "calibration_id": "x", "pose": {}}'
|
||
Sha256Sidecar.write_atomic_and_sidecar(tmp_path / HINT_FILENAME, bad_payload)
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id="x")
|
||
|
||
# Act
|
||
with caplog.at_level(logging.WARNING):
|
||
loaded = store.load()
|
||
|
||
# Assert
|
||
assert loaded is None
|
||
kinds = [getattr(r, "kind", "") for r in caplog.records]
|
||
assert "c1.warm_start.corrupted" in kinds
|
||
|
||
|
||
class TestStoreAc3CalibrationMismatch:
|
||
def test_calibration_mismatch_returns_none_with_specific_warn(
|
||
self, tmp_path: Path, caplog: Any
|
||
) -> None:
|
||
# Arrange
|
||
producer = JsonSidecarWarmStartHintStore(tmp_path, calibration_id="OLD_CAL")
|
||
producer.save(_make_hint(), pre_reboot_covariance_norm=0.1)
|
||
consumer = JsonSidecarWarmStartHintStore(tmp_path, calibration_id="NEW_CAL")
|
||
|
||
# Act
|
||
with caplog.at_level(logging.WARNING):
|
||
loaded = consumer.load()
|
||
|
||
# Assert
|
||
assert loaded is None
|
||
warn_records = [
|
||
r
|
||
for r in caplog.records
|
||
if getattr(r, "kind", "") == "c1.warm_start.calibration_mismatch"
|
||
]
|
||
assert len(warn_records) == 1
|
||
|
||
|
||
class TestStoreAc9Clear:
|
||
def test_clear_removes_payload_and_sidecar(self, tmp_path: Path) -> None:
|
||
# Arrange
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
store.save(_make_hint(), pre_reboot_covariance_norm=0.1)
|
||
|
||
# Act
|
||
store.clear()
|
||
|
||
# Assert
|
||
assert not (tmp_path / HINT_FILENAME).exists()
|
||
assert not (tmp_path / (HINT_FILENAME + SIDECAR_SUFFIX)).exists()
|
||
assert store.load() is None
|
||
|
||
def test_clear_emits_info_log(self, tmp_path: Path, caplog: Any) -> None:
|
||
# Arrange
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
|
||
# Act
|
||
with caplog.at_level(logging.INFO):
|
||
store.clear()
|
||
|
||
# Assert
|
||
info_records = [
|
||
r for r in caplog.records if getattr(r, "kind", "") == "c1.warm_start.cleared"
|
||
]
|
||
assert len(info_records) == 1
|
||
assert info_records[0].levelname == "INFO"
|
||
|
||
def test_clear_is_idempotent(self, tmp_path: Path) -> None:
|
||
# Arrange
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
|
||
# Act + Assert — first clear with no files MUST NOT raise
|
||
store.clear()
|
||
store.clear()
|
||
|
||
|
||
class TestStoreAc10Atomicity:
|
||
def test_kill_mid_save_leaves_prior_hint_loadable(self, tmp_path: Path) -> None:
|
||
"""Simulate a crash mid-save by writing a temp file but never renaming.
|
||
|
||
``Sha256Sidecar.write_atomic_and_sidecar`` uses
|
||
``atomicwrites.atomic_write`` (temp-file + ``os.replace``), so
|
||
a mid-write crash never leaves a partial `c1_warm_start.json`.
|
||
We model the "process killed mid-save" scenario by leaving a
|
||
stray temp file alongside an already-committed prior hint;
|
||
:meth:`load` must still return the prior valid hint.
|
||
"""
|
||
# Arrange — first save commits a known hint
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
prior = _make_hint(yaw_deg=0.0)
|
||
store.save(prior, pre_reboot_covariance_norm=0.1)
|
||
|
||
# Simulate a half-written temp file from a "killed" second save.
|
||
# atomicwrites uses a temp file with a `.<name>.<rand>` prefix.
|
||
stray = tmp_path / f".{HINT_FILENAME}.partial-write-stray"
|
||
stray.write_bytes(b"this-is-half-written-junk")
|
||
|
||
# Act
|
||
loaded = store.load()
|
||
|
||
# Assert — the prior valid hint loads despite the stray temp file.
|
||
assert loaded is not None
|
||
np.testing.assert_array_almost_equal(
|
||
loaded.pose.body_T_world.matrix(), prior.body_T_world.matrix()
|
||
)
|
||
# The stray file was NOT consumed as the hint.
|
||
assert stray.exists()
|
||
|
||
|
||
class TestStoreLifecycle:
|
||
def test_load_returns_none_when_no_file(self, tmp_path: Path) -> None:
|
||
# Arrange
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
|
||
# Act + Assert
|
||
assert store.load() is None
|
||
|
||
def test_default_impl_satisfies_protocol(self, tmp_path: Path) -> None:
|
||
# Arrange
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
|
||
# Assert — runtime_checkable Protocol conformance
|
||
assert isinstance(store, WarmStartHintStore)
|
||
|
||
|
||
class TestStoreNfrPerf:
|
||
def test_nfr_perf_save_p99_under_50ms(self, tmp_path: Path) -> None:
|
||
# Arrange
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
hint = _make_hint()
|
||
n = 200 # bounded — full perf bench lives in C1-PT-01 Tier-2
|
||
|
||
# Act
|
||
timings_ms = []
|
||
for _ in range(n):
|
||
t0 = time.perf_counter()
|
||
store.save(hint, pre_reboot_covariance_norm=0.1)
|
||
timings_ms.append((time.perf_counter() - t0) * 1000.0)
|
||
|
||
# Assert — p99 under 50ms; this is a smoke-budget on dev hardware,
|
||
# the production budget is on Tier-2 NVMe per the task NFR.
|
||
p99 = float(np.percentile(timings_ms, 99))
|
||
assert p99 < 50.0, f"save p99 = {p99:.2f}ms exceeds 50ms NFR budget"
|
||
|
||
def test_nfr_perf_load_p99_under_20ms(self, tmp_path: Path) -> None:
|
||
# Arrange
|
||
store = JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
store.save(_make_hint(), pre_reboot_covariance_norm=0.1)
|
||
n = 200
|
||
|
||
# Act
|
||
timings_ms = []
|
||
for _ in range(n):
|
||
t0 = time.perf_counter()
|
||
loaded = store.load()
|
||
timings_ms.append((time.perf_counter() - t0) * 1000.0)
|
||
assert loaded is not None # sanity
|
||
|
||
# Assert
|
||
p99 = float(np.percentile(timings_ms, 99))
|
||
assert p99 < 20.0, f"load p99 = {p99:.2f}ms exceeds 20ms NFR budget"
|
||
|
||
|
||
# ===========================================================================
|
||
# Wiring tests — AC-3 .. AC-8, NFR-no-crash.
|
||
|
||
|
||
@pytest.fixture
|
||
def fdr_sink() -> FakeFdrSink:
|
||
return FakeFdrSink(producer_id=WARM_START_PRODUCER_ID)
|
||
|
||
|
||
@pytest.fixture
|
||
def fake_strategy() -> _FakeVioStrategy:
|
||
return _FakeVioStrategy()
|
||
|
||
|
||
@pytest.fixture
|
||
def store(tmp_path: Path) -> JsonSidecarWarmStartHintStore:
|
||
return JsonSidecarWarmStartHintStore(tmp_path, calibration_id=_DEFAULT_CALIBRATION_ID)
|
||
|
||
|
||
class TestWiringAc3ColdStart:
|
||
def test_cold_start_does_not_invoke_reset(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
fdr_sink: FakeFdrSink,
|
||
caplog: Any,
|
||
) -> None:
|
||
# Arrange
|
||
wired = _make_wired(fake_strategy, store)
|
||
|
||
# Act
|
||
with caplog.at_level(logging.INFO):
|
||
applied = prime_warm_start_from_disk(wired, store, fdr_client=fdr_sink)
|
||
|
||
# Assert
|
||
assert applied is False
|
||
assert fake_strategy.reset_calls == []
|
||
info_records = [
|
||
r
|
||
for r in caplog.records
|
||
if getattr(r, "kind", "") == "c1.warm_start.cold_start_no_hint"
|
||
]
|
||
assert len(info_records) == 1
|
||
cold_records = [r for r in fdr_sink.records if r.kind == "vio.warm_start"]
|
||
assert len(cold_records) == 1
|
||
assert cold_records[0].payload["source"] == "cold_start_no_hint"
|
||
assert cold_records[0].payload["bias_norm"] is None
|
||
|
||
|
||
class TestWiringAc4F8Reboot:
|
||
def test_f8_reboot_loads_hint_calls_reset_emits_fdr(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
fdr_sink: FakeFdrSink,
|
||
caplog: Any,
|
||
) -> None:
|
||
# Arrange — seed a hint on disk
|
||
prior_hint = _make_hint(yaw_deg=10.0)
|
||
store.save(prior_hint, pre_reboot_covariance_norm=0.0625)
|
||
wired = _make_wired(fake_strategy, store)
|
||
|
||
# Act
|
||
with caplog.at_level(logging.INFO):
|
||
applied = prime_warm_start_from_disk(wired, store, fdr_client=fdr_sink)
|
||
|
||
# Assert
|
||
assert applied is True
|
||
assert len(fake_strategy.reset_calls) == 1
|
||
np.testing.assert_array_almost_equal(
|
||
fake_strategy.reset_calls[0].hint.body_T_world.matrix(),
|
||
prior_hint.body_T_world.matrix(),
|
||
)
|
||
# AC-8 baseline floor installed
|
||
assert wired.baseline_floor == pytest.approx(0.0625)
|
||
info_records = [
|
||
r
|
||
for r in caplog.records
|
||
if getattr(r, "kind", "") == "c1.warm_start.f8_reboot_disk"
|
||
]
|
||
assert len(info_records) == 1
|
||
fdr_records = [r for r in fdr_sink.records if r.kind == "vio.warm_start"]
|
||
assert len(fdr_records) == 1
|
||
assert fdr_records[0].payload["source"] == "f8_reboot_disk"
|
||
assert fdr_records[0].payload["pre_reboot_covariance_norm"] == pytest.approx(0.0625)
|
||
assert fdr_records[0].payload["bias_norm"] is not None
|
||
assert fdr_records[0].payload["staleness_ns"] is not None
|
||
|
||
|
||
class TestWiringAc5F2Takeoff:
|
||
def test_f2_takeoff_fetches_fc_calls_reset_persists(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
fdr_sink: FakeFdrSink,
|
||
caplog: Any,
|
||
) -> None:
|
||
# Arrange
|
||
fc_hint = _make_hint(yaw_deg=20.0)
|
||
source = _FakeFcSource(hint=fc_hint)
|
||
wired = _make_wired(fake_strategy, store)
|
||
|
||
# Act
|
||
with caplog.at_level(logging.INFO):
|
||
applied = prime_warm_start_from_fc(wired, source, store, fdr_client=fdr_sink)
|
||
|
||
# Assert
|
||
assert applied is True
|
||
assert source.fetch_call_count == 1
|
||
assert len(fake_strategy.reset_calls) == 1
|
||
np.testing.assert_array_almost_equal(
|
||
fake_strategy.reset_calls[0].hint.body_T_world.matrix(),
|
||
fc_hint.body_T_world.matrix(),
|
||
)
|
||
# F2 path persists the hint so a subsequent F8 reboot can recover it.
|
||
loaded = store.load()
|
||
assert loaded is not None
|
||
np.testing.assert_array_almost_equal(
|
||
loaded.pose.body_T_world.matrix(), fc_hint.body_T_world.matrix()
|
||
)
|
||
# AC-8 floor is NOT installed on the F2 path (no pre-reboot baseline).
|
||
assert wired.baseline_floor == pytest.approx(0.0)
|
||
info_records = [
|
||
r
|
||
for r in caplog.records
|
||
if getattr(r, "kind", "") == "c1.warm_start.f2_takeoff_fc"
|
||
]
|
||
assert len(info_records) == 1
|
||
fdr_records = [r for r in fdr_sink.records if r.kind == "vio.warm_start"]
|
||
assert len(fdr_records) == 1
|
||
assert fdr_records[0].payload["source"] == "f2_takeoff_fc"
|
||
|
||
|
||
class TestWiringAc6PerFrameSave:
|
||
def test_per_frame_save_respects_period(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
tmp_path: Path,
|
||
) -> None:
|
||
# Arrange — period = 5; 12 frames → save fires at frames 5 and 10 only
|
||
wired = _make_wired(fake_strategy, store, save_period=5)
|
||
|
||
# Act
|
||
outputs = _drive_frames(wired, 12)
|
||
|
||
# Assert
|
||
assert len(outputs) == 12
|
||
# The on-disk hint should reflect frame 10's emit, not frame 12's.
|
||
loaded = store.load()
|
||
assert loaded is not None
|
||
assert loaded.pose.captured_at_ns == outputs[9].emitted_at_ns
|
||
|
||
def test_save_period_one_saves_every_frame(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
) -> None:
|
||
# Arrange
|
||
wired = _make_wired(fake_strategy, store, save_period=1)
|
||
|
||
# Act
|
||
outputs = _drive_frames(wired, 3)
|
||
|
||
# Assert — last save reflects the most recent frame
|
||
loaded = store.load()
|
||
assert loaded is not None
|
||
assert loaded.pose.captured_at_ns == outputs[-1].emitted_at_ns
|
||
|
||
|
||
class TestWiringAc7PostResetInflation:
|
||
def test_first_n_frames_inflated_then_unmodified(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
) -> None:
|
||
# Arrange — strategy emits cov of Frobenius norm 1.0, factor=2.0,
|
||
# window=5 frames. Save period large enough that no save fires
|
||
# in the inflation window for cleaner assertion.
|
||
emit_cov = np.eye(6, dtype=np.float64) * (1.0 / np.sqrt(6)) # ||·||_F = 1.0
|
||
fake_strategy.set_emit_covariance(emit_cov)
|
||
wired = _make_wired(
|
||
fake_strategy,
|
||
store,
|
||
warm_start_max_frames=5,
|
||
inflation_factor=2.0,
|
||
save_period=100,
|
||
)
|
||
wired.reset_to_warm_start(_make_hint())
|
||
|
||
# Act — drive 6 frames; the first 5 inflated, the 6th unmodified.
|
||
outputs = _drive_frames(wired, 6)
|
||
|
||
# Assert
|
||
for i in range(5):
|
||
norm = float(np.linalg.norm(outputs[i].pose_covariance_6x6, ord="fro"))
|
||
assert norm == pytest.approx(2.0, abs=1e-9), (
|
||
f"Frame {i + 1}: expected inflated norm 2.0, got {norm}"
|
||
)
|
||
norm6 = float(np.linalg.norm(outputs[5].pose_covariance_6x6, ord="fro"))
|
||
assert norm6 == pytest.approx(1.0, abs=1e-9)
|
||
|
||
def test_no_inflation_when_no_reset_was_called(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
) -> None:
|
||
# Arrange — the wrapper without any reset call should pass through.
|
||
emit_cov = np.eye(6, dtype=np.float64) * (1.0 / np.sqrt(6))
|
||
fake_strategy.set_emit_covariance(emit_cov)
|
||
wired = _make_wired(
|
||
fake_strategy, store, save_period=100, warm_start_max_frames=5, inflation_factor=2.0
|
||
)
|
||
|
||
# Act
|
||
out = wired.process_frame(_make_frame(), _make_imu_window(), _make_calibration())
|
||
|
||
# Assert
|
||
norm = float(np.linalg.norm(out.pose_covariance_6x6, ord="fro"))
|
||
assert norm == pytest.approx(1.0, abs=1e-9)
|
||
|
||
|
||
class TestWiringAc8CovarianceFloor:
|
||
def test_post_reboot_floor_enforced_above_inflation_alone(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
fdr_sink: FakeFdrSink,
|
||
) -> None:
|
||
# Arrange — pre-reboot baseline X = 5.0; strategy emits norm 1.0
|
||
# so 2× inflation alone is only 2.0, well below X. Floor must
|
||
# bump every output up to ≥ 5.0.
|
||
baseline_x = 5.0
|
||
store.save(_make_hint(), pre_reboot_covariance_norm=baseline_x)
|
||
emit_cov = np.eye(6, dtype=np.float64) * (1.0 / np.sqrt(6))
|
||
fake_strategy.set_emit_covariance(emit_cov)
|
||
wired = _make_wired(
|
||
fake_strategy,
|
||
store,
|
||
warm_start_max_frames=5,
|
||
inflation_factor=2.0,
|
||
save_period=100,
|
||
)
|
||
|
||
# Act — F8 prime installs the floor, then 5 frames flow through
|
||
applied = prime_warm_start_from_disk(wired, store, fdr_client=fdr_sink)
|
||
assert applied is True
|
||
outputs = _drive_frames(wired, 5)
|
||
|
||
# Assert — every post-reset frame's emitted norm ≥ X
|
||
for i, out in enumerate(outputs):
|
||
norm = float(np.linalg.norm(out.pose_covariance_6x6, ord="fro"))
|
||
assert norm >= baseline_x - 1e-9, (
|
||
f"AC-8 floor breached on frame {i + 1}: norm {norm} < baseline {baseline_x}"
|
||
)
|
||
|
||
def test_post_reboot_floor_does_not_lower_when_inflation_alone_already_above(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
fdr_sink: FakeFdrSink,
|
||
) -> None:
|
||
# Arrange — baseline X = 0.5; strategy emits norm 1.0; inflation 2.0
|
||
# alone gives 2.0 which already exceeds X. Floor must NOT scale down.
|
||
baseline_x = 0.5
|
||
store.save(_make_hint(), pre_reboot_covariance_norm=baseline_x)
|
||
emit_cov = np.eye(6, dtype=np.float64) * (1.0 / np.sqrt(6))
|
||
fake_strategy.set_emit_covariance(emit_cov)
|
||
wired = _make_wired(fake_strategy, store, save_period=100)
|
||
|
||
# Act
|
||
applied = prime_warm_start_from_disk(wired, store, fdr_client=fdr_sink)
|
||
assert applied is True
|
||
outputs = _drive_frames(wired, 1)
|
||
|
||
# Assert — norm is the inflated value (2.0), NOT the baseline (0.5)
|
||
norm = float(np.linalg.norm(outputs[0].pose_covariance_6x6, ord="fro"))
|
||
assert norm == pytest.approx(2.0, abs=1e-9)
|
||
|
||
|
||
class TestWiringNfrNoCrash:
|
||
def test_fc_source_raising_does_not_crash(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
fdr_sink: FakeFdrSink,
|
||
caplog: Any,
|
||
) -> None:
|
||
# Arrange
|
||
source = _FakeFcSource(raise_with=RuntimeError("FC link down"))
|
||
wired = _make_wired(fake_strategy, store)
|
||
|
||
# Act
|
||
with caplog.at_level(logging.WARNING):
|
||
applied = prime_warm_start_from_fc(wired, source, store, fdr_client=fdr_sink)
|
||
|
||
# Assert — degrades to cold-start; process keeps running
|
||
assert applied is False
|
||
assert fake_strategy.reset_calls == []
|
||
warn_records = [
|
||
r
|
||
for r in caplog.records
|
||
if getattr(r, "kind", "") == "c1.warm_start.f2_takeoff_fc_unavailable"
|
||
]
|
||
assert len(warn_records) == 1
|
||
|
||
def test_fc_source_returning_none_does_not_crash(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
fdr_sink: FakeFdrSink,
|
||
caplog: Any,
|
||
) -> None:
|
||
# Arrange
|
||
source = _FakeFcSource(hint=None)
|
||
wired = _make_wired(fake_strategy, store)
|
||
|
||
# Act
|
||
with caplog.at_level(logging.WARNING):
|
||
applied = prime_warm_start_from_fc(wired, source, store, fdr_client=fdr_sink)
|
||
|
||
# Assert
|
||
assert applied is False
|
||
assert fake_strategy.reset_calls == []
|
||
|
||
def test_per_frame_save_failure_does_not_crash(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
caplog: Any,
|
||
) -> None:
|
||
# Arrange — a store whose save always raises
|
||
class _BoomStore:
|
||
def save(self, hint: WarmStartPose, *, pre_reboot_covariance_norm: float) -> None:
|
||
raise OSError("disk full")
|
||
|
||
def load(self) -> LoadedWarmStartHint | None:
|
||
return None
|
||
|
||
def clear(self) -> None:
|
||
return None
|
||
|
||
wired = _make_wired(fake_strategy, _BoomStore(), save_period=1) # type: ignore[arg-type]
|
||
|
||
# Act
|
||
with caplog.at_level(logging.ERROR):
|
||
out = wired.process_frame(_make_frame(), _make_imu_window(), _make_calibration())
|
||
|
||
# Assert — frame still emitted, error logged, no exception escapes
|
||
assert out is not None
|
||
err_records = [
|
||
r for r in caplog.records if getattr(r, "kind", "") == "c1.warm_start.save_failed"
|
||
]
|
||
assert len(err_records) == 1
|
||
|
||
def test_inner_strategy_reset_failure_does_not_crash_prime(
|
||
self,
|
||
fake_strategy: _FakeVioStrategy,
|
||
store: JsonSidecarWarmStartHintStore,
|
||
fdr_sink: FakeFdrSink,
|
||
caplog: Any,
|
||
) -> None:
|
||
# Arrange
|
||
store.save(_make_hint(), pre_reboot_covariance_norm=0.1)
|
||
fake_strategy.script_reset_failure(RuntimeError("native bridge boom"))
|
||
wired = _make_wired(fake_strategy, store)
|
||
|
||
# Act
|
||
with caplog.at_level(logging.ERROR):
|
||
applied = prime_warm_start_from_disk(wired, store, fdr_client=fdr_sink)
|
||
|
||
# Assert
|
||
assert applied is False
|
||
err_records = [
|
||
r for r in caplog.records if getattr(r, "kind", "") == "c1.warm_start.reset_failed"
|
||
]
|
||
assert len(err_records) == 1
|
||
|
||
|
||
class TestWiringForwarders:
|
||
def test_health_snapshot_forwards_to_inner(
|
||
self, fake_strategy: _FakeVioStrategy, store: JsonSidecarWarmStartHintStore
|
||
) -> None:
|
||
# Arrange
|
||
wired = _make_wired(fake_strategy, store)
|
||
|
||
# Assert
|
||
assert wired.health_snapshot().state == VioState.TRACKING
|
||
|
||
def test_current_strategy_label_forwards_to_inner(
|
||
self, fake_strategy: _FakeVioStrategy, store: JsonSidecarWarmStartHintStore
|
||
) -> None:
|
||
# Arrange
|
||
wired = _make_wired(fake_strategy, store)
|
||
|
||
# Assert
|
||
assert wired.current_strategy_label() == "klt_ransac"
|
||
|
||
def test_wrapper_constructor_rejects_inflation_factor_le_one(
|
||
self, fake_strategy: _FakeVioStrategy, store: JsonSidecarWarmStartHintStore
|
||
) -> None:
|
||
# Arrange + Act + Assert
|
||
with pytest.raises(ValueError):
|
||
WarmStartWiredStrategy(
|
||
inner=fake_strategy,
|
||
store=store,
|
||
warm_start_max_frames=5,
|
||
post_reset_covariance_inflation_factor=1.0,
|
||
warm_start_save_period_frames=5,
|
||
)
|
||
|
||
|
||
# ===========================================================================
|
||
# Hint-schema sanity guard.
|
||
|
||
|
||
class TestHintSchemaConstants:
|
||
def test_hint_schema_version_is_v1(self) -> None:
|
||
# Assert
|
||
assert HINT_SCHEMA_VERSION == 1
|
||
|
||
def test_hint_filename_is_canonical(self) -> None:
|
||
# Assert
|
||
assert HINT_FILENAME == "c1_warm_start.json"
|
||
|
||
def test_warm_start_fc_source_is_runtime_checkable(self) -> None:
|
||
# Arrange — local fake conforms to the runtime_checkable Protocol
|
||
source = _FakeFcSource(hint=_make_hint())
|
||
|
||
# Assert
|
||
assert isinstance(source, WarmStartFcSource)
|