"""AC tests for AZ-277: SE3Utils helper module. Verifies the `se3_utils` contract v1.0.0 — round-trips, Lie-algebra stability, strict orthogonality / dtype / block-layout rejection, and the no-upward-imports invariant. """ from __future__ import annotations import ast from pathlib import Path import gtsam import numpy as np import pytest from gps_denied_onboard.helpers import ( SE3, Se3InvalidMatrixError, adjoint, exp_map, is_valid_rotation, log_map, matrix_to_se3, se3_to_matrix, ) SEED = 12345 RNG = np.random.default_rng(SEED) def _random_valid_T(rng: np.random.Generator) -> np.ndarray: """Produce a valid SE(3) 4x4 matrix from a random twist.""" xi = rng.standard_normal(6) * 0.5 pose = gtsam.Pose3.Expmap(xi.astype(np.float64)) return np.ascontiguousarray(pose.matrix(), dtype=np.float64) def test_ac1_matrix_se3_roundtrip_within_tolerance() -> None: # Arrange + Act + Assert for _ in range(20): T = _random_valid_T(RNG) pose = matrix_to_se3(T) recovered = se3_to_matrix(pose) assert np.allclose(recovered, T, atol=1e-9), ( f"matrix_to_se3 -> se3_to_matrix round-trip diverged for T:\n{T}" ) def test_ac2_lie_algebra_roundtrip() -> None: # Arrange + Act + Assert for _ in range(20): xi = RNG.standard_normal(6).astype(np.float64) norm = float(np.linalg.norm(xi)) if norm == 0.0: continue xi *= 1.0 / norm # norm ~= 1.0 recovered = log_map(exp_map(xi)) assert np.allclose(recovered, xi, atol=1e-9), ( f"log_map(exp_map(xi)) round-trip diverged for xi={xi}" ) def test_ac3_near_identity_exp_map_is_stable() -> None: # Arrange xi = np.full(6, 1e-12, dtype=np.float64) # Act pose = exp_map(xi) matrix = se3_to_matrix(pose) # Assert identity = np.eye(4, dtype=np.float64) assert np.allclose(matrix, identity, atol=1e-9), ( f"near-identity exp_map must return identity within atol=1e-9; got\n{matrix}" ) assert not np.any(np.isnan(matrix)), "near-identity exp_map must not produce NaNs" def test_ac4_strict_orthogonality_rejection() -> None: # Arrange — drift R^T R away from I by 1e-3 in Frobenius norm T = _random_valid_T(RNG) T[:3, :3] += np.eye(3, dtype=np.float64) * 1e-3 # Act + Assert with pytest.raises(Se3InvalidMatrixError) as excinfo: matrix_to_se3(T) msg = str(excinfo.value) assert "orthogonal" in msg.lower() or "R^T R" in msg, ( "rejection message must explain the orthogonality drift" ) def test_ac5_mirror_rotation_rejected() -> None: # Arrange — flip the sign of one column to produce det(R) = -1 T = _random_valid_T(RNG) T[:3, 0] = -T[:3, 0] # Act + Assert with pytest.raises(Se3InvalidMatrixError) as excinfo: matrix_to_se3(T) msg = str(excinfo.value) assert "determinant" in msg.lower() or "negative" in msg.lower() def test_ac6_bottom_row_guard() -> None: # Arrange T = _random_valid_T(RNG) T[3] = np.array([0.0, 0.0, 0.0, 2.0], dtype=np.float64) # Act + Assert with pytest.raises(Se3InvalidMatrixError) as excinfo: matrix_to_se3(T) assert "bottom row" in str(excinfo.value).lower() def test_ac7_float32_dtype_rejected() -> None: # Arrange T = _random_valid_T(RNG).astype(np.float32) # Act + Assert with pytest.raises(Se3InvalidMatrixError) as excinfo: matrix_to_se3(T) assert "dtype" in str(excinfo.value).lower() or "float" in str(excinfo.value).lower() def test_ac8_determinism_byte_equal_outputs() -> None: # Arrange T = _random_valid_T(RNG) # Act pose_a = matrix_to_se3(T) pose_b = matrix_to_se3(T) matrix_a = se3_to_matrix(pose_a) matrix_b = se3_to_matrix(pose_b) # Assert assert np.array_equal(matrix_a, matrix_b), ( "matrix_to_se3 -> se3_to_matrix must be byte-equal across calls" ) def test_ac9_no_upward_imports_to_components() -> None: """The helper module MUST NOT import from gps_denied_onboard.components.*.""" # Arrange module_path = ( Path(__file__).resolve().parents[2] / "src" / "gps_denied_onboard" / "helpers" / "se3_utils.py" ) tree = ast.parse(module_path.read_text()) bad_imports: list[str] = [] # Act for node in ast.walk(tree): if isinstance(node, ast.ImportFrom): module = node.module or "" if module.startswith("gps_denied_onboard.components"): bad_imports.append(module) elif isinstance(node, ast.Import): for alias in node.names: if alias.name.startswith("gps_denied_onboard.components"): bad_imports.append(alias.name) # Assert assert not bad_imports, ( f"helpers.se3_utils must not import from components.*; found {bad_imports}" ) def test_is_valid_rotation_predicate() -> None: # Arrange R_good = np.eye(3, dtype=np.float64) R_drift = np.eye(3, dtype=np.float64) + 1e-2 # off-orthogonal R_mirror = np.diag([-1.0, 1.0, 1.0]).astype(np.float64) R_f32 = np.eye(3, dtype=np.float32) # Assert assert is_valid_rotation(R_good) is True assert is_valid_rotation(R_drift) is False assert is_valid_rotation(R_mirror) is False assert is_valid_rotation(R_f32) is False assert is_valid_rotation("not an array") is False # type: ignore[arg-type] def test_adjoint_shape() -> None: # Arrange T = _random_valid_T(RNG) pose = matrix_to_se3(T) # Act ad = adjoint(pose) # Assert assert ad.shape == (6, 6) assert ad.dtype == np.float64 def test_se3_is_gtsam_pose3_alias() -> None: # Assert assert SE3 is gtsam.Pose3