use rayon::prelude::*;
use std::borrow::Cow;
use std::time::Instant;
use crate::prelude::*;
use crate::single_cell::sc_analysis::vision::SignatureGenes;
fn validate_signature_indices(
signatures: &[SignatureGenes],
n_genes: usize,
) -> Result<(), BixverseErrors> {
for sig in signatures {
for &idx in sig.positive.iter().chain(sig.negative.iter()) {
if idx >= n_genes {
return Err(BixverseErrors::SliceIndexOutOfBounds {
index: idx,
len: n_genes,
});
}
}
}
Ok(())
}
fn vision_scores_for_metacell(
indices: &[u32],
values: &[f32],
signatures: &[SignatureGenes],
n_genes: usize,
scratch: &mut [f32],
) -> Vec<f32> {
for (&idx, &value) in indices.iter().zip(values.iter()) {
scratch[idx as usize] = value;
}
let mut sum = 0.0_f64;
let mut sum_sq = 0.0_f64;
for &value in values {
let value = value as f64;
sum += value;
sum_sq += value * value;
}
let mu = sum / n_genes as f64;
let sigma = ((sum_sq / n_genes as f64) - mu * mu).max(0.0).sqrt();
let scores: Vec<f32> = signatures
.iter()
.map(|sig| {
let signature_size = (sig.positive.len() + sig.negative.len()) as f64;
if signature_size == 0.0 || sigma == 0.0 {
return 0.0;
}
let sum_pos: f64 = sig.positive.iter().map(|&idx| scratch[idx] as f64).sum();
let sum_neg: f64 = sig.negative.iter().map(|&idx| scratch[idx] as f64).sum();
let mean_sig = (sum_pos - sum_neg) / signature_size;
((mean_sig - mu) / sigma) as f32
})
.collect();
for &idx in indices {
scratch[idx as usize] = 0.0;
}
scores
}
pub fn calculate_vision_metacells<T>(
matrix: &CompressedSparseData2<T, f32>,
gene_signs: &[SignatureGenes],
verbose: usize,
) -> Result<Vec<Vec<f32>>, BixverseErrors>
where
T: BixverseNumeric,
{
let verbosity = parse_verbosity_level(verbose);
matrix.validate()?;
let csr = match matrix.cs_type {
CompressedSparseFormat::Csr => Cow::Borrowed(matrix),
CompressedSparseFormat::Csc => Cow::Owned(matrix.transform()),
};
let (n_metacells, n_genes) = csr.shape;
let data_2 = csr
.data_2
.as_ref()
.ok_or(BixverseErrors::Data2NotAvailable)?;
if data_2.len() < csr.indices.len() {
return Err(BixverseErrors::DimensionMisMatchSparse {
indices_len: csr.indices.len(),
data_len: data_2.len(),
});
}
validate_signature_indices(gene_signs, n_genes)?;
let start_signatures = Instant::now();
let signature_scores: Vec<Vec<f32>> = (0..n_metacells)
.into_par_iter()
.map_init(
|| vec![0.0_f32; n_genes],
|scratch, row| {
let lo = csr.indptr[row] as usize;
let hi = csr.indptr[row + 1] as usize;
vision_scores_for_metacell(
&csr.indices[lo..hi],
&data_2[lo..hi],
gene_signs,
n_genes,
scratch,
)
},
)
.collect();
if verbosity.normal_verbosity() {
println!(
"Calculated VISION scores: {:.2?}",
start_signatures.elapsed()
);
}
Ok(signature_scores)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn csr_from_dense(dense: &[Vec<f32>]) -> CompressedSparseData2<f32, f32> {
let n_rows = dense.len();
let n_cols = dense[0].len();
let mut data: Vec<f32> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let mut indptr: Vec<u32> = vec![0];
for row in dense {
for (col, &value) in row.iter().enumerate() {
if value != 0.0 {
data.push(value);
indices.push(col as u32);
}
}
indptr.push(data.len() as u32);
}
CompressedSparseData2 {
data: data.clone(),
indices,
indptr,
cs_type: CompressedSparseFormat::Csr,
data_2: Some(data),
shape: (n_rows, n_cols),
}
}
fn reference_score(row: &[f32], sig: &SignatureGenes) -> f32 {
let n = row.len() as f64;
let sum: f64 = row.iter().map(|&v| v as f64).sum();
let sum_sq: f64 = row.iter().map(|&v| (v as f64) * (v as f64)).sum();
let mu = sum / n;
let sigma = ((sum_sq / n) - mu * mu).max(0.0).sqrt();
let size = (sig.positive.len() + sig.negative.len()) as f64;
if size == 0.0 || sigma == 0.0 {
return 0.0;
}
let sum_pos: f64 = sig.positive.iter().map(|&i| row[i] as f64).sum();
let sum_neg: f64 = sig.negative.iter().map(|&i| row[i] as f64).sum();
(((sum_pos - sum_neg) / size - mu) / sigma) as f32
}
fn toy_dense() -> Vec<Vec<f32>> {
vec![
vec![1.0, 0.0, 3.0, 0.5, 0.0, 2.0],
vec![0.0, 4.0, 0.0, 0.0, 1.5, 0.25],
vec![2.0, 2.0, 2.0, 2.0, 2.0, 2.0],
]
}
#[test]
fn test_vision_metacells_matches_dense_reference() {
let dense = toy_dense();
let matrix = csr_from_dense(&dense);
let signatures = vec![
SignatureGenes {
positive: vec![0, 2, 5],
negative: vec![],
},
SignatureGenes {
positive: vec![1, 4],
negative: vec![3],
},
];
let scores = calculate_vision_metacells(&matrix, &signatures, 0).unwrap();
assert_eq!(scores.len(), dense.len());
for (row, row_scores) in dense.iter().zip(scores.iter()) {
assert_eq!(row_scores.len(), signatures.len());
for (sig, &got) in signatures.iter().zip(row_scores.iter()) {
assert_relative_eq!(got, reference_score(row, sig), epsilon = 1e-6);
}
}
}
#[test]
fn test_vision_metacells_csc_and_csr_agree() {
let dense = toy_dense();
let csr = csr_from_dense(&dense);
let csc = csr.transform();
let signatures = vec![SignatureGenes {
positive: vec![0, 2, 5],
negative: vec![1],
}];
let from_csr = calculate_vision_metacells(&csr, &signatures, 0).unwrap();
let from_csc = calculate_vision_metacells(&csc, &signatures, 0).unwrap();
assert_eq!(from_csr.len(), from_csc.len());
for (a, b) in from_csr.iter().zip(from_csc.iter()) {
for (x, y) in a.iter().zip(b.iter()) {
assert_relative_eq!(x, y, epsilon = 1e-6);
}
}
}
#[test]
fn test_vision_metacells_scratch_is_left_clean() {
let dense = vec![
vec![5.0, 5.0, 5.0, 0.0, 0.0, 0.0],
vec![0.0, 0.0, 0.0, 1.0, 2.0, 3.0],
];
let signatures = vec![SignatureGenes {
positive: vec![0, 1, 2],
negative: vec![],
}];
let mut scratch = vec![0.0_f32; 6];
let matrix = csr_from_dense(&dense);
let data_2 = matrix.data_2.as_ref().unwrap();
let mut scores = Vec::new();
for row in 0..dense.len() {
let lo = matrix.indptr[row] as usize;
let hi = matrix.indptr[row + 1] as usize;
scores.push(vision_scores_for_metacell(
&matrix.indices[lo..hi],
&data_2[lo..hi],
&signatures,
6,
&mut scratch,
));
}
assert!(scratch.iter().all(|&v| v == 0.0));
assert_relative_eq!(scores[1][0], reference_score(&dense[1], &signatures[0]));
}
#[test]
fn test_vision_metacells_negative_genes_subtract() {
let dense = vec![vec![1.0, 0.0, 3.0, 0.5, 0.0, 2.0]];
let matrix = csr_from_dense(&dense);
let positive_only = vec![SignatureGenes {
positive: vec![0, 2],
negative: vec![],
}];
let with_negative = vec![SignatureGenes {
positive: vec![0, 2],
negative: vec![5],
}];
let pos = calculate_vision_metacells(&matrix, &positive_only, 0).unwrap()[0][0];
let mixed = calculate_vision_metacells(&matrix, &with_negative, 0).unwrap()[0][0];
assert!(mixed < pos);
assert_relative_eq!(mixed, reference_score(&dense[0], &with_negative[0]));
}
#[test]
fn test_vision_metacells_degenerate_signatures() {
let dense = vec![vec![1.0, 0.0, 3.0], vec![2.0, 2.0, 2.0]];
let matrix = csr_from_dense(&dense);
let signatures = vec![
SignatureGenes {
positive: vec![],
negative: vec![],
},
SignatureGenes {
positive: vec![0, 2],
negative: vec![],
},
];
let scores = calculate_vision_metacells(&matrix, &signatures, 0).unwrap();
assert_eq!(scores[0][0], 0.0);
assert_eq!(scores[1][0], 0.0);
assert_eq!(scores[1][1], 0.0);
}
#[test]
fn test_vision_metacells_rejects_out_of_range_gene() {
let matrix = csr_from_dense(&toy_dense());
let signatures = vec![SignatureGenes {
positive: vec![0],
negative: vec![6],
}];
let res = calculate_vision_metacells(&matrix, &signatures, 0);
assert!(matches!(
res,
Err(BixverseErrors::SliceIndexOutOfBounds { index: 6, len: 6 })
));
}
#[test]
fn test_vision_metacells_rejects_a_gene_index_past_the_axis() {
let mut matrix = csr_from_dense(&toy_dense());
let last = matrix.indices.len() - 1;
matrix.indices[last] = 99;
let signatures = vec![SignatureGenes {
positive: vec![0],
negative: vec![],
}];
assert!(matches!(
calculate_vision_metacells(&matrix, &signatures, 0),
Err(BixverseErrors::SliceIndexOutOfBounds { index: 99, len: 6 })
));
}
#[test]
fn test_vision_metacells_rejects_an_inconsistent_indptr() {
let mut matrix = csr_from_dense(&toy_dense());
matrix.indptr.push(*matrix.indptr.last().unwrap());
let signatures = vec![SignatureGenes {
positive: vec![0],
negative: vec![],
}];
assert!(matches!(
calculate_vision_metacells(&matrix, &signatures, 0),
Err(BixverseErrors::SparseIndptrInvalid { .. })
));
}
#[test]
fn test_vision_metacells_requires_data_2() {
let mut matrix = csr_from_dense(&toy_dense());
matrix.data_2 = None;
let signatures = vec![SignatureGenes {
positive: vec![0],
negative: vec![],
}];
let res = calculate_vision_metacells(&matrix, &signatures, 0);
assert!(matches!(res, Err(BixverseErrors::Data2NotAvailable)));
}
}