use crate::array::Array;
use crate::error::{NumRs2Error, Result};
use num_traits::Float;
use std::fmt::Debug;
pub struct StableDecompositions;
impl StableDecompositions {
pub fn qr_pivoted<T>(a: &Array<T>) -> Result<QRPivotedResult<T>>
where
T: Float + Clone + Debug,
{
let shape = a.shape();
if shape.len() != 2 {
return Err(NumRs2Error::DimensionMismatch(
"QR decomposition requires a 2D matrix".to_string(),
));
}
let m = shape[0];
let n = shape[1];
let min_mn = m.min(n);
let mut q = Array::eye(m, m, 0);
let mut r = a.clone();
let mut p: Vec<usize> = (0..n).collect();
let mut col_norms = Vec::with_capacity(n);
for j in 0..n {
let mut norm_sq = T::zero();
for i in 0..m {
let val = r.get(&[i, j])?;
norm_sq = norm_sq + val * val;
}
col_norms.push(norm_sq.sqrt());
}
for k in 0..min_mn {
let mut max_norm = T::zero();
let mut pivot_col = k;
for j in k..n {
if col_norms[j] > max_norm {
max_norm = col_norms[j];
pivot_col = j;
}
}
if pivot_col != k {
for i in 0..m {
let temp = r.get(&[i, k])?;
r.set(&[i, k], r.get(&[i, pivot_col])?)?;
r.set(&[i, pivot_col], temp)?;
}
p.swap(k, pivot_col);
col_norms.swap(k, pivot_col);
}
let x_k = Self::extract_column_slice(&r, k, k, m)?;
let (v, beta) = Self::householder_vector(&x_k)?;
for j in k..n {
let x_j = Self::extract_column_slice(&r, j, k, m)?;
let y_j = Self::apply_householder(&x_j, &v, beta)?;
for (idx, &val) in y_j.iter().enumerate() {
r.set(&[k + idx, j], val)?;
}
}
for j in 0..m {
let x_j = Self::extract_column_slice(&q, j, k, m)?;
let y_j = Self::apply_householder(&x_j, &v, beta)?;
for (idx, &val) in y_j.iter().enumerate() {
q.set(&[k + idx, j], val)?;
}
}
for j in (k + 1)..n {
let r_kj = r.get(&[k, j])?;
let old_norm = col_norms[j];
let new_norm_sq = old_norm * old_norm - r_kj * r_kj;
col_norms[j] = if new_norm_sq > T::zero() {
new_norm_sq.sqrt()
} else {
T::zero()
};
}
}
let mut r_diag_min = T::infinity();
let mut r_diag_max = T::zero();
for i in 0..min_mn {
let diag_val = num_traits::Float::abs(r.get(&[i, i])?);
r_diag_min = r_diag_min.min(diag_val);
r_diag_max = r_diag_max.max(diag_val);
}
let condition_number = if r_diag_min > T::zero() {
r_diag_max / r_diag_min
} else {
T::infinity()
};
let eps = T::epsilon();
let threshold = eps
* <T as num_traits::NumCast>::from(m.max(n)).unwrap_or_else(|| T::one())
* r_diag_max;
let mut rank = 0;
for i in 0..min_mn {
if num_traits::Float::abs(r.get(&[i, i])?) > threshold {
rank += 1;
}
}
Ok(QRPivotedResult {
q,
r,
p: Array::from_vec(p.iter().map(|&x| x as f64).collect()),
condition_number,
rank,
})
}
pub fn cholesky_stable<T>(a: &Array<T>) -> Result<CholeskyStableResult<T>>
where
T: Float + Clone + Debug,
{
let shape = a.shape();
if shape.len() != 2 || shape[0] != shape[1] {
return Err(NumRs2Error::DimensionMismatch(
"Cholesky decomposition requires a square matrix".to_string(),
));
}
let n = shape[0];
let mut l = Array::zeros(&[n, n]);
let mut is_positive_definite = true;
for i in 0..n {
let mut sum = T::zero();
for k in 0..i {
let l_ik = l.get(&[i, k])?;
sum = sum + l_ik * l_ik;
}
let a_ii = a.get(&[i, i])?;
let l_ii_sq = a_ii - sum;
if l_ii_sq <= T::zero() {
is_positive_definite = false;
break;
}
let l_ii = l_ii_sq.sqrt();
l.set(&[i, i], l_ii)?;
for j in (i + 1)..n {
let mut sum = T::zero();
for k in 0..i {
sum = sum + l.get(&[i, k])? * l.get(&[j, k])?;
}
let a_ji = a.get(&[j, i])?;
let l_ji = (a_ji - sum) / l_ii;
l.set(&[j, i], l_ji)?;
}
}
if !is_positive_definite {
return Self::ldlt_pivoted(a);
}
let mut l_diag_min = T::infinity();
let mut l_diag_max = T::zero();
for i in 0..n {
let diag_val = l.get(&[i, i])?;
l_diag_min = l_diag_min.min(diag_val);
l_diag_max = l_diag_max.max(diag_val);
}
let condition_number = if l_diag_min > T::zero() {
let ratio = l_diag_max / l_diag_min;
ratio * ratio } else {
T::infinity()
};
Ok(CholeskyStableResult {
l,
condition_number,
is_positive_definite: true,
pivoting_used: false,
p: None,
d: None,
})
}
fn ldlt_pivoted<T>(a: &Array<T>) -> Result<CholeskyStableResult<T>>
where
T: Float + Clone + Debug,
{
let shape = a.shape();
let n = shape[0];
let mut l = Array::eye(n, n, 0);
let mut d = Array::zeros(&[n, n]);
let mut p: Vec<usize> = (0..n).collect();
let mut a_work = a.clone();
for k in 0..n {
let mut max_val = T::zero();
let mut pivot_idx = k;
for i in k..n {
let abs_val = num_traits::Float::abs(a_work.get(&[i, k])?);
if abs_val > max_val {
max_val = abs_val;
pivot_idx = i;
}
}
if pivot_idx != k {
for j in 0..n {
let temp = a_work.get(&[k, j])?;
a_work.set(&[k, j], a_work.get(&[pivot_idx, j])?)?;
a_work.set(&[pivot_idx, j], temp)?;
let temp = a_work.get(&[j, k])?;
a_work.set(&[j, k], a_work.get(&[j, pivot_idx])?)?;
a_work.set(&[j, pivot_idx], temp)?;
}
p.swap(k, pivot_idx);
}
let d_kk = a_work.get(&[k, k])?;
d.set(&[k, k], d_kk)?;
if d_kk == T::zero() {
continue; }
for i in (k + 1)..n {
let l_ik = a_work.get(&[i, k])? / d_kk;
l.set(&[i, k], l_ik)?;
for j in (k + 1)..n {
let old_val = a_work.get(&[i, j])?;
let update = l_ik * a_work.get(&[k, j])?;
a_work.set(&[i, j], old_val - update)?;
}
}
}
let mut d_min = T::infinity();
let mut d_max = T::zero();
for i in 0..n {
let d_val = num_traits::Float::abs(d.get(&[i, i])?);
if d_val > T::zero() {
d_min = d_min.min(d_val);
d_max = d_max.max(d_val);
}
}
let condition_number = if d_min > T::zero() {
d_max / d_min
} else {
T::infinity()
};
Ok(CholeskyStableResult {
l,
condition_number,
is_positive_definite: false,
pivoting_used: true,
p: Some(Array::from_vec(p.iter().map(|&x| x as f64).collect())),
d: Some(d),
})
}
pub fn svd_stable<T>(a: &Array<T>) -> Result<SVDStableResult<T>>
where
T: Float + Clone + Debug + Send + Sync + 'static,
{
let shape = a.shape();
if shape.len() != 2 {
return Err(NumRs2Error::DimensionMismatch(
"SVD requires a 2D matrix".to_string(),
));
}
let m = shape[0];
let n = shape[1];
let min_mn = m.min(n);
if min_mn <= 4 {
return Self::svd_small_stable(a);
}
Self::svd_bidiagonal(a)
}
fn svd_small_stable<T>(a: &Array<T>) -> Result<SVDStableResult<T>>
where
T: Float + Clone + Debug + Send + Sync + 'static,
{
let shape = a.shape();
let m = shape[0];
let n = shape[1];
let min_mn = m.min(n);
let at = Self::transpose(a)?;
let ata = Self::matrix_multiply(&at, a)?;
let (eigenvalues, eigenvectors) = Self::symmetric_eigendecomposition(&ata)?;
let mut singular_values = Vec::with_capacity(eigenvalues.len());
for &lambda in &eigenvalues {
singular_values.push(if lambda >= T::zero() {
lambda.sqrt()
} else {
T::zero()
});
}
let mut indices: Vec<usize> = (0..singular_values.len()).collect();
indices.sort_by(|&i, &j| {
singular_values[j]
.partial_cmp(&singular_values[i])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut s_sorted = Vec::with_capacity(singular_values.len());
let mut vt_cols = Vec::with_capacity(n);
for &idx in &indices {
s_sorted.push(singular_values[idx]);
let mut col = Vec::with_capacity(n);
for i in 0..n {
col.push(eigenvectors.get(&[i, idx])?);
}
vt_cols.push(col);
}
let mut vt_data = Vec::with_capacity(n * n);
for row in vt_cols {
vt_data.extend(row);
}
let vt = Array::from_vec(vt_data).reshape(&[n, n]);
let mut u_data = Vec::with_capacity(m * min_mn);
for i in 0..m {
for j in 0..min_mn {
if s_sorted[j] > T::zero() {
let mut sum = T::zero();
for k in 0..n {
sum = sum + a.get(&[i, k])? * vt.get(&[j, k])? / s_sorted[j];
}
u_data.push(sum);
} else {
u_data.push(T::zero());
}
}
}
let u = Array::from_vec(u_data).reshape(&[m, min_mn]);
let s_max = s_sorted[0];
let s_min = if min_mn > 0 {
s_sorted[min_mn - 1]
} else {
s_sorted[0]
};
let condition_number = if s_min > T::zero() {
s_max / s_min
} else {
T::infinity()
};
let eps = T::epsilon();
let threshold =
eps * <T as num_traits::NumCast>::from(m.max(n)).unwrap_or_else(|| T::one()) * s_max;
let rank = s_sorted.iter().take_while(|&&s| s > threshold).count();
Ok(SVDStableResult {
u,
s: Array::from_vec(s_sorted),
vt,
condition_number,
rank,
})
}
fn svd_bidiagonal<T>(a: &Array<T>) -> Result<SVDStableResult<T>>
where
T: Float + Clone + Debug + Send + Sync + 'static,
{
Self::svd_small_stable(a)
}
pub fn symmetric_eigendecomposition<T>(a: &Array<T>) -> Result<(Vec<T>, Array<T>)>
where
T: Float + Clone + Debug,
{
let shape = a.shape();
if shape.len() != 2 || shape[0] != shape[1] {
return Err(NumRs2Error::DimensionMismatch(
"Eigendecomposition requires a square matrix".to_string(),
));
}
let n = shape[0];
if n <= 3 {
return Self::symmetric_eigen_small(a);
}
Self::symmetric_eigen_tridiagonal(a)
}
fn symmetric_eigen_small<T>(a: &Array<T>) -> Result<(Vec<T>, Array<T>)>
where
T: Float + Clone + Debug,
{
let n = a.shape()[0];
if n == 1 {
let eigenvalue = a.get(&[0, 0])?;
let eigenvector = Array::from_vec(vec![T::one()]).reshape(&[1, 1]);
return Ok((vec![eigenvalue], eigenvector));
}
if n == 2 {
let a11 = a.get(&[0, 0])?;
let a12 = a.get(&[0, 1])?;
let a22 = a.get(&[1, 1])?;
let trace = a11 + a22;
let det = a11 * a22 - a12 * a12;
let four = T::from(4.0).expect("Failed to convert 4.0 to type T");
let two = T::from(2.0).expect("Failed to convert 2.0 to type T");
let discriminant = (trace * trace - four * det).sqrt();
let lambda1 = (trace + discriminant) / two;
let lambda2 = (trace - discriminant) / two;
let mut eigenvectors = Array::zeros(&[2, 2]);
if num_traits::Float::abs(a12) > T::epsilon() {
let v1_x = a12;
let v1_y = lambda1 - a11;
let norm1 = (v1_x * v1_x + v1_y * v1_y).sqrt();
eigenvectors.set(&[0, 0], v1_x / norm1)?;
eigenvectors.set(&[1, 0], v1_y / norm1)?;
} else {
eigenvectors.set(&[0, 0], T::one())?;
eigenvectors.set(&[1, 0], T::zero())?;
}
if num_traits::Float::abs(a12) > T::epsilon() {
let v2_x = a12;
let v2_y = lambda2 - a11;
let norm2 = (v2_x * v2_x + v2_y * v2_y).sqrt();
eigenvectors.set(&[0, 1], v2_x / norm2)?;
eigenvectors.set(&[1, 1], v2_y / norm2)?;
} else {
eigenvectors.set(&[0, 1], T::zero())?;
eigenvectors.set(&[1, 1], T::one())?;
}
return Ok((vec![lambda1, lambda2], eigenvectors));
}
Err(NumRs2Error::ComputationError(
"3x3 eigendecomposition not yet implemented".to_string(),
))
}
fn symmetric_eigen_tridiagonal<T>(a: &Array<T>) -> Result<(Vec<T>, Array<T>)>
where
T: Float + Clone + Debug,
{
Self::symmetric_eigen_small(a)
}
fn householder_vector<T>(x: &[T]) -> Result<(Vec<T>, T)>
where
T: Float + Clone,
{
let n = x.len();
if n == 0 {
return Err(NumRs2Error::InvalidOperation("Empty vector".to_string()));
}
let x_norm = x
.iter()
.map(|&xi| xi * xi)
.fold(T::zero(), |acc, xi| acc + xi)
.sqrt();
if x_norm == T::zero() {
return Ok((vec![T::zero(); n], T::zero()));
}
let alpha = if x[0] >= T::zero() { -x_norm } else { x_norm };
let mut v = vec![T::zero(); n];
v[0] = x[0] - alpha;
v[1..n].copy_from_slice(&x[1..n]);
let v_norm_sq = v
.iter()
.map(|&vi| vi * vi)
.fold(T::zero(), |acc, vi| acc + vi);
if v_norm_sq == T::zero() {
return Ok((v, T::zero()));
}
let beta = T::from(2.0).expect("Failed to convert 2.0 to type T") / v_norm_sq;
Ok((v, beta))
}
fn apply_householder<T>(x: &[T], v: &[T], beta: T) -> Result<Vec<T>>
where
T: Float + Clone,
{
if x.len() != v.len() {
return Err(NumRs2Error::DimensionMismatch(
"Vector length mismatch".to_string(),
));
}
let dot_product = x
.iter()
.zip(v.iter())
.map(|(&xi, &vi)| xi * vi)
.fold(T::zero(), |acc, prod| acc + prod);
let mut result = Vec::with_capacity(x.len());
for (&xi, &vi) in x.iter().zip(v.iter()) {
result.push(xi - beta * dot_product * vi);
}
Ok(result)
}
fn extract_column_slice<T>(
matrix: &Array<T>,
col: usize,
start_row: usize,
end_row: usize,
) -> Result<Vec<T>>
where
T: Float + Clone,
{
let mut result = Vec::with_capacity(end_row - start_row);
for i in start_row..end_row {
result.push(matrix.get(&[i, col])?);
}
Ok(result)
}
fn transpose<T>(a: &Array<T>) -> Result<Array<T>>
where
T: Float + Clone,
{
let shape = a.shape();
if shape.len() != 2 {
return Err(NumRs2Error::DimensionMismatch(
"Transpose requires 2D matrix".to_string(),
));
}
let m = shape[0];
let n = shape[1];
let mut result = Array::zeros(&[n, m]);
for i in 0..m {
for j in 0..n {
result.set(&[j, i], a.get(&[i, j])?)?;
}
}
Ok(result)
}
fn matrix_multiply<T>(a: &Array<T>, b: &Array<T>) -> Result<Array<T>>
where
T: Float + Clone + Debug + Send + Sync + 'static,
{
let a_shape = a.shape();
let b_shape = b.shape();
if a_shape.len() != 2 || b_shape.len() != 2 || a_shape[1] != b_shape[0] {
return Err(NumRs2Error::DimensionMismatch(
"Invalid matrix multiplication dimensions".to_string(),
));
}
let m = a_shape[0];
let n = b_shape[1];
let k = a_shape[1];
let mut c = Array::zeros(&[m, n]);
for i in 0..m {
for j in 0..n {
let mut sum = T::zero();
for l in 0..k {
sum = sum + a.get(&[i, l])? * b.get(&[l, j])?;
}
c.set(&[i, j], sum)?;
}
}
Ok(c)
}
}
#[derive(Debug)]
pub struct QRPivotedResult<T: Clone> {
pub q: Array<T>,
pub r: Array<T>,
pub p: Array<f64>, pub condition_number: T,
pub rank: usize,
}
#[derive(Debug)]
pub struct CholeskyStableResult<T: Clone> {
pub l: Array<T>,
pub condition_number: T,
pub is_positive_definite: bool,
pub pivoting_used: bool,
pub p: Option<Array<f64>>, pub d: Option<Array<T>>, }
#[derive(Debug)]
pub struct SVDStableResult<T: Clone> {
pub u: Array<T>,
pub s: Array<T>,
pub vt: Array<T>,
pub condition_number: T,
pub rank: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_qr_pivoted() {
let a = Array::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).reshape(&[2, 3]);
let result = StableDecompositions::qr_pivoted(&a).expect("QR pivoted should succeed");
assert_eq!(result.q.shape(), vec![2, 2]);
assert_eq!(result.r.shape(), vec![2, 3]);
assert_eq!(result.p.shape(), vec![3]);
let qt = StableDecompositions::transpose(&result.q).expect("transpose should succeed");
let qtq = StableDecompositions::matrix_multiply(&qt, &result.q)
.expect("matrix multiply should succeed");
for i in 0..2 {
for j in 0..2 {
let expected = if i == j { 1.0 } else { 0.0 };
assert_relative_eq!(
qtq.get(&[i, j]).expect("valid index"),
expected,
epsilon = 1e-10
);
}
}
}
#[test]
fn test_cholesky_stable_positive_definite() {
let a = Array::from_vec(vec![4.0, 2.0, 2.0, 3.0]).reshape(&[2, 2]);
let result = StableDecompositions::cholesky_stable(&a).expect("Cholesky should succeed");
assert!(result.is_positive_definite);
assert!(!result.pivoting_used);
let lt = StableDecompositions::transpose(&result.l).expect("transpose should succeed");
let llt = StableDecompositions::matrix_multiply(&result.l, <)
.expect("matrix multiply should succeed");
for i in 0..2 {
for j in 0..2 {
assert_relative_eq!(
llt.get(&[i, j]).expect("valid index"),
a.get(&[i, j]).expect("valid index"),
epsilon = 1e-10
);
}
}
}
#[test]
fn test_symmetric_eigendecomposition_2x2() {
let a = Array::from_vec(vec![3.0, 1.0, 1.0, 3.0]).reshape(&[2, 2]);
let (eigenvalues, eigenvectors) = StableDecompositions::symmetric_eigendecomposition(&a)
.expect("eigendecomposition should succeed");
assert_eq!(eigenvalues.len(), 2);
assert_eq!(eigenvectors.shape(), vec![2, 2]);
let mut sorted_eigenvalues = eigenvalues.clone();
sorted_eigenvalues.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
assert_relative_eq!(sorted_eigenvalues[0], 4.0, epsilon = 1e-10);
assert_relative_eq!(sorted_eigenvalues[1], 2.0, epsilon = 1e-10);
}
#[test]
fn test_svd_stable_small() {
let a = Array::from_vec(vec![1.0, 2.0, 3.0, 4.0]).reshape(&[2, 2]);
let result = StableDecompositions::svd_stable(&a).expect("SVD should succeed");
assert_eq!(result.u.shape(), vec![2, 2]);
assert_eq!(result.s.shape(), vec![2]);
assert_eq!(result.vt.shape(), vec![2, 2]);
let s_data = result.s.to_vec();
assert!(s_data[0] >= s_data[1]);
assert!(s_data[1] >= 0.0);
}
#[test]
fn test_householder_vector() {
let x = vec![1.0, 2.0, 3.0];
let (v, beta) =
StableDecompositions::householder_vector(&x).expect("householder should succeed");
assert_eq!(v.len(), 3);
assert!(beta >= 0.0);
let result = StableDecompositions::apply_householder(&x, &v, beta)
.expect("apply householder should succeed");
let x_norm = (1.0 + 4.0 + 9.0_f64).sqrt();
assert_relative_eq!(result[0].abs(), x_norm, epsilon = 1e-10);
assert_relative_eq!(result[1], 0.0, epsilon = 1e-10);
assert_relative_eq!(result[2], 0.0, epsilon = 1e-10);
}
}