glamx/
eigen3.rs

1//! Eigendecomposition for symmetric 3x3 matrices.
2//!
3//! The eigensolver is a Rust adaptation, with modifications, of the pseudocode and approach described in
4//! "A Robust Eigensolver for 3 x 3 Symmetric Matrices" by David Eberly, Geometric Tools, Redmond WA 98052.
5//! <https://www.geometrictools.com/Documentation/RobustEigenSymmetric3x3.pdf>
6//!
7//! Adapted from <https://github.com/Jondolf/barry/tree/main/src/math/eigen>
8
9use glam::Vec3Swizzles;
10use num_traits::float::FloatConst;
11
12#[cfg(not(feature = "std"))]
13use simba::scalar::ComplexField;
14
15/// Macro to generate SymmetricEigen3 for a specific scalar type.
16macro_rules! impl_symmetric_eigen3 {
17    ($SymmetricEigen3:ident, $Mat3:ty, $Vec3:ty, $Real:ty $(, #[$attr:meta])*) => {
18        #[doc = concat!("The eigen decomposition of a symmetric 3x3 matrix (", stringify!($Real), " precision).")]
19        #[derive(Clone, Copy, Debug, PartialEq)]
20        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21        $(#[$attr])*
22        pub struct $SymmetricEigen3 {
23            /// The eigenvalues of the symmetric 3x3 matrix.
24            pub eigenvalues: $Vec3,
25            /// The three eigenvectors of the symmetric 3x3 matrix (as columns).
26            pub eigenvectors: $Mat3,
27        }
28
29        impl $SymmetricEigen3 {
30            /// Computes the eigen decomposition of the given symmetric 3x3 matrix.
31            ///
32            /// The eigenvalues are returned in ascending order `eigen1 < eigen2 < eigen3`.
33            /// This can be reversed with the [`reverse`](Self::reverse) method.
34            pub fn new(mat: $Mat3) -> Self {
35                let eigenvalues = Self::eigenvalues(mat);
36                let eigenvector1 = Self::eigenvector1(mat, eigenvalues.x);
37                let eigenvector2 = Self::eigenvector2(mat, eigenvector1, eigenvalues.y);
38                let eigenvector3 = Self::eigenvector3(eigenvector1, eigenvector2);
39
40                Self {
41                    eigenvalues,
42                    eigenvectors: <$Mat3>::from_cols(eigenvector1, eigenvector2, eigenvector3),
43                }
44            }
45
46            /// Reverses the order of the eigenvalues and their corresponding eigenvectors.
47            pub fn reverse(&self) -> Self {
48                Self {
49                    eigenvalues: self.eigenvalues.zyx(),
50                    eigenvectors: <$Mat3>::from_cols(
51                        self.eigenvectors.z_axis,
52                        self.eigenvectors.y_axis,
53                        self.eigenvectors.x_axis,
54                    ),
55                }
56            }
57
58            /// Computes the eigenvalues of a symmetric 3x3 matrix.
59            ///
60            /// Reference: <https://en.wikipedia.org/wiki/Eigenvalue_algorithm#3%C3%973_matrices>
61            pub fn eigenvalues(mat: $Mat3) -> $Vec3 {
62                let p1 = mat.y_axis.x.powi(2) + mat.z_axis.x.powi(2) + mat.z_axis.y.powi(2);
63                if p1 == 0.0 {
64                    // The matrix is diagonal.
65                    let mut eigenvalues = [mat.x_axis.x, mat.y_axis.y, mat.z_axis.z];
66                    eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
67                    <$Vec3>::from_array(eigenvalues)
68                } else {
69                    let q = (mat.x_axis.x + mat.y_axis.y + mat.z_axis.z) / 3.0;
70                    let p2 = (mat.x_axis.x - q).powi(2)
71                        + (mat.y_axis.y - q).powi(2)
72                        + (mat.z_axis.z - q).powi(2)
73                        + 2.0 * p1;
74                    let p = (p2 / 6.0).sqrt();
75                    let mat_b = 1.0 / p * (mat - q * <$Mat3>::IDENTITY);
76                    let r = mat_b.determinant() / 2.0;
77
78                    // r should be in the [-1, 1] range for a symmetric matrix,
79                    // but computation error can leave it slightly outside this range.
80                    let pi: $Real = <$Real as FloatConst>::PI();
81                    let phi = if r <= -1.0 {
82                        pi / 3.0
83                    } else if r >= 1.0 {
84                        0.0
85                    } else {
86                        r.acos() / 3.0
87                    };
88
89                    // The eigenvalues satisfy eigen3 <= eigen2 <= eigen1
90                    let eigen1 = q + 2.0 * p * phi.cos();
91                    let eigen3 = q + 2.0 * p * (phi + 2.0 * pi / 3.0).cos();
92                    let eigen2 = 3.0 * q - eigen1 - eigen3; // trace(mat) = eigen1 + eigen2 + eigen3
93                    <$Vec3>::new(eigen3, eigen2, eigen1)
94                }
95            }
96
97            /// Computes the unit-length eigenvector corresponding to the `eigenvalue1` of `mat` that was
98            /// computed from the root of a cubic polynomial with a multiplicity of 1.
99            ///
100            /// If the other two eigenvalues are well separated, this method can be used for computing
101            /// all three eigenvectors. However, to avoid numerical issues when eigenvalues are close to
102            /// each other, it's recommended to use the `eigenvector2` method for the second eigenvector.
103            ///
104            /// The third eigenvector can be computed as the cross product of the first two.
105            pub fn eigenvector1(mat: $Mat3, eigenvalue1: $Real) -> $Vec3 {
106                let cols = mat - <$Mat3>::from_diagonal(<$Vec3>::splat(eigenvalue1).into());
107                let c0xc1 = cols.x_axis.cross(cols.y_axis);
108                let c0xc2 = cols.x_axis.cross(cols.z_axis);
109                let c1xc2 = cols.y_axis.cross(cols.z_axis);
110                let d0 = c0xc1.length_squared();
111                let d1 = c0xc2.length_squared();
112                let d2 = c1xc2.length_squared();
113
114                let mut d_max = d0;
115                let mut i_max = 0;
116
117                if d1 > d_max {
118                    d_max = d1;
119                    i_max = 1;
120                }
121                if d2 > d_max {
122                    d_max = d2;
123                    i_max = 2;
124                }
125
126                // When all eigenvalues are equal (multiplicity 3), mat - eigenvalue * I is the
127                // zero matrix, so all cross products are zero. Any unit vector is a valid
128                // eigenvector in this case.
129                if d_max < 1e-20 {
130                    return <$Vec3>::X;
131                }
132
133                if i_max == 0 {
134                    c0xc1 / d0.sqrt()
135                } else if i_max == 1 {
136                    c0xc2 / d1.sqrt()
137                } else {
138                    c1xc2 / d2.sqrt()
139                }
140            }
141
142            /// Computes the unit-length eigenvector corresponding to the `eigenvalue2` of `mat` that was
143            /// computed from the root of a cubic polynomial with a potential multiplicity of 2.
144            ///
145            /// The third eigenvector can be computed as the cross product of the first two.
146            pub fn eigenvector2(mat: $Mat3, eigenvector1: $Vec3, eigenvalue2: $Real) -> $Vec3 {
147                // Compute right-handed orthonormal set { U, V, W }, where W is eigenvector1.
148                let (u, v) = Self::any_orthonormal_pair(eigenvector1);
149
150                // The unit-length eigenvector is E = x0 * U + x1 * V. We need to compute x0 and x1.
151                //
152                // Define the symmetric 2x2 matrix M = J^T * (mat - eigenvalue2 * I), where J = [U V]
153                // and I is a 3x3 identity matrix. This means that E = J * X, where X is a column vector
154                // with rows x0 and x1. The 3x3 linear system (mat - eigenvalue2 * I) * E = 0 reduces to
155                // the 2x2 linear system M * X = 0.
156                //
157                // When eigenvalue2 != eigenvalue3, M has rank 1 and is not the zero matrix.
158                // Otherwise, it has rank 0, and it is the zero matrix.
159
160                let au = mat * u;
161                let av = mat * v;
162
163                let mut m00 = u.dot(au) - eigenvalue2;
164                let mut m01 = u.dot(av);
165                let mut m11 = v.dot(av) - eigenvalue2;
166                let (abs_m00, abs_m01, abs_m11) = (m00.abs(), m01.abs(), m11.abs());
167
168                if abs_m00 >= abs_m11 {
169                    let max_abs_component = abs_m00.max(abs_m01);
170                    if max_abs_component > 0.0 {
171                        if abs_m00 >= abs_m01 {
172                            // m00 is the largest component of the row.
173                            // Factor it out for normalization and discard to avoid underflow or overflow.
174                            m01 /= m00;
175                            m00 = 1.0 / (1.0 + m01 * m01).sqrt();
176                            m01 *= m00;
177                        } else {
178                            // m01 is the largest component of the row.
179                            // Factor it out for normalization and discard to avoid underflow or overflow.
180                            m00 /= m01;
181                            m01 = 1.0 / (1.0 + m00 * m00).sqrt();
182                            m00 *= m01;
183                        }
184                        return m01 * u - m00 * v;
185                    }
186                } else {
187                    let max_abs_component = abs_m11.max(abs_m01);
188                    if max_abs_component > 0.0 {
189                        if abs_m11 >= abs_m01 {
190                            // m11 is the largest component of the row.
191                            // Factor it out for normalization and discard to avoid underflow or overflow.
192                            m01 /= m11;
193                            m11 = 1.0 / (1.0 + m01 * m01).sqrt();
194                            m01 *= m11;
195                        } else {
196                            // m01 is the largest component of the row.
197                            // Factor it out for normalization and discard to avoid underflow or overflow.
198                            m11 /= m01;
199                            m01 = 1.0 / (1.0 + m11 * m11).sqrt();
200                            m11 *= m01;
201                        }
202                        return m11 * u - m01 * v;
203                    }
204                }
205
206                // M is the zero matrix, any unit-length solution suffices.
207                u
208            }
209
210            /// Computes the third eigenvector as the cross product of the first two.
211            /// If the given eigenvectors are valid, the returned vector should be unit length.
212            pub fn eigenvector3(eigenvector1: $Vec3, eigenvector2: $Vec3) -> $Vec3 {
213                eigenvector1.cross(eigenvector2)
214            }
215
216            /// Compute any orthonormal pair of vectors perpendicular to `v`.
217            fn any_orthonormal_pair(v: $Vec3) -> ($Vec3, $Vec3) {
218                let sign = if v.z >= 0.0 { 1.0 } else { -1.0 };
219                let a = -1.0 / (sign + v.z);
220                let b = v.x * v.y * a;
221                let u = <$Vec3>::new(1.0 + sign * v.x * v.x * a, sign * b, -sign * v.x);
222                let w = <$Vec3>::new(b, sign + v.y * v.y * a, -v.y);
223                (u, w)
224            }
225        }
226    };
227}
228
229impl_symmetric_eigen3!(SymmetricEigen3, glam::Mat3, glam::Vec3, f32);
230impl_symmetric_eigen3!(SymmetricEigen3A, glam::Mat3A, glam::Vec3A, f32);
231impl_symmetric_eigen3!(DSymmetricEigen3, glam::DMat3, glam::DVec3, f64);
232
233// f32 <-> f64 conversions
234impl From<SymmetricEigen3> for DSymmetricEigen3 {
235    #[inline]
236    fn from(e: SymmetricEigen3) -> Self {
237        Self {
238            eigenvalues: e.eigenvalues.as_dvec3(),
239            eigenvectors: e.eigenvectors.as_dmat3(),
240        }
241    }
242}
243
244impl From<DSymmetricEigen3> for SymmetricEigen3 {
245    #[inline]
246    fn from(e: DSymmetricEigen3) -> Self {
247        Self {
248            eigenvalues: e.eigenvalues.as_vec3(),
249            eigenvectors: e.eigenvectors.as_mat3(),
250        }
251    }
252}
253
254impl From<SymmetricEigen3A> for DSymmetricEigen3 {
255    #[inline]
256    fn from(e: SymmetricEigen3A) -> Self {
257        Self {
258            eigenvalues: e.eigenvalues.as_dvec3(),
259            eigenvectors: e.eigenvectors.as_dmat3(),
260        }
261    }
262}
263
264impl From<DSymmetricEigen3> for SymmetricEigen3A {
265    #[inline]
266    fn from(e: DSymmetricEigen3) -> Self {
267        Self {
268            eigenvalues: e.eigenvalues.as_vec3a(),
269            eigenvectors: e.eigenvectors.as_mat3().into(),
270        }
271    }
272}
273
274// SymmetricEigen3 <-> SymmetricEigen3A conversions
275impl From<SymmetricEigen3> for SymmetricEigen3A {
276    #[inline]
277    fn from(e: SymmetricEigen3) -> Self {
278        Self {
279            eigenvalues: e.eigenvalues.into(),
280            eigenvectors: glam::Mat3A::from(e.eigenvectors),
281        }
282    }
283}
284
285impl From<SymmetricEigen3A> for SymmetricEigen3 {
286    #[inline]
287    fn from(e: SymmetricEigen3A) -> Self {
288        Self {
289            eigenvalues: e.eigenvalues.into(),
290            eigenvectors: glam::Mat3::from(e.eigenvectors),
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use approx::assert_relative_eq;
299
300    #[test]
301    fn eigen_3x3() {
302        let mat =
303            glam::Mat3::from_cols_array_2d(&[[2.0, 7.0, 8.0], [7.0, 6.0, 3.0], [8.0, 3.0, 0.0]]);
304        let eigen = SymmetricEigen3::new(mat);
305
306        assert_relative_eq!(
307            eigen.eigenvalues,
308            glam::Vec3::new(-7.6051016780, 0.5774961930, 15.0276054850),
309            epsilon = 1.0e-6
310        );
311        assert_relative_eq!(
312            glam::Mat3::from_cols(
313                eigen.eigenvectors.x_axis,
314                eigen.eigenvectors.y_axis,
315                eigen.eigenvectors.z_axis
316            ),
317            glam::Mat3::from_cols(
318                glam::Vec3::new(1.0754474819, -0.3328260589, -1.0).normalize(),
319                glam::Vec3::new(-0.5420669703, 1.2530131898, -1.0).normalize(),
320                glam::Vec3::new(1.3587443369, 1.3858835966, 1.0).normalize()
321            ),
322            epsilon = 1.0e-6
323        );
324    }
325
326    #[test]
327    fn eigen_3x3_diagonal() {
328        let mat =
329            glam::Mat3::from_cols_array_2d(&[[2.0, 0.0, 0.0], [0.0, 5.0, 0.0], [0.0, 0.0, 3.0]]);
330        let eigen = SymmetricEigen3::new(mat);
331
332        assert_eq!(eigen.eigenvalues, glam::Vec3::new(2.0, 3.0, 5.0));
333        assert_eq!(
334            glam::Mat3::from_cols(
335                eigen.eigenvectors.x_axis.normalize().abs(),
336                eigen.eigenvectors.y_axis.normalize().abs(),
337                eigen.eigenvectors.z_axis.normalize().abs()
338            ),
339            glam::Mat3::from_cols_array_2d(&[[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]])
340        );
341    }
342
343    #[test]
344    fn eigen_3x3_f64() {
345        let mat =
346            glam::DMat3::from_cols_array_2d(&[[2.0, 7.0, 8.0], [7.0, 6.0, 3.0], [8.0, 3.0, 0.0]]);
347        let eigen = DSymmetricEigen3::new(mat);
348
349        assert_relative_eq!(
350            eigen.eigenvalues,
351            glam::DVec3::new(-7.6051016780, 0.5774961930, 15.0276054850),
352            epsilon = 1.0e-8
353        );
354    }
355
356    #[test]
357    fn eigen_3x3_identity() {
358        // Identity matrix has all eigenvalues equal to 1 (multiplicity 3)
359        // Any orthonormal basis is a valid set of eigenvectors
360        let mat = glam::Mat3::IDENTITY;
361        let eigen = SymmetricEigen3::new(mat);
362
363        assert_eq!(eigen.eigenvalues, glam::Vec3::ONE);
364
365        // Verify eigenvectors are orthonormal (any orthonormal basis is valid)
366        assert_relative_eq!(
367            eigen.eigenvectors * eigen.eigenvectors.transpose(),
368            glam::Mat3::IDENTITY,
369            epsilon = 1e-6
370        );
371    }
372
373    #[test]
374    fn eigen_3x3_uniform_scaling() {
375        // Uniform scaling matrix: 5*I has all eigenvalues equal to 5
376        let mat = glam::Mat3::from_diagonal(glam::Vec3::splat(5.0));
377        let eigen = SymmetricEigen3::new(mat);
378
379        assert_eq!(eigen.eigenvalues, glam::Vec3::splat(5.0));
380
381        // Verify eigenvectors are orthonormal
382        assert_relative_eq!(
383            eigen.eigenvectors * eigen.eigenvectors.transpose(),
384            glam::Mat3::IDENTITY,
385            epsilon = 1e-6
386        );
387    }
388}