Files
gps-denied-onboard/src/gps_denied_onboard/components/c4_pose/config.py
T
Oleksandr Bezdieniezhnykh 8de2716500 [AZ-776] Open-loop ESKF composition profile via c4_pose.enabled
ADR-012: add c4_pose.enabled (default True) and enforce the
(c4_pose.enabled, c5_state.strategy) 2x2 pairing matrix at compose
time. When enabled=false, compose_root removes c4_pose from the
selection map and build_pre_constructed omits c5_isam2_graph_handle.
Replay protocol Invariant 13 owns the gate. Tier-2 conftest YAML
writes the open-loop profile; un-xfails AC-1/2/5 and both AC-6
variants in Derkachi (AC-3 stays xfailed for AZ-777). 319/319
runtime_root + c4_pose + c5_state tests green.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 13:40:01 +03:00

111 lines
4.8 KiB
Python

"""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}"
)