# Common Helper — `LightGlueRuntime` ## Purpose Shared LightGlue inference handle. C2.5 (Re-rank) does single-pair LightGlue matching for inlier counting on K=10 candidates per frame; C3 (CrossDomainMatcher) does the heavier matching pass on the surviving N=3 candidates. Both use the same LightGlue engine; sharing the engine avoids paying the engine-build / GPU-memory cost twice. ## Used By - C2.5 — Inlier-based Re-rank. - C3 — Cross-domain Matcher. ## Interface (sketch) ``` class LightGlueRuntime: def __init__(engine_handle: EngineHandle): ... def match(features_a: KeypointSet, features_b: KeypointSet) -> CorrespondenceSet def match_batch(features_a_list, features_b_list) -> list[CorrespondenceSet] def descriptor_dim() -> int ``` ## Implementation Notes - Owned by the composition root; the same instance is constructor-injected into both C2.5 and C3. - Backed by C7's `InferenceRuntime.deserialize_engine(LIGHTGLUE_ENGINE_CACHE_ENTRY)` at takeoff. - Single CUDA stream; concurrent calls forbidden — composition root binds the runtime to the single F3 hot-path thread. ## Caveats - The features fed in MUST come from the same backbone as the LightGlue engine was trained for (DISK in production-default; ALIKED / XFeat in alternates). Mixing backbones is a runtime error caught by the matcher's input shape check. ## Cycle-1 operational reality The shipped surface in `src/gps_denied_onboard/helpers/lightglue_runtime.py` (AZ-278) is the structural fix for R14 (re-rank vs. matcher double-load of the LightGlue engine). Composition root wires ONE `LightGlueRuntime` instance and constructor-injects it into both C2.5 (`InlierBasedReranker`) and C3 (`CrossDomainMatcher`). - **Constructor** — `LightGlueRuntime(engine_handle: EngineHandle)`. `EngineHandle` is a `Protocol` from `_types/manifests.py` (descriptor_dim, forward(...)) — Layer 1 helper invariant means we do NOT import `gps_denied_onboard.components.*`. C7's `InferenceRuntime.deserialize_engine(LIGHTGLUE_ENGINE_CACHE_ENTRY)` returns the concrete handle at takeoff; the composition root passes it in. - **Construction guards** — `LightGlueRuntimeError` is raised for: `engine_handle is None`; `engine_handle` missing the `descriptor_dim` Protocol attribute; `descriptor_dim < 1`. - **Descriptor-dim mismatch** — both `match` and `match_batch` validate every `KeypointSet.descriptors` against the engine's `descriptor_dim` and raise `LightGlueRuntimeError` on mismatch (catches "DISK features fed into an ALIKED-trained LightGlue" regressions). - **Concurrent-access guard is non-blocking** — the runtime owns a `threading.Lock` but never `.acquire(blocking=True)`. Concurrent entry raises `LightGlueConcurrentAccessError` immediately rather than serialising. This is intentional: if you see this exception, the composition root wired the runtime into more than one thread by mistake — fix the composition, do NOT add blocking serialisation. The lock guards the body of both `match` and `match_batch`; `descriptor_dim()` is lock-free. - **`match_batch` equal-length precondition** — `LightGlueRuntimeError` if `len(features_a_list) != len(features_b_list)`. Iteration uses `zip(..., strict=True)`. Indexed validation labels (`features_a_list[i]`) so a downstream test failure points to the offending pair. - **`descriptor_dim()` accessor** — returns the engine's descriptor dim as a plain `int` (cached on construction so per-call overhead is one attribute lookup). - **Public exceptions** — `LightGlueRuntimeError` (construction / descriptor-dim mismatch / batch-length mismatch) and `LightGlueConcurrentAccessError` (composition-root violation). Both subclass `RuntimeError`. ### Cycle-1 task lineage - AZ-278 — initial helper, contract producer. - R14 structural fix: composition-root single-instance injection into C2.5 + C3 lands in `runtime_root/airborne_bootstrap.py` (the `lightglue_runtime` pre-constructed key consumed by both `_C2_5_STRATEGIES` and `_C3_STRATEGIES`).