"""C4 ``PoseEstimator`` Protocol (AZ-355). Per ADR-009 (interface-first DI) consumers (C5, runtime root, tests) hold a typed reference to ``PoseEstimator`` rather than the concrete ``OpenCVGtsamPoseEstimator`` (AZ-358) class. ADR-002 build-time exclusion does NOT apply — exactly one concrete implementation exists — but the Protocol + factory wiring matches the C2 / C2.5 / C3 / C3.5 pattern for symmetry. The Protocol surface is two methods: * :meth:`PoseEstimator.estimate` — full per-frame pose estimation pipeline (PnP + factor add + covariance recovery). Raises :class:`PnpFailureError` on RANSAC failure. * :meth:`PoseEstimator.current_covariance_mode` — exposes the per-frame decision (marginals / jacobian) for C5 FDR provenance and the C4-IT-03 mode-switch test. The Protocol is ``@runtime_checkable`` so test fakes pass ``isinstance(fake, PoseEstimator)``. """ 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.matching import MatchResult from gps_denied_onboard._types.pose import CovarianceMode, PoseEstimate from gps_denied_onboard._types.thermal import ThermalState __all__ = ["PoseEstimator"] @runtime_checkable class PoseEstimator(Protocol): """Single-pose estimator producing WGS84 + 6x6 covariance + provenance label. Stateless per-frame except for the constructor-injected shared GTSAM substrate (owned by C5). Per Invariant 1 the implementation is bound to the same ingest thread as C5 (composition root enforces). """ def estimate( self, match_result: MatchResult, calibration: CameraCalibration, thermal_state: ThermalState, ) -> PoseEstimate: """Run PnP → factor add → covariance recovery. Per-frame thermal decision: ``thermal_state.throttle == True`` engages the Jacobian path (cheap, ~5-10 % accuracy loss); ``False`` engages the Marginals path (production default). Raises: PnpFailureError: RANSAC convergence failure or degenerate match geometry. C5 owns the fallback decision; this method NEVER returns a fallback ``PoseEstimate``. """ def current_covariance_mode(self) -> CovarianceMode: """Return the mode used for the LAST :meth:`estimate` call. Consumed by C5 for FDR provenance and by C4-IT-03 to verify the per-frame mode switch. """