|
|
|
@@ -0,0 +1,485 @@
|
|
|
|
|
"""AZ-625 — Phase E.5 of AZ-618: c5_isam2_graph_handle ordering.
|
|
|
|
|
|
|
|
|
|
Verifies the contract at
|
|
|
|
|
``_docs/02_tasks/todo/AZ-625_c5_isam2_graph_handle_ordering.md``:
|
|
|
|
|
|
|
|
|
|
* AC-625.1: ``build_pre_constructed(default_config)`` adds key
|
|
|
|
|
``c5_isam2_graph_handle`` on top of AZ-619..AZ-623; the value
|
|
|
|
|
satisfies the C4 :class:`ISam2GraphHandle` Protocol (``get_pose_key``,
|
|
|
|
|
``add_factor``, ``update``, ``compute_marginals``,
|
|
|
|
|
``last_anchor_age_ms``).
|
|
|
|
|
* AC-625.2: when the configured ``c5_state`` strategy's
|
|
|
|
|
``BUILD_STATE_*`` flag is OFF (or the strategy is unknown),
|
|
|
|
|
``build_pre_constructed`` raises :class:`AirborneBootstrapError`
|
|
|
|
|
whose message names the gating flag and the consuming component
|
|
|
|
|
slug ``c5_state``.
|
|
|
|
|
* AC-625.3: ``compose_root(config, pre_constructed=...)`` produces a
|
|
|
|
|
runtime where the handle held by C4 IS the same handle exposed by
|
|
|
|
|
the C5 estimator's ``_isam2_handle``. We exercise the cross-seam
|
|
|
|
|
identity invariant via the bootstrap's
|
|
|
|
|
``_c5_prebuilt_estimator`` look-aside key + the
|
|
|
|
|
``_c5_state_wrapper`` short-circuit, so the unit-test path does
|
|
|
|
|
not need to stand up the full ``compose_root`` graph (which would
|
|
|
|
|
pull in gtsam, FAISS, TensorRT — all out of scope per the AZ-625
|
|
|
|
|
task spec's Tier-2 Note).
|
|
|
|
|
|
|
|
|
|
AC-625.4 (this file exists with the above tests) is satisfied by the
|
|
|
|
|
existence of this module.
|
|
|
|
|
|
|
|
|
|
The tests stub the heavy ``build_state_estimator`` seam through
|
|
|
|
|
:func:`_build_c5_state_estimator_pair`'s ``__module__`` attribute path
|
|
|
|
|
so they exercise the bootstrap-error contract + identity-share
|
|
|
|
|
contract without standing up gtsam or constructing a real
|
|
|
|
|
:class:`GtsamIsam2StateEstimator`. The upstream AZ-619..AZ-623
|
|
|
|
|
builders are stubbed at the airborne_bootstrap module boundary,
|
|
|
|
|
mirroring the prior phase pattern (see
|
|
|
|
|
:mod:`tests.unit.runtime_root.test_az623_pre_constructed_phase_e`).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import dataclasses
|
|
|
|
|
from collections.abc import Iterator
|
|
|
|
|
from typing import Any
|
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from gps_denied_onboard.components.c4_pose._isam2_handle import (
|
|
|
|
|
ISam2GraphHandle as C4ISam2GraphHandle,
|
|
|
|
|
)
|
|
|
|
|
from gps_denied_onboard.config import Config
|
|
|
|
|
from gps_denied_onboard.fdr_client import client as fdr_client_module
|
|
|
|
|
from gps_denied_onboard.runtime_root import airborne_bootstrap
|
|
|
|
|
from gps_denied_onboard.runtime_root.airborne_bootstrap import (
|
|
|
|
|
AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS,
|
|
|
|
|
C5_STATE_BUILD_FLAGS,
|
|
|
|
|
AirborneBootstrapError,
|
|
|
|
|
build_pre_constructed,
|
|
|
|
|
clear_imu_preintegrator_cache,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
_C5_PREBUILT_ESTIMATOR_KEY = "_c5_prebuilt_estimator"
|
|
|
|
|
"""Mirror of ``airborne_bootstrap._C5_PREBUILT_ESTIMATOR_KEY`` for
|
|
|
|
|
test-side identity assertions. Internal coordination key (deliberately
|
|
|
|
|
NOT exposed via __all__); duplicated here so the test does not import
|
|
|
|
|
a private name.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _config_with_calibration_path(path: str) -> Config:
|
|
|
|
|
"""Return a fresh ``Config`` whose ``runtime.camera_calibration_path`` is set.
|
|
|
|
|
|
|
|
|
|
Mirrors the helper in
|
|
|
|
|
:mod:`tests.unit.runtime_root.test_az623_pre_constructed_phase_e` —
|
|
|
|
|
AZ-625 still walks the AZ-623 ``_build_c5_imu_preintegrator``
|
|
|
|
|
builder which empty-checks the same field.
|
|
|
|
|
"""
|
|
|
|
|
base = Config()
|
|
|
|
|
runtime = dataclasses.replace(base.runtime, camera_calibration_path=path)
|
|
|
|
|
return dataclasses.replace(base, runtime=runtime)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeIsam2GraphHandle:
|
|
|
|
|
"""Lightweight stand-in for the production
|
|
|
|
|
:class:`gps_denied_onboard.components.c5_state._isam2_handle.\
|
|
|
|
|
ISam2GraphHandleImpl` used by AC-625.1's Protocol-conformance
|
|
|
|
|
assertion.
|
|
|
|
|
|
|
|
|
|
Implements every method named by the C4 ``ISam2GraphHandle``
|
|
|
|
|
Protocol so :func:`isinstance` against the runtime-checkable
|
|
|
|
|
Protocol returns ``True`` — the production
|
|
|
|
|
:class:`ISam2GraphHandleImpl` is the same shape.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def get_pose_key(self, frame_id: int) -> int:
|
|
|
|
|
del frame_id
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
def add_factor(self, factor: Any) -> None:
|
|
|
|
|
del factor
|
|
|
|
|
|
|
|
|
|
def update(self, graph: Any, values: Any, timestamps: Any | None = None) -> None:
|
|
|
|
|
del graph, values, timestamps
|
|
|
|
|
|
|
|
|
|
def compute_marginals(self) -> Any:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def last_anchor_age_ms(self) -> int:
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeStateEstimator:
|
|
|
|
|
"""Stand-in for ``GtsamIsam2StateEstimator``.
|
|
|
|
|
|
|
|
|
|
The only attribute AC-625.3 inspects is ``_isam2_handle`` — the
|
|
|
|
|
production estimator stores the same handle there
|
|
|
|
|
(:class:`gps_denied_onboard.components.c5_state.gtsam_isam2_estimator.\
|
|
|
|
|
GtsamIsam2StateEstimator.__init__` builds
|
|
|
|
|
``ISam2GraphHandleImpl(self)`` and assigns it to ``self._isam2_handle``).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, handle: _FakeIsam2GraphHandle) -> None:
|
|
|
|
|
self._isam2_handle = handle
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _stub_state_pair(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> tuple[_FakeStateEstimator, _FakeIsam2GraphHandle]:
|
|
|
|
|
"""Replace ``_build_c5_state_estimator_pair`` with a fixed (estimator, handle).
|
|
|
|
|
|
|
|
|
|
Returns the same tuple values the stub injects, so individual
|
|
|
|
|
tests can perform identity-share assertions against them without
|
|
|
|
|
re-discovering the sentinels through ``pre_constructed``.
|
|
|
|
|
"""
|
|
|
|
|
handle = _FakeIsam2GraphHandle()
|
|
|
|
|
estimator = _FakeStateEstimator(handle)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_build_c5_state_estimator_pair",
|
|
|
|
|
lambda *_args, **_kwargs: (estimator, handle),
|
|
|
|
|
)
|
|
|
|
|
return estimator, handle
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _isolated_caches() -> Iterator[None]:
|
|
|
|
|
# Arrange: every test starts with empty FdrClient cache + empty
|
|
|
|
|
# ImuPreintegrator cache so the AZ-619..AZ-623 builders behind
|
|
|
|
|
# build_pre_constructed do not pick up stale instances.
|
|
|
|
|
fdr_client_module._reset_for_tests()
|
|
|
|
|
clear_imu_preintegrator_cache()
|
|
|
|
|
yield
|
|
|
|
|
fdr_client_module._reset_for_tests()
|
|
|
|
|
clear_imu_preintegrator_cache()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _stub_az619_to_az623_builders(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
|
|
# Arrange: stub the AZ-620 (Phase B) C6 builders, AZ-621 (Phase C) C7
|
|
|
|
|
# inference builder, AZ-622 (Phase D) C3 builders, and AZ-623 (Phase E)
|
|
|
|
|
# c5 helper builders so AZ-625 stays focused on the Phase E.5 contract.
|
|
|
|
|
# Sentinels are opaque — AZ-625 assertions never inspect them.
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_build_c6_descriptor_index",
|
|
|
|
|
lambda _config: MagicMock(name="DescriptorIndex"),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_build_c6_tile_store",
|
|
|
|
|
lambda _config: MagicMock(name="TileStore"),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_build_c7_inference",
|
|
|
|
|
lambda _config: MagicMock(name="InferenceRuntime"),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_build_c3_lightglue_runtime",
|
|
|
|
|
lambda _config, *, inference_runtime: MagicMock(name="LightGlueRuntime"),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_build_c3_feature_extractor",
|
|
|
|
|
lambda _config: MagicMock(name="FeatureExtractor"),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_build_c282_ransac_filter",
|
|
|
|
|
lambda _config: MagicMock(name="RansacFilter"),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_build_c5_imu_preintegrator",
|
|
|
|
|
lambda _config: MagicMock(name="ImuPreintegrator"),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_build_c5_se3_utils",
|
|
|
|
|
lambda _config: MagicMock(name="Se3Utils"),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_build_c5_wgs_converter",
|
|
|
|
|
lambda _config: MagicMock(name="WgsConverter"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ac_625_1_adds_c5_isam2_graph_handle_with_protocol_surface(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
# Arrange: stub the (estimator, handle) pair builder; default Config()
|
|
|
|
|
# with a populated camera_calibration_path so AZ-623's
|
|
|
|
|
# _build_c5_imu_preintegrator empty-check passes (autouse fixture has
|
|
|
|
|
# already stubbed the builder, so the path is only structurally set).
|
|
|
|
|
config = _config_with_calibration_path("/tmp/az625-fixture-calib.json")
|
|
|
|
|
_, handle = _stub_state_pair(monkeypatch)
|
|
|
|
|
|
|
|
|
|
# Act
|
|
|
|
|
pre_constructed = build_pre_constructed(config)
|
|
|
|
|
|
|
|
|
|
# Assert: the new key is present and identifies the stubbed handle.
|
|
|
|
|
assert "c5_isam2_graph_handle" in pre_constructed
|
|
|
|
|
assert pre_constructed["c5_isam2_graph_handle"] is handle
|
|
|
|
|
|
|
|
|
|
# Protocol-conformance: the handle must satisfy the C4 consumer's
|
|
|
|
|
# runtime-checkable Protocol — get_pose_key + add_factor + update +
|
|
|
|
|
# compute_marginals + last_anchor_age_ms.
|
|
|
|
|
assert isinstance(handle, C4ISam2GraphHandle), (
|
|
|
|
|
"c5_isam2_graph_handle must satisfy the C4 ISam2GraphHandle Protocol; "
|
|
|
|
|
f"missing attributes on {type(handle).__name__}"
|
|
|
|
|
)
|
|
|
|
|
for method_name in (
|
|
|
|
|
"get_pose_key",
|
|
|
|
|
"add_factor",
|
|
|
|
|
"update",
|
|
|
|
|
"compute_marginals",
|
|
|
|
|
"last_anchor_age_ms",
|
|
|
|
|
):
|
|
|
|
|
assert hasattr(handle, method_name), (
|
|
|
|
|
f"c5_isam2_graph_handle is missing {method_name!r}; "
|
|
|
|
|
f"the C4 consumer (OpenCVGtsamPoseEstimator) dispatches via attribute access"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# AZ-619..AZ-623 keys remain populated (additivity invariant).
|
|
|
|
|
assert {
|
|
|
|
|
"c13_fdr",
|
|
|
|
|
"clock",
|
|
|
|
|
"c6_descriptor_index",
|
|
|
|
|
"c6_tile_store",
|
|
|
|
|
"c7_inference",
|
|
|
|
|
"c3_lightglue_runtime",
|
|
|
|
|
"c3_feature_extractor",
|
|
|
|
|
"c282_ransac_filter",
|
|
|
|
|
"c5_imu_preintegrator",
|
|
|
|
|
"c5_se3_utils",
|
|
|
|
|
"c5_wgs_converter",
|
|
|
|
|
}.issubset(pre_constructed.keys()), (
|
|
|
|
|
f"AZ-625 must be additive on top of AZ-619..AZ-623; got "
|
|
|
|
|
f"keys: {sorted(pre_constructed.keys())}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ac_625_1_internal_prebuilt_estimator_key_not_in_required_keys() -> None:
|
|
|
|
|
"""The ``_c5_prebuilt_estimator`` look-aside key is internal coordination
|
|
|
|
|
only — it MUST NOT appear under
|
|
|
|
|
:data:`AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS` because no consuming
|
|
|
|
|
component queries it via the public surface (only
|
|
|
|
|
``_c5_state_wrapper`` consults it as a fast path).
|
|
|
|
|
"""
|
|
|
|
|
# Assert: the look-aside key is not in any consumer's required-keys row.
|
|
|
|
|
for slug, required_keys in AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS.items():
|
|
|
|
|
assert _C5_PREBUILT_ESTIMATOR_KEY not in required_keys, (
|
|
|
|
|
f"{_C5_PREBUILT_ESTIMATOR_KEY!r} is an internal coordination key; "
|
|
|
|
|
f"it must not be exposed via "
|
|
|
|
|
f"AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS[{slug!r}]"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ac_625_2_build_state_gtsam_isam2_off_raises_named_error(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
# Arrange: explicitly set BUILD_STATE_GTSAM_ISAM2=OFF (the gating
|
|
|
|
|
# flag check uses os.environ.get(flag, "ON").upper() == "OFF" — same
|
|
|
|
|
# default ladder as state_factory). Default config resolves to
|
|
|
|
|
# gtsam_isam2 strategy. Do NOT stub _build_c5_state_estimator_pair
|
|
|
|
|
# here — we want the real flag-OFF guard to fire.
|
|
|
|
|
monkeypatch.setenv("BUILD_STATE_GTSAM_ISAM2", "OFF")
|
|
|
|
|
config = _config_with_calibration_path("/tmp/az625-flag-off-fixture-calib.json")
|
|
|
|
|
|
|
|
|
|
# Act + Assert
|
|
|
|
|
with pytest.raises(AirborneBootstrapError) as excinfo:
|
|
|
|
|
build_pre_constructed(config)
|
|
|
|
|
|
|
|
|
|
message = str(excinfo.value)
|
|
|
|
|
# The missing key, the gating flag, and the consuming component
|
|
|
|
|
# slug must all appear in the operator-facing message.
|
|
|
|
|
assert "c5_isam2_graph_handle" in message
|
|
|
|
|
assert C5_STATE_BUILD_FLAGS["gtsam_isam2"] in message, (
|
|
|
|
|
f"BUILD_STATE_GTSAM_ISAM2 missing from error: {message!r}"
|
|
|
|
|
)
|
|
|
|
|
assert "c5_state" in message
|
|
|
|
|
# The flag-OFF branch raises directly — there is no upstream cause.
|
|
|
|
|
assert excinfo.value.__cause__ is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ac_625_2_unknown_strategy_raises_named_error_with_supported_set(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Defence-in-depth for the strategy-resolution path.
|
|
|
|
|
|
|
|
|
|
Even when BUILD_STATE_* flags are ON, an unknown C5 strategy must
|
|
|
|
|
raise :class:`AirborneBootstrapError` naming the supported set
|
|
|
|
|
(``gtsam_isam2`` / ``eskf``). The error must fire BEFORE
|
|
|
|
|
``build_state_estimator`` is consulted — otherwise the operator
|
|
|
|
|
sees ``StateEstimatorConfigError`` rather than the bootstrap-error
|
|
|
|
|
contract this module owns.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Arrange: smuggle an unknown strategy past the upstream
|
|
|
|
|
# KNOWN_STATE_STRATEGIES validator (lives in
|
|
|
|
|
# ``components.c5_state.config``) by stubbing the bootstrap's own
|
|
|
|
|
# strategy resolver. The bootstrap's own check is what we want to
|
|
|
|
|
# exercise here.
|
|
|
|
|
config = _config_with_calibration_path("/tmp/az625-unknown-fixture.json")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_resolve_c5_state_strategy",
|
|
|
|
|
lambda _config: "lqg_baseline_does_not_exist",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Act + Assert
|
|
|
|
|
with pytest.raises(AirborneBootstrapError) as excinfo:
|
|
|
|
|
build_pre_constructed(config)
|
|
|
|
|
|
|
|
|
|
message = str(excinfo.value)
|
|
|
|
|
assert "c5_isam2_graph_handle" in message
|
|
|
|
|
assert "lqg_baseline_does_not_exist" in message
|
|
|
|
|
assert "c5_state" in message
|
|
|
|
|
# The supported set is enumerated so the operator sees the fix.
|
|
|
|
|
for supported in C5_STATE_BUILD_FLAGS:
|
|
|
|
|
assert supported in message, (
|
|
|
|
|
f"supported strategy {supported!r} missing from error: {message!r}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ac_625_2_build_state_estimator_config_error_wraps_into_bootstrap_error(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""When the BUILD flag is ON but the state-factory itself rejects the
|
|
|
|
|
config, ``StateEstimatorConfigError`` must wrap into
|
|
|
|
|
:class:`AirborneBootstrapError` with the cause chain preserved
|
|
|
|
|
(mirrors AZ-621 / AZ-622's wrapping pattern)."""
|
|
|
|
|
# Arrange
|
|
|
|
|
from gps_denied_onboard.components.c5_state.errors import (
|
|
|
|
|
StateEstimatorConfigError,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setenv("BUILD_STATE_GTSAM_ISAM2", "ON")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_ensure_state_strategy_registered",
|
|
|
|
|
lambda _config: None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _raise_config_error(*_args: Any, **_kwargs: Any) -> Any:
|
|
|
|
|
raise StateEstimatorConfigError("simulated state factory rejection")
|
|
|
|
|
|
|
|
|
|
# _build_c5_state_estimator_pair calls the imported reference
|
|
|
|
|
# ``build_state_estimator`` from the airborne_bootstrap module
|
|
|
|
|
# namespace; monkeypatch that attribute directly.
|
|
|
|
|
monkeypatch.setattr(airborne_bootstrap, "build_state_estimator", _raise_config_error)
|
|
|
|
|
config = _config_with_calibration_path("/tmp/az625-cfg-err-fixture.json")
|
|
|
|
|
|
|
|
|
|
# Act + Assert
|
|
|
|
|
with pytest.raises(AirborneBootstrapError) as excinfo:
|
|
|
|
|
build_pre_constructed(config)
|
|
|
|
|
|
|
|
|
|
message = str(excinfo.value)
|
|
|
|
|
assert "c5_isam2_graph_handle" in message
|
|
|
|
|
assert "c5_state" in message
|
|
|
|
|
assert "gtsam_isam2" in message
|
|
|
|
|
assert isinstance(excinfo.value.__cause__, StateEstimatorConfigError)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ac_625_3_handle_is_same_object_as_estimator_isam2_handle(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Cross-seam identity: the handle that c4_pose receives via
|
|
|
|
|
``pre_constructed['c5_isam2_graph_handle']`` IS the SAME object
|
|
|
|
|
that the c5_state estimator exposes as ``_isam2_handle``.
|
|
|
|
|
|
|
|
|
|
AC-625.3's ``compose_root(...)`` end-to-end form requires gtsam +
|
|
|
|
|
FAISS + TensorRT and is exercised by the Jetson tier-2 e2e harness
|
|
|
|
|
(AZ-624 AC-5). At unit-test scope the equivalent invariant is
|
|
|
|
|
``pre_constructed[_c5_prebuilt_estimator]._isam2_handle is
|
|
|
|
|
pre_constructed['c5_isam2_graph_handle']`` — which is what the
|
|
|
|
|
bootstrap's seeding ordering must guarantee.
|
|
|
|
|
"""
|
|
|
|
|
# Arrange
|
|
|
|
|
config = _config_with_calibration_path("/tmp/az625-identity-fixture.json")
|
|
|
|
|
estimator, handle = _stub_state_pair(monkeypatch)
|
|
|
|
|
|
|
|
|
|
# Act
|
|
|
|
|
pre_constructed = build_pre_constructed(config)
|
|
|
|
|
|
|
|
|
|
# Assert: identity-share across the C4 / C5 seam.
|
|
|
|
|
assert pre_constructed["c5_isam2_graph_handle"] is handle
|
|
|
|
|
assert pre_constructed[_C5_PREBUILT_ESTIMATOR_KEY] is estimator
|
|
|
|
|
assert (
|
|
|
|
|
pre_constructed[_C5_PREBUILT_ESTIMATOR_KEY]._isam2_handle
|
|
|
|
|
is (pre_constructed["c5_isam2_graph_handle"])
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ac_625_3_c5_state_wrapper_short_circuits_on_prebuilt_estimator(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""The :func:`_c5_state_wrapper` returns the prebuilt estimator as-is
|
|
|
|
|
when ``_c5_prebuilt_estimator`` is present in ``constructed``.
|
|
|
|
|
|
|
|
|
|
This is the seam that lets AC-625.3 hold under the live
|
|
|
|
|
``compose_root`` topo walk: the C5 wrapper does NOT re-invoke
|
|
|
|
|
:func:`build_state_estimator` (which would produce a different
|
|
|
|
|
``(estimator, handle)`` pair than the one C4 has already
|
|
|
|
|
consumed). Verified by ensuring the wrapper does not consult the
|
|
|
|
|
fallback infra-key set at all when the look-aside key is set.
|
|
|
|
|
"""
|
|
|
|
|
# Arrange: build a constructed dict with ONLY the look-aside key.
|
|
|
|
|
# If the wrapper short-circuits correctly it must NOT read any of
|
|
|
|
|
# c5_imu_preintegrator / c5_se3_utils / c5_wgs_converter / c13_fdr —
|
|
|
|
|
# the absence of those keys would otherwise raise
|
|
|
|
|
# AirborneBootstrapError via _require.
|
|
|
|
|
estimator, _handle = _stub_state_pair(monkeypatch)
|
|
|
|
|
constructed = {_C5_PREBUILT_ESTIMATOR_KEY: estimator}
|
|
|
|
|
|
|
|
|
|
# Act
|
|
|
|
|
returned = airborne_bootstrap._c5_state_wrapper(Config(), constructed)
|
|
|
|
|
|
|
|
|
|
# Assert
|
|
|
|
|
assert returned is estimator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_ac_625_3_c5_state_wrapper_falls_back_when_prebuilt_absent(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""The :func:`_c5_state_wrapper` falls back to the original
|
|
|
|
|
:func:`build_state_estimator` path when ``_c5_prebuilt_estimator``
|
|
|
|
|
is absent.
|
|
|
|
|
|
|
|
|
|
Test isolation contract: existing fixtures (e.g. the
|
|
|
|
|
``test_az401_compose_root_replay`` suite) seed ``pre_constructed``
|
|
|
|
|
manually without going through :func:`build_pre_constructed` and
|
|
|
|
|
therefore have no look-aside key. The fallback must still work —
|
|
|
|
|
AZ-625's seam is additive, never replacing the existing one.
|
|
|
|
|
"""
|
|
|
|
|
# Arrange: no _c5_prebuilt_estimator. Stub the heavy
|
|
|
|
|
# build_state_estimator seam so the fallback path returns
|
|
|
|
|
# deterministically.
|
|
|
|
|
fallback_estimator = _FakeStateEstimator(_FakeIsam2GraphHandle())
|
|
|
|
|
fallback_handle = MagicMock(name="FallbackHandle")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"_ensure_state_strategy_registered",
|
|
|
|
|
lambda _config: None,
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
airborne_bootstrap,
|
|
|
|
|
"build_state_estimator",
|
|
|
|
|
lambda *_args, **_kwargs: (fallback_estimator, fallback_handle),
|
|
|
|
|
)
|
|
|
|
|
constructed = {
|
|
|
|
|
"c5_imu_preintegrator": MagicMock(name="ImuPreintegrator"),
|
|
|
|
|
"c5_se3_utils": MagicMock(name="Se3Utils"),
|
|
|
|
|
"c5_wgs_converter": MagicMock(name="WgsConverter"),
|
|
|
|
|
"c13_fdr": MagicMock(name="FdrClient"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Act
|
|
|
|
|
returned = airborne_bootstrap._c5_state_wrapper(Config(), constructed)
|
|
|
|
|
|
|
|
|
|
# Assert: fallback path returned the build_state_estimator-built
|
|
|
|
|
# estimator (not the prebuilt sentinel — there is none).
|
|
|
|
|
assert returned is fallback_estimator
|