use indexmap::IndexSet;
use num_traits::FromPrimitive;
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use crate::data_io::{
CscGeneChunk, CsrCellChunk, DataLayerReturn, ParallelSparseReader, RawCounts, RunSource,
SingleCellReading, SparseDataHeader, cell_row_map, extend_raw_checked, read_fused,
};
use crate::prelude::*;
use crate::sc_traits::F16;
use bixverse_sparse::sparse::transpose_sparse;
pub struct InMemorySparseReader<'a> {
matrix: &'a CompressedSparseData2<u32, f32>,
transposed: Option<CompressedSparseData2<u32, f32>>,
library_sizes: Vec<usize>,
header: SparseDataHeader,
target_size: Option<f32>,
}
impl<'a> InMemorySparseReader<'a> {
pub fn new(
matrix: &'a CompressedSparseData2<u32, f32>,
target_size: Option<f32>,
) -> Result<Self, ScDataErrors> {
if !matrix.cs_type.is_csc() {
return Err(SparseErrors::SparseMatrixMustBeCsc.into());
}
if matrix.data_2.is_none() {
return Err(SparseErrors::Data2NotAvailable.into());
}
let (n_cells, n_genes) = matrix.shape;
let library_sizes = cell_library_sizes(matrix, n_cells);
let header = SparseDataHeader {
total_cells: n_cells,
total_genes: n_genes,
cell_based: false,
no_chunks: n_genes,
chunk_offsets: Vec::new(),
index_map: (0..n_genes)
.map(|gene| (gene, gene))
.collect::<FxHashMap<_, _>>(),
};
Ok(Self {
matrix,
transposed: None,
library_sizes,
header,
target_size,
})
}
pub fn new_cell_major(
matrix: &'a CompressedSparseData2<u32, f32>,
target_size: Option<f32>,
) -> Result<Self, ScDataErrors> {
let mut reader = Self::new(matrix, target_size)?;
let n_cells = matrix.shape.0;
reader.transposed = Some(transpose_sparse(matrix));
reader.header.cell_based = true;
reader.header.no_chunks = n_cells;
reader.header.index_map = (0..n_cells).map(|cell| (cell, cell)).collect();
Ok(reader)
}
fn cell_chunk(&self, cell: usize) -> Result<CsrCellChunk, ScDataErrors> {
let transposed = self
.transposed
.as_ref()
.ok_or(ScDataErrors::ReaderModeMismatch {
actual: "gene-based",
requested: "cell-based",
})?;
if cell >= self.matrix.shape.0 {
return Err(ScDataErrors::ChunkIndexNotFound(cell));
}
let lo = transposed.indptr[cell] as usize;
let hi = transposed.indptr[cell + 1] as usize;
let data_raw = RawCounts::from_u32_auto(&transposed.data[lo..hi]);
let data_norm: Vec<F16> = transposed.data_2.as_ref().expect("checked in `new`")[lo..hi]
.iter()
.map(|&v| F16::from_f32(v))
.collect();
Ok(CsrCellChunk {
data_raw,
data_norm,
library_size: self.library_sizes[cell],
indices: transposed.indices[lo..hi].to_vec(),
original_index: cell,
to_keep: true,
})
}
fn gene_chunk(&self, gene: usize) -> Result<CscGeneChunk, ScDataErrors> {
if gene >= self.matrix.shape.1 {
return Err(ScDataErrors::ChunkIndexNotFound(gene));
}
let lo = self.matrix.indptr[gene] as usize;
let hi = self.matrix.indptr[gene + 1] as usize;
let data_raw = RawCounts::from_u32_auto(&self.matrix.data[lo..hi]);
let data_norm: Vec<F16> = self.matrix.data_2.as_ref().expect("checked in `new`")[lo..hi]
.iter()
.map(|&v| F16::from_f32(v))
.collect();
let avg_exp = F16::from_f32(data_norm.iter().map(|v| v.to_f32()).sum::<f32>());
Ok(CscGeneChunk {
data_raw,
data_norm,
avg_exp,
nnz: hi - lo,
indices: self.matrix.indices[lo..hi].to_vec(),
original_index: gene,
to_keep: true,
})
}
}
struct SliceRun<'a> {
indices: &'a [u32],
raw: &'a [u32],
norm: &'a [f32],
}
impl RunSource for SliceRun<'_> {
#[inline]
fn nnz(&self) -> usize {
self.indices.len()
}
#[inline(always)]
fn index_at(&self, i: usize) -> u32 {
self.indices[i]
}
#[inline(always)]
fn raw_at(&self, i: usize) -> u32 {
self.raw[i]
}
#[inline(always)]
fn norm_at(&self, i: usize) -> f32 {
self.norm[i]
}
fn extend_indices(&self, out: &mut Vec<u32>) {
out.extend_from_slice(self.indices);
}
fn extend_raw<T: FromPrimitive>(&self, out: &mut Vec<T>) -> Result<(), ScDataErrors> {
extend_raw_checked(out, self.raw.iter().copied())
}
fn extend_norm(&self, out: &mut Vec<f32>) {
out.extend_from_slice(self.norm);
}
}
fn read_runs<T>(
store: &CompressedSparseData2<u32, f32>,
indices: &[usize],
row_map: Option<&[u32]>,
data_layer: &DataLayerReturn,
cs_type: CompressedSparseFormat,
shape: (usize, usize),
) -> Result<CompressedSparseData2<T, f32>, ScDataErrors>
where
T: BixverseNumeric + FromPrimitive,
{
let norm = store.data_2.as_ref().expect("checked in `new`");
let n_runs = store.indptr.len() - 1;
read_fused(indices, data_layer, cs_type, shape, |run, _, fragment| {
if run >= n_runs {
return Err(ScDataErrors::ChunkIndexNotFound(run));
}
let lo = store.indptr[run] as usize;
let hi = store.indptr[run + 1] as usize;
let run = SliceRun {
indices: &store.indices[lo..hi],
raw: &store.data[lo..hi],
norm: &norm[lo..hi],
};
fragment.push_run(&run, row_map)
})
}
fn cell_library_sizes(matrix: &CompressedSparseData2<u32, f32>, n_cells: usize) -> Vec<usize> {
let n_genes = matrix.shape.1;
let totals = thread_chunks(n_genes)
.par_iter()
.map(|&(gene_start, gene_end)| {
let mut acc = vec![0u64; n_cells];
let lo = matrix.indptr[gene_start] as usize;
let hi = matrix.indptr[gene_end] as usize;
for (&cell, &count) in matrix.indices[lo..hi]
.iter()
.zip(matrix.data[lo..hi].iter())
{
acc[cell as usize] += count as u64;
}
acc
})
.reduce(
|| vec![0u64; n_cells],
|mut a, b| {
for (total, add) in a.iter_mut().zip(b.iter()) {
*total += add;
}
a
},
);
totals.into_iter().map(|x| x as usize).collect()
}
pub const CELL_SUBSET_MAX_FRACTION: f64 = 0.10;
pub const CELL_SUBSET_MAX_CELLS: usize = 100_000;
pub fn prefer_cell_subset(n_selected: usize, n_total: usize) -> bool {
n_selected <= CELL_SUBSET_MAX_CELLS
&& (n_selected as f64) <= CELL_SUBSET_MAX_FRACTION * n_total as f64
}
pub fn read_cell_subset_csc(
cell_reader: &ParallelSparseReader,
cell_indices: &[usize],
) -> Result<CompressedSparseData2<u32, f32>, ScDataErrors> {
let batch = cell_reader.read_cells_csr(cell_indices, &DataLayerReturn::BothLayers)?;
let data: Vec<u32> = batch
.data_raw
.expect("requested both layers")
.par_iter()
.map(|&v| v as u32)
.collect();
let csr = CompressedSparseData2::from_parts(
data,
batch.indices,
batch.indptr,
batch.data_norm,
CompressedSparseFormat::Csr,
batch.shape,
);
Ok(transpose_sparse(&csr))
}
impl SingleCellReading for InMemorySparseReader<'_> {
fn read_cells_parallel(&self, indices: &[usize]) -> Result<Vec<CsrCellChunk>, ScDataErrors> {
indices
.par_iter()
.map(|&cell| self.cell_chunk(cell))
.collect()
}
fn read_cells_sparse<T>(
&self,
indices: &[usize],
data_layer: &DataLayerReturn,
) -> Result<CompressedSparseData2<T, f32>, ScDataErrors>
where
T: BixverseNumeric + FromPrimitive,
{
let transposed = self
.transposed
.as_ref()
.ok_or(ScDataErrors::ReaderModeMismatch {
actual: "gene-based",
requested: "cell-based",
})?;
read_runs(
transposed,
indices,
None,
data_layer,
CompressedSparseFormat::Csr,
(indices.len(), self.matrix.shape.1),
)
}
fn read_genes_sparse<T>(
&self,
indices: &[usize],
cells_to_keep: Option<&IndexSet<u32>>,
data_layer: &DataLayerReturn,
) -> Result<CompressedSparseData2<T, f32>, ScDataErrors>
where
T: BixverseNumeric + FromPrimitive,
{
if self.transposed.is_some() {
return Err(ScDataErrors::ReaderModeMismatch {
actual: "cell-based",
requested: "gene-based",
});
}
let (row_map, n_rows) = cell_row_map(cells_to_keep, self.matrix.shape.0);
read_runs(
self.matrix,
indices,
row_map.as_deref(),
data_layer,
CompressedSparseFormat::Csc,
(n_rows, indices.len()),
)
}
fn read_gene_parallel(&self, indices: &[usize]) -> Result<Vec<CscGeneChunk>, ScDataErrors> {
if self.transposed.is_some() {
return Err(ScDataErrors::ReaderModeMismatch {
actual: "cell-based",
requested: "gene-based",
});
}
indices
.par_iter()
.map(|&gene| self.gene_chunk(gene))
.collect()
}
fn get_header(&self) -> SparseDataHeader {
self.header.clone()
}
fn is_cell_based(&self) -> bool {
self.transposed.is_some()
}
fn target_size(&self) -> Option<f32> {
self.target_size
}
fn read_cell_library_sizes(&self, indices: &[usize]) -> Result<Vec<usize>, ScDataErrors> {
indices
.iter()
.map(|&cell| {
self.library_sizes
.get(cell)
.copied()
.ok_or(ScDataErrors::ChunkIndexNotFound(cell))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn toy_csc() -> CompressedSparseData2<u32, f32> {
let data: Vec<u32> = vec![
3, 5, 1, 2, 4, 1, 7, ];
let data_2: Vec<f32> = data.iter().map(|&v| (v as f32).ln_1p()).collect();
let indices: Vec<u32> = vec![0, 2, 0, 1, 2, 3, 3];
let indptr: Vec<u32> = vec![0, 2, 6, 7];
CompressedSparseData2::new_csc(&data, &indices, &indptr, Some(&data_2), (4, 3))
}
#[test]
fn test_library_sizes_are_the_cell_totals() {
let matrix = toy_csc();
let reader = InMemorySparseReader::new(&matrix, None).unwrap();
let sizes = reader.read_cell_library_sizes(&[0, 1, 2, 3]).unwrap();
assert_eq!(sizes, vec![4, 2, 9, 8]);
}
#[test]
fn test_gene_chunks_carry_the_column_verbatim() {
let matrix = toy_csc();
let reader = InMemorySparseReader::new(&matrix, None).unwrap();
let chunks = reader.read_gene_parallel(&[2, 0]).unwrap();
assert_eq!(chunks[0].original_index, 2);
assert_eq!(chunks[0].indices, vec![3]);
assert_eq!(chunks[0].data_raw.iter().collect::<Vec<u32>>(), vec![7]);
assert_eq!(chunks[0].nnz, 1);
assert_eq!(chunks[1].original_index, 0);
assert_eq!(chunks[1].indices, vec![0, 2]);
assert_eq!(chunks[1].data_raw.iter().collect::<Vec<u32>>(), vec![3, 5]);
}
#[test]
fn test_header_and_mode() {
let matrix = toy_csc();
let reader = InMemorySparseReader::new(&matrix, Some(1e4)).unwrap();
let header = reader.get_header();
assert_eq!(header.total_cells, 4);
assert_eq!(header.total_genes, 3);
assert!(!reader.is_cell_based());
assert!(reader.is_gene_based());
assert_eq!(reader.target_size(), Some(1e4));
assert!(reader.read_cells_parallel(&[0]).is_err());
}
#[test]
fn test_csr_input_is_rejected() {
let csc = toy_csc();
let csr = csc.transform();
assert!(InMemorySparseReader::new(&csr, None).is_err());
}
#[test]
fn test_out_of_range_indices_error() {
let matrix = toy_csc();
let reader = InMemorySparseReader::new(&matrix, None).unwrap();
assert!(reader.read_gene_parallel(&[3]).is_err());
assert!(reader.read_cell_library_sizes(&[4]).is_err());
}
#[test]
fn test_cell_chunks_carry_the_row_verbatim() {
let matrix = toy_csc();
let reader = InMemorySparseReader::new_cell_major(&matrix, None).unwrap();
let chunks = reader.read_cells_parallel(&[3, 0]).unwrap();
assert_eq!(chunks[0].original_index, 3);
assert_eq!(chunks[0].indices, vec![1, 2]);
assert_eq!(chunks[0].data_raw.iter().collect::<Vec<u32>>(), vec![1, 7]);
assert_eq!(chunks[0].library_size, 8);
assert_eq!(chunks[1].original_index, 0);
assert_eq!(chunks[1].indices, vec![0, 1]);
assert_eq!(chunks[1].data_raw.iter().collect::<Vec<u32>>(), vec![3, 1]);
assert_eq!(chunks[1].library_size, 4);
}
#[test]
fn test_cell_major_mode_refuses_gene_reads() {
let matrix = toy_csc();
let reader = InMemorySparseReader::new_cell_major(&matrix, Some(1e4)).unwrap();
assert!(reader.is_cell_based());
assert!(!reader.is_gene_based());
assert!(reader.get_header().cell_based);
assert_eq!(reader.get_header().total_cells, 4);
assert_eq!(reader.target_size(), Some(1e4));
assert!(reader.read_gene_parallel(&[0]).is_err());
}
#[test]
fn test_cell_major_preserves_every_entry() {
let matrix = toy_csc();
let gene_major = InMemorySparseReader::new(&matrix, None).unwrap();
let cell_major = InMemorySparseReader::new_cell_major(&matrix, None).unwrap();
let mut from_genes: Vec<(u32, usize, u32)> = Vec::new();
for chunk in gene_major.read_gene_parallel(&[0, 1, 2]).unwrap() {
for (&cell, count) in chunk.indices.iter().zip(chunk.data_raw.iter()) {
from_genes.push((cell, chunk.original_index, count));
}
}
let mut from_cells: Vec<(u32, usize, u32)> = Vec::new();
for chunk in cell_major.read_cells_parallel(&[0, 1, 2, 3]).unwrap() {
for (&gene, count) in chunk.indices.iter().zip(chunk.data_raw.iter()) {
from_cells.push((chunk.original_index as u32, gene as usize, count));
}
}
from_genes.sort_unstable();
from_cells.sort_unstable();
assert_eq!(from_genes, from_cells);
}
#[test]
fn test_cell_major_out_of_range_errors() {
let matrix = toy_csc();
let reader = InMemorySparseReader::new_cell_major(&matrix, None).unwrap();
assert!(reader.read_cells_parallel(&[4]).is_err());
}
#[test]
fn test_read_genes_sparse_is_the_matrix() {
let matrix = toy_csc();
let reader = InMemorySparseReader::new(&matrix, None).unwrap();
let csc = reader
.read_genes_sparse::<u32>(&[0, 1, 2], None, &DataLayerReturn::BothLayers)
.unwrap();
assert_eq!(csc.shape, matrix.shape);
assert_eq!(csc.indptr, matrix.indptr);
assert_eq!(csc.indices, matrix.indices);
assert_eq!(csc.data, matrix.data);
assert_eq!(csc.data_2, matrix.data_2);
}
#[test]
fn test_read_genes_sparse_filtered_matches_chunk_path() {
let matrix = toy_csc();
let reader = InMemorySparseReader::new(&matrix, None).unwrap();
let genes = [2usize, 1, 0];
let keep: IndexSet<u32> = [3_u32, 0, 2].into_iter().collect();
let want = from_gene_chunks::<u32>(
reader.read_gene_parallel_filtered(&genes, &keep).unwrap(),
&DataLayerReturn::BothLayers,
keep.len(),
)
.unwrap();
let got = reader
.read_genes_sparse::<u32>(&genes, Some(&keep), &DataLayerReturn::BothLayers)
.unwrap();
assert_eq!(got.shape, want.shape);
assert_eq!(got.indptr, want.indptr);
assert_eq!(got.indices, want.indices);
assert_eq!(got.data, want.data);
let narrowed: Vec<f32> = got
.data_2
.unwrap()
.iter()
.map(|&v| F16::from_f32(v).to_f32())
.collect();
assert_eq!(narrowed, want.data_2.unwrap());
}
#[test]
fn test_read_sparse_modes_and_bounds() {
let matrix = toy_csc();
let gene_major = InMemorySparseReader::new(&matrix, None).unwrap();
let cell_major = InMemorySparseReader::new_cell_major(&matrix, None).unwrap();
let csr = cell_major
.read_cells_sparse::<u32>(&[0, 1, 2, 3], &DataLayerReturn::BothLayers)
.unwrap();
let twin = transpose_sparse(&matrix);
assert_eq!(csr.shape, twin.shape);
assert_eq!(csr.indptr, twin.indptr);
assert_eq!(csr.indices, twin.indices);
assert_eq!(csr.data, twin.data);
assert_eq!(csr.data_2, twin.data_2);
let layer = DataLayerReturn::Raw;
assert!(gene_major.read_cells_sparse::<u32>(&[0], &layer).is_err());
assert!(
cell_major
.read_genes_sparse::<u32>(&[0], None, &layer)
.is_err()
);
assert!(
gene_major
.read_genes_sparse::<u32>(&[3], None, &layer)
.is_err()
);
assert!(cell_major.read_cells_sparse::<u32>(&[4], &layer).is_err());
}
}