use super::{DimensionMismatch, abs, n_as_scalar};
use crate::scalar::{FloatTolerance, Scalar};
use crate::storage::Storage;
pub fn rank<S, T>(
a: &S,
rows: usize,
cols: usize,
scratch: &mut [T],
) -> Result<usize, DimensionMismatch>
where
S: Storage<Item = T>,
T: Scalar + FloatTolerance + PartialOrd,
{
let mut scale = T::zero();
for i in 0..a.len() {
let Some(&x) = a.get(i) else {
return Err(DimensionMismatch);
};
let x_abs = abs(x);
if x_abs > scale {
scale = x_abs;
}
}
let n: T = n_as_scalar(rows.max(cols));
let tolerance = n.mul(T::epsilon()).mul(scale);
rank_with_tolerance(a, rows, cols, scratch, tolerance)
}
pub fn rank_with_tolerance<S, T>(
a: &S,
rows: usize,
cols: usize,
scratch: &mut [T],
tolerance: T,
) -> Result<usize, DimensionMismatch>
where
S: Storage<Item = T>,
T: Scalar + PartialOrd,
{
let len = rows * cols;
if a.len() != len || scratch.len() != len {
return Err(DimensionMismatch);
}
for (i, slot) in scratch.iter_mut().enumerate() {
let Some(&x) = a.get(i) else {
return Err(DimensionMismatch);
};
*slot = x;
}
let mut pivot_row = 0;
for col in 0..cols {
if pivot_row >= rows {
break;
}
let mut best_row = pivot_row;
let mut best_abs = abs(scratch[pivot_row * cols + col]);
for r in (pivot_row + 1)..rows {
let candidate_abs = abs(scratch[r * cols + col]);
if candidate_abs > best_abs {
best_abs = candidate_abs;
best_row = r;
}
}
if best_abs <= tolerance {
continue;
}
if best_row != pivot_row {
for c in 0..cols {
scratch.swap(best_row * cols + c, pivot_row * cols + c);
}
}
let pivot_value = scratch[pivot_row * cols + col];
for r in (pivot_row + 1)..rows {
let factor = scratch[r * cols + col].div(pivot_value);
for c in col..cols {
let term = factor.mul(scratch[pivot_row * cols + c]);
scratch[r * cols + c] = scratch[r * cols + c].sub(term);
}
}
pivot_row += 1;
}
let rank = (0..rows)
.filter(|&r| (0..cols).any(|c| abs(scratch[r * cols + c]) > tolerance))
.count();
Ok(rank)
}
#[cfg(test)]
mod tests {
use super::{DimensionMismatch, rank, rank_with_tolerance};
use crate::storage::StaticStorage;
#[test]
fn rank_of_full_rank_matrix() {
let a = StaticStorage::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0]);
let mut scratch = [0.0; 9];
assert_eq!(rank(&a, 3, 3, &mut scratch), Ok(3));
}
#[test]
fn rank_of_rank_deficient_matrix() {
let a = StaticStorage::new([1.0, 2.0, 3.0, 2.0, 4.0, 6.0, 0.0, 1.0, 1.0]);
let mut scratch = [0.0; 9];
assert_eq!(rank(&a, 3, 3, &mut scratch), Ok(2));
}
#[test]
fn rank_of_non_square_matrix() {
let a = StaticStorage::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut scratch = [0.0; 6];
assert_eq!(rank(&a, 2, 3, &mut scratch), Ok(2));
}
#[test]
fn rank_mismatched_scratch_length_is_an_error_not_a_panic() {
let a = StaticStorage::new([1.0, 2.0, 3.0, 4.0]);
let mut scratch = [0.0; 3];
assert_eq!(rank(&a, 2, 2, &mut scratch), Err(DimensionMismatch));
}
#[test]
fn rank_with_explicit_tolerance_matches_the_default() {
let a = StaticStorage::new([1.0, 2.0, 3.0, 2.0, 4.0, 6.0, 0.0, 1.0, 1.0]);
let mut scratch = [0.0; 9];
assert_eq!(rank_with_tolerance(&a, 3, 3, &mut scratch, 1e-9), Ok(2));
}
#[test]
fn rank_default_tolerance_absorbs_rounding_noise_from_an_otherwise_dependent_row() {
let perturbed = 3.0 * 0.1 * 10.0;
assert_ne!(
perturbed, 3.0,
"test setup requires a genuine rounding residual"
);
let a = StaticStorage::new([1.0, 2.0, 3.0, 1.0, 2.0, perturbed]);
let mut scratch = [0.0; 6];
assert_eq!(rank(&a, 2, 3, &mut scratch), Ok(1));
}
#[test]
fn rank_explicit_tolerance_of_zero_is_not_robust_to_the_same_rounding_noise() {
let perturbed = 3.0 * 0.1 * 10.0;
let a = StaticStorage::new([1.0, 2.0, 3.0, 1.0, 2.0, perturbed]);
let mut scratch = [0.0; 6];
assert_eq!(rank_with_tolerance(&a, 2, 3, &mut scratch, 0.0), Ok(2));
}
#[test]
fn rank_of_zero_by_zero_matrix_is_zero_not_a_panic() {
let a: StaticStorage<f64, 0> = StaticStorage::new([]);
let mut scratch: [f64; 0] = [];
assert_eq!(rank(&a, 0, 0, &mut scratch), Ok(0));
}
#[test]
fn rank_pivots_to_the_largest_magnitude_entry_even_when_neither_is_zero() {
let a = StaticStorage::new([1.0, 2.0, 2.0, 1.0]);
let mut scratch = [0.0; 4];
assert_eq!(rank(&a, 2, 2, &mut scratch), Ok(2));
}
}