Skip to main content

glamx/
svd2.rs

1//! SVD decomposition for 2x2 matrices.
2
3#[cfg(not(feature = "std"))]
4use simba::scalar::{ComplexField, RealField};
5
6/// Macro to generate Svd2 for a specific scalar type.
7macro_rules! impl_svd2 {
8    ($Svd2:ident, $Mat2:ty, $Vec2:ty, $Real:ty $(, #[$attr:meta])*) => {
9        #[doc = concat!("The SVD of a 2x2 matrix (", stringify!($Real), " precision).")]
10        #[derive(Clone, Copy, Debug, PartialEq)]
11        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12        $(#[$attr])*
13        pub struct $Svd2 {
14            /// The left orthogonal matrix U.
15            pub u: $Mat2,
16            /// The singular values (diagonal entries of S).
17            pub s: $Vec2,
18            /// The transposed right orthogonal matrix V^T.
19            pub vt: $Mat2,
20        }
21
22        impl $Svd2 {
23            /// Creates a new SVD from components.
24            #[inline]
25            pub fn new(u: $Mat2, s: $Vec2, vt: $Mat2) -> Self {
26                Self { u, s, vt }
27            }
28
29            /// Computes the SVD of a 2x2 matrix.
30            ///
31            /// Returns the decomposition `M = U * S * V^T` where:
32            /// - `U` is an orthogonal matrix
33            /// - `S` is a diagonal matrix (stored as a vector of singular values)
34            /// - `V^T` is the transpose of an orthogonal matrix
35            ///
36            /// The singular values are sorted in descending order (s.x >= s.y).
37            #[inline]
38            pub fn from_matrix(m: $Mat2) -> Self {
39                let e = (m.col(0).x + m.col(1).y) * 0.5;
40                let f = (m.col(0).x - m.col(1).y) * 0.5;
41                let g = (m.col(0).y + m.col(1).x) * 0.5;
42                let h = (m.col(0).y - m.col(1).x) * 0.5;
43                let q = (e * e + h * h).sqrt();
44                let r = (f * f + g * g).sqrt();
45
46                // Note that the singular values are always sorted because sx >= sy
47                // because q >= 0 and r >= 0.
48                let sx = q + r;
49                let sy = q - r;
50                let sy_sign: $Real = if sy < 0.0 { -1.0 } else { 1.0 };
51                let singular_values = <$Vec2>::new(sx, sy * sy_sign);
52
53                let a2 = h.atan2(e);
54                // When r ≈ 0 (pure rotation case), a1 is arbitrary. Set a1 = a2
55                // so that theta = 0 and phi = a2 (the rotation angle).
56                let a1 = if r < 1e-10 { a2 } else { g.atan2(f) };
57                let theta = (a2 - a1) * 0.5;
58                let phi = (a2 + a1) * 0.5;
59                let st = theta.sin();
60                let ct = theta.cos();
61                let sp = phi.sin();
62                let cp = phi.cos();
63
64                let u = <$Mat2>::from_cols(<$Vec2>::new(cp, sp), <$Vec2>::new(-sp, cp));
65                let vt = <$Mat2>::from_cols(
66                    <$Vec2>::new(ct, st * sy_sign),
67                    <$Vec2>::new(-st, ct * sy_sign),
68                );
69
70                Self {
71                    u,
72                    s: singular_values,
73                    vt,
74                }
75            }
76
77            /// Rebuilds the matrix this SVD is the decomposition of.
78            ///
79            /// Returns `U * S * V^T`.
80            #[inline]
81            pub fn recompose(&self) -> $Mat2 {
82                let u_s = <$Mat2>::from_cols(self.u.col(0) * self.s.x, self.u.col(1) * self.s.y);
83                u_s * self.vt.transpose()
84            }
85
86        }
87    };
88}
89
90impl_svd2!(Svd2, glam::Mat2, glam::Vec2, f32);
91#[cfg(feature = "f64")]
92impl_svd2!(DSvd2, glam::DMat2, glam::DVec2, f64);
93
94// f32 <-> f64 conversions
95#[cfg(feature = "f64")]
96impl From<Svd2> for DSvd2 {
97    #[inline]
98    fn from(svd: Svd2) -> Self {
99        Self {
100            u: svd.u.as_dmat2(),
101            s: svd.s.as_dvec2(),
102            vt: svd.vt.as_dmat2(),
103        }
104    }
105}
106
107#[cfg(feature = "f64")]
108impl From<DSvd2> for Svd2 {
109    #[inline]
110    fn from(svd: DSvd2) -> Self {
111        Self {
112            u: svd.u.as_mat2(),
113            s: svd.s.as_vec2(),
114            vt: svd.vt.as_mat2(),
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use approx::assert_relative_eq;
123
124    #[test]
125    fn svd_2x2_identity() {
126        let mat = glam::Mat2::IDENTITY;
127        let svd = Svd2::from_matrix(mat);
128
129        assert_relative_eq!(svd.s, glam::Vec2::new(1.0, 1.0), epsilon = 1e-6);
130        assert_relative_eq!(svd.recompose(), mat, epsilon = 1e-6);
131    }
132
133    #[test]
134    fn svd_2x2_diagonal() {
135        let mat = glam::Mat2::from_diagonal(glam::Vec2::new(3.0, 2.0));
136        let svd = Svd2::from_matrix(mat);
137
138        assert_relative_eq!(svd.s, glam::Vec2::new(3.0, 2.0), epsilon = 1e-6);
139        assert_relative_eq!(svd.recompose(), mat, epsilon = 1e-6);
140    }
141
142    #[test]
143    fn svd_2x2_general() {
144        let mat = glam::Mat2::from_cols_array_2d(&[[4.0, 3.0], [2.0, 1.0]]);
145        let svd = Svd2::from_matrix(mat);
146
147        // Verify recomposition
148        assert_relative_eq!(svd.recompose(), mat, epsilon = 1e-5);
149
150        // Verify singular values are non-negative and sorted
151        assert!(svd.s.x >= svd.s.y);
152        assert!(svd.s.y >= 0.0);
153
154        // Verify U and V^T are orthogonal
155        assert_relative_eq!(
156            svd.u * svd.u.transpose(),
157            glam::Mat2::IDENTITY,
158            epsilon = 1e-6
159        );
160        assert_relative_eq!(
161            svd.vt * svd.vt.transpose(),
162            glam::Mat2::IDENTITY,
163            epsilon = 1e-6
164        );
165    }
166
167    #[test]
168    fn svd_2x2_rotation() {
169        let angle = 0.5_f32;
170        let mat = glam::Mat2::from_angle(angle);
171        let svd = Svd2::from_matrix(mat);
172
173        // A rotation matrix has singular values of 1
174        assert_relative_eq!(svd.s, glam::Vec2::new(1.0, 1.0), epsilon = 1e-6);
175        assert_relative_eq!(svd.recompose(), mat, epsilon = 1e-6);
176    }
177
178    #[cfg(feature = "f64")]
179    #[test]
180    fn svd_2x2_f64() {
181        let mat = glam::DMat2::from_cols_array_2d(&[[4.0, 3.0], [2.0, 1.0]]);
182        let svd = DSvd2::from_matrix(mat);
183
184        assert_relative_eq!(svd.recompose(), mat, epsilon = 1e-10);
185    }
186}