use crate::{Error, Result};
#[derive(Debug, Clone)]
pub struct SquareMatrix {
pub n: usize,
pub data: Vec<f64>,
}
impl SquareMatrix {
pub fn zeros(n: usize) -> Self {
Self {
n,
data: vec![0.0; n * n],
}
}
#[inline]
pub fn get(&self, i: usize, j: usize) -> f64 {
self.data[i * self.n + j]
}
#[inline]
pub fn set(&mut self, i: usize, j: usize, v: f64) {
self.data[i * self.n + j] = v;
}
pub fn cholesky(&self) -> Result<SquareMatrix> {
let n = self.n;
let mut l = SquareMatrix::zeros(n);
for i in 0..n {
for j in 0..=i {
let mut s = self.get(i, j);
for k in 0..j {
s -= l.get(i, k) * l.get(j, k);
}
if i == j {
if s <= 0.0 || !s.is_finite() {
return Err(Error::Singular);
}
l.set(i, j, s.sqrt());
} else {
l.set(i, j, s / l.get(j, j));
}
}
}
Ok(l)
}
pub fn cholesky_solve(l: &SquareMatrix, b: &[f64]) -> Vec<f64> {
Self::cholesky_solve_leading(l, l.n, b)
}
#[allow(clippy::needless_range_loop)] pub fn cholesky_solve_leading(l: &SquareMatrix, k: usize, b: &[f64]) -> Vec<f64> {
debug_assert!(k <= l.n && b.len() >= k);
let mut y = vec![0.0; k];
for i in 0..k {
let mut s = b[i];
for j in 0..i {
s -= l.get(i, j) * y[j];
}
y[i] = s / l.get(i, i);
}
let mut x = vec![0.0; k];
for i in (0..k).rev() {
let mut s = y[i];
for j in i + 1..k {
s -= l.get(j, i) * x[j];
}
x[i] = s / l.get(i, i);
}
x
}
pub fn cholesky_inverse_diag(l: &SquareMatrix) -> Vec<f64> {
let n = l.n;
let mut diag = vec![0.0; n];
let mut e = vec![0.0; n];
for j in 0..n {
e.iter_mut().for_each(|v| *v = 0.0);
e[j] = 1.0;
let col = SquareMatrix::cholesky_solve(l, &e);
diag[j] = col[j];
}
diag
}
}
pub fn singular_values(a: &[f64], rows: usize, cols: usize) -> Vec<f64> {
debug_assert_eq!(a.len(), rows * cols);
let mut u: Vec<f64> = Vec::with_capacity(rows * cols);
for j in 0..cols {
for i in 0..rows {
u.push(a[i * cols + j]);
}
}
let tol = 1e-14;
for _sweep in 0..60 {
let mut rotated = false;
for i in 0..cols {
for j in i + 1..cols {
let (mut aii, mut ajj, mut aij) = (0.0f64, 0.0f64, 0.0f64);
for t in 0..rows {
let ui = u[i * rows + t];
let uj = u[j * rows + t];
aii += ui * ui;
ajj += uj * uj;
aij += ui * uj;
}
if aij.abs() <= tol * (aii * ajj).sqrt() || aii == 0.0 || ajj == 0.0 {
continue;
}
rotated = true;
let zeta = (ajj - aii) / (2.0 * aij);
let t_ = zeta.signum() / (zeta.abs() + (1.0 + zeta * zeta).sqrt());
let c = 1.0 / (1.0 + t_ * t_).sqrt();
let s = c * t_;
for t in 0..rows {
let ii = i * rows + t;
let jj = j * rows + t;
let (ui, uj) = (u[ii], u[jj]);
u[ii] = c * ui - s * uj;
u[jj] = s * ui + c * uj;
}
}
}
if !rotated {
break;
}
}
let mut sv: Vec<f64> = (0..cols)
.map(|j| {
(0..rows)
.map(|t| u[j * rows + t].powi(2))
.sum::<f64>()
.sqrt()
})
.collect();
sv.sort_unstable_by(|a, b| b.total_cmp(a));
sv
}
pub fn ols(x: &[f64], rows: usize, cols: usize, y: &[f64]) -> Result<(Vec<f64>, f64, Vec<f64>)> {
if rows <= cols {
return Err(Error::TooShort {
n: rows,
needed: cols + 1,
});
}
let mut xtx = SquareMatrix::zeros(cols);
let mut xty = vec![0.0; cols];
for t in 0..rows {
let row = &x[t * cols..(t + 1) * cols];
for i in 0..cols {
xty[i] += row[i] * y[t];
for j in 0..=i {
let v = xtx.get(i, j) + row[i] * row[j];
xtx.set(i, j, v);
xtx.set(j, i, v);
}
}
}
let l = xtx.cholesky()?;
let beta = SquareMatrix::cholesky_solve(&l, &xty);
let mut rss = 0.0;
for t in 0..rows {
let row = &x[t * cols..(t + 1) * cols];
let fit: f64 = row.iter().zip(&beta).map(|(a, b)| a * b).sum();
rss += (y[t] - fit).powi(2);
}
let s2 = rss / (rows - cols) as f64;
let dinv = SquareMatrix::cholesky_inverse_diag(&l);
let se = dinv.iter().map(|d| (s2 * d).sqrt()).collect();
Ok((beta, s2, se))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn singular_values_known_cases() {
let d = [3.0, 0.0, 0.0, 2.0];
let sv = singular_values(&d, 2, 2);
assert!((sv[0] - 3.0).abs() < 1e-12 && (sv[1] - 2.0).abs() < 1e-12);
let a = [1.0, 1.0, 0.0, 1.0];
let sv = singular_values(&a, 2, 2);
let s1 = ((3.0 + 5.0f64.sqrt()) / 2.0).sqrt();
let s2 = ((3.0 - 5.0f64.sqrt()) / 2.0).sqrt();
assert!((sv[0] - s1).abs() < 1e-12, "{sv:?}");
assert!((sv[1] - s2).abs() < 1e-12);
let b = [1.0, 2.0, -1.0, 0.5, 3.0, 1.0];
let sv = singular_values(&b, 3, 2);
let fro: f64 = b.iter().map(|v| v * v).sum();
assert!((sv.iter().map(|v| v * v).sum::<f64>() - fro).abs() < 1e-10);
let (g00, g01, g11) = (1.0f64 + 1.0 + 9.0, 2.0 - 0.5 + 3.0, 4.0f64 + 0.25 + 1.0);
let det = g00 * g11 - g01 * g01;
assert!(((sv[0] * sv[1]).powi(2) - det).abs() < 1e-9);
}
#[test]
fn cholesky_solve_2x2() {
let mut a = SquareMatrix::zeros(2);
a.set(0, 0, 4.0);
a.set(0, 1, 2.0);
a.set(1, 0, 2.0);
a.set(1, 1, 3.0);
let l = a.cholesky().unwrap();
let x = SquareMatrix::cholesky_solve(&l, &[2.0, 1.0]);
assert!((x[0] - 0.5).abs() < 1e-12 && x[1].abs() < 1e-12);
let d = SquareMatrix::cholesky_inverse_diag(&l);
assert!((d[0] - 3.0 / 8.0).abs() < 1e-12 && (d[1] - 0.5).abs() < 1e-12);
}
}