use rayon::prelude::*;
use crate::core::math::sparse::{undirected_edges_to_csr, validate_square_csr};
use crate::prelude::*;
const PARALLEL_PARTITION_LIMIT: usize = 512;
const PARALLEL_EDGES_PER_SLOT: usize = 1;
const MAX_PARTITIONS: usize = 4096;
pub struct PartitionConnectivity<T>
where
T: Clone,
{
pub connectivities: CompressedSparseData2<T>,
pub sizes: Vec<usize>,
}
fn use_parallel_counting(k: usize, nnz: usize, n_threads: usize) -> bool {
if k > PARALLEL_PARTITION_LIMIT {
return false;
}
let slots = k.saturating_mul(k).saturating_mul(n_threads.max(1));
nnz >= slots.saturating_mul(PARALLEL_EDGES_PER_SLOT)
}
fn count_partition_edges<U>(
graph: &CompressedSparseData2<U>,
partitions: &[usize],
k: usize,
parallel: bool,
) -> Vec<u64>
where
U: Clone + Sync,
{
let n = partitions.len();
if !parallel {
let mut counts = vec![0u64; k * k];
for i in 0..n {
let row = partitions[i] * k;
for idx in graph.indptr[i] as usize..graph.indptr[i + 1] as usize {
counts[row + partitions[graph.indices[idx] as usize]] += 1;
}
}
return counts;
}
let chunk_len = n.div_ceil(rayon::current_num_threads().max(1)).max(1);
(0..n)
.into_par_iter()
.with_min_len(chunk_len)
.fold(
|| vec![0u64; k * k],
|mut counts, i| {
let row = partitions[i] * k;
for idx in graph.indptr[i] as usize..graph.indptr[i + 1] as usize {
counts[row + partitions[graph.indices[idx] as usize]] += 1;
}
counts
},
)
.reduce(
|| vec![0u64; k * k],
|mut a, b| {
for (x, y) in a.iter_mut().zip(b) {
*x += y;
}
a
},
)
}
fn connectivity_edges<T>(
counts: &[u64],
out_edges: &[f64],
sizes: &[usize],
n: usize,
k: usize,
) -> Vec<(u32, u32, T)>
where
T: BixverseFloat,
{
if n < 2 {
return Vec::new();
}
let denominator = (n - 1) as f64;
let mut edges = Vec::new();
for a in 0..k {
for b in (a + 1)..k {
let observed = counts[a * k + b] + counts[b * k + a];
if observed == 0 {
continue;
}
let expected =
(out_edges[a] * sizes[b] as f64 + out_edges[b] * sizes[a] as f64) / denominator;
let connectivity = if expected != 0.0 {
(observed as f64 / expected).min(1.0)
} else {
1.0
};
edges.push((a as u32, b as u32, T::from_f64(connectivity).unwrap()));
}
}
edges
}
pub fn partition_connectivities<T, U>(
graph: &CompressedSparseData2<U>,
partitions: &[usize],
n_partitions: Option<usize>,
) -> Result<PartitionConnectivity<T>, BixverseErrors>
where
T: BixverseFloat + BixverseNumeric,
U: Clone + Sync,
{
let n = validate_square_csr(graph)?;
if partitions.len() != n {
return Err(BixverseErrors::CommunityAssignmentMismatch);
}
let k = match n_partitions {
Some(k) => k,
None => match partitions.iter().copied().max() {
Some(m) => m
.checked_add(1)
.ok_or(BixverseErrors::PartitionLabelOutOfRange {
label: m,
n_partitions: MAX_PARTITIONS,
})?,
None => 0,
},
};
if k > MAX_PARTITIONS {
return Err(BixverseErrors::TooManyPartitions {
n_partitions: k,
max: MAX_PARTITIONS,
});
}
if let Some(&label) = partitions.iter().find(|&&p| p >= k) {
return Err(BixverseErrors::PartitionLabelOutOfRange {
label,
n_partitions: k,
});
}
let mut sizes = vec![0usize; k];
for &p in partitions {
sizes[p] += 1;
}
if k == 0 {
return Ok(PartitionConnectivity {
connectivities: undirected_edges_to_csr::<T>(0, &[]),
sizes,
});
}
let counts = count_partition_edges(
graph,
partitions,
k,
use_parallel_counting(k, graph.indices.len(), rayon::current_num_threads()),
);
let out_edges: Vec<f64> = (0..k)
.map(|a| counts[a * k..(a + 1) * k].iter().sum::<u64>() as f64)
.collect();
let edges = connectivity_edges::<T>(&counts, &out_edges, &sizes, n, k);
Ok(PartitionConnectivity {
connectivities: undirected_edges_to_csr(k, &edges),
sizes,
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn directed_csr(n: usize, edges: &[(usize, usize)]) -> CompressedSparseData2<u8> {
let mut rows: Vec<Vec<u32>> = vec![Vec::new(); n];
for &(a, b) in edges {
rows[a].push(b as u32);
}
let mut indptr: Vec<u32> = vec![0];
let mut indices: Vec<u32> = Vec::new();
let mut data: Vec<u8> = Vec::new();
for row in rows.iter_mut() {
row.sort_unstable();
for &j in row.iter() {
indices.push(j);
data.push(1);
}
indptr.push(indices.len() as u32);
}
CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, n))
}
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 ring(members: &[usize]) -> Vec<(usize, usize)> {
(0..members.len())
.map(|i| (members[i], members[(i + 1) % members.len()]))
.collect()
}
#[test]
fn test_two_blocks_bridged_by_one_edge() {
let mut edges = ring(&[0, 1, 2, 3]);
edges.extend(ring(&[4, 5, 6, 7]));
edges.push((3, 4));
let graph = directed_csr(8, &edges);
let partitions = vec![0, 0, 0, 0, 1, 1, 1, 1];
let res: PartitionConnectivity<f64> =
partition_connectivities(&graph, &partitions, None).unwrap();
assert_eq!(res.sizes, vec![4, 4]);
let dense = densify(&res.connectivities);
let want = 1.0 / (36.0 / 7.0);
assert_relative_eq!(dense[0][1], want, epsilon = 1e-12);
assert_relative_eq!(dense[1][0], want, epsilon = 1e-12);
assert_eq!(dense[0][0], 0.0);
assert_eq!(dense[1][1], 0.0);
assert!(want < 1.0);
}
#[test]
fn test_chain_of_three_partitions_leaves_the_ends_unconnected() {
let mut edges = ring(&[0, 1]);
edges.extend(ring(&[2, 3]));
edges.extend(ring(&[4, 5]));
edges.push((1, 2));
edges.push((3, 4));
let graph = directed_csr(6, &edges);
let partitions = vec![0, 0, 1, 1, 2, 2];
let res: PartitionConnectivity<f64> =
partition_connectivities(&graph, &partitions, None).unwrap();
let dense = densify(&res.connectivities);
assert!(dense[0][1] > 0.0);
assert!(dense[1][2] > 0.0);
assert_eq!(dense[0][2], 0.0);
assert_eq!(dense[2][0], 0.0);
}
#[test]
fn test_connectivity_of_exactly_one_is_left_alone() {
let graph = directed_csr(2, &[(0, 1), (1, 0)]);
let partitions = vec![0, 1];
let res: PartitionConnectivity<f64> =
partition_connectivities(&graph, &partitions, None).unwrap();
assert_relative_eq!(densify(&res.connectivities)[0][1], 1.0, epsilon = 1e-12);
}
#[test]
fn test_connectivity_is_capped_at_one() {
let cross = directed_csr(
4,
&[
(0, 2),
(0, 3),
(1, 2),
(1, 3),
(2, 0),
(2, 1),
(3, 0),
(3, 1),
],
);
let res: PartitionConnectivity<f64> =
partition_connectivities(&cross, &[0, 0, 1, 1], None).unwrap();
assert_relative_eq!(densify(&res.connectivities)[0][1], 1.0, epsilon = 1e-12);
}
#[test]
fn test_self_loops_only_move_the_expected_value() {
let plain = directed_csr(2, &[(0, 1), (1, 0)]);
let looped = directed_csr(2, &[(0, 0), (0, 1), (1, 0)]);
let a: PartitionConnectivity<f64> =
partition_connectivities(&plain, &[0, 1], None).unwrap();
let b: PartitionConnectivity<f64> =
partition_connectivities(&looped, &[0, 1], None).unwrap();
let (da, db) = (densify(&a.connectivities), densify(&b.connectivities));
assert!(db[0][1] < da[0][1]);
assert_eq!(db[0][0], 0.0);
assert_eq!(db[1][1], 0.0);
}
#[test]
fn test_single_partition_has_no_connectivities() {
let graph = directed_csr(4, &ring(&[0, 1, 2, 3]));
let res: PartitionConnectivity<f64> =
partition_connectivities(&graph, &[0, 0, 0, 0], None).unwrap();
assert_eq!(res.sizes, vec![4]);
assert_eq!(res.connectivities.shape, (1, 1));
assert_eq!(res.connectivities.get_nnz(), 0);
}
#[test]
fn test_declared_empty_partition_is_kept() {
let graph = directed_csr(2, &[(0, 1), (1, 0)]);
let res: PartitionConnectivity<f64> =
partition_connectivities(&graph, &[0, 1], Some(4)).unwrap();
assert_eq!(res.sizes, vec![1, 1, 0, 0]);
assert_eq!(res.connectivities.shape, (4, 4));
assert_eq!(res.connectivities.get_nnz(), 2);
}
#[test]
fn test_both_counting_arms_agree() {
let mut edges = ring(&[0, 1, 2, 3]);
edges.extend(ring(&[4, 5, 6, 7]));
edges.push((3, 4));
edges.push((7, 0));
edges.push((0, 0));
let graph = directed_csr(8, &edges);
let partitions = vec![0, 0, 0, 1, 1, 2, 2, 2];
let sequential = count_partition_edges(&graph, &partitions, 3, false);
let parallel = count_partition_edges(&graph, &partitions, 3, true);
assert_eq!(sequential, parallel);
assert_eq!(sequential.iter().sum::<u64>(), graph.indices.len() as u64);
}
#[test]
fn test_parallel_counting_needs_edges_as_well_as_few_partitions() {
assert!(!use_parallel_counting(500, 750_000, 64));
assert!(!use_parallel_counting(MAX_PARTITIONS, 10_000_000, 8));
assert!(use_parallel_counting(30, 30_000_000, 16));
assert!(!use_parallel_counting(
PARALLEL_PARTITION_LIMIT + 1,
usize::MAX,
1
));
}
#[test]
fn test_rejects_a_non_monotonic_indptr() {
let data = vec![1u8; 7];
let indices = vec![1u32, 2, 0, 2, 0, 0, 1];
let indptr = vec![0u32, 5, 2, 7];
let graph = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (3, 3));
assert!(matches!(
partition_connectivities::<f64, _>(&graph, &[0, 0, 1], None),
Err(BixverseErrors::MalformedCsr(_))
));
}
#[test]
fn test_rejects_an_out_of_range_column_index() {
let data = vec![1u8, 1];
let indices = vec![1u32, 9];
let indptr = vec![0u32, 1, 2, 2];
let graph = CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (3, 3));
assert!(matches!(
partition_connectivities::<f64, _>(&graph, &[0, 0, 1], None),
Err(BixverseErrors::MalformedCsr(_))
));
}
#[test]
fn test_rejects_label_count_mismatch() {
let graph = directed_csr(3, &[(0, 1)]);
let res = partition_connectivities::<f64, _>(&graph, &[0, 1], None);
assert!(matches!(
res,
Err(BixverseErrors::CommunityAssignmentMismatch)
));
}
#[test]
fn test_rejects_out_of_range_label() {
let graph = directed_csr(2, &[(0, 1)]);
let res = partition_connectivities::<f64, _>(&graph, &[0, 5], Some(2));
assert!(matches!(
res,
Err(BixverseErrors::PartitionLabelOutOfRange {
label: 5,
n_partitions: 2
})
));
}
#[test]
fn test_rejects_too_many_partitions() {
let graph = directed_csr(2, &[(0, 1)]);
let res = partition_connectivities::<f64, _>(&graph, &[0, 1], Some(MAX_PARTITIONS + 1));
assert!(matches!(res, Err(BixverseErrors::TooManyPartitions { .. })));
}
#[test]
fn test_rejects_csc_input() {
let graph = directed_csr(2, &[(0, 1)]).transform();
let res = partition_connectivities::<f64, _>(&graph, &[0, 1], None);
assert!(matches!(res, Err(BixverseErrors::SparseMatrixMustBeCsr)));
}
}