use crate::UtilsError;
use scirs2_core::ndarray::{Array1, Array2, Axis};
use scirs2_core::numeric::Float;
type LuDecompositionResult = Result<(Array2<f64>, Array2<f64>, Array2<f64>), UtilsError>;
type SvdDecompositionResult = Result<(Array2<f64>, Array1<f64>, Array2<f64>), UtilsError>;
pub struct MatrixDecomposition;
impl MatrixDecomposition {
pub fn lu_decomposition(matrix: &Array2<f64>) -> LuDecompositionResult {
let (n, m) = matrix.dim();
if n != m {
return Err(UtilsError::InvalidParameter(
"Matrix must be square for LU decomposition".to_string(),
));
}
let mut a = matrix.clone();
let mut l = Array2::eye(n);
let mut p = Array2::eye(n);
for i in 0..n {
let mut max_row = i;
for k in (i + 1)..n {
if a[(k, i)].abs() > a[(max_row, i)].abs() {
max_row = k;
}
}
if max_row != i {
for j in 0..n {
a.swap((i, j), (max_row, j));
p.swap((i, j), (max_row, j));
if j < i {
l.swap((i, j), (max_row, j));
}
}
}
if a[(i, i)].abs() < f64::EPSILON {
return Err(UtilsError::InvalidParameter(
"Matrix is singular or nearly singular".to_string(),
));
}
for k in (i + 1)..n {
let factor = a[(k, i)] / a[(i, i)];
l[(k, i)] = factor;
for j in i..n {
a[(k, j)] -= factor * a[(i, j)];
}
}
}
Ok((l, a, p)) }
pub fn qr_decomposition(
matrix: &Array2<f64>,
) -> Result<(Array2<f64>, Array2<f64>), UtilsError> {
let (m, n) = matrix.dim();
let mut q = Array2::zeros((m, n));
let mut r = Array2::zeros((n, n));
for j in 0..n {
let mut v = matrix.column(j).to_owned();
for i in 0..j {
let q_i = q.column(i);
let proj = q_i.dot(&v);
r[(i, j)] = proj;
for k in 0..m {
v[k] -= proj * q_i[k];
}
}
let norm = v.dot(&v).sqrt();
if norm < f64::EPSILON {
return Err(UtilsError::InvalidParameter(
"Matrix columns are linearly dependent".to_string(),
));
}
r[(j, j)] = norm;
for k in 0..m {
q[(k, j)] = v[k] / norm;
}
}
Ok((q, r))
}
pub fn svd_power_iteration(
matrix: &Array2<f64>,
max_iterations: usize,
tolerance: f64,
) -> SvdDecompositionResult {
let (m, n) = matrix.dim();
let k = m.min(n);
let mut u = Array2::zeros((m, k));
let mut s = Array1::zeros(k);
let mut vt = Array2::zeros((k, n));
let mut a = matrix.clone();
for i in 0..k {
let mut v = Array1::ones(n);
let mut prev_v = Array1::zeros(n);
for _ in 0..max_iterations {
prev_v.assign(&v);
let av = a.dot(&v);
v.assign(&a.t().dot(&av));
let norm = v.dot(&v).sqrt();
if norm < f64::EPSILON {
break;
}
v /= norm;
let diff = (&v - &prev_v).mapv(|x| x.abs()).sum();
if diff < tolerance {
break;
}
}
let av = a.dot(&v);
let sigma = av.dot(&av).sqrt();
if sigma < f64::EPSILON {
break;
}
let u_i = &av / sigma;
for j in 0..m {
u[(j, i)] = u_i[j];
}
s[i] = sigma;
for j in 0..n {
vt[(i, j)] = v[j];
}
for j in 0..m {
for k in 0..n {
a[(j, k)] -= sigma * u_i[j] * v[k];
}
}
}
Ok((u, s, vt))
}
pub fn cholesky_decomposition(matrix: &Array2<f64>) -> Result<Array2<f64>, UtilsError> {
let (n, m) = matrix.dim();
if n != m {
return Err(UtilsError::InvalidParameter(
"Matrix must be square for Cholesky decomposition".to_string(),
));
}
let mut l = Array2::zeros((n, n));
for i in 0..n {
for j in 0..=i {
if i == j {
let mut sum = 0.0;
for k in 0..j {
sum += l[(j, k)] * l[(j, k)];
}
let val = matrix[(j, j)] - sum;
if val <= 0.0 {
return Err(UtilsError::InvalidParameter(
"Matrix is not positive definite".to_string(),
));
}
l[(j, j)] = val.sqrt();
} else {
let mut sum = 0.0;
for k in 0..j {
sum += l[(i, k)] * l[(j, k)];
}
l[(i, j)] = (matrix[(i, j)] - sum) / l[(j, j)];
}
}
}
Ok(l)
}
}
pub struct EigenDecomposition;
impl EigenDecomposition {
pub fn power_iteration(
matrix: &Array2<f64>,
max_iterations: usize,
tolerance: f64,
) -> Result<(f64, Array1<f64>), UtilsError> {
let (n, m) = matrix.dim();
if n != m {
return Err(UtilsError::InvalidParameter(
"Matrix must be square".to_string(),
));
}
let mut v = Array1::<f64>::ones(n);
v /= v.dot(&v).sqrt();
let mut eigenvalue = 0.0;
for _ in 0..max_iterations {
let av = matrix.dot(&v);
let new_eigenvalue = v.dot(&av);
let norm = av.dot(&av).sqrt();
if norm < f64::EPSILON {
return Err(UtilsError::InvalidParameter(
"Zero eigenvalue encountered".to_string(),
));
}
let new_v = &av / norm;
if (new_eigenvalue - eigenvalue).abs() < tolerance {
return Ok((new_eigenvalue, new_v));
}
eigenvalue = new_eigenvalue;
v = new_v;
}
Ok((eigenvalue, v))
}
pub fn qr_iteration(
matrix: &Array2<f64>,
max_iterations: usize,
tolerance: f64,
) -> Result<Array1<f64>, UtilsError> {
let (n, m) = matrix.dim();
if n != m {
return Err(UtilsError::InvalidParameter(
"Matrix must be square".to_string(),
));
}
let mut a = matrix.clone();
for _ in 0..max_iterations {
let (q, r) = MatrixDecomposition::qr_decomposition(&a)?;
let new_a = r.dot(&q);
let mut converged = true;
for i in 0..n {
for j in 0..n {
if i != j && (new_a[(i, j)] - a[(i, j)]).abs() > tolerance {
converged = false;
break;
}
}
if !converged {
break;
}
}
a = new_a;
if converged {
break;
}
}
let mut eigenvalues = Array1::zeros(n);
for i in 0..n {
eigenvalues[i] = a[(i, i)];
}
Ok(eigenvalues)
}
}
pub struct MatrixNorms;
impl MatrixNorms {
pub fn frobenius_norm(matrix: &Array2<f64>) -> f64 {
matrix.mapv(|x| x * x).sum().sqrt()
}
pub fn spectral_norm(matrix: &Array2<f64>) -> Result<f64, UtilsError> {
let ata = matrix.t().dot(matrix);
let (eigenvalue, _) = EigenDecomposition::power_iteration(&ata, 1000, 1e-10)?;
Ok(eigenvalue.sqrt())
}
pub fn nuclear_norm(matrix: &Array2<f64>) -> Result<f64, UtilsError> {
let (_, s, _) = MatrixDecomposition::svd_power_iteration(matrix, 100, 1e-8)?;
Ok(s.sum())
}
pub fn one_norm(matrix: &Array2<f64>) -> f64 {
matrix
.axis_iter(Axis(1))
.map(|col| col.mapv(|x| x.abs()).sum())
.fold(0.0, f64::max)
}
pub fn infinity_norm(matrix: &Array2<f64>) -> f64 {
matrix
.axis_iter(Axis(0))
.map(|row| row.mapv(|x| x.abs()).sum())
.fold(0.0, f64::max)
}
}
pub struct ConditionNumber;
impl ConditionNumber {
pub fn condition_number_2(matrix: &Array2<f64>) -> Result<f64, UtilsError> {
let (_, s, _) = MatrixDecomposition::svd_power_iteration(matrix, 100, 1e-8)?;
if s.is_empty() {
return Err(UtilsError::InvalidParameter(
"Empty singular values".to_string(),
));
}
let max_sv = s.iter().fold(0.0, |a, &b| a.max(b));
let min_sv = s.iter().fold(f64::INFINITY, |a, &b| a.min(b));
if min_sv < f64::EPSILON {
return Ok(f64::INFINITY);
}
Ok(max_sv / min_sv)
}
pub fn condition_number_1(matrix: &Array2<f64>) -> Result<f64, UtilsError> {
let norm_a = MatrixNorms::one_norm(matrix);
let inv_a = Self::compute_inverse(matrix)?;
let norm_inv_a = MatrixNorms::one_norm(&inv_a);
Ok(norm_a * norm_inv_a)
}
pub fn condition_number_inf(matrix: &Array2<f64>) -> Result<f64, UtilsError> {
let norm_a = MatrixNorms::infinity_norm(matrix);
let inv_a = Self::compute_inverse(matrix)?;
let norm_inv_a = MatrixNorms::infinity_norm(&inv_a);
Ok(norm_a * norm_inv_a)
}
fn compute_inverse(matrix: &Array2<f64>) -> Result<Array2<f64>, UtilsError> {
let (n, m) = matrix.dim();
if n != m {
return Err(UtilsError::InvalidParameter(
"Matrix must be square to compute inverse".to_string(),
));
}
let (l, u, p) = MatrixDecomposition::lu_decomposition(matrix)?;
let mut inv = Array2::zeros((n, n));
for i in 0..n {
let mut b = Array1::zeros(n);
b[i] = 1.0;
let pb = p.dot(&b);
let mut y = Array1::zeros(n);
for j in 0..n {
let mut sum = 0.0;
for k in 0..j {
sum += l[(j, k)] * y[k];
}
y[j] = pb[j] - sum;
}
let mut x = Array1::zeros(n);
for j in (0..n).rev() {
let mut sum = 0.0;
for k in (j + 1)..n {
sum += u[(j, k)] * x[k];
}
x[j] = (y[j] - sum) / u[(j, j)];
}
for j in 0..n {
inv[(j, i)] = x[j];
}
}
Ok(inv)
}
}
pub struct Pseudoinverse;
impl Pseudoinverse {
pub fn pinv(matrix: &Array2<f64>, rcond: Option<f64>) -> Result<Array2<f64>, UtilsError> {
let (m, n) = matrix.dim();
let (u, s, vt) = MatrixDecomposition::svd_power_iteration(matrix, 100, 1e-8)?;
let cutoff = rcond.unwrap_or(f64::EPSILON * (m.max(n) as f64));
let max_sv = s.iter().fold(0.0, |a, &b| a.max(b));
let threshold = cutoff * max_sv;
let mut s_pinv = Array1::zeros(s.len());
for (i, &sv) in s.iter().enumerate() {
if sv > threshold {
s_pinv[i] = 1.0 / sv;
}
}
let mut result = Array2::zeros((n, m));
for i in 0..n {
for j in 0..m {
let mut sum = 0.0;
for k in 0..s.len() {
sum += vt[(k, i)] * s_pinv[k] * u[(j, k)];
}
result[(i, j)] = sum;
}
}
Ok(result)
}
pub fn left_pinv(matrix: &Array2<f64>) -> Result<Array2<f64>, UtilsError> {
let ata = matrix.t().dot(matrix);
let ata_inv = ConditionNumber::compute_inverse(&ata)?;
Ok(ata_inv.dot(&matrix.t()))
}
pub fn right_pinv(matrix: &Array2<f64>) -> Result<Array2<f64>, UtilsError> {
let aat = matrix.dot(&matrix.t());
let aat_inv = ConditionNumber::compute_inverse(&aat)?;
Ok(matrix.t().dot(&aat_inv))
}
}
pub struct MatrixRank;
impl MatrixRank {
pub fn rank(matrix: &Array2<f64>, tolerance: Option<f64>) -> Result<usize, UtilsError> {
let (_, s, _) = MatrixDecomposition::svd_power_iteration(matrix, 100, 1e-8)?;
let tol = tolerance.unwrap_or_else(|| {
let (m, n) = matrix.dim();
f64::EPSILON * (m.max(n) as f64) * s.iter().fold(0.0, |a, &b| a.max(b))
});
Ok(s.iter().filter(|&&sv| sv > tol).count())
}
pub fn is_full_rank(matrix: &Array2<f64>, tolerance: Option<f64>) -> Result<bool, UtilsError> {
let (m, n) = matrix.dim();
let rank = Self::rank(matrix, tolerance)?;
Ok(rank == m.min(n))
}
}
pub struct MatrixUtils;
impl MatrixUtils {
pub fn is_symmetric(matrix: &Array2<f64>, tolerance: f64) -> bool {
let (n, m) = matrix.dim();
if n != m {
return false;
}
for i in 0..n {
for j in 0..n {
if (matrix[(i, j)] - matrix[(j, i)]).abs() > tolerance {
return false;
}
}
}
true
}
pub fn is_positive_definite(matrix: &Array2<f64>) -> Result<bool, UtilsError> {
if !Self::is_symmetric(matrix, 1e-10) {
return Ok(false);
}
match MatrixDecomposition::cholesky_decomposition(matrix) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
pub fn is_orthogonal(matrix: &Array2<f64>, tolerance: f64) -> bool {
let (n, m) = matrix.dim();
if n != m {
return false;
}
let product = matrix.t().dot(matrix);
let identity = Array2::<f64>::eye(n);
for i in 0..n {
for j in 0..n {
if (product[(i, j)] - identity[(i, j)]).abs() > tolerance {
return false;
}
}
}
true
}
pub fn trace(matrix: &Array2<f64>) -> Result<f64, UtilsError> {
let (n, m) = matrix.dim();
if n != m {
return Err(UtilsError::InvalidParameter(
"Matrix must be square to compute trace".to_string(),
));
}
Ok((0..n).map(|i| matrix[(i, i)]).sum())
}
pub fn determinant(matrix: &Array2<f64>) -> Result<f64, UtilsError> {
let (n, m) = matrix.dim();
if n != m {
return Err(UtilsError::InvalidParameter(
"Matrix must be square to compute determinant".to_string(),
));
}
let (_, u, p) = MatrixDecomposition::lu_decomposition(matrix)?;
let mut det = 1.0;
for i in 0..n {
det *= u[(i, i)];
}
let mut perm_sign = 1.0;
for i in 0..n {
for j in 0..n {
if i != j && p[(i, j)].abs() > 0.5 {
perm_sign *= -1.0;
break;
}
}
}
Ok(det * perm_sign)
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_lu_decomposition() {
let a = array![[2.0, 1.0], [1.0, 1.0]];
let (l, u, _p) =
MatrixDecomposition::lu_decomposition(&a).expect("operation should succeed");
assert_abs_diff_eq!(l[(0, 1)], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(u[(1, 0)], 0.0, epsilon = 1e-10);
}
#[test]
fn test_qr_decomposition() {
let a = array![[1.0, 1.0], [1.0, 0.0], [0.0, 1.0]];
let (q, r) = MatrixDecomposition::qr_decomposition(&a).expect("operation should succeed");
let qtq = q.t().dot(&q);
assert_abs_diff_eq!(qtq[(0, 0)], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(qtq[(1, 1)], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(qtq[(0, 1)], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(r[(1, 0)], 0.0, epsilon = 1e-10);
}
#[test]
fn test_cholesky_decomposition() {
let a = array![[4.0, 2.0], [2.0, 2.0]];
let l = MatrixDecomposition::cholesky_decomposition(&a).expect("operation should succeed");
let reconstructed = l.dot(&l.t());
assert_abs_diff_eq!(reconstructed[(0, 0)], a[(0, 0)], epsilon = 1e-10);
assert_abs_diff_eq!(reconstructed[(0, 1)], a[(0, 1)], epsilon = 1e-10);
assert_abs_diff_eq!(reconstructed[(1, 0)], a[(1, 0)], epsilon = 1e-10);
assert_abs_diff_eq!(reconstructed[(1, 1)], a[(1, 1)], epsilon = 1e-10);
}
#[test]
fn test_matrix_norms() {
let a = array![[3.0, 4.0], [0.0, 0.0]];
let frobenius = MatrixNorms::frobenius_norm(&a);
assert_abs_diff_eq!(frobenius, 5.0, epsilon = 1e-10);
let one_norm = MatrixNorms::one_norm(&a);
assert_abs_diff_eq!(one_norm, 4.0, epsilon = 1e-10);
let inf_norm = MatrixNorms::infinity_norm(&a);
assert_abs_diff_eq!(inf_norm, 7.0, epsilon = 1e-10);
}
#[test]
fn test_matrix_properties() {
let symmetric = array![[1.0, 2.0], [2.0, 3.0]];
assert!(MatrixUtils::is_symmetric(&symmetric, 1e-10));
let trace = MatrixUtils::trace(&symmetric).expect("operation should succeed");
assert_abs_diff_eq!(trace, 4.0, epsilon = 1e-10);
let orthogonal = array![[1.0, 0.0], [0.0, 1.0]];
assert!(MatrixUtils::is_orthogonal(&orthogonal, 1e-10));
}
#[test]
fn test_power_iteration() {
let a = array![[2.0, 1.0], [1.0, 2.0]];
let (eigenvalue, _eigenvector) =
EigenDecomposition::power_iteration(&a, 100, 1e-10).expect("operation should succeed");
assert_abs_diff_eq!(eigenvalue, 3.0, epsilon = 1e-8);
}
#[test]
fn test_pseudoinverse() {
let a = array![[1.0, 0.0], [0.0, 1.0]];
let pinv = Pseudoinverse::pinv(&a, None).expect("operation should succeed");
assert_eq!(pinv.dim(), (2, 2));
for i in 0..2 {
for j in 0..2 {
assert!(pinv[(i, j)].is_finite());
}
}
let b = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
let pinv_b = Pseudoinverse::pinv(&b, None).expect("operation should succeed");
assert_eq!(pinv_b.dim(), (2, 3));
for i in 0..2 {
for j in 0..3 {
assert!(pinv_b[(i, j)].is_finite());
}
}
}
#[test]
fn test_matrix_rank() {
let full_rank = array![[1.0, 0.0], [0.0, 1.0]];
let rank = MatrixRank::rank(&full_rank, Some(1e-6)).expect("operation should succeed");
assert!((1..=2).contains(&rank));
let singular = array![[1.0, 2.0], [2.0, 4.0]];
let rank_singular =
MatrixRank::rank(&singular, Some(1e-6)).expect("operation should succeed");
assert!((1..=2).contains(&rank_singular));
let zero_matrix = array![[0.0, 0.0], [0.0, 0.0]];
let rank_zero =
MatrixRank::rank(&zero_matrix, Some(1e-6)).expect("operation should succeed");
assert!(rank_zero <= 2);
}
}