1use std::collections::HashMap;
33
34use scirs2_core::random::{Rng, RngExt, SeedableRng, StdRng};
35
36use super::louvain::compact_communities;
37use crate::error::{GraphError, Result};
38
39#[derive(Debug, Clone)]
45pub struct InfomapConfig {
46 pub n_trials: usize,
48 pub max_iter: usize,
50 pub tol: f64,
52}
53
54impl Default for InfomapConfig {
55 fn default() -> Self {
56 Self {
57 n_trials: 10,
58 max_iter: 200,
59 tol: 1e-6,
60 }
61 }
62}
63
64struct SparseAdj {
69 adj: Vec<Vec<(usize, f64)>>,
70 degree: Vec<f64>,
71 two_m: f64,
72}
73
74impl SparseAdj {
75 fn from_edge_list(edges: &[(usize, usize, f64)], n: usize) -> Self {
76 let mut adj: Vec<Vec<(usize, f64)>> = vec![vec![]; n];
77 let mut degree = vec![0.0f64; n];
78 let mut two_m = 0.0f64;
79
80 for &(u, v, w) in edges {
81 if u >= n || v >= n {
82 continue;
83 }
84 adj[u].push((v, w));
85 if u != v {
86 adj[v].push((u, w));
87 }
88 degree[u] += w;
89 if u != v {
90 degree[v] += w;
91 }
92 two_m += 2.0 * w;
93 }
94 Self { adj, degree, two_m }
95 }
96
97 fn stationary(&self) -> Vec<f64> {
99 let total = self.two_m;
100 if total == 0.0 {
101 let n = self.adj.len();
102 return vec![1.0 / n.max(1) as f64; n];
103 }
104 self.degree.iter().map(|&d| d / total).collect()
105 }
106}
107
108fn map_equation(g: &SparseAdj, assignments: &[usize]) -> f64 {
116 let n = g.adj.len();
117 if n == 0 || g.two_m == 0.0 {
118 return 0.0;
119 }
120
121 let pi = g.stationary(); let n_comms = assignments.iter().max().copied().unwrap_or(0) + 1;
123
124 let mut module_visit: Vec<f64> = vec![0.0; n_comms];
126 let mut module_exit: Vec<f64> = vec![0.0; n_comms];
127
128 for node in 0..n {
129 let c = assignments[node];
130 if c >= n_comms {
131 continue;
132 }
133 module_visit[c] += pi[node];
134 let mut exit_w = 0.0f64;
136 let mut total_w = 0.0f64;
137 for &(nbr, w) in &g.adj[node] {
138 total_w += w;
139 if nbr < assignments.len() && assignments[nbr] != c {
140 exit_w += w;
141 }
142 }
143 if total_w > 0.0 {
144 module_exit[c] += pi[node] * exit_w / total_w;
145 }
146 }
147
148 let q_total: f64 = module_exit.iter().sum();
149
150 let h_q = if q_total > 0.0 {
152 -module_exit
153 .iter()
154 .filter(|&&q| q > 0.0)
155 .map(|&q| {
156 let p = q / q_total;
157 p * p.ln()
158 })
159 .sum::<f64>()
160 } else {
161 0.0
162 };
163
164 let mut h_modules = 0.0f64;
166 for i in 0..n_comms {
167 let p_stay = module_visit[i] - module_exit[i];
168 let p_total = module_visit[i] + module_exit[i]; if p_total <= 0.0 {
170 continue;
171 }
172 let mut within: Vec<f64> = Vec::new();
174 for node in 0..n {
175 if assignments[node] == i {
176 within.push(pi[node]);
177 }
178 }
179 within.push(module_exit[i]); let entropy: f64 = within
182 .iter()
183 .filter(|&&v| v > 0.0)
184 .map(|&v| {
185 let frac = v / p_total;
186 if frac > 0.0 {
187 -frac * frac.ln()
188 } else {
189 0.0
190 }
191 })
192 .sum();
193 h_modules += (p_total) * entropy;
194 let _ = p_stay;
195 }
196
197 q_total * h_q + h_modules
198}
199
200pub fn infomap(
214 adj: &[(usize, usize, f64)],
215 n_nodes: usize,
216 config: &InfomapConfig,
217) -> Result<Vec<usize>> {
218 if n_nodes == 0 {
219 return Err(GraphError::InvalidGraph(
220 "infomap: n_nodes must be > 0".into(),
221 ));
222 }
223
224 let g = SparseAdj::from_edge_list(adj, n_nodes);
225 if g.two_m == 0.0 {
226 return Ok((0..n_nodes).collect());
228 }
229
230 let mut best_assignments: Option<Vec<usize>> = None;
231 let mut best_code_length = f64::INFINITY;
232
233 for trial in 0..config.n_trials.max(1) {
234 let seed = 0xabcdef01_u64.wrapping_add(trial as u64 * 0x9e3779b9);
235 let result = infomap_trial(&g, config, seed)?;
236 let code_len = map_equation(&g, &result);
237 if code_len < best_code_length {
238 best_code_length = code_len;
239 best_assignments = Some(result);
240 }
241 }
242
243 let mut assignments = best_assignments
244 .ok_or_else(|| GraphError::AlgorithmError("infomap: no trials completed".into()))?;
245 compact_communities(&mut assignments);
246 Ok(assignments)
247}
248
249fn infomap_trial(g: &SparseAdj, config: &InfomapConfig, seed: u64) -> Result<Vec<usize>> {
254 let n = g.adj.len();
255 let mut rng = StdRng::seed_from_u64(seed);
256
257 let init_comms = ((n as f64).sqrt().ceil() as usize).max(1).min(n);
259 let mut assignments: Vec<usize> = (0..n).map(|_| rng.random_range(0..init_comms)).collect();
260
261 let mut prev_code_len = map_equation(g, &assignments);
262
263 for _iter in 0..config.max_iter {
264 let improved = infomap_move_phase(g, &mut assignments, &mut rng);
265 if !improved {
266 break;
267 }
268 compact_communities(&mut assignments);
269
270 let code_len = map_equation(g, &assignments);
271 if (prev_code_len - code_len).abs() < config.tol {
272 break;
273 }
274 prev_code_len = code_len;
275 }
276
277 Ok(assignments)
278}
279
280fn infomap_move_phase(g: &SparseAdj, assignments: &mut [usize], rng: &mut impl Rng) -> bool {
287 let n = g.adj.len();
288 let mut improved = false;
289
290 let mut order: Vec<usize> = (0..n).collect();
292 for i in (1..n).rev() {
293 let j = rng.random_range(0..=i);
294 order.swap(i, j);
295 }
296
297 let current_code = map_equation(g, assignments);
298 let mut running_code = current_code;
299
300 for &node in &order {
301 let orig_comm = assignments[node];
302
303 let mut candidate_comms: Vec<usize> = vec![orig_comm];
305 for &(nbr, _) in &g.adj[node] {
306 let c = assignments[nbr];
307 if !candidate_comms.contains(&c) {
308 candidate_comms.push(c);
309 }
310 }
311
312 let mut best_comm = orig_comm;
313 let mut best_code = running_code;
314
315 for c in candidate_comms {
316 if c == orig_comm {
317 continue;
318 }
319 assignments[node] = c;
320 let new_code = map_equation(g, assignments);
321 if new_code < best_code {
322 best_code = new_code;
323 best_comm = c;
324 }
325 }
326
327 assignments[node] = best_comm;
328 if best_comm != orig_comm {
329 running_code = best_code;
330 improved = true;
331 }
332 }
333 improved
334}
335
336#[cfg(test)]
341mod tests {
342 use super::*;
343
344 fn two_clique_edges(k: usize) -> (Vec<(usize, usize, f64)>, usize) {
345 let n = 2 * k;
346 let mut edges = Vec::new();
347 for i in 0..k {
348 for j in (i + 1)..k {
349 edges.push((i, j, 1.0));
350 edges.push((k + i, k + j, 1.0));
351 }
352 }
353 edges.push((0, k, 0.05));
354 (edges, n)
355 }
356
357 #[test]
358 fn test_infomap_two_cliques() {
359 let (edges, n) = two_clique_edges(4);
360 let config = InfomapConfig {
361 n_trials: 5,
362 max_iter: 50,
363 tol: 1e-6,
364 };
365 let labels = infomap(&edges, n, &config).expect("infomap");
366 assert_eq!(labels.len(), 8);
367 let l0 = labels[0];
369 for i in 1..4 {
370 assert_eq!(labels[i], l0, "clique1 node {} wrong", i);
371 }
372 let l1 = labels[4];
374 for i in 5..8 {
375 assert_eq!(labels[i], l1, "clique2 node {} wrong", i);
376 }
377 assert_ne!(l0, l1, "different communities expected");
378 }
379
380 #[test]
381 fn test_infomap_empty_error() {
382 let config = InfomapConfig::default();
383 assert!(infomap(&[], 0, &config).is_err());
384 }
385
386 #[test]
387 fn test_infomap_no_edges() {
388 let config = InfomapConfig {
389 n_trials: 1,
390 max_iter: 10,
391 tol: 1e-6,
392 };
393 let labels = infomap(&[], 4, &config).expect("infomap no edges");
394 assert_eq!(labels.len(), 4);
396 let unique: std::collections::HashSet<usize> = labels.iter().cloned().collect();
397 assert_eq!(unique.len(), 4);
398 }
399
400 #[test]
401 fn test_map_equation_perfect_partition() {
402 let (edges, n) = two_clique_edges(3);
403 let g = SparseAdj::from_edge_list(&edges, n);
404 let perfect: Vec<usize> = (0..6).map(|i| if i < 3 { 0 } else { 1 }).collect();
405 let single: Vec<usize> = vec![0; 6];
406 let code_perfect = map_equation(&g, &perfect);
407 let code_single = map_equation(&g, &single);
408 assert!(
410 code_perfect <= code_single + 1e-9,
411 "perfect partition code={code_perfect}, single={code_single}"
412 );
413 }
414
415 #[test]
416 fn test_default_config() {
417 let cfg = InfomapConfig::default();
418 assert_eq!(cfg.n_trials, 10);
419 assert_eq!(cfg.max_iter, 200);
420 assert!(cfg.tol < 1e-5);
421 }
422}