1use bincode::{Decode, Encode, config, decode_from_slice, serde::encode_to_vec};
6use half::f16;
7use half::slice::HalfFloatSliceExt;
8use indexmap::IndexSet;
9use lz4_flex::block::{decompress_into, uncompressed_size};
10use lz4_flex::{compress_prepend_size, decompress_size_prepended};
11use memmap2::MmapOptions;
12use num_traits::FromPrimitive;
13use rayon::prelude::*;
14use rustc_hash::{FxHashMap, FxHashSet};
15use serde::{Deserialize, Serialize};
16use std::{
17 fs::File,
18 io::{BufWriter, Seek, SeekFrom, Write},
19 marker::Sync,
20 path::Path,
21 sync::Arc,
22};
23
24use crate::prelude::*;
25use crate::v4_io::{
26 ChunkCodec, ChunkFormat, V4Ctx, V4Decoded, V4Header, V4Run, decode_v4, decode_v4_into,
27 gene_norm_table, v4_cell_chunk, v4_gene_chunk,
28};
29
30#[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone)]
42pub enum RawCounts {
43 U16(Vec<u16>),
45 U32(Vec<u32>),
48}
49
50pub(crate) const RAW_ELEM_U16: u8 = 2;
53pub(crate) const RAW_ELEM_U32: u8 = 4;
54
55const CSR_CHUNK_HEADER_LEN: usize = 32;
58
59const CSC_CHUNK_HEADER_LEN: usize = 36;
62
63const GENE_FLUSH_FREQUENCY: usize = 1_000;
68
69const CELL_FLUSH_FREQUENCY: usize = 100_000;
74
75impl RawCounts {
76 pub fn get(&self, i: usize) -> u32 {
82 match self {
83 RawCounts::U16(v) => v[i] as u32,
84 RawCounts::U32(v) => v[i],
85 }
86 }
87
88 pub fn len(&self) -> usize {
94 match self {
95 RawCounts::U16(v) => v.len(),
96 RawCounts::U32(v) => v.len(),
97 }
98 }
99
100 pub fn is_empty(&self) -> bool {
106 self.len() == 0
107 }
108
109 pub fn iter(&self) -> RawCountsIter<'_> {
115 RawCountsIter {
116 inner: self,
117 pos: 0,
118 }
119 }
120
121 pub fn elem_size(&self) -> u8 {
127 match self {
128 RawCounts::U16(_) => RAW_ELEM_U16,
129 RawCounts::U32(_) => RAW_ELEM_U32,
130 }
131 }
132
133 pub fn write_bytes(&self, writer: &mut impl Write) -> std::io::Result<()> {
143 match self {
145 RawCounts::U16(v) => {
146 let mut bytes = Vec::with_capacity(v.len() * 2);
147 for x in v {
148 bytes.extend_from_slice(&x.to_le_bytes());
149 }
150 writer.write_all(&bytes)
151 }
152 RawCounts::U32(v) => {
153 let mut bytes = Vec::with_capacity(v.len() * 4);
154 for x in v {
155 bytes.extend_from_slice(&x.to_le_bytes());
156 }
157 writer.write_all(&bytes)
158 }
159 }
160 }
161
162 pub fn read_from_buffer(
177 buffer: &[u8],
178 count: usize,
179 elem_size: u8,
180 ) -> Result<Self, ScDataErrors> {
181 let width = match elem_size {
183 RAW_ELEM_U32 => 4_usize,
184 RAW_ELEM_U16 | 0 => 2_usize,
185 other => return Err(ScDataErrors::RawElemSizeInvalid(other)),
186 };
187
188 let needed = count * width;
189 if buffer.len() < needed {
190 return Err(ScDataErrors::ChunkPayloadTruncated {
191 expected: needed,
192 found: buffer.len(),
193 });
194 }
195
196 Ok(if width == 4 {
197 RawCounts::U32(
198 buffer[..needed]
199 .chunks_exact(4)
200 .map(|c| {
201 u32::from_le_bytes(c.try_into().expect("4-byte chunk by construction"))
202 })
203 .collect(),
204 )
205 } else {
206 RawCounts::U16(
207 buffer[..needed]
208 .chunks_exact(2)
209 .map(|c| {
210 u16::from_le_bytes(c.try_into().expect("2-byte chunk by construction"))
211 })
212 .collect(),
213 )
214 })
215 }
216
217 pub fn from_u32_auto(data: &[u32]) -> Self {
222 if data.iter().all(|&v| v <= u16::MAX as u32) {
223 RawCounts::U16(data.iter().map(|&v| v as u16).collect())
224 } else {
225 RawCounts::U32(data.to_vec())
226 }
227 }
228}
229
230pub struct RawCountsIter<'a> {
232 inner: &'a RawCounts,
234 pos: usize,
236}
237
238impl<'a> Iterator for RawCountsIter<'a> {
239 type Item = u32;
240 fn next(&mut self) -> Option<u32> {
241 if self.pos < self.inner.len() {
242 let val = self.inner.get(self.pos);
243 self.pos += 1;
244 Some(val)
245 } else {
246 None
247 }
248 }
249}
250
251#[derive(Clone, Debug)]
257pub struct CellQuality {
258 pub cell_indices: Vec<usize>,
260 pub gene_indices: Vec<usize>,
262 pub lib_size: Vec<usize>,
264 pub nnz: Vec<usize>,
266}
267
268impl CellQuality {
269 pub fn set_cell_indices(&mut self, cell_indices: &[usize]) {
275 self.cell_indices = cell_indices.to_vec();
276 }
277
278 pub fn set_gene_indices(&mut self, gene_indices: &[usize]) {
284 self.gene_indices = gene_indices.to_vec();
285 }
286}
287
288#[derive(Clone, Debug)]
290pub struct MinCellQuality {
291 pub min_unique_genes: usize,
294 pub min_lib_size: usize,
296 pub min_cells: usize,
298 pub target_size: f32,
301}
302
303#[derive(Debug, Clone)]
316pub struct CellOnFileQuality {
317 pub cells_to_keep: Vec<usize>,
319 pub genes_to_keep: Vec<usize>,
321 pub cells_to_keep_set: FxHashSet<usize>,
323 pub genes_to_keep_set: FxHashSet<usize>,
325 pub cell_old_to_new: FxHashMap<usize, usize>,
327 pub gene_old_to_new: FxHashMap<usize, usize>,
329}
330
331impl CellOnFileQuality {
332 pub fn new(cells_to_keep: Vec<usize>, genes_to_keep: Vec<usize>) -> Self {
343 Self {
344 cells_to_keep,
345 genes_to_keep,
346 cells_to_keep_set: FxHashSet::default(),
347 genes_to_keep_set: FxHashSet::default(),
348 cell_old_to_new: FxHashMap::default(),
349 gene_old_to_new: FxHashMap::default(),
350 }
351 }
352
353 pub fn generate_maps_sets(&mut self) {
355 let cells_to_keep_set: FxHashSet<usize> = self.cells_to_keep.iter().cloned().collect();
356 let genes_to_keep_set: FxHashSet<usize> = self.genes_to_keep.iter().cloned().collect();
357 let cell_old_to_new: FxHashMap<usize, usize> = self
358 .cells_to_keep
359 .iter()
360 .enumerate()
361 .map(|(new_idx, &old_idx)| (old_idx, new_idx))
362 .collect();
363 let gene_old_to_new: FxHashMap<usize, usize> = self
364 .genes_to_keep
365 .iter()
366 .enumerate()
367 .map(|(new_idx, &old_idx)| (old_idx, new_idx))
368 .collect();
369
370 self.cells_to_keep_set = cells_to_keep_set;
371 self.genes_to_keep_set = genes_to_keep_set;
372 self.cell_old_to_new = cell_old_to_new;
373 self.gene_old_to_new = gene_old_to_new
374 }
375}
376
377pub fn norm_value(x: f32, lib_size: f32, target_size: f32) -> F16 {
397 F16::from_f32((x / lib_size * target_size).ln_1p())
398}
399
400struct ChunkPayload<'a> {
413 data_raw: &'a [u8],
415 data_raw_len: usize,
417 raw_elem_size: u8,
419 raw_width: usize,
421 data_norm: &'a [u8],
423 indices: &'a [u8],
425}
426
427impl<'a> ChunkPayload<'a> {
428 fn slice(
443 buffer: &'a [u8],
444 header_len: usize,
445 lens: (usize, usize, usize),
446 raw_elem_size: u8,
447 ) -> Result<Self, ScDataErrors> {
448 let (data_raw_len, data_norm_len, indices_len) = lens;
449
450 let raw_width = match raw_elem_size {
452 RAW_ELEM_U32 => 4,
453 RAW_ELEM_U16 | 0 => 2,
454 other => return Err(ScDataErrors::RawElemSizeInvalid(other)),
455 };
456
457 let data_end = header_len + data_raw_len * raw_width;
458 let norm_end = data_end + data_norm_len * 2;
459 let total_end = norm_end + indices_len * 4;
460
461 if buffer.len() < total_end {
464 return Err(ScDataErrors::ChunkPayloadTruncated {
465 expected: total_end,
466 found: buffer.len(),
467 });
468 }
469
470 Ok(Self {
471 data_raw: &buffer[header_len..data_end],
472 data_raw_len,
473 raw_elem_size,
474 raw_width,
475 data_norm: &buffer[data_end..norm_end],
476 indices: &buffer[norm_end..total_end],
477 })
478 }
479
480 fn norm_iter(&self) -> impl Iterator<Item = F16> + '_ {
486 self.data_norm
487 .chunks_exact(2)
488 .map(|c| F16::from_le_bytes(c.try_into().expect("2-byte chunk by construction")))
489 }
490
491 fn index_iter(&self) -> impl Iterator<Item = u32> + '_ {
497 self.indices
498 .chunks_exact(4)
499 .map(|c| u32::from_le_bytes(c.try_into().expect("4-byte chunk by construction")))
500 }
501
502 fn raw_counts(&self) -> Result<RawCounts, ScDataErrors> {
508 RawCounts::read_from_buffer(self.data_raw, self.data_raw_len, self.raw_elem_size)
509 }
510}
511
512const F16_BLOCK: usize = 64;
520
521pub(crate) trait RunSource {
525 fn nnz(&self) -> usize;
531
532 fn index_at(&self, i: usize) -> u32;
542
543 fn raw_at(&self, i: usize) -> u32;
553
554 fn norm_at(&self, i: usize) -> f32;
564
565 fn extend_indices(&self, out: &mut Vec<u32>);
571
572 fn extend_raw<T: FromPrimitive>(&self, out: &mut Vec<T>) -> Result<(), ScDataErrors>;
583
584 fn extend_norm(&self, out: &mut Vec<f32>);
590}
591
592#[inline]
607pub(crate) fn extend_raw_checked<T, I>(out: &mut Vec<T>, vals: I) -> Result<(), ScDataErrors>
608where
609 T: FromPrimitive,
610 I: Iterator<Item = u32> + Clone,
611{
612 raw_to::<T>(vals.clone().max().unwrap_or(0))?;
613 out.extend(vals.map(|v| T::from_u32(v).expect("the maximum fits, so every value does")));
614 Ok(())
615}
616
617impl RunSource for ChunkPayload<'_> {
618 #[inline]
619 fn nnz(&self) -> usize {
620 self.indices.len() / 4
621 }
622
623 #[inline(always)]
624 fn index_at(&self, i: usize) -> u32 {
625 let b = &self.indices[i * 4..i * 4 + 4];
626 u32::from_le_bytes([b[0], b[1], b[2], b[3]])
627 }
628
629 #[inline(always)]
630 fn raw_at(&self, i: usize) -> u32 {
631 if self.raw_width == 4 {
632 let b = &self.data_raw[i * 4..i * 4 + 4];
633 u32::from_le_bytes([b[0], b[1], b[2], b[3]])
634 } else {
635 let b = &self.data_raw[i * 2..i * 2 + 2];
636 u16::from_le_bytes([b[0], b[1]]) as u32
637 }
638 }
639
640 #[inline(always)]
641 fn norm_at(&self, i: usize) -> f32 {
642 let b = &self.data_norm[i * 2..i * 2 + 2];
643 F16::from_le_bytes([b[0], b[1]]).to_f32()
644 }
645
646 fn extend_indices(&self, out: &mut Vec<u32>) {
647 out.extend(self.index_iter());
648 }
649
650 fn extend_raw<T: FromPrimitive>(&self, out: &mut Vec<T>) -> Result<(), ScDataErrors> {
651 if self.raw_width == 4 {
652 extend_raw_checked(
653 out,
654 self.data_raw.chunks_exact(4).map(|c| {
655 u32::from_le_bytes(c.try_into().expect("4-byte chunk by construction"))
656 }),
657 )
658 } else {
659 extend_raw_checked(
660 out,
661 self.data_raw.chunks_exact(2).map(|c| {
662 u16::from_le_bytes(c.try_into().expect("2-byte chunk by construction")) as u32
663 }),
664 )
665 }
666 }
667
668 fn extend_norm(&self, out: &mut Vec<f32>) {
669 let start = out.len();
670 out.resize(start + self.data_norm.len() / 2, 0.0);
671 let mut block = [f16::ZERO; F16_BLOCK];
672 for (src, dst) in self
673 .data_norm
674 .chunks(2 * F16_BLOCK)
675 .zip(out[start..].chunks_mut(F16_BLOCK))
676 {
677 for (b, c) in block.iter_mut().zip(src.chunks_exact(2)) {
678 *b = f16::from_le_bytes(c.try_into().expect("2-byte chunk by construction"));
679 }
680 block[..dst.len()].convert_to_f32_slice(dst);
681 }
682 }
683}
684
685#[inline]
696fn header_u32(header: &[u8], at: usize) -> usize {
697 u32::from_le_bytes([header[at], header[at + 1], header[at + 2], header[at + 3]]) as usize
698}
699
700struct CsrChunkView<'a> {
710 payload: ChunkPayload<'a>,
712 library_size: usize,
714 original_index: usize,
716 to_keep: bool,
718}
719
720impl<'a> CsrChunkView<'a> {
721 fn parse(buffer: &'a [u8]) -> Result<Self, ScDataErrors> {
733 if buffer.len() < CSR_CHUNK_HEADER_LEN {
734 return Err(ScDataErrors::ChunkBufferTooSmall {
735 expected: CSR_CHUNK_HEADER_LEN,
736 found: buffer.len(),
737 });
738 }
739
740 let header = &buffer[0..CSR_CHUNK_HEADER_LEN];
741 let lens = (
742 header_u32(header, 0),
743 header_u32(header, 4),
744 header_u32(header, 8),
745 );
746
747 Ok(Self {
748 payload: ChunkPayload::slice(buffer, CSR_CHUNK_HEADER_LEN, lens, header[29])?,
749 library_size: le_u64_at(header, 12) as usize,
750 original_index: le_u64_at(header, 20) as usize,
751 to_keep: header[28] != 0,
752 })
753 }
754}
755
756fn csr_payload(buffer: &[u8]) -> Result<ChunkPayload<'_>, ScDataErrors> {
766 CsrChunkView::parse(buffer).map(|view| view.payload)
767}
768
769fn csc_payload(buffer: &[u8]) -> Result<ChunkPayload<'_>, ScDataErrors> {
784 if buffer.len() < CSC_CHUNK_HEADER_LEN {
785 return Err(ScDataErrors::ChunkBufferTooSmall {
786 expected: CSC_CHUNK_HEADER_LEN,
787 found: buffer.len(),
788 });
789 }
790
791 let lens = (
792 header_u32(buffer, 0),
793 header_u32(buffer, 4),
794 header_u32(buffer, 8),
795 );
796
797 ChunkPayload::slice(buffer, CSC_CHUNK_HEADER_LEN, lens, buffer[33])
798}
799
800#[derive(Debug)]
809pub struct CsrCellChunk {
810 pub data_raw: RawCounts,
813 pub data_norm: Vec<F16>,
816 pub library_size: usize,
818 pub indices: Vec<u32>,
820 pub original_index: usize,
822 pub to_keep: bool,
824}
825
826impl CsrCellChunk {
827 pub fn from_data<T, U>(
843 data: &[T],
844 col_idx: &[U],
845 original_index: usize,
846 size_factor: f32,
847 to_keep: bool,
848 ) -> Self
849 where
850 T: FloatAndUInt,
851 U: FloatAndUInt,
852 {
853 let sum = data.iter().map(|&x| x.to_f32()).sum::<f32>();
856 let data_norm: Vec<F16> = data
857 .iter()
858 .map(|&x| norm_value(x.to_f32(), sum, size_factor))
859 .collect();
860
861 let data_raw = if data.iter().all(|&x| x.to_u32() <= u16::MAX as u32) {
862 RawCounts::U16(data.iter().map(|&x| x.to_u32() as u16).collect())
863 } else {
864 RawCounts::U32(data.iter().map(|&x| x.to_u32()).collect())
865 };
866
867 Self {
868 data_raw,
869 data_norm,
870 library_size: sum as usize,
871 indices: col_idx.iter().map(|&x| x.to_u32()).collect::<Vec<u32>>(),
872 original_index,
873 to_keep,
874 }
875 }
876
877 pub fn write_to_bytes(&self, writer: &mut impl Write) -> std::io::Result<()> {
897 writer.write_all(&self.to_bytes())
898 }
899
900 fn to_bytes(&self) -> Vec<u8> {
907 let raw_bytes = self.data_raw.len() * self.data_raw.elem_size() as usize;
908 let mut buffer = Vec::with_capacity(
909 CSR_CHUNK_HEADER_LEN + raw_bytes + self.data_norm.len() * 2 + self.indices.len() * 4,
910 );
911
912 buffer.extend_from_slice(&(self.data_raw.len() as u32).to_le_bytes());
913 buffer.extend_from_slice(&(self.data_norm.len() as u32).to_le_bytes());
914 buffer.extend_from_slice(&(self.indices.len() as u32).to_le_bytes());
915 buffer.extend_from_slice(&(self.library_size as u64).to_le_bytes());
916 buffer.extend_from_slice(&(self.original_index as u64).to_le_bytes());
917 buffer.extend_from_slice(&[self.to_keep as u8, self.data_raw.elem_size(), 0, 0]);
918
919 match &self.data_raw {
920 RawCounts::U16(v) => {
921 for x in v {
922 buffer.extend_from_slice(&x.to_le_bytes());
923 }
924 }
925 RawCounts::U32(v) => {
926 for x in v {
927 buffer.extend_from_slice(&x.to_le_bytes());
928 }
929 }
930 }
931 for v in &self.data_norm {
932 buffer.extend_from_slice(&v.to_bits().to_le_bytes());
933 }
934 for i in &self.indices {
935 buffer.extend_from_slice(&i.to_le_bytes());
936 }
937
938 buffer
939 }
940
941 pub fn to_compressed_bytes(&self) -> std::io::Result<Vec<u8>> {
951 Ok(compress_prepend_size(&self.to_bytes()))
952 }
953
954 pub fn read_from_buffer(buffer: &[u8]) -> Result<Self, ScDataErrors> {
964 let view = CsrChunkView::parse(buffer)?;
965
966 Ok(Self {
967 data_raw: view.payload.raw_counts()?,
968 data_norm: view.payload.norm_iter().collect(),
969 library_size: view.library_size,
970 indices: view.payload.index_iter().collect(),
971 original_index: view.original_index,
972 to_keep: view.to_keep,
973 })
974 }
975
976 pub fn generate_chunks_sparse_data<T, U>(
987 sparse_data: CompressedSparseData2<T, U>,
988 cell_qc: MinCellQuality,
989 ) -> (Vec<CsrCellChunk>, CellQuality)
990 where
991 T: Clone + Default + Into<u32> + Sync,
992 U: Clone + Default,
993 {
994 let n_cells = sparse_data.indptr.len() - 1;
995 let n_genes = sparse_data.shape.1;
996
997 let mut no_cells_exp_gene = vec![0usize; n_genes];
999 for i in 0..n_cells {
1000 let start_i = sparse_data.indptr[i] as usize;
1001 let end_i = sparse_data.indptr[i + 1] as usize;
1002 for &gene_idx in &sparse_data.indices[start_i..end_i] {
1003 no_cells_exp_gene[gene_idx as usize] += 1;
1004 }
1005 }
1006
1007 let genes_to_keep: Vec<usize> = (0..n_genes)
1009 .filter(|&i| no_cells_exp_gene[i] >= cell_qc.min_cells)
1010 .collect();
1011
1012 let mut gene_index_map = vec![None; n_genes];
1014 for (new_idx, &old_idx) in genes_to_keep.iter().enumerate() {
1015 gene_index_map[old_idx] = Some(new_idx);
1016 }
1017
1018 let results: Vec<_> = (0..n_cells)
1020 .into_par_iter()
1021 .map(|i| {
1022 let start_i = sparse_data.indptr[i] as usize;
1023 let end_i = sparse_data.indptr[i + 1] as usize;
1024
1025 let mut filtered_data = Vec::new();
1026 let mut filtered_indices = Vec::new();
1027
1028 for idx in start_i..end_i {
1029 let old_gene_idx = sparse_data.indices[idx] as usize;
1030 if let Some(new_gene_idx) = gene_index_map[old_gene_idx] {
1031 filtered_data.push(sparse_data.data[idx].clone().into());
1032 filtered_indices.push(new_gene_idx);
1033 }
1034 }
1035
1036 let sum_data_i = filtered_data.iter().sum::<u32>() as usize;
1037 let nnz_i = filtered_indices.len();
1038
1039 let to_keep_i =
1040 (nnz_i >= cell_qc.min_unique_genes) && (sum_data_i >= cell_qc.min_lib_size);
1041
1042 let chunk_i = CsrCellChunk::from_data(
1043 &filtered_data,
1044 &filtered_indices,
1045 i,
1046 cell_qc.target_size,
1047 to_keep_i,
1048 );
1049
1050 (chunk_i, sum_data_i, nnz_i, to_keep_i, i)
1051 })
1052 .collect();
1053
1054 let mut res = Vec::with_capacity(n_cells);
1055 let mut lib_size = Vec::new();
1056 let mut nnz = Vec::new();
1057 let mut cells_to_keep = Vec::new();
1058
1059 for (chunk, lib, n, keep, idx) in results {
1060 res.push(chunk);
1061 if keep {
1062 lib_size.push(lib);
1063 nnz.push(n);
1064 cells_to_keep.push(idx);
1065 }
1066 }
1067
1068 let qc_data = CellQuality {
1069 cell_indices: cells_to_keep,
1070 gene_indices: genes_to_keep,
1071 lib_size,
1072 nnz,
1073 };
1074
1075 (res, qc_data)
1076 }
1077
1078 pub fn get_qc_info(&self) -> (usize, usize) {
1084 (self.indices.len(), self.library_size)
1085 }
1086
1087 pub fn filter_genes(&mut self, genes_to_keep: &[bool]) {
1097 let mut index_map = vec![None; genes_to_keep.len()];
1098 let mut new_idx = 0;
1099 for (old_idx, &keep) in genes_to_keep.iter().enumerate() {
1100 if keep {
1101 index_map[old_idx] = Some(new_idx);
1102 new_idx += 1;
1103 }
1104 }
1105
1106 let mut new_indices = Vec::new();
1107 let mut new_norm = Vec::new();
1108 let mut keep_positions = Vec::new();
1109
1110 for (i, &gene_idx) in self.indices.iter().enumerate() {
1111 if let Some(new_gene_idx) = index_map[gene_idx as usize] {
1112 new_indices.push(new_gene_idx);
1113 new_norm.push(self.data_norm[i]);
1114 keep_positions.push(i);
1115 }
1116 }
1117
1118 let new_raw = match &self.data_raw {
1119 RawCounts::U16(v) => RawCounts::U16(keep_positions.iter().map(|&p| v[p]).collect()),
1120 RawCounts::U32(v) => RawCounts::U32(keep_positions.iter().map(|&p| v[p]).collect()),
1121 };
1122
1123 self.indices = new_indices;
1124 self.data_raw = new_raw;
1125 self.data_norm = new_norm;
1126 }
1127}
1128
1129#[derive(Encode, Decode, Serialize, Deserialize, Debug)]
1141pub struct CscGeneChunk {
1142 pub data_raw: RawCounts,
1144 pub data_norm: Vec<F16>,
1147 pub avg_exp: F16,
1149 pub nnz: usize,
1151 pub indices: Vec<u32>,
1153 pub original_index: usize,
1155 pub to_keep: bool,
1157}
1158
1159impl CscGeneChunk {
1160 pub fn from_conversion(
1174 data_raw: RawCounts,
1175 data_norm: &[F16],
1176 col_idx: &[usize],
1177 original_index: usize,
1178 to_keep: bool,
1179 ) -> Self {
1180 let avg_exp = F16::from_f32(data_norm.iter().map(|v| v.to_f32()).sum::<f32>());
1186 let nnz = data_raw.len();
1187
1188 Self {
1189 data_raw,
1190 data_norm: data_norm.to_vec(),
1191 avg_exp,
1192 nnz,
1193 indices: col_idx.iter().map(|x| *x as u32).collect::<Vec<u32>>(),
1194 original_index,
1195 to_keep,
1196 }
1197 }
1198
1199 pub fn write_to_bytes(&self, writer: &mut impl Write) -> std::io::Result<()> {
1221 writer.write_all(&(self.data_raw.len() as u32).to_le_bytes())?;
1222 writer.write_all(&(self.data_norm.len() as u32).to_le_bytes())?;
1223 writer.write_all(&(self.indices.len() as u32).to_le_bytes())?;
1224 writer.write_all(&self.avg_exp.to_le_bytes())?;
1225 writer.write_all(&[0, 0])?;
1227 writer.write_all(&(self.nnz as u64).to_le_bytes())?;
1228 writer.write_all(&(self.original_index as u64).to_le_bytes())?;
1229 writer.write_all(&[self.to_keep as u8, self.data_raw.elem_size(), 0, 0])?;
1230
1231 self.data_raw.write_bytes(writer)?;
1232
1233 let mut data_norm_bytes = Vec::with_capacity(self.data_norm.len() * 2);
1234 for v in &self.data_norm {
1235 data_norm_bytes.extend_from_slice(&v.to_bits().to_le_bytes());
1236 }
1237 writer.write_all(&data_norm_bytes)?;
1238
1239 let mut row_indices_bytes = Vec::with_capacity(self.indices.len() * 4);
1240 for i in &self.indices {
1241 row_indices_bytes.extend_from_slice(&i.to_le_bytes());
1242 }
1243 writer.write_all(&row_indices_bytes)?;
1244
1245 Ok(())
1246 }
1247
1248 pub fn to_compressed_bytes(&self) -> std::io::Result<Vec<u8>> {
1258 let mut buffer = Vec::new();
1259 self.write_to_bytes(&mut buffer)?;
1260 Ok(compress_prepend_size(&buffer))
1261 }
1262
1263 pub fn read_from_buffer(buffer: &[u8]) -> Result<Self, ScDataErrors> {
1273 let payload = csc_payload(buffer)?;
1274
1275 Ok(Self {
1276 data_raw: payload.raw_counts()?,
1277 data_norm: payload.norm_iter().collect(),
1278 avg_exp: F16::from_le_bytes([buffer[12], buffer[13]]),
1279 nnz: le_u64_at(buffer, 16) as usize,
1280 indices: payload.index_iter().collect(),
1281 original_index: le_u64_at(buffer, 24) as usize,
1282 to_keep: buffer[32] != 0,
1283 })
1284 }
1285
1286 pub fn filter_selected_cells(&mut self, cells_to_keep: &IndexSet<u32>) {
1295 self.filter_cells_by(|cell| cells_to_keep.get_index_of(&cell).map(|i| i as u32));
1296 }
1297
1298 pub fn filter_selected_cells_dense(&mut self, cell_map: &[u32]) {
1309 self.filter_cells_by(|cell| match cell_map.get(cell as usize) {
1310 Some(&row) if row != INDEX_DROPPED => Some(row),
1311 _ => None,
1312 });
1313 }
1314
1315 #[inline]
1324 fn filter_cells_by(&mut self, row_of: impl Fn(u32) -> Option<u32>) {
1325 let mut kept: Vec<(u32, u32)> = self
1326 .indices
1327 .iter()
1328 .enumerate()
1329 .filter_map(|(pos, &cell)| row_of(cell).map(|row| (row, pos as u32)))
1330 .collect();
1331
1332 if !kept.is_sorted_by_key(|&(row, _)| row) {
1333 kept.sort_unstable_by_key(|&(row, _)| row);
1334 }
1335
1336 self.data_norm = kept
1337 .iter()
1338 .map(|&(_, p)| self.data_norm[p as usize])
1339 .collect();
1340 self.data_raw = match &self.data_raw {
1341 RawCounts::U16(v) => RawCounts::U16(kept.iter().map(|&(_, p)| v[p as usize]).collect()),
1342 RawCounts::U32(v) => RawCounts::U32(kept.iter().map(|&(_, p)| v[p as usize]).collect()),
1343 };
1344 self.indices = kept.iter().map(|&(row, _)| row).collect();
1345 self.nnz = kept.len();
1346 }
1347
1348 pub fn calculate_avg_exp(&self, cells_to_keep: &IndexSet<u32>) -> (usize, f32) {
1358 let sum: f32 = self
1359 .indices
1360 .iter()
1361 .zip(self.data_norm.iter())
1362 .filter(|(cell_idx, _)| cells_to_keep.contains(*cell_idx))
1363 .map(|(_, val)| val.to_f32())
1364 .sum();
1365 let avg = sum / cells_to_keep.len() as f32;
1366
1367 (self.original_index, avg)
1368 }
1369
1370 pub fn to_sparse_axis(&self, n_cells: usize) -> SparseAxis<u32, f32> {
1380 let raw_u32: Vec<u32> = self.data_raw.iter().collect();
1381 SparseAxis::new_csc(
1382 self.indices.iter().map(|x| *x as usize).collect(),
1383 raw_u32,
1384 Some(self.data_norm.iter().map(|x| x.to_f32()).collect()),
1385 n_cells,
1386 )
1387 }
1388}
1389
1390#[derive(Encode, Decode, Serialize, Deserialize, Clone)]
1395pub struct SparseDataHeader {
1396 pub total_cells: usize,
1398 pub total_genes: usize,
1400 pub cell_based: bool,
1403 pub no_chunks: usize,
1405 pub chunk_offsets: Vec<u64>,
1407 pub index_map: FxHashMap<usize, usize>,
1409}
1410
1411const FILE_MAGIC: &[u8; 8] = b"SCRNASEQ";
1414
1415fn le_u64_at(bytes: &[u8], at: usize) -> u64 {
1417 u64::from_le_bytes(
1418 bytes[at..at + 8]
1419 .try_into()
1420 .expect("an 8-byte slice converts to [u8; 8]"),
1421 )
1422}
1423
1424#[repr(C)]
1426#[derive(Encode, Decode, Serialize, Deserialize)]
1427struct FileHeader {
1428 magic: [u8; 8],
1430 version: u32,
1432 main_header_offset: u64,
1434 cell_based: bool,
1436 target_size: f32,
1440 library_index_offset: u64,
1444 _reserved_1: [u8; 20],
1446 _reserved_2: [u8; 3],
1448}
1449
1450impl FileHeader {
1451 fn new(cell_based: bool, target_size: f32) -> Self {
1463 Self {
1464 magic: *FILE_MAGIC,
1465 version: SC_FILE_VERSION,
1466 main_header_offset: 0,
1467 cell_based,
1468 target_size,
1469 library_index_offset: 0,
1470 _reserved_1: [0; 20],
1471 _reserved_2: [0; 3],
1472 }
1473 }
1474}
1475
1476pub struct CellPayload {
1485 pub original_index: usize,
1487 pub library_size: usize,
1489 pub bytes: Vec<u8>,
1491}
1492
1493pub struct CellGeneSparseWriter {
1498 header: SparseDataHeader,
1500 writer: BufWriter<File>,
1502 chunks_start_pos: u64,
1504 cell_based: bool,
1507 chunks_since_flush: usize,
1509 flush_frequency: usize,
1512 target_size: f32,
1516 library_sizes: Vec<u64>,
1519 version: u32,
1521 codec: ChunkCodec,
1523 cell_library_table: Option<Vec<u64>>,
1526}
1527
1528impl CellGeneSparseWriter {
1529 pub fn new<P: AsRef<Path>>(
1553 path_f: P,
1554 cell_based: bool,
1555 total_cells: usize,
1556 total_genes: usize,
1557 target_size: f32,
1558 ) -> Result<Self, ScDataErrors> {
1559 let file = File::create(path_f)?;
1560 let mut writer = BufWriter::with_capacity(128 * 1024 * 1024, file);
1561
1562 let file_header = FileHeader::new(cell_based, target_size);
1563 let file_header_enc = encode_to_vec(&file_header, config::standard())
1564 .map_err(|_| ScDataErrors::HeaderEncodeFailed)?;
1565 if file_header_enc.len() < 64 {
1566 writer.write_all(&file_header_enc)?;
1567 writer.write_all(&vec![0u8; 64 - file_header_enc.len()])?;
1568 } else {
1569 writer.write_all(&file_header_enc[..64])?;
1570 }
1571 writer.flush()?;
1572
1573 let chunks_start_pos = 64;
1574
1575 let flush_frequency = if cell_based {
1576 CELL_FLUSH_FREQUENCY
1577 } else {
1578 GENE_FLUSH_FREQUENCY
1579 };
1580
1581 let header = SparseDataHeader {
1582 total_cells,
1583 total_genes,
1584 cell_based,
1585 no_chunks: 0,
1586 chunk_offsets: Vec::new(),
1587 index_map: FxHashMap::default(),
1588 };
1589
1590 Ok(Self {
1591 header,
1592 writer,
1593 chunks_start_pos,
1594 cell_based,
1595 chunks_since_flush: 0_usize,
1596 flush_frequency,
1597 target_size,
1598 library_sizes: Vec::new(),
1599 version: SC_FILE_VERSION,
1600 codec: ChunkCodec::Lz4,
1601 cell_library_table: None,
1602 })
1603 }
1604
1605 pub fn new_v3<P: AsRef<Path>>(
1622 path_f: P,
1623 cell_based: bool,
1624 total_cells: usize,
1625 total_genes: usize,
1626 target_size: f32,
1627 ) -> Result<Self, ScDataErrors> {
1628 let mut writer = Self::new(path_f, cell_based, total_cells, total_genes, target_size)?;
1629 writer.version = SC_FILE_VERSION_V3;
1630 Ok(writer)
1631 }
1632
1633 pub fn with_codec(mut self, codec: ChunkCodec) -> Self {
1645 self.codec = codec;
1646 self
1647 }
1648
1649 pub fn format(&self) -> ChunkFormat {
1658 ChunkFormat {
1659 version: self.version,
1660 codec: self.codec,
1661 target_size: self.target_size,
1662 }
1663 }
1664
1665 pub fn set_cell_library_table(&mut self, table: Vec<u64>) -> Result<(), ScDataErrors> {
1680 if self.version != SC_FILE_VERSION {
1681 return Err(ScDataErrors::FileVersionMismatch {
1682 expected: SC_FILE_VERSION,
1683 found: self.version,
1684 });
1685 }
1686 self.check_mode(false)?;
1687 self.cell_library_table = Some(table);
1688 Ok(())
1689 }
1690
1691 fn check_mode(&self, want_cell_based: bool) -> Result<(), ScDataErrors> {
1701 if self.cell_based == want_cell_based {
1702 return Ok(());
1703 }
1704 let label = |b: bool| if b { "cell-based" } else { "gene-based" };
1705 Err(ScDataErrors::ReaderModeMismatch {
1706 actual: label(self.cell_based),
1707 requested: label(want_cell_based),
1708 })
1709 }
1710
1711 fn append_compressed(
1724 &mut self,
1725 compressed: &[u8],
1726 original_index: usize,
1727 ) -> Result<(), ScDataErrors> {
1728 let current_pos = self.writer.stream_position()?;
1729 let chunk_offset = current_pos - self.chunks_start_pos;
1730 self.header.chunk_offsets.push(chunk_offset);
1731 self.header
1732 .index_map
1733 .insert(original_index, self.header.no_chunks);
1734
1735 self.writer
1736 .write_all(&(compressed.len() as u64).to_le_bytes())?;
1737 self.writer.write_all(compressed)?;
1738
1739 self.header.no_chunks += 1;
1740 self.chunks_since_flush += 1;
1741
1742 if self.chunks_since_flush >= self.flush_frequency {
1743 self.writer.flush()?;
1744 self.chunks_since_flush = 0;
1745 }
1746
1747 Ok(())
1748 }
1749
1750 fn append_cell(
1762 &mut self,
1763 compressed: &[u8],
1764 original_index: usize,
1765 library_size: usize,
1766 ) -> Result<(), ScDataErrors> {
1767 self.append_compressed(compressed, original_index)?;
1768 self.library_sizes.push(library_size as u64);
1769 Ok(())
1770 }
1771
1772 pub fn write_cell_chunk(&mut self, cell_chunk: CsrCellChunk) -> Result<(), ScDataErrors> {
1785 self.check_mode(true)?;
1786
1787 let bytes = self.format().encode_cell(&cell_chunk)?;
1788 self.append_cell(&bytes, cell_chunk.original_index, cell_chunk.library_size)
1789 }
1790
1791 pub fn write_gene_chunk(&mut self, gene_chunk: CscGeneChunk) -> Result<(), ScDataErrors> {
1806 self.check_mode(false)?;
1807
1808 let bytes = self.format().encode_gene(&gene_chunk, &[])?;
1809 self.append_compressed(&bytes, gene_chunk.original_index)
1810 }
1811
1812 pub fn write_compressed_gene_chunks(
1825 &mut self,
1826 chunks: &[(usize, Vec<u8>)],
1827 ) -> Result<(), ScDataErrors> {
1828 self.check_mode(false)?;
1829 self.append_compressed_batch(chunks)
1830 }
1831
1832 pub fn write_compressed_cell_chunks(
1844 &mut self,
1845 chunks: &[CellPayload],
1846 ) -> Result<(), ScDataErrors> {
1847 self.check_mode(true)?;
1848 for chunk in chunks {
1849 self.append_cell(&chunk.bytes, chunk.original_index, chunk.library_size)?;
1850 }
1851 Ok(())
1852 }
1853
1854 fn append_compressed_batch(&mut self, chunks: &[(usize, Vec<u8>)]) -> Result<(), ScDataErrors> {
1864 for (original_index, payload) in chunks {
1865 self.append_compressed(payload, *original_index)?;
1866 }
1867 Ok(())
1868 }
1869
1870 pub fn finalise(mut self) -> Result<(), ScDataErrors> {
1881 let main_header_offset = self.writer.stream_position()?;
1883
1884 let header_data = encode_to_vec(&self.header, config::standard())
1885 .map_err(|_| ScDataErrors::HeaderEncodeFailed)?;
1886 let header_size = header_data.len() as u64;
1887
1888 self.writer.write_all(&header_size.to_le_bytes())?;
1889 self.writer.write_all(&header_data)?;
1890
1891 let mut file_header = FileHeader::new(self.cell_based, self.target_size);
1892 file_header.version = self.version;
1893 file_header.main_header_offset = main_header_offset;
1894 if self.cell_based {
1895 file_header.library_index_offset = self.write_library_index()?;
1896 } else if let Some(table) = self.cell_library_table.take() {
1897 file_header.library_index_offset = self.write_library_table(&table)?;
1898 }
1899
1900 self.writer.seek(SeekFrom::Start(0))?;
1901 let file_header_enc = encode_to_vec(&file_header, config::standard())
1902 .map_err(|_| ScDataErrors::HeaderEncodeFailed)?;
1903
1904 if file_header_enc.len() < 64 {
1906 self.writer.write_all(&file_header_enc)?;
1907 self.writer
1908 .write_all(&vec![0u8; 64 - file_header_enc.len()])?;
1909 } else {
1910 self.writer.write_all(&file_header_enc[..64])?;
1911 }
1912
1913 self.writer.flush()?;
1914
1915 Ok(())
1916 }
1917
1918 fn write_library_index(&mut self) -> Result<u64, ScDataErrors> {
1925 assert!(self.cell_based, "library-size index on a gene-based file");
1926 assert_eq!(
1927 self.library_sizes.len(),
1928 self.header.no_chunks,
1929 "library-size index has {} entries for {} cell chunks",
1930 self.library_sizes.len(),
1931 self.header.no_chunks
1932 );
1933 let offset = self.writer.stream_position()?;
1934 self.writer
1935 .write_all(&(self.library_sizes.len() as u64).to_le_bytes())?;
1936 for library_size in &self.library_sizes {
1937 self.writer.write_all(&library_size.to_le_bytes())?;
1938 }
1939 Ok(offset)
1940 }
1941
1942 fn write_library_table(&mut self, table: &[u64]) -> Result<u64, ScDataErrors> {
1953 let offset = self.writer.stream_position()?;
1954 self.writer.write_all(&(table.len() as u64).to_le_bytes())?;
1955 for library_size in table {
1956 self.writer.write_all(&library_size.to_le_bytes())?;
1957 }
1958 Ok(offset)
1959 }
1960
1961 pub fn update_header_no_cells(&mut self, no_cells: usize) {
1970 self.header.total_cells = no_cells;
1971 }
1972
1973 pub fn update_header_no_genes(&mut self, no_genes: usize) {
1982 self.header.total_genes = no_genes;
1983 }
1984}
1985
1986pub const INDEX_DROPPED: u32 = u32::MAX;
1992
1993const CELL_WRITE_BATCH: usize = 4_096;
1998
1999pub fn dense_index_map(kept: &[usize]) -> Vec<u32> {
2012 let mut map = vec![INDEX_DROPPED; kept.iter().max().map_or(0, |&g| g + 1)];
2013 for (new, &old) in kept.iter().enumerate() {
2014 map[old] = new as u32;
2015 }
2016 map
2017}
2018
2019pub fn keep_set_index_map(cells_to_keep: &IndexSet<u32>) -> Vec<u32> {
2029 let kept: Vec<usize> = cells_to_keep.iter().map(|&c| c as usize).collect();
2030 dense_index_map(&kept)
2031}
2032
2033pub fn dense_gene_map_from_options(mapping: &[Option<usize>]) -> Vec<u32> {
2043 mapping
2044 .iter()
2045 .map(|g| g.map_or(INDEX_DROPPED, |g| g as u32))
2046 .collect()
2047}
2048
2049pub fn compress_cell_row(
2062 row: &mut [(u32, u32)],
2063 original_index: usize,
2064 target_size: f32,
2065 format: ChunkFormat,
2066) -> Result<(usize, CellPayload), ScDataErrors> {
2067 if row.windows(2).any(|w| w[0].0 > w[1].0) {
2068 row.sort_by_key(|&(g, _)| g);
2069 }
2070 let genes: Vec<u32> = row.iter().map(|&(g, _)| g).collect();
2071 let counts: Vec<u32> = row.iter().map(|&(_, c)| c).collect();
2072 let chunk = CsrCellChunk::from_data(&counts, &genes, original_index, target_size, true);
2073 let (nnz, library_size) = chunk.get_qc_info();
2074 let payload = CellPayload {
2075 original_index,
2076 library_size,
2077 bytes: format.encode_cell(&chunk)?,
2078 };
2079 Ok((nnz, payload))
2080}
2081
2082pub fn write_cell_rows(
2100 rows: &mut [Vec<(u32, u32)>],
2101 cell_offset: usize,
2102 target_size: f32,
2103 writer: &mut CellGeneSparseWriter,
2104) -> Result<(Vec<usize>, Vec<usize>), ScDataErrors> {
2105 let mut nnz = Vec::with_capacity(rows.len());
2106 let mut lib_size = Vec::with_capacity(rows.len());
2107 let format = writer.format();
2108
2109 for (batch_idx, batch) in rows.chunks_mut(CELL_WRITE_BATCH).enumerate() {
2110 let first = cell_offset + batch_idx * CELL_WRITE_BATCH;
2111 let built = batch
2112 .par_iter_mut()
2113 .enumerate()
2114 .map(|(k, row)| {
2115 let mut row = std::mem::take(row);
2116 compress_cell_row(&mut row, first + k, target_size, format)
2117 })
2118 .collect::<Result<Vec<_>, ScDataErrors>>()?;
2119
2120 let mut payloads = Vec::with_capacity(built.len());
2121 for (nnz_i, payload) in built {
2122 nnz.push(nnz_i);
2123 lib_size.push(payload.library_size);
2124 payloads.push(payload);
2125 }
2126 writer.write_compressed_cell_chunks(&payloads)?;
2127 }
2128
2129 Ok((nnz, lib_size))
2130}
2131
2132pub fn write_cell_chunks_built<F>(
2148 n_chunks: usize,
2149 build: F,
2150 writer: &mut CellGeneSparseWriter,
2151) -> Result<(Vec<usize>, Vec<usize>), ScDataErrors>
2152where
2153 F: Fn(usize) -> CsrCellChunk + Sync,
2154{
2155 let mut nnz = Vec::with_capacity(n_chunks);
2156 let mut lib_size = Vec::with_capacity(n_chunks);
2157 let format = writer.format();
2158
2159 for start in (0..n_chunks).step_by(CELL_WRITE_BATCH) {
2160 let end = (start + CELL_WRITE_BATCH).min(n_chunks);
2161 let built = (start..end)
2162 .into_par_iter()
2163 .map(|k| {
2164 let chunk = build(k);
2165 let (nnz_i, lib_i) = chunk.get_qc_info();
2166 Ok::<_, ScDataErrors>((
2167 nnz_i,
2168 CellPayload {
2169 original_index: chunk.original_index,
2170 library_size: lib_i,
2171 bytes: format.encode_cell(&chunk)?,
2172 },
2173 ))
2174 })
2175 .collect::<Result<Vec<_>, _>>()?;
2176
2177 let mut payloads = Vec::with_capacity(built.len());
2178 for (nnz_i, payload) in built {
2179 nnz.push(nnz_i);
2180 lib_size.push(payload.library_size);
2181 payloads.push(payload);
2182 }
2183 writer.write_compressed_cell_chunks(&payloads)?;
2184 }
2185
2186 Ok((nnz, lib_size))
2187}
2188
2189pub fn write_cell_chunks_parallel(
2200 chunks: &[CsrCellChunk],
2201 writer: &mut CellGeneSparseWriter,
2202) -> Result<(), ScDataErrors> {
2203 let format = writer.format();
2204 for batch in chunks.chunks(CELL_WRITE_BATCH) {
2205 let payloads = batch
2206 .par_iter()
2207 .map(|chunk| {
2208 Ok::<_, ScDataErrors>(CellPayload {
2209 original_index: chunk.original_index,
2210 library_size: chunk.library_size,
2211 bytes: format.encode_cell(chunk)?,
2212 })
2213 })
2214 .collect::<Result<Vec<_>, _>>()?;
2215 writer.write_compressed_cell_chunks(&payloads)?;
2216 }
2217 Ok(())
2218}
2219
2220pub trait SingleCellReading: Send + Sync {
2244 fn read_cells_parallel(&self, indices: &[usize]) -> Result<Vec<CsrCellChunk>, ScDataErrors>;
2254
2255 fn read_gene_parallel(&self, indices: &[usize]) -> Result<Vec<CscGeneChunk>, ScDataErrors>;
2265
2266 fn read_gene_parallel_raw(&self, indices: &[usize]) -> Result<Vec<CscGeneChunk>, ScDataErrors> {
2279 self.read_gene_parallel(indices)
2280 }
2281
2282 fn read_cells_sparse<T>(
2300 &self,
2301 indices: &[usize],
2302 data_layer: &DataLayerReturn,
2303 ) -> Result<CompressedSparseData2<T, f32>, ScDataErrors>
2304 where
2305 T: BixverseNumeric + FromPrimitive;
2306
2307 fn read_genes_sparse<T>(
2329 &self,
2330 indices: &[usize],
2331 cells_to_keep: Option<&IndexSet<u32>>,
2332 data_layer: &DataLayerReturn,
2333 ) -> Result<CompressedSparseData2<T, f32>, ScDataErrors>
2334 where
2335 T: BixverseNumeric + FromPrimitive;
2336
2337 fn get_header(&self) -> SparseDataHeader;
2343
2344 fn is_cell_based(&self) -> bool;
2350
2351 fn is_gene_based(&self) -> bool {
2357 !self.is_cell_based()
2358 }
2359
2360 fn target_size(&self) -> Option<f32> {
2370 None
2371 }
2372
2373 fn read_cell(&self, index: usize) -> Result<CsrCellChunk, ScDataErrors> {
2383 self.read_cells_parallel(&[index])?
2384 .into_iter()
2385 .next()
2386 .ok_or(ScDataErrors::ChunkIndexNotFound(index))
2387 }
2388
2389 fn read_gene(&self, index: usize) -> Result<CscGeneChunk, ScDataErrors> {
2399 self.read_gene_parallel(&[index])?
2400 .into_iter()
2401 .next()
2402 .ok_or(ScDataErrors::ChunkIndexNotFound(index))
2403 }
2404
2405 fn read_cells_range(
2418 &self,
2419 start: usize,
2420 end: usize,
2421 ) -> Result<Vec<CsrCellChunk>, ScDataErrors> {
2422 let indices: Vec<usize> = (start..end).collect();
2423 self.read_cells_parallel(&indices)
2424 }
2425
2426 fn get_all_cells(&self) -> Result<Vec<CsrCellChunk>, ScDataErrors> {
2432 let indices: Vec<usize> = (0..self.get_header().total_cells).collect();
2433 self.read_cells_parallel(&indices)
2434 }
2435
2436 fn get_all_genes(&self) -> Result<Vec<CscGeneChunk>, ScDataErrors> {
2442 let indices: Vec<usize> = (0..self.get_header().total_genes).collect();
2443 self.read_gene_parallel(&indices)
2444 }
2445
2446 fn read_gene_parallel_filtered(
2457 &self,
2458 indices: &[usize],
2459 cells_to_keep: &IndexSet<u32>,
2460 ) -> Result<Vec<CscGeneChunk>, ScDataErrors> {
2461 let mut chunks = self.read_gene_parallel(indices)?;
2462 let cell_map = keep_set_index_map(cells_to_keep);
2463 chunks
2464 .par_iter_mut()
2465 .for_each(|chunk| chunk.filter_selected_cells_dense(&cell_map));
2466 Ok(chunks)
2467 }
2468
2469 fn read_cell_library_sizes(&self, indices: &[usize]) -> Result<Vec<usize>, ScDataErrors> {
2479 Ok(self
2480 .read_cells_parallel(indices)?
2481 .iter()
2482 .map(|chunk| chunk.library_size)
2483 .collect())
2484 }
2485
2486 fn read_gene_nnz(&self, indices: &[usize]) -> Result<Vec<usize>, ScDataErrors> {
2496 Ok(self
2497 .read_gene_parallel(indices)?
2498 .iter()
2499 .map(|chunk| chunk.nnz)
2500 .collect())
2501 }
2502
2503 fn get_all_gene_nnz(&self) -> Result<Vec<usize>, ScDataErrors> {
2509 let indices: Vec<usize> = (0..self.get_header().total_genes).collect();
2510 self.read_gene_nnz(&indices)
2511 }
2512
2513 fn get_clr_offsets(
2530 &self,
2531 indices: &[usize],
2532 batch_size: Option<usize>,
2533 ) -> Result<Vec<f64>, ScDataErrors> {
2534 if !self.is_cell_based() {
2535 return Err(ScDataErrors::ReaderModeMismatch {
2536 actual: "gene-based",
2537 requested: "cell-based",
2538 });
2539 }
2540 let batch_size = batch_size.unwrap_or(CELL_BATCH_SIZE);
2541
2542 let n_genes = self.get_header().total_genes as f64;
2543 let mut offsets = Vec::with_capacity(indices.len());
2544
2545 for batch in indices.chunks(batch_size) {
2546 let cells = self.read_cells_parallel(batch)?;
2547 let batch_offsets: Vec<f64> = cells
2548 .par_iter()
2549 .map(|chunk| {
2550 let inv_s = 1.0_f64 / chunk.library_size as f64;
2551 let sum: f64 = chunk
2552 .data_raw
2553 .iter()
2554 .map(|c| (c as f64 * inv_s).ln_1p())
2555 .sum();
2556 sum / n_genes
2557 })
2558 .collect();
2559 offsets.extend_from_slice(&batch_offsets);
2560 }
2561
2562 Ok(offsets)
2563 }
2564}
2565
2566pub fn peek_target_size<P: AsRef<Path>>(f_path: P) -> Result<Option<f32>, ScDataErrors> {
2584 use std::io::Read;
2585
2586 let mut buffer = [0u8; 64];
2587 File::open(f_path)?.read_exact(&mut buffer)?;
2588
2589 let (file_header, _) = decode_from_slice::<FileHeader, _>(&buffer, config::standard())
2590 .map_err(|_| ScDataErrors::HeaderDecodeFailed)?;
2591
2592 Ok((file_header.target_size != 0.0).then_some(file_header.target_size))
2593}
2594
2595pub fn peek_file_version<P: AsRef<Path>>(f_path: P) -> Result<u32, ScDataErrors> {
2611 use std::io::Read;
2612
2613 let mut buffer = [0u8; 64];
2614 File::open(f_path)?.read_exact(&mut buffer)?;
2615
2616 let (file_header, _) = decode_from_slice::<FileHeader, _>(&buffer, config::standard())
2617 .map_err(|_| ScDataErrors::HeaderDecodeFailed)?;
2618 if &file_header.magic != FILE_MAGIC {
2619 return Err(ScDataErrors::HeaderDecodeFailed);
2620 }
2621
2622 Ok(file_header.version)
2623}
2624
2625pub struct ParallelSparseReader {
2627 header: SparseDataHeader,
2629 mmap: Arc<memmap2::Mmap>,
2631 file: File,
2633 chunks_start: u64,
2635 target_size: f32,
2638 library_index: Option<usize>,
2641 identity_index_map: bool,
2643 version: u32,
2645 cell_lib: Option<Arc<Vec<f32>>>,
2648 cell_norm: Option<Arc<Vec<u16>>>,
2651}
2652
2653impl ParallelSparseReader {
2654 pub fn new(f_path: &str) -> Result<Self, ScDataErrors> {
2664 let file = File::open(f_path)?;
2665 let file_size = file.metadata()?.len();
2666
2667 let mmap = unsafe {
2668 let mut opts = MmapOptions::new();
2669 if file_size <= 8 * 1024 * 1024 * 1024 {
2670 opts.populate();
2671 }
2672 opts.map(&file)?
2673 };
2674
2675 #[cfg(unix)]
2676 mmap.advise(memmap2::Advice::Random)?;
2677
2678 if mmap.len() < 64 {
2682 return Err(ScDataErrors::HeaderDecodeFailed);
2683 }
2684
2685 let file_header_bytes = &mmap[0..64];
2686 let (file_header, _) =
2687 decode_from_slice::<FileHeader, _>(file_header_bytes, config::standard())
2688 .map_err(|_| ScDataErrors::HeaderDecodeFailed)?;
2689
2690 if &file_header.magic != FILE_MAGIC {
2691 return Err(ScDataErrors::HeaderDecodeFailed);
2692 }
2693
2694 if file_header.version != SC_FILE_VERSION_V3 && file_header.version != SC_FILE_VERSION {
2695 return Err(ScDataErrors::FileVersionMismatch {
2696 expected: SC_FILE_VERSION,
2697 found: file_header.version,
2698 });
2699 }
2700
2701 let main_header_offset = file_header.main_header_offset as usize;
2702 if main_header_offset.saturating_add(8) > mmap.len() {
2703 return Err(ScDataErrors::HeaderDecodeFailed);
2704 }
2705
2706 let header_size = u64::from_le_bytes(
2707 mmap[main_header_offset..main_header_offset + 8]
2708 .try_into()
2709 .expect("8-byte slice, bounds checked above"),
2710 ) as usize;
2711
2712 let header_end = main_header_offset
2713 .saturating_add(8)
2714 .saturating_add(header_size);
2715 if header_end > mmap.len() {
2716 return Err(ScDataErrors::HeaderDecodeFailed);
2717 }
2718
2719 let header_bytes = &mmap[main_header_offset + 8..header_end];
2720 let (header, _) =
2721 decode_from_slice::<SparseDataHeader, _>(header_bytes, config::standard())
2722 .map_err(|_| ScDataErrors::HeaderDecodeFailed)?;
2723
2724 let is_v4_gene = file_header.version == SC_FILE_VERSION && !header.cell_based;
2725 let library_index = if is_v4_gene {
2726 None
2727 } else {
2728 Self::check_library_index(&mmap, file_header.library_index_offset, &header)?
2729 };
2730 let cell_lib = if is_v4_gene {
2731 Self::load_cell_library_table(&mmap, file_header.library_index_offset)?
2732 } else {
2733 None
2734 };
2735 let identity_index_map = header.index_map.len() == header.chunk_offsets.len()
2736 && header
2737 .index_map
2738 .iter()
2739 .all(|(&original, &chunk)| original == chunk);
2740
2741 Ok(Self {
2742 header,
2743 mmap: Arc::new(mmap),
2744 file,
2745 chunks_start: 64,
2746 target_size: file_header.target_size,
2747 library_index,
2748 identity_index_map,
2749 version: file_header.version,
2750 cell_norm: cell_lib
2751 .as_ref()
2752 .filter(|_| file_header.target_size != 0.0)
2753 .map(|lib| Arc::new(gene_norm_table(lib, file_header.target_size))),
2754 cell_lib,
2755 })
2756 }
2757
2758 fn load_cell_library_table(
2771 mmap: &memmap2::Mmap,
2772 offset: u64,
2773 ) -> Result<Option<Arc<Vec<f32>>>, ScDataErrors> {
2774 if offset == 0 {
2775 return Ok(None);
2776 }
2777 let offset = offset as usize;
2778 if offset.saturating_add(8) > mmap.len() {
2779 return Err(ScDataErrors::HeaderDecodeFailed);
2780 }
2781 let count = le_u64_at(mmap, offset) as usize;
2782 let end = count
2783 .checked_mul(8)
2784 .and_then(|bytes| bytes.checked_add(offset + 8));
2785 if end.is_none_or(|end| end > mmap.len()) {
2786 return Err(ScDataErrors::HeaderDecodeFailed);
2787 }
2788 Ok(Some(Arc::new(
2789 (0..count)
2790 .map(|i| le_u64_at(mmap, offset + 8 + 8 * i) as f32)
2791 .collect(),
2792 )))
2793 }
2794
2795 pub fn file_version(&self) -> u32 {
2801 self.version
2802 }
2803
2804 fn is_v4(&self) -> bool {
2810 self.version == SC_FILE_VERSION
2811 }
2812
2813 fn v4_ctx(&self) -> V4Ctx<'_> {
2819 V4Ctx {
2820 target_size: self.target_size,
2821 cell_lib: self.cell_lib.as_deref().map(Vec::as_slice),
2822 cell_norm: self.cell_norm.as_deref().map(Vec::as_slice),
2823 is_cell: self.header.cell_based,
2824 }
2825 }
2826
2827 fn check_library_index(
2845 mmap: &memmap2::Mmap,
2846 offset: u64,
2847 header: &SparseDataHeader,
2848 ) -> Result<Option<usize>, ScDataErrors> {
2849 if offset == 0 {
2850 return Ok(None);
2851 }
2852 let offset = offset as usize;
2853 if !header.cell_based || offset.saturating_add(8) > mmap.len() {
2854 return Err(ScDataErrors::HeaderDecodeFailed);
2855 }
2856 let count = le_u64_at(mmap, offset) as usize;
2857 let end = count
2858 .checked_mul(8)
2859 .and_then(|bytes| bytes.checked_add(offset + 8));
2860 if count != header.no_chunks || end.is_none_or(|end| end > mmap.len()) {
2861 return Err(ScDataErrors::HeaderDecodeFailed);
2862 }
2863 Ok(Some(offset + 8))
2864 }
2865
2866 fn library_sizes_from_index(
2877 &self,
2878 start: usize,
2879 indices: &[usize],
2880 ) -> Result<Vec<usize>, ScDataErrors> {
2881 indices
2882 .iter()
2883 .map(|&original_index| {
2884 let chunk = self
2885 .header
2886 .index_map
2887 .get(&original_index)
2888 .copied()
2889 .filter(|&chunk| chunk < self.header.no_chunks)
2890 .ok_or(ScDataErrors::ChunkIndexNotFound(original_index))?;
2891 Ok(le_u64_at(&self.mmap, start + 8 * chunk) as usize)
2892 })
2893 .collect()
2894 }
2895
2896 #[cfg(test)]
2899 pub(crate) fn has_library_index(&self) -> bool {
2900 self.library_index.is_some()
2901 }
2902
2903 fn chunk_index_of(&self, original_index: usize) -> Result<usize, ScDataErrors> {
2918 if self.identity_index_map {
2919 return if original_index < self.header.chunk_offsets.len() {
2920 Ok(original_index)
2921 } else {
2922 Err(ScDataErrors::ChunkIndexNotFound(original_index))
2923 };
2924 }
2925
2926 self.header
2927 .index_map
2928 .get(&original_index)
2929 .copied()
2930 .ok_or(ScDataErrors::ChunkIndexNotFound(original_index))
2931 }
2932
2933 pub fn count_norm_stored_chunks(&self) -> Result<usize, ScDataErrors> {
2944 if !self.is_v4() {
2945 return Err(ScDataErrors::FileVersionMismatch {
2946 expected: SC_FILE_VERSION,
2947 found: self.version,
2948 });
2949 }
2950 let offsets = &self.header.chunk_offsets;
2951 (0..offsets.len())
2952 .into_par_iter()
2953 .map(|c| {
2954 let at = (self.chunks_start + offsets[c]) as usize + 8;
2956 Ok(V4Header::parse(&self.mmap[at..])?.norm_stored() as usize)
2957 })
2958 .sum()
2959 }
2960
2961 pub(crate) fn visit_cells_v4(
2979 &self,
2980 start: usize,
2981 end: usize,
2982 want_raw: bool,
2983 mut f: impl FnMut(usize, &[u32], &[u32]),
2984 ) -> Result<(), ScDataErrors> {
2985 if !self.is_cell_based() {
2986 return Err(ScDataErrors::ReaderModeMismatch {
2987 actual: "gene-based",
2988 requested: "cell-based",
2989 });
2990 }
2991 if !self.is_v4() {
2992 return Err(ScDataErrors::FileVersionMismatch {
2993 expected: SC_FILE_VERSION,
2994 found: self.version,
2995 });
2996 }
2997
2998 self.prefetch(&(start..end).collect::<Vec<_>>());
2999 let ctx = self.v4_ctx();
3000 let mut s = V4Decoded::default();
3001 for cell in start..end {
3002 let (chunk, _) = self.locate_chunk(cell)?;
3003 decode_v4(chunk, &ctx, want_raw, false, &mut s)?;
3004 let raw = if want_raw { &s.raw[..s.idx.len()] } else { &[] };
3005 f(cell, &s.idx, raw);
3006 }
3007 Ok(())
3008 }
3009
3010 fn locate_chunk(&self, original_index: usize) -> Result<(&[u8], usize), ScDataErrors> {
3021 let chunk_index = self.chunk_index_of(original_index)?;
3022 let chunk_offset = (self.chunks_start + self.header.chunk_offsets[chunk_index]) as usize;
3023
3024 let compressed_size = u64::from_le_bytes(
3025 self.mmap[chunk_offset..chunk_offset + 8]
3026 .try_into()
3027 .expect("8-byte slice by construction"),
3028 ) as usize;
3029
3030 Ok((
3031 &self.mmap[chunk_offset + 8..chunk_offset + 8 + compressed_size],
3032 chunk_offset,
3033 ))
3034 }
3035
3036 fn decompress_chunk(&self, original_index: usize) -> Result<Vec<u8>, ScDataErrors> {
3049 let (compressed, chunk_offset) = self.locate_chunk(original_index)?;
3050
3051 decompress_size_prepended(compressed)
3052 .map_err(|_| ScDataErrors::ChunkDecompressionFailed(chunk_offset as u64))
3053 }
3054
3055 fn decompress_chunk_into(
3072 &self,
3073 original_index: usize,
3074 scratch: &mut Vec<u8>,
3075 ) -> Result<(), ScDataErrors> {
3076 let (compressed, chunk_offset) = self.locate_chunk(original_index)?;
3077
3078 let (size, payload) = uncompressed_size(compressed)
3079 .map_err(|_| ScDataErrors::ChunkDecompressionFailed(chunk_offset as u64))?;
3080
3081 scratch.resize(size, 0);
3082 let written = decompress_into(payload, scratch)
3083 .map_err(|_| ScDataErrors::ChunkDecompressionFailed(chunk_offset as u64))?;
3084 scratch.truncate(written);
3085
3086 Ok(())
3087 }
3088}
3089
3090#[cfg(unix)]
3100const PREFETCH_BLOCK: usize = 8 * 1024 * 1024;
3101
3102#[cfg(unix)]
3106const PREFETCH_MIN_BYTES: usize = if cfg!(test) { 1 } else { 16 * 1024 * 1024 };
3107
3108#[cfg(unix)]
3111const PREFETCH_MERGE_GAP: usize = 256 * 1024;
3112
3113#[cfg(unix)]
3115const PREFETCH_SAMPLES: usize = 32;
3116
3117#[cfg(unix)]
3120const PREFETCH_SKIP_RESIDENT: f64 = 0.9;
3121
3122impl ParallelSparseReader {
3123 #[cfg(unix)]
3135 fn prefetch(&self, indices: &[usize]) {
3136 use std::os::unix::fs::FileExt;
3137
3138 let offsets = &self.header.chunk_offsets;
3139 let mut spans: Vec<(usize, usize)> = indices
3140 .iter()
3141 .filter_map(|&i| {
3142 let chunk = self.chunk_index_of(i).ok()?;
3143 let start = (self.chunks_start + offsets[chunk]) as usize;
3144 let end = offsets
3145 .get(chunk + 1)
3146 .map_or(start + 8, |&next| (self.chunks_start + next) as usize);
3147 Some((start, end))
3148 })
3149 .collect();
3150 if spans.iter().map(|&(s, e)| e - s).sum::<usize>() < PREFETCH_MIN_BYTES {
3151 return;
3152 }
3153
3154 spans.sort_unstable();
3155 let mut ranges: Vec<(usize, usize)> = Vec::new();
3156 for (start, end) in spans {
3157 match ranges.last_mut() {
3158 Some(last) if start <= last.1 + PREFETCH_MERGE_GAP => last.1 = last.1.max(end),
3159 _ => ranges.push((start, end)),
3160 }
3161 }
3162
3163 let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(4096) as usize;
3165
3166 let step = (ranges.len() / PREFETCH_SAMPLES).max(1);
3169 let probed: Vec<f64> = ranges
3170 .iter()
3171 .step_by(step)
3172 .filter(|&&(s, e)| s / page * page < self.mmap.len() && e > s)
3173 .map(|&(s, e)| {
3174 let s = s / page * page;
3175 let e = e.min(self.mmap.len());
3176 self.resident_fraction(s, e - s, page)
3177 })
3178 .collect();
3179 if probed.is_empty()
3180 || probed.iter().sum::<f64>() / probed.len() as f64 >= PREFETCH_SKIP_RESIDENT
3181 {
3182 return;
3183 }
3184 let blocks: Vec<(usize, usize)> = ranges
3185 .iter()
3186 .flat_map(|&(start, end)| {
3187 let start = start / page * page;
3188 (start..end)
3189 .step_by(PREFETCH_BLOCK)
3190 .map(move |s| (s, (s + PREFETCH_BLOCK).min(end)))
3191 })
3192 .filter(|&(s, e)| s < self.mmap.len() && e > s)
3193 .collect();
3194
3195 blocks.par_iter().for_each_init(
3196 || vec![0u8; PREFETCH_BLOCK],
3197 |buf, &(start, end)| {
3198 let end = end.min(self.mmap.len());
3199 if self.resident_fraction(start, end - start, page) >= PREFETCH_SKIP_RESIDENT {
3200 return;
3201 }
3202 let mut done = 0;
3203 while done < end - start {
3204 match self
3205 .file
3206 .read_at(&mut buf[..end - start - done], (start + done) as u64)
3207 {
3208 Ok(0) | Err(_) => break,
3209 Ok(n) => done += n,
3210 }
3211 }
3212 },
3213 );
3214 }
3215
3216 #[cfg(unix)]
3228 fn resident_fraction(&self, start: usize, len: usize, page: usize) -> f64 {
3229 let mut vec = vec![0u8; len.div_ceil(page)];
3230 let rc = unsafe {
3233 libc::mincore(
3234 self.mmap.as_ptr().add(start) as *mut libc::c_void,
3235 len,
3236 vec.as_mut_ptr().cast(),
3237 )
3238 };
3239 if rc != 0 {
3240 return 0.0;
3241 }
3242 vec.iter().filter(|&&b| b & 1 != 0).count() as f64 / vec.len().max(1) as f64
3243 }
3244
3245 #[cfg(not(unix))]
3251 fn prefetch(&self, _indices: &[usize]) {}
3252}
3253
3254const FUSED_PIECES_PER_THREAD: usize = 8;
3265
3266#[inline(always)]
3277fn raw_to<T: FromPrimitive>(val: u32) -> Result<T, ScDataErrors> {
3278 T::from_u32(val).ok_or(ScDataErrors::RawCountOverflow {
3279 value: val,
3280 target_type: std::any::type_name::<T>(),
3281 })
3282}
3283
3284pub(crate) struct SparseFragment<T> {
3290 data: Option<Vec<T>>,
3292 data_2: Option<Vec<f32>>,
3294 indices: Vec<u32>,
3296 lengths: Vec<usize>,
3298 kept: Vec<(u32, u32)>,
3300}
3301
3302impl<T: BixverseNumeric + FromPrimitive> SparseFragment<T> {
3303 fn new(layers: (bool, bool), n_runs: usize) -> Self {
3314 Self {
3315 data: layers.0.then(Vec::new),
3316 data_2: layers.1.then(Vec::new),
3317 indices: Vec::new(),
3318 lengths: Vec::with_capacity(n_runs),
3319 kept: Vec::new(),
3320 }
3321 }
3322
3323 pub(crate) fn push_v4(
3341 &mut self,
3342 chunk: &[u8],
3343 ctx: &V4Ctx,
3344 scratch: &mut V4Decoded,
3345 row_map: Option<&[u32]>,
3346 ) -> Result<(), ScDataErrors> {
3347 let (want_raw, want_norm) = self.layers();
3348 if row_map.is_some() {
3349 decode_v4(chunk, ctx, want_raw, want_norm, scratch)?;
3350 return self.push_run(&V4Run::new(scratch), row_map);
3351 }
3352
3353 let h = decode_v4_into(
3354 chunk,
3355 ctx,
3356 want_raw,
3357 &mut scratch.bytes,
3358 &mut scratch.raw,
3359 &mut self.indices,
3360 self.data_2.as_mut(),
3361 )?;
3362 if let Some(data) = self.data.as_mut() {
3363 extend_raw_checked(data, scratch.raw.iter().copied())?;
3364 }
3365 self.lengths.push(h.nnz);
3366 Ok(())
3367 }
3368
3369 pub(crate) fn layers(&self) -> (bool, bool) {
3375 (self.data.is_some(), self.data_2.is_some())
3376 }
3377
3378 #[inline]
3397 pub(crate) fn push_run(
3398 &mut self,
3399 run: &impl RunSource,
3400 row_map: Option<&[u32]>,
3401 ) -> Result<(), ScDataErrors> {
3402 let Some(map) = row_map else {
3403 if let Some(data) = self.data.as_mut() {
3404 run.extend_raw(data)?;
3405 }
3406 if let Some(data_2) = self.data_2.as_mut() {
3407 run.extend_norm(data_2);
3408 }
3409 run.extend_indices(&mut self.indices);
3410 self.lengths.push(run.nnz());
3411 return Ok(());
3412 };
3413
3414 self.kept.clear();
3415 self.kept.extend(
3416 (0..run.nnz()).filter_map(|i| match map.get(run.index_at(i) as usize) {
3417 Some(&row) if row != INDEX_DROPPED => Some((row, i as u32)),
3418 _ => None,
3419 }),
3420 );
3421 if !self.kept.is_sorted_by_key(|&(row, _)| row) {
3422 self.kept.sort_unstable_by_key(|&(row, _)| row);
3423 }
3424
3425 if let Some(data) = self.data.as_mut() {
3426 for &(_, i) in &self.kept {
3427 data.push(raw_to(run.raw_at(i as usize))?);
3428 }
3429 }
3430 if let Some(data_2) = self.data_2.as_mut() {
3431 data_2.extend(self.kept.iter().map(|&(_, i)| run.norm_at(i as usize)));
3432 }
3433 self.indices.extend(self.kept.iter().map(|&(row, _)| row));
3434 self.lengths.push(self.kept.len());
3435
3436 Ok(())
3437 }
3438}
3439
3440fn wanted_layers(data_layer: &DataLayerReturn) -> (bool, bool) {
3450 (
3451 matches!(
3452 data_layer,
3453 DataLayerReturn::Raw | DataLayerReturn::BothLayers
3454 ),
3455 matches!(
3456 data_layer,
3457 DataLayerReturn::Norm | DataLayerReturn::BothLayers
3458 ),
3459 )
3460}
3461
3462pub(crate) fn read_fused<T, F>(
3486 indices: &[usize],
3487 data_layer: &DataLayerReturn,
3488 cs_type: CompressedSparseFormat,
3489 shape: (usize, usize),
3490 fill: F,
3491) -> Result<CompressedSparseData2<T, f32>, ScDataErrors>
3492where
3493 T: BixverseNumeric + FromPrimitive,
3494 F: Fn(usize, &mut V4Decoded, &mut SparseFragment<T>) -> Result<(), ScDataErrors> + Sync,
3495{
3496 let layers = wanted_layers(data_layer);
3497 let n_pieces = rayon::current_num_threads().max(1) * FUSED_PIECES_PER_THREAD;
3498 let piece = indices.len().div_ceil(n_pieces).max(1);
3499
3500 let fragments: Vec<SparseFragment<T>> = indices
3501 .par_chunks(piece)
3502 .map(|runs| {
3503 let mut scratch = V4Decoded::default();
3504 let mut fragment = SparseFragment::new(layers, runs.len());
3505 for &run in runs {
3506 fill(run, &mut scratch, &mut fragment)?;
3507 }
3508 Ok(fragment)
3509 })
3510 .collect::<Result<Vec<_>, ScDataErrors>>()?;
3511
3512 let nnz_offsets: Vec<usize> = std::iter::once(0)
3516 .chain(fragments.iter().scan(0, |acc, f| {
3517 *acc += f.indices.len();
3518 Some(*acc)
3519 }))
3520 .collect();
3521 let run_offsets: Vec<usize> = std::iter::once(0)
3522 .chain(fragments.iter().scan(0, |acc, f| {
3523 *acc += f.lengths.len();
3524 Some(*acc)
3525 }))
3526 .collect();
3527 let nnz = *nnz_offsets.last().expect("seeded with 0");
3528
3529 let mut data: Vec<T> = if layers.0 {
3530 vec![T::default(); nnz]
3531 } else {
3532 Vec::new()
3533 };
3534 let mut data_2: Option<Vec<f32>> = layers.1.then(|| vec![0.0; nnz]);
3535 let mut out_indices: Vec<u32> = vec![0; nnz];
3536 let mut indptr: Vec<u32> = vec![0; indices.len() + 1];
3537
3538 let data_parts = split_at_offsets(&mut data, &nnz_offsets, layers.0);
3539 let data_2_parts = split_at_offsets(
3540 data_2.as_deref_mut().unwrap_or_default(),
3541 &nnz_offsets,
3542 layers.1,
3543 );
3544 let index_parts = split_at_offsets(&mut out_indices, &nnz_offsets, true);
3545 let indptr_parts = split_at_offsets(&mut indptr[1..], &run_offsets, true);
3546
3547 fragments
3548 .par_iter()
3549 .zip(data_parts)
3550 .zip(data_2_parts)
3551 .zip(index_parts)
3552 .zip(indptr_parts)
3553 .zip(nnz_offsets.par_iter())
3554 .for_each(|(((((fragment, d), d2), idx), ptr), &base)| {
3555 if let Some(src) = fragment.data.as_ref() {
3556 d.copy_from_slice(src);
3557 }
3558 if let Some(src) = fragment.data_2.as_ref() {
3559 d2.copy_from_slice(src);
3560 }
3561 idx.copy_from_slice(&fragment.indices);
3562 let mut offset = base;
3563 for (out, &len) in ptr.iter_mut().zip(&fragment.lengths) {
3564 offset += len;
3565 *out = offset as u32;
3566 }
3567 });
3568
3569 Ok(CompressedSparseData2 {
3570 data,
3571 indices: out_indices,
3572 indptr,
3573 cs_type,
3574 data_2,
3575 shape,
3576 })
3577}
3578
3579fn split_at_offsets<'a, X>(buf: &'a mut [X], offsets: &[usize], active: bool) -> Vec<&'a mut [X]> {
3592 let n_parts = offsets.len().saturating_sub(1);
3593 if !active {
3594 return (0..n_parts).map(|_| &mut [][..]).collect();
3595 }
3596 let mut parts = Vec::with_capacity(n_parts);
3597 let mut rest = buf;
3598 for w in offsets.windows(2) {
3599 let (head, tail) = rest.split_at_mut(w[1] - w[0]);
3600 parts.push(head);
3601 rest = tail;
3602 }
3603 parts
3604}
3605
3606pub(crate) fn cell_row_map(
3621 cells_to_keep: Option<&IndexSet<u32>>,
3622 total_cells: usize,
3623) -> (Option<Vec<u32>>, usize) {
3624 match cells_to_keep {
3625 Some(set) => (Some(keep_set_index_map(set)), set.len()),
3626 None => (None, total_cells),
3627 }
3628}
3629
3630#[derive(Clone, Debug)]
3641pub struct CsrBatch {
3642 pub data_raw: Option<Vec<f32>>,
3644 pub data_norm: Option<Vec<f32>>,
3646 pub indices: Vec<u32>,
3648 pub indptr: Vec<u32>,
3650 pub shape: (usize, usize),
3652}
3653
3654impl ParallelSparseReader {
3655 pub fn read_cells_csr(
3670 &self,
3671 indices: &[usize],
3672 data_layer: &DataLayerReturn,
3673 ) -> Result<CsrBatch, ScDataErrors> {
3674 let csr = self.read_cells_sparse::<f32>(indices, data_layer)?;
3675
3676 Ok(CsrBatch {
3677 data_raw: wanted_layers(data_layer).0.then_some(csr.data),
3678 data_norm: csr.data_2,
3679 indices: csr.indices,
3680 indptr: csr.indptr,
3681 shape: csr.shape,
3682 })
3683 }
3684}
3685
3686impl SingleCellReading for ParallelSparseReader {
3691 fn read_cells_parallel(&self, indices: &[usize]) -> Result<Vec<CsrCellChunk>, ScDataErrors> {
3701 if !self.is_cell_based() {
3702 return Err(ScDataErrors::ReaderModeMismatch {
3703 actual: "gene-based",
3704 requested: "cell-based",
3705 });
3706 }
3707
3708 self.prefetch(indices);
3709
3710 if self.is_v4() {
3711 let ctx = self.v4_ctx();
3712 return indices
3713 .par_iter()
3714 .map_init(V4Decoded::default, |scratch, &original_index| {
3715 let (chunk, _) = self.locate_chunk(original_index)?;
3716 v4_cell_chunk(chunk, &ctx, scratch)
3717 })
3718 .collect();
3719 }
3720
3721 indices
3722 .par_iter()
3723 .map(|&original_index| {
3724 let decompressed = self.decompress_chunk(original_index)?;
3725
3726 CsrCellChunk::read_from_buffer(&decompressed)
3727 })
3728 .collect()
3729 }
3730
3731 fn read_cells_sparse<T>(
3745 &self,
3746 indices: &[usize],
3747 data_layer: &DataLayerReturn,
3748 ) -> Result<CompressedSparseData2<T, f32>, ScDataErrors>
3749 where
3750 T: BixverseNumeric + FromPrimitive,
3751 {
3752 if !self.is_cell_based() {
3753 return Err(ScDataErrors::ReaderModeMismatch {
3754 actual: "gene-based",
3755 requested: "cell-based",
3756 });
3757 }
3758
3759 self.prefetch(indices);
3760
3761 read_fused(
3762 indices,
3763 data_layer,
3764 CompressedSparseFormat::Csr,
3765 (indices.len(), self.header.total_genes),
3766 |cell, scratch, fragment| {
3767 if self.is_v4() {
3768 let (chunk, _) = self.locate_chunk(cell)?;
3769 return fragment.push_v4(chunk, &self.v4_ctx(), scratch, None);
3770 }
3771 self.decompress_chunk_into(cell, &mut scratch.bytes)?;
3772 let p = csr_payload(&scratch.bytes)?;
3773 fragment.push_run(&p, None)
3774 },
3775 )
3776 }
3777
3778 fn read_genes_sparse<T>(
3794 &self,
3795 indices: &[usize],
3796 cells_to_keep: Option<&IndexSet<u32>>,
3797 data_layer: &DataLayerReturn,
3798 ) -> Result<CompressedSparseData2<T, f32>, ScDataErrors>
3799 where
3800 T: BixverseNumeric + FromPrimitive,
3801 {
3802 if !self.is_gene_based() {
3803 return Err(ScDataErrors::ReaderModeMismatch {
3804 actual: "cell-based",
3805 requested: "gene-based",
3806 });
3807 }
3808
3809 let (row_map, n_rows) = cell_row_map(cells_to_keep, self.header.total_cells);
3810
3811 self.prefetch(indices);
3812
3813 read_fused(
3814 indices,
3815 data_layer,
3816 CompressedSparseFormat::Csc,
3817 (n_rows, indices.len()),
3818 |gene, scratch, fragment| {
3819 if self.is_v4() {
3820 let (chunk, _) = self.locate_chunk(gene)?;
3821 return fragment.push_v4(chunk, &self.v4_ctx(), scratch, row_map.as_deref());
3822 }
3823 self.decompress_chunk_into(gene, &mut scratch.bytes)?;
3824 let p = csc_payload(&scratch.bytes)?;
3825 fragment.push_run(&p, row_map.as_deref())
3826 },
3827 )
3828 }
3829
3830 fn read_gene_parallel(&self, indices: &[usize]) -> Result<Vec<CscGeneChunk>, ScDataErrors> {
3840 if !self.is_gene_based() {
3841 return Err(ScDataErrors::ReaderModeMismatch {
3842 actual: "cell-based",
3843 requested: "gene-based",
3844 });
3845 }
3846
3847 self.prefetch(indices);
3848
3849 if self.is_v4() {
3850 let ctx = self.v4_ctx();
3851 return indices
3852 .par_iter()
3853 .map_init(V4Decoded::default, |scratch, &original_index| {
3854 let (chunk, _) = self.locate_chunk(original_index)?;
3855 v4_gene_chunk(chunk, &ctx, scratch)
3856 })
3857 .collect();
3858 }
3859
3860 indices
3861 .par_iter()
3862 .map(|&original_index| {
3863 let decompressed = self.decompress_chunk(original_index)?;
3864
3865 CscGeneChunk::read_from_buffer(&decompressed)
3866 })
3867 .collect()
3868 }
3869
3870 fn read_gene_parallel_raw(&self, indices: &[usize]) -> Result<Vec<CscGeneChunk>, ScDataErrors> {
3884 if !self.is_v4() {
3885 return self.read_gene_parallel(indices);
3886 }
3887 if !self.is_gene_based() {
3888 return Err(ScDataErrors::ReaderModeMismatch {
3889 actual: "cell-based",
3890 requested: "gene-based",
3891 });
3892 }
3893
3894 self.prefetch(indices);
3895
3896 let ctx = self.v4_ctx();
3897 indices
3898 .par_iter()
3899 .map_init(V4Decoded::default, |s, &original_index| {
3900 let (chunk, _) = self.locate_chunk(original_index)?;
3901 let h = decode_v4(chunk, &ctx, true, false, s)?;
3902 Ok(CscGeneChunk {
3903 data_raw: RawCounts::from_u32_auto(&s.raw[..h.nnz]),
3904 data_norm: Vec::new(),
3905 avg_exp: F16::from_bits(h.aux as u16),
3906 nnz: h.nnz,
3907 indices: std::mem::take(&mut s.idx),
3908 original_index: h.original_index,
3909 to_keep: h.to_keep,
3910 })
3911 })
3912 .collect()
3913 }
3914
3915 fn read_gene_parallel_filtered(
3929 &self,
3930 indices: &[usize],
3931 cells_to_keep: &IndexSet<u32>,
3932 ) -> Result<Vec<CscGeneChunk>, ScDataErrors> {
3933 if !self.is_gene_based() {
3934 return Err(ScDataErrors::ReaderModeMismatch {
3935 actual: "cell-based",
3936 requested: "gene-based",
3937 });
3938 }
3939
3940 let cell_map = keep_set_index_map(cells_to_keep);
3941
3942 self.prefetch(indices);
3943
3944 if self.is_v4() {
3945 let ctx = self.v4_ctx();
3946 return indices
3947 .par_iter()
3948 .map_init(V4Decoded::default, |scratch, &original_index| {
3949 let (chunk, _) = self.locate_chunk(original_index)?;
3950 let mut chunk = v4_gene_chunk(chunk, &ctx, scratch)?;
3951 chunk.filter_selected_cells_dense(&cell_map);
3952 Ok(chunk)
3953 })
3954 .collect();
3955 }
3956
3957 indices
3958 .par_iter()
3959 .map(|&original_index| {
3960 let decompressed = self.decompress_chunk(original_index)?;
3961
3962 let mut chunk = CscGeneChunk::read_from_buffer(&decompressed)?;
3963
3964 chunk.filter_selected_cells_dense(&cell_map);
3965
3966 Ok(chunk)
3967 })
3968 .collect()
3969 }
3970
3971 fn get_header(&self) -> SparseDataHeader {
3977 self.header.clone()
3978 }
3979
3980 fn read_cell_library_sizes(&self, indices: &[usize]) -> Result<Vec<usize>, ScDataErrors> {
3995 if !self.is_cell_based() {
3996 return Err(ScDataErrors::ReaderModeMismatch {
3997 actual: "gene-based",
3998 requested: "cell-based",
3999 });
4000 }
4001
4002 if let Some(start) = self.library_index {
4003 return self.library_sizes_from_index(start, indices);
4004 }
4005
4006 if self.is_v4() {
4007 return indices
4008 .iter()
4009 .map(|&original_index| {
4010 let (chunk, _) = self.locate_chunk(original_index)?;
4011 Ok(V4Header::parse(chunk)?.aux as usize)
4012 })
4013 .collect();
4014 }
4015
4016 indices
4017 .par_iter()
4018 .map(|&original_index| {
4019 let decompressed = self.decompress_chunk(original_index)?;
4020
4021 Ok(u64::from_le_bytes([
4022 decompressed[12],
4023 decompressed[13],
4024 decompressed[14],
4025 decompressed[15],
4026 decompressed[16],
4027 decompressed[17],
4028 decompressed[18],
4029 decompressed[19],
4030 ]) as usize)
4031 })
4032 .collect()
4033 }
4034
4035 fn read_gene_nnz(&self, indices: &[usize]) -> Result<Vec<usize>, ScDataErrors> {
4048 if !self.is_gene_based() {
4049 return Err(ScDataErrors::ReaderModeMismatch {
4050 actual: "cell-based",
4051 requested: "gene-based",
4052 });
4053 }
4054
4055 if self.is_v4() {
4056 return indices
4057 .iter()
4058 .map(|&original_index| {
4059 let (chunk, _) = self.locate_chunk(original_index)?;
4060 Ok(V4Header::parse(chunk)?.nnz)
4061 })
4062 .collect();
4063 }
4064
4065 indices
4066 .par_iter()
4067 .map(|&original_index| {
4068 let decompressed = self.decompress_chunk(original_index)?;
4069
4070 Ok(u64::from_le_bytes([
4071 decompressed[16],
4072 decompressed[17],
4073 decompressed[18],
4074 decompressed[19],
4075 decompressed[20],
4076 decompressed[21],
4077 decompressed[22],
4078 decompressed[23],
4079 ]) as usize)
4080 })
4081 .collect()
4082 }
4083
4084 fn is_cell_based(&self) -> bool {
4090 self.header.cell_based
4091 }
4092
4093 fn target_size(&self) -> Option<f32> {
4100 (self.target_size != 0.0).then_some(self.target_size)
4101 }
4102}
4103
4104#[derive(Clone, Debug, Copy)]
4109pub enum DataLayerReturn {
4112 BothLayers,
4114 Raw,
4116 Norm,
4118}
4119
4120fn extend_raw_counts<T>(data: &mut Vec<T>, raw: &RawCounts) -> Result<(), ScDataErrors>
4134where
4135 T: BixverseNumeric + FromPrimitive,
4136{
4137 let convert = |val: u32| {
4138 T::from_u32(val).ok_or(ScDataErrors::RawCountOverflow {
4139 value: val,
4140 target_type: std::any::type_name::<T>(),
4141 })
4142 };
4143 match raw {
4144 RawCounts::U16(v) => {
4145 for &x in v {
4146 data.push(convert(x as u32)?);
4147 }
4148 }
4149 RawCounts::U32(v) => {
4150 for &x in v {
4151 data.push(convert(x)?);
4152 }
4153 }
4154 }
4155 Ok(())
4156}
4157
4158pub fn from_gene_chunks<T>(
4179 chunks: Vec<CscGeneChunk>,
4180 data_layer: &DataLayerReturn,
4181 n_cells: usize,
4182) -> Result<CompressedSparseData2<T, f32>, ScDataErrors>
4183where
4184 T: BixverseNumeric + FromPrimitive,
4185{
4186 let n_genes = chunks.len();
4187 let keep_raw = matches!(
4188 data_layer,
4189 DataLayerReturn::BothLayers | DataLayerReturn::Raw
4190 );
4191 let keep_norm = matches!(
4192 data_layer,
4193 DataLayerReturn::BothLayers | DataLayerReturn::Norm
4194 );
4195
4196 let total_nnz: usize = chunks.iter().map(|c| c.indices.len()).sum();
4197 let mut data: Vec<T> = Vec::with_capacity(if keep_raw { total_nnz } else { 0 });
4198 let mut data_2: Vec<f32> = Vec::with_capacity(if keep_norm { total_nnz } else { 0 });
4199 let mut indices: Vec<u32> = Vec::with_capacity(total_nnz);
4200 let mut indptr: Vec<usize> = Vec::with_capacity(n_genes + 1);
4201 indptr.push(0);
4202
4203 for chunk in chunks {
4204 if keep_raw {
4205 extend_raw_counts(&mut data, &chunk.data_raw)?;
4206 }
4207 if keep_norm {
4208 data_2.extend(chunk.data_norm.iter().map(|v| v.to_f32()));
4209 }
4210 indices.extend_from_slice(&chunk.indices);
4211 indptr.push(indices.len());
4212 }
4213
4214 Ok(CompressedSparseData2 {
4215 data,
4216 indices,
4217 indptr: indptr.index_cast(),
4218 cs_type: CompressedSparseFormat::Csc,
4219 data_2: if keep_norm { Some(data_2) } else { None },
4220 shape: (n_cells, n_genes),
4221 })
4222}
4223
4224pub fn from_cell_chunks<T>(
4241 chunks: &[CsrCellChunk],
4242 data_layer: &DataLayerReturn,
4243 n_genes: usize,
4244) -> Result<CompressedSparseData2<T, f32>, ScDataErrors>
4245where
4246 T: BixverseNumeric + FromPrimitive,
4247{
4248 let n_cells = chunks.len();
4249 let keep_raw = matches!(
4250 data_layer,
4251 DataLayerReturn::BothLayers | DataLayerReturn::Raw
4252 );
4253 let keep_norm = matches!(
4254 data_layer,
4255 DataLayerReturn::BothLayers | DataLayerReturn::Norm
4256 );
4257
4258 let total_nnz: usize = chunks.iter().map(|c| c.indices.len()).sum();
4259 let mut data: Vec<T> = Vec::with_capacity(if keep_raw { total_nnz } else { 0 });
4260 let mut data_2: Vec<f32> = Vec::with_capacity(if keep_norm { total_nnz } else { 0 });
4261 let mut indices: Vec<u32> = Vec::with_capacity(total_nnz);
4262 let mut indptr: Vec<usize> = Vec::with_capacity(n_cells + 1);
4263 indptr.push(0);
4264
4265 for chunk in chunks {
4266 if keep_raw {
4267 extend_raw_counts(&mut data, &chunk.data_raw)?;
4268 }
4269 if keep_norm {
4270 data_2.extend(chunk.data_norm.iter().map(|v| v.to_f32()));
4271 }
4272 indices.extend_from_slice(&chunk.indices);
4273 indptr.push(indices.len());
4274 }
4275
4276 Ok(CompressedSparseData2 {
4277 data,
4278 indices,
4279 indptr: indptr.index_cast(),
4280 cs_type: CompressedSparseFormat::Csr,
4281 data_2: if keep_norm { Some(data_2) } else { None },
4282 shape: (n_cells, n_genes),
4283 })
4284}
4285
4286impl CscGeneChunk {
4291 pub fn transform_to_clr(&mut self, size_factor: f32) {
4300 let sf_inv = 1.0_f32 / size_factor;
4301 let mut new_sum = 0.0_f32;
4302 for v in self.data_norm.iter_mut() {
4303 let new_val = (v.to_f32().exp_m1() * sf_inv).ln_1p();
4304 new_sum += new_val;
4305 *v = F16::from(f16::from_f32(new_val));
4306 }
4307 self.avg_exp = F16::from(f16::from_f32(new_sum));
4308 }
4309}
4310
4311impl CsrCellChunk {
4312 #[allow(clippy::too_many_arguments)]
4332 pub fn add_cells_scrublet(
4333 cell1: &CsrCellChunk,
4334 cell2: &CsrCellChunk,
4335 hvg_indices: &FxHashSet<usize>,
4336 hvg_lib_size_combined: usize,
4337 target_size: f32,
4338 log_transform: bool,
4339 doublet_index: usize,
4340 ) -> Self {
4341 let mut gene_counts: FxHashMap<u32, u32> = FxHashMap::default();
4342
4343 for i in 0..cell1.indices.len() {
4344 if hvg_indices.contains(&(cell1.indices[i] as usize)) {
4345 *gene_counts.entry(cell1.indices[i]).or_insert(0) += cell1.data_raw.get(i);
4346 }
4347 }
4348 for i in 0..cell2.indices.len() {
4349 if hvg_indices.contains(&(cell2.indices[i] as usize)) {
4350 *gene_counts.entry(cell2.indices[i]).or_insert(0) += cell2.data_raw.get(i);
4351 }
4352 }
4353
4354 let mut gene_vec: Vec<(u32, u32)> = gene_counts.into_iter().collect();
4355 gene_vec.sort_unstable_by_key(|&(gene, _)| gene);
4356
4357 let mut data_raw: Vec<u32> = Vec::with_capacity(gene_vec.len());
4358 let mut data_norm = Vec::with_capacity(gene_vec.len());
4359 let mut indices = Vec::with_capacity(gene_vec.len());
4360
4361 let norm_factor = target_size / hvg_lib_size_combined as f32;
4362
4363 for (gene, count) in gene_vec {
4364 data_raw.push(count);
4365
4366 let normalised = if log_transform {
4367 (count as f32 * norm_factor).ln_1p()
4368 } else {
4369 count as f32 * norm_factor
4370 };
4371 data_norm.push(F16::from(f16::from_f32(
4372 normalised.clamp(-65504.0, 65504.0),
4373 )));
4374 indices.push(gene);
4375 }
4376
4377 Self {
4378 data_raw: RawCounts::from_u32_auto(&data_raw),
4379 data_norm,
4380 library_size: hvg_lib_size_combined,
4381 indices,
4382 original_index: doublet_index,
4383 to_keep: true,
4384 }
4385 }
4386}
4387
4388impl CsrCellChunk {
4389 pub fn from_doublet_simulation(
4411 indices: Vec<u32>,
4412 raw_counts: Vec<u32>,
4413 norm_values: Vec<f32>,
4414 effective_lib_size: usize,
4415 doublet_index: usize,
4416 ) -> Self {
4417 debug_assert_eq!(indices.len(), raw_counts.len());
4418 debug_assert_eq!(indices.len(), norm_values.len());
4419
4420 let data_raw = RawCounts::from_u32_auto(&raw_counts);
4421 let data_norm: Vec<F16> = norm_values
4422 .iter()
4423 .map(|&v| F16::from(f16::from_f32(v)))
4424 .collect();
4425
4426 Self {
4427 data_raw,
4428 data_norm,
4429 library_size: effective_lib_size,
4430 indices,
4431 original_index: doublet_index,
4432 to_keep: true,
4433 }
4434 }
4435}
4436
4437#[cfg(test)]
4442mod tests {
4443 use super::*;
4444 use half::f16;
4445
4446 struct TempBin(std::path::PathBuf);
4448
4449 impl Drop for TempBin {
4452 fn drop(&mut self) {
4453 let _ = std::fs::remove_file(&self.0);
4454 }
4455 }
4456
4457 impl TempBin {
4458 fn new(name: &str) -> Self {
4468 Self(std::env::temp_dir().join(format!("bixverse_data_io_{name}.bin")))
4469 }
4470
4471 fn path(&self) -> &str {
4473 self.0.to_str().expect("temp path is valid UTF-8")
4474 }
4475 }
4476
4477 fn cell_chunk(raw: &[u32], indices: &[u32], original_index: usize) -> CsrCellChunk {
4489 CsrCellChunk {
4490 data_raw: RawCounts::from_u32_auto(raw),
4491 data_norm: raw
4492 .iter()
4493 .map(|&v| F16::from(f16::from_f32(v as f32)))
4494 .collect(),
4495 library_size: raw.iter().map(|&v| v as usize).sum(),
4496 indices: indices.to_vec(),
4497 original_index,
4498 to_keep: true,
4499 }
4500 }
4501
4502 fn gene_chunk(raw: &[u32], indices: &[u32], original_index: usize) -> CscGeneChunk {
4514 CscGeneChunk {
4515 data_raw: RawCounts::from_u32_auto(raw),
4516 data_norm: raw
4517 .iter()
4518 .map(|&v| F16::from(f16::from_f32(v as f32)))
4519 .collect(),
4520 avg_exp: F16::from(f16::from_f32(1.0)),
4521 nnz: raw.len(),
4522 indices: indices.to_vec(),
4523 original_index,
4524 to_keep: true,
4525 }
4526 }
4527
4528 #[test]
4535 fn test_filter_selected_cells_follows_keep_order() {
4536 let keep: IndexSet<u32> = [7_u32, 1, 4, 900].into_iter().collect();
4537
4538 let mut by_set = gene_chunk(&[10, 30, 50, 70], &[1, 3, 5, 7], 0);
4539 by_set.filter_selected_cells(&keep);
4540 assert_eq!(by_set.indices, vec![0, 1]);
4541 assert_eq!(by_set.data_raw.iter().collect::<Vec<u32>>(), vec![70, 10]);
4542 assert_eq!(by_set.data_norm[0].to_f32(), 70.0);
4543 assert_eq!(by_set.nnz, 2);
4544
4545 let mut by_map = gene_chunk(&[10, 30, 50, 70, 90], &[1, 3, 5, 7, 2_000], 0);
4547 by_map.filter_selected_cells_dense(&keep_set_index_map(&keep));
4548 assert_eq!(by_map.indices, by_set.indices);
4549 assert_eq!(
4550 by_map.data_raw.iter().collect::<Vec<u32>>(),
4551 by_set.data_raw.iter().collect::<Vec<u32>>()
4552 );
4553 assert_eq!(by_map.data_norm, by_set.data_norm);
4554 }
4555
4556 #[test]
4562 fn test_cell_chunk_round_trip_preserves_fields() {
4563 let raw = [1_u32, 7, 42, 65_535, 3];
4564 let indices = [0_u32, 5, 9, 100_000, 300_000];
4565 let chunk = cell_chunk(&raw, &indices, 17);
4566
4567 let mut buffer = Vec::new();
4568 chunk.write_to_bytes(&mut buffer).expect("serialise");
4569 let back = CsrCellChunk::read_from_buffer(&buffer).expect("deserialise");
4570
4571 assert_eq!(back.data_raw.iter().collect::<Vec<_>>(), raw.to_vec());
4572 assert_eq!(back.indices, indices.to_vec());
4573 assert_eq!(back.library_size, chunk.library_size);
4574 assert_eq!(back.original_index, 17);
4575 assert!(back.to_keep);
4576 let bits: Vec<u16> = back.data_norm.iter().map(|v| v.to_bits()).collect();
4577 let want: Vec<u16> = chunk.data_norm.iter().map(|v| v.to_bits()).collect();
4578 assert_eq!(bits, want);
4579 }
4580
4581 #[test]
4583 fn test_gene_chunk_round_trip_preserves_fields() {
4584 let raw = [2_u32, 0, 99, 70_000];
4585 let indices = [1_u32, 4, 8, 12];
4586 let chunk = gene_chunk(&raw, &indices, 3);
4587
4588 let mut buffer = Vec::new();
4589 chunk.write_to_bytes(&mut buffer).expect("serialise");
4590 let back = CscGeneChunk::read_from_buffer(&buffer).expect("deserialise");
4591
4592 assert_eq!(back.data_raw.iter().collect::<Vec<_>>(), raw.to_vec());
4593 assert_eq!(back.indices, indices.to_vec());
4594 assert_eq!(back.nnz, 4);
4595 assert_eq!(back.original_index, 3);
4596 assert_eq!(back.avg_exp.to_bits(), chunk.avg_exp.to_bits());
4597 }
4598
4599 #[test]
4601 fn test_gene_chunk_short_buffer_errors_instead_of_panicking() {
4602 for len in CSR_CHUNK_HEADER_LEN..CSC_CHUNK_HEADER_LEN {
4603 let buffer = vec![0_u8; len];
4604 match CscGeneChunk::read_from_buffer(&buffer) {
4605 Err(ScDataErrors::ChunkBufferTooSmall { expected, found }) => {
4606 assert_eq!(expected, CSC_CHUNK_HEADER_LEN);
4607 assert_eq!(found, len);
4608 }
4609 other => panic!("expected ChunkBufferTooSmall for len {len}, got {other:?}"),
4610 }
4611 }
4612 }
4613
4614 #[test]
4617 fn test_chunk_payload_truncation_is_caught() {
4618 let chunk = cell_chunk(&[1, 2, 3], &[0, 1, 2], 0);
4619 let mut buffer = Vec::new();
4620 chunk.write_to_bytes(&mut buffer).expect("serialise");
4621
4622 buffer[0..4].copy_from_slice(&12_u32.to_le_bytes());
4624
4625 assert!(matches!(
4626 CsrCellChunk::read_from_buffer(&buffer),
4627 Err(ScDataErrors::ChunkPayloadTruncated { .. })
4628 ));
4629
4630 let gene = gene_chunk(&[1, 2, 3], &[0, 1, 2], 0);
4632 let mut buffer = Vec::new();
4633 gene.write_to_bytes(&mut buffer).expect("serialise");
4634 buffer[8..12].copy_from_slice(&64_u32.to_le_bytes());
4635
4636 assert!(matches!(
4637 CscGeneChunk::read_from_buffer(&buffer),
4638 Err(ScDataErrors::ChunkPayloadTruncated { .. })
4639 ));
4640 }
4641
4642 #[test]
4645 fn test_chunk_round_trip_with_unaligned_index_offset() {
4646 let raw = [1_u32, 2, 3];
4649 let indices = [7_u32, 8, 9];
4650 let chunk = cell_chunk(&raw, &indices, 0);
4651 assert_eq!(chunk.data_raw.elem_size(), RAW_ELEM_U16);
4652
4653 let mut buffer = Vec::new();
4654 chunk.write_to_bytes(&mut buffer).expect("serialise");
4655 assert_eq!(buffer.len() % 4, 0);
4656
4657 let back = CsrCellChunk::read_from_buffer(&buffer).expect("deserialise");
4658 assert_eq!(back.indices, indices.to_vec());
4659 assert_eq!(back.data_raw.iter().collect::<Vec<_>>(), raw.to_vec());
4660 }
4661
4662 #[test]
4664 fn test_raw_counts_element_size_discriminant() {
4665 let payload = [1_u8, 0, 2, 0];
4666
4667 let legacy = RawCounts::read_from_buffer(&payload, 2, 0).expect("legacy parses as u16");
4669 assert_eq!(legacy.iter().collect::<Vec<_>>(), vec![1, 2]);
4670
4671 let explicit =
4672 RawCounts::read_from_buffer(&payload, 2, RAW_ELEM_U16).expect("explicit u16 parses");
4673 assert_eq!(explicit.iter().collect::<Vec<_>>(), vec![1, 2]);
4674
4675 assert!(matches!(
4676 RawCounts::read_from_buffer(&payload, 2, 3),
4677 Err(ScDataErrors::RawElemSizeInvalid(3))
4678 ));
4679 }
4680
4681 #[test]
4687 fn test_large_raw_count_survives_round_trip() {
4688 let raw = [70_000_u32, 1];
4689 let chunk = cell_chunk(&raw, &[0, 1], 0);
4690 assert_eq!(chunk.data_raw.elem_size(), RAW_ELEM_U32);
4691
4692 let mut buffer = Vec::new();
4693 chunk.write_to_bytes(&mut buffer).expect("serialise");
4694 let back = CsrCellChunk::read_from_buffer(&buffer).expect("deserialise");
4695
4696 let csr = from_cell_chunks::<u32>(&[back], &DataLayerReturn::Raw, 2).expect("assemble");
4697 assert_eq!(csr.data, vec![70_000_u32, 1]);
4698 }
4699
4700 #[test]
4702 fn test_narrow_target_type_reports_overflow() {
4703 let chunk = gene_chunk(&[70_000, 1], &[0, 1], 0);
4704
4705 match from_gene_chunks::<u16>(vec![chunk], &DataLayerReturn::Raw, 2) {
4706 Err(ScDataErrors::RawCountOverflow { value, .. }) => assert_eq!(value, 70_000),
4707 other => panic!("expected RawCountOverflow, got {:?}", other.map(|_| ())),
4708 }
4709 }
4710
4711 #[test]
4717 fn test_cell_based_file_round_trip() {
4718 let temp = TempBin::new("cell_round_trip");
4719 let chunks = vec![
4720 cell_chunk(&[5, 70_000], &[0, 2], 0),
4721 cell_chunk(&[1, 2, 3], &[0, 1, 2], 1),
4722 ];
4723
4724 let mut writer =
4725 CellGeneSparseWriter::new(temp.path(), true, 2, 3, 1e4).expect("writer opens");
4726 for chunk in chunks {
4727 writer.write_cell_chunk(chunk).expect("write");
4728 }
4729 writer.finalise().expect("finalise");
4730
4731 let reader = ParallelSparseReader::new(temp.path()).expect("reader opens");
4732 assert_eq!(reader.target_size(), Some(1e4));
4733 assert!(reader.is_cell_based());
4734
4735 let back = reader.read_cells_parallel(&[0, 1]).expect("read");
4736 assert_eq!(back.len(), 2);
4737 assert_eq!(back[0].data_raw.iter().collect::<Vec<_>>(), vec![5, 70_000]);
4738 assert_eq!(back[1].indices, vec![0, 1, 2]);
4739 }
4740
4741 fn csr_test_store(name: &str) -> (TempBin, ParallelSparseReader) {
4755 csr_test_store_with(name, false)
4756 }
4757
4758 fn csr_test_store_with(name: &str, legacy: bool) -> (TempBin, ParallelSparseReader) {
4760 let temp = TempBin::new(name);
4761 let chunks = vec![
4762 cell_chunk(&[5, 70_000], &[0, 2], 0),
4763 cell_chunk(&[1, 2, 3], &[0, 1, 2], 1),
4764 cell_chunk(&[9], &[3], 2),
4765 ];
4766
4767 let ctor = if legacy {
4768 CellGeneSparseWriter::new_v3
4769 } else {
4770 CellGeneSparseWriter::new
4771 };
4772 let mut writer = ctor(temp.path(), true, 3, 4, 1e4).expect("writer opens");
4773 for chunk in chunks {
4774 writer.write_cell_chunk(chunk).expect("write");
4775 }
4776 writer.finalise().expect("finalise");
4777
4778 let reader = ParallelSparseReader::new(temp.path()).expect("reader opens");
4779 (temp, reader)
4780 }
4781
4782 #[test]
4783 fn test_read_cells_csr_matches_chunk_path() {
4784 let (_temp, reader) = csr_test_store("csr_matches_chunks");
4785 let order = [2usize, 0, 1];
4786
4787 let batch = reader
4788 .read_cells_csr(&order, &DataLayerReturn::BothLayers)
4789 .expect("csr batch");
4790 let chunks = reader.read_cells_parallel(&order).expect("chunk batch");
4791
4792 assert_eq!(batch.shape, (3, 4));
4793 assert_eq!(batch.indptr.len(), order.len() + 1);
4794 assert_eq!(batch.indptr[0], 0);
4795 assert_eq!(
4796 *batch.indptr.last().expect("non-empty"),
4797 batch.indices.len() as u32
4798 );
4799
4800 let raw = batch.data_raw.as_ref().expect("raw requested");
4801 let norm = batch.data_norm.as_ref().expect("norm requested");
4802
4803 for (row, chunk) in chunks.iter().enumerate() {
4804 let lo = batch.indptr[row] as usize;
4805 let hi = batch.indptr[row + 1] as usize;
4806
4807 assert_eq!(&batch.indices[lo..hi], &chunk.indices[..]);
4808 assert_eq!(
4809 raw[lo..hi].to_vec(),
4810 chunk.data_raw.iter().map(|v| v as f32).collect::<Vec<_>>()
4811 );
4812 assert_eq!(
4813 norm[lo..hi].to_vec(),
4814 chunk
4815 .data_norm
4816 .iter()
4817 .map(|v| v.to_f32())
4818 .collect::<Vec<_>>()
4819 );
4820 }
4821 }
4822
4823 #[test]
4824 fn test_read_cells_csr_layer_selection() {
4825 let (_temp, reader) = csr_test_store("csr_layers");
4826
4827 let raw_only = reader
4828 .read_cells_csr(&[0, 1, 2], &DataLayerReturn::Raw)
4829 .expect("raw batch");
4830 assert!(raw_only.data_raw.is_some());
4831 assert!(raw_only.data_norm.is_none());
4832
4833 let norm_only = reader
4834 .read_cells_csr(&[0, 1, 2], &DataLayerReturn::Norm)
4835 .expect("norm batch");
4836 assert!(norm_only.data_raw.is_none());
4837 assert!(norm_only.data_norm.is_some());
4838 }
4839
4840 #[test]
4841 fn test_read_cells_csr_repeats_and_subsets() {
4842 let (_temp, reader) = csr_test_store("csr_repeats");
4843
4844 let batch = reader
4847 .read_cells_csr(&[1, 1], &DataLayerReturn::Raw)
4848 .expect("csr batch");
4849
4850 assert_eq!(batch.shape.0, 2);
4851 assert_eq!(batch.indptr, vec![0, 3, 6]);
4852 assert_eq!(batch.indices, vec![0, 1, 2, 0, 1, 2]);
4853 }
4854
4855 #[test]
4856 fn test_read_cells_csr_rejects_unknown_cell() {
4857 let (_temp, reader) = csr_test_store("csr_unknown_cell");
4858
4859 let err = reader
4860 .read_cells_csr(&[99], &DataLayerReturn::Raw)
4861 .expect_err("index is out of range");
4862 assert!(matches!(err, ScDataErrors::ChunkIndexNotFound(99)));
4863 }
4864
4865 #[test]
4866 fn test_read_cells_csr_rejects_gene_major_store() {
4867 let temp = TempBin::new("csr_gene_major");
4868 let mut writer =
4869 CellGeneSparseWriter::new(temp.path(), false, 2, 1, 1e4).expect("writer opens");
4870 writer
4871 .write_gene_chunk(gene_chunk(&[1, 2], &[0, 1], 0))
4872 .expect("write");
4873 writer.finalise().expect("finalise");
4874
4875 let reader = ParallelSparseReader::new(temp.path()).expect("reader opens");
4876 let err = reader
4877 .read_cells_csr(&[0], &DataLayerReturn::Raw)
4878 .expect_err("store is gene-major");
4879 assert!(matches!(err, ScDataErrors::ReaderModeMismatch { .. }));
4880 }
4881
4882 fn csc_test_store(name: &str) -> (TempBin, ParallelSparseReader) {
4895 let temp = TempBin::new(name);
4896 let chunks = vec![
4897 gene_chunk(&[5, 70_000, 2], &[0, 3, 5], 0),
4898 gene_chunk(&[1, 2, 3, 4, 5, 6], &[0, 1, 2, 3, 4, 5], 1),
4899 gene_chunk(&[9, 11], &[1, 4], 2),
4900 gene_chunk(&[], &[], 3),
4901 ];
4902
4903 let mut writer =
4904 CellGeneSparseWriter::new(temp.path(), false, 6, 4, 1e4).expect("writer opens");
4905 for chunk in chunks {
4906 writer.write_gene_chunk(chunk).expect("write");
4907 }
4908 writer.finalise().expect("finalise");
4909
4910 let reader = ParallelSparseReader::new(temp.path()).expect("reader opens");
4911 (temp, reader)
4912 }
4913
4914 fn assert_same_sparse<T: BixverseNumeric + std::fmt::Debug + PartialEq>(
4921 got: &CompressedSparseData2<T, f32>,
4922 want: &CompressedSparseData2<T, f32>,
4923 ) {
4924 assert_eq!(got.shape, want.shape);
4925 assert_eq!(got.cs_type.is_csc(), want.cs_type.is_csc());
4926 assert_eq!(got.indptr, want.indptr);
4927 assert_eq!(got.indices, want.indices);
4928 assert_eq!(got.data, want.data);
4929 assert_eq!(got.data_2, want.data_2);
4930 }
4931
4932 #[test]
4936 fn test_read_genes_sparse_matches_chunk_path() {
4937 let (_temp, reader) = csc_test_store("csc_matches_chunks");
4938 let genes = [2usize, 0, 3, 1, 0];
4939
4940 let subsets: Vec<Option<IndexSet<u32>>> = vec![
4941 None,
4942 Some([0_u32, 3, 4, 5].into_iter().collect()),
4943 Some([5_u32, 1, 3].into_iter().collect()),
4944 Some(IndexSet::new()),
4945 ];
4946
4947 for layer in [
4948 DataLayerReturn::Raw,
4949 DataLayerReturn::Norm,
4950 DataLayerReturn::BothLayers,
4951 ] {
4952 for subset in &subsets {
4953 let (chunks, n_cells) = match subset {
4954 Some(set) => (
4955 reader
4956 .read_gene_parallel_filtered(&genes, set)
4957 .expect("chunks"),
4958 set.len(),
4959 ),
4960 None => (reader.read_gene_parallel(&genes).expect("chunks"), 6),
4961 };
4962 let want = from_gene_chunks::<u32>(chunks, &layer, n_cells).expect("reference");
4963 let got = reader
4964 .read_genes_sparse::<u32>(&genes, subset.as_ref(), &layer)
4965 .expect("fused");
4966 assert_same_sparse(&got, &want);
4967 }
4968 }
4969 }
4970
4971 #[test]
4973 fn test_read_cells_sparse_matches_chunk_path() {
4974 let (_temp, reader) = csr_test_store("csr_sparse_matches_chunks");
4975 let order = [2usize, 0, 1, 0];
4976
4977 for layer in [
4978 DataLayerReturn::Raw,
4979 DataLayerReturn::Norm,
4980 DataLayerReturn::BothLayers,
4981 ] {
4982 let chunks = reader.read_cells_parallel(&order).expect("chunks");
4983 let want = from_cell_chunks::<u32>(&chunks, &layer, 4).expect("reference");
4984 let got = reader
4985 .read_cells_sparse::<u32>(&order, &layer)
4986 .expect("fused");
4987 assert_same_sparse(&got, &want);
4988 }
4989 }
4990
4991 #[test]
4995 fn test_read_genes_sparse_reports_overflow() {
4996 let (_temp, reader) = csc_test_store("csc_overflow");
4997 let keep: IndexSet<u32> = [3_u32].into_iter().collect();
4998
4999 for cells in [None, Some(&keep)] {
5000 match reader.read_genes_sparse::<u16>(&[0], cells, &DataLayerReturn::Raw) {
5001 Err(ScDataErrors::RawCountOverflow { value, .. }) => assert_eq!(value, 70_000),
5002 other => panic!("expected RawCountOverflow, got {:?}", other.map(|_| ())),
5003 }
5004 }
5005
5006 let keep: IndexSet<u32> = [0_u32, 5].into_iter().collect();
5008 let csc = reader
5009 .read_genes_sparse::<u16>(&[0], Some(&keep), &DataLayerReturn::Raw)
5010 .expect("fits u16");
5011 assert_eq!(csc.data, vec![5, 2]);
5012 }
5013
5014 #[test]
5015 fn test_read_genes_sparse_rejects_cell_major_store() {
5016 let (_temp, reader) = csr_test_store("csc_cell_major");
5017 let err = reader
5018 .read_genes_sparse::<f32>(&[0], None, &DataLayerReturn::Raw)
5019 .expect_err("store is cell-major");
5020 assert!(matches!(err, ScDataErrors::ReaderModeMismatch { .. }));
5021 }
5022
5023 #[test]
5024 fn test_decompress_chunk_into_matches_allocating_path() {
5025 let (_temp, reader) = csr_test_store_with("csr_scratch", true);
5026
5027 let mut scratch = vec![0xAAu8; 4096];
5030 for cell in 0..3 {
5031 reader
5032 .decompress_chunk_into(cell, &mut scratch)
5033 .expect("scratch decompress");
5034 let owned = reader.decompress_chunk(cell).expect("owned decompress");
5035 assert_eq!(scratch, owned);
5036 }
5037 }
5038
5039 #[test]
5040 fn test_identity_index_map_is_detected() {
5041 let (_temp, reader) = csr_test_store("csr_identity_map");
5042 assert!(reader.identity_index_map);
5043 assert_eq!(reader.chunk_index_of(2).expect("in range"), 2);
5044 }
5045
5046 fn patch_file_header(path: &str, edit: impl FnOnce(&mut FileHeader)) {
5050 let mut bytes = std::fs::read(path).expect("read back");
5051 let (mut header, _) = decode_from_slice::<FileHeader, _>(&bytes[0..64], config::standard())
5052 .expect("header decodes");
5053
5054 edit(&mut header);
5055
5056 let encoded = encode_to_vec(&header, config::standard()).expect("header encodes");
5057 let mut slot = [0_u8; 64];
5058 slot[..encoded.len()].copy_from_slice(&encoded);
5059 bytes[0..64].copy_from_slice(&slot);
5060
5061 std::fs::write(path, bytes).expect("write back");
5062 }
5063
5064 fn write_valid_file(path: &str) {
5066 let mut writer = CellGeneSparseWriter::new(path, true, 1, 3, 1e4).expect("writer opens");
5067 writer
5068 .write_cell_chunk(cell_chunk(&[1, 2], &[0, 1], 0))
5069 .expect("write");
5070 writer.finalise().expect("finalise");
5071 }
5072
5073 #[test]
5076 fn test_reader_rejects_a_version_mismatch() {
5077 let temp = TempBin::new("version_mismatch");
5078 write_valid_file(temp.path());
5079 patch_file_header(temp.path(), |h| h.version = SC_FILE_VERSION + 1);
5080
5081 match ParallelSparseReader::new(temp.path()) {
5082 Err(ScDataErrors::FileVersionMismatch { expected, found }) => {
5083 assert_eq!(expected, SC_FILE_VERSION);
5084 assert_eq!(found, SC_FILE_VERSION + 1);
5085 }
5086 Err(other) => panic!("expected a version mismatch, got {other:?}"),
5087 Ok(_) => panic!("a mismatched version must not open"),
5088 }
5089 }
5090
5091 #[test]
5093 fn test_reader_rejects_a_bad_magic() {
5094 let temp = TempBin::new("bad_magic");
5095 write_valid_file(temp.path());
5096 patch_file_header(temp.path(), |h| h.magic = *b"NOTOURS!");
5097
5098 assert!(
5099 ParallelSparseReader::new(temp.path()).is_err(),
5100 "a wrong magic string must not open"
5101 );
5102 }
5103
5104 #[test]
5107 fn test_reader_rejects_a_main_header_offset_past_eof() {
5108 let temp = TempBin::new("offset_past_eof");
5109 write_valid_file(temp.path());
5110 patch_file_header(temp.path(), |h| h.main_header_offset = u64::MAX / 2);
5111
5112 assert!(
5113 ParallelSparseReader::new(temp.path()).is_err(),
5114 "an out-of-range main header offset must not open"
5115 );
5116 }
5117
5118 #[test]
5120 fn test_reader_rejects_a_truncated_file() {
5121 let temp = TempBin::new("truncated");
5122 write_valid_file(temp.path());
5123
5124 let bytes = std::fs::read(temp.path()).expect("read back");
5125 std::fs::write(temp.path(), &bytes[..bytes.len() / 2]).expect("truncate");
5126
5127 assert!(
5128 ParallelSparseReader::new(temp.path()).is_err(),
5129 "a truncated file must not open"
5130 );
5131 }
5132
5133 #[test]
5135 fn test_gene_based_file_round_trip() {
5136 let temp = TempBin::new("gene_round_trip");
5137
5138 let mut writer =
5139 CellGeneSparseWriter::new(temp.path(), false, 4, 2, 1e5).expect("writer opens");
5140 writer
5141 .write_gene_chunk(gene_chunk(&[9, 8], &[0, 3], 0))
5142 .expect("write");
5143 writer
5144 .write_gene_chunk(gene_chunk(&[100_000], &[2], 1))
5145 .expect("write");
5146 writer.finalise().expect("finalise");
5147
5148 let reader = ParallelSparseReader::new(temp.path()).expect("reader opens");
5149 assert_eq!(reader.target_size(), Some(1e5));
5150 assert!(reader.is_gene_based());
5151
5152 let back = reader.read_gene_parallel(&[0, 1]).expect("read");
5153 assert_eq!(back[0].data_raw.iter().collect::<Vec<_>>(), vec![9, 8]);
5154 assert_eq!(back[1].data_raw.iter().collect::<Vec<_>>(), vec![100_000]);
5155 }
5156
5157 #[test]
5159 fn test_writer_rejects_wrong_chunk_orientation() {
5160 let temp = TempBin::new("mode_mismatch");
5161 let mut writer =
5162 CellGeneSparseWriter::new(temp.path(), true, 1, 1, 0.0).expect("writer opens");
5163
5164 match writer.write_gene_chunk(gene_chunk(&[1], &[0], 0)) {
5165 Err(ScDataErrors::ReaderModeMismatch { actual, requested }) => {
5166 assert_eq!(actual, "cell-based");
5167 assert_eq!(requested, "gene-based");
5168 }
5169 other => panic!("expected ReaderModeMismatch, got {other:?}"),
5170 }
5171 }
5172
5173 #[test]
5175 fn test_raw_only_file_reports_no_target_size() {
5176 let temp = TempBin::new("no_target_size");
5177 let mut writer =
5178 CellGeneSparseWriter::new(temp.path(), true, 1, 2, 0.0).expect("writer opens");
5179 writer
5180 .write_cell_chunk(cell_chunk(&[1, 2], &[0, 1], 0))
5181 .expect("write");
5182 writer.finalise().expect("finalise");
5183
5184 let reader = ParallelSparseReader::new(temp.path()).expect("reader opens");
5185 assert_eq!(reader.target_size(), None);
5186 assert_eq!(peek_target_size(temp.path()).expect("peek"), None);
5187 }
5188
5189 #[derive(Encode, Decode, Serialize, Deserialize)]
5197 struct LegacyFileHeader {
5198 magic: [u8; 8],
5200 version: u32,
5202 main_header_offset: u64,
5204 cell_based: bool,
5206 _reserved_1: [u8; 32],
5208 _reserved_2: [u8; 3],
5210 }
5211
5212 fn encode_slot<T: Serialize>(header: &T) -> [u8; 64] {
5222 let encoded = encode_to_vec(header, config::standard()).expect("encode");
5223 assert!(encoded.len() <= 64, "header must fit the fixed slot");
5224 let mut slot = [0_u8; 64];
5225 slot[..encoded.len()].copy_from_slice(&encoded);
5226 slot
5227 }
5228
5229 #[test]
5231 fn test_legacy_header_decodes_with_zero_target_size() {
5232 let legacy = LegacyFileHeader {
5233 magic: *b"SCRNASEQ",
5234 version: SC_FILE_VERSION_V3,
5235 main_header_offset: 123_456,
5236 cell_based: true,
5237 _reserved_1: [0; 32],
5238 _reserved_2: [0; 3],
5239 };
5240
5241 let slot = encode_slot(&legacy);
5242 let (decoded, _) =
5243 decode_from_slice::<FileHeader, _>(&slot, config::standard()).expect("decode as v3");
5244
5245 assert_eq!(decoded.magic, *b"SCRNASEQ");
5246 assert_eq!(decoded.version, SC_FILE_VERSION_V3);
5247 assert_eq!(decoded.main_header_offset, 123_456);
5248 assert!(decoded.cell_based);
5249 assert_eq!(decoded.target_size, 0.0);
5250 }
5251
5252 #[test]
5254 fn test_new_header_still_decodes_on_the_old_layout() {
5255 let mut header = FileHeader::new(false, 1e4);
5256 header.main_header_offset = 987_654_321;
5257
5258 let slot = encode_slot(&header);
5259 let (decoded, _) = decode_from_slice::<LegacyFileHeader, _>(&slot, config::standard())
5260 .expect("decode as legacy");
5261
5262 assert_eq!(decoded.magic, *b"SCRNASEQ");
5263 assert_eq!(decoded.version, SC_FILE_VERSION);
5264 assert_eq!(decoded.main_header_offset, 987_654_321);
5265 assert!(!decoded.cell_based);
5266 }
5267
5268 #[test]
5270 fn test_target_size_survives_header_round_trip() {
5271 let slot = encode_slot(&FileHeader::new(true, 1e5));
5272 let (decoded, _) =
5273 decode_from_slice::<FileHeader, _>(&slot, config::standard()).expect("decode");
5274 assert_eq!(decoded.target_size, 1e5);
5275 }
5276
5277 #[derive(Encode, Decode, Serialize, Deserialize)]
5284 struct TargetSizeFileHeader {
5285 magic: [u8; 8],
5286 version: u32,
5287 main_header_offset: u64,
5288 cell_based: bool,
5289 target_size: f32,
5290 _reserved_1: [u8; 28],
5291 _reserved_2: [u8; 3],
5292 }
5293
5294 #[test]
5297 fn test_pre_index_header_decodes_with_no_library_index() {
5298 let old = TargetSizeFileHeader {
5299 magic: *b"SCRNASEQ",
5300 version: SC_FILE_VERSION_V3,
5301 main_header_offset: 123_456,
5302 cell_based: true,
5303 target_size: 1e4,
5304 _reserved_1: [0; 28],
5305 _reserved_2: [0; 3],
5306 };
5307
5308 let slot = encode_slot(&old);
5309 let (decoded, _) =
5310 decode_from_slice::<FileHeader, _>(&slot, config::standard()).expect("decode");
5311
5312 assert_eq!(decoded.main_header_offset, 123_456);
5313 assert_eq!(decoded.target_size, 1e4);
5314 assert_eq!(decoded.library_index_offset, 0);
5315 }
5316
5317 #[test]
5320 fn test_header_with_library_index_decodes_on_the_pre_index_layout() {
5321 let mut header = FileHeader::new(true, 1e4);
5322 header.main_header_offset = 987_654_321;
5323 header.library_index_offset = u64::MAX;
5324
5325 let slot = encode_slot(&header);
5326 let (decoded, _) = decode_from_slice::<TargetSizeFileHeader, _>(&slot, config::standard())
5327 .expect("decode as pre-index");
5328
5329 assert_eq!(decoded.magic, *b"SCRNASEQ");
5330 assert_eq!(decoded.version, SC_FILE_VERSION);
5331 assert_eq!(decoded.main_header_offset, 987_654_321);
5332 assert!(decoded.cell_based);
5333 assert_eq!(decoded.target_size, 1e4);
5334 }
5335
5336 fn read_file_header(path: &str) -> FileHeader {
5346 let bytes = std::fs::read(path).expect("read back");
5347 decode_from_slice::<FileHeader, _>(&bytes[0..64], config::standard())
5348 .expect("header decodes")
5349 .0
5350 }
5351
5352 fn write_index_test_file(path: &str, compressed: bool) {
5355 let chunks = vec![
5356 cell_chunk(&[5, 70_000], &[0, 2], 2),
5357 cell_chunk(&[1, 2, 3], &[0, 1, 2], 0),
5358 cell_chunk(&[9], &[1], 1),
5359 ];
5360 let mut writer = CellGeneSparseWriter::new(path, true, 3, 3, 1e4).expect("writer opens");
5361 if compressed {
5362 write_cell_chunks_parallel(&chunks, &mut writer).expect("write");
5363 } else {
5364 for chunk in chunks {
5365 writer.write_cell_chunk(chunk).expect("write");
5366 }
5367 }
5368 writer.finalise().expect("finalise");
5369 }
5370
5371 fn library_sizes_both_ways(
5374 reader: &ParallelSparseReader,
5375 indices: &[usize],
5376 ) -> (Vec<usize>, Vec<usize>) {
5377 let answered = reader.read_cell_library_sizes(indices).expect("read");
5378 let stored = reader
5379 .read_cells_parallel(indices)
5380 .expect("read chunks")
5381 .iter()
5382 .map(|chunk| chunk.library_size)
5383 .collect();
5384 (answered, stored)
5385 }
5386
5387 #[test]
5390 fn test_library_index_matches_chunk_headers() {
5391 for compressed in [false, true] {
5392 let temp = TempBin::new(&format!("library_index_{compressed}"));
5393 write_index_test_file(temp.path(), compressed);
5394
5395 let reader = ParallelSparseReader::new(temp.path()).expect("reader opens");
5396 assert!(reader.library_index.is_some());
5397 let (answered, stored) = library_sizes_both_ways(&reader, &[1, 2, 0, 2]);
5398 assert_eq!(answered, vec![9, 70_005, 6, 70_005]);
5399 assert_eq!(answered, stored);
5400 }
5401 }
5402
5403 #[test]
5405 fn test_library_sizes_fall_back_without_an_index() {
5406 let temp = TempBin::new("library_index_absent");
5407 write_index_test_file(temp.path(), false);
5408 patch_file_header(temp.path(), |h| h.library_index_offset = 0);
5409
5410 let reader = ParallelSparseReader::new(temp.path()).expect("reader opens");
5411 assert!(reader.library_index.is_none());
5412 let (answered, _) = library_sizes_both_ways(&reader, &[0, 1, 2]);
5413 assert_eq!(answered, vec![6, 9, 70_005]);
5414 }
5415
5416 #[test]
5418 fn test_reader_rejects_a_library_index_past_eof() {
5419 let temp = TempBin::new("library_index_past_eof");
5420 write_valid_file(temp.path());
5421 let len = std::fs::metadata(temp.path()).expect("stat").len();
5422 patch_file_header(temp.path(), |h| h.library_index_offset = len);
5423
5424 assert!(matches!(
5425 ParallelSparseReader::new(temp.path()),
5426 Err(ScDataErrors::HeaderDecodeFailed)
5427 ));
5428 }
5429
5430 #[test]
5432 fn test_reader_rejects_a_library_index_with_the_wrong_count() {
5433 let temp = TempBin::new("library_index_wrong_count");
5434 write_valid_file(temp.path());
5435 let main_header_offset = read_file_header(temp.path()).main_header_offset;
5437 patch_file_header(temp.path(), |h| h.library_index_offset = main_header_offset);
5438
5439 assert!(matches!(
5440 ParallelSparseReader::new(temp.path()),
5441 Err(ScDataErrors::HeaderDecodeFailed)
5442 ));
5443 }
5444
5445 #[test]
5448 fn test_library_index_on_the_ingest_write_path() {
5449 let temp = TempBin::new("library_index_ingest");
5450 let mut rows: Vec<Vec<(u32, u32)>> = (0..300_u32)
5451 .map(|c| {
5452 (0..20_u32)
5453 .filter(|g| (c * 7 + g * 3) % 5 == 0)
5454 .map(|g| (g, 1 + (c + g) % 4 + if c == 7 { 70_000 } else { 0 }))
5455 .collect()
5456 })
5457 .collect();
5458 let mut writer =
5459 CellGeneSparseWriter::new(temp.path(), true, 300, 20, 1e4).expect("writer opens");
5460 let (_, reported) = write_cell_rows(&mut rows, 0, 1e4, &mut writer).expect("write");
5461 writer.finalise().expect("finalise");
5462
5463 let reader = ParallelSparseReader::new(temp.path()).expect("reader opens");
5464 assert!(reader.library_index.is_some());
5465 let (answered, stored) = library_sizes_both_ways(&reader, &(0..300).collect::<Vec<_>>());
5466 assert_eq!(answered, stored);
5467 assert_eq!(answered, reported);
5468 }
5469
5470 #[test]
5472 fn test_gene_based_file_has_no_library_index() {
5473 let temp = TempBin::new("library_index_gene_file");
5474 let mut writer =
5475 CellGeneSparseWriter::new(temp.path(), false, 2, 1, 1e4).expect("writer opens");
5476 writer
5477 .write_gene_chunk(gene_chunk(&[1, 2], &[0, 1], 0))
5478 .expect("write");
5479 writer.finalise().expect("finalise");
5480
5481 assert_eq!(read_file_header(temp.path()).library_index_offset, 0);
5482 let reader = ParallelSparseReader::new(temp.path()).expect("reader opens");
5483 assert!(reader.library_index.is_none());
5484 }
5485}