1use flate2::read::MultiGzDecoder;
18use rayon::prelude::*;
19use std::fs::File;
20use std::io::{BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
21use std::path::{Path, PathBuf};
22use std::sync::Mutex;
23use std::sync::atomic::{AtomicUsize, Ordering};
24use std::sync::mpsc::sync_channel;
25use std::time::Instant;
26use thousands::Separable;
27
28use crate::data_io::{
29 CellGeneSparseWriter, CellOnFileQuality, INDEX_DROPPED, compress_cell_row, dense_index_map,
30};
31use crate::prelude::*;
32
33const BUCKET_RECORD_LEN: usize = 12;
43
44const MTX_BLOCK_BYTES: usize = 8 * 1024 * 1024;
49
50const GZ_BLOCKS_PER_THREAD: usize = 2;
53
54const BUCKET_FLUSH_BYTES: usize = 64 * 1024;
58
59const MAX_BUCKETS: usize = 128;
61
62const CELLS_PER_BUCKET: usize = 5_000;
65
66const REAL_INTEGRAL_TOL: f64 = 1e-6;
70
71const GZ_MAGIC: [u8; 2] = [0x1f, 0x8b];
73
74static SPILL_COUNTER: AtomicUsize = AtomicUsize::new(0);
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum MtxField {
84 Integer,
86 Real,
88 Pattern,
90}
91
92#[derive(Debug, Clone)]
94pub struct MtxHeader {
95 pub total_cells: usize,
97 pub total_genes: usize,
99 pub total_entries: usize,
101 pub field: MtxField,
103}
104
105fn parse_banner(line: &[u8]) -> Result<MtxField, ScDataErrors> {
116 let text = String::from_utf8_lossy(line).to_ascii_lowercase();
117 let tokens: Vec<&str> = text.split_whitespace().collect();
118 let [_, object, format, field, symmetry] = tokens[..] else {
119 return Err(ScDataErrors::MtxHeaderInvalid(
120 "the banner needs object, format, field and symmetry",
121 ));
122 };
123 if object != "matrix" {
124 return Err(ScDataErrors::MtxUnsupported(format!("object '{object}'")));
125 }
126 if format != "coordinate" {
127 return Err(ScDataErrors::MtxUnsupported(format!(
128 "format '{format}', only sparse 'coordinate' is read"
129 )));
130 }
131 if symmetry != "general" {
132 return Err(ScDataErrors::MtxUnsupported(format!(
133 "symmetry '{symmetry}', only 'general' is read"
134 )));
135 }
136 match field {
137 "integer" => Ok(MtxField::Integer),
138 "real" | "double" => Ok(MtxField::Real),
139 "pattern" => Ok(MtxField::Pattern),
140 other => Err(ScDataErrors::MtxUnsupported(format!(
141 "field '{other}', counts must be integer, real or pattern"
142 ))),
143 }
144}
145
146fn parse_preamble(
157 reader: &mut impl BufRead,
158 cells_as_rows: bool,
159) -> Result<(MtxField, [usize; 3], u64), ScDataErrors> {
160 let mut line = Vec::new();
161 let mut consumed = 0u64;
162 let mut field = MtxField::Integer;
163 let mut first = true;
164 loop {
165 line.clear();
166 let n = reader.read_until(b'\n', &mut line)?;
167 if n == 0 {
168 return Err(ScDataErrors::MtxHeaderInvalid("no shape line"));
169 }
170 consumed += n as u64;
171 if first && line.starts_with(b"%%") {
172 field = parse_banner(&line)?;
173 }
174 first = false;
175 if line.first() != Some(&b'%') {
176 break;
177 }
178 }
179
180 let text = std::str::from_utf8(&line)
181 .map_err(|_| ScDataErrors::MtxHeaderInvalid("the shape line is not text"))?;
182 let parts: Vec<&str> = text.split_whitespace().collect();
183 if parts.len() != 3 {
184 return Err(ScDataErrors::MtxHeaderInvalid(
185 "expected three whitespace-separated fields on the shape line",
186 ));
187 }
188 let names = if cells_as_rows {
189 ["cell count", "gene count", "entry count"]
190 } else {
191 ["gene count", "cell count", "entry count"]
192 };
193 let mut shape = [0usize; 3];
194 for (k, (s, field)) in parts.iter().zip(names).enumerate() {
195 shape[k] = s
196 .parse()
197 .map_err(|_| ScDataErrors::MtxParseError { field })?;
198 }
199 Ok((field, shape, consumed))
200}
201
202pub(crate) struct MtxSource {
208 path: PathBuf,
210 gz: bool,
212 pub(crate) field: MtxField,
214 pub(crate) shape: [usize; 3],
216 data_start: u64,
218 block_bytes: usize,
220}
221
222impl MtxSource {
223 pub(crate) fn open(path: &Path, cells_as_rows: bool) -> Result<Self, ScDataErrors> {
234 let mut magic = [0u8; 2];
235 let mut file = File::open(path)?;
236 let gz = file.read(&mut magic)? == 2 && magic == GZ_MAGIC;
237 file.rewind()?;
238 let mut source = Self {
239 path: path.to_path_buf(),
240 gz,
241 field: MtxField::Integer,
242 shape: [0; 3],
243 data_start: 0,
244 block_bytes: MTX_BLOCK_BYTES,
245 };
246 let mut reader = BufReader::new(source.stream()?);
247 let (field, shape, data_start) = parse_preamble(&mut reader, cells_as_rows)?;
248 source.field = field;
249 source.shape = shape;
250 source.data_start = data_start;
251 Ok(source)
252 }
253
254 fn stream(&self) -> Result<Box<dyn Read + Send>, ScDataErrors> {
260 let file = File::open(&self.path)?;
261 Ok(if self.gz {
262 Box::new(MultiGzDecoder::new(file))
263 } else {
264 Box::new(file)
265 })
266 }
267
268 pub(crate) fn for_each_block_seq(
279 &self,
280 mut f: impl FnMut(usize, Vec<u8>) -> Result<(), ScDataErrors>,
281 ) -> Result<(), ScDataErrors> {
282 let mut reader = self.stream()?;
283 std::io::copy(
284 &mut (&mut reader).take(self.data_start),
285 &mut std::io::sink(),
286 )?;
287
288 let mut carry = Vec::new();
289 let mut seq = 0;
290 loop {
291 let mut block = std::mem::take(&mut carry);
292 block.reserve(self.block_bytes);
293 let n = (&mut reader)
294 .take(self.block_bytes as u64)
295 .read_to_end(&mut block)?;
296 if n == 0 {
297 if !block.is_empty() {
298 f(seq, block)?;
299 }
300 return Ok(());
301 }
302 if let Some(p) = block.iter().rposition(|&b| b == b'\n') {
304 carry = block[p + 1..].to_vec();
305 block.truncate(p + 1);
306 f(seq, block)?;
307 seq += 1;
308 } else {
309 carry = block;
310 }
311 }
312 }
313
314 fn plain_ranges(&self) -> Result<Vec<(u64, u64)>, ScDataErrors> {
320 let mut file = File::open(&self.path)?;
321 let len = file.metadata()?.len();
322 let mut ranges = Vec::new();
323 let mut a = self.data_start;
324 let mut probe = vec![0u8; 64 * 1024];
325 while a < len {
326 let mut b = a + self.block_bytes as u64;
327 if b >= len {
328 ranges.push((a, len));
329 break;
330 }
331 file.seek(SeekFrom::Start(b))?;
332 loop {
333 let k = file.read(&mut probe)?;
334 if k == 0 {
335 b = len;
336 break;
337 }
338 if let Some(p) = probe[..k].iter().position(|&c| c == b'\n') {
339 b += p as u64 + 1;
340 break;
341 }
342 b += k as u64;
343 }
344 ranges.push((a, b));
345 a = b;
346 }
347 Ok(ranges)
348 }
349
350 pub(crate) fn par_fold_blocks<A, I, F>(&self, init: I, f: F) -> Result<Vec<A>, ScDataErrors>
366 where
367 A: Send,
368 I: Fn() -> A + Sync,
369 F: Fn(&mut A, usize, &[u8]) -> Result<(), ScDataErrors> + Sync,
370 {
371 let slots: Vec<Mutex<Option<A>>> = (0..rayon::current_num_threads())
374 .map(|_| Mutex::new(None))
375 .collect();
376 let fold = |seq: usize, bytes: &[u8]| -> Result<(), ScDataErrors> {
377 let k = rayon::current_thread_index().unwrap_or(0) % slots.len();
378 let mut slot = slots[k]
379 .lock()
380 .expect("an accumulator lock is never held across a panic");
381 f(slot.get_or_insert_with(&init), seq, bytes)
382 };
383
384 if self.gz {
385 let (tx, rx) = sync_channel::<(usize, Vec<u8>)>(slots.len() * GZ_BLOCKS_PER_THREAD);
386 std::thread::scope(|s| {
387 let reader = s.spawn(move || {
388 self.for_each_block_seq(|seq, block| {
389 tx.send((seq, block)).map_err(|_| {
392 ScDataErrors::BinaryIo(std::io::Error::other("mtx parsers stopped"))
393 })
394 })
395 });
396 let parsed = rx
397 .into_iter()
398 .par_bridge()
399 .try_for_each(|(seq, block)| fold(seq, &block));
400 let read = reader.join().expect("the gzip reader thread panicked");
401 parsed.and(read)
402 })?;
403 } else {
404 self.plain_ranges()?
405 .par_iter()
406 .enumerate()
407 .try_for_each_init(
408 Vec::new,
409 |buf, (seq, &(a, b))| -> Result<(), ScDataErrors> {
410 let mut file = File::open(&self.path)?;
411 file.seek(SeekFrom::Start(a))?;
412 buf.resize((b - a) as usize, 0);
413 file.read_exact(buf)?;
414 fold(seq, buf)
415 },
416 )?;
417 }
418
419 Ok(slots
420 .into_iter()
421 .filter_map(|m| {
422 m.into_inner()
423 .expect("an accumulator lock is never held across a panic")
424 })
425 .collect())
426 }
427}
428
429#[inline]
445fn parse_index(line: &[u8], i: &mut usize) -> Option<u32> {
446 let start = *i;
447 let mut v = 0u32;
448 while *i < line.len() && line[*i].is_ascii_digit() {
449 v = v.wrapping_mul(10).wrapping_add((line[*i] - b'0') as u32);
450 *i += 1;
451 }
452 (*i > start).then_some(v)
453}
454
455#[inline]
462fn skip_blank(line: &[u8], i: &mut usize) {
463 while *i < line.len() && (line[*i] == b' ' || line[*i] == b'\t') {
464 *i += 1;
465 }
466}
467
468#[inline]
482fn parse_count(token: &[u8]) -> Result<u32, ScDataErrors> {
483 let not_a_count = || ScDataErrors::MtxNotACount {
484 value: String::from_utf8_lossy(token).into_owned(),
485 };
486 if !token.is_empty() && token.iter().all(u8::is_ascii_digit) {
487 let mut v = 0u64;
488 for &b in token {
489 v = v * 10 + (b - b'0') as u64;
490 if v > u32::MAX as u64 {
491 return Err(not_a_count());
492 }
493 }
494 return Ok(v as u32);
495 }
496 let v: f64 = std::str::from_utf8(token)
497 .ok()
498 .and_then(|s| s.parse().ok())
499 .ok_or_else(not_a_count)?;
500 let r = v.round();
501 if !v.is_finite()
502 || r < 0.0
503 || r > u32::MAX as f64
504 || (v - r).abs() > REAL_INTEGRAL_TOL * r.abs().max(1.0)
505 {
506 return Err(not_a_count());
507 }
508 Ok(r as u32)
509}
510
511#[inline]
524fn parse_entry(line: &[u8], field: MtxField) -> Result<Option<(u32, u32, u32)>, ScDataErrors> {
525 let mut i = 0;
526 let Some(row) = parse_index(line, &mut i) else {
527 return Ok(None);
528 };
529 skip_blank(line, &mut i);
530 let Some(col) = parse_index(line, &mut i) else {
531 return Ok(None);
532 };
533 if field == MtxField::Pattern {
534 return Ok(Some((row, col, 1)));
535 }
536 skip_blank(line, &mut i);
537 let start = i;
538 while i < line.len() && !matches!(line[i], b' ' | b'\t') {
539 i += 1;
540 }
541 if i == start {
542 return Ok(None);
543 }
544 Ok(Some((row, col, parse_count(&line[start..i])?)))
545}
546
547pub(crate) fn parse_block<F: FnMut(u32, u32, u32)>(
559 bytes: &[u8],
560 field: MtxField,
561 mut f: F,
562) -> Result<(), ScDataErrors> {
563 for line in bytes.split(|&b| b == b'\n') {
564 let line = line.strip_suffix(b"\r").unwrap_or(line);
565 if let Some((row, col, value)) = parse_entry(line, field)? {
566 f(row, col, value);
567 }
568 }
569 Ok(())
570}
571
572struct TempFileGuard(Vec<PathBuf>);
581
582impl Drop for TempFileGuard {
588 fn drop(&mut self) {
589 for p in &self.0 {
590 let _ = std::fs::remove_file(p);
591 }
592 }
593}
594
595struct BucketFile {
597 writer: BufWriter<File>,
599 len: u64,
601 segments: Vec<(usize, u64, u64)>,
603}
604
605struct BlockOrder {
607 seq: usize,
609 first: u32,
611 last: u32,
613 sorted: bool,
615}
616
617struct TextPassAcc {
627 gene_nnz: Vec<u32>,
629 rare: Vec<Vec<(u32, u32)>>,
631 cell_stats: Vec<(u32, u32)>,
633 bufs: Vec<Vec<u8>>,
635 blocks: Vec<BlockOrder>,
637}
638
639struct Spill {
641 files: TempFileGuard,
643 cells_per_bucket: usize,
645 segments: Vec<Vec<(usize, u64, u64)>>,
647 cell_major: bool,
649 quality: CellOnFileQuality,
651}
652
653#[derive(Debug, Clone)]
661pub struct MtxFinalData {
662 pub cell_qc: CellQuality,
665 pub no_genes: usize,
667 pub no_cells: usize,
669}
670
671pub struct MtxReader {
677 source: MtxSource,
679 header: MtxHeader,
681 qc_params: MinCellQuality,
683 cells_as_rows: bool,
685 temp_dir: PathBuf,
687 spill: Option<Spill>,
689}
690
691impl MtxReader {
692 pub fn new<P: AsRef<Path>>(
706 path: P,
707 qc_params: MinCellQuality,
708 cells_as_rows: bool,
709 ) -> Result<Self, ScDataErrors> {
710 let source = MtxSource::open(path.as_ref(), cells_as_rows)?;
711 let [rows, cols, entries] = source.shape;
712 let (total_cells, total_genes) = if cells_as_rows {
713 (rows, cols)
714 } else {
715 (cols, rows)
716 };
717 let header = MtxHeader {
718 total_cells,
719 total_genes,
720 total_entries: entries,
721 field: source.field,
722 };
723 Ok(Self {
724 source,
725 header,
726 qc_params,
727 cells_as_rows,
728 temp_dir: std::env::temp_dir(),
729 spill: None,
730 })
731 }
732
733 pub fn with_temp_dir<P: AsRef<Path>>(mut self, dir: P) -> Self {
743 self.temp_dir = dir.as_ref().to_path_buf();
744 self
745 }
746
747 pub fn parse_mtx_quality(&mut self, verbose: bool) -> Result<CellOnFileQuality, ScDataErrors> {
762 if self.spill.is_none() {
763 self.spill = Some(self.text_pass(verbose)?);
764 }
765 Ok(self
766 .spill
767 .as_ref()
768 .expect("spill set above")
769 .quality
770 .clone())
771 }
772
773 fn text_pass(&self, verbose: bool) -> Result<Spill, ScDataErrors> {
783 let start = Instant::now();
784 let n_cells = self.header.total_cells;
785 let n_genes = self.header.total_genes;
786 let min_cells = self.qc_params.min_cells;
787 let cells_as_rows = self.cells_as_rows;
788 let field = self.source.field;
789
790 let n_buckets = n_cells.div_ceil(CELLS_PER_BUCKET).clamp(1, MAX_BUCKETS);
791 let cells_per_bucket = n_cells.div_ceil(n_buckets).max(1);
792
793 let tag = SPILL_COUNTER.fetch_add(1, Ordering::Relaxed);
794 let paths: Vec<PathBuf> = (0..n_buckets)
795 .map(|i| {
796 self.temp_dir.join(format!(
797 ".bixverse_mtx_{}_{tag}.bucket_{i:04}.tmp",
798 std::process::id()
799 ))
800 })
801 .collect();
802 let files = TempFileGuard(paths.clone());
803 let buckets: Vec<Mutex<BucketFile>> = paths
804 .iter()
805 .map(|p| {
806 File::create(p).map(|f| {
807 Mutex::new(BucketFile {
808 writer: BufWriter::with_capacity(256 * 1024, f),
809 len: 0,
810 segments: Vec::new(),
811 })
812 })
813 })
814 .collect::<std::io::Result<_>>()?;
815
816 if verbose {
817 let kind = match field {
818 MtxField::Integer => "integer",
819 MtxField::Real => "real",
820 MtxField::Pattern => "pattern",
821 };
822 println!(
823 "Text pass ({}{kind}): {n_buckets} buckets, ~{cells_per_bucket} cells/bucket",
824 if self.source.gz { "gzip, " } else { "" },
825 );
826 }
827
828 let flush = |bucket: usize, seq: usize, buf: &mut Vec<u8>| -> Result<(), ScDataErrors> {
829 let mut b = buckets[bucket]
830 .lock()
831 .expect("bucket writer lock is never held across a panic");
832 let len = buf.len() as u64;
833 let offset = b.len;
834 b.writer.write_all(buf)?;
835 b.segments.push((seq, offset, len));
836 b.len += len;
837 buf.clear();
838 Ok(())
839 };
840
841 let accs = self.source.par_fold_blocks(
842 || TextPassAcc {
843 gene_nnz: vec![0; n_genes],
844 rare: vec![Vec::new(); n_genes],
845 cell_stats: vec![(0, 0); n_cells],
846 bufs: vec![Vec::new(); n_buckets],
847 blocks: Vec::new(),
848 },
849 |acc, seq, bytes| {
850 let mut order: Option<BlockOrder> = None;
851 let mut flush_err = Ok(());
852 parse_block(bytes, field, |row, col, value| {
853 let (cell, gene) = if cells_as_rows {
854 (row, col)
855 } else {
856 (col, row)
857 };
858 let Some(g) = (gene as usize).checked_sub(1).filter(|&g| g < n_genes) else {
859 return;
860 };
861 acc.gene_nnz[g] += 1;
862 let Some(c) = (cell as usize).checked_sub(1).filter(|&c| c < n_cells) else {
863 return;
864 };
865 let seen = acc.gene_nnz[g] as usize;
866 if seen < min_cells {
867 acc.rare[g].push((c as u32, value));
868 } else if seen == min_cells {
869 acc.rare[g] = Vec::new();
870 }
871 let stats = &mut acc.cell_stats[c];
872 stats.0 += 1;
873 stats.1 += value;
874
875 let c32 = c as u32;
876 match &mut order {
877 None => {
878 order = Some(BlockOrder {
879 seq,
880 first: c32,
881 last: c32,
882 sorted: true,
883 })
884 }
885 Some(o) => {
886 o.sorted &= c32 >= o.last;
887 o.last = c32;
888 }
889 }
890
891 let bucket = c / cells_per_bucket;
892 let buf = &mut acc.bufs[bucket];
893 buf.extend_from_slice(&c32.to_le_bytes());
894 buf.extend_from_slice(&(g as u32).to_le_bytes());
895 buf.extend_from_slice(&value.to_le_bytes());
896 if buf.len() >= BUCKET_FLUSH_BYTES && flush_err.is_ok() {
897 flush_err = flush(bucket, seq, buf);
898 }
899 })?;
900 flush_err?;
901 for (bucket, buf) in acc.bufs.iter_mut().enumerate() {
904 if !buf.is_empty() {
905 flush(bucket, seq, buf)?;
906 }
907 }
908 acc.blocks.extend(order);
909 Ok(())
910 },
911 )?;
912
913 let mut segments = Vec::with_capacity(n_buckets);
914 for bucket in buckets {
915 let mut b = bucket
916 .into_inner()
917 .expect("bucket writer lock is never held across a panic");
918 b.writer.flush()?;
919 segments.push(b.segments);
920 }
921
922 let mut gene_nnz = vec![0usize; n_genes];
924 let mut cell_stats = vec![(0u32, 0u32); n_cells];
925 let mut blocks = Vec::new();
926 for acc in &accs {
927 gene_nnz
928 .iter_mut()
929 .zip(&acc.gene_nnz)
930 .for_each(|(t, &n)| *t += n as usize);
931 cell_stats
932 .par_iter_mut()
933 .zip(&acc.cell_stats)
934 .for_each(|(t, s)| {
935 t.0 += s.0;
936 t.1 += s.1;
937 });
938 }
939 let genes_to_keep: Vec<usize> =
940 (0..n_genes).filter(|&g| gene_nnz[g] >= min_cells).collect();
941 for mut acc in accs {
942 for (g, rare) in acc.rare.iter().enumerate() {
943 if gene_nnz[g] < min_cells {
944 for &(c, v) in rare {
945 let s = &mut cell_stats[c as usize];
946 s.0 -= 1;
947 s.1 -= v;
948 }
949 }
950 }
951 blocks.append(&mut acc.blocks);
952 }
953
954 blocks.sort_unstable_by_key(|b| b.seq);
955 let cell_major =
956 blocks.iter().all(|b| b.sorted) && blocks.windows(2).all(|w| w[0].last <= w[1].first);
957
958 let cells_to_keep: Vec<usize> = (0..n_cells)
959 .filter(|&c| {
960 cell_stats[c].0 as usize >= self.qc_params.min_unique_genes
961 && cell_stats[c].1 as f32 >= self.qc_params.min_lib_size as f32
962 })
963 .collect();
964
965 let mut quality = CellOnFileQuality::new(cells_to_keep, genes_to_keep);
966 quality.generate_maps_sets();
967
968 if verbose {
969 println!(
970 "Text pass done: {:.2?} ({} order)",
971 start.elapsed(),
972 if cell_major { "cell-major" } else { "unsorted" }
973 );
974 println!(
975 "Genes passing QC: {} / {}",
976 quality.genes_to_keep.len().separate_with_underscores(),
977 n_genes.separate_with_underscores()
978 );
979 println!(
980 "Cells passing QC: {} / {}",
981 quality.cells_to_keep.len().separate_with_underscores(),
982 n_cells.separate_with_underscores()
983 );
984 }
985
986 Ok(Spill {
987 files,
988 cells_per_bucket,
989 segments,
990 cell_major,
991 quality,
992 })
993 }
994
995 pub fn process_mtx_and_write_bin(
1013 self,
1014 bin_path: &str,
1015 quality: &CellOnFileQuality,
1016 verbose: bool,
1017 ) -> Result<MtxFinalData, ScDataErrors> {
1018 self.process_mtx_and_write_bin_streaming(bin_path, quality, verbose)
1019 }
1020
1021 pub fn process_mtx_and_write_bin_streaming(
1043 mut self,
1044 bin_path: &str,
1045 quality: &CellOnFileQuality,
1046 verbose: bool,
1047 ) -> Result<MtxFinalData, ScDataErrors> {
1048 let kept = &quality.cells_to_keep;
1049 if kept.windows(2).any(|w| w[0] >= w[1]) {
1050 return Err(ScDataErrors::InvalidArgument(
1051 "cells_to_keep must be strictly ascending".into(),
1052 ));
1053 }
1054 let n_kept_cells = kept.len();
1055 let n_kept_genes = quality.genes_to_keep.len();
1056 let target_size = self.qc_params.target_size;
1057
1058 if n_kept_cells == 0 {
1059 let writer = CellGeneSparseWriter::new(bin_path, true, 0, n_kept_genes, target_size)?;
1060 writer.finalise()?;
1061 return Ok(MtxFinalData {
1062 cell_qc: CellQuality {
1063 cell_indices: vec![],
1064 gene_indices: quality.genes_to_keep.to_vec(),
1065 lib_size: vec![],
1066 nnz: vec![],
1067 },
1068 no_genes: n_kept_genes,
1069 no_cells: 0,
1070 });
1071 }
1072
1073 let spill = match self.spill.take() {
1074 Some(s) => s,
1075 None => self.text_pass(verbose)?,
1076 };
1077
1078 let start = Instant::now();
1079 if verbose {
1080 println!(
1081 "Writing pass: {} buckets{}",
1082 spill.files.0.len(),
1083 if spill.cell_major { ", no sort" } else { "" }
1084 );
1085 }
1086
1087 let cell_map = dense_index_map(kept);
1088 let gene_map = dense_index_map(&quality.genes_to_keep);
1089 let lookup = |map: &[u32], i: u32| -> Option<u32> {
1090 map.get(i as usize).copied().filter(|&m| m != INDEX_DROPPED)
1091 };
1092
1093 let mut writer =
1094 CellGeneSparseWriter::new(bin_path, true, n_kept_cells, n_kept_genes, target_size)?;
1095 let mut lib_size = Vec::with_capacity(n_kept_cells);
1096 let mut nnz = Vec::with_capacity(n_kept_cells);
1097 let n_buckets = spill.files.0.len();
1098
1099 for (bucket_idx, temp_path) in spill.files.0.iter().enumerate() {
1100 let raw = std::fs::read(temp_path)?;
1101 let bytes = if spill.cell_major {
1102 let mut segs = spill.segments[bucket_idx].clone();
1104 segs.sort_unstable_by_key(|&(seq, off, _)| (seq, off));
1105 let mut ordered = Vec::with_capacity(raw.len());
1106 for (_, off, len) in segs {
1107 ordered.extend_from_slice(&raw[off as usize..(off + len) as usize]);
1108 }
1109 ordered
1110 } else {
1111 raw
1112 };
1113
1114 let mut entries: Vec<(u32, u32, u32)> = bytes
1115 .par_chunks_exact(BUCKET_RECORD_LEN)
1116 .filter_map(|r| {
1117 let word =
1118 |k: usize| u32::from_le_bytes(r[k..k + 4].try_into().expect("4 bytes"));
1119 Some((
1120 lookup(&cell_map, word(0))?,
1121 lookup(&gene_map, word(4))?,
1122 word(8),
1123 ))
1124 })
1125 .collect();
1126 drop(bytes);
1127 if !spill.cell_major {
1128 entries.par_sort_unstable();
1129 }
1130
1131 let first_cell = bucket_idx * spill.cells_per_bucket;
1132 let end_cell = first_cell + spill.cells_per_bucket;
1133 let k0 = kept.partition_point(|&c| c < first_cell);
1134 let k1 = kept.partition_point(|&c| c < end_cell);
1135 let run_starts: Vec<usize> = (k0..=k1)
1136 .map(|k| entries.partition_point(|e| (e.0 as usize) < k))
1137 .collect();
1138
1139 let built = (k0..k1)
1140 .into_par_iter()
1141 .map(|k| {
1142 let j = k - k0;
1143 let mut row: Vec<(u32, u32)> = entries[run_starts[j]..run_starts[j + 1]]
1144 .iter()
1145 .map(|&(_, g, v)| (g, v))
1146 .collect();
1147 compress_cell_row(&mut row, k, target_size, writer.format())
1148 })
1149 .collect::<Result<Vec<_>, ScDataErrors>>()?;
1150
1151 let mut payloads = Vec::with_capacity(built.len());
1152 for (nnz_i, payload) in built {
1153 nnz.push(nnz_i);
1154 lib_size.push(payload.library_size);
1155 payloads.push(payload);
1156 }
1157 writer.write_compressed_cell_chunks(&payloads)?;
1158
1159 let _ = std::fs::remove_file(temp_path);
1160
1161 if verbose
1162 && ((bucket_idx + 1) % (n_buckets / 10).max(1) == 0 || bucket_idx + 1 == n_buckets)
1163 {
1164 println!(
1165 " Wrote bucket {}/{} ({}%)",
1166 bucket_idx + 1,
1167 n_buckets,
1168 (bucket_idx + 1) * 100 / n_buckets
1169 );
1170 }
1171 }
1172
1173 writer.finalise()?;
1174
1175 if verbose {
1176 println!("Writing pass done: {:.2?}", start.elapsed());
1177 }
1178
1179 Ok(MtxFinalData {
1180 cell_qc: CellQuality {
1181 cell_indices: kept.to_vec(),
1182 gene_indices: quality.genes_to_keep.to_vec(),
1183 lib_size,
1184 nnz,
1185 },
1186 no_genes: n_kept_genes,
1187 no_cells: n_kept_cells,
1188 })
1189 }
1190}
1191
1192#[cfg(test)]
1197mod tests {
1198 use super::*;
1199 use crate::data_io::write_cell_rows;
1200
1201 const CELLS_AS_ROWS_MTX: &str = "%%MatrixMarket matrix coordinate integer general\n\
1215%\n\
12163 4 5\n\
12171 1 5\n\
12181 3 7\n\
12192 2 3\n\
12203 1 2\n\
12213 4 9\n";
1222
1223 const GENES_AS_ROWS_MTX: &str = "%%MatrixMarket matrix coordinate integer general\n\
1225%\n\
12264 3 5\n\
12271 1 5\n\
12283 1 7\n\
12292 2 3\n\
12301 3 2\n\
12314 3 9\n";
1232
1233 fn expected_cells() -> Vec<(Vec<u32>, Vec<u32>)> {
1235 vec![
1236 (vec![0, 2], vec![5, 7]),
1237 (vec![1], vec![3]),
1238 (vec![0, 3], vec![2, 9]),
1239 ]
1240 }
1241
1242 struct TempPath(PathBuf);
1244
1245 impl Drop for TempPath {
1248 fn drop(&mut self) {
1249 let _ = std::fs::remove_file(&self.0);
1250 }
1251 }
1252
1253 impl TempPath {
1254 fn new(name: &str, ext: &str) -> Self {
1258 Self(std::env::temp_dir().join(format!("bixverse_mtx_io_{name}.{ext}")))
1259 }
1260
1261 fn path(&self) -> &str {
1263 self.0.to_str().expect("temp path is valid UTF-8")
1264 }
1265 }
1266
1267 fn write_mtx(name: &str, body: &str) -> TempPath {
1269 let temp = TempPath::new(name, "mtx");
1270 std::fs::write(&temp.0, body).expect("mtx file written");
1271 temp
1272 }
1273
1274 fn keep_all_qc() -> MinCellQuality {
1276 MinCellQuality {
1277 min_unique_genes: 1,
1278 min_lib_size: 1,
1279 min_cells: 1,
1280 target_size: 1e4,
1281 }
1282 }
1283
1284 fn read_back(bin_path: &str, n_cells: usize) -> Vec<(Vec<u32>, Vec<u32>)> {
1287 let reader = ParallelSparseReader::new(bin_path).expect("reader opens");
1288 let indices: Vec<usize> = (0..n_cells).collect();
1289 reader
1290 .read_cells_parallel(&indices)
1291 .expect("cells read back")
1292 .into_iter()
1293 .map(|c| (c.indices.clone(), c.data_raw.iter().collect()))
1294 .collect()
1295 }
1296
1297 fn int(line: &[u8]) -> Option<(u32, u32, u32)> {
1303 parse_entry(line, MtxField::Integer).expect("no value error")
1304 }
1305
1306 #[test]
1308 fn test_parse_mtx_line_keeps_full_u32_counts() {
1309 assert_eq!(int(b"1 2 3"), Some((1, 2, 3)));
1310 assert_eq!(int(b"7\t9\t65535"), Some((7, 9, 65_535)));
1311 assert_eq!(int(b"4 5 70000"), Some((4, 5, 70_000)));
1312 assert_eq!(int(b"1 2 4294967295"), Some((1, 2, u32::MAX)));
1313 }
1314
1315 #[test]
1317 fn test_parse_mtx_line_rejects_malformed_input() {
1318 assert_eq!(int(b""), None);
1319 assert_eq!(int(b"abc"), None);
1320 assert_eq!(int(b"1"), None);
1321 assert_eq!(int(b"1 2"), None);
1322 }
1323
1324 #[test]
1331 fn mtx_header_shape_line_is_read_per_orientation() {
1332 let mtx = write_mtx("header_shape", CELLS_AS_ROWS_MTX);
1333
1334 let cells_as_rows = MtxReader::new(mtx.path(), keep_all_qc(), true).expect("reader opens");
1335 assert_eq!(cells_as_rows.header.total_cells, 3);
1336 assert_eq!(cells_as_rows.header.total_genes, 4);
1337 assert_eq!(cells_as_rows.header.total_entries, 5);
1338
1339 let genes_as_rows = MtxReader::new(mtx.path(), keep_all_qc(), false).expect("reader opens");
1341 assert_eq!(genes_as_rows.header.total_cells, 4);
1342 assert_eq!(genes_as_rows.header.total_genes, 3);
1343 assert_eq!(genes_as_rows.header.total_entries, 5);
1344 }
1345
1346 #[test]
1349 fn mtx_header_with_wrong_field_count_is_rejected() {
1350 let mtx = write_mtx(
1351 "header_two_fields",
1352 "%%MatrixMarket matrix coordinate integer general\n3 4\n1 1 5\n",
1353 );
1354
1355 assert!(matches!(
1356 MtxReader::new(mtx.path(), keep_all_qc(), true),
1357 Err(ScDataErrors::MtxHeaderInvalid(_))
1358 ));
1359 }
1360
1361 #[test]
1364 fn mtx_header_parse_error_names_the_failing_field() {
1365 let bad_second = write_mtx(
1366 "header_bad_second",
1367 "%%MatrixMarket matrix coordinate integer general\n3 x 5\n",
1368 );
1369 assert!(matches!(
1370 MtxReader::new(bad_second.path(), keep_all_qc(), true),
1371 Err(ScDataErrors::MtxParseError {
1372 field: "gene count"
1373 })
1374 ));
1375 assert!(matches!(
1377 MtxReader::new(bad_second.path(), keep_all_qc(), false),
1378 Err(ScDataErrors::MtxParseError {
1379 field: "cell count"
1380 })
1381 ));
1382
1383 let bad_third = write_mtx(
1384 "header_bad_third",
1385 "%%MatrixMarket matrix coordinate integer general\n3 4 x\n",
1386 );
1387 assert!(matches!(
1388 MtxReader::new(bad_third.path(), keep_all_qc(), true),
1389 Err(ScDataErrors::MtxParseError {
1390 field: "entry count"
1391 })
1392 ));
1393 }
1394
1395 #[test]
1397 fn mtx_reader_reports_a_missing_file() {
1398 let missing = std::env::temp_dir().join("bixverse_mtx_io_does_not_exist.mtx");
1399 assert!(matches!(
1400 MtxReader::new(&missing, keep_all_qc(), true),
1401 Err(ScDataErrors::BinaryIo(_))
1402 ));
1403 }
1404
1405 #[test]
1412 fn parse_mtx_quality_filters_genes_then_cells() {
1413 let mtx = write_mtx("quality_filters", CELLS_AS_ROWS_MTX);
1414
1415 let mut reader = MtxReader::new(mtx.path(), keep_all_qc(), true).expect("reader opens");
1417 let quality = reader.parse_mtx_quality(false).expect("quality pass");
1418 assert_eq!(quality.cells_to_keep, vec![0, 1, 2]);
1419 assert_eq!(quality.genes_to_keep, vec![0, 1, 2, 3]);
1420
1421 let qc = MinCellQuality {
1424 min_unique_genes: 1,
1425 min_lib_size: 1,
1426 min_cells: 2,
1427 target_size: 1e4,
1428 };
1429 let mut reader = MtxReader::new(mtx.path(), qc, true).expect("reader opens");
1430 let quality = reader.parse_mtx_quality(false).expect("quality pass");
1431 assert_eq!(quality.genes_to_keep, vec![0]);
1432 assert_eq!(quality.cells_to_keep, vec![0, 2]);
1433 assert_eq!(quality.cell_old_to_new[&0], 0);
1434 assert_eq!(quality.cell_old_to_new[&2], 1);
1435 assert_eq!(quality.gene_old_to_new[&0], 0);
1436 }
1437
1438 #[test]
1445 fn mtx_round_trip_preserves_counts_and_indices() {
1446 let mtx = write_mtx("round_trip", CELLS_AS_ROWS_MTX);
1447 let bin = TempPath::new("round_trip", "bin");
1448
1449 let mut reader = MtxReader::new(mtx.path(), keep_all_qc(), true).expect("reader opens");
1450 let quality = reader.parse_mtx_quality(false).expect("quality pass");
1451 let final_data = reader
1452 .process_mtx_and_write_bin(bin.path(), &quality, false)
1453 .expect("conversion");
1454
1455 assert_eq!(final_data.no_cells, 3);
1456 assert_eq!(final_data.no_genes, 4);
1457 assert_eq!(final_data.cell_qc.lib_size, vec![12, 3, 11]);
1459 assert_eq!(final_data.cell_qc.nnz, vec![2, 1, 2]);
1460
1461 assert_eq!(read_back(bin.path(), 3), expected_cells());
1462 }
1463
1464 #[test]
1467 fn mtx_indices_shift_from_base_one_to_base_zero() {
1468 let mtx = write_mtx(
1469 "base_one",
1470 "%%MatrixMarket matrix coordinate integer general\n2 3 2\n1 1 42\n2 3 7\n",
1471 );
1472 let bin = TempPath::new("base_one", "bin");
1473
1474 let mut reader = MtxReader::new(mtx.path(), keep_all_qc(), true).expect("reader opens");
1475 let quality = reader.parse_mtx_quality(false).expect("quality pass");
1476 reader
1477 .process_mtx_and_write_bin(bin.path(), &quality, false)
1478 .expect("conversion");
1479
1480 assert_eq!(quality.genes_to_keep, vec![0, 2]);
1483 assert_eq!(
1484 read_back(bin.path(), 2),
1485 vec![(vec![0], vec![42]), (vec![1], vec![7])]
1486 );
1487 }
1488
1489 #[test]
1492 fn mtx_gene_as_rows_matches_cell_as_rows() {
1493 let mtx = write_mtx("orientation", GENES_AS_ROWS_MTX);
1494 let bin = TempPath::new("orientation", "bin");
1495
1496 let mut reader = MtxReader::new(mtx.path(), keep_all_qc(), false).expect("reader opens");
1497 let quality = reader.parse_mtx_quality(false).expect("quality pass");
1498 let final_data = reader
1499 .process_mtx_and_write_bin(bin.path(), &quality, false)
1500 .expect("conversion");
1501
1502 assert_eq!(final_data.no_cells, 3);
1503 assert_eq!(final_data.no_genes, 4);
1504 assert_eq!(final_data.cell_qc.lib_size, vec![12, 3, 11]);
1505 assert_eq!(read_back(bin.path(), 3), expected_cells());
1506 }
1507
1508 #[test]
1511 fn mtx_streaming_matches_non_streaming() {
1512 let mtx = write_mtx("streaming", CELLS_AS_ROWS_MTX);
1513 let bin_plain = TempPath::new("streaming_plain", "bin");
1514 let bin_stream = TempPath::new("streaming_stream", "bin");
1515
1516 let mut reader = MtxReader::new(mtx.path(), keep_all_qc(), true).expect("reader opens");
1517 let quality = reader.parse_mtx_quality(false).expect("quality pass");
1518 let plain = reader
1519 .process_mtx_and_write_bin(bin_plain.path(), &quality, false)
1520 .expect("conversion");
1521
1522 let reader = MtxReader::new(mtx.path(), keep_all_qc(), true).expect("reader opens");
1523 let streamed = reader
1524 .process_mtx_and_write_bin_streaming(bin_stream.path(), &quality, false)
1525 .expect("streaming conversion");
1526
1527 assert_eq!(streamed.no_cells, plain.no_cells);
1528 assert_eq!(streamed.no_genes, plain.no_genes);
1529 assert_eq!(streamed.cell_qc.lib_size, plain.cell_qc.lib_size);
1530 assert_eq!(streamed.cell_qc.nnz, plain.cell_qc.nnz);
1531 assert_eq!(
1532 read_back(bin_stream.path(), 3),
1533 read_back(bin_plain.path(), 3)
1534 );
1535 }
1536
1537 #[derive(Clone, Copy, Debug)]
1543 enum EntryOrder {
1544 CellMajor,
1546 GeneMajor,
1548 Shuffled,
1550 }
1551
1552 fn random_counts(n_cells: usize, n_genes: usize, seed: u64) -> Vec<(u32, u32, u32)> {
1564 use rand::rngs::StdRng;
1565 use rand::{Rng, SeedableRng};
1566 let mut rng = StdRng::seed_from_u64(seed);
1567 let mut out = Vec::new();
1568 for c in 0..n_cells {
1569 let k = rng.random_range(0..40usize);
1570 let mut genes: Vec<u32> = (0..k)
1571 .map(|_| {
1572 let u: f64 = rng.random();
1575 ((u * u * u) * n_genes as f64) as u32
1576 })
1577 .collect();
1578 genes.sort_unstable();
1579 genes.dedup();
1580 for g in genes {
1581 let v = if rng.random_range(0..200) == 0 {
1582 70_000
1583 } else {
1584 rng.random_range(1..30)
1585 };
1586 out.push((c as u32, g, v));
1587 }
1588 }
1589 out
1590 }
1591
1592 fn render_mtx(
1606 entries: &[(u32, u32, u32)],
1607 n_cells: usize,
1608 n_genes: usize,
1609 cells_as_rows: bool,
1610 order: EntryOrder,
1611 ) -> String {
1612 use rand::SeedableRng;
1613 use rand::rngs::StdRng;
1614 use rand::seq::SliceRandom;
1615 let mut e = entries.to_vec();
1616 match order {
1617 EntryOrder::CellMajor => {}
1618 EntryOrder::GeneMajor => e.sort_by_key(|&(c, g, _)| (g, c)),
1619 EntryOrder::Shuffled => e.shuffle(&mut StdRng::seed_from_u64(7)),
1620 }
1621 let (rows, cols) = if cells_as_rows {
1622 (n_cells, n_genes)
1623 } else {
1624 (n_genes, n_cells)
1625 };
1626 let mut s = format!(
1627 "%%MatrixMarket matrix coordinate integer general\n%\n{rows} {cols} {}\n",
1628 e.len()
1629 );
1630 for (c, g, v) in e {
1631 let (r, k) = if cells_as_rows { (c, g) } else { (g, c) };
1632 s.push_str(&format!("{} {} {v}\n", r + 1, k + 1));
1633 }
1634 s
1635 }
1636
1637 fn reference_store(
1652 entries: &[(u32, u32, u32)],
1653 n_cells: usize,
1654 n_genes: usize,
1655 qc: &MinCellQuality,
1656 bin_path: &str,
1657 ) -> (Vec<usize>, Vec<usize>) {
1658 let mut gene_nnz = vec![0usize; n_genes];
1659 for &(_, g, _) in entries {
1660 gene_nnz[g as usize] += 1;
1661 }
1662 let genes: Vec<usize> = (0..n_genes)
1663 .filter(|&g| gene_nnz[g] >= qc.min_cells)
1664 .collect();
1665 let gene_map = dense_index_map(&genes);
1666
1667 let mut stats = vec![(0usize, 0u64); n_cells];
1668 for &(c, g, v) in entries {
1669 if gene_map
1670 .get(g as usize)
1671 .is_some_and(|&m| m != INDEX_DROPPED)
1672 {
1673 stats[c as usize].0 += 1;
1674 stats[c as usize].1 += v as u64;
1675 }
1676 }
1677 let cells: Vec<usize> = (0..n_cells)
1678 .filter(|&c| {
1679 stats[c].0 >= qc.min_unique_genes && stats[c].1 as f32 >= qc.min_lib_size as f32
1680 })
1681 .collect();
1682 let cell_map = dense_index_map(&cells);
1683
1684 let mut rows: Vec<Vec<(u32, u32)>> = vec![Vec::new(); cells.len()];
1685 for &(c, g, v) in entries {
1686 let (Some(&nc), Some(&ng)) = (cell_map.get(c as usize), gene_map.get(g as usize))
1687 else {
1688 continue;
1689 };
1690 if nc != INDEX_DROPPED && ng != INDEX_DROPPED {
1691 rows[nc as usize].push((ng, v));
1692 }
1693 }
1694 let mut writer =
1695 CellGeneSparseWriter::new(bin_path, true, cells.len(), genes.len(), qc.target_size)
1696 .expect("writer");
1697 write_cell_rows(&mut rows, 0, qc.target_size, &mut writer).expect("rows written");
1698 writer.finalise().expect("finalised");
1699 (cells, genes)
1700 }
1701
1702 fn dropping_qc() -> MinCellQuality {
1704 MinCellQuality {
1705 min_unique_genes: 8,
1706 min_lib_size: 60,
1707 min_cells: 3,
1708 target_size: 1e4,
1709 }
1710 }
1711
1712 #[test]
1715 fn mtx_store_matches_reference_for_every_layout() {
1716 let (n_cells, n_genes) = (400, 1_500);
1717 let entries = random_counts(n_cells, n_genes, 42);
1718 let qc = dropping_qc();
1719 let reference = TempPath::new("reference_store", "bin");
1720 let (cells, genes) = reference_store(&entries, n_cells, n_genes, &qc, reference.path());
1721 assert!(
1722 cells.len() < n_cells && genes.len() < n_genes,
1723 "QC drops both"
1724 );
1725 let expected = std::fs::read(&reference.0).expect("reference store");
1726
1727 for cells_as_rows in [true, false] {
1728 for order in [
1729 EntryOrder::CellMajor,
1730 EntryOrder::GeneMajor,
1731 EntryOrder::Shuffled,
1732 ] {
1733 let body = render_mtx(&entries, n_cells, n_genes, cells_as_rows, order);
1734 for (gz, block_bytes) in [(false, MTX_BLOCK_BYTES), (false, 101), (true, 101)] {
1737 let name = format!("layout_{cells_as_rows}_{order:?}_{gz}_{block_bytes}");
1738 let mtx = if gz {
1739 write_gz(&name, &body, 2)
1740 } else {
1741 write_mtx(&name, &body)
1742 };
1743 let bin = TempPath::new(&name, "bin");
1744
1745 let mut reader = MtxReader::new(mtx.path(), qc.clone(), cells_as_rows)
1746 .expect("reader opens");
1747 reader.source.block_bytes = block_bytes;
1748 let quality = reader.parse_mtx_quality(false).expect("quality pass");
1749 assert_eq!(quality.cells_to_keep, cells, "{name}: kept cells");
1750 assert_eq!(quality.genes_to_keep, genes, "{name}: kept genes");
1751 let spill = reader.spill.as_ref().expect("spilled");
1752 assert_eq!(
1753 spill.cell_major,
1754 matches!(order, EntryOrder::CellMajor),
1755 "{name}: order detection"
1756 );
1757 reader
1758 .process_mtx_and_write_bin_streaming(bin.path(), &quality, false)
1759 .expect("conversion");
1760 let got = std::fs::read(&bin.0).expect("store");
1761 assert!(got == expected, "{name}: store differs from the reference");
1762 }
1763 }
1764 }
1765 }
1766
1767 fn write_gz(name: &str, body: &str, members: usize) -> TempPath {
1774 use flate2::Compression;
1775 use flate2::write::GzEncoder;
1776 let temp = TempPath::new(name, "mtx.gz");
1777 let bytes = body.as_bytes();
1778 let step = bytes.len().div_ceil(members.max(1)).max(1);
1779 let mut out = Vec::new();
1780 for part in bytes.chunks(step) {
1781 let mut enc = GzEncoder::new(Vec::new(), Compression::fast());
1782 enc.write_all(part).expect("gzip member");
1783 out.extend(enc.finish().expect("gzip member"));
1784 }
1785 std::fs::write(&temp.0, out).expect("gz file written");
1786 temp
1787 }
1788
1789 #[test]
1792 fn real_values_must_be_integral_counts() {
1793 let real = |l: &[u8]| parse_entry(l, MtxField::Real);
1794 assert_eq!(real(b"1 2 3").unwrap(), Some((1, 2, 3)));
1795 assert_eq!(real(b"1 2 3.0").unwrap(), Some((1, 2, 3)));
1796 assert_eq!(real(b"1 2 1.5e+01").unwrap(), Some((1, 2, 15)));
1797 assert_eq!(real(b"1 2 2.000000e+00").unwrap(), Some((1, 2, 2)));
1798 assert_eq!(real(b"1 2 0").unwrap(), Some((1, 2, 0)));
1799 for bad in [
1800 &b"1 2 1.5"[..],
1801 b"1 2 -1",
1802 b"1 2 abc",
1803 b"1 2 1e300",
1804 b"1 2 nan",
1805 ] {
1806 assert!(
1807 matches!(real(bad), Err(ScDataErrors::MtxNotACount { .. })),
1808 "{}",
1809 String::from_utf8_lossy(bad)
1810 );
1811 }
1812 assert!(matches!(
1814 parse_entry(b"1 2 1.5", MtxField::Integer),
1815 Err(ScDataErrors::MtxNotACount { .. })
1816 ));
1817 }
1818
1819 #[test]
1821 fn pattern_entries_count_one() {
1822 assert_eq!(
1823 parse_entry(b"3 4", MtxField::Pattern).unwrap(),
1824 Some((3, 4, 1))
1825 );
1826 assert_eq!(
1827 parse_entry(b"3 4 9", MtxField::Pattern).unwrap(),
1828 Some((3, 4, 1))
1829 );
1830 assert_eq!(parse_entry(b"3", MtxField::Pattern).unwrap(), None);
1831 }
1832
1833 #[test]
1836 fn mtx_banner_field_and_symmetry() {
1837 let shape = "3 4 5\n1 1 5\n";
1838 let open = |name: &str, banner: &str| {
1839 let mtx = write_mtx(name, &format!("{banner}\n{shape}"));
1840 MtxReader::new(mtx.path(), keep_all_qc(), true).map(|r| r.header.field)
1841 };
1842 assert_eq!(
1843 open(
1844 "banner_int",
1845 "%%MatrixMarket matrix coordinate integer general"
1846 )
1847 .unwrap(),
1848 MtxField::Integer
1849 );
1850 assert_eq!(
1851 open(
1852 "banner_real",
1853 "%%MatrixMarket Matrix Coordinate Real General"
1854 )
1855 .unwrap(),
1856 MtxField::Real
1857 );
1858 assert_eq!(
1859 open(
1860 "banner_pattern",
1861 "%%MatrixMarket matrix coordinate pattern general"
1862 )
1863 .unwrap(),
1864 MtxField::Pattern
1865 );
1866 assert_eq!(
1867 open("banner_none", "% no banner").unwrap(),
1868 MtxField::Integer
1869 );
1870 for (name, banner) in [
1871 (
1872 "banner_complex",
1873 "%%MatrixMarket matrix coordinate complex general",
1874 ),
1875 (
1876 "banner_sym",
1877 "%%MatrixMarket matrix coordinate integer symmetric",
1878 ),
1879 (
1880 "banner_array",
1881 "%%MatrixMarket matrix array integer general",
1882 ),
1883 ] {
1884 assert!(
1885 matches!(open(name, banner), Err(ScDataErrors::MtxUnsupported(_))),
1886 "{name}"
1887 );
1888 }
1889 }
1890
1891 #[test]
1894 fn mtx_real_and_pattern_files_round_trip() {
1895 let real = write_mtx(
1896 "real_file",
1897 "%%MatrixMarket matrix coordinate real general\n2 3 3\n1 1 5.0\n1 3 1.2e+01\n2 2 3\n",
1898 );
1899 let bin = TempPath::new("real_file", "bin");
1900 let mut reader = MtxReader::new(real.path(), keep_all_qc(), true).expect("reader opens");
1901 let quality = reader.parse_mtx_quality(false).expect("quality pass");
1902 let data = reader
1903 .process_mtx_and_write_bin_streaming(bin.path(), &quality, false)
1904 .expect("conversion");
1905 assert_eq!(data.cell_qc.lib_size, vec![17, 3]);
1906 assert_eq!(
1907 read_back(bin.path(), 2),
1908 vec![(vec![0, 2], vec![5, 12]), (vec![1], vec![3])]
1909 );
1910
1911 let pattern = write_mtx(
1912 "pattern_file",
1913 "%%MatrixMarket matrix coordinate pattern general\n2 3 3\n1 1\n1 3\n2 2\n",
1914 );
1915 let bin = TempPath::new("pattern_file", "bin");
1916 let mut reader = MtxReader::new(pattern.path(), keep_all_qc(), true).expect("reader opens");
1917 let quality = reader.parse_mtx_quality(false).expect("quality pass");
1918 reader
1919 .process_mtx_and_write_bin_streaming(bin.path(), &quality, false)
1920 .expect("conversion");
1921 assert_eq!(
1922 read_back(bin.path(), 2),
1923 vec![(vec![0, 2], vec![1, 1]), (vec![1], vec![1])]
1924 );
1925
1926 let fraction = write_mtx(
1927 "real_fraction",
1928 "%%MatrixMarket matrix coordinate real general\n2 3 2\n1 1 5.0\n2 2 0.25\n",
1929 );
1930 let mut reader =
1931 MtxReader::new(fraction.path(), keep_all_qc(), true).expect("reader opens");
1932 assert!(matches!(
1933 reader.parse_mtx_quality(false),
1934 Err(ScDataErrors::MtxNotACount { .. })
1935 ));
1936 }
1937
1938 #[test]
1940 fn mtx_gzip_is_detected_by_magic_bytes() {
1941 let gz = write_gz("gz_magic", CELLS_AS_ROWS_MTX, 1);
1942 let renamed = TempPath::new("gz_magic_renamed", "mtx");
1943 std::fs::copy(&gz.0, &renamed.0).expect("copy");
1944 let bin = TempPath::new("gz_magic", "bin");
1945 let mut reader = MtxReader::new(renamed.path(), keep_all_qc(), true).expect("reader opens");
1946 assert!(reader.source.gz);
1947 let quality = reader.parse_mtx_quality(false).expect("quality pass");
1948 reader
1949 .process_mtx_and_write_bin_streaming(bin.path(), &quality, false)
1950 .expect("conversion");
1951 assert_eq!(read_back(bin.path(), 3), expected_cells());
1952 }
1953
1954 #[test]
1957 fn mtx_streaming_writes_a_readable_store_when_nothing_is_kept() {
1958 let bin = TempPath::new("streaming_empty", "bin");
1959 let mtx = write_mtx("streaming_empty", CELLS_AS_ROWS_MTX);
1960
1961 let reader = MtxReader::new(mtx.path(), keep_all_qc(), true).expect("reader opens");
1962 let mut quality = CellOnFileQuality::new(vec![], vec![0, 1, 2, 3]);
1963 quality.generate_maps_sets();
1964
1965 let final_data = reader
1966 .process_mtx_and_write_bin_streaming(bin.path(), &quality, false)
1967 .expect("streaming conversion");
1968
1969 assert_eq!(final_data.no_cells, 0);
1970 assert_eq!(final_data.no_genes, 4);
1971 assert!(final_data.cell_qc.lib_size.is_empty());
1972
1973 let reader = ParallelSparseReader::new(bin.path()).expect("reader opens");
1974 assert!(reader.is_cell_based());
1975 assert_eq!(reader.get_header().total_cells, 0);
1976 }
1977
1978 #[test]
1981 fn mtx_kept_cell_without_entries_is_written_empty() {
1982 for streaming in [false, true] {
1983 let name = format!("empty_cell_{streaming}");
1984 let mtx = write_mtx(&name, CELLS_AS_ROWS_MTX);
1985 let bin = TempPath::new(&name, "bin");
1986
1987 let reader = MtxReader::new(mtx.path(), keep_all_qc(), true).expect("reader opens");
1989 let mut quality = CellOnFileQuality::new(vec![0, 1, 2], vec![0]);
1990 quality.generate_maps_sets();
1991
1992 let final_data = if streaming {
1993 reader.process_mtx_and_write_bin_streaming(bin.path(), &quality, false)
1994 } else {
1995 reader.process_mtx_and_write_bin(bin.path(), &quality, false)
1996 }
1997 .expect("conversion");
1998
1999 assert_eq!(final_data.cell_qc.cell_indices.len(), 3);
2000 assert_eq!(final_data.cell_qc.lib_size, vec![5, 0, 2]);
2001 assert_eq!(final_data.cell_qc.nnz, vec![1, 0, 1]);
2002
2003 let store = ParallelSparseReader::new(bin.path()).expect("reader opens");
2004 let cell = store
2005 .read_cells_parallel(&[1])
2006 .expect("empty cell is on disk");
2007 assert!(cell[0].indices.is_empty());
2008 }
2009 }
2010}