"""C4 pose-estimator config block (AZ-355). Registered into the global config registry via ``register_component_block("c4_pose", C4PoseConfig)`` on import of ``gps_denied_onboard.components.c4_pose``. The runtime root reads ``config.components["c4_pose"]`` and dispatches to the ``opencv_gtsam`` strategy (the only one defined; the Protocol exists for ADR-009). """ from __future__ import annotations from dataclasses import dataclass from typing import Final from gps_denied_onboard.config.schema import ConfigError __all__ = ["KNOWN_POSE_STRATEGIES", "C4PoseConfig"] KNOWN_POSE_STRATEGIES: Final[frozenset[str]] = frozenset({"opencv_gtsam"}) @dataclass(frozen=True) class C4PoseConfig: """C4 pose-estimator config block. Fields per the C4 contract §"Config-load-time validation": * ``enabled`` — AZ-776 composition-graph flag. When ``False`` the composition root strips ``c4_pose`` from the airborne component graph entirely (open-loop ESKF profile per ADR-012). Default ``True`` preserves the full GTSAM pipeline that ADR-003 mandates as the steady-state airborne path. Forbidden pairings (rejected by ``compose_root`` with :class:`CompositionError`): ``enabled=False`` + ``c5_state.strategy="gtsam_isam2"`` (iSAM2 requires a C4 anchor); ``enabled=True`` + ``c5_state.strategy="eskf"`` (ESKF has no iSAM2 graph for C4 to anchor against). * ``strategy`` — selects the concrete estimator. Currently only ``"opencv_gtsam"`` is defined. * ``ransac_iterations`` — OpenCV ``solvePnPRansac`` iteration budget. Default 200 per the contract. * ``ransac_reprojection_threshold_px`` — RANSAC inlier-distance threshold. Default 4.0 pixels per the contract. * ``thermal_throttle_threshold_celsius`` — informational only; the actual ``ThermalState.thermal_throttle_active`` decision is owned by C7 (AZ-302). Default 75.0 °C. * ``covariance_degraded_warn_window_ns`` — AZ-361 rate-limit window for ``CovarianceDegradedWarning`` emissions AND the paired ``c4.pose.covariance_degraded`` WARN log + FDR record. Default 60 s. Set to 0 to disable rate-limiting (useful in tests that want to see every warning). * ``ridge_regularisation_epsilon`` — AZ-361 ridge added to ``JᵀJ/σ²`` before inversion to stabilise near-singular Jacobians on degenerate inlier sets. Default 1e-9; raise to 1e-6 if Jacobian-path SPD failures begin spiking in forensics. * ``tile_size_px`` — AZ-358 satellite-tile pixel dimensions (square). Used to map ``MatchResult`` inlier tile-pixel coordinates back to WGS84 lat/lon → local-ENU world points consumed by ``solvePnPRansac``. Default 256 matches the OSM / C6 tile-cache convention. If the upstream tile source provides a different square size, override at composition time; the spec assumes a square tile (any non-square tile handling would land in a future config extension). """ enabled: bool = True strategy: str = "opencv_gtsam" ransac_iterations: int = 200 ransac_reprojection_threshold_px: float = 4.0 thermal_throttle_threshold_celsius: float = 75.0 covariance_degraded_warn_window_ns: int = 60_000_000_000 ridge_regularisation_epsilon: float = 1e-9 tile_size_px: int = 256 def __post_init__(self) -> None: if self.strategy not in KNOWN_POSE_STRATEGIES: raise ConfigError( f"C4PoseConfig.strategy={self.strategy!r} not in {sorted(KNOWN_POSE_STRATEGIES)}" ) if self.ransac_iterations <= 0: raise ConfigError( f"C4PoseConfig.ransac_iterations must be > 0; got {self.ransac_iterations}" ) if self.ransac_reprojection_threshold_px <= 0.0: raise ConfigError( "C4PoseConfig.ransac_reprojection_threshold_px must be > 0; " f"got {self.ransac_reprojection_threshold_px}" ) if self.thermal_throttle_threshold_celsius <= 0.0: raise ConfigError( "C4PoseConfig.thermal_throttle_threshold_celsius must be > 0; " f"got {self.thermal_throttle_threshold_celsius}" ) if self.covariance_degraded_warn_window_ns < 0: raise ConfigError( "C4PoseConfig.covariance_degraded_warn_window_ns must be >= 0; " f"got {self.covariance_degraded_warn_window_ns}" ) if self.ridge_regularisation_epsilon <= 0.0: raise ConfigError( "C4PoseConfig.ridge_regularisation_epsilon must be > 0; " f"got {self.ridge_regularisation_epsilon}" ) if self.tile_size_px <= 0: raise ConfigError( f"C4PoseConfig.tile_size_px must be > 0; got {self.tile_size_px}" )