use data_beans::sparse_io::create_sparse_from_triplets;
use data_beans::sparse_io_vector::{RowAlignment, SparseIoVec};
use data_beans::alg::collapse_data::{
collapse_columns_multilevel_with_hierarchy, CollapsedOut, MultilevelParams,
};
use data_beans::alg::random_projection::RandProjOps;
use legume_numeric::param::traits::Inference;
use std::sync::Arc;
const D_FULL: usize = 6;
const MISSING_GENE: usize = 5;
const N_PER_BATCH: usize = 60;
const TRUE_RATE: f32 = 8.0;
fn rate(cell_type: u8, gene: usize, c: usize) -> f32 {
let wiggle = 1.0 + 0.05 * ((c % 5) as f32 - 2.0);
match cell_type {
1 => TRUE_RATE * wiggle,
_ => (if gene == 0 { 40.0 } else { 2.0 }) * wiggle,
}
}
fn one_backend(
dir: &std::path::Path,
tag: &str,
genes: &[usize],
cell_types: &[u8],
) -> Arc<dyn data_beans::sparse_io::SparseIo<IndexIter = Vec<usize>>> {
let n_cells = cell_types.len();
let path = dir.join(format!("{tag}.zarr"));
let _ = std::fs::remove_dir_all(&path);
let mut triplets: Vec<(u64, u64, f32)> = Vec::new();
for (c, &t) in cell_types.iter().enumerate() {
for (local, &g) in genes.iter().enumerate() {
triplets.push((local as u64, c as u64, rate(t, g, c)));
}
}
let shape = (genes.len(), n_cells, 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(
&genes
.iter()
.map(|g| format!("g{g}").into_boxed_str())
.collect::<Vec<_>>(),
);
b.register_column_names_vec(
&(0..n_cells)
.map(|c| format!("{tag}_c{c}").into_boxed_str())
.collect::<Vec<_>>(),
);
Arc::from(b)
}
fn cohort(tag: &str) -> (SparseIoVec, Vec<&'static str>) {
let dir = std::env::temp_dir().join(format!("dba_panel_{tag}_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
let full: Vec<usize> = (0..D_FULL).collect();
let narrow: Vec<usize> = (0..D_FULL).filter(|&g| g != MISSING_GENE).collect();
let full_types = vec![1u8; N_PER_BATCH];
let mut narrow_types = vec![1u8; N_PER_BATCH / 2];
narrow_types.extend(vec![2u8; N_PER_BATCH - N_PER_BATCH / 2]);
let mut v = SparseIoVec::new()
.with_row_alignment(RowAlignment::Union)
.expect("union rows");
v.push(
one_backend(&dir, &format!("{tag}_full"), &full, &full_types),
None,
)
.expect("push full");
v.push(
one_backend(&dir, &format!("{tag}_narrow"), &narrow, &narrow_types),
None,
)
.expect("push narrow");
let mut batches = vec!["full"; N_PER_BATCH];
batches.extend(vec!["narrow"; N_PER_BATCH]);
(v, batches)
}
fn run(tag: &str, observe_panels: bool) -> (CollapsedOut, Vec<Box<str>>) {
let (mut v, batches) = cohort(tag);
let proj = v
.project_columns_with_batch_correction(3, None, Some(&batches))
.expect("proj")
.proj;
let params = MultilevelParams {
knn_pb_samples: 3,
num_levels: 1,
sort_dim: 1,
num_opt_iter: 30,
refine: data_beans::alg::refine_multilevel::RefineParams::default(),
output_calibration: legume_numeric::param::traits::CalibrateTarget::All,
anchor_batches: None,
bulk_batches: None,
observe_panels,
keep_finest_stats: false,
pb_tree: None,
strata: None,
};
let mut out = collapse_columns_multilevel_with_hierarchy(&mut v, &proj, &batches, ¶ms)
.expect("collapse");
let names = v.batch_names().expect("batch names");
(out.levels.remove(0), names)
}
fn missing_row(v_names_hint: usize) -> usize {
let _ = v_names_hint;
MISSING_GENE
}
#[test]
fn a_panel_gap_does_not_drag_mu_toward_zero() {
let (masked, _) = run("mu_masked", true);
let (naive, _) = run("mu_naive", false);
let g = missing_row(D_FULL);
let prior_mean = 1.0;
let m_masked = masked.mu_observed.posterior_mean();
let m_naive = naive.mu_observed.posterior_mean();
let (mut saw_measured, mut saw_prior, mut saw_dragged) = (false, false, false);
for s_idx in 0..m_masked.ncols() {
let ctrl: f32 = (1..MISSING_GENE).map(|r| m_masked[(r, s_idx)]).sum::<f32>()
/ (MISSING_GENE - 1) as f32;
let vm = m_masked[(g, s_idx)];
let vn = m_naive[(g, s_idx)];
let is_prior = (vm - prior_mean).abs() < 0.35;
let tracks_ctrl = vm > 0.8 * ctrl;
assert!(
is_prior || tracks_ctrl,
"masked estimate {vm} is neither the prior nor its sample control \
{ctrl} — unmeasured mass leaked into the denominator"
);
saw_measured |= tracks_ctrl && ctrl > 4.0;
saw_prior |= is_prior;
if vm > 4.0 {
saw_dragged |= vn > 2.0 * prior_mean && vn < 0.8 * vm;
}
}
assert!(saw_measured, "no mixed (type-1) sample formed");
assert!(saw_prior, "no narrow-only (type-2) sample formed");
assert!(
saw_dragged,
"the naive arm never exhibited the drag — the contrast is untested"
);
}
#[test]
fn a_panel_gap_does_not_masquerade_as_a_batch_effect() {
let (masked, names) = run("delta_masked", true);
let (naive, _) = run("delta_naive", false);
let g = missing_row(D_FULL);
let narrow_b = names
.iter()
.position(|n| n.as_ref() == "narrow")
.expect("narrow batch");
let delta_at =
|out: &CollapsedOut| out.delta.as_ref().expect("delta").posterior_mean()[(g, narrow_b)];
let naive_delta = delta_at(&naive);
let masked_delta = delta_at(&masked);
assert!(
naive_delta < 0.5,
"naive delta {naive_delta} should read the panel gap as a large fake effect"
);
assert!(
(masked_delta.ln()).abs() < 0.2,
"masked delta {masked_delta} should stay at the prior (~1)"
);
}
#[test]
fn identical_panels_are_a_bitwise_no_op() {
let dir = std::env::temp_dir().join(format!("dba_panel_noop_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
let full: Vec<usize> = (0..D_FULL).collect();
let build = || {
let mut v = SparseIoVec::new()
.with_row_alignment(RowAlignment::Union)
.expect("union rows");
let types = vec![1u8; N_PER_BATCH];
v.push(one_backend(&dir, "noop_a", &full, &types), None)
.expect("push");
v.push(one_backend(&dir, "noop_b", &full, &types), None)
.expect("push");
v
};
assert!(
build().row_coverage_by_backend().is_none(),
"identical panels must probe as fully covered"
);
let mut batches = vec!["a"; N_PER_BATCH];
batches.extend(vec!["b"; N_PER_BATCH]);
let run = |observe: bool| {
let mut v = build();
let proj = v
.project_columns_with_batch_correction(3, None, Some(&batches))
.expect("proj")
.proj;
let params = MultilevelParams {
knn_pb_samples: 3,
num_levels: 1,
sort_dim: 2,
num_opt_iter: 20,
refine: data_beans::alg::refine_multilevel::RefineParams::default(),
output_calibration: legume_numeric::param::traits::CalibrateTarget::All,
anchor_batches: None,
bulk_batches: None,
observe_panels: observe,
keep_finest_stats: false,
pb_tree: None,
strata: None,
};
let mut out = collapse_columns_multilevel_with_hierarchy(&mut v, &proj, &batches, ¶ms)
.expect("collapse");
out.levels.remove(0)
};
let on = run(true);
let off = run(false);
assert_eq!(
on.mu_observed.posterior_mean(),
off.mu_observed.posterior_mean(),
"full coverage must be bitwise-identical with the flag on or off"
);
}