use crate::graph::graph_abstraction::partition_connectivities;
use crate::graph::spanning_tree::maximum_spanning_forest;
use crate::prelude::*;
const MAX_OUT_OF_RANGE_SHARE: f64 = 0.5;
pub struct PagaResult<T>
where
T: Clone,
{
pub connectivities: CompressedSparseData2<T>,
pub connectivities_tree: CompressedSparseData2<T>,
pub sizes: Vec<usize>,
}
fn directed_knn_csr(
knn_indices: &[Vec<usize>],
) -> Result<CompressedSparseData2<u8>, BixverseErrors> {
let n = knn_indices.len();
let total: usize = knn_indices.iter().map(|r| r.len()).sum();
let mut indptr: Vec<u32> = Vec::with_capacity(n + 1);
let mut indices: Vec<u32> = Vec::with_capacity(total);
indptr.push(0);
let mut out_of_range = 0usize;
let mut row: Vec<u32> = Vec::new();
for (i, neighbours) in knn_indices.iter().enumerate() {
row.clear();
for &j in neighbours.iter() {
if j >= n {
out_of_range += 1;
continue;
}
if j != i {
row.push(j as u32);
}
}
row.sort_unstable();
row.dedup();
indices.extend_from_slice(&row);
indptr.push(indices.len() as u32);
}
if total > 0 && (out_of_range as f64) > MAX_OUT_OF_RANGE_SHARE * total as f64 {
return Err(BixverseErrors::PagaKnnIndicesOutOfRange {
out_of_range,
total,
n_cells: n,
});
}
let data = vec![1u8; indices.len()];
Ok(CompressedSparseData2::from_parts(
data,
indices,
indptr,
None,
CompressedSparseFormat::Csr,
(n, n),
))
}
pub fn run_paga<T>(
knn_indices: &[Vec<usize>],
partitions: &[usize],
n_partitions: Option<usize>,
) -> Result<PagaResult<T>, BixverseErrors>
where
T: BixverseFloat + BixverseNumeric,
{
let graph = directed_knn_csr(knn_indices)?;
let abstracted = partition_connectivities::<T, u8>(&graph, partitions, n_partitions)?;
let connectivities_tree = maximum_spanning_forest(&abstracted.connectivities)?;
Ok(PagaResult {
connectivities: abstracted.connectivities,
connectivities_tree,
sizes: abstracted.sizes,
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn densify(csr: &CompressedSparseData2<f64>) -> Vec<Vec<f64>> {
let k = csr.nrows();
let mut out = vec![vec![0.0; k]; k];
for (i, row) in out.iter_mut().enumerate() {
for idx in csr.indptr[i] as usize..csr.indptr[i + 1] as usize {
row[csr.indices[idx] as usize] = csr.data[idx];
}
}
out
}
fn y_shape() -> Vec<Vec<usize>> {
let blocks = [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]];
let mut knn: Vec<Vec<usize>> = vec![Vec::new(); 12];
for block in blocks.iter() {
for &c in block.iter() {
knn[c] = block.iter().copied().filter(|&o| o != c).collect();
}
}
for (a, b) in [(3, 4), (2, 5), (0, 8), (1, 9)] {
knn[a].push(b);
knn[b].push(a);
}
knn[7].push(8);
knn
}
#[test]
fn test_y_shape_connects_the_trunk_to_both_arms_most_strongly() {
let knn = y_shape();
let partitions = vec![0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2];
let res: PagaResult<f64> = run_paga(&knn, &partitions, None).unwrap();
assert_eq!(res.sizes, vec![4, 4, 4]);
let dense = densify(&res.connectivities);
assert!(dense[0][1] > 0.0);
assert!(dense[0][2] > 0.0);
assert!(dense[1][2] > 0.0);
assert!(dense[1][2] < dense[0][1]);
assert!(dense[1][2] < dense[0][2]);
for a in 0..3 {
assert_eq!(dense[a][a], 0.0);
for b in 0..3 {
assert_relative_eq!(dense[a][b], dense[b][a], epsilon = 1e-12);
}
}
}
#[test]
fn test_tree_keeps_both_arms_of_the_y() {
let knn = y_shape();
let partitions = vec![0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2];
let res: PagaResult<f64> = run_paga(&knn, &partitions, None).unwrap();
assert_eq!(res.connectivities_tree.get_nnz(), 4);
let tree = densify(&res.connectivities_tree);
assert!(tree[0][1] > 0.0);
assert!(tree[0][2] > 0.0);
assert_eq!(tree[1][2], 0.0);
}
#[test]
fn test_tree_drops_the_weakest_edge_of_a_triangle() {
let blocks = [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]];
let mut knn: Vec<Vec<usize>> = vec![Vec::new(); 12];
for block in blocks.iter() {
for &c in block.iter() {
knn[c] = block.iter().copied().filter(|&o| o != c).collect();
}
}
for (a, b) in [(3, 4), (2, 5), (0, 8), (1, 9), (7, 8)] {
knn[a].push(b);
knn[b].push(a);
}
let partitions = vec![0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2];
let res: PagaResult<f64> = run_paga(&knn, &partitions, None).unwrap();
let dense = densify(&res.connectivities);
assert_relative_eq!(dense[0][1], 4.0 / (124.0 / 11.0), epsilon = 1e-12);
assert_relative_eq!(dense[0][2], 4.0 / (124.0 / 11.0), epsilon = 1e-12);
assert_relative_eq!(dense[1][2], 2.0 / (120.0 / 11.0), epsilon = 1e-12);
assert!(dense[1][2] < dense[0][1]);
let tree = densify(&res.connectivities_tree);
assert_eq!(res.connectivities_tree.get_nnz(), 4);
assert_relative_eq!(tree[0][1], dense[0][1], epsilon = 1e-12);
assert_relative_eq!(tree[0][2], dense[0][2], epsilon = 1e-12);
assert_eq!(tree[1][2], 0.0);
}
#[test]
fn test_self_hits_and_out_of_range_neighbours_are_dropped() {
let dirty = vec![vec![0, 1, 99], vec![0], vec![1]];
let clean = vec![vec![1], vec![0], vec![1]];
let a: PagaResult<f64> = run_paga(&dirty, &[0, 1, 1], None).unwrap();
let b: PagaResult<f64> = run_paga(&clean, &[0, 1, 1], None).unwrap();
assert_eq!(a.connectivities.data, b.connectivities.data);
assert_eq!(a.connectivities.indices, b.connectivities.indices);
assert_eq!(a.sizes, b.sizes);
}
#[test]
fn test_rejects_a_knn_built_against_another_index() {
let knn = vec![vec![50, 51], vec![52, 53], vec![54, 55]];
assert!(matches!(
run_paga::<f64>(&knn, &[0, 1, 1], None),
Err(BixverseErrors::PagaKnnIndicesOutOfRange {
out_of_range: 6,
total: 6,
n_cells: 3
})
));
}
#[test]
fn test_duplicate_neighbours_are_counted_once() {
let duped = vec![vec![1, 1, 1], vec![0]];
let clean = vec![vec![1], vec![0]];
let a: PagaResult<f64> = run_paga(&duped, &[0, 1], None).unwrap();
let b: PagaResult<f64> = run_paga(&clean, &[0, 1], None).unwrap();
assert_eq!(a.connectivities.data, b.connectivities.data);
}
fn scanpy_reference_knn() -> Vec<Vec<usize>> {
vec![
vec![1, 8, 5, 2],
vec![0, 15, 9, 3],
vec![5, 8, 0, 9],
vec![11, 9, 14, 16],
vec![7, 8, 5, 0],
vec![2, 8, 0, 7],
vec![7, 5, 8, 2],
vec![4, 8, 5, 6],
vec![5, 2, 0, 7],
vec![3, 1, 2, 0],
vec![16, 17, 3, 11],
vec![14, 16, 15, 3],
vec![17, 16, 14, 11],
vec![15, 14, 11, 1],
vec![11, 16, 17, 15],
vec![11, 14, 13, 1],
vec![17, 14, 11, 10],
vec![16, 14, 12, 11],
vec![22, 20, 12, 19],
vec![23, 21, 22, 20],
vec![18, 12, 19, 22],
vec![19, 23, 22, 18],
vec![18, 21, 19, 20],
vec![19, 21, 22, 20],
]
}
#[test]
fn test_matches_the_scanpy_reference() {
let knn = scanpy_reference_knn();
let partitions: Vec<usize> = (0..24)
.map(|i| {
if i < 10 {
0
} else if i < 18 {
1
} else {
2
}
})
.collect();
let res: PagaResult<f64> = run_paga(&knn, &partitions, None).unwrap();
assert_eq!(res.sizes, vec![10, 8, 6]);
let dense = densify(&res.connectivities);
assert_relative_eq!(dense[0][1], 0.287_500_000_000_000_03, epsilon = 1e-15);
assert_relative_eq!(dense[1][2], 0.119_791_666_666_666_67, epsilon = 1e-15);
assert_eq!(dense[0][2], 0.0);
let tree = densify(&res.connectivities_tree);
assert_eq!(res.connectivities_tree.get_nnz(), 4);
assert_relative_eq!(tree[0][1], dense[0][1], epsilon = 1e-15);
assert_relative_eq!(tree[1][2], dense[1][2], epsilon = 1e-15);
}
#[test]
fn test_rejects_label_count_mismatch() {
let knn = vec![vec![1], vec![0]];
let res = run_paga::<f64>(&knn, &[0], None);
assert!(matches!(
res,
Err(BixverseErrors::CommunityAssignmentMismatch)
));
}
#[test]
fn test_rejects_out_of_range_label() {
let knn = vec![vec![1], vec![0]];
let res = run_paga::<f64>(&knn, &[0, 7], Some(2));
assert!(matches!(
res,
Err(BixverseErrors::PartitionLabelOutOfRange { .. })
));
}
}