use crate::debug::ResidKind;
use faer::Mat;
use pounce_common::types::Number;
const CULPRIT_WEIGHT_TOL: Number = 1e-3;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RankRow {
pub kind: ResidKind,
pub index: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct RankCulprit {
pub row: usize,
pub weight: Number,
}
#[derive(Clone, Debug)]
pub struct RankReport {
pub rows: Vec<RankRow>,
pub n_cols: usize,
pub singular_values: Vec<Number>,
pub tol: Number,
pub rank: usize,
pub cond: Number,
pub culprits: Vec<RankCulprit>,
}
impl RankReport {
pub fn n_rows(&self) -> usize {
self.rows.len()
}
pub fn deficiency(&self) -> usize {
self.rows.len().saturating_sub(self.rank)
}
pub fn is_rank_deficient(&self) -> bool {
self.rank < self.rows.len()
}
pub fn sigma_min(&self) -> Number {
self.singular_values.last().copied().unwrap_or(0.0)
}
pub fn sigma_max(&self) -> Number {
self.singular_values.first().copied().unwrap_or(0.0)
}
}
pub(crate) fn svd_rank(
m: usize,
n: usize,
dense_row_major: &[Number],
rows: Vec<RankRow>,
) -> Option<RankReport> {
if m == 0 || n == 0 {
return None;
}
debug_assert_eq!(dense_row_major.len(), m * n, "dense buffer is not m*n");
debug_assert_eq!(rows.len(), m, "row-identity count must equal m");
let a = Mat::from_fn(m, n, |i, j| dense_row_major[i * n + j]);
let svd = a.svd().ok()?;
let s: Vec<Number> = svd.S().column_vector().iter().copied().collect();
let smax = s.first().copied().unwrap_or(0.0);
let tol = smax * (m.max(n) as Number) * Number::EPSILON;
let rank = s.iter().filter(|&&sv| sv > tol).count();
let smin = s.last().copied().unwrap_or(0.0);
let cond = if smin > 0.0 {
smax / smin
} else {
Number::INFINITY
};
let u = svd.U();
let mut weights = vec![0.0_f64; m];
for k in rank..m {
let col = u.col(k);
for (i, &val) in col.iter().enumerate() {
weights[i] += val * val;
}
}
let mut culprits: Vec<RankCulprit> = weights
.iter()
.enumerate()
.filter(|(_, &w)| w > CULPRIT_WEIGHT_TOL)
.map(|(row, &w)| RankCulprit { row, weight: w })
.collect();
culprits.sort_by(|a, b| {
b.weight
.partial_cmp(&a.weight)
.unwrap_or(std::cmp::Ordering::Equal)
});
Some(RankReport {
rows,
n_cols: n,
singular_values: s,
tol,
rank,
cond,
culprits,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn eq_rows(m: usize) -> Vec<RankRow> {
(0..m)
.map(|index| RankRow {
kind: ResidKind::Eq,
index,
})
.collect()
}
#[test]
fn full_rank_block_has_no_culprits() {
let dense = vec![
1.0, 0.0, 0.0, 0.0, 1.0, 0.0,
];
let r = svd_rank(2, 3, &dense, eq_rows(2)).expect("svd");
assert_eq!(r.rank, 2);
assert!(!r.is_rank_deficient());
assert_eq!(r.deficiency(), 0);
assert!(r.culprits.is_empty());
assert!(r.cond.is_finite());
}
#[test]
fn duplicate_rows_are_flagged_as_culprits() {
let dense = vec![
1.0, 2.0, 3.0, 0.0, 1.0, 0.0, 1.0, 2.0, 3.0,
];
let r = svd_rank(3, 3, &dense, eq_rows(3)).expect("svd");
assert_eq!(r.rank, 2, "one redundant row");
assert!(r.is_rank_deficient());
assert_eq!(r.deficiency(), 1);
let flagged: Vec<usize> = r.culprits.iter().map(|c| c.row).collect();
assert!(
flagged.contains(&0),
"row 0 should be implicated: {flagged:?}"
);
assert!(
flagged.contains(&2),
"row 2 should be implicated: {flagged:?}"
);
assert!(!flagged.contains(&1), "row 1 is independent: {flagged:?}");
for c in &r.culprits {
assert!((c.weight - 0.5).abs() < 0.1, "weight {} ~ 0.5", c.weight);
}
}
#[test]
fn value_dependence_is_caught_numerically() {
let dense = vec![
1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 2.0,
];
let r = svd_rank(3, 3, &dense, eq_rows(3)).expect("svd");
assert_eq!(r.rank, 2, "value-dependent third row");
assert!(r.is_rank_deficient());
assert_eq!(r.deficiency(), 1);
assert!(
r.cond > 1e14 || r.cond.is_infinite(),
"ill-conditioned: cond={:.2e}",
r.cond
);
assert!(!r.culprits.is_empty(), "the dependency must be localized");
}
#[test]
fn rank_threshold_keeps_small_but_resolvable_singular_values() {
let dense = vec![
1.0,
0.0,
1.0, 0.0,
1.0,
1.0, 1.0,
1.0,
2.0 + 1e-9,
];
let r = svd_rank(3, 3, &dense, eq_rows(3)).expect("svd");
assert_eq!(r.rank, 3, "1e-9 perturbation is above the rank tol");
assert!(!r.is_rank_deficient());
assert!(r.culprits.is_empty());
}
#[test]
fn empty_block_returns_none() {
assert!(svd_rank(0, 3, &[], vec![]).is_none());
assert!(svd_rank(3, 0, &[], eq_rows(3)).is_none());
}
}