1use num_traits::Float;
5use rayon::prelude::*;
6use std::ops::{Add, AddAssign, Mul};
7
8use crate::errors::SparseErrors;
9use crate::traits::BixverseNumeric;
10
11pub fn thread_chunks(n: usize) -> Vec<(usize, usize)> {
26 let n_threads = rayon::current_num_threads().max(1);
27 let chunk = n.div_ceil(n_threads).max(1);
28
29 (0..n)
30 .step_by(chunk)
31 .map(|start| (start, (start + chunk).min(n)))
32 .collect()
33}
34
35#[derive(Debug, Clone, Copy)]
44pub enum CompressedSparseFormat {
45 Csc,
47 Csr,
49}
50
51impl std::fmt::Display for CompressedSparseFormat {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 CompressedSparseFormat::Csc => write!(f, "CSC"),
56 CompressedSparseFormat::Csr => write!(f, "CSR"),
57 }
58 }
59}
60
61impl CompressedSparseFormat {
62 #[inline(always)]
68 pub fn is_csc(&self) -> bool {
69 matches!(self, CompressedSparseFormat::Csc)
70 }
71 #[inline(always)]
77 pub fn is_csr(&self) -> bool {
78 matches!(self, CompressedSparseFormat::Csr)
79 }
80}
81
82pub struct SparseAxis<T, U = T> {
84 pub indices: Vec<usize>,
86 pub data: Vec<T>,
88 pub data_2: Option<Vec<U>>,
90 pub cs_type: CompressedSparseFormat,
93 pub len: usize,
95}
96
97impl<T, U> SparseAxis<T, U>
98where
99 T: BixverseNumeric,
100 U: BixverseNumeric,
101{
102 pub fn new_csc(indices: Vec<usize>, data: Vec<T>, data_2: Option<Vec<U>>, len: usize) -> Self {
111 SparseAxis {
112 indices,
113 data,
114 data_2,
115 cs_type: CompressedSparseFormat::Csc,
116 len,
117 }
118 }
119
120 pub fn get_indices_data_2(&self) -> Result<(&[usize], &[U]), SparseErrors> {
126 let indices = &self.indices;
127 let data_2 = self
128 .data_2
129 .as_ref()
130 .ok_or(SparseErrors::Data2NotAvailable)?;
131
132 Ok((indices, data_2))
133 }
134
135 pub fn from_vecs_to_csc(indices: Vec<T>, values: Vec<U>) -> Result<Self, SparseErrors>
151 where
152 T: Into<usize>,
153 {
154 if indices.len() != values.len() {
155 return Err(SparseErrors::DimensionMisMatchSparse {
156 indices_len: indices.len(),
157 data_len: values.len(),
158 });
159 }
160
161 let usize_indices: Vec<usize> = indices.into_iter().map(Into::into).collect();
162 let len = usize_indices.last().map_or(0, |&i| i + 1);
163
164 Ok(Self {
165 indices: usize_indices,
166 data: Vec::new(),
167 data_2: Some(values),
168 cs_type: CompressedSparseFormat::Csc,
169 len,
170 })
171 }
172}
173
174#[derive(Debug, Clone)]
177pub struct CompressedSparseData2<T, U = T>
178where
179 T: Clone,
180 U: Clone,
181{
182 pub data: Vec<T>,
184 pub indices: Vec<u32>,
186 pub indptr: Vec<u32>,
188 pub cs_type: CompressedSparseFormat,
191 pub data_2: Option<Vec<U>>,
194 pub shape: (usize, usize),
196}
197
198impl<T, U> CompressedSparseData2<T, U>
199where
200 T: BixverseNumeric,
201 U: BixverseNumeric,
202{
203 pub fn new_csc(
217 data: &[T],
218 indices: &[u32],
219 indptr: &[u32],
220 data2: Option<&[U]>,
221 shape: (usize, usize),
222 ) -> Self {
223 Self::from_parts(
224 data.to_vec(),
225 indices.to_vec(),
226 indptr.to_vec(),
227 data2.map(|d| d.to_vec()),
228 CompressedSparseFormat::Csc,
229 shape,
230 )
231 }
232
233 pub fn new_csr(
247 data: &[T],
248 indices: &[u32],
249 indptr: &[u32],
250 data2: Option<&[U]>,
251 shape: (usize, usize),
252 ) -> Self {
253 Self::from_parts(
254 data.to_vec(),
255 indices.to_vec(),
256 indptr.to_vec(),
257 data2.map(|d| d.to_vec()),
258 CompressedSparseFormat::Csr,
259 shape,
260 )
261 }
262
263 pub fn from_parts(
282 data: Vec<T>,
283 indices: Vec<u32>,
284 indptr: Vec<u32>,
285 data2: Option<Vec<U>>,
286 cs_type: CompressedSparseFormat,
287 shape: (usize, usize),
288 ) -> Self {
289 Self {
290 data,
291 indices,
292 indptr,
293 cs_type,
294 data_2: data2,
295 shape,
296 }
297 }
298
299 pub fn transform(&self) -> Self {
305 transpose_sparse(self)
306 }
307
308 pub fn transform_single_layer(&self, use_second_layer: bool) -> Result<Self, SparseErrors> {
319 transpose_sparse_single_layer(self, use_second_layer)
320 }
321
322 pub fn transpose_and_convert(&self) -> Self {
328 let mut out = match self.cs_type {
331 CompressedSparseFormat::Csr => transpose_sparse(self),
332 CompressedSparseFormat::Csc => self.clone(),
333 };
334 out.cs_type = CompressedSparseFormat::Csr;
335 out.shape = (self.shape.1, self.shape.0);
336 out
337 }
338
339 pub fn from_upper_triangle_sym(
353 upper_triangle: &[T],
354 n: usize,
355 include_diagonal: bool,
356 format: CompressedSparseFormat,
357 ) -> Self
358 where
359 T: BixverseNumeric + Float,
360 {
361 let get_value = |row: usize, col: usize| -> T {
363 if row == col {
364 if include_diagonal {
365 let offset = row * n - row * (row + 1) / 2 + col;
366 upper_triangle[offset]
367 } else {
368 T::one()
369 }
370 } else if row < col {
371 let offset = if include_diagonal {
372 row * n - row * (row + 1) / 2 + col
373 } else {
374 row * (n - 1) - row * (row + 1) / 2 + col - 1
375 };
376 upper_triangle[offset]
377 } else {
378 let offset = if include_diagonal {
379 col * n - col * (col + 1) / 2 + row
380 } else {
381 col * (n - 1) - col * (col + 1) / 2 + row - 1
382 };
383 upper_triangle[offset]
384 }
385 };
386
387 Self::from_entry_fn(n, n, format, get_value)
388 }
389
390 pub fn shape(&self) -> (usize, usize) {
396 self.shape
397 }
398
399 pub fn nrows(&self) -> usize {
405 self.shape.0
406 }
407
408 pub fn ncols(&self) -> usize {
414 self.shape.1
415 }
416
417 pub fn get_nnz(&self) -> usize {
423 self.indices.len()
424 }
425
426 pub fn get_data2_unsafe(&self) -> Vec<U> {
434 self.data_2.clone().unwrap()
435 }
436
437 pub fn to_sparse_axes(&self) -> Vec<SparseAxis<T, U>> {
446 let len = self.minor_dim();
447 let has_data = self.data.len() == self.indices.len();
448
449 self.indptr
450 .par_windows(2)
451 .map(|w| {
452 let (lo, hi) = (w[0] as usize, w[1] as usize);
453 SparseAxis {
454 indices: self.indices[lo..hi].iter().map(|&i| i as usize).collect(),
455 data: if has_data {
456 self.data[lo..hi].to_vec()
457 } else {
458 Vec::new()
459 },
460 data_2: self.data_2.as_ref().map(|d| d[lo..hi].to_vec()),
461 cs_type: self.cs_type,
462 len,
463 }
464 })
465 .collect()
466 }
467
468 fn slice_major(&self, keep: &[usize]) -> Result<Self, SparseErrors> {
484 let major = self.major_dim();
485 let mut seen = vec![false; major];
486
487 let mut indptr = Vec::with_capacity(keep.len() + 1);
488 indptr.push(0u32);
489 let mut indices = Vec::new();
490 let mut data = Vec::new();
491 let mut data_2 = self.data_2.as_ref().map(|_| Vec::new());
492
493 for &m in keep {
494 if m >= major {
495 return Err(SparseErrors::SliceIndexOutOfBounds {
496 index: m,
497 len: major,
498 });
499 }
500 if seen[m] {
501 return Err(SparseErrors::DuplicateSliceIndex(m));
502 }
503 seen[m] = true;
504
505 let s = self.indptr[m] as usize;
506 let e = self.indptr[m + 1] as usize;
507 indices.extend_from_slice(&self.indices[s..e]);
508 data.extend_from_slice(&self.data[s..e]);
509 if let (Some(dst), Some(src)) = (data_2.as_mut(), self.data_2.as_ref()) {
510 dst.extend_from_slice(&src[s..e]);
511 }
512 indptr.push(indices.len() as u32);
513 }
514
515 let shape = match self.cs_type {
516 CompressedSparseFormat::Csr => (keep.len(), self.shape.1),
517 CompressedSparseFormat::Csc => (self.shape.0, keep.len()),
518 };
519
520 let out = Self {
521 data,
522 indices,
523 indptr,
524 cs_type: self.cs_type,
525 data_2,
526 shape,
527 };
528 out.assert_invariants();
529 Ok(out)
530 }
531
532 fn slice_minor(&self, keep: &[usize]) -> Result<Self, SparseErrors> {
545 let minor = self.minor_dim();
546 let mut map: Vec<Option<usize>> = vec![None; minor];
547 for (new_idx, &old) in keep.iter().enumerate() {
548 if old >= minor {
549 return Err(SparseErrors::SliceIndexOutOfBounds {
550 index: old,
551 len: minor,
552 });
553 }
554 if map[old].is_some() {
555 return Err(SparseErrors::DuplicateSliceIndex(old));
556 }
557 map[old] = Some(new_idx);
558 }
559
560 let major = self.major_dim();
561 let mut indptr = Vec::with_capacity(major + 1);
562 indptr.push(0u32);
563 let mut indices = Vec::new();
564 let mut data = Vec::new();
565 let mut data_2 = self.data_2.as_ref().map(|_| Vec::new());
566
567 let mut kept: Vec<(usize, usize)> = Vec::new(); for m in 0..major {
569 let s = self.indptr[m];
570 let e = self.indptr[m + 1];
571 kept.clear();
572 for p in s..e {
573 if let Some(nm) = map[self.indices[p as usize] as usize] {
574 kept.push((nm, p as usize));
575 }
576 }
577 kept.sort_unstable_by_key(|&(nm, _)| nm);
578 for &(nm, p) in &kept {
579 indices.push(nm as u32);
580 data.push(self.data[p]);
581 if let (Some(dst), Some(src)) = (data_2.as_mut(), self.data_2.as_ref()) {
582 dst.push(src[p]);
583 }
584 }
585 indptr.push(indices.len() as u32);
586 }
587
588 let shape = match self.cs_type {
589 CompressedSparseFormat::Csr => (self.shape.0, keep.len()),
590 CompressedSparseFormat::Csc => (keep.len(), self.shape.1),
591 };
592
593 let out = Self {
594 data,
595 indices,
596 indptr,
597 cs_type: self.cs_type,
598 data_2,
599 shape,
600 };
601 out.assert_invariants();
602 Ok(out)
603 }
604
605 pub fn slice_rows(&self, rows: &[usize]) -> Result<Self, SparseErrors> {
615 match self.cs_type {
616 CompressedSparseFormat::Csr => self.slice_major(rows),
617 CompressedSparseFormat::Csc => self.slice_minor(rows),
618 }
619 }
620
621 pub fn slice_cols(&self, cols: &[usize]) -> Result<Self, SparseErrors> {
631 match self.cs_type {
632 CompressedSparseFormat::Csr => self.slice_minor(cols),
633 CompressedSparseFormat::Csc => self.slice_major(cols),
634 }
635 }
636
637 pub fn slice(&self, rows: &[usize], cols: &[usize]) -> Result<Self, SparseErrors> {
648 self.slice_rows(rows)?.slice_cols(cols)
649 }
650
651 pub fn from_entry_fn<G>(
669 nrows: usize,
670 ncols: usize,
671 format: CompressedSparseFormat,
672 get: G,
673 ) -> Self
674 where
675 T: BixverseNumeric + Float,
676 G: Fn(usize, usize) -> T,
677 {
678 let (major, minor) = match format {
679 CompressedSparseFormat::Csr => (nrows, ncols),
680 CompressedSparseFormat::Csc => (ncols, nrows),
681 };
682
683 let mut data = Vec::new();
684 let mut indices: Vec<u32> = Vec::new();
685 let mut indptr: Vec<u32> = Vec::with_capacity(major + 1);
686 indptr.push(0);
687
688 for outer in 0..major {
689 for inner in 0..minor {
690 let val = match format {
691 CompressedSparseFormat::Csr => get(outer, inner),
692 CompressedSparseFormat::Csc => get(inner, outer),
693 };
694 if val != T::zero() {
695 data.push(val);
696 indices.push(inner as u32);
697 }
698 }
699 indptr.push(data.len() as u32);
700 }
701
702 Self::from_parts(data, indices, indptr, None, format, (nrows, ncols))
703 }
704
705 fn major_dim(&self) -> usize {
711 self.indptr.len() - 1
712 }
713
714 fn expected_major(&self) -> usize {
720 match self.cs_type {
721 CompressedSparseFormat::Csr => self.shape.0,
722 CompressedSparseFormat::Csc => self.shape.1,
723 }
724 }
725
726 fn minor_dim(&self) -> usize {
732 match self.cs_type {
733 CompressedSparseFormat::Csr => self.shape.1,
734 CompressedSparseFormat::Csc => self.shape.0,
735 }
736 }
737
738 pub fn validate(&self) -> Result<(), SparseErrors> {
763 let expected_major = self.expected_major();
764
765 if self.indptr.len() != expected_major + 1 {
766 return Err(SparseErrors::SparseIndptrInvalid {
767 detail: "length against the declared major axis",
768 expected: expected_major + 1,
769 got: self.indptr.len(),
770 });
771 }
772
773 let nnz = *self.indptr.last().expect("indptr length checked above") as usize;
775 if nnz != self.indices.len() {
776 return Err(SparseErrors::SparseIndptrInvalid {
777 detail: "final offset against the stored indices",
778 expected: self.indices.len(),
779 got: nnz,
780 });
781 }
782
783 if !self.indptr.windows(2).all(|w| w[0] <= w[1]) {
784 return Err(SparseErrors::SparseIndptrInvalid {
785 detail: "offsets must be non-decreasing",
786 expected: nnz,
787 got: nnz,
788 });
789 }
790
791 if !self.data.is_empty() && self.data.len() != self.indices.len() {
792 return Err(SparseErrors::DimensionMisMatchSparse {
793 indices_len: self.indices.len(),
794 data_len: self.data.len(),
795 });
796 }
797
798 if let Some(data_2) = &self.data_2
799 && !data_2.is_empty()
800 && data_2.len() != self.indices.len()
801 {
802 return Err(SparseErrors::DimensionMisMatchSparse {
803 indices_len: self.indices.len(),
804 data_len: data_2.len(),
805 });
806 }
807
808 let minor = self.minor_dim();
809 if let Some(&bad) = self
810 .indices
811 .par_iter()
812 .find_any(|&&idx| idx as usize >= minor)
813 {
814 return Err(SparseErrors::SliceIndexOutOfBounds {
815 index: bad as usize,
816 len: minor,
817 });
818 }
819
820 Ok(())
821 }
822
823 pub fn assert_invariants(&self) {
825 debug_assert_eq!(self.indices.len(), self.data.len());
826 debug_assert_eq!(self.indptr.len(), self.expected_major() + 1);
827 debug_assert_eq!(*self.indptr.last().unwrap() as usize, self.data.len());
828 if let Some(d2) = &self.data_2 {
829 debug_assert_eq!(d2.len(), self.data.len());
830 }
831 }
832}
833
834pub fn require_csr<T, U>(mat: &CompressedSparseData2<T, U>) -> Result<(), SparseErrors>
848where
849 T: Clone,
850 U: Clone,
851{
852 if mat.cs_type.is_csr() {
853 Ok(())
854 } else {
855 Err(SparseErrors::SparseMatrixMustBeCsr)
856 }
857}
858
859pub fn require_csc<T, U>(mat: &CompressedSparseData2<T, U>) -> Result<(), SparseErrors>
869where
870 T: Clone,
871 U: Clone,
872{
873 if mat.cs_type.is_csc() {
874 Ok(())
875 } else {
876 Err(SparseErrors::SparseMatrixMustBeCsc)
877 }
878}
879
880fn require_csr_pair_same_shape<T>(
893 a: &CompressedSparseData2<T>,
894 b: &CompressedSparseData2<T>,
895) -> Result<(), SparseErrors>
896where
897 T: Clone,
898{
899 require_csr(a)?;
900 require_csr(b)?;
901 if a.shape != b.shape {
902 return Err(SparseErrors::ShapeMismatchSparse);
903 }
904 Ok(())
905}
906
907fn cumulate_indptr(indptr: &mut [u32]) {
914 for i in 1..indptr.len() {
915 indptr[i] += indptr[i - 1];
916 }
917}
918
919pub fn parse_compressed_sparse_format(s: &str) -> Option<CompressedSparseFormat> {
933 match s.to_lowercase().as_str() {
934 "csr" => Some(CompressedSparseFormat::Csr),
935 "csc" => Some(CompressedSparseFormat::Csc),
936 _ => None,
937 }
938}
939
940pub fn validate_square_csr<T>(graph: &CompressedSparseData2<T>) -> Result<usize, SparseErrors>
957where
958 T: Clone,
959{
960 if !graph.cs_type.is_csr() {
961 return Err(SparseErrors::SparseMatrixMustBeCsr);
962 }
963 let (rows, cols) = graph.shape;
964 if rows != cols {
965 return Err(SparseErrors::ShapeMismatch {
966 expected: (rows, rows),
967 got: (rows, cols),
968 });
969 }
970
971 if graph.indptr.len() != rows + 1 {
972 return Err(SparseErrors::MalformedCsr(
973 "indptr length must be the row count plus one",
974 ));
975 }
976 if graph.indices.len() != graph.data.len() {
977 return Err(SparseErrors::MalformedCsr(
978 "indices and data must have the same length",
979 ));
980 }
981 if graph.indptr.windows(2).any(|w| w[0] > w[1]) {
982 return Err(SparseErrors::MalformedCsr("indptr must be non-decreasing"));
983 }
984 if graph.indptr[rows] as usize != graph.indices.len() {
986 return Err(SparseErrors::MalformedCsr(
987 "the last indptr entry must equal the number of stored values",
988 ));
989 }
990 if graph.indices.iter().any(|&j| j as usize >= rows) {
991 return Err(SparseErrors::MalformedCsr(
992 "a column index sits outside the matrix",
993 ));
994 }
995
996 Ok(rows)
997}
998
999const PARALLEL_TRANSPOSE_MIN_NNZ: usize = 1 << 20;
1015
1016const TRANSPOSE_MINOR_BAND: usize = 1024;
1025
1026pub struct ScatterPtr<T>(pub *mut T);
1037
1038unsafe impl<T: Send> Send for ScatterPtr<T> {}
1040unsafe impl<T: Send> Sync for ScatterPtr<T> {}
1042
1043impl<T> ScatterPtr<T> {
1044 #[inline(always)]
1056 pub unsafe fn write(&self, pos: usize, value: T) {
1057 unsafe { *self.0.add(pos) = value }
1058 }
1059}
1060
1061fn converted_layout<T, U>(
1078 sparse_data: &CompressedSparseData2<T, U>,
1079) -> (usize, CompressedSparseFormat)
1080where
1081 T: BixverseNumeric,
1082 U: BixverseNumeric,
1083{
1084 let new_type = match sparse_data.cs_type {
1085 CompressedSparseFormat::Csc => CompressedSparseFormat::Csr,
1086 CompressedSparseFormat::Csr => CompressedSparseFormat::Csc,
1087 };
1088 (sparse_data.minor_dim(), new_type)
1089}
1090
1091fn transpose_cursors<T, U>(
1109 sparse_data: &CompressedSparseData2<T, U>,
1110 chunks: &[(usize, usize)],
1111 new_major: usize,
1112) -> (Vec<u32>, Vec<u32>, bool)
1113where
1114 T: BixverseNumeric,
1115 U: BixverseNumeric,
1116{
1117 let mut cursors = vec![0u32; chunks.len() * new_major];
1118
1119 let sorted = chunks
1120 .par_iter()
1121 .zip(cursors.par_chunks_mut(new_major.max(1)))
1122 .map(|(&(start, end), counts)| {
1123 let mut sorted = true;
1124 for major in start..end {
1125 let lo = sparse_data.indptr[major] as usize;
1126 let hi = sparse_data.indptr[major + 1] as usize;
1127 let run = &sparse_data.indices[lo..hi];
1128 for &minor in run {
1129 counts[minor as usize] += 1;
1130 }
1131 sorted &= run.windows(2).all(|w| w[0] < w[1]);
1132 }
1133 sorted
1134 })
1135 .reduce(|| true, |a, b| a && b);
1136
1137 let mut new_indptr = vec![0u32; new_major + 1];
1139 for counts in cursors.chunks(new_major.max(1)) {
1140 for (total, &count) in new_indptr[1..].iter_mut().zip(counts.iter()) {
1141 *total += count;
1142 }
1143 }
1144 cumulate_indptr(&mut new_indptr);
1145
1146 let mut running: Vec<u32> = new_indptr[..new_major].to_vec();
1150 for counts in cursors.chunks_mut(new_major.max(1)) {
1151 for (count, offset) in counts.iter_mut().zip(running.iter_mut()) {
1152 let start = *offset;
1153 *offset += *count;
1154 *count = start;
1155 }
1156 }
1157
1158 (new_indptr, cursors, sorted)
1159}
1160
1161#[allow(clippy::too_many_arguments)]
1176fn transpose_scatter<T, U>(
1177 sparse_data: &CompressedSparseData2<T, U>,
1178 chunks: &[(usize, usize)],
1179 cursors: &mut [u32],
1180 new_major: usize,
1181 band: Option<usize>,
1182 out_indices: &mut [u32],
1183 out_data: Option<(&[T], &mut [T])>,
1184 out_data2: Option<(&[U], &mut [U])>,
1185) where
1186 T: BixverseNumeric,
1187 U: BixverseNumeric,
1188{
1189 let indices_ptr = ScatterPtr(out_indices.as_mut_ptr());
1190 let (data_src, data_ptr) = match out_data {
1191 Some((src, dst)) => (Some(src), Some(ScatterPtr(dst.as_mut_ptr()))),
1192 None => (None, None),
1193 };
1194 let (data2_src, data2_ptr) = match out_data2 {
1195 Some((src, dst)) => (Some(src), Some(ScatterPtr(dst.as_mut_ptr()))),
1196 None => (None, None),
1197 };
1198
1199 chunks
1200 .par_iter()
1201 .zip(cursors.par_chunks_mut(new_major.max(1)))
1202 .for_each(|(&(start, end), cursor)| {
1203 let mut read: Vec<usize> = (start..end)
1205 .map(|major| sparse_data.indptr[major] as usize)
1206 .collect();
1207 let band = band.unwrap_or(usize::MAX).max(1);
1208 let mut band_lo = 0usize;
1209 while band_lo < new_major.max(1) {
1210 let band_hi = band_lo.saturating_add(band);
1211 for (major, rd) in (start..end).zip(read.iter_mut()) {
1212 let hi = sparse_data.indptr[major + 1] as usize;
1213 while *rd < hi {
1214 let idx = *rd;
1215 let minor = sparse_data.indices[idx] as usize;
1216 if minor >= band_hi {
1217 break;
1218 }
1219 *rd += 1;
1220 let pos = cursor[minor] as usize;
1221 cursor[minor] += 1;
1222
1223 unsafe {
1227 indices_ptr.write(pos, major as u32);
1228 if let (Some(src), Some(dst)) = (data_src, &data_ptr) {
1229 dst.write(pos, src[idx]);
1230 }
1231 if let (Some(src), Some(dst)) = (data2_src, &data2_ptr) {
1232 dst.write(pos, src[idx]);
1233 }
1234 }
1235 }
1236 }
1237 band_lo = band_hi;
1238 }
1239 });
1240}
1241
1242fn major_axis_chunks<T, U>(sparse_data: &CompressedSparseData2<T, U>) -> Vec<(usize, usize)>
1256where
1257 T: BixverseNumeric,
1258 U: BixverseNumeric,
1259{
1260 let old_major_len = sparse_data.indptr.len() - 1;
1261
1262 if sparse_data.get_nnz() >= PARALLEL_TRANSPOSE_MIN_NNZ {
1263 thread_chunks(old_major_len)
1264 } else {
1265 vec![(0, old_major_len)]
1266 }
1267}
1268
1269fn transpose_sparse_chunked<T, U>(
1283 sparse_data: &CompressedSparseData2<T, U>,
1284 chunks: &[(usize, usize)],
1285 band: usize,
1286) -> CompressedSparseData2<T, U>
1287where
1288 T: BixverseNumeric,
1289 U: BixverseNumeric,
1290{
1291 let nnz = sparse_data.get_nnz();
1292 let (nrow, ncol) = sparse_data.shape();
1293 let (new_major, new_type) = converted_layout(sparse_data);
1294
1295 let mut new_data: Vec<T> = vec![T::default(); nnz];
1296 let mut new_indices: Vec<u32> = vec![0u32; nnz];
1297 let mut new_data2: Option<Vec<U>> =
1298 sparse_data.data_2.as_ref().map(|_| vec![U::default(); nnz]);
1299
1300 let (new_indptr, mut cursors, sorted) = transpose_cursors(sparse_data, chunks, new_major);
1301
1302 let data_pair = Some((sparse_data.data.as_slice(), new_data.as_mut_slice()));
1303 let data2_pair = match (&sparse_data.data_2, &mut new_data2) {
1304 (Some(src), Some(dst)) => Some((src.as_slice(), dst.as_mut_slice())),
1305 _ => None,
1306 };
1307
1308 transpose_scatter(
1309 sparse_data,
1310 chunks,
1311 &mut cursors,
1312 new_major,
1313 sorted.then_some(band),
1314 &mut new_indices,
1315 data_pair,
1316 data2_pair,
1317 );
1318
1319 CompressedSparseData2 {
1320 data: new_data,
1321 indices: new_indices,
1322 indptr: new_indptr,
1323 cs_type: new_type,
1324 data_2: new_data2,
1325 shape: (nrow, ncol),
1326 }
1327}
1328
1329pub fn transpose_sparse<T, U>(
1356 sparse_data: &CompressedSparseData2<T, U>,
1357) -> CompressedSparseData2<T, U>
1358where
1359 T: BixverseNumeric,
1360 U: BixverseNumeric,
1361{
1362 transpose_sparse_chunked(
1363 sparse_data,
1364 &major_axis_chunks(sparse_data),
1365 TRANSPOSE_MINOR_BAND,
1366 )
1367}
1368
1369pub fn transpose_sparse_single_layer<T, U>(
1388 sparse_data: &CompressedSparseData2<T, U>,
1389 use_second_layer: bool,
1390) -> Result<CompressedSparseData2<T, U>, SparseErrors>
1391where
1392 T: BixverseNumeric,
1393 U: BixverseNumeric,
1394{
1395 let nnz = sparse_data.get_nnz();
1396 let (nrow, ncol) = sparse_data.shape();
1397 let (new_major, new_type) = converted_layout(sparse_data);
1398
1399 let mut new_indices: Vec<u32> = vec![0u32; nnz];
1400
1401 let (mut new_data, mut new_data2): (Vec<T>, Option<Vec<U>>) = if use_second_layer {
1403 (Vec::new(), Some(vec![U::default(); nnz]))
1404 } else {
1405 (vec![T::default(); nnz], None)
1406 };
1407
1408 let chunks = major_axis_chunks(sparse_data);
1409 let (new_indptr, mut cursors, sorted) = transpose_cursors(sparse_data, &chunks, new_major);
1410
1411 let (data_pair, data2_pair) = if use_second_layer {
1412 let src = sparse_data
1413 .data_2
1414 .as_ref()
1415 .ok_or(SparseErrors::Data2NotAvailable)?
1416 .as_slice();
1417 let dst = new_data2.as_mut().unwrap().as_mut_slice();
1418 (None, Some((src, dst)))
1419 } else {
1420 (
1421 Some((sparse_data.data.as_slice(), new_data.as_mut_slice())),
1422 None,
1423 )
1424 };
1425
1426 transpose_scatter(
1427 sparse_data,
1428 &chunks,
1429 &mut cursors,
1430 new_major,
1431 sorted.then_some(TRANSPOSE_MINOR_BAND),
1432 &mut new_indices,
1433 data_pair,
1434 data2_pair,
1435 );
1436
1437 Ok(CompressedSparseData2 {
1438 data: new_data,
1439 indices: new_indices,
1440 indptr: new_indptr,
1441 cs_type: new_type,
1442 data_2: new_data2,
1443 shape: (nrow, ncol),
1444 })
1445}
1446
1447pub fn coo_to_csr<T>(
1459 rows: &[u32],
1460 cols: &[u32],
1461 vals: &[T],
1462 shape: (usize, usize),
1463) -> CompressedSparseData2<T>
1464where
1465 T: BixverseNumeric,
1466{
1467 let n_rows = shape.0;
1468
1469 let mut entries: Vec<(u32, u32, T)> = rows
1471 .iter()
1472 .zip(cols.iter())
1473 .zip(vals.iter())
1474 .map(|((&r, &c), &v)| (r, c, v))
1475 .collect();
1476
1477 entries.sort_unstable_by_key(|&(r, c, _)| (r, c));
1478
1479 let mut merged_entries = Vec::new();
1481 if !entries.is_empty() {
1482 let mut current = entries[0];
1483
1484 for &(r, c, v) in &entries[1..] {
1485 if r == current.0 && c == current.1 {
1486 current.2 += v;
1487 } else {
1488 if current.2 != T::default() {
1489 merged_entries.push(current);
1490 }
1491 current = (r, c, v);
1492 }
1493 }
1494 if current.2 != T::default() {
1495 merged_entries.push(current);
1496 }
1497 }
1498
1499 let final_nnz = merged_entries.len();
1501 let mut data = Vec::with_capacity(final_nnz);
1502 let mut indices = Vec::with_capacity(final_nnz);
1503 let mut indptr = vec![0u32; n_rows + 1];
1504
1505 for &(row, col, val) in &merged_entries {
1506 data.push(val);
1507 indices.push(col);
1508 indptr[(row + 1) as usize] += 1;
1509 }
1510
1511 cumulate_indptr(&mut indptr);
1512
1513 CompressedSparseData2::new_csr(&data, &indices, &indptr, None, shape)
1514}
1515
1516pub fn coo_to_csr_presorted<T>(
1530 rows: &[u32],
1531 cols: &[u32],
1532 vals: &[T],
1533 shape: (usize, usize),
1534) -> CompressedSparseData2<T>
1535where
1536 T: BixverseNumeric,
1537{
1538 let n_rows = shape.0;
1539 let nnz = rows.len();
1540
1541 let mut data = Vec::with_capacity(nnz);
1542 let mut indices: Vec<u32> = Vec::with_capacity(nnz);
1543 let mut indptr = vec![0u32; n_rows + 1];
1544
1545 unsafe {
1547 data.set_len(nnz);
1548 indices.set_len(nnz);
1549
1550 let data_ptr: *mut T = data.as_mut_ptr();
1551 let indices_ptr: *mut u32 = indices.as_mut_ptr();
1552 let indptr_ptr: *mut u32 = indptr.as_mut_ptr();
1553
1554 for i in 0..nnz {
1555 *data_ptr.add(i) = *vals.get_unchecked(i);
1556 *indices_ptr.add(i) = *cols.get_unchecked(i);
1557 let row = *rows.get_unchecked(i) as usize;
1558 *indptr_ptr.add(row + 1) += 1;
1559 }
1560 }
1561
1562 cumulate_indptr(&mut indptr);
1563
1564 CompressedSparseData2::new_csr(&data, &indices, &indptr, None, shape)
1565}
1566
1567pub fn undirected_edges_to_csr<T>(n: usize, edges: &[(u32, u32, T)]) -> CompressedSparseData2<T>
1595where
1596 T: BixverseNumeric,
1597{
1598 debug_assert!(
1599 edges
1600 .iter()
1601 .all(|&(lo, hi, _)| lo < hi && (hi as usize) < n),
1602 "undirected_edges_to_csr wants lo < hi < n"
1603 );
1604 debug_assert!(
1605 edges
1606 .windows(2)
1607 .all(|w| (w[0].0, w[0].1) < (w[1].0, w[1].1)),
1608 "undirected_edges_to_csr wants strictly ascending, deduplicated (lo, hi)"
1609 );
1610
1611 let mut indptr = vec![0u32; n + 1];
1612 for &(lo, hi, _) in edges {
1613 indptr[lo as usize + 1] += 1;
1614 indptr[hi as usize + 1] += 1;
1615 }
1616 cumulate_indptr(&mut indptr);
1617
1618 let nnz = 2 * edges.len();
1619 let mut indices = vec![0u32; nnz];
1620 let mut data = vec![T::default(); nnz];
1621 let mut cursor: Vec<u32> = indptr[..n].to_vec();
1622
1623 for &(lo, hi, v) in edges {
1624 let pos = cursor[hi as usize] as usize;
1625 indices[pos] = lo;
1626 data[pos] = v;
1627 cursor[hi as usize] += 1;
1628 }
1629 for &(lo, hi, v) in edges {
1630 let pos = cursor[lo as usize] as usize;
1631 indices[pos] = hi;
1632 data[pos] = v;
1633 cursor[lo as usize] += 1;
1634 }
1635
1636 CompressedSparseData2::from_parts(
1637 data,
1638 indices,
1639 indptr,
1640 None,
1641 CompressedSparseFormat::Csr,
1642 (n, n),
1643 )
1644}
1645
1646const SPARSE_MERGE_ROW_CHUNK: usize = 1024;
1657
1658const SPARSE_MERGE_EPSILON: f32 = 1e-9;
1662
1663const SPARSE_MATMUL_ROW_CHUNK: usize = 256;
1667
1668const NORMALISE_MIN_SUM: f64 = 1e-15;
1671
1672struct SparseAccumulator<T>
1684where
1685 T: Copy + Default + AddAssign,
1686{
1687 values: Vec<T>,
1688 indices: Vec<usize>,
1689 flags: Vec<bool>,
1690}
1691
1692impl<T> SparseAccumulator<T>
1693where
1694 T: Copy + Default + AddAssign,
1695{
1696 fn new(size: usize) -> Self {
1702 Self {
1703 values: vec![T::default(); size],
1704 indices: Vec::with_capacity(size / 10),
1705 flags: vec![false; size],
1706 }
1707 }
1708
1709 #[inline]
1720 unsafe fn add(&mut self, idx: usize, val: T) {
1721 unsafe {
1722 if !*self.flags.get_unchecked(idx) {
1723 *self.flags.get_unchecked_mut(idx) = true;
1724 self.indices.push(idx);
1725 *self.values.get_unchecked_mut(idx) = val;
1726 } else {
1727 *self.values.get_unchecked_mut(idx) += val;
1728 }
1729 }
1730 }
1731
1732 #[inline]
1744 fn extract_into(&mut self, indices_out: &mut Vec<usize>, data_out: &mut Vec<T>) -> usize {
1745 self.indices.sort_unstable();
1746 let n = self.indices.len();
1747 unsafe {
1748 for &i in &self.indices {
1749 indices_out.push(i);
1750 data_out.push(*self.values.get_unchecked(i));
1751 *self.flags.get_unchecked_mut(i) = false;
1752 *self.values.get_unchecked_mut(i) = T::default();
1753 }
1754 }
1755 self.indices.clear();
1756 n
1757 }
1758}
1759
1760pub fn require_matmul_operands<T>(
1777 a: &CompressedSparseData2<T>,
1778 b: &CompressedSparseData2<T>,
1779) -> Result<(), SparseErrors>
1780where
1781 T: Clone,
1782{
1783 require_csr(a)?;
1784 require_csr(b)?;
1785
1786 let (ncol_a, nrow_b) = (a.shape.1, b.shape.0);
1787 if ncol_a != nrow_b {
1788 return Err(SparseErrors::SparseMatrixMultiplication {
1789 n_col_a: ncol_a,
1790 n_row_b: nrow_b,
1791 });
1792 }
1793 Ok(())
1794}
1795
1796fn merge_csr_rows<T, C, N>(
1815 a: &CompressedSparseData2<T>,
1816 b: &CompressedSparseData2<T>,
1817 combine: C,
1818 lone_b: N,
1819) -> CompressedSparseData2<T>
1820where
1821 T: BixverseNumeric + Into<f64>,
1822 C: Fn(T, T) -> T + Sync,
1823 N: Fn(T) -> T + Sync,
1824{
1825 let n_rows = a.shape.0;
1826
1827 let chunks: Vec<(Vec<u32>, Vec<T>, Vec<u32>)> = (0..n_rows.div_ceil(SPARSE_MERGE_ROW_CHUNK))
1828 .into_par_iter()
1829 .map(|chunk_idx| {
1830 let start = chunk_idx * SPARSE_MERGE_ROW_CHUNK;
1831 let end = (start + SPARSE_MERGE_ROW_CHUNK).min(n_rows);
1832 let cap = (a.indptr[end] - a.indptr[start] + b.indptr[end] - b.indptr[start]) as usize;
1833 let mut cols: Vec<u32> = Vec::with_capacity(cap);
1834 let mut vals: Vec<T> = Vec::with_capacity(cap);
1835 let mut lengths: Vec<u32> = Vec::with_capacity(end - start);
1836
1837 for i in start..end {
1838 let before = cols.len();
1839 let a_end = a.indptr[i + 1] as usize;
1840 let b_end = b.indptr[i + 1] as usize;
1841 let mut a_idx = a.indptr[i] as usize;
1842 let mut b_idx = b.indptr[i] as usize;
1843
1844 while a_idx < a_end && b_idx < b_end {
1845 let a_col = a.indices[a_idx];
1846 let b_col = b.indices[b_idx];
1847 if a_col < b_col {
1848 cols.push(a_col);
1849 vals.push(a.data[a_idx]);
1850 a_idx += 1;
1851 } else if b_col < a_col {
1852 cols.push(b_col);
1853 vals.push(lone_b(b.data[b_idx]));
1854 b_idx += 1;
1855 } else {
1856 let val = combine(a.data[a_idx], b.data[b_idx]);
1857 if val.into().abs() > SPARSE_MERGE_EPSILON as f64 {
1858 cols.push(a_col);
1859 vals.push(val);
1860 }
1861 a_idx += 1;
1862 b_idx += 1;
1863 }
1864 }
1865 cols.extend_from_slice(&a.indices[a_idx..a_end]);
1866 vals.extend_from_slice(&a.data[a_idx..a_end]);
1867 cols.extend_from_slice(&b.indices[b_idx..b_end]);
1868 vals.extend(b.data[b_idx..b_end].iter().map(|&v| lone_b(v)));
1869
1870 lengths.push((cols.len() - before) as u32);
1871 }
1872
1873 (cols, vals, lengths)
1874 })
1875 .collect();
1876
1877 let nnz: usize = chunks.iter().map(|(c, _, _)| c.len()).sum();
1878 let mut indptr: Vec<u32> = Vec::with_capacity(n_rows + 1);
1879 let mut indices: Vec<u32> = Vec::with_capacity(nnz);
1880 let mut data: Vec<T> = Vec::with_capacity(nnz);
1881 indptr.push(0);
1882 for (cols, vals, lengths) in chunks {
1883 let mut running = *indptr.last().unwrap_or(&0);
1884 for len in lengths {
1885 running += len;
1886 indptr.push(running);
1887 }
1888 indices.extend_from_slice(&cols);
1889 data.extend_from_slice(&vals);
1890 }
1891
1892 CompressedSparseData2::from_parts(
1893 data,
1894 indices,
1895 indptr,
1896 None,
1897 CompressedSparseFormat::Csr,
1898 a.shape,
1899 )
1900}
1901
1902pub fn sparse_add_csr<T>(
1920 a: &CompressedSparseData2<T>,
1921 b: &CompressedSparseData2<T>,
1922) -> Result<CompressedSparseData2<T>, SparseErrors>
1923where
1924 T: BixverseNumeric + Into<f64> + Add<Output = T>,
1925{
1926 require_csr_pair_same_shape(a, b)?;
1927 Ok(merge_csr_rows(a, b, |x, y| x + y, |y| y))
1928}
1929
1930pub fn sparse_scalar_multiply_csr<T>(
1941 a: &CompressedSparseData2<T>,
1942 scalar: T,
1943) -> CompressedSparseData2<T>
1944where
1945 T: BixverseNumeric,
1946 <T as Mul>::Output: Send,
1947 Vec<T>: FromParallelIterator<<T as Mul>::Output>,
1948{
1949 let data: Vec<T> = a.data.par_iter().map(|&v| v * scalar).collect();
1950 CompressedSparseData2::new_csr(&data, &a.indices, &a.indptr, None, a.shape)
1951}
1952
1953pub fn sparse_subtract_csr<T>(
1967 a: &CompressedSparseData2<T>,
1968 b: &CompressedSparseData2<T>,
1969) -> Result<CompressedSparseData2<T>, SparseErrors>
1970where
1971 T: BixverseNumeric + Into<f64>,
1972{
1973 require_csr_pair_same_shape(a, b)?;
1974 Ok(merge_csr_rows(a, b, |x, y| x - y, |y| T::default() - y))
1975}
1976
1977pub fn sparse_multiply_elementwise_csr<T>(
1988 a: &CompressedSparseData2<T>,
1989 b: &CompressedSparseData2<T>,
1990) -> Result<CompressedSparseData2<T>, SparseErrors>
1991where
1992 T: BixverseNumeric,
1993 <T as std::ops::Add>::Output: std::cmp::PartialEq<T>,
1994{
1995 require_csr_pair_same_shape(a, b)?;
1996
1997 let n_rows = a.shape.0;
1998 let mut rows: Vec<u32> = Vec::new();
1999 let mut cols: Vec<u32> = Vec::new();
2000 let mut vals = Vec::new();
2001 for i in 0..n_rows {
2002 let a_start = a.indptr[i] as usize;
2003 let a_end = a.indptr[i + 1] as usize;
2004 let b_start = b.indptr[i] as usize;
2005 let b_end = b.indptr[i + 1] as usize;
2006 let mut a_idx = a_start;
2007 let mut b_idx = b_start;
2008 while a_idx < a_end && b_idx < b_end {
2009 match a.indices[a_idx].cmp(&b.indices[b_idx]) {
2010 std::cmp::Ordering::Less => {
2011 a_idx += 1;
2012 }
2013 std::cmp::Ordering::Greater => {
2014 b_idx += 1;
2015 }
2016 std::cmp::Ordering::Equal => {
2017 let val = a.data[a_idx] * b.data[b_idx];
2019 if val != T::default() {
2020 rows.push(i as u32);
2021 cols.push(a.indices[a_idx]);
2022 vals.push(val);
2023 }
2024 a_idx += 1;
2025 b_idx += 1;
2026 }
2027 }
2028 }
2029 }
2030
2031 Ok(coo_to_csr(&rows, &cols, &vals, a.shape))
2032}
2033
2034pub fn normalise_csr_columns_l1<T>(csr: &mut CompressedSparseData2<T>)
2040where
2041 T: BixverseNumeric + Into<f64>,
2042 <T as std::ops::Add>::Output: std::cmp::PartialEq<T>,
2043{
2044 assert!(csr.cs_type.is_csr(), "Matrix must be in CSR format");
2045
2046 let ncols = csr.shape.1;
2047
2048 let mut col_sums = vec![T::default(); ncols];
2049
2050 for (idx, &col) in csr.indices.iter().enumerate() {
2051 col_sums[col as usize] += csr.data[idx]
2052 }
2053
2054 for (idx, &col) in csr.indices.iter().enumerate() {
2055 let sum = col_sums[col as usize];
2056 if sum.into() > NORMALISE_MIN_SUM {
2057 csr.data[idx] /= sum;
2058 }
2059 }
2060}
2061
2062pub fn normalise_csr_rows_l1<T>(csr: &mut CompressedSparseData2<T>) -> Result<(), SparseErrors>
2084where
2085 T: BixverseNumeric + Into<f64>,
2086 T: std::iter::Sum<T>,
2087{
2088 require_csr(csr)?;
2089
2090 let nrows = csr.shape.0;
2091
2092 for i in 0..nrows {
2093 let start = csr.indptr[i] as usize;
2094 let end = csr.indptr[i + 1] as usize;
2095 let row_data_slice = &mut csr.data[start..end];
2096
2097 let row_sum: T = row_data_slice.iter().copied().sum();
2098
2099 if row_sum.into() <= NORMALISE_MIN_SUM {
2100 return Err(SparseErrors::SparseMatrixIsolatedRow {
2101 row: i,
2102 row_sum: row_sum.into(),
2103 });
2104 }
2105
2106 for val in row_data_slice.iter_mut() {
2107 *val /= row_sum;
2108 }
2109 }
2110
2111 Ok(())
2112}
2113
2114pub fn eliminate_zeros_csr<T>(mat: CompressedSparseData2<T>) -> CompressedSparseData2<T>
2124where
2125 T: BixverseNumeric,
2126 <T as std::ops::Add>::Output: std::cmp::PartialEq<T>,
2127{
2128 let mut rows: Vec<u32> = Vec::new();
2129 let mut cols: Vec<u32> = Vec::new();
2130 let mut vals = Vec::new();
2131
2132 let n_rows = mat.shape.0;
2133 for i in 0..n_rows {
2134 let start = mat.indptr[i] as usize;
2135 let end = mat.indptr[i + 1] as usize;
2136
2137 for j in start..end {
2138 if mat.data[j] != T::default() {
2139 rows.push(i as u32);
2140 cols.push(mat.indices[j]);
2141 vals.push(mat.data[j]);
2142 }
2143 }
2144 }
2145
2146 coo_to_csr(&rows, &cols, &vals, mat.shape)
2147}
2148
2149pub fn csr_matvec<T>(mat: &CompressedSparseData2<T>, vec: &[T]) -> Result<Vec<T>, SparseErrors>
2162where
2163 T: BixverseNumeric,
2164 <T as std::ops::Add>::Output: std::cmp::PartialEq<T>,
2165{
2166 require_csr(mat)?;
2167
2168 let mut result = vec![T::default(); mat.shape.0];
2169 for i in 0..mat.shape.0 {
2170 let row_start = mat.indptr[i] as usize;
2171 let row_end = mat.indptr[i + 1] as usize;
2172 let data = &mat.data[row_start..row_end];
2173 let cols = &mat.indices[row_start..row_end];
2174 let mut acc = [T::default(); 4];
2175 let mut data_chunks = data.chunks_exact(4);
2176 let mut col_chunks = cols.chunks_exact(4);
2177 for (d, c) in (&mut data_chunks).zip(&mut col_chunks) {
2178 for l in 0..4 {
2179 acc[l] += d[l] * vec[c[l] as usize];
2180 }
2181 }
2182 let mut sum = (acc[0] + acc[1]) + (acc[2] + acc[3]);
2183 for (&d, &c) in data_chunks.remainder().iter().zip(col_chunks.remainder()) {
2184 sum += d * vec[c as usize];
2185 }
2186 result[i] = sum;
2187 }
2188
2189 Ok(result)
2190}
2191
2192pub fn csr_vecmat<T>(vec: &[T], mat: &CompressedSparseData2<T>) -> Result<Vec<T>, SparseErrors>
2207where
2208 T: BixverseNumeric,
2209{
2210 require_csr(mat)?;
2211
2212 let n_cols = mat.ncols();
2213 let mut out = vec![T::default(); n_cols];
2214 for (j, &vj) in vec.iter().enumerate() {
2215 if vj == T::default() {
2216 continue;
2217 }
2218 let gs = mat.indptr[j] as usize;
2219 let ge = mat.indptr[j + 1] as usize;
2220 for q in gs..ge {
2221 let t = mat.indices[q] as usize;
2222 out[t] += vj * mat.data[q];
2223 }
2224 }
2225
2226 Ok(out)
2227}
2228
2229pub fn csr_matmul_csr<T>(
2240 a: &CompressedSparseData2<T>,
2241 b: &CompressedSparseData2<T>,
2242) -> Result<CompressedSparseData2<T>, SparseErrors>
2243where
2244 T: BixverseNumeric,
2245{
2246 require_matmul_operands(a, b)?;
2247
2248 let nrows = a.shape.0;
2249 let ncols = b.shape.1;
2250
2251 let n_chunks = nrows.div_ceil(SPARSE_MATMUL_ROW_CHUNK);
2252
2253 let chunks: Vec<(Vec<usize>, Vec<T>, Vec<usize>)> = (0..n_chunks)
2254 .into_par_iter()
2255 .map_init(
2256 || SparseAccumulator::new(ncols),
2257 |acc, chunk_idx| {
2258 let start = chunk_idx * SPARSE_MATMUL_ROW_CHUNK;
2259 let end = ((chunk_idx + 1) * SPARSE_MATMUL_ROW_CHUNK).min(nrows);
2260
2261 let mut idx_buf = Vec::new();
2262 let mut data_buf = Vec::new();
2263 let mut lengths = Vec::with_capacity(end - start);
2264
2265 for i in start..end {
2266 for a_idx in a.indptr[i] as usize..a.indptr[i + 1] as usize {
2267 let k = a.indices[a_idx] as usize;
2268 let a_val = a.data[a_idx];
2269 for b_idx in b.indptr[k] as usize..b.indptr[k + 1] as usize {
2270 unsafe {
2271 acc.add(b.indices[b_idx] as usize, a_val * b.data[b_idx]);
2272 }
2273 }
2274 }
2275 lengths.push(acc.extract_into(&mut idx_buf, &mut data_buf));
2276 }
2277
2278 (idx_buf, data_buf, lengths)
2279 },
2280 )
2281 .collect();
2282
2283 let total_nnz: usize = chunks.iter().map(|(i, _, _)| i.len()).sum();
2284
2285 let mut indptr: Vec<u32> = Vec::with_capacity(nrows + 1);
2286 indptr.push(0);
2287 let mut running = 0usize;
2288 for (_, _, lengths) in &chunks {
2289 for &len in lengths {
2290 running += len;
2291 indptr.push(running as u32);
2292 }
2293 }
2294
2295 let mut indices: Vec<u32> = Vec::with_capacity(total_nnz);
2296 let mut data = Vec::with_capacity(total_nnz);
2297 for (idx_buf, data_buf, _) in chunks {
2298 indices.extend(idx_buf.into_iter().map(|x| x as u32));
2299 data.extend(data_buf);
2300 }
2301
2302 Ok(CompressedSparseData2 {
2304 data,
2305 indices,
2306 indptr,
2307 cs_type: CompressedSparseFormat::Csr,
2308 data_2: None,
2309 shape: (nrows, ncols),
2310 })
2311}
2312
2313#[cfg(test)]
2320mod tests {
2321 use super::*;
2322 use approx::assert_relative_eq;
2323
2324 fn validatable_csr() -> CompressedSparseData2<f32, f32> {
2330 CompressedSparseData2 {
2331 data: vec![1.0, 2.0, 3.0, 4.0],
2332 indices: vec![0, 3, 1, 2],
2333 indptr: vec![0, 2, 2, 4],
2334 cs_type: CompressedSparseFormat::Csr,
2335 data_2: Some(vec![10.0, 20.0, 30.0, 40.0]),
2336 shape: (3, 4),
2337 }
2338 }
2339
2340 #[test]
2341 fn test_validate_accepts_a_well_formed_matrix() {
2342 assert!(validatable_csr().validate().is_ok());
2343 assert!(validatable_csr().transform().validate().is_ok());
2344 }
2345
2346 #[test]
2349 fn test_validate_accepts_an_empty_unused_layer() {
2350 let single = validatable_csr().transform_single_layer(true).unwrap();
2351
2352 assert!(single.data.is_empty());
2353 assert!(single.validate().is_ok());
2354 }
2355
2356 #[test]
2359 fn test_validate_rejects_a_short_indptr() {
2360 let mut m = validatable_csr();
2361 m.indptr = vec![0, 2, 4];
2362
2363 assert!(matches!(
2364 m.validate(),
2365 Err(SparseErrors::SparseIndptrInvalid {
2366 expected: 4,
2367 got: 3,
2368 ..
2369 })
2370 ));
2371 }
2372
2373 #[test]
2374 fn test_validate_rejects_a_final_offset_past_the_indices() {
2375 let mut m = validatable_csr();
2376 m.indptr = vec![0, 2, 2, 9];
2377
2378 assert!(matches!(
2379 m.validate(),
2380 Err(SparseErrors::SparseIndptrInvalid {
2381 expected: 4,
2382 got: 9,
2383 ..
2384 })
2385 ));
2386 }
2387
2388 #[test]
2389 fn test_validate_rejects_decreasing_offsets() {
2390 let mut m = validatable_csr();
2391 m.indptr = vec![0, 3, 1, 4];
2392
2393 assert!(matches!(
2394 m.validate(),
2395 Err(SparseErrors::SparseIndptrInvalid { .. })
2396 ));
2397 }
2398
2399 #[test]
2402 fn test_validate_rejects_an_index_past_the_minor_axis() {
2403 let mut m = validatable_csr();
2404 m.indices = vec![0, 3, 1, 4];
2405
2406 assert!(matches!(
2407 m.validate(),
2408 Err(SparseErrors::SliceIndexOutOfBounds { index: 4, len: 4 })
2409 ));
2410 }
2411
2412 #[test]
2415 fn test_validate_bounds_csc_by_the_row_count() {
2416 let mut m = validatable_csr();
2417 m.cs_type = CompressedSparseFormat::Csc;
2418 m.shape = (4, 3);
2419
2420 assert!(m.validate().is_ok());
2422
2423 m.shape = (3, 3);
2424 assert!(matches!(
2425 m.validate(),
2426 Err(SparseErrors::SliceIndexOutOfBounds { index: 3, len: 3 })
2427 ));
2428 }
2429
2430 #[test]
2431 fn test_validate_rejects_a_truncated_second_layer() {
2432 let mut m = validatable_csr();
2433 m.data_2 = Some(vec![10.0, 20.0]);
2434
2435 assert!(matches!(
2436 m.validate(),
2437 Err(SparseErrors::DimensionMisMatchSparse {
2438 indices_len: 4,
2439 data_len: 2
2440 })
2441 ));
2442 }
2443
2444 #[test]
2446 fn test_parse_sparse_format() {
2447 assert!(parse_compressed_sparse_format("csr").unwrap().is_csr());
2448 assert!(parse_compressed_sparse_format("CSC").unwrap().is_csc());
2449 assert!(parse_compressed_sparse_format("dense").is_none());
2450 }
2451
2452 fn transpose_fixture() -> CompressedSparseData2<f64, f64> {
2455 let data: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
2456 let data2: Vec<f64> = data.iter().map(|v| v * 10.0).collect();
2457 let indices: Vec<u32> = vec![0, 2, 1, 0, 1, 3, 2, 3];
2458 let indptr: Vec<u32> = vec![0, 2, 3, 3, 6, 8];
2459
2460 CompressedSparseData2::new_csr(&data, &indices, &indptr, Some(&data2), (5, 4))
2461 }
2462
2463 #[test]
2468 fn test_transpose_chunked_matches_single_chunk() {
2469 let csr = transpose_fixture();
2470
2471 let serial = transpose_sparse_chunked(&csr, &[(0, 5)], usize::MAX);
2472
2473 for (chunks, band) in [
2474 (vec![(0, 1), (1, 3), (3, 5)], usize::MAX),
2475 (vec![(0, 2), (2, 5)], usize::MAX),
2476 (vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], usize::MAX),
2477 (vec![(0, 5)], 1),
2478 (vec![(0, 2), (2, 5)], 1),
2479 (vec![(0, 1), (1, 3), (3, 5)], 2),
2480 (vec![(0, 2), (2, 5)], 3),
2481 (vec![(0, 2), (2, 5)], 4),
2482 ] {
2483 let parallel = transpose_sparse_chunked(&csr, &chunks, band);
2484
2485 assert_eq!(parallel.indptr, serial.indptr);
2486 assert_eq!(parallel.indices, serial.indices);
2487 assert_eq!(parallel.data, serial.data);
2488 assert_eq!(parallel.data_2, serial.data_2);
2489 assert_eq!(parallel.cs_type.is_csc(), serial.cs_type.is_csc());
2490 assert_eq!(parallel.shape, serial.shape);
2491 }
2492 }
2493
2494 #[test]
2497 fn test_transpose_unsorted_runs_ignore_band() {
2498 let data: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0];
2499 let indices: Vec<u32> = vec![2, 0, 3, 1, 0];
2500 let indptr: Vec<u32> = vec![0, 2, 4, 5];
2501 let csr =
2502 CompressedSparseData2::<f64, f64>::new_csr(&data, &indices, &indptr, None, (3, 4));
2503
2504 let csc = transpose_sparse_chunked(&csr, &[(0, 1), (1, 3)], 1);
2505 assert_eq!(csc.indptr, vec![0, 2, 3, 4, 5]);
2506 assert_eq!(csc.indices, vec![0, 2, 1, 0, 1]);
2507 assert_eq!(csc.data, vec![2.0, 5.0, 4.0, 1.0, 3.0]);
2508 }
2509
2510 #[test]
2513 fn test_transpose_round_trips_and_stays_sorted() {
2514 let csr = transpose_fixture();
2515 let csc = transpose_sparse(&csr);
2516
2517 assert!(csc.cs_type.is_csc());
2518 for major in 0..csc.indptr.len() - 1 {
2519 let lo = csc.indptr[major] as usize;
2520 let hi = csc.indptr[major + 1] as usize;
2521 assert!(csc.indices[lo..hi].windows(2).all(|w| w[0] < w[1]));
2522 }
2523
2524 let round_trip = transpose_sparse(&csc);
2525 assert_eq!(round_trip.indptr, csr.indptr);
2526 assert_eq!(round_trip.indices, csr.indices);
2527 assert_eq!(round_trip.data, csr.data);
2528 assert_eq!(round_trip.data_2, csr.data_2);
2529 }
2530
2531 #[test]
2534 fn test_transpose_single_layer_matches_full_transpose() {
2535 let csr = transpose_fixture();
2536 let full = transpose_sparse(&csr);
2537
2538 let counts_only = csr.transform_single_layer(false).unwrap();
2539 assert_eq!(counts_only.data, full.data);
2540 assert!(counts_only.data_2.is_none());
2541
2542 let norm_only = csr.transform_single_layer(true).unwrap();
2543 assert_eq!(norm_only.data_2, full.data_2);
2544 assert!(norm_only.data.is_empty());
2545
2546 assert_eq!(counts_only.indptr, full.indptr);
2547 assert_eq!(norm_only.indices, full.indices);
2548 }
2549
2550 #[test]
2552 fn test_undirected_edges_to_csr_is_symmetric_and_row_sorted() {
2553 let edges = vec![(0u32, 1u32, 1.0f64), (0, 3, 2.0), (1, 2, 3.0), (2, 3, 4.0)];
2558
2559 let csr = undirected_edges_to_csr(4, &edges);
2560
2561 assert_eq!(csr.shape, (4, 4));
2562 assert_eq!(csr.indptr, vec![0, 2, 4, 6, 8]);
2563 assert_eq!(csr.indices, vec![1, 3, 0, 2, 1, 3, 0, 2]);
2564 assert_eq!(csr.data, vec![1.0, 2.0, 1.0, 3.0, 3.0, 4.0, 2.0, 4.0]);
2565
2566 for i in 0..4 {
2568 let lo = csr.indptr[i] as usize;
2569 let hi = csr.indptr[i + 1] as usize;
2570 assert!(csr.indices[lo..hi].windows(2).all(|w| w[0] < w[1]));
2571 }
2572 }
2573
2574 #[test]
2576 fn test_undirected_edges_to_csr_handles_empty_input() {
2577 let csr = undirected_edges_to_csr::<f64>(3, &[]);
2578
2579 assert_eq!(csr.shape, (3, 3));
2580 assert_eq!(csr.indptr, vec![0, 0, 0, 0]);
2581 assert!(csr.indices.is_empty());
2582 }
2583
2584 #[test]
2586 fn test_undirected_edges_to_csr_keeps_isolated_nodes() {
2587 let edges = vec![(1u32, 2u32, 7.0f64)];
2590
2591 let csr = undirected_edges_to_csr(3, &edges);
2592
2593 assert_eq!(csr.indptr, vec![0, 0, 1, 2]);
2594 assert_eq!(csr.indices, vec![2, 1]);
2595 }
2596
2597 #[test]
2599 fn test_validate_square_csr_accepts_a_sound_matrix() {
2600 let csr = undirected_edges_to_csr(3, &[(0u32, 1u32, 1.0f64), (1, 2, 2.0)]);
2601
2602 assert_eq!(validate_square_csr(&csr).unwrap(), 3);
2603 }
2604
2605 #[test]
2607 fn test_validate_square_csr_catches_structural_faults() {
2608 let bad_indptr = CompressedSparseData2::new_csr(
2611 &[1.0f64; 7],
2612 &[1u32, 2, 0, 2, 0, 0, 1],
2613 &[0u32, 5, 2, 7],
2614 None,
2615 (3, 3),
2616 );
2617 assert!(matches!(
2618 validate_square_csr(&bad_indptr),
2619 Err(SparseErrors::MalformedCsr(_))
2620 ));
2621
2622 let bad_index = CompressedSparseData2::new_csr(
2625 &[1.0f64, 1.0],
2626 &[1u32, 9],
2627 &[0u32, 1, 2, 2],
2628 None,
2629 (3, 3),
2630 );
2631 assert!(matches!(
2632 validate_square_csr(&bad_index),
2633 Err(SparseErrors::MalformedCsr(_))
2634 ));
2635
2636 let short_indptr =
2638 CompressedSparseData2::new_csr(&[1.0f64], &[1u32], &[0u32, 1], None, (3, 3));
2639 assert!(matches!(
2640 validate_square_csr(&short_indptr),
2641 Err(SparseErrors::MalformedCsr(_))
2642 ));
2643
2644 let short_tail = CompressedSparseData2::new_csr(
2646 &[1.0f64, 1.0],
2647 &[1u32, 0],
2648 &[0u32, 1, 1, 1],
2649 None,
2650 (3, 3),
2651 );
2652 assert!(matches!(
2653 validate_square_csr(&short_tail),
2654 Err(SparseErrors::MalformedCsr(_))
2655 ));
2656 }
2657 #[test]
2659 fn test_sparse_add_csr() {
2660 let shape = (2, 2);
2661 let a = CompressedSparseData2::<f64, f64>::new_csr(
2662 &[1.0, 2.0],
2663 &[0, 1],
2664 &[0, 1, 2],
2665 None,
2666 shape,
2667 );
2668 let b = CompressedSparseData2::<f64, f64>::new_csr(
2669 &[3.0, 4.0],
2670 &[1, 1],
2671 &[0, 1, 2],
2672 None,
2673 shape,
2674 );
2675 let c = sparse_add_csr(&a, &b).unwrap();
2676
2677 assert_eq!(c.data, vec![1.0, 3.0, 6.0]);
2678 assert_eq!(c.indices, vec![0, 1, 1]);
2679 assert_eq!(c.indptr, vec![0, 2, 3]);
2680 }
2681
2682 #[test]
2684 fn test_csr_matvec() {
2685 let a = CompressedSparseData2::<f64, f64>::new_csr(
2686 &[1.0, 2.0, 3.0],
2687 &[0, 1, 1],
2688 &[0, 2, 3],
2689 None,
2690 (2, 2),
2691 );
2692 let vec = vec![2.0, 1.0];
2693 let result = csr_matvec(&a, &vec).unwrap();
2694 assert_eq!(result, vec![4.0, 3.0]);
2695 }
2696
2697 #[test]
2703 fn test_normalise_csr_rows_l1_isolated_row_errors() {
2704 let data = vec![1.0f64, 3.0, 0.0];
2707 let indices = vec![0u32, 2, 1];
2708 let indptr = vec![0u32, 2, 2, 3];
2709 let mut csr = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (3, 3));
2710
2711 match normalise_csr_rows_l1(&mut csr) {
2712 Err(SparseErrors::SparseMatrixIsolatedRow { row, row_sum }) => {
2713 assert_eq!(row, 1);
2714 assert_eq!(row_sum, 0.0);
2715 }
2716 other => panic!("expected SparseMatrixIsolatedRow, got {:?}", other),
2717 }
2718 }
2719
2720 #[test]
2722 fn test_normalise_csr_rows_l1_rows_sum_to_one() {
2723 let data = vec![1.0f64, 3.0, 2.0, 2.0, 5.0];
2724 let indices = vec![0u32, 2, 1, 2, 0];
2725 let indptr = vec![0u32, 2, 4, 5];
2726 let mut csr = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (3, 3));
2727
2728 normalise_csr_rows_l1(&mut csr).unwrap();
2729
2730 for i in 0..3 {
2731 let lo = csr.indptr[i] as usize;
2732 let hi = csr.indptr[i + 1] as usize;
2733 let sum: f64 = csr.data[lo..hi].iter().sum();
2734 assert_relative_eq!(sum, 1.0, epsilon = 1e-12);
2735 }
2736 assert_relative_eq!(csr.data[0], 0.25, epsilon = 1e-12);
2737 assert_relative_eq!(csr.data[1], 0.75, epsilon = 1e-12);
2738 }
2739}