# Contract: lightglue_runtime

**Component**: shared_helpers / `helpers.lightglue_runtime` (cross-cutting concern owned by E-CC-HELPERS / AZ-264)
**Producer task**: AZ-278 — `_docs/02_tasks/todo/AZ-278_lightglue_runtime.md`
**Consumer tasks**: C2.5 InlierBasedReranker (single-pair LightGlue inlier counter); C3 CrossDomainMatcher (heavier matching pass)
**Version**: 1.0.0
**Status**: draft
**Last Updated**: 2026-05-10

## Purpose

Single owner of the LightGlue inference engine. C2.5 does single-pair LightGlue matching for inlier counting on K=10 candidates per frame; C3 does the heavier matching pass on the surviving N=3 candidates. Both consume the SAME LightGlue engine — sharing avoids paying the engine-build / GPU-memory cost twice and structurally prevents the C2.5 ↔ C3 import cycle (R14 fix in `_docs/02_document/epics.md`). Per `_docs/02_document/common-helpers/03_helper_lightglue_runtime.md`.

## Shape

### For function / method APIs

```python
class LightGlueRuntime:
    def __init__(self, engine_handle: EngineHandle) -> None: ...
    def descriptor_dim(self) -> int: ...
    def match(
        self,
        features_a: KeypointSet,
        features_b: KeypointSet,
    ) -> CorrespondenceSet: ...
    def match_batch(
        self,
        features_a_list: list[KeypointSet],
        features_b_list: list[KeypointSet],
    ) -> list[CorrespondenceSet]: ...
```

| Name | Signature | Throws / Errors | Blocking? |
|------|-----------|-----------------|-----------|
| `__init__` | `(engine_handle: EngineHandle) -> None` | `LightGlueRuntimeError` if `engine_handle` is None or descriptor_dim < 1 | sync, one-time |
| `descriptor_dim` | `() -> int` | none | sync, in-memory |
| `match` | `(KeypointSet, KeypointSet) -> CorrespondenceSet` | `LightGlueRuntimeError` if descriptor dims mismatch the engine's expected dim, or if a concurrent caller tries to enter | sync, GPU-bound |
| `match_batch` | `(list[KeypointSet], list[KeypointSet]) -> list[CorrespondenceSet]` | same as `match` | sync, GPU-bound |

`EngineHandle`, `KeypointSet`, and `CorrespondenceSet` are imported from `gps_denied_onboard._types`. `EngineHandle` is a Protocol (NOT a concrete class) so this helper does not import any Layer 2+ component; the production handle is created by C7's `InferenceRuntime.deserialize_engine` and injected by the composition root.

### Construction

The composition root constructs the runtime once at takeoff:

```python
engine_handle = inference_runtime.deserialize_engine(LIGHTGLUE_ENGINE_CACHE_ENTRY)
runtime = LightGlueRuntime(engine_handle)
# inject the SAME instance into both consumers
c2_5_reranker = InlierBasedReranker(..., lightglue_runtime=runtime, ...)
c3_matcher = CrossDomainMatcher(..., lightglue_runtime=runtime, ...)
```

## Invariants

- **Serial-access invariant** (R14 cross-component): the runtime owns ONE CUDA stream. Concurrent calls to `match` / `match_batch` from multiple threads are FORBIDDEN. The composition root binds the runtime to the single F3 hot-path thread (per `_docs/02_document/epics.md` R14 entry). The helper's contract test asserts a guard exists that rejects concurrent entry with `LightGlueConcurrentAccessError`.
- **Backbone consistency**: features fed in MUST come from the same backbone as the LightGlue engine was trained for (DISK in production-default; ALIKED / XFeat alternates). Mixing backbones is a runtime error caught by the input shape check (`descriptor_dim` mismatch raises `LightGlueRuntimeError`). The helper does NOT silently coerce dimensions.
- **No shared mutable state**: the runtime exposes no `set_*` / `update_*` methods. Once constructed with an `engine_handle`, its behaviour is fixed for its lifetime.
- **No upward imports** (Layer 1): the module imports ONLY from `_types`, numpy, and stdlib. NO `gps_denied_onboard.components.*` imports — neither C2.5 nor C3 nor C7 — under any circumstance. This is the structural fix for R14: the helper sits below the components in the layering, so the C2.5 ↔ C3 cycle becomes impossible to express.
- **Engine handle is opaque**: the helper does not know whether the handle wraps a TensorRT engine, an ONNX session, or a PyTorch model. It calls a fixed Protocol surface (`forward(...)`, `descriptor_dim`); the implementation owner is C7.

## Non-Goals

- Engine compilation / serialisation — owned by C7 (via `EngineFilenameSchema` + the inference runtime).
- Engine cache management / takeoff load — owned by C10 (`CacheProvisioner`).
- Backbone-specific feature extraction (DISK, ALIKED, XFeat) — owned by C3 / C7.
- Multi-GPU sharding — out of scope; production target is single-GPU Tier-2.
- Mixed-backbone matching (cross-DISK-ALIKED) — out of scope; consumers ensure backbone consistency before calling.

## Versioning Rules

- **Breaking changes** (method renamed/removed, signature changed, `EngineHandle` Protocol changed, serial-access invariant relaxed) require a new major version + a deprecation pass through C2.5 and C3.
- **Non-breaking additions** (new optional kwarg with safe default, new diagnostic accessor) require a minor version bump.
- Changing the underlying engine format (TensorRT → ONNX) is NOT a contract change because the helper's surface treats the handle as opaque — but it IS a C7 contract change and must follow C7's versioning rules.

## Test Cases

| Case | Input | Expected | Notes |
|------|-------|----------|-------|
| valid-single-pair | two `KeypointSet`s of matching descriptor dim | `CorrespondenceSet` returned with `len > 0` for a synthetic-overlap pair | Round-trip happy path (C2.5 use) |
| valid-batch-3 | three pairs of `KeypointSet`s | three `CorrespondenceSet`s returned in order | Batch path (C3 use) |
| invalid-dim-mismatch | features with `descriptor_dim` not matching the engine | `LightGlueRuntimeError` mentions the expected vs actual dim | Backbone-consistency invariant |
| invalid-concurrent-access | two threads call `match` simultaneously | `LightGlueConcurrentAccessError` raised in the second-entering thread | R14 serial-access invariant |
| invalid-empty-handle | `LightGlueRuntime(engine_handle=None)` | `LightGlueRuntimeError` raised at construction | Construction guard |
| no-upward-imports | static import scan | only `_types`, numpy, stdlib — no `components.*` | R14 structural fix |
| determinism-given-engine | same `(features_a, features_b)` matched twice with the same engine handle | byte-equal `CorrespondenceSet` outputs | Pure-function determinism downstream of the engine |

## Change Log

| Version | Date | Change | Author |
|---------|------|--------|--------|
| 1.0.0 | 2026-05-10 | Initial contract derived from `_docs/02_document/common-helpers/03_helper_lightglue_runtime.md` (R14 fix) | autodev decompose Step 2 |
