pub mod fidelity;
pub mod gaussian;
pub mod latent;
pub mod marginals;
pub mod mixture;
pub mod reference;
pub mod sampler;
use gaussian::CopulaCovariance;
use latent::ReferenceLatent;
use log::info;
use mixture::PoissonMixture;
use rayon::prelude::*;
use reference::SparseRef;
pub struct GlobalCopulaFit {
pub gene_names: Vec<Box<str>>,
pub n_genes: usize,
pub hvg_indices: Vec<usize>,
pub mu_hat: Vec<f32>,
pub marginals: Vec<PoissonMixture>,
pub depth_rho: Vec<f32>,
pub active_genes: Vec<usize>,
pub hvg_pos: Vec<Option<u32>>,
pub copula: CopulaCovariance,
pub profile: fidelity::Profile,
pub correlation_genes: Vec<usize>,
}
impl GlobalCopulaFit {
pub fn write_effective_nb_size(&self, path: &str, names: &[Box<str>]) -> anyhow::Result<()> {
use legume_numeric::matrix::traits::IoOps;
let r = nalgebra::DMatrix::<f32>::from_iterator(
self.n_genes,
1,
self.marginals.iter().map(|m| {
let cv2 = m.rate_cv2();
if cv2 > 0.0 {
(1.0 / cv2) as f32
} else {
1e9
}
}),
);
let label = ["r_hat".to_string().into_boxed_str()];
r.to_parquet_with_names(path, (Some(names), Some("feature")), Some(&label))
}
pub fn write_fidelity_report(
&self,
triplets: &[(u64, u64, f32)],
n_cells: usize,
reference: &str,
prefix: &str,
) -> anyhow::Result<String> {
let simulated = fidelity::profile(
triplets,
self.n_genes,
n_cells,
&self.active_genes,
&self.correlation_genes,
);
let report = fidelity::FidelityReport {
reference: reference.to_string(),
output: prefix.to_string(),
gene_names: self
.active_genes
.iter()
.map(|&g| self.gene_names[g].to_string())
.collect(),
correlation_genes: self.correlation_genes.clone(),
profiles: fidelity::Paired {
reference: self.profile.clone(),
simulated,
},
};
let path = fidelity::report_path(prefix);
report.write(&path)?;
Ok(path)
}
}
#[derive(clap::Args, Debug, Clone)]
pub struct ReferenceFitArgs {
#[arg(
long,
default_value_t = 2000,
help = "Features (HVGs) in the latent copula",
long_help = "Features (HVGs) in the latent copula, per reference.\n\
Features outside it are drawn independently given depth.\n\
This is used only with a reference."
)]
pub n_hvg: usize,
#[arg(
long,
default_value_t = 100,
help = "Maximum rank of the copula's latent correlation factor",
long_help = "Maximum rank of the factor fitted to the HVGs' latent correlation.\n\
Only factors with positive eigenvalues are kept, so the effective rank\n\
is at most `n_hvg`. This is used only with a reference."
)]
pub copula_rank: usize,
#[arg(
long,
default_value_t = 1e-3,
help = "Smallest share of each HVG's latent variance kept outside the shared factors",
long_help = "Smallest share of each HVG's latent variance kept as its own,\n\
outside the shared factors, so no feature is a copy of another.\n\
This is used only with a reference."
)]
pub regularization: f32,
#[arg(
long,
default_value_t = 0.005,
help = "Smallest share of reference cells an HVG must be detected in",
long_help = "Smallest share of reference cells an HVG must be detected in,\n\
and never fewer than 50 cells. Features seen in fewer cells carry no\n\
correlation the reference can estimate; they are drawn independently\n\
given depth. This is used only with a reference."
)]
pub hvg_min_detection: f32,
#[arg(
long,
default_value_t = false,
help = "Do not couple features to a per-cell depth (reference mode)",
long_help = "Do not couple features to a per-cell depth.\n\
By default each cell draws a depth coordinate (one per modality), and\n\
every feature follows it as strongly as it follows library size in the\n\
reference, so library sizes and detected features vary as the\n\
reference's do. With this flag every cell has the same expected depth.\n\
Each feature keeps the reference's distribution either way.\n\
This is used only with a reference."
)]
pub fixed_depth: bool,
#[arg(
long,
hide = true,
help = "No longer used: reference marginals are Poisson mixtures, with no NB size to bound"
)]
pub r_floor: Option<f32>,
}
impl ReferenceFitArgs {
pub fn global_args<'a>(&self, sc: &'a SparseRef) -> GlobalCopulaArgs<'a> {
if self.r_floor.is_some() {
log::warn!("--r-floor is no longer used: reference marginals are Poisson mixtures");
}
GlobalCopulaArgs {
sc,
n_hvg: self.n_hvg,
copula_rank: self.copula_rank,
regularization: self.regularization,
hvg_min_detection: self.hvg_min_detection,
}
}
}
pub struct GlobalCopulaArgs<'a> {
pub sc: &'a SparseRef,
pub n_hvg: usize,
pub copula_rank: usize,
pub regularization: f32,
pub hvg_min_detection: f32,
}
pub fn fit_global_copula(args: &GlobalCopulaArgs) -> anyhow::Result<GlobalCopulaFit> {
let n_genes = args
.sc
.num_rows()
.ok_or_else(|| anyhow::anyhow!("reference has no num_rows"))?;
let n_cells = args
.sc
.num_columns()
.ok_or_else(|| anyhow::anyhow!("reference has no num_columns"))?;
if n_cells < 2 {
anyhow::bail!(
"reference has only {} cells; need ≥2 to fit a copula",
n_cells
);
}
let gene_names = args.sc.row_names()?;
info!("reference: {} genes × {} cells", n_genes, n_cells);
let cells: Vec<usize> = (0..n_cells).collect();
let (stats, libs, mat) = reference::per_gene_stats(args.sc, &cells, n_genes)?;
let mu_hat: Vec<f32> = stats.iter().map(|s| s.mu as f32).collect();
let active_genes: Vec<usize> = (0..n_genes).filter(|&g| stats[g].nnz > 0).collect();
let hvg_indices = reference::select_hvg(&stats, n_cells, args.n_hvg, args.hvg_min_detection);
anyhow::ensure!(
!hvg_indices.is_empty(),
"no gene is detected in {:.1}% (and 50) of the {} reference cells; lower --hvg-min-detection",
100.0 * args.hvg_min_detection,
n_cells
);
info!(
"active genes (detected in the reference): {}/{} ({:.1}%)",
active_genes.len(),
n_genes,
100.0 * active_genes.len() as f32 / n_genes as f32
);
let hist = latent::gene_histograms(&mat, n_cells, &active_genes);
let marginals: Vec<PoissonMixture> = hist
.par_iter()
.map(|h| {
h.as_deref()
.map_or_else(PoissonMixture::zero, PoissonMixture::fit)
})
.collect();
let latent = ReferenceLatent::fit(&mat, &libs, &hist, &hvg_indices);
drop(hist);
let zero: Vec<f32> = active_genes
.iter()
.map(|&g| 1.0 - stats[g].nnz as f32 / n_cells as f32)
.collect();
let correlation_genes = fidelity::correlation_genes(&active_genes, &hvg_indices, &zero);
let profile = fidelity::profile(&mat, n_genes, n_cells, &active_genes, &correlation_genes);
drop(mat);
let copula = CopulaCovariance::from_correlation(
&latent.hvg_residual,
args.copula_rank,
args.regularization,
)?;
let mean_abs_depth = active_genes
.iter()
.map(|&g| latent.depth_rho[g].abs())
.sum::<f32>()
/ active_genes.len().max(1) as f32;
info!(
"fit global copula: {} HVGs (detected in ≥ {:.1}% of cells), rank {} (cap {}), \
mean residual sd {:.3}; mean |ρ| with depth {:.3}",
hvg_indices.len(),
100.0 * args.hvg_min_detection,
copula.rank(),
args.copula_rank,
copula.ridge_sd.mean(),
mean_abs_depth
);
let mut hvg_pos = vec![None; n_genes];
for (h, &g) in hvg_indices.iter().enumerate() {
hvg_pos[g] = Some(h as u32);
}
Ok(GlobalCopulaFit {
gene_names,
n_genes,
hvg_indices,
mu_hat,
marginals,
depth_rho: latent.depth_rho,
active_genes,
hvg_pos,
copula,
profile,
correlation_genes,
})
}