use super::{
build_pb_samples, knn_distinct_pbsamples_in_batch, per_batch_sc_neighbors, MatchablePbSamples,
PbSampleLayout,
};
use crate::sparse_io_vector::SparseIoVec;
use legume_numeric::matrix::knn_match::ColumnDict;
use nalgebra::DMatrix;
use std::collections::HashSet;
#[test]
fn adaptive_recovers_knn_distinct_pbsamples() {
let knn = 10usize;
let mut feats: Vec<f32> = Vec::new();
let mut cell_to_pbsamp: Vec<usize> = Vec::new();
for pb in 0..3 {
for _ in 0..20 {
feats.push(pb as f32);
cell_to_pbsamp.push(pb);
}
}
for pb in 3..15 {
for _ in 0..3 {
feats.push(pb as f32);
cell_to_pbsamp.push(pb);
}
}
let n = feats.len();
let names: Vec<usize> = (0..n).collect(); let mat = DMatrix::<f32>::from_row_slice(1, n, &feats);
let bknn = ColumnDict::<usize>::from_dmatrix(mat, names);
let query = vec![0.0f32];
let hits = knn_distinct_pbsamples_in_batch(
&bknn,
&query,
knn,
15,
&cell_to_pbsamp,
usize::MAX - 1,
None,
)
.unwrap();
let distinct: HashSet<usize> = hits.iter().map(|&(p, _)| p).collect();
assert_eq!(
distinct.len(),
knn,
"should recover exactly knn distinct pb-samples, got {distinct:?}"
);
assert!(
distinct.iter().any(|&p| p >= 3),
"must reach pb-samples beyond the dense near cluster"
);
assert!(hits.windows(2).all(|w| w[0].1 <= w[1].1));
}
#[test]
fn adaptive_returns_all_when_fewer_than_knn() {
let knn = 10usize;
let mut feats: Vec<f32> = Vec::new();
let mut cell_to_pbsamp: Vec<usize> = Vec::new();
for pb in 0..3 {
for _ in 0..5 {
feats.push(pb as f32);
cell_to_pbsamp.push(pb);
}
}
let n = feats.len();
let names: Vec<usize> = (0..n).collect();
let mat = DMatrix::<f32>::from_row_slice(1, n, &feats);
let bknn = ColumnDict::<usize>::from_dmatrix(mat, names);
let query = vec![0.0f32];
for available in [3, knn] {
let hits = knn_distinct_pbsamples_in_batch(
&bknn,
&query,
knn,
available,
&cell_to_pbsamp,
usize::MAX - 1,
None,
)
.unwrap();
let ids: Vec<usize> = hits.iter().map(|&(p, _)| p).collect();
assert_eq!(ids, vec![0, 1, 2]);
let dists: Vec<f32> = hits.iter().map(|&(_, d)| d).collect();
assert_eq!(dists, vec![0.0, 1.0, 2.0]);
}
}
fn rare_stratum_batch() -> (ColumnDict<usize>, Vec<usize>, Vec<usize>) {
let mut feats: Vec<f32> = Vec::new();
let mut cell_to_pbsamp: Vec<usize> = Vec::new();
for pb in 0..22 {
for i in 0..3 {
feats.push(pb as f32 + 0.1 * i as f32);
cell_to_pbsamp.push(pb);
}
}
let strata: Vec<usize> = (0..22).map(|pb| usize::from(pb >= 20)).collect();
let n = feats.len();
let mat = DMatrix::<f32>::from_row_slice(1, n, &feats);
let bknn = ColumnDict::<usize>::from_dmatrix(mat, (0..n).collect());
(bknn, cell_to_pbsamp, strata)
}
#[test]
fn rare_stratum_returns_its_few_pbsamples() {
let (bknn, cell_to_pbsamp, strata) = rare_stratum_batch();
let query = vec![0.0f32];
let search = |available: usize, own_stratum: usize| {
knn_distinct_pbsamples_in_batch(
&bknn,
&query,
10,
available,
&cell_to_pbsamp,
usize::MAX - 1,
Some((&strata, own_stratum)),
)
.unwrap()
};
assert_eq!(search(2, 1), vec![(20, 20.0), (21, 21.0)]);
assert_eq!(search(10, 1), vec![(20, 20.0), (21, 21.0)]);
assert!(search(0, 2).is_empty());
}
#[test]
fn match_table_respects_the_stratum_census() {
let (bknn_b, mut cell_to_pbsamp, mut strata) = rare_stratum_batch();
let n_b = cell_to_pbsamp.len();
cell_to_pbsamp.extend([22, 23]);
strata.extend([1, 5]);
let bknn_a = ColumnDict::<usize>::from_dmatrix(
DMatrix::<f32>::from_row_slice(1, 2, &[0.0, 0.5]),
vec![n_b, n_b + 1],
);
let num_pb = 24;
let mut centroids = DMatrix::<f32>::zeros(1, num_pb);
for pb in 0..22 {
centroids[(0, pb)] = pb as f32 + 0.1;
}
centroids[(0, 23)] = 0.5;
let mut pb_sample_to_batch = vec![1usize; 22];
pb_sample_to_batch.extend([0, 0]);
let layout = PbSampleLayout {
centroids,
cell_counts: vec![1.0; num_pb],
pb_sample_to_batch,
pb_sample_to_group: (0..num_pb).collect(),
bg_to_pbsamp: Default::default(),
cell_to_pbsamp,
singleton_col: vec![None; num_pb],
bulk_batches: Vec::new(),
pb_sample_to_stratum: Some(strata),
};
let lookups = [bknn_a, bknn_b];
let census = MatchablePbSamples::new(&layout, &lookups);
assert_eq!(census.count(0, None), 2);
assert_eq!(census.count(1, None), 22);
assert_eq!(census.count(1, Some(0)), 20);
assert_eq!(census.count(1, Some(1)), 2);
assert_eq!(census.count(1, Some(5)), 0);
assert_eq!(census.count(0, Some(5)), 1);
let table = per_batch_sc_neighbors(&layout, &lookups, 10, None).unwrap();
assert_eq!(table[22], vec![(20, 20.0), (21, 21.0)]);
assert!(table[23].is_empty());
assert!(table[0].is_empty());
assert_eq!(table[20].iter().map(|m| m.0).collect::<Vec<_>>(), vec![22]);
}
#[test]
fn gene_sums_are_the_weighted_column_sums_of_each_pbsample() {
use crate::sparse_io::{create_sparse_from_triplets, SparseIoBackend};
const D: usize = 7;
const N: usize = 30;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("sums.zarr");
let mut triplets: Vec<(u64, u64, f32)> = Vec::new();
for j in 0..N {
for g in 0..D {
let v = (g * 5 + j * 3) % 4;
if v > 0 {
triplets.push((g as u64, j as u64, v as f32));
}
}
}
let mut backend = create_sparse_from_triplets(
&triplets,
(D, N, triplets.len()),
Some(path.to_str().expect("utf8")),
Some(&SparseIoBackend::Zarr),
)
.expect("backend");
let genes: Vec<Box<str>> = (0..D).map(|g| format!("g{g}").into()).collect();
let cols: Vec<Box<str>> = (0..N).map(|c| format!("c{c}").into()).collect();
backend.register_row_names_vec(&genes);
backend.register_column_names_vec(&cols);
let mut data = SparseIoVec::new();
data.push(std::sync::Arc::from(backend), None)
.expect("push");
let batches: Vec<usize> = (0..N).map(|c| c % 3).collect();
data.register_batch_membership(&batches);
let multiplicity: Vec<f32> = (0..N).map(|c| 1.0 + (c % 2) as f32).collect();
data.register_column_multiplicity(&multiplicity).unwrap();
let groups: Vec<String> = (0..N).map(|c| format!("{}", (c * 7) % 4)).collect();
data.assign_groups(&groups, None);
let proj = DMatrix::<f32>::from_fn(2, N, |d, c| ((c + d) % 5) as f32);
let batch_of = data.get_batch_membership(0..N);
let pbs = build_pb_samples(&data, &proj, D, &[batch_of[0]], &[], None).unwrap();
let num_pb = pbs.layout.cell_counts.len();
assert!(pbs.layout.singleton_col.iter().any(Option::is_some));
assert!(pbs.layout.singleton_col.iter().any(Option::is_none));
let mut want = vec![vec![0f32; D]; num_pb];
let mut present = vec![vec![false; D]; num_pb];
for (c, (&p, &w)) in pbs
.layout
.cell_to_pbsamp
.iter()
.zip(&multiplicity)
.enumerate()
{
let y = data.read_columns_csc(std::iter::once(c)).unwrap();
let col = y.col(0);
for (&g, &v) in col.row_indices().iter().zip(col.values()) {
want[p][g] += v * w;
present[p][g] = true;
}
}
assert_eq!(pbs.gene_sums.len(), num_pb);
for p in 0..num_pb {
let expected: Vec<(usize, f32)> = (0..D)
.filter(|&g| present[p][g])
.map(|g| (g, want[p][g]))
.collect();
assert!(!expected.is_empty());
assert_eq!(pbs.gene_sums[p], expected, "pb-sample {p}");
}
}