use rayon::prelude::*;
use crate::prelude::*;
use bixverse_core::core::math::vector_helpers::{quantile, quantile_sorted};
use super::params::DeviantsParams;
fn build_dense_views(
raw_umis: &CompressedSparseData2<u32, f32>,
umis_per_cell: &[f32],
) -> (Vec<f32>, Vec<f32>) {
let n_cells = raw_umis.shape.0;
let n_genes = raw_umis.shape.1;
let total = n_cells * n_genes;
let mut umi = vec![0.0_f32; total];
let mut frac = vec![0.0_f32; total];
for cell in 0..n_cells {
let lib = umis_per_cell[cell];
let inv_lib = if lib > 0.0 { 1.0 / lib } else { 0.0 };
let row_base = cell * n_genes;
let start = raw_umis.indptr[cell] as usize;
let end = raw_umis.indptr[cell + 1] as usize;
for idx in start..end {
let g = raw_umis.indices[idx] as usize;
let u = raw_umis.data[idx] as f32;
umi[row_base + g] = u;
frac[row_base + g] = u * inv_lib;
}
}
(umi, frac)
}
pub(super) fn bucket_cells_by_candidate(
candidate_of_cell: &[i32],
n_candidates: usize,
) -> Vec<Vec<usize>> {
let mut buckets = vec![Vec::new(); n_candidates];
for (cell, &c) in candidate_of_cell.iter().enumerate() {
if c >= 0 && (c as usize) < n_candidates {
buckets[c as usize].push(cell);
}
}
buckets
}
fn compute_regularisation_per_cell(
umis_per_cell: &[f32],
candidate_of_cell: &[i32],
n_candidates: usize,
regularisation_quantile: f32,
) -> Vec<f32> {
let n_cells = umis_per_cell.len();
let mut reg = vec![0.0_f32; n_cells];
let buckets = bucket_cells_by_candidate(candidate_of_cell, n_candidates);
let per_candidate: Vec<Vec<f32>> = buckets
.par_iter()
.map(|cells| {
if cells.is_empty() {
return Vec::new();
}
let mut sizes: Vec<f32> = cells.iter().map(|&i| umis_per_cell[i]).collect();
sizes.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let q = quantile_sorted(&sizes, regularisation_quantile);
let floor = q.max(1.0);
cells
.iter()
.map(|&i| 1.0 / umis_per_cell[i].max(floor))
.collect()
})
.collect();
for (cells, values) in buckets.iter().zip(per_candidate) {
for (&i, v) in cells.iter().zip(values) {
reg[i] = v;
}
}
reg
}
fn compute_log_fractions(
fraction_dense: &[f32],
reg_per_cell: &[f32],
n_cells: usize,
n_genes: usize,
) -> Vec<f32> {
let mut out = vec![0.0_f32; n_cells * n_genes];
out.par_chunks_mut(n_genes)
.zip(fraction_dense.par_chunks(n_genes))
.zip(reg_per_cell.par_iter())
.for_each(|((out_row, frac_row), ®)| {
for (o, &f) in out_row.iter_mut().zip(frac_row.iter()) {
let v = f + reg;
*o = if v > 0.0 { v.log2() } else { f32::NEG_INFINITY };
}
});
out
}
#[allow(clippy::too_many_arguments)]
fn compute_cell_gaps(
umi_dense: &[f32],
log_fraction_dense: &[f32],
cells_of_candidate: &[Vec<usize>],
deviant_per_cell: &[bool],
active_per_cell: &[bool],
min_gap_per_gene: &[f32],
n_cells: usize,
n_genes: usize,
gap_skip_cells: usize,
max_gap_cells_count: usize,
max_gap_cells_fraction: f32,
min_compare_umis: f32,
max_gap_per_cell: &mut [f32],
) {
let max_gap_addr = max_gap_per_cell.as_mut_ptr() as usize;
cells_of_candidate.par_iter().for_each(|members| {
let active_candidate = members.iter().any(|&cell| active_per_cell[cell]);
let cells: Vec<usize> = members
.iter()
.copied()
.filter(|&cell| !deviant_per_cell[cell])
.collect();
let candidate_cells_count = cells.len();
if !active_candidate || candidate_cells_count < 4 {
return;
}
let max_deviant_of_candidate = (candidate_cells_count - gap_skip_cells).min(
max_gap_cells_count
.max((max_gap_cells_fraction * candidate_cells_count as f32 + 0.5) as usize),
);
let max_gap = unsafe { std::slice::from_raw_parts_mut(max_gap_addr as *mut f32, n_cells) };
let mut umi_gene = vec![0.0_f32; candidate_cells_count];
let mut lf_gene = vec![0.0_f32; candidate_cells_count];
let mut sorted: Vec<usize> = (0..candidate_cells_count).collect();
for gene in 0..n_genes {
let min_gap = min_gap_per_gene[gene];
for (slot, &c) in umi_gene.iter_mut().zip(cells.iter()) {
*slot = umi_dense[c * n_genes + gene];
}
if !umi_gene.iter().any(|&u| u * 2.0 + 1e-6 > min_compare_umis) {
continue;
}
for (slot, &c) in lf_gene.iter_mut().zip(cells.iter()) {
*slot = log_fraction_dense[c * n_genes + gene];
}
for (pos, slot) in sorted.iter_mut().enumerate() {
*slot = pos;
}
sorted.sort_unstable_by(|&a, &b| {
lf_gene[a]
.partial_cmp(&lf_gene[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let max_cell_position = ((candidate_cells_count - 1) / 2).min(max_deviant_of_candidate);
for cell_pos in 0..max_cell_position {
let first = sorted[cell_pos];
let second = sorted[cell_pos + gap_skip_cells];
let first_u = umi_gene[first];
let second_u = umi_gene[second];
if first_u + second_u + 1e-6 < min_compare_umis {
continue;
}
let first_lf = lf_gene[first];
let second_lf = lf_gene[second];
let full_gap = second_lf - first_lf - min_gap;
if full_gap < 0.0 {
continue;
}
let mut stop_cell_position = cell_pos + 1;
if gap_skip_cells > 1 {
let middle = sorted[cell_pos + 1];
let middle_lf = lf_gene[middle];
let start_gap = 2.0 * (middle_lf - first_lf) - min_gap;
if start_gap < full_gap {
stop_cell_position += 1;
}
}
for gap_cell_position in 0..=stop_cell_position {
let gap_cell = cells[sorted[gap_cell_position]];
let cur = max_gap[gap_cell];
if full_gap > cur {
max_gap[gap_cell] = full_gap;
}
}
}
for cell_offset in 0..max_cell_position {
let cell_pos = candidate_cells_count - 1 - cell_offset - gap_skip_cells;
let first = sorted[cell_pos];
let second = sorted[cell_pos + gap_skip_cells];
let first_u = umi_gene[first];
let second_u = umi_gene[second];
if first_u + second_u < min_compare_umis {
continue;
}
let first_lf = lf_gene[first];
let second_lf = lf_gene[second];
let full_gap = second_lf - first_lf - min_gap;
if full_gap <= 0.0 {
continue;
}
let mut start_cell_position = cell_pos + 1;
if gap_skip_cells > 1 {
let middle = sorted[cell_pos + 1];
let middle_lf = lf_gene[middle];
let start_gap = 2.0 * (middle_lf - first_lf) - min_gap;
if start_gap < full_gap {
start_cell_position += 1;
}
}
for gap_cell_position in start_cell_position..candidate_cells_count {
let gap_cell = cells[sorted[gap_cell_position]];
let cur = max_gap[gap_cell];
if full_gap > cur {
max_gap[gap_cell] = full_gap;
}
}
}
}
});
}
pub fn find_deviant_cells(
raw_umis: &CompressedSparseData2<u32, f32>,
umis_per_cell: &[f32],
candidate_of_cell: &[i32],
params: &DeviantsParams,
) -> Vec<bool> {
let n_cells = raw_umis.shape.0;
let n_genes = raw_umis.shape.1;
assert_eq!(umis_per_cell.len(), n_cells);
assert_eq!(candidate_of_cell.len(), n_cells);
if n_cells <= params.max_gap_cells_count {
return vec![false; n_cells];
}
let n_candidates = (candidate_of_cell.iter().copied().max().unwrap_or(-1) + 1) as usize;
if n_candidates == 0 {
return vec![false; n_cells];
}
let (umi_dense, fraction_dense) = build_dense_views(raw_umis, umis_per_cell);
let regularisation_per_cell = compute_regularisation_per_cell(
umis_per_cell,
candidate_of_cell,
n_candidates,
params.cells_regularisation_quantile,
);
let log_fraction_dense =
compute_log_fractions(&fraction_dense, ®ularisation_per_cell, n_cells, n_genes);
let min_gap_per_gene = vec![params.min_gene_fold_factor; n_genes];
let max_cell_fraction = params.max_cell_fraction.clamp(1e-6, 1.0);
let mut min_gene_fold_factor = params.min_gene_fold_factor;
let mut min_gap_per_gene = min_gap_per_gene;
let gap_skip_cells = params.gap_skip_cells.clamp(1, 3);
let cells_of_candidate = bucket_cells_by_candidate(candidate_of_cell, n_candidates);
let mut deviant_per_cell = vec![false; n_cells];
loop {
let mut new_deviant_per_cell = vec![true; n_cells];
deviant_per_cell.iter_mut().for_each(|d| *d = false);
let mut deviants_fraction = 0.0_f64;
while deviants_fraction <= max_cell_fraction as f64 {
let mut max_gap_per_cell = vec![0.0_f32; n_cells];
compute_cell_gaps(
&umi_dense,
&log_fraction_dense,
&cells_of_candidate,
&deviant_per_cell,
&new_deviant_per_cell,
&min_gap_per_gene,
n_cells,
n_genes,
gap_skip_cells,
params.max_gap_cells_count,
params.max_gap_cells_fraction,
params.min_compare_umis,
&mut max_gap_per_cell,
);
let large_gap_count = max_gap_per_cell.iter().filter(|&&v| v > 0.0).count();
let remaining_fraction = 1.0 - (max_cell_fraction as f64 - deviants_fraction).max(0.0);
let gap_threshold = if (large_gap_count as f64) / (n_cells as f64) <= remaining_fraction
{
0.0_f32
} else {
quantile(&max_gap_per_cell, remaining_fraction as f32)
};
new_deviant_per_cell = max_gap_per_cell
.iter()
.zip(deviant_per_cell.iter())
.map(|(&g, &d)| g > gap_threshold && !d)
.collect();
let new_count = new_deviant_per_cell.iter().filter(|&&v| v).count();
if new_count == 0 {
break;
}
deviants_fraction += new_count as f64 / n_cells as f64;
for (d, &nd) in deviant_per_cell.iter_mut().zip(new_deviant_per_cell.iter()) {
*d |= nd;
}
}
if deviants_fraction <= max_cell_fraction as f64 {
return deviant_per_cell;
}
for g in min_gap_per_gene.iter_mut() {
*g += 1.0 / 8.0;
}
min_gene_fold_factor += 1.0 / 8.0;
let _ = min_gene_fold_factor; }
}
#[cfg(test)]
mod tests {
use super::*;
use bixverse_core::core::math::sparse::CompressedSparseFormat;
fn make_csr(rows: Vec<Vec<(usize, u32)>>, n_genes: usize) -> CompressedSparseData2<u32, f32> {
let mut data = Vec::new();
let mut indices = Vec::new();
let mut indptr = vec![0usize];
for row in &rows {
for &(g, u) in row {
data.push(u);
indices.push(g);
}
indptr.push(data.len());
}
CompressedSparseData2 {
data,
indices: indices.index_cast(),
indptr: indptr.index_cast(),
cs_type: CompressedSparseFormat::Csr,
data_2: None,
shape: (rows.len(), n_genes),
}
}
#[test]
fn quantile_known_values() {
let v = vec![1.0_f32, 2.0, 3.0, 4.0, 5.0];
assert!((quantile(&v, 0.0) - 1.0).abs() < 1e-6);
assert!((quantile(&v, 1.0) - 5.0).abs() < 1e-6);
assert!((quantile(&v, 0.5) - 3.0).abs() < 1e-6);
}
#[test]
fn no_deviants_when_metacell_too_small() {
let raw = make_csr(
vec![
vec![(0, 100), (1, 1)],
vec![(0, 99), (1, 2)],
vec![(0, 101), (1, 1)],
],
2,
);
let umis = vec![101.0_f32, 101.0, 102.0];
let cand = vec![0_i32, 0, 0];
let params = DeviantsParams::default();
let dev = find_deviant_cells(&raw, &umis, &cand, ¶ms);
assert_eq!(dev, vec![false, false, false]);
}
#[test]
fn detects_clear_outlier_in_metacell() {
let raw = make_csr(
vec![
vec![(0, 100), (1, 50)],
vec![(0, 102), (1, 50)],
vec![(0, 98), (1, 50)],
vec![(0, 101), (1, 50)],
vec![(0, 1), (1, 50)], vec![(0, 99), (1, 50)],
vec![(0, 103), (1, 50)],
vec![(0, 97), (1, 50)],
],
2,
);
let umis: Vec<f32> = vec![150.0, 152.0, 148.0, 151.0, 51.0, 149.0, 153.0, 147.0];
let cand = vec![0_i32; 8];
let params = DeviantsParams {
min_gene_fold_factor: 1.0, min_compare_umis: 5.0,
gap_skip_cells: 1,
max_cell_fraction: 0.5,
max_gap_cells_count: 2,
max_gap_cells_fraction: 0.4,
cells_regularisation_quantile: 0.1,
..Default::default()
};
let dev = find_deviant_cells(&raw, &umis, &cand, ¶ms);
assert!(dev[4], "expected cell 4 to be flagged: {:?}", dev);
let well_behaved = [0_usize, 1, 2, 3, 5, 6, 7];
let n_clean = well_behaved.iter().filter(|&&i| !dev[i]).count();
assert!(
n_clean >= 5,
"expected >= 5 clean well-behaved cells, got {}: {:?}",
n_clean,
dev
);
}
}