use crate::alg::feature_coarsening::FeatureCoarsening;
use crate::alg::nb_dispersion::DispersionTrend;
use crate::alg::sparse_streaming::streaming_sparse_running_stats;
use crate::sparse_io_vector::SparseIoVec;
use indicatif::ParallelProgressIterator;
use legume_numeric::matrix::sparse_stat::SparseRunningStatistics;
use legume_numeric::matrix::traits::{IoOps, RunningStatOps};
use legume_numeric::matrix::utils::generate_minibatch_intervals;
use rayon::prelude::*;
pub fn fisher_weights_from_stats(
stats: &SparseRunningStatistics<f32>,
n_entities: usize,
) -> Vec<f32> {
let trend = DispersionTrend::from_sparse_stats(stats);
let means = stats.mean();
let sums = stats.sum();
let total_mass: f64 = sums.iter().map(|&s| s as f64).sum();
let avg_s = if n_entities > 0 {
(total_mass / n_entities as f64) as f32
} else {
1.0
};
let inv_total = if total_mass > 0.0 {
1.0 / total_mass as f32
} else {
0.0
};
(0..means.len())
.map(|g| trend.fisher_weight(sums[g] * inv_total, avg_s, means[g]))
.collect()
}
pub fn fisher_weights_from_pseudobulk(
mu_ds: &nalgebra::DMatrix<f32>,
size_s: &[f32],
coarsening: Option<&FeatureCoarsening>,
) -> anyhow::Result<Vec<f32>> {
let coarse = coarsening.map(|fc| fc.aggregate_rows_ds(mu_ds));
let m = coarse.as_ref().unwrap_or(mu_ds);
anyhow::ensure!(
size_s.len() == m.ncols(),
"pseudobulk NB-Fisher: {} cell counts for {} pseudobulks",
size_s.len(),
m.ncols(),
);
let d = m.nrows();
let mut stats = SparseRunningStatistics::<f32>::new(d);
for (col, &n) in m.as_slice().chunks_exact(d).zip(size_s) {
stats.add_dense_column_scaled(col, n.max(1.0));
}
let n_cells = f64::from(size_s.iter().sum::<f32>()).max(1.0) as usize;
Ok(fisher_weights_from_stats(&stats, n_cells))
}
pub fn compute_nb_fisher_weights(
data_vec: &SparseIoVec,
block_size: Option<usize>,
) -> anyhow::Result<Vec<f32>> {
let n_cells = data_vec.num_columns();
let stats = streaming_sparse_running_stats(data_vec, block_size, "NB-Fisher")?;
Ok(fisher_weights_from_stats(&stats, n_cells))
}
pub fn compute_nb_fisher_weights_coarsened(
data_vec: &SparseIoVec,
coarsening: &FeatureCoarsening,
block_size: Option<usize>,
) -> anyhow::Result<Vec<f32>> {
let n_features_coarse = coarsening.num_coarse;
let n_total = data_vec.num_columns();
let jobs = generate_minibatch_intervals(n_total, n_features_coarse, block_size);
let prog_bar = legume_numeric::matrix::progress::new_progress_bar(jobs.len() as u64)
.with_message("NB-Fisher (coarse) blocks");
let stats: SparseRunningStatistics<f32> = jobs
.par_iter()
.progress_with(prog_bar.clone())
.try_fold(
|| SparseRunningStatistics::<f32>::new(n_features_coarse),
|mut acc, &(lb, ub)| -> anyhow::Result<SparseRunningStatistics<f32>> {
let chunk = data_vec.read_columns_csc(lb..ub)?;
let coarse = coarsening.aggregate_sparse_csc(&chunk);
acc.add_dense_columns(&coarse);
Ok(acc)
},
)
.try_reduce(
|| SparseRunningStatistics::<f32>::new(n_features_coarse),
|mut a, b| {
a.merge(&b);
Ok(a)
},
)?;
prog_bar.finish_and_clear();
Ok(fisher_weights_from_stats(&stats, n_total))
}
pub fn save_per_gene_weights(
weights: &[f32],
gene_names: &[Box<str>],
out_path: &str,
) -> anyhow::Result<()> {
let mat = nalgebra::DMatrix::<f32>::from_column_slice(weights.len(), 1, weights);
let weight_col = vec![Box::<str>::from("weight")];
mat.to_parquet_with_names(
out_path,
(Some(gene_names), Some("gene")),
Some(&weight_col),
)?;
Ok(())
}
pub fn save_fisher_weights(
out_prefix: &str,
weights: &[f32],
gene_names: &[Box<str>],
) -> anyhow::Result<()> {
save_per_gene_weights(
weights,
gene_names,
&format!("{out_prefix}.fisher_weights.parquet"),
)
}
pub type GeneWeights = (Vec<Box<str>>, Vec<f32>);
pub fn load_per_gene_weights(path: &str) -> anyhow::Result<GeneWeights> {
let result = nalgebra::DMatrix::<f32>::from_parquet_with_row_names(path, Some(0))?;
anyhow::ensure!(
result.mat.ncols() >= 1,
"per-gene weights parquet at {path} has no value column",
);
let weights: Vec<f32> = result.mat.column(0).iter().copied().collect();
Ok((result.rows, weights))
}
pub fn load_fisher_weights(prefix: &str) -> anyhow::Result<Option<GeneWeights>> {
let path = format!("{prefix}.fisher_weights.parquet");
if !std::path::Path::new(&path).exists() {
return Ok(None);
}
Ok(Some(load_per_gene_weights(&path)?))
}
pub fn save_fisher_weights_coarse(out_prefix: &str, weights: &[f32]) -> anyhow::Result<()> {
let axis_names: Vec<Box<str>> = (0..weights.len())
.map(|i| format!("coarse_{i}").into_boxed_str())
.collect();
save_per_gene_weights(
weights,
&axis_names,
&format!("{out_prefix}.fisher_weights_coarse.parquet"),
)
}
pub fn load_fisher_weights_coarse(prefix: &str) -> anyhow::Result<Option<GeneWeights>> {
let path = format!("{prefix}.fisher_weights_coarse.parquet");
if !std::path::Path::new(&path).exists() {
return Ok(None);
}
Ok(Some(load_per_gene_weights(&path)?))
}
pub fn apply_gene_weights(sum_gk: &mut nalgebra::DMatrix<f32>, weights: &[f32]) {
debug_assert_eq!(weights.len(), sum_gk.nrows());
for (g, &w) in weights.iter().enumerate() {
sum_gk.row_mut(g).scale_mut(w);
}
}