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