use data_beans::sparse_io::create_sparse_from_triplets;
use data_beans::sparse_io_vector::SparseIoVec;
use data_beans::alg::collapse_data::CollapsingOps;
const D: usize = 6;
fn backend(tag: &str, cells: &[Vec<f32>]) -> SparseIoVec {
let dir = std::env::temp_dir().join(format!("dba_wcol_{tag}_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
let path = dir.join(format!("{tag}.zarr"));
let _ = std::fs::remove_dir_all(&path);
let mut triplets: Vec<(u64, u64, f32)> = Vec::new();
for (j, col) in cells.iter().enumerate() {
for (g, &v) in col.iter().enumerate() {
if v != 0.0 {
triplets.push((g as u64, j as u64, v));
}
}
}
let shape = (D, cells.len(), triplets.len());
let mut b = create_sparse_from_triplets(
&triplets,
shape,
Some(path.to_str().expect("utf8")),
Some(&data_beans::sparse_io::SparseIoBackend::Zarr),
)
.expect("create backend");
b.register_row_names_vec(
&(0..D)
.map(|g| format!("g{g}").into_boxed_str())
.collect::<Vec<_>>(),
);
b.register_column_names_vec(
&(0..cells.len())
.map(|c| format!("c{c}").into_boxed_str())
.collect::<Vec<_>>(),
);
let mut v = SparseIoVec::new();
v.push(std::sync::Arc::from(b), None).expect("push");
v
}
fn one_group(v: &mut SparseIoVec, weights: Option<&[f32]>) -> (Vec<f32>, f32) {
let n = v.num_columns();
v.register_batch_membership(&vec!["b0"; n]);
if let Some(w) = weights {
v.register_column_multiplicity(w).expect("weights");
}
v.assign_groups(&vec!["g0".to_string(); n], None);
let out = v
.collapse_columns(None, None, None, Some(1))
.expect("collapse");
let mu = legume_numeric::param::traits::Inference::posterior_mean(&out.mu_observed);
let rates: Vec<f32> = (0..D).map(|g| mu[(g, 0)]).collect();
(rates, n as f32)
}
#[test]
fn unit_weights_change_nothing() {
let cells: Vec<Vec<f32>> = (0..8)
.map(|j| (0..D).map(|g| ((g + j) % 5) as f32).collect())
.collect();
let (plain, _) = one_group(&mut backend("plain", &cells), None);
let ones = vec![1.0f32; cells.len()];
let (weighted, _) = one_group(&mut backend("ones", &cells), Some(&ones));
assert_eq!(
plain, weighted,
"registering all-ones multiplicity must be bit-for-bit inert"
);
}
#[test]
fn one_weighted_column_equals_the_cells_it_summarizes() {
const M: usize = 20;
let profile: Vec<f32> = (0..D).map(|g| 1.0 + g as f32).collect();
let many: Vec<Vec<f32>> = (0..M).map(|_| profile.clone()).collect();
let (from_cells, n_cells) = one_group(&mut backend("many", &many), None);
let (from_summary, n_summary) = one_group(
&mut backend("one", std::slice::from_ref(&profile)),
Some(&[M as f32]),
);
assert_eq!(n_cells, M as f32);
assert_eq!(n_summary, 1.0, "one physical column");
for g in 0..D {
assert!(
(from_cells[g] - from_summary[g]).abs() < 1e-4,
"gene {g}: {M} cells gave {}, the weighted summary gave {}",
from_cells[g],
from_summary[g]
);
}
}
#[test]
fn non_positive_and_mismatched_weights_are_refused() {
let cells: Vec<Vec<f32>> = (0..3).map(|_| vec![1.0; D]).collect();
let mut v = backend("bad", &cells);
let err = v
.register_column_multiplicity(&[1.0, 1.0])
.expect_err("wrong length must be refused");
assert!(err.to_string().contains("3 columns"), "{err}");
let err = v
.register_column_multiplicity(&[1.0, 0.0, 1.0])
.expect_err("zero weight must be refused");
assert!(err.to_string().contains("multiplicity"), "{err}");
let err = v
.register_column_multiplicity(&[1.0, f32::NAN, 1.0])
.expect_err("NaN weight must be refused");
assert!(err.to_string().contains("multiplicity"), "{err}");
}