mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 17:41:13 +00:00
[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:
@@ -48,15 +48,24 @@ internals).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from gps_denied_onboard._types.calibration import CameraCalibration
|
||||
from gps_denied_onboard.clock.wall_clock import WallClock
|
||||
from gps_denied_onboard.fdr_client.client import make_fdr_client
|
||||
from gps_denied_onboard.helpers.feature_extractor import OpenCvOrbExtractor
|
||||
from gps_denied_onboard.helpers.imu_preintegrator import (
|
||||
ImuPreintegrator,
|
||||
make_imu_preintegrator,
|
||||
)
|
||||
from gps_denied_onboard.helpers.lightglue_runtime import LightGlueRuntime
|
||||
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 register_strategy
|
||||
from gps_denied_onboard.runtime_root.errors import RuntimeNotAvailableError
|
||||
from gps_denied_onboard.runtime_root.inference_factory import build_inference_runtime
|
||||
@@ -86,10 +95,36 @@ __all__ = [
|
||||
"FAISS_BUILD_FLAG",
|
||||
"AirborneBootstrapError",
|
||||
"build_pre_constructed",
|
||||
"clear_imu_preintegrator_cache",
|
||||
"register_airborne_strategies",
|
||||
]
|
||||
|
||||
|
||||
_IMU_PREINTEGRATOR_CACHE: dict[str, ImuPreintegrator] = {}
|
||||
"""Per-process cache mapping ``camera_calibration_path`` to the
|
||||
:class:`ImuPreintegrator` built for that path.
|
||||
|
||||
Backs AC-623.2: invoking :func:`build_pre_constructed` twice in the
|
||||
same process MUST return the SAME ``c5_imu_preintegrator`` instance
|
||||
when the calibration path is unchanged. The preintegrator is the only
|
||||
stateful c5 helper this phase wires; caching protects its bias /
|
||||
sample accumulator from being silently rebuilt on a re-invocation.
|
||||
|
||||
Tests call :func:`clear_imu_preintegrator_cache` to isolate state.
|
||||
"""
|
||||
|
||||
|
||||
def clear_imu_preintegrator_cache() -> None:
|
||||
"""Drop every cached :class:`ImuPreintegrator` (test-isolation only).
|
||||
|
||||
Mirrors :func:`gps_denied_onboard.fdr_client.client.clear_fdr_client_cache` /
|
||||
:func:`gps_denied_onboard.runtime_root.state_factory.clear_state_registry`'s
|
||||
test-only contract: production code never calls this, but unit tests
|
||||
that exercise the per-path cache need a way to reset between cases.
|
||||
"""
|
||||
_IMU_PREINTEGRATOR_CACHE.clear()
|
||||
|
||||
|
||||
FAISS_BUILD_FLAG: Final[str] = "BUILD_FAISS_INDEX"
|
||||
"""Env flag gating the FAISS-backed ``DescriptorIndex`` impl.
|
||||
|
||||
@@ -229,9 +264,7 @@ _C4_POSE_STRATEGIES: tuple[str, ...] = ("opencv_gtsam",)
|
||||
_C5_STATE_STRATEGIES: tuple[str, ...] = ("gtsam_isam2", "eskf")
|
||||
|
||||
|
||||
def _require(
|
||||
constructed: Mapping[str, Any], key: str, component_slug: str
|
||||
) -> Any:
|
||||
def _require(constructed: Mapping[str, Any], key: str, component_slug: str) -> Any:
|
||||
"""Extract ``constructed[key]`` or raise AirborneBootstrapError."""
|
||||
if key not in constructed:
|
||||
available = sorted(constructed.keys())
|
||||
@@ -262,16 +295,10 @@ def _c2_vpr_wrapper(config: Config, constructed: Mapping[str, Any]) -> Any:
|
||||
)
|
||||
|
||||
|
||||
def _c2_5_rerank_wrapper(
|
||||
config: Config, constructed: Mapping[str, Any]
|
||||
) -> Any:
|
||||
def _c2_5_rerank_wrapper(config: Config, constructed: Mapping[str, Any]) -> Any:
|
||||
tile_store = _require(constructed, "c6_tile_store", "c2_5_rerank")
|
||||
lightglue_runtime = _require(
|
||||
constructed, "c3_lightglue_runtime", "c2_5_rerank"
|
||||
)
|
||||
feature_extractor = _require(
|
||||
constructed, "c3_feature_extractor", "c2_5_rerank"
|
||||
)
|
||||
lightglue_runtime = _require(constructed, "c3_lightglue_runtime", "c2_5_rerank")
|
||||
feature_extractor = _require(constructed, "c3_feature_extractor", "c2_5_rerank")
|
||||
clock = _require(constructed, "clock", "c2_5_rerank")
|
||||
fdr_client = constructed.get("c13_fdr")
|
||||
return build_rerank_strategy(
|
||||
@@ -284,12 +311,8 @@ def _c2_5_rerank_wrapper(
|
||||
)
|
||||
|
||||
|
||||
def _c3_matcher_wrapper(
|
||||
config: Config, constructed: Mapping[str, Any]
|
||||
) -> Any:
|
||||
lightglue_runtime = _require(
|
||||
constructed, "c3_lightglue_runtime", "c3_matcher"
|
||||
)
|
||||
def _c3_matcher_wrapper(config: Config, constructed: Mapping[str, Any]) -> Any:
|
||||
lightglue_runtime = _require(constructed, "c3_lightglue_runtime", "c3_matcher")
|
||||
ransac_filter = _require(constructed, "c282_ransac_filter", "c3_matcher")
|
||||
inference_runtime = _require(constructed, "c7_inference", "c3_matcher")
|
||||
clock = constructed.get("clock")
|
||||
@@ -304,9 +327,7 @@ def _c3_matcher_wrapper(
|
||||
)
|
||||
|
||||
|
||||
def _c3_5_adhop_wrapper(
|
||||
config: Config, constructed: Mapping[str, Any]
|
||||
) -> Any:
|
||||
def _c3_5_adhop_wrapper(config: Config, constructed: Mapping[str, Any]) -> Any:
|
||||
ransac_filter = _require(constructed, "c282_ransac_filter", "c3_5_adhop")
|
||||
inference_runtime = _require(constructed, "c7_inference", "c3_5_adhop")
|
||||
clock = constructed.get("clock")
|
||||
@@ -324,9 +345,7 @@ def _c4_pose_wrapper(config: Config, constructed: Mapping[str, Any]) -> Any:
|
||||
ransac_filter = _require(constructed, "c282_ransac_filter", "c4_pose")
|
||||
wgs_converter = _require(constructed, "c5_wgs_converter", "c4_pose")
|
||||
se3_utils = _require(constructed, "c5_se3_utils", "c4_pose")
|
||||
isam2_graph_handle = _require(
|
||||
constructed, "c5_isam2_graph_handle", "c4_pose"
|
||||
)
|
||||
isam2_graph_handle = _require(constructed, "c5_isam2_graph_handle", "c4_pose")
|
||||
fdr_client = constructed.get("c13_fdr")
|
||||
clock = constructed.get("clock")
|
||||
return build_pose_estimator(
|
||||
@@ -341,9 +360,7 @@ def _c4_pose_wrapper(config: Config, constructed: Mapping[str, Any]) -> Any:
|
||||
|
||||
|
||||
def _c5_state_wrapper(config: Config, constructed: Mapping[str, Any]) -> Any:
|
||||
imu_preintegrator = _require(
|
||||
constructed, "c5_imu_preintegrator", "c5_state"
|
||||
)
|
||||
imu_preintegrator = _require(constructed, "c5_imu_preintegrator", "c5_state")
|
||||
se3_utils = _require(constructed, "c5_se3_utils", "c5_state")
|
||||
wgs_converter = _require(constructed, "c5_wgs_converter", "c5_state")
|
||||
fdr_client = _require(constructed, "c13_fdr", "c5_state")
|
||||
@@ -378,9 +395,7 @@ def _ensure_state_strategy_registered(config: Config) -> None:
|
||||
block = getattr(config, "components", None) or {}
|
||||
c5_block = block.get("c5_state") if isinstance(block, dict) else None
|
||||
strategy = (
|
||||
getattr(c5_block, "strategy", "gtsam_isam2")
|
||||
if c5_block is not None
|
||||
else "gtsam_isam2"
|
||||
getattr(c5_block, "strategy", "gtsam_isam2") if c5_block is not None else "gtsam_isam2"
|
||||
)
|
||||
# state_factory._STATE_BUILD_FLAGS: gtsam_isam2 defaults ON-when-unset;
|
||||
# eskf defaults OFF-when-unset (mirror state_factory's own logic).
|
||||
@@ -420,9 +435,7 @@ descriptor_index, etc.) come from ``pre_constructed``.
|
||||
"""
|
||||
|
||||
|
||||
_AIRBORNE_REGISTRATIONS: tuple[
|
||||
tuple[str, tuple[str, ...], Any, tuple[str, ...]], ...
|
||||
] = (
|
||||
_AIRBORNE_REGISTRATIONS: tuple[tuple[str, tuple[str, ...], Any, tuple[str, ...]], ...] = (
|
||||
("c1_vio", _C1_VIO_STRATEGIES, _c1_vio_wrapper, _C1_VIO_DEPENDS_ON),
|
||||
("c2_vpr", _C2_VPR_STRATEGIES, _c2_vpr_wrapper, _C2_VPR_DEPENDS_ON),
|
||||
(
|
||||
@@ -468,9 +481,7 @@ def _consumers_of_pre_constructed_key(key: str) -> tuple[str, ...]:
|
||||
)
|
||||
|
||||
|
||||
def _configured_consumers_of_pre_constructed_key(
|
||||
config: Config, key: str
|
||||
) -> tuple[str, ...]:
|
||||
def _configured_consumers_of_pre_constructed_key(config: Config, key: str) -> tuple[str, ...]:
|
||||
"""Return consumers of ``key`` that are present in ``config.components``.
|
||||
|
||||
Used to narrow an error message from "every theoretical consumer of
|
||||
@@ -503,9 +514,7 @@ def _build_c6_descriptor_index(config: Config) -> Any:
|
||||
try:
|
||||
return build_descriptor_index(config)
|
||||
except RuntimeNotAvailableError as exc:
|
||||
consumers = _configured_consumers_of_pre_constructed_key(
|
||||
config, "c6_descriptor_index"
|
||||
)
|
||||
consumers = _configured_consumers_of_pre_constructed_key(config, "c6_descriptor_index")
|
||||
raise AirborneBootstrapError(
|
||||
f"airborne_bootstrap: cannot construct "
|
||||
f"pre_constructed['c6_descriptor_index'] because "
|
||||
@@ -558,12 +567,9 @@ def _build_c7_inference(config: Config) -> Any:
|
||||
try:
|
||||
return build_inference_runtime(config)
|
||||
except RuntimeNotAvailableError as exc:
|
||||
consumers = _configured_consumers_of_pre_constructed_key(
|
||||
config, "c7_inference"
|
||||
)
|
||||
consumers = _configured_consumers_of_pre_constructed_key(config, "c7_inference")
|
||||
flag_options = ", ".join(
|
||||
f"{flag}=ON for runtime {runtime!r}"
|
||||
for runtime, flag in C7_AIRBORNE_BUILD_FLAGS
|
||||
f"{flag}=ON for runtime {runtime!r}" for runtime, flag in C7_AIRBORNE_BUILD_FLAGS
|
||||
)
|
||||
raise AirborneBootstrapError(
|
||||
f"airborne_bootstrap: cannot construct "
|
||||
@@ -630,9 +636,7 @@ def _load_lightglue_engine_handle(
|
||||
into an :class:`AirborneBootstrapError`).
|
||||
"""
|
||||
block = config.components.get("c3_matcher")
|
||||
weights_path = (
|
||||
getattr(block, "lightglue_weights_path", None) if block is not None else None
|
||||
)
|
||||
weights_path = getattr(block, "lightglue_weights_path", None) if block is not None else None
|
||||
if weights_path is None:
|
||||
raise AirborneBootstrapError(
|
||||
"airborne_bootstrap: cannot construct "
|
||||
@@ -721,6 +725,185 @@ def _build_c3_lightglue_runtime(
|
||||
return LightGlueRuntime(engine_handle)
|
||||
|
||||
|
||||
def _load_camera_calibration(config: Config) -> CameraCalibration:
|
||||
"""Read the camera calibration JSON into a :class:`CameraCalibration` DTO.
|
||||
|
||||
Mirrors the on-disk JSON shape that
|
||||
:func:`gps_denied_onboard.runtime_root._replay_branch._load_camera_calibration`
|
||||
already accepts (same calibration file the live and replay binaries
|
||||
share). Replicated here \u2014 not imported from ``_replay_branch`` \u2014 because
|
||||
the replay-branch helper raises ``CompositionError`` (replay-flow
|
||||
contract) where the airborne bootstrap MUST raise
|
||||
:class:`AirborneBootstrapError` per the AZ-618 umbrella's
|
||||
operator-error contract. Both helpers consume the same on-disk
|
||||
format; any future change to that format MUST land in lockstep
|
||||
here and in ``_replay_branch.py``.
|
||||
|
||||
AZ-623 unit tests monkey-patch this function with a sentinel
|
||||
:class:`CameraCalibration` so they exercise the
|
||||
:func:`_build_c5_imu_preintegrator` wiring without an on-disk JSON
|
||||
file (per the same Tier-2 monkeypatch pattern the AZ-622 builders
|
||||
use for the heavy LightGlue seam).
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
path = config.runtime.camera_calibration_path
|
||||
if not path:
|
||||
raise AirborneBootstrapError(
|
||||
"airborne_bootstrap: cannot construct "
|
||||
"pre_constructed['c5_imu_preintegrator'] because "
|
||||
"config.runtime.camera_calibration_path is empty. "
|
||||
"Consuming component: c5_state. Production main() (AZ-624) "
|
||||
"must populate the path to the camera calibration JSON; "
|
||||
"tests stub _load_camera_calibration via monkeypatch."
|
||||
)
|
||||
calib_path = Path(path)
|
||||
try:
|
||||
blob = json.loads(calib_path.read_text(encoding="utf-8"))
|
||||
except OSError as exc:
|
||||
raise AirborneBootstrapError(
|
||||
f"airborne_bootstrap: cannot construct "
|
||||
f"pre_constructed['c5_imu_preintegrator'] because the camera "
|
||||
f"calibration file at {path!r} could not be read: {exc!r}. "
|
||||
f"Consuming component: c5_state. Ensure "
|
||||
f"config.runtime.camera_calibration_path points at a readable "
|
||||
f"JSON file."
|
||||
) from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise AirborneBootstrapError(
|
||||
f"airborne_bootstrap: cannot construct "
|
||||
f"pre_constructed['c5_imu_preintegrator'] because the camera "
|
||||
f"calibration file at {path!r} is not valid JSON: {exc!r}. "
|
||||
f"Consuming component: c5_state. Validate the calibration JSON "
|
||||
f"shape against the on-disk format documented in "
|
||||
f"runtime_root._replay_branch._load_camera_calibration."
|
||||
) from exc
|
||||
if not isinstance(blob, Mapping):
|
||||
raise AirborneBootstrapError(
|
||||
f"airborne_bootstrap: cannot construct "
|
||||
f"pre_constructed['c5_imu_preintegrator'] because the camera "
|
||||
f"calibration at {path!r} must decode to a JSON object; got "
|
||||
f"{type(blob).__name__}. Consuming component: c5_state."
|
||||
)
|
||||
intrinsics = np.asarray(blob.get("intrinsics_3x3"), dtype=np.float64)
|
||||
if intrinsics.shape != (3, 3):
|
||||
raise AirborneBootstrapError(
|
||||
f"airborne_bootstrap: cannot construct "
|
||||
f"pre_constructed['c5_imu_preintegrator'] because the camera "
|
||||
f"calibration at {path!r} 'intrinsics_3x3' must be 3x3; got "
|
||||
f"shape {intrinsics.shape}. Consuming component: c5_state."
|
||||
)
|
||||
distortion = np.asarray(blob.get("distortion", []), dtype=np.float64)
|
||||
body_to_camera = np.asarray(
|
||||
blob.get("body_to_camera_se3", np.eye(4).tolist()),
|
||||
dtype=np.float64,
|
||||
)
|
||||
return CameraCalibration(
|
||||
camera_id=str(blob.get("camera_id", "airborne-camera")),
|
||||
intrinsics_3x3=intrinsics,
|
||||
distortion=distortion,
|
||||
body_to_camera_se3=body_to_camera,
|
||||
acquisition_method=str(blob.get("acquisition_method", "operator")),
|
||||
metadata=dict(blob.get("metadata", {})),
|
||||
)
|
||||
|
||||
|
||||
def _build_c282_ransac_filter(config: Config) -> RansacFilter:
|
||||
"""Build ``pre_constructed['c282_ransac_filter']`` for the airborne binary.
|
||||
|
||||
:class:`RansacFilter` is a static-only OpenCV wrapper (per AZ-282 /
|
||||
E-CC-HELPERS); a fresh instance carries no state. Consumers
|
||||
(``c3_matcher``, ``c3_5_adhop``, ``c4_pose``) dispatch to its static
|
||||
methods, so identity-share is irrelevant \u2014 AC-623.2 explicitly
|
||||
permits a fresh instance per :func:`build_pre_constructed` call.
|
||||
|
||||
No ``BUILD_*`` flag check applies: the helper is a CPU-only OpenCV
|
||||
wrapper with no compile-time gate.
|
||||
"""
|
||||
del config # placeholder for future config-driven RANSAC variant selection
|
||||
return RansacFilter()
|
||||
|
||||
|
||||
def _build_c5_imu_preintegrator(config: Config) -> ImuPreintegrator:
|
||||
"""Build (or retrieve cached) ``pre_constructed['c5_imu_preintegrator']``.
|
||||
|
||||
Reads ``config.runtime.camera_calibration_path``, loads the
|
||||
:class:`CameraCalibration` DTO via :func:`_load_camera_calibration`,
|
||||
and constructs an :class:`ImuPreintegrator` via
|
||||
:func:`make_imu_preintegrator`. The preintegrator is cached at module
|
||||
level keyed by the calibration path \u2014 AC-623.2 requires that two
|
||||
invocations of :func:`build_pre_constructed` return the SAME
|
||||
instance for the same path, so the bias / sample accumulator is not
|
||||
silently reset across re-invocations.
|
||||
|
||||
Raises:
|
||||
AirborneBootstrapError: when ``camera_calibration_path`` is
|
||||
empty / unreadable / malformed (AC-623.3); message names
|
||||
both the missing input AND the consuming component slug
|
||||
``c5_state`` per the AZ-618 umbrella's operator-error
|
||||
contract.
|
||||
"""
|
||||
path = config.runtime.camera_calibration_path
|
||||
if not path:
|
||||
raise AirborneBootstrapError(
|
||||
"airborne_bootstrap: cannot construct "
|
||||
"pre_constructed['c5_imu_preintegrator'] because "
|
||||
"config.runtime.camera_calibration_path is empty. "
|
||||
"Consuming component: c5_state. Production main() (AZ-624) "
|
||||
"must populate the path before calling build_pre_constructed; "
|
||||
"tests stub _load_camera_calibration via monkeypatch."
|
||||
)
|
||||
cached = _IMU_PREINTEGRATOR_CACHE.get(path)
|
||||
if cached is not None:
|
||||
return cached
|
||||
calibration = _load_camera_calibration(config)
|
||||
preintegrator = make_imu_preintegrator(calibration)
|
||||
_IMU_PREINTEGRATOR_CACHE[path] = preintegrator
|
||||
return preintegrator
|
||||
|
||||
|
||||
def _build_c5_se3_utils(config: Config) -> Any:
|
||||
"""Build ``pre_constructed['c5_se3_utils']`` for the airborne binary.
|
||||
|
||||
Returns the :mod:`gps_denied_onboard.helpers.se3_utils` module
|
||||
itself as the namespace handle. Python modules support attribute
|
||||
access for their public names (``exp_map``, ``log_map``,
|
||||
``matrix_to_se3``, ``se3_to_matrix``, ``adjoint``,
|
||||
``is_valid_rotation``, ``SE3``); both
|
||||
:class:`OpenCVGtsamPoseEstimator` and
|
||||
:class:`GtsamIsam2StateEstimator` store the injected handle as
|
||||
``self._se3_utils: Any`` and dispatch via attribute access, so the
|
||||
module satisfies the contract without an extra wrapper class. The
|
||||
existing C5 unit-test fixtures (e.g.
|
||||
``tests/unit/c5_state/test_az386_eskf_baseline.py``) inject
|
||||
``mock.MagicMock()`` for the same slot \u2014 attribute-access shape
|
||||
matches.
|
||||
|
||||
Returning the module also satisfies AC-623.2's caching note
|
||||
incidentally: Python's import machinery returns the same module
|
||||
object across calls, so two invocations of
|
||||
:func:`build_pre_constructed` see the SAME ``c5_se3_utils`` value.
|
||||
"""
|
||||
del config # the module-as-namespace selection is config-independent
|
||||
from gps_denied_onboard.helpers import se3_utils as se3_utils_module
|
||||
|
||||
return se3_utils_module
|
||||
|
||||
|
||||
def _build_c5_wgs_converter(config: Config) -> WgsConverter:
|
||||
"""Build ``pre_constructed['c5_wgs_converter']`` for the airborne binary.
|
||||
|
||||
:class:`WgsConverter` is a stateless static-only class (per AZ-279);
|
||||
a fresh instance carries no module-level state beyond pyproj's
|
||||
cached transformer pair. Returns ``WgsConverter()`` to match the
|
||||
same construction pattern :mod:`runtime_root._replay_branch`
|
||||
already uses (``wgs_converter = WgsConverter()`` at line 205). No
|
||||
``BUILD_*`` flag check applies.
|
||||
"""
|
||||
del config # placeholder for future config-driven coord-system selection
|
||||
return WgsConverter()
|
||||
|
||||
|
||||
def _build_c3_feature_extractor(config: Config) -> FeatureExtractor:
|
||||
"""Build ``pre_constructed['c3_feature_extractor']`` for the airborne binary.
|
||||
|
||||
@@ -756,15 +939,27 @@ def build_pre_constructed(config: Config) -> dict[str, Any]:
|
||||
added the two C6 storage entries (``c6_descriptor_index`` +
|
||||
``c6_tile_store``). AZ-621 (Phase C) added ``c7_inference``
|
||||
(PyTorch FP16 vs. TensorRT, gated by
|
||||
:data:`C7_AIRBORNE_BUILD_FLAGS`). AZ-622 (Phase D) adds
|
||||
:data:`C7_AIRBORNE_BUILD_FLAGS`). AZ-622 (Phase D) added
|
||||
``c3_lightglue_runtime`` (single shared
|
||||
:class:`gps_denied_onboard.helpers.lightglue_runtime.LightGlueRuntime`
|
||||
instance, gated by :data:`C3_MATCHER_BUILD_FLAGS` per the
|
||||
configured strategy) + ``c3_feature_extractor`` (the shared
|
||||
:class:`gps_denied_onboard.helpers.feature_extractor.FeatureExtractor`
|
||||
used by C2.5). Phases E..F (AZ-623..AZ-624) will extend this
|
||||
function to populate the remaining keys in
|
||||
:data:`AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS`.
|
||||
used by C2.5). AZ-623 (Phase E) adds the four stateless / cached c5
|
||||
helpers: ``c282_ransac_filter`` (shared
|
||||
:class:`gps_denied_onboard.helpers.ransac_filter.RansacFilter`),
|
||||
``c5_imu_preintegrator`` (per-calibration-path-cached
|
||||
:class:`gps_denied_onboard.helpers.imu_preintegrator.ImuPreintegrator`),
|
||||
``c5_se3_utils`` (the
|
||||
:mod:`gps_denied_onboard.helpers.se3_utils` module as a
|
||||
namespace handle), and ``c5_wgs_converter`` (shared
|
||||
:class:`gps_denied_onboard.helpers.wgs_converter.WgsConverter`).
|
||||
The ``c5_isam2_graph_handle`` slot is the special-case ordering
|
||||
work tracked separately in AZ-625 (split out of AZ-623 on
|
||||
2026-05-19 because Path 1 of the AZ-623 spec required a
|
||||
Protocol seam change forbidden by the AZ-618 umbrella). Phase F
|
||||
(AZ-624) will wire main() and verify AC-1..AC-5 once both AZ-623
|
||||
and AZ-625 land.
|
||||
|
||||
Returns a fresh dict on each call. The ``c13_fdr`` instance is cached
|
||||
inside :func:`make_fdr_client` (per-producer cache) so two calls within
|
||||
@@ -776,7 +971,13 @@ def build_pre_constructed(config: Config) -> dict[str, Any]:
|
||||
caching at this layer; the C7 :class:`InferenceRuntime` built for the
|
||||
``c7_inference`` slot is reused as the engine source for the LightGlue
|
||||
matcher load (AZ-622) so the bootstrap does not double-build the
|
||||
inference runtime.
|
||||
inference runtime. AZ-623's ``c5_imu_preintegrator`` is cached at module
|
||||
level (:data:`_IMU_PREINTEGRATOR_CACHE`) keyed by
|
||||
``config.runtime.camera_calibration_path`` so its bias / sample
|
||||
accumulator survives a re-invocation. The remaining AZ-623 c5 helpers
|
||||
are stateless: ``c282_ransac_filter`` and ``c5_wgs_converter`` are
|
||||
fresh static-only instances; ``c5_se3_utils`` is the
|
||||
:mod:`gps_denied_onboard.helpers.se3_utils` module.
|
||||
|
||||
Replay-mode override: :func:`compose_root` merges ``replay_components``
|
||||
over ``pre_constructed`` so the :class:`WallClock` here is replaced by
|
||||
@@ -794,8 +995,11 @@ def build_pre_constructed(config: Config) -> dict[str, Any]:
|
||||
requires ``c7_inference``; OR if the configured C3 matcher
|
||||
strategy's :data:`C3_MATCHER_BUILD_FLAGS` flag is OFF (or
|
||||
the strategy is unknown), or if the LightGlue engine load
|
||||
fails. The message names the consuming component slug(s)
|
||||
and the relevant gating flag(s).
|
||||
fails; OR (AZ-623) if ``config.runtime.camera_calibration_path``
|
||||
is empty / unreadable / malformed JSON, blocking the
|
||||
``c5_imu_preintegrator`` build. The message names the
|
||||
consuming component slug(s) and the relevant gating flag(s)
|
||||
or missing inputs.
|
||||
"""
|
||||
constructed: dict[str, Any] = {}
|
||||
constructed["c13_fdr"] = make_fdr_client(AIRBORNE_MAIN_PRODUCER_ID, config)
|
||||
@@ -807,6 +1011,10 @@ def build_pre_constructed(config: Config) -> dict[str, Any]:
|
||||
config, inference_runtime=constructed["c7_inference"]
|
||||
)
|
||||
constructed["c3_feature_extractor"] = _build_c3_feature_extractor(config)
|
||||
constructed["c282_ransac_filter"] = _build_c282_ransac_filter(config)
|
||||
constructed["c5_imu_preintegrator"] = _build_c5_imu_preintegrator(config)
|
||||
constructed["c5_se3_utils"] = _build_c5_se3_utils(config)
|
||||
constructed["c5_wgs_converter"] = _build_c5_wgs_converter(config)
|
||||
return constructed
|
||||
|
||||
|
||||
@@ -851,8 +1059,7 @@ def register_airborne_strategies() -> None:
|
||||
"kv": {
|
||||
"slots": [slug for slug, *_ in _AIRBORNE_REGISTRATIONS],
|
||||
"total_registrations": sum(
|
||||
len(strategies)
|
||||
for _, strategies, *_ in _AIRBORNE_REGISTRATIONS
|
||||
len(strategies) for _, strategies, *_ in _AIRBORNE_REGISTRATIONS
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user