use indexmap::IndexSet;
use num_traits::FromPrimitive;
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, DataLayerReturn, ParallelSparseReader, RawCounts, RunSource,
SingleCellReading, SparseDataHeader, cell_row_map, extend_raw_checked, read_fused,
};
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,
})
}
}
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<(), BixverseErrors> {
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>, BixverseErrors>
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(BixverseErrors::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>, BixverseErrors> {
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>, BixverseErrors> {
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>, BixverseErrors>
where
T: BixverseNumeric + FromPrimitive,
{
let transposed = self
.transposed
.as_ref()
.ok_or(BixverseErrors::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>, BixverseErrors>
where
T: BixverseNumeric + FromPrimitive,
{
if self.transposed.is_some() {
return Err(BixverseErrors::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>, 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());
}
#[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());
}
mod cell_subset {
use half::f16;
use super::*;
use crate::single_cell::sc_data::data_io::CellGeneSparseWriter;
use crate::single_cell::sc_processing::hvg::get_hvg_vst;
use crate::single_cell::sc_processing::sctransform::model::{SctCovariates, SctParams};
use crate::single_cell::sc_processing::sctransform::stream::{
SctStreamOpts, fit_sctransform,
};
const N_CELLS: usize = 300;
const N_GENES: usize = 60;
const SIZE_FACTOR: f32 = 1e4;
struct TempStore(std::path::PathBuf);
impl Drop for TempStore {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
impl TempStore {
fn new(name: &str) -> Self {
Self(std::env::temp_dir().join(format!(
"bixverse_cell_subset_{name}_{}.bin",
std::process::id()
)))
}
fn path(&self) -> &str {
self.0.to_str().expect("temp path is valid UTF-8")
}
}
fn synthetic_counts() -> Vec<Vec<u32>> {
let mut state = 0x2545_F491_4F6C_DD1Du64;
(0..N_GENES)
.map(|gene| {
(0..N_CELLS)
.map(|_| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let draw = (state >> 33) as u32;
if draw % 10 < 6 {
0
} else {
draw % (gene as u32 + 3)
}
})
.collect()
})
.collect()
}
fn write_stores(dense: &[Vec<u32>], genes: &TempStore, cells: &TempStore) {
let lib: Vec<f32> = (0..N_CELLS)
.map(|c| dense.iter().map(|g| g[c] as f32).sum::<f32>().max(1.0))
.collect();
let mut gene_writer =
CellGeneSparseWriter::new(genes.path(), false, N_CELLS, N_GENES, SIZE_FACTOR)
.expect("gene writer opens");
for (gene_idx, gene) in dense.iter().enumerate() {
let (mut raw, mut idx, mut norms) = (Vec::new(), Vec::new(), Vec::new());
for (cell, &value) in gene.iter().enumerate() {
if value > 0 {
raw.push(value);
idx.push(cell);
let norm = (value as f32 / lib[cell] * SIZE_FACTOR).ln_1p();
norms.push(F16::from(f16::from_f32(norm)));
}
}
gene_writer
.write_gene_chunk(CscGeneChunk::from_conversion(
RawCounts::from_u32_auto(&raw),
&norms,
&idx,
gene_idx,
true,
))
.expect("write gene chunk");
}
gene_writer.finalise().expect("finalise genes");
let mut cell_writer =
CellGeneSparseWriter::new(cells.path(), true, N_CELLS, N_GENES, SIZE_FACTOR)
.expect("cell writer opens");
for cell in 0..N_CELLS {
let (mut raw, mut idx) = (Vec::new(), Vec::new());
for (gene_idx, gene) in dense.iter().enumerate() {
if gene[cell] > 0 {
raw.push(gene[cell]);
idx.push(gene_idx as u32);
}
}
cell_writer
.write_cell_chunk(CsrCellChunk::from_data(&raw, &idx, cell, SIZE_FACTOR, true))
.expect("write cell chunk");
}
cell_writer.finalise().expect("finalise cells");
}
fn subset() -> Vec<usize> {
let mut cells: Vec<usize> = (0..N_CELLS).filter(|c| c % 3 != 1).collect();
cells.reverse();
cells.swap(0, 50);
cells
}
#[test]
fn test_cell_subset_matches_filtered_gene_read() {
let (genes, cells) = (TempStore::new("read_g"), TempStore::new("read_c"));
let dense = synthetic_counts();
write_stores(&dense, &genes, &cells);
let gene_reader = ParallelSparseReader::new(genes.path()).unwrap();
let cell_reader = ParallelSparseReader::new(cells.path()).unwrap();
let subset = subset();
let csc = read_cell_subset_csc(&cell_reader, &subset).unwrap();
assert_eq!(csc.shape, (subset.len(), N_GENES));
let keep: indexmap::IndexSet<u32> = subset.iter().map(|&c| c as u32).collect();
let all: Vec<usize> = (0..N_GENES).collect();
for chunk in gene_reader
.read_gene_parallel_filtered(&all, &keep)
.unwrap()
{
let g = chunk.original_index;
let (lo, hi) = (csc.indptr[g] as usize, csc.indptr[g + 1] as usize);
assert_eq!(&csc.indices[lo..hi], &chunk.indices[..]);
assert_eq!(
csc.data[lo..hi].to_vec(),
chunk.data_raw.iter().collect::<Vec<u32>>()
);
}
}
#[test]
fn test_cell_subset_hvg_and_sct_match_gene_path() {
let (genes, cells) = (TempStore::new("fit_g"), TempStore::new("fit_c"));
let dense = synthetic_counts();
write_stores(&dense, &genes, &cells);
let gene_reader = ParallelSparseReader::new(genes.path()).unwrap();
let cell_reader = ParallelSparseReader::new(cells.path()).unwrap();
let subset = subset();
let local: Vec<usize> = (0..subset.len()).collect();
let csc = read_cell_subset_csc(&cell_reader, &subset).unwrap();
let mem = InMemorySparseReader::new(&csc, cell_reader.target_size()).unwrap();
let hvg_gene = get_hvg_vst(&gene_reader, &subset, 0.3, None, 0).unwrap();
let hvg_cell = get_hvg_vst(&mem, &local, 0.3, None, 0).unwrap();
assert_eq!(hvg_gene.var_std, hvg_cell.var_std);
assert_eq!(hvg_gene.mean, hvg_cell.mean);
let lib: Vec<f64> = cell_reader
.read_cell_library_sizes(&subset)
.unwrap()
.into_iter()
.map(|s| s as f64)
.collect();
let (params, opts, cov) = (
SctParams::default(),
SctStreamOpts::default(),
SctCovariates::default(),
);
let (m_gene, _) =
fit_sctransform(&gene_reader, &subset, &lib, &cov, ¶ms, None, None, opts)
.unwrap();
let (m_cell, _) =
fit_sctransform(&mem, &local, &lib, &cov, ¶ms, None, None, opts).unwrap();
assert_eq!(format!("{m_gene:?}"), format!("{m_cell:?}"));
}
#[test]
fn test_prefer_cell_subset_boundaries() {
assert!(prefer_cell_subset(100, 1_000));
assert!(!prefer_cell_subset(101, 1_000));
assert!(prefer_cell_subset(CELL_SUBSET_MAX_CELLS, 10_000_000));
assert!(!prefer_cell_subset(CELL_SUBSET_MAX_CELLS + 1, 10_000_000));
}
}
}