mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 20:51:14 +00:00
[AZ-687] Guard build_pre_constructed seeds in replay mode
Replay CLI synthesizes a minimal Config whose `components` mapping omits the strategy-component blocks (`c6_tile_cache`, `c7_inference`, `c5_state`) the airborne bootstrap historically read unconditionally. Add `_replay_omits_component_block` and gate the c6 seeds, the c7 + c3_lightglue_runtime pair, and the c5 (estimator, handle) eager build on `config.mode == "replay" AND block absent`. Live mode and any replay config that DOES populate the blocks remain unchanged — the guard is conditional, not blanket. The skip is safe because compose_root's per-component wrappers only run for slugs in `config.components`; absent blocks mean absent wrappers, so the seeded slots would never be read. Fix lives at the BUILD-PRE-CONSTRUCTED layer per the spec's explicit "no silent fallback in `_c6_config`" constraint. Covers AC-687-1 / AC-687-2 / AC-687-4. AC-687-3 (Jetson Tier-2 e2e replay) requires an out-of-band hardware re-run; evidence destination documented in autodev state. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1135,6 +1135,43 @@ def _build_c3_feature_extractor(config: Config) -> FeatureExtractor:
|
||||
return OpenCvOrbExtractor()
|
||||
|
||||
|
||||
def _replay_omits_component_block(config: Config, block_name: str) -> bool:
|
||||
"""True iff replay-mode :class:`Config` has no ``components[block_name]`` entry.
|
||||
|
||||
AZ-687: the replay CLI (``gps-denied-replay``) synthesizes a minimal
|
||||
:class:`Config` carrying only ``mode == "replay"`` plus the
|
||||
``replay`` sub-block. The strategy packages that register their own
|
||||
component config blocks via :func:`register_component_block` (e.g.
|
||||
:mod:`gps_denied_onboard.components.c6_tile_cache`,
|
||||
:mod:`gps_denied_onboard.components.c7_inference`,
|
||||
:mod:`gps_denied_onboard.components.c5_state`) are not imported on
|
||||
that path, so :func:`gps_denied_onboard.config.loader.load_config`
|
||||
produces an empty ``components`` mapping.
|
||||
|
||||
The :func:`build_pre_constructed` seeds that depend on those blocks
|
||||
(``c6_descriptor_index``, ``c6_tile_store``, ``c7_inference``,
|
||||
``c3_lightglue_runtime``, the C5 ``(estimator, handle)`` pair) are
|
||||
only consumed by :func:`compose_root`'s per-component wrappers,
|
||||
which only execute for slugs actually present in
|
||||
``config.components``. With an empty components mapping, no wrapper
|
||||
asks for those slots and skipping their bootstrap is safe.
|
||||
|
||||
The fix lives at this layer (BUILD-PRE-CONSTRUCTED), NOT at
|
||||
:func:`storage_factory._c6_config` / :func:`inference_factory._c7_config`:
|
||||
those helpers deliberately raise :class:`KeyError` on missing blocks
|
||||
(per their docstrings, silent fallback would mask a missing import).
|
||||
|
||||
Returns ``False`` outside replay mode regardless of block presence —
|
||||
live-mode contracts (AC-687-2) MUST keep seeding every key.
|
||||
"""
|
||||
if config.mode != "replay":
|
||||
return False
|
||||
components = getattr(config, "components", None) or {}
|
||||
if not isinstance(components, Mapping):
|
||||
return False
|
||||
return block_name not in components
|
||||
|
||||
|
||||
def build_pre_constructed(config: Config) -> dict[str, Any]:
|
||||
"""Build the airborne ``pre_constructed`` dict for :func:`compose_root`.
|
||||
|
||||
@@ -1191,6 +1228,17 @@ def build_pre_constructed(config: Config) -> dict[str, Any]:
|
||||
the replay branch's :class:`TlogDerivedClock`. That's intentional and
|
||||
matches the contract in :func:`compose_root`'s docstring.
|
||||
|
||||
Replay-mode guard (AZ-687): when ``config.mode == "replay"`` and the
|
||||
minimal replay Config omits a strategy-component block
|
||||
(``c6_tile_cache``, ``c7_inference``, ``c5_state``), the bootstrap
|
||||
skips the seeds that would otherwise call the corresponding
|
||||
``_cN_config`` helper and raise :class:`KeyError`. The skipped slots
|
||||
are absent from the returned dict; ``compose_root``'s per-component
|
||||
wrappers only run for slugs in ``config.components`` and never read
|
||||
the skipped slots. Live mode is unaffected — every documented key in
|
||||
:data:`AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS` is still seeded. See
|
||||
:func:`_replay_omits_component_block` for the precise predicate.
|
||||
|
||||
Raises:
|
||||
AirborneBootstrapError: if ``BUILD_FAISS_INDEX`` is OFF and any
|
||||
configured consumer (per
|
||||
@@ -1216,27 +1264,45 @@ def build_pre_constructed(config: Config) -> dict[str, Any]:
|
||||
constructed: dict[str, Any] = {}
|
||||
constructed["c13_fdr"] = make_fdr_client(AIRBORNE_MAIN_PRODUCER_ID, config)
|
||||
constructed["clock"] = WallClock()
|
||||
constructed["c6_descriptor_index"] = _build_c6_descriptor_index(config)
|
||||
constructed["c6_tile_store"] = _build_c6_tile_store(config)
|
||||
constructed["c7_inference"] = _build_c7_inference(config)
|
||||
constructed["c3_lightglue_runtime"] = _build_c3_lightglue_runtime(
|
||||
config, inference_runtime=constructed["c7_inference"]
|
||||
)
|
||||
# AZ-687: a minimal replay Config carries no `c6_tile_cache` block,
|
||||
# so `_build_c6_*` would KeyError inside `_c6_config`. The skip is
|
||||
# safe because the only wrappers that read these slots
|
||||
# (c2_vpr / c2_5_rerank / c5_state) require their own component
|
||||
# entries in `config.components`, which the minimal replay Config
|
||||
# also omits.
|
||||
if not _replay_omits_component_block(config, "c6_tile_cache"):
|
||||
constructed["c6_descriptor_index"] = _build_c6_descriptor_index(config)
|
||||
constructed["c6_tile_store"] = _build_c6_tile_store(config)
|
||||
# AZ-687: c3_lightglue_runtime cascades on `constructed["c7_inference"]`,
|
||||
# so it's gated under the same `c7_inference` block guard. Replay
|
||||
# mode without the c7_inference block also drops the c3_matcher /
|
||||
# c2_5_rerank wrappers that would have consumed the runtime.
|
||||
if not _replay_omits_component_block(config, "c7_inference"):
|
||||
constructed["c7_inference"] = _build_c7_inference(config)
|
||||
constructed["c3_lightglue_runtime"] = _build_c3_lightglue_runtime(
|
||||
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)
|
||||
estimator, handle = _build_c5_state_estimator_pair(
|
||||
config,
|
||||
imu_preintegrator=constructed["c5_imu_preintegrator"],
|
||||
se3_utils=constructed["c5_se3_utils"],
|
||||
wgs_converter=constructed["c5_wgs_converter"],
|
||||
fdr_client=constructed["c13_fdr"],
|
||||
tile_store=constructed["c6_tile_store"],
|
||||
)
|
||||
constructed["c5_isam2_graph_handle"] = handle
|
||||
constructed[_C5_PREBUILT_ESTIMATOR_KEY] = estimator
|
||||
# AZ-687: the eager (estimator, handle) build registers the gtsam-bound
|
||||
# state factory and reads `config.components["c5_state"]`. When the
|
||||
# block is absent in replay mode the c5_state wrapper itself will not
|
||||
# run, so the handle / prebuilt-estimator slots are unread; skip the
|
||||
# build to avoid forcing the gtsam import on the replay binary.
|
||||
if not _replay_omits_component_block(config, "c5_state"):
|
||||
estimator, handle = _build_c5_state_estimator_pair(
|
||||
config,
|
||||
imu_preintegrator=constructed["c5_imu_preintegrator"],
|
||||
se3_utils=constructed["c5_se3_utils"],
|
||||
wgs_converter=constructed["c5_wgs_converter"],
|
||||
fdr_client=constructed["c13_fdr"],
|
||||
tile_store=constructed.get("c6_tile_store"),
|
||||
)
|
||||
constructed["c5_isam2_graph_handle"] = handle
|
||||
constructed[_C5_PREBUILT_ESTIMATOR_KEY] = estimator
|
||||
return constructed
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user