use crate::error::{LinalgError, LinalgResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
pub fn nuclear_norm(a: &ArrayView2<f64>) -> LinalgResult<f64> {
let s = singular_values(a)?;
Ok(s.iter().sum())
}
pub fn schatten_norm(a: &ArrayView2<f64>, p: f64) -> LinalgResult<f64> {
if p < 1.0 {
return Err(LinalgError::ValueError(format!(
"Schatten p-norm requires p >= 1, got p = {p}"
)));
}
let s = singular_values(a)?;
let sum_p: f64 = s.iter().map(|&sv| sv.powf(p)).sum();
Ok(sum_p.powf(1.0 / p))
}
pub fn ky_fan_norm(a: &ArrayView2<f64>, k: usize) -> LinalgResult<f64> {
if k == 0 {
return Err(LinalgError::ValueError("k must be >= 1".into()));
}
let mut s = singular_values(a)?;
s.iter_mut().for_each(|sv| {
if *sv < 0.0 {
*sv = 0.0;
}
});
let mut sorted: Vec<f64> = s.to_vec();
sorted.sort_by(|a, b| b.partial_cmp(a).expect("NaN in singular values"));
let k_eff = k.min(sorted.len());
Ok(sorted[..k_eff].iter().sum())
}
pub fn numerical_rank(a: &ArrayView2<f64>, tol: Option<f64>) -> LinalgResult<usize> {
let m = a.nrows();
let n = a.ncols();
let s = singular_values(a)?;
let sigma_max = s.iter().cloned().fold(0.0_f64, f64::max);
let threshold = tol.unwrap_or_else(|| {
let eps = f64::EPSILON;
eps * (m.max(n) as f64) * sigma_max
});
Ok(s.iter().filter(|&&sv| sv > threshold).count())
}
pub fn pinv(a: &ArrayView2<f64>, tol: Option<f64>) -> LinalgResult<Array2<f64>> {
let m = a.nrows();
let n = a.ncols();
let (u, s, vt) = crate::decomposition::svd(a, true, None)?;
let sigma_max = s.iter().cloned().fold(0.0_f64, f64::max);
let threshold = tol.unwrap_or_else(|| {
let eps = f64::EPSILON;
eps * (m.max(n) as f64) * sigma_max
});
let k = s.len();
let mut result = Array2::<f64>::zeros((n, m));
for i in 0..k {
if s[i] > threshold {
let inv_si = 1.0 / s[i];
let v_i = vt.row(i); let u_i = u.column(i); for r in 0..n {
for c in 0..m {
result[[r, c]] += inv_si * v_i[r] * u_i[c];
}
}
}
}
Ok(result)
}
pub fn cond(a: &ArrayView2<f64>, p: Option<f64>) -> LinalgResult<f64> {
let p_val = p.unwrap_or(2.0);
match p_val {
v if (v - 2.0).abs() < f64::EPSILON => {
let s = singular_values(a)?;
if s.is_empty() {
return Err(LinalgError::ValueError("Empty matrix".into()));
}
let s_max = s.iter().cloned().fold(0.0_f64, f64::max);
let s_min = s.iter().cloned().fold(f64::INFINITY, f64::min);
if s_min < f64::EPSILON * s_max {
Ok(f64::INFINITY)
} else {
Ok(s_max / s_min)
}
}
v if (v - 1.0).abs() < f64::EPSILON => {
let norm_a = matrix_norm_1(a);
let a_pinv = pinv(a, None)?;
let norm_pinv = matrix_norm_1(&a_pinv.view());
Ok(norm_a * norm_pinv)
}
v if v.is_infinite() && v > 0.0 => {
let norm_a = matrix_norm_inf(a);
let a_pinv = pinv(a, None)?;
let norm_pinv = matrix_norm_inf(&a_pinv.view());
Ok(norm_a * norm_pinv)
}
_ => Err(LinalgError::ValueError(format!(
"Unsupported p for cond: {p_val}"
))),
}
}
pub fn stable_rank(a: &ArrayView2<f64>) -> LinalgResult<f64> {
let s = singular_values(a)?;
if s.is_empty() {
return Err(LinalgError::ValueError(
"Empty matrix for stable_rank".into(),
));
}
let frob_sq: f64 = s.iter().map(|&sv| sv * sv).sum();
let spec_sq = s.iter().cloned().fold(0.0_f64, f64::max).powi(2);
if spec_sq < f64::EPSILON {
return Ok(0.0);
}
Ok(frob_sq / spec_sq)
}
pub fn incoherence(a: &ArrayView2<f64>) -> LinalgResult<f64> {
let m = a.nrows();
let (u, s, _) = crate::decomposition::svd(a, true, None)?;
let sigma_max = s.iter().cloned().fold(0.0_f64, f64::max);
let threshold = f64::EPSILON * (m.max(a.ncols()) as f64) * sigma_max;
let r = s.iter().filter(|&&sv| sv > threshold).count();
if r == 0 {
return Ok(1.0);
}
let mu_unnorm: f64 = (0..m)
.map(|i| (0..r).map(|j| u[[i, j]] * u[[i, j]]).sum::<f64>())
.fold(0.0_f64, f64::max);
Ok((m as f64 / r as f64) * mu_unnorm)
}
fn singular_values(a: &ArrayView2<f64>) -> LinalgResult<Array1<f64>> {
let (_, s, _) = crate::decomposition::svd(a, false, None)?;
Ok(s)
}
fn matrix_norm_1(a: &ArrayView2<f64>) -> f64 {
(0..a.ncols())
.map(|j| a.column(j).iter().map(|&v| v.abs()).sum::<f64>())
.fold(0.0_f64, f64::max)
}
fn matrix_norm_inf(a: &ArrayView2<f64>) -> f64 {
(0..a.nrows())
.map(|i| a.row(i).iter().map(|&v| v.abs()).sum::<f64>())
.fold(0.0_f64, f64::max)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{array, Array2};
#[test]
fn test_nuclear_norm_diagonal() {
let a = array![[3.0_f64, 0.0], [0.0, 4.0]];
let nn = nuclear_norm(&a.view()).expect("nuclear_norm");
assert!((nn - 7.0).abs() < 1e-10, "nuclear norm diagonal: {nn}");
}
#[test]
fn test_nuclear_norm_identity() {
let a = Array2::<f64>::eye(4);
let nn = nuclear_norm(&a.view()).expect("nuclear_norm eye");
assert!((nn - 4.0).abs() < 1e-10);
}
#[test]
fn test_nuclear_norm_lower_bound() {
let a = array![[1.0_f64, 2.0], [3.0, 4.0]];
let nn = nuclear_norm(&a.view()).expect("nuclear_norm");
let frob_sq: f64 = a.iter().map(|&v| v * v).sum();
let frob = frob_sq.sqrt();
let rank = numerical_rank(&a.view(), None).expect("rank") as f64;
let lower_bound = frob / rank.sqrt();
assert!(
nn >= lower_bound - 1e-10,
"nuclear_norm {nn} < lower bound {lower_bound}"
);
}
#[test]
fn test_schatten_p1_equals_nuclear() {
let a = array![[3.0_f64, 0.0], [0.0, 4.0]];
let s1 = schatten_norm(&a.view(), 1.0).expect("schatten p=1");
let nn = nuclear_norm(&a.view()).expect("nuclear_norm");
assert!((s1 - nn).abs() < 1e-10);
}
#[test]
fn test_schatten_p2_equals_frobenius() {
let a = array![[1.0_f64, 2.0], [3.0, 4.0]];
let s2 = schatten_norm(&a.view(), 2.0).expect("schatten p=2");
let frob_sq: f64 = a.iter().map(|&v| v * v).sum();
let frob = frob_sq.sqrt();
assert!((s2 - frob).abs() < 1e-10, "schatten 2 vs frobenius");
}
#[test]
fn test_schatten_invalid_p() {
let a = array![[1.0_f64, 0.0], [0.0, 1.0]];
assert!(schatten_norm(&a.view(), 0.5).is_err());
}
#[test]
fn test_ky_fan_k1_equals_spectral() {
let a = array![[3.0_f64, 0.0, 0.0], [0.0, 2.0, 0.0]];
let kf1 = ky_fan_norm(&a.view(), 1).expect("ky_fan k=1");
assert!((kf1 - 3.0).abs() < 1e-10);
}
#[test]
fn test_ky_fan_k2_equals_nuclear_for_rank2() {
let a = array![[3.0_f64, 0.0], [0.0, 2.0]];
let kf2 = ky_fan_norm(&a.view(), 2).expect("ky_fan k=2");
let nn = nuclear_norm(&a.view()).expect("nuclear_norm");
assert!((kf2 - nn).abs() < 1e-10);
}
#[test]
fn test_ky_fan_monotone() {
let a = array![[5.0_f64, 0.0, 0.0], [0.0, 3.0, 0.0], [0.0, 0.0, 1.0]];
let kf1 = ky_fan_norm(&a.view(), 1).expect("kf1");
let kf2 = ky_fan_norm(&a.view(), 2).expect("kf2");
let kf3 = ky_fan_norm(&a.view(), 3).expect("kf3");
assert!(kf1 <= kf2 && kf2 <= kf3);
}
#[test]
fn test_numerical_rank_full() {
let a = array![[1.0_f64, 0.0], [0.0, 1.0]];
let r = numerical_rank(&a.view(), None).expect("rank full");
assert_eq!(r, 2);
}
#[test]
fn test_numerical_rank_deficient() {
let a = array![[1.0_f64, 2.0, 3.0], [2.0, 4.0, 6.0],];
let r = numerical_rank(&a.view(), None).expect("rank deficient");
assert_eq!(r, 1, "rank should be 1, got {r}");
}
#[test]
fn test_numerical_rank_zero_matrix() {
let a = Array2::<f64>::zeros((3, 3));
let r = numerical_rank(&a.view(), None).expect("rank zero");
assert_eq!(r, 0);
}
#[test]
fn test_pinv_full_rank_square() {
let a = array![[2.0_f64, 1.0], [1.0, 3.0]];
let ai = pinv(&a.view(), None).expect("pinv square");
let prod = a.dot(&ai);
let eye2 = Array2::<f64>::eye(2);
let err: f64 = (&prod - &eye2).iter().map(|v| v.abs()).fold(0.0, f64::max);
assert!(err < 1e-10, "pinv * A not I: {err}");
}
#[test]
fn test_pinv_tall_matrix() {
let a = array![[1.0_f64, 0.0], [0.0, 1.0], [0.0, 0.0]];
let ai = pinv(&a.view(), None).expect("pinv tall");
let prod = ai.dot(&a);
let eye2 = Array2::<f64>::eye(2);
let err: f64 = (&prod - &eye2).iter().map(|v| v.abs()).fold(0.0, f64::max);
assert!(err < 1e-10, "A† A not I₂: {err}");
}
#[test]
fn test_pinv_rank_deficient() {
let a = array![[1.0_f64, 2.0], [2.0, 4.0]];
let ai = pinv(&a.view(), None).expect("pinv rank-def");
let recon = a.dot(&ai).dot(&a);
let err: f64 = (&recon - &a).iter().map(|v| v.abs()).fold(0.0, f64::max);
assert!(err < 1e-10, "pinv Moore-Penrose cond 1: {err}");
}
#[test]
fn test_cond_identity() {
let a = Array2::<f64>::eye(3);
let c = cond(&a.view(), None).expect("cond identity");
assert!((c - 1.0).abs() < 1e-10, "cond(I) should be 1");
}
#[test]
fn test_cond_diagonal() {
let a = array![[2.0_f64, 0.0], [0.0, 1.0]];
let c = cond(&a.view(), Some(2.0)).expect("cond diagonal");
assert!((c - 2.0).abs() < 1e-10, "cond diag: {c}");
}
#[test]
fn test_cond_singular() {
let a = array![[1.0_f64, 0.0], [0.0, 0.0]];
let c = cond(&a.view(), None).expect("cond singular");
assert!(c.is_infinite() || c > 1e10, "cond singular should be large");
}
#[test]
fn test_stable_rank_identity() {
let a = Array2::<f64>::eye(3);
let sr = stable_rank(&a.view()).expect("stable_rank eye");
assert!((sr - 3.0).abs() < 1e-10, "stable_rank(I₃) = 3, got {sr}");
}
#[test]
fn test_stable_rank_rank1() {
let a = array![[1.0_f64, 2.0], [2.0, 4.0]];
let sr = stable_rank(&a.view()).expect("stable_rank rank1");
assert!((sr - 1.0).abs() < 1e-10, "stable_rank rank-1 = 1, got {sr}");
}
#[test]
fn test_stable_rank_bounds() {
let a = array![[3.0_f64, 1.0], [1.0, 2.0]];
let sr = stable_rank(&a.view()).expect("stable_rank bounds");
assert!(sr >= 1.0 - 1e-10 && sr <= 2.0 + 1e-10);
}
#[test]
fn test_incoherence_identity() {
let a = Array2::<f64>::eye(3);
let mu = incoherence(&a.view()).expect("incoherence identity");
assert!((mu - 1.0).abs() < 1e-10, "incoherence(I) = 1, got {mu}");
}
#[test]
fn test_incoherence_range() {
let a = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0]];
let mu = incoherence(&a.view()).expect("incoherence range");
assert!(mu >= 1.0 - 1e-10, "incoherence must be >= 1, got {mu}");
}
}