[AZ-623] [AZ-625] Phase E: c282_ransac + c5 helpers; split handle work

Wire 4 stateless / cached helpers into airborne_bootstrap.build_pre_constructed:
c282_ransac_filter, c5_imu_preintegrator (cached on calibration path),
c5_se3_utils (helpers.se3_utils module as namespace handle), c5_wgs_converter.

The original AZ-623 5th deliverable (c5_isam2_graph_handle) hit an
unresolvable construction-order conflict between c4_pose (consumes the handle)
and c5_state (creates it inside build_state_estimator's tuple return) under
the umbrella's "MUST NOT touch any per-component factory signature" constraint.
Per AZ-623 spec's escalation gate, scope was split: AZ-625 captures the handle
ordering work; AZ-624 dependency edge updated to require both.

Tests: tests/unit/runtime_root/test_az623_pre_constructed_phase_e.py adds 7
tests covering AC-623.1..3 (4 new keys + correct types, IMU preintegrator
caching, operator-actionable error messages for empty / unreadable / malformed
calibration paths). Autouse stubs added to test_az619/620/621/622 so prior
phase tests remain isolated from new builders.

Quality gates: ruff format clean, ruff lint clean, 24/24 phase tests pass,
247/247 runtime_root + c5_state regression suite passes. Code review verdict
PASS_WITH_WARNINGS (3 Low findings; full report in
_docs/03_implementation/reviews/batch_94_review.md).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-19 09:20:28 +03:00
parent 5c4d129f80
commit 02208c577e
13 changed files with 1014 additions and 151 deletions
@@ -59,12 +59,8 @@ def _stub_c6_builders(monkeypatch: pytest.MonkeyPatch) -> None:
# Config() used below would hit KeyError inside storage_factory's
# config.components["c6_tile_cache"] lookup. Sentinel objects are
# opaque on purpose — the AZ-619 assertions never inspect them.
monkeypatch.setattr(
airborne_bootstrap, "_build_c6_descriptor_index", lambda _config: object()
)
monkeypatch.setattr(
airborne_bootstrap, "_build_c6_tile_store", lambda _config: object()
)
monkeypatch.setattr(airborne_bootstrap, "_build_c6_descriptor_index", lambda _config: object())
monkeypatch.setattr(airborne_bootstrap, "_build_c6_tile_store", lambda _config: object())
@pytest.fixture(autouse=True)
@@ -75,9 +71,7 @@ def _stub_c7_inference_builder(monkeypatch: pytest.MonkeyPatch) -> None:
# config.components["c7_inference"] lookup, AND the airborne BUILD_*
# flags are typically unset in the test env. The sentinel is opaque
# on purpose — AZ-619 assertions never inspect it.
monkeypatch.setattr(
airborne_bootstrap, "_build_c7_inference", lambda _config: object()
)
monkeypatch.setattr(airborne_bootstrap, "_build_c7_inference", lambda _config: object())
@pytest.fixture(autouse=True)
@@ -93,9 +87,22 @@ def _stub_c3_matcher_builders(monkeypatch: pytest.MonkeyPatch) -> None:
"_build_c3_lightglue_runtime",
lambda _config, *, inference_runtime: object(),
)
monkeypatch.setattr(
airborne_bootstrap, "_build_c3_feature_extractor", lambda _config: object()
)
monkeypatch.setattr(airborne_bootstrap, "_build_c3_feature_extractor", lambda _config: object())
@pytest.fixture(autouse=True)
def _stub_c5_builders(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange: stub the AZ-623 Phase E c5 / RANSAC builders so the AZ-619
# tests stay focused on Phase A. Without this the bare Config() below
# would hit _build_c5_imu_preintegrator's
# config.runtime.camera_calibration_path empty-check and raise
# AirborneBootstrapError before the AC-619 keys could be asserted.
# Sentinels are opaque on purpose — AZ-619 assertions never inspect
# them.
monkeypatch.setattr(airborne_bootstrap, "_build_c282_ransac_filter", lambda _config: object())
monkeypatch.setattr(airborne_bootstrap, "_build_c5_imu_preintegrator", lambda _config: object())
monkeypatch.setattr(airborne_bootstrap, "_build_c5_se3_utils", lambda _config: object())
monkeypatch.setattr(airborne_bootstrap, "_build_c5_wgs_converter", lambda _config: object())
def test_ac_619_1_default_config_seeds_c13_fdr_and_clock() -> None:
@@ -95,6 +95,37 @@ def _stub_c3_matcher_builders(monkeypatch: pytest.MonkeyPatch) -> None:
)
@pytest.fixture(autouse=True)
def _stub_c5_builders(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange: stub the AZ-623 Phase E c5 / RANSAC builders so AZ-620
# tests stay focused on the Phase B contract. Without this the configs
# used below would hit _build_c5_imu_preintegrator's
# config.runtime.camera_calibration_path empty-check and raise
# AirborneBootstrapError before the AC-620 keys could be asserted.
# MagicMock sentinels are opaque on purpose — AZ-620 assertions
# never inspect them.
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_620_1_adds_c6_descriptor_index_and_c6_tile_store(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -98,6 +98,37 @@ def _stub_c3_matcher_builders(monkeypatch: pytest.MonkeyPatch) -> None:
)
@pytest.fixture(autouse=True)
def _stub_c5_builders(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange: stub the AZ-623 Phase E c5 / RANSAC builders so AZ-621
# tests stay focused on the Phase C contract. Without this the configs
# used below would hit _build_c5_imu_preintegrator's
# config.runtime.camera_calibration_path empty-check and raise
# AirborneBootstrapError before the AC-621 keys could be asserted.
# Sentinels are opaque on purpose — AZ-621 assertions never inspect
# them.
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_621_1_adds_c7_inference(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange: stub build_inference_runtime to a sentinel so we can
# assert wiring without standing up real GPU/TensorRT/PyTorch.
@@ -133,9 +164,7 @@ def test_ac_621_2_both_build_flags_off_with_configured_consumer_raises_named_err
"build_inference_runtime",
_raise_no_c7_runtime_available,
)
config = Config.with_blocks(
c3_matcher=_C3MatcherBlock(strategy="disk_lightglue")
)
config = Config.with_blocks(c3_matcher=_C3MatcherBlock(strategy="disk_lightglue"))
# Act + Assert
with pytest.raises(AirborneBootstrapError) as excinfo:
@@ -104,6 +104,35 @@ def _stub_c6_and_c7_builders(monkeypatch: pytest.MonkeyPatch) -> None:
)
@pytest.fixture(autouse=True)
def _stub_c5_builders(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange: stub the AZ-623 Phase E c5 / RANSAC builders so AZ-622
# tests stay focused on the Phase D contract. Without this the configs
# used below would hit _build_c5_imu_preintegrator's
# config.runtime.camera_calibration_path empty-check and raise
# AirborneBootstrapError before the AC-622 keys could be asserted.
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_622_1_adds_c3_lightglue_runtime_and_c3_feature_extractor(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -130,10 +159,7 @@ def test_ac_622_1_adds_c3_lightglue_runtime_and_c3_feature_extractor(
assert "c3_lightglue_runtime" in pre_constructed
assert "c3_feature_extractor" in pre_constructed
assert isinstance(pre_constructed["c3_lightglue_runtime"], LightGlueRuntime)
assert (
pre_constructed["c3_lightglue_runtime"].descriptor_dim()
== engine_handle.descriptor_dim
)
assert pre_constructed["c3_lightglue_runtime"].descriptor_dim() == engine_handle.descriptor_dim
assert isinstance(pre_constructed["c3_feature_extractor"], FeatureExtractor)
assert isinstance(pre_constructed["c3_feature_extractor"], OpenCvOrbExtractor)
assert {
@@ -154,9 +180,7 @@ def test_ac_622_2_build_flag_off_with_configured_strategy_raises_named_error(
# fire BEFORE _load_lightglue_engine_handle is consulted, so we don't
# need to stub the loader for this branch.
monkeypatch.delenv("BUILD_MATCHER_DISK_LIGHTGLUE", raising=False)
config = Config.with_blocks(
c3_matcher=_C3MatcherBlock(strategy="disk_lightglue")
)
config = Config.with_blocks(c3_matcher=_C3MatcherBlock(strategy="disk_lightglue"))
# Act + Assert
with pytest.raises(AirborneBootstrapError) as excinfo:
@@ -168,9 +192,7 @@ def test_ac_622_2_build_flag_off_with_configured_strategy_raises_named_error(
# AC-622.2 + AZ-618 NFR "operator-facing error contract").
assert "c3_lightglue_runtime" in message
expected_flag = C3_MATCHER_BUILD_FLAGS["disk_lightglue"]
assert expected_flag in message, (
f"{expected_flag!r} missing from error: {message!r}"
)
assert expected_flag in message, f"{expected_flag!r} missing from error: {message!r}"
assert "c3_matcher" in message
# The flag-OFF branch raises directly — there is no upstream cause to
# preserve (cause-chain preservation is exercised in
@@ -184,9 +206,7 @@ def test_ac_622_2_build_flag_off_with_aliked_strategy_names_aliked_flag(
"""Per-strategy flag specificity: aliked_lightglue surfaces ALIKED's flag."""
# Arrange: ALIKED strategy with its own gating flag OFF.
monkeypatch.delenv("BUILD_MATCHER_ALIKED_LIGHTGLUE", raising=False)
config = Config.with_blocks(
c3_matcher=_C3MatcherBlock(strategy="aliked_lightglue")
)
config = Config.with_blocks(c3_matcher=_C3MatcherBlock(strategy="aliked_lightglue"))
# Act + Assert
with pytest.raises(AirborneBootstrapError) as excinfo:
@@ -245,9 +265,7 @@ def test_ac_622_2_lightglue_engine_load_failure_wraps_runtime_error(
"_load_lightglue_engine_handle",
_raise_engine_load_failure,
)
config = Config.with_blocks(
c3_matcher=_C3MatcherBlock(strategy="disk_lightglue")
)
config = Config.with_blocks(c3_matcher=_C3MatcherBlock(strategy="disk_lightglue"))
# Act + Assert
with pytest.raises(AirborneBootstrapError) as excinfo:
@@ -281,9 +299,7 @@ def test_lightglue_runtime_uses_c7_inference_from_pre_constructed(
captured["inference_runtime"] = inference_runtime
return _make_engine_handle_mock(descriptor_dim=128)
monkeypatch.setattr(
airborne_bootstrap, "_load_lightglue_engine_handle", _capture_loader
)
monkeypatch.setattr(airborne_bootstrap, "_load_lightglue_engine_handle", _capture_loader)
config = Config()
# Act
@@ -0,0 +1,301 @@
"""AZ-623 — Phase E of AZ-618: ``build_pre_constructed`` seeds c282_ransac_filter + 3 c5 helpers.
Verifies the contract at
``_docs/02_tasks/todo/AZ-623_pre_constructed_phase_e_ransac_c5_helpers.md``
(scope-narrowed 2026-05-19; ``c5_isam2_graph_handle`` deferred to AZ-625):
* AC-623.1: ``build_pre_constructed(default_config)`` adds
``c282_ransac_filter`` (a :class:`RansacFilter` instance),
``c5_imu_preintegrator`` (an :class:`ImuPreintegrator` instance),
``c5_se3_utils`` (the :mod:`gps_denied_onboard.helpers.se3_utils`
module), and ``c5_wgs_converter`` (a :class:`WgsConverter` instance)
on top of AZ-619..AZ-622.
* AC-623.2: invoking twice in the same process produces dicts where
``c5_imu_preintegrator`` is the SAME instance both calls (cached by
``config.runtime.camera_calibration_path``); the 3 stateless helpers
may be either fresh or cached.
* AC-623.3: when ``config.runtime.camera_calibration_path`` is empty or
unreadable, ``_build_c5_imu_preintegrator`` raises
:class:`AirborneBootstrapError` whose message names the missing input
AND the consuming component slug ``c5_state``.
AC-623.4 (this file exists with the above tests) is satisfied by the
existence of this module.
The tests stub the heavy ``_load_camera_calibration`` seam so they
exercise the helper-wiring contract without standing up an on-disk
calibration JSON. The upstream AZ-619..AZ-622 builders are stubbed at
the airborne_bootstrap module boundary, mirroring the prior phase
pattern (see :mod:`tests.unit.runtime_root.test_az622_pre_constructed_phase_d`).
"""
from __future__ import annotations
import dataclasses
from collections.abc import Iterator
from pathlib import Path
from unittest.mock import MagicMock
import numpy as np
import pytest
from gps_denied_onboard._types.calibration import CameraCalibration
from gps_denied_onboard.config import Config
from gps_denied_onboard.fdr_client import client as fdr_client_module
from gps_denied_onboard.helpers import se3_utils as se3_utils_module
from gps_denied_onboard.helpers.imu_preintegrator import ImuPreintegrator
from gps_denied_onboard.helpers.ransac_filter import RansacFilter
from gps_denied_onboard.helpers.wgs_converter import WgsConverter
from gps_denied_onboard.runtime_root import airborne_bootstrap
from gps_denied_onboard.runtime_root.airborne_bootstrap import (
AirborneBootstrapError,
build_pre_constructed,
clear_imu_preintegrator_cache,
)
def _config_with_calibration_path(path: str) -> Config:
"""Return a fresh ``Config`` whose ``runtime.camera_calibration_path`` is set.
``RuntimeConfig`` is a frozen dataclass; mutate via
:func:`dataclasses.replace` rather than ``object.__setattr__``.
"""
base = Config()
runtime = dataclasses.replace(base.runtime, camera_calibration_path=path)
return dataclasses.replace(base, runtime=runtime)
def _make_calibration(camera_id: str = "az623-test-camera") -> CameraCalibration:
"""Sentinel CameraCalibration matching the C5 estimator's expected shape.
The bias / sample accumulator inside :class:`ImuPreintegrator` only
needs intrinsics-shape inputs to be present; AZ-623 tests do not
integrate samples, so the contents are irrelevant beyond passing
:func:`make_imu_preintegrator`'s own internal validators.
"""
return CameraCalibration(
camera_id=camera_id,
intrinsics_3x3=np.eye(3, dtype=np.float64),
distortion=np.zeros(5, dtype=np.float64),
body_to_camera_se3=np.eye(4, dtype=np.float64),
acquisition_method="operator",
metadata={},
)
@pytest.fixture(autouse=True)
def _isolated_caches() -> Iterator[None]:
# Arrange: every test starts with empty FdrClient cache + empty
# ImuPreintegrator cache so AC-623.2's "same instance across calls"
# assertion is exercised against fresh state.
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_az622_builders(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange: stub the AZ-619 (clock seeded by build_pre_constructed
# directly via WallClock — no builder), AZ-620 (Phase B) C6 builders,
# AZ-621 (Phase C) C7 inference builder, and AZ-622 (Phase D) C3
# builders so AZ-623 stays focused on the Phase E contract. Sentinels
# are opaque on purpose — AZ-623 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"),
)
def test_ac_623_1_adds_c282_ransac_and_c5_helpers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Arrange: a Config with a populated camera_calibration_path so
# _build_c5_imu_preintegrator's empty-check passes. Stub
# _load_camera_calibration to return a sentinel calibration so the
# bootstrap does not touch the filesystem.
config = _config_with_calibration_path("/tmp/az623-fixture-calib.json")
monkeypatch.setattr(
airborne_bootstrap,
"_load_camera_calibration",
lambda _config: _make_calibration(),
)
# Act
pre_constructed = build_pre_constructed(config)
# Assert: each new key is present and typed per AC-623.1.
assert "c282_ransac_filter" in pre_constructed
assert isinstance(pre_constructed["c282_ransac_filter"], RansacFilter)
assert "c5_imu_preintegrator" in pre_constructed
assert isinstance(pre_constructed["c5_imu_preintegrator"], ImuPreintegrator)
assert "c5_se3_utils" in pre_constructed
se3_utils = pre_constructed["c5_se3_utils"]
# The se3_utils handle is the helpers.se3_utils module — a module is
# the simplest namespace object that exposes exp_map, log_map etc.
# as attributes (matches the MagicMock fixture pattern existing C5
# tests already use).
assert se3_utils is se3_utils_module
for func_name in ("exp_map", "log_map", "matrix_to_se3", "se3_to_matrix"):
assert hasattr(se3_utils, func_name), (
f"c5_se3_utils handle is missing {func_name!r}; "
f"consumers (build_state_estimator, build_pose_estimator) "
f"dispatch via attribute access."
)
assert "c5_wgs_converter" in pre_constructed
assert isinstance(pre_constructed["c5_wgs_converter"], WgsConverter)
def test_ac_623_1_keeps_existing_keys_intact(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
config = _config_with_calibration_path("/tmp/az623-fixture-calib.json")
monkeypatch.setattr(
airborne_bootstrap,
"_load_camera_calibration",
lambda _config: _make_calibration(),
)
# Act
pre_constructed = build_pre_constructed(config)
# Assert: AZ-619..AZ-622 keys remain populated; AZ-623 is additive.
assert {
"c13_fdr",
"clock",
"c6_descriptor_index",
"c6_tile_store",
"c7_inference",
"c3_lightglue_runtime",
"c3_feature_extractor",
}.issubset(pre_constructed.keys()), (
f"AZ-623 must be additive on top of AZ-619..AZ-622; got "
f"keys: {sorted(pre_constructed.keys())}"
)
def test_ac_623_2_imu_preintegrator_cached_across_calls(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Arrange: stub the calibration loader with a counter so we can
# assert the cache short-circuit fires on the second call.
config = _config_with_calibration_path("/tmp/az623-cache-fixture.json")
load_count = {"n": 0}
def _counting_load(_config: Config) -> CameraCalibration:
load_count["n"] += 1
return _make_calibration()
monkeypatch.setattr(airborne_bootstrap, "_load_camera_calibration", _counting_load)
# Act
first = build_pre_constructed(config)
second = build_pre_constructed(config)
# Assert: cache holds the same ImuPreintegrator instance; the
# calibration loader fires exactly once across the two calls.
assert first["c5_imu_preintegrator"] is second["c5_imu_preintegrator"], (
"AC-623.2 requires c5_imu_preintegrator to be the same instance "
"across two build_pre_constructed calls (cache keyed on "
"camera_calibration_path)"
)
assert load_count["n"] == 1, (
f"calibration loader should fire once (cached on second call); "
f"got {load_count['n']} invocations"
)
def test_ac_623_2_imu_preintegrator_per_path_cache(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Arrange: two configs with DIFFERENT calibration paths. The cache
# must produce DIFFERENT preintegrator instances (one per path).
config_a = _config_with_calibration_path("/tmp/az623-path-a.json")
config_b = _config_with_calibration_path("/tmp/az623-path-b.json")
monkeypatch.setattr(
airborne_bootstrap,
"_load_camera_calibration",
lambda _config: _make_calibration(),
)
# Act
a = build_pre_constructed(config_a)
b = build_pre_constructed(config_b)
# Assert
assert a["c5_imu_preintegrator"] is not b["c5_imu_preintegrator"], (
"Two distinct calibration paths must produce two distinct "
"ImuPreintegrator instances; the cache key is the path."
)
def test_ac_623_3_empty_calibration_path_raises_named_error() -> None:
# Arrange: default Config() has camera_calibration_path="".
config = Config()
# Act + Assert
with pytest.raises(AirborneBootstrapError) as exc_info:
build_pre_constructed(config)
msg = str(exc_info.value)
assert "c5_imu_preintegrator" in msg, msg
assert "camera_calibration_path" in msg, msg
assert "c5_state" in msg, msg
def test_ac_623_3_unreadable_calibration_path_raises_named_error(
tmp_path: Path,
) -> None:
# Arrange: pass a path that does not exist on disk; the OSError
# surfaces as AirborneBootstrapError with the operator-actionable
# message.
missing = tmp_path / "this-file-does-not-exist.json"
config = _config_with_calibration_path(str(missing))
# Act + Assert
with pytest.raises(AirborneBootstrapError) as exc_info:
build_pre_constructed(config)
msg = str(exc_info.value)
assert "c5_imu_preintegrator" in msg
assert "c5_state" in msg
assert str(missing) in msg
def test_ac_623_3_malformed_json_calibration_raises_named_error(
tmp_path: Path,
) -> None:
# Arrange
bad = tmp_path / "bad-calib.json"
bad.write_text("{ this is not valid json", encoding="utf-8")
config = _config_with_calibration_path(str(bad))
# Act + Assert
with pytest.raises(AirborneBootstrapError) as exc_info:
build_pre_constructed(config)
msg = str(exc_info.value)
assert "c5_imu_preintegrator" in msg
assert "c5_state" in msg
assert "not valid JSON" in msg