use crate::core::error::{IgraphError, IgraphResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommunityToMembershipResult {
pub membership: Vec<u32>,
pub csize: Vec<u32>,
}
pub fn community_to_membership(
merges: &[[u32; 2]],
nodes: u32,
steps: u32,
) -> IgraphResult<CommunityToMembershipResult> {
let n = nodes as usize;
let rows = merges.len();
let steps_usize = steps as usize;
if steps_usize > rows {
return Err(IgraphError::InvalidArgument(format!(
"Number of steps is greater than number of rows in merges matrix: \
found {steps} steps, {rows} rows."
)));
}
let mut membership = vec![0u32; n];
let total_nodes = n
.checked_add(steps_usize)
.ok_or_else(|| IgraphError::InvalidArgument("dendrogram size overflows usize".into()))?;
let mut already_merged = vec![false; total_nodes];
let mut tmp = vec![0u32; steps_usize];
let mut found: u32 = 0;
for i in (0..steps_usize).rev() {
let [c1, c2] = merges[i];
let limit = u32::try_from(n + i).map_err(|_| {
IgraphError::InvalidArgument(format!(
"Merges matrix row {i} dendrogram node id overflows u32."
))
})?;
if c1 >= limit {
return Err(IgraphError::InvalidArgument(format!(
"Merges matrix row {i}: child {c1} exceeds the maximum valid \
dendrogram node id ({} = nodes + i).",
limit - 1
)));
}
if c2 >= limit {
return Err(IgraphError::InvalidArgument(format!(
"Merges matrix row {i}: child {c2} exceeds the maximum valid \
dendrogram node id ({} = nodes + i).",
limit - 1
)));
}
let c1_idx = c1 as usize;
let c2_idx = c2 as usize;
if already_merged[c1_idx] {
return Err(IgraphError::InvalidArgument(format!(
"Merges matrix contains multiple merges of cluster {c1}."
)));
}
already_merged[c1_idx] = true;
if already_merged[c2_idx] {
return Err(IgraphError::InvalidArgument(format!(
"Merges matrix contains multiple merges of cluster {c2}."
)));
}
already_merged[c2_idx] = true;
if tmp[i] == 0 {
found += 1;
tmp[i] = found;
}
let cid_one_based = tmp[i];
for &child in &[c1, c2] {
if (child as usize) < n {
membership[child as usize] = cid_one_based;
} else {
tmp[child as usize - n] = cid_one_based;
}
}
}
let total_clusters_estimate = (n - usize::min(n, 2 * steps_usize)) + found as usize;
let mut csize: Vec<u32> = vec![0; found as usize];
csize.reserve(total_clusters_estimate);
for slot in &mut membership {
if *slot == 0 {
*slot = found;
csize.push(1);
found += 1;
} else {
let cid = *slot - 1;
*slot = cid;
csize[cid as usize] += 1;
}
}
Ok(CommunityToMembershipResult { membership, csize })
}
pub fn le_community_to_membership(
merges: &[[u32; 2]],
steps: u32,
membership: &[u32],
) -> IgraphResult<CommunityToMembershipResult> {
let no_of_nodes = membership.len();
if no_of_nodes == 0 {
return Err(IgraphError::InvalidArgument(
"le_community_to_membership: membership vector must not be empty.".into(),
));
}
let components = membership
.iter()
.copied()
.max()
.map_or(0u32, |m| m.saturating_add(1));
if components as usize > no_of_nodes {
return Err(IgraphError::InvalidArgument(format!(
"Invalid membership vector: number of components ({components}) must not be \
greater than the number of nodes ({no_of_nodes})."
)));
}
if steps >= components {
return Err(IgraphError::InvalidArgument(format!(
"Number of steps ({steps}) must be smaller than number of components ({components})."
)));
}
let mut seen = vec![false; components as usize];
for &c in membership {
seen[c as usize] = true;
}
if let Some(empty) = seen.iter().position(|&s| !s) {
return Err(IgraphError::InvalidArgument(format!(
"Invalid membership vector, empty cluster found at id {empty}."
)));
}
let comp = community_to_membership(merges, components, steps)?;
let new_cluster_count = comp.csize.len();
let mut new_membership = vec![0u32; no_of_nodes];
let mut csize = vec![0u32; new_cluster_count];
for (i, &old_cluster) in membership.iter().enumerate() {
let new_cluster = comp.membership[old_cluster as usize];
new_membership[i] = new_cluster;
csize[new_cluster as usize] += 1;
}
Ok(CommunityToMembershipResult {
membership: new_membership,
csize,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_steps_yields_singletons() {
let r = community_to_membership(&[], 4, 0).unwrap();
assert_eq!(r.membership, vec![0, 1, 2, 3]);
assert_eq!(r.csize, vec![1, 1, 1, 1]);
}
#[test]
fn full_collapse_to_one_cluster() {
let merges = vec![[0u32, 1], [2, 3], [4, 5]];
let r = community_to_membership(&merges, 4, 3).unwrap();
assert_eq!(r.membership, vec![0, 0, 0, 0]);
assert_eq!(r.csize, vec![4]);
}
#[test]
fn intermediate_cut_two_clusters() {
let merges = vec![[0u32, 1], [2, 3], [4, 5]];
let r = community_to_membership(&merges, 4, 2).unwrap();
assert_eq!(r.membership, vec![1, 1, 0, 0]);
assert_eq!(r.csize, vec![2, 2]);
}
#[test]
fn one_merge_with_untouched_leaves() {
let r = community_to_membership(&[[0u32, 2]], 5, 1).unwrap();
assert_eq!(r.membership, vec![0, 1, 0, 2, 3]);
assert_eq!(r.csize, vec![2, 1, 1, 1]);
}
#[test]
fn chained_merges_three_levels() {
let merges = vec![[0u32, 1], [4, 2], [5, 3]];
let r = community_to_membership(&merges, 4, 3).unwrap();
assert_eq!(r.membership, vec![0, 0, 0, 0]);
assert_eq!(r.csize, vec![4]);
}
#[test]
fn empty_graph_zero_steps() {
let r = community_to_membership(&[], 0, 0).unwrap();
assert!(r.membership.is_empty());
assert!(r.csize.is_empty());
}
#[test]
fn err_steps_exceeds_rows() {
let err = community_to_membership(&[[0u32, 1]], 4, 2).unwrap_err();
match err {
IgraphError::InvalidArgument(m) => assert!(m.contains("greater than")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn err_double_merge_of_same_cluster() {
let merges = vec![[0u32, 1], [0, 2]];
let err = community_to_membership(&merges, 4, 2).unwrap_err();
match err {
IgraphError::InvalidArgument(m) => assert!(m.contains("multiple merges")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn err_merge_entry_references_future_node() {
let merges = vec![[0u32, 4]];
let err = community_to_membership(&merges, 4, 1).unwrap_err();
match err {
IgraphError::InvalidArgument(m) => assert!(m.contains("exceeds")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn partial_dendrogram_with_fewer_steps_than_rows() {
let merges = vec![[0u32, 1], [2, 3], [4, 5]];
let r = community_to_membership(&merges, 4, 1).unwrap();
assert_eq!(r.membership, vec![0, 0, 1, 2]);
assert_eq!(r.csize, vec![2, 1, 1]);
}
#[test]
fn matches_walktrap_dendrogram_on_two_k4_bridge() {
use crate::Graph;
use crate::walktrap;
let mut g = Graph::with_vertices(8);
for u in 0u32..4 {
for v in (u + 1)..4 {
g.add_edge(u, v).unwrap();
}
}
for u in 4u32..8 {
for v in (u + 1)..8 {
g.add_edge(u, v).unwrap();
}
}
g.add_edge(3, 4).unwrap();
let r = walktrap(&g).unwrap();
assert_eq!(r.nb_clusters, 2);
let step = 8u32 - r.nb_clusters;
let cut = community_to_membership(&r.merges, 8, step).unwrap();
assert_eq!(cut.membership.len(), 8);
for u in 0u32..3 {
assert_eq!(
cut.membership[u as usize],
cut.membership[(u + 1) as usize],
"vertices {} and {} should share a cluster",
u,
u + 1
);
}
for u in 4u32..7 {
assert_eq!(cut.membership[u as usize], cut.membership[(u + 1) as usize]);
}
assert_ne!(cut.membership[0], cut.membership[7]);
assert_eq!(cut.csize.iter().sum::<u32>(), 8);
assert_eq!(cut.csize.len(), 2);
}
#[test]
fn le_zero_steps_keeps_initial_clusters() {
let membership = [0u32, 0, 1, 1, 2, 2];
let r = le_community_to_membership(&[], 0, &membership).unwrap();
assert_eq!(r.membership, vec![0, 0, 1, 1, 2, 2]);
assert_eq!(r.csize, vec![2, 2, 2]);
}
#[test]
fn le_one_merge_fuses_two_clusters() {
let membership = [0u32, 0, 1, 1, 2, 2];
let r = le_community_to_membership(&[[0, 1]], 1, &membership).unwrap();
assert_eq!(r.membership, vec![0, 0, 0, 0, 1, 1]);
assert_eq!(r.csize, vec![4, 2]);
}
#[test]
fn le_full_collapse() {
let membership = [0u32, 0, 1, 2, 2, 2];
let merges = [[0u32, 1], [3, 2]];
let r = le_community_to_membership(&merges, 2, &membership).unwrap();
assert_eq!(r.membership, vec![0, 0, 0, 0, 0, 0]);
assert_eq!(r.csize, vec![6]);
}
#[test]
fn le_csize_sums_to_node_count() {
let membership = [0u32, 1, 2, 3, 0, 1, 2, 3];
let r = le_community_to_membership(&[[0, 1]], 1, &membership).unwrap();
assert_eq!(r.csize.iter().sum::<u32>(), 8);
assert_eq!(r.csize.len(), 3); for &c in &r.membership {
assert!((c as usize) < r.csize.len());
}
}
#[test]
fn le_err_empty_membership() {
let err = le_community_to_membership(&[], 0, &[]).unwrap_err();
match err {
IgraphError::InvalidArgument(m) => assert!(m.contains("must not be empty")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn le_err_steps_not_smaller_than_components() {
let membership = [0u32, 0, 1, 1];
let err = le_community_to_membership(&[[0, 1]], 2, &membership).unwrap_err();
match err {
IgraphError::InvalidArgument(m) => assert!(m.contains("must be smaller")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn le_err_empty_cluster() {
let membership = [0u32, 0, 2, 2];
let err = le_community_to_membership(&[[0, 1]], 1, &membership).unwrap_err();
match err {
IgraphError::InvalidArgument(m) => assert!(m.contains("empty cluster")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn le_matches_singleton_case() {
let membership = [0u32, 1, 2, 3];
let merges = [[0u32, 1], [2, 3]];
let le = le_community_to_membership(&merges, 2, &membership).unwrap();
let plain = community_to_membership(&merges, 4, 2).unwrap();
assert_eq!(le.membership, plain.membership);
assert_eq!(le.csize, plain.csize);
}
#[cfg(all(test, feature = "proptest-harness"))]
mod prop {
use super::*;
use proptest::prelude::*;
prop_compose! {
fn arb_dendrogram()(n in 2u32..=10u32, seed in any::<u64>())
-> (u32, Vec<[u32; 2]>)
{
let mut rng: u64 = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut live: Vec<u32> = (0..n).collect();
let mut merges: Vec<[u32; 2]> = Vec::with_capacity((n as usize).saturating_sub(1));
let mut next_id: u32 = n;
while live.len() >= 2 {
rng = rng.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(1);
let i = (rng >> 32) as usize % live.len();
rng = rng.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(1);
let mut j = (rng >> 32) as usize % live.len();
if j == i { j = (j + 1) % live.len(); }
let (i, j) = if i < j { (i, j) } else { (j, i) };
let c2 = live.remove(j);
let c1 = live.remove(i);
merges.push([c1, c2]);
live.push(next_id);
next_id += 1;
}
(n, merges)
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 80, ..ProptestConfig::default() })]
#[test]
fn zero_steps_singletons((n, merges) in arb_dendrogram()) {
let r = community_to_membership(&merges, n, 0).unwrap();
let expected: Vec<u32> = (0..n).collect();
prop_assert_eq!(r.membership, expected);
prop_assert_eq!(r.csize, vec![1u32; n as usize]);
}
#[test]
fn full_collapse((n, merges) in arb_dendrogram()) {
let steps = u32::try_from(merges.len()).unwrap();
let r = community_to_membership(&merges, n, steps).unwrap();
let expected = vec![0u32; n as usize];
prop_assert_eq!(r.membership, expected);
prop_assert_eq!(r.csize, vec![n]);
}
#[test]
fn cut_invariants((n, merges) in arb_dendrogram(), step_frac in 0u32..=100) {
let rows = u32::try_from(merges.len()).unwrap();
let steps = (rows * step_frac) / 100;
let r = community_to_membership(&merges, n, steps).unwrap();
let expected_k = n - steps;
prop_assert_eq!(r.csize.len() as u32, expected_k);
prop_assert_eq!(r.csize.iter().sum::<u32>(), n);
for &c in &r.membership {
prop_assert!(c < expected_k);
}
for c in 0..expected_k {
let count = r.membership.iter().filter(|&&x| x == c).count() as u32;
prop_assert_eq!(count, r.csize[c as usize]);
}
}
}
}
}