use rayon::prelude::*;
const CHUNK: usize = 4_096;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tsqr<const K: usize> {
triangle: [[f64; K]; K],
}
impl<const K: usize> Default for Tsqr<K> {
fn default() -> Self {
Self::new()
}
}
impl<const K: usize> Tsqr<K> {
pub fn new() -> Self {
Self {
triangle: [[0.0; K]; K],
}
}
pub fn triangle(&self) -> &[[f64; K]; K] {
&self.triangle
}
pub fn absorb_row(&mut self, row: &[f64; K]) {
let mut row = *row;
for column in 0..K {
let lower = row[column];
if lower == 0.0 {
continue;
}
let upper = self.triangle[column][column];
let radius = upper.hypot(lower);
if radius == 0.0 {
continue;
}
let cosine = upper / radius;
let sine = lower / radius;
self.triangle[column][column] = radius;
row[column] = 0.0;
let tail = column + 1;
for (above, below) in self.triangle[column][tail..]
.iter_mut()
.zip(row[tail..].iter_mut())
{
let upper_value = *above;
let lower_value = *below;
*above = cosine * upper_value + sine * lower_value;
*below = cosine * lower_value - sine * upper_value;
}
}
}
pub fn merge(&mut self, other: &Self) {
for row in &other.triangle {
self.absorb_row(row);
}
}
}
pub fn reduce<const K: usize, F>(count: usize, row: F) -> Tsqr<K>
where
F: Fn(usize) -> Option<[f64; K]> + Sync,
{
if count == 0 {
return Tsqr::new();
}
let chunks = count.div_ceil(CHUNK);
let mut level: Vec<Tsqr<K>> = (0..chunks)
.into_par_iter()
.map(|chunk| {
let begin = chunk * CHUNK;
let end = ((chunk + 1) * CHUNK).min(count);
let mut accumulator = Tsqr::new();
for index in begin..end {
if let Some(values) = row(index) {
accumulator.absorb_row(&values);
}
}
accumulator
})
.collect();
while level.len() > 1 {
level = level
.par_chunks(2)
.map(|pair| {
let mut left = pair[0];
if let Some(right) = pair.get(1) {
left.merge(right);
}
left
})
.collect();
}
level[0]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frobenius_norm_is_preserved() {
let mut state = 12_345u64;
let mut next = || {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((state >> 11) as f64) / ((1u64 << 53) as f64) * 2.0 - 1.0
};
let rows: Vec<[f64; 6]> = (0..5_000)
.map(|_| [next(), next(), next(), next(), next(), next()])
.collect();
let source_norm: f64 = rows
.iter()
.flat_map(|row| row.iter())
.map(|v| v * v)
.sum::<f64>()
.sqrt();
let result = reduce::<6, _>(rows.len(), |i| Some(rows[i]));
let triangle_norm: f64 = result
.triangle()
.iter()
.flat_map(|row| row.iter())
.map(|v| v * v)
.sum::<f64>()
.sqrt();
let relative = (triangle_norm - source_norm).abs() / source_norm;
assert!(relative < 1e-14, "‖R‖_F differs by {relative:.3e}");
}
#[test]
fn result_is_upper_triangular() {
let rows: Vec<[f64; 6]> = (0..100)
.map(|i| {
let t = i as f64;
[t, t * 0.5, -t, 1.0, t * t * 0.01, 3.0]
})
.collect();
let result = reduce::<6, _>(rows.len(), |i| Some(rows[i]));
for (i, row) in result.triangle().iter().enumerate() {
for value in row.iter().take(i) {
assert_eq!(*value, 0.0, "an element below the diagonal is non-zero");
}
}
}
#[test]
fn skipped_rows_do_not_contribute() {
let rows: Vec<[f64; 6]> = (0..1_000)
.map(|i| [i as f64, 1.0, 2.0, 3.0, 4.0, 5.0])
.collect();
let all = reduce::<6, _>(rows.len(), |i| Some(rows[i]));
let even = reduce::<6, _>(rows.len(), |i| (i % 2 == 0).then_some(rows[i]));
assert_ne!(all, even);
let only_even: Vec<[f64; 6]> = rows.iter().step_by(2).copied().collect();
let direct = reduce::<6, _>(only_even.len(), |i| Some(only_even[i]));
assert_eq!(even, direct);
}
}