1pub mod evaluation;
29pub mod infomap;
30pub mod label_propagation;
31pub mod leiden;
32pub mod louvain;
33
34pub use evaluation::{
36 adjusted_rand_index, conductance, coverage, modularity as eval_modularity, nmi, normalized_cut,
37};
38pub use infomap::{infomap, InfomapConfig};
39pub use label_propagation::{async_label_propagation, label_propagation_edge_list};
40pub use leiden::{leiden, LeidenCommunity};
41pub use louvain::{louvain, modularity as edge_list_modularity, LouvainCommunity};
42
43use std::collections::HashMap;
44
45use scirs2_core::ndarray::Array2;
46use scirs2_core::random::{Rng, RngExt, SeedableRng, StdRng};
47
48use crate::error::{GraphError, Result};
49
50#[derive(Debug, Clone)]
56pub struct LouvainResult {
57 pub assignments: Vec<usize>,
59 pub modularity: f64,
61 pub n_communities: usize,
63 pub iterations: usize,
65}
66
67pub fn modularity(adj: &Array2<f64>, assignments: &[usize]) -> f64 {
78 let n = adj.nrows();
79 if n == 0 || assignments.len() != n {
80 return 0.0;
81 }
82
83 let two_m: f64 = adj.iter().sum();
85 if two_m == 0.0 {
86 return 0.0;
87 }
88
89 let degrees: Vec<f64> = (0..n).map(|i| adj.row(i).sum()).collect();
90
91 let mut q = 0.0;
92 for i in 0..n {
93 for j in 0..n {
94 if assignments[i] == assignments[j] {
95 q += adj[[i, j]] - degrees[i] * degrees[j] / two_m;
96 }
97 }
98 }
99 q / two_m
100}
101
102pub fn louvain_communities(
122 adj: &Array2<f64>,
123 resolution: f64,
124 max_iter: usize,
125 seed: u64,
126) -> Result<LouvainResult> {
127 let n = adj.nrows();
128 if n == 0 {
129 return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
130 }
131 if adj.ncols() != n {
132 return Err(GraphError::InvalidGraph(
133 "adjacency matrix must be square".into(),
134 ));
135 }
136
137 let mut assignments: Vec<usize> = (0..n).collect();
139 let two_m: f64 = adj.iter().sum();
140 if two_m == 0.0 {
141 return Ok(LouvainResult {
142 assignments,
143 modularity: 0.0,
144 n_communities: n,
145 iterations: 0,
146 });
147 }
148
149 let mut rng = StdRng::seed_from_u64(seed);
150 let mut iteration = 0;
151
152 for _outer in 0..max_iter {
153 iteration += 1;
154 let improved = louvain_phase1(adj, &mut assignments, two_m, resolution, &mut rng);
155 if !improved {
156 break;
157 }
158 compact_communities(&mut assignments);
160 }
161
162 let q = modularity(adj, &assignments);
163 let n_communities = *assignments.iter().max().unwrap_or(&0) + 1;
164
165 Ok(LouvainResult {
166 assignments,
167 modularity: q,
168 n_communities,
169 iterations: iteration,
170 })
171}
172
173fn louvain_phase1(
176 adj: &Array2<f64>,
177 assignments: &mut [usize],
178 two_m: f64,
179 resolution: f64,
180 rng: &mut impl Rng,
181) -> bool {
182 let n = adj.nrows();
183 let degrees: Vec<f64> = (0..n).map(|i| adj.row(i).sum()).collect();
184
185 let n_communities = *assignments.iter().max().unwrap_or(&0) + 1;
187 let mut sigma_tot: Vec<f64> = vec![0.0; n_communities + n]; for i in 0..n {
189 let c = assignments[i];
190 sigma_tot[c] += degrees[i];
191 }
192
193 let mut improved = false;
194
195 let mut order: Vec<usize> = (0..n).collect();
197 for i in (1..n).rev() {
199 let j = rng.random_range(0..=i);
200 order.swap(i, j);
201 }
202
203 for &node in &order {
204 let current_comm = assignments[node];
205 let k_i = degrees[node];
206
207 let mut comm_weights: HashMap<usize, f64> = HashMap::new();
209 for j in 0..n {
210 if j == node {
211 continue;
212 }
213 let w = adj[[node, j]];
214 if w == 0.0 {
215 continue;
216 }
217 let c = assignments[j];
218 *comm_weights.entry(c).or_insert(0.0) += w;
219 }
220
221 let k_i_in_current = comm_weights.get(¤t_comm).copied().unwrap_or(0.0);
223 let remove_gain = k_i_in_current / two_m
224 - resolution * (sigma_tot[current_comm] - k_i) * k_i / (two_m * two_m);
225
226 let mut best_comm = current_comm;
228 let mut best_gain = 0.0;
229
230 for (&comm, &k_i_in_c) in &comm_weights {
231 if comm == current_comm {
232 continue;
233 }
234 let gain = k_i_in_c / two_m
235 - resolution * sigma_tot[comm] * k_i / (two_m * two_m)
236 - remove_gain;
237 if gain > best_gain {
238 best_gain = gain;
239 best_comm = comm;
240 }
241 }
242
243 if best_comm != current_comm {
244 sigma_tot[current_comm] -= k_i;
246 if best_comm >= sigma_tot.len() {
248 sigma_tot.resize(best_comm + 1, 0.0);
249 }
250 sigma_tot[best_comm] += k_i;
251 assignments[node] = best_comm;
252 improved = true;
253 }
254 }
255
256 improved
257}
258
259fn compact_communities(assignments: &mut [usize]) {
261 let mut mapping: HashMap<usize, usize> = HashMap::new();
262 let mut next_id = 0usize;
263 for a in assignments.iter_mut() {
264 let new_id = mapping.entry(*a).or_insert_with(|| {
265 let id = next_id;
266 next_id += 1;
267 id
268 });
269 *a = *new_id;
270 }
271}
272
273pub fn label_propagation(adj: &Array2<f64>, max_iter: usize, seed: u64) -> Result<Vec<usize>> {
287 let n = adj.nrows();
288 if n == 0 {
289 return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
290 }
291 if adj.ncols() != n {
292 return Err(GraphError::InvalidGraph(
293 "adjacency matrix must be square".into(),
294 ));
295 }
296
297 let mut labels: Vec<usize> = (0..n).collect();
298 let mut rng = StdRng::seed_from_u64(seed);
299
300 for _iter in 0..max_iter {
301 let mut changed = false;
302
303 let mut order: Vec<usize> = (0..n).collect();
305 for i in (1..n).rev() {
306 let j = rng.random_range(0..=i);
307 order.swap(i, j);
308 }
309
310 for &node in &order {
311 let mut label_weight: HashMap<usize, f64> = HashMap::new();
312 for j in 0..n {
313 let w = adj[[node, j]];
314 if w > 0.0 {
315 *label_weight.entry(labels[j]).or_insert(0.0) += w;
316 }
317 }
318
319 if label_weight.is_empty() {
320 continue;
321 }
322
323 let max_w = label_weight
325 .values()
326 .cloned()
327 .fold(f64::NEG_INFINITY, f64::max);
328 let best_labels: Vec<usize> = label_weight
330 .iter()
331 .filter(|(_, &w)| (w - max_w).abs() < 1e-12)
332 .map(|(&l, _)| l)
333 .collect();
334
335 let chosen = if best_labels.len() == 1 {
336 best_labels[0]
337 } else {
338 let idx = rng.random_range(0..best_labels.len());
339 best_labels[idx]
340 };
341
342 if chosen != labels[node] {
343 labels[node] = chosen;
344 changed = true;
345 }
346 }
347
348 if !changed {
349 break;
350 }
351 }
352
353 compact_communities(&mut labels);
354 Ok(labels)
355}
356
357pub fn girvan_newman(adj: &Array2<f64>, n_communities: usize) -> Result<Vec<usize>> {
372 let n = adj.nrows();
373 if n == 0 {
374 return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
375 }
376 if adj.ncols() != n {
377 return Err(GraphError::InvalidGraph(
378 "adjacency matrix must be square".into(),
379 ));
380 }
381 if n_communities == 0 {
382 return Err(GraphError::InvalidParameter {
383 param: "n_communities".into(),
384 value: "0".into(),
385 expected: ">= 1".into(),
386 context: "girvan_newman".into(),
387 });
388 }
389
390 let mut working = adj.to_owned();
392
393 loop {
394 let comps = connected_components_adj(&working);
395 let n_comps = {
397 let mut seen = std::collections::HashSet::new();
398 for &c in &comps {
399 seen.insert(c);
400 }
401 seen.len()
402 };
403 if n_comps >= n_communities {
404 return Ok(comps);
405 }
406
407 let betweenness = edge_betweenness_centrality(&working);
409 if betweenness.is_empty() {
410 return Ok(comps);
412 }
413
414 let (bi, bj, _) = betweenness
416 .into_iter()
417 .max_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
418 .ok_or_else(|| GraphError::AlgorithmError("no edge found".into()))?;
419
420 working[[bi, bj]] = 0.0;
421 working[[bj, bi]] = 0.0;
422 }
423}
424
425fn edge_betweenness_centrality(adj: &Array2<f64>) -> Vec<(usize, usize, f64)> {
428 use std::collections::VecDeque;
429
430 let n = adj.nrows();
431 let mut edge_scores = vec![vec![0.0f64; n]; n];
432
433 for source in 0..n {
434 let mut stack: Vec<usize> = Vec::new();
435 let mut pred: Vec<Vec<usize>> = vec![vec![]; n];
436 let mut sigma = vec![0.0f64; n];
437 let mut dist = vec![-1i64; n];
438
439 sigma[source] = 1.0;
440 dist[source] = 0;
441 let mut queue = VecDeque::new();
442 queue.push_back(source);
443
444 while let Some(v) = queue.pop_front() {
445 stack.push(v);
446 for w in 0..n {
447 if adj[[v, w]] == 0.0 {
448 continue;
449 }
450 if dist[w] < 0 {
451 dist[w] = dist[v] + 1;
452 queue.push_back(w);
453 }
454 if dist[w] == dist[v] + 1 {
455 sigma[w] += sigma[v];
456 pred[w].push(v);
457 }
458 }
459 }
460
461 let mut delta = vec![0.0f64; n];
462 while let Some(w) = stack.pop() {
463 for &v in &pred[w] {
464 let c = sigma[v] / sigma[w] * (1.0 + delta[w]);
465 edge_scores[v][w] += c;
466 edge_scores[w][v] += c;
467 delta[v] += c;
468 }
469 }
470 }
471
472 let mut result = Vec::new();
473 for i in 0..n {
474 for j in (i + 1)..n {
475 if adj[[i, j]] > 0.0 {
476 result.push((i, j, edge_scores[i][j]));
477 }
478 }
479 }
480 result
481}
482
483fn connected_components_adj(adj: &Array2<f64>) -> Vec<usize> {
485 use std::collections::VecDeque;
486 let n = adj.nrows();
487 let mut labels = vec![usize::MAX; n];
488 let mut comp_id = 0;
489
490 for start in 0..n {
491 if labels[start] != usize::MAX {
492 continue;
493 }
494 let mut queue = VecDeque::new();
495 queue.push_back(start);
496 labels[start] = comp_id;
497 while let Some(v) = queue.pop_front() {
498 for w in 0..n {
499 if adj[[v, w]] > 0.0 && labels[w] == usize::MAX {
500 labels[w] = comp_id;
501 queue.push_back(w);
502 }
503 }
504 }
505 comp_id += 1;
506 }
507
508 labels
509}
510
511pub fn infomap_communities(adj: &Array2<f64>, n_trials: usize, seed: u64) -> Result<LouvainResult> {
527 let n = adj.nrows();
528 if n == 0 {
529 return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
530 }
531 if adj.ncols() != n {
532 return Err(GraphError::InvalidGraph(
533 "adjacency matrix must be square".into(),
534 ));
535 }
536
537 let two_m: f64 = adj.iter().sum();
538 if two_m == 0.0 {
539 return Ok(LouvainResult {
540 assignments: (0..n).collect(),
541 modularity: 0.0,
542 n_communities: n,
543 iterations: 0,
544 });
545 }
546
547 let mut best_result: Option<LouvainResult> = None;
548
549 for trial in 0..n_trials.max(1) {
550 let trial_seed = seed.wrapping_add(trial as u64);
551 let result = infomap_single_trial(adj, two_m, trial_seed)?;
552 let better = match &best_result {
553 None => true,
554 Some(prev) => result.modularity > prev.modularity,
555 };
556 if better {
557 best_result = Some(result);
558 }
559 }
560
561 best_result.ok_or_else(|| GraphError::AlgorithmError("infomap: no trials completed".into()))
562}
563
564fn infomap_single_trial(adj: &Array2<f64>, two_m: f64, seed: u64) -> Result<LouvainResult> {
566 let n = adj.nrows();
567 let mut rng = StdRng::seed_from_u64(seed);
568
569 let degrees: Vec<f64> = (0..n).map(|i| adj.row(i).sum()).collect();
571
572 let init_comms = ((n as f64).sqrt().ceil() as usize).max(1);
574 let mut assignments: Vec<usize> = (0..n).map(|_| rng.random_range(0..init_comms)).collect();
575 compact_communities(&mut assignments);
576
577 let max_iter = 200;
578 let mut iteration = 0;
579
580 for _outer in 0..max_iter {
581 iteration += 1;
582 let improved = infomap_phase1(adj, &mut assignments, °rees, two_m, &mut rng);
583 if !improved {
584 break;
585 }
586 compact_communities(&mut assignments);
587 }
588
589 let q = modularity(adj, &assignments);
590 let n_communities = *assignments.iter().max().unwrap_or(&0) + 1;
591
592 Ok(LouvainResult {
593 assignments,
594 modularity: q,
595 n_communities,
596 iterations: iteration,
597 })
598}
599
600fn infomap_phase1(
603 adj: &Array2<f64>,
604 assignments: &mut [usize],
605 _degrees: &[f64],
606 two_m: f64,
607 rng: &mut impl Rng,
608) -> bool {
609 louvain_phase1(adj, assignments, two_m, 1.0, rng)
613}
614
615#[cfg(test)]
620mod tests {
621 use super::*;
622 use scirs2_core::ndarray::Array2;
623
624 fn make_clique_adj(k: usize, clique_size: usize) -> Array2<f64> {
626 let n = k * clique_size;
627 let mut adj = Array2::zeros((n, n));
628 for c in 0..k {
629 let base = c * clique_size;
630 for i in 0..clique_size {
631 for j in 0..clique_size {
632 if i != j {
633 adj[[base + i, base + j]] = 1.0;
634 }
635 }
636 }
637 }
638 if k > 1 {
640 for c in 0..(k - 1) {
641 let u = c * clique_size;
642 let v = (c + 1) * clique_size;
643 adj[[u, v]] = 0.05;
644 adj[[v, u]] = 0.05;
645 }
646 }
647 adj
648 }
649
650 #[test]
651 fn test_modularity_perfect_partition() {
652 let adj = make_clique_adj(2, 3);
654 let assignments = vec![0, 0, 0, 1, 1, 1];
655 let q = modularity(&adj, &assignments);
656 assert!(q > 0.0, "modularity should be positive: {q}");
658 }
659
660 #[test]
661 fn test_modularity_empty_graph() {
662 let adj = Array2::<f64>::zeros((4, 4));
663 let q = modularity(&adj, &[0, 0, 1, 1]);
664 assert_eq!(q, 0.0);
665 }
666
667 #[test]
668 fn test_modularity_wrong_assignments() {
669 let adj = Array2::<f64>::zeros((4, 4));
670 let q = modularity(&adj, &[0, 1]); assert_eq!(q, 0.0);
672 }
673
674 #[test]
675 fn test_louvain_two_cliques() {
676 let adj = make_clique_adj(2, 4);
677 let result = louvain_communities(&adj, 1.0, 100, 42).expect("louvain");
678 assert!(result.modularity > 0.0, "modularity should be positive");
679 let comms_left: std::collections::HashSet<usize> =
681 result.assignments[0..4].iter().cloned().collect();
682 let comms_right: std::collections::HashSet<usize> =
683 result.assignments[4..8].iter().cloned().collect();
684 assert_eq!(comms_left.len(), 1, "left clique should be one community");
686 assert_eq!(comms_right.len(), 1, "right clique should be one community");
687 assert_ne!(
688 result.assignments[0], result.assignments[4],
689 "two cliques must be in different communities"
690 );
691 }
692
693 #[test]
694 fn test_louvain_three_cliques() {
695 let adj = make_clique_adj(3, 3);
696 let result = louvain_communities(&adj, 1.0, 50, 7).expect("louvain");
697 assert!(result.modularity > 0.0);
698 assert!(result.n_communities >= 2);
699 }
700
701 #[test]
702 fn test_louvain_empty_graph_error() {
703 let adj = Array2::<f64>::zeros((0, 0));
704 assert!(louvain_communities(&adj, 1.0, 10, 0).is_err());
705 }
706
707 #[test]
708 fn test_label_propagation_converges() {
709 let adj = make_clique_adj(2, 4);
710 let labels = label_propagation(&adj, 100, 99).expect("label_propagation");
711 assert_eq!(labels.len(), 8);
712 let l0 = labels[0];
714 for i in 1..4 {
715 assert_eq!(labels[i], l0, "clique 1 should be uniform");
716 }
717 let l1 = labels[4];
718 for i in 5..8 {
719 assert_eq!(labels[i], l1, "clique 2 should be uniform");
720 }
721 assert_ne!(l0, l1, "two cliques should have different labels");
722 }
723
724 #[test]
725 fn test_label_propagation_single_node() {
726 let adj = Array2::<f64>::zeros((1, 1));
727 let labels = label_propagation(&adj, 10, 0).expect("lp");
728 assert_eq!(labels, vec![0]);
729 }
730
731 #[test]
732 fn test_girvan_newman_two_communities() {
733 let adj = make_clique_adj(2, 3);
734 let comms = girvan_newman(&adj, 2).expect("girvan_newman");
735 assert_eq!(comms.len(), 6);
736 let unique: std::collections::HashSet<usize> = comms.iter().cloned().collect();
738 assert!(unique.len() >= 2);
739 }
740
741 #[test]
742 fn test_girvan_newman_invalid() {
743 let adj = Array2::<f64>::zeros((0, 0));
744 assert!(girvan_newman(&adj, 2).is_err());
745 let adj2 = Array2::<f64>::zeros((4, 4));
746 assert!(girvan_newman(&adj2, 0).is_err());
747 }
748
749 #[test]
750 fn test_infomap_two_cliques() {
751 let adj = make_clique_adj(2, 4);
752 let result = infomap_communities(&adj, 5, 13).expect("infomap");
753 assert!(result.modularity > 0.0);
754 assert!(result.n_communities >= 2);
755 }
756
757 #[test]
758 fn test_infomap_empty_error() {
759 let adj = Array2::<f64>::zeros((0, 0));
760 assert!(infomap_communities(&adj, 3, 0).is_err());
761 }
762
763 #[test]
764 fn test_compact_communities() {
765 let mut a = vec![5, 5, 10, 10, 5];
766 compact_communities(&mut a);
767 assert_eq!(a[0], a[1]);
769 assert_eq!(a[1], a[4]);
770 assert_ne!(a[0], a[2]);
771 assert_eq!(a[2], a[3]);
772 }
773}