Skip to main content

rust_igraph/algorithms/community/
infomap.rs

1//! Infomap community detection (ALGO-CO-018).
2//!
3//! Rosvall & Bergstrom (2008): *Maps of information flow reveal community
4//! structure in complex networks.* PNAS 105(4):1118-1123.
5//! <https://doi.org/10.1073/pnas.0706851105>
6//!
7//! Minimises the two-level *map equation* — the expected per-step
8//! description length of a random walker that uses module-level
9//! codebooks. A lower codelength means the partition captures more
10//! of the graph's flow structure.
11//!
12//! Algorithm (greedy, Louvain-style):
13//!
14//! 1. Compute PageRank (ergodic visit frequencies) with teleportation
15//!    τ = 0.15.
16//! 2. Place each vertex in its own module.
17//! 3. Greedily move vertices to neighbouring modules that decrease the
18//!    map equation the most.
19//! 4. Repeat until no improvement.
20//! 5. (Optional) Run `nb_trials` independent attempts and keep the
21//!    partition with the shortest codelength.
22//!
23//! Self-rolled, dependency-free. Supports directed and undirected
24//! graphs, optional edge weights and vertex weights (teleportation
25//! targets).
26
27#![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/// Result of an Infomap run.
46#[derive(Debug, Clone)]
47pub struct InfomapResult {
48    /// Community assignment for each vertex (0-indexed, contiguous).
49    pub membership: Vec<u32>,
50    /// Map equation codelength of the best partition found.
51    pub codelength: f64,
52}
53
54/// Run Infomap with the default options (unweighted, 1 trial).
55///
56/// # Examples
57///
58/// ```
59/// use rust_igraph::{Graph, infomap};
60///
61/// // Two triangles connected by a bridge — two obvious communities.
62/// let mut g = Graph::with_vertices(6);
63/// for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
64///     g.add_edge(u, v).unwrap();
65/// }
66/// let r = infomap(&g).unwrap();
67/// assert_eq!(r.membership[0], r.membership[1]);
68/// assert_eq!(r.membership[3], r.membership[4]);
69/// assert_ne!(r.membership[0], r.membership[3]);
70/// ```
71pub fn infomap(graph: &Graph) -> IgraphResult<InfomapResult> {
72    infomap_with_options(graph, None, None, 1, 0)
73}
74
75/// Run Infomap with per-edge weights (1 trial).
76///
77/// # Errors
78/// - [`IgraphError::InvalidArgument`] if `weights.len() != ecount` or
79///   any weight is negative or non-finite.
80///
81/// # Examples
82///
83/// ```
84/// use rust_igraph::{Graph, infomap_weighted};
85///
86/// let mut g = Graph::with_vertices(6);
87/// for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
88///     g.add_edge(u, v).unwrap();
89/// }
90/// let w = vec![10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 0.01];
91/// let r = infomap_weighted(&g, &w).unwrap();
92/// assert_eq!(r.membership[0], r.membership[1]);
93/// assert_ne!(r.membership[0], r.membership[3]);
94/// ```
95pub fn infomap_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<InfomapResult> {
96    infomap_with_options(graph, Some(weights), None, 1, 0)
97}
98
99/// Run Infomap with full control over parameters.
100///
101/// - `edge_weights`: per-edge weights; `None` for unit weights. Must be
102///   non-negative and finite.
103/// - `vertex_weights`: teleportation target weights; `None` for uniform.
104///   Must be non-negative and finite; at least one must be positive.
105/// - `nb_trials`: number of independent optimisation attempts (≥ 1).
106/// - `seed`: PRNG seed for the node-order shuffle.
107///
108/// # Errors
109/// - [`IgraphError::InvalidArgument`] for malformed weights or
110///   `nb_trials < 1`.
111///
112/// # Examples
113///
114/// ```
115/// use rust_igraph::{Graph, infomap_with_options};
116///
117/// let mut g = Graph::with_vertices(6);
118/// for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
119///     g.add_edge(u, v).unwrap();
120/// }
121/// let a = infomap_with_options(&g, None, None, 3, 42).unwrap();
122/// let b = infomap_with_options(&g, None, None, 3, 42).unwrap();
123/// assert_eq!(a.membership, b.membership);
124/// ```
125pub 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
172// ──────────────────── Validation ────────────────────
173
174fn 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
221// ──────────────────── Internal types ────────────────────
222
223struct 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
231// ──────────────────── Flow computation ────────────────────
232
233fn 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    // Build weighted out-adjacency and compute out-strength
244    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    // Teleportation weights
262    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    // Compute PageRank
274    let node_flow = power_iteration_pagerank(n, &out_adj, &out_strength, &tele_weight)?;
275
276    // Build edge flows: for each edge u→v, flow = node_flow[u] * (1-τ) * w / out_strength[u]
277    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    // Build in-adjacency for iteration: in_adj[v] = list of (u, w/out_strength[u])
313    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
355// ──────────────────── Map equation ────────────────────
356
357fn plogp(x: f64) -> f64 {
358    if x > 0.0 { x * x.log2() } else { 0.0 }
359}
360
361/// Compute the map equation codelength from per-module exit flows and
362/// node flows.
363///
364/// L(M) = plogp(q↷) − 2·Σᵢ plogp(qᵢ) + Σᵢ plogp(qᵢ + pᵢ) − Σ_α plogp(p_α)
365fn 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
387/// Compute the exit flow for a single module from scratch.
388fn 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    // Teleportation exit: each node α in module i teleports outside with
414    // probability τ * (1 - tele_weight_sum_in_module). Weighted by node flow.
415    let tele_exit = TAU * mod_flow * (1.0 - tele_in_mod);
416    edge_exit + tele_exit
417}
418
419// ──────────────────── Greedy optimisation ────────────────────
420
421fn 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    // Each node starts in its own module
431    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            // Collect unique neighbour modules
459            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            // Current codelength contributions of the affected modules
478            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                    // Apply the move
492                    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                    // Recompute exit for affected modules
501                    // (old_mod, new_mod, and any module with edges to/from v)
502                    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
537/// Try moving node v from old_mod to each candidate module.
538/// Returns `Some((best_mod, delta_cl))` if an improvement exists.
539fn 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    // State before: module_exit[old_mod], module_flow[old_mod], etc.
552    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    // If v is the only node in old_mod, moving it would empty the module.
557    let old_becomes_empty = module_size[old_mod as usize] == 1;
558
559    // Compute what old_mod's exit would be after removing v
560    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        // What new_mod's exit would be after adding v
575        let new_exit_after = exit_after_add(v, new_mod, fs, membership);
576        let new_flow_after = new_flow_before + p_v;
577
578        // Change in total exit
579        let new_total_exit =
580            total_exit_before - old_exit_before + old_exit_after - new_exit_before + new_exit_after;
581
582        // Delta codelength:
583        // ΔL = plogp(new_total) - plogp(old_total)
584        //    + [-2·plogp(old_exit_after) + 2·plogp(old_exit_before)]
585        //    + [-2·plogp(new_exit_after) + 2·plogp(new_exit_before)]
586        //    + [plogp(old_exit_after + old_flow_after) - plogp(old_exit_before + old_flow_before)]
587        //    + [plogp(new_exit_after + new_flow_after) - plogp(new_exit_before + new_flow_before)]
588        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    // Add v itself
665    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
677// ──────────────────── Helpers ────────────────────
678
679fn 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        // Should find at least 2 communities
918        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}