Files
gps-denied-onboard/src/gps_denied_onboard/runtime_root/rerank_factory.py
T
Oleksandr Bezdieniezhnykh 48ea1e2fc2 [AZ-343] C2.5 InlierCountReRanker + shared FeatureExtractor helper
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>
2026-05-12 06:22:40 +03:00

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