1use crate::Snapshot;
74use crate::algo::{Components, tidy};
75use yo_common::Rng;
76
77pub const RESOLUTION: f64 = 1.0;
79
80const THETA: f64 = 0.01;
87
88const LEVELS: u32 = 64;
93
94const PASSES: u32 = 8;
101
102const SEED: u64 = 0x1ead_e401;
103
104#[must_use]
109pub fn leiden(g: &Snapshot) -> Components {
110 leiden_with(g, RESOLUTION)
111}
112
113#[must_use]
115pub fn leiden_with(g: &Snapshot, resolution: f64) -> Components {
116 unfold(g, resolution, true)
117}
118
119#[must_use]
125pub fn louvain(g: &Snapshot) -> Components {
126 louvain_with(g, RESOLUTION)
127}
128
129#[must_use]
131pub fn louvain_with(g: &Snapshot, resolution: f64) -> Components {
132 unfold(g, resolution, false)
133}
134
135#[must_use]
146pub fn modularity(g: &Snapshot, labels: &[u32]) -> f64 {
147 modularity_with(g, labels, RESOLUTION)
148}
149
150#[must_use]
156pub fn modularity_with(g: &Snapshot, labels: &[u32], resolution: f64) -> f64 {
157 let w = Weighted::of(g);
158 assert_eq!(labels.len(), w.nodes(), "one label a node");
159 let (labels, groups) = renumber(labels);
160 w.quality(&labels, groups, resolution)
161}
162
163#[derive(Clone)]
171struct Weighted {
172 at: Vec<u64>,
173 to: Vec<u32>,
174 w: Vec<f64>,
175 self_w: Vec<f64>,
177 strength: Vec<f64>,
179 total: f64,
181}
182
183impl Weighted {
184 fn of(g: &Snapshot) -> Weighted {
186 let n = g.nodes() as usize;
187 let mut at = vec![0u64; n + 1];
188 for node in 0..n {
189 let both = g.out_degree(node as u32) + g.in_degree(node as u32);
190 at[node + 1] = at[node] + u64::from(both);
191 }
192
193 let mut to = vec![0u32; at[n] as usize];
198 let mut self_w = vec![0f64; n];
199 let mut fill = at.clone();
200 for node in 0..n as u32 {
201 for other in g.out(node) {
202 if *other == node {
203 self_w[node as usize] += 1.0;
204 continue;
205 }
206 to[fill[node as usize] as usize] = *other;
207 fill[node as usize] += 1;
208 }
209 for other in g.into_(node) {
210 if *other == node {
211 continue;
212 }
213 to[fill[node as usize] as usize] = *other;
214 fill[node as usize] += 1;
215 }
216 }
217
218 let mut edges: Vec<(u32, f64)> = Vec::new();
221 let mut out = Vec::with_capacity(to.len());
222 let mut w = Vec::with_capacity(to.len());
223 let mut next = vec![0u64; n + 1];
224 for node in 0..n {
225 let mine = &mut to[at[node] as usize..fill[node] as usize];
226 mine.sort_unstable();
227 edges.clear();
228 for other in mine.iter() {
229 match edges.last_mut() {
230 Some((last, weight)) if last == other => *weight += 1.0,
231 _ => edges.push((*other, 1.0)),
232 }
233 }
234 for (other, weight) in &edges {
235 out.push(*other);
236 w.push(*weight);
237 }
238 next[node + 1] = out.len() as u64;
239 }
240
241 Weighted::new(next, out, w, self_w)
242 }
243
244 fn new(at: Vec<u64>, to: Vec<u32>, w: Vec<f64>, self_w: Vec<f64>) -> Weighted {
246 let n = self_w.len();
247 let mut strength = vec![0f64; n];
248 for node in 0..n {
249 let mine = at[node] as usize..at[node + 1] as usize;
250 strength[node] = w[mine].iter().sum::<f64>() + 2.0 * self_w[node];
251 }
252 let total = strength.iter().sum();
253 Weighted {
254 at,
255 to,
256 w,
257 self_w,
258 strength,
259 total,
260 }
261 }
262
263 fn nodes(&self) -> usize {
264 self.self_w.len()
265 }
266
267 fn near(&self, node: u32) -> (&[u32], &[f64]) {
269 let mine = self.at[node as usize] as usize..self.at[node as usize + 1] as usize;
270 (&self.to[mine.clone()], &self.w[mine])
271 }
272
273 fn quality(&self, of: &[u32], groups: usize, resolution: f64) -> f64 {
276 if self.total == 0.0 {
277 return 0.0;
278 }
279 let mut inside = vec![0f64; groups];
280 let mut tot = vec![0f64; groups];
281 for node in 0..self.nodes() {
282 let mine = of[node] as usize;
283 tot[mine] += self.strength[node];
284 inside[mine] += 2.0 * self.self_w[node];
285 let (near, w) = self.near(node as u32);
286 for (other, weight) in near.iter().zip(w) {
287 if of[*other as usize] as usize == mine {
288 inside[mine] += weight;
289 }
290 }
291 }
292 (0..groups)
293 .map(|c| inside[c] / self.total - resolution * (tot[c] / self.total).powi(2))
294 .sum()
295 }
296}
297
298fn unfold(g: &Snapshot, resolution: f64, refined: bool) -> Components {
308 let base = Weighted::of(g);
309 let n = base.nodes();
310 let mut answer: Vec<u32> = (0..n as u32).collect();
311 if n == 0 {
312 return tidy(answer);
313 }
314
315 let mut rng = Rng::new(SEED);
316 for _ in 0..PASSES {
317 let next = pass(&base, &answer, resolution, refined, &mut rng);
318 if next == answer {
319 break;
320 }
321 answer = next;
322 }
323 tidy(answer)
324}
325
326fn pass(base: &Weighted, start: &[u32], resolution: f64, refined: bool, rng: &mut Rng) -> Vec<u32> {
328 let n = base.nodes();
329 let mut answer = vec![0u32; n];
330 let mut w = base.clone();
331 let mut at: Vec<u32> = (0..n as u32).collect();
333 let (mut comm, _) = renumber(start);
334
335 for _ in 0..LEVELS {
336 local_move(&w, &mut comm, resolution, rng);
337 let (tidied, groups) = renumber(&comm);
338 for (node, at) in at.iter().enumerate() {
339 answer[node] = tidied[*at as usize];
340 }
341 if groups == w.nodes() {
344 break;
345 }
346
347 let split = if refined {
348 refine(&w, &tidied, groups, resolution, rng)
349 } else {
350 tidied.clone()
351 };
352 let (next, next_comm, moved) = aggregate(&w, &split, &tidied);
353 for at in &mut at {
354 *at = moved[*at as usize];
355 }
356 w = next;
357 comm = next_comm;
358 }
359 renumber(&answer).0
363}
364
365fn local_move(g: &Weighted, comm: &mut [u32], resolution: f64, rng: &mut Rng) {
374 let n = g.nodes();
375 if n == 0 || g.total == 0.0 {
376 return;
377 }
378 let mut tot = vec![0f64; n];
379 let mut size = vec![0u32; n];
380 for node in 0..n {
381 tot[comm[node] as usize] += g.strength[node];
382 size[comm[node] as usize] += 1;
383 }
384 let mut free: Vec<u32> = (0..n as u32).filter(|c| size[*c as usize] == 0).collect();
387
388 let mut queue: Vec<u32> = (0..n as u32).collect();
389 shuffle(&mut queue, rng);
390 let mut queued = vec![true; n];
391 let mut head = 0usize;
392
393 let mut link = vec![0f64; n];
395 let mut seen: Vec<u32> = Vec::new();
396
397 while head < queue.len() {
398 let node = queue[head];
399 head += 1;
400 queued[node as usize] = false;
401 let was = comm[node as usize];
402 let strength = g.strength[node as usize];
403
404 tot[was as usize] -= strength;
405 size[was as usize] -= 1;
406 if size[was as usize] == 0 {
407 free.push(was);
408 }
409
410 seen.clear();
411 let (near, w) = g.near(node);
412 for (other, weight) in near.iter().zip(w) {
413 let at = comm[*other as usize] as usize;
414 if link[at] == 0.0 {
415 seen.push(comm[*other as usize]);
416 }
417 link[at] += weight;
418 }
419
420 let value = |c: u32, link: &[f64]| {
423 link[c as usize] - resolution * strength * tot[c as usize] / g.total
424 };
425 let mut best = was;
426 let mut most = value(was, &link);
427 if most < 0.0 && size[was as usize] > 0 {
428 while let Some(empty) = free.pop() {
429 if size[empty as usize] == 0 {
430 (best, most) = (empty, 0.0);
431 free.push(empty);
432 break;
433 }
434 }
435 }
436 for c in &seen {
437 let worth = value(*c, &link);
438 if worth > most || (worth == most && *c < best) {
441 (best, most) = (*c, worth);
442 }
443 }
444 for c in &seen {
445 link[*c as usize] = 0.0;
446 }
447
448 comm[node as usize] = best;
449 tot[best as usize] += strength;
450 size[best as usize] += 1;
451 if best == was {
452 continue;
453 }
454 for other in near {
457 if comm[*other as usize] != best && !queued[*other as usize] {
458 queued[*other as usize] = true;
459 queue.push(*other);
460 }
461 }
462 }
463}
464
465fn refine(g: &Weighted, comm: &[u32], groups: usize, resolution: f64, rng: &mut Rng) -> Vec<u32> {
478 let n = g.nodes();
479 let mut refined: Vec<u32> = (0..n as u32).collect();
480 if g.total == 0.0 {
481 return refined;
482 }
483
484 let mut at = vec![0u32; groups + 1];
486 for c in comm {
487 at[*c as usize + 1] += 1;
488 }
489 for c in 0..groups {
490 at[c + 1] += at[c];
491 }
492 let mut member = vec![0u32; n];
493 let mut fill = at.clone();
494 for (node, c) in comm.iter().enumerate() {
495 member[fill[*c as usize] as usize] = node as u32;
496 fill[*c as usize] += 1;
497 }
498
499 let mut tot = g.strength.clone();
502 let mut out = vec![0f64; n];
503 let mut link = vec![0f64; n];
504 let mut seen: Vec<u32> = Vec::new();
505 let mut pick: Vec<(u32, f64)> = Vec::new();
506
507 let mut order: Vec<u32> = Vec::new();
508 for c in 0..groups {
509 let mine = &member[at[c] as usize..at[c + 1] as usize];
510 if mine.len() < 3 {
511 continue;
512 }
513 let whole: f64 = mine.iter().map(|node| g.strength[*node as usize]).sum();
514
515 for node in mine {
519 let (near, w) = g.near(*node);
520 out[*node as usize] = near
521 .iter()
522 .zip(w)
523 .filter(|(other, _)| comm[**other as usize] as usize == c)
524 .map(|(_, weight)| *weight)
525 .sum();
526 }
527
528 order.clear();
529 order.extend_from_slice(mine);
530 shuffle(&mut order, rng);
531 for node in &order {
532 let node = *node;
533 if refined[node as usize] != node || tot[node as usize] != g.strength[node as usize] {
536 continue;
537 }
538 let strength = g.strength[node as usize];
539 if out[node as usize] < resolution * strength * (whole - strength) / g.total {
540 continue;
541 }
542
543 seen.clear();
544 let (near, w) = g.near(node);
545 for (other, weight) in near.iter().zip(w) {
546 if comm[*other as usize] as usize != c {
547 continue;
548 }
549 let into = refined[*other as usize] as usize;
550 if into == node as usize {
551 continue;
552 }
553 if link[into] == 0.0 {
554 seen.push(refined[*other as usize]);
555 }
556 link[into] += weight;
557 }
558
559 pick.clear();
560 let mut top = f64::NEG_INFINITY;
561 for subset in &seen {
562 let there = tot[*subset as usize];
563 if out[*subset as usize] < resolution * there * (whole - there) / g.total {
564 continue;
565 }
566 let worth = link[*subset as usize] - resolution * strength * there / g.total;
567 if worth >= 0.0 {
568 top = top.max(worth);
569 pick.push((*subset, worth));
570 }
571 }
572
573 if !pick.is_empty() {
576 let mut sum = 0.0;
577 for (_, worth) in &mut pick {
578 *worth = ((*worth - top) / THETA).exp();
579 sum += *worth;
580 }
581 let mut want = uniform(rng) * sum;
582 let mut into = pick[pick.len() - 1].0;
583 for (subset, weight) in &pick {
584 want -= weight;
585 if want <= 0.0 {
586 into = *subset;
587 break;
588 }
589 }
590 refined[node as usize] = into;
591 tot[into as usize] += strength;
592 out[into as usize] += out[node as usize] - 2.0 * link[into as usize];
596 tot[node as usize] = 0.0;
597 }
598
599 for subset in &seen {
600 link[*subset as usize] = 0.0;
601 }
602 }
603 }
604 refined
605}
606
607fn aggregate(g: &Weighted, split: &[u32], comm: &[u32]) -> (Weighted, Vec<u32>, Vec<u32>) {
615 let (moved, n) = renumber(split);
616
617 let mut self_w = vec![0f64; n];
618 let mut edges: Vec<(u32, u32, f64)> = Vec::new();
619 for node in 0..g.nodes() {
620 let mine = moved[node];
621 self_w[mine as usize] += g.self_w[node];
622 let (near, w) = g.near(node as u32);
623 for (other, weight) in near.iter().zip(w) {
624 let theirs = moved[*other as usize];
625 if theirs == mine {
626 self_w[mine as usize] += weight / 2.0;
629 } else {
630 edges.push((mine, theirs, *weight));
631 }
632 }
633 }
634
635 edges.sort_unstable_by_key(|(from, to, _)| (*from, *to));
636 let mut at = vec![0u64; n + 1];
637 let mut to = Vec::new();
638 let mut w = Vec::new();
639 for (from, other, weight) in &edges {
640 match to.last() {
641 Some(last) if *last == *other && at[*from as usize + 1] == to.len() as u64 => {
642 *w.last_mut().expect("a weight") += weight;
643 }
644 _ => {
645 to.push(*other);
646 w.push(*weight);
647 at[*from as usize + 1] = to.len() as u64;
648 }
649 }
650 at[*from as usize + 1] = to.len() as u64;
651 }
652 for node in 0..n {
653 at[node + 1] = at[node + 1].max(at[node]);
654 }
655
656 let mut starts = vec![0u32; n];
658 for node in 0..g.nodes() {
659 starts[moved[node] as usize] = comm[node];
660 }
661 let (starts, _) = renumber(&starts);
662 (Weighted::new(at, to, w, self_w), starts, moved)
663}
664
665fn renumber(of: &[u32]) -> (Vec<u32>, usize) {
667 let mut seen = vec![u32::MAX; of.len()];
668 let mut next = 0u32;
669 let mut out = vec![0u32; of.len()];
670 for (node, at) in of.iter().enumerate() {
671 let seen = &mut seen[*at as usize];
672 if *seen == u32::MAX {
673 *seen = next;
674 next += 1;
675 }
676 out[node] = *seen;
677 }
678 (out, next as usize)
679}
680
681fn shuffle(order: &mut [u32], rng: &mut Rng) {
683 for at in (1..order.len()).rev() {
684 order.swap(at, (rng.next_u64() % (at as u64 + 1)) as usize);
685 }
686}
687
688fn uniform(rng: &mut Rng) -> f64 {
690 (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696 use crate::algo::{label_propagation, wcc};
697 use crate::graph::NO_PROPS;
698 use crate::{Graph, Snapshot};
699 use yo_common::Rng;
700
701 fn linked(edges: &[(u64, u64)]) -> Graph {
702 let mut g = Graph::new();
703 for (from, to) in edges {
704 g.link(*from, *to, 1, NO_PROPS).expect("an edge");
705 }
706 g
707 }
708
709 fn clique(first: u64, size: u64) -> Vec<(u64, u64)> {
710 let mut edges = Vec::new();
711 for a in first..first + size {
712 for b in a + 1..first + size {
713 edges.push((a, b));
714 }
715 }
716 edges
717 }
718
719 fn ring(groups: u64, size: u64) -> Vec<(u64, u64)> {
722 let mut edges = Vec::new();
723 for group in 0..groups {
724 edges.extend(clique(group * 1000, size));
725 }
726 for group in 0..groups {
727 edges.push((group * 1000, (group + 1) % groups * 1000 + 1));
728 }
729 edges
730 }
731
732 fn slow(g: &Snapshot, of: &[u32], resolution: f64) -> f64 {
735 let n = g.nodes() as usize;
736 let mut a = vec![vec![0f64; n]; n];
737 for node in 0..n as u32 {
738 for other in g.out(node) {
739 a[node as usize][*other as usize] += 1.0;
740 a[*other as usize][node as usize] += 1.0;
741 }
742 }
743 let degree: Vec<f64> = (0..n).map(|node| a[node].iter().sum()).collect();
744 let total: f64 = degree.iter().sum();
745 if total == 0.0 {
746 return 0.0;
747 }
748 let mut q = 0.0;
749 for i in 0..n {
750 for j in 0..n {
751 if of[i] == of[j] {
752 q += a[i][j] - resolution * degree[i] * degree[j] / total;
753 }
754 }
755 }
756 q / total
757 }
758
759 #[test]
760 fn the_measure_agrees_with_the_definition() {
761 let mut rng = Rng::new(0x9d1);
762 for case in 0..40 {
763 let nodes = 2 + rng.next_u64() % 30;
764 let edges: Vec<(u64, u64)> = (0..nodes * 2)
765 .map(|_| (rng.next_u64() % nodes, rng.next_u64() % nodes))
766 .collect();
767 let s = Snapshot::of(&linked(&edges));
768 let of: Vec<u32> = (0..s.nodes())
769 .map(|_| (rng.next_u64() % 3) as u32)
770 .collect();
771 for resolution in [0.5, 1.0, 2.0] {
772 let (mine, theirs) = (
773 modularity_with(&s, &of, resolution),
774 slow(&s, &of, resolution),
775 );
776 assert!(
777 (mine - theirs).abs() < 1e-9,
778 "case {case} at {resolution}, {mine} against {theirs}"
779 );
780 }
781 }
782 }
783
784 #[test]
785 fn the_measure_knows_a_good_split_from_a_bad_one() {
786 let s = Snapshot::of(&linked(&ring(4, 8)));
787 let good: Vec<u32> = (0..s.nodes()).map(|node| node / 8).collect();
788 let one = vec![0u32; s.nodes() as usize];
789 let each: Vec<u32> = (0..s.nodes()).collect();
790 assert!(modularity(&s, &good) > 0.6, "{}", modularity(&s, &good));
791 assert!((modularity(&s, &one)).abs() < 1e-9);
792 assert!(modularity(&s, &each) < 0.0);
793 }
794
795 #[test]
796 fn both_find_the_ring_of_cliques() {
797 let s = Snapshot::of(&linked(&ring(6, 10)));
798 for c in [leiden(&s), louvain(&s)] {
799 assert_eq!(c.count(), 6);
800 for group in 0..6u64 {
801 let a = s.dense(group * 1000 + 2).expect("a");
802 let b = s.dense(group * 1000 + 7).expect("b");
803 assert!(c.same(a, b), "group {group}");
804 }
805 }
806 }
807
808 #[test]
809 fn both_beat_label_propagation_for_modularity() {
810 let s = Snapshot::of(&linked(&ring(8, 6)));
811 let quick = modularity(&s, label_propagation(&s).labels());
812 for c in [leiden(&s), louvain(&s)] {
813 assert!(modularity(&s, c.labels()) >= quick - 1e-9);
814 }
815 }
816
817 #[test]
820 fn leiden_communities_are_never_disconnected() {
821 let mut rng = Rng::new(0x1ead);
822 for case in 0..30 {
823 let nodes = 10 + rng.next_u64() % 90;
824 let edges: Vec<(u64, u64)> = (0..nodes * 3)
825 .map(|_| (rng.next_u64() % nodes, rng.next_u64() % nodes))
826 .collect();
827 let s = Snapshot::of(&linked(&edges));
828 let c = leiden(&s);
829 assert!(connected(&s, c.labels()), "case {case}");
830 }
831 }
832
833 fn connected(g: &Snapshot, of: &[u32]) -> bool {
836 let n = g.nodes() as usize;
837 let mut seen = vec![false; n];
838 let mut groups = std::collections::HashSet::new();
839 for node in 0..n {
840 if seen[node] || !groups.insert(of[node]) {
841 if !seen[node] {
842 return false;
843 }
844 continue;
845 }
846 let mut todo = vec![node as u32];
847 seen[node] = true;
848 while let Some(at) = todo.pop() {
849 for other in g.out(at).iter().chain(g.into_(at)) {
850 if of[*other as usize] == of[node] && !seen[*other as usize] {
851 seen[*other as usize] = true;
852 todo.push(*other);
853 }
854 }
855 }
856 }
857 true
858 }
859
860 #[test]
861 fn a_community_never_crosses_a_component() {
862 let mut rng = Rng::new(0x1ea0);
863 for case in 0..30 {
864 let nodes = 2 + rng.next_u64() % 50;
865 let edges: Vec<(u64, u64)> = (0..nodes)
866 .map(|_| (rng.next_u64() % nodes, rng.next_u64() % nodes))
867 .collect();
868 let s = Snapshot::of(&linked(&edges));
869 let weak = wcc(&s);
870 for c in [leiden(&s), louvain(&s)] {
871 for node in 0..s.nodes() {
872 for other in 0..s.nodes() {
873 if c.same(node, other) {
874 assert!(weak.same(node, other), "case {case}");
875 }
876 }
877 }
878 }
879 }
880 }
881
882 #[test]
883 fn one_clique_is_one_community() {
884 let s = Snapshot::of(&linked(&clique(0, 15)));
885 assert_eq!(leiden(&s).count(), 1);
886 assert_eq!(louvain(&s).count(), 1);
887 }
888
889 #[test]
890 fn a_higher_resolution_cuts_finer() {
891 let s = Snapshot::of(&linked(&ring(4, 12)));
892 let coarse = leiden_with(&s, 0.25).count();
893 let plain = leiden_with(&s, 1.0).count();
894 let fine = leiden_with(&s, 6.0).count();
895 assert!(coarse <= plain, "{coarse} against {plain}");
896 assert!(fine > plain, "{fine} against {plain}");
897 }
898
899 #[test]
900 fn nothing_at_all() {
901 for c in [leiden(&Snapshot::default()), louvain(&Snapshot::default())] {
902 assert_eq!(c.count(), 0);
903 assert!(c.is_empty());
904 }
905 assert_eq!(modularity(&Snapshot::default(), &[]), 0.0);
906 }
907
908 #[test]
909 fn a_graph_with_no_edges_is_all_singletons() {
910 let mut g = Graph::new();
911 for id in 0..6u64 {
912 g.add_node(id).expect("a node");
913 }
914 let s = Snapshot::of(&g);
915 assert_eq!(leiden(&s).count(), 6);
916 assert_eq!(louvain(&s).count(), 6);
917 }
918
919 #[test]
920 fn a_self_loop_does_not_break_the_measure() {
921 let s = Snapshot::of(&linked(&[(1, 1), (1, 2), (2, 3), (3, 1)]));
925 for c in [leiden(&s), louvain(&s)] {
926 assert!(modularity(&s, c.labels()).abs() < 1e-9);
927 }
928 }
929
930 #[test]
931 fn two_runs_agree() {
932 let s = Snapshot::of(&linked(&ring(5, 9)));
933 assert_eq!(leiden(&s).labels(), leiden(&s).labels());
934 assert_eq!(louvain(&s).labels(), louvain(&s).labels());
935 }
936
937 #[test]
938 fn direction_does_not_matter() {
939 let edges = ring(4, 8);
940 let forward = Snapshot::of(&linked(&edges));
941 let flipped: Vec<(u64, u64)> = edges.iter().map(|(a, b)| (*b, *a)).collect();
942 let back = Snapshot::of(&linked(&flipped));
943 assert_eq!(leiden(&forward).labels(), leiden(&back).labels());
944 }
945
946 #[test]
948 fn no_single_node_move_helps() {
949 let mut rng = Rng::new(0x1ea2);
950 for case in 0..15 {
951 let nodes = 20 + rng.next_u64() % 40;
952 let edges: Vec<(u64, u64)> = (0..nodes * 4)
953 .map(|_| (rng.next_u64() % nodes, rng.next_u64() % nodes))
954 .collect();
955 let s = Snapshot::of(&linked(&edges));
956 for c in [leiden(&s), louvain(&s)] {
957 let mut of = c.labels().to_vec();
958 let now = modularity(&s, &of);
959 for node in 0..s.nodes() {
960 let was = of[node as usize];
961 for other in c.labels() {
962 of[node as usize] = *other;
963 let then = modularity(&s, &of);
964 assert!(then <= now + 1e-9, "case {case}, node {node}");
965 }
966 of[node as usize] = was;
967 }
968 }
969 }
970 }
971
972 #[test]
974 fn the_labels_are_tidy() {
975 let s = Snapshot::of(&linked(&ring(4, 7)));
976 let c = leiden(&s);
977 for node in 0..s.nodes() {
978 assert_eq!(c.of(c.of(node)), c.of(node));
979 assert!(c.of(node) <= node);
980 }
981 }
982}