use faer::{Mat, MatRef};
use indexmap::IndexSet;
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use std::time::Instant;
use thousands::Separable;
use crate::prelude::*;
use crate::sc_batch_correction::batch_utils::{
batch_knn_search, cosine_normalise, par_for_each_col_mut, standardise_per_column,
};
use crate::sc_batch_correction::fast_mnn::{reorder_to_original, split_pca_by_batch};
use crate::sc_batch_correction::seurat_anchors::{
AnchorSet, build_sample_tree, filter_anchor_pairs, find_anchor_pairs, score_anchors,
tree_merge_embeddings,
};
use crate::sc_processing::pca::{
SingleCellPcaParams, pca_on_sc_sparse, resolve_clr_size_factor, scale_csc_chunk,
};
use bixverse_core::core::math::pca_svd::randomised_svd_matfree;
const CELL_TILE: usize = 16;
type AnchorPairsScored = (Vec<(u32, u32)>, Vec<f32>);
#[derive(Clone, Debug)]
pub struct SeuratCcaParams {
pub num_cc: usize,
pub dims: usize,
pub k_anchor: usize,
pub k_filter: usize,
pub k_score: usize,
pub k_weight: usize,
pub n_top_features: usize,
pub l2_norm: bool,
pub sd: f32,
pub knn_params: KnnParams,
pub pca_params: SingleCellPcaParams,
}
impl Default for SeuratCcaParams {
fn default() -> Self {
Self {
num_cc: 30,
dims: 30,
k_anchor: 5,
k_filter: 200,
k_score: 30,
k_weight: 100,
n_top_features: 200,
l2_norm: true,
sd: 1.0,
knn_params: KnnParams::default(),
pca_params: SingleCellPcaParams::default(),
}
}
}
pub(crate) fn load_hvg_standardised<S: SingleCellReading>(
reader: &S,
batch_cell_indices: &[usize],
hvg_indices: &[usize],
clr_offsets: Option<&[f64]>,
use_clr: bool,
size_factor: f32,
verbosity: &Verbosity,
) -> Result<Mat<f32>, ScErrors> {
let cell_set: IndexSet<u32> = batch_cell_indices.iter().map(|&x| x as u32).collect();
let n_cells = batch_cell_indices.len();
let n_hvg = hvg_indices.len();
let mut chunks = reader.read_gene_parallel_filtered(hvg_indices, &cell_set)?;
if use_clr {
let size_factor = resolve_clr_size_factor(reader, size_factor, verbosity)?;
chunks
.par_iter_mut()
.for_each(|chunk| chunk.transform_to_clr(size_factor));
}
let rows: Vec<Vec<f32>> = chunks
.par_iter()
.map(|chunk| {
let (dense, _, _) = scale_csc_chunk(chunk, n_cells, false, false, clr_offsets);
dense
})
.collect();
let mut mat = Mat::<f32>::zeros(n_hvg, n_cells);
mat.as_mut()
.par_col_chunks_mut(CELL_TILE)
.enumerate()
.for_each(|(tile, mut block)| {
let c0 = tile * CELL_TILE;
for (g, row) in rows.iter().enumerate() {
for j in 0..block.ncols() {
block[(g, j)] = row[c0 + j];
}
}
});
Ok(standardise_per_column(mat.as_ref()))
}
fn load_filter_expression<S: SingleCellReading>(
reader: &S,
batch_cell_indices: &[usize],
feature_indices: &[usize],
clr_offsets: Option<&[f64]>,
use_clr: bool,
size_factor: f32,
verbosity: &Verbosity,
) -> Result<Mat<f32>, ScErrors> {
let cell_set: IndexSet<u32> = batch_cell_indices.iter().map(|&x| x as u32).collect();
let n_cells = batch_cell_indices.len();
let n_feat = feature_indices.len();
let mut chunks = reader.read_gene_parallel_filtered(feature_indices, &cell_set)?;
if use_clr {
let size_factor = resolve_clr_size_factor(reader, size_factor, verbosity)?;
chunks
.par_iter_mut()
.for_each(|chunk| chunk.transform_to_clr(size_factor));
}
let rows: Vec<Vec<f32>> = chunks
.par_iter()
.map(|chunk| {
let (dense, _, _) = scale_csc_chunk(chunk, n_cells, false, false, clr_offsets);
dense
})
.collect();
let mut mat = Mat::<f32>::zeros(n_cells, n_feat);
par_for_each_col_mut(&mut mat, |g, col| col.copy_from_slice(&rows[g]));
Ok(cosine_normalise(&mat))
}
fn build_cca_anchor_space(
x1: MatRef<f32>,
x2: MatRef<f32>,
num_cc: usize,
l2_norm: bool,
seed: usize,
) -> Result<(Mat<f32>, Mat<f32>), ScErrors> {
assert_eq!(x1.nrows(), x2.nrows(), "batch feature dimension mismatch");
let n_a = x1.ncols();
let n_b = x2.ncols();
let apply = |v: MatRef<f32>| -> Mat<f32> {
let tmp = x2 * v; x1.transpose() * tmp.as_ref() };
let apply_t = |u: MatRef<f32>| -> Mat<f32> {
let tmp = x1 * u; x2.transpose() * tmp.as_ref() };
let svd = randomised_svd_matfree(n_a, n_b, num_cc, seed, None, None, apply, apply_t)?;
let keep = num_cc
.min(svd.s.len())
.min(svd.u.ncols())
.min(svd.v.ncols());
let mut cc_a = Mat::from_fn(n_a, keep, |i, j| svd.u[(i, j)]);
let mut cc_b = Mat::from_fn(n_b, keep, |i, j| svd.v[(i, j)]);
for k in 0..keep {
let mut pivot = 0.0_f32;
for i in 0..n_a {
if cc_a[(i, k)].abs() > 1e-12 {
pivot = cc_a[(i, k)];
break;
}
}
if pivot == 0.0 {
for i in 0..n_b {
if cc_b[(i, k)].abs() > 1e-12 {
pivot = cc_b[(i, k)];
break;
}
}
}
if pivot < 0.0 {
for i in 0..n_a {
cc_a[(i, k)] = -cc_a[(i, k)];
}
for i in 0..n_b {
cc_b[(i, k)] = -cc_b[(i, k)];
}
}
}
if l2_norm {
cc_a = cosine_normalise(&cc_a);
cc_b = cosine_normalise(&cc_b);
}
Ok((cc_a, cc_b))
}
fn top_loading_gene_positions(loadings: MatRef<f32>, dims: usize, n_top: usize) -> Vec<usize> {
let n_genes = loadings.nrows();
let k = dims.min(loadings.ncols());
let mut scored: Vec<(usize, f32)> = (0..n_genes)
.into_par_iter()
.map(|g| {
let mut m = 0.0_f32;
for j in 0..k {
let v = loadings[(g, j)].abs();
if v > m {
m = v;
}
}
(g, m)
})
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let mut kept: Vec<usize> = scored.into_iter().take(n_top).map(|(g, _)| g).collect();
kept.sort_unstable();
kept
}
#[allow(clippy::too_many_arguments)]
fn find_cca_anchors_for_pair(
cc_a: MatRef<f32>,
cc_b: MatRef<f32>,
filter_a: Option<MatRef<f32>>,
filter_b: Option<MatRef<f32>>,
params: &SeuratCcaParams,
knn_params: &KnnParams,
seed: usize,
verbose: usize,
) -> Result<AnchorPairsScored, ScErrors> {
let k_nn = params.k_anchor.max(params.k_score);
let k_aa = k_nn.min(cc_a.nrows());
let k_bb = k_nn.min(cc_b.nrows());
let k_ab = k_nn.min(cc_b.nrows());
let k_ba = k_nn.min(cc_a.nrows());
let (nnaa, _) = batch_knn_search(cc_a, cc_a, k_aa, knn_params, seed, verbose)?;
let (nnbb, _) = batch_knn_search(cc_b, cc_b, k_bb, knn_params, seed, verbose)?;
let (nnab, _) = batch_knn_search(cc_a, cc_b, k_ab, knn_params, seed, verbose)?;
let (nnba, _) = batch_knn_search(cc_b, cc_a, k_ba, knn_params, seed, verbose)?;
let mut pairs = find_anchor_pairs(&nnab, &nnba, params.k_anchor);
if let (Some(fa), Some(fb)) = (filter_a, filter_b) {
let k_filter = params.k_filter.min(fb.nrows());
if k_filter > 0 {
let (nn_filter, _) = batch_knn_search(fa, fb, k_filter, knn_params, seed, verbose)?;
pairs = filter_anchor_pairs(&pairs, &nn_filter, params.k_filter);
}
}
if pairs.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let scores = score_anchors(
&pairs,
&nnaa,
&nnab,
&nnba,
&nnbb,
params.k_score,
cc_a.nrows(),
);
Ok((pairs, scores))
}
#[allow(clippy::too_many_arguments)]
pub fn seurat_cca_integration<S: SingleCellReading>(
reader: &S,
cell_indices: &[usize],
gene_indices: &[usize],
batch_indices: &[usize],
pre_computed_pca: Option<Mat<f32>>,
clr_offsets: Option<&[f64]>,
params: &SeuratCcaParams,
verbose: usize,
seed: usize,
) -> Result<Mat<f32>, ScErrors> {
let verbosity = parse_verbosity_level(verbose);
let start_total = Instant::now();
if batch_indices.len() != cell_indices.len() {
return Err(CoreErrors::NumberLabelsNotEqualSampleNumber {
label_length: batch_indices.len(),
n_samples: cell_indices.len(),
}
.into());
}
let n_batches = batch_indices.iter().copied().max().unwrap_or(0) + 1;
if n_batches < 2 {
return Err(ScErrors::NeedAtLeastTwoBatches { n_batches });
}
let min_cells = params.k_anchor.max(params.k_score) + 1;
let mut per_batch_positions: Vec<Vec<usize>> = vec![Vec::new(); n_batches];
for (row, &b) in batch_indices.iter().enumerate() {
per_batch_positions[b].push(row);
}
for (b, positions) in per_batch_positions.iter().enumerate() {
if positions.len() < min_cells {
return Err(ScErrors::TooFewCellsForAnchor {
batch: b,
n_cells: positions.len(),
required: min_cells,
});
}
}
let (pca_scores, pca_loadings, _s) = if let Some(scores) = pre_computed_pca {
if verbosity.normal_verbosity() {
println!("Using pre-computed PCA; computing loadings for filter step");
}
let (_pc, load, s) = pca_on_sc_sparse(
reader,
cell_indices,
gene_indices,
params.dims.max(scores.ncols()),
¶ms.pca_params,
clr_offsets,
seed,
verbose,
)?;
(scores, load, s)
} else {
if verbosity.normal_verbosity() {
println!("Computing base PCA");
}
if params.pca_params.clr && clr_offsets.is_none() {
return Err(ScErrors::OffsetsNotProvidedForClrPCA);
}
pca_on_sc_sparse(
reader,
cell_indices,
gene_indices,
params.dims,
¶ms.pca_params,
clr_offsets,
seed,
verbose,
)?
};
let top_positions =
top_loading_gene_positions(pca_loadings.as_ref(), params.dims, params.n_top_features);
let top_absolute_genes: Vec<usize> =
top_positions.iter().map(|&pos| gene_indices[pos]).collect();
let mut anchors_by_pair: FxHashMap<(usize, usize), AnchorSet> = FxHashMap::default();
let mut anchor_counts: Vec<Vec<u32>> = vec![vec![0_u32; n_batches]; n_batches];
for a in 0..n_batches {
for b in (a + 1)..n_batches {
let pair_start = Instant::now();
if verbosity.normal_verbosity() {
println!("CCA: finding anchors for batch pair ({a}, {b})");
}
let cells_a: Vec<usize> = per_batch_positions[a]
.iter()
.map(|&row| cell_indices[row])
.collect();
let cells_b: Vec<usize> = per_batch_positions[b]
.iter()
.map(|&row| cell_indices[row])
.collect();
let clr_a = clr_offsets.map(|offs| -> Vec<f64> {
per_batch_positions[a]
.iter()
.map(|&row| offs[row])
.collect()
});
let clr_b = clr_offsets.map(|offs| -> Vec<f64> {
per_batch_positions[b]
.iter()
.map(|&row| offs[row])
.collect()
});
let x1 = load_hvg_standardised(
reader,
&cells_a,
gene_indices,
clr_a.as_deref(),
params.pca_params.clr,
params.pca_params.size_factor,
&verbosity,
)?;
let x2 = load_hvg_standardised(
reader,
&cells_b,
gene_indices,
clr_b.as_deref(),
params.pca_params.clr,
params.pca_params.size_factor,
&verbosity,
)?;
let (cc_a, cc_b) = build_cca_anchor_space(
x1.as_ref(),
x2.as_ref(),
params.num_cc.max(params.dims),
params.l2_norm,
seed,
)?;
drop(x1);
drop(x2);
let dims_use = params.dims.min(cc_a.ncols());
let cc_a_dim = Mat::from_fn(cc_a.nrows(), dims_use, |i, j| cc_a[(i, j)]);
let cc_b_dim = Mat::from_fn(cc_b.nrows(), dims_use, |i, j| cc_b[(i, j)]);
let filter_a = if params.k_filter > 0 && !top_absolute_genes.is_empty() {
Some(load_filter_expression(
reader,
&cells_a,
&top_absolute_genes,
clr_a.as_deref(),
params.pca_params.clr,
params.pca_params.size_factor,
&verbosity,
)?)
} else {
None
};
let filter_b = if params.k_filter > 0 && !top_absolute_genes.is_empty() {
Some(load_filter_expression(
reader,
&cells_b,
&top_absolute_genes,
clr_b.as_deref(),
params.pca_params.clr,
params.pca_params.size_factor,
&verbosity,
)?)
} else {
None
};
let (pairs, scores) = find_cca_anchors_for_pair(
cc_a_dim.as_ref(),
cc_b_dim.as_ref(),
filter_a.as_ref().map(|m| m.as_ref()),
filter_b.as_ref().map(|m| m.as_ref()),
params,
¶ms.knn_params,
seed,
verbose,
)?;
let count = pairs.len();
if verbosity.normal_verbosity() {
println!(
" found {} anchors in {:.2?}",
count.separate_with_underscores(),
pair_start.elapsed()
);
}
anchor_counts[a][b] = count as u32;
anchor_counts[b][a] = count as u32;
anchors_by_pair.insert(
(a, b),
AnchorSet {
pairs,
scores,
batch_pair: (a, b),
},
);
}
}
let merge_order = build_sample_tree(&anchor_counts);
let (per_batch_scores, per_batch_union_indices) =
split_pca_by_batch(&pca_scores, batch_indices);
let (merged, index_map) = tree_merge_embeddings(
per_batch_scores,
per_batch_union_indices,
&anchors_by_pair,
&merge_order,
params.k_weight,
params.sd,
¶ms.knn_params,
seed,
verbose,
)?;
let corrected = reorder_to_original(&merged, &index_map);
if verbosity.normal_verbosity() {
println!("CCA integration complete in {:.2?}", start_total.elapsed());
}
Ok(corrected)
}