use rayon::prelude::*;
use rustc_hash::FxHashMap;
use crate::core::math::sparse::transpose_sparse;
use crate::prelude::*;
use crate::single_cell::sc_data::data_io::{
CscGeneChunk, CsrCellChunk, RawCounts, SingleCellReading, SparseDataHeader,
};
use crate::single_cell::sc_traits::F16;
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, BixverseErrors> {
if !matrix.cs_type.is_csc() {
return Err(BixverseErrors::SparseMatrixMustBeCsc);
}
if matrix.data_2.is_none() {
return Err(BixverseErrors::Data2NotAvailable);
}
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, BixverseErrors> {
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, BixverseErrors> {
let transposed = self
.transposed
.as_ref()
.ok_or(BixverseErrors::ReaderModeMismatch {
actual: "gene-based",
requested: "cell-based",
})?;
if cell >= self.matrix.shape.0 {
return Err(BixverseErrors::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, BixverseErrors> {
if gene >= self.matrix.shape.1 {
return Err(BixverseErrors::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,
})
}
}
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()
}
impl SingleCellReading for InMemorySparseReader<'_> {
fn read_cells_parallel(&self, indices: &[usize]) -> Result<Vec<CsrCellChunk>, BixverseErrors> {
indices
.par_iter()
.map(|&cell| self.cell_chunk(cell))
.collect()
}
fn read_gene_parallel(&self, indices: &[usize]) -> Result<Vec<CscGeneChunk>, BixverseErrors> {
if self.transposed.is_some() {
return Err(BixverseErrors::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>, BixverseErrors> {
indices
.iter()
.map(|&cell| {
self.library_sizes
.get(cell)
.copied()
.ok_or(BixverseErrors::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());
}
}