use rayon::prelude::*;
use std::time::Instant;
use thousands::Separable;
use indexmap::IndexSet;
use crate::errors::BixverseErrors;
use crate::prelude::*;
use crate::single_cell::sc_data::data_io::{CscGeneChunk, RawCounts, SingleCellReading};
use crate::utils::simd::{sum_squared_dev_widen_simd_f32, sum_widen_simd_f32};
use super::sctransform::stream::SctStreamOpts;
pub trait ResidualSource: Sync {
fn genes(&self) -> &[usize];
fn position(&self, gene: usize) -> Option<usize> {
self.genes().binary_search(&gene).ok()
}
fn n_cells(&self) -> usize;
fn group_of_cell(&self) -> &[u32];
fn n_groups(&self) -> usize;
fn residual_row(
&self,
counts: &[f64],
indices: &[u32],
gene_pos: usize,
out: &mut [f32],
) -> Result<(), BixverseErrors>;
}
pub fn validate_groups(group_of_cell: &[u32], n_cells: usize) -> Result<usize, BixverseErrors> {
if group_of_cell.len() != n_cells {
return Err(BixverseErrors::ResidualGroupLabelLengthMismatch {
n_labels: group_of_cell.len(),
n_cells,
});
}
if n_cells == 0 {
return Err(BixverseErrors::ResidualEmptySelectionRequest {
what: "no cells were selected",
});
}
let max = group_of_cell.iter().copied().max().unwrap_or(0) as usize;
if max >= n_cells {
return Err(BixverseErrors::ResidualGroupLabelOutOfRange {
label: max,
n_groups: n_cells,
});
}
let implied = max + 1;
let mut sizes = vec![0_usize; implied];
for &g in group_of_cell {
sizes[g as usize] += 1;
}
if let Some(group) = sizes.iter().position(|&s| s == 0) {
return Err(BixverseErrors::ResidualEmptyGroup {
group,
n_groups: implied,
});
}
Ok(implied)
}
pub fn distinct_cell_set(cell_indices: &[usize]) -> Result<IndexSet<u32>, BixverseErrors> {
let cell_set: IndexSet<u32> = cell_indices.iter().map(|&c| c as u32).collect();
if cell_set.len() != cell_indices.len() {
return Err(BixverseErrors::ResidualDuplicateCells {
n_unique: cell_set.len(),
n_cells: cell_indices.len(),
});
}
Ok(cell_set)
}
pub fn assert_ascending(genes: &[usize], name: &'static str) -> Result<(), BixverseErrors> {
if genes.windows(2).any(|w| w[0] >= w[1]) {
return Err(BixverseErrors::ResidualGenesNotAscending { name });
}
Ok(())
}
pub fn validate_clip_range(clip: (f64, f64)) -> Result<(), BixverseErrors> {
let (lo, hi) = clip;
if lo.is_nan() || hi.is_nan() || lo > hi {
return Err(BixverseErrors::ResidualInvalidClipRange { lo, hi });
}
Ok(())
}
pub fn validate_residual_row_inputs(
counts: &[f64],
indices: &[u32],
n_cells: usize,
) -> Result<(), BixverseErrors> {
if counts.len() != indices.len() {
return Err(BixverseErrors::ResidualRowLengthMismatch {
n_counts: counts.len(),
n_indices: indices.len(),
});
}
if let Some(&bad) = indices.iter().find(|&&i| i as usize >= n_cells) {
return Err(BixverseErrors::ResidualCellIndexOutOfRange {
index: bad as usize,
n_cells,
});
}
Ok(())
}
pub fn intersect_gene_sets(per_group: &[&[usize]]) -> Result<Vec<usize>, BixverseErrors> {
let n_groups = per_group.len();
let Some((first, rest)) = per_group.split_first() else {
return Err(BixverseErrors::ResidualEmptySelectionRequest {
what: "no gene sets were given to intersect",
});
};
for set in per_group {
assert_ascending(set, "model genes")?;
}
let shared: Vec<usize> = first
.iter()
.copied()
.filter(|g| rest.iter().all(|set| set.binary_search(g).is_ok()))
.collect();
if shared.is_empty() {
return Err(BixverseErrors::ResidualEmptyGeneIntersection { n_groups });
}
Ok(shared)
}
pub fn chunk_counts(chunk: &CscGeneChunk) -> Vec<f64> {
match &chunk.data_raw {
RawCounts::U16(v) => v.iter().map(|&x| x as f64).collect(),
RawCounts::U32(v) => v.iter().map(|&x| x as f64).collect(),
}
}
fn grouped_sample_variance(
row: &[f32],
group_of_cell: &[u32],
n_groups: usize,
gene: usize,
) -> Result<Vec<f64>, BixverseErrors> {
let (n, sum, sum_sq) = if n_groups == 1 {
if let Some(&g) = group_of_cell.iter().find(|&&g| g != 0) {
return Err(BixverseErrors::ResidualGroupLabelOutOfRange {
label: g as usize,
n_groups,
});
}
let len = row.len().min(group_of_cell.len());
let row = &row[..len];
(
vec![len],
vec![sum_widen_simd_f32(row)],
vec![sum_squared_dev_widen_simd_f32(row, 0.0)],
)
} else {
let mut n = vec![0_usize; n_groups];
let mut sum = vec![0.0_f64; n_groups];
let mut sum_sq = vec![0.0_f64; n_groups];
for (&x, &g) in row.iter().zip(group_of_cell) {
let g = g as usize;
if g >= n_groups {
return Err(BixverseErrors::ResidualGroupLabelOutOfRange { label: g, n_groups });
}
let x = x as f64;
n[g] += 1;
sum[g] += x;
sum_sq[g] += x * x;
}
(n, sum, sum_sq)
};
(0..n_groups)
.map(|g| {
if n[g] < 2 {
return Ok(0.0);
}
let n_f = n[g] as f64;
let mean = sum[g] / n_f;
let var = (sum_sq[g] - n_f * mean * mean) / (n_f - 1.0);
if !var.is_finite() {
return Err(BixverseErrors::ResidualNonFiniteVariance { gene, value: var });
}
Ok(var.max(0.0))
})
.collect()
}
pub fn residual_variance<S: SingleCellReading>(
reader: &S,
source: &dyn ResidualSource,
cell_indices: &[usize],
opts: SctStreamOpts,
) -> Result<Vec<Vec<f64>>, BixverseErrors> {
if !reader.is_gene_based() {
return Err(BixverseErrors::ReaderModeMismatch {
actual: "cell-based",
requested: "gene-based",
});
}
if source.n_cells() != cell_indices.len() {
return Err(BixverseErrors::LengthMismatch {
name: "cell_indices",
expected: source.n_cells(),
found: cell_indices.len(),
});
}
let verbosity = parse_verbosity_level(opts.verbose);
let start = Instant::now();
let genes = source.genes();
let n_genes = genes.len();
let n_cells = cell_indices.len();
let n_groups = source.n_groups();
let group_of_cell = source.group_of_cell();
let cell_set = distinct_cell_set(cell_indices)?;
let step = opts.gene_batch_size.unwrap_or(n_genes).max(1);
let mut out = vec![vec![0.0_f64; n_genes]; n_groups];
let mut done = 0_usize;
for block in genes.chunks(step) {
let chunks = reader.read_gene_parallel_filtered(block, &cell_set)?;
let vars: Vec<(usize, Vec<f64>)> = chunks
.par_iter()
.map_init(
|| (Vec::<f64>::new(), vec![0.0_f32; n_cells]),
|(counts, row), chunk| {
let pos = source.position(chunk.original_index).ok_or(
BixverseErrors::SctGeneNotModelled {
gene: chunk.original_index,
},
)?;
counts.clear();
match &chunk.data_raw {
RawCounts::U16(v) => counts.extend(v.iter().map(|&x| x as f64)),
RawCounts::U32(v) => counts.extend(v.iter().map(|&x| x as f64)),
}
row.fill(0.0);
source.residual_row(counts, &chunk.indices, pos, row)?;
let vars = grouped_sample_variance(
row,
group_of_cell,
n_groups,
chunk.original_index,
)?;
Ok((pos, vars))
},
)
.collect::<Result<Vec<_>, BixverseErrors>>()?;
for (pos, per_group) in vars {
for (g, v) in per_group.into_iter().enumerate() {
out[g][pos] = v;
}
}
let prev = done;
done += block.len();
if verbosity.detailed_verbosity() {
report_decile_progress(done, prev, n_genes, "genes", start.elapsed());
}
}
if verbosity.normal_verbosity() {
println!(
"Residual variance for {} gene(s) across {n_groups} group(s) in {:.2?}",
n_genes.separate_with_underscores(),
start.elapsed()
);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_validate_groups_single_group() {
let groups = vec![0_u32; 5];
assert_eq!(validate_groups(&groups, 5).unwrap(), 1);
}
#[test]
fn test_validate_groups_rejects_length_mismatch() {
let groups = vec![0_u32; 4];
assert!(matches!(
validate_groups(&groups, 5),
Err(BixverseErrors::ResidualGroupLabelLengthMismatch { .. })
));
}
#[test]
fn test_validate_groups_rejects_gap() {
let groups = vec![0_u32, 0, 2, 2];
assert!(matches!(
validate_groups(&groups, 4),
Err(BixverseErrors::ResidualEmptyGroup { group: 1, .. })
));
}
#[test]
fn test_intersect_gene_sets_keeps_shared_only() {
let a: &[usize] = &[0, 2, 4, 6];
let b: &[usize] = &[2, 3, 4, 7];
let c: &[usize] = &[1, 2, 4];
assert_eq!(intersect_gene_sets(&[a, b, c]).unwrap(), vec![2, 4]);
}
#[test]
fn test_validate_groups_rejects_an_out_of_range_label() {
assert!(matches!(
validate_groups(&[0, 5], 2),
Err(BixverseErrors::ResidualGroupLabelOutOfRange { label: 5, .. })
));
assert!(matches!(
validate_groups(&[1], 1),
Err(BixverseErrors::ResidualGroupLabelOutOfRange { label: 1, .. })
));
assert!(matches!(
validate_groups(&[0, u32::MAX], 2),
Err(BixverseErrors::ResidualGroupLabelOutOfRange { .. })
));
}
#[test]
fn test_validate_groups_rejects_an_empty_selection() {
assert!(matches!(
validate_groups(&[], 0),
Err(BixverseErrors::ResidualEmptySelectionRequest { .. })
));
}
#[test]
fn test_distinct_cell_set_rejects_duplicates() {
assert_eq!(distinct_cell_set(&[0, 1, 2]).unwrap().len(), 3);
assert!(matches!(
distinct_cell_set(&[0, 1, 1, 2]),
Err(BixverseErrors::ResidualDuplicateCells {
n_unique: 3,
n_cells: 4
})
));
}
#[test]
fn test_assert_ascending_rejects_unsorted_and_repeated() {
assert!(assert_ascending(&[0, 1, 2], "genes").is_ok());
assert!(matches!(
assert_ascending(&[0, 2, 1], "genes"),
Err(BixverseErrors::ResidualGenesNotAscending { .. })
));
assert!(matches!(
assert_ascending(&[0, 1, 1], "genes"),
Err(BixverseErrors::ResidualGenesNotAscending { .. })
));
}
#[test]
fn test_validate_clip_range_rejects_what_clamp_would_panic_on() {
assert!(validate_clip_range((-1.0, 1.0)).is_ok());
assert!(validate_clip_range((1.0, 1.0)).is_ok());
assert!(matches!(
validate_clip_range((1.0, -1.0)),
Err(BixverseErrors::ResidualInvalidClipRange { .. })
));
assert!(matches!(
validate_clip_range((f64::NAN, 1.0)),
Err(BixverseErrors::ResidualInvalidClipRange { .. })
));
assert!(matches!(
validate_clip_range((-1.0, f64::NAN)),
Err(BixverseErrors::ResidualInvalidClipRange { .. })
));
}
#[test]
fn test_validate_residual_row_inputs_catches_ragged_and_out_of_range() {
assert!(validate_residual_row_inputs(&[1.0], &[0], 3).is_ok());
assert!(matches!(
validate_residual_row_inputs(&[1.0, 2.0], &[0], 3),
Err(BixverseErrors::ResidualRowLengthMismatch { .. })
));
assert!(matches!(
validate_residual_row_inputs(&[1.0], &[9], 3),
Err(BixverseErrors::ResidualCellIndexOutOfRange { index: 9, .. })
));
}
#[test]
fn test_grouped_sample_variance_rejects_a_non_finite_result() {
let row = [f32::NAN, 1.0, 2.0, 3.0];
let groups = [0_u32; 4];
assert!(matches!(
grouped_sample_variance(&row, &groups, 1, 17),
Err(BixverseErrors::ResidualNonFiniteVariance { gene: 17, .. })
));
}
#[test]
fn test_grouped_sample_variance_rejects_an_out_of_range_label() {
let row = [1.0_f32, 2.0];
let groups = [0_u32, 3];
assert!(matches!(
grouped_sample_variance(&row, &groups, 1, 0),
Err(BixverseErrors::ResidualGroupLabelOutOfRange { label: 3, .. })
));
}
#[test]
fn test_grouped_sample_variance_single_cell_group_is_zero() {
let row = [1.0_f32, 5.0, 9.0];
let groups = [0_u32, 0, 1];
let vars = grouped_sample_variance(&row, &groups, 2, 0).unwrap();
assert_relative_eq!(vars[0], reference_variance(&row[..2]), epsilon = 1e-9);
assert_eq!(vars[1], 0.0);
}
#[test]
fn test_intersect_gene_sets_rejects_an_unsorted_input() {
let a: &[usize] = &[0, 2, 1];
let b: &[usize] = &[0, 1, 2];
assert!(matches!(
intersect_gene_sets(&[a, b]),
Err(BixverseErrors::ResidualGenesNotAscending { .. })
));
}
#[test]
fn test_intersect_gene_sets_single_group_is_the_group() {
let a: &[usize] = &[3, 5, 8];
assert_eq!(intersect_gene_sets(&[a]).unwrap(), vec![3, 5, 8]);
}
#[test]
fn test_intersect_gene_sets_rejects_no_groups() {
assert!(matches!(
intersect_gene_sets(&[]),
Err(BixverseErrors::ResidualEmptySelectionRequest { .. })
));
}
#[test]
fn test_intersect_gene_sets_errors_when_disjoint() {
let a: &[usize] = &[0, 1];
let b: &[usize] = &[2, 3];
assert!(matches!(
intersect_gene_sets(&[a, b]),
Err(BixverseErrors::ResidualEmptyGeneIntersection { n_groups: 2 })
));
}
#[test]
fn test_grouped_sample_variance_splits_by_group() {
let row = [1.0_f32, 3.0, 10.0, 20.0];
let groups = [0_u32, 0, 1, 1];
let vars = grouped_sample_variance(&row, &groups, 2, 0).unwrap();
assert_relative_eq!(vars[0], reference_variance(&row[..2]), epsilon = 1e-9);
assert_relative_eq!(vars[1], reference_variance(&row[2..]), epsilon = 1e-9);
}
fn reference_variance(values: &[f32]) -> f64 {
let n = values.len() as f64;
let mean = values.iter().map(|&x| x as f64).sum::<f64>() / n;
values
.iter()
.map(|&x| {
let d = x as f64 - mean;
d * d
})
.sum::<f64>()
/ (n - 1.0)
}
#[test]
fn test_grouped_sample_variance_matches_ungrouped() {
let row = [1.0_f32, 3.0, 10.0, 20.0];
let groups = [0_u32; 4];
let vars = grouped_sample_variance(&row, &groups, 1, 0).unwrap();
assert_relative_eq!(vars[0], reference_variance(&row), epsilon = 1e-9);
}
}