# C2.5 — Inlier-based Re-rank

## 1. High-Level Overview

**Purpose**: re-rank C2's top-K=10 VPR candidates down to top-N=3 by single-pair LightGlue inlier count, producing a higher-precision input for the cross-domain matcher (C3). The re-rank step is the architectural boundary between cheap descriptor retrieval (C2) and expensive cross-domain matching (C3) — it pays a small extra cost so C3 only operates on the most promising candidates.

**Architectural Pattern**: Strategy (single concrete implementation today: `InlierCountReRanker`, AZ-343). Future re-rank algorithms can be added as additional `ReRankStrategy` implementations behind the same interface.

**Upstream dependencies**:
- C2 → `VprResult` (top-K=10 candidates).
- Shared `LightGlueRuntime` helper (used in single-pair mode for inlier counting; the same matcher object is shared with C3 — owned by the helper, not by C3, so neither component depends on the other at build time).
- Shared `FeatureExtractor` helper (`helpers/feature_extractor.py`, AZ-343 scope expansion) — extracts `KeypointSet` from both the per-frame nav image and each candidate's tile JPEG; the placeholder impl is `OpenCvOrbExtractor`, swapped out for a TRT-backed deep extractor before flight.
- C6 TileStore → fetch tile pixels for each candidate (cheap, in-memory page-cache hit during a flight).
- Camera calibration artifact — for nav-frame preprocessing.

**Downstream consumers**:
- C3 CrossDomainMatcher (consumes `RerankResult`).

## 2. Internal Interfaces

### Interface: `ReRankStrategy`

| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `rerank` | `NavCameraFrame, VprResult, n: int` | `RerankResult` | No | `RerankBackboneError`, `TileFetchError` |

**Input DTOs**:
```
NavCameraFrame:                see C1
VprResult:                     see C2
```

**Output DTOs**:
```
RerankResult:
  frame_id:            uuid
  candidates:          list[RerankCandidate] (length = n=3, ranked by inlier_count descending)
  reranked_at:         monotonic_ns

RerankCandidate:
  tile_id:             composite (zoomLevel, lat, lon)
  inlier_count:        int — single-pair LightGlue inliers
  descriptor_distance: float — carried forward from C2 for FDR provenance
  tile_pixels_handle:  Tile pixel reference (do not copy — page-cache hit)
```

## 3. External API Specification

Not applicable.

## 4. Data Access Patterns

| Query | Frequency | Hot Path | Index Needed |
|-------|-----------|----------|--------------|
| Tile pixel fetch from C6 (10 tiles per frame) | 3 Hz × 10 = 30 Hz | Yes | tile filesystem already mmap-backed in C6 |

No caching layer beyond C6's mmap. The same tile may be fetched repeatedly across frames; OS page cache absorbs that cost.

## 5. Implementation Details

**Algorithmic Complexity**: `O(K)` LightGlue forward passes per frame (K=10), each `O(M_tile · M_query)` in feature counts. The whole step is GPU-bound on the same engine that C3 uses — hence the shared LightGlue runtime.

**State Management**: stateless per-frame. Holds references to the constructor-injected `LightGlueRuntime`, `FeatureExtractor`, `TileStore`, `Clock`, and (optionally) `FdrClient` — all lifecycle-owned by the runtime root, not by C2.5.

**Key Dependencies**:

| Library | Version | Purpose |
|---------|---------|---------|
| LightGlue (Python) | upstream HEAD pinned per Plan-phase | Single-pair matching for inlier count |
| TensorRT | matches C7 | LightGlue inference engine reuse |

**Error Handling Strategy**:
- `RerankBackboneError`: LightGlue forward pass failed on one or more candidates. The candidate is dropped from the rerank set; if fewer than N=3 candidates survive, C2.5 returns whatever it has and C3 proceeds with reduced N.
- `TileFetchError`: C6 read failure for a candidate tile. Same drop-and-continue behaviour as above.
- Hard failure (zero candidates left after rerank): emit no `RerankResult`; C5 falls back to VIO-only with provenance label `visual_propagated`.

## 6. Extensions and Helpers

| Helper | Purpose | Used By |
|--------|---------|---------|
| `LightGlueRuntime` | shared LightGlue inference handle (one engine, many call sites) | C2.5, C3 |
| `FeatureExtractor` (`helpers/feature_extractor.py`) | shared image → `KeypointSet` extractor; default `OpenCvOrbExtractor`, target TRT-backed DISK/ALIKED | C2.5, future C3 backbones |
| `Clock` (`gps_denied_onboard.clock`) | composition-root time source; stamps `RerankResult.reranked_at` via `clock.monotonic_ns()` (Invariant 2 of the replay contract — no direct `time.*` in components) | every C* component |

## 7. Caveats & Edge Cases

**Known limitations**:
- The re-rank correctness depends on LightGlue inlier-count being a meaningful proxy for cross-domain match quality at single-pair resolution. If a backbone in C2 returns visually-similar-but-geographically-wrong candidates, C2.5's inlier count can still rank them above the true match — AC-NEW-7 cache-poisoning safety budget catches this downstream.

**Potential race conditions**:
- Shared LightGlue runtime is the same object as C3 uses. Serial access from a single ingest thread; concurrent calls forbidden.

**Performance bottlenecks**:
- 10 LightGlue passes per frame is non-trivial; budget allocation lives in `tests/performance-tests.md` NFT-PERF-01 partition.

## 8. Dependency Graph

**Must be implemented after**: C2 (input), shared `LightGlueRuntime` helper (which both C2.5 and C3 consume), C6 (tile pixels), C7 (inference runtime). C2.5 does **not** depend on C3 at build time — they are sibling consumers of the helper, and the data flow is C2.5 → C3 (not the other way).

**Can be implemented in parallel with**: C1 (independent path).

**Blocks**: C3 (no `RerankResult`, C3 has no input), F3 / F6.

## 9. Logging Strategy

| Log Level | When | Example |
|-----------|------|---------|
| ERROR | Zero candidates surviving rerank | `Re-rank produced 0 candidates; frame=12345; falling back to visual_propagated` |
| WARN | <N=3 candidates surviving | `Re-rank produced 1 candidate of N=3; frame=12345` |
| INFO | Strategy ready | `Re-rank ready: strategy=inlier_count, N=3, K=10` |
| DEBUG | Per-frame inlier counts | `Re-rank frame=12345 inlier_counts=[412, 287, 198, ...]` |

**Log format**: structured JSON.
**Log storage**: stdout / journald / FDR via C13 (ERROR + WARN only).
