1#![allow(
28 clippy::cast_possible_truncation,
29 clippy::cast_sign_loss,
30 clippy::many_single_char_names,
31 clippy::cast_precision_loss,
32 clippy::doc_markdown,
33 clippy::too_many_arguments,
34 clippy::unnecessary_wraps
35)]
36
37use crate::core::{Graph, IgraphError, IgraphResult};
38
39const TAU: f64 = 0.15;
40const PR_EPS: f64 = 1e-10;
41const PR_MAX_ITER: usize = 1000;
42const MAX_PASSES: usize = 256;
43const DL_EPS: f64 = 1e-12;
44
45#[derive(Debug, Clone)]
47pub struct InfomapResult {
48 pub membership: Vec<u32>,
50 pub codelength: f64,
52}
53
54pub fn infomap(graph: &Graph) -> IgraphResult<InfomapResult> {
72 infomap_with_options(graph, None, None, 1, 0)
73}
74
75pub fn infomap_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<InfomapResult> {
96 infomap_with_options(graph, Some(weights), None, 1, 0)
97}
98
99pub fn infomap_with_options(
126 graph: &Graph,
127 edge_weights: Option<&[f64]>,
128 vertex_weights: Option<&[f64]>,
129 nb_trials: u32,
130 seed: u64,
131) -> IgraphResult<InfomapResult> {
132 let n = graph.vcount() as usize;
133 let m = graph.ecount();
134
135 if nb_trials < 1 {
136 return Err(IgraphError::InvalidArgument(
137 "nb_trials must be at least 1".to_string(),
138 ));
139 }
140 validate_edge_weights(m, edge_weights)?;
141 validate_vertex_weights(n, vertex_weights)?;
142
143 if n == 0 {
144 return Ok(InfomapResult {
145 membership: Vec::new(),
146 codelength: f64::NAN,
147 });
148 }
149
150 let fs = build_flow_state(graph, edge_weights, vertex_weights)?;
151
152 let mut rng = SplitMix64::new(seed);
153 let mut best: Option<InfomapResult> = None;
154
155 for _ in 0..nb_trials {
156 let result = run_trial(&fs, &mut rng);
157 let dominated = match &best {
158 None => false,
159 Some(b) => result.codelength >= b.codelength,
160 };
161 if !dominated {
162 best = Some(result);
163 }
164 }
165
166 Ok(best.unwrap_or(InfomapResult {
167 membership: vec![0; n],
168 codelength: f64::NAN,
169 }))
170}
171
172fn validate_edge_weights(m: usize, weights: Option<&[f64]>) -> IgraphResult<()> {
175 if let Some(ew) = weights {
176 if ew.len() != m {
177 return Err(IgraphError::InvalidArgument(format!(
178 "edge weight vector length {} does not match edge count {m}",
179 ew.len()
180 )));
181 }
182 for (i, &w) in ew.iter().enumerate() {
183 if w < 0.0 || !w.is_finite() {
184 return Err(IgraphError::InvalidArgument(format!(
185 "edge weight[{i}] = {w} is invalid (must be non-negative and finite)"
186 )));
187 }
188 }
189 }
190 Ok(())
191}
192
193fn validate_vertex_weights(n: usize, weights: Option<&[f64]>) -> IgraphResult<()> {
194 if let Some(vw) = weights {
195 if vw.len() != n {
196 return Err(IgraphError::InvalidArgument(format!(
197 "vertex weight vector length {} does not match vertex count {n}",
198 vw.len()
199 )));
200 }
201 let mut any_positive = false;
202 for (i, &w) in vw.iter().enumerate() {
203 if w < 0.0 || !w.is_finite() {
204 return Err(IgraphError::InvalidArgument(format!(
205 "vertex weight[{i}] = {w} is invalid"
206 )));
207 }
208 if w > 0.0 {
209 any_positive = true;
210 }
211 }
212 if !any_positive && n > 0 {
213 return Err(IgraphError::InvalidArgument(
214 "all vertex weights are zero".to_string(),
215 ));
216 }
217 }
218 Ok(())
219}
220
221struct FlowState {
224 n: usize,
225 node_flow: Vec<f64>,
226 out_flow: Vec<Vec<(u32, f64)>>,
227 in_flow: Vec<Vec<(u32, f64)>>,
228 tele_weight: Vec<f64>,
229}
230
231fn build_flow_state(
234 graph: &Graph,
235 edge_weights: Option<&[f64]>,
236 vertex_weights: Option<&[f64]>,
237) -> IgraphResult<FlowState> {
238 let n = graph.vcount() as usize;
239 let directed = graph.is_directed();
240 let ecount = graph.ecount();
241 let m32 = u32::try_from(ecount).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
242
243 let mut out_adj: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
245 let mut out_strength = vec![0.0_f64; n];
246
247 for e in 0..m32 {
248 let (u, v) = graph.edge(e)?;
249 let w = edge_weights.map_or(1.0, |ew| ew[e as usize]);
250 if directed {
251 out_adj[u as usize].push((v, w));
252 out_strength[u as usize] += w;
253 } else {
254 out_adj[u as usize].push((v, w));
255 out_adj[v as usize].push((u, w));
256 out_strength[u as usize] += w;
257 out_strength[v as usize] += w;
258 }
259 }
260
261 let tele_weight: Vec<f64> = if let Some(vw) = vertex_weights {
263 let s: f64 = vw.iter().sum();
264 if s > 0.0 {
265 vw.iter().map(|&w| w / s).collect()
266 } else {
267 vec![1.0 / n as f64; n]
268 }
269 } else {
270 vec![1.0 / n as f64; n]
271 };
272
273 let node_flow = power_iteration_pagerank(n, &out_adj, &out_strength, &tele_weight)?;
275
276 let mut out_flow: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
278 let mut in_flow: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
279
280 for u in 0..n {
281 if out_strength[u] > 0.0 {
282 for &(v, w) in &out_adj[u] {
283 let ef = node_flow[u] * (1.0 - TAU) * w / out_strength[u];
284 out_flow[u].push((v, ef));
285 in_flow[v as usize].push((u as u32, ef));
286 }
287 }
288 }
289
290 Ok(FlowState {
291 n,
292 node_flow,
293 out_flow,
294 in_flow,
295 tele_weight,
296 })
297}
298
299fn power_iteration_pagerank(
300 n: usize,
301 out_adj: &[Vec<(u32, f64)>],
302 out_strength: &[f64],
303 tele_weight: &[f64],
304) -> IgraphResult<Vec<f64>> {
305 if n == 0 {
306 return Ok(Vec::new());
307 }
308 if n == 1 {
309 return Ok(vec![1.0]);
310 }
311
312 let mut in_adj: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
314 for u in 0..n {
315 if out_strength[u] > 0.0 {
316 for &(v, w) in &out_adj[u] {
317 in_adj[v as usize].push((u as u32, w / out_strength[u]));
318 }
319 }
320 }
321
322 let dangling: Vec<bool> = out_strength.iter().map(|&s| s <= 0.0).collect();
323 let mut pr = vec![1.0 / n as f64; n];
324 let mut buf = vec![0.0_f64; n];
325
326 for _ in 0..PR_MAX_ITER {
327 let dangle_sum: f64 = (0..n).filter(|&i| dangling[i]).map(|i| pr[i]).sum();
328
329 for v in 0..n {
330 let link: f64 = in_adj[v]
331 .iter()
332 .map(|&(u, w_norm)| pr[u as usize] * w_norm)
333 .sum();
334 buf[v] = TAU * tele_weight[v] + (1.0 - TAU) * link + dangle_sum * tele_weight[v];
335 }
336
337 let s: f64 = buf.iter().sum();
338 if s > 0.0 {
339 for p in &mut buf {
340 *p /= s;
341 }
342 }
343
344 let diff: f64 = pr.iter().zip(buf.iter()).map(|(a, b)| (a - b).abs()).sum();
345 std::mem::swap(&mut pr, &mut buf);
346
347 if diff < PR_EPS {
348 break;
349 }
350 }
351
352 Ok(pr)
353}
354
355fn plogp(x: f64) -> f64 {
358 if x > 0.0 { x * x.log2() } else { 0.0 }
359}
360
361fn codelength(
366 _membership: &[u32],
367 module_exit: &[f64],
368 module_flow: &[f64],
369 node_flow: &[f64],
370 n_modules: usize,
371) -> f64 {
372 let total_exit: f64 = module_exit[..n_modules].iter().sum();
373
374 let mut sum_plogp_exit = 0.0_f64;
375 let mut sum_plogp_exit_plus_flow = 0.0_f64;
376
377 for i in 0..n_modules {
378 sum_plogp_exit += plogp(module_exit[i]);
379 sum_plogp_exit_plus_flow += plogp(module_exit[i] + module_flow[i]);
380 }
381
382 let sum_plogp_node: f64 = node_flow.iter().copied().map(plogp).sum();
383
384 plogp(total_exit) - 2.0 * sum_plogp_exit + sum_plogp_exit_plus_flow - sum_plogp_node
385}
386
387fn module_exit_flow(mod_id: u32, membership: &[u32], fs: &FlowState) -> f64 {
389 let mut edge_exit = 0.0_f64;
390 let mut mod_flow = 0.0_f64;
391 let mut tele_in_mod = 0.0_f64;
392 let mut any = false;
393
394 for v in 0..fs.n {
395 if membership[v] != mod_id {
396 continue;
397 }
398 any = true;
399 mod_flow += fs.node_flow[v];
400 tele_in_mod += fs.tele_weight[v];
401
402 for &(u, ef) in &fs.out_flow[v] {
403 if membership[u as usize] != mod_id {
404 edge_exit += ef;
405 }
406 }
407 }
408
409 if !any {
410 return 0.0;
411 }
412
413 let tele_exit = TAU * mod_flow * (1.0 - tele_in_mod);
416 edge_exit + tele_exit
417}
418
419fn run_trial(fs: &FlowState, rng: &mut SplitMix64) -> InfomapResult {
422 let n = fs.n;
423 if n == 0 {
424 return InfomapResult {
425 membership: Vec::new(),
426 codelength: f64::NAN,
427 };
428 }
429
430 let mut membership: Vec<u32> = (0..n as u32).collect();
432 let mut module_flow: Vec<f64> = fs.node_flow.clone();
433 let mut module_size: Vec<u32> = vec![1; n];
434 let mut module_exit: Vec<f64> = (0..n)
435 .map(|v| module_exit_flow(v as u32, &membership, fs))
436 .collect();
437
438 let mut cl = codelength(&membership, &module_exit, &module_flow, &fs.node_flow, n);
439
440 let mut node_order: Vec<usize> = (0..n).collect();
441 if n > 1 {
442 shuffle(&mut node_order, rng);
443 }
444
445 for _pass in 0..MAX_PASSES {
446 let prev_cl = cl;
447 let mut changed = false;
448
449 if n > 1 {
450 let a = rng.gen_index(n);
451 let b = rng.gen_index(n);
452 node_order.swap(a, b);
453 }
454
455 for &v in &node_order {
456 let old_mod = membership[v];
457
458 let mut nbr_mods: Vec<u32> = Vec::new();
460 for &(u, _) in &fs.out_flow[v] {
461 let um = membership[u as usize];
462 if um != old_mod && !nbr_mods.contains(&um) {
463 nbr_mods.push(um);
464 }
465 }
466 for &(u, _) in &fs.in_flow[v] {
467 let um = membership[u as usize];
468 if um != old_mod && !nbr_mods.contains(&um) {
469 nbr_mods.push(um);
470 }
471 }
472
473 if nbr_mods.is_empty() {
474 continue;
475 }
476
477 let best = try_moves(
479 v,
480 old_mod,
481 &nbr_mods,
482 fs,
483 &membership,
484 &module_exit,
485 &module_flow,
486 &module_size,
487 );
488
489 if let Some((new_mod, delta)) = best {
490 if delta < -DL_EPS {
491 let p_v = fs.node_flow[v];
493 membership[v] = new_mod;
494
495 module_flow[old_mod as usize] -= p_v;
496 module_flow[new_mod as usize] += p_v;
497 module_size[old_mod as usize] -= 1;
498 module_size[new_mod as usize] += 1;
499
500 let mut to_update: Vec<u32> = vec![old_mod, new_mod];
503 for &(u, _) in &fs.out_flow[v] {
504 let um = membership[u as usize];
505 if !to_update.contains(&um) {
506 to_update.push(um);
507 }
508 }
509 for &(u, _) in &fs.in_flow[v] {
510 let um = membership[u as usize];
511 if !to_update.contains(&um) {
512 to_update.push(um);
513 }
514 }
515 for &mid in &to_update {
516 module_exit[mid as usize] = module_exit_flow(mid, &membership, fs);
517 }
518
519 changed = true;
520 }
521 }
522 }
523
524 cl = codelength(&membership, &module_exit, &module_flow, &fs.node_flow, n);
525
526 if !changed || prev_cl - cl < DL_EPS {
527 break;
528 }
529 }
530
531 InfomapResult {
532 membership: reindex(&membership),
533 codelength: cl,
534 }
535}
536
537fn try_moves(
540 v: usize,
541 old_mod: u32,
542 candidates: &[u32],
543 fs: &FlowState,
544 membership: &[u32],
545 module_exit: &[f64],
546 module_flow: &[f64],
547 module_size: &[u32],
548) -> Option<(u32, f64)> {
549 let p_v = fs.node_flow[v];
550
551 let total_exit_before: f64 = module_exit.iter().sum();
553 let old_exit_before = module_exit[old_mod as usize];
554 let old_flow_before = module_flow[old_mod as usize];
555
556 let old_becomes_empty = module_size[old_mod as usize] == 1;
558
559 let old_exit_after = if old_becomes_empty {
561 0.0
562 } else {
563 exit_after_remove(v, old_mod, fs, membership)
564 };
565 let old_flow_after = old_flow_before - p_v;
566
567 let mut best_mod = old_mod;
568 let mut best_delta = 0.0_f64;
569
570 for &new_mod in candidates {
571 let new_exit_before = module_exit[new_mod as usize];
572 let new_flow_before = module_flow[new_mod as usize];
573
574 let new_exit_after = exit_after_add(v, new_mod, fs, membership);
576 let new_flow_after = new_flow_before + p_v;
577
578 let new_total_exit =
580 total_exit_before - old_exit_before + old_exit_after - new_exit_before + new_exit_after;
581
582 let mut delta = plogp(new_total_exit) - plogp(total_exit_before);
589 delta += 2.0 * (plogp(old_exit_before) - plogp(old_exit_after));
590 delta += 2.0 * (plogp(new_exit_before) - plogp(new_exit_after));
591 delta += plogp(old_exit_after + old_flow_after) - plogp(old_exit_before + old_flow_before);
592 delta += plogp(new_exit_after + new_flow_after) - plogp(new_exit_before + new_flow_before);
593
594 if delta < best_delta - DL_EPS {
595 best_delta = delta;
596 best_mod = new_mod;
597 }
598 }
599
600 if best_mod == old_mod {
601 None
602 } else {
603 Some((best_mod, best_delta))
604 }
605}
606
607fn exit_after_remove(v: usize, mod_id: u32, fs: &FlowState, membership: &[u32]) -> f64 {
608 let mut edge_exit = 0.0_f64;
609 let mut mod_flow = 0.0_f64;
610 let mut tele_in = 0.0_f64;
611 let mut any = false;
612
613 for u in 0..fs.n {
614 if membership[u] != mod_id || u == v {
615 continue;
616 }
617 any = true;
618 mod_flow += fs.node_flow[u];
619 tele_in += fs.tele_weight[u];
620
621 for &(w, ef) in &fs.out_flow[u] {
622 let wm = if w as usize == v {
623 u32::MAX
624 } else {
625 membership[w as usize]
626 };
627 if wm != mod_id {
628 edge_exit += ef;
629 }
630 }
631 }
632
633 if !any {
634 return 0.0;
635 }
636
637 edge_exit + TAU * mod_flow * (1.0 - tele_in)
638}
639
640fn exit_after_add(v: usize, mod_id: u32, fs: &FlowState, membership: &[u32]) -> f64 {
641 let mut edge_exit = 0.0_f64;
642 let mut mod_flow = 0.0_f64;
643 let mut tele_in = 0.0_f64;
644
645 for u in 0..fs.n {
646 if membership[u] != mod_id {
647 continue;
648 }
649 mod_flow += fs.node_flow[u];
650 tele_in += fs.tele_weight[u];
651
652 for &(w, ef) in &fs.out_flow[u] {
653 let wm = if w as usize == v {
654 mod_id
655 } else {
656 membership[w as usize]
657 };
658 if wm != mod_id {
659 edge_exit += ef;
660 }
661 }
662 }
663
664 mod_flow += fs.node_flow[v];
666 tele_in += fs.tele_weight[v];
667
668 for &(w, ef) in &fs.out_flow[v] {
669 if membership[w as usize] != mod_id && w as usize != v {
670 edge_exit += ef;
671 }
672 }
673
674 edge_exit + TAU * mod_flow * (1.0 - tele_in)
675}
676
677fn reindex(membership: &[u32]) -> Vec<u32> {
680 let mut map: Vec<Option<u32>> = Vec::new();
681 let mut next_id = 0u32;
682 let mut result = Vec::with_capacity(membership.len());
683
684 for &m in membership {
685 let idx = m as usize;
686 while map.len() <= idx {
687 map.push(None);
688 }
689 let new_id = if let Some(id) = map[idx] {
690 id
691 } else {
692 let id = next_id;
693 next_id = next_id.saturating_add(1);
694 map[idx] = Some(id);
695 id
696 };
697 result.push(new_id);
698 }
699
700 result
701}
702
703struct SplitMix64 {
704 state: u64,
705}
706
707impl SplitMix64 {
708 fn new(seed: u64) -> Self {
709 Self { state: seed }
710 }
711
712 fn next_u64(&mut self) -> u64 {
713 self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
714 let mut z = self.state;
715 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
716 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
717 z ^ (z >> 31)
718 }
719
720 fn gen_index(&mut self, n: usize) -> usize {
721 (self.next_u64() as usize) % n
722 }
723}
724
725fn shuffle<T>(slice: &mut [T], rng: &mut SplitMix64) {
726 for i in (1..slice.len()).rev() {
727 let j = rng.gen_index(i + 1);
728 slice.swap(i, j);
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735
736 fn two_triangles() -> Graph {
737 let mut g = Graph::with_vertices(6);
738 for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
739 g.add_edge(u, v).unwrap();
740 }
741 g
742 }
743
744 #[test]
745 fn basic_two_communities() {
746 let g = two_triangles();
747 let r = infomap(&g).unwrap();
748 assert_eq!(r.membership[0], r.membership[1]);
749 assert_eq!(r.membership[0], r.membership[2]);
750 assert_eq!(r.membership[3], r.membership[4]);
751 assert_eq!(r.membership[3], r.membership[5]);
752 assert_ne!(r.membership[0], r.membership[3]);
753 }
754
755 #[test]
756 fn empty_graph() {
757 let g = Graph::with_vertices(0);
758 let r = infomap(&g).unwrap();
759 assert!(r.membership.is_empty());
760 assert!(r.codelength.is_nan());
761 }
762
763 #[test]
764 fn single_vertex() {
765 let g = Graph::with_vertices(1);
766 let r = infomap(&g).unwrap();
767 assert_eq!(r.membership, vec![0]);
768 }
769
770 #[test]
771 fn disconnected_components() {
772 let mut g = Graph::with_vertices(6);
773 g.add_edge(0, 1).unwrap();
774 g.add_edge(1, 2).unwrap();
775 g.add_edge(3, 4).unwrap();
776 g.add_edge(4, 5).unwrap();
777 let r = infomap(&g).unwrap();
778 assert_eq!(r.membership[0], r.membership[1]);
779 assert_eq!(r.membership[0], r.membership[2]);
780 assert_eq!(r.membership[3], r.membership[4]);
781 assert_eq!(r.membership[3], r.membership[5]);
782 assert_ne!(r.membership[0], r.membership[3]);
783 }
784
785 #[test]
786 fn deterministic_with_seed() {
787 let g = two_triangles();
788 let a = infomap_with_options(&g, None, None, 3, 42).unwrap();
789 let b = infomap_with_options(&g, None, None, 3, 42).unwrap();
790 assert_eq!(a.membership, b.membership);
791 }
792
793 #[test]
794 fn weighted_edges() {
795 let g = two_triangles();
796 let w = vec![10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 0.01];
797 let r = infomap_weighted(&g, &w).unwrap();
798 assert_eq!(r.membership[0], r.membership[1]);
799 assert_ne!(r.membership[0], r.membership[3]);
800 }
801
802 #[test]
803 fn directed_graph() {
804 let g = Graph::from_edges(
805 &[(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)],
806 true,
807 Some(6),
808 )
809 .unwrap();
810 let r = infomap(&g).unwrap();
811 assert_eq!(r.membership[0], r.membership[1]);
812 assert_eq!(r.membership[3], r.membership[4]);
813 assert_ne!(r.membership[0], r.membership[3]);
814 }
815
816 #[test]
817 fn invalid_edge_weights() {
818 let g = two_triangles();
819 let w = vec![-1.0; 7];
820 assert!(infomap_weighted(&g, &w).is_err());
821 }
822
823 #[test]
824 fn invalid_nb_trials() {
825 let g = two_triangles();
826 assert!(infomap_with_options(&g, None, None, 0, 0).is_err());
827 }
828
829 #[test]
830 fn karate_club() {
831 let g = Graph::from_edges(
832 &[
833 (0, 1),
834 (0, 2),
835 (0, 3),
836 (0, 4),
837 (0, 5),
838 (0, 6),
839 (0, 7),
840 (0, 8),
841 (0, 10),
842 (0, 11),
843 (0, 12),
844 (0, 13),
845 (0, 17),
846 (0, 19),
847 (0, 21),
848 (0, 31),
849 (1, 2),
850 (1, 3),
851 (1, 7),
852 (1, 13),
853 (1, 17),
854 (1, 19),
855 (1, 21),
856 (1, 30),
857 (2, 3),
858 (2, 7),
859 (2, 8),
860 (2, 9),
861 (2, 13),
862 (2, 27),
863 (2, 28),
864 (2, 32),
865 (3, 7),
866 (3, 12),
867 (3, 13),
868 (4, 6),
869 (4, 10),
870 (5, 6),
871 (5, 10),
872 (5, 16),
873 (6, 16),
874 (8, 30),
875 (8, 32),
876 (8, 33),
877 (9, 33),
878 (13, 33),
879 (14, 32),
880 (14, 33),
881 (15, 32),
882 (15, 33),
883 (18, 32),
884 (18, 33),
885 (19, 33),
886 (20, 32),
887 (20, 33),
888 (22, 32),
889 (22, 33),
890 (23, 25),
891 (23, 27),
892 (23, 29),
893 (23, 32),
894 (23, 33),
895 (24, 25),
896 (24, 27),
897 (24, 31),
898 (25, 31),
899 (26, 29),
900 (26, 33),
901 (27, 33),
902 (28, 31),
903 (28, 33),
904 (29, 32),
905 (29, 33),
906 (30, 32),
907 (30, 33),
908 (31, 32),
909 (31, 33),
910 (32, 33),
911 ],
912 false,
913 None,
914 )
915 .unwrap();
916 let r = infomap(&g).unwrap();
917 let k = *r.membership.iter().max().unwrap_or(&0) + 1;
919 assert!(
920 k >= 2,
921 "karate club should have at least 2 communities, got {k}"
922 );
923 assert!(r.codelength.is_finite());
924 }
925}