Skip to main content

rigidity_core/linalg/
tsqr.rs

1//! Tall-skinny QR by Givens rotations.
2//!
3//! # Why
4//!
5//! Moving to the normal equations squares the condition number:
6//! `κ(JᵀJ) = κ(J)²`. This project is entirely about the small singular
7//! values, and forming `H` destroys those first. At `κ(J) = 10⁷` — the
8//! ceiling imposed by storing points as `f32` — we get `κ(H) = 10¹⁴`,
9//! already comparable to the resolution of `f64`.
10//!
11//! `J = QR` with orthogonal `Q` gives `σᵢ(J) = σᵢ(R)` exactly, and `R` is
12//! only 6×6 (or 7×7 with an appended residual column). The singular values
13//! then come from `R`, and all available accuracy survives.
14//!
15//! # Why Givens rather than Householder
16//!
17//! Rows arrive one at a time, and a rotation folds a row into the triangle
18//! in place: no buffer for all `N` rows, constant memory, strictly
19//! streaming traversal. Backward stability is the same for both methods.
20//!
21//! The same "absorb a row" primitive also merges two triangles in the
22//! reduction tree — feed one's rows to the other.
23
24use rayon::prelude::*;
25
26/// How many rows one block handles.
27///
28/// Block boundaries depend only on this constant and the row count.
29/// Neither the thread count nor the scheduler's split affects the result.
30const CHUNK: usize = 4_096;
31
32/// The upper-triangular factor `R` of the decomposition `J = QR`.
33///
34/// `Q` is neither stored nor computed: the spectrum does not need it, and
35/// for the least-squares solve it is enough to append a residual column to
36/// `J` — then `Qᵀe` falls out of the same rotations.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct Tsqr<const K: usize> {
39    triangle: [[f64; K]; K],
40}
41
42impl<const K: usize> Default for Tsqr<K> {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl<const K: usize> Tsqr<K> {
49    /// An empty accumulator.
50    pub fn new() -> Self {
51        Self {
52            triangle: [[0.0; K]; K],
53        }
54    }
55
56    /// The upper-triangular matrix `R`, row by row.
57    pub fn triangle(&self) -> &[[f64; K]; K] {
58        &self.triangle
59    }
60
61    /// Folds a row into the triangle using Givens rotations.
62    ///
63    /// For each column a rotation is built that zeroes the next element of
64    /// the row. The radius comes from [`f64::hypot`], which neither
65    /// overflows on large values nor loses accuracy on small ones, unlike
66    /// `(a*a + b*b).sqrt()`.
67    pub fn absorb_row(&mut self, row: &[f64; K]) {
68        let mut row = *row;
69        for column in 0..K {
70            let lower = row[column];
71            if lower == 0.0 {
72                continue;
73            }
74            let upper = self.triangle[column][column];
75            let radius = upper.hypot(lower);
76            if radius == 0.0 {
77                continue;
78            }
79            let cosine = upper / radius;
80            let sine = lower / radius;
81
82            self.triangle[column][column] = radius;
83            row[column] = 0.0;
84            let tail = column + 1;
85            for (above, below) in self.triangle[column][tail..]
86                .iter_mut()
87                .zip(row[tail..].iter_mut())
88            {
89                let upper_value = *above;
90                let lower_value = *below;
91                *above = cosine * upper_value + sine * lower_value;
92                *below = cosine * lower_value - sine * upper_value;
93            }
94        }
95    }
96
97    /// Merges another triangle into this one.
98    pub fn merge(&mut self, other: &Self) {
99        for row in &other.triangle {
100            self.absorb_row(row);
101        }
102    }
103}
104
105/// Assembles `R` from the rows returned by `row`.
106///
107/// `row(i)` returns `None` for rows to skip — rejected correspondences,
108/// for instance.
109///
110/// # Determinism
111///
112/// The reduction tree is fully specified: blocks are cut by a fixed
113/// internal constant, rows within a block are folded in increasing index
114/// order, and blocks merge through a balanced binary tree in a fixed
115/// order. The result is bit-for-bit identical at any thread count.
116///
117/// A balanced tree rather than a sequential fold: error accumulation grows
118/// as `log P` for the former and as `P` for the latter.
119pub fn reduce<const K: usize, F>(count: usize, row: F) -> Tsqr<K>
120where
121    F: Fn(usize) -> Option<[f64; K]> + Sync,
122{
123    if count == 0 {
124        return Tsqr::new();
125    }
126
127    let chunks = count.div_ceil(CHUNK);
128    let mut level: Vec<Tsqr<K>> = (0..chunks)
129        .into_par_iter()
130        .map(|chunk| {
131            let begin = chunk * CHUNK;
132            let end = ((chunk + 1) * CHUNK).min(count);
133            let mut accumulator = Tsqr::new();
134            for index in begin..end {
135                if let Some(values) = row(index) {
136                    accumulator.absorb_row(&values);
137                }
138            }
139            accumulator
140        })
141        .collect();
142
143    while level.len() > 1 {
144        level = level
145            .par_chunks(2)
146            .map(|pair| {
147                let mut left = pair[0];
148                if let Some(right) = pair.get(1) {
149                    left.merge(right);
150                }
151                left
152            })
153            .collect();
154    }
155    level[0]
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    /// The Frobenius norm is invariant under orthogonal transformations,
163    /// so `‖R‖_F` must equal `‖J‖_F`.
164    ///
165    /// This checks the orthogonality of `Q` without ever computing it: had
166    /// the rotations been built wrongly, the norm would not survive.
167    #[test]
168    fn frobenius_norm_is_preserved() {
169        let mut state = 12_345u64;
170        let mut next = || {
171            state = state
172                .wrapping_mul(6_364_136_223_846_793_005)
173                .wrapping_add(1_442_695_040_888_963_407);
174            ((state >> 11) as f64) / ((1u64 << 53) as f64) * 2.0 - 1.0
175        };
176
177        let rows: Vec<[f64; 6]> = (0..5_000)
178            .map(|_| [next(), next(), next(), next(), next(), next()])
179            .collect();
180        let source_norm: f64 = rows
181            .iter()
182            .flat_map(|row| row.iter())
183            .map(|v| v * v)
184            .sum::<f64>()
185            .sqrt();
186
187        let result = reduce::<6, _>(rows.len(), |i| Some(rows[i]));
188        let triangle_norm: f64 = result
189            .triangle()
190            .iter()
191            .flat_map(|row| row.iter())
192            .map(|v| v * v)
193            .sum::<f64>()
194            .sqrt();
195
196        let relative = (triangle_norm - source_norm).abs() / source_norm;
197        assert!(relative < 1e-14, "‖R‖_F differs by {relative:.3e}");
198    }
199
200    /// `R` is upper triangular.
201    #[test]
202    fn result_is_upper_triangular() {
203        let rows: Vec<[f64; 6]> = (0..100)
204            .map(|i| {
205                let t = i as f64;
206                [t, t * 0.5, -t, 1.0, t * t * 0.01, 3.0]
207            })
208            .collect();
209        let result = reduce::<6, _>(rows.len(), |i| Some(rows[i]));
210        for (i, row) in result.triangle().iter().enumerate() {
211            for value in row.iter().take(i) {
212                assert_eq!(*value, 0.0, "an element below the diagonal is non-zero");
213            }
214        }
215    }
216
217    /// Skipping rows really skips them.
218    #[test]
219    fn skipped_rows_do_not_contribute() {
220        let rows: Vec<[f64; 6]> = (0..1_000)
221            .map(|i| [i as f64, 1.0, 2.0, 3.0, 4.0, 5.0])
222            .collect();
223        let all = reduce::<6, _>(rows.len(), |i| Some(rows[i]));
224        let even = reduce::<6, _>(rows.len(), |i| (i % 2 == 0).then_some(rows[i]));
225        assert_ne!(all, even);
226
227        let only_even: Vec<[f64; 6]> = rows.iter().step_by(2).copied().collect();
228        let direct = reduce::<6, _>(only_even.len(), |i| Some(only_even[i]));
229        assert_eq!(even, direct);
230    }
231}