use faer::{Mat, MatRef};
use rayon::prelude::*;
use std::borrow::Cow;
use crate::core::base::info::parse_bin_strategy_type;
use crate::core::base::loess::LoessRegression;
use crate::core::math::pca_svd::*;
use crate::prelude::*;
use crate::single_cell::sc_processing::hvg::*;
use crate::single_cell::sc_processing::pca::{SingleCellPcaParams, SingleCellPcaRes};
use crate::utils::simd::{sum_squared_dev_widen_simd_f32, sum_widen_simd_f32};
#[derive(Clone, Copy, Default)]
struct DenseGeneStats {
mean: f32,
var: f32,
max: f32,
min: f32,
nnz: usize,
}
struct GeneAccum {
sum: Vec<f64>,
sum_sq: Vec<f64>,
nnz: Vec<u32>,
min: Vec<f64>,
max: Vec<f64>,
}
impl GeneAccum {
fn new(n_genes: usize) -> Self {
Self {
sum: vec![0.0; n_genes],
sum_sq: vec![0.0; n_genes],
nnz: vec![0; n_genes],
min: vec![f64::INFINITY; n_genes],
max: vec![f64::NEG_INFINITY; n_genes],
}
}
#[inline(always)]
fn push(&mut self, gene: usize, value: f64) {
self.sum[gene] += value;
self.sum_sq[gene] += value * value;
self.nnz[gene] += 1;
if value < self.min[gene] {
self.min[gene] = value;
}
if value > self.max[gene] {
self.max[gene] = value;
}
}
fn merge(&mut self, other: Self) {
for gene in 0..self.sum.len() {
self.sum[gene] += other.sum[gene];
self.sum_sq[gene] += other.sum_sq[gene];
self.nnz[gene] += other.nnz[gene];
if other.min[gene] < self.min[gene] {
self.min[gene] = other.min[gene];
}
if other.max[gene] > self.max[gene] {
self.max[gene] = other.max[gene];
}
}
}
}
fn accumulate_genes_from_csr<F>(matrix: &CompressedSparseData2<f32>, transform: F) -> GeneAccum
where
F: Fn(f32) -> f64 + Sync,
{
let (n_rows, n_genes) = matrix.shape;
thread_chunks(n_rows)
.par_iter()
.map(|&(row_start, row_end)| {
let mut acc = GeneAccum::new(n_genes);
let lo = matrix.indptr[row_start] as usize;
let hi = matrix.indptr[row_end] as usize;
for (&gene, &value) in matrix.indices[lo..hi]
.iter()
.zip(matrix.data[lo..hi].iter())
{
acc.push(gene as usize, transform(value));
}
acc
})
.reduce(
|| GeneAccum::new(n_genes),
|mut a, b| {
a.merge(b);
a
},
)
}
fn gene_stats_from_csc(csc: &CompressedSparseData2<f32>) -> Vec<DenseGeneStats> {
let n_cells = csc.shape.0;
let n_cells_f64 = n_cells as f64;
csc.indptr
.par_windows(2)
.map(|window| {
let slice = &csc.data[window[0] as usize..window[1] as usize];
let nnz = slice.len();
if nnz == 0 {
return DenseGeneStats::default();
}
let mean = sum_widen_simd_f32(slice) / n_cells_f64;
let ss_nonzero = sum_squared_dev_widen_simd_f32(slice, mean);
let ss_zeros = (n_cells - nnz) as f64 * mean * mean;
let (min, max) = slice
.iter()
.fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), &v| {
(lo.min(v), hi.max(v))
});
DenseGeneStats {
mean: mean as f32,
var: ((ss_nonzero + ss_zeros) / n_cells_f64) as f32,
max,
min: if nnz < n_cells { 0.0 } else { min },
nnz,
}
})
.collect()
}
fn gene_stats_from_csr(csr: &CompressedSparseData2<f32>) -> Vec<DenseGeneStats> {
let n_cells = csr.shape.0;
let n_cells_f64 = n_cells as f64;
let acc = accumulate_genes_from_csr(csr, |v| v as f64);
(0..csr.shape.1)
.map(|gene| {
let nnz = acc.nnz[gene] as usize;
if nnz == 0 {
return DenseGeneStats::default();
}
let mean = acc.sum[gene] / n_cells_f64;
let var = (acc.sum_sq[gene] / n_cells_f64 - mean * mean).max(0.0);
DenseGeneStats {
mean: mean as f32,
var: var as f32,
max: acc.max[gene] as f32,
min: if nnz < n_cells {
0.0
} else {
acc.min[gene] as f32
},
nnz,
}
})
.collect()
}
#[inline]
fn var_std_from_sums(
mut sum_std: f64,
mut sum_sq_std: f64,
mean: f32,
expected_sd: f32,
clip_max: f32,
n_zeros: usize,
n_cells: usize,
) -> f32 {
if n_zeros > 0 {
let std_zero = ((-mean) / expected_sd).min(clip_max).max(-clip_max) as f64;
sum_std += n_zeros as f64 * std_zero;
sum_sq_std += n_zeros as f64 * std_zero * std_zero;
}
let n_cells_f64 = n_cells as f64;
let std_mean = sum_std / n_cells_f64;
((sum_sq_std / n_cells_f64) - std_mean * std_mean) as f32
}
fn var_std_from_csc(
csc: &CompressedSparseData2<f32>,
stats: &[DenseGeneStats],
expected_var: &[f32],
clip_max: f32,
) -> Vec<f32> {
let n_cells = csc.shape.0;
csc.indptr
.par_windows(2)
.zip(stats.par_iter())
.zip(expected_var.par_iter())
.map(|((window, stats), &expected_var)| {
if !clip_is_reachable(stats.mean, stats.min, stats.max, expected_var, clip_max) {
return stats.var / expected_var;
}
let slice = &csc.data[window[0] as usize..window[1] as usize];
let expected_sd = expected_var.sqrt();
let mean = stats.mean;
let (sum_std, sum_sq_std) = slice.iter().fold((0f64, 0f64), |(s, sq), &val| {
let norm = ((val - mean) / expected_sd).min(clip_max).max(-clip_max) as f64;
(s + norm, sq + norm * norm)
});
var_std_from_sums(
sum_std,
sum_sq_std,
mean,
expected_sd,
clip_max,
n_cells - slice.len(),
n_cells,
)
})
.collect()
}
fn var_std_from_csr(
csr: &CompressedSparseData2<f32>,
stats: &[DenseGeneStats],
expected_var: &[f32],
clip_max: f32,
) -> Vec<f32> {
let (n_cells, n_genes) = csr.shape;
let needs_exact: Vec<bool> = stats
.iter()
.zip(expected_var.iter())
.map(|(s, &ev)| clip_is_reachable(s.mean, s.min, s.max, ev, clip_max))
.collect();
let mut var_std: Vec<f32> = stats
.iter()
.zip(expected_var.iter())
.map(|(s, &ev)| s.var / ev)
.collect();
if !needs_exact.iter().any(|&flagged| flagged) {
return var_std;
}
let expected_sd: Vec<f32> = expected_var.iter().map(|&ev| ev.sqrt()).collect();
let (sum_std, sum_sq_std) = thread_chunks(n_cells)
.par_iter()
.map(|&(row_start, row_end)| {
let mut sums = vec![0f64; n_genes];
let mut sums_sq = vec![0f64; n_genes];
let lo = csr.indptr[row_start] as usize;
let hi = csr.indptr[row_end] as usize;
for (&gene, &value) in csr.indices[lo..hi].iter().zip(csr.data[lo..hi].iter()) {
let gene = gene as usize;
if !needs_exact[gene] {
continue;
}
let norm = ((value - stats[gene].mean) / expected_sd[gene])
.min(clip_max)
.max(-clip_max) as f64;
sums[gene] += norm;
sums_sq[gene] += norm * norm;
}
(sums, sums_sq)
})
.reduce(
|| (vec![0f64; n_genes], vec![0f64; n_genes]),
|mut a, b| {
for gene in 0..n_genes {
a.0[gene] += b.0[gene];
a.1[gene] += b.1[gene];
}
a
},
);
for gene in 0..n_genes {
if !needs_exact[gene] {
continue;
}
var_std[gene] = var_std_from_sums(
sum_std[gene],
sum_sq_std[gene],
stats[gene].mean,
expected_sd[gene],
clip_max,
n_cells - stats[gene].nnz,
n_cells,
);
}
var_std
}
pub fn get_hvg_vst_from_sparse(
matrix: &CompressedSparseData2<f32>,
loess_span: f32,
clip_max: Option<f32>,
) -> HvgRes {
let n_cells = matrix.shape.0;
let clip_max = clip_max.unwrap_or((n_cells as f32).sqrt());
let stats = match matrix.cs_type {
CompressedSparseFormat::Csc => gene_stats_from_csc(matrix),
CompressedSparseFormat::Csr => gene_stats_from_csr(matrix),
};
let (means_log10, vars_log10): (Vec<f32>, Vec<f32>) = stats
.par_iter()
.map(|s| (s.mean.log10(), s.var.log10()))
.unzip();
let loess = LoessRegression::new(loess_span, 2);
let loess_res = loess.fit(&means_log10, &vars_log10);
let expected_var: Vec<f32> = loess_res
.fitted_vals
.iter()
.map(|&fitted| 10_f32.powf(fitted))
.collect();
let var_standardised = match matrix.cs_type {
CompressedSparseFormat::Csc => var_std_from_csc(matrix, &stats, &expected_var, clip_max),
CompressedSparseFormat::Csr => var_std_from_csr(matrix, &stats, &expected_var, clip_max),
};
HvgRes {
mean: stats.iter().map(|s| s.mean as f64).collect(),
var: stats.iter().map(|s| s.var as f64).collect(),
var_exp: loess_res.fitted_vals.r_float_convert(),
var_std: var_standardised.r_float_convert(),
}
}
pub fn get_hvg_dispersion_from_sparse(
matrix: &CompressedSparseData2<f32>,
binning: &str,
n_bins: usize,
) -> HvgDispersionRes {
let n_cells = matrix.shape.0;
let binning = parse_bin_strategy_type(binning).unwrap_or_default();
let (means, dispersions): (Vec<f32>, Vec<f32>) = match matrix.cs_type {
CompressedSparseFormat::Csc => matrix
.indptr
.par_windows(2)
.map(|window| {
let slice = &matrix.data[window[0] as usize..window[1] as usize];
let (sum, sum_sq) = slice.iter().fold((0f64, 0f64), |(s, sq), &val| {
let v = (val as f64).exp_m1();
(s + v, sq + v * v)
});
disp_stats_from_sums(sum, sum_sq, n_cells)
})
.unzip(),
CompressedSparseFormat::Csr => {
let acc = accumulate_genes_from_csr(matrix, |v| (v as f64).exp_m1());
(0..matrix.shape.1)
.map(|gene| disp_stats_from_sums(acc.sum[gene], acc.sum_sq[gene], n_cells))
.unzip()
}
};
build_disp_result(means, dispersions, binning, n_bins)
}
pub fn get_hvg_mvb_from_sparse(
matrix: &CompressedSparseData2<f32>,
binning: &str,
n_bins: usize,
) -> HvgDispersionRes {
get_hvg_dispersion_from_sparse(matrix, binning, n_bins)
}
pub fn pca_on_metacells<T: BixverseNumeric>(
matrix: &CompressedSparseData2<T, f32>,
no_pcs: usize,
params_pca: &SingleCellPcaParams,
clr_offsets: Option<&[f64]>,
seed: usize,
) -> SingleCellPcaRes {
let (n_cells, n_genes) = matrix.shape;
if params_pca.clr
&& let Some(offs) = clr_offsets
&& offs.len() != n_cells
{
return Err(BixverseErrors::OffsetsLengthDoesNotMatchNCells {
len_offset: offs.len(),
n_cells,
});
}
let csc = match matrix.cs_type {
CompressedSparseFormat::Csc => Cow::Borrowed(matrix),
CompressedSparseFormat::Csr => Cow::Owned(matrix.transform_single_layer(true)?),
};
let vals = csc
.data_2
.as_ref()
.expect("pca_on_metacells requires normalised counts in data_2");
let vals_active: Cow<[f32]> = if params_pca.clr {
let sf_inv = 1.0_f32 / params_pca.size_factor;
let transformed: Vec<f32> = vals
.par_iter()
.map(|&v| (v.exp_m1() * sf_inv).ln_1p())
.collect();
Cow::Owned(transformed)
} else {
Cow::Borrowed(vals.as_slice())
};
let row_offsets = if params_pca.clr { clr_offsets } else { None };
let mut buffer = vec![0.0_f64; n_cells * n_genes];
buffer
.par_chunks_mut(n_cells)
.zip(csc.indptr.par_windows(2))
.for_each(|(col, window)| {
for idx in window[0] as usize..window[1] as usize {
col[csc.indices[idx] as usize] = vals_active[idx] as f64;
}
if let Some(off) = row_offsets {
for (v, o) in col.iter_mut().zip(off.iter()) {
*v -= o;
}
}
let need_mean = params_pca.mean_center || params_pca.normalise_variance;
let mean = if need_mean {
col.iter().sum::<f64>() / n_cells as f64
} else {
0.0
};
let std_dev = if params_pca.normalise_variance {
let var: f64 = col
.iter()
.map(|&x| {
let d = x - mean;
d * d
})
.sum::<f64>()
/ (n_cells as f64 - 1.0);
var.max(0.0).sqrt()
} else {
1.0
};
for v in col.iter_mut() {
if params_pca.mean_center {
*v -= mean;
}
if params_pca.normalise_variance {
*v = if std_dev < 1e-8 { 0.0 } else { *v / std_dev };
}
}
});
let scaled = MatRef::<f64>::from_column_major_slice(&buffer, n_cells, n_genes);
let (scores, loadings, s) = if params_pca.randomised {
let res: RandomSvdResults<f64> =
randomised_svd(scaled, no_pcs, seed, Some(100_usize), None)?;
let loadings = Mat::<f32>::from_fn(n_genes, no_pcs, |i, j| res.v[(i, j)] as f32);
let scores = Mat::<f32>::from_fn(n_cells, no_pcs, |i, j| (res.u[(i, j)] * res.s[j]) as f32);
let s: Vec<f32> = res.s[..no_pcs].iter().map(|&x| x as f32).collect();
(scores, loadings, s)
} else {
let res = scaled
.thin_svd()
.map_err(|e| BixverseErrors::FaerSvdError(format!("{e:?}")))?;
let loadings = Mat::<f32>::from_fn(n_genes, no_pcs, |i, j| res.V()[(i, j)] as f32);
let scores = Mat::<f32>::from_fn(n_cells, no_pcs, |i, j| {
(res.U()[(i, j)] * res.S().column_vector()[j]) as f32
});
let s: Vec<f32> = res
.S()
.column_vector()
.iter()
.take(no_pcs)
.map(|&x| x as f32)
.collect();
(scores, loadings, s)
};
Ok((scores, loadings, s))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn toy_csc() -> CompressedSparseData2<f32> {
let data: Vec<f32> = vec![
3.0, 5.0, 2.0, 1.0, 2.0, 1.0, 4.0, 1.0, 3.0, 7.0, 10.0, 12.0, 9.0, 11.0, 10.0, 13.0, ];
let indices: Vec<u32> = vec![0, 2, 4, 0, 1, 2, 3, 4, 5, 5, 0, 1, 2, 3, 4, 5];
let indptr: Vec<u32> = vec![0, 3, 9, 10, 16];
CompressedSparseData2::new_csc(&data, &indices, &indptr, None::<&[f32]>, (6, 4))
}
fn dense_var_std(
matrix: &CompressedSparseData2<f32>,
gene: usize,
res: &HvgRes,
clip: f32,
) -> f64 {
let n_cells = matrix.shape.0;
let mut dense = vec![0f32; n_cells];
for idx in matrix.indptr[gene] as usize..matrix.indptr[gene + 1] as usize {
dense[matrix.indices[idx] as usize] = matrix.data[idx];
}
let mean = res.mean[gene] as f32;
let expected_sd = (10f64.powf(res.var_exp[gene]) as f32).sqrt();
let (sum, sum_sq) = dense.iter().fold((0f64, 0f64), |(s, sq), &v| {
let norm = ((v - mean) / expected_sd).min(clip).max(-clip) as f64;
(s + norm, sq + norm * norm)
});
let n = n_cells as f64;
(sum_sq / n) - (sum / n) * (sum / n)
}
#[test]
fn test_vst_clip_branches_match_the_dense_reference() {
let matrix = toy_csc();
for clip in [1e6_f32, 0.25] {
let res = get_hvg_vst_from_sparse(&matrix, 0.3, Some(clip));
for gene in 0..matrix.shape.1 {
assert_relative_eq!(
res.var_std[gene],
dense_var_std(&matrix, gene, &res, clip),
epsilon = 1e-4
);
}
}
}
#[test]
fn test_vst_csr_matches_csc() {
let csc = toy_csc();
let csr = csc.transform();
assert!(csr.cs_type.is_csr());
for clip in [1e6_f32, 0.25] {
let from_csc = get_hvg_vst_from_sparse(&csc, 0.3, Some(clip));
let from_csr = get_hvg_vst_from_sparse(&csr, 0.3, Some(clip));
for gene in 0..csc.shape.1 {
assert_relative_eq!(from_csr.mean[gene], from_csc.mean[gene], epsilon = 1e-5);
assert_relative_eq!(from_csr.var[gene], from_csc.var[gene], epsilon = 1e-5);
assert_relative_eq!(
from_csr.var_exp[gene],
from_csc.var_exp[gene],
epsilon = 1e-5
);
assert_relative_eq!(
from_csr.var_std[gene],
from_csc.var_std[gene],
epsilon = 1e-5
);
}
}
}
#[test]
fn test_dispersion_csr_matches_csc() {
let csc = toy_csc();
let csr = csc.transform();
let from_csc = get_hvg_dispersion_from_sparse(&csc, "equal_width", 2);
let from_csr = get_hvg_dispersion_from_sparse(&csr, "equal_width", 2);
for gene in 0..csc.shape.1 {
assert_relative_eq!(from_csr.mean[gene], from_csc.mean[gene], epsilon = 1e-5);
assert_relative_eq!(
from_csr.dispersion[gene],
from_csc.dispersion[gene],
epsilon = 1e-5
);
assert_eq!(from_csr.bin[gene], from_csc.bin[gene]);
}
}
#[test]
fn test_dispersion_matches_the_seurat_definition() {
let matrix = toy_csc();
let res = get_hvg_dispersion_from_sparse(&matrix, "equal_width", 2);
let n_cells = matrix.shape.0;
for gene in 0..matrix.shape.1 {
let mut dense = vec![0f64; n_cells];
for idx in matrix.indptr[gene] as usize..matrix.indptr[gene + 1] as usize {
dense[matrix.indices[idx] as usize] = matrix.data[idx] as f64;
}
let values: Vec<f64> = dense.iter().map(|v| v.exp_m1()).collect();
let n = n_cells as f64;
let mean = values.iter().sum::<f64>() / n;
let var = values.iter().map(|v| (v - mean) * (v - mean)).sum::<f64>() / (n - 1.0);
assert_relative_eq!(res.mean[gene], mean.ln_1p(), epsilon = 1e-4);
assert_relative_eq!(res.dispersion[gene], (var / mean).ln(), epsilon = 1e-4);
}
}
}