mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 10:51:13 +00:00
48ea1e2fc2
Implements the production-default ReRankStrategy: K=10 → N=3 by single-pair LightGlue inlier count, with strict drop-and-continue (INV-8) on per-candidate TileFetch / backbone / zero-inlier failures and RerankAllCandidatesFailedError on zero survivors. Composition root injects the shared LightGlueRuntime + Clock + the new FeatureExtractor helper (an L1 placeholder OpenCvOrbExtractor that unblocks AZ-343 and future C3 strategies — task scope expansion). Architectural notes: - Cross-component imports stay banned; tile_store types as `object` and the C6 TileCacheError family is duck-typed by class module prefix (same workaround AZ-348 adopted for c7_inference; proper fix is to relocate TileCacheError to _types/ in a follow-up). - Clock injection follows the replay contract (AZ-398 Invariant 2); reranked_at is sourced from clock.monotonic_ns(). - AZ-342 factory grew `feature_extractor` + `clock` + `fdr_client` parameters; existing AZ-342 conformance tests updated. Tests: 19 new AC-1..AC-12 + mixed-failure scenarios in test_inlier_count_reranker.py; existing AZ-342 suite (26) still green. Full repo sweep 1093 passed / 2 skipped (cmake/actionlint not on PATH). Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
6.1 KiB
Python
167 lines
6.1 KiB
Python
"""C2.5 ReRank strategy composition-root factory (AZ-342).
|
|
|
|
:func:`build_rerank_strategy` selects exactly one strategy by
|
|
``config.components['c2_5_rerank'].strategy`` and respects
|
|
compile-time ``BUILD_RERANK_<variant>`` gating: requesting a
|
|
strategy whose flag is OFF raises
|
|
:class:`StrategyNotAvailableError` at composition time (NOT at
|
|
first frame).
|
|
|
|
The shared :class:`LightGlueRuntime` is constructor-injected — the
|
|
factory does NOT own its lifecycle. The runtime root constructs ONE
|
|
``LightGlueRuntime`` instance and passes the same reference to both
|
|
this factory (C2.5) and the future C3 matcher factory (R14 fix; see
|
|
``description.md`` § 6).
|
|
|
|
Concrete strategy modules are imported lazily — a Tier-0 workstation
|
|
build with ``BUILD_RERANK_INLIER_COUNT=OFF`` MUST NOT load
|
|
``c2_5_rerank.inlier_based_reranker`` (ADR-002 / I-5; verifiable via
|
|
``sys.modules``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from typing import TYPE_CHECKING
|
|
|
|
from gps_denied_onboard.runtime_root.errors import StrategyNotAvailableError
|
|
|
|
if TYPE_CHECKING:
|
|
from gps_denied_onboard.clock import Clock
|
|
from gps_denied_onboard.components.c2_5_rerank import (
|
|
C2_5RerankConfig,
|
|
ReRankStrategy,
|
|
)
|
|
from gps_denied_onboard.components.c6_tile_cache import TileStore
|
|
from gps_denied_onboard.config.schema import Config
|
|
from gps_denied_onboard.fdr_client import FdrClient
|
|
from gps_denied_onboard.helpers.feature_extractor import FeatureExtractor
|
|
from gps_denied_onboard.helpers.lightglue_runtime import LightGlueRuntime
|
|
|
|
__all__ = ["build_rerank_strategy"]
|
|
|
|
|
|
_LOG = logging.getLogger("gps_denied_onboard.c2_5_rerank")
|
|
|
|
|
|
# Strategy resolution table — mirrors the contract's
|
|
# ``rerank_strategy_protocol.md`` v1.0.0 § Composition-Root Factory
|
|
# table verbatim. ANY mutation here MUST be mirrored in the contract.
|
|
_STRATEGY_TO_BUILD_FLAG: dict[str, str] = {
|
|
"inlier_count": "BUILD_RERANK_INLIER_COUNT",
|
|
}
|
|
|
|
_STRATEGY_TO_MODULE: dict[str, tuple[str, str]] = {
|
|
"inlier_count": (
|
|
"gps_denied_onboard.components.c2_5_rerank.inlier_based_reranker",
|
|
"InlierCountReRanker",
|
|
),
|
|
}
|
|
|
|
|
|
def _is_build_flag_on(flag_name: str) -> bool:
|
|
raw = os.environ.get(flag_name, "")
|
|
return raw.strip().lower() in {"on", "1", "true", "yes"}
|
|
|
|
|
|
def _c2_5_config(config: "Config") -> "C2_5RerankConfig":
|
|
return config.components["c2_5_rerank"]
|
|
|
|
|
|
def build_rerank_strategy(
|
|
config: "Config",
|
|
*,
|
|
tile_store: "TileStore",
|
|
lightglue_runtime: "LightGlueRuntime",
|
|
feature_extractor: "FeatureExtractor",
|
|
clock: "Clock",
|
|
fdr_client: "FdrClient | None" = None,
|
|
) -> "ReRankStrategy":
|
|
"""Construct the :class:`ReRankStrategy` impl selected by config.
|
|
|
|
1. Reads ``config.components['c2_5_rerank'].strategy``.
|
|
2. Checks the matching ``BUILD_RERANK_<variant>`` flag — if OFF,
|
|
raises :class:`StrategyNotAvailableError` BEFORE any import.
|
|
3. Lazily imports the concrete strategy module.
|
|
4. Constructs the strategy via its module-level
|
|
``create(config, tile_store, lightglue_runtime, feature_extractor, fdr_client)``
|
|
factory function (each concrete strategy module exports
|
|
``create`` as its public entry-point; concrete constructors
|
|
stay private).
|
|
5. Emits ONE INFO log ``kind="c2_5.rerank.strategy_loaded"`` with
|
|
structured fields ``{strategy, top_n}``.
|
|
|
|
``feature_extractor`` is a shared L1 helper (AZ-343 scope
|
|
expansion) used by the concrete strategy to extract keypoints +
|
|
descriptors from each per-frame nav image AND from each
|
|
candidate's tile pixels. ``clock`` is the composition-root
|
|
:class:`Clock` (AZ-398) — strategies stamp
|
|
:attr:`RerankResult.reranked_at` via ``clock.monotonic_ns()``
|
|
rather than calling stdlib ``time`` directly (Invariant 2 of
|
|
the replay contract). ``fdr_client`` is optional — passed
|
|
through to strategies that emit FDR records; ``None`` lets the
|
|
strategy run without FDR emission (useful for tests).
|
|
|
|
Raises:
|
|
StrategyNotAvailableError: compile-time flag OFF or
|
|
concrete module not yet built.
|
|
"""
|
|
block = _c2_5_config(config)
|
|
strategy = block.strategy
|
|
flag_name = _STRATEGY_TO_BUILD_FLAG.get(strategy)
|
|
module_info = _STRATEGY_TO_MODULE.get(strategy)
|
|
if flag_name is None or module_info is None:
|
|
# Defensive — config validation rejects unknown strategy labels
|
|
# at load (C2_5RerankConfig.__post_init__).
|
|
_LOG.error(
|
|
"c2_5.rerank.build_flag_off",
|
|
extra={"strategy": strategy, "reason": "unknown_strategy"},
|
|
)
|
|
raise StrategyNotAvailableError(
|
|
f"ReRankStrategy {strategy!r} is not buildable in this binary."
|
|
)
|
|
if not _is_build_flag_on(flag_name):
|
|
_LOG.error(
|
|
"c2_5.rerank.build_flag_off",
|
|
extra={"strategy": strategy, "flag": flag_name},
|
|
)
|
|
raise StrategyNotAvailableError(
|
|
f"BUILD_RERANK_{strategy.upper()} is OFF for this binary; "
|
|
f"cannot select strategy={strategy}."
|
|
)
|
|
module_name, class_name = module_info
|
|
try:
|
|
module = __import__(module_name, fromlist=[class_name])
|
|
except ModuleNotFoundError as exc:
|
|
raise StrategyNotAvailableError(
|
|
f"ReRankStrategy {strategy!r} is configured but its concrete impl "
|
|
f"module {module_name!r} has not been built into this binary "
|
|
"yet (AZ-343 pending)."
|
|
) from exc
|
|
create_fn = getattr(module, "create", None)
|
|
if create_fn is None:
|
|
strategy_cls = getattr(module, class_name)
|
|
instance = strategy_cls(
|
|
config,
|
|
tile_store=tile_store,
|
|
lightglue_runtime=lightglue_runtime,
|
|
feature_extractor=feature_extractor,
|
|
clock=clock,
|
|
fdr_client=fdr_client,
|
|
)
|
|
else:
|
|
instance = create_fn(
|
|
config,
|
|
tile_store=tile_store,
|
|
lightglue_runtime=lightglue_runtime,
|
|
feature_extractor=feature_extractor,
|
|
clock=clock,
|
|
fdr_client=fdr_client,
|
|
)
|
|
_LOG.info(
|
|
"c2_5.rerank.strategy_loaded",
|
|
extra={"strategy": strategy, "top_n": block.top_n},
|
|
)
|
|
return instance
|