1use num_traits::{ToPrimitive, Zero};
28use rand::prelude::*;
29use rayon::prelude::*;
30use std::cell::RefCell;
31use std::collections::VecDeque;
32use std::sync::atomic::{AtomicBool, Ordering::Relaxed};
33
34use crate::graph::community_detections::remap_communities_by_size;
35use crate::prelude::*;
36
37const LEIDEN_PAR_MIN_NODES: usize = 4096;
45
46const PAR_MOVE_MAX_ROUNDS: usize = 1000;
50
51const PAR_MOVE_MIN_LEN: usize = 64;
54
55const NEW_CLUSTER: u32 = u32::MAX;
57
58const MAX_ITERATIONS: usize = 1000;
61
62const QUALITY_CHUNK: usize = 4096;
65
66#[derive(Clone, Copy, Debug, Default, PartialEq)]
72pub enum LocalMoving {
73 #[default]
75 Queue,
76 Coloured,
83}
84
85pub fn parse_local_moving(s: &str) -> Option<LocalMoving> {
95 match s.to_lowercase().as_str() {
96 "queue" => Some(LocalMoving::Queue),
97 "coloured" | "colored" => Some(LocalMoving::Coloured),
98 _ => None,
99 }
100}
101
102#[derive(Clone, Copy, Debug)]
120pub struct LeidenParams {
121 pub resolution: f64,
123 pub beta: f64,
125 pub n_iterations: Option<usize>,
127 pub min_gain: f64,
129 pub refine: bool,
131 pub seed: u64,
133 pub local_moving: LocalMoving,
135}
136
137impl LeidenParams {
138 pub fn new(
152 resolution: f64,
153 beta: f64,
154 n_iterations: Option<usize>,
155 refine: bool,
156 seed: u64,
157 ) -> Self {
158 Self {
159 resolution,
160 beta,
161 n_iterations,
162 min_gain: 0.0,
163 refine,
164 seed,
165 local_moving: LocalMoving::Queue,
166 }
167 }
168}
169
170impl Default for LeidenParams {
174 fn default() -> Self {
175 Self {
176 resolution: 1.0,
177 beta: 0.01,
178 n_iterations: Some(5),
179 min_gain: 0.0,
180 refine: true,
181 seed: 42,
182 local_moving: LocalMoving::Queue,
183 }
184 }
185}
186
187trait LevelGraph: Sync {
197 type W: Copy + Zero + Send + Sync + ToPrimitive;
199
200 fn n(&self) -> usize;
202
203 fn row(&self, i: usize) -> (&[u32], &[Self::W]);
205}
206
207impl<T> LevelGraph for SparseGraph<T>
208where
209 T: BixverseFloat + Send + Sync + std::iter::Sum,
210{
211 type W = T;
212
213 #[inline]
214 fn n(&self) -> usize {
215 self.get_node_number()
216 }
217
218 #[inline]
219 fn row(&self, i: usize) -> (&[u32], &[T]) {
220 self.get_neighbours_raw(i)
221 }
222}
223
224struct AggGraph<W> {
235 indptr: Vec<usize>,
237 indices: Vec<u32>,
239 weights: Vec<W>,
241}
242
243impl<W: Copy + Zero + Send + Sync + ToPrimitive> LevelGraph for AggGraph<W> {
244 type W = W;
245
246 #[inline]
247 fn n(&self) -> usize {
248 self.indptr.len() - 1
249 }
250
251 #[inline]
252 fn row(&self, i: usize) -> (&[u32], &[W]) {
253 let (s, e) = (self.indptr[i], self.indptr[i + 1]);
254 (&self.indices[s..e], &self.weights[s..e])
255 }
256}
257
258#[inline(always)]
272fn wf<W: ToPrimitive>(w: W) -> f64 {
273 w.to_f64().expect("float to f64 cannot fail")
274}
275
276#[inline]
286fn splitmix64(mut x: u64) -> u64 {
287 x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
288 x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
289 x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
290 x ^ (x >> 31)
291}
292
293fn derive_seed(seed: u64, parts: &[u64]) -> u64 {
304 parts
305 .iter()
306 .fold(splitmix64(seed), |acc, &p| splitmix64(acc ^ p))
307}
308
309fn reindex(labels: &mut [u32]) -> usize {
319 let mut map = vec![u32::MAX; labels.len()];
320 let mut next = 0u32;
321 for l in labels.iter_mut() {
322 let m = &mut map[*l as usize];
323 if *m == u32::MAX {
324 *m = next;
325 next += 1;
326 }
327 *l = *m;
328 }
329 next as usize
330}
331
332fn group_by_label(labels: &[u32], k: usize) -> (Vec<usize>, Vec<u32>) {
344 let mut offsets = vec![0usize; k + 1];
345 for &l in labels {
346 offsets[l as usize + 1] += 1;
347 }
348 for c in 0..k {
349 offsets[c + 1] += offsets[c];
350 }
351 let mut cursor = offsets.clone();
352 let mut members = vec![0u32; labels.len()];
353 for (v, &l) in labels.iter().enumerate() {
354 members[cursor[l as usize]] = v as u32;
355 cursor[l as usize] += 1;
356 }
357 (offsets, members)
358}
359
360fn level_quality<G: LevelGraph>(
379 g: &G,
380 node_w: &[f64],
381 membership: &[u32],
382 res: f64,
383 two_m: f64,
384) -> f64 {
385 let n = g.n();
386 let partials: Vec<f64> = (0..n.div_ceil(QUALITY_CHUNK))
387 .into_par_iter()
388 .map(|chunk| {
389 let mut s = 0.0;
390 for v in chunk * QUALITY_CHUNK..((chunk + 1) * QUALITY_CHUNK).min(n) {
391 let c = membership[v];
392 let (nbrs, ws) = g.row(v);
393 for (&u, &w) in nbrs.iter().zip(ws) {
394 if membership[u as usize] == c {
395 s += wf(w);
396 }
397 }
398 }
399 s
400 })
401 .collect();
402 let internal: f64 = partials.iter().sum();
403 let mut tot = vec![0.0f64; n];
404 for (&c, &k) in membership.iter().zip(node_w) {
405 tot[c as usize] += k;
406 }
407 let squares: f64 = tot.iter().map(|t| t * t).sum();
408 (internal - res * squares) / two_m
409}
410
411pub fn modularity<T>(graph: &SparseGraph<T>, labels: &[usize], resolution: f64) -> f64
427where
428 T: BixverseFloat + Send + Sync + std::iter::Sum,
429{
430 let n = graph.get_node_number();
431 let k = labels.iter().copied().max().map_or(0, |m| m + 1);
432 let mut internal = 0.0f64;
433 let mut two_m = 0.0f64;
434 let mut cluster_w = vec![0.0f64; k];
435 for v in 0..n {
436 let (nbrs, ws) = graph.get_neighbours_raw(v);
437 let mut deg = 0.0;
438 for (&u, &w) in nbrs.iter().zip(ws) {
439 let w = wf(w);
440 deg += w;
441 if labels[u as usize] == labels[v] {
442 internal += w;
443 }
444 }
445 two_m += deg;
446 cluster_w[labels[v]] += deg;
447 }
448 if two_m == 0.0 {
449 return 0.0;
450 }
451 let penalty: f64 = cluster_w.iter().map(|&c| c * c).sum::<f64>() * resolution / two_m;
452 (internal - penalty) / two_m
453}
454
455fn move_nodes<G: LevelGraph>(
480 g: &G,
481 node_w: &[f64],
482 membership: &mut [u32],
483 res: f64,
484 max_evals: usize,
485 shuffle: bool,
486 rng: &mut StdRng,
487) -> (usize, bool) {
488 let n = g.n();
489
490 let queue_order: Vec<u32> = if shuffle {
496 let mut order: Vec<u32> = (0..n as u32).collect();
497 order.shuffle(rng);
498 order
499 } else {
500 let start = if n > 0 { rng.random_range(0..n) } else { 0 };
501 (start..n).chain(0..start).map(|v| v as u32).collect()
502 };
503 let mut changed = false;
504 let mut queue: VecDeque<u32> = queue_order.into();
505 let mut stable = vec![false; n];
506
507 let mut cluster_w = vec![0.0f64; n];
508 let mut cluster_n = vec![0u32; n];
509 for v in 0..n {
510 let c = membership[v] as usize;
511 cluster_w[c] += node_w[v];
512 cluster_n[c] += 1;
513 }
514 let mut empty: Vec<u32> = (0..n as u32)
515 .filter(|&c| cluster_n[c as usize] == 0)
516 .collect();
517
518 let mut w_to = vec![0.0f64; n];
519 let mut added = vec![false; n];
520 let mut nbr_clusters: Vec<u32> = Vec::with_capacity(64);
521
522 let mut evals = 0usize;
523 while let Some(v) = queue.pop_front() {
524 if evals >= max_evals {
525 break;
526 }
527 evals += 1;
528
529 let v = v as usize;
530 let cur = membership[v];
531 let kv = node_w[v];
532
533 cluster_w[cur as usize] -= kv;
534 cluster_n[cur as usize] -= 1;
535 if cluster_n[cur as usize] == 0 {
536 empty.push(cur);
537 }
538
539 let e = *empty
541 .last()
542 .expect("node just left a cluster, so one is free or v is alone");
543 nbr_clusters.push(e);
544 added[e as usize] = true;
545
546 let (nbrs, ws) = g.row(v);
547 for (&u, &w) in nbrs.iter().zip(ws) {
548 if u as usize == v {
549 continue;
550 }
551 let c = membership[u as usize];
552 if !added[c as usize] {
553 added[c as usize] = true;
554 nbr_clusters.push(c);
555 }
556 w_to[c as usize] += wf(w);
557 }
558
559 let mut best = cur;
560 let mut best_gain = w_to[cur as usize] - kv * cluster_w[cur as usize] * res;
561 for &c in &nbr_clusters {
562 let gain = w_to[c as usize] - kv * cluster_w[c as usize] * res;
563 if gain > best_gain {
565 best = c;
566 best_gain = gain;
567 }
568 w_to[c as usize] = 0.0;
569 added[c as usize] = false;
570 }
571 nbr_clusters.clear();
572
573 cluster_w[best as usize] += kv;
574 cluster_n[best as usize] += 1;
575 if empty.last() == Some(&best) {
576 empty.pop();
577 }
578
579 stable[v] = true;
580 if best != cur {
581 changed = true;
582 membership[v] = best;
583 for &u in nbrs {
584 let u = u as usize;
585 if stable[u] && membership[u] != best {
586 stable[u] = false;
587 queue.push_back(u as u32);
588 }
589 }
590 }
591 }
592
593 (reindex(membership), changed)
594}
595
596#[derive(Default)]
605struct ClusterTable {
606 keys: Vec<u32>,
608 vals: Vec<f64>,
610 used: Vec<u32>,
612}
613
614impl ClusterTable {
615 #[inline]
621 fn reserve(&mut self, deg: usize) {
622 let cap = (2 * deg).next_power_of_two().max(16);
623 if self.keys.len() < cap {
624 self.keys = vec![u32::MAX; cap];
625 self.vals = vec![0.0; cap];
626 }
627 }
628
629 #[inline]
639 fn slot(&self, c: u32) -> usize {
640 let mask = self.keys.len() - 1;
641 let mut i = (c.wrapping_mul(0x9E37_79B1) as usize) & mask;
642 while self.keys[i] != c && self.keys[i] != u32::MAX {
643 i = (i + 1) & mask;
644 }
645 i
646 }
647
648 #[inline]
655 fn add(&mut self, c: u32, w: f64) {
656 let i = self.slot(c);
657 if self.keys[i] == u32::MAX {
658 self.keys[i] = c;
659 self.vals[i] = w;
660 self.used.push(i as u32);
661 } else {
662 self.vals[i] += w;
663 }
664 }
665
666 #[inline]
676 fn get(&self, c: u32) -> f64 {
677 let i = self.slot(c);
678 if self.keys[i] == c { self.vals[i] } else { 0.0 }
679 }
680
681 #[inline]
683 fn clear(&mut self) {
684 for &i in &self.used {
685 self.keys[i as usize] = u32::MAX;
686 }
687 self.used.clear();
688 }
689}
690
691thread_local! {
692 static MOVE_SCRATCH: RefCell<ClusterTable> = RefCell::new(ClusterTable::default());
694}
695
696#[derive(Clone, Copy, Debug)]
698struct Move {
699 v: u32,
701 to: u32,
703 w_to: f64,
705 w_from: f64,
707}
708
709#[allow(clippy::too_many_arguments)]
729fn local_move<G: LevelGraph>(
730 g: &G,
731 node_w: &[f64],
732 membership: &mut [u32],
733 res: f64,
734 max_evals: usize,
735 shuffle: bool,
736 mode: LocalMoving,
737 rng: &mut StdRng,
738) -> (usize, bool) {
739 if g.n() < LEIDEN_PAR_MIN_NODES {
740 return move_nodes(g, node_w, membership, res, max_evals, shuffle, rng);
741 }
742 match mode {
743 LocalMoving::Queue => move_nodes(g, node_w, membership, res, max_evals, shuffle, rng),
744 LocalMoving::Coloured => move_nodes_coloured(g, node_w, membership, res, max_evals, rng),
745 }
746}
747
748#[allow(clippy::too_many_arguments)]
770#[inline]
771fn best_move<G: LevelGraph>(
772 g: &G,
773 v: u32,
774 membership: &[u32],
775 cluster_w: &[f64],
776 cluster_n: &[u32],
777 kv: f64,
778 res: f64,
779 table: &mut ClusterTable,
780) -> Option<Move> {
781 let (nbrs, ws) = g.row(v as usize);
782 table.reserve(nbrs.len());
783 for (&u, &w) in nbrs.iter().zip(ws) {
784 if u != v {
785 table.add(membership[u as usize], wf(w));
786 }
787 }
788 let own = membership[v as usize];
789 let w_from = table.get(own);
790 let stay = w_from - kv * (cluster_w[own as usize] - kv) * res;
791
792 let mut best = (own, stay);
793 let mut w_best = 0.0;
794 for &i in &table.used {
795 let c = table.keys[i as usize];
796 if c == own {
797 continue;
798 }
799 let gain = table.vals[i as usize] - kv * cluster_w[c as usize] * res;
800 if gain > best.1 || (gain == best.1 && best.0 != own && c < best.0) {
801 best = (c, gain);
802 w_best = table.vals[i as usize];
803 }
804 }
805 if best.0 == own && cluster_n[own as usize] > 1 && 0.0 > stay {
806 best = (NEW_CLUSTER, 0.0);
807 w_best = 0.0;
808 }
809 table.clear();
810 (best.0 != own).then_some(Move {
811 v,
812 to: best.0,
813 w_to: w_best,
814 w_from,
815 })
816}
817
818fn decide_moves<G: LevelGraph>(
834 g: &G,
835 nodes: &[u32],
836 membership: &[u32],
837 cluster_w: &[f64],
838 cluster_n: &[u32],
839 node_w: &[f64],
840 res: f64,
841) -> Vec<Move> {
842 let decide = |&v: &u32| {
843 MOVE_SCRATCH.with(|s| {
844 best_move(
845 g,
846 v,
847 membership,
848 cluster_w,
849 cluster_n,
850 node_w[v as usize],
851 res,
852 &mut s.borrow_mut(),
853 )
854 })
855 };
856 nodes
857 .par_iter()
858 .with_min_len(PAR_MOVE_MIN_LEN)
859 .filter_map(decide)
860 .collect()
861}
862
863fn flag_neighbours<G: LevelGraph>(
876 g: &G,
877 moved: &[u32],
878 membership: &[u32],
879 pending: &[AtomicBool],
880) -> Vec<u32> {
881 let flag = |&v: &u32| {
884 let c = membership[v as usize];
885 g.row(v as usize).0.iter().copied().filter(move |&u| {
886 let p = &pending[u as usize];
887 membership[u as usize] != c && !p.load(Relaxed) && !p.swap(true, Relaxed)
888 })
889 };
890 moved
891 .par_iter()
892 .with_min_len(PAR_MOVE_MIN_LEN)
893 .flat_map_iter(flag)
894 .collect()
895}
896
897fn take_pending(list: &mut Vec<u32>, pending: &[AtomicBool]) -> Vec<u32> {
909 let mut nodes = std::mem::take(list);
910 nodes.par_sort_unstable();
911 nodes
912 .par_iter()
913 .with_min_len(PAR_MOVE_MIN_LEN)
914 .for_each(|&v| pending[v as usize].store(false, Relaxed));
915 nodes
916}
917
918fn cluster_totals(membership: &[u32], node_w: &[f64]) -> (Vec<f64>, Vec<u32>, Vec<u32>) {
929 let n = membership.len();
930 let mut cluster_w = vec![0.0f64; n];
931 let mut cluster_n = vec![0u32; n];
932 for (v, &c) in membership.iter().enumerate() {
933 cluster_w[c as usize] += node_w[v];
934 cluster_n[c as usize] += 1;
935 }
936 let empty = (0..n as u32)
937 .rev()
938 .filter(|&c| cluster_n[c as usize] == 0)
939 .collect();
940 (cluster_w, cluster_n, empty)
941}
942
943fn greedy_colouring<G: LevelGraph>(g: &G, order: &[u32]) -> (Vec<Vec<u32>>, Vec<u32>) {
959 let n = g.n();
960 let mut colour = vec![u32::MAX; n];
961 let mut used: Vec<bool> = Vec::new();
962 let mut classes: Vec<Vec<u32>> = Vec::new();
963 for &v in order {
964 let nbrs = g.row(v as usize).0;
965 let mut mask = 0u64;
966 for &u in nbrs {
967 let c = colour[u as usize];
968 if c < 64 {
969 mask |= 1 << c;
970 }
971 }
972 let c = if mask != u64::MAX {
973 mask.trailing_ones() as usize
974 } else {
975 used.resize(classes.len(), false);
976 for &u in nbrs {
977 let cu = colour[u as usize];
978 if cu != u32::MAX {
979 used[cu as usize] = true;
980 }
981 }
982 let c = used.iter().position(|&x| !x).unwrap_or(used.len());
983 used.iter_mut().for_each(|x| *x = false);
984 c
985 };
986 if c == classes.len() {
987 classes.push(Vec::new());
988 }
989 colour[v as usize] = c as u32;
990 classes[c].push(v);
991 }
992 classes.par_iter_mut().for_each(|c| c.sort_unstable());
993 (classes, colour)
994}
995
996fn seeded_colouring<G: LevelGraph>(g: &G, rng: &mut StdRng) -> (Vec<Vec<u32>>, Vec<u32>) {
1009 let n = g.n();
1010 let start = rng.random_range(0..n) as u32;
1011 let order: Vec<u32> = (start..n as u32).chain(0..start).collect();
1012 greedy_colouring(g, &order)
1013}
1014
1015fn move_nodes_coloured<G: LevelGraph>(
1039 g: &G,
1040 node_w: &[f64],
1041 membership: &mut [u32],
1042 res: f64,
1043 max_evals: usize,
1044 rng: &mut StdRng,
1045) -> (usize, bool) {
1046 let n = g.n();
1047 let (classes, colour) = seeded_colouring(g, rng);
1048
1049 let (mut cluster_w, mut cluster_n, mut empty) = cluster_totals(membership, node_w);
1050 let pending: Vec<AtomicBool> = (0..n).map(|_| AtomicBool::new(true)).collect();
1051 let mut lists = classes;
1052 let mut moved: Vec<u32> = Vec::new();
1053 let mut changed = false;
1054 let mut evals = 0usize;
1055
1056 'sweeps: for _ in 0..PAR_MOVE_MAX_ROUNDS {
1057 if lists.iter().all(|l| l.is_empty()) {
1058 break;
1059 }
1060 for c in 0..lists.len() {
1061 if lists[c].is_empty() {
1062 continue;
1063 }
1064 let nodes = take_pending(&mut lists[c], &pending);
1065 evals += nodes.len();
1066 let moves = decide_moves(g, &nodes, membership, &cluster_w, &cluster_n, node_w, res);
1067 moved.clear();
1068 for m in moves {
1069 let (v, kv) = (m.v as usize, node_w[m.v as usize]);
1070 let from = membership[v] as usize;
1071 let stay = m.w_from - kv * (cluster_w[from] - kv) * res;
1072 let go = if m.to == NEW_CLUSTER {
1073 0.0
1074 } else {
1075 m.w_to - kv * cluster_w[m.to as usize] * res
1076 };
1077 if go <= stay || (m.to == NEW_CLUSTER && cluster_n[from] == 1) {
1079 continue;
1080 }
1081 let to = if m.to == NEW_CLUSTER {
1082 empty
1083 .pop()
1084 .expect("a cluster with two or more nodes leaves one id free")
1085 } else {
1086 m.to
1087 } as usize;
1088 cluster_w[from] -= kv;
1089 cluster_n[from] -= 1;
1090 if cluster_n[from] == 0 {
1091 empty.push(from as u32);
1092 }
1093 cluster_w[to] += kv;
1094 cluster_n[to] += 1;
1095 membership[v] = to as u32;
1096 moved.push(v as u32);
1097 }
1098 changed |= !moved.is_empty();
1099 for u in flag_neighbours(g, &moved, membership, &pending) {
1100 lists[colour[u as usize] as usize].push(u);
1101 }
1102 if evals >= max_evals {
1103 break 'sweeps;
1104 }
1105 }
1106 }
1107
1108 (reindex(membership), changed)
1109}
1110
1111#[derive(Default)]
1128struct RefineScratch {
1129 cluster_w: Vec<f64>,
1130 ext: Vec<f64>,
1131 w_to: Vec<f64>,
1132 added: Vec<bool>,
1133 non_singleton: Vec<bool>,
1134 cands: Vec<u32>,
1135 cum: Vec<f64>,
1136 gains: Vec<f64>,
1137}
1138
1139impl RefineScratch {
1140 fn reset(&mut self, s: usize) {
1146 for v in [&mut self.cluster_w, &mut self.ext, &mut self.w_to] {
1147 v.clear();
1148 v.resize(s, 0.0);
1149 }
1150 for v in [&mut self.added, &mut self.non_singleton] {
1151 v.clear();
1152 v.resize(s, false);
1153 }
1154 }
1155}
1156
1157#[allow(clippy::too_many_arguments)]
1184fn refine_cluster<G: LevelGraph>(
1185 g: &G,
1186 node_w: &[f64],
1187 membership: &[u32],
1188 local_idx: &[u32],
1189 members: &[u32],
1190 cluster: u32,
1191 res: f64,
1192 beta: f64,
1193 seed: u64,
1194 sc: &mut RefineScratch,
1195) -> (Vec<u32>, usize) {
1196 let s = members.len();
1197 let mut label: Vec<u32> = (0..s as u32).collect();
1198 if s == 1 {
1199 return (label, 1);
1200 }
1201 sc.reset(s);
1202
1203 let mut total_w = 0.0f64;
1204 for (i, &v) in members.iter().enumerate() {
1205 let v = v as usize;
1206 sc.cluster_w[i] = node_w[v];
1207 total_w += node_w[v];
1208 let (nbrs, ws) = g.row(v);
1209 for (&u, &w) in nbrs.iter().zip(ws) {
1210 if u as usize != v && membership[u as usize] == cluster {
1211 sc.ext[i] += wf(w);
1212 }
1213 }
1214 }
1215
1216 let mut rng = StdRng::seed_from_u64(seed);
1217 let mut order: Vec<u32> = (0..s as u32).collect();
1218 order.shuffle(&mut rng);
1219
1220 for &i in &order {
1221 let i = i as usize;
1222 let cur = label[i] as usize;
1223 let kv = node_w[members[i] as usize];
1224 let well_connected = |sc: &RefineScratch, c: usize| {
1225 sc.ext[c] >= sc.cluster_w[c] * (total_w - sc.cluster_w[c]) * res
1226 };
1227
1228 if sc.non_singleton[cur] || !well_connected(sc, cur) {
1229 continue;
1230 }
1231
1232 let ext_v = sc.ext[cur];
1233 sc.cluster_w[cur] = 0.0;
1234
1235 sc.cands.push(cur as u32);
1236 sc.added[cur] = true;
1237 let (nbrs, ws) = g.row(members[i] as usize);
1238 for (&u, &w) in nbrs.iter().zip(ws) {
1239 let u = u as usize;
1240 if u == members[i] as usize || membership[u] != cluster {
1241 continue;
1242 }
1243 let c = label[local_idx[u] as usize] as usize;
1244 if !sc.added[c] {
1245 sc.added[c] = true;
1246 sc.cands.push(c as u32);
1247 }
1248 sc.w_to[c] += wf(w);
1249 }
1250
1251 let mut best = cur;
1252 let mut max_gain = 0.0f64;
1253 for &c in &sc.cands {
1254 let c = c as usize;
1255 let gain = if well_connected(sc, c) {
1256 sc.w_to[c] - kv * sc.cluster_w[c] * res
1257 } else {
1258 f64::NAN
1259 };
1260 if gain > max_gain {
1261 max_gain = gain;
1262 best = c;
1263 }
1264 sc.gains.push(gain);
1265 }
1266
1267 let mut total = 0.0f64;
1269 for &gain in &sc.gains {
1270 if gain >= 0.0 {
1271 total += ((gain - max_gain) / beta).exp();
1272 }
1273 sc.cum.push(total);
1274 }
1275 let chosen = if total > 0.0 && total.is_finite() {
1276 let r = rng.random::<f64>() * total;
1277 let idx = sc.cum.partition_point(|&x| x <= r).min(sc.cands.len() - 1);
1278 sc.cands[idx] as usize
1279 } else {
1280 best
1281 };
1282
1283 let w_chosen = sc.w_to[chosen];
1284 for &c in &sc.cands {
1285 sc.w_to[c as usize] = 0.0;
1286 sc.added[c as usize] = false;
1287 }
1288 sc.cands.clear();
1289 sc.gains.clear();
1290 sc.cum.clear();
1291
1292 sc.cluster_w[chosen] += kv;
1293 if chosen != cur {
1294 sc.ext[chosen] += ext_v - 2.0 * w_chosen;
1296 label[i] = chosen as u32;
1297 sc.non_singleton[chosen] = true;
1298 }
1299 }
1300
1301 let k = reindex(&mut label);
1302 (label, k)
1303}
1304
1305fn refine_partition<G: LevelGraph>(
1325 g: &G,
1326 node_w: &[f64],
1327 membership: &[u32],
1328 n_clusters: usize,
1329 res: f64,
1330 beta: f64,
1331 seed: u64,
1332) -> (Vec<u32>, usize) {
1333 let n = g.n();
1334 let (offsets, members) = group_by_label(membership, n_clusters);
1335 let mut local_idx = vec![0u32; n];
1336 for c in 0..n_clusters {
1337 for (i, &v) in members[offsets[c]..offsets[c + 1]].iter().enumerate() {
1338 local_idx[v as usize] = i as u32;
1339 }
1340 }
1341
1342 let run = |sc: &mut RefineScratch, c: usize| {
1343 refine_cluster(
1344 g,
1345 node_w,
1346 membership,
1347 &local_idx,
1348 &members[offsets[c]..offsets[c + 1]],
1349 c as u32,
1350 res,
1351 beta,
1352 derive_seed(seed, &[c as u64]),
1353 sc,
1354 )
1355 };
1356 let per_cluster: Vec<(Vec<u32>, usize)> = if n >= LEIDEN_PAR_MIN_NODES {
1357 (0..n_clusters)
1358 .into_par_iter()
1359 .map_init(RefineScratch::default, run)
1360 .collect()
1361 } else {
1362 let mut sc = RefineScratch::default();
1363 (0..n_clusters).map(|c| run(&mut sc, c)).collect()
1364 };
1365
1366 let mut refined = vec![0u32; n];
1367 let mut base = 0u32;
1368 for (c, (labels, k)) in per_cluster.into_iter().enumerate() {
1369 for (&v, &l) in members[offsets[c]..offsets[c + 1]].iter().zip(&labels) {
1370 refined[v as usize] = base + l;
1371 }
1372 base += k as u32;
1373 }
1374 (refined, base as usize)
1375}
1376
1377fn aggregate<G: LevelGraph>(
1399 g: &G,
1400 node_w: &[f64],
1401 refined: &[u32],
1402 n_refined: usize,
1403 membership: &[u32],
1404) -> (AggGraph<f64>, Vec<f64>, Vec<u32>) {
1405 let (offsets, members) = group_by_label(refined, n_refined);
1406
1407 let row = |(acc, seen, touched): &mut (Vec<f64>, Vec<bool>, Vec<u32>), c: usize| {
1408 if acc.len() < n_refined {
1409 acc.resize(n_refined, 0.0);
1410 seen.resize(n_refined, false);
1411 }
1412 let mut w_sum = 0.0;
1413 for &v in &members[offsets[c]..offsets[c + 1]] {
1414 w_sum += node_w[v as usize];
1415 let (nbrs, ws) = g.row(v as usize);
1416 for (&u, &w) in nbrs.iter().zip(ws) {
1417 let c2 = refined[u as usize];
1418 if c2 as usize == c {
1419 continue;
1420 }
1421 if !seen[c2 as usize] {
1422 seen[c2 as usize] = true;
1423 touched.push(c2);
1424 }
1425 acc[c2 as usize] += wf(w);
1426 }
1427 }
1428 let mut idx = Vec::with_capacity(touched.len());
1429 let mut wts = Vec::with_capacity(touched.len());
1430 for &c2 in touched.iter() {
1431 idx.push(c2);
1432 wts.push(acc[c2 as usize]);
1433 acc[c2 as usize] = 0.0;
1434 seen[c2 as usize] = false;
1435 }
1436 touched.clear();
1437 (idx, wts, w_sum)
1438 };
1439
1440 let rows: Vec<(Vec<u32>, Vec<f64>, f64)> = if g.n() >= LEIDEN_PAR_MIN_NODES {
1441 (0..n_refined)
1442 .into_par_iter()
1443 .map_init(|| (Vec::new(), Vec::new(), Vec::new()), row)
1444 .collect()
1445 } else {
1446 let mut scratch = (Vec::new(), Vec::new(), Vec::new());
1447 (0..n_refined).map(|c| row(&mut scratch, c)).collect()
1448 };
1449
1450 let mut indptr = Vec::with_capacity(n_refined + 1);
1451 indptr.push(0usize);
1452 for r in &rows {
1453 indptr.push(indptr.last().unwrap() + r.0.len());
1454 }
1455 let nnz = *indptr.last().unwrap();
1456 let mut indices = Vec::with_capacity(nnz);
1457 let mut weights = Vec::with_capacity(nnz);
1458 let mut agg_w = Vec::with_capacity(n_refined);
1459 for (idx, wts, w_sum) in rows {
1460 indices.extend_from_slice(&idx);
1461 weights.extend_from_slice(&wts);
1462 agg_w.push(w_sum);
1463 }
1464
1465 let agg_membership = (0..n_refined)
1466 .map(|c| membership[members[offsets[c]] as usize])
1467 .collect();
1468
1469 (
1470 AggGraph {
1471 indptr,
1472 indices,
1473 weights,
1474 },
1475 agg_w,
1476 agg_membership,
1477 )
1478}
1479
1480fn bfs_order<G: LevelGraph>(g: &G) -> Vec<u32> {
1495 let n = g.n();
1496 let mut seen = vec![false; n];
1497 let mut order = Vec::with_capacity(n);
1498 for s in 0..n {
1499 if seen[s] {
1500 continue;
1501 }
1502 seen[s] = true;
1503 let mut head = order.len();
1504 order.push(s as u32);
1505 while head < order.len() {
1506 let v = order[head] as usize;
1507 head += 1;
1508 for &u in g.row(v).0 {
1509 if !seen[u as usize] {
1510 seen[u as usize] = true;
1511 order.push(u);
1512 }
1513 }
1514 }
1515 }
1516 order
1517}
1518
1519fn permute_graph<G: LevelGraph>(g: &G, order: &[u32], pos: &[u32]) -> AggGraph<G::W> {
1531 let mut indptr = Vec::with_capacity(order.len() + 1);
1532 indptr.push(0usize);
1533 for &old in order {
1534 indptr.push(indptr.last().unwrap() + g.row(old as usize).0.len());
1535 }
1536 let nnz = *indptr.last().unwrap();
1537 let mut indices = vec![0u32; nnz];
1538 let mut weights = vec![G::W::zero(); nnz];
1539
1540 let mut rows = Vec::with_capacity(order.len());
1542 let (mut idx_rest, mut w_rest) = (indices.as_mut_slice(), weights.as_mut_slice());
1543 for r in 0..order.len() {
1544 let len = indptr[r + 1] - indptr[r];
1545 let (idx_row, idx_tail) = idx_rest.split_at_mut(len);
1546 let (w_row, w_tail) = w_rest.split_at_mut(len);
1547 rows.push((idx_row, w_row));
1548 idx_rest = idx_tail;
1549 w_rest = w_tail;
1550 }
1551 rows.par_iter_mut()
1552 .zip(order.par_iter())
1553 .for_each(|((idx_row, w_row), &old)| {
1554 let (nbrs, ws) = g.row(old as usize);
1555 for (d, &u) in idx_row.iter_mut().zip(nbrs) {
1556 *d = pos[u as usize];
1557 }
1558 w_row.copy_from_slice(ws);
1559 });
1560
1561 AggGraph {
1562 indptr,
1563 indices,
1564 weights,
1565 }
1566}
1567
1568#[allow(clippy::too_many_arguments)]
1595fn leiden_iteration<G, E, F>(
1596 graph: &G,
1597 node_w: &[f64],
1598 membership: &mut [u32],
1599 res: f64,
1600 params: &LeidenParams,
1601 iteration: u64,
1602 max_evals: usize,
1603 multi_level: bool,
1604 level0: &mut F,
1605) -> Result<bool, E>
1606where
1607 G: LevelGraph,
1608 F: FnMut(&G, &[f64], &mut [u32], f64, &mut StdRng) -> Result<(usize, bool), E>,
1609{
1610 let n = graph.n();
1611 let mut rng = StdRng::seed_from_u64(derive_seed(params.seed, &[iteration]));
1612 reindex(membership);
1613
1614 let (n_clusters, mut changed) = level0(graph, node_w, membership, res, &mut rng)?;
1615 if !multi_level || n_clusters == n {
1616 return Ok(changed);
1617 }
1618
1619 let (refined, n_refined) = refined_or_unrefined(
1620 graph, node_w, membership, n_clusters, res, params, iteration, 0,
1621 );
1622 let mut aggregate_node = refined.clone();
1623 let (mut g, mut w, mut agg_memb) = aggregate(graph, node_w, &refined, n_refined, membership);
1624
1625 let mut level = 1u64;
1626 loop {
1627 let gn = g.n();
1628 let (nc, ch) = local_move(
1629 &g,
1630 &w,
1631 &mut agg_memb,
1632 res,
1633 max_evals.saturating_mul(gn),
1634 !params.refine,
1635 params.local_moving,
1636 &mut rng,
1637 );
1638 changed |= ch;
1639 for (m, &a) in membership.iter_mut().zip(&aggregate_node) {
1640 *m = agg_memb[a as usize];
1641 }
1642 if nc == gn {
1643 break;
1644 }
1645
1646 let (refined, n_refined) =
1647 refined_or_unrefined(&g, &w, &agg_memb, nc, res, params, iteration, level);
1648 for a in aggregate_node.iter_mut() {
1649 *a = refined[*a as usize];
1650 }
1651 (g, w, agg_memb) = aggregate(&g, &w, &refined, n_refined, &agg_memb);
1652 level += 1;
1653 }
1654
1655 Ok(changed)
1656}
1657
1658#[allow(clippy::too_many_arguments)]
1678fn refined_or_unrefined<G: LevelGraph>(
1679 g: &G,
1680 node_w: &[f64],
1681 membership: &[u32],
1682 n_clusters: usize,
1683 res: f64,
1684 params: &LeidenParams,
1685 iteration: u64,
1686 level: u64,
1687) -> (Vec<u32>, usize) {
1688 if params.refine {
1689 let seed = derive_seed(params.seed, &[iteration, level]);
1690 let (refined, k) =
1691 refine_partition(g, node_w, membership, n_clusters, res, params.beta, seed);
1692 if k < g.n() {
1693 return (refined, k);
1694 }
1695 }
1696 (membership.to_vec(), n_clusters)
1697}
1698
1699pub trait LocalMover {
1706 type Error: From<MethodsErrors>;
1708
1709 fn prepare<W: ToPrimitive + Sync>(
1723 &mut self,
1724 indptr: &[usize],
1725 indices: &[u32],
1726 weights: &[W],
1727 node_w: &[f64],
1728 ) -> Result<(), Self::Error>;
1729
1730 fn move_nodes(
1745 &mut self,
1746 membership: &mut [u32],
1747 classes: &[Vec<u32>],
1748 colour: &[u32],
1749 res: f64,
1750 ) -> Result<bool, Self::Error>;
1751}
1752
1753fn detect<T, E, F>(
1767 graph: &SparseGraph<T>,
1768 params: &LeidenParams,
1769 max_evals: usize,
1770 multi_level: bool,
1771 mut level0: F,
1772) -> Result<Vec<usize>, E>
1773where
1774 T: BixverseFloat + Send + Sync + std::iter::Sum,
1775 E: From<MethodsErrors> + Send,
1776 F: FnMut(&AggGraph<T>, &[f64], &mut [u32], f64, &mut StdRng) -> Result<(usize, bool), E> + Send,
1777{
1778 if graph.is_directed() {
1779 return Err(MethodsErrors::GraphDirectedError.into());
1780 }
1781 let n = graph.get_node_number();
1782 if n == 0 {
1783 return Ok(Vec::new());
1784 }
1785 assert!(n < u32::MAX as usize, "more than u32::MAX nodes");
1786
1787 let order = bfs_order(graph);
1788 let mut pos = vec![0u32; n];
1789 for (new, &old) in order.iter().enumerate() {
1790 pos[old as usize] = new as u32;
1791 }
1792 let g = permute_graph(graph, &order, &pos);
1793
1794 let node_w: Vec<f64> = (0..n)
1795 .into_par_iter()
1796 .map(|v| g.row(v).1.iter().map(|&w| wf(w)).sum())
1797 .collect();
1798 let two_m: f64 = node_w.iter().sum();
1799
1800 let mut membership: Vec<u32> = (0..n as u32).collect();
1801 if two_m > 0.0 {
1802 let res = params.resolution / two_m;
1803 rayon::scope(|_| -> Result<(), E> {
1807 let until_stable = params.n_iterations.is_none();
1808 let cap = params.n_iterations.map_or(MAX_ITERATIONS, |k| k.max(1));
1809 let use_gain = until_stable && params.min_gain > 0.0;
1810 let mut quality = if use_gain {
1811 level_quality(&g, &node_w, &membership, res, two_m)
1812 } else {
1813 0.0
1814 };
1815 for it in 0..cap {
1816 let before = until_stable.then(|| membership.clone());
1817 leiden_iteration(
1818 &g,
1819 &node_w,
1820 &mut membership,
1821 res,
1822 params,
1823 it as u64,
1824 max_evals,
1825 multi_level,
1826 &mut level0,
1827 )?;
1828 if let Some(mut before) = before {
1829 reindex(&mut before);
1832 let mut after = membership.clone();
1833 reindex(&mut after);
1834 if before == after {
1835 break;
1836 }
1837 }
1838 if use_gain {
1839 let q = level_quality(&g, &node_w, &membership, res, two_m);
1840 if q - quality <= params.min_gain {
1841 break;
1842 }
1843 quality = q;
1844 }
1845 }
1846 Ok(())
1847 })?;
1848 }
1849
1850 let labels: Vec<usize> = pos
1852 .iter()
1853 .map(|&new| membership[new as usize] as usize)
1854 .collect();
1855 Ok(remap_communities_by_size(&labels))
1856}
1857
1858pub fn community_detection<T>(
1874 graph: &SparseGraph<T>,
1875 params: &LeidenParams,
1876 max_evals: usize,
1877 multi_level: bool,
1878) -> Result<Vec<usize>, MethodsErrors>
1879where
1880 T: BixverseFloat + Send + Sync + std::iter::Sum,
1881{
1882 let (shuffle, mode) = (!params.refine, params.local_moving);
1883 detect(
1884 graph,
1885 params,
1886 max_evals,
1887 multi_level,
1888 |g, node_w, membership, res, rng| {
1889 let evals = max_evals.saturating_mul(g.n());
1890 Ok(local_move(
1891 g, node_w, membership, res, evals, shuffle, mode, rng,
1892 ))
1893 },
1894 )
1895}
1896
1897pub fn community_detection_with<T, M>(
1916 graph: &SparseGraph<T>,
1917 params: &LeidenParams,
1918 max_evals: usize,
1919 multi_level: bool,
1920 mover: &mut M,
1921) -> Result<Vec<usize>, M::Error>
1922where
1923 T: BixverseFloat + Send + Sync + std::iter::Sum,
1924 M: LocalMover + Send,
1925 M::Error: Send,
1926{
1927 let mut prepared = false;
1928 detect(
1929 graph,
1930 params,
1931 max_evals,
1932 multi_level,
1933 |g, node_w, membership, res, rng| {
1934 if !prepared {
1935 mover.prepare(&g.indptr, &g.indices, &g.weights, node_w)?;
1936 prepared = true;
1937 }
1938 let (classes, colour) = seeded_colouring(g, rng);
1939 let changed = mover.move_nodes(membership, &classes, &colour, res)?;
1940 Ok((reindex(membership), changed))
1941 },
1942 )
1943}
1944
1945pub fn leiden_sparse_graph<T>(
1967 graph: &SparseGraph<T>,
1968 params: Option<LeidenParams>,
1969) -> Result<Vec<usize>, MethodsErrors>
1970where
1971 T: BixverseFloat + Send + Sync + std::iter::Sum,
1972{
1973 let params = params.unwrap_or_default();
1974 community_detection(graph, ¶ms, usize::MAX, true)
1975}
1976
1977pub fn louvain_sparse_graph<T>(
2004 graph: &SparseGraph<T>,
2005 resolution: T,
2006 max_iter: usize,
2007 multi_level: bool,
2008 seed: usize,
2009) -> Result<Vec<usize>, MethodsErrors>
2010where
2011 T: BixverseFloat + Send + Sync + std::iter::Sum,
2012{
2013 let iterations = if multi_level { 2 } else { 1 };
2014 let params = LeidenParams::new(wf(resolution), 0.01, Some(iterations), false, seed as u64);
2015 community_detection(graph, ¶ms, max_iter, multi_level)
2016}
2017
2018#[cfg(test)]
2023mod tests {
2024 use super::*;
2025 use bixverse_core::core::math::sparse::coo_to_csr;
2026
2027 const KARATE: [usize; 156] = [
2029 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 10, 0, 11, 0, 12, 0, 13, 0, 17, 0, 19,
2030 0, 21, 0, 31, 1, 2, 1, 3, 1, 7, 1, 13, 1, 17, 1, 19, 1, 21, 1, 30, 2, 3, 2, 7, 2, 27, 2,
2031 28, 2, 32, 2, 9, 2, 8, 2, 13, 3, 7, 3, 12, 3, 13, 4, 6, 4, 10, 5, 6, 5, 10, 5, 16, 6, 16,
2032 8, 30, 8, 32, 8, 33, 9, 33, 13, 33, 14, 32, 14, 33, 15, 32, 15, 33, 18, 32, 18, 33, 19, 33,
2033 20, 32, 20, 33, 22, 32, 22, 33, 23, 25, 23, 27, 23, 32, 23, 33, 23, 29, 24, 25, 24, 27, 24,
2034 31, 25, 31, 26, 29, 26, 33, 27, 33, 28, 31, 28, 33, 29, 32, 29, 33, 30, 32, 30, 33, 31, 32,
2035 31, 33, 32, 33,
2036 ];
2037
2038 fn build_undirected<T: BixverseFloat + BixverseNumeric + std::iter::Sum>(
2039 n: usize,
2040 edges: &[(usize, usize, T)],
2041 directed: bool,
2042 ) -> SparseGraph<T> {
2043 let mut rows = Vec::with_capacity(edges.len() * 2);
2044 let mut cols = Vec::with_capacity(edges.len() * 2);
2045 let mut vals = Vec::with_capacity(edges.len() * 2);
2046 for &(u, v, w) in edges {
2047 rows.extend([u, v]);
2048 cols.extend([v, u]);
2049 vals.extend([w, w]);
2050 }
2051 let csr = coo_to_csr(&rows.index_cast(), &cols.index_cast(), &vals, (n, n));
2052 SparseGraph::new(n, csr, directed)
2053 }
2054
2055 fn karate() -> SparseGraph<f64> {
2056 let edges: Vec<(usize, usize, f64)> =
2057 KARATE.chunks_exact(2).map(|e| (e[0], e[1], 1.0)).collect();
2058 build_undirected(34, &edges, false)
2059 }
2060
2061 fn planted(
2064 k: usize,
2065 size: usize,
2066 d_in: usize,
2067 d_out: usize,
2068 seed: u64,
2069 ) -> Vec<(usize, usize, f64)> {
2070 let n = k * size;
2071 let mut rng = StdRng::seed_from_u64(seed);
2072 let mut edges = Vec::new();
2073 for v in 0..n {
2074 let b = v / size;
2075 for _ in 0..d_in {
2076 let u = b * size + rng.random_range(0..size);
2077 if u != v {
2078 edges.push((v, u, rng.random_range(1..=3) as f64));
2079 }
2080 }
2081 for _ in 0..d_out {
2082 let u = rng.random_range(0..n);
2083 if u / size != b {
2084 edges.push((v, u, 1.0));
2085 }
2086 }
2087 }
2088 edges
2089 }
2090
2091 fn n_communities(labels: &[usize]) -> usize {
2092 labels.iter().max().map_or(0, |m| m + 1)
2093 }
2094
2095 fn all_connected(graph: &SparseGraph<f64>, labels: &[usize]) -> bool {
2097 let n = graph.get_node_number();
2098 let mut seen = vec![false; n];
2099 let mut roots = vec![false; n_communities(labels)];
2100 for s in 0..n {
2101 if seen[s] {
2102 continue;
2103 }
2104 if roots[labels[s]] {
2105 return false;
2106 }
2107 roots[labels[s]] = true;
2108 let mut stack = vec![s];
2109 seen[s] = true;
2110 while let Some(v) = stack.pop() {
2111 for &u in graph.get_neighbours_raw(v).0 {
2112 let u = u as usize;
2113 if !seen[u] && labels[u] == labels[v] {
2114 seen[u] = true;
2115 stack.push(u);
2116 }
2117 }
2118 }
2119 }
2120 true
2121 }
2122
2123 #[test]
2125 fn test_modularity_matches_igraph() {
2126 let g = karate();
2127 let labels: Vec<usize> = (0..34).map(|i| i % 2).collect();
2128 approx::assert_relative_eq!(
2129 modularity(&g, &labels, 1.0),
2130 -0.000082182774,
2131 epsilon = 1e-10
2132 );
2133 approx::assert_relative_eq!(
2134 modularity(&g, &labels, 0.5),
2135 0.249958908613,
2136 epsilon = 1e-10
2137 );
2138 }
2139
2140 #[test]
2143 fn test_leiden_karate_modularity() {
2144 let g = karate();
2145 for seed in 0..10 {
2146 let p = LeidenParams {
2147 seed,
2148 ..Default::default()
2149 };
2150 let labels = leiden_sparse_graph(&g, Some(p)).unwrap();
2151 let q = modularity(&g, &labels, 1.0);
2152 assert!(q >= 0.418803 - 1e-6, "seed {seed}: modularity {q}");
2153 assert!(all_connected(&g, &labels));
2154 }
2155 }
2156
2157 #[test]
2159 fn test_louvain_karate_modularity() {
2160 let g = karate();
2161 for seed in 0..10 {
2162 let labels = louvain_sparse_graph(&g, 1.0, 50, true, seed).unwrap();
2163 let q = modularity(&g, &labels, 1.0);
2164 assert!(q >= 0.388560 - 1e-6, "seed {seed}: modularity {q}");
2165 }
2166 }
2167
2168 #[test]
2170 fn test_leiden_recovers_planted_blocks() {
2171 let edges = planted(8, 200, 10, 1, 7);
2172 let g = build_undirected(1600, &edges, false);
2173 let labels = leiden_sparse_graph(&g, None).unwrap();
2174 assert_eq!(n_communities(&labels), 8);
2175 for v in 0..1600 {
2176 assert_eq!(labels[v], labels[(v / 200) * 200]);
2177 }
2178 }
2179
2180 #[test]
2183 fn test_leiden_thread_count_invariant() {
2184 let edges = planted(20, 400, 8, 3, 11);
2185 let g = build_undirected(8000, &edges, false);
2186 assert!(g.get_node_number() >= LEIDEN_PAR_MIN_NODES);
2187 let run = |threads: usize| {
2188 rayon::ThreadPoolBuilder::new()
2189 .num_threads(threads)
2190 .build()
2191 .unwrap()
2192 .install(|| leiden_sparse_graph(&g, None).unwrap())
2193 };
2194 let one = run(1);
2195 assert_eq!(one, run(4));
2196 assert!(all_connected(&g, &one));
2197 }
2198
2199 #[test]
2204 fn test_leiden_until_stable() {
2205 let edges = planted(20, 400, 8, 3, 11);
2206 let g = build_undirected(8000, &edges, false);
2207 let q_two = modularity(&g, &leiden_sparse_graph(&g, None).unwrap(), 1.0);
2208 for min_gain in [0.0, 1e-4] {
2209 let p = LeidenParams {
2210 n_iterations: None,
2211 min_gain,
2212 ..Default::default()
2213 };
2214 let run = |threads: usize| {
2215 rayon::ThreadPoolBuilder::new()
2216 .num_threads(threads)
2217 .build()
2218 .unwrap()
2219 .install(|| leiden_sparse_graph(&g, Some(p)).unwrap())
2220 };
2221 let one = run(1);
2222 assert_eq!(one, run(4), "min_gain {min_gain}");
2223 assert!(all_connected(&g, &one));
2224 let q = modularity(&g, &one, 1.0);
2225 assert!(q >= q_two - 1e-3, "min_gain {min_gain}: {q} vs {q_two}");
2226 }
2227 }
2228
2229 fn run_mode(g: &SparseGraph<f64>, mode: LocalMoving, refine: bool, seed: u64) -> Vec<usize> {
2231 let p = LeidenParams {
2232 refine,
2233 n_iterations: Some(if refine { 2 } else { 1 }),
2234 seed,
2235 local_moving: mode,
2236 ..Default::default()
2237 };
2238 community_detection(g, &p, usize::MAX, true).unwrap()
2239 }
2240
2241 #[test]
2244 fn test_parallel_local_moving_thread_count_invariant() {
2245 let edges = planted(20, 400, 8, 3, 11);
2246 let g = build_undirected(8000, &edges, false);
2247 for refine in [true, false] {
2248 let run = |threads: usize| {
2249 rayon::ThreadPoolBuilder::new()
2250 .num_threads(threads)
2251 .build()
2252 .unwrap()
2253 .install(|| run_mode(&g, LocalMoving::Coloured, refine, 3))
2254 };
2255 let one = run(1);
2256 assert_eq!(one, run(4), "refine {refine}");
2257 if refine {
2258 assert!(all_connected(&g, &one));
2259 }
2260 }
2261 }
2262
2263 #[test]
2266 fn test_parallel_local_moving_recovers_planted_blocks() {
2267 let edges = planted(8, 600, 10, 1, 7);
2268 let g = build_undirected(4800, &edges, false);
2269 assert!(g.get_node_number() >= LEIDEN_PAR_MIN_NODES);
2270 for refine in [true, false] {
2271 let labels = run_mode(&g, LocalMoving::Coloured, refine, 1);
2272 assert_eq!(n_communities(&labels), 8, "refine {refine}");
2273 for v in 0..4800 {
2274 assert_eq!(labels[v], labels[(v / 600) * 600], "refine {refine}");
2275 }
2276 }
2277 }
2278
2279 #[test]
2282 fn test_greedy_colouring_matches_first_fit() {
2283 let mut edges = planted(4, 100, 12, 2, 5);
2284 for u in 0..80 {
2285 for v in u + 1..80 {
2286 edges.push((u, v, 1.0));
2287 }
2288 }
2289 let g = build_undirected(400, &edges, false);
2290 let order: Vec<u32> = (150..400).chain(0..150).collect();
2291 let (classes, colour) = greedy_colouring(&g, &order);
2292
2293 let mut want = vec![u32::MAX; 400];
2294 for &v in &order {
2295 let taken: Vec<u32> = g
2296 .row(v as usize)
2297 .0
2298 .iter()
2299 .map(|&u| want[u as usize])
2300 .collect();
2301 want[v as usize] = (0..).find(|c| !taken.contains(c)).unwrap();
2302 }
2303 assert_eq!(colour, want);
2304 assert!(classes.len() >= 80);
2305 for (c, members) in classes.iter().enumerate() {
2306 assert!(members.is_sorted());
2307 assert!(members.iter().all(|&v| colour[v as usize] == c as u32));
2308 }
2309 }
2310
2311 #[test]
2314 fn test_parallel_local_moving_small_graph_uses_queue() {
2315 let g = karate();
2316 assert_eq!(
2317 run_mode(&g, LocalMoving::Coloured, true, 5),
2318 run_mode(&g, LocalMoving::Queue, true, 5)
2319 );
2320 }
2321
2322 #[test]
2325 fn test_parallel_local_moving_modularity_close_to_queue() {
2326 let edges = planted(20, 400, 8, 3, 11);
2327 let g = build_undirected(8000, &edges, false);
2328 for refine in [true, false] {
2329 let q_queue = modularity(&g, &run_mode(&g, LocalMoving::Queue, refine, 2), 1.0);
2330 let q = modularity(&g, &run_mode(&g, LocalMoving::Coloured, refine, 2), 1.0);
2331 assert!(
2332 q >= 0.99 * q_queue,
2333 "refine {refine}: {q} vs queue {q_queue}"
2334 );
2335 }
2336 }
2337
2338 #[test]
2340 fn test_leiden_same_seed_same_labels() {
2341 let edges = planted(10, 100, 6, 3, 3);
2342 let g = build_undirected(1000, &edges, false);
2343 let p = LeidenParams {
2344 seed: 5,
2345 ..Default::default()
2346 };
2347 assert_eq!(
2348 leiden_sparse_graph(&g, Some(p)).unwrap(),
2349 leiden_sparse_graph(&g, Some(p)).unwrap()
2350 );
2351 }
2352
2353 #[test]
2356 fn test_leiden_f32_matches_f64() {
2357 let edges = planted(6, 150, 6, 3, 9);
2358 let edges32: Vec<(usize, usize, f32)> =
2359 edges.iter().map(|&(u, v, w)| (u, v, w as f32)).collect();
2360 let g64 = build_undirected(900, &edges, false);
2361 let g32 = build_undirected(900, &edges32, false);
2362 assert_eq!(
2363 leiden_sparse_graph(&g64, None).unwrap(),
2364 leiden_sparse_graph(&g32, None).unwrap()
2365 );
2366 }
2367
2368 #[test]
2370 fn test_leiden_communities_connected() {
2371 let edges = planted(30, 60, 3, 2, 21);
2372 let g = build_undirected(1800, &edges, false);
2373 for seed in 0..5 {
2374 let p = LeidenParams {
2375 seed,
2376 ..Default::default()
2377 };
2378 let labels = leiden_sparse_graph(&g, Some(p)).unwrap();
2379 assert!(all_connected(&g, &labels), "seed {seed}");
2380 }
2381 }
2382
2383 #[test]
2384 fn test_leiden_directed_errors() {
2385 let edges: Vec<(usize, usize, f64)> =
2386 KARATE.chunks_exact(2).map(|e| (e[0], e[1], 1.0)).collect();
2387 let g = build_undirected(34, &edges, true);
2388 assert!(leiden_sparse_graph(&g, None).is_err());
2389 }
2390
2391 #[test]
2392 fn test_leiden_empty_and_edgeless() {
2393 let g = build_undirected::<f64>(0, &[], false);
2394 assert!(leiden_sparse_graph(&g, None).unwrap().is_empty());
2395 let g = build_undirected::<f64>(3, &[], false);
2396 assert_eq!(leiden_sparse_graph(&g, None).unwrap().len(), 3);
2397 }
2398}