use bonsai_rs::bonsai::{BonsaiParams, bonsai};
use bonsai_rs::errors::BonsaiErrors;
use bonsai_rs::ingest::{from_sanity_output, sanity_gene_passes};
use bonsai_rs::sanity_sc_rs::config::{SanityParams, Verbosity as SanityVerbosity};
use bonsai_rs::sanity_sc_rs::input::CountMatrix;
use bonsai_rs::sanity_sc_rs::{GeneView, SanityOutput, sanity_select};
use bonsai_rs::tree::layout::{Layout, dendrogram, equal_angle, equal_daylight};
use bonsai_rs::tree::{NO_NODE, Tree};
use indexmap::IndexSet;
use std::time::Instant;
use crate::prelude::*;
use crate::single_cell::sc_data::data_io::{RawCounts, SingleCellReading};
pub const SANITY_GENE_CHUNK: usize = 1024;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BonsaiLayout {
#[default]
EqualAngle,
EqualDaylight,
Dendrogram,
}
pub fn parse_bonsai_layout(s: &str) -> Option<BonsaiLayout> {
match s.to_lowercase().as_str() {
"equal_angle" => Some(BonsaiLayout::EqualAngle),
"equal_daylight" => Some(BonsaiLayout::EqualDaylight),
"dendrogram" => Some(BonsaiLayout::Dendrogram),
_ => None,
}
}
#[derive(Clone, Copy, Debug)]
pub struct BonsaiScParams {
pub sanity: SanityParams,
pub bonsai: BonsaiParams,
pub layout: BonsaiLayout,
pub hyperbolic: bool,
}
impl Default for BonsaiScParams {
fn default() -> Self {
Self {
sanity: SanityParams::default(),
bonsai: BonsaiParams {
reroot: true,
skip_posteriors: true,
..BonsaiParams::default()
},
layout: BonsaiLayout::default(),
hyperbolic: false,
}
}
}
pub struct BonsaiScResult {
pub parent: Vec<u32>,
pub branch: Vec<f64>,
pub x: Vec<f64>,
pub y: Vec<f64>,
pub n_leaves: usize,
pub loglik: f64,
pub steps: Vec<(String, f64, f64)>,
pub timings: Vec<(String, f64)>,
pub genes_used: Vec<usize>,
}
pub fn read_count_chunk<G: SingleCellReading>(
gene_reader: &G,
genes: &[usize],
cell_set: &IndexSet<u32>,
) -> Result<Option<(CountMatrix, Vec<usize>)>, BixverseErrors> {
let mut indices: Vec<u32> = Vec::new();
let mut values: Vec<u32> = Vec::new();
let mut indptr: Vec<usize> = vec![0];
let mut kept: Vec<usize> = Vec::with_capacity(genes.len());
for (&gene, chunk) in genes
.iter()
.zip(gene_reader.read_gene_parallel_filtered(genes, cell_set)?)
{
if chunk.indices.is_empty() {
continue;
}
indices.extend_from_slice(&chunk.indices);
match chunk.data_raw {
RawCounts::U16(v) => values.extend(v.into_iter().map(u32::from)),
RawCounts::U32(v) => values.extend(v),
}
indptr.push(indices.len());
kept.push(gene);
}
if kept.is_empty() {
return Ok(None);
}
Ok(Some((
CountMatrix::new(indices, values, indptr, cell_set.len())?,
kept,
)))
}
pub fn stream_sanity<G, C, F>(
gene_reader: &G,
cell_reader: &C,
cell_indices: &[usize],
gene_indices: &[usize],
gene_chunk: usize,
verbosity: Verbosity,
run_chunk: F,
) -> Result<SanityOutput<f32>, BixverseErrors>
where
G: SingleCellReading,
C: SingleCellReading,
F: FnMut(&CountMatrix, &[f64]) -> Result<SanityOutput<f32>, BixverseErrors>,
{
let cell_set: IndexSet<u32> = cell_indices.iter().map(|&c| c as u32).collect();
let cell_totals: Vec<f64> = cell_reader
.read_cell_library_sizes(cell_indices)?
.into_iter()
.map(|t| t as f64)
.collect();
stream_chunks(
gene_indices,
gene_chunk,
&cell_totals,
verbosity,
|genes| read_count_chunk(gene_reader, genes, &cell_set),
run_chunk,
)
}
pub(crate) fn stream_chunks<R, F>(
gene_indices: &[usize],
gene_chunk: usize,
cell_totals: &[f64],
verbosity: Verbosity,
mut read_chunk: R,
mut run_chunk: F,
) -> Result<SanityOutput<f32>, BixverseErrors>
where
R: FnMut(&[usize]) -> Result<Option<(CountMatrix, Vec<usize>)>, BixverseErrors>,
F: FnMut(&CountMatrix, &[f64]) -> Result<SanityOutput<f32>, BixverseErrors>,
{
let started = Instant::now();
let n_cells = cell_totals.len();
let mut out = SanityOutput {
log_fold_changes: Vec::new(),
error_bars: Vec::new(),
mean_log_quotient: Vec::new(),
mean_log_quotient_error: Vec::new(),
variance: Vec::new(),
genes: Vec::new(),
n_genes: 0,
n_cells,
};
let n_chunks = gene_indices.len().div_ceil(gene_chunk.max(1));
for (i, genes) in gene_indices.chunks(gene_chunk.max(1)).enumerate() {
let Some((counts, chunk_genes)) = read_chunk(genes)? else {
continue;
};
let post = run_chunk(&counts, cell_totals)?;
drop(counts);
out.log_fold_changes.extend(post.log_fold_changes);
out.error_bars.extend(post.error_bars);
out.mean_log_quotient.extend(post.mean_log_quotient);
out.mean_log_quotient_error
.extend(post.mean_log_quotient_error);
out.variance.extend(post.variance);
out.genes.extend(post.genes.iter().map(|&g| chunk_genes[g]));
if verbosity.normal_verbosity() {
println!(
" Sanity chunk {}/{}: kept {} of {} genes ({} kept so far, {:.2?}).",
i + 1,
n_chunks,
post.n_genes,
genes.len(),
out.genes.len(),
started.elapsed()
);
}
}
out.n_genes = out.genes.len();
if verbosity.normal_verbosity() {
println!(
"Sanity: kept {} of {} candidate genes over {} cells in {:.2?}.",
out.n_genes,
gene_indices.len(),
n_cells,
started.elapsed()
);
}
Ok(out)
}
pub fn keep_for_bonsai(params: &BonsaiScParams) -> impl Fn(GeneView<'_>) -> bool + Sync + use<> {
let ingest = params.bonsai.ingest;
move |g| sanity_gene_passes(g.log_fold_changes, g.error_bars, g.variance, &ingest)
}
pub(crate) fn chunk_sanity_params(params: &BonsaiScParams) -> SanityParams {
SanityParams {
verbosity: SanityVerbosity::Quiet,
..params.sanity
}
}
fn layout_tree(
tree: &Tree,
layout: BonsaiLayout,
hyperbolic: bool,
) -> Result<Layout, BixverseErrors> {
let out = match layout {
BonsaiLayout::EqualAngle => equal_angle(tree, None)?,
BonsaiLayout::EqualDaylight => equal_daylight(tree, None)?,
BonsaiLayout::Dendrogram => dendrogram(tree, None)?,
};
Ok(if hyperbolic {
out.hyperbolic(None)
} else {
out
})
}
pub fn bonsai_layout(
parent: Vec<u32>,
branch: Vec<f64>,
n_leaves: usize,
layout: BonsaiLayout,
hyperbolic: bool,
) -> Result<(Vec<u32>, Vec<f64>, Layout), BixverseErrors> {
let tree = Tree::from_parents(parent, branch, n_leaves)?;
let coords = layout_tree(&tree, layout, hyperbolic)?;
let (parent, branch) = tree_arrays(&tree);
Ok((parent, branch, coords))
}
fn tree_arrays(tree: &Tree) -> (Vec<u32>, Vec<f64>) {
let parent = (0..tree.n_nodes() as u32)
.map(|v| tree.parent(v).unwrap_or(NO_NODE))
.collect();
(parent, tree.branches().to_vec())
}
pub fn run_bonsai_sc(
post: SanityOutput<f32>,
n_candidates: usize,
params: &BonsaiScParams,
verbosity: Verbosity,
) -> Result<BonsaiScResult, BixverseErrors> {
if post.n_genes == 0 {
return Err(BonsaiErrors::NoFeaturesRetained {
n_features: n_candidates,
threshold: params.bonsai.ingest.min_signal_to_noise,
}
.into());
}
let started = Instant::now();
let lik = from_sanity_output(&post, Some(params.bonsai.ingest))?;
drop(post);
let t_ingest = started.elapsed().as_secs_f64();
if verbosity.normal_verbosity() {
println!(
"Sanity ingest kept {} genes, dropped {} as ill-conditioned ({:.2?}).",
lik.features.len(),
lik.dropped.len(),
started.elapsed()
);
}
let started = Instant::now();
let out = bonsai(
&lik.means,
&lik.sds,
lik.n_cells,
lik.features.len(),
Some(&lik.variances),
Some(params.bonsai),
bonsai_verbosity(verbosity),
)?;
let t_bonsai = started.elapsed().as_secs_f64();
if verbosity.normal_verbosity() {
println!("Bonsai finished in {t_bonsai:.2} s.");
}
let started = Instant::now();
let coords = layout_tree(&out.tree, params.layout, params.hyperbolic)?;
let (parent, branch) = tree_arrays(&out.tree);
let t_layout = started.elapsed().as_secs_f64();
if verbosity.normal_verbosity() {
println!("Layout of {} nodes in {t_layout:.2} s.", parent.len());
}
Ok(BonsaiScResult {
parent,
branch,
x: coords.x,
y: coords.y,
n_leaves: out.tree.n_leaves(),
loglik: out.loglik,
steps: out
.steps
.iter()
.map(|s| (s.step.to_string(), s.loglik, s.seconds))
.collect(),
timings: vec![
("ingest".to_string(), t_ingest),
("bonsai".to_string(), t_bonsai),
("layout".to_string(), t_layout),
],
genes_used: out.features.iter().map(|&f| lik.features[f]).collect(),
})
}
pub fn sanity_bonsai_sc<G, C>(
gene_reader: &G,
cell_reader: &C,
cell_indices: &[usize],
gene_indices: &[usize],
params: &BonsaiScParams,
verbosity: Verbosity,
) -> Result<BonsaiScResult, BixverseErrors>
where
G: SingleCellReading,
C: SingleCellReading,
{
let started = Instant::now();
let sanity_params = chunk_sanity_params(params);
let keep = keep_for_bonsai(params);
let post = stream_sanity(
gene_reader,
cell_reader,
cell_indices,
gene_indices,
SANITY_GENE_CHUNK,
verbosity,
|counts, totals| Ok(sanity_select(counts, totals, Some(sanity_params), &keep)?),
)?;
let t_sanity = started.elapsed().as_secs_f64();
let mut res = run_bonsai_sc(post, gene_indices.len(), params, verbosity)?;
res.timings.insert(0, ("sanity".to_string(), t_sanity));
Ok(res)
}
fn bonsai_verbosity(verbosity: Verbosity) -> bonsai_rs::utils::verbosity::Verbosity {
use bonsai_rs::utils::verbosity::Verbosity as BonsaiVerbosity;
match verbosity {
Verbosity::Quiet => BonsaiVerbosity::Quiet,
Verbosity::Normal => BonsaiVerbosity::Normal,
Verbosity::Detailed => BonsaiVerbosity::Detailed,
}
}