mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 15:51:14 +00:00
8de2716500
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>
111 lines
4.8 KiB
Python
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}"
|
|
)
|