use faer::Mat;
use half::f16;
use rayon::prelude::*;
use rustc_hash::{FxHashMap, FxHashSet};
use std::time::Instant;
use crate::prelude::*;
use crate::single_cell::sc_processing::utils_doublets::*;
type ScrubletDoubletScores = (Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>);
pub type FinalScrubletRes = (
ScrubletResult,
Option<Mat<f32>>,
Option<Vec<usize>>,
Option<Vec<usize>>,
);
#[derive(Clone, Debug)]
pub struct ScrubletParams {
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 binning_strategy: String,
pub n_bins: usize,
pub no_pcs: usize,
pub random_svd: bool,
pub sim_doublet_ratio: f32,
pub expected_doublet_rate: f32,
pub stdev_doublet_rate: f32,
pub n_bins_hist: usize,
pub manual_threshold: Option<f32>,
pub knn_params: KnnParams,
}
#[derive(Clone, Debug)]
pub struct ScrubletResult {
pub predicted_doublets: Vec<bool>,
pub doublet_scores_obs: Vec<f32>,
pub doublet_scores_sim: Vec<f32>,
pub doublet_errors_obs: Vec<f32>,
pub z_scores: Vec<f32>,
pub threshold: f32,
pub detected_doublet_rate: f32,
pub detectable_doublet_fraction: f32,
pub overall_doublet_rate: f32,
}
impl CsrCellChunk {
#[allow(clippy::too_many_arguments)]
pub fn add_cells_scrublet(
cell1: &CsrCellChunk,
cell2: &CsrCellChunk,
hvg_indices: &FxHashSet<usize>,
hvg_lib_size_combined: usize,
target_size: f32,
log_transform: bool,
doublet_index: usize,
) -> Self {
let mut gene_counts: FxHashMap<u32, u32> = FxHashMap::default();
for i in 0..cell1.indices.len() {
if hvg_indices.contains(&(cell1.indices[i] as usize)) {
*gene_counts.entry(cell1.indices[i]).or_insert(0) += cell1.data_raw.get(i);
}
}
for i in 0..cell2.indices.len() {
if hvg_indices.contains(&(cell2.indices[i] as usize)) {
*gene_counts.entry(cell2.indices[i]).or_insert(0) += cell2.data_raw.get(i);
}
}
let mut gene_vec: Vec<(u32, u32)> = gene_counts.into_iter().collect();
gene_vec.sort_unstable_by_key(|&(gene, _)| gene);
let mut data_raw = Vec::with_capacity(gene_vec.len());
let mut data_norm = Vec::with_capacity(gene_vec.len());
let mut indices = Vec::with_capacity(gene_vec.len());
let norm_factor = target_size / hvg_lib_size_combined as f32;
for (gene, count) in gene_vec {
data_raw.push(count.min(u16::MAX as u32) as u16);
let normalised = if log_transform {
(count as f32 * norm_factor).ln_1p()
} else {
count as f32 * norm_factor
};
data_norm.push(F16::from(f16::from_f32(
normalised.clamp(-65504.0, 65504.0),
)));
indices.push(gene);
}
Self {
data_raw: RawCounts::U16(data_raw),
data_norm,
library_size: hvg_lib_size_combined,
indices,
original_index: doublet_index,
to_keep: true,
}
}
}
#[derive(Clone, Debug)]
pub struct Scrublet {
f_path_gene: String,
f_path_cell: String,
params: ScrubletParams,
n_cells: usize,
n_cells_sim: usize,
cells_to_keep: Vec<usize>,
hvg_library_sizes: Vec<usize>,
}
impl Scrublet {
pub fn new(
f_path_gene: &str,
f_path_cells: &str,
params: ScrubletParams,
cell_indices: &[usize],
) -> Self {
Scrublet {
f_path_gene: f_path_gene.to_string(),
f_path_cell: f_path_cells.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_scrublet(
&mut self,
streaming: bool,
seed: usize,
verbose: usize,
return_combined_pca: bool,
return_pairs: bool,
) -> Result<FinalScrubletRes, BixverseErrors> {
let verbosity = parse_verbosity_level(verbose);
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,
binning_strategy: self.params.binning_strategy.clone(),
n_bins: self.params.n_bins,
};
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,
};
if verbosity.normal_verbosity() {
println!("Identifying highly variable genes...");
}
let start_all = Instant::now();
let start_hvg = Instant::now();
let hvg_genes = select_hvg(
&self.f_path_gene,
&self.cells_to_keep,
&hvg_opts,
streaming,
verbose,
)?;
if verbosity.normal_verbosity() {
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);
if verbosity.normal_verbosity() {
println!("Simulating doublets...");
}
let start_sim = Instant::now();
self.n_cells_sim = (self.n_cells as f32 * self.params.sim_doublet_ratio) as usize;
let pairs = random_pairs(self.cells_to_keep.len(), self.n_cells_sim, seed);
let (pair_1, pair_2): (Vec<usize>, Vec<usize>) = pairs.iter().copied().unzip();
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,
)?;
if verbosity.normal_verbosity() {
println!(
"Simulated {} doublets. Done in {:.2?}",
sim_chunks.len(),
start_sim.elapsed()
);
}
if verbosity.normal_verbosity() {
println!("Running PCA...");
}
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,
)?;
if verbosity.normal_verbosity() {
println!("Building kNN graph...");
}
let start_knn = Instant::now();
let k_adj = adjusted_k(self.params.knn_params.k, self.n_cells, self.n_cells_sim);
if verbosity.normal_verbosity() {
println!("Using {} neighbours in the kNN generation.", k_adj);
}
let knn_indices = dispatch_knn(
combined_pca.as_ref(),
k_adj,
&self.params.knn_params,
seed,
verbosity.detailed_verbosity(),
)?;
if verbosity.normal_verbosity() {
println!("Done with kNN generation in {:.2?}", start_knn.elapsed());
}
if verbosity.normal_verbosity() {
println!("Calculating doublet scores...");
}
let start_score = Instant::now();
let doublet_scores = self.calculate_doublet_scores(&knn_indices);
let res = self.call_doublets(
doublet_scores,
self.params.manual_threshold,
self.params.n_bins_hist,
verbose,
);
if verbosity.normal_verbosity() {
println!(
"Done with doublet scoring and calling {:.2?}",
start_score.elapsed()
);
println!("Finished Scrublet {:.2?}", start_all.elapsed());
}
let pca_out = if return_combined_pca {
Some(combined_pca)
} else {
None
};
let pair_1_out = if return_pairs { Some(pair_1) } else { None };
let pair_2_out = if return_pairs { Some(pair_2) } else { None };
Ok((res, pca_out, pair_1_out, pair_2_out))
}
fn calculate_doublet_scores(&self, knn_indices: &[Vec<usize>]) -> ScrubletDoubletScores {
let n_obs = self.n_cells;
let n_sim = self.n_cells_sim;
let r = n_sim as f32 / n_obs as f32;
let rho = self.params.expected_doublet_rate;
let se_rho = self.params.stdev_doublet_rate;
let k_adj = adjusted_k(self.params.knn_params.k, self.n_cells, self.n_cells_sim);
let n_adj = k_adj as f32;
let scores_errors: Vec<(f32, f32)> = knn_indices
.par_iter()
.map(|neighbours| {
let n_sim_neigh = neighbours.iter().filter(|&&idx| idx >= n_obs).count() as f32;
let q = (n_sim_neigh + 1.0) / (n_adj + 2.0);
let denominator = 1.0 - rho - q * (1.0 - rho - rho / r);
let score = if denominator.abs() > 1e-10 {
(q * rho / r) / denominator
} else {
0.0
};
let se_q = (q * (1.0 - q) / (n_adj + 3.0)).sqrt();
let factor = q * rho / r / (denominator * denominator);
let se_score = factor
* ((se_q / q * (1.0 - rho)).powi(2) + (se_rho / rho * (1.0 - q)).powi(2))
.sqrt();
(score.max(0.0), se_score.max(1e-10))
})
.collect();
let (scores_obs, errors_obs): (Vec<f32>, Vec<f32>) =
scores_errors[..n_obs].iter().copied().unzip();
let (scores_sim, errors_sim): (Vec<f32>, Vec<f32>) =
scores_errors[n_obs..].iter().copied().unzip();
(scores_obs, errors_obs, scores_sim, errors_sim)
}
fn call_doublets(
&self,
doublet_scores: ScrubletDoubletScores,
manual_threshold: Option<f32>,
n_bins: usize,
verbose: usize,
) -> ScrubletResult {
let verbosity = parse_verbosity_level(verbose);
let threshold = manual_threshold.unwrap_or_else(|| {
let t = find_threshold_otsu(&doublet_scores.0, n_bins);
if verbosity.normal_verbosity() {
println!("Automatically set threshold at doublet score = {:.4}", t);
}
t
});
let predicted_doublets: Vec<bool> = doublet_scores
.0
.iter()
.map(|&score| score > threshold)
.collect();
let z_scores: Vec<f32> = doublet_scores
.0
.iter()
.zip(doublet_scores.1.iter())
.map(|(&score, &error)| (score - threshold) / error)
.collect();
let n_detected = predicted_doublets.iter().filter(|&&x| x).count();
let detected_doublet_rate = n_detected as f32 / doublet_scores.0.len() as f32;
let n_detectable = doublet_scores.2.iter().filter(|&&s| s > threshold).count();
let detectable_doublet_fraction = n_detectable as f32 / doublet_scores.2.len() as f32;
let overall_doublet_rate = if detectable_doublet_fraction > 0.01 {
detected_doublet_rate / detectable_doublet_fraction
} else {
0.0
};
if verbosity.normal_verbosity() {
println!(
"Detected doublet rate = {:.1}%",
100.0 * detected_doublet_rate
);
println!(
"Estimated detectable doublet fraction = {:.1}%",
100.0 * detectable_doublet_fraction
);
println!("Overall doublet rate:");
println!(
" Expected = {:.1}%",
100.0 * self.params.expected_doublet_rate
);
println!(" Estimated = {:.1}%", 100.0 * overall_doublet_rate);
}
ScrubletResult {
predicted_doublets,
doublet_scores_obs: doublet_scores.0,
doublet_scores_sim: doublet_scores.2,
doublet_errors_obs: doublet_scores.1,
z_scores,
threshold,
detected_doublet_rate,
detectable_doublet_fraction,
overall_doublet_rate,
}
}
}