"""C2 ``VprStrategy`` Protocol (AZ-336). PEP 544 ``typing.Protocol`` with ``runtime_checkable=True``; three methods spanning the camera-ingest hot path (:meth:`embed_query` + :meth:`retrieve_topk`) and the composition-time pre-flight check (:meth:`descriptor_dim`). Concrete impls — :class:`UltraVprStrategy` (AZ-337), :class:`NetVladStrategy` (AZ-338), :class:`MegaLocStrategy` / :class:`MixVprStrategy` (AZ-339), :class:`SelaVprStrategy` / :class:`EigenPlacesStrategy` / :class:`SaladStrategy` (AZ-340) — live in sibling modules and are imported lazily by :mod:`gps_denied_onboard.runtime_root.vpr_factory`. The contract at ``_docs/02_document/contracts/c2_vpr/vpr_strategy_protocol.md`` v1.0.0 is the authoritative shape; this module mirrors it 1:1. """ from __future__ import annotations from typing import TYPE_CHECKING, Protocol, runtime_checkable if TYPE_CHECKING: from gps_denied_onboard._types.calibration import CameraCalibration from gps_denied_onboard._types.nav import NavCameraFrame from gps_denied_onboard._types.vpr import VprQuery, VprResult __all__ = ["VprStrategy"] @runtime_checkable class VprStrategy(Protocol): """Single-camera visual place recognition strategy. Stateless per-frame; the only persistent state is the loaded backbone weights and the C6-owned FAISS index handle (passed in via constructor by the strategy's ``create(...)`` factory). Invariants (see ``vpr_strategy_protocol.md`` v1.0.0): - **INV-1 single-threaded** — each instance is bound to one ingest thread; the composition root enforces. Concurrent :meth:`embed_query` calls on a single instance race the GPU stream. - **INV-2 stateless per-frame** — no implicit dependency on prior frames; reordering :meth:`embed_query` calls yields identical embeddings. - **INV-3 L2-normalised** — :attr:`VprQuery.embedding` is L2-normalised before return (cosine ≡ Euclidean on the FAISS HNSW lookup). - **INV-4 top-K size + order** — :meth:`retrieve_topk` returns exactly ``k`` candidates, ascending by :attr:`VprCandidate.descriptor_distance`. - **INV-5 backbone_label non-empty** — every :attr:`VprResult.backbone_label` matches the strategy's ``BUILD_VPR_`` lowercase form. - **INV-6 deterministic** — same frame + calibration + corpus → identical embedding + identical top-K (bit-exact for float32; ULP-tolerant for float16). - **INV-7 descriptor_dim stable** — :meth:`descriptor_dim` never changes after construction; reflects the loaded weights' output dim, NOT a config knob. Error envelope: only members of :class:`gps_denied_onboard.components.c2_vpr.errors.VprError` escape the three methods. Lower-level exceptions (CUDA, TRT, FAISS) MUST be rewrapped by the concrete strategy. """ def embed_query( self, frame: "NavCameraFrame", calibration: "CameraCalibration", ) -> "VprQuery": """Run the backbone forward pass; return a ``VprQuery``. Calibration is consumed by the strategy's internal :class:`BackbonePreprocessor` for resize / crop / normalise. Raises :class:`VprBackboneError` on backbone failure (CUDA OOM, TRT deserialize mismatch, etc.) and :class:`VprPreprocessError` on preprocessor contract violation. """ ... def retrieve_topk(self, query: "VprQuery", k: int) -> "VprResult": """Run the FAISS HNSW top-K lookup against the corpus index. The strategy holds the FAISS index handle (constructor-injected from C6's ``DescriptorIndex``). Top-K candidates are returned ascending by :attr:`VprCandidate.descriptor_distance`. Raises :class:`IndexUnavailableError` when the FAISS index handle is invalid (post-F8 reboot before warm-up; out-of-band file replacement caught by mmap defence; fewer than ``k`` indexed vectors). """ ... def descriptor_dim(self) -> int: """Backbone embedding dimensionality. Examples: 512 for UltraVPR; 4096 for NetVLAD-VGG16. Stable for the strategy's lifetime. Consumed by the composition root at startup to pre-validate index compatibility against the C6 ``DescriptorIndex`` sidecar (mismatch → :class:`ConfigError` at startup, NOT at first frame). """ ...