use nalgebra::{Matrix3, Vector3};
use std::f64::consts::PI;
use super::series;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct So3 {
matrix: Matrix3<f64>,
}
impl So3 {
pub fn identity() -> Self {
Self {
matrix: Matrix3::identity(),
}
}
pub fn from_matrix_unchecked(matrix: Matrix3<f64>) -> Self {
Self { matrix }
}
pub fn matrix(&self) -> &Matrix3<f64> {
&self.matrix
}
pub fn exp(phi: &Vector3<f64>) -> Self {
let theta = phi.norm();
let hat_phi = hat(phi);
let matrix = Matrix3::identity()
+ hat_phi * series::sin_over_theta(theta)
+ hat_phi * hat_phi * series::one_minus_cos_over_theta_sq(theta);
Self { matrix }
}
pub fn log(&self) -> Vector3<f64> {
let m = &self.matrix;
let skew = Vector3::new(
m[(2, 1)] - m[(1, 2)],
m[(0, 2)] - m[(2, 0)],
m[(1, 0)] - m[(0, 1)],
) * 0.5;
let sin_theta = skew.norm();
let cos_theta = 0.5 * (m.trace() - 1.0);
let theta = sin_theta.atan2(cos_theta);
if theta < series::THETA_SMALL {
skew * series::theta_over_sin_theta(theta)
} else if theta < PI - series::THETA_NEAR_PI {
skew * (theta / sin_theta)
} else {
self.log_near_pi(theta, cos_theta, &skew)
}
}
fn log_near_pi(&self, theta: f64, cos_theta: f64, skew: &Vector3<f64>) -> Vector3<f64> {
let m = &self.matrix;
let outer = (m + m.transpose() - Matrix3::identity() * (2.0 * cos_theta))
/ (2.0 * (1.0 - cos_theta));
let mut best = 0;
for i in 1..3 {
if outer[(i, i)] > outer[(best, best)] {
best = i;
}
}
let mut axis = outer.column(best) / outer[(best, best)].max(0.0).sqrt();
if axis.dot(skew) < 0.0 {
axis = -axis;
}
axis * theta
}
pub fn inverse(&self) -> Self {
Self {
matrix: self.matrix.transpose(),
}
}
pub fn adjoint(&self) -> Matrix3<f64> {
self.matrix
}
}
impl std::ops::Mul for So3 {
type Output = So3;
fn mul(self, rhs: So3) -> So3 {
So3 {
matrix: self.matrix * rhs.matrix,
}
}
}
impl std::ops::Mul<Vector3<f64>> for So3 {
type Output = Vector3<f64>;
fn mul(self, rhs: Vector3<f64>) -> Vector3<f64> {
self.matrix * rhs
}
}
impl Default for So3 {
fn default() -> Self {
Self::identity()
}
}
pub fn hat(v: &Vector3<f64>) -> Matrix3<f64> {
Matrix3::new(0.0, -v.z, v.y, v.z, 0.0, -v.x, -v.y, v.x, 0.0)
}
pub fn vee(m: &Matrix3<f64>) -> Vector3<f64> {
Vector3::new(m[(2, 1)], m[(0, 2)], m[(1, 0)])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hat_is_cross_product() {
let a = Vector3::new(1.0, 2.0, 3.0);
let b = Vector3::new(-4.0, 5.0, 6.0);
assert!((hat(&a) * b - a.cross(&b)).norm() < 1e-15);
}
#[test]
fn vee_inverts_hat() {
let v = Vector3::new(0.3, -1.7, 2.2);
assert!((vee(&hat(&v)) - v).norm() < 1e-15);
}
#[test]
fn log_near_pi_is_exact() {
for offset in [0.0, 1e-12, 1e-8, 1e-4, 5e-3] {
let theta = PI - offset;
let axis = Vector3::new(1.0, -2.0, 0.5).normalize();
let phi = axis * theta;
let recovered = So3::exp(&phi).log();
let err = (recovered - phi).norm().min((recovered + phi).norm());
assert!(err < 1e-12, "θ = π − {offset:e}: error {err:.3e}");
}
}
#[test]
fn log_at_exactly_pi() {
let axis = Vector3::new(0.0, 0.0, 1.0);
let recovered = So3::exp(&(axis * PI)).log();
assert!((recovered.norm() - PI).abs() < 1e-12);
assert!(recovered.normalize().cross(&axis).norm() < 1e-12);
}
}