use faer::{Mat, MatRef};
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};
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, find_anchor_pairs, score_anchors, tree_merge_embeddings,
};
use crate::sc_batch_correction::seurat_cca::load_hvg_standardised;
use crate::sc_processing::pca::{SingleCellPcaParams, pca_on_sc_sparse};
use bixverse_core::core::math::pca_svd::randomised_svd;
type AnchorPairsScored = (Vec<(u32, u32)>, Vec<f32>);
#[derive(Clone, Debug)]
pub struct SeuratRpcaParams {
pub dims: usize,
pub k_anchor: usize,
pub k_score: usize,
pub k_weight: usize,
pub l2_norm: bool,
pub sd: f32,
pub knn_params: KnnParams,
pub pca_params: SingleCellPcaParams,
}
impl Default for SeuratRpcaParams {
fn default() -> Self {
Self {
dims: 30,
k_anchor: 5,
k_score: 30,
k_weight: 100,
l2_norm: true,
sd: 1.0,
knn_params: KnnParams::default(),
pca_params: SingleCellPcaParams::default(),
}
}
}
fn per_batch_pca_standardised(
x_standardised: MatRef<f32>,
dims: usize,
seed: usize,
) -> Result<(Mat<f32>, Mat<f32>), ScErrors> {
let n_hvg = x_standardised.nrows();
let n_cells = x_standardised.ncols();
let effective = dims.min(n_hvg).min(n_cells);
let svd = randomised_svd(x_standardised, effective, seed, None, None)?;
let keep = effective
.min(svd.s.len())
.min(svd.u.ncols())
.min(svd.v.ncols());
let loadings = Mat::from_fn(n_hvg, keep, |i, j| svd.u[(i, j)]);
let scores = Mat::from_fn(n_cells, keep, |i, j| svd.v[(i, j)] * svd.s[j]);
Ok((scores, loadings))
}
fn project_into_basis(loadings_a: MatRef<f32>, x_b: MatRef<f32>, l2_norm: bool) -> Mat<f32> {
let projected = x_b.transpose() * loadings_a;
if l2_norm {
cosine_normalise(&projected)
} else {
projected
}
}
fn normalise_own_scores(scores: &Mat<f32>, l2_norm: bool) -> Mat<f32> {
if l2_norm {
cosine_normalise(scores)
} else {
scores.clone()
}
}
#[allow(clippy::too_many_arguments)]
fn find_rpca_anchors_for_pair(
a_in_a: MatRef<f32>,
b_in_b: MatRef<f32>,
a_in_b: MatRef<f32>,
b_in_a: MatRef<f32>,
params: &SeuratRpcaParams,
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(a_in_a.nrows());
let k_bb = k_nn.min(b_in_b.nrows());
let k_ab = k_nn.min(b_in_b.nrows());
let k_ba = k_nn.min(a_in_a.nrows());
let (nnaa, _) = batch_knn_search(a_in_a, a_in_a, k_aa, knn_params, seed, verbose)?;
let (nnbb, _) = batch_knn_search(b_in_b, b_in_b, k_bb, knn_params, seed, verbose)?;
let (nnab, _) = batch_knn_search(a_in_b, b_in_b, k_ab, knn_params, seed, verbose)?;
let (nnba, _) = batch_knn_search(b_in_a, a_in_a, k_ba, knn_params, seed, verbose)?;
let pairs = find_anchor_pairs(&nnab, &nnba, params.k_anchor);
if pairs.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let scores = score_anchors(
&pairs,
&nnaa,
&nnab,
&nnba,
&nnbb,
params.k_score,
a_in_a.nrows(),
);
Ok((pairs, scores))
}
#[allow(clippy::too_many_arguments)]
pub fn seurat_rpca_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: &SeuratRpcaParams,
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,
});
}
}
if params.pca_params.clr && clr_offsets.is_none() {
return Err(ScErrors::OffsetsNotProvidedForClrPCA);
}
let pca_scores = if let Some(scores) = pre_computed_pca {
if verbosity.normal_verbosity() {
println!("Using pre-computed union PCA");
}
scores
} else {
if verbosity.normal_verbosity() {
println!("Computing base union PCA");
}
let (sc, _load, _s) = pca_on_sc_sparse(
reader,
cell_indices,
gene_indices,
params.dims,
¶ms.pca_params,
clr_offsets,
seed,
verbose,
)?;
sc
};
if verbosity.normal_verbosity() {
println!("Loading per-batch standardised HVG expression");
}
let mut per_batch_standardised: Vec<Mat<f32>> = Vec::with_capacity(n_batches);
for b in 0..n_batches {
let cells_b: Vec<usize> = per_batch_positions[b]
.iter()
.map(|&row| cell_indices[row])
.collect();
let clr_b: Option<Vec<f64>> = clr_offsets.map(|offs| {
per_batch_positions[b]
.iter()
.map(|&row| offs[row])
.collect()
});
let x = load_hvg_standardised(
reader,
&cells_b,
gene_indices,
clr_b.as_deref(),
params.pca_params.clr,
params.pca_params.size_factor,
&verbosity,
)?;
per_batch_standardised.push(x);
}
if verbosity.normal_verbosity() {
println!("Computing per-batch PCAs ({n_batches} batches)");
}
let mut per_batch_pca_scores: Vec<Mat<f32>> = Vec::with_capacity(n_batches);
let mut per_batch_loadings: Vec<Mat<f32>> = Vec::with_capacity(n_batches);
for b in 0..n_batches {
let (sc, load) =
per_batch_pca_standardised(per_batch_standardised[b].as_ref(), params.dims, seed)?;
per_batch_pca_scores.push(sc);
per_batch_loadings.push(load);
}
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!("rPCA: finding anchors for batch pair ({a}, {b})");
}
let dims_use = params
.dims
.min(per_batch_loadings[a].ncols())
.min(per_batch_loadings[b].ncols());
let load_a_trunc = Mat::from_fn(per_batch_loadings[a].nrows(), dims_use, |i, j| {
per_batch_loadings[a][(i, j)]
});
let load_b_trunc = Mat::from_fn(per_batch_loadings[b].nrows(), dims_use, |i, j| {
per_batch_loadings[b][(i, j)]
});
let a_in_a = normalise_own_scores(&per_batch_pca_scores[a], params.l2_norm);
let b_in_b = normalise_own_scores(&per_batch_pca_scores[b], params.l2_norm);
let a_in_b = project_into_basis(
load_b_trunc.as_ref(),
per_batch_standardised[a].as_ref(),
params.l2_norm,
);
let b_in_a = project_into_basis(
load_a_trunc.as_ref(),
per_batch_standardised[b].as_ref(),
params.l2_norm,
);
let a_in_a = Mat::from_fn(a_in_a.nrows(), dims_use, |i, j| a_in_a[(i, j)]);
let b_in_b = Mat::from_fn(b_in_b.nrows(), dims_use, |i, j| b_in_b[(i, j)]);
let (pairs, scores) = find_rpca_anchors_for_pair(
a_in_a.as_ref(),
b_in_b.as_ref(),
a_in_b.as_ref(),
b_in_a.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),
},
);
}
}
drop(per_batch_standardised);
drop(per_batch_loadings);
drop(per_batch_pca_scores);
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!("rPCA integration complete in {:.2?}", start_total.elapsed());
}
Ok(corrected)
}