1use lz4_flex::block::{compress as lz4_compress, decompress_into as lz4_decompress_into};
35use rayon::prelude::*;
36use std::time::Instant;
37use thousands::Separable;
38
39use crate::data_io::*;
40use crate::prelude::*;
41
42pub(crate) const V4_HEADER_LEN: usize = 32;
48
49const PACK_BLOCK: usize = 128;
53
54const FLAG_NORM_STORED: u8 = 1;
56
57const FLAG_RAW_MINUS_ONE: u8 = 2;
60
61const NORM_LUT_MAX: usize = 255;
64
65pub(crate) const GENE_NORM_LUT_COUNTS: usize = 4;
70
71const ZSTD_LEVEL: i32 = 1;
75
76const CONVERT_CELL_BATCH: usize = 4_096;
79
80const CONVERT_GENE_BATCH: usize = 64;
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum ChunkCodec {
94 None,
96 Lz4,
98 Zstd,
100}
101
102impl ChunkCodec {
103 fn to_u8(self) -> u8 {
109 match self {
110 ChunkCodec::None => 0,
111 ChunkCodec::Lz4 => 1,
112 ChunkCodec::Zstd => 2,
113 }
114 }
115
116 fn from_u8(b: u8) -> Result<Self, ScDataErrors> {
126 match b {
127 0 => Ok(ChunkCodec::None),
128 1 => Ok(ChunkCodec::Lz4),
129 2 => Ok(ChunkCodec::Zstd),
130 other => Err(ScDataErrors::V4ChunkCorrupt(format!(
131 "unknown codec byte {other}"
132 ))),
133 }
134 }
135}
136
137fn pack(vals: &[u32], out: &mut Vec<u8>) {
149 for block in vals.chunks(PACK_BLOCK) {
150 let max = block.iter().copied().max().unwrap_or(0);
151 let width = (32 - max.leading_zeros()) as usize;
152 out.push(width as u8);
153 let (mut acc, mut nbits) = (0u64, 0usize);
154 for &v in block {
155 acc |= (v as u64) << nbits;
156 nbits += width;
157 while nbits >= 8 {
158 out.push(acc as u8);
159 acc >>= 8;
160 nbits -= 8;
161 }
162 }
163 if nbits > 0 {
164 out.push(acc as u8);
165 }
166 }
167}
168
169#[inline(always)]
180fn read_word(b: &[u8], at: usize) -> u64 {
181 match b.get(at..at + 8) {
182 Some(s) => u64::from_le_bytes(s.try_into().expect("8-byte slice")),
183 None => {
184 let mut tmp = [0u8; 8];
185 let rest = &b[at..];
186 tmp[..rest.len()].copy_from_slice(rest);
187 u64::from_le_bytes(tmp)
188 }
189 }
190}
191
192#[inline(always)]
205fn unpack_full<const W: usize>(src: &[u8], out: &mut [u32; PACK_BLOCK]) {
206 let mask = if W >= 32 { u32::MAX } else { (1u32 << W) - 1 };
207 for g in 0..PACK_BLOCK / 8 {
208 for j in 0..8 {
209 let bit = j * W;
210 let at = g * W + (bit >> 3);
211 let word = u64::from_le_bytes(src[at..at + 8].try_into().expect("8-byte slice"));
212 out[g * 8 + j] = ((word >> (bit & 7)) as u32) & mask;
213 }
214 }
215}
216
217#[inline(always)]
225fn unpack_full_dispatch(width: usize, src: &[u8], out: &mut [u32; PACK_BLOCK]) {
226 macro_rules! by_width {
227 ($($w:literal),*) => {
228 match width {
229 $($w => unpack_full::<$w>(src, out),)*
230 _ => unreachable!("width checked to be 1..=32"),
231 }
232 };
233 }
234 by_width!(
235 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
236 26, 27, 28, 29, 30, 31, 32
237 );
238}
239
240fn unpack(buf: &[u8], n: usize, out: &mut Vec<u32>) -> Result<usize, ScDataErrors> {
257 out.reserve(n);
258 let mut pos = 0usize;
259 let mut left = n;
260 while left > 0 {
261 let take = left.min(PACK_BLOCK);
262 let width = *buf.get(pos).ok_or_else(truncated)? as usize;
263 if width > 32 {
264 return Err(ScDataErrors::V4ChunkCorrupt(format!(
265 "bit width {width} above 32"
266 )));
267 }
268 pos += 1;
269 let need = (take * width).div_ceil(8);
270 let block = buf.get(pos..pos + need).ok_or_else(truncated)?;
271
272 if width == 0 {
273 out.resize(out.len() + take, 0);
274 } else if take == PACK_BLOCK && buf.len() >= pos + need + 8 {
275 let at = out.len();
276 out.resize(at + PACK_BLOCK, 0);
277 let dst: &mut [u32; PACK_BLOCK] = (&mut out[at..at + PACK_BLOCK])
278 .try_into()
279 .expect("128-value slice");
280 unpack_full_dispatch(width, &buf[pos..], dst);
281 } else {
282 let mask = if width == 32 {
283 u32::MAX
284 } else {
285 (1u32 << width) - 1
286 };
287 for k in 0..take {
288 let bit = k * width;
289 out.push(((read_word(block, bit >> 3) >> (bit & 7)) as u32) & mask);
290 }
291 }
292 pos += need;
293 left -= take;
294 }
295 Ok(pos)
296}
297
298fn packed_len(buf: &[u8], n: usize) -> Result<usize, ScDataErrors> {
310 let mut pos = 0usize;
311 let mut left = n;
312 while left > 0 {
313 let take = left.min(PACK_BLOCK);
314 let width = *buf.get(pos).ok_or_else(truncated)? as usize;
315 if width > 32 {
316 return Err(ScDataErrors::V4ChunkCorrupt(format!(
317 "bit width {width} above 32"
318 )));
319 }
320 pos += 1 + (take * width).div_ceil(8);
321 left -= take;
322 }
323 if pos > buf.len() {
324 return Err(truncated());
325 }
326 Ok(pos)
327}
328
329fn truncated() -> ScDataErrors {
335 ScDataErrors::V4ChunkCorrupt("body ends early".to_string())
336}
337
338#[derive(Clone, Copy, Debug)]
344pub(crate) struct V4Header {
345 pub nnz: usize,
347 body_len: usize,
349 pub aux: u64,
352 pub original_index: usize,
354 pub to_keep: bool,
356 flags: u8,
358 codec: ChunkCodec,
360}
361
362impl V4Header {
363 pub(crate) fn parse(chunk: &[u8]) -> Result<Self, ScDataErrors> {
374 if chunk.len() < V4_HEADER_LEN {
375 return Err(ScDataErrors::ChunkBufferTooSmall {
376 expected: V4_HEADER_LEN,
377 found: chunk.len(),
378 });
379 }
380 let u32_at = |at: usize| {
381 u32::from_le_bytes(chunk[at..at + 4].try_into().expect("4-byte slice")) as usize
382 };
383 let u64_at =
384 |at: usize| u64::from_le_bytes(chunk[at..at + 8].try_into().expect("8-byte slice"));
385 Ok(Self {
386 nnz: u32_at(0),
387 body_len: u32_at(4),
388 aux: u64_at(8),
389 original_index: u64_at(16) as usize,
390 to_keep: chunk[24] != 0,
391 flags: chunk[25],
392 codec: ChunkCodec::from_u8(chunk[26])?,
393 })
394 }
395
396 pub(crate) fn norm_stored(&self) -> bool {
403 self.flags & FLAG_NORM_STORED != 0
404 }
405}
406
407struct EncodeIn<'a> {
413 indices: &'a [u32],
415 raw: &'a [u32],
417 norm: &'a [F16],
419 aux: u64,
421 original_index: usize,
423 to_keep: bool,
425 norm_derivable: bool,
427}
428
429fn encode_chunk(p: &EncodeIn, codec: ChunkCodec) -> Vec<u8> {
444 let n = p.indices.len();
445
446 let mut deltas = Vec::with_capacity(n);
447 let mut prev = 0u32;
448 for (k, &x) in p.indices.iter().enumerate() {
449 deltas.push(if k == 0 {
450 x
451 } else {
452 x.wrapping_sub(prev).wrapping_sub(1)
453 });
454 prev = x;
455 }
456
457 let mut flags = 0u8;
458 let mut body = Vec::with_capacity(n * 2);
459 pack(&deltas, &mut body);
460
461 if p.raw.iter().all(|&x| x >= 1) {
462 flags |= FLAG_RAW_MINUS_ONE;
463 let minus_one: Vec<u32> = p.raw.iter().map(|&x| x - 1).collect();
464 pack(&minus_one, &mut body);
465 } else {
466 pack(p.raw, &mut body);
467 }
468
469 if !p.norm_derivable {
470 flags |= FLAG_NORM_STORED;
471 for v in p.norm {
472 body.extend_from_slice(&v.to_le_bytes());
473 }
474 }
475
476 let (used, payload) = match codec {
477 ChunkCodec::None => (ChunkCodec::None, None),
478 ChunkCodec::Lz4 => (ChunkCodec::Lz4, Some(lz4_compress(&body))),
479 ChunkCodec::Zstd => (
480 ChunkCodec::Zstd,
481 zstd::bulk::compress(&body, ZSTD_LEVEL).ok(),
482 ),
483 };
484 let (used, payload) = match payload {
485 Some(c) if c.len() < body.len() => (used, c),
486 _ => (ChunkCodec::None, body.clone()),
487 };
488
489 let mut out = Vec::with_capacity(V4_HEADER_LEN + payload.len());
490 out.extend_from_slice(&(n as u32).to_le_bytes());
491 out.extend_from_slice(&(body.len() as u32).to_le_bytes());
492 out.extend_from_slice(&p.aux.to_le_bytes());
493 out.extend_from_slice(&(p.original_index as u64).to_le_bytes());
494 out.extend_from_slice(&[p.to_keep as u8, flags, used.to_u8(), 0, 0, 0, 0, 0]);
495 out.extend_from_slice(&payload);
496 out
497}
498
499impl CsrCellChunk {
500 pub fn to_v4_bytes(
516 &self,
517 target_size: f32,
518 codec: ChunkCodec,
519 ) -> Result<Vec<u8>, ScDataErrors> {
520 let n = self.indices.len();
521 if self.data_raw.len() != n || self.data_norm.len() != n {
522 return Err(ScDataErrors::V4ChunkCorrupt(format!(
523 "cell {} has layers of unequal length",
524 self.original_index
525 )));
526 }
527 let raw: Vec<u32> = self.data_raw.iter().collect();
528 let lib = self.library_size as f32;
529 let norm_derivable = target_size != 0.0
530 && raw
531 .iter()
532 .zip(&self.data_norm)
533 .all(|(&x, v)| norm_value(x as f32, lib, target_size).to_bits() == v.to_bits());
534
535 Ok(encode_chunk(
536 &EncodeIn {
537 indices: &self.indices,
538 raw: &raw,
539 norm: &self.data_norm,
540 aux: self.library_size as u64,
541 original_index: self.original_index,
542 to_keep: self.to_keep,
543 norm_derivable,
544 },
545 codec,
546 ))
547 }
548}
549
550impl CscGeneChunk {
551 pub fn to_v4_bytes(
569 &self,
570 cell_lib: &[f32],
571 target_size: f32,
572 codec: ChunkCodec,
573 ) -> Result<Vec<u8>, ScDataErrors> {
574 let n = self.indices.len();
575 if self.data_raw.len() != n || self.data_norm.len() != n {
576 return Err(ScDataErrors::V4ChunkCorrupt(format!(
577 "gene {} has layers of unequal length",
578 self.original_index
579 )));
580 }
581 let raw: Vec<u32> = self.data_raw.iter().collect();
582 let norm_derivable = target_size != 0.0
583 && self
584 .indices
585 .iter()
586 .zip(&raw)
587 .zip(&self.data_norm)
588 .all(|((&cell, &x), v)| {
589 cell_lib.get(cell as usize).is_some_and(|&lib| {
590 norm_value(x as f32, lib, target_size).to_bits() == v.to_bits()
591 })
592 });
593
594 Ok(encode_chunk(
595 &EncodeIn {
596 indices: &self.indices,
597 raw: &raw,
598 norm: &self.data_norm,
599 aux: self.avg_exp.to_bits() as u64,
600 original_index: self.original_index,
601 to_keep: self.to_keep,
602 norm_derivable,
603 },
604 codec,
605 ))
606 }
607}
608
609#[derive(Default)]
618pub(crate) struct V4Decoded {
619 pub bytes: Vec<u8>,
621 pub idx: Vec<u32>,
623 pub raw: Vec<u32>,
625 pub norm: Vec<f32>,
627}
628
629pub(crate) struct V4Ctx<'a> {
631 pub target_size: f32,
633 pub cell_lib: Option<&'a [f32]>,
635 pub cell_norm: Option<&'a [u16]>,
637 pub is_cell: bool,
639}
640
641pub(crate) fn decode_v4(
659 chunk: &[u8],
660 ctx: &V4Ctx,
661 want_raw: bool,
662 want_norm: bool,
663 s: &mut V4Decoded,
664) -> Result<V4Header, ScDataErrors> {
665 let V4Decoded {
666 bytes,
667 idx,
668 raw,
669 norm,
670 } = s;
671 idx.clear();
672 norm.clear();
673 decode_v4_into(
674 chunk,
675 ctx,
676 want_raw,
677 bytes,
678 raw,
679 idx,
680 want_norm.then_some(norm),
681 )
682}
683
684pub(crate) fn decode_v4_into(
706 chunk: &[u8],
707 ctx: &V4Ctx,
708 want_raw: bool,
709 bytes: &mut Vec<u8>,
710 raw: &mut Vec<u32>,
711 idx: &mut Vec<u32>,
712 norm: Option<&mut Vec<f32>>,
713) -> Result<V4Header, ScDataErrors> {
714 let h = V4Header::parse(chunk)?;
715
716 let stored = &chunk[V4_HEADER_LEN..];
717 let body: &[u8] = match h.codec {
718 ChunkCodec::None => stored,
719 ChunkCodec::Lz4 => {
720 bytes.resize(h.body_len, 0);
721 let n = lz4_decompress_into(stored, bytes)
722 .map_err(|e| ScDataErrors::V4ChunkCorrupt(format!("lz4: {e}")))?;
723 &bytes[..n]
724 }
725 ChunkCodec::Zstd => {
726 bytes.resize(h.body_len, 0);
727 let n = zstd::bulk::decompress_to_buffer(stored, &mut bytes[..])
728 .map_err(|e| ScDataErrors::V4ChunkCorrupt(format!("zstd: {e}")))?;
729 &bytes[..n]
730 }
731 };
732
733 let n = h.nnz;
734 let start = idx.len();
735 let used = unpack(body, n, idx)?;
736 let mut prev = 0u32;
737 for (k, v) in idx[start..].iter_mut().enumerate() {
738 if k > 0 {
739 *v = prev.wrapping_add(*v).wrapping_add(1);
740 }
741 prev = *v;
742 }
743
744 let norm_stored = h.flags & FLAG_NORM_STORED != 0;
745 let want_norm = norm.is_some();
746 let need_raw = want_raw || (want_norm && !norm_stored);
747 let rest = &body[used..];
748
749 let raw_len = if need_raw {
750 raw.clear();
751 let len = unpack(rest, n, raw)?;
752 if h.flags & FLAG_RAW_MINUS_ONE != 0 {
753 raw.iter_mut().for_each(|x| *x = x.wrapping_add(1));
754 }
755 len
756 } else if want_norm {
757 packed_len(rest, n)?
758 } else {
759 0
760 };
761
762 let Some(norm) = norm else {
763 return Ok(h);
764 };
765
766 norm.reserve(n);
767 if norm_stored {
768 let stored_norm = rest.get(raw_len..raw_len + n * 2).ok_or_else(truncated)?;
769 norm.extend(
770 stored_norm
771 .chunks_exact(2)
772 .map(|c| F16::from_le_bytes([c[0], c[1]]).to_f32()),
773 );
774 } else if ctx.is_cell {
775 let lib = h.aux as f32;
776 let top = raw.iter().copied().max().unwrap_or(0) as usize;
777 let lut: Vec<f32> = (0..=top.min(NORM_LUT_MAX))
778 .map(|x| norm_value(x as f32, lib, ctx.target_size).to_f32())
779 .collect();
780 norm.extend(raw.iter().map(|&x| match lut.get(x as usize) {
781 Some(&v) => v,
782 None => norm_value(x as f32, lib, ctx.target_size).to_f32(),
783 }));
784 } else {
785 let cell_lib = match ctx.cell_lib {
787 Some(table) => table,
788 None if n == 0 => &[],
789 None => return Err(ScDataErrors::V4MissingCellLibraryTable),
790 };
791 let mut outside = None;
794 let table = ctx.cell_norm.unwrap_or(&[]);
795 norm.extend(idx[start..].iter().zip(raw.iter()).map(|(&cell, &x)| {
796 let slot = (x as usize).wrapping_sub(1);
797 if slot < GENE_NORM_LUT_COUNTS
798 && let Some(&bits) = table.get(cell as usize * GENE_NORM_LUT_COUNTS + slot)
799 {
800 return F16::from_bits(bits).to_f32();
801 }
802 match cell_lib.get(cell as usize) {
803 Some(&lib) => norm_value(x as f32, lib, ctx.target_size).to_f32(),
804 None => {
805 outside = Some(cell);
806 0.0
807 }
808 }
809 }));
810 if let Some(cell) = outside {
811 return Err(ScDataErrors::V4ChunkCorrupt(format!(
812 "cell {cell} outside the library table"
813 )));
814 }
815 }
816
817 Ok(h)
818}
819
820pub(crate) fn gene_norm_table(cell_lib: &[f32], target_size: f32) -> Vec<u16> {
838 let mut table = vec![0u16; cell_lib.len() * GENE_NORM_LUT_COUNTS];
839 table
840 .par_chunks_mut(GENE_NORM_LUT_COUNTS)
841 .zip(cell_lib.par_iter())
842 .for_each(|(row, &lib)| {
843 for (k, v) in row.iter_mut().enumerate() {
844 *v = norm_value((k + 1) as f32, lib, target_size).to_bits();
845 }
846 });
847 table
848}
849
850pub(crate) struct V4Run<'a> {
852 idx: &'a [u32],
854 raw: &'a [u32],
856 norm: &'a [f32],
858}
859
860impl<'a> V4Run<'a> {
861 pub(crate) fn new(s: &'a V4Decoded) -> Self {
871 Self {
872 idx: &s.idx,
873 raw: &s.raw,
874 norm: &s.norm,
875 }
876 }
877}
878
879impl RunSource for V4Run<'_> {
880 #[inline]
881 fn nnz(&self) -> usize {
882 self.idx.len()
883 }
884
885 #[inline(always)]
886 fn index_at(&self, i: usize) -> u32 {
887 self.idx[i]
888 }
889
890 #[inline(always)]
891 fn raw_at(&self, i: usize) -> u32 {
892 self.raw[i]
893 }
894
895 #[inline(always)]
896 fn norm_at(&self, i: usize) -> f32 {
897 self.norm[i]
898 }
899
900 fn extend_indices(&self, out: &mut Vec<u32>) {
901 out.extend_from_slice(self.idx);
902 }
903
904 fn extend_raw<T: num_traits::FromPrimitive>(
905 &self,
906 out: &mut Vec<T>,
907 ) -> Result<(), ScDataErrors> {
908 extend_raw_checked(out, self.raw[..self.idx.len()].iter().copied())
909 }
910
911 fn extend_norm(&self, out: &mut Vec<f32>) {
912 out.extend_from_slice(&self.norm[..self.idx.len()]);
913 }
914}
915
916pub(crate) fn v4_cell_chunk(
928 chunk: &[u8],
929 ctx: &V4Ctx,
930 s: &mut V4Decoded,
931) -> Result<CsrCellChunk, ScDataErrors> {
932 let h = decode_v4(chunk, ctx, true, true, s)?;
933 Ok(CsrCellChunk {
934 data_raw: RawCounts::from_u32_auto(&s.raw),
935 data_norm: s.norm.iter().map(|&x| F16::from_f32(x)).collect(),
936 library_size: h.aux as usize,
937 indices: s.idx.clone(),
938 original_index: h.original_index,
939 to_keep: h.to_keep,
940 })
941}
942
943pub(crate) fn v4_gene_chunk(
955 chunk: &[u8],
956 ctx: &V4Ctx,
957 s: &mut V4Decoded,
958) -> Result<CscGeneChunk, ScDataErrors> {
959 let h = decode_v4(chunk, ctx, true, true, s)?;
960 Ok(CscGeneChunk {
961 data_raw: RawCounts::from_u32_auto(&s.raw),
962 data_norm: s.norm.iter().map(|&x| F16::from_f32(x)).collect(),
963 avg_exp: F16::from_bits(h.aux as u16),
964 nnz: h.nnz,
965 indices: s.idx.clone(),
966 original_index: h.original_index,
967 to_keep: h.to_keep,
968 })
969}
970
971#[derive(Clone, Copy, Debug)]
980pub struct ChunkFormat {
981 pub version: u32,
983 pub codec: ChunkCodec,
985 pub target_size: f32,
988}
989
990impl ChunkFormat {
991 pub fn is_v4(&self) -> bool {
997 self.version == SC_FILE_VERSION
998 }
999
1000 pub fn encode_cell(&self, chunk: &CsrCellChunk) -> Result<Vec<u8>, ScDataErrors> {
1010 if self.is_v4() {
1011 chunk.to_v4_bytes(self.target_size, self.codec)
1012 } else {
1013 Ok(chunk.to_compressed_bytes()?)
1014 }
1015 }
1016
1017 pub fn encode_gene(
1029 &self,
1030 chunk: &CscGeneChunk,
1031 cell_lib: &[f32],
1032 ) -> Result<Vec<u8>, ScDataErrors> {
1033 if self.is_v4() {
1034 chunk.to_v4_bytes(cell_lib, self.target_size, self.codec)
1035 } else {
1036 Ok(chunk.to_compressed_bytes()?)
1037 }
1038 }
1039
1040 pub(crate) fn encode_gene_derivable(
1058 &self,
1059 indices: &[u32],
1060 raw: &[u32],
1061 avg_exp: F16,
1062 original_index: usize,
1063 ) -> Vec<u8> {
1064 debug_assert!(self.is_v4() && indices.len() == raw.len());
1065 encode_chunk(
1066 &EncodeIn {
1067 indices,
1068 raw,
1069 norm: &[],
1070 aux: avg_exp.to_bits() as u64,
1071 original_index,
1072 to_keep: true,
1073 norm_derivable: true,
1074 },
1075 self.codec,
1076 )
1077 }
1078}
1079
1080fn chunk_order(header: &SparseDataHeader) -> Result<Vec<usize>, ScDataErrors> {
1095 let mut order = vec![usize::MAX; header.no_chunks];
1096 for (&orig, &chunk) in &header.index_map {
1097 *order.get_mut(chunk).ok_or_else(|| {
1098 ScDataErrors::V4ChunkCorrupt(format!("chunk {chunk} outside the file"))
1099 })? = orig;
1100 }
1101 Ok(order)
1102}
1103
1104fn require_v3(reader: &ParallelSparseReader) -> Result<(), ScDataErrors> {
1114 if reader.file_version() != SC_FILE_VERSION_V3 {
1115 return Err(ScDataErrors::FileVersionMismatch {
1116 expected: SC_FILE_VERSION_V3,
1117 found: reader.file_version(),
1118 });
1119 }
1120 Ok(())
1121}
1122
1123pub fn convert_cell_file_to_v4(
1140 cell_in: &str,
1141 cell_out: &str,
1142 codec: ChunkCodec,
1143 verbose: bool,
1144) -> Result<Vec<u64>, ScDataErrors> {
1145 let start = Instant::now();
1146 let reader = ParallelSparseReader::new(cell_in)?;
1147 require_v3(&reader)?;
1148 if !reader.is_cell_based() {
1149 return Err(ScDataErrors::ReaderModeMismatch {
1150 actual: "gene-based",
1151 requested: "cell-based",
1152 });
1153 }
1154 let header = reader.get_header();
1155 let target_size = reader.target_size().unwrap_or(0.0);
1156 let order = chunk_order(&header)?;
1157
1158 let mut writer = CellGeneSparseWriter::new(
1159 cell_out,
1160 true,
1161 header.total_cells,
1162 header.total_genes,
1163 target_size,
1164 )?;
1165
1166 let table_len = header
1167 .total_cells
1168 .max(order.iter().copied().max().map_or(0, |m| m + 1));
1169 let mut lib_table = vec![0u64; table_len];
1170 let mut stored_norm = 0usize;
1171
1172 for batch in order.chunks(CONVERT_CELL_BATCH) {
1173 let cells = reader.read_cells_parallel(batch)?;
1174 let payloads = cells
1175 .par_iter()
1176 .map(|cell| {
1177 Ok(CellPayload {
1178 original_index: cell.original_index,
1179 library_size: cell.library_size,
1180 bytes: cell.to_v4_bytes(target_size, codec)?,
1181 })
1182 })
1183 .collect::<Result<Vec<_>, ScDataErrors>>()?;
1184 for p in &payloads {
1185 lib_table[p.original_index] = p.library_size as u64;
1186 stored_norm += (V4Header::parse(&p.bytes)?.flags & FLAG_NORM_STORED != 0) as usize;
1187 }
1188 writer.write_compressed_cell_chunks(&payloads)?;
1189 }
1190 writer.finalise()?;
1191
1192 if verbose {
1193 println!(
1194 " Converted {} cells to v4 ({} with stored norm) in {:.2?}.",
1195 header.no_chunks.separate_with_underscores(),
1196 stored_norm.separate_with_underscores(),
1197 start.elapsed()
1198 );
1199 }
1200
1201 Ok(lib_table)
1202}
1203
1204pub fn convert_gene_file_to_v4(
1222 gene_in: &str,
1223 gene_out: &str,
1224 cell_lib: &[u64],
1225 codec: ChunkCodec,
1226 verbose: bool,
1227) -> Result<(), ScDataErrors> {
1228 let start = Instant::now();
1229 let reader = ParallelSparseReader::new(gene_in)?;
1230 require_v3(&reader)?;
1231 if !reader.is_gene_based() {
1232 return Err(ScDataErrors::ReaderModeMismatch {
1233 actual: "cell-based",
1234 requested: "gene-based",
1235 });
1236 }
1237 let header = reader.get_header();
1238 let target_size = reader.target_size().unwrap_or(0.0);
1239 let order = chunk_order(&header)?;
1240 let lib_f32: Vec<f32> = cell_lib.iter().map(|&l| l as f32).collect();
1241
1242 let mut writer = CellGeneSparseWriter::new(
1243 gene_out,
1244 false,
1245 header.total_cells,
1246 header.total_genes,
1247 target_size,
1248 )?;
1249
1250 let mut stored_norm = 0usize;
1251 for batch in order.chunks(CONVERT_GENE_BATCH) {
1252 let genes = reader.read_gene_parallel(batch)?;
1253 let payloads = genes
1254 .par_iter()
1255 .map(|gene| {
1256 Ok((
1257 gene.original_index,
1258 gene.to_v4_bytes(&lib_f32, target_size, codec)?,
1259 ))
1260 })
1261 .collect::<Result<Vec<_>, ScDataErrors>>()?;
1262 for (_, bytes) in &payloads {
1263 stored_norm += (V4Header::parse(bytes)?.flags & FLAG_NORM_STORED != 0) as usize;
1264 }
1265 writer.write_compressed_gene_chunks(&payloads)?;
1266 }
1267 writer.set_cell_library_table(cell_lib.to_vec())?;
1268 writer.finalise()?;
1269
1270 if verbose {
1271 println!(
1272 " Converted {} genes to v4 ({} with stored norm) in {:.2?}.",
1273 header.no_chunks.separate_with_underscores(),
1274 stored_norm.separate_with_underscores(),
1275 start.elapsed()
1276 );
1277 }
1278
1279 Ok(())
1280}
1281
1282pub fn convert_store_v3_to_v4(
1299 cell_in: &str,
1300 gene_in: Option<&str>,
1301 cell_out: &str,
1302 gene_out: Option<&str>,
1303 codec: ChunkCodec,
1304 verbose: bool,
1305) -> Result<(), ScDataErrors> {
1306 let lib = convert_cell_file_to_v4(cell_in, cell_out, codec, verbose)?;
1307 if let (Some(gene_in), Some(gene_out)) = (gene_in, gene_out) {
1308 convert_gene_file_to_v4(gene_in, gene_out, &lib, codec, verbose)?;
1309 }
1310 Ok(())
1311}
1312
1313pub fn upgrade_store_to_v4(
1332 cell_path: &str,
1333 gene_path: Option<&str>,
1334 codec: ChunkCodec,
1335 keep_backup: bool,
1336 verbose: bool,
1337) -> Result<(), ScDataErrors> {
1338 let tmp = |p: &str| format!("{p}.v4.tmp");
1339 let cleanup = |paths: &[Option<&str>]| {
1340 for p in paths.iter().flatten() {
1341 let _ = std::fs::remove_file(tmp(p));
1342 }
1343 };
1344
1345 let result = (|| {
1346 let cell_tmp = tmp(cell_path);
1347 let lib = convert_cell_file_to_v4(cell_path, &cell_tmp, codec, verbose)?;
1348 if let Some(gene_path) = gene_path {
1349 convert_gene_file_to_v4(gene_path, &tmp(gene_path), &lib, codec, verbose)?;
1350 }
1351 for p in std::iter::once(cell_path).chain(gene_path) {
1353 let (old, new) = (
1354 ParallelSparseReader::new(p)?.get_header(),
1355 ParallelSparseReader::new(&tmp(p))?.get_header(),
1356 );
1357 if (old.total_cells, old.total_genes, old.no_chunks)
1358 != (new.total_cells, new.total_genes, new.no_chunks)
1359 {
1360 return Err(ScDataErrors::V4ChunkCorrupt(format!(
1361 "converted {p} does not match the original"
1362 )));
1363 }
1364 }
1365 Ok(())
1366 })();
1367 if let Err(e) = result {
1368 cleanup(&[Some(cell_path), gene_path]);
1369 return Err(e);
1370 }
1371
1372 for p in std::iter::once(cell_path).chain(gene_path) {
1373 if keep_backup {
1374 std::fs::rename(p, format!("{p}.v3.bak"))?;
1375 }
1376 std::fs::rename(tmp(p), p)?;
1377 }
1378 Ok(())
1379}
1380
1381#[cfg(test)]
1386mod tests {
1387 use super::*;
1388 use indexmap::IndexSet;
1389
1390 #[test]
1391 fn test_pack_unpack_round_trip_widths() {
1392 for &(n, max) in &[
1393 (0usize, 0u32),
1394 (1, 5),
1395 (127, 1),
1396 (128, 255),
1397 (129, 70_000),
1398 (300, 0),
1399 (500, u32::MAX),
1400 ] {
1401 let vals: Vec<u32> = (0..n)
1402 .map(|i| ((i as u64 * 2_654_435_761) % (max as u64 + 1)) as u32)
1403 .collect();
1404 let mut buf = Vec::new();
1405 pack(&vals, &mut buf);
1406 let mut out = Vec::new();
1407 let used = unpack(&buf, n, &mut out).unwrap();
1408 assert_eq!(out, vals, "n {n} max {max}");
1409 assert_eq!(used, buf.len());
1410 assert_eq!(packed_len(&buf, n).unwrap(), buf.len());
1411 }
1412 }
1413
1414 #[test]
1415 fn test_unpack_every_width_through_the_specialised_path() {
1416 for width in 1..=32u32 {
1417 let max = if width == 32 {
1418 u32::MAX
1419 } else {
1420 (1u32 << width) - 1
1421 };
1422 let vals: Vec<u32> = (0..PACK_BLOCK * 3 + 5)
1423 .map(|i| {
1424 if i % 128 == 0 {
1425 max
1426 } else {
1427 (i as u32).wrapping_mul(2_654_435_761) & max
1428 }
1429 })
1430 .collect();
1431 let mut buf = Vec::new();
1432 pack(&vals, &mut buf);
1433 let mut out = Vec::new();
1434 assert_eq!(unpack(&buf, vals.len(), &mut out).unwrap(), buf.len());
1435 assert_eq!(out, vals, "width {width}");
1436 }
1437 }
1438
1439 #[test]
1440 fn test_unpack_rejects_truncated_and_wide() {
1441 let mut buf = Vec::new();
1442 pack(&[1, 2, 3, 400], &mut buf);
1443 let mut out = Vec::new();
1444 assert!(unpack(&buf[..buf.len() - 1], 4, &mut out).is_err());
1445 assert!(unpack(&[33], 1, &mut out).is_err());
1446 }
1447
1448 struct Scratch(Vec<std::path::PathBuf>);
1450
1451 impl Scratch {
1452 fn new(tag: &str, n: usize) -> Self {
1453 let dir = std::env::temp_dir();
1454 Self(
1455 (0..n)
1456 .map(|i| dir.join(format!("bx_v4_{tag}_{}_{i}.bin", std::process::id())))
1457 .collect(),
1458 )
1459 }
1460
1461 fn path(&self, i: usize) -> &str {
1462 self.0[i].to_str().unwrap()
1463 }
1464 }
1465
1466 impl Drop for Scratch {
1467 fn drop(&mut self) {
1468 for p in &self.0 {
1469 let _ = std::fs::remove_file(p);
1470 }
1471 }
1472 }
1473
1474 const N_CELLS: usize = 300;
1475 const N_GENES: usize = 40;
1476 const TARGET: f32 = 1e4;
1477
1478 fn synthetic_cells() -> Vec<CsrCellChunk> {
1481 let mut state = 12345u64;
1482 let mut next = move || {
1483 state = state
1484 .wrapping_mul(6364136223846793005)
1485 .wrapping_add(1442695040888963407);
1486 (state >> 33) as u32
1487 };
1488 (0..N_CELLS)
1489 .map(|c| {
1490 let mut genes = Vec::new();
1491 let mut counts = Vec::new();
1492 if c != 7 {
1493 for g in 0..N_GENES as u32 {
1494 if next() % 3 == 0 {
1495 genes.push(g);
1496 counts.push(1 + next() % 4 + if next() % 50 == 0 { 300 } else { 0 });
1497 }
1498 }
1499 }
1500 if c == 11 {
1501 match genes.last() {
1502 Some(&g) if g == N_GENES as u32 - 1 => *counts.last_mut().unwrap() = 70_000,
1503 _ => {
1504 genes.push(N_GENES as u32 - 1);
1505 counts.push(70_000);
1506 }
1507 }
1508 }
1509 let mut chunk = CsrCellChunk::from_data(&counts, &genes, c, TARGET, true);
1510 if c == 13 && !chunk.data_norm.is_empty() {
1511 chunk.data_norm[0] = F16::from_f32(chunk.data_norm[0].to_f32() + 0.5);
1512 }
1513 chunk
1514 })
1515 .collect()
1516 }
1517
1518 fn write_v3_store(cell_path: &str, gene_path: &str, target: f32) {
1519 let mut writer =
1520 CellGeneSparseWriter::new_v3(cell_path, true, N_CELLS, N_GENES, target).unwrap();
1521 for chunk in synthetic_cells() {
1522 writer.write_cell_chunk(chunk).unwrap();
1523 }
1524 writer.finalise().unwrap();
1525 crate::gene_file_io::write_gene_file_v3(cell_path, gene_path, Some(500), false).unwrap();
1526 }
1527
1528 fn assert_cells_equal(a: &CsrCellChunk, b: &CsrCellChunk) {
1529 assert_eq!(a.original_index, b.original_index);
1530 assert_eq!(a.library_size, b.library_size);
1531 assert_eq!(a.to_keep, b.to_keep);
1532 assert_eq!(a.indices, b.indices);
1533 assert_eq!(
1534 a.data_raw.iter().collect::<Vec<_>>(),
1535 b.data_raw.iter().collect::<Vec<_>>()
1536 );
1537 let bits = |c: &CsrCellChunk| c.data_norm.iter().map(|v| v.to_bits()).collect::<Vec<_>>();
1538 assert_eq!(bits(a), bits(b));
1539 }
1540
1541 fn assert_genes_equal(a: &CscGeneChunk, b: &CscGeneChunk) {
1542 assert_eq!(a.original_index, b.original_index);
1543 assert_eq!(a.nnz, b.nnz);
1544 assert_eq!(a.avg_exp.to_bits(), b.avg_exp.to_bits());
1545 assert_eq!(a.indices, b.indices);
1546 assert_eq!(
1547 a.data_raw.iter().collect::<Vec<_>>(),
1548 b.data_raw.iter().collect::<Vec<_>>()
1549 );
1550 let bits = |c: &CscGeneChunk| c.data_norm.iter().map(|v| v.to_bits()).collect::<Vec<_>>();
1551 assert_eq!(bits(a), bits(b));
1552 }
1553
1554 fn assert_sparse_equal(
1555 a: &CompressedSparseData2<f32, f32>,
1556 b: &CompressedSparseData2<f32, f32>,
1557 ) {
1558 assert_eq!(a.shape, b.shape);
1559 assert_eq!(a.indptr, b.indptr);
1560 assert_eq!(a.indices, b.indices);
1561 assert_eq!(a.data, b.data);
1562 assert_eq!(a.data_2, b.data_2);
1563 }
1564
1565 fn check_conversion(codec: ChunkCodec, target: f32) {
1568 let tag = format!("{codec:?}_{}", target as u32);
1569 let s = Scratch::new(&tag, 4);
1570 write_v3_store(s.path(0), s.path(1), target);
1571 convert_store_v3_to_v4(
1572 s.path(0),
1573 Some(s.path(1)),
1574 s.path(2),
1575 Some(s.path(3)),
1576 codec,
1577 false,
1578 )
1579 .unwrap();
1580
1581 let (cells3, cells4) = (
1582 ParallelSparseReader::new(s.path(0)).unwrap(),
1583 ParallelSparseReader::new(s.path(2)).unwrap(),
1584 );
1585 let (genes3, genes4) = (
1586 ParallelSparseReader::new(s.path(1)).unwrap(),
1587 ParallelSparseReader::new(s.path(3)).unwrap(),
1588 );
1589 assert_eq!(cells3.file_version(), SC_FILE_VERSION_V3);
1590 assert_eq!(cells4.file_version(), SC_FILE_VERSION);
1591 assert_eq!(genes4.file_version(), SC_FILE_VERSION);
1592 assert_eq!(cells4.target_size(), cells3.target_size());
1593
1594 let all_cells: Vec<usize> = (0..N_CELLS).collect();
1595 let all_genes: Vec<usize> = (0..N_GENES).collect();
1596 for (a, b) in cells3
1597 .read_cells_parallel(&all_cells)
1598 .unwrap()
1599 .iter()
1600 .zip(&cells4.read_cells_parallel(&all_cells).unwrap())
1601 {
1602 assert_cells_equal(a, b);
1603 }
1604 for (a, b) in genes3
1605 .read_gene_parallel(&all_genes)
1606 .unwrap()
1607 .iter()
1608 .zip(&genes4.read_gene_parallel(&all_genes).unwrap())
1609 {
1610 assert_genes_equal(a, b);
1611 }
1612
1613 let shuffled: Vec<usize> = (0..N_CELLS).rev().step_by(3).collect();
1614 let keep: IndexSet<u32> = (0..N_CELLS as u32).filter(|c| c % 4 != 1).collect();
1615 for layer in [
1616 DataLayerReturn::Raw,
1617 DataLayerReturn::Norm,
1618 DataLayerReturn::BothLayers,
1619 ] {
1620 assert_sparse_equal(
1621 &cells3.read_cells_sparse::<f32>(&shuffled, &layer).unwrap(),
1622 &cells4.read_cells_sparse::<f32>(&shuffled, &layer).unwrap(),
1623 );
1624 for subset in [None, Some(&keep)] {
1625 assert_sparse_equal(
1626 &genes3
1627 .read_genes_sparse::<f32>(&all_genes, subset, &layer)
1628 .unwrap(),
1629 &genes4
1630 .read_genes_sparse::<f32>(&all_genes, subset, &layer)
1631 .unwrap(),
1632 );
1633 }
1634 }
1635
1636 for (a, b) in genes3
1637 .read_gene_parallel_filtered(&all_genes, &keep)
1638 .unwrap()
1639 .iter()
1640 .zip(
1641 &genes4
1642 .read_gene_parallel_filtered(&all_genes, &keep)
1643 .unwrap(),
1644 )
1645 {
1646 assert_genes_equal(a, b);
1647 }
1648 assert_eq!(
1649 cells3.read_cell_library_sizes(&all_cells).unwrap(),
1650 cells4.read_cell_library_sizes(&all_cells).unwrap()
1651 );
1652 assert_eq!(
1653 genes3.read_gene_nnz(&all_genes).unwrap(),
1654 genes4.read_gene_nnz(&all_genes).unwrap()
1655 );
1656 assert_eq!(
1657 cells3.get_header().chunk_offsets.len(),
1658 cells4.get_header().chunk_offsets.len()
1659 );
1660 }
1661
1662 #[test]
1663 fn test_v3_to_v4_round_trip_none() {
1664 check_conversion(ChunkCodec::None, TARGET);
1665 }
1666
1667 #[test]
1668 fn test_v3_to_v4_round_trip_lz4() {
1669 check_conversion(ChunkCodec::Lz4, TARGET);
1670 }
1671
1672 #[test]
1673 fn test_v3_to_v4_round_trip_zstd() {
1674 check_conversion(ChunkCodec::Zstd, TARGET);
1675 }
1676
1677 #[test]
1678 fn test_v3_to_v4_round_trip_unknown_target_stores_norm() {
1679 check_conversion(ChunkCodec::Lz4, 0.0);
1680 }
1681
1682 #[test]
1683 fn test_v4_drops_norm_and_shrinks() {
1684 let s = Scratch::new("shrink", 4);
1685 write_v3_store(s.path(0), s.path(1), TARGET);
1686 convert_store_v3_to_v4(
1687 s.path(0),
1688 Some(s.path(1)),
1689 s.path(2),
1690 Some(s.path(3)),
1691 ChunkCodec::Lz4,
1692 false,
1693 )
1694 .unwrap();
1695 let size = |i: usize| std::fs::metadata(s.path(i)).unwrap().len();
1696 assert!(size(2) < size(0), "cell file {} vs {}", size(2), size(0));
1697 assert!(size(3) < size(1), "gene file {} vs {}", size(3), size(1));
1698 }
1699
1700 #[test]
1701 fn test_v4_chunk_keeps_unsorted_and_duplicate_indices() {
1702 let chunk = CsrCellChunk::from_data(&[3u32, 1, 2, 5], &[9u32, 2, 2, 400], 5, TARGET, true);
1703 let bytes = chunk.to_v4_bytes(TARGET, ChunkCodec::Lz4).unwrap();
1704 let ctx = V4Ctx {
1705 target_size: TARGET,
1706 cell_lib: None,
1707 cell_norm: None,
1708 is_cell: true,
1709 };
1710 let back = v4_cell_chunk(&bytes, &ctx, &mut V4Decoded::default()).unwrap();
1711 assert_cells_equal(&chunk, &back);
1712 }
1713
1714 #[test]
1715 fn test_default_writer_emits_v4_and_converter_refuses_it() {
1716 let s = Scratch::new("guards", 2);
1717 let mut writer = CellGeneSparseWriter::new(s.path(0), true, 1, 3, TARGET).unwrap();
1718 writer
1719 .write_cell_chunk(CsrCellChunk::from_data(&[1u32], &[0u32], 0, TARGET, true))
1720 .unwrap();
1721 writer.finalise().unwrap();
1722 assert_eq!(peek_file_version(s.path(0)).unwrap(), SC_FILE_VERSION);
1723 assert!(matches!(
1724 convert_cell_file_to_v4(s.path(0), s.path(1), ChunkCodec::Lz4, false),
1725 Err(ScDataErrors::FileVersionMismatch { .. })
1726 ));
1727 }
1728
1729 #[test]
1730 fn test_peek_file_version_distinguishes_v3_and_v4() {
1731 let s = Scratch::new("peek", 2);
1732 let chunk = || CsrCellChunk::from_data(&[1u32, 2], &[0u32, 1], 0, TARGET, true);
1733 let mut v3 = CellGeneSparseWriter::new_v3(s.path(0), true, 1, 3, TARGET).unwrap();
1734 v3.write_cell_chunk(chunk()).unwrap();
1735 v3.finalise().unwrap();
1736 let mut v4 = CellGeneSparseWriter::new(s.path(1), true, 1, 3, TARGET).unwrap();
1737 v4.write_cell_chunk(chunk()).unwrap();
1738 v4.finalise().unwrap();
1739 assert_eq!(peek_file_version(s.path(0)).unwrap(), SC_FILE_VERSION_V3);
1740 assert_eq!(peek_file_version(s.path(1)).unwrap(), SC_FILE_VERSION);
1741 }
1742
1743 #[test]
1744 fn test_default_gene_file_is_v4_and_matches_the_v3_one() {
1745 let s = Scratch::new("genefile", 3);
1746 write_v3_store(s.path(0), s.path(1), TARGET);
1747 crate::gene_file_io::write_gene_file(s.path(0), s.path(2), Some(500), false).unwrap();
1748 assert_eq!(peek_file_version(s.path(2)).unwrap(), SC_FILE_VERSION);
1749
1750 let (v3, v4) = (
1751 ParallelSparseReader::new(s.path(1)).unwrap(),
1752 ParallelSparseReader::new(s.path(2)).unwrap(),
1753 );
1754 let all: Vec<usize> = (0..N_GENES).collect();
1755 for (a, b) in v3
1756 .read_gene_parallel(&all)
1757 .unwrap()
1758 .iter()
1759 .zip(&v4.read_gene_parallel(&all).unwrap())
1760 {
1761 assert_genes_equal(a, b);
1762 }
1763 }
1764
1765 #[test]
1766 fn test_upgrade_store_replaces_files_and_keeps_backups() {
1767 let s = Scratch::new("upgrade", 4);
1768 write_v3_store(s.path(0), s.path(1), TARGET);
1769 let cells_before = ParallelSparseReader::new(s.path(0))
1770 .unwrap()
1771 .get_all_cells()
1772 .unwrap();
1773 upgrade_store_to_v4(s.path(0), Some(s.path(1)), ChunkCodec::Lz4, true, false).unwrap();
1774
1775 assert_eq!(peek_file_version(s.path(0)).unwrap(), SC_FILE_VERSION);
1776 assert_eq!(peek_file_version(s.path(1)).unwrap(), SC_FILE_VERSION);
1777 let backup = format!("{}.v3.bak", s.path(0));
1778 assert_eq!(peek_file_version(&backup).unwrap(), SC_FILE_VERSION_V3);
1779 for (a, b) in cells_before.iter().zip(
1780 &ParallelSparseReader::new(s.path(0))
1781 .unwrap()
1782 .get_all_cells()
1783 .unwrap(),
1784 ) {
1785 assert_cells_equal(a, b);
1786 }
1787 for p in [backup, format!("{}.v3.bak", s.path(1))] {
1788 let _ = std::fs::remove_file(p);
1789 }
1790 }
1791
1792 #[test]
1793 fn test_archive_round_trips_a_v4_cell_file() {
1794 let s = Scratch::new("archive", 6);
1795 write_v3_store(s.path(0), s.path(1), TARGET);
1796 convert_cell_file_to_v4(s.path(0), s.path(2), ChunkCodec::Lz4, false).unwrap();
1797 crate::archive_io::archive_cell_file(s.path(2), s.path(3), 3, false).unwrap();
1798 crate::archive_io::restore_archive(s.path(3), s.path(4), s.path(5), Some(500), false)
1799 .unwrap();
1800
1801 let original = ParallelSparseReader::new(s.path(0)).unwrap();
1802 let restored = ParallelSparseReader::new(s.path(4)).unwrap();
1803 let all: Vec<usize> = (0..N_CELLS).collect();
1804 for (a, b) in original
1805 .read_cells_parallel(&all)
1806 .unwrap()
1807 .iter()
1808 .zip(&restored.read_cells_parallel(&all).unwrap())
1809 {
1810 assert_cells_equal(a, b);
1811 }
1812 }
1813}