use rand::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::utils_doublets::*;
#[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>,
}
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 {
BoostClassifier {
f_path_gene: f_path_gene.to_string(),
f_path_cell: f_path_cell.to_string(),
params,
n_cells: cell_indices.len(),
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();
let hvg_opts = HvgOpts {
method: self.params.hvg_method.clone(),
loess_span: self.params.loess_span as f32,
clip_max: self.params.clip_max,
min_gene_var_pctl: self.params.min_gene_var_pctl,
};
if verbose {
println!("Identifying highly variable genes...");
}
let start_hvg = Instant::now();
let hvg_genes = select_hvg(
&self.f_path_gene,
&self.cells_to_keep,
&hvg_opts,
streaming,
verbose,
);
if verbose {
println!(
"Using {} highly variable genes. Done in {:.2?}",
hvg_genes.len(),
start_hvg.elapsed()
);
}
self.hvg_library_sizes =
compute_hvg_library_sizes(&self.f_path_cell, &self.cells_to_keep, &hvg_genes);
let target_size = resolve_target_size(self.params.target_size, &self.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();
if verbose {
println!(
"Completed {} iterations in {:.2?}\n",
self.params.n_iters,
start_iters.elapsed()
);
}
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);
}
BoostResult {
predicted_doublets: scores.iter().map(|&s| s >= cutoff).collect(),
doublet_scores: scores.clone(),
voting_average: vec![0.0; self.n_cells],
}
};
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?}", start_all.elapsed());
}
result
}
fn one_iteration(
&self,
target_size: f32,
hvg_genes: &[usize],
seed: usize,
verbose: bool,
) -> (Vec<f32>, Vec<f32>) {
let pca_opts = PcaOpts {
log_transform: self.params.log_transform,
mean_center: self.params.mean_center,
normalise_variance: self.params.normalise_variance,
no_pcs: self.params.no_pcs,
random_svd: self.params.random_svd,
};
let pairs = self.generate_pairs(seed);
let sim_chunks = simulate_from_pairs(
&pairs,
&self.cells_to_keep,
&self.hvg_library_sizes,
hvg_genes,
&self.f_path_cell,
target_size,
self.params.log_transform,
);
let (combined_pca, _) = pca_and_project(
&self.f_path_gene,
&self.cells_to_keep,
hvg_genes,
&self.hvg_library_sizes,
target_size,
&sim_chunks,
&pca_opts,
seed,
verbose,
);
let k_adj = adjusted_k(self.params.knn_params.k, self.n_cells, self.n_cells_sim);
if verbose {
println!("Using {} neighbours in the kNN generation.", k_adj);
}
let knn = dispatch_knn(
combined_pca.as_ref(),
k_adj,
&self.params.knn_params,
seed,
verbose,
);
let start_graph = Instant::now();
let graph = knn_to_sparse_graph(&knn);
if verbose {
println!(
"Transformed kNN graph. Done in {:.2?}",
start_graph.elapsed()
);
}
let start_cluster = Instant::now();
let communities = louvain_sparse_graph(
&graph,
self.params.resolution,
self.params.louvain_iters,
seed,
);
if verbose {
println!(
"Generated communities via Louvain clustering. Done in {:.2?}",
start_cluster.elapsed()
);
}
let orig_communities = communities[..self.n_cells].to_vec();
let synth_communities = communities[self.n_cells..].to_vec();
score_communities(&orig_communities, &synth_communities)
}
fn generate_pairs(&self, seed: usize) -> Vec<(usize, usize)> {
let n_sim = (self.n_cells as f32 * self.params.boost_rate) as usize;
if self.params.replace {
random_pairs(self.cells_to_keep.len(), n_sim, seed)
} else {
let mut rng = StdRng::seed_from_u64(seed as u64);
let mut available: Vec<usize> = (0..self.cells_to_keep.len()).collect();
available.shuffle(&mut rng);
available
.chunks(2)
.take(n_sim)
.filter_map(|chunk| {
if chunk.len() == 2 {
Some((chunk[0], chunk[1]))
} else {
None
}
})
.collect()
}
}
}