glam_matrix_extras/eigen/
symmetric_eigen3.rs

1// The eigensolver is a Rust adaptation, with modifications, of the pseudocode and approach described in
2// "A Robust Eigensolver for 3 x 3 Symmetric Matrices" by David Eberly, Geometric Tools, Redmond WA 98052.
3// https://www.geometrictools.com/Documentation/RobustEigenSymmetric3x3.pdf
4
5use crate::{
6    SymmetricMat3,
7    ops::{self, FloatPow},
8};
9use glam::{Mat3, Vec3, Vec3Swizzles};
10
11/// The [eigen decomposition] of a [`SymmetricMat3`].
12///
13/// [eigen decomposition]: https://en.wikipedia.org/wiki/Eigendecomposition_of_a_matrix
14#[derive(Clone, Copy, Debug, PartialEq)]
15#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct SymmetricEigen3 {
18    /// The eigenvalues of the [`SymmetricMat3`].
19    ///
20    /// These should be in ascending order `eigen1 <= eigen2 <= eigen3`.
21    pub eigenvalues: Vec3,
22    /// The three eigenvectors of the [`SymmetricMat3`].
23    /// They should be unit length and orthogonal to the other eigenvectors.
24    ///
25    /// The eigenvectors are ordered to correspond to the eigenvalues. For example,
26    /// `eigenvectors.x_axis` corresponds to `eigenvalues.x`.
27    pub eigenvectors: Mat3,
28}
29
30impl SymmetricEigen3 {
31    /// Computes the eigen decomposition of the given [`SymmetricMat3`].
32    ///
33    /// The eigenvalues are returned in ascending order `eigen1 <= eigen2 <= eigen3`.
34    /// This can be reversed with the [`reverse`](Self::reverse) method.
35    pub fn new(mat: SymmetricMat3) -> Self {
36        let (mut eigenvalues, is_diagonal) = Self::eigenvalues(mat);
37
38        if is_diagonal {
39            // The matrix is already diagonal. Sort the eigenvalues in ascending order,
40            // ordering the eigenvectors accordingly, and return early.
41            let mut eigenvectors = Mat3::IDENTITY;
42            if eigenvalues[0] > eigenvalues[1] {
43                core::mem::swap(&mut eigenvalues.x, &mut eigenvalues.y);
44                core::mem::swap(&mut eigenvectors.x_axis, &mut eigenvectors.y_axis);
45            }
46            if eigenvalues[1] > eigenvalues[2] {
47                core::mem::swap(&mut eigenvalues.y, &mut eigenvalues.z);
48                core::mem::swap(&mut eigenvectors.y_axis, &mut eigenvectors.z_axis);
49            }
50            if eigenvalues[0] > eigenvalues[1] {
51                core::mem::swap(&mut eigenvalues.x, &mut eigenvalues.y);
52                core::mem::swap(&mut eigenvectors.x_axis, &mut eigenvectors.y_axis);
53            }
54            return Self {
55                eigenvalues,
56                eigenvectors,
57            };
58        }
59
60        // Compute the eigenvectors corresponding to the eigenvalues.
61        let eigenvector1 = Self::eigenvector1(mat, eigenvalues.x);
62        let eigenvector2 = Self::eigenvector2(mat, eigenvector1, eigenvalues.y);
63        let eigenvector3 = Self::eigenvector3(eigenvector1, eigenvector2);
64
65        Self {
66            eigenvalues,
67            eigenvectors: Mat3::from_cols(eigenvector1, eigenvector2, eigenvector3),
68        }
69    }
70
71    /// Reverses the order of the eigenvalues and their corresponding eigenvectors.
72    pub fn reverse(&self) -> Self {
73        Self {
74            eigenvalues: self.eigenvalues.zyx(),
75            eigenvectors: Mat3::from_cols(
76                self.eigenvectors.z_axis,
77                self.eigenvectors.y_axis,
78                self.eigenvectors.x_axis,
79            ),
80        }
81    }
82
83    /// Computes the eigenvalues of a [`SymmetricMat3`], also returning whether the input matrix is diagonal.
84    ///
85    /// If the matrix is already diagonal, the eigenvalues are returned as is without reordering.
86    /// Otherwise, the eigenvalues are computed and returned in ascending order
87    /// such that `eigen1 <= eigen2 <= eigen3`.
88    pub fn eigenvalues(mat: SymmetricMat3) -> (Vec3, bool) {
89        // Reference: https://en.wikipedia.org/wiki/Eigenvalue_algorithm#Symmetric_3%C3%973_matrices
90
91        let p1 = mat.m01.squared() + mat.m02.squared() + mat.m12.squared();
92
93        // Check if the matrix is nearly diagonal.
94        // Without this check, the algorithm can produce NaN values.
95        // TODO: What is the ideal threshold here?
96        if p1 < 1e-10 {
97            return (Vec3::new(mat.m00, mat.m11, mat.m22), true);
98        }
99
100        let q = (mat.m00 + mat.m11 + mat.m22) / 3.0;
101        let p2 =
102            (mat.m00 - q).squared() + (mat.m11 - q).squared() + (mat.m22 - q).squared() + 2.0 * p1;
103        let p = ops::sqrt(p2 / 6.0);
104
105        let mat_b = 1.0 / p * (mat - q * Mat3::IDENTITY);
106        let r = mat_b.determinant() / 2.0;
107
108        // r should be in the [-1, 1] range for a symmetric matrix,
109        // but computation error can leave it slightly outside this range.
110        let phi = if r <= -1.0 {
111            core::f32::consts::FRAC_PI_3
112        } else if r >= 1.0 {
113            0.0
114        } else {
115            ops::acos(r) / 3.0
116        };
117
118        // The eigenvalues satisfy eigen3 <= eigen2 <= eigen1
119        let eigen1 = q + 2.0 * p * ops::cos(phi);
120        let eigen3 = q + 2.0 * p * ops::cos(phi + 2.0 * core::f32::consts::FRAC_PI_3);
121        let eigen2 = 3.0 * q - eigen1 - eigen3; // trace(mat) = eigen1 + eigen2 + eigen3
122        (Vec3::new(eigen3, eigen2, eigen1), false)
123    }
124
125    // TODO: Fall back to QL when the eigenvalue precision is poor.
126    /// Computes the unit-length eigenvector corresponding to the `eigenvalue1` of `mat` that was
127    /// computed from the root of a cubic polynomial with a multiplicity of 1.
128    ///
129    /// If the other two eigenvalues are well separated, this method can be used for computing
130    /// all three eigenvectors. However, to avoid numerical issues when eigenvalues are close to
131    /// each other, it's recommended to use the `eigenvector2` method for the second eigenvector.
132    ///
133    /// The third eigenvector can be computed as the cross product of the first two.
134    pub fn eigenvector1(mat: SymmetricMat3, eigenvalue1: f32) -> Vec3 {
135        let cols = (mat - SymmetricMat3::from_diagonal(Vec3::splat(eigenvalue1))).to_mat3();
136        let c0xc1 = cols.x_axis.cross(cols.y_axis);
137        let c0xc2 = cols.x_axis.cross(cols.z_axis);
138        let c1xc2 = cols.y_axis.cross(cols.z_axis);
139        let d0 = c0xc1.length_squared();
140        let d1 = c0xc2.length_squared();
141        let d2 = c1xc2.length_squared();
142
143        let mut d_max = d0;
144        let mut i_max = 0;
145
146        if d1 > d_max {
147            d_max = d1;
148            i_max = 1;
149        }
150        if d2 > d_max {
151            i_max = 2;
152        }
153        if i_max == 0 {
154            c0xc1 / ops::sqrt(d0)
155        } else if i_max == 1 {
156            c0xc2 / ops::sqrt(d1)
157        } else {
158            c1xc2 / ops::sqrt(d2)
159        }
160    }
161
162    /// Computes the unit-length eigenvector corresponding to the `eigenvalue2` of `mat` that was
163    /// computed from the root of a cubic polynomial with a potential multiplicity of 2.
164    ///
165    /// The third eigenvector can be computed as the cross product of the first two.
166    pub fn eigenvector2(mat: SymmetricMat3, eigenvector1: Vec3, eigenvalue2: f32) -> Vec3 {
167        // Compute right-handed orthonormal set { U, V, W }, where W is eigenvector1.
168        let (u, v) = eigenvector1.any_orthonormal_pair();
169
170        // The unit-length eigenvector is E = x0 * U + x1 * V. We need to compute x0 and x1.
171        //
172        // Define the symmetrix 2x2 matrix M = J^T * (mat - eigenvalue2 * I), where J = [U V]
173        // and I is a 3x3 identity matrix. This means that E = J * X, where X is a column vector
174        // with rows x0 and x1. The 3x3 linear system (mat - eigenvalue2 * I) * E = 0 reduces to
175        // the 2x2 linear system M * X = 0.
176        //
177        // When eigenvalue2 != eigenvalue3, M has rank 1 and is not the zero matrix.
178        // Otherwise, it has rank 0, and it is the zero matrix.
179
180        let au = mat * u;
181        let av = mat * v;
182
183        let mut m00 = u.dot(au) - eigenvalue2;
184        let mut m01 = u.dot(av);
185        let mut m11 = v.dot(av) - eigenvalue2;
186        let (abs_m00, abs_m01, abs_m11) = (ops::abs(m00), ops::abs(m01), ops::abs(m11));
187
188        if abs_m00 >= abs_m11 {
189            let max_abs_component = abs_m00.max(abs_m01);
190            if max_abs_component > 0.0 {
191                if abs_m00 >= abs_m01 {
192                    // m00 is the largest component of the row.
193                    // Factor it out for normalization and discard to avoid underflow or overflow.
194                    m01 /= m00;
195                    m00 = 1.0 / ops::sqrt(1.0 + m01 * m01);
196                    m01 *= m00;
197                } else {
198                    // m01 is the largest component of the row.
199                    // Factor it out for normalization and discard to avoid underflow or overflow.
200                    m00 /= m01;
201                    m01 = 1.0 / ops::sqrt(1.0 + m00 * m00);
202                    m00 *= m01;
203                }
204                return m01 * u - m00 * v;
205            }
206        } else {
207            let max_abs_component = abs_m11.max(abs_m01);
208            if max_abs_component > 0.0 {
209                if abs_m11 >= abs_m01 {
210                    // m11 is the largest component of the row.
211                    // Factor it out for normalization and discard to avoid underflow or overflow.
212                    m01 /= m11;
213                    m11 = 1.0 / ops::sqrt(1.0 + m01 * m01);
214                    m01 *= m11;
215                } else {
216                    // m01 is the largest component of the row.
217                    // Factor it out for normalization and discard to avoid underflow or overflow.
218                    m11 /= m01;
219                    m01 = 1.0 / ops::sqrt(1.0 + m11 * m11);
220                    m11 *= m01;
221                }
222                return m11 * u - m01 * v;
223            }
224        }
225
226        // M is the zero matrix, any unit-length solution suffices.
227        u
228    }
229
230    /// Computes the third eigenvector as the cross product of the first two.
231    /// If the given eigenvectors are valid, the returned vector should be unit length.
232    pub fn eigenvector3(eigenvector1: Vec3, eigenvector2: Vec3) -> Vec3 {
233        eigenvector1.cross(eigenvector2)
234    }
235}
236
237#[cfg(test)]
238mod test {
239    use super::SymmetricEigen3;
240    use crate::SymmetricMat3;
241    use approx::assert_relative_eq;
242    use glam::{Mat3, Vec3};
243    use rand::{Rng, SeedableRng};
244
245    #[test]
246    fn eigen_3x3() {
247        let mat = SymmetricMat3::new(2.0, 7.0, 8.0, 6.0, 3.0, 0.0);
248        let eigen = SymmetricEigen3::new(mat);
249
250        assert_relative_eq!(
251            eigen.eigenvalues,
252            Vec3::new(-7.605, 0.577, 15.028),
253            epsilon = 0.001
254        );
255        assert_relative_eq!(
256            Mat3::from_cols(
257                eigen.eigenvectors.x_axis.abs(),
258                eigen.eigenvectors.y_axis.abs(),
259                eigen.eigenvectors.z_axis.abs()
260            ),
261            Mat3::from_cols(
262                Vec3::new(-1.075, 0.333, 1.0).normalize().abs(),
263                Vec3::new(0.542, -1.253, 1.0).normalize().abs(),
264                Vec3::new(1.359, 1.386, 1.0).normalize().abs()
265            ),
266            epsilon = 0.001
267        );
268    }
269
270    #[test]
271    fn eigen_3x3_diagonal() {
272        let mat = SymmetricMat3::from_diagonal(Vec3::new(2.0, 5.0, 3.0));
273        let eigen = SymmetricEigen3::new(mat);
274
275        assert_eq!(eigen.eigenvalues, Vec3::new(2.0, 3.0, 5.0));
276        assert_eq!(
277            Mat3::from_cols(
278                eigen.eigenvectors.x_axis.normalize().abs(),
279                eigen.eigenvectors.y_axis.normalize().abs(),
280                eigen.eigenvectors.z_axis.normalize().abs()
281            ),
282            Mat3::from_cols_array_2d(&[[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]])
283        );
284    }
285
286    #[test]
287    fn eigen_3x3_reconstruction() {
288        let mut rng = rand_chacha::ChaCha8Rng::from_seed(Default::default());
289
290        // Generate random symmetric matrices and verify that the eigen decomposition is correct.
291        for _ in 0..10_000 {
292            let eigenvalues = Vec3::new(
293                rng.random_range(0.1..100.0),
294                rng.random_range(0.1..100.0),
295                rng.random_range(0.1..100.0),
296            );
297            let eigenvectors = Mat3::from_cols(
298                Vec3::new(
299                    rng.random_range(-1.0..1.0),
300                    rng.random_range(-1.0..1.0),
301                    rng.random_range(-1.0..1.0),
302                )
303                .normalize(),
304                Vec3::new(
305                    rng.random_range(-1.0..1.0),
306                    rng.random_range(-1.0..1.0),
307                    rng.random_range(-1.0..1.0),
308                )
309                .normalize(),
310                Vec3::new(
311                    rng.random_range(-1.0..1.0),
312                    rng.random_range(-1.0..1.0),
313                    rng.random_range(-1.0..1.0),
314                )
315                .normalize(),
316            );
317
318            // Construct the symmetric matrix from the eigenvalues and eigenvectors.
319            let mat1 = eigenvectors * Mat3::from_diagonal(eigenvalues) * eigenvectors.transpose();
320
321            // Compute the eigen decomposition of the constructed matrix.
322            let eigen = SymmetricEigen3::new(SymmetricMat3::from_mat3_unchecked(mat1));
323
324            // Reconstruct the matrix from the computed eigenvalues and eigenvectors.
325            let mat2 = eigen.eigenvectors
326                * Mat3::from_diagonal(eigen.eigenvalues)
327                * eigen.eigenvectors.transpose();
328
329            // The reconstructed matrix should be close to the original matrix.
330            // Note: The precision depends on how large the eigenvalues are.
331            //       Larger eigenvalues can lead to larger absolute error.
332            assert_relative_eq!(mat1, mat2, epsilon = 1e-2);
333        }
334    }
335
336    #[test]
337    fn eigen_pathological() {
338        // The algorithm sometimes produces NaN eigenvalues and eigenvectors for matrices
339        // that are already nearly diagonal. There is a diagonality check that should avoid this.
340        let mat = SymmetricMat3 {
341            m00: 5.3333335,
342            m01: 3.4465857e-20,
343            m02: 0.0,
344            m11: 5.3333335,
345            m12: 0.0,
346            m22: 5.3333335,
347        };
348        let eigen = SymmetricEigen3::new(mat);
349        assert_relative_eq!(eigen.eigenvalues, Vec3::splat(5.3333335), epsilon = 1e-6);
350        assert_relative_eq!(
351            eigen.eigenvectors.x_axis.abs(),
352            Vec3::new(1.0, 0.0, 0.0),
353            epsilon = 1e-6
354        );
355        assert_relative_eq!(
356            eigen.eigenvectors.y_axis.abs(),
357            Vec3::new(0.0, 1.0, 0.0),
358            epsilon = 1e-6
359        );
360        assert_relative_eq!(
361            eigen.eigenvectors.z_axis.abs(),
362            Vec3::new(0.0, 0.0, 1.0),
363            epsilon = 1e-6
364        );
365    }
366}