mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-23 06:11:12 +00:00
[AZ-271] [AZ-276] [AZ-278] [AZ-282] Finish cross-cutting helpers + relax opencv pin
E-CC-HELPERS closes with the three remaining Layer-1 helpers and E-CC-CONF closes with the env > YAML > defaults precedence test gate. All four tickets ship with frozen public surfaces, hermetic unit tests, and no upward (components.*) imports. * AZ-271 — tests/unit/shared/config/test_precedence.py (5 ACs + smoke test + helper that names the layer in failure messages). * AZ-282 — helpers/ransac_filter.py: static RansacFilter + RansacResult; cv2.setRNGSeed(0) for byte-equal determinism; median residual semantics pinned by contract. * AZ-276 — helpers/imu_preintegrator.py + make_imu_preintegrator; GTSAM PreintegratedCombinedMeasurements; strict-monotonic ts_ns guard runs before any state mutation. Adjacent hygiene: _types/nav.py ImuSample/ImuWindow now use ts_ns:int and the spec-mandated ImuBias dataclass. * AZ-278 — helpers/lightglue_runtime.py: structural R14 fix. LightGlueRuntime + non-blocking concurrent-access guard that raises rather than serialising. EngineHandle Protocol in _types/manifests.py + KeypointSet/CorrespondenceSet in _types/matching.py (Protocol surface adds approved by spec). Dependency conflict (Finding 1, user-approved): gtsam 4.2 (PyPI) is numpy-1.x-ABI only; opencv-python>=4.12 needs numpy>=2 at runtime. Resolution: opencv-python pin relaxed to >=4.11.0.86,<4.12. The D-CROSS-CVE-1 ratchet at ci/opencv_pin_gate.py is held at 4.11.0 with the original 4.12.0 floor restored once a numpy-2-compatible gtsam wheel ships. Full replay procedure in _docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md. Tests: 294 passed, 2 skipped (cmake/actionlint env-skips, pre-existing). 43 new tests added for batch 5. Ruff check + format clean. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,19 +1,133 @@
|
||||
"""Shared LightGlue inference runtime — STUB.
|
||||
"""`LightGlueRuntime` — shared LightGlue matcher (AZ-278 / E-CC-HELPERS / R14 fix).
|
||||
|
||||
R14 fix: this helper is the single owner; both C2.5 (single-pair inlier counter)
|
||||
and C3 (matcher) import it. Neither component depends on the other.
|
||||
Implements the `lightglue_runtime` contract v1.0.0 at
|
||||
`_docs/02_document/contracts/shared_helpers/lightglue_runtime.md`.
|
||||
|
||||
Concrete implementation is owned by AZ-278. Contract:
|
||||
`_docs/02_document/common-helpers/03_helper_lightglue_runtime.md`.
|
||||
Layer 1 helper — NO `gps_denied_onboard.components.*` imports. The
|
||||
engine handle is an opaque Protocol defined in `_types/manifests.py`;
|
||||
C7's `InferenceRuntime.deserialize_engine` produces the concrete handle
|
||||
and the composition root injects ONE shared instance into both C2.5
|
||||
(InlierBasedReranker) and C3 (CrossDomainMatcher) — the structural
|
||||
fix for R14.
|
||||
|
||||
Single-threaded by contract. The concurrent-access guard is
|
||||
non-blocking: concurrent entry RAISES `LightGlueConcurrentAccessError`
|
||||
rather than serialising, so a composition-root regression that wires
|
||||
the runtime into multiple threads is caught immediately instead of
|
||||
silently corrupting CUDA state.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
import threading
|
||||
|
||||
from gps_denied_onboard._types.manifests import EngineHandle
|
||||
from gps_denied_onboard._types.matching import CorrespondenceSet, KeypointSet
|
||||
|
||||
__all__ = [
|
||||
"LightGlueConcurrentAccessError",
|
||||
"LightGlueRuntime",
|
||||
"LightGlueRuntimeError",
|
||||
]
|
||||
|
||||
|
||||
class LightGlueRuntimeError(RuntimeError):
|
||||
"""Raised on construction guards or descriptor-dim mismatch."""
|
||||
|
||||
|
||||
class LightGlueConcurrentAccessError(RuntimeError):
|
||||
"""Raised when a concurrent ``match`` / ``match_batch`` entry is detected.
|
||||
|
||||
The serial-access invariant is a composition-root contract — if you
|
||||
see this exception, the runtime was wired into more than one thread
|
||||
by mistake. Fix the composition root, do NOT add a lock here.
|
||||
"""
|
||||
|
||||
|
||||
def _validate_keypoint_set(features: KeypointSet, *, name: str, expected_dim: int) -> None:
|
||||
if features.descriptors.ndim != 2 or features.descriptors.shape[1] != expected_dim:
|
||||
actual_dim = (
|
||||
features.descriptors.shape[1] if features.descriptors.ndim == 2 else "<bad shape>"
|
||||
)
|
||||
raise LightGlueRuntimeError(
|
||||
f"{name}: descriptor dim mismatch — engine expects {expected_dim}, "
|
||||
f"got {actual_dim} (descriptors.shape={features.descriptors.shape})"
|
||||
)
|
||||
|
||||
|
||||
class LightGlueRuntime:
|
||||
"""Shared LightGlue matcher runtime."""
|
||||
"""Shared LightGlue inference runtime.
|
||||
|
||||
def match(self, descriptors_a: Any, descriptors_b: Any) -> Any:
|
||||
raise NotImplementedError("LightGlueRuntime concrete impl is AZ-278")
|
||||
Single-thread by contract; concurrent entry raises.
|
||||
"""
|
||||
|
||||
def __init__(self, engine_handle: EngineHandle) -> None:
|
||||
if engine_handle is None:
|
||||
raise LightGlueRuntimeError(
|
||||
"LightGlueRuntime requires a non-None engine_handle (got None); "
|
||||
"composition root must inject the engine produced by C7's "
|
||||
"InferenceRuntime.deserialize_engine"
|
||||
)
|
||||
try:
|
||||
descriptor_dim = int(engine_handle.descriptor_dim)
|
||||
except AttributeError as exc:
|
||||
raise LightGlueRuntimeError(
|
||||
f"engine_handle missing required Protocol attribute 'descriptor_dim': {exc}"
|
||||
) from exc
|
||||
if descriptor_dim < 1:
|
||||
raise LightGlueRuntimeError(
|
||||
f"engine_handle.descriptor_dim must be >= 1; got {descriptor_dim}"
|
||||
)
|
||||
self._engine = engine_handle
|
||||
self._descriptor_dim = descriptor_dim
|
||||
# Non-blocking guard: ``try_acquire`` raises on contention rather
|
||||
# than serialising callers, per the contract's "concurrent calls
|
||||
# are a bug" stance.
|
||||
self._in_use = threading.Lock()
|
||||
|
||||
def descriptor_dim(self) -> int:
|
||||
return self._descriptor_dim
|
||||
|
||||
def match(self, features_a: KeypointSet, features_b: KeypointSet) -> CorrespondenceSet:
|
||||
"""Match a single pair (C2.5 path)."""
|
||||
if not self._in_use.acquire(blocking=False):
|
||||
raise LightGlueConcurrentAccessError(
|
||||
"LightGlueRuntime.match called from a second thread while another "
|
||||
"match is in flight — the runtime owns ONE CUDA stream and must be "
|
||||
"bound to a single hot-path thread by the composition root"
|
||||
)
|
||||
try:
|
||||
_validate_keypoint_set(features_a, name="features_a", expected_dim=self._descriptor_dim)
|
||||
_validate_keypoint_set(features_b, name="features_b", expected_dim=self._descriptor_dim)
|
||||
return self._engine.forward(features_a, features_b)
|
||||
finally:
|
||||
self._in_use.release()
|
||||
|
||||
def match_batch(
|
||||
self,
|
||||
features_a_list: list[KeypointSet],
|
||||
features_b_list: list[KeypointSet],
|
||||
) -> list[CorrespondenceSet]:
|
||||
"""Batch-match (C3 path) — iterates serially over the single CUDA stream."""
|
||||
if len(features_a_list) != len(features_b_list):
|
||||
raise LightGlueRuntimeError(
|
||||
f"match_batch: features_a_list (len={len(features_a_list)}) and "
|
||||
f"features_b_list (len={len(features_b_list)}) must have equal length"
|
||||
)
|
||||
if not self._in_use.acquire(blocking=False):
|
||||
raise LightGlueConcurrentAccessError(
|
||||
"LightGlueRuntime.match_batch called concurrently with another match"
|
||||
)
|
||||
try:
|
||||
results: list[CorrespondenceSet] = []
|
||||
for idx, (fa, fb) in enumerate(zip(features_a_list, features_b_list, strict=True)):
|
||||
_validate_keypoint_set(
|
||||
fa, name=f"features_a_list[{idx}]", expected_dim=self._descriptor_dim
|
||||
)
|
||||
_validate_keypoint_set(
|
||||
fb, name=f"features_b_list[{idx}]", expected_dim=self._descriptor_dim
|
||||
)
|
||||
results.append(self._engine.forward(fa, fb))
|
||||
return results
|
||||
finally:
|
||||
self._in_use.release()
|
||||
|
||||
Reference in New Issue
Block a user