1use crate::utils::gemm::gemm;
5use faer::{Accum, Mat, MatMut, MatRef, Par};
6use num_traits::ToPrimitive;
7use rand::rngs::StdRng;
8use rand::{Rng, SeedableRng};
9use rayon::prelude::*;
10use rustc_hash::FxHashMap;
11
12pub use bixverse_sparse::sparse::*;
13
14use crate::core::math::pca_svd::SvdResults;
15use crate::core::math::vector_helpers::sum_sq_f64;
16use crate::prelude::*;
17use crate::utils::faer_parallelism;
18use crate::utils::simd::{sum_squared_dev_widen_simd_f32, sum_widen_simd_f32};
19
20pub trait SparseFromDense<T> {
29 fn from_dense_matrix(mat: MatRef<T>, format: CompressedSparseFormat) -> Self;
40}
41
42impl<T, U> SparseFromDense<T> for CompressedSparseData2<T, U>
43where
44 T: BixverseFloat + BixverseNumeric,
45 U: BixverseNumeric,
46{
47 fn from_dense_matrix(mat: MatRef<T>, format: CompressedSparseFormat) -> Self {
48 Self::from_entry_fn(mat.nrows(), mat.ncols(), format, |i, j| mat[(i, j)])
49 }
50}
51
52const MATMUL_DENSE_ROW_BLOCK: usize = 256;
62
63pub fn csr_sparse_matmul_dense<T>(
81 a: &CompressedSparseData2<T>,
82 b: &CompressedSparseData2<T>,
83) -> Result<Mat<T>, CoreErrors>
84where
85 T: BixverseFloat + Send + Sync + Default,
86{
87 require_matmul_operands(a, b)?;
88
89 let n_rows = a.nrows();
90 let n_cols = b.ncols();
91 let m_indptr = &a.indptr;
92 let m_indices = &a.indices;
93 let m_data = &a.data;
94 let g_indptr = &b.indptr;
95 let g_indices = &b.indices;
96 let g_data = &b.data;
97
98 let mut out = Mat::<T>::zeros(n_rows, n_cols);
101 for block_start in (0..n_rows).step_by(MATMUL_DENSE_ROW_BLOCK) {
102 let rows = MATMUL_DENSE_ROW_BLOCK.min(n_rows - block_start);
103 let mut tile = vec![T::zero(); rows * n_cols];
104 tile.par_chunks_mut(n_cols.max(1))
105 .take(rows)
106 .enumerate()
107 .for_each(|(r, row)| {
108 let i = block_start + r;
109 for p in m_indptr[i] as usize..m_indptr[i + 1] as usize {
110 let j = m_indices[p] as usize;
111 let w = m_data[p];
112 let gs = g_indptr[j] as usize;
113 let ge = g_indptr[j + 1] as usize;
114 for q in gs..ge {
115 let t = g_indices[q] as usize;
116 row[t] += w * g_data[q];
117 }
118 }
119 });
120
121 out.par_col_iter_mut().enumerate().for_each(|(j, mut col)| {
122 for r in 0..rows {
123 col[block_start + r] = tile[r * n_cols + j];
124 }
125 });
126 }
127
128 Ok(out)
129}
130
131pub fn csr_matmul_dense_block<T>(
172 a: &CompressedSparseData2<T>,
173 block: &[T],
174 width: usize,
175 out: &mut [T],
176) -> Result<(), CoreErrors>
177where
178 T: BixverseFloat + BixverseSimd,
179{
180 require_csr(a)?;
181
182 let (n_rows, n_cols) = a.shape();
183
184 if width == 0 || block.len() != n_cols * width || out.len() != n_rows * width {
187 return Err(CoreErrors::SparseMatrixMultiplication {
188 n_col_a: n_cols,
189 n_row_b: block.len().checked_div(width).unwrap_or(0),
190 });
191 }
192
193 let indptr = &a.indptr;
194 let indices = &a.indices;
195 let data = &a.data;
196
197 out.par_chunks_mut(width)
198 .enumerate()
199 .for_each(|(i, out_row)| {
200 out_row.fill(T::zero());
201 for p in indptr[i] as usize..indptr[i + 1] as usize {
202 let j = indices[p] as usize;
203 T::bxv_axpy_simd(out_row, data[p], &block[j * width..(j + 1) * width]);
204 }
205 });
206
207 Ok(())
208}
209
210const PAR_COLUMN_STATS_MIN_NNZ: usize = 1 << 20;
220
221fn csc_layer<T>(csc: &CompressedSparseData2<T>, use_second_layer: bool) -> Result<&[T], CoreErrors>
237where
238 T: BixverseNumeric,
239{
240 require_csc(csc)?;
241
242 if use_second_layer {
243 Ok(csc
244 .data_2
245 .as_ref()
246 .ok_or(CoreErrors::Data2NotAvailable)?
247 .as_slice())
248 } else {
249 Ok(csc.data.as_slice())
250 }
251}
252
253fn collect_per_column<T, F>(ncols: usize, nnz: usize, stat: F) -> Vec<T>
267where
268 T: Send,
269 F: Fn(usize) -> T + Sync + Send,
270{
271 if nnz >= PAR_COLUMN_STATS_MIN_NNZ {
272 (0..ncols).into_par_iter().map(stat).collect()
273 } else {
274 (0..ncols).map(stat).collect()
275 }
276}
277
278pub fn sparse_col_means_csc<T>(
293 csc: &CompressedSparseData2<T>,
294 use_second_layer: bool,
295) -> Result<Vec<T>, CoreErrors>
296where
297 T: BixverseFloat + BixverseSimd,
298{
299 let active_data = csc_layer(csc, use_second_layer)?;
300
301 let (nrows, ncols) = csc.shape();
302 let nrows_t = T::from_usize(nrows).unwrap();
303
304 Ok(collect_per_column(ncols, active_data.len(), |j| {
305 let start = csc.indptr[j] as usize;
306 let end = csc.indptr[j + 1] as usize;
307 T::bxv_sum(&active_data[start..end]) / nrows_t
308 }))
309}
310
311pub fn sparse_col_sds_csc<T>(
322 csc: &CompressedSparseData2<T>,
323 use_second_layer: bool,
324) -> Result<Vec<T>, CoreErrors>
325where
326 T: BixverseFloat + BixverseSimd,
327{
328 let active_data = csc_layer(csc, use_second_layer)?;
329
330 let (nrows, ncols) = csc.shape();
331 let nrows_t = T::from_usize(nrows).unwrap();
332 let denom = nrows_t - T::one();
333
334 Ok(collect_per_column(ncols, active_data.len(), |j| {
335 let start = csc.indptr[j] as usize;
336 let end = csc.indptr[j + 1] as usize;
337 let col_slice = &active_data[start..end];
338 let implicit_zeros = T::from_usize(nrows - (end - start)).unwrap();
339
340 let mean = T::bxv_sum(col_slice) / nrows_t;
341 let ssd_nonzero = T::bxv_sum_squared_deviation(col_slice, mean);
342 let ssd_total = ssd_nonzero + implicit_zeros * mean * mean;
343 (ssd_total / denom).sqrt()
344 }))
345}
346
347pub fn frobenius_norm<T>(mat: &CompressedSparseData2<T>) -> f32
361where
362 T: BixverseNumeric + ToPrimitive,
363{
364 sum_sq_f64(&mat.data).sqrt() as f32
365}
366
367pub fn frobenius_norm_sq_f64<T>(mat: &CompressedSparseData2<T>) -> f64
381where
382 T: BixverseNumeric + ToPrimitive,
383{
384 sum_sq_f64(&mat.data)
385}
386
387pub fn count_zeroes<T>(mat: &MatRef<T>) -> (usize, Vec<usize>, Vec<usize>)
398where
399 T: BixverseFloat,
400{
401 let (nrow, ncol) = mat.shape();
402 let mut total_zeroes = 0_usize;
403 let mut row_zeroes = vec![0_usize; nrow];
404 let mut col_zeroes = vec![0_usize; ncol];
405
406 let zero = T::zero();
407
408 for j in 0..ncol {
409 for i in 0..nrow {
410 let val = unsafe { mat.get_unchecked(i, j) };
411 if *val == zero {
412 total_zeroes += 1;
413 row_zeroes[i] += 1;
414 col_zeroes[j] += 1;
415 }
416 }
417 }
418
419 (total_zeroes, row_zeroes, col_zeroes)
420}
421
422const SPARSE_COR_SD_EPS: f64 = 1e-8;
433
434#[derive(Clone, Debug)]
443pub struct SparseColMoments {
444 pub indices: Vec<u32>,
446 pub values: Vec<f32>,
450 pub mean: f64,
452 pub sd: f64,
455}
456
457fn shifted_ranks_sparse(values: &[f32], n_rows: usize) -> Vec<f32> {
472 let n_stored = values.len();
473 let n_implicit = n_rows.saturating_sub(n_stored);
474
475 let n_neg = values.iter().filter(|&&v| v < 0.0).count();
476 let n_stored_zero = values.iter().filter(|&&v| v == 0.0).count();
477 let zero_block = n_stored_zero + n_implicit;
478
479 let r_zero = n_neg as f64 + (zero_block as f64 + 1.0) / 2.0;
481
482 let mut order: Vec<u32> = (0..n_stored as u32).collect();
483 order.sort_unstable_by(|&a, &b| {
484 values[a as usize]
485 .partial_cmp(&values[b as usize])
486 .unwrap_or(std::cmp::Ordering::Equal)
487 });
488
489 let mut ranks = vec![0_f32; n_stored];
490 let mut i = 0;
491 while i < n_stored {
492 let value = values[order[i] as usize];
493 let start = i;
494 while i < n_stored && values[order[i] as usize] == value {
495 i += 1;
496 }
497
498 let avg_rank = if value == 0.0 {
499 r_zero
500 } else {
501 let offset = if value < 0.0 { 0 } else { n_implicit };
505 (start + i + 1 + 2 * offset) as f64 / 2.0
506 };
507
508 let shifted = (avg_rank - r_zero) as f32;
509 for &slot in &order[start..i] {
510 ranks[slot as usize] = shifted;
511 }
512 }
513
514 ranks
515}
516
517pub fn sparse_col_moments(
536 indices: &[u32],
537 values: &[f32],
538 n_rows: usize,
539 spearman: bool,
540) -> SparseColMoments {
541 let stored: Vec<f32> = if spearman {
542 shifted_ranks_sparse(values, n_rows)
543 } else {
544 values.to_vec()
545 };
546
547 let n = n_rows as f64;
548 let mean = sum_widen_simd_f32(&stored) / n;
549 let ss_stored = sum_squared_dev_widen_simd_f32(&stored, mean);
550 let ss = ss_stored + n_rows.saturating_sub(stored.len()) as f64 * mean * mean;
551 let sd = if n_rows < 2 {
552 0.0
553 } else {
554 (ss / (n - 1.0)).sqrt()
555 };
556
557 SparseColMoments {
558 indices: indices.to_vec(),
559 values: stored,
560 mean,
561 sd,
562 }
563}
564
565pub fn sparse_pairwise_correlations(
589 moments: &[SparseColMoments],
590 pairs: &[(usize, usize)],
591 n_rows: usize,
592) -> Vec<f32> {
593 let mut grouped: FxHashMap<usize, Vec<(usize, usize)>> = FxHashMap::default();
594 for (slot, &(a, b)) in pairs.iter().enumerate() {
595 grouped.entry(a).or_default().push((slot, b));
596 }
597 let groups: Vec<(usize, Vec<(usize, usize)>)> = grouped.into_iter().collect();
598
599 let n = n_rows as f64;
600 let denom = n - 1.0;
601
602 let solved: Vec<Vec<(usize, f32)>> = groups
603 .par_iter()
604 .map_init(
605 || vec![0_f32; n_rows],
606 |scratch, (first, partners)| {
607 let a = &moments[*first];
608 for (&idx, &value) in a.indices.iter().zip(a.values.iter()) {
609 scratch[idx as usize] = value;
610 }
611
612 let out = partners
613 .iter()
614 .map(|&(slot, second)| {
615 let b = &moments[second];
616 if a.sd < SPARSE_COR_SD_EPS || b.sd < SPARSE_COR_SD_EPS {
617 return (slot, 0_f32);
618 }
619 let mut cross = 0_f64;
622 for (&idx, &value) in b.indices.iter().zip(b.values.iter()) {
623 cross += scratch[idx as usize] as f64 * value as f64;
624 }
625 let cov = cross - n * a.mean * b.mean;
626 let cor = cov / (denom * a.sd * b.sd);
627 (slot, (cor as f32).clamp(-1_f32, 1_f32))
628 })
629 .collect();
630
631 for &idx in &a.indices {
634 scratch[idx as usize] = 0_f32;
635 }
636
637 out
638 },
639 )
640 .collect();
641
642 let mut res = vec![0_f32; pairs.len()];
643 for group in solved {
644 for (slot, value) in group {
645 res[slot] = value;
646 }
647 }
648 res
649}
650
651const LANCZOS_RESTART_GUARD: usize = 5;
661
662const LANCZOS_ORTHO_REFINE_RATIO: f64 = 0.707;
668
669const LANCZOS_BREAKDOWN_RATIO: f64 = 1e-12;
676
677#[derive(Clone, Copy, Debug)]
683pub struct LanczosParams {
684 pub basis_size: Option<usize>,
688 pub max_restarts: usize,
692 pub tol: f64,
697}
698
699impl LanczosParams {
700 pub fn new() -> Self {
707 Self {
708 basis_size: None,
709 max_restarts: 64,
710 tol: 1e-8,
711 }
712 }
713
714 fn resolve_basis(&self, n_components: usize, n: usize) -> usize {
725 self.basis_size
726 .unwrap_or_else(|| (n_components * 2 + 10).max(n_components + 20))
727 .max(n_components + 1)
728 .min(n)
729 .max(1)
730 }
731}
732
733impl Default for LanczosParams {
734 fn default() -> Self {
735 Self::new()
736 }
737}
738
739#[derive(Clone, Debug)]
745pub struct LanczosResult {
746 pub eigenvalues: Vec<f64>,
750 pub eigenvectors: Vec<Vec<f32>>,
753 pub residual: f64,
755 pub norm_estimate: f64,
757 pub converged: bool,
759 pub restarts: usize,
761}
762
763#[derive(Clone, Debug)]
766pub struct RitzPairs {
767 pub values: Vec<f64>,
769 pub vectors: Vec<f64>,
772 pub residual: f64,
774 pub norm_estimate: f64,
776 pub converged: bool,
778 pub restarts: usize,
780}
781
782fn dot<T>(a: &[T], b: &[T]) -> T
797where
798 T: BixverseSimd,
799{
800 assert_same_len!(a, b);
801 T::bxv_dot_simd(a, b)
802}
803
804fn norm<T>(v: &[T]) -> T
814where
815 T: BixverseSimd + BixverseFloat,
816{
817 let dot = dot(v, v);
818 dot.sqrt()
819}
820
821fn normalise<T>(v: &mut [T])
827where
828 T: BixverseSimd + BixverseFloat,
829{
830 let n = norm(v);
831 v.par_iter_mut().for_each(|x| *x /= n);
832}
833
834fn lanczos_thick_restart<M>(
876 n: usize,
877 n_components: usize,
878 seed: u64,
879 params: LanczosParams,
880 matvec: M,
881) -> Result<RitzPairs, CoreErrors>
882where
883 M: Fn(&[f64], &mut [f64]),
884{
885 if n_components == 0 || n == 0 {
886 return Err(CoreErrors::MustBePositive(
887 "n_components and the matrix dimension".to_string(),
888 ));
889 }
890
891 let n_keep = n_components.min(n);
892 let m = params.resolve_basis(n_keep, n);
893
894 let mut basis = vec![0.0f64; m * n];
897 let mut h = Mat::<f64>::zeros(m, m);
898 let mut w = vec![0.0f64; n];
899 let mut coeffs = vec![0.0f64; m];
900
901 let mut rng = StdRng::seed_from_u64(seed);
902 for x in basis[..n].iter_mut() {
903 *x = rng.random::<f64>() - 0.5;
904 }
905 normalise(&mut basis[..n]);
906
907 let mut locked = 0usize;
909 let mut ritz_values: Vec<f64> = Vec::new();
910 let mut ritz_coeffs = Mat::<f64>::zeros(0, 0);
911 let mut m_final = 0usize;
912
913 let mut norm_estimate = 0.0f64;
915 let mut residual = f64::INFINITY;
916 let mut converged = false;
917 let mut restarts = 0usize;
918
919 let budget = params.max_restarts.max(1);
920
921 for cycle in 0..budget {
922 restarts = cycle + 1;
923 let mut m_eff = m;
924 let mut last_beta = 0.0f64;
925
926 for j in locked..m {
927 matvec(&basis[j * n..(j + 1) * n], &mut w);
928
929 let width = j + 1;
930
931 basis_project(&basis, n, width, &w, &mut coeffs[..width]);
935 for i in 0..width {
936 h[(i, j)] = coeffs[i];
937 h[(j, i)] = coeffs[i];
938 }
939
940 let before = norm(&w);
941 norm_estimate = norm_estimate.max(before);
942
943 basis_subtract(&basis, n, width, &coeffs[..width], &mut w);
944 last_beta = norm(&w);
945
946 if last_beta < LANCZOS_ORTHO_REFINE_RATIO * before {
949 basis_project(&basis, n, width, &w, &mut coeffs[..width]);
950 basis_subtract(&basis, n, width, &coeffs[..width], &mut w);
951 last_beta = norm(&w);
952 }
953
954 if last_beta <= LANCZOS_BREAKDOWN_RATIO * norm_estimate {
957 m_eff = width;
958 last_beta = 0.0;
959 break;
960 }
961
962 if width < m {
963 let inv = 1.0 / last_beta;
964 let next = &mut basis[width * n..(width + 1) * n];
965 for (dst, &src) in next.iter_mut().zip(w.iter()) {
966 *dst = src * inv;
967 }
968 }
969 }
970
971 let (values, vectors) = symmetric_eigen_descending(h.as_ref(), m_eff)?;
972
973 let n_out = n_keep.min(m_eff);
976 residual = (0..n_out).fold(0.0f64, |acc, i| {
977 acc.max(last_beta * vectors[(m_eff - 1, i)].abs())
978 });
979
980 norm_estimate = norm_estimate
984 .max(values[0].abs())
985 .max(values[m_eff - 1].abs());
986 converged = residual <= params.tol * norm_estimate;
987
988 ritz_values = values;
989 ritz_coeffs = vectors;
990 m_final = m_eff;
991
992 if converged || m_eff < m || last_beta == 0.0 || cycle + 1 == budget {
993 break;
994 }
995
996 let keep = (n_keep + LANCZOS_RESTART_GUARD).min(m / 2).max(1);
1003 let mut restarted = vec![0.0f64; keep * n];
1004 basis_expand(&basis, n, m_eff, ritz_coeffs.as_ref(), keep, &mut restarted);
1005
1006 let inv = 1.0 / last_beta;
1007 for x in w.iter_mut() {
1008 *x *= inv;
1009 }
1010
1011 h.fill(0.0);
1012 basis[..keep * n].copy_from_slice(&restarted);
1013 for (i, &value) in ritz_values.iter().take(keep).enumerate() {
1014 h[(i, i)] = value;
1015 }
1016 basis[keep * n..(keep + 1) * n].copy_from_slice(&w);
1017 locked = keep;
1018 }
1019
1020 let n_out = n_keep.min(m_final);
1024 let mut ritz = vec![0.0f64; n_out * n];
1025 basis_expand(&basis, n, m_final, ritz_coeffs.as_ref(), n_out, &mut ritz);
1026
1027 for column in ritz.chunks_exact_mut(n) {
1028 let scale: f64 = column.iter().map(|v| v * v).sum::<f64>().sqrt();
1029 let inv = if scale > 0.0 { 1.0 / scale } else { 0.0 };
1030 column.iter_mut().for_each(|v| *v *= inv);
1031 }
1032
1033 Ok(RitzPairs {
1034 values: ritz_values[..n_out].to_vec(),
1035 vectors: ritz,
1036 residual,
1037 norm_estimate,
1038 converged,
1039 restarts,
1040 })
1041}
1042
1043fn basis_project(basis: &[f64], n: usize, width: usize, v: &[f64], out: &mut [f64]) {
1057 let b = MatRef::from_column_major_slice(&basis[..width * n], n, width);
1058 let v = MatRef::from_column_major_slice(v, n, 1);
1059 let mut out = MatMut::from_column_major_slice_mut(out, width, 1);
1060
1061 gemm(
1062 out.as_mut(),
1063 Accum::Replace,
1064 b.transpose(),
1065 v,
1066 1.0,
1067 faer_parallelism(),
1068 );
1069}
1070
1071fn basis_subtract(basis: &[f64], n: usize, width: usize, coeffs: &[f64], v: &mut [f64]) {
1085 let b = MatRef::from_column_major_slice(&basis[..width * n], n, width);
1086 let c = MatRef::from_column_major_slice(coeffs, width, 1);
1087 let mut v = MatMut::from_column_major_slice_mut(v, n, 1);
1088
1089 gemm(v.as_mut(), Accum::Add, b, c, -1.0, faer_parallelism());
1090}
1091
1092fn basis_expand(
1107 basis: &[f64],
1108 n: usize,
1109 width: usize,
1110 coeffs: MatRef<f64>,
1111 n_out: usize,
1112 out: &mut [f64],
1113) {
1114 let b = MatRef::from_column_major_slice(&basis[..width * n], n, width);
1115 let mut out = MatMut::from_column_major_slice_mut(out, n, n_out);
1116
1117 gemm(
1118 out.as_mut(),
1119 Accum::Replace,
1120 b,
1121 coeffs.subcols(0, n_out),
1122 1.0,
1123 faer_parallelism(),
1124 );
1125}
1126
1127pub(crate) fn symmetric_eigen_descending(
1139 h: MatRef<f64>,
1140 size: usize,
1141) -> Result<(Vec<f64>, Mat<f64>), CoreErrors> {
1142 let block = Mat::<f64>::from_fn(size, size, |i, j| h[(i, j)]);
1143
1144 let eig = block
1145 .self_adjoint_eigen(faer::Side::Lower)
1146 .map_err(|_| CoreErrors::FaerEigenError)?;
1147
1148 let raw_values = eig.S();
1149 let raw_vectors = eig.U();
1150
1151 let mut order: Vec<usize> = (0..size).collect();
1152 order.sort_by(|&a, &b| raw_values[b].total_cmp(&raw_values[a]));
1153
1154 let values: Vec<f64> = order.iter().map(|&i| raw_values[i]).collect();
1155 let vectors = Mat::<f64>::from_fn(size, size, |i, j| raw_vectors[(i, order[j])]);
1156
1157 Ok((values, vectors))
1158}
1159
1160pub fn compute_largest_eigenpairs_lanczos<T>(
1183 matrix: &CompressedSparseData2<T>,
1184 n_components: usize,
1185 seed: u64,
1186 params: Option<LanczosParams>,
1187) -> Result<(Vec<f64>, Vec<Vec<f32>>), CoreErrors>
1188where
1189 T: BixverseNumeric + BixverseSimd + Into<f64>,
1190{
1191 let res = compute_largest_eigenpairs_lanczos_diag(matrix, n_components, seed, params)?;
1192 Ok((res.eigenvalues, res.eigenvectors))
1193}
1194
1195pub fn compute_largest_eigenpairs_lanczos_diag<T>(
1214 matrix: &CompressedSparseData2<T>,
1215 n_components: usize,
1216 seed: u64,
1217 params: Option<LanczosParams>,
1218) -> Result<LanczosResult, CoreErrors>
1219where
1220 T: BixverseNumeric + BixverseSimd + Into<f64>,
1221{
1222 let n = matrix.shape.0;
1223
1224 let csr = match matrix.cs_type {
1226 CompressedSparseFormat::Csr => matrix.clone(),
1227 CompressedSparseFormat::Csc => matrix.transform(),
1228 };
1229 let data_f64: Vec<f64> = csr.data.iter().map(|&v| v.into()).collect();
1230
1231 let matvec = |x: &[f64], y: &mut [f64]| {
1232 y.par_iter_mut().enumerate().for_each(|(i, yi)| {
1233 let mut sum = 0.0;
1234 for idx in csr.indptr[i] as usize..csr.indptr[i + 1] as usize {
1235 let j = csr.indices[idx] as usize;
1236 sum += data_f64[idx] * x[j];
1237 }
1238 *yi = sum;
1239 });
1240 };
1241
1242 let ritz = lanczos_thick_restart(n, n_components, seed, params.unwrap_or_default(), matvec)?;
1243
1244 let n_out = ritz.values.len();
1245 let mut eigenvectors = vec![vec![0.0f32; n_out]; n];
1246 for comp in 0..n_out {
1247 let column = &ritz.vectors[comp * n..(comp + 1) * n];
1248 for (point, &v) in column.iter().enumerate() {
1249 eigenvectors[point][comp] = v as f32;
1250 }
1251 }
1252
1253 Ok(LanczosResult {
1254 eigenvalues: ritz.values,
1255 eigenvectors,
1256 residual: ritz.residual,
1257 norm_estimate: ritz.norm_estimate,
1258 converged: ritz.converged,
1259 restarts: ritz.restarts,
1260 })
1261}
1262
1263const BLOCK_LANCZOS_MIN_NEW_BLOCKS: usize = 3;
1277
1278#[derive(Clone, Copy, Debug)]
1284pub struct BlockLanczosParams {
1285 pub block_size: usize,
1289 pub basis_size: Option<usize>,
1294 pub max_restarts: usize,
1296 pub tol: f64,
1299}
1300
1301impl BlockLanczosParams {
1302 pub fn new() -> Self {
1313 Self {
1314 block_size: 32,
1315 basis_size: None,
1316 max_restarts: 64,
1317 tol: 1e-6,
1318 }
1319 }
1320
1321 fn resolve(&self, n_keep: usize, n: usize) -> (usize, usize, usize) {
1336 let b = self.block_size.min(n / 2).max(1);
1337 let round_up = |x: usize| x.div_ceil(b) * b;
1338 let floor = |x: usize| (x / b) * b;
1339
1340 let keep_target = round_up(n_keep + LANCZOS_RESTART_GUARD);
1341 let basis = match self.basis_size {
1342 Some(size) => floor(size),
1343 None => round_up((n_keep * 2 + 10).max(n_keep + 20))
1344 .max(2 * keep_target)
1345 .max(keep_target + BLOCK_LANCZOS_MIN_NEW_BLOCKS * b),
1346 }
1347 .max(2 * b)
1348 .min(floor(n))
1349 .max(b);
1350 let keep = keep_target.min(floor(basis / 2)).max(b);
1351
1352 (b, basis, keep)
1353 }
1354}
1355
1356impl Default for BlockLanczosParams {
1357 fn default() -> Self {
1358 Self::new()
1359 }
1360}
1361
1362fn check_operator_output(out: &Mat<f64>, n: usize, width: usize) -> Result<(), CoreErrors> {
1378 if out.nrows() != n || out.ncols() != width {
1379 return Err(CoreErrors::InvalidArgument(format!(
1380 "block Lanczos: operator returned {} x {}, expected {n} x {width}",
1381 out.nrows(),
1382 out.ncols()
1383 )));
1384 }
1385 Ok(())
1386}
1387
1388fn dense_eigenpairs<E, M>(n: usize, n_keep: usize, b: usize, op: &mut M) -> Result<RitzPairs, E>
1401where
1402 E: From<CoreErrors>,
1403 M: FnMut(MatRef<f64>) -> Result<Mat<f64>, E>,
1404{
1405 let mut a = Mat::<f64>::zeros(n, n);
1406 for col in (0..n).step_by(b) {
1407 let w = b.min(n - col);
1408 let unit = Mat::<f64>::from_fn(n, w, |i, c| if i == col + c { 1.0 } else { 0.0 });
1409 let out = op(unit.as_ref())?;
1410 check_operator_output(&out, n, w)?;
1411 a.subcols_mut(col, w).copy_from(&out);
1412 }
1413 let sym = Mat::<f64>::from_fn(n, n, |i, j| 0.5 * (a[(i, j)] + a[(j, i)]));
1415
1416 let (values, vectors) = symmetric_eigen_descending(sym.as_ref(), n)?;
1417 let flat: Vec<f64> = (0..n_keep)
1418 .flat_map(|c| vectors.col(c).iter().copied().collect::<Vec<_>>())
1419 .collect();
1420
1421 Ok(RitzPairs {
1422 values: values[..n_keep].to_vec(),
1423 vectors: flat,
1424 residual: 0.0,
1425 norm_estimate: values[0].abs().max(values[n - 1].abs()),
1426 converged: true,
1427 restarts: 0,
1428 })
1429}
1430
1431fn column_norms(x: MatRef<f64>) -> Vec<f64> {
1441 (0..x.ncols()).map(|c| x.col(c).norm_l2()).collect()
1442}
1443
1444fn orthonormalise_block(
1467 w: MatRef<f64>,
1468 basis: MatRef<f64>,
1469 threshold: f64,
1470 rng: &mut StdRng,
1471) -> (Mat<f64>, Mat<f64>, usize) {
1472 let (n, b) = w.shape();
1473 let mut q = w.to_owned();
1474 let mut r = Mat::<f64>::zeros(b, b);
1475 let mut n_deficient = 0usize;
1476
1477 for c in 0..b {
1478 for _ in 0..2 {
1479 for i in 0..c {
1480 let d = q.col(i).transpose() * q.col(c);
1481 r[(i, c)] += d;
1482 for k in 0..n {
1483 q[(k, c)] -= d * q[(k, i)];
1484 }
1485 }
1486 }
1487
1488 let remainder = q.col(c).norm_l2();
1489 if remainder > threshold {
1490 r[(c, c)] = remainder;
1491 let inv = 1.0 / remainder;
1492 q.col_mut(c).iter_mut().for_each(|x| *x *= inv);
1493 continue;
1494 }
1495
1496 n_deficient += 1;
1497 let mut x = Mat::<f64>::from_fn(n, 1, |_, _| rng.random::<f64>() - 0.5);
1498 for _ in 0..2 {
1499 let mut coeffs = Mat::<f64>::zeros(basis.ncols(), 1);
1500 gemm(
1501 coeffs.as_mut(),
1502 Accum::Replace,
1503 basis.transpose(),
1504 x.as_ref(),
1505 1.0,
1506 Par::Seq,
1507 );
1508 gemm(
1509 x.as_mut(),
1510 Accum::Add,
1511 basis,
1512 coeffs.as_ref(),
1513 -1.0,
1514 Par::Seq,
1515 );
1516 for i in 0..c {
1517 let d = q.col(i).transpose() * x.col(0);
1518 for k in 0..n {
1519 x[(k, 0)] -= d * q[(k, i)];
1520 }
1521 }
1522 }
1523 let scale = x.col(0).norm_l2();
1524 for k in 0..n {
1525 q[(k, c)] = x[(k, 0)] / scale;
1526 }
1527 }
1528
1529 (q, r, n_deficient)
1530}
1531
1532pub fn block_lanczos_thick_restart<E, M>(
1577 n: usize,
1578 n_components: usize,
1579 seed: u64,
1580 params: BlockLanczosParams,
1581 mut op: M,
1582) -> Result<RitzPairs, E>
1583where
1584 E: From<CoreErrors>,
1585 M: FnMut(MatRef<f64>) -> Result<Mat<f64>, E>,
1586{
1587 if n_components == 0 || n == 0 {
1588 return Err(CoreErrors::MustBePositive(
1589 "n_components and the operator dimension".to_string(),
1590 )
1591 .into());
1592 }
1593
1594 let n_keep = n_components.min(n);
1595 let (b, m, keep) = params.resolve(n_keep, n);
1596 let par = faer_parallelism();
1597
1598 if m + b > n {
1599 return dense_eigenpairs(n, n_keep, b, &mut op);
1600 }
1601
1602 let mut rng = StdRng::seed_from_u64(seed);
1603 let mut basis = Mat::<f64>::zeros(n, m);
1604 let mut h = Mat::<f64>::zeros(m, m);
1605 let start = Mat::<f64>::from_fn(n, b, |_, _| rng.random::<f64>() - 0.5);
1606 basis
1607 .subcols_mut(0, b)
1608 .copy_from(start.qr().compute_thin_Q());
1609
1610 let mut resid_q = Mat::<f64>::zeros(n, b);
1612 let mut resid_r = Mat::<f64>::zeros(b, b);
1613
1614 let mut locked = 0usize;
1615 let mut ritz_values: Vec<f64> = Vec::new();
1616 let mut ritz_coeffs = Mat::<f64>::zeros(0, 0);
1617 let mut m_final = 0usize;
1618
1619 let mut norm_estimate = 0.0f64;
1620 let mut residual = f64::INFINITY;
1621 let mut converged = false;
1622 let mut restarts = 0usize;
1623
1624 let budget = params.max_restarts.max(1);
1625
1626 for cycle in 0..budget {
1627 restarts = cycle + 1;
1628 let mut m_eff = m;
1629 let mut invariant = false;
1630
1631 let mut j = locked;
1632 while j < m {
1633 let width = j + b;
1634 let mut w = op(basis.subcols(j, b))?;
1635 check_operator_output(&w, n, b)?;
1636
1637 let v = basis.subcols(0, width);
1640 let mut coeffs = Mat::<f64>::zeros(width, b);
1641 gemm(
1642 coeffs.as_mut(),
1643 Accum::Replace,
1644 v.transpose(),
1645 w.as_ref(),
1646 1.0,
1647 par,
1648 );
1649 for c in 0..b {
1650 for i in 0..width {
1651 h[(i, j + c)] = coeffs[(i, c)];
1652 h[(j + c, i)] = coeffs[(i, c)];
1653 }
1654 }
1655
1656 let before = column_norms(w.as_ref());
1657 norm_estimate = before.iter().fold(norm_estimate, |acc, &x| acc.max(x));
1658
1659 gemm(w.as_mut(), Accum::Add, v, coeffs.as_ref(), -1.0, par);
1660 let after = column_norms(w.as_ref());
1661
1662 if after
1663 .iter()
1664 .zip(&before)
1665 .any(|(&a, &bf)| a < LANCZOS_ORTHO_REFINE_RATIO * bf)
1666 {
1667 gemm(
1668 coeffs.as_mut(),
1669 Accum::Replace,
1670 v.transpose(),
1671 w.as_ref(),
1672 1.0,
1673 par,
1674 );
1675 gemm(w.as_mut(), Accum::Add, v, coeffs.as_ref(), -1.0, par);
1676 }
1677
1678 let threshold = LANCZOS_BREAKDOWN_RATIO * norm_estimate;
1679 let (q, r, n_deficient) = orthonormalise_block(w.as_ref(), v, threshold, &mut rng);
1680
1681 if n_deficient == b {
1682 m_eff = width;
1683 invariant = true;
1684 break;
1685 }
1686
1687 if width < m {
1688 basis.subcols_mut(width, b).copy_from(&q);
1689 } else {
1690 resid_q = q;
1691 resid_r = r;
1692 }
1693 j = width;
1694 }
1695
1696 let (values, vectors) = symmetric_eigen_descending(h.as_ref(), m_eff)?;
1697
1698 let n_out = n_keep.min(m_eff);
1699 residual = if invariant {
1700 0.0
1701 } else {
1702 let mut y_last = Mat::<f64>::zeros(b, n_out);
1703 y_last.copy_from(vectors.submatrix(m_eff - b, 0, b, n_out));
1704 let mut ry = Mat::<f64>::zeros(b, n_out);
1705 gemm(
1706 ry.as_mut(),
1707 Accum::Replace,
1708 resid_r.as_ref(),
1709 y_last.as_ref(),
1710 1.0,
1711 Par::Seq,
1712 );
1713 column_norms(ry.as_ref()).into_iter().fold(0.0f64, f64::max)
1714 };
1715
1716 norm_estimate = norm_estimate
1717 .max(values[0].abs())
1718 .max(values[m_eff - 1].abs());
1719 converged = residual <= params.tol * norm_estimate;
1720
1721 ritz_values = values;
1722 ritz_coeffs = vectors;
1723 m_final = m_eff;
1724
1725 if converged || invariant || cycle + 1 == budget {
1726 break;
1727 }
1728
1729 let mut restarted = Mat::<f64>::zeros(n, keep);
1731 gemm(
1732 restarted.as_mut(),
1733 Accum::Replace,
1734 basis.subcols(0, m_eff),
1735 ritz_coeffs.subcols(0, keep),
1736 1.0,
1737 par,
1738 );
1739
1740 h.fill(0.0);
1741 basis.subcols_mut(0, keep).copy_from(&restarted);
1742 for (i, &value) in ritz_values.iter().take(keep).enumerate() {
1743 h[(i, i)] = value;
1744 }
1745 basis.subcols_mut(keep, b).copy_from(&resid_q);
1746 locked = keep;
1747 }
1748
1749 let n_out = n_keep.min(m_final);
1750 let mut ritz = Mat::<f64>::zeros(n, n_out);
1751 gemm(
1752 ritz.as_mut(),
1753 Accum::Replace,
1754 basis.subcols(0, m_final),
1755 ritz_coeffs.subcols(0, n_out),
1756 1.0,
1757 par,
1758 );
1759
1760 let mut vectors = vec![0.0f64; n * n_out];
1761 for (c, column) in vectors.chunks_exact_mut(n).enumerate() {
1762 let src = ritz.col(c);
1763 let scale = src.norm_l2();
1764 let inv = if scale > 0.0 { 1.0 / scale } else { 0.0 };
1765 for (dst, &v) in column.iter_mut().zip(src.iter()) {
1766 *dst = v * inv;
1767 }
1768 }
1769
1770 Ok(RitzPairs {
1771 values: ritz_values[..n_out].to_vec(),
1772 vectors,
1773 residual,
1774 norm_estimate,
1775 converged,
1776 restarts,
1777 })
1778}
1779
1780pub fn sparse_svd_lanczos<T, U, F>(
1808 matrix: &CompressedSparseData2<T, U>,
1809 n_components: usize,
1810 seed: u64,
1811 use_second_layer: bool,
1812 col_means: Option<&[F]>,
1813 col_stds: Option<&[F]>,
1814 row_offsets: Option<&[F]>,
1815) -> Result<SvdResults<F>, CoreErrors>
1816where
1817 T: BixverseNumeric + BixverseSimd + Into<F> + Clone,
1818 U: BixverseNumeric + Into<F> + Clone,
1819 F: BixverseFloat + BixverseSimd + std::iter::Sum,
1820{
1821 let (n, m) = matrix.shape;
1822 let use_ata = n > m;
1823 let krylov_dim = if use_ata { m } else { n };
1824
1825 let (csr, csc);
1827 let csr_owned;
1828 let csc_owned;
1829
1830 match matrix.cs_type {
1831 CompressedSparseFormat::Csr => {
1832 csr = matrix;
1833 csc_owned = matrix.transform_single_layer(use_second_layer)?;
1834 csc = &csc_owned;
1835 }
1836 CompressedSparseFormat::Csc => {
1837 csc = matrix;
1838 csr_owned = matrix.transform_single_layer(use_second_layer)?;
1839 csr = &csr_owned;
1840 }
1841 };
1842
1843 let extract_data = |mat: &CompressedSparseData2<T, U>| -> Vec<F> {
1845 if use_second_layer {
1846 mat.data_2
1847 .as_ref()
1848 .expect("data_2 is None but use_second_layer is true")
1849 .iter()
1850 .copied()
1851 .map(|v| v.into())
1852 .collect()
1853 } else {
1854 mat.data.iter().copied().map(|v| v.into()).collect()
1855 }
1856 };
1857
1858 let data_csr_f = extract_data(csr);
1859 let data_csc_f = extract_data(csc);
1860
1861 let matvec_a = |x: &[F], y: &mut [F]| {
1863 let x_scaled: Vec<F> = if let Some(sd) = col_stds {
1864 x.iter().enumerate().map(|(j, &v)| v / sd[j]).collect()
1865 } else {
1866 x.to_vec()
1867 };
1868 let mean_dot: F = if let Some(mu) = col_means {
1869 x_scaled.iter().enumerate().map(|(j, &v)| mu[j] * v).sum()
1870 } else {
1871 F::zero()
1872 };
1873 let x_sum: F = if row_offsets.is_some() {
1874 x_scaled.iter().copied().sum()
1875 } else {
1876 F::zero()
1877 };
1878
1879 y.par_iter_mut().enumerate().for_each(|(i, yi)| {
1880 let mut sum = F::zero();
1881 for idx in csr.indptr[i] as usize..csr.indptr[i + 1] as usize {
1882 let j = csr.indices[idx] as usize;
1883 sum += data_csr_f[idx] * x_scaled[j];
1884 }
1885 if col_means.is_some() {
1886 sum -= mean_dot;
1887 }
1888 if let Some(off) = row_offsets {
1889 sum -= off[i] * x_sum;
1890 }
1891 *yi = sum;
1892 });
1893 };
1894
1895 let matvec_at = |x: &[F], y: &mut [F]| {
1896 let x_sum: F = x.iter().copied().sum();
1897 let m_dot_x: F = if let Some(off) = row_offsets {
1898 off.iter().zip(x.iter()).map(|(&m_i, &xi)| m_i * xi).sum()
1899 } else {
1900 F::zero()
1901 };
1902
1903 y.par_iter_mut().enumerate().for_each(|(j, yj)| {
1904 let mut sum = F::zero();
1905 for idx in csc.indptr[j] as usize..csc.indptr[j + 1] as usize {
1906 let i = csc.indices[idx] as usize;
1907 sum += data_csc_f[idx] * x[i];
1908 }
1909 if let Some(mu) = col_means {
1910 sum -= mu[j] * x_sum;
1911 }
1912 if row_offsets.is_some() {
1913 sum -= m_dot_x;
1914 }
1915 if let Some(sd) = col_stds {
1916 sum /= sd[j];
1917 }
1918 *yj = sum;
1919 });
1920 };
1921
1922 #[allow(clippy::type_complexity)]
1924 let matvec_gram: Box<dyn Fn(&[F], &mut [F]) + Sync> = if use_ata {
1925 Box::new(|x: &[F], y: &mut [F]| {
1926 let mut temp = vec![F::zero(); n];
1927 matvec_a(x, &mut temp);
1928 matvec_at(&temp, y);
1929 })
1930 } else {
1931 Box::new(|x: &[F], y: &mut [F]| {
1932 let mut temp = vec![F::zero(); m];
1933 matvec_at(x, &mut temp);
1934 matvec_a(&temp, y);
1935 })
1936 };
1937
1938 let matvec_gram_f64 = |x: &[f64], y: &mut [f64]| {
1940 let x_f: Vec<F> = x
1941 .iter()
1942 .map(|&v| F::from(v).expect("f64 casts into any float type"))
1943 .collect();
1944 let mut y_f = vec![F::zero(); krylov_dim];
1945 matvec_gram(&x_f, &mut y_f);
1946 for (dst, src) in y.iter_mut().zip(y_f) {
1947 *dst = src.to_f64().expect("float casts into f64");
1948 }
1949 };
1950
1951 let ritz = lanczos_thick_restart(
1952 krylov_dim,
1953 n_components,
1954 seed,
1955 LanczosParams::default(),
1956 matvec_gram_f64,
1957 )?;
1958
1959 let mut singular_values: Vec<F> = Vec::with_capacity(n_components);
1960 let mut u_vecs: Vec<Vec<F>> = Vec::with_capacity(n_components);
1961 let mut v_vecs: Vec<Vec<F>> = Vec::with_capacity(n_components);
1962
1963 for (idx, &eval) in ritz.values.iter().enumerate() {
1964 if eval <= 0.0 {
1965 continue;
1966 }
1967
1968 let sigma = F::from(eval.sqrt()).expect("f64 casts into any float type");
1969 singular_values.push(sigma);
1970
1971 let gram_evec: Vec<F> = ritz.vectors[idx * krylov_dim..(idx + 1) * krylov_dim]
1972 .iter()
1973 .map(|&v| F::from(v).expect("f64 casts into any float type"))
1974 .collect();
1975
1976 if use_ata {
1977 let mut u_vec = vec![F::zero(); n];
1978 matvec_a(&gram_evec, &mut u_vec);
1979 for x in &mut u_vec {
1980 *x /= sigma;
1981 }
1982 u_vecs.push(u_vec);
1983 v_vecs.push(gram_evec);
1984 } else {
1985 let mut v_vec = vec![F::zero(); m];
1986 matvec_at(&gram_evec, &mut v_vec);
1987 for x in &mut v_vec {
1988 *x /= sigma;
1989 }
1990 u_vecs.push(gram_evec);
1991 v_vecs.push(v_vec);
1992 }
1993 }
1994
1995 let u = Mat::from_fn(n, singular_values.len(), |i, j| u_vecs[j][i]);
1996 let v = Mat::from_fn(m, singular_values.len(), |i, j| v_vecs[j][i]);
1997
1998 Ok(SvdResults {
1999 u,
2000 s: singular_values,
2001 v,
2002 })
2003}
2004
2005#[cfg(test)]
2010mod tests {
2011 use super::*;
2012 use crate::core::math::vector_helpers::rank_vector;
2013 use faer::Mat;
2014
2015 #[test]
2017 fn test_from_dense_and_count_zeroes() {
2018 let mat: Mat<f64> = Mat::from_fn(3, 2, |i, j| if i == j { (i + 1) as f64 } else { 0.0 });
2019 let (total_zeroes, row_zeroes, col_zeroes) = count_zeroes(&mat.as_ref());
2020
2021 assert_eq!(total_zeroes, 4);
2022 assert_eq!(row_zeroes, vec![1, 1, 2]);
2023 assert_eq!(col_zeroes, vec![2, 2]);
2024
2025 let csr = CompressedSparseData2::<f64, f64>::from_dense_matrix(
2026 mat.as_ref(),
2027 CompressedSparseFormat::Csr,
2028 );
2029 assert_eq!(csr.shape, (3, 2));
2030 assert_eq!(csr.data, vec![1.0, 2.0]);
2031 }
2032
2033 #[test]
2035 fn test_lanczos_eigenpairs_logic() {
2036 let data = vec![1.0, 2.0, 2.0, 4.0];
2039 let indices = vec![0, 2, 0, 2];
2040 let indptr = vec![0, 2, 2, 4, 4]; let shape = (4, 4);
2042
2043 let csr = CompressedSparseData2::<f64, f64>::new_csr(&data, &indices, &indptr, None, shape);
2044
2045 let (evals, evecs) = compute_largest_eigenpairs_lanczos(&csr, 1, 42, None).unwrap();
2047
2048 assert!((evals[0] - 5.0).abs() < 1e-3);
2050
2051 let x_norm = 5.0_f32.sqrt();
2054 let dot_x = (evecs[0][0] * 1.0 + evecs[2][0] * 2.0) / x_norm;
2055
2056 assert!(dot_x.abs() > 0.999);
2057 }
2058
2059 #[test]
2061 fn test_sparse_svd_lanczos_logic() {
2062 let data = vec![2.0, 1.0, 4.0, 2.0];
2066 let indices = vec![0, 2, 0, 2];
2067 let indptr = vec![0, 0, 2, 2, 4];
2068 let shape = (4, 3);
2069
2070 let csr = CompressedSparseData2::<f64, f64>::new_csr(&data, &indices, &indptr, None, shape);
2071 let no_params: Option<&[f64]> = None;
2072
2073 let svd = sparse_svd_lanczos(&csr, 1, 42, false, no_params, no_params, None).unwrap();
2074
2075 let u_col = svd.u.col(0);
2077 let x_norm = 20.0_f64.sqrt();
2078 let dot_u = (u_col[1] * 2.0 + u_col[3] * 4.0) / x_norm;
2079 assert!(dot_u.abs() > 0.999);
2080
2081 let v_col = svd.v.col(0);
2083 let y_norm = 1.25_f64.sqrt();
2084 let dot_v = (v_col[0] * 1.0 + v_col[2] * 0.5) / y_norm;
2085 assert!(dot_v.abs() > 0.999);
2086 }
2087
2088 #[test]
2093 fn test_sparse_svd_lanczos_flat_spectrum() {
2094 let (n, m, k) = (400usize, 200usize, 30usize);
2095 let mut rng = StdRng::seed_from_u64(1);
2096 let vals: Vec<f64> = (0..n * m).map(|_| rng.random::<f64>()).collect();
2097 let indices: Vec<u32> = (0..n).flat_map(|_| 0..m as u32).collect();
2098 let indptr: Vec<u32> = (0..=n).map(|i| (i * m) as u32).collect();
2099 let csr =
2100 CompressedSparseData2::<f64, f64>::new_csr(&vals, &indices, &indptr, None, (n, m));
2101
2102 let means: Vec<f64> = (0..m)
2103 .map(|j| (0..n).map(|i| vals[i * m + j]).sum::<f64>() / n as f64)
2104 .collect();
2105 let svd = sparse_svd_lanczos(&csr, k, 42, false, Some(&means[..]), None, None).unwrap();
2106
2107 let centred = Mat::<f64>::from_fn(n, m, |i, j| vals[i * m + j] - means[j]);
2108 let reference = centred.thin_svd().unwrap();
2109 let s_ref = reference.S().column_vector();
2110
2111 assert_eq!(svd.s.len(), k);
2112 for i in 0..k {
2113 assert_relative_eq!(svd.s[i], s_ref[i], max_relative = 1e-6);
2114 }
2115 }
2116
2117 use approx::assert_relative_eq;
2118
2119 fn path_graph(n: usize) -> CompressedSparseData2<f64, f64> {
2124 let mut rows: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
2125 for i in 0..n - 1 {
2126 rows[i].push(((i + 1) as u32, 1.0));
2127 rows[i + 1].push((i as u32, 1.0));
2128 }
2129 let mut indptr = vec![0u32];
2130 let mut indices = Vec::new();
2131 let mut data = Vec::new();
2132 for row in rows.iter_mut() {
2133 row.sort_by_key(|&(j, _)| j);
2134 for &(j, v) in row.iter() {
2135 indices.push(j);
2136 data.push(v);
2137 }
2138 indptr.push(indices.len() as u32);
2139 }
2140
2141 CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, n))
2142 }
2143
2144 fn diagonal(diag: &[f64]) -> CompressedSparseData2<f64, f64> {
2146 let indices: Vec<u32> = (0..diag.len() as u32).collect();
2147 let indptr: Vec<u32> = (0..=diag.len() as u32).collect();
2148
2149 CompressedSparseData2::new_csr(diag, &indices, &indptr, None, (diag.len(), diag.len()))
2150 }
2151
2152 fn pair_residual(
2154 mat: &CompressedSparseData2<f64, f64>,
2155 values: &[f64],
2156 vectors: &[Vec<f32>],
2157 k: usize,
2158 ) -> f64 {
2159 let n = mat.shape.0;
2160 let v: Vec<f64> = (0..n).map(|i| vectors[i][k] as f64).collect();
2161 let lam = values[k];
2162
2163 (0..n)
2164 .map(|i| {
2165 let mut sum = 0.0;
2166 for x in mat.indptr[i] as usize..mat.indptr[i + 1] as usize {
2167 sum += mat.data[x] * v[mat.indices[x] as usize];
2168 }
2169 (sum - lam * v[i]).powi(2)
2170 })
2171 .sum::<f64>()
2172 .sqrt()
2173 }
2174
2175 #[test]
2177 fn test_lanczos_resolves_a_clustered_spectrum_at_defaults() {
2178 let n = 300usize;
2179 let mat = path_graph(n);
2180
2181 let (values, vectors) = compute_largest_eigenpairs_lanczos(&mat, 4, 42, None).unwrap();
2182
2183 for k in 0..4 {
2184 let expected = 2.0 * ((k + 1) as f64 * std::f64::consts::PI / (n + 1) as f64).cos();
2185 assert_relative_eq!(values[k], expected, epsilon = 1e-3);
2186 }
2187
2188 let sign = vectors[0][0].signum();
2191 assert!(
2192 (0..n).all(|i| vectors[i][0].signum() == sign || vectors[i][0] == 0.0),
2193 "leading eigenvector of a path graph changed sign"
2194 );
2195 }
2196
2197 #[test]
2199 fn test_lanczos_restarts_converge_a_clustered_spectrum() {
2200 let n = 300usize;
2203 let mat = path_graph(n);
2204 let params = LanczosParams {
2205 basis_size: None,
2206 max_restarts: 64,
2207 tol: 1e-8,
2208 };
2209
2210 let res = compute_largest_eigenpairs_lanczos_diag(&mat, 4, 42, Some(params)).unwrap();
2211
2212 assert!(res.converged, "residual stalled at {:e}", res.residual);
2213 for k in 0..4 {
2214 let expected = 2.0 * ((k + 1) as f64 * std::f64::consts::PI / (n + 1) as f64).cos();
2215 assert_relative_eq!(res.eigenvalues[k], expected, epsilon = 1e-6);
2216
2217 let residual = pair_residual(&mat, &res.eigenvalues, &res.eigenvectors, k);
2218 assert!(residual < 1e-5, "pair {k} has residual {residual:e}");
2219 }
2220 }
2221
2222 #[test]
2224 fn test_lanczos_caps_components_at_the_matrix_dimension() {
2225 let mat = diagonal(&[4.0, 3.0, 2.0, 1.0]);
2227
2228 for requested in [4usize, 5, 40] {
2229 let (values, vectors) =
2230 compute_largest_eigenpairs_lanczos(&mat, requested, 11, None).unwrap();
2231
2232 assert_eq!(values.len(), 4, "requested {requested}");
2233 assert_eq!(vectors[0].len(), values.len(), "requested {requested}");
2234 assert_relative_eq!(values[0], 4.0, epsilon = 1e-9);
2235 assert_relative_eq!(values[3], 1.0, epsilon = 1e-9);
2236 }
2237 }
2238
2239 #[test]
2241 fn test_lanczos_breakdown_keeps_values_and_vectors_the_same_length() {
2242 let mut diag = vec![0.0f64; 12];
2247 diag[0] = 5.0;
2248 diag[1] = 3.0;
2249 let mat = diagonal(&diag);
2250
2251 let res = compute_largest_eigenpairs_lanczos_diag(&mat, 10, 3, None).unwrap();
2252
2253 assert!(res.eigenvalues.len() < 10, "expected an early breakdown");
2254 assert_eq!(res.eigenvalues.len(), res.eigenvectors[0].len());
2255 assert_relative_eq!(res.eigenvalues[0], 5.0, epsilon = 1e-9);
2256 assert_relative_eq!(res.eigenvalues[1], 3.0, epsilon = 1e-9);
2257 }
2258
2259 #[test]
2261 fn test_lanczos_reports_budget_exhaustion() {
2262 let mat = path_graph(300);
2265 let params = LanczosParams {
2266 basis_size: None,
2267 max_restarts: 1,
2268 tol: 1e-8,
2269 };
2270
2271 let res = compute_largest_eigenpairs_lanczos_diag(&mat, 4, 42, Some(params)).unwrap();
2272
2273 assert!(!res.converged);
2274 assert_eq!(res.restarts, 1);
2275 assert!(res.residual > params.tol * res.norm_estimate);
2276 }
2277
2278 #[test]
2280 fn test_lanczos_tolerance_is_relative_to_the_matrix_norm() {
2281 let small = diagonal(&[4.0, 3.0, 2.0, 1.0]);
2285 let large = diagonal(&[4e8, 3e8, 2e8, 1e8]);
2286
2287 let a = compute_largest_eigenpairs_lanczos_diag(&small, 2, 5, None).unwrap();
2288 let b = compute_largest_eigenpairs_lanczos_diag(&large, 2, 5, None).unwrap();
2289
2290 assert!(a.converged && b.converged);
2291 assert_relative_eq!(b.eigenvalues[0] / a.eigenvalues[0], 1e8, epsilon = 1e-1);
2292 }
2293
2294 #[test]
2296 fn test_lanczos_params_basis_size_is_honoured() {
2297 let n = 40usize;
2298 let mut indptr = vec![0u32];
2299 let mut indices = Vec::new();
2300 let mut data = Vec::new();
2301 for i in 0..n {
2302 indices.push(i as u32);
2303 data.push((i + 1) as f64);
2304 indptr.push(indices.len() as u32);
2305 }
2306 let mat = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, n));
2307
2308 let tight = LanczosParams {
2310 basis_size: Some(6),
2311 max_restarts: 200,
2312 tol: 1e-10,
2313 };
2314 let (values, _) = compute_largest_eigenpairs_lanczos(&mat, 3, 7, Some(tight)).unwrap();
2315
2316 assert_relative_eq!(values[0], 40.0, epsilon = 1e-8);
2317 assert_relative_eq!(values[1], 39.0, epsilon = 1e-8);
2318 assert_relative_eq!(values[2], 38.0, epsilon = 1e-8);
2319 }
2320
2321 fn block_op(
2327 mat: &CompressedSparseData2<f64, f64>,
2328 x: MatRef<f64>,
2329 ) -> Result<Mat<f64>, CoreErrors> {
2330 let n = mat.shape.0;
2331 Ok(Mat::from_fn(n, x.ncols(), |i, c| {
2332 (mat.indptr[i] as usize..mat.indptr[i + 1] as usize)
2333 .map(|k| mat.data[k] * x[(mat.indices[k] as usize, c)])
2334 .sum()
2335 }))
2336 }
2337
2338 #[test]
2340 fn test_block_lanczos_matches_scalar_on_a_clustered_spectrum() {
2341 let n = 300usize;
2342 let mat = path_graph(n);
2343 let scalar = compute_largest_eigenpairs_lanczos_diag(&mat, 4, 42, None).unwrap();
2344
2345 for block_size in [1usize, 4, 16] {
2348 let params = BlockLanczosParams {
2349 block_size,
2350 basis_size: None,
2351 max_restarts: 1000,
2352 tol: 1e-8,
2353 };
2354 let res = block_lanczos_thick_restart(n, 4, 42, params, |x| block_op(&mat, x)).unwrap();
2355
2356 assert!(
2357 res.converged,
2358 "b = {block_size}: residual {:e}",
2359 res.residual
2360 );
2361 for k in 0..4 {
2362 assert_relative_eq!(res.values[k], scalar.eigenvalues[k], epsilon = 1e-7);
2363 let v: Vec<Vec<f32>> = (0..n)
2364 .map(|i| vec![res.vectors[k * n + i] as f32])
2365 .collect();
2366 let residual = pair_residual(&mat, &res.values[k..k + 1], &v, 0);
2367 assert!(residual < 1e-5, "b = {block_size}, pair {k}: {residual:e}");
2368 }
2369 }
2370 }
2371
2372 #[test]
2374 fn test_block_lanczos_low_rank_recovers_the_nonzero_pairs() {
2375 let mut diag = vec![0.0f64; 200];
2378 diag[0] = 5.0;
2379 diag[1] = 3.0;
2380 let mat = diagonal(&diag);
2381 let params = BlockLanczosParams {
2382 block_size: 4,
2383 ..BlockLanczosParams::new()
2384 };
2385
2386 let res = block_lanczos_thick_restart(200, 2, 3, params, |x| block_op(&mat, x)).unwrap();
2387
2388 assert!(res.converged);
2389 assert_relative_eq!(res.values[0], 5.0, epsilon = 1e-9);
2390 assert_relative_eq!(res.values[1], 3.0, epsilon = 1e-9);
2391 assert_relative_eq!(res.vectors[0].abs(), 1.0, epsilon = 1e-6);
2392 assert_relative_eq!(res.vectors[200 + 1].abs(), 1.0, epsilon = 1e-6);
2393 }
2394
2395 #[test]
2397 fn test_block_lanczos_caps_the_block_at_the_dimension() {
2398 let mat = diagonal(&[4.0, 3.0, 2.0, 1.0]);
2399 let params = BlockLanczosParams {
2400 block_size: 64,
2401 ..BlockLanczosParams::new()
2402 };
2403
2404 let res = block_lanczos_thick_restart(4, 10, 1, params, |x| block_op(&mat, x)).unwrap();
2405
2406 assert_eq!(res.values.len(), 4);
2407 assert_eq!(res.vectors.len(), 16);
2408 assert_relative_eq!(res.values[0], 4.0, epsilon = 1e-9);
2409 assert_relative_eq!(res.values[3], 1.0, epsilon = 1e-9);
2410 }
2411
2412 #[test]
2414 fn test_block_lanczos_ragged_dimension_is_exact() {
2415 let diag: Vec<f64> = (0..50).map(|i| 1.0 + i as f64).collect();
2419 let mat = diagonal(&diag);
2420 let params = BlockLanczosParams {
2421 block_size: 16,
2422 ..BlockLanczosParams::new()
2423 };
2424
2425 let res = block_lanczos_thick_restart(50, 3, 9, params, |x| block_op(&mat, x)).unwrap();
2426
2427 assert!(res.converged);
2428 assert_relative_eq!(res.values[0], 50.0, epsilon = 1e-9);
2429 assert_relative_eq!(res.values[1], 49.0, epsilon = 1e-9);
2430 assert_relative_eq!(res.values[2], 48.0, epsilon = 1e-9);
2431 }
2432
2433 #[test]
2435 fn test_block_lanczos_rejects_a_misshapen_operator() {
2436 let res = block_lanczos_thick_restart::<CoreErrors, _>(
2437 10,
2438 2,
2439 1,
2440 BlockLanczosParams::new(),
2441 |_| Ok(Mat::<f64>::zeros(10, 1)),
2442 );
2443
2444 assert!(matches!(res, Err(CoreErrors::InvalidArgument(_))));
2445 }
2446
2447 fn dense_block_fixture(
2449 n: usize,
2450 k: usize,
2451 width: usize,
2452 seed: u64,
2453 ) -> (CompressedSparseData2<f64>, Vec<f64>) {
2454 let mut rng = StdRng::seed_from_u64(seed);
2455 let mut indptr = vec![0u32];
2456 let mut indices: Vec<u32> = Vec::new();
2457 let mut data: Vec<f64> = Vec::new();
2458
2459 for _ in 0..n {
2460 for j in 0..k {
2462 if rng.random::<f64>() < 0.35 {
2463 indices.push(j as u32);
2464 data.push(rng.random::<f64>() * 2.0 - 1.0);
2465 }
2466 }
2467 indptr.push(indices.len() as u32);
2468 }
2469
2470 let csr = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, k));
2471 let block: Vec<f64> = (0..k * width)
2472 .map(|_| rng.random::<f64>() * 2.0 - 1.0)
2473 .collect();
2474
2475 (csr, block)
2476 }
2477
2478 #[test]
2481 fn test_csr_matmul_dense_block_matches_scalar() {
2482 for width in [1usize, 3, 4, 7, 16, 17, 33] {
2485 let (csr, block) = dense_block_fixture(11, 9, width, 4242 + width as u64);
2486
2487 let mut got = vec![0.0f64; 11 * width];
2488 csr_matmul_dense_block(&csr, &block, width, &mut got).unwrap();
2489
2490 let mut want = vec![0.0f64; 11 * width];
2491 for i in 0..11 {
2492 for p in csr.indptr[i] as usize..csr.indptr[i + 1] as usize {
2493 let j = csr.indices[p] as usize;
2494 for c in 0..width {
2495 want[i * width + c] += csr.data[p] * block[j * width + c];
2496 }
2497 }
2498 }
2499
2500 for (g, w) in got.iter().zip(want.iter()) {
2501 assert_relative_eq!(g, w, epsilon = 1e-12, max_relative = 1e-12);
2502 }
2503 }
2504 }
2505
2506 #[test]
2511 fn test_csr_matmul_dense_block_repeated_matches_explicit_power() {
2512 let width = 5usize;
2513 let n = 8usize;
2514 let (csr, block) = dense_block_fixture(n, n, width, 77);
2515
2516 let mut buf_a = block.clone();
2518 let mut buf_b = vec![0.0f64; n * width];
2519 for _ in 0..3 {
2520 csr_matmul_dense_block(&csr, &buf_a, width, &mut buf_b).unwrap();
2521 std::mem::swap(&mut buf_a, &mut buf_b);
2522 }
2523
2524 let sq = csr_matmul_csr(&csr, &csr).unwrap();
2526 let cube = csr_matmul_csr(&sq, &csr).unwrap();
2527 let mut direct = vec![0.0f64; n * width];
2528 csr_matmul_dense_block(&cube, &block, width, &mut direct).unwrap();
2529
2530 for (rep, dir) in buf_a.iter().zip(direct.iter()) {
2531 assert_relative_eq!(rep, dir, epsilon = 1e-10, max_relative = 1e-10);
2532 }
2533 }
2534
2535 #[test]
2538 fn test_csr_matmul_dense_block_preserves_constant_column() {
2539 let n = 6usize;
2540 let width = 3usize;
2541
2542 let mut indptr = vec![0u32];
2544 let mut indices: Vec<u32> = Vec::new();
2545 let mut data: Vec<f64> = Vec::new();
2546 for i in 0..n {
2547 for j in 0..n {
2548 indices.push(j as u32);
2549 data.push(1.0 + ((i * n + j) % 3) as f64);
2550 }
2551 indptr.push(indices.len() as u32);
2552 }
2553 let mut csr = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, n));
2554 normalise_csr_rows_l1(&mut csr).unwrap();
2555
2556 let block = vec![2.5f64; n * width];
2557 let mut out = vec![0.0f64; n * width];
2558 csr_matmul_dense_block(&csr, &block, width, &mut out).unwrap();
2559
2560 for v in out {
2561 assert_relative_eq!(v, 2.5, epsilon = 1e-12);
2562 }
2563 }
2564
2565 #[test]
2568 fn test_csr_matmul_dense_block_rejects_bad_shapes() {
2569 let (csr, block) = dense_block_fixture(4, 4, 2, 1);
2570 let mut out = vec![0.0f64; 4 * 2];
2571
2572 assert!(csr_matmul_dense_block(&csr, &block, 3, &mut out).is_err());
2574 assert!(csr_matmul_dense_block(&csr, &block, 0, &mut out).is_err());
2576 let mut short = vec![0.0f64; 4];
2578 assert!(csr_matmul_dense_block(&csr, &block, 2, &mut short).is_err());
2579
2580 let csc = csr.transform();
2581 assert!(matches!(
2582 csr_matmul_dense_block(&csc, &block, 2, &mut out),
2583 Err(CoreErrors::SparseMatrixMustBeCsr)
2584 ));
2585 }
2586
2587 #[test]
2589 fn test_shifted_ranks_sparse_matches_dense_rank_vector() {
2590 let cases: Vec<Vec<f32>> = vec![
2591 vec![3.0, 0.0, 1.0, 0.0, 0.0],
2592 vec![0.0, 0.0, 0.0, 0.0, 0.0],
2593 vec![-2.0, 0.0, 5.0, 0.0, 0.0, 0.0],
2594 vec![-1.0, -3.0, 0.0, 2.0, 4.0, 0.0, 0.0],
2595 vec![2.0, 2.0, 0.0, 2.0, 0.0, 5.0],
2596 vec![-4.0, -4.0, 0.0, 0.0, -4.0, 1.0],
2597 vec![1.0, 2.0, 3.0, 4.0],
2598 vec![-1.0, -2.0, -3.0],
2599 vec![7.0],
2600 ];
2601
2602 for dense in cases {
2603 let n = dense.len();
2604 let indices: Vec<u32> = (0..n as u32)
2605 .filter(|&i| dense[i as usize] != 0.0)
2606 .collect();
2607 let values: Vec<f32> = indices.iter().map(|&i| dense[i as usize]).collect();
2608
2609 let got = shifted_ranks_sparse(&values, n);
2610 let dense_ranks = rank_vector(&dense);
2611 assert_eq!(got.len(), indices.len());
2612
2613 if indices.is_empty() {
2614 continue;
2615 }
2616
2617 for (slot, &i) in indices.iter().enumerate() {
2621 assert_relative_eq!(
2622 got[slot] - got[0],
2623 dense_ranks[i as usize] - dense_ranks[indices[0] as usize],
2624 epsilon = 1e-6
2625 );
2626 }
2627
2628 if indices.len() < n {
2631 let r_zero = dense_ranks[(0..n).find(|&i| dense[i] == 0.0).unwrap()];
2632 for (slot, &i) in indices.iter().enumerate() {
2633 assert_relative_eq!(
2634 got[slot],
2635 dense_ranks[i as usize] - r_zero,
2636 epsilon = 1e-6
2637 );
2638 }
2639 }
2640 }
2641 }
2642
2643 #[test]
2651 fn test_shifted_ranks_sparse_merges_explicit_zeros() {
2652 let values: Vec<f32> = vec![-2.0, 0.0, 3.0, 1.0];
2654 let got = shifted_ranks_sparse(&values, 6);
2655
2656 assert_relative_eq!(got[1], 0.0, epsilon = 1e-6);
2657 assert!(got[0] < 0.0, "the negative must rank below the zero block");
2658 assert!(got[2] > got[3], "3.0 must rank above 1.0");
2659 assert!(got[3] > 0.0, "positives must rank above the zero block");
2660 }
2661}