use rayon::prelude::*;
const BLOCK: usize = 64;
pub(super) fn dot(a: &[f64], b: &[f64]) -> f64 {
let n = a.len().min(b.len());
let (a, b) = (&a[..n], &b[..n]);
let mut lanes = [0.0f64; 4];
let (chunks_a, rest_a) = a.as_chunks::<4>();
let (chunks_b, rest_b) = b.as_chunks::<4>();
for (x, y) in chunks_a.iter().zip(chunks_b) {
for l in 0..4 {
lanes[l] += x[l] * y[l];
}
}
let mut tail = 0.0;
for (x, y) in rest_a.iter().zip(rest_b) {
tail += x * y;
}
(lanes[0] + lanes[1]) + (lanes[2] + lanes[3]) + tail
}
pub(super) fn axpy(alpha: f64, x: &[f64], y: &mut [f64]) {
for (yi, &xi) in y.iter_mut().zip(x) {
*yi += alpha * xi;
}
}
pub(super) fn cholesky_in_place(a: &mut [f64], n: usize) -> Result<(), usize> {
debug_assert_eq!(a.len(), n * n);
let mut diag_block = vec![0.0; BLOCK * BLOCK];
let mut panel: Vec<f64> = Vec::new();
for kb in (0..n).step_by(BLOCK) {
let ke = (kb + BLOCK).min(n);
let nb = ke - kb;
for j in kb..ke {
let row_j = &mut a[j * n..(j + 1) * n];
let s = row_j[j] - dot(&row_j[kb..j], &row_j[kb..j]);
if !(s > 0.0 && s.is_finite()) {
return Err(j);
}
let d = s.sqrt();
row_j[j] = d;
for i in j + 1..ke {
let (upper, lower) = a.split_at_mut(i * n);
let row_j = &upper[j * n..j * n + n];
let row_i = &mut lower[..n];
row_i[j] = (row_i[j] - dot(&row_i[kb..j], &row_j[kb..j])) / d;
}
}
if ke == n {
break;
}
for (r, i) in (kb..ke).enumerate() {
diag_block[r * BLOCK..r * BLOCK + nb].copy_from_slice(&a[i * n + kb..i * n + ke]);
}
let (_, below) = a.split_at_mut(ke * n);
below.par_chunks_mut(n).with_min_len(16).for_each(|row| {
for (c, j) in (kb..ke).enumerate() {
let lj = &diag_block[c * BLOCK..c * BLOCK + c];
row[j] = (row[j] - dot(&row[kb..j], lj)) / diag_block[c * BLOCK + c];
}
});
panel.clear();
for row in below.chunks(n) {
panel.extend_from_slice(&row[kb..ke]);
}
below
.par_chunks_mut(n)
.with_min_len(8)
.enumerate()
.for_each(|(r, row)| {
let pi = &panel[r * nb..(r + 1) * nb];
for (c, v) in row[ke..=ke + r].iter_mut().enumerate() {
*v -= dot(pi, &panel[c * nb..(c + 1) * nb]);
}
});
}
Ok(())
}
pub(super) fn forward_solve(l: &[f64], n: usize, b: &mut [f64], m: usize) {
for i in 0..n {
let (done, rest) = b.split_at_mut(i * m);
let row = &mut rest[..m];
let li = &l[i * n..i * n + i];
for (k, &lik) in li.iter().enumerate() {
if lik != 0.0 {
axpy(-lik, &done[k * m..(k + 1) * m], row);
}
}
let d = l[i * n + i];
for v in row.iter_mut() {
*v /= d;
}
}
}
pub(super) fn backward_solve(l: &[f64], n: usize, y: &mut [f64], m: usize) {
for i in (0..n).rev() {
let (top, rest) = y.split_at_mut(i * m);
let row = &mut rest[..m];
let d = l[i * n + i];
for v in row.iter_mut() {
*v /= d;
}
let li = &l[i * n..i * n + i];
for (k, &lik) in li.iter().enumerate() {
if lik != 0.0 {
axpy(-lik, row, &mut top[k * m..(k + 1) * m]);
}
}
}
}
pub(super) struct PivotedCholesky {
pub(super) pivots: Vec<usize>,
pub(super) factor: Vec<f64>,
}
impl PivotedCholesky {
pub(super) fn rank(&self) -> usize {
self.pivots.len()
}
}
pub(super) fn pivoted_cholesky(a: &[f64], n: usize, rel_tol: f64) -> PivotedCholesky {
let mut residual: Vec<f64> = (0..n).map(|i| a[i * n + i]).collect();
let largest = residual
.iter()
.copied()
.filter(|d| d.is_finite())
.fold(0.0f64, f64::max);
let tol = rel_tol * largest;
let mut perm: Vec<usize> = (0..n).collect();
let mut l = vec![0.0; n * n];
let mut rank = 0;
for k in 0..n {
let mut best = None;
for (j, &q) in perm.iter().enumerate().skip(k) {
let d = residual[q];
if d.is_finite() && d > tol && best.is_none_or(|(_, bd)| d > bd) {
best = Some((j, d));
}
}
let Some((jb, d)) = best else {
break;
};
perm.swap(k, jb);
for c in 0..k {
l.swap(k * n + c, jb * n + c);
}
let p = perm[k];
let lkk = d.sqrt();
l[k * n + k] = lkk;
let (head, tail) = l.split_at_mut((k + 1) * n);
let lk = &head[k * n..k * n + k];
let rest = &perm[k + 1..];
tail.par_chunks_mut(n)
.with_min_len(64)
.zip(rest)
.for_each(|(row, &q)| {
row[k] = (a[q * n + p] - dot(&row[..k], lk)) / lkk;
});
for (row, &q) in tail.chunks(n).zip(rest) {
residual[q] -= row[k] * row[k];
}
rank = k + 1;
}
let mut factor = vec![0.0; rank * rank];
for i in 0..rank {
factor[i * rank..=i * rank + i].copy_from_slice(&l[i * n..=i * n + i]);
}
perm.truncate(rank);
PivotedCholesky {
pivots: perm,
factor,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn spd(n: usize, seed: u64) -> Vec<f64> {
let mut rng = crate::rng::Rng::new(seed);
let b: Vec<f64> = (0..n * n).map(|_| rng.f64() - 0.5).collect();
let mut a = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
a[i * n + j] = dot(&b[i * n..(i + 1) * n], &b[j * n..(j + 1) * n]);
}
a[i * n + i] += n as f64;
}
a
}
#[test]
fn blocked_cholesky_solves_across_block_boundaries() {
for n in [1, 5, BLOCK, BLOCK + 3, 2 * BLOCK + 17] {
let a = spd(n, n as u64);
let mut l = a.clone();
cholesky_in_place(&mut l, n).unwrap();
let x: Vec<f64> = (0..2 * n).map(|i| (i as f64).sin()).collect();
let mut b = vec![0.0; 2 * n];
for i in 0..n {
for c in 0..2 {
b[i * 2 + c] = (0..n).map(|j| a[i * n + j] * x[j * 2 + c]).sum();
}
}
forward_solve(&l, n, &mut b, 2);
backward_solve(&l, n, &mut b, 2);
for (got, want) in b.iter().zip(&x) {
assert!((got - want).abs() < 1e-9, "n={n}: {got} vs {want}");
}
}
}
#[test]
fn pivoted_cholesky_finds_the_rank_of_duplicated_columns() {
let v = [1.0, 2.0, 0.5];
let w = [0.0, 1.0, -1.0];
let rows = [v, w, v];
let mut a = vec![0.0; 9];
for i in 0..3 {
for j in 0..3 {
a[i * 3 + j] = dot(&rows[i], &rows[j]);
}
}
let p = pivoted_cholesky(&a, 3, 1e-12);
assert_eq!(p.rank(), 2);
assert!(p.pivots.contains(&1));
}
}