#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use crate::core::error::{IgraphError, IgraphResult};
use super::compare_communities::split_join_distances;
use super::reindex_membership::reindex_membership;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct SplitJoinDistance {
pub d12: u64,
pub d21: u64,
}
impl SplitJoinDistance {
#[must_use]
pub fn total(self) -> u64 {
self.d12 + self.d21
}
}
pub fn split_join_distance(comm1: &[u32], comm2: &[u32]) -> IgraphResult<SplitJoinDistance> {
if comm1.len() != comm2.len() {
return Err(IgraphError::InvalidArgument(format!(
"community membership vectors have different lengths: {} and {}",
comm1.len(),
comm2.len(),
)));
}
let n = comm1.len();
if n == 0 {
return Ok(SplitJoinDistance { d12: 0, d21: 0 });
}
let c1 = reindex_membership(comm1)?;
let c2 = reindex_membership(comm2)?;
let (d12, d21) = split_join_distances(&c1.membership, &c2.membership, n);
Ok(SplitJoinDistance { d12, d21 })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_inputs_yield_zero_zero() {
let r = split_join_distance(&[], &[]).unwrap();
assert_eq!(r.d12, 0);
assert_eq!(r.d21, 0);
assert_eq!(r.total(), 0);
}
#[test]
fn length_mismatch_errors() {
let err = split_join_distance(&[0, 0], &[0, 0, 0]).unwrap_err();
match err {
IgraphError::InvalidArgument(_) => (),
other => panic!("expected InvalidArgument, got {other:?}"),
}
}
#[test]
fn identical_partition_is_zero() {
let r = split_join_distance(&[0, 0, 1, 1, 2, 2], &[3, 3, 5, 5, 7, 7]).unwrap();
assert_eq!(r.d12, 0);
assert_eq!(r.d21, 0);
}
#[test]
fn subpartition_asymmetry() {
let r = split_join_distance(&[0, 0, 1, 2], &[0, 0, 0, 1]).unwrap();
assert_eq!(r.d12, 0);
assert_eq!(r.d21, 1);
assert_eq!(r.total(), 1);
}
#[test]
fn full_disagreement_2x2() {
let r = split_join_distance(&[0, 0, 1, 1], &[0, 1, 0, 1]).unwrap();
assert_eq!(r.d12, 2);
assert_eq!(r.d21, 2);
assert_eq!(r.total(), 4);
}
#[test]
fn matches_compare_communities_split_join_sum() {
use crate::{CommunityComparison, compare_communities};
let a = [0u32, 0, 1, 2, 2, 3, 3, 3];
let b = [0u32, 1, 1, 2, 3, 3, 0, 2];
let r = split_join_distance(&a, &b).unwrap();
let total = compare_communities(&a, &b, CommunityComparison::SplitJoin).unwrap();
assert!((total - (r.total() as f64)).abs() < 1e-12);
}
#[test]
fn relabel_invariant() {
let a = [0u32, 0, 1, 1, 2, 2];
let b = [0u32, 1, 0, 1, 0, 1];
let r1 = split_join_distance(&a, &b).unwrap();
let a2: Vec<u32> = a
.iter()
.map(|&c| match c {
0 => 17,
1 => 4,
_ => 999,
})
.collect();
let b2: Vec<u32> = b
.iter()
.map(|&c| if c == 0 { 1_000_000 } else { 5 })
.collect();
let r2 = split_join_distance(&a2, &b2).unwrap();
assert_eq!(r1, r2);
}
#[test]
fn single_singleton_block() {
let v: Vec<u32> = (0..5).collect();
let r = split_join_distance(&v, &v).unwrap();
assert_eq!(r.d12, 0);
assert_eq!(r.d21, 0);
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use proptest::collection::vec;
use proptest::prelude::*;
proptest! {
#[test]
fn non_negative_components(
(a, b) in (1usize..40).prop_flat_map(|n| (vec(0u32..5, n), vec(0u32..5, n)))
) {
let r = split_join_distance(&a, &b).unwrap();
let n = a.len() as u64;
prop_assert!(r.d12 <= n);
prop_assert!(r.d21 <= n);
}
#[test]
fn total_equals_compare_communities_split_join(
(a, b) in (1usize..40).prop_flat_map(|n| (vec(0u32..5, n), vec(0u32..5, n)))
) {
use crate::{CommunityComparison, compare_communities};
let r = split_join_distance(&a, &b).unwrap();
let total = compare_communities(&a, &b, CommunityComparison::SplitJoin).unwrap();
prop_assert!((total - (r.total() as f64)).abs() < 1e-12);
}
#[test]
fn relabel_invariant_prop(
(a, b) in (2usize..30).prop_flat_map(|n| (vec(0u32..6, n), vec(0u32..6, n))),
offset in 1u32..1000,
) {
let r1 = split_join_distance(&a, &b).unwrap();
let a2: Vec<u32> = a.iter().map(|&c| c.wrapping_add(offset).wrapping_mul(31)).collect();
let b2: Vec<u32> = b.iter().map(|&c| c.wrapping_add(offset.wrapping_mul(7))).collect();
let r2 = split_join_distance(&a2, &b2).unwrap();
prop_assert_eq!(r1, r2);
}
#[test]
fn identical_partitions_are_zero(
a in vec(0u32..7, 1..40),
) {
let r = split_join_distance(&a, &a).unwrap();
prop_assert_eq!(r.d12, 0);
prop_assert_eq!(r.d21, 0);
}
#[test]
fn subpartition_one_side_is_zero(
a in vec(0u32..4, 2..25),
) {
let b: Vec<u32> = a.iter().map(|&c| c / 2).collect();
let r = split_join_distance(&a, &b).unwrap();
prop_assert_eq!(r.d12, 0);
}
}
}