use super::decomp::{apply_qt_to_vec, qr, svd};
use crate::error::EmlError;
fn cholesky_factor(a: &mut [f64], n: usize) -> Result<(), EmlError> {
let scale = (0..n).map(|k| a[k * n + k].abs()).fold(1.0_f64, f64::max);
for j in 0..n {
let mut sum = a[j * n + j];
for k in 0..j {
sum -= a[j * n + k] * a[j * n + k];
}
if sum <= 1e-14 * scale {
return Err(EmlError::NotSpd);
}
a[j * n + j] = sum.sqrt();
let diag_j = a[j * n + j];
for i in (j + 1)..n {
let mut s = a[i * n + j];
for k in 0..j {
s -= a[i * n + k] * a[j * n + k];
}
a[i * n + j] = s / diag_j;
}
}
Ok(())
}
fn cholesky_solve_with_factor(l: &[f64], b: &mut [f64], n: usize) {
for i in 0..n {
let mut s = b[i];
for k in 0..i {
s -= l[i * n + k] * b[k];
}
b[i] = s / l[i * n + i];
}
for i in (0..n).rev() {
let mut s = b[i];
for k in (i + 1)..n {
s -= l[k * n + i] * b[k];
}
b[i] = s / l[i * n + i];
}
}
pub fn solve_spd_cholesky(a: &mut [f64], b: &mut [f64], n: usize) -> Result<(), EmlError> {
cholesky_factor(a, n)?;
cholesky_solve_with_factor(a, b, n);
Ok(())
}
pub fn solve_lu(a: &mut [f64], b: &mut [f64], n: usize) -> Result<(), EmlError> {
for k in 0..n {
let mut max_val = a[k * n + k].abs();
let mut max_row = k;
for i in (k + 1)..n {
let v = a[i * n + k].abs();
if v > max_val {
max_val = v;
max_row = i;
}
}
if max_val < 1e-15 {
return Err(EmlError::SingularMatrix);
}
if max_row != k {
for j in 0..n {
a.swap(k * n + j, max_row * n + j);
}
b.swap(k, max_row);
}
let pivot = a[k * n + k];
for i in (k + 1)..n {
let factor = a[i * n + k] / pivot;
a[i * n + k] = factor;
for j in (k + 1)..n {
let updated = a[i * n + j] - factor * a[k * n + j];
a[i * n + j] = updated;
}
}
}
for i in 0..n {
let mut s = b[i];
for k in 0..i {
s -= a[i * n + k] * b[k];
}
b[i] = s;
}
for i in (0..n).rev() {
let mut s = b[i];
for k in (i + 1)..n {
s -= a[i * n + k] * b[k];
}
b[i] = s / a[i * n + i];
}
Ok(())
}
pub fn solve_normal_equations(a: &[f64], b: &mut [f64], n: usize) -> Result<(), EmlError> {
let mut a_chol = a.to_vec();
match solve_spd_cholesky(&mut a_chol, b, n) {
Ok(()) => Ok(()),
Err(EmlError::NotSpd) => {
let mut a_lu = a.to_vec();
solve_lu(&mut a_lu, b, n)
}
Err(e) => Err(e),
}
}
pub fn invert_spd(a: &[f64], n: usize) -> Result<Vec<f64>, EmlError> {
let mut l = a.to_vec();
cholesky_factor(&mut l, n)?;
let mut inv = vec![0.0_f64; n * n];
for j in 0..n {
let mut col = vec![0.0_f64; n];
col[j] = 1.0_f64;
cholesky_solve_with_factor(&l, &mut col, n);
for i in 0..n {
inv[i * n + j] = col[i];
}
}
Ok(inv)
}
pub(crate) fn back_sub_upper_from_qr(data: &[f64], b: &[f64], n: usize) -> Option<Vec<f64>> {
let mut x = b[..n].to_vec();
for j in (0..n).rev() {
let r_jj = data[j * n + j];
if r_jj.abs() < 1e-15 {
return None;
}
x[j] /= r_jj;
for i in 0..j {
x[i] -= data[i * n + j] * x[j];
}
}
Some(x)
}
pub fn solve_least_squares(a: &[f64], b: &[f64], m: usize, n: usize) -> Result<Vec<f64>, EmlError> {
if a.len() != m * n {
return Err(EmlError::DimensionMismatch(m * n, a.len()));
}
if b.len() != m {
return Err(EmlError::DimensionMismatch(m, b.len()));
}
let factors = qr(a, m, n)?;
let mut rhs = b.to_vec();
apply_qt_to_vec(&factors, &mut rhs);
back_sub_upper_from_qr(&factors.data, &rhs, n).ok_or(EmlError::SingularMatrix)
}
pub fn pinv(a: &[f64], m: usize, n: usize, rcond: Option<f64>) -> Result<Vec<f64>, EmlError> {
if a.len() != m * n {
return Err(EmlError::DimensionMismatch(m * n, a.len()));
}
let svd_r = svd(a, m, n)?;
let s0 = svd_r.s.first().copied().unwrap_or(0.0);
let thresh = rcond.unwrap_or(1e-12) * s0;
let mut result = vec![0.0f64; n * m];
for j in 0..svd_r.s.len() {
if svd_r.s[j] <= thresh {
continue;
}
let inv_s = 1.0 / svd_r.s[j];
for i in 0..n {
let v_ij = svd_r.v[i * n + j];
for k in 0..m {
let u_kj = svd_r.u[k * m + j];
result[i * m + k] += inv_s * v_ij * u_kj;
}
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-10;
fn approx_eq(a: f64, b: f64, eps: f64) -> bool {
(a - b).abs() < eps
}
#[test]
fn test_cholesky_solve() {
let mut a = [4.0_f64, 2.0, 2.0, 3.0];
let mut b = [6.0_f64, 5.0];
solve_spd_cholesky(&mut a, &mut b, 2).expect("Cholesky solve should succeed");
assert!(approx_eq(b[0], 1.0, EPS), "x[0] = {}", b[0]);
assert!(approx_eq(b[1], 1.0, EPS), "x[1] = {}", b[1]);
}
#[test]
fn test_lu_solve() {
let mut a = [4.0_f64, 2.0, 2.0, 3.0];
let mut b = [6.0_f64, 5.0];
solve_lu(&mut a, &mut b, 2).expect("LU solve should succeed");
assert!(approx_eq(b[0], 1.0, EPS), "x[0] = {}", b[0]);
assert!(approx_eq(b[1], 1.0, EPS), "x[1] = {}", b[1]);
}
#[test]
fn test_solve_normal_equations_round_trip() {
use crate::linalg::builders::{jtj, jtr};
let j = [1.0_f64, 0.0, 0.0, 1.0, 1.0, 1.0];
let y = [1.0_f64, 1.0, 2.0];
let a = jtj(&j, 3, 2, 0.0);
let mut b = jtr(&j, &y, 3, 2);
solve_normal_equations(&a, &mut b, 2).expect("solve_normal_equations should succeed");
assert!(approx_eq(b[0], 1.0, EPS), "x[0] = {}", b[0]);
assert!(approx_eq(b[1], 1.0, EPS), "x[1] = {}", b[1]);
}
#[test]
fn test_invert_spd() {
let a = [2.0_f64, 1.0, 1.0, 2.0];
let inv = invert_spd(&a, 2).expect("invert_spd should succeed");
assert!(approx_eq(inv[0], 2.0 / 3.0, EPS), "inv[0,0] = {}", inv[0]);
assert!(approx_eq(inv[1], -1.0 / 3.0, EPS), "inv[0,1] = {}", inv[1]);
assert!(approx_eq(inv[2], -1.0 / 3.0, EPS), "inv[1,0] = {}", inv[2]);
assert!(approx_eq(inv[3], 2.0 / 3.0, EPS), "inv[1,1] = {}", inv[3]);
let n = 2;
for i in 0..n {
for j in 0..n {
let mut s = 0.0_f64;
for k in 0..n {
s += a[i * n + k] * inv[k * n + j];
}
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
approx_eq(s, expected, 1e-10),
"A*A\u{207b}\u{00b9}[{i},{j}] = {s} (expected {expected})"
);
}
}
}
#[test]
fn test_not_spd() {
let mut a = [-1.0_f64, 0.0, 0.0, 1.0];
let mut b = [0.0_f64, 0.0];
let result = solve_spd_cholesky(&mut a, &mut b, 2);
assert_eq!(result, Err(EmlError::NotSpd));
}
#[test]
fn test_singular_matrix() {
let mut a = [1.0_f64, 2.0, 2.0, 4.0];
let mut b = [1.0_f64, 2.0];
let result = solve_lu(&mut a, &mut b, 2);
assert_eq!(result, Err(EmlError::SingularMatrix));
}
#[test]
fn test_3x3_cholesky() {
#[rustfmt::skip]
let mut a = [
4.0_f64, 2.0, 0.0,
2.0, 5.0, 2.0,
0.0, 2.0, 5.0,
];
let mut b = [6.0_f64, 9.0, 7.0];
solve_spd_cholesky(&mut a, &mut b, 3).expect("3x3 Cholesky should succeed");
assert!(approx_eq(b[0], 1.0, EPS), "x[0] = {}", b[0]);
assert!(approx_eq(b[1], 1.0, EPS), "x[1] = {}", b[1]);
assert!(approx_eq(b[2], 1.0, EPS), "x[2] = {}", b[2]);
}
#[test]
fn test_3x3_lu() {
#[rustfmt::skip]
let mut a = [
2.0_f64, 1.0, 1.0,
4.0, 3.0, 3.0,
8.0, 7.0, 9.0,
];
let mut b = [1.0_f64, 1.0, 1.0];
solve_lu(&mut a, &mut b, 3).expect("3x3 LU should succeed");
#[rustfmt::skip]
let a_orig = [
2.0_f64, 1.0, 1.0,
4.0, 3.0, 3.0,
8.0, 7.0, 9.0,
];
for i in 0..3 {
let mut s = 0.0_f64;
for j in 0..3 {
s += a_orig[i * 3 + j] * b[j];
}
let b_orig = 1.0_f64;
assert!(approx_eq(s, b_orig, 1e-9), "residual[{i}] = {s}");
}
}
}
#[cfg(test)]
mod tests_qr_svd {
use super::*;
fn matmul_rm(a: &[f64], p: usize, q: usize, b: &[f64], r: usize) -> Vec<f64> {
let mut c = vec![0.0f64; p * r];
for i in 0..p {
for k in 0..q {
for j in 0..r {
c[i * r + j] += a[i * q + k] * b[k * r + j];
}
}
}
c
}
fn transpose_rm(a: &[f64], p: usize, q: usize) -> Vec<f64> {
let mut t = vec![0.0f64; q * p];
for i in 0..p {
for j in 0..q {
t[j * p + i] = a[i * q + j];
}
}
t
}
#[test]
fn test_solve_least_squares_exact() {
let a = vec![1.0_f64, 0.0, 1.0, 1.0, 1.0, 2.0];
let b = vec![1.0, 2.0, 3.0];
let x = solve_least_squares(&a, &b, 3, 2).unwrap();
assert!((x[0] - 1.0).abs() < 1e-8, "x[0] = {}", x[0]);
assert!((x[1] - 1.0).abs() < 1e-8, "x[1] = {}", x[1]);
}
#[test]
fn test_pinv_moore_penrose_cond1() {
let a = vec![1.0_f64, 4.0, 2.0, 5.0, 3.0, 6.0];
let m = 3;
let n = 2;
let ap = pinv(&a, m, n, None).unwrap();
let aap = matmul_rm(&a, m, n, &ap, m);
let aapa = matmul_rm(&aap, m, m, &a, n);
for (v1, v2) in aapa.iter().zip(a.iter()) {
assert!((v1 - v2).abs() < 1e-6, "cond1 failed: {} != {}", v1, v2);
}
}
#[test]
fn test_pinv_hermitian_projectors() {
let a = vec![1.0_f64, 0.0, 0.0, 1.0, 0.0, 1.0];
let m = 3;
let n = 2;
let ap = pinv(&a, m, n, None).unwrap();
let aap = matmul_rm(&a, m, n, &ap, m);
let aap_t = transpose_rm(&aap, m, m);
for i in 0..m {
for j in 0..m {
assert!(
(aap[i * m + j] - aap_t[i * m + j]).abs() < 1e-6,
"(A A^+)[{i},{j}] is not symmetric"
);
}
}
}
#[test]
fn test_solve_least_squares_overdetermined() {
let a = vec![1.0_f64, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 1.0];
let b = vec![1.0, 2.0, 3.0, 4.0];
let x = solve_least_squares(&a, &b, 4, 2).unwrap();
assert!(x[0].is_finite(), "x[0] must be finite");
assert!(x[1].is_finite(), "x[1] must be finite");
}
}