use indexmap::IndexSet;
use rand::prelude::IndexedRandom;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use std::time::Instant;
use crate::prelude::*;
struct GeneBins {
gene_to_bin: FxHashMap<usize, usize>,
bins: Vec<Vec<usize>>,
}
fn get_average_expression<S: SingleCellReading>(
gene_reader: &S,
cell_set: &IndexSet<u32>,
streaming: bool,
) -> Result<Vec<(usize, f32)>, ScErrors> {
let total_genes = gene_reader.get_header().total_genes;
if streaming {
const CHUNK_SIZE: usize = 500;
let gene_indices: Vec<usize> = (0..total_genes).collect();
let mut results: Vec<(usize, f32)> = Vec::with_capacity(total_genes);
for chunk in gene_indices.chunks(CHUNK_SIZE) {
let gene_chunks = gene_reader.read_gene_parallel(chunk)?;
let chunk_results: Vec<(usize, f32)> = gene_chunks
.par_iter()
.map(|gene| gene.calculate_avg_exp(cell_set))
.collect();
results.extend(chunk_results);
}
Ok(results)
} else {
let gene_chunks = gene_reader.get_all_genes()?;
let results: Vec<(usize, f32)> = gene_chunks
.par_iter()
.map(|gene| gene.calculate_avg_exp(cell_set))
.collect();
Ok(results)
}
}
fn create_expression_bins(gene_means: &[(usize, f32)], nbin: usize, seed: &usize) -> GeneBins {
let mut sorted_genes = gene_means.to_vec();
let mut rng = StdRng::seed_from_u64(*seed as u64);
for (_, exp) in sorted_genes.iter_mut() {
*exp += rng.random::<f32>() / 1e30;
}
sorted_genes.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
let total_genes = sorted_genes.len();
let genes_per_bin = (total_genes as f32 / nbin as f32).ceil() as usize;
let mut gene_to_bin = FxHashMap::default();
let mut bins: Vec<Vec<usize>> = vec![Vec::new(); nbin];
for (i, (gene_idx, _)) in sorted_genes.iter().enumerate() {
let bin_id = (i / genes_per_bin).min(nbin - 1);
gene_to_bin.insert(*gene_idx, bin_id);
bins[bin_id].push(*gene_idx);
}
GeneBins { gene_to_bin, bins }
}
fn sample_control_genes(
gene_set: &[usize],
gene_bins: &GeneBins,
ctrl: usize,
rng: &mut impl Rng,
) -> Vec<usize> {
let mut controls = IndexSet::new();
for &gene_idx in gene_set {
if let Some(&bin_id) = gene_bins.gene_to_bin.get(&gene_idx) {
let bin_genes = &gene_bins.bins[bin_id];
let sampled = bin_genes.choose_multiple(rng, ctrl.min(bin_genes.len()));
controls.extend(sampled.copied());
}
}
controls.into_iter().collect()
}
fn validate_gene_indices(gene_sets: &[Vec<usize>], n_genes: usize) -> Result<(), ScErrors> {
for gene_set in gene_sets {
for &gene in gene_set {
if gene >= n_genes {
return Err(CoreErrors::SliceIndexOutOfBounds {
index: gene,
len: n_genes,
}
.into());
}
}
}
Ok(())
}
#[inline]
fn mean_over_genes(dense: &[f32], genes: &[usize]) -> f32 {
if genes.is_empty() {
return 0.0;
}
genes.iter().map(|&idx| dense[idx]).sum::<f32>() / genes.len() as f32
}
fn calculate_cell_module_scores(
cell: &CsrCellChunk,
gene_sets: &[Vec<usize>],
control_sets: &[Vec<usize>],
scratch: &mut [f32],
) -> Vec<f32> {
for (&idx, value) in cell.indices.iter().zip(cell.data_norm.iter()) {
scratch[idx as usize] = value.to_f32();
}
let scores: Vec<f32> = gene_sets
.iter()
.zip(control_sets.iter())
.map(|(gene_set, control_set)| {
mean_over_genes(scratch, gene_set) - mean_over_genes(scratch, control_set)
})
.collect();
for &idx in &cell.indices {
scratch[idx as usize] = 0.0;
}
scores
}
fn calculate_module_scores<S: SingleCellReading>(
cell_reader: &S,
gene_sets: &[Vec<usize>],
control_sets: &[Vec<usize>],
cells_to_keep: &[usize],
n_genes: usize,
) -> Result<Vec<Vec<f32>>, ScErrors> {
let cell_chunks = cell_reader.read_cells_parallel(cells_to_keep)?;
let all_scores: Vec<Vec<f32>> = cell_chunks
.par_iter()
.map_init(
|| vec![0.0_f32; n_genes],
|scratch, cell| calculate_cell_module_scores(cell, gene_sets, control_sets, scratch),
)
.collect();
let mut results: Vec<Vec<f32>> = vec![Vec::with_capacity(cell_chunks.len()); gene_sets.len()];
for cell_scores in all_scores {
for (module_idx, score) in cell_scores.into_iter().enumerate() {
results[module_idx].push(score);
}
}
Ok(results)
}
fn calculate_module_scores_streaming<S: SingleCellReading>(
cell_reader: &S,
gene_sets: &[Vec<usize>],
control_sets: &[Vec<usize>],
cells_to_keep: &[usize],
n_genes: usize,
verbose: bool,
) -> Result<Vec<Vec<f32>>, ScErrors> {
const CHUNK_SIZE: usize = 50000;
let total_chunks = cells_to_keep.len().div_ceil(CHUNK_SIZE);
let mut results: Vec<Vec<f32>> = vec![Vec::with_capacity(cells_to_keep.len()); gene_sets.len()];
for (chunk_idx, cell_indices_chunk) in cells_to_keep.chunks(CHUNK_SIZE).enumerate() {
let start = Instant::now();
let cell_chunks = cell_reader.read_cells_parallel(cell_indices_chunk)?;
let chunk_scores: Vec<Vec<f32>> = cell_chunks
.par_iter()
.map_init(
|| vec![0.0_f32; n_genes],
|scratch, cell| {
calculate_cell_module_scores(cell, gene_sets, control_sets, scratch)
},
)
.collect();
for cell_scores in chunk_scores {
for (module_idx, score) in cell_scores.into_iter().enumerate() {
results[module_idx].push(score);
}
}
if verbose {
let elapsed = start.elapsed();
let pct = ((chunk_idx + 1) as f32 / total_chunks as f32) * 100.0;
println!(
"Chunk {} of {} (took {:.2?}, {:.1}% complete)",
chunk_idx + 1,
total_chunks,
elapsed,
pct
);
}
}
Ok(results)
}
#[allow(clippy::too_many_arguments)]
pub fn calculate_module_scores_main<S: SingleCellReading>(
gene_reader: &S,
cell_reader: &S,
gene_sets: &[Vec<usize>],
cells_to_use: &[usize],
nbin: usize,
ctrl: usize,
streaming: bool,
seed: usize,
verbose: usize,
) -> Result<Vec<Vec<f32>>, ScErrors> {
let verbosity = parse_verbosity_level(verbose);
let n_genes = cell_reader.get_header().total_genes;
let n_genes_gene_store = gene_reader.get_header().total_genes;
if n_genes != n_genes_gene_store {
return Err(ScErrors::GeneAxisMismatch {
gene_store: n_genes_gene_store,
cell_store: n_genes,
});
}
validate_gene_indices(gene_sets, n_genes)?;
let cell_set: IndexSet<u32> = cells_to_use.iter().map(|&x| x as u32).collect();
let start_total = Instant::now();
if verbosity.normal_verbosity() {
println!("Calculating the average expression across the cells.")
}
let start_avg_exp = Instant::now();
let avg_exp = get_average_expression(gene_reader, &cell_set, streaming)?;
let end_evg_exp = start_avg_exp.elapsed();
if verbosity.normal_verbosity() {
println!(
"Finished the calculation of the avg gene expression in {:.2?}",
end_evg_exp
);
println!("Calculating the module scores now.")
}
let start_modules = Instant::now();
let gene_bins = create_expression_bins(&avg_exp, nbin, &seed);
let mut rng = StdRng::seed_from_u64(seed as u64);
let control_sets: Vec<Vec<usize>> = gene_sets
.iter()
.map(|gene_set| sample_control_genes(gene_set, &gene_bins, ctrl, &mut rng))
.collect();
validate_gene_indices(&control_sets, n_genes)?;
let module_scores = if streaming {
calculate_module_scores_streaming(
cell_reader,
gene_sets,
&control_sets,
cells_to_use,
n_genes,
verbosity.detailed_verbosity(),
)
} else {
calculate_module_scores(cell_reader, gene_sets, &control_sets, cells_to_use, n_genes)
}?;
let end_modules = start_modules.elapsed();
let end_total = start_total.elapsed();
if verbosity.normal_verbosity() {
println!(
"Finished the calculation of the modules in {:.2?}",
end_modules
);
println!("Total runtime: {:.2?}", end_total)
}
Ok(module_scores)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn chunk_from_dense(row: &[u32], original_index: usize) -> CsrCellChunk {
let idx: Vec<u32> = (0..row.len() as u32)
.filter(|&g| row[g as usize] > 0)
.collect();
let data: Vec<u32> = idx.iter().map(|&g| row[g as usize]).collect();
CsrCellChunk::from_data(&data, &idx, original_index, 1e4, true)
}
fn densify(cell: &CsrCellChunk, n_genes: usize) -> Vec<f32> {
let mut dense = vec![0.0_f32; n_genes];
for (&idx, value) in cell.indices.iter().zip(cell.data_norm.iter()) {
dense[idx as usize] = value.to_f32();
}
dense
}
fn reference_score(dense: &[f32], gene_set: &[usize], control_set: &[usize]) -> f32 {
let mean = |genes: &[usize]| -> f32 {
if genes.is_empty() {
return 0.0;
}
genes.iter().map(|&g| dense[g]).sum::<f32>() / genes.len() as f32
};
mean(gene_set) - mean(control_set)
}
fn toy_cells() -> Vec<CsrCellChunk> {
vec![
chunk_from_dense(&[5, 0, 3, 0, 11, 0, 2, 0], 0),
chunk_from_dense(&[0, 7, 0, 1, 0, 4, 0, 9], 1),
chunk_from_dense(&[1, 1, 0, 0, 6, 0, 0, 2], 2),
]
}
#[test]
fn test_validate_gene_indices_bounds_every_set() {
let ok = vec![vec![0, 3], vec![7]];
assert!(validate_gene_indices(&ok, 8).is_ok());
let bad = vec![vec![0, 3], vec![8]];
assert!(matches!(
validate_gene_indices(&bad, 8),
Err(ScErrors::Core(CoreErrors::SliceIndexOutOfBounds {
index: 8,
len: 8
}))
));
assert!(validate_gene_indices(&[], 8).is_ok());
assert!(validate_gene_indices(&[vec![]], 8).is_ok());
}
#[test]
fn test_module_scores_match_the_dense_reference() {
let n_genes = 8;
let cells = toy_cells();
let gene_sets = vec![vec![0, 2, 4], vec![1, 7]];
let control_sets = vec![vec![1, 3, 5, 6, 7], vec![0, 2, 4, 6]];
let mut scratch = vec![0.0_f32; n_genes];
for cell in &cells {
let got = calculate_cell_module_scores(cell, &gene_sets, &control_sets, &mut scratch);
let dense = densify(cell, n_genes);
assert_eq!(got.len(), gene_sets.len());
for (module, &score) in got.iter().enumerate() {
assert_relative_eq!(
score,
reference_score(&dense, &gene_sets[module], &control_sets[module]),
epsilon = 1e-6
);
}
}
}
#[test]
fn test_module_scores_scratch_is_left_clean() {
let n_genes = 8;
let cells = vec![
chunk_from_dense(&[9, 9, 9, 9, 0, 0, 0, 0], 0),
chunk_from_dense(&[0, 0, 0, 0, 1, 2, 3, 4], 1),
];
let gene_sets = vec![vec![0, 1, 2, 3]];
let control_sets = vec![vec![4, 5]];
let mut scratch = vec![0.0_f32; n_genes];
let mut scores = Vec::new();
for cell in &cells {
scores.push(calculate_cell_module_scores(
cell,
&gene_sets,
&control_sets,
&mut scratch,
));
}
assert!(scratch.iter().all(|&v| v == 0.0));
let dense = densify(&cells[1], n_genes);
assert_relative_eq!(
scores[1][0],
reference_score(&dense, &gene_sets[0], &control_sets[0]),
epsilon = 1e-6
);
}
#[test]
fn test_module_scores_handle_empty_sets() {
let n_genes = 8;
let cells = toy_cells();
let gene_sets = vec![vec![], vec![0, 4]];
let control_sets = vec![vec![1, 3], vec![]];
let mut scratch = vec![0.0_f32; n_genes];
let got = calculate_cell_module_scores(&cells[0], &gene_sets, &control_sets, &mut scratch);
let dense = densify(&cells[0], n_genes);
assert_relative_eq!(
got[0],
-reference_score(&dense, &control_sets[0], &[]),
epsilon = 1e-6
);
assert_relative_eq!(
got[1],
reference_score(&dense, &gene_sets[1], &[]),
epsilon = 1e-6
);
}
}