mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 13:41:14 +00:00
[AZ-358] [AZ-361] C4 OpenCVGtsamPoseEstimator + Jacobian thermal hybrid
Implement the single production-default C4 PoseEstimator strategy. AZ-358 — Marginals path: OpenCV solvePnPRansac (SOLVEPNP_IPPE) on best-candidate inliers, PriorFactorPose3 with Jacobian-derived initial covariance, flushed into C5's iSAM2 graph via the widened ISam2GraphHandle.update(graph, values, None) (Option B). Posterior covariance from compute_marginals().marginalCovariance(pose_key) with SPD-defensive Cholesky check. Tile pixel -> ENU world conversion via the shared WgsConverter + a configurable tile_size_px. Two spec deviations now documented in the AZ-358 task file: PriorFactorPose3 over GenericProjectionFactorCal3DS2 (avoids unbounded landmark variables; same Fisher information on the pose marginal) and explicit (graph, values, timestamps) update args (aligns with C5's impl). AZ-361 — Jacobian + thermal hybrid: per-frame dispatch on thermal_state.thermal_throttle_active selects the cv2.projectPoints- derived 6x6 information matrix (with ridge regularisation) as the emitted covariance. Skips the iSAM2 factor add under throttle (Invariant 12). Emits CovarianceDegradedWarning via warnings.warn (never raised); paired WARN log + FDR record rate-limited per covariance_degraded_warn_window_ns (default 60 s) via an injected monotonic Clock. Supersedes the AZ-358 NotImplementedError stub. Widens ISam2GraphHandle from get_pose_key only to all five C4-facing methods (add_factor, update, compute_marginals, last_anchor_age_ms); C5's existing ISam2GraphHandleImpl already satisfies the superset, so no C5 source change this batch. Threads fdr_client + clock through pose_factory composition. Registers two new FDR payload kinds: pose.frame_done (per-call telemetry; both success and PnpFailureError paths) and pose.covariance_degraded (per-window throttle exposure). Tests: 21 new (AZ-358 AC-1..11 + AZ-361 AC-1..10/12/13; AZ-361 AC-11 RMSE-ratio informational per spec, not asserted). Updates 2 existing test files for Protocol widening and the FDR-schema round trip. Code review verdict: PASS_WITH_WARNINGS (5 findings: Medium x2, Low x3; none blocking). Full suite: 1958 passed, 1 unrelated host-dependent perf failure (c12 CLI cold-start, pre-existing). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -78,11 +78,28 @@ def _build_config(**overrides: Any) -> Config:
|
||||
|
||||
|
||||
class _FakeISam2GraphHandle:
|
||||
"""Minimal handle stub for factory / Protocol tests."""
|
||||
"""Minimal handle stub for factory / Protocol tests.
|
||||
|
||||
Implements the AZ-358-extended 5-method surface — the AZ-355
|
||||
AC-10 ``isinstance(handle, ISam2GraphHandle)`` runtime-checkable
|
||||
test now expects all five methods.
|
||||
"""
|
||||
|
||||
def get_pose_key(self, frame_id: int) -> int:
|
||||
return int(frame_id)
|
||||
|
||||
def add_factor(self, factor: Any) -> None:
|
||||
return None
|
||||
|
||||
def update(self, graph: Any, values: Any, timestamps: Any | None = None) -> None:
|
||||
return None
|
||||
|
||||
def compute_marginals(self) -> Any:
|
||||
return None
|
||||
|
||||
def last_anchor_age_ms(self) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
class _FakePoseEstimator:
|
||||
"""Test double satisfying the full PoseEstimator Protocol."""
|
||||
@@ -378,6 +395,18 @@ def test_ac10_isam2_graph_handle_rejects_missing_method() -> None:
|
||||
assert not isinstance(_NoMethod(), ISam2GraphHandle)
|
||||
|
||||
|
||||
def test_ac10_isam2_graph_handle_rejects_partial_surface() -> None:
|
||||
"""AZ-358 widened the Protocol to 5 methods; a handle that only
|
||||
implements the original ``get_pose_key`` no longer satisfies
|
||||
runtime_checkable conformance."""
|
||||
|
||||
class _OnlyGetPoseKey:
|
||||
def get_pose_key(self, frame_id: int) -> int:
|
||||
return int(frame_id)
|
||||
|
||||
assert not isinstance(_OnlyGetPoseKey(), ISam2GraphHandle)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Bonus: factory wires constructor dependencies through to the strategy
|
||||
|
||||
@@ -411,19 +440,23 @@ def test_factory_passes_dependencies_to_strategy() -> None:
|
||||
|
||||
|
||||
def test_factory_lazy_imports_when_registry_empty() -> None:
|
||||
# Arrange — registry is empty (fixture cleared it); the
|
||||
# lazy-import fallback should pick up the AZ-358 concrete
|
||||
# ``opencv_gtsam_estimator`` module and resolve its ``create``
|
||||
# callable.
|
||||
cfg = _build_config()
|
||||
# Registry is cleared by the fixture; the lazy-import fallback
|
||||
# should attempt to import the concrete module. We have not
|
||||
# shipped opencv_gtsam_estimator yet (AZ-358), so the import
|
||||
# raises and gets wrapped in PoseEstimatorConfigError.
|
||||
with pytest.raises(PoseEstimatorConfigError):
|
||||
build_pose_estimator(
|
||||
cfg,
|
||||
ransac_filter=mock.MagicMock(),
|
||||
wgs_converter=mock.MagicMock(),
|
||||
se3_utils=mock.MagicMock(),
|
||||
isam2_graph_handle=_FakeISam2GraphHandle(),
|
||||
)
|
||||
|
||||
# Act — call should succeed (lazy import resolves to AZ-358).
|
||||
estimator = build_pose_estimator(
|
||||
cfg,
|
||||
ransac_filter=mock.MagicMock(),
|
||||
wgs_converter=mock.MagicMock(),
|
||||
se3_utils=mock.MagicMock(),
|
||||
isam2_graph_handle=_FakeISam2GraphHandle(),
|
||||
)
|
||||
|
||||
# Assert — the returned object satisfies the PoseEstimator Protocol.
|
||||
assert isinstance(estimator, PoseEstimator)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user