mod common;
use cartan_core::{GeodesicInterpolation, Manifold, ParallelTransport, Real, Retraction};
use cartan_manifolds::{
SpecialEuclidean,
se::{SEPoint, SETangent},
};
use nalgebra::{SMatrix, SVector};
use rand::SeedableRng;
use rand::rngs::StdRng;
fn se_point_diff<const N: usize>(a: &SEPoint<N>, b: &SEPoint<N>) -> f64 {
let rot_diff = (a.rotation - b.rotation).norm();
let trans_diff = (a.translation - b.translation).norm();
rot_diff + trans_diff
}
fn se_tangent_diff<const N: usize>(a: &SETangent<N>, b: &SETangent<N>) -> f64 {
let rot_diff = (a.rotation - b.rotation).norm();
let trans_diff = (a.translation - b.translation).norm();
rot_diff + trans_diff
}
fn scale_tangent<const N: usize>(v: &SETangent<N>, s: f64) -> SETangent<N> {
SETangent {
rotation: v.rotation * s,
translation: v.translation * s,
}
}
#[test]
fn se3_exp_log_roundtrip_forward() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(100);
let tol = 1e-8;
let target_norm = 0.5;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let v = se3.random_tangent(&p, &mut rng);
let v_norm = se3.norm(&p, &v);
let scale = if v_norm > 1e-10 {
target_norm / v_norm
} else {
1.0
};
let v_small = scale_tangent(&v, scale);
let q = se3.exp(&p, &v_small);
let v_recovered = se3
.log(&p, &q)
.unwrap_or_else(|e| panic!("sample {}: log(p, exp(p, v)) failed: {}", i, e));
let diff = se_tangent_diff(&v_recovered, &v_small);
assert!(
diff < tol,
"sample {}: log(p, exp(p, v)) ≠ v: tangent diff = {:.2e} > tol = {:.2e}",
i,
diff,
tol
);
}
}
#[test]
fn se3_exp_log_roundtrip_reverse() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(101);
let tol = 1e-8;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let v = se3.random_tangent(&p, &mut rng);
let v_norm = se3.norm(&p, &v);
let scale = if v_norm > 1e-10 { 0.4 / v_norm } else { 1.0 };
let v_small = scale_tangent(&v, scale);
let q = se3.exp(&p, &v_small);
let v_pq = se3
.log(&p, &q)
.unwrap_or_else(|e| panic!("sample {}: log(p, q) failed: {}", i, e));
let q_recovered = se3.exp(&p, &v_pq);
let diff = se_point_diff(&q_recovered, &q);
assert!(
diff < tol,
"sample {}: exp(p, log(p, q)) ≠ q: point diff = {:.2e} > tol = {:.2e}",
i,
diff,
tol
);
}
}
#[test]
fn se3_check_point_on_random_points() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(102);
for i in 0..10 {
let p = se3.random_point(&mut rng);
se3.check_point(&p)
.unwrap_or_else(|e| panic!("sample {}: random_point failed check_point: {}", i, e));
}
}
#[test]
fn se3_check_tangent_on_random_tangents() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(103);
for i in 0..10 {
let p = se3.random_point(&mut rng);
let v = se3.random_tangent(&p, &mut rng);
se3.check_tangent(&p, &v)
.unwrap_or_else(|e| panic!("sample {}: random_tangent failed check_tangent: {}", i, e));
}
}
#[test]
fn se3_inner_product_symmetry() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(104);
let tol = 1e-14;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let u = se3.random_tangent(&p, &mut rng);
let v = se3.random_tangent(&p, &mut rng);
let inner_uv = se3.inner(&p, &u, &v);
let inner_vu = se3.inner(&p, &v, &u);
let diff = (inner_uv - inner_vu).abs();
assert!(
diff < tol,
"sample {}: |<u,v> - <v,u>| = {:.2e} > tol = {:.2e}",
i,
diff,
tol
);
}
}
#[test]
fn se3_inner_product_linearity() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(105);
let tol = 1e-10;
let a: Real = 2.3;
let b: Real = -0.7;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let u = se3.random_tangent(&p, &mut rng);
let v = se3.random_tangent(&p, &mut rng);
let w = se3.random_tangent(&p, &mut rng);
let lin_combo = scale_tangent(&u, a) + scale_tangent(&w, b);
let lhs = se3.inner(&p, &lin_combo, &v);
let rhs = a * se3.inner(&p, &u, &v) + b * se3.inner(&p, &w, &v);
let diff = (lhs - rhs).abs();
assert!(
diff < tol,
"sample {}: |<au+bw, v> - a<u,v> - b<w,v>| = {:.2e} > tol = {:.2e}",
i,
diff,
tol
);
}
}
#[test]
fn se3_distance_symmetry_and_self() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(106);
let tol_sym = 1e-12;
let tol_self = 1e-14;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let v = se3.random_tangent(&p, &mut rng);
let v_norm = se3.norm(&p, &v);
let scale = if v_norm > 1e-10 { 0.4 / v_norm } else { 1.0 };
let q = se3.exp(&p, &scale_tangent(&v, scale));
if let (Ok(d_pq), Ok(d_qp)) = (se3.dist(&p, &q), se3.dist(&q, &p)) {
let sym_err = (d_pq - d_qp).abs();
assert!(
sym_err < tol_sym,
"sample {}: |d(p,q) - d(q,p)| = {:.2e} > tol = {:.2e}",
i,
sym_err,
tol_sym
);
}
if let Ok(d_pp) = se3.dist(&p, &p) {
assert!(
d_pp < tol_self,
"sample {}: d(p,p) = {:.2e} > tol = {:.2e} (should be 0)",
i,
d_pp,
tol_self
);
}
}
}
#[test]
fn se3_project_tangent_idempotent() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(107);
let tol = 1e-8;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let v = se3.random_tangent(&p, &mut rng);
let proj_v = se3.project_tangent(&p, &v);
let proj_proj_v = se3.project_tangent(&p, &proj_v);
let diff = se_tangent_diff(&proj_proj_v, &proj_v);
assert!(
diff < tol,
"sample {}: project_tangent not idempotent: diff = {:.2e} > tol = {:.2e}",
i,
diff,
tol
);
}
}
#[test]
fn se3_project_point_idempotent() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(108);
let tol = 1e-8;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let proj_p = se3.project_point(&p);
let proj_proj_p = se3.project_point(&proj_p);
let diff = se_point_diff(&proj_proj_p, &proj_p);
assert!(
diff < tol,
"sample {}: project_point not idempotent: diff = {:.2e} > tol = {:.2e}",
i,
diff,
tol
);
se3.check_point(&proj_p).unwrap_or_else(|e| {
panic!(
"sample {}: project_point result failed check_point: {}",
i, e
)
});
}
}
#[test]
fn se3_zero_tangent_has_zero_norm() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(109);
let tol = 1e-14;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let zero = se3.zero_tangent(&p);
let norm = se3.norm(&p, &zero);
assert!(
norm < tol,
"sample {}: ||zero_tangent|| = {:.2e} > tol = {:.2e} (should be 0)",
i,
norm,
tol
);
}
}
#[test]
fn se3_retraction_lands_on_manifold() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(200);
for i in 0..10 {
let p = se3.random_point(&mut rng);
let v = se3.random_tangent(&p, &mut rng);
let v_norm = se3.norm(&p, &v);
let scale = if v_norm > 1e-10 { 0.4 / v_norm } else { 1.0 };
let v_small = scale_tangent(&v, scale);
let q = Retraction::retract(&se3, &p, &v_small);
se3.check_point(&q)
.unwrap_or_else(|e| panic!("sample {}: retract(p, v) failed check_point: {}", i, e));
}
}
#[test]
fn se3_transport_preserves_norm() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(201);
let tol = 1e-6;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let u = se3.random_tangent(&p, &mut rng);
let d = se3.random_tangent(&p, &mut rng);
let d_norm = se3.norm(&p, &d);
let scale = if d_norm > 1e-10 { 0.3 / d_norm } else { 1.0 };
let d_small = scale_tangent(&d, scale);
let q = se3.exp(&p, &d_small);
let u_transported = se3
.transport(&p, &q, &u)
.unwrap_or_else(|e| panic!("sample {}: transport failed: {}", i, e));
let norm_before = se3.norm(&p, &u);
let norm_after = se3.norm(&q, &u_transported);
let norm_diff = (norm_before - norm_after).abs();
assert!(
norm_diff < tol,
"sample {}: |||P u||_q - ||u||_p| = {:.2e} > tol = {:.2e}",
i,
norm_diff,
tol
);
}
}
#[test]
fn se3_geodesic_boundary_conditions() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(202);
let tol = 1e-8;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let v = se3.random_tangent(&p, &mut rng);
let v_norm = se3.norm(&p, &v);
let scale = if v_norm > 1e-10 { 0.4 / v_norm } else { 1.0 };
let q = se3.exp(&p, &scale_tangent(&v, scale));
let g0 = se3
.geodesic(&p, &q, 0.0)
.unwrap_or_else(|e| panic!("sample {}: geodesic(p,q,0) failed: {}", i, e));
let diff0 = se_point_diff(&g0, &p);
assert!(
diff0 < tol,
"sample {}: geodesic(p,q,0) ≠ p: diff = {:.2e} > tol = {:.2e}",
i,
diff0,
tol
);
let g1 = se3
.geodesic(&p, &q, 1.0)
.unwrap_or_else(|e| panic!("sample {}: geodesic(p,q,1) failed: {}", i, e));
let diff1 = se_point_diff(&g1, &q);
assert!(
diff1 < tol,
"sample {}: geodesic(p,q,1) ≠ q: diff = {:.2e} > tol = {:.2e}",
i,
diff1,
tol
);
}
}
#[test]
fn se3_geodesic_constant_speed() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(203);
let tol = 1e-7;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let v = se3.random_tangent(&p, &mut rng);
let v_norm = se3.norm(&p, &v);
let scale = if v_norm > 1e-10 { 0.3 / v_norm } else { 1.0 };
let q = se3.exp(&p, &scale_tangent(&v, scale));
let d_pq = se3
.dist(&p, &q)
.unwrap_or_else(|e| panic!("sample {}: dist(p,q) failed: {}", i, e));
let g_half = se3
.geodesic(&p, &q, 0.5)
.unwrap_or_else(|e| panic!("sample {}: geodesic(p,q,0.5) failed: {}", i, e));
let d_half = se3
.dist(&p, &g_half)
.unwrap_or_else(|e| panic!("sample {}: dist(p, g(0.5)) failed: {}", i, e));
let speed_err = (d_half - 0.5 * d_pq).abs();
assert!(
speed_err < tol,
"sample {}: |d(p, g(0.5)) - d(p,q)/2| = {:.2e} > tol = {:.2e}",
i,
speed_err,
tol
);
}
}
#[test]
fn se3_pure_rotation_preserves_zero_translation() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let p_identity = SEPoint::<3> {
rotation: SMatrix::<Real, 3, 3>::identity(),
translation: SVector::<Real, 3>::zeros(),
};
let angle = std::f64::consts::FRAC_PI_4; let mut omega = SMatrix::<Real, 3, 3>::zeros();
omega[(0, 1)] = -angle; omega[(1, 0)] = angle;
let v_pure_rot = SETangent::<3> {
rotation: omega, translation: SVector::<Real, 3>::zeros(), };
let q = se3.exp(&p_identity, &v_pure_rot);
let trans_norm = q.translation.norm();
assert!(
trans_norm < 1e-12,
"pure rotation at identity moved translation: ||t_new|| = {:.2e} (expected 0)",
trans_norm
);
se3.check_point(&q)
.expect("pure rotation result should be a valid SE(3) point");
}
#[test]
fn se3_pure_translation_gives_correct_result() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let p_identity = SEPoint::<3> {
rotation: SMatrix::<Real, 3, 3>::identity(),
translation: SVector::<Real, 3>::zeros(),
};
let t_desired = SVector::<Real, 3>::new(1.0, 2.0, 3.0);
let v_pure_trans = SETangent::<3> {
rotation: SMatrix::<Real, 3, 3>::zeros(), translation: t_desired, };
let q = se3.exp(&p_identity, &v_pure_trans);
let id = SMatrix::<Real, 3, 3>::identity();
let rot_err = (q.rotation - id).norm();
assert!(
rot_err < 1e-14,
"pure translation changed rotation: ||R_new - I||_F = {:.2e} (expected 0)",
rot_err
);
let trans_err = (q.translation - t_desired).norm();
assert!(
trans_err < 1e-14,
"pure translation: ||t_new - [1,2,3]|| = {:.2e} (expected 0)",
trans_err
);
}
#[test]
fn se3_exp_zero_tangent_is_identity() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(300);
let tol = 1e-14;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let zero = se3.zero_tangent(&p);
let q = se3.exp(&p, &zero);
let diff = se_point_diff(&q, &p);
assert!(
diff < tol,
"sample {}: exp(p, 0) ≠ p: diff = {:.2e} > tol = {:.2e}",
i,
diff,
tol
);
}
}
#[test]
fn se3_log_self_is_zero() {
let se3 = SpecialEuclidean::<3> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(301);
let tol = 1e-14;
for i in 0..10 {
let p = se3.random_point(&mut rng);
let v = se3
.log(&p, &p)
.unwrap_or_else(|e| panic!("sample {}: log(p, p) failed: {}", i, e));
let v_norm = se3.norm(&p, &v);
assert!(
v_norm < tol,
"sample {}: ||log(p, p)||_p = {:.2e} > tol = {:.2e} (expected 0)",
i,
v_norm,
tol
);
}
}
#[test]
fn se3_weight_affects_translation_distance() {
let se3_low = SpecialEuclidean::<3> { weight: 0.1 }; let se3_high = SpecialEuclidean::<3> { weight: 10.0 };
let id = SMatrix::<Real, 3, 3>::identity();
let t1 = SVector::<Real, 3>::zeros(); let t2 = SVector::<Real, 3>::new(1.0, 0.0, 0.0);
let p = SEPoint::<3> {
rotation: id,
translation: t1,
};
let q = SEPoint::<3> {
rotation: id,
translation: t2,
};
let d_low = se3_low
.dist(&p, &q)
.expect("dist with low weight should succeed");
let d_high = se3_high
.dist(&p, &q)
.expect("dist with high weight should succeed");
assert!(
d_high > d_low,
"weight=10 distance ({:.4}) should be greater than weight=0.1 distance ({:.4})",
d_high,
d_low
);
let ratio = d_high / d_low;
assert!(
ratio > 5.0,
"weight ratio should produce d_high/d_low ≈ 10, got {:.4}",
ratio
);
let expected_low = (0.1_f64).sqrt();
let expected_high = (10.0_f64).sqrt();
assert!(
(d_low - expected_low).abs() < 1e-8,
"d_low = {:.6}, expected {:.6}",
d_low,
expected_low
);
assert!(
(d_high - expected_high).abs() < 1e-8,
"d_high = {:.6}, expected {:.6}",
d_high,
expected_high
);
}
#[test]
fn se2_exp_log_roundtrip() {
let se2 = SpecialEuclidean::<2> { weight: 1.0 };
let mut rng = StdRng::seed_from_u64(400);
let tol = 1e-8;
let target_norm = 0.5;
for i in 0..10 {
let p = se2.random_point(&mut rng);
let v = se2.random_tangent(&p, &mut rng);
let v_norm = se2.norm(&p, &v);
let scale = if v_norm > 1e-10 {
target_norm / v_norm
} else {
1.0
};
let v_small = scale_tangent(&v, scale);
let q = se2.exp(&p, &v_small);
let v_recovered = se2
.log(&p, &q)
.unwrap_or_else(|e| panic!("SE(2) sample {}: log(p, exp(p, v)) failed: {}", i, e));
let diff = se_tangent_diff(&v_recovered, &v_small);
assert!(
diff < tol,
"SE(2) sample {}: log(p, exp(p, v)) ≠ v: diff = {:.2e} > tol = {:.2e}",
i,
diff,
tol
);
}
}