mod common;
use cartan_core::{CartanError, Curvature, Manifold, ParallelTransport, Real, Retraction};
use cartan_manifolds::SpecialOrthogonal;
use nalgebra::SMatrix;
use rand::SeedableRng;
use rand::rngs::StdRng;
#[test]
fn so3_base_identities() {
let manifold = SpecialOrthogonal::<3>;
common::matrix_harness::test_matrix_manifold_base::<3, _>(&manifold, 1e-8, 100);
}
#[test]
fn so3_transport() {
let manifold = SpecialOrthogonal::<3>;
common::matrix_harness::test_matrix_transport::<3, _>(&manifold, 1e-8, 100);
}
#[test]
fn so3_parallel_transport() {
let manifold = SpecialOrthogonal::<3>;
common::matrix_harness::test_matrix_parallel_transport::<3, _>(&manifold, 1e-8, 100);
}
#[test]
fn so3_retraction() {
let manifold = SpecialOrthogonal::<3>;
common::matrix_harness::test_matrix_retraction::<3, _>(&manifold, 1e-8, 100);
}
#[test]
fn so3_curvature() {
let manifold = SpecialOrthogonal::<3>;
common::matrix_harness::test_matrix_curvature::<3, _>(&manifold, 1e-8, 100);
}
#[test]
fn so3_geodesic() {
let manifold = SpecialOrthogonal::<3>;
common::matrix_harness::test_matrix_geodesic::<3, _>(&manifold, 1e-8, 100);
}
#[test]
fn so2_base_identities() {
let manifold = SpecialOrthogonal::<2>;
common::matrix_harness::test_matrix_manifold_base::<2, _>(&manifold, 1e-8, 100);
}
#[test]
fn so4_base_identities() {
let manifold = SpecialOrthogonal::<4>;
common::matrix_harness::test_matrix_manifold_base::<4, _>(&manifold, 1e-7, 100);
}
#[test]
fn so3_sectional_curvature_constant() {
let manifold = SpecialOrthogonal::<3>;
let mut rng = StdRng::seed_from_u64(50);
for _ in 0..100 {
let p = manifold.random_point(&mut rng);
let u = manifold.random_tangent(&p, &mut rng);
let v = manifold.random_tangent(&p, &mut rng);
let k = manifold.sectional_curvature(&p, &u, &v);
let uu = manifold.inner(&p, &u, &u);
let vv = manifold.inner(&p, &v, &v);
let uv = manifold.inner(&p, &u, &v);
let denom = uu * vv - uv * uv;
if denom > 1e-10 {
common::approx::assert_real_eq(k, 0.25, 1e-9, "SO(3) sectional curvature = 1/4");
}
}
}
#[test]
fn so3_scalar_curvature() {
let manifold = SpecialOrthogonal::<3>;
let id = SMatrix::<Real, 3, 3>::identity();
let expected = 3.0 * 2.0 * 1.0 / 8.0;
common::approx::assert_real_eq(
manifold.scalar_curvature(&id),
expected,
1e-12,
"SO(3) scalar curvature = N(N-1)(N-2)/8 = 3/4",
);
}
#[test]
fn so4_scalar_curvature() {
let manifold = SpecialOrthogonal::<4>;
let id = SMatrix::<Real, 4, 4>::identity();
let expected = 4.0 * 3.0 * 2.0 / 8.0;
common::approx::assert_real_eq(
manifold.scalar_curvature(&id),
expected,
1e-12,
"SO(4) scalar curvature = N(N-1)(N-2)/8 = 3.0",
);
}
#[test]
fn so3_known_rotation_exp_log() {
let manifold = SpecialOrthogonal::<3>;
let id = SMatrix::<Real, 3, 3>::identity();
let half_pi = std::f64::consts::FRAC_PI_2;
let mut omega = SMatrix::<Real, 3, 3>::zeros();
omega[(0, 1)] = -half_pi; omega[(1, 0)] = half_pi;
let mut expected_r = SMatrix::<Real, 3, 3>::zeros();
expected_r[(0, 0)] = 0.0;
expected_r[(0, 1)] = -1.0;
expected_r[(0, 2)] = 0.0;
expected_r[(1, 0)] = 1.0;
expected_r[(1, 1)] = 0.0;
expected_r[(1, 2)] = 0.0;
expected_r[(2, 0)] = 0.0;
expected_r[(2, 1)] = 0.0;
expected_r[(2, 2)] = 1.0;
let r_computed = manifold.exp(&id, &omega);
let exp_err = (r_computed - expected_r).norm();
assert!(
exp_err < 1e-12,
"exp(I, Ω_z(π/2)) should be R_90: ||error||_F = {:.2e}",
exp_err
);
let v_recovered = manifold
.log(&id, &expected_r)
.expect("log should succeed for 90° rotation");
let log_err = (v_recovered - omega).norm();
assert!(
log_err < 1e-12,
"log(I, R_90) should recover Ω: ||error||_F = {:.2e}",
log_err
);
}
#[test]
fn so3_log_identity() {
let manifold = SpecialOrthogonal::<3>;
let mut rng = StdRng::seed_from_u64(51);
let id = SMatrix::<Real, 3, 3>::identity();
let v = manifold.log(&id, &id).expect("log(I, I) should succeed");
let v_norm = v.norm();
assert!(
v_norm < 1e-12,
"log(I, I) should be zero, got ||V||_F = {:.2e}",
v_norm
);
for i in 0..50 {
let r = manifold.random_point(&mut rng);
let v = manifold.log(&r, &r).expect("log(R, R) should succeed");
let v_norm = v.norm();
assert!(
v_norm < 1e-12,
"sample {}: log(R, R) should be zero, got ||V||_F = {:.2e}",
i,
v_norm
);
}
}
#[test]
fn so3_parallel_transport_is_left_translation() {
let manifold = SpecialOrthogonal::<3>;
let mut rng = StdRng::seed_from_u64(52);
let inj_radius = manifold.injectivity_radius(&manifold.random_point(&mut rng));
for i in 0..100 {
let r = manifold.random_point(&mut rng);
let v = manifold.random_tangent(&r, &mut rng);
let d = manifold.random_tangent(&r, &mut rng);
let d_norm = manifold.norm(&r, &d);
let scale = if d_norm > 1e-10 {
(inj_radius * 0.3) / d_norm
} else {
1.0
};
let d_small = d * scale;
let q = manifold.exp(&r, &d_small);
let transported = manifold
.transport(&r, &q, &v)
.expect("transport should succeed for R, Q near each other");
let direct = q * (r.transpose() * v);
let err = (transported - direct).norm();
assert!(
err < 1e-12,
"sample {}: transport formula mismatch: ||transport(R,Q,V) - Q R^T V||_F = {:.2e}",
i,
err
);
}
}
#[test]
fn so3_cayley_retraction_lands_on_so() {
let manifold = SpecialOrthogonal::<3>;
let mut rng = StdRng::seed_from_u64(53);
let inj_radius = manifold.injectivity_radius(&manifold.random_point(&mut rng));
for i in 0..100 {
let r = manifold.random_point(&mut rng);
let v = manifold.random_tangent(&r, &mut rng);
let v_norm = manifold.norm(&r, &v);
let scale = if v_norm > 1e-10 {
(inj_radius * 0.8).min(v_norm) / v_norm
} else {
1.0
};
let v_scaled = v * scale;
let q = Retraction::retract(&manifold, &r, &v_scaled);
manifold.check_point(&q).unwrap_or_else(|e| {
panic!(
"sample {}: Cayley retract(R, V) failed check_point: {}",
i, e
)
});
}
}
#[test]
fn so3_injectivity_radius() {
let manifold = SpecialOrthogonal::<3>;
let id = SMatrix::<Real, 3, 3>::identity();
common::approx::assert_real_eq(
manifold.injectivity_radius(&id),
std::f64::consts::PI,
1e-14,
"SO(3) injectivity radius = π",
);
}
#[test]
fn so3_cut_locus() {
let manifold = SpecialOrthogonal::<3>;
let id = SMatrix::<Real, 3, 3>::identity();
let mut q_cut = SMatrix::<Real, 3, 3>::zeros();
q_cut[(0, 0)] = -1.0;
q_cut[(1, 1)] = -1.0;
q_cut[(2, 2)] = 1.0;
manifold
.check_point(&q_cut)
.expect("diag(-1,-1,1) should be in SO(3)");
let result = manifold.log(&id, &q_cut);
assert!(
result.is_err(),
"log(I, diag(-1,-1,1)) should fail at cut locus, but returned Ok"
);
match result.unwrap_err() {
CartanError::CutLocus { .. } => {
}
other => panic!(
"log(I, diag(-1,-1,1)) should return CutLocus, got: {:?}",
other
),
}
}