use edge_rs::errors::EdgeErrors;
use edge_rs::prelude::{CompressedSparse, SparseFormat};
use edge_rs::sc::nebula::{NebulaFit, NebulaMethod, NebulaParams, nebula_sparse};
use edge_rs::sc::shrink::{sc_residual_df, shrink_sc_dispersion};
use edge_rs::sc::test::{ScTested, glm_sc_test};
use indexmap::IndexSet;
use std::time::Instant;
use crate::prelude::*;
const GENE_BATCH_SIZE: usize = 1000;
const SHRINK_COVARIATE_OFFSET: f64 = 0.5;
pub fn parse_nebula_method(s: &str) -> Option<NebulaMethod> {
match s.to_lowercase().as_str() {
"ln" => Some(NebulaMethod::Ln),
"hl" => Some(NebulaMethod::Hl),
_ => None,
}
}
#[derive(Clone, Debug)]
pub struct NebulaScParams {
pub nebula: NebulaParams,
pub gene_batch_size: usize,
pub shrink_dispersion: bool,
pub tested: ScTested,
}
impl Default for NebulaScParams {
fn default() -> Self {
Self {
nebula: NebulaParams::default(),
gene_batch_size: GENE_BATCH_SIZE,
shrink_dispersion: true,
tested: ScTested::Coef(0),
}
}
}
#[derive(Clone, Debug)]
pub struct NebulaScRes {
pub gene_idx: Vec<usize>,
pub coefficients: Vec<f64>,
pub se: Vec<f64>,
pub subject_overdispersion: Vec<f64>,
pub cell_overdispersion: Vec<f64>,
pub cell_overdispersion_shrunk: Option<Vec<f64>>,
pub convergence: Vec<i32>,
pub sigma_at_bound: Vec<bool>,
pub log_fc: Vec<f64>,
pub effect_se: Vec<f64>,
pub z: Vec<f64>,
pub p_val: Vec<f64>,
pub fdr: Vec<f64>,
pub n_coef: usize,
}
fn subject_order(cells_to_keep: &[usize], subject_ids: &[usize]) -> Vec<usize> {
let mut order: Vec<usize> = (0..cells_to_keep.len()).collect();
order.sort_by_key(|&i| (subject_ids[cells_to_keep[i]], cells_to_keep[i]));
order
}
fn chunks_to_csr(
chunks: &[CscGeneChunk],
n_cells: usize,
) -> Result<CompressedSparse<f64>, BixverseErrors> {
let nnz: usize = chunks.iter().map(|c| c.data_raw.len()).sum();
let mut data = Vec::with_capacity(nnz);
let mut indices = Vec::with_capacity(nnz);
let mut indptr = Vec::with_capacity(chunks.len() + 1);
indptr.push(0u32);
for chunk in chunks {
data.extend(chunk.data_raw.iter().map(|v| v as f64));
indices.extend_from_slice(&chunk.indices);
indptr.push(data.len() as u32);
}
Ok(CompressedSparse::from_parts(
data,
indices,
indptr,
SparseFormat::Csr,
(chunks.len(), n_cells),
)?)
}
#[allow(clippy::too_many_arguments)]
pub fn run_nebula<S: SingleCellReading>(
gene_reader: &S,
cell_reader: &S,
cells_to_keep: &[usize],
gene_indices: &[usize],
subject_ids: &[usize],
design: &[f64],
n_coef: usize,
offset: Option<&[f64]>,
params: &NebulaScParams,
verbose: usize,
) -> Result<NebulaScRes, BixverseErrors> {
run_nebula_with(
gene_reader,
cell_reader,
cells_to_keep,
gene_indices,
subject_ids,
design,
n_coef,
offset,
params,
verbose,
|counts, subject_run, design_ordered, n_coef, offsets, nebula_params| {
nebula_sparse(
counts,
subject_run,
design_ordered,
n_coef,
Some(offsets),
Some(nebula_params),
)
},
)
}
#[allow(clippy::too_many_arguments)]
pub fn run_nebula_with<S, F>(
gene_reader: &S,
cell_reader: &S,
cells_to_keep: &[usize],
gene_indices: &[usize],
subject_ids: &[usize],
design: &[f64],
n_coef: usize,
offset: Option<&[f64]>,
params: &NebulaScParams,
verbose: usize,
mut fit_batch: F,
) -> Result<NebulaScRes, BixverseErrors>
where
S: SingleCellReading,
F: FnMut(
&CompressedSparse<f64>,
&[usize],
&[f64],
usize,
&[f64],
NebulaParams,
) -> Result<NebulaFit, EdgeErrors>,
{
let verbosity = parse_verbosity_level(verbose);
let start_all = Instant::now();
let n_cells = cells_to_keep.len();
let header = gene_reader.get_header();
if n_coef == 0 {
return Err(BixverseErrors::MustBePositive("n_coef".to_string()));
}
if params.gene_batch_size == 0 {
return Err(BixverseErrors::MustBePositive(
"gene_batch_size".to_string(),
));
}
if design.len() != n_cells * n_coef {
return Err(BixverseErrors::DgeShapeMismatch {
name: "design",
expected: n_cells * n_coef,
got: design.len(),
});
}
if subject_ids.len() != header.total_cells {
return Err(BixverseErrors::DgeShapeMismatch {
name: "subject_ids",
expected: header.total_cells,
got: subject_ids.len(),
});
}
if let Some(o) = offset
&& o.len() != n_cells
{
return Err(BixverseErrors::DgeShapeMismatch {
name: "offset",
expected: n_cells,
got: o.len(),
});
}
if let Some(&cell) = cells_to_keep.iter().find(|&&c| c >= header.total_cells) {
return Err(BixverseErrors::ChunkIndexNotFound(cell));
}
if let Some(&gene) = gene_indices.iter().find(|&&g| g >= header.total_genes) {
return Err(BixverseErrors::ChunkIndexNotFound(gene));
}
let order = subject_order(cells_to_keep, subject_ids);
let ordered_cells: Vec<usize> = order.iter().map(|&i| cells_to_keep[i]).collect();
let subject_run: Vec<usize> = ordered_cells.iter().map(|&c| subject_ids[c]).collect();
let n_subjects = subject_run.windows(2).filter(|w| w[0] != w[1]).count() + 1;
let mut design_ordered = Vec::with_capacity(design.len());
for &i in &order {
design_ordered.extend_from_slice(&design[i * n_coef..(i + 1) * n_coef]);
}
let offsets: Vec<f64> = match offset {
Some(o) => order.iter().map(|&i| o[i]).collect(),
None => cell_reader
.read_cell_library_sizes(&ordered_cells)?
.iter()
.map(|&v| v as f64)
.collect(),
};
let cell_set: IndexSet<u32> = ordered_cells.iter().map(|&c| c as u32).collect();
if cell_set.len() != n_cells {
return Err(BixverseErrors::DgeShapeMismatch {
name: "cells_to_keep (holds duplicates)",
expected: n_cells,
got: cell_set.len(),
});
}
let n_batches = gene_indices.len().div_ceil(params.gene_batch_size);
let mut gene_idx: Vec<usize> = Vec::new();
let mut coefficients: Vec<f64> = Vec::new();
let mut covariance: Vec<f64> = Vec::new();
let mut se: Vec<f64> = Vec::new();
let mut subject_overdispersion: Vec<f64> = Vec::new();
let mut cell_overdispersion: Vec<f64> = Vec::new();
let mut convergence: Vec<i32> = Vec::new();
let mut sigma_at_bound: Vec<bool> = Vec::new();
let mut mean_count: Vec<f64> = Vec::new();
for batch in 0..n_batches {
let start = batch * params.gene_batch_size;
let end = ((batch + 1) * params.gene_batch_size).min(gene_indices.len());
let batch_genes = &gene_indices[start..end];
let start_loading = Instant::now();
let chunks = gene_reader.read_gene_parallel_filtered(batch_genes, &cell_set)?;
let sparse = chunks_to_csr(&chunks, n_cells)?;
if verbosity.detailed_verbosity() {
println!(" Loaded batch in: {:.2?}.", start_loading.elapsed());
}
let start_fit = Instant::now();
let fit = match fit_batch(
&sparse,
&subject_run,
&design_ordered,
n_coef,
&offsets,
params.nebula,
) {
Ok(fit) => Some(fit),
Err(edge_rs::errors::EdgeErrors::NoGenesAfterFiltering { .. }) => None,
Err(e) => return Err(e.into()),
};
if verbosity.detailed_verbosity() {
println!(" Fitted batch in: {:.2?}.", start_fit.elapsed());
}
if let Some(fit) = fit {
for &row in &fit.gene_index {
gene_idx.push(chunks[row].original_index);
let (_, values) = sparse.outer(row);
mean_count.push(values.iter().sum::<f64>() / n_cells as f64);
}
coefficients.extend_from_slice(&fit.coefficients);
covariance.extend_from_slice(&fit.covariance);
se.extend_from_slice(&fit.se);
subject_overdispersion.extend_from_slice(&fit.subject_overdispersion);
cell_overdispersion.extend_from_slice(&fit.cell_overdispersion);
convergence.extend_from_slice(&fit.convergence);
sigma_at_bound.extend_from_slice(&fit.sigma_at_bound);
}
if verbosity.normal_verbosity() {
report_decile_progress(end, start, gene_indices.len(), "genes", start_all.elapsed());
}
}
let n_kept = gene_idx.len();
if n_kept == 0 {
return Err(BixverseErrors::NebulaNoGenesKept {
n_genes: gene_indices.len(),
});
}
let test = glm_sc_test(&coefficients, &covariance, n_kept, n_coef, ¶ms.tested)?;
let cell_overdispersion_shrunk = if params.shrink_dispersion {
let covariate: Vec<f64> = mean_count
.iter()
.map(|m| (m + SHRINK_COVARIATE_OFFSET).log2())
.collect();
let df_residual = sc_residual_df(n_cells, n_coef, n_subjects);
let shrunk = shrink_sc_dispersion(
&cell_overdispersion,
&convergence,
Some(&covariate),
df_residual,
None,
)?;
Some(shrunk.dispersion_shrunk)
} else {
None
};
if verbosity.normal_verbosity() {
println!(
"NEBULA: fitted {n_kept} of {} genes in {:.2?}.",
gene_indices.len(),
start_all.elapsed()
);
}
Ok(NebulaScRes {
gene_idx,
coefficients,
se,
subject_overdispersion,
cell_overdispersion,
cell_overdispersion_shrunk,
convergence,
sigma_at_bound,
log_fc: test.log_fc,
effect_se: test.se,
z: test.z,
p_val: test.p_value,
fdr: test.fdr,
n_coef,
})
}