use std::f64::consts::PI;
use nalgebra::{Matrix3, Vector3, Vector6};
use proptest::prelude::*;
use rigidity_core::lie::{
Se3, So3, hat, hat_se3, inverse_left_jacobian_so3, left_jacobian_so3, vee, vee_se3,
};
fn so3_vector() -> impl Strategy<Value = Vector3<f64>> {
(
-1.0f64..1.0,
-1.0f64..1.0,
-1.0f64..1.0,
0.0f64..(PI - 1e-9),
)
.prop_filter_map("degenerate axis", |(x, y, z, theta)| {
let axis = Vector3::new(x, y, z);
(axis.norm() > 1e-3).then(|| axis.normalize() * theta)
})
}
fn se3_vector() -> impl Strategy<Value = Vector6<f64>> {
(so3_vector(), -10.0f64..10.0, -10.0f64..10.0, -10.0f64..10.0)
.prop_map(|(phi, rx, ry, rz)| Vector6::new(rx, ry, rz, phi[0], phi[1], phi[2]))
}
fn numeric_left_jacobian(phi: Vector3<f64>) -> Matrix3<f64> {
const H: f64 = 1e-5;
let base_inverse = So3::exp(&phi).inverse();
let mut jacobian = Matrix3::zeros();
for i in 0..3 {
let mut delta = Vector3::zeros();
delta[i] = H;
let forward = (So3::exp(&(phi + delta)) * base_inverse).log();
let backward = (So3::exp(&(phi - delta)) * base_inverse).log();
jacobian.set_column(i, &((forward - backward) / (2.0 * H)));
}
jacobian
}
proptest! {
#![proptest_config(ProptestConfig { cases: 4096, ..ProptestConfig::default() })]
#[test]
fn so3_log_exp_roundtrip(phi in so3_vector()) {
let recovered = So3::exp(&phi).log();
prop_assert!((recovered - phi).norm() < 1e-12, "error {:.3e}", (recovered - phi).norm());
}
#[test]
fn so3_exp_log_roundtrip(phi in so3_vector()) {
let rotation = So3::exp(&phi);
let recovered = So3::exp(&rotation.log());
let err = (recovered.matrix() - rotation.matrix()).norm();
prop_assert!(err < 1e-12, "error {err:.3e}");
}
#[test]
fn so3_exp_is_a_rotation(phi in so3_vector()) {
let m = *So3::exp(&phi).matrix();
let orthogonality = (m.transpose() * m - Matrix3::identity()).norm();
prop_assert!(orthogonality < 1e-14, "RᵀR − I = {orthogonality:.3e}");
prop_assert!((m.determinant() - 1.0).abs() < 1e-14);
}
#[test]
fn so3_exp_matches_nalgebra(phi in so3_vector()) {
let ours = *So3::exp(&phi).matrix();
let theirs = *nalgebra::Rotation3::from_scaled_axis(phi).matrix();
prop_assert!((ours - theirs).norm() < 1e-14);
}
#[test]
fn so3_adjoint_identity(phi in so3_vector(), psi in so3_vector()) {
let rotation = So3::exp(&phi);
let conjugated = vee(&(rotation.matrix() * hat(&psi) * rotation.matrix().transpose()));
let via_adjoint = rotation.adjoint() * psi;
prop_assert!((conjugated - via_adjoint).norm() < 1e-12);
}
#[test]
fn left_jacobian_inverse_is_inverse(phi in so3_vector()) {
let product = inverse_left_jacobian_so3(&phi) * left_jacobian_so3(&phi);
let err = (product - Matrix3::identity()).norm();
prop_assert!(err < 1e-12, "J⁻¹J − I = {err:.3e}");
}
#[test]
fn left_jacobian_matches_numeric(phi in so3_vector()) {
let analytic = left_jacobian_so3(&phi);
let numeric = numeric_left_jacobian(phi);
let err = (analytic - numeric).norm();
prop_assert!(err < 1e-7, "analytic against numeric: {err:.3e}");
}
#[test]
fn se3_log_exp_roundtrip(xi in se3_vector()) {
let recovered = Se3::exp(&xi).log();
prop_assert!((recovered - xi).norm() < 1e-12, "error {:.3e}", (recovered - xi).norm());
}
#[test]
fn se3_exp_log_roundtrip(xi in se3_vector()) {
let transform = Se3::exp(&xi);
let recovered = Se3::exp(&transform.log());
let err = (recovered.matrix() - transform.matrix()).norm();
prop_assert!(err < 1e-12, "error {err:.3e}");
}
#[test]
fn se3_adjoint_identity(xi in se3_vector(), eta in se3_vector()) {
let transform = Se3::exp(&xi);
let conjugated = vee_se3(&(transform.matrix() * hat_se3(&eta) * transform.inverse().matrix()));
let via_adjoint = transform.adjoint() * eta;
let err = (conjugated - via_adjoint).norm();
prop_assert!(err < 1e-12, "error {err:.3e}");
}
#[test]
fn se3_adjoint_is_a_homomorphism(xi in se3_vector(), eta in se3_vector()) {
let (a, b) = (Se3::exp(&xi), Se3::exp(&eta));
let err = ((a * b).adjoint() - a.adjoint() * b.adjoint()).norm();
prop_assert!(err < 1e-11, "error {err:.3e}");
}
#[test]
fn se3_inverse_is_inverse(xi in se3_vector()) {
let transform = Se3::exp(&xi);
let product = transform.inverse() * transform;
let err = (product.matrix() - Se3::identity().matrix()).norm();
prop_assert!(err < 1e-12, "error {err:.3e}");
}
#[test]
fn se3_transform_point_matches_matrix(xi in se3_vector(), px in -5.0f64..5.0, py in -5.0f64..5.0, pz in -5.0f64..5.0) {
let transform = Se3::exp(&xi);
let point = Vector3::new(px, py, pz);
let direct = transform.transform_point(&point);
let homogeneous = transform.matrix() * nalgebra::Vector4::new(px, py, pz, 1.0);
let err = (direct - homogeneous.xyz()).norm();
prop_assert!(err < 1e-12, "error {err:.3e}");
}
}
#[test]
fn taylor_branch_is_continuous_at_small_angles() {
let axis = Vector3::new(1.0, -2.0, 0.5).normalize();
for theta in [0.0, 1e-12, 1e-8, 1e-4, 1e-2, 1e-1] {
let phi = axis * theta;
let rotation = So3::exp(&phi);
let orthogonality =
(rotation.matrix().transpose() * rotation.matrix() - Matrix3::identity()).norm();
assert!(
orthogonality < 1e-14,
"θ = {theta:e}: RᵀR − I = {orthogonality:.3e}"
);
let err = (rotation.log() - phi).norm();
assert!(err < 1e-10, "θ = {theta:e}: log∘exp error {err:.3e}");
let deviation = (left_jacobian_so3(&phi) - Matrix3::identity()).norm();
assert!(
deviation <= theta,
"θ = {theta:e}: J_l deviation {deviation:.3e}"
);
let product = inverse_left_jacobian_so3(&phi) * left_jacobian_so3(&phi);
let inverse_err = (product - Matrix3::identity()).norm();
assert!(
inverse_err < 1e-13,
"θ = {theta:e}: J⁻¹J − I = {inverse_err:.3e}"
);
}
}
#[test]
fn exact_zero_is_identity() {
let zero3 = Vector3::zeros();
assert_eq!(*So3::exp(&zero3).matrix(), Matrix3::identity());
assert_eq!(left_jacobian_so3(&zero3), Matrix3::identity());
assert_eq!(inverse_left_jacobian_so3(&zero3), Matrix3::identity());
assert_eq!(Se3::exp(&Vector6::zeros()), Se3::identity());
assert_eq!(So3::identity().log(), zero3);
assert_eq!(Se3::identity().log(), Vector6::zeros());
}
#[test]
fn inverse_left_jacobian_is_stable_near_pi() {
let axis = Vector3::new(1.0, -2.0, 0.5).normalize();
for delta in [1e-2, 1e-4, 1e-6, 1e-8, 0.0] {
let phi = axis * (PI - delta);
let product = inverse_left_jacobian_so3(&phi) * left_jacobian_so3(&phi);
let err = (product - Matrix3::identity()).norm();
assert!(err < 1e-13, "θ = π − {delta:e}: J⁻¹J − I = {err:.3e}");
}
}
#[test]
fn se3_roundtrip_regression_near_pi() {
let xi = Vector6::new(
0.0,
7.263_142_168_465_041,
-6.727_543_297_705_218_5,
0.0,
-1.999_203_834_958_153_6,
-2.423_031_111_602_081_7,
);
let err = (Se3::exp(&xi).log() - xi).norm();
assert!(err < 1e-12, "error {err:.3e}");
}