1use nalgebra::{Matrix3, Vector3};
4use std::f64::consts::PI;
5
6use super::series;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct So3 {
15 matrix: Matrix3<f64>,
16}
17
18impl So3 {
19 pub fn identity() -> Self {
21 Self {
22 matrix: Matrix3::identity(),
23 }
24 }
25
26 pub fn from_matrix_unchecked(matrix: Matrix3<f64>) -> Self {
30 Self { matrix }
31 }
32
33 pub fn matrix(&self) -> &Matrix3<f64> {
35 &self.matrix
36 }
37
38 pub fn exp(phi: &Vector3<f64>) -> Self {
45 let theta = phi.norm();
46 let hat_phi = hat(phi);
47 let matrix = Matrix3::identity()
48 + hat_phi * series::sin_over_theta(theta)
49 + hat_phi * hat_phi * series::one_minus_cos_over_theta_sq(theta);
50 Self { matrix }
51 }
52
53 pub fn log(&self) -> Vector3<f64> {
64 let m = &self.matrix;
65 let skew = Vector3::new(
67 m[(2, 1)] - m[(1, 2)],
68 m[(0, 2)] - m[(2, 0)],
69 m[(1, 0)] - m[(0, 1)],
70 ) * 0.5;
71
72 let sin_theta = skew.norm();
73 let cos_theta = 0.5 * (m.trace() - 1.0);
74 let theta = sin_theta.atan2(cos_theta);
76
77 if theta < series::THETA_SMALL {
78 skew * series::theta_over_sin_theta(theta)
79 } else if theta < PI - series::THETA_NEAR_PI {
80 skew * (theta / sin_theta)
81 } else {
82 self.log_near_pi(theta, cos_theta, &skew)
83 }
84 }
85
86 fn log_near_pi(&self, theta: f64, cos_theta: f64, skew: &Vector3<f64>) -> Vector3<f64> {
89 let m = &self.matrix;
90 let outer = (m + m.transpose() - Matrix3::identity() * (2.0 * cos_theta))
91 / (2.0 * (1.0 - cos_theta));
92
93 let mut best = 0;
96 for i in 1..3 {
97 if outer[(i, i)] > outer[(best, best)] {
98 best = i;
99 }
100 }
101 let mut axis = outer.column(best) / outer[(best, best)].max(0.0).sqrt();
102
103 if axis.dot(skew) < 0.0 {
106 axis = -axis;
107 }
108 axis * theta
109 }
110
111 pub fn inverse(&self) -> Self {
113 Self {
114 matrix: self.matrix.transpose(),
115 }
116 }
117
118 pub fn adjoint(&self) -> Matrix3<f64> {
120 self.matrix
121 }
122}
123
124impl std::ops::Mul for So3 {
125 type Output = So3;
126 fn mul(self, rhs: So3) -> So3 {
127 So3 {
128 matrix: self.matrix * rhs.matrix,
129 }
130 }
131}
132
133impl std::ops::Mul<Vector3<f64>> for So3 {
134 type Output = Vector3<f64>;
135 fn mul(self, rhs: Vector3<f64>) -> Vector3<f64> {
136 self.matrix * rhs
137 }
138}
139
140impl Default for So3 {
141 fn default() -> Self {
142 Self::identity()
143 }
144}
145
146pub fn hat(v: &Vector3<f64>) -> Matrix3<f64> {
148 Matrix3::new(0.0, -v.z, v.y, v.z, 0.0, -v.x, -v.y, v.x, 0.0)
149}
150
151pub fn vee(m: &Matrix3<f64>) -> Vector3<f64> {
153 Vector3::new(m[(2, 1)], m[(0, 2)], m[(1, 0)])
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn hat_is_cross_product() {
162 let a = Vector3::new(1.0, 2.0, 3.0);
163 let b = Vector3::new(-4.0, 5.0, 6.0);
164 assert!((hat(&a) * b - a.cross(&b)).norm() < 1e-15);
165 }
166
167 #[test]
168 fn vee_inverts_hat() {
169 let v = Vector3::new(0.3, -1.7, 2.2);
170 assert!((vee(&hat(&v)) - v).norm() < 1e-15);
171 }
172
173 #[test]
177 fn log_near_pi_is_exact() {
178 for offset in [0.0, 1e-12, 1e-8, 1e-4, 5e-3] {
179 let theta = PI - offset;
180 let axis = Vector3::new(1.0, -2.0, 0.5).normalize();
181 let phi = axis * theta;
182 let recovered = So3::exp(&phi).log();
183 let err = (recovered - phi).norm().min((recovered + phi).norm());
185 assert!(err < 1e-12, "θ = π − {offset:e}: error {err:.3e}");
186 }
187 }
188
189 #[test]
191 fn log_at_exactly_pi() {
192 let axis = Vector3::new(0.0, 0.0, 1.0);
193 let recovered = So3::exp(&(axis * PI)).log();
194 assert!((recovered.norm() - PI).abs() < 1e-12);
195 assert!(recovered.normalize().cross(&axis).norm() < 1e-12);
196 }
197}