rust_igraph/algorithms/community/
split_join_distance.rs1#![allow(
25 clippy::cast_precision_loss,
26 clippy::cast_possible_truncation,
27 clippy::cast_sign_loss
28)]
29
30use crate::core::error::{IgraphError, IgraphResult};
31
32use super::compare_communities::split_join_distances;
33use super::reindex_membership::reindex_membership;
34
35#[derive(Debug, Copy, Clone, Eq, PartialEq)]
41pub struct SplitJoinDistance {
42 pub d12: u64,
44 pub d21: u64,
46}
47
48impl SplitJoinDistance {
49 #[must_use]
51 pub fn total(self) -> u64 {
52 self.d12 + self.d21
53 }
54}
55
56pub fn split_join_distance(comm1: &[u32], comm2: &[u32]) -> IgraphResult<SplitJoinDistance> {
83 if comm1.len() != comm2.len() {
84 return Err(IgraphError::InvalidArgument(format!(
85 "community membership vectors have different lengths: {} and {}",
86 comm1.len(),
87 comm2.len(),
88 )));
89 }
90 let n = comm1.len();
91 if n == 0 {
92 return Ok(SplitJoinDistance { d12: 0, d21: 0 });
93 }
94 let c1 = reindex_membership(comm1)?;
95 let c2 = reindex_membership(comm2)?;
96 let (d12, d21) = split_join_distances(&c1.membership, &c2.membership, n);
97 Ok(SplitJoinDistance { d12, d21 })
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn empty_inputs_yield_zero_zero() {
106 let r = split_join_distance(&[], &[]).unwrap();
107 assert_eq!(r.d12, 0);
108 assert_eq!(r.d21, 0);
109 assert_eq!(r.total(), 0);
110 }
111
112 #[test]
113 fn length_mismatch_errors() {
114 let err = split_join_distance(&[0, 0], &[0, 0, 0]).unwrap_err();
115 match err {
116 IgraphError::InvalidArgument(_) => (),
117 other => panic!("expected InvalidArgument, got {other:?}"),
118 }
119 }
120
121 #[test]
122 fn identical_partition_is_zero() {
123 let r = split_join_distance(&[0, 0, 1, 1, 2, 2], &[3, 3, 5, 5, 7, 7]).unwrap();
124 assert_eq!(r.d12, 0);
125 assert_eq!(r.d21, 0);
126 }
127
128 #[test]
129 fn subpartition_asymmetry() {
130 let r = split_join_distance(&[0, 0, 1, 2], &[0, 0, 0, 1]).unwrap();
136 assert_eq!(r.d12, 0);
137 assert_eq!(r.d21, 1);
138 assert_eq!(r.total(), 1);
139 }
140
141 #[test]
142 fn full_disagreement_2x2() {
143 let r = split_join_distance(&[0, 0, 1, 1], &[0, 1, 0, 1]).unwrap();
148 assert_eq!(r.d12, 2);
149 assert_eq!(r.d21, 2);
150 assert_eq!(r.total(), 4);
151 }
152
153 #[test]
154 fn matches_compare_communities_split_join_sum() {
155 use crate::{CommunityComparison, compare_communities};
156 let a = [0u32, 0, 1, 2, 2, 3, 3, 3];
157 let b = [0u32, 1, 1, 2, 3, 3, 0, 2];
158 let r = split_join_distance(&a, &b).unwrap();
159 let total = compare_communities(&a, &b, CommunityComparison::SplitJoin).unwrap();
160 assert!((total - (r.total() as f64)).abs() < 1e-12);
161 }
162
163 #[test]
164 fn relabel_invariant() {
165 let a = [0u32, 0, 1, 1, 2, 2];
167 let b = [0u32, 1, 0, 1, 0, 1];
168 let r1 = split_join_distance(&a, &b).unwrap();
169 let a2: Vec<u32> = a
171 .iter()
172 .map(|&c| match c {
173 0 => 17,
174 1 => 4,
175 _ => 999,
176 })
177 .collect();
178 let b2: Vec<u32> = b
179 .iter()
180 .map(|&c| if c == 0 { 1_000_000 } else { 5 })
181 .collect();
182 let r2 = split_join_distance(&a2, &b2).unwrap();
183 assert_eq!(r1, r2);
184 }
185
186 #[test]
187 fn single_singleton_block() {
188 let v: Vec<u32> = (0..5).collect();
191 let r = split_join_distance(&v, &v).unwrap();
192 assert_eq!(r.d12, 0);
193 assert_eq!(r.d21, 0);
194 }
195}
196
197#[cfg(all(test, feature = "proptest-harness"))]
198mod proptests {
199 use super::*;
200 use proptest::collection::vec;
201 use proptest::prelude::*;
202
203 proptest! {
204 #[test]
205 fn non_negative_components(
206 (a, b) in (1usize..40).prop_flat_map(|n| (vec(0u32..5, n), vec(0u32..5, n)))
207 ) {
208 let r = split_join_distance(&a, &b).unwrap();
209 let n = a.len() as u64;
211 prop_assert!(r.d12 <= n);
212 prop_assert!(r.d21 <= n);
213 }
214
215 #[test]
216 fn total_equals_compare_communities_split_join(
217 (a, b) in (1usize..40).prop_flat_map(|n| (vec(0u32..5, n), vec(0u32..5, n)))
218 ) {
219 use crate::{CommunityComparison, compare_communities};
220 let r = split_join_distance(&a, &b).unwrap();
221 let total = compare_communities(&a, &b, CommunityComparison::SplitJoin).unwrap();
222 prop_assert!((total - (r.total() as f64)).abs() < 1e-12);
223 }
224
225 #[test]
226 fn relabel_invariant_prop(
227 (a, b) in (2usize..30).prop_flat_map(|n| (vec(0u32..6, n), vec(0u32..6, n))),
228 offset in 1u32..1000,
229 ) {
230 let r1 = split_join_distance(&a, &b).unwrap();
231 let a2: Vec<u32> = a.iter().map(|&c| c.wrapping_add(offset).wrapping_mul(31)).collect();
232 let b2: Vec<u32> = b.iter().map(|&c| c.wrapping_add(offset.wrapping_mul(7))).collect();
233 let r2 = split_join_distance(&a2, &b2).unwrap();
234 prop_assert_eq!(r1, r2);
235 }
236
237 #[test]
238 fn identical_partitions_are_zero(
239 a in vec(0u32..7, 1..40),
240 ) {
241 let r = split_join_distance(&a, &a).unwrap();
242 prop_assert_eq!(r.d12, 0);
243 prop_assert_eq!(r.d21, 0);
244 }
245
246 #[test]
247 fn subpartition_one_side_is_zero(
248 a in vec(0u32..4, 2..25),
249 ) {
250 let b: Vec<u32> = a.iter().map(|&c| c / 2).collect();
255 let r = split_join_distance(&a, &b).unwrap();
256 prop_assert_eq!(r.d12, 0);
257 }
258 }
259}