rust_igraph/algorithms/community/
fluid_communities.rs1#![allow(
42 clippy::cast_possible_truncation,
43 clippy::cast_possible_wrap,
44 clippy::cast_precision_loss,
45 clippy::cast_sign_loss,
46 clippy::float_cmp,
47 clippy::items_after_statements,
48 clippy::many_single_char_names,
49 clippy::needless_range_loop,
50 clippy::too_many_lines
51)]
52
53use crate::core::{Graph, IgraphError, IgraphResult};
54
55#[derive(Debug, Clone)]
57pub struct FluidOptions {
58 pub seed: u64,
62 pub max_iterations: u32,
68}
69
70pub const FLUID_DEFAULT_MAX_ITERATIONS: u32 = 1000;
73
74impl Default for FluidOptions {
75 fn default() -> Self {
76 Self {
77 seed: 0,
78 max_iterations: FLUID_DEFAULT_MAX_ITERATIONS,
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct FluidResult {
86 pub membership: Vec<u32>,
88 pub nb_clusters: u32,
93 pub n_iterations_run: u32,
96}
97
98pub fn fluid_communities(graph: &Graph, k: u32) -> IgraphResult<FluidResult> {
134 fluid_communities_with_options(graph, k, &FluidOptions::default())
135}
136
137pub fn fluid_communities_with_options(
160 graph: &Graph,
161 k: u32,
162 opts: &FluidOptions,
163) -> IgraphResult<FluidResult> {
164 let n = graph.vcount();
165
166 if n < 2 {
170 if k == 0 {
171 return Err(IgraphError::InvalidArgument(
172 "number of requested communities must be positive".to_string(),
173 ));
174 }
175 return Ok(FluidResult {
176 membership: vec![0; n as usize],
177 nb_clusters: u32::from(n == 1),
178 n_iterations_run: 0,
179 });
180 }
181
182 if k == 0 {
183 return Err(IgraphError::InvalidArgument(
184 "number of requested communities must be positive".to_string(),
185 ));
186 }
187 if k > n {
188 return Err(IgraphError::InvalidArgument(format!(
189 "number of requested communities ({k}) must not exceed the number of vertices ({n})"
190 )));
191 }
192 if opts.max_iterations == 0 {
193 return Err(IgraphError::Unsupported(
194 "FluidOptions.max_iterations must be ≥ 1",
195 ));
196 }
197
198 let simple = crate::algorithms::properties::is_simple::is_simple(graph)?;
200 if !simple {
201 return Err(IgraphError::InvalidArgument(
202 "fluid community detection requires a simple graph (no self-loops, no parallel edges)"
203 .to_string(),
204 ));
205 }
206 let components = crate::algorithms::connectivity::components::connected_components(graph)?;
207 if components.count != 1 {
208 return Err(IgraphError::InvalidArgument(
209 "fluid community detection requires a connected graph".to_string(),
210 ));
211 }
212
213 let adjacency = build_adjacency(graph)?;
217
218 let mut rng = SplitMix64::new(opts.seed);
219
220 let mut membership: Vec<u32> = vec![0; n as usize];
224 let mut com_to_numvertices: Vec<u32> = vec![0; k as usize];
225 let mut density: Vec<f64> = vec![1.0; k as usize];
226
227 let mut node_order: Vec<u32> = (0..n).collect();
230 shuffle_in_place(&mut node_order, &mut rng);
231 for i in 0..k as usize {
232 let v = node_order[i];
233 let label = u32::try_from(i)
234 .map_err(|_| IgraphError::InvalidArgument("k exceeds u32 range".into()))?
235 + 1;
236 membership[v as usize] = label;
237 com_to_numvertices[i] = 1;
238 }
239
240 let mut label_counters: Vec<f64> = vec![0.0; k as usize];
242 let mut dominant_labels: Vec<u32> = Vec::with_capacity(k as usize);
243 let mut touched: Vec<u32> = Vec::new();
244
245 const EPS: f64 = 1e-4;
246
247 let mut iter_count: u32 = 0;
248 let mut running = true;
249 while running && iter_count < opts.max_iterations {
250 running = false;
251 iter_count += 1;
252 shuffle_in_place(&mut node_order, &mut rng);
253
254 for idx in 0..n as usize {
255 let v1 = node_order[idx];
256 dominant_labels.clear();
257 for &lbl in &touched {
258 label_counters[(lbl - 1) as usize] = 0.0;
259 }
260 touched.clear();
261
262 let kv1 = membership[v1 as usize];
263 let mut max_count = 0.0_f64;
264
265 if kv1 != 0 {
268 let d = density[(kv1 - 1) as usize];
269 label_counters[(kv1 - 1) as usize] = d;
270 touched.push(kv1);
271 max_count = d;
272 dominant_labels.push(kv1);
273 }
274
275 for &nbr in &adjacency[v1 as usize] {
277 let k_label = membership[nbr as usize];
278 if k_label == 0 {
279 continue;
280 }
281 let idx_l = (k_label - 1) as usize;
282 if label_counters[idx_l] == 0.0 {
283 touched.push(k_label);
284 }
285 label_counters[idx_l] += density[idx_l];
286 let diff = label_counters[idx_l] - max_count;
287 if diff > EPS {
288 max_count = label_counters[idx_l];
289 dominant_labels.clear();
290 dominant_labels.push(k_label);
291 } else if diff > -EPS {
292 if !dominant_labels.contains(&k_label) {
296 dominant_labels.push(k_label);
297 }
298 }
299 }
300
301 if dominant_labels.is_empty() {
302 continue;
303 }
304
305 if dominant_labels.contains(&kv1) {
309 continue;
310 }
311
312 running = true;
313 let pick_idx = rng.gen_index(dominant_labels.len());
314 let new_label = dominant_labels[pick_idx];
315
316 if kv1 != 0 {
317 com_to_numvertices[(kv1 - 1) as usize] -= 1;
318 density[(kv1 - 1) as usize] =
324 1.0 / f64::from(com_to_numvertices[(kv1 - 1) as usize]);
325 }
326
327 membership[v1 as usize] = new_label;
328 com_to_numvertices[(new_label - 1) as usize] += 1;
329 density[(new_label - 1) as usize] =
330 1.0 / f64::from(com_to_numvertices[(new_label - 1) as usize]);
331 }
332 }
333
334 let mut remap: Vec<i32> = vec![-1; k as usize];
338 let mut next_dense: u32 = 0;
339 let mut dense_membership: Vec<u32> = vec![0; n as usize];
340 for (v, &lbl) in membership.iter().enumerate() {
341 debug_assert!(lbl >= 1, "fluid: vertex {v} ended unlabelled");
342 let idx_l = (lbl - 1) as usize;
343 if remap[idx_l] < 0 {
344 remap[idx_l] = next_dense as i32;
345 next_dense += 1;
346 }
347 dense_membership[v] = remap[idx_l] as u32;
348 }
349
350 Ok(FluidResult {
351 membership: dense_membership,
352 nb_clusters: next_dense,
353 n_iterations_run: iter_count,
354 })
355}
356
357fn build_adjacency(graph: &Graph) -> IgraphResult<Vec<Vec<u32>>> {
365 let n = graph.vcount() as usize;
366 let mut adj: Vec<Vec<u32>> = vec![Vec::new(); n];
367 for v in 0..n as u32 {
368 for nbr in graph.neighbors(v)? {
369 adj[v as usize].push(nbr);
370 }
371 }
372 Ok(adj)
373}
374
375struct SplitMix64(u64);
380
381impl SplitMix64 {
382 fn new(seed: u64) -> Self {
383 Self(seed.wrapping_add(0x9E37_79B9_7F4A_7C15))
384 }
385 fn next_u64(&mut self) -> u64 {
386 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
387 let mut z = self.0;
388 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
389 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
390 z ^ (z >> 31)
391 }
392 fn gen_index(&mut self, bound: usize) -> usize {
393 debug_assert!(bound > 0);
394 let r = self.next_u64() % (bound as u64);
395 usize::try_from(r).unwrap_or(0)
396 }
397}
398
399fn shuffle_in_place<T>(slice: &mut [T], rng: &mut SplitMix64) {
400 let len = slice.len();
401 for i in (1..len).rev() {
402 let j = rng.gen_index(i + 1);
403 slice.swap(i, j);
404 }
405}
406
407#[cfg(test)]
412#[allow(clippy::float_cmp)]
413mod tests {
414 use super::*;
415
416 fn k_clique(n: u32) -> Graph {
417 let mut g = Graph::with_vertices(n);
418 for u in 0..n {
419 for v in (u + 1)..n {
420 g.add_edge(u, v).expect("clique edge");
421 }
422 }
423 g
424 }
425
426 fn two_cliques_with_bridge(size: u32) -> Graph {
427 let mut g = Graph::with_vertices(size * 2);
428 for u in 0..size {
429 for v in (u + 1)..size {
430 g.add_edge(u, v).expect("clique edge");
431 }
432 }
433 for u in size..size * 2 {
434 for v in (u + 1)..size * 2 {
435 g.add_edge(u, v).expect("clique edge");
436 }
437 }
438 g.add_edge(size - 1, size).expect("bridge edge");
439 g
440 }
441
442 fn assert_partition_well_formed(r: &FluidResult, expected_n: usize) {
443 assert_eq!(r.membership.len(), expected_n);
444 if expected_n == 0 {
445 assert_eq!(r.nb_clusters, 0);
446 return;
447 }
448 let max_label = *r.membership.iter().max().unwrap_or(&0);
449 assert!((max_label as usize) < expected_n);
450 assert_eq!(max_label + 1, r.nb_clusters);
451 let mut seen = vec![false; r.nb_clusters as usize];
452 for &m in &r.membership {
453 seen[m as usize] = true;
454 }
455 assert!(seen.into_iter().all(|b| b));
456 }
457
458 #[test]
459 fn k_equals_1_is_one_big_community() {
460 let g = k_clique(6);
461 let r = fluid_communities(&g, 1).unwrap();
462 assert_eq!(r.nb_clusters, 1);
463 for &m in &r.membership {
464 assert_eq!(m, 0);
465 }
466 }
467
468 #[test]
469 fn k_equals_n_is_singletons() {
470 let g = k_clique(5);
471 let r = fluid_communities(&g, 5).unwrap();
472 assert_partition_well_formed(&r, 5);
473 assert_eq!(r.nb_clusters, 5);
476 }
477
478 #[test]
479 fn two_k4_bridge_splits_at_bridge() {
480 let g = two_cliques_with_bridge(4);
481 let r = fluid_communities(&g, 2).unwrap();
482 assert_eq!(r.nb_clusters, 2);
483 for u in 0..4 {
484 assert_eq!(r.membership[u], r.membership[0]);
485 }
486 for u in 4..8 {
487 assert_eq!(r.membership[u], r.membership[4]);
488 }
489 assert_ne!(r.membership[0], r.membership[4]);
490 }
491
492 #[test]
493 fn determinism_under_seed() {
494 let g = two_cliques_with_bridge(5);
495 let opts = FluidOptions {
496 seed: 12345,
497 ..FluidOptions::default()
498 };
499 let a = fluid_communities_with_options(&g, 3, &opts).unwrap();
500 let b = fluid_communities_with_options(&g, 3, &opts).unwrap();
501 assert_eq!(a.membership, b.membership);
502 assert_eq!(a.nb_clusters, b.nb_clusters);
503 }
504
505 #[test]
506 fn different_seeds_can_differ() {
507 let g = two_cliques_with_bridge(5);
508 let a = fluid_communities_with_options(
509 &g,
510 3,
511 &FluidOptions {
512 seed: 1,
513 max_iterations: FLUID_DEFAULT_MAX_ITERATIONS,
514 },
515 )
516 .unwrap();
517 let b = fluid_communities_with_options(
518 &g,
519 3,
520 &FluidOptions {
521 seed: 99,
522 max_iterations: FLUID_DEFAULT_MAX_ITERATIONS,
523 },
524 )
525 .unwrap();
526 assert_partition_well_formed(&a, g.vcount() as usize);
528 assert_partition_well_formed(&b, g.vcount() as usize);
529 }
530
531 #[test]
532 fn empty_graph_returns_empty() {
533 let g = Graph::with_vertices(0);
534 let r = fluid_communities(&g, 1).unwrap();
535 assert_eq!(r.membership.len(), 0);
536 assert_eq!(r.nb_clusters, 0);
537 }
538
539 #[test]
540 fn single_vertex_returns_one_singleton() {
541 let g = Graph::with_vertices(1);
542 let r = fluid_communities(&g, 1).unwrap();
543 assert_eq!(r.membership.len(), 1);
544 assert_eq!(r.nb_clusters, 1);
545 }
546
547 #[test]
548 fn rejects_k_zero() {
549 let g = k_clique(4);
550 assert!(fluid_communities(&g, 0).is_err());
551 }
552
553 #[test]
554 fn rejects_k_greater_than_vcount() {
555 let g = k_clique(4);
556 assert!(fluid_communities(&g, 5).is_err());
557 }
558
559 #[test]
560 fn rejects_disconnected_graph() {
561 let mut g = Graph::with_vertices(4);
562 g.add_edge(0, 1).unwrap();
563 g.add_edge(2, 3).unwrap();
564 assert!(fluid_communities(&g, 2).is_err());
565 }
566
567 #[test]
568 fn rejects_non_simple_graph() {
569 let mut g = Graph::with_vertices(3);
570 g.add_edge(0, 1).unwrap();
571 g.add_edge(1, 2).unwrap();
572 g.add_edge(0, 1).unwrap(); assert!(fluid_communities(&g, 2).is_err());
574 }
575
576 #[test]
577 fn rejects_max_iterations_zero() {
578 let g = k_clique(4);
579 let opts = FluidOptions {
580 seed: 0,
581 max_iterations: 0,
582 };
583 assert!(fluid_communities_with_options(&g, 2, &opts).is_err());
584 }
585
586 #[test]
587 fn ring_of_4_k5_cliques_finds_4_groups() {
588 let mut g = Graph::with_vertices(20);
590 for c in 0..4 {
591 let base = c * 5;
592 for u in 0..5 {
593 for v in (u + 1)..5 {
594 g.add_edge(base + u, base + v).unwrap();
595 }
596 }
597 let next_base = ((c + 1) % 4) * 5;
598 g.add_edge(base, next_base).unwrap();
599 }
600 let r = fluid_communities_with_options(
601 &g,
602 4,
603 &FluidOptions {
604 seed: 7,
605 max_iterations: FLUID_DEFAULT_MAX_ITERATIONS,
606 },
607 )
608 .unwrap();
609 assert_eq!(r.nb_clusters, 4);
610 for c in 0..4 {
611 let base = (c * 5) as usize;
612 let label = r.membership[base];
613 for offset in 1..5 {
614 assert_eq!(r.membership[base + offset], label);
615 }
616 }
617 }
618
619 #[test]
620 fn converges_in_reasonable_iterations() {
621 let g = two_cliques_with_bridge(6);
622 let r = fluid_communities_with_options(
623 &g,
624 2,
625 &FluidOptions {
626 seed: 0,
627 max_iterations: 100,
628 },
629 )
630 .unwrap();
631 assert!(r.n_iterations_run <= 100);
633 assert!(r.n_iterations_run >= 1);
634 }
635}