use faer::{Mat, MatRef, concat};
use rand::prelude::*;
use rayon::prelude::*;
use rustc_hash::{FxHashMap, FxHashSet};
use std::time::Instant;
use crate::core::math::stats::*;
use crate::graph::community_detections::*;
use crate::graph::graph_structures::*;
use crate::prelude::*;
use crate::single_cell::sc_processing::hvg::*;
use crate::single_cell::sc_processing::scrublet::*;
type BoostPcaRes = (Mat<f32>, Mat<f32>, Vec<f32>, Vec<f32>);
#[derive(Clone, Debug)]
pub struct BoostParams {
pub log_transform: bool,
pub mean_center: bool,
pub normalise_variance: bool,
pub target_size: Option<f32>,
pub min_gene_var_pctl: f32,
pub hvg_method: String,
pub loess_span: f64,
pub clip_max: Option<f32>,
pub boost_rate: f32,
pub replace: bool,
pub no_pcs: usize,
pub random_svd: bool,
pub resolution: f32,
pub louvain_iters: usize,
pub n_iters: usize,
pub p_thresh: f32,
pub voter_thresh: f32,
pub knn_params: KnnParams,
}
#[derive(Clone, Debug)]
pub struct BoostResult {
pub predicted_doublets: Vec<bool>,
pub doublet_scores: Vec<f32>,
pub voting_average: Vec<f32>,
}
#[allow(clippy::too_many_arguments)]
fn pca_boost(
f_path_gene: &str,
cell_indices: &[usize],
gene_indices: &[usize],
library_sizes: &[usize],
target_size: f32,
params: &BoostParams,
seed: usize,
verbose: bool,
) -> BoostPcaRes {
pca_scrublet(
f_path_gene,
cell_indices,
gene_indices,
library_sizes,
target_size,
&ScrubletParams {
log_transform: params.log_transform,
mean_center: params.mean_center,
normalise_variance: params.normalise_variance,
target_size: params.target_size,
min_gene_var_pctl: params.min_gene_var_pctl,
hvg_method: params.hvg_method.clone(),
loess_span: params.loess_span,
clip_max: params.clip_max,
sim_doublet_ratio: 0.0, expected_doublet_rate: 0.0,
stdev_doublet_rate: 0.0,
no_pcs: params.no_pcs,
random_svd: params.random_svd,
n_bins: 0,
manual_threshold: None,
knn_params: KnnParams::new(),
},
seed,
verbose,
)
}
fn score_communities(
orig_communities: &[usize],
synth_communities: &[usize],
) -> (Vec<f32>, Vec<f32>) {
let mut synth_counts: FxHashMap<usize, usize> = FxHashMap::default();
let mut orig_counts: FxHashMap<usize, usize> = FxHashMap::default();
for &c in synth_communities {
*synth_counts.entry(c).or_insert(0) += 1;
}
for &c in orig_communities {
*orig_counts.entry(c).or_insert(0) += 1;
}
let unique_comms: FxHashSet<usize> = orig_communities.iter().copied().collect();
let mut comm_scores: FxHashMap<usize, f32> = FxHashMap::default();
let mut comm_log_p: FxHashMap<usize, f32> = FxHashMap::default();
for &c in &unique_comms {
let n_synth = *synth_counts.get(&c).unwrap_or(&0);
let n_orig = *orig_counts.get(&c).unwrap_or(&0);
comm_scores.insert(c, n_synth as f32 / (n_synth + n_orig) as f32);
let k = n_synth + n_orig;
let log_p =
hypergeom_pval::<f32>(n_synth, synth_communities.len(), orig_communities.len(), k).ln();
comm_log_p.insert(c, log_p);
}
let scores: Vec<f32> = orig_communities.iter().map(|&c| comm_scores[&c]).collect();
let log_p_values: Vec<f32> = orig_communities.iter().map(|&c| comm_log_p[&c]).collect();
(scores, log_p_values)
}
fn predict_voting(
all_log_p_values: &[Vec<f32>],
p_thresh: f32,
voter_thresh: f32,
) -> (Vec<bool>, Vec<f32>) {
let n_cells = all_log_p_values[0].len();
let n_iters = all_log_p_values.len();
let log_p_thresh = p_thresh.ln();
let voting_avg: Vec<f32> = (0..n_cells)
.map(|cell_idx| {
let votes: usize = all_log_p_values
.iter()
.filter(|iter_vals| iter_vals[cell_idx] <= log_p_thresh)
.count();
votes as f32 / n_iters as f32
})
.collect();
let labels: Vec<bool> = voting_avg.iter().map(|&avg| avg >= voter_thresh).collect();
(labels, voting_avg)
}
fn find_score_cutoff(scores: &[f32]) -> f32 {
let mut sorted = scores.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
if sorted.len() < 2 {
return sorted[0];
}
let mut max_gap = 0.0f32;
let mut gap_idx = 0;
for i in 1..sorted.len() {
let gap = sorted[i] - sorted[i - 1];
if gap > max_gap {
max_gap = gap;
gap_idx = i;
}
}
sorted[gap_idx]
}
#[derive(Clone, Debug)]
pub struct BoostClassifier {
f_path_gene: String,
f_path_cell: String,
params: BoostParams,
n_cells: usize,
n_cells_sim: usize,
cells_to_keep: Vec<usize>,
hvg_library_sizes: Vec<usize>,
}
impl BoostClassifier {
pub fn new(
f_path_gene: &str,
f_path_cell: &str,
params: BoostParams,
cell_indices: &[usize],
) -> Self {
let n_cells = cell_indices.len();
BoostClassifier {
f_path_gene: f_path_gene.to_string(),
f_path_cell: f_path_cell.to_string(),
params,
n_cells,
n_cells_sim: 0,
cells_to_keep: cell_indices.to_vec(),
hvg_library_sizes: Vec::new(),
}
}
pub fn run_boost(&mut self, streaming: bool, seed: usize, verbose: bool) -> BoostResult {
let start_all = Instant::now();
if verbose {
println!("Identifying highly variable genes...");
}
let start_hvg = Instant::now();
let hvg_genes = self.get_hvg(streaming, verbose);
let end_hvg = start_hvg.elapsed();
if verbose {
println!(
"Using {} highly variable genes. Done in {:.2?}",
hvg_genes.len(),
end_hvg
);
}
let cell_reader = ParallelSparseReader::new(&self.f_path_cell).unwrap();
let hvg_set: FxHashSet<usize> = hvg_genes.iter().copied().collect();
let hvg_library_sizes: Vec<usize> = self
.cells_to_keep
.par_iter()
.map(|&cell_idx| {
let chunk = cell_reader.read_cell(cell_idx);
chunk
.indices
.iter()
.enumerate()
.filter(|&(_, &gene_idx)| hvg_set.contains(&(gene_idx as usize)))
.map(|(i, _)| chunk.data_raw[i] as usize)
.sum()
})
.collect();
let target_size = self.params.target_size.unwrap_or_else(|| {
let sum = hvg_library_sizes.iter().sum::<usize>() as f32;
sum / hvg_library_sizes.len() as f32
});
self.hvg_library_sizes = hvg_library_sizes;
self.n_cells_sim = (self.n_cells as f32 * self.params.boost_rate) as usize;
let start_iters = Instant::now();
let iter_results: Vec<(Vec<f32>, Vec<f32>)> = (0..self.params.n_iters)
.map(|iter| {
if verbose {
println!(
" == Running iteration {} of {} == ",
iter + 1,
self.params.n_iters
);
}
self.one_iteration(target_size, &hvg_genes, seed + iter, verbose)
})
.collect();
let end_iters = start_iters.elapsed();
if verbose {
println!(
"Completed {} iterations in {:.2?}",
self.params.n_iters, end_iters
);
}
let all_scores: Vec<Vec<f32>> = iter_results.iter().map(|(s, _)| s.clone()).collect();
let all_log_p_values: Vec<Vec<f32>> = iter_results.iter().map(|(_, p)| p.clone()).collect();
let result = if self.params.n_iters > 1 {
let (labels, voting_avg) = predict_voting(
&all_log_p_values,
self.params.p_thresh,
self.params.voter_thresh,
);
let avg_scores: Vec<f32> = (0..self.n_cells)
.map(|i| {
all_scores.iter().map(|iter| iter[i]).sum::<f32>() / self.params.n_iters as f32
})
.collect();
BoostResult {
predicted_doublets: labels,
doublet_scores: avg_scores,
voting_average: voting_avg,
}
} else {
let scores = &all_scores[0];
let cutoff = find_score_cutoff(scores);
if verbose {
println!("Score cutoff: {:.4}", cutoff);
}
let labels: Vec<bool> = scores.iter().map(|&s| s >= cutoff).collect();
let voting_avg = vec![0.0; self.n_cells];
BoostResult {
predicted_doublets: labels,
doublet_scores: scores.clone(),
voting_average: voting_avg,
}
};
let end_all = start_all.elapsed();
if verbose {
let n_doublets = result.predicted_doublets.iter().filter(|&&d| d).count();
println!(
"Detected {} doublets ({:.1}%)",
n_doublets,
100.0 * n_doublets as f32 / self.n_cells as f32
);
println!("Total runtime: {:.2?}", end_all);
}
result
}
pub fn one_iteration(
&self,
target_size: f32,
hvg_genes: &[usize],
seed: usize,
verbose: bool,
) -> (Vec<f32>, Vec<f32>) {
let sim_chunks = self.simulate_doublets(target_size, hvg_genes, seed);
let combined_pca = self.run_pca(&sim_chunks, hvg_genes, target_size, verbose, seed);
let knn = self
.build_combined_knn(combined_pca.as_ref(), seed, verbose)
.unwrap();
let start_graph = Instant::now();
let graph = knn_to_sparse_graph(&knn);
let end_graph = start_graph.elapsed();
if verbose {
println!("Transformed kNN graph. Done in {:.2?}", end_graph);
}
let start_cluster = Instant::now();
let communities = louvain_sparse_graph(
&graph,
self.params.resolution,
self.params.louvain_iters,
seed,
);
let end_cluster = start_cluster.elapsed();
if verbose {
println!(
"Generated communities via Louvain clustering. Done in {:.2?}",
end_cluster
);
}
let orig_communities = communities[..self.n_cells].to_vec();
let synth_communities = communities[self.n_cells..].to_vec();
let (scores, log_p_values) = score_communities(&orig_communities, &synth_communities);
(scores, log_p_values)
}
fn get_hvg(&self, streaming: bool, verbose: bool) -> Vec<usize> {
let hvg_type = parse_hvg_method(&self.params.hvg_method)
.ok_or_else(|| format!("Invalid HVG method: {}", &self.params.hvg_method))
.unwrap();
let hvg_res: HvgRes = if streaming {
match hvg_type {
HvgMethod::Vst => get_hvg_vst_streaming(
&self.f_path_gene,
&self.cells_to_keep,
self.params.loess_span as f32,
self.params.clip_max,
verbose,
),
HvgMethod::MeanVarBin => get_hvg_mvb_streaming(),
HvgMethod::Dispersion => get_hvg_dispersion_streaming(),
}
} else {
match hvg_type {
HvgMethod::Vst => get_hvg_vst(
&self.f_path_gene,
&self.cells_to_keep,
self.params.loess_span as f32,
self.params.clip_max,
verbose,
),
HvgMethod::MeanVarBin => get_hvg_mvb(),
HvgMethod::Dispersion => get_hvg_dispersion(),
}
};
let n_genes = hvg_res.mean.len() as f32;
let n_genes_to_take = (n_genes * (1.0 - self.params.min_gene_var_pctl)).ceil() as usize;
let mut indices: Vec<(usize, f64)> = hvg_res
.var_std
.iter()
.enumerate()
.map(|(i, &v)| (i, v))
.collect();
indices.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
indices.truncate(n_genes_to_take);
let mut result: Vec<usize> = indices.into_iter().map(|(i, _)| i).collect();
result.sort_unstable();
result
}
fn simulate_doublets(
&self,
target_size: f32,
hvg_genes: &[usize],
seed: usize,
) -> Vec<CsrCellChunk> {
let n_sim_doublets = (self.n_cells as f32 * self.params.boost_rate) as usize;
let mut rng = StdRng::seed_from_u64(seed as u64);
let pairs: Vec<(usize, usize)> = if self.params.replace {
(0..n_sim_doublets)
.map(|_| {
let i = rng.random_range(0..self.cells_to_keep.len());
let j = rng.random_range(0..self.cells_to_keep.len());
(i, j)
})
.collect()
} else {
let mut available: Vec<usize> = (0..self.cells_to_keep.len()).collect();
available.shuffle(&mut rng);
available
.chunks(2)
.take(n_sim_doublets)
.filter_map(|chunk| {
if chunk.len() == 2 {
Some((chunk[0], chunk[1]))
} else {
None
}
})
.collect()
};
let hvg_set: FxHashSet<usize> = hvg_genes.iter().copied().collect();
let gene_to_hvg_idx: FxHashMap<usize, u16> = hvg_genes
.iter()
.enumerate()
.map(|(hvg_idx, &orig_idx)| (orig_idx, hvg_idx as u16))
.collect();
let reader = ParallelSparseReader::new(&self.f_path_cell).unwrap();
pairs
.par_iter()
.enumerate()
.map(|(doublet_idx, &(pos_i, pos_j))| {
let cell_idx_i = self.cells_to_keep[pos_i];
let cell_idx_j = self.cells_to_keep[pos_j];
let cell1 = reader.read_cell(cell_idx_i);
let cell2 = reader.read_cell(cell_idx_j);
let hvg_combined_lib_size =
self.hvg_library_sizes[pos_i] + self.hvg_library_sizes[pos_j];
let mut doublet = CsrCellChunk::add_cells_scrublet(
&cell1,
&cell2,
&hvg_set,
hvg_combined_lib_size,
target_size,
self.params.log_transform,
doublet_idx,
);
for idx in doublet.indices.iter_mut() {
*idx = gene_to_hvg_idx[&(*idx as usize)];
}
doublet
})
.collect()
}
fn run_pca(
&self,
sim_chunks: &[CsrCellChunk],
hvg_genes: &[usize],
target_size: f32,
verbose: bool,
seed: usize,
) -> Mat<f32> {
let pca_res: BoostPcaRes = pca_boost(
&self.f_path_gene,
&self.cells_to_keep,
hvg_genes,
&self.hvg_library_sizes,
target_size,
&self.params,
seed,
verbose,
);
let scaled_sim = scale_cell_chunks_with_stats(
sim_chunks,
&pca_res.2,
&pca_res.3,
self.params.mean_center,
self.params.normalise_variance,
hvg_genes.len(),
);
let pca_sim = &scaled_sim * pca_res.1;
concat![[pca_res.0], [pca_sim]]
}
fn build_combined_knn(
&self,
embd: MatRef<f32>,
seed: usize,
verbose: bool,
) -> Result<Vec<Vec<usize>>, String> {
let knn_method = parse_knn_method(&self.params.knn_params.knn_method).unwrap_or_default();
let k_adj = self.calculate_k_adj();
if verbose {
println!("Using {} neighbours in the kNN generation.", k_adj);
}
let knn = match knn_method {
KnnSearch::Hnsw => generate_knn_hnsw(
embd.as_ref(),
&self.params.knn_params.ann_dist,
k_adj,
self.params.knn_params.m,
self.params.knn_params.ef_construction,
self.params.knn_params.ef_search,
seed,
verbose,
),
KnnSearch::Annoy => generate_knn_annoy(
embd.as_ref(),
&self.params.knn_params.ann_dist,
k_adj,
self.params.knn_params.n_tree,
self.params.knn_params.search_budget,
seed,
verbose,
),
KnnSearch::NNDescent => generate_knn_nndescent(
embd.as_ref(),
&self.params.knn_params.ann_dist,
k_adj,
self.params.knn_params.diversify_prob,
self.params.knn_params.ef_budget,
self.params.knn_params.delta,
seed,
verbose,
),
KnnSearch::Exhaustive => generate_knn_exhaustive(
embd.as_ref(),
&self.params.knn_params.ann_dist,
k_adj,
verbose,
),
};
Ok(knn)
}
fn calculate_k_adj(&self) -> usize {
let k = if self.params.knn_params.k == 0 {
((self.n_cells as f32).sqrt() * 0.5).round() as usize
} else {
self.params.knn_params.k
};
let r = self.n_cells_sim as f32 / self.n_cells as f32;
(k as f32 * (1.0 + r)).round() as usize
}
}