Move completed task specs from _docs/02_tasks/todo/ to _docs/02_tasks/done/ now that the four tickets are In Testing. Co-authored-by: Cursor <cursoragent@cursor.com>
10 KiB
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_XFEATrow._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.pydefining:XFeatMatcherclass implementing theCrossDomainMatcherProtocol.- Constructor:
__init__(self, runtime: InferenceRuntime, ransac_filter: RansacFilter, fdr_client: FdrClient, health_window: RollingHealthWindow, config: MatcherConfig). Note: NOlightglue_runtimeargument — XFeat does not use LightGlue. match(frame, rerank_result, calibration):- Decode + preprocess the nav-camera frame ONCE.
- For each
RerankCandidateinrerank_result.candidates: a. Decode + preprocess the candidate tile. b. Run XFeat forward (single pass: outputs combinedcorrespondencesdirectly — XFeat fuses extraction + matching). c. On failure: drop-and-continue (MatcherBackboneError,phase="xfeat_forward"). d. RANSAC + median residual viaransac_filter.filter(correspondences, threshold_px=...)— same helper as DISK+LightGlue. e. AppendCandidateMatchSetif survivors > 0. - Below-threshold / all-failed →
InsufficientInliersError(same semantics as AZ-345). - Sort survivors descending by
inlier_count; ties broken byper_candidate_residual_pxascending. - WARN on residual above threshold; INFO on ready; FDR
matcher.frame_doneper frame. RollingHealthWindow.updateafter each frame (success or failure).matcher_label = "xfeat".
- Module-level
create(config, lightglue_runtime, ransac_filter, inference_runtime, health_window) -> CrossDomainMatcher:lightglue_runtimeis accepted in the signature for factory uniformity but NOT stored / used.xfeat_weights_path = config.matcher.xfeat_weights_path(TRT engine produced by AZ-321).- Load XFeat engine via
inference_runtime.load_engine(...). - Construct
XFeatMatcher(...).
- Composition-root wiring path for
config.matcher.strategy == "xfeat". BUILD_MATCHER_XFEATflag 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
XFeatMatcherimplementation per theCrossDomainMatcherProtocol.- 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.updateinvocation 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_XFEATflag 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'screateignores 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):
matchp95 ≤ 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
createtime,RollingHealthWindow.updatecalled exactly once permatch. LightGlueRuntimeis NOT consumed — the factory'screatesignature accepts it for uniformity (so AZ-344's factory can call all three matchers'createwith 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
correspondencesndarray of shape(M, 4)with columns(px_query, py_query, px_tile, py_tile)— same as the post-LightGlue output of DISK+LightGlue. The sharedRansacFilterconsumes 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
createtime. Linking does NOT load engines; loading happens lazily in each backbone'screate. The factory only invokes ONEcreateper binary lifetime.
Runtime Completeness
- Named capability:
XFeatMatcher— alternate lightweightCrossDomainMatcher(architecture / E-C3 /solution.md/ AC-2.1a engine rule at matcher level). - Production code that must exist: real
XFeatMatchercalling real C7InferenceRuntimewith real TRT-compiled XFeat engine; real sharedRansacFilterfor inlier filtering + median residual; realRollingHealthWindow.updateafter 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).