mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 12:01:13 +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:
@@ -4,7 +4,7 @@
|
||||
**Producer task**: AZ-342 (`ReRankStrategy` Protocol + factory + composition)
|
||||
**Consumer tasks**: AZ-343 (`InlierCountReRanker` impl); downstream c3_matcher (epic AZ-257 / E-C3 — TBD at AZ-257 decompose time) which consumes `RerankResult`
|
||||
**Version**: 1.0.0
|
||||
**Status**: draft, awaiting AZ-342 implementation
|
||||
**Status**: v1.0.0 (AZ-342 implemented 2026-05-12)
|
||||
**Last Updated**: 2026-05-10
|
||||
**Module-layout home**: `src/gps_denied_onboard/components/c2_5_rerank/interface.py` (Protocol), `src/gps_denied_onboard/components/c2_5_rerank/__init__.py` (re-exports), `src/gps_denied_onboard/runtime_root/rerank_factory.py` (factory)
|
||||
|
||||
@@ -75,31 +75,29 @@ class ReRankStrategy(Protocol):
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RerankCandidate:
|
||||
"""One re-rank survivor. Carries the C2-stage descriptor_distance forward for FDR provenance plus the new inlier_count from single-pair LightGlue."""
|
||||
|
||||
tile_id: tuple # composite (zoomLevel, lat, lon); see C6 TileRecord
|
||||
inlier_count: int # single-pair LightGlue inliers; > 0 for any survivor
|
||||
descriptor_distance: float # carried forward from C2's VprCandidate
|
||||
descriptor_dim: int # carried forward from C2 for sanity assertions
|
||||
tile_pixels_handle: object # opaque page-cache-backed pixel reference; see C6 TileStore contract
|
||||
tile_id: tuple[int, float, float] # composite (zoom_level, lat, lon); matches c6_tile_cache.TileId. tuple form keeps _types/ free of an L1→L3 import per module-layout.md.
|
||||
inlier_count: int # single-pair LightGlue inliers; > 0 for any survivor
|
||||
descriptor_distance: float # carried forward from C2's VprCandidate
|
||||
descriptor_dim: int # carried forward from C2 for sanity assertions
|
||||
tile_pixels_handle: object # opaque page-cache-backed pixel reference; see C6 TileStore contract
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RerankResult:
|
||||
"""Top-N survivors from `ReRankStrategy.rerank`. Consumed by C3 CrossDomainMatcher."""
|
||||
|
||||
frame_id: UUID
|
||||
candidates: list[RerankCandidate] # 0 < len <= n; sorted descending by inlier_count, ties broken by descriptor_distance ascending
|
||||
reranked_at: int # monotonic_ns
|
||||
rerank_label: str # non-empty; matches BUILD_RERANK_<variant> lowercase (e.g., "inlier_count")
|
||||
candidates_input: int # len(vpr_result.candidates) at entry — for FDR observability
|
||||
candidates_dropped: int # candidates_input - len(candidates)
|
||||
frame_id: int # echoes NavCameraFrame.frame_id (int across the pipeline)
|
||||
candidates: tuple[RerankCandidate, ...] # 0 < len <= n; descending by inlier_count, ties broken by descriptor_distance ascending. tuple (not list) so the frozen+slots invariant holds.
|
||||
reranked_at: int # monotonic_ns from injected Clock
|
||||
rerank_label: str # non-empty; matches BUILD_RERANK_<variant> lowercase
|
||||
candidates_input: int # len(vpr_result.candidates) at entry — for FDR observability
|
||||
candidates_dropped: int # candidates_input - len(candidates)
|
||||
```
|
||||
|
||||
### Error Hierarchy (in `c2_5_rerank/errors.py`)
|
||||
|
||||
Reference in New Issue
Block a user