# C3 XFeat Alternate Lightweight Matcher

**Task**: AZ-347_c3_xfeat
**Name**: C3 XFeat Alternate Lightweight Matcher
**Description**: Implement `XFeatMatcher`, the lightweight alternate `CrossDomainMatcher`. XFeat combines feature extraction AND matching in a single forward pass (no separate LightGlue stage); selectable via `config.matcher.strategy = "xfeat"`. Target use case: low-power / thermal-throttled scenarios where DISK+LightGlue's combined cost (~180 ms p95) exceeds the C4 hybrid's degraded budget. Drop-and-continue + below-threshold + best-candidate selection contracts inherited from the Protocol unchanged. RANSAC + median residual still computed via the shared `RansacFilter`.
**Complexity**: 3 points
**Dependencies**: AZ-344 (Protocol + factory + DTOs + errors + RollingHealthWindow), AZ-263_initial_structure, AZ-269_config_loader, AZ-282_ransac_filter, AZ-298_c7_tensorrt_runtime, AZ-299_c7_onnxrt_fallback, AZ-303_c6_storage_interfaces, AZ-281_engine_filename_schema (XFeat engine self-describing filename), AZ-321_c10_engine_compiler (XFeat engine compile path), AZ-266_log_module, AZ-272_fdr_record_schema
**Component**: c3_matcher (epic AZ-257 / E-C3)
**Tracker**: AZ-347
**Epic**: AZ-257 (E-C3)

### Document Dependencies

- `_docs/02_document/contracts/c3_matcher/cross_domain_matcher_protocol.md` — Protocol contract.
- `_docs/02_document/components/04_c3_matcher/description.md` — § 1 XFeat alternate (lightweight); § 5 error handling; § 9 logging.
- `_docs/02_document/module-layout.md` — `BUILD_MATCHER_XFEAT` row.
- `_docs/02_document/contracts/shared_helpers/ransac_filter.md` — RANSAC filtering API.
- `_docs/02_document/contracts/c2_5_rerank/rerank_strategy_protocol.md`.
- `_docs/02_document/contracts/c7_inference/inference_runtime_protocol.md`.

## Problem

Without this task: there is no lightweight matcher option for thermal-throttled scenarios; if the C4 hybrid switches to Jacobian (per ADR-006 / D-CROSS-LATENCY-1) but C3's per-frame budget still allows the heavy DISK+LightGlue path, the system has no mechanism to reduce C3's cost too. XFeat is also the documented mandatory simple-baseline alternative for IT-12 comparative study (AC-2.1a engine rule applied at the matcher level, with NetVLAD acting at the VPR level).

## Outcome

- `src/gps_denied_onboard/components/c3_matcher/xfeat.py` defining:
  - `XFeatMatcher` class implementing the `CrossDomainMatcher` Protocol.
  - Constructor: `__init__(self, runtime: InferenceRuntime, ransac_filter: RansacFilter, fdr_client: FdrClient, health_window: RollingHealthWindow, config: MatcherConfig)`. Note: NO `lightglue_runtime` argument — XFeat does not use LightGlue.
  - `match(frame, rerank_result, calibration)`:
    1. Decode + preprocess the nav-camera frame ONCE.
    2. For each `RerankCandidate` in `rerank_result.candidates`:
       a. Decode + preprocess the candidate tile.
       b. Run XFeat forward (single pass: outputs combined `correspondences` directly — XFeat fuses extraction + matching).
       c. On failure: drop-and-continue (`MatcherBackboneError`, `phase="xfeat_forward"`).
       d. RANSAC + median residual via `ransac_filter.filter(correspondences, threshold_px=...)` — same helper as DISK+LightGlue.
       e. Append `CandidateMatchSet` if survivors > 0.
    3. Below-threshold / all-failed → `InsufficientInliersError` (same semantics as AZ-345).
    4. Sort survivors descending by `inlier_count`; ties broken by `per_candidate_residual_px` ascending.
    5. WARN on residual above threshold; INFO on ready; FDR `matcher.frame_done` per frame.
    6. `RollingHealthWindow.update` after each frame (success or failure).
    7. `matcher_label = "xfeat"`.
  - Module-level `create(config, lightglue_runtime, ransac_filter, inference_runtime, health_window) -> CrossDomainMatcher`:
    1. `lightglue_runtime` is accepted in the signature for factory uniformity but NOT stored / used.
    2. `xfeat_weights_path = config.matcher.xfeat_weights_path` (TRT engine produced by AZ-321).
    3. Load XFeat engine via `inference_runtime.load_engine(...)`.
    4. Construct `XFeatMatcher(...)`.
