use crate::utils::gemm::gemm;
use faer::{Accum, Mat, MatMut, MatRef, Par};
use num_traits::ToPrimitive;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rayon::prelude::*;
use rustc_hash::FxHashMap;
pub use bixverse_sparse::sparse::*;
use crate::core::math::pca_svd::SvdResults;
use crate::core::math::vector_helpers::sum_sq_f64;
use crate::prelude::*;
use crate::utils::faer_parallelism;
use crate::utils::simd::{sum_squared_dev_widen_simd_f32, sum_widen_simd_f32};
pub trait SparseFromDense<T> {
fn from_dense_matrix(mat: MatRef<T>, format: CompressedSparseFormat) -> Self;
}
impl<T, U> SparseFromDense<T> for CompressedSparseData2<T, U>
where
T: BixverseFloat + BixverseNumeric,
U: BixverseNumeric,
{
fn from_dense_matrix(mat: MatRef<T>, format: CompressedSparseFormat) -> Self {
Self::from_entry_fn(mat.nrows(), mat.ncols(), format, |i, j| mat[(i, j)])
}
}
const MATMUL_DENSE_ROW_BLOCK: usize = 256;
pub fn csr_sparse_matmul_dense<T>(
a: &CompressedSparseData2<T>,
b: &CompressedSparseData2<T>,
) -> Result<Mat<T>, CoreErrors>
where
T: BixverseFloat + Send + Sync + Default,
{
require_matmul_operands(a, b)?;
let n_rows = a.nrows();
let n_cols = b.ncols();
let m_indptr = &a.indptr;
let m_indices = &a.indices;
let m_data = &a.data;
let g_indptr = &b.indptr;
let g_indices = &b.indices;
let g_data = &b.data;
let mut out = Mat::<T>::zeros(n_rows, n_cols);
for block_start in (0..n_rows).step_by(MATMUL_DENSE_ROW_BLOCK) {
let rows = MATMUL_DENSE_ROW_BLOCK.min(n_rows - block_start);
let mut tile = vec![T::zero(); rows * n_cols];
tile.par_chunks_mut(n_cols.max(1))
.take(rows)
.enumerate()
.for_each(|(r, row)| {
let i = block_start + r;
for p in m_indptr[i] as usize..m_indptr[i + 1] as usize {
let j = m_indices[p] as usize;
let w = m_data[p];
let gs = g_indptr[j] as usize;
let ge = g_indptr[j + 1] as usize;
for q in gs..ge {
let t = g_indices[q] as usize;
row[t] += w * g_data[q];
}
}
});
out.par_col_iter_mut().enumerate().for_each(|(j, mut col)| {
for r in 0..rows {
col[block_start + r] = tile[r * n_cols + j];
}
});
}
Ok(out)
}
pub fn csr_matmul_dense_block<T>(
a: &CompressedSparseData2<T>,
block: &[T],
width: usize,
out: &mut [T],
) -> Result<(), CoreErrors>
where
T: BixverseFloat + BixverseSimd,
{
require_csr(a)?;
let (n_rows, n_cols) = a.shape();
if width == 0 || block.len() != n_cols * width || out.len() != n_rows * width {
return Err(CoreErrors::SparseMatrixMultiplication {
n_col_a: n_cols,
n_row_b: block.len().checked_div(width).unwrap_or(0),
});
}
let indptr = &a.indptr;
let indices = &a.indices;
let data = &a.data;
out.par_chunks_mut(width)
.enumerate()
.for_each(|(i, out_row)| {
out_row.fill(T::zero());
for p in indptr[i] as usize..indptr[i + 1] as usize {
let j = indices[p] as usize;
T::bxv_axpy_simd(out_row, data[p], &block[j * width..(j + 1) * width]);
}
});
Ok(())
}
const PAR_COLUMN_STATS_MIN_NNZ: usize = 1 << 20;
fn csc_layer<T>(csc: &CompressedSparseData2<T>, use_second_layer: bool) -> Result<&[T], CoreErrors>
where
T: BixverseNumeric,
{
require_csc(csc)?;
if use_second_layer {
Ok(csc
.data_2
.as_ref()
.ok_or(CoreErrors::Data2NotAvailable)?
.as_slice())
} else {
Ok(csc.data.as_slice())
}
}
fn collect_per_column<T, F>(ncols: usize, nnz: usize, stat: F) -> Vec<T>
where
T: Send,
F: Fn(usize) -> T + Sync + Send,
{
if nnz >= PAR_COLUMN_STATS_MIN_NNZ {
(0..ncols).into_par_iter().map(stat).collect()
} else {
(0..ncols).map(stat).collect()
}
}
pub fn sparse_col_means_csc<T>(
csc: &CompressedSparseData2<T>,
use_second_layer: bool,
) -> Result<Vec<T>, CoreErrors>
where
T: BixverseFloat + BixverseSimd,
{
let active_data = csc_layer(csc, use_second_layer)?;
let (nrows, ncols) = csc.shape();
let nrows_t = T::from_usize(nrows).unwrap();
Ok(collect_per_column(ncols, active_data.len(), |j| {
let start = csc.indptr[j] as usize;
let end = csc.indptr[j + 1] as usize;
T::bxv_sum(&active_data[start..end]) / nrows_t
}))
}
pub fn sparse_col_sds_csc<T>(
csc: &CompressedSparseData2<T>,
use_second_layer: bool,
) -> Result<Vec<T>, CoreErrors>
where
T: BixverseFloat + BixverseSimd,
{
let active_data = csc_layer(csc, use_second_layer)?;
let (nrows, ncols) = csc.shape();
let nrows_t = T::from_usize(nrows).unwrap();
let denom = nrows_t - T::one();
Ok(collect_per_column(ncols, active_data.len(), |j| {
let start = csc.indptr[j] as usize;
let end = csc.indptr[j + 1] as usize;
let col_slice = &active_data[start..end];
let implicit_zeros = T::from_usize(nrows - (end - start)).unwrap();
let mean = T::bxv_sum(col_slice) / nrows_t;
let ssd_nonzero = T::bxv_sum_squared_deviation(col_slice, mean);
let ssd_total = ssd_nonzero + implicit_zeros * mean * mean;
(ssd_total / denom).sqrt()
}))
}
pub fn frobenius_norm<T>(mat: &CompressedSparseData2<T>) -> f32
where
T: BixverseNumeric + ToPrimitive,
{
sum_sq_f64(&mat.data).sqrt() as f32
}
pub fn frobenius_norm_sq_f64<T>(mat: &CompressedSparseData2<T>) -> f64
where
T: BixverseNumeric + ToPrimitive,
{
sum_sq_f64(&mat.data)
}
pub fn count_zeroes<T>(mat: &MatRef<T>) -> (usize, Vec<usize>, Vec<usize>)
where
T: BixverseFloat,
{
let (nrow, ncol) = mat.shape();
let mut total_zeroes = 0_usize;
let mut row_zeroes = vec![0_usize; nrow];
let mut col_zeroes = vec![0_usize; ncol];
let zero = T::zero();
for j in 0..ncol {
for i in 0..nrow {
let val = unsafe { mat.get_unchecked(i, j) };
if *val == zero {
total_zeroes += 1;
row_zeroes[i] += 1;
col_zeroes[j] += 1;
}
}
}
(total_zeroes, row_zeroes, col_zeroes)
}
const SPARSE_COR_SD_EPS: f64 = 1e-8;
#[derive(Clone, Debug)]
pub struct SparseColMoments {
pub indices: Vec<u32>,
pub values: Vec<f32>,
pub mean: f64,
pub sd: f64,
}
fn shifted_ranks_sparse(values: &[f32], n_rows: usize) -> Vec<f32> {
let n_stored = values.len();
let n_implicit = n_rows.saturating_sub(n_stored);
let n_neg = values.iter().filter(|&&v| v < 0.0).count();
let n_stored_zero = values.iter().filter(|&&v| v == 0.0).count();
let zero_block = n_stored_zero + n_implicit;
let r_zero = n_neg as f64 + (zero_block as f64 + 1.0) / 2.0;
let mut order: Vec<u32> = (0..n_stored as u32).collect();
order.sort_unstable_by(|&a, &b| {
values[a as usize]
.partial_cmp(&values[b as usize])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut ranks = vec![0_f32; n_stored];
let mut i = 0;
while i < n_stored {
let value = values[order[i] as usize];
let start = i;
while i < n_stored && values[order[i] as usize] == value {
i += 1;
}
let avg_rank = if value == 0.0 {
r_zero
} else {
let offset = if value < 0.0 { 0 } else { n_implicit };
(start + i + 1 + 2 * offset) as f64 / 2.0
};
let shifted = (avg_rank - r_zero) as f32;
for &slot in &order[start..i] {
ranks[slot as usize] = shifted;
}
}
ranks
}
pub fn sparse_col_moments(
indices: &[u32],
values: &[f32],
n_rows: usize,
spearman: bool,
) -> SparseColMoments {
let stored: Vec<f32> = if spearman {
shifted_ranks_sparse(values, n_rows)
} else {
values.to_vec()
};
let n = n_rows as f64;
let mean = sum_widen_simd_f32(&stored) / n;
let ss_stored = sum_squared_dev_widen_simd_f32(&stored, mean);
let ss = ss_stored + n_rows.saturating_sub(stored.len()) as f64 * mean * mean;
let sd = if n_rows < 2 {
0.0
} else {
(ss / (n - 1.0)).sqrt()
};
SparseColMoments {
indices: indices.to_vec(),
values: stored,
mean,
sd,
}
}
pub fn sparse_pairwise_correlations(
moments: &[SparseColMoments],
pairs: &[(usize, usize)],
n_rows: usize,
) -> Vec<f32> {
let mut grouped: FxHashMap<usize, Vec<(usize, usize)>> = FxHashMap::default();
for (slot, &(a, b)) in pairs.iter().enumerate() {
grouped.entry(a).or_default().push((slot, b));
}
let groups: Vec<(usize, Vec<(usize, usize)>)> = grouped.into_iter().collect();
let n = n_rows as f64;
let denom = n - 1.0;
let solved: Vec<Vec<(usize, f32)>> = groups
.par_iter()
.map_init(
|| vec![0_f32; n_rows],
|scratch, (first, partners)| {
let a = &moments[*first];
for (&idx, &value) in a.indices.iter().zip(a.values.iter()) {
scratch[idx as usize] = value;
}
let out = partners
.iter()
.map(|&(slot, second)| {
let b = &moments[second];
if a.sd < SPARSE_COR_SD_EPS || b.sd < SPARSE_COR_SD_EPS {
return (slot, 0_f32);
}
let mut cross = 0_f64;
for (&idx, &value) in b.indices.iter().zip(b.values.iter()) {
cross += scratch[idx as usize] as f64 * value as f64;
}
let cov = cross - n * a.mean * b.mean;
let cor = cov / (denom * a.sd * b.sd);
(slot, (cor as f32).clamp(-1_f32, 1_f32))
})
.collect();
for &idx in &a.indices {
scratch[idx as usize] = 0_f32;
}
out
},
)
.collect();
let mut res = vec![0_f32; pairs.len()];
for group in solved {
for (slot, value) in group {
res[slot] = value;
}
}
res
}
const LANCZOS_RESTART_GUARD: usize = 5;
const LANCZOS_ORTHO_REFINE_RATIO: f64 = 0.707;
const LANCZOS_BREAKDOWN_RATIO: f64 = 1e-12;
#[derive(Clone, Copy, Debug)]
pub struct LanczosParams {
pub basis_size: Option<usize>,
pub max_restarts: usize,
pub tol: f64,
}
impl LanczosParams {
pub fn new() -> Self {
Self {
basis_size: None,
max_restarts: 64,
tol: 1e-8,
}
}
fn resolve_basis(&self, n_components: usize, n: usize) -> usize {
self.basis_size
.unwrap_or_else(|| (n_components * 2 + 10).max(n_components + 20))
.max(n_components + 1)
.min(n)
.max(1)
}
}
impl Default for LanczosParams {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct LanczosResult {
pub eigenvalues: Vec<f64>,
pub eigenvectors: Vec<Vec<f32>>,
pub residual: f64,
pub norm_estimate: f64,
pub converged: bool,
pub restarts: usize,
}
#[derive(Clone, Debug)]
pub struct RitzPairs {
pub values: Vec<f64>,
pub vectors: Vec<f64>,
pub residual: f64,
pub norm_estimate: f64,
pub converged: bool,
pub restarts: usize,
}
fn dot<T>(a: &[T], b: &[T]) -> T
where
T: BixverseSimd,
{
assert_same_len!(a, b);
T::bxv_dot_simd(a, b)
}
fn norm<T>(v: &[T]) -> T
where
T: BixverseSimd + BixverseFloat,
{
let dot = dot(v, v);
dot.sqrt()
}
fn normalise<T>(v: &mut [T])
where
T: BixverseSimd + BixverseFloat,
{
let n = norm(v);
v.par_iter_mut().for_each(|x| *x /= n);
}
fn lanczos_thick_restart<M>(
n: usize,
n_components: usize,
seed: u64,
params: LanczosParams,
matvec: M,
) -> Result<RitzPairs, CoreErrors>
where
M: Fn(&[f64], &mut [f64]),
{
if n_components == 0 || n == 0 {
return Err(CoreErrors::MustBePositive(
"n_components and the matrix dimension".to_string(),
));
}
let n_keep = n_components.min(n);
let m = params.resolve_basis(n_keep, n);
let mut basis = vec![0.0f64; m * n];
let mut h = Mat::<f64>::zeros(m, m);
let mut w = vec![0.0f64; n];
let mut coeffs = vec![0.0f64; m];
let mut rng = StdRng::seed_from_u64(seed);
for x in basis[..n].iter_mut() {
*x = rng.random::<f64>() - 0.5;
}
normalise(&mut basis[..n]);
let mut locked = 0usize;
let mut ritz_values: Vec<f64> = Vec::new();
let mut ritz_coeffs = Mat::<f64>::zeros(0, 0);
let mut m_final = 0usize;
let mut norm_estimate = 0.0f64;
let mut residual = f64::INFINITY;
let mut converged = false;
let mut restarts = 0usize;
let budget = params.max_restarts.max(1);
for cycle in 0..budget {
restarts = cycle + 1;
let mut m_eff = m;
let mut last_beta = 0.0f64;
for j in locked..m {
matvec(&basis[j * n..(j + 1) * n], &mut w);
let width = j + 1;
basis_project(&basis, n, width, &w, &mut coeffs[..width]);
for i in 0..width {
h[(i, j)] = coeffs[i];
h[(j, i)] = coeffs[i];
}
let before = norm(&w);
norm_estimate = norm_estimate.max(before);
basis_subtract(&basis, n, width, &coeffs[..width], &mut w);
last_beta = norm(&w);
if last_beta < LANCZOS_ORTHO_REFINE_RATIO * before {
basis_project(&basis, n, width, &w, &mut coeffs[..width]);
basis_subtract(&basis, n, width, &coeffs[..width], &mut w);
last_beta = norm(&w);
}
if last_beta <= LANCZOS_BREAKDOWN_RATIO * norm_estimate {
m_eff = width;
last_beta = 0.0;
break;
}
if width < m {
let inv = 1.0 / last_beta;
let next = &mut basis[width * n..(width + 1) * n];
for (dst, &src) in next.iter_mut().zip(w.iter()) {
*dst = src * inv;
}
}
}
let (values, vectors) = symmetric_eigen_descending(h.as_ref(), m_eff)?;
let n_out = n_keep.min(m_eff);
residual = (0..n_out).fold(0.0f64, |acc, i| {
acc.max(last_beta * vectors[(m_eff - 1, i)].abs())
});
norm_estimate = norm_estimate
.max(values[0].abs())
.max(values[m_eff - 1].abs());
converged = residual <= params.tol * norm_estimate;
ritz_values = values;
ritz_coeffs = vectors;
m_final = m_eff;
if converged || m_eff < m || last_beta == 0.0 || cycle + 1 == budget {
break;
}
let keep = (n_keep + LANCZOS_RESTART_GUARD).min(m / 2).max(1);
let mut restarted = vec![0.0f64; keep * n];
basis_expand(&basis, n, m_eff, ritz_coeffs.as_ref(), keep, &mut restarted);
let inv = 1.0 / last_beta;
for x in w.iter_mut() {
*x *= inv;
}
h.fill(0.0);
basis[..keep * n].copy_from_slice(&restarted);
for (i, &value) in ritz_values.iter().take(keep).enumerate() {
h[(i, i)] = value;
}
basis[keep * n..(keep + 1) * n].copy_from_slice(&w);
locked = keep;
}
let n_out = n_keep.min(m_final);
let mut ritz = vec![0.0f64; n_out * n];
basis_expand(&basis, n, m_final, ritz_coeffs.as_ref(), n_out, &mut ritz);
for column in ritz.chunks_exact_mut(n) {
let scale: f64 = column.iter().map(|v| v * v).sum::<f64>().sqrt();
let inv = if scale > 0.0 { 1.0 / scale } else { 0.0 };
column.iter_mut().for_each(|v| *v *= inv);
}
Ok(RitzPairs {
values: ritz_values[..n_out].to_vec(),
vectors: ritz,
residual,
norm_estimate,
converged,
restarts,
})
}
fn basis_project(basis: &[f64], n: usize, width: usize, v: &[f64], out: &mut [f64]) {
let b = MatRef::from_column_major_slice(&basis[..width * n], n, width);
let v = MatRef::from_column_major_slice(v, n, 1);
let mut out = MatMut::from_column_major_slice_mut(out, width, 1);
gemm(
out.as_mut(),
Accum::Replace,
b.transpose(),
v,
1.0,
faer_parallelism(),
);
}
fn basis_subtract(basis: &[f64], n: usize, width: usize, coeffs: &[f64], v: &mut [f64]) {
let b = MatRef::from_column_major_slice(&basis[..width * n], n, width);
let c = MatRef::from_column_major_slice(coeffs, width, 1);
let mut v = MatMut::from_column_major_slice_mut(v, n, 1);
gemm(v.as_mut(), Accum::Add, b, c, -1.0, faer_parallelism());
}
fn basis_expand(
basis: &[f64],
n: usize,
width: usize,
coeffs: MatRef<f64>,
n_out: usize,
out: &mut [f64],
) {
let b = MatRef::from_column_major_slice(&basis[..width * n], n, width);
let mut out = MatMut::from_column_major_slice_mut(out, n, n_out);
gemm(
out.as_mut(),
Accum::Replace,
b,
coeffs.subcols(0, n_out),
1.0,
faer_parallelism(),
);
}
pub(crate) fn symmetric_eigen_descending(
h: MatRef<f64>,
size: usize,
) -> Result<(Vec<f64>, Mat<f64>), CoreErrors> {
let block = Mat::<f64>::from_fn(size, size, |i, j| h[(i, j)]);
let eig = block
.self_adjoint_eigen(faer::Side::Lower)
.map_err(|_| CoreErrors::FaerEigenError)?;
let raw_values = eig.S();
let raw_vectors = eig.U();
let mut order: Vec<usize> = (0..size).collect();
order.sort_by(|&a, &b| raw_values[b].total_cmp(&raw_values[a]));
let values: Vec<f64> = order.iter().map(|&i| raw_values[i]).collect();
let vectors = Mat::<f64>::from_fn(size, size, |i, j| raw_vectors[(i, order[j])]);
Ok((values, vectors))
}
pub fn compute_largest_eigenpairs_lanczos<T>(
matrix: &CompressedSparseData2<T>,
n_components: usize,
seed: u64,
params: Option<LanczosParams>,
) -> Result<(Vec<f64>, Vec<Vec<f32>>), CoreErrors>
where
T: BixverseNumeric + BixverseSimd + Into<f64>,
{
let res = compute_largest_eigenpairs_lanczos_diag(matrix, n_components, seed, params)?;
Ok((res.eigenvalues, res.eigenvectors))
}
pub fn compute_largest_eigenpairs_lanczos_diag<T>(
matrix: &CompressedSparseData2<T>,
n_components: usize,
seed: u64,
params: Option<LanczosParams>,
) -> Result<LanczosResult, CoreErrors>
where
T: BixverseNumeric + BixverseSimd + Into<f64>,
{
let n = matrix.shape.0;
let csr = match matrix.cs_type {
CompressedSparseFormat::Csr => matrix.clone(),
CompressedSparseFormat::Csc => matrix.transform(),
};
let data_f64: Vec<f64> = csr.data.iter().map(|&v| v.into()).collect();
let matvec = |x: &[f64], y: &mut [f64]| {
y.par_iter_mut().enumerate().for_each(|(i, yi)| {
let mut sum = 0.0;
for idx in csr.indptr[i] as usize..csr.indptr[i + 1] as usize {
let j = csr.indices[idx] as usize;
sum += data_f64[idx] * x[j];
}
*yi = sum;
});
};
let ritz = lanczos_thick_restart(n, n_components, seed, params.unwrap_or_default(), matvec)?;
let n_out = ritz.values.len();
let mut eigenvectors = vec![vec![0.0f32; n_out]; n];
for comp in 0..n_out {
let column = &ritz.vectors[comp * n..(comp + 1) * n];
for (point, &v) in column.iter().enumerate() {
eigenvectors[point][comp] = v as f32;
}
}
Ok(LanczosResult {
eigenvalues: ritz.values,
eigenvectors,
residual: ritz.residual,
norm_estimate: ritz.norm_estimate,
converged: ritz.converged,
restarts: ritz.restarts,
})
}
const BLOCK_LANCZOS_MIN_NEW_BLOCKS: usize = 3;
#[derive(Clone, Copy, Debug)]
pub struct BlockLanczosParams {
pub block_size: usize,
pub basis_size: Option<usize>,
pub max_restarts: usize,
pub tol: f64,
}
impl BlockLanczosParams {
pub fn new() -> Self {
Self {
block_size: 32,
basis_size: None,
max_restarts: 64,
tol: 1e-6,
}
}
fn resolve(&self, n_keep: usize, n: usize) -> (usize, usize, usize) {
let b = self.block_size.min(n / 2).max(1);
let round_up = |x: usize| x.div_ceil(b) * b;
let floor = |x: usize| (x / b) * b;
let keep_target = round_up(n_keep + LANCZOS_RESTART_GUARD);
let basis = match self.basis_size {
Some(size) => floor(size),
None => round_up((n_keep * 2 + 10).max(n_keep + 20))
.max(2 * keep_target)
.max(keep_target + BLOCK_LANCZOS_MIN_NEW_BLOCKS * b),
}
.max(2 * b)
.min(floor(n))
.max(b);
let keep = keep_target.min(floor(basis / 2)).max(b);
(b, basis, keep)
}
}
impl Default for BlockLanczosParams {
fn default() -> Self {
Self::new()
}
}
fn check_operator_output(out: &Mat<f64>, n: usize, width: usize) -> Result<(), CoreErrors> {
if out.nrows() != n || out.ncols() != width {
return Err(CoreErrors::InvalidArgument(format!(
"block Lanczos: operator returned {} x {}, expected {n} x {width}",
out.nrows(),
out.ncols()
)));
}
Ok(())
}
fn dense_eigenpairs<E, M>(n: usize, n_keep: usize, b: usize, op: &mut M) -> Result<RitzPairs, E>
where
E: From<CoreErrors>,
M: FnMut(MatRef<f64>) -> Result<Mat<f64>, E>,
{
let mut a = Mat::<f64>::zeros(n, n);
for col in (0..n).step_by(b) {
let w = b.min(n - col);
let unit = Mat::<f64>::from_fn(n, w, |i, c| if i == col + c { 1.0 } else { 0.0 });
let out = op(unit.as_ref())?;
check_operator_output(&out, n, w)?;
a.subcols_mut(col, w).copy_from(&out);
}
let sym = Mat::<f64>::from_fn(n, n, |i, j| 0.5 * (a[(i, j)] + a[(j, i)]));
let (values, vectors) = symmetric_eigen_descending(sym.as_ref(), n)?;
let flat: Vec<f64> = (0..n_keep)
.flat_map(|c| vectors.col(c).iter().copied().collect::<Vec<_>>())
.collect();
Ok(RitzPairs {
values: values[..n_keep].to_vec(),
vectors: flat,
residual: 0.0,
norm_estimate: values[0].abs().max(values[n - 1].abs()),
converged: true,
restarts: 0,
})
}
fn column_norms(x: MatRef<f64>) -> Vec<f64> {
(0..x.ncols()).map(|c| x.col(c).norm_l2()).collect()
}
fn orthonormalise_block(
w: MatRef<f64>,
basis: MatRef<f64>,
threshold: f64,
rng: &mut StdRng,
) -> (Mat<f64>, Mat<f64>, usize) {
let (n, b) = w.shape();
let mut q = w.to_owned();
let mut r = Mat::<f64>::zeros(b, b);
let mut n_deficient = 0usize;
for c in 0..b {
for _ in 0..2 {
for i in 0..c {
let d = q.col(i).transpose() * q.col(c);
r[(i, c)] += d;
for k in 0..n {
q[(k, c)] -= d * q[(k, i)];
}
}
}
let remainder = q.col(c).norm_l2();
if remainder > threshold {
r[(c, c)] = remainder;
let inv = 1.0 / remainder;
q.col_mut(c).iter_mut().for_each(|x| *x *= inv);
continue;
}
n_deficient += 1;
let mut x = Mat::<f64>::from_fn(n, 1, |_, _| rng.random::<f64>() - 0.5);
for _ in 0..2 {
let mut coeffs = Mat::<f64>::zeros(basis.ncols(), 1);
gemm(
coeffs.as_mut(),
Accum::Replace,
basis.transpose(),
x.as_ref(),
1.0,
Par::Seq,
);
gemm(
x.as_mut(),
Accum::Add,
basis,
coeffs.as_ref(),
-1.0,
Par::Seq,
);
for i in 0..c {
let d = q.col(i).transpose() * x.col(0);
for k in 0..n {
x[(k, 0)] -= d * q[(k, i)];
}
}
}
let scale = x.col(0).norm_l2();
for k in 0..n {
q[(k, c)] = x[(k, 0)] / scale;
}
}
(q, r, n_deficient)
}
pub fn block_lanczos_thick_restart<E, M>(
n: usize,
n_components: usize,
seed: u64,
params: BlockLanczosParams,
mut op: M,
) -> Result<RitzPairs, E>
where
E: From<CoreErrors>,
M: FnMut(MatRef<f64>) -> Result<Mat<f64>, E>,
{
if n_components == 0 || n == 0 {
return Err(CoreErrors::MustBePositive(
"n_components and the operator dimension".to_string(),
)
.into());
}
let n_keep = n_components.min(n);
let (b, m, keep) = params.resolve(n_keep, n);
let par = faer_parallelism();
if m + b > n {
return dense_eigenpairs(n, n_keep, b, &mut op);
}
let mut rng = StdRng::seed_from_u64(seed);
let mut basis = Mat::<f64>::zeros(n, m);
let mut h = Mat::<f64>::zeros(m, m);
let start = Mat::<f64>::from_fn(n, b, |_, _| rng.random::<f64>() - 0.5);
basis
.subcols_mut(0, b)
.copy_from(start.qr().compute_thin_Q());
let mut resid_q = Mat::<f64>::zeros(n, b);
let mut resid_r = Mat::<f64>::zeros(b, b);
let mut locked = 0usize;
let mut ritz_values: Vec<f64> = Vec::new();
let mut ritz_coeffs = Mat::<f64>::zeros(0, 0);
let mut m_final = 0usize;
let mut norm_estimate = 0.0f64;
let mut residual = f64::INFINITY;
let mut converged = false;
let mut restarts = 0usize;
let budget = params.max_restarts.max(1);
for cycle in 0..budget {
restarts = cycle + 1;
let mut m_eff = m;
let mut invariant = false;
let mut j = locked;
while j < m {
let width = j + b;
let mut w = op(basis.subcols(j, b))?;
check_operator_output(&w, n, b)?;
let v = basis.subcols(0, width);
let mut coeffs = Mat::<f64>::zeros(width, b);
gemm(
coeffs.as_mut(),
Accum::Replace,
v.transpose(),
w.as_ref(),
1.0,
par,
);
for c in 0..b {
for i in 0..width {
h[(i, j + c)] = coeffs[(i, c)];
h[(j + c, i)] = coeffs[(i, c)];
}
}
let before = column_norms(w.as_ref());
norm_estimate = before.iter().fold(norm_estimate, |acc, &x| acc.max(x));
gemm(w.as_mut(), Accum::Add, v, coeffs.as_ref(), -1.0, par);
let after = column_norms(w.as_ref());
if after
.iter()
.zip(&before)
.any(|(&a, &bf)| a < LANCZOS_ORTHO_REFINE_RATIO * bf)
{
gemm(
coeffs.as_mut(),
Accum::Replace,
v.transpose(),
w.as_ref(),
1.0,
par,
);
gemm(w.as_mut(), Accum::Add, v, coeffs.as_ref(), -1.0, par);
}
let threshold = LANCZOS_BREAKDOWN_RATIO * norm_estimate;
let (q, r, n_deficient) = orthonormalise_block(w.as_ref(), v, threshold, &mut rng);
if n_deficient == b {
m_eff = width;
invariant = true;
break;
}
if width < m {
basis.subcols_mut(width, b).copy_from(&q);
} else {
resid_q = q;
resid_r = r;
}
j = width;
}
let (values, vectors) = symmetric_eigen_descending(h.as_ref(), m_eff)?;
let n_out = n_keep.min(m_eff);
residual = if invariant {
0.0
} else {
let mut y_last = Mat::<f64>::zeros(b, n_out);
y_last.copy_from(vectors.submatrix(m_eff - b, 0, b, n_out));
let mut ry = Mat::<f64>::zeros(b, n_out);
gemm(
ry.as_mut(),
Accum::Replace,
resid_r.as_ref(),
y_last.as_ref(),
1.0,
Par::Seq,
);
column_norms(ry.as_ref()).into_iter().fold(0.0f64, f64::max)
};
norm_estimate = norm_estimate
.max(values[0].abs())
.max(values[m_eff - 1].abs());
converged = residual <= params.tol * norm_estimate;
ritz_values = values;
ritz_coeffs = vectors;
m_final = m_eff;
if converged || invariant || cycle + 1 == budget {
break;
}
let mut restarted = Mat::<f64>::zeros(n, keep);
gemm(
restarted.as_mut(),
Accum::Replace,
basis.subcols(0, m_eff),
ritz_coeffs.subcols(0, keep),
1.0,
par,
);
h.fill(0.0);
basis.subcols_mut(0, keep).copy_from(&restarted);
for (i, &value) in ritz_values.iter().take(keep).enumerate() {
h[(i, i)] = value;
}
basis.subcols_mut(keep, b).copy_from(&resid_q);
locked = keep;
}
let n_out = n_keep.min(m_final);
let mut ritz = Mat::<f64>::zeros(n, n_out);
gemm(
ritz.as_mut(),
Accum::Replace,
basis.subcols(0, m_final),
ritz_coeffs.subcols(0, n_out),
1.0,
par,
);
let mut vectors = vec![0.0f64; n * n_out];
for (c, column) in vectors.chunks_exact_mut(n).enumerate() {
let src = ritz.col(c);
let scale = src.norm_l2();
let inv = if scale > 0.0 { 1.0 / scale } else { 0.0 };
for (dst, &v) in column.iter_mut().zip(src.iter()) {
*dst = v * inv;
}
}
Ok(RitzPairs {
values: ritz_values[..n_out].to_vec(),
vectors,
residual,
norm_estimate,
converged,
restarts,
})
}
pub fn sparse_svd_lanczos<T, U, F>(
matrix: &CompressedSparseData2<T, U>,
n_components: usize,
seed: u64,
use_second_layer: bool,
col_means: Option<&[F]>,
col_stds: Option<&[F]>,
row_offsets: Option<&[F]>,
) -> Result<SvdResults<F>, CoreErrors>
where
T: BixverseNumeric + BixverseSimd + Into<F> + Clone,
U: BixverseNumeric + Into<F> + Clone,
F: BixverseFloat + BixverseSimd + std::iter::Sum,
{
let (n, m) = matrix.shape;
let use_ata = n > m;
let krylov_dim = if use_ata { m } else { n };
let (csr, csc);
let csr_owned;
let csc_owned;
match matrix.cs_type {
CompressedSparseFormat::Csr => {
csr = matrix;
csc_owned = matrix.transform_single_layer(use_second_layer)?;
csc = &csc_owned;
}
CompressedSparseFormat::Csc => {
csc = matrix;
csr_owned = matrix.transform_single_layer(use_second_layer)?;
csr = &csr_owned;
}
};
let extract_data = |mat: &CompressedSparseData2<T, U>| -> Vec<F> {
if use_second_layer {
mat.data_2
.as_ref()
.expect("data_2 is None but use_second_layer is true")
.iter()
.copied()
.map(|v| v.into())
.collect()
} else {
mat.data.iter().copied().map(|v| v.into()).collect()
}
};
let data_csr_f = extract_data(csr);
let data_csc_f = extract_data(csc);
let matvec_a = |x: &[F], y: &mut [F]| {
let x_scaled: Vec<F> = if let Some(sd) = col_stds {
x.iter().enumerate().map(|(j, &v)| v / sd[j]).collect()
} else {
x.to_vec()
};
let mean_dot: F = if let Some(mu) = col_means {
x_scaled.iter().enumerate().map(|(j, &v)| mu[j] * v).sum()
} else {
F::zero()
};
let x_sum: F = if row_offsets.is_some() {
x_scaled.iter().copied().sum()
} else {
F::zero()
};
y.par_iter_mut().enumerate().for_each(|(i, yi)| {
let mut sum = F::zero();
for idx in csr.indptr[i] as usize..csr.indptr[i + 1] as usize {
let j = csr.indices[idx] as usize;
sum += data_csr_f[idx] * x_scaled[j];
}
if col_means.is_some() {
sum -= mean_dot;
}
if let Some(off) = row_offsets {
sum -= off[i] * x_sum;
}
*yi = sum;
});
};
let matvec_at = |x: &[F], y: &mut [F]| {
let x_sum: F = x.iter().copied().sum();
let m_dot_x: F = if let Some(off) = row_offsets {
off.iter().zip(x.iter()).map(|(&m_i, &xi)| m_i * xi).sum()
} else {
F::zero()
};
y.par_iter_mut().enumerate().for_each(|(j, yj)| {
let mut sum = F::zero();
for idx in csc.indptr[j] as usize..csc.indptr[j + 1] as usize {
let i = csc.indices[idx] as usize;
sum += data_csc_f[idx] * x[i];
}
if let Some(mu) = col_means {
sum -= mu[j] * x_sum;
}
if row_offsets.is_some() {
sum -= m_dot_x;
}
if let Some(sd) = col_stds {
sum /= sd[j];
}
*yj = sum;
});
};
#[allow(clippy::type_complexity)]
let matvec_gram: Box<dyn Fn(&[F], &mut [F]) + Sync> = if use_ata {
Box::new(|x: &[F], y: &mut [F]| {
let mut temp = vec![F::zero(); n];
matvec_a(x, &mut temp);
matvec_at(&temp, y);
})
} else {
Box::new(|x: &[F], y: &mut [F]| {
let mut temp = vec![F::zero(); m];
matvec_at(x, &mut temp);
matvec_a(&temp, y);
})
};
let matvec_gram_f64 = |x: &[f64], y: &mut [f64]| {
let x_f: Vec<F> = x
.iter()
.map(|&v| F::from(v).expect("f64 casts into any float type"))
.collect();
let mut y_f = vec![F::zero(); krylov_dim];
matvec_gram(&x_f, &mut y_f);
for (dst, src) in y.iter_mut().zip(y_f) {
*dst = src.to_f64().expect("float casts into f64");
}
};
let ritz = lanczos_thick_restart(
krylov_dim,
n_components,
seed,
LanczosParams::default(),
matvec_gram_f64,
)?;
let mut singular_values: Vec<F> = Vec::with_capacity(n_components);
let mut u_vecs: Vec<Vec<F>> = Vec::with_capacity(n_components);
let mut v_vecs: Vec<Vec<F>> = Vec::with_capacity(n_components);
for (idx, &eval) in ritz.values.iter().enumerate() {
if eval <= 0.0 {
continue;
}
let sigma = F::from(eval.sqrt()).expect("f64 casts into any float type");
singular_values.push(sigma);
let gram_evec: Vec<F> = ritz.vectors[idx * krylov_dim..(idx + 1) * krylov_dim]
.iter()
.map(|&v| F::from(v).expect("f64 casts into any float type"))
.collect();
if use_ata {
let mut u_vec = vec![F::zero(); n];
matvec_a(&gram_evec, &mut u_vec);
for x in &mut u_vec {
*x /= sigma;
}
u_vecs.push(u_vec);
v_vecs.push(gram_evec);
} else {
let mut v_vec = vec![F::zero(); m];
matvec_at(&gram_evec, &mut v_vec);
for x in &mut v_vec {
*x /= sigma;
}
u_vecs.push(gram_evec);
v_vecs.push(v_vec);
}
}
let u = Mat::from_fn(n, singular_values.len(), |i, j| u_vecs[j][i]);
let v = Mat::from_fn(m, singular_values.len(), |i, j| v_vecs[j][i]);
Ok(SvdResults {
u,
s: singular_values,
v,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::math::vector_helpers::rank_vector;
use faer::Mat;
#[test]
fn test_from_dense_and_count_zeroes() {
let mat: Mat<f64> = Mat::from_fn(3, 2, |i, j| if i == j { (i + 1) as f64 } else { 0.0 });
let (total_zeroes, row_zeroes, col_zeroes) = count_zeroes(&mat.as_ref());
assert_eq!(total_zeroes, 4);
assert_eq!(row_zeroes, vec![1, 1, 2]);
assert_eq!(col_zeroes, vec![2, 2]);
let csr = CompressedSparseData2::<f64, f64>::from_dense_matrix(
mat.as_ref(),
CompressedSparseFormat::Csr,
);
assert_eq!(csr.shape, (3, 2));
assert_eq!(csr.data, vec![1.0, 2.0]);
}
#[test]
fn test_lanczos_eigenpairs_logic() {
let data = vec![1.0, 2.0, 2.0, 4.0];
let indices = vec![0, 2, 0, 2];
let indptr = vec![0, 2, 2, 4, 4]; let shape = (4, 4);
let csr = CompressedSparseData2::<f64, f64>::new_csr(&data, &indices, &indptr, None, shape);
let (evals, evecs) = compute_largest_eigenpairs_lanczos(&csr, 1, 42, None).unwrap();
assert!((evals[0] - 5.0).abs() < 1e-3);
let x_norm = 5.0_f32.sqrt();
let dot_x = (evecs[0][0] * 1.0 + evecs[2][0] * 2.0) / x_norm;
assert!(dot_x.abs() > 0.999);
}
#[test]
fn test_sparse_svd_lanczos_logic() {
let data = vec![2.0, 1.0, 4.0, 2.0];
let indices = vec![0, 2, 0, 2];
let indptr = vec![0, 0, 2, 2, 4];
let shape = (4, 3);
let csr = CompressedSparseData2::<f64, f64>::new_csr(&data, &indices, &indptr, None, shape);
let no_params: Option<&[f64]> = None;
let svd = sparse_svd_lanczos(&csr, 1, 42, false, no_params, no_params, None).unwrap();
let u_col = svd.u.col(0);
let x_norm = 20.0_f64.sqrt();
let dot_u = (u_col[1] * 2.0 + u_col[3] * 4.0) / x_norm;
assert!(dot_u.abs() > 0.999);
let v_col = svd.v.col(0);
let y_norm = 1.25_f64.sqrt();
let dot_v = (v_col[0] * 1.0 + v_col[2] * 0.5) / y_norm;
assert!(dot_v.abs() > 0.999);
}
#[test]
fn test_sparse_svd_lanczos_flat_spectrum() {
let (n, m, k) = (400usize, 200usize, 30usize);
let mut rng = StdRng::seed_from_u64(1);
let vals: Vec<f64> = (0..n * m).map(|_| rng.random::<f64>()).collect();
let indices: Vec<u32> = (0..n).flat_map(|_| 0..m as u32).collect();
let indptr: Vec<u32> = (0..=n).map(|i| (i * m) as u32).collect();
let csr =
CompressedSparseData2::<f64, f64>::new_csr(&vals, &indices, &indptr, None, (n, m));
let means: Vec<f64> = (0..m)
.map(|j| (0..n).map(|i| vals[i * m + j]).sum::<f64>() / n as f64)
.collect();
let svd = sparse_svd_lanczos(&csr, k, 42, false, Some(&means[..]), None, None).unwrap();
let centred = Mat::<f64>::from_fn(n, m, |i, j| vals[i * m + j] - means[j]);
let reference = centred.thin_svd().unwrap();
let s_ref = reference.S().column_vector();
assert_eq!(svd.s.len(), k);
for i in 0..k {
assert_relative_eq!(svd.s[i], s_ref[i], max_relative = 1e-6);
}
}
use approx::assert_relative_eq;
fn path_graph(n: usize) -> CompressedSparseData2<f64, f64> {
let mut rows: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
for i in 0..n - 1 {
rows[i].push(((i + 1) as u32, 1.0));
rows[i + 1].push((i as u32, 1.0));
}
let mut indptr = vec![0u32];
let mut indices = Vec::new();
let mut data = Vec::new();
for row in rows.iter_mut() {
row.sort_by_key(|&(j, _)| j);
for &(j, v) in row.iter() {
indices.push(j);
data.push(v);
}
indptr.push(indices.len() as u32);
}
CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, n))
}
fn diagonal(diag: &[f64]) -> CompressedSparseData2<f64, f64> {
let indices: Vec<u32> = (0..diag.len() as u32).collect();
let indptr: Vec<u32> = (0..=diag.len() as u32).collect();
CompressedSparseData2::new_csr(diag, &indices, &indptr, None, (diag.len(), diag.len()))
}
fn pair_residual(
mat: &CompressedSparseData2<f64, f64>,
values: &[f64],
vectors: &[Vec<f32>],
k: usize,
) -> f64 {
let n = mat.shape.0;
let v: Vec<f64> = (0..n).map(|i| vectors[i][k] as f64).collect();
let lam = values[k];
(0..n)
.map(|i| {
let mut sum = 0.0;
for x in mat.indptr[i] as usize..mat.indptr[i + 1] as usize {
sum += mat.data[x] * v[mat.indices[x] as usize];
}
(sum - lam * v[i]).powi(2)
})
.sum::<f64>()
.sqrt()
}
#[test]
fn test_lanczos_resolves_a_clustered_spectrum_at_defaults() {
let n = 300usize;
let mat = path_graph(n);
let (values, vectors) = compute_largest_eigenpairs_lanczos(&mat, 4, 42, None).unwrap();
for k in 0..4 {
let expected = 2.0 * ((k + 1) as f64 * std::f64::consts::PI / (n + 1) as f64).cos();
assert_relative_eq!(values[k], expected, epsilon = 1e-3);
}
let sign = vectors[0][0].signum();
assert!(
(0..n).all(|i| vectors[i][0].signum() == sign || vectors[i][0] == 0.0),
"leading eigenvector of a path graph changed sign"
);
}
#[test]
fn test_lanczos_restarts_converge_a_clustered_spectrum() {
let n = 300usize;
let mat = path_graph(n);
let params = LanczosParams {
basis_size: None,
max_restarts: 64,
tol: 1e-8,
};
let res = compute_largest_eigenpairs_lanczos_diag(&mat, 4, 42, Some(params)).unwrap();
assert!(res.converged, "residual stalled at {:e}", res.residual);
for k in 0..4 {
let expected = 2.0 * ((k + 1) as f64 * std::f64::consts::PI / (n + 1) as f64).cos();
assert_relative_eq!(res.eigenvalues[k], expected, epsilon = 1e-6);
let residual = pair_residual(&mat, &res.eigenvalues, &res.eigenvectors, k);
assert!(residual < 1e-5, "pair {k} has residual {residual:e}");
}
}
#[test]
fn test_lanczos_caps_components_at_the_matrix_dimension() {
let mat = diagonal(&[4.0, 3.0, 2.0, 1.0]);
for requested in [4usize, 5, 40] {
let (values, vectors) =
compute_largest_eigenpairs_lanczos(&mat, requested, 11, None).unwrap();
assert_eq!(values.len(), 4, "requested {requested}");
assert_eq!(vectors[0].len(), values.len(), "requested {requested}");
assert_relative_eq!(values[0], 4.0, epsilon = 1e-9);
assert_relative_eq!(values[3], 1.0, epsilon = 1e-9);
}
}
#[test]
fn test_lanczos_breakdown_keeps_values_and_vectors_the_same_length() {
let mut diag = vec![0.0f64; 12];
diag[0] = 5.0;
diag[1] = 3.0;
let mat = diagonal(&diag);
let res = compute_largest_eigenpairs_lanczos_diag(&mat, 10, 3, None).unwrap();
assert!(res.eigenvalues.len() < 10, "expected an early breakdown");
assert_eq!(res.eigenvalues.len(), res.eigenvectors[0].len());
assert_relative_eq!(res.eigenvalues[0], 5.0, epsilon = 1e-9);
assert_relative_eq!(res.eigenvalues[1], 3.0, epsilon = 1e-9);
}
#[test]
fn test_lanczos_reports_budget_exhaustion() {
let mat = path_graph(300);
let params = LanczosParams {
basis_size: None,
max_restarts: 1,
tol: 1e-8,
};
let res = compute_largest_eigenpairs_lanczos_diag(&mat, 4, 42, Some(params)).unwrap();
assert!(!res.converged);
assert_eq!(res.restarts, 1);
assert!(res.residual > params.tol * res.norm_estimate);
}
#[test]
fn test_lanczos_tolerance_is_relative_to_the_matrix_norm() {
let small = diagonal(&[4.0, 3.0, 2.0, 1.0]);
let large = diagonal(&[4e8, 3e8, 2e8, 1e8]);
let a = compute_largest_eigenpairs_lanczos_diag(&small, 2, 5, None).unwrap();
let b = compute_largest_eigenpairs_lanczos_diag(&large, 2, 5, None).unwrap();
assert!(a.converged && b.converged);
assert_relative_eq!(b.eigenvalues[0] / a.eigenvalues[0], 1e8, epsilon = 1e-1);
}
#[test]
fn test_lanczos_params_basis_size_is_honoured() {
let n = 40usize;
let mut indptr = vec![0u32];
let mut indices = Vec::new();
let mut data = Vec::new();
for i in 0..n {
indices.push(i as u32);
data.push((i + 1) as f64);
indptr.push(indices.len() as u32);
}
let mat = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, n));
let tight = LanczosParams {
basis_size: Some(6),
max_restarts: 200,
tol: 1e-10,
};
let (values, _) = compute_largest_eigenpairs_lanczos(&mat, 3, 7, Some(tight)).unwrap();
assert_relative_eq!(values[0], 40.0, epsilon = 1e-8);
assert_relative_eq!(values[1], 39.0, epsilon = 1e-8);
assert_relative_eq!(values[2], 38.0, epsilon = 1e-8);
}
fn block_op(
mat: &CompressedSparseData2<f64, f64>,
x: MatRef<f64>,
) -> Result<Mat<f64>, CoreErrors> {
let n = mat.shape.0;
Ok(Mat::from_fn(n, x.ncols(), |i, c| {
(mat.indptr[i] as usize..mat.indptr[i + 1] as usize)
.map(|k| mat.data[k] * x[(mat.indices[k] as usize, c)])
.sum()
}))
}
#[test]
fn test_block_lanczos_matches_scalar_on_a_clustered_spectrum() {
let n = 300usize;
let mat = path_graph(n);
let scalar = compute_largest_eigenpairs_lanczos_diag(&mat, 4, 42, None).unwrap();
for block_size in [1usize, 4, 16] {
let params = BlockLanczosParams {
block_size,
basis_size: None,
max_restarts: 1000,
tol: 1e-8,
};
let res = block_lanczos_thick_restart(n, 4, 42, params, |x| block_op(&mat, x)).unwrap();
assert!(
res.converged,
"b = {block_size}: residual {:e}",
res.residual
);
for k in 0..4 {
assert_relative_eq!(res.values[k], scalar.eigenvalues[k], epsilon = 1e-7);
let v: Vec<Vec<f32>> = (0..n)
.map(|i| vec![res.vectors[k * n + i] as f32])
.collect();
let residual = pair_residual(&mat, &res.values[k..k + 1], &v, 0);
assert!(residual < 1e-5, "b = {block_size}, pair {k}: {residual:e}");
}
}
}
#[test]
fn test_block_lanczos_low_rank_recovers_the_nonzero_pairs() {
let mut diag = vec![0.0f64; 200];
diag[0] = 5.0;
diag[1] = 3.0;
let mat = diagonal(&diag);
let params = BlockLanczosParams {
block_size: 4,
..BlockLanczosParams::new()
};
let res = block_lanczos_thick_restart(200, 2, 3, params, |x| block_op(&mat, x)).unwrap();
assert!(res.converged);
assert_relative_eq!(res.values[0], 5.0, epsilon = 1e-9);
assert_relative_eq!(res.values[1], 3.0, epsilon = 1e-9);
assert_relative_eq!(res.vectors[0].abs(), 1.0, epsilon = 1e-6);
assert_relative_eq!(res.vectors[200 + 1].abs(), 1.0, epsilon = 1e-6);
}
#[test]
fn test_block_lanczos_caps_the_block_at_the_dimension() {
let mat = diagonal(&[4.0, 3.0, 2.0, 1.0]);
let params = BlockLanczosParams {
block_size: 64,
..BlockLanczosParams::new()
};
let res = block_lanczos_thick_restart(4, 10, 1, params, |x| block_op(&mat, x)).unwrap();
assert_eq!(res.values.len(), 4);
assert_eq!(res.vectors.len(), 16);
assert_relative_eq!(res.values[0], 4.0, epsilon = 1e-9);
assert_relative_eq!(res.values[3], 1.0, epsilon = 1e-9);
}
#[test]
fn test_block_lanczos_ragged_dimension_is_exact() {
let diag: Vec<f64> = (0..50).map(|i| 1.0 + i as f64).collect();
let mat = diagonal(&diag);
let params = BlockLanczosParams {
block_size: 16,
..BlockLanczosParams::new()
};
let res = block_lanczos_thick_restart(50, 3, 9, params, |x| block_op(&mat, x)).unwrap();
assert!(res.converged);
assert_relative_eq!(res.values[0], 50.0, epsilon = 1e-9);
assert_relative_eq!(res.values[1], 49.0, epsilon = 1e-9);
assert_relative_eq!(res.values[2], 48.0, epsilon = 1e-9);
}
#[test]
fn test_block_lanczos_rejects_a_misshapen_operator() {
let res = block_lanczos_thick_restart::<CoreErrors, _>(
10,
2,
1,
BlockLanczosParams::new(),
|_| Ok(Mat::<f64>::zeros(10, 1)),
);
assert!(matches!(res, Err(CoreErrors::InvalidArgument(_))));
}
fn dense_block_fixture(
n: usize,
k: usize,
width: usize,
seed: u64,
) -> (CompressedSparseData2<f64>, Vec<f64>) {
let mut rng = StdRng::seed_from_u64(seed);
let mut indptr = vec![0u32];
let mut indices: Vec<u32> = Vec::new();
let mut data: Vec<f64> = Vec::new();
for _ in 0..n {
for j in 0..k {
if rng.random::<f64>() < 0.35 {
indices.push(j as u32);
data.push(rng.random::<f64>() * 2.0 - 1.0);
}
}
indptr.push(indices.len() as u32);
}
let csr = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, k));
let block: Vec<f64> = (0..k * width)
.map(|_| rng.random::<f64>() * 2.0 - 1.0)
.collect();
(csr, block)
}
#[test]
fn test_csr_matmul_dense_block_matches_scalar() {
for width in [1usize, 3, 4, 7, 16, 17, 33] {
let (csr, block) = dense_block_fixture(11, 9, width, 4242 + width as u64);
let mut got = vec![0.0f64; 11 * width];
csr_matmul_dense_block(&csr, &block, width, &mut got).unwrap();
let mut want = vec![0.0f64; 11 * width];
for i in 0..11 {
for p in csr.indptr[i] as usize..csr.indptr[i + 1] as usize {
let j = csr.indices[p] as usize;
for c in 0..width {
want[i * width + c] += csr.data[p] * block[j * width + c];
}
}
}
for (g, w) in got.iter().zip(want.iter()) {
assert_relative_eq!(g, w, epsilon = 1e-12, max_relative = 1e-12);
}
}
}
#[test]
fn test_csr_matmul_dense_block_repeated_matches_explicit_power() {
let width = 5usize;
let n = 8usize;
let (csr, block) = dense_block_fixture(n, n, width, 77);
let mut buf_a = block.clone();
let mut buf_b = vec![0.0f64; n * width];
for _ in 0..3 {
csr_matmul_dense_block(&csr, &buf_a, width, &mut buf_b).unwrap();
std::mem::swap(&mut buf_a, &mut buf_b);
}
let sq = csr_matmul_csr(&csr, &csr).unwrap();
let cube = csr_matmul_csr(&sq, &csr).unwrap();
let mut direct = vec![0.0f64; n * width];
csr_matmul_dense_block(&cube, &block, width, &mut direct).unwrap();
for (rep, dir) in buf_a.iter().zip(direct.iter()) {
assert_relative_eq!(rep, dir, epsilon = 1e-10, max_relative = 1e-10);
}
}
#[test]
fn test_csr_matmul_dense_block_preserves_constant_column() {
let n = 6usize;
let width = 3usize;
let mut indptr = vec![0u32];
let mut indices: Vec<u32> = Vec::new();
let mut data: Vec<f64> = Vec::new();
for i in 0..n {
for j in 0..n {
indices.push(j as u32);
data.push(1.0 + ((i * n + j) % 3) as f64);
}
indptr.push(indices.len() as u32);
}
let mut csr = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, n));
normalise_csr_rows_l1(&mut csr).unwrap();
let block = vec![2.5f64; n * width];
let mut out = vec![0.0f64; n * width];
csr_matmul_dense_block(&csr, &block, width, &mut out).unwrap();
for v in out {
assert_relative_eq!(v, 2.5, epsilon = 1e-12);
}
}
#[test]
fn test_csr_matmul_dense_block_rejects_bad_shapes() {
let (csr, block) = dense_block_fixture(4, 4, 2, 1);
let mut out = vec![0.0f64; 4 * 2];
assert!(csr_matmul_dense_block(&csr, &block, 3, &mut out).is_err());
assert!(csr_matmul_dense_block(&csr, &block, 0, &mut out).is_err());
let mut short = vec![0.0f64; 4];
assert!(csr_matmul_dense_block(&csr, &block, 2, &mut short).is_err());
let csc = csr.transform();
assert!(matches!(
csr_matmul_dense_block(&csc, &block, 2, &mut out),
Err(CoreErrors::SparseMatrixMustBeCsr)
));
}
#[test]
fn test_shifted_ranks_sparse_matches_dense_rank_vector() {
let cases: Vec<Vec<f32>> = vec![
vec![3.0, 0.0, 1.0, 0.0, 0.0],
vec![0.0, 0.0, 0.0, 0.0, 0.0],
vec![-2.0, 0.0, 5.0, 0.0, 0.0, 0.0],
vec![-1.0, -3.0, 0.0, 2.0, 4.0, 0.0, 0.0],
vec![2.0, 2.0, 0.0, 2.0, 0.0, 5.0],
vec![-4.0, -4.0, 0.0, 0.0, -4.0, 1.0],
vec![1.0, 2.0, 3.0, 4.0],
vec![-1.0, -2.0, -3.0],
vec![7.0],
];
for dense in cases {
let n = dense.len();
let indices: Vec<u32> = (0..n as u32)
.filter(|&i| dense[i as usize] != 0.0)
.collect();
let values: Vec<f32> = indices.iter().map(|&i| dense[i as usize]).collect();
let got = shifted_ranks_sparse(&values, n);
let dense_ranks = rank_vector(&dense);
assert_eq!(got.len(), indices.len());
if indices.is_empty() {
continue;
}
for (slot, &i) in indices.iter().enumerate() {
assert_relative_eq!(
got[slot] - got[0],
dense_ranks[i as usize] - dense_ranks[indices[0] as usize],
epsilon = 1e-6
);
}
if indices.len() < n {
let r_zero = dense_ranks[(0..n).find(|&i| dense[i] == 0.0).unwrap()];
for (slot, &i) in indices.iter().enumerate() {
assert_relative_eq!(
got[slot],
dense_ranks[i as usize] - r_zero,
epsilon = 1e-6
);
}
}
}
}
#[test]
fn test_shifted_ranks_sparse_merges_explicit_zeros() {
let values: Vec<f32> = vec![-2.0, 0.0, 3.0, 1.0];
let got = shifted_ranks_sparse(&values, 6);
assert_relative_eq!(got[1], 0.0, epsilon = 1e-6);
assert!(got[0] < 0.0, "the negative must rank below the zero block");
assert!(got[2] > got[3], "3.0 must rank above 1.0");
assert!(got[3] > 0.0, "positives must rank above the zero block");
}
}