1#![allow(
47 clippy::cast_possible_truncation,
48 clippy::cast_possible_wrap,
49 clippy::cast_precision_loss,
50 clippy::cast_sign_loss,
51 clippy::float_cmp,
52 clippy::many_single_char_names,
53 clippy::needless_range_loop,
54 clippy::ptr_arg,
55 clippy::too_many_arguments,
56 clippy::too_many_lines,
57 clippy::unnecessary_wraps
58)]
59
60use std::collections::VecDeque;
61
62use crate::core::graph::EdgeId;
63use crate::core::{Graph, IgraphError, IgraphResult};
64
65#[derive(Debug, Copy, Clone, Eq, PartialEq)]
68pub enum LpaVariant {
69 Fast,
72 Dominance,
74 Retention,
77}
78
79#[derive(Debug, Clone)]
81pub struct LpaOptions {
82 pub variant: LpaVariant,
84 pub initial: Option<Vec<i32>>,
89 pub fixed: Option<Vec<bool>>,
95 pub seed: u64,
98}
99
100impl Default for LpaOptions {
101 fn default() -> Self {
102 Self {
103 variant: LpaVariant::Fast,
104 initial: None,
105 fixed: None,
106 seed: 0,
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct LpaResult {
114 pub membership: Vec<u32>,
116 pub nb_clusters: u32,
118}
119
120pub fn label_propagation(graph: &Graph) -> IgraphResult<LpaResult> {
157 label_propagation_with_options(graph, None, &LpaOptions::default())
158}
159
160pub fn label_propagation_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<LpaResult> {
183 label_propagation_with_options(graph, Some(weights), &LpaOptions::default())
184}
185
186pub fn label_propagation_with_options(
213 graph: &Graph,
214 weights: Option<&[f64]>,
215 opts: &LpaOptions,
216) -> IgraphResult<LpaResult> {
217 if graph.is_directed() {
218 return Err(IgraphError::Unsupported(
219 "label_propagation currently supports undirected graphs only",
220 ));
221 }
222 validate_weights(graph, weights)?;
223
224 let n = graph.vcount() as usize;
225 if n == 0 {
226 return Ok(LpaResult {
227 membership: Vec::new(),
228 nb_clusters: 0,
229 });
230 }
231
232 let mut membership: Vec<i32> = if let Some(init) = opts.initial.as_deref() {
236 if init.len() != n {
237 return Err(IgraphError::InvalidArgument(format!(
238 "initial labels length ({}) differs from vcount ({n})",
239 init.len()
240 )));
241 }
242 let mut max_label: i32 = -1;
243 for &k in init {
244 if k > max_label {
245 max_label = k;
246 }
247 }
248 if max_label >= 0 && i64::from(max_label) > (n as i64 - 1) {
249 return Err(IgraphError::InvalidArgument(format!(
250 "initial labels must be in [0, vcount - 1] = [0, {}]; saw {max_label}",
251 n - 1
252 )));
253 }
254 init.iter().map(|&k| if k < 0 { -1 } else { k }).collect()
255 } else {
256 (0..n).map(|i| i as i32).collect()
257 };
258
259 let fixed: Option<Vec<bool>> = match opts.fixed.as_deref() {
263 Some(f) => {
264 if f.len() != n {
265 return Err(IgraphError::InvalidArgument(format!(
266 "fixed mask length ({}) differs from vcount ({n})",
267 f.len()
268 )));
269 }
270 if opts.initial.is_none() {
271 None
274 } else {
275 let mut out = f.to_vec();
276 for v in 0..n {
277 if out[v] && membership[v] < 0 {
278 out[v] = false;
279 }
280 }
281 Some(out)
282 }
283 }
284 None => None,
285 };
286
287 let edge_weights: Vec<f64> = match weights {
290 Some(w) => w.to_vec(),
291 None => vec![1.0; graph.ecount()],
292 };
293 let adj = build_adjacency(graph, &edge_weights)?;
294
295 let mut rng = SplitMix64::new(opts.seed);
296
297 match opts.variant {
298 LpaVariant::Fast => {
299 lpa_fast(&adj, &mut membership, fixed.as_deref(), &mut rng);
300 }
301 LpaVariant::Dominance => {
302 lpa_dominance_or_retention(
303 &adj,
304 &mut membership,
305 fixed.as_deref(),
306 false,
307 &mut rng,
308 );
309 }
310 LpaVariant::Retention => {
311 lpa_dominance_or_retention(
312 &adj,
313 &mut membership,
314 fixed.as_deref(),
315 true,
316 &mut rng,
317 );
318 }
319 }
320
321 let nb_clusters = finalize_labels(graph, &mut membership, fixed.as_deref(), &mut rng)?;
324
325 let membership: Vec<u32> = membership.iter().map(|&k| k as u32).collect();
327 Ok(LpaResult {
328 membership,
329 nb_clusters,
330 })
331}
332
333fn build_adjacency(graph: &Graph, edge_weights: &[f64]) -> IgraphResult<Vec<Vec<(u32, f64)>>> {
341 let n = graph.vcount() as usize;
342 let m = graph.ecount();
343 let mut adj: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
344 for e in 0..m {
345 let e_id = u32::try_from(e)
346 .map_err(|_| IgraphError::InvalidArgument("edge count exceeds u32::MAX".into()))?;
347 let (u, v) = graph.edge(e_id as EdgeId)?;
348 let w = edge_weights[e];
349 adj[u as usize].push((v, w));
350 if u != v {
351 adj[v as usize].push((u, w));
352 }
353 }
354 Ok(adj)
355}
356
357fn lpa_fast(
362 adj: &[Vec<(u32, f64)>],
363 membership: &mut [i32],
364 fixed: Option<&[bool]>,
365 rng: &mut SplitMix64,
366) {
367 let n = adj.len();
368 if n == 0 {
369 return;
370 }
371
372 let mut label_weight = vec![0.0_f64; n];
375 let mut touched: Vec<i32> = Vec::with_capacity(8);
376 let mut dominant: Vec<i32> = Vec::with_capacity(2);
377
378 let mut order: Vec<u32> = match fixed {
380 Some(mask) => (0..n as u32).filter(|&v| !mask[v as usize]).collect(),
381 None => (0..n as u32).collect(),
382 };
383 shuffle_in_place(&mut order, rng);
384
385 let mut queue: VecDeque<u32> = order.iter().copied().collect();
386 let mut in_queue = vec![false; n];
387 for &v in &order {
388 in_queue[v as usize] = true;
389 }
390
391 while let Some(v1) = queue.pop_front() {
392 in_queue[v1 as usize] = false;
393
394 let neighbours = &adj[v1 as usize];
396 let mut max_count: f64 = 0.0;
397 dominant.clear();
398 touched.clear();
399 for &(v2, w) in neighbours {
400 let k = membership[v2 as usize];
401 if k < 0 {
402 continue;
403 }
404 let was_zero = label_weight[k as usize] == 0.0;
405 label_weight[k as usize] += w;
406 if was_zero {
407 touched.push(k);
408 }
409 let lw = label_weight[k as usize];
410 if max_count < lw {
411 max_count = lw;
412 dominant.clear();
413 dominant.push(k);
414 } else if (max_count - lw).abs() < f64::EPSILON || max_count == lw {
415 dominant.push(k);
416 }
417 }
418
419 if !dominant.is_empty() {
420 let current = membership[v1 as usize];
421 let pick = rng.gen_index(dominant.len());
422 let new_label = dominant[pick];
423
424 if new_label != current {
425 for &(v2, _) in neighbours {
429 let v2u = v2 as usize;
430 if in_queue[v2u] {
431 continue;
432 }
433 let neigh_label = membership[v2u];
434 let is_fixed = fixed.is_some_and(|m| m[v2u]);
435 if neigh_label != new_label && !is_fixed {
436 queue.push_back(v2);
437 in_queue[v2u] = true;
438 }
439 }
440 }
441 membership[v1 as usize] = new_label;
442 }
443
444 for &k in &touched {
446 label_weight[k as usize] = 0.0;
447 }
448 }
449}
450
451fn lpa_dominance_or_retention(
456 adj: &[Vec<(u32, f64)>],
457 membership: &mut [i32],
458 fixed: Option<&[bool]>,
459 retention: bool,
460 rng: &mut SplitMix64,
461) {
462 let n = adj.len();
463 if n == 0 {
464 return;
465 }
466
467 let mut label_weight = vec![0.0_f64; n];
468 let mut touched: Vec<i32> = Vec::with_capacity(8);
469 let mut dominant: Vec<i32> = Vec::with_capacity(2);
470
471 let mut order: Vec<u32> = match fixed {
472 Some(mask) => (0..n as u32).filter(|&v| !mask[v as usize]).collect(),
473 None => (0..n as u32).collect(),
474 };
475
476 let mut running = true;
477 let mut control_iteration = true;
478 while running {
479 if retention {
480 running = false;
481 shuffle_in_place(&mut order, rng);
482 } else if control_iteration {
483 running = false;
484 } else {
485 shuffle_in_place(&mut order, rng);
486 }
487
488 for &v1 in &order {
489 let neighbours = &adj[v1 as usize];
491 let mut max_count: f64 = 0.0;
492 dominant.clear();
493 touched.clear();
494 for &(v2, w) in neighbours {
495 let k = membership[v2 as usize];
496 if k < 0 {
497 continue;
498 }
499 let was_zero = label_weight[k as usize] == 0.0;
500 label_weight[k as usize] += w;
501 if was_zero {
502 touched.push(k);
503 }
504 let lw = label_weight[k as usize];
505 if max_count < lw {
506 max_count = lw;
507 dominant.clear();
508 dominant.push(k);
509 } else if max_count == lw {
510 dominant.push(k);
511 }
512 }
513
514 if !dominant.is_empty() {
515 if retention {
516 let current = membership[v1 as usize];
517 let keep_current = current >= 0
518 && (current as usize) < n
519 && label_weight[current as usize] >= max_count;
520 if !keep_current {
521 let pick = rng.gen_index(dominant.len());
522 let new_label = dominant[pick];
523 if new_label != current {
524 running = true;
525 }
526 membership[v1 as usize] = new_label;
527 }
528 } else if control_iteration {
529 let current = membership[v1 as usize];
530 let still_dominant = current >= 0
531 && (current as usize) < n
532 && label_weight[current as usize] >= max_count;
533 if !still_dominant {
534 running = true;
535 }
536 } else {
537 let pick = rng.gen_index(dominant.len());
538 membership[v1 as usize] = dominant[pick];
539 }
540 }
541
542 for &k in &touched {
543 label_weight[k as usize] = 0.0;
544 }
545 }
546
547 if !retention {
548 control_iteration = !control_iteration;
549 }
550 }
551}
552
553fn finalize_labels(
561 graph: &Graph,
562 membership: &mut [i32],
563 fixed: Option<&[bool]>,
564 rng: &mut SplitMix64,
565) -> IgraphResult<u32> {
566 let n = membership.len();
567
568 let mut max_seen: i32 = -1;
571 for &k in membership.iter() {
572 if k > max_seen {
573 max_seen = k;
574 }
575 }
576 let span = max_seen.max(-1) + 1;
577 let mut relabel: Vec<i32> = vec![-1; span.max(0) as usize];
578
579 let mut next_label: i32 = 0;
580 let mut unlabelled_left = false;
581 for v in 0..n {
582 let k = membership[v];
583 if k >= 0 {
584 let slot = k as usize;
585 if relabel[slot] == -1 {
586 relabel[slot] = next_label;
587 membership[v] = next_label;
588 next_label += 1;
589 } else {
590 membership[v] = relabel[slot];
591 }
592 } else {
593 unlabelled_left = true;
594 }
595 }
596
597 if unlabelled_left {
598 let mut order: Vec<u32> = match fixed {
602 Some(mask) => (0..n as u32).filter(|&v| !mask[v as usize]).collect(),
603 None => (0..n as u32).collect(),
604 };
605 shuffle_in_place(&mut order, rng);
606
607 let mut bfs_queue: VecDeque<u32> = VecDeque::new();
608 for &seed in &order {
609 if membership[seed as usize] >= 0 {
610 continue;
611 }
612 let label = next_label;
613 next_label += 1;
614 membership[seed as usize] = label;
615 bfs_queue.push_back(seed);
616 while let Some(v) = bfs_queue.pop_front() {
617 let neighbours = graph.neighbors(v)?;
618 for n_v in neighbours {
619 if membership[n_v as usize] < 0 {
620 membership[n_v as usize] = label;
621 bfs_queue.push_back(n_v);
622 }
623 }
624 }
625 }
626 }
627
628 Ok(next_label as u32)
629}
630
631fn validate_weights(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<()> {
636 let Some(w) = weights else {
637 return Ok(());
638 };
639 let m = graph.ecount();
640 if w.len() != m {
641 return Err(IgraphError::InvalidArgument(format!(
642 "weights length ({}) differs from edge count ({m})",
643 w.len()
644 )));
645 }
646 for (e, &v) in w.iter().enumerate() {
647 if !v.is_finite() {
648 return Err(IgraphError::InvalidArgument(format!(
649 "weight at edge {e} is not finite ({v})"
650 )));
651 }
652 if v < 0.0 {
653 return Err(IgraphError::InvalidArgument(format!(
654 "weight at edge {e} is negative ({v}); label_propagation requires non-negative weights"
655 )));
656 }
657 }
658 Ok(())
659}
660
661struct SplitMix64(u64);
666
667impl SplitMix64 {
668 fn new(seed: u64) -> Self {
669 Self(seed.wrapping_add(0x9E37_79B9_7F4A_7C15))
670 }
671 fn next_u64(&mut self) -> u64 {
672 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
673 let mut z = self.0;
674 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
675 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
676 z ^ (z >> 31)
677 }
678 fn gen_index(&mut self, bound: usize) -> usize {
679 debug_assert!(bound > 0);
680 let r = self.next_u64() % (bound as u64);
681 usize::try_from(r).unwrap_or(0)
682 }
683}
684
685fn shuffle_in_place<T>(slice: &mut [T], rng: &mut SplitMix64) {
686 let len = slice.len();
687 for i in (1..len).rev() {
688 let j = rng.gen_index(i + 1);
689 slice.swap(i, j);
690 }
691}
692
693#[cfg(test)]
698#[allow(clippy::float_cmp)]
699mod tests {
700 use super::*;
701
702 fn add_edges(g: &mut Graph, edges: &[(u32, u32)]) {
703 for &(u, v) in edges {
704 g.add_edge(u, v).unwrap();
705 }
706 }
707
708 fn assert_dense_labels(r: &LpaResult, n: usize) {
709 assert_eq!(r.membership.len(), n);
710 if n == 0 {
711 assert_eq!(r.nb_clusters, 0);
712 return;
713 }
714 let max = *r.membership.iter().max().unwrap_or(&0);
715 assert!((max as usize) < n);
716 assert_eq!(max + 1, r.nb_clusters);
717 let mut seen = vec![false; r.nb_clusters as usize];
718 for &m in &r.membership {
719 seen[m as usize] = true;
720 }
721 assert!(seen.into_iter().all(|b| b));
722 }
723
724 #[test]
725 fn empty_graph_returns_empty_membership() {
726 let g = Graph::with_vertices(0);
727 let r = label_propagation(&g).unwrap();
728 assert_eq!(r.membership.len(), 0);
729 assert_eq!(r.nb_clusters, 0);
730 }
731
732 #[test]
733 fn isolated_vertices_each_get_own_label() {
734 let g = Graph::with_vertices(4);
735 let r = label_propagation(&g).unwrap();
736 assert_dense_labels(&r, 4);
737 assert_eq!(r.nb_clusters, 4);
738 }
739
740 #[test]
741 fn two_triangles_bridge_split() {
742 let mut g = Graph::with_vertices(6);
743 add_edges(
744 &mut g,
745 &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
746 );
747 let r = label_propagation(&g).unwrap();
748 assert_eq!(r.membership[0], r.membership[1]);
751 assert_eq!(r.membership[0], r.membership[2]);
752 assert_eq!(r.membership[3], r.membership[4]);
753 assert_eq!(r.membership[3], r.membership[5]);
754 assert_dense_labels(&r, 6);
755 }
756
757 #[test]
758 fn directed_input_rejected() {
759 let mut g = Graph::new(3, true).unwrap();
760 g.add_edge(0, 1).unwrap();
761 assert!(label_propagation(&g).is_err());
762 assert!(label_propagation_weighted(&g, &[1.0]).is_err());
763 assert!(label_propagation_with_options(&g, None, &LpaOptions::default()).is_err());
764 }
765
766 #[test]
767 fn weight_validation() {
768 let mut g = Graph::with_vertices(3);
769 g.add_edge(0, 1).unwrap();
770 g.add_edge(1, 2).unwrap();
771 assert!(label_propagation_weighted(&g, &[1.0]).is_err());
773 assert!(label_propagation_weighted(&g, &[1.0, f64::NAN]).is_err());
775 assert!(label_propagation_weighted(&g, &[1.0, -0.1]).is_err());
777 }
778
779 #[test]
780 fn determinism_under_seed_fast() {
781 let mut g = Graph::with_vertices(8);
782 for &(u, v) in &[
783 (0, 1),
784 (0, 2),
785 (1, 2),
786 (2, 3),
787 (3, 4),
788 (4, 5),
789 (4, 6),
790 (5, 6),
791 (5, 7),
792 ] {
793 g.add_edge(u, v).unwrap();
794 }
795 let opts = LpaOptions {
796 seed: 12345,
797 ..LpaOptions::default()
798 };
799 let a = label_propagation_with_options(&g, None, &opts).unwrap();
800 let b = label_propagation_with_options(&g, None, &opts).unwrap();
801 assert_eq!(a.membership, b.membership);
802 }
803
804 #[test]
805 fn determinism_under_seed_dominance() {
806 let mut g = Graph::with_vertices(6);
807 add_edges(
808 &mut g,
809 &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
810 );
811 let opts = LpaOptions {
812 variant: LpaVariant::Dominance,
813 seed: 99,
814 ..LpaOptions::default()
815 };
816 let a = label_propagation_with_options(&g, None, &opts).unwrap();
817 let b = label_propagation_with_options(&g, None, &opts).unwrap();
818 assert_eq!(a.membership, b.membership);
819 }
820
821 #[test]
822 fn determinism_under_seed_retention() {
823 let mut g = Graph::with_vertices(6);
824 add_edges(
825 &mut g,
826 &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
827 );
828 let opts = LpaOptions {
829 variant: LpaVariant::Retention,
830 seed: 7,
831 ..LpaOptions::default()
832 };
833 let a = label_propagation_with_options(&g, None, &opts).unwrap();
834 let b = label_propagation_with_options(&g, None, &opts).unwrap();
835 assert_eq!(a.membership, b.membership);
836 }
837
838 #[test]
839 fn unit_weights_match_unweighted() {
840 let mut g = Graph::with_vertices(6);
841 add_edges(
842 &mut g,
843 &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
844 );
845 let opts = LpaOptions {
846 seed: 42,
847 ..LpaOptions::default()
848 };
849 let a = label_propagation_with_options(&g, None, &opts).unwrap();
850 let ones = vec![1.0; g.ecount()];
851 let b = label_propagation_with_options(&g, Some(&ones), &opts).unwrap();
852 assert_eq!(a.membership, b.membership);
853 }
854
855 #[test]
856 fn initial_validation() {
857 let mut g = Graph::with_vertices(3);
858 g.add_edge(0, 1).unwrap();
859 let opts = LpaOptions {
861 initial: Some(vec![0, 1]),
862 ..LpaOptions::default()
863 };
864 assert!(label_propagation_with_options(&g, None, &opts).is_err());
865 let opts = LpaOptions {
867 initial: Some(vec![0, 1, 99]),
868 ..LpaOptions::default()
869 };
870 assert!(label_propagation_with_options(&g, None, &opts).is_err());
871 }
872
873 #[test]
874 fn fixed_validation() {
875 let mut g = Graph::with_vertices(3);
876 g.add_edge(0, 1).unwrap();
877 let opts = LpaOptions {
879 initial: Some(vec![0, 1, 2]),
880 fixed: Some(vec![false, false]),
881 ..LpaOptions::default()
882 };
883 assert!(label_propagation_with_options(&g, None, &opts).is_err());
884 }
885
886 #[test]
887 fn fixed_vertices_keep_their_labels() {
888 let mut g = Graph::with_vertices(3);
890 add_edges(&mut g, &[(0, 1), (0, 2), (1, 2)]);
891 let opts = LpaOptions {
892 initial: Some(vec![0, 0, 1]),
893 fixed: Some(vec![true, true, true]),
894 ..LpaOptions::default()
895 };
896 let r = label_propagation_with_options(&g, None, &opts).unwrap();
897 assert_eq!(r.membership.len(), 3);
898 assert_eq!(r.membership[0], r.membership[1]);
901 assert_ne!(r.membership[0], r.membership[2]);
902 }
903
904 #[test]
905 fn unlabelled_component_gets_fresh_label() {
906 let mut g = Graph::with_vertices(6);
908 add_edges(&mut g, &[(0, 1), (2, 3), (4, 5)]);
909 let opts = LpaOptions {
910 initial: Some(vec![0, 0, -1, -1, -1, -1]),
911 ..LpaOptions::default()
912 };
913 let r = label_propagation_with_options(&g, None, &opts).unwrap();
914 assert_eq!(r.nb_clusters, 3);
917 assert_eq!(r.membership[0], r.membership[1]);
918 assert_eq!(r.membership[2], r.membership[3]);
919 assert_eq!(r.membership[4], r.membership[5]);
920 }
921
922 #[test]
923 fn ignore_fixed_when_no_initial() {
924 let mut g = Graph::with_vertices(3);
927 add_edges(&mut g, &[(0, 1), (1, 2)]);
928 let opts = LpaOptions {
929 fixed: Some(vec![true, true, true]),
930 ..LpaOptions::default()
931 };
932 let r = label_propagation_with_options(&g, None, &opts).unwrap();
934 assert_eq!(r.membership.len(), 3);
935 }
936
937 #[test]
938 fn self_loops_handled() {
939 let mut g = Graph::with_vertices(3);
943 add_edges(&mut g, &[(0, 0), (0, 1), (1, 2)]);
944 let r = label_propagation(&g).unwrap();
945 assert_dense_labels(&r, 3);
946 }
947
948 #[test]
949 fn three_variants_all_run_on_karate_size_input() {
950 let mut g = Graph::with_vertices(10);
952 for c in 0..2 {
953 let base = c * 5;
954 for u in 0..5 {
955 for v in (u + 1)..5 {
956 g.add_edge(base + u, base + v).unwrap();
957 }
958 }
959 }
960 g.add_edge(4, 5).unwrap();
961
962 for variant in [
963 LpaVariant::Fast,
964 LpaVariant::Dominance,
965 LpaVariant::Retention,
966 ] {
967 let opts = LpaOptions {
968 variant,
969 seed: 0,
970 ..LpaOptions::default()
971 };
972 let r = label_propagation_with_options(&g, None, &opts).unwrap();
973 assert_dense_labels(&r, 10);
974 for u in 0..5 {
976 assert_eq!(r.membership[u], r.membership[0]);
977 assert_eq!(r.membership[u + 5], r.membership[5]);
978 }
979 }
980 }
981}