1#![allow(
64 clippy::cast_possible_truncation,
65 clippy::cast_precision_loss,
66 clippy::cast_sign_loss,
67 clippy::many_single_char_names,
68 clippy::needless_range_loop,
69 clippy::ptr_arg,
70 clippy::too_many_arguments,
71 clippy::too_many_lines,
72 clippy::unnecessary_wraps
73)]
74
75use crate::core::graph::EdgeId;
76use crate::core::{Graph, IgraphError, IgraphResult};
77
78const MAX_LEVELS_PER_ITERATION: usize = 64;
82
83pub const LEIDEN_DEFAULT_BETA: f64 = 0.01;
86
87pub const LEIDEN_DEFAULT_ITERATIONS: i32 = 2;
90
91#[derive(Debug, Copy, Clone, Eq, PartialEq)]
94pub enum LeidenObjective {
95 Modularity,
99 Cpm,
103 Er,
106}
107
108#[derive(Debug, Clone)]
112pub struct LeidenOptions {
113 pub objective: LeidenObjective,
115 pub resolution: f64,
119 pub beta: f64,
121 pub n_iterations: i32,
124 pub seed: u64,
127 pub start: Option<Vec<u32>>,
130}
131
132impl Default for LeidenOptions {
133 fn default() -> Self {
134 Self {
135 objective: LeidenObjective::Modularity,
136 resolution: 1.0,
137 beta: LEIDEN_DEFAULT_BETA,
138 n_iterations: LEIDEN_DEFAULT_ITERATIONS,
139 seed: 0,
140 start: None,
141 }
142 }
143}
144
145#[derive(Debug, Clone)]
147pub struct LeidenResult {
148 pub membership: Vec<u32>,
150 pub quality: f64,
152 pub nb_clusters: u32,
154 pub n_iterations_run: u32,
156 pub qualities: Vec<f64>,
158}
159
160pub fn leiden(graph: &Graph) -> IgraphResult<LeidenResult> {
187 leiden_with_options(graph, None, &LeidenOptions::default())
188}
189
190pub fn leiden_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<LeidenResult> {
215 leiden_with_options(graph, Some(weights), &LeidenOptions::default())
216}
217
218pub fn leiden_with_options(
249 graph: &Graph,
250 weights: Option<&[f64]>,
251 opts: &LeidenOptions,
252) -> IgraphResult<LeidenResult> {
253 if graph.is_directed() {
254 return Err(IgraphError::Unsupported(
255 "leiden currently supports undirected graphs only",
256 ));
257 }
258 if !opts.resolution.is_finite() || opts.resolution < 0.0 {
259 return Err(IgraphError::InvalidArgument(
260 "resolution parameter must be non-negative and finite".to_string(),
261 ));
262 }
263 if !opts.beta.is_finite() || opts.beta < 0.0 {
264 return Err(IgraphError::InvalidArgument(
265 "beta parameter must be non-negative and finite".to_string(),
266 ));
267 }
268 validate_weights(graph, weights, opts.objective)?;
269
270 let n = graph.vcount() as usize;
271
272 if n == 0 {
273 return Ok(LeidenResult {
274 membership: Vec::new(),
275 quality: 0.0,
276 nb_clusters: 0,
277 n_iterations_run: 0,
278 qualities: Vec::new(),
279 });
280 }
281
282 let mut membership: Vec<u32> = if let Some(start) = opts.start.as_deref() {
284 if start.len() != n {
285 return Err(IgraphError::InvalidArgument(format!(
286 "start membership length ({}) differs from vcount ({n})",
287 start.len()
288 )));
289 }
290 let mut m = start.to_vec();
291 reindex_membership(&mut m);
293 m
294 } else {
295 (0..n as u32).collect()
296 };
297
298 let edge_weights: Vec<f64> = match weights {
301 Some(w) => w.to_vec(),
302 None => vec![1.0; graph.ecount()],
303 };
304 let (eff_resolution, vertex_weights) =
305 derive_objective_params(graph, &edge_weights, opts.objective, opts.resolution)?;
306
307 let mut rng = SplitMix64::new(opts.seed);
308
309 let mut qualities: Vec<f64> = Vec::new();
310 let mut n_iterations_run: u32 = 0;
311 let mut changed = true;
312
313 let max_iter = if opts.n_iterations < 0 {
314 u32::MAX
315 } else {
316 u32::try_from(opts.n_iterations).unwrap_or(u32::MAX)
317 };
318
319 for _itr in 0..max_iter {
320 if opts.n_iterations < 0 && !changed {
321 break;
322 }
323 changed = false;
324 community_leiden_pass(
325 graph,
326 &edge_weights,
327 &vertex_weights,
328 eff_resolution,
329 opts.beta,
330 &mut membership,
331 &mut rng,
332 &mut changed,
333 )?;
334 n_iterations_run += 1;
335 let q = compute_quality(
336 graph,
337 &edge_weights,
338 &vertex_weights,
339 &membership,
340 eff_resolution,
341 );
342 qualities.push(q);
343 }
344
345 let nb_clusters = reindex_membership(&mut membership);
346 let quality = compute_quality(
347 graph,
348 &edge_weights,
349 &vertex_weights,
350 &membership,
351 eff_resolution,
352 );
353
354 Ok(LeidenResult {
355 membership,
356 quality,
357 nb_clusters,
358 n_iterations_run,
359 qualities,
360 })
361}
362
363fn derive_objective_params(
370 graph: &Graph,
371 edge_weights: &[f64],
372 objective: LeidenObjective,
373 user_resolution: f64,
374) -> IgraphResult<(f64, Vec<f64>)> {
375 let n = graph.vcount() as usize;
376 let m = graph.ecount();
377
378 match objective {
379 LeidenObjective::Modularity => {
380 let mut strength = vec![0.0_f64; n];
383 for e in 0..m {
384 let e_id = u32::try_from(e).map_err(|_| {
385 IgraphError::InvalidArgument("edge count exceeds u32::MAX".into())
386 })?;
387 let (u, v) = graph.edge(e_id as EdgeId)?;
388 let w = edge_weights[e];
389 strength[u as usize] += w;
390 if u != v {
391 strength[v as usize] += w;
392 }
393 }
394 let total: f64 = strength.iter().sum();
395 if total <= 0.0 {
396 return Ok((user_resolution, strength));
399 }
400 Ok((user_resolution / total, strength))
402 }
403 LeidenObjective::Cpm => Ok((user_resolution, vec![1.0; n])),
404 LeidenObjective::Er => {
405 let total: f64 = edge_weights.iter().sum();
407 if n == 0 {
408 return Ok((user_resolution, Vec::new()));
409 }
410 let denom = (n as f64) * (n as f64 + 1.0) * 0.5;
411 let p = if denom > 0.0 { total / denom } else { 0.0 };
412 Ok((user_resolution * p, vec![1.0; n]))
413 }
414 }
415}
416
417struct LeidenLevel {
431 n: usize,
432 adj: Vec<Vec<(u32, f64)>>,
434 loop_weight: Vec<f64>,
436 vertex_weight: Vec<f64>,
439}
440
441fn build_level_from_graph(
442 graph: &Graph,
443 edge_weights: &[f64],
444 vertex_weights: &[f64],
445) -> LeidenLevel {
446 let n = graph.vcount() as usize;
447 let mut adj: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
448 let mut loop_weight = vec![0.0_f64; n];
449 let m = graph.ecount();
450 for e in 0..m {
451 let Ok((u, v)) = graph.edge(e as EdgeId) else {
454 continue;
455 };
456 let w = edge_weights[e];
457 if u == v {
458 loop_weight[u as usize] += w;
459 } else {
460 adj[u as usize].push((v, w));
461 adj[v as usize].push((u, w));
462 }
463 }
464 LeidenLevel {
465 n,
466 adj,
467 loop_weight,
468 vertex_weight: vertex_weights.to_vec(),
469 }
470}
471
472fn build_level_from_edges(
473 n: usize,
474 edges: &[(u32, u32, f64)],
475 vertex_weights: Vec<f64>,
476) -> LeidenLevel {
477 let mut adj: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
478 let mut loop_weight = vec![0.0_f64; n];
479 for &(u, v, w) in edges {
480 if u == v {
481 loop_weight[u as usize] += w;
482 } else {
483 adj[u as usize].push((v, w));
484 adj[v as usize].push((u, w));
485 }
486 }
487 LeidenLevel {
488 n,
489 adj,
490 loop_weight,
491 vertex_weight: vertex_weights,
492 }
493}
494
495fn community_leiden_pass(
500 graph: &Graph,
501 edge_weights: &[f64],
502 vertex_weights: &[f64],
503 resolution: f64,
504 beta: f64,
505 membership: &mut Vec<u32>,
506 rng: &mut SplitMix64,
507 changed: &mut bool,
508) -> IgraphResult<()> {
509 let n = graph.vcount() as usize;
510 if n == 0 {
511 return Ok(());
512 }
513
514 let mut level = build_level_from_graph(graph, edge_weights, vertex_weights);
516
517 let mut aggregate_vertex: Vec<u32> = (0..n as u32).collect();
520
521 let mut work_membership: Vec<u32> = membership.clone();
524
525 reindex_membership(&mut work_membership);
528
529 for level_idx in 0..MAX_LEVELS_PER_ITERATION {
530 let (nb_clusters, level_changed) =
532 leiden_fastmove_vertices(&level, resolution, &mut work_membership, rng);
533 if level_changed {
534 *changed = true;
535 }
536
537 let continue_clustering = (nb_clusters as usize) < level.n;
539 if !continue_clustering {
540 break;
541 }
542
543 if level_idx > 0 {
548 for i in 0..n {
549 let v_agg = aggregate_vertex[i] as usize;
550 membership[i] = work_membership[v_agg];
551 }
552 } else {
553 membership.copy_from_slice(&work_membership);
555 }
556
557 let mut refined_membership: Vec<u32> = vec![0; level.n];
559 let mut nb_refined_clusters: usize = 0;
560
561 let mut clusters: Vec<Vec<u32>> = vec![Vec::new(); nb_clusters as usize];
563 for v in 0..level.n {
564 let c = work_membership[v] as usize;
565 clusters[c].push(v as u32);
566 }
567
568 for (c, vertex_subset) in clusters.iter().enumerate() {
569 leiden_merge_vertices(
570 &level,
571 vertex_subset,
572 &work_membership,
573 c as u32,
574 resolution,
575 beta,
576 &mut nb_refined_clusters,
577 &mut refined_membership,
578 rng,
579 );
580 }
581
582 if nb_refined_clusters >= level.n {
586 refined_membership.copy_from_slice(&work_membership);
587 nb_refined_clusters = nb_clusters as usize;
588 }
589
590 for i in 0..n {
592 let v_agg = aggregate_vertex[i] as usize;
593 aggregate_vertex[i] = refined_membership[v_agg];
594 }
595
596 let (next_level, next_membership) = leiden_aggregate(
598 &level,
599 &work_membership,
600 &refined_membership,
601 nb_refined_clusters,
602 );
603
604 level = next_level;
605 work_membership = next_membership;
606 reindex_membership(&mut work_membership);
607 }
608
609 if level.n == n {
614 membership.copy_from_slice(&work_membership);
615 } else {
616 for i in 0..n {
617 let v_agg = aggregate_vertex[i] as usize;
618 membership[i] = work_membership[v_agg];
619 }
620 }
621
622 reindex_membership(membership);
624 Ok(())
625}
626
627fn leiden_fastmove_vertices(
632 level: &LeidenLevel,
633 resolution: f64,
634 membership: &mut [u32],
635 rng: &mut SplitMix64,
636) -> (u32, bool) {
637 let n = level.n;
638 if n == 0 {
639 return (0, false);
640 }
641
642 let mut cluster_weight = vec![0.0_f64; n];
644 let mut cluster_size: Vec<u32> = vec![0; n];
645 for v in 0..n {
646 let c = membership[v] as usize;
647 cluster_weight[c] += level.vertex_weight[v];
648 cluster_size[c] += 1;
649 }
650
651 let mut empty_clusters: Vec<u32> = Vec::new();
654 for c in 0..n {
655 if cluster_size[c] == 0 {
656 empty_clusters.push(c as u32);
657 }
658 }
659
660 let mut order: Vec<u32> = (0..n as u32).collect();
662 if n > 1 {
663 shuffle_in_place(&mut order, rng);
664 }
665 let mut queue: std::collections::VecDeque<u32> = order.into_iter().collect();
666 let mut stable: Vec<bool> = vec![false; n];
667
668 let mut edge_weight_per_cluster = vec![0.0_f64; n];
672 let mut neighbor_clusters: Vec<u32> = Vec::new();
673 let mut cluster_touched: Vec<bool> = vec![false; n];
674
675 let mut changed = false;
676
677 while let Some(v) = queue.pop_front() {
678 let v_idx = v as usize;
679 let weight_v = level.vertex_weight[v_idx];
680 let current_cluster = membership[v_idx] as usize;
681
682 cluster_weight[current_cluster] -= weight_v;
684 cluster_size[current_cluster] -= 1;
685 if cluster_size[current_cluster] == 0 {
686 empty_clusters.push(current_cluster as u32);
687 }
688
689 neighbor_clusters.clear();
692 let empty_top = empty_clusters.last().copied();
693 if let Some(top) = empty_top {
694 let top_us = top as usize;
695 if !cluster_touched[top_us] {
696 cluster_touched[top_us] = true;
697 neighbor_clusters.push(top);
698 }
699 }
700
701 for &(u, w) in &level.adj[v_idx] {
703 let u_us = u as usize;
704 if u_us == v_idx {
705 continue;
706 }
707 let c = membership[u_us];
708 let c_us = c as usize;
709 if !cluster_touched[c_us] {
710 cluster_touched[c_us] = true;
711 neighbor_clusters.push(c);
712 }
713 edge_weight_per_cluster[c_us] += w;
714 }
715
716 let mut best_cluster = current_cluster as u32;
718 let mut max_diff = edge_weight_per_cluster[current_cluster]
719 - weight_v * cluster_weight[current_cluster] * resolution;
720
721 for &c in &neighbor_clusters {
722 let c_us = c as usize;
723 let diff = edge_weight_per_cluster[c_us] - weight_v * cluster_weight[c_us] * resolution;
724 if diff > max_diff {
725 best_cluster = c;
726 max_diff = diff;
727 }
728 }
729
730 for &c in &neighbor_clusters {
732 let c_us = c as usize;
733 edge_weight_per_cluster[c_us] = 0.0;
734 cluster_touched[c_us] = false;
735 }
736
737 let best_us = best_cluster as usize;
738
739 cluster_weight[best_us] += weight_v;
741 cluster_size[best_us] += 1;
742 if empty_top == Some(best_cluster) && cluster_size[best_us] == 1 {
743 empty_clusters.pop();
744 }
745
746 stable[v_idx] = true;
748
749 if best_us != current_cluster {
750 changed = true;
751 membership[v_idx] = best_cluster;
752 for &(u, _) in &level.adj[v_idx] {
755 let u_us = u as usize;
756 if stable[u_us] && membership[u_us] as usize != best_us {
757 queue.push_back(u);
758 stable[u_us] = false;
759 }
760 }
761 }
762 }
763
764 let nb_clusters = reindex_membership(membership);
765 (nb_clusters, changed)
766}
767
768fn leiden_merge_vertices(
773 level: &LeidenLevel,
774 vertex_subset: &[u32],
775 parent_membership: &[u32],
776 parent_cluster: u32,
777 resolution: f64,
778 beta: f64,
779 nb_refined_clusters: &mut usize,
780 refined_membership: &mut [u32],
781 rng: &mut SplitMix64,
782) {
783 let n_sub = vertex_subset.len();
784 if n_sub == 0 {
785 return;
786 }
787 if n_sub == 1 {
788 refined_membership[vertex_subset[0] as usize] = (*nb_refined_clusters) as u32;
791 *nb_refined_clusters += 1;
792 return;
793 }
794
795 let mut sub_cluster_weight = vec![0.0_f64; n_sub];
797 let mut sub_nb_vertices: Vec<u32> = vec![0; n_sub];
798 let mut external_edge_weight = vec![0.0_f64; n_sub];
799 let mut total_subset_weight = 0.0_f64;
800
801 for (i, &v) in vertex_subset.iter().enumerate() {
806 let v_us = v as usize;
807 refined_membership[v_us] = i as u32;
808 sub_cluster_weight[i] += level.vertex_weight[v_us];
809 total_subset_weight += level.vertex_weight[v_us];
810 sub_nb_vertices[i] += 1;
811
812 for &(u, w) in &level.adj[v_us] {
813 let u_us = u as usize;
814 if u_us != v_us && parent_membership[u_us] == parent_cluster {
815 external_edge_weight[i] += w;
816 }
817 }
818 }
819
820 let mut order: Vec<u32> = vertex_subset.to_vec();
822 if n_sub > 1 {
823 shuffle_in_place(&mut order, rng);
824 }
825
826 let mut non_singleton: Vec<bool> = vec![false; n_sub];
827 let mut edge_weight_per_local = vec![0.0_f64; n_sub];
828 let mut neighbor_locals: Vec<u32> = Vec::new();
829 let mut local_touched: Vec<bool> = vec![false; n_sub];
830 let mut cum_trans_diff = vec![0.0_f64; n_sub];
831
832 for &v in &order {
833 let v_us = v as usize;
834 let current = refined_membership[v_us] as usize;
835
836 if non_singleton[current] {
839 continue;
840 }
841
842 let vw_prod =
845 sub_cluster_weight[current] * (total_subset_weight - sub_cluster_weight[current]);
846 if external_edge_weight[current] < vw_prod * resolution {
847 continue;
848 }
849
850 sub_cluster_weight[current] = 0.0;
852 sub_nb_vertices[current] = 0;
853
854 neighbor_locals.clear();
857 if !local_touched[current] {
858 local_touched[current] = true;
859 neighbor_locals.push(current as u32);
860 }
861 for &(u, w) in &level.adj[v_us] {
862 let u_us = u as usize;
863 if u_us != v_us && parent_membership[u_us] == parent_cluster {
864 let c = refined_membership[u_us];
865 let c_us = c as usize;
866 if !local_touched[c_us] {
867 local_touched[c_us] = true;
868 neighbor_locals.push(c);
869 }
870 edge_weight_per_local[c_us] += w;
871 }
872 }
873
874 let weight_v = level.vertex_weight[v_us];
876 let mut best_cluster = current as u32;
877 let mut max_diff = 0.0_f64;
878 let mut total_cum = 0.0_f64;
879
880 for (j, &c) in neighbor_locals.iter().enumerate() {
881 let c_us = c as usize;
882 let vw_prod_c =
883 sub_cluster_weight[c_us] * (total_subset_weight - sub_cluster_weight[c_us]);
884 if external_edge_weight[c_us] >= vw_prod_c * resolution {
885 let diff =
886 edge_weight_per_local[c_us] - weight_v * sub_cluster_weight[c_us] * resolution;
887 if diff > max_diff {
888 best_cluster = c;
889 max_diff = diff;
890 }
891 if diff >= 0.0 {
892 if beta > 0.0 {
893 total_cum += (diff / beta).exp();
894 } else if diff > 0.0 {
895 total_cum = f64::INFINITY;
898 }
899 }
900 }
901 cum_trans_diff[j] = total_cum;
902 }
903
904 let chosen_cluster = if total_cum.is_finite() && total_cum > 0.0 {
906 let r = rng.next_f64() * total_cum;
907 let mut idx = neighbor_locals.len() - 1;
909 for (j, &cum) in cum_trans_diff
910 .iter()
911 .enumerate()
912 .take(neighbor_locals.len())
913 {
914 if cum >= r {
915 idx = j;
916 break;
917 }
918 }
919 neighbor_locals[idx]
920 } else if total_cum == 0.0 {
921 current as u32
923 } else {
924 best_cluster
925 };
926
927 let chosen_us = chosen_cluster as usize;
928
929 sub_cluster_weight[chosen_us] += weight_v;
931 sub_nb_vertices[chosen_us] += 1;
932
933 for &(u, w) in &level.adj[v_us] {
937 let u_us = u as usize;
938 if parent_membership[u_us] == parent_cluster {
939 if refined_membership[u_us] as usize == chosen_us {
940 external_edge_weight[chosen_us] -= w;
941 } else {
942 external_edge_weight[chosen_us] += w;
943 }
944 }
945 }
946
947 if chosen_us != current {
948 refined_membership[v_us] = chosen_cluster;
949 non_singleton[chosen_us] = true;
950 }
951
952 for &c in &neighbor_locals {
954 let c_us = c as usize;
955 edge_weight_per_local[c_us] = 0.0;
956 local_touched[c_us] = false;
957 }
958 }
959
960 let base = *nb_refined_clusters;
963 let mut local_to_global: Vec<u32> = vec![u32::MAX; n_sub];
965 let mut next_global = base;
966 for &v in vertex_subset {
967 let v_us = v as usize;
968 let local = refined_membership[v_us] as usize;
969 if local_to_global[local] == u32::MAX {
970 local_to_global[local] = next_global as u32;
971 next_global += 1;
972 }
973 refined_membership[v_us] = local_to_global[local];
974 }
975 *nb_refined_clusters = next_global;
976}
977
978fn leiden_aggregate(
986 level: &LeidenLevel,
987 parent_membership: &[u32],
988 refined_membership: &[u32],
989 nb_refined: usize,
990) -> (LeidenLevel, Vec<u32>) {
991 let k = nb_refined;
992
993 let mut inter: Vec<Vec<(u32, f64)>> = vec![Vec::new(); k];
998 let mut loops: Vec<f64> = vec![0.0_f64; k];
999 let mut agg_vertex_weight: Vec<f64> = vec![0.0_f64; k];
1000 let mut new_membership: Vec<u32> = vec![0; k];
1003
1004 for v in 0..level.n {
1005 let c = refined_membership[v] as usize;
1006 agg_vertex_weight[c] += level.vertex_weight[v];
1007 new_membership[c] = parent_membership[v];
1008
1009 let lw = level.loop_weight[v];
1011 if lw > 0.0 {
1012 loops[c] += lw;
1013 }
1014
1015 for &(u, w) in &level.adj[v] {
1016 let u_us = u as usize;
1017 if u_us <= v {
1020 continue;
1021 }
1022 let c2 = refined_membership[u_us] as usize;
1023 if c == c2 {
1024 loops[c] += w;
1025 } else {
1026 let (a, b) = if c < c2 { (c, c2) } else { (c2, c) };
1027 push_or_merge(&mut inter[a], b as u32, w);
1028 }
1029 }
1030 }
1031
1032 let mut edges: Vec<(u32, u32, f64)> = Vec::new();
1034 for c in 0..k {
1035 if loops[c] > 0.0 {
1036 edges.push((c as u32, c as u32, loops[c]));
1037 }
1038 for &(c2, w) in &inter[c] {
1039 edges.push((c as u32, c2, w));
1040 }
1041 }
1042
1043 let next_level = build_level_from_edges(k, &edges, agg_vertex_weight);
1044 (next_level, new_membership)
1045}
1046
1047fn push_or_merge(list: &mut Vec<(u32, f64)>, neighbour: u32, weight: f64) {
1048 for slot in list.iter_mut() {
1049 if slot.0 == neighbour {
1050 slot.1 += weight;
1051 return;
1052 }
1053 }
1054 list.push((neighbour, weight));
1055}
1056
1057fn compute_quality(
1064 graph: &Graph,
1065 edge_weights: &[f64],
1066 vertex_weights: &[f64],
1067 membership: &[u32],
1068 resolution: f64,
1069) -> f64 {
1070 let n = graph.vcount() as usize;
1071 let m = graph.ecount();
1072 if n == 0 {
1073 return 0.0;
1074 }
1075
1076 let mut q = 0.0_f64;
1077 let mut total_edge_weight = 0.0_f64;
1078 for e in 0..m {
1079 let Ok((u, v)) = graph.edge(e as EdgeId) else {
1080 continue;
1081 };
1082 let w = edge_weights[e];
1083 total_edge_weight += w;
1084 if membership[u as usize] == membership[v as usize] {
1085 q += 2.0 * w;
1087 }
1088 }
1089
1090 let k = membership.iter().copied().max().map_or(0, |m| m + 1) as usize;
1092 let mut cluster_weight = vec![0.0_f64; k];
1093 for v in 0..n {
1094 cluster_weight[membership[v] as usize] += vertex_weights[v];
1095 }
1096 for c in 0..k {
1097 q -= resolution * cluster_weight[c] * cluster_weight[c];
1098 }
1099
1100 if total_edge_weight <= 0.0 {
1101 return 0.0;
1102 }
1103 q / (2.0 * total_edge_weight)
1104}
1105
1106fn reindex_membership(membership: &mut [u32]) -> u32 {
1113 if membership.is_empty() {
1114 return 0;
1115 }
1116 let max = *membership.iter().max().unwrap_or(&0);
1117 let mut remap: Vec<i64> = vec![-1; max as usize + 1];
1118 let mut next: u32 = 0;
1119 for slot in membership.iter_mut() {
1120 let old = *slot as usize;
1121 if remap[old] < 0 {
1122 remap[old] = i64::from(next);
1123 next += 1;
1124 }
1125 *slot = remap[old] as u32;
1126 }
1127 next
1128}
1129
1130fn validate_weights(
1135 graph: &Graph,
1136 weights: Option<&[f64]>,
1137 objective: LeidenObjective,
1138) -> IgraphResult<()> {
1139 let Some(w) = weights else {
1140 return Ok(());
1141 };
1142 let m = graph.ecount();
1143 if w.len() != m {
1144 return Err(IgraphError::InvalidArgument(format!(
1145 "weights vector size ({}) differs from edge count ({m})",
1146 w.len(),
1147 )));
1148 }
1149 let allow_negative = matches!(objective, LeidenObjective::Cpm);
1150 for (e, &v) in w.iter().enumerate() {
1151 if !v.is_finite() {
1152 return Err(IgraphError::InvalidArgument(format!(
1153 "weight at edge {e} is not finite ({v})"
1154 )));
1155 }
1156 if !allow_negative && v < 0.0 {
1157 return Err(IgraphError::InvalidArgument(format!(
1158 "weight at edge {e} is negative ({v}); leiden with this objective requires non-negative weights"
1159 )));
1160 }
1161 }
1162 Ok(())
1163}
1164
1165struct SplitMix64(u64);
1170
1171impl SplitMix64 {
1172 fn new(seed: u64) -> Self {
1173 Self(seed.wrapping_add(0x9E37_79B9_7F4A_7C15))
1175 }
1176 fn next_u64(&mut self) -> u64 {
1177 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
1178 let mut z = self.0;
1179 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1180 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1181 z ^ (z >> 31)
1182 }
1183 fn gen_index(&mut self, bound: usize) -> usize {
1184 debug_assert!(bound > 0);
1185 let r = self.next_u64() % (bound as u64);
1186 usize::try_from(r).unwrap_or(0)
1187 }
1188 fn next_f64(&mut self) -> f64 {
1190 ((self.next_u64() >> 11) as f64) * (1.0 / ((1u64 << 53) as f64))
1191 }
1192}
1193
1194fn shuffle_in_place<T>(slice: &mut [T], rng: &mut SplitMix64) {
1195 let len = slice.len();
1196 for i in (1..len).rev() {
1197 let j = rng.gen_index(i + 1);
1198 slice.swap(i, j);
1199 }
1200}
1201
1202#[cfg(test)]
1207#[allow(clippy::float_cmp)]
1208mod tests {
1209 use super::*;
1210
1211 fn add_edges(g: &mut Graph, edges: &[(u32, u32)]) {
1212 for &(u, v) in edges {
1213 g.add_edge(u, v).unwrap();
1214 }
1215 }
1216
1217 #[test]
1218 fn empty_graph_returns_empty_membership() {
1219 let g = Graph::with_vertices(0);
1220 let r = leiden(&g).unwrap();
1221 assert_eq!(r.membership.len(), 0);
1222 assert_eq!(r.quality, 0.0);
1223 assert_eq!(r.nb_clusters, 0);
1224 }
1225
1226 #[test]
1227 fn isolated_vertices_keep_singletons_after_modularity_reduction() {
1228 let g = Graph::with_vertices(4);
1234 let r = leiden(&g).unwrap();
1235 assert_eq!(r.membership.len(), 4);
1236 let k = r.nb_clusters;
1237 assert!((1..=4).contains(&k));
1238 }
1239
1240 #[test]
1241 fn two_triangles_bridge_split() {
1242 let mut g = Graph::with_vertices(6);
1243 add_edges(
1244 &mut g,
1245 &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
1246 );
1247 let r = leiden(&g).unwrap();
1248 assert_eq!(r.membership[0], r.membership[1]);
1249 assert_eq!(r.membership[0], r.membership[2]);
1250 assert_eq!(r.membership[3], r.membership[4]);
1251 assert_eq!(r.membership[3], r.membership[5]);
1252 assert_ne!(r.membership[0], r.membership[3]);
1253 }
1254
1255 #[test]
1256 fn directed_input_rejected() {
1257 let mut g = Graph::new(3, true).unwrap();
1258 g.add_edge(0, 1).unwrap();
1259 assert!(leiden(&g).is_err());
1260 assert!(leiden_weighted(&g, &[1.0]).is_err());
1261 assert!(leiden_with_options(&g, None, &LeidenOptions::default()).is_err());
1262 }
1263
1264 #[test]
1265 fn weight_validation() {
1266 let mut g = Graph::with_vertices(3);
1267 g.add_edge(0, 1).unwrap();
1268 g.add_edge(1, 2).unwrap();
1269 assert!(leiden_weighted(&g, &[1.0]).is_err());
1270 assert!(leiden_weighted(&g, &[1.0, -0.1]).is_err());
1271 assert!(leiden_weighted(&g, &[1.0, f64::NAN]).is_err());
1272 assert!(leiden_weighted(&g, &[1.0, f64::INFINITY]).is_err());
1273 let opts = LeidenOptions {
1275 objective: LeidenObjective::Cpm,
1276 ..LeidenOptions::default()
1277 };
1278 assert!(leiden_with_options(&g, Some(&[1.0, -0.1]), &opts).is_ok());
1279 }
1280
1281 #[test]
1282 fn deterministic_under_seed() {
1283 let mut g = Graph::with_vertices(6);
1284 add_edges(
1285 &mut g,
1286 &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
1287 );
1288 let opts = LeidenOptions {
1289 seed: 12345,
1290 ..LeidenOptions::default()
1291 };
1292 let a = leiden_with_options(&g, None, &opts).unwrap();
1293 let b = leiden_with_options(&g, None, &opts).unwrap();
1294 assert_eq!(a.membership, b.membership);
1295 assert!((a.quality - b.quality).abs() < 1e-12);
1296 }
1297
1298 #[test]
1299 fn cpm_objective_works() {
1300 let mut g = Graph::with_vertices(6);
1301 add_edges(
1302 &mut g,
1303 &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)],
1304 );
1305 let opts = LeidenOptions {
1311 objective: LeidenObjective::Cpm,
1312 resolution: 0.5,
1313 ..LeidenOptions::default()
1314 };
1315 let r = leiden_with_options(&g, None, &opts).unwrap();
1316 assert_eq!(r.membership[0], r.membership[1]);
1317 assert_eq!(r.membership[0], r.membership[2]);
1318 assert_eq!(r.membership[3], r.membership[4]);
1319 assert_eq!(r.membership[3], r.membership[5]);
1320 assert_ne!(r.membership[0], r.membership[3]);
1321 }
1322
1323 #[test]
1324 fn negative_resolution_rejected() {
1325 let g = Graph::with_vertices(3);
1326 let opts = LeidenOptions {
1327 resolution: -0.1,
1328 ..LeidenOptions::default()
1329 };
1330 assert!(leiden_with_options(&g, None, &opts).is_err());
1331 }
1332
1333 #[test]
1334 fn negative_beta_rejected() {
1335 let g = Graph::with_vertices(3);
1336 let opts = LeidenOptions {
1337 beta: -0.1,
1338 ..LeidenOptions::default()
1339 };
1340 assert!(leiden_with_options(&g, None, &opts).is_err());
1341 }
1342}