mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 20:11:14 +00:00
[AZ-342] C2.5 ReRankStrategy: Protocol + DTOs + factory + composition
Foundational scaffolding for the InlierCountReRanker (AZ-343) and the future C3 CrossDomainMatcher consumer (AZ-344). No concrete re-ranker is implemented here. * ReRankStrategy Protocol (single rerank(frame, vpr_result, n, calibration) -> RerankResult method) with all 8 invariants in the docstring — notably INV-8 drop-and-continue (per-candidate failure NEVER propagates unless every candidate fails). * DTOs moved to L1 _types/rerank.py — RerankCandidate, RerankResult; frozen+slots; tuple-not-list for RerankResult.candidates; tile_id encoded as (zoom_level, lat, lon) tuple to keep _types/ free of any c6_tile_cache (L3) import per module-layout.md. * Error family: RerankError + RerankBackboneError + RerankAllCandidatesFailedError. Only RerankAllCandidatesFailedError escapes rerank(); RerankBackboneError is caught inside the per- candidate loop, logged ERROR, FDR-stamped, candidate dropped. * C2_5RerankConfig (strategy enum default "inlier_count", top_n int default 3) with strict validation at load; registered into Config.components on c2_5_rerank import. * build_rerank_strategy(config, *, tile_store, lightglue_runtime) factory: 1-strategy resolution table, lazy import, BUILD_RERANK_<variant> gate, ImportError → StrategyNotAvailableError mapping. The shared LightGlueRuntime is constructor-injected (R14 fix: neither C2.5 nor C3 owns its lifecycle). Renamed the Protocol from the existing stub "RerankStrategy" to "ReRankStrategy" to match the contract; updated module-layout.md. Removed the legacy RerankResult shape from _types/vpr.py — the v1.0.0 shape lives in _types/rerank.py. Excluded per task spec: * Concrete InlierCountReRanker (AZ-343). * C3 matcher protocol task (AZ-344, next in batch). * AC-9 single-thread binding + AC-10 LightGlueRuntime identity-share between C2.5/C3 — deferred per task spec Risk 3 until the generic compose_root thread-binding registry and the C3 factory both land. Tests: AC-1..AC-8 + AC-11 + NFR-perf-factory in tests/unit/c2_5_rerank/test_protocol_conformance.py. The legacy smoke test is removed. Full sweep: 997 passed (one pre-existing flake in test_az296_takeoff_abort, subprocess timing, unrelated to this commit; passes in isolation). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""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.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.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",
|
||||
) -> "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)`` 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}``.
|
||||
|
||||
Raises:
|
||||
StrategyNotAvailableError: compile-time flag OFF or
|
||||
concrete module not yet built (AZ-343 pending).
|
||||
"""
|
||||
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,
|
||||
)
|
||||
else:
|
||||
instance = create_fn(
|
||||
config,
|
||||
tile_store=tile_store,
|
||||
lightglue_runtime=lightglue_runtime,
|
||||
)
|
||||
_LOG.info(
|
||||
"c2_5.rerank.strategy_loaded",
|
||||
extra={"strategy": strategy, "top_n": block.top_n},
|
||||
)
|
||||
return instance
|
||||
Reference in New Issue
Block a user