- Composition-root wiring path for `config.matcher.strategy == "xfeat"`.
- `BUILD_MATCHER_XFEAT` flag wiring (ON in research; ON in airborne if config selects it; OFF in operator-tooling).
- All logging + FDR records identical structure to AZ-345 with `matcher_label = "xfeat"`.

## Scope

### Included
- `XFeatMatcher` implementation per the `CrossDomainMatcher` Protocol.
- XFeat forward via C7 `InferenceRuntime`.
- RANSAC + median residual via shared `RansacFilter` (NO LightGlue).
- Same drop-and-continue + below-threshold + best-candidate selection as AZ-345.
- Same `RollingHealthWindow.update` invocation pattern.
- Composition-root wiring path.
- XFeat-specific preprocessor inline.
- Unit tests covering Invariants 1–9 + drop-and-continue + below-threshold + deterministic ordering. Parametrised across XFeat-specific test fixtures (lightweight model output is different shape from DISK).
- `BUILD_MATCHER_XFEAT` flag wiring.

### Excluded
- The Protocol + DTOs + errors + factory + `RollingHealthWindow` — owned by AZ-344.
- `RansacFilter` (AZ-282).
- `LightGlueRuntime` (AZ-278) — XFeat does NOT consume this helper; the factory's signature includes it for uniformity but XFeat's `create` ignores the parameter.
- C7 runtime stack (AZ-297..AZ-300).
- XFeat engine compile (AZ-321).
- Component-internal acceptance tests beyond Protocol + invariants smoke.
- DISK matcher (AZ-345) and ALIKED matcher (AZ-346).

## Acceptance Criteria

**AC-1 through AC-10**: identical contract to AZ-345 AC-1..AC-10 (Protocol conformance, best-candidate selection, drop-and-continue, below-threshold, residual WARN, health update, correspondences shape, determinism). `matcher_label = "xfeat"`.

**AC-11: Composition-root wiring**
Given `config.matcher.strategy = "xfeat"` AND `BUILD_MATCHER_XFEAT = ON`
When `compose_root(config)` runs
Then an `XFeatMatcher` instance is wired; ONE INFO log `kind="c3.matcher.ready"` with `{strategy: "xfeat", ...}` is emitted. The strategy does NOT hold a reference to `LightGlueRuntime` (verifiable via `not hasattr(strategy, "_lightglue_runtime")` OR `strategy._lightglue_runtime is None`).

**AC-12: FDR `matcher.frame_done` per frame**
Same shape as AZ-345 AC-12 with `matcher_label = "xfeat"`.

**AC-special-1: XFeat single-pass forward — no LightGlue call**
Given a `match(...)` call where the `LightGlueRuntime` test double is provided to the factory
When the call completes
Then `lightglue_runtime.match_*` is NEVER invoked (verified by mock assertion `lightglue_runtime.match_pair.assert_not_called()`).

**AC-special-2: XFeat lower latency than DISK+LightGlue (informational, not gated)**
Given identical hardware and identical inputs
When `match(...)` is microbenchmarked × 100 frames
Then XFeat's per-call p95 is < AZ-345's per-call p95 (informational metric; if XFeat is NOT faster, that's a backbone misconfiguration, not a contract violation. Documented in the test report; does NOT block this AC).

## Non-Functional Requirements

**Performance** (deferred to C3-PT-01):
- `match` p95 ≤ 100 ms (informational target; XFeat is the lightweight option). NOT a hard gate; the hard gate is C3-PT-01's overall envelope.
- GPU memory ≤ 300 MB (XFeat single engine; smaller than DISK+LightGlue).

**Compatibility**
- XFeat engine file format owned by C10 + C7.

**Reliability**
- Same as AZ-345.

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|--------------|------------------|
| AC-1..AC-10 | Identical to AZ-345 AC-1..AC-10 with `matcher_label = "xfeat"` | Same outcomes |
| AC-11 | `compose_root(config="xfeat")` | Wired; INFO log; no LightGlue dependency |
| AC-12 | FDR `frame_done` emission | Correct fields; `matcher_label = "xfeat"` |
| AC-special-1 | LightGlue NOT invoked | `lightglue_runtime.match_pair.assert_not_called()` |
| AC-special-2 | Latency comparison | (Informational; not gated) |

## Constraints

- **Same constraints as AZ-345** — drop-and-continue mandatory, median residual, constructor injection, helpers constructor-injected, engine load at `create` time, `RollingHealthWindow.update` called exactly once per `match`.
- **`LightGlueRuntime` is NOT consumed** — the factory's `create` signature accepts it for uniformity (so AZ-344's factory can call all three matchers' `create` with the same args) but XFeatMatcher does NOT store or use it. Test AC-special-1 enforces this.
- **XFeat-specific preprocessing parameters are hard-coded** (weights-coupled, same rule as DISK and ALIKED).

## Risks & Mitigation

**Risk 1: XFeat output schema differs from DISK+LightGlue output (correspondences format)**
- *Mitigation*: XFeat outputs `correspondences` ndarray of shape `(M, 4)` with columns `(px_query, py_query, px_tile, py_tile)` — same as the post-LightGlue output of DISK+LightGlue. The shared `RansacFilter` consumes this format identically. If XFeat's upstream output differs, this task adapts inside the strategy.

**Risk 2: XFeat's RANSAC inlier counts may be systematically lower** (lighter-weight model produces noisier matches)
- *Mitigation*: AC-2.1a engine rule applies (XFeat is the simple baseline at the matcher level); the ≥ 80 inlier count floor (AC-1.1) may not hold for XFeat. C3-IT-01 measures this; if XFeat fails AC-1.1 on Derkachi, it remains as the "engine rule" comparison baseline NOT the production-default — same engine-rule semantics as NetVLAD at C2.

**Risk 3: Linking three backbones into one binary inflates GPU memory headroom**
- *Mitigation*: per ADR-002 / D-C7-13, only the SELECTED backbone's engine is loaded at `create` time. Linking does NOT load engines; loading happens lazily in each backbone's `create`. The factory only invokes ONE `create` per binary lifetime.

## Runtime Completeness

- **Named capability**: `XFeatMatcher` — alternate lightweight `CrossDomainMatcher` (architecture / E-C3 / `solution.md` / AC-2.1a engine rule at matcher level).
- **Production code that must exist**: real `XFeatMatcher` calling real C7 `InferenceRuntime` with real TRT-compiled XFeat engine; real shared `RansacFilter` for inlier filtering + median residual; real `RollingHealthWindow.update` after each frame; real composition-root wiring.
- **Allowed external stubs**: `FakeInferenceRuntime`, `FakeRansacFilter`, `FakeFdrClient`, `FakeLightGlueRuntime` (passed but unused).
- **Unacceptable substitutes**: a Python+NumPy XFeat forward (would not satisfy the lightweight-target latency); using a different RANSAC implementation; storing/calling `LightGlueRuntime` (would defeat XFeat's single-pass design).
