rust_igraph/algorithms/community/
leading_eigenvector.rs1use std::collections::VecDeque;
14
15use crate::algorithms::community::lanczos::lanczos_largest;
16use crate::core::rng::SplitMix64;
17use crate::core::{Graph, IgraphError, IgraphResult};
18
19#[derive(Debug, Clone)]
21pub struct LeadingEigenvectorResult {
22 pub membership: Vec<u32>,
24 pub eigenvalues: Vec<f64>,
27 pub merges: Vec<(u32, u32)>,
31 pub modularity: f64,
33}
34
35#[allow(clippy::too_many_lines)]
71pub fn leading_eigenvector(
72 graph: &Graph,
73 weights: Option<&[f64]>,
74 steps: Option<u32>,
75) -> IgraphResult<LeadingEigenvectorResult> {
76 let n = graph.vcount() as usize;
77 if n == 0 {
78 return Ok(LeadingEigenvectorResult {
79 membership: Vec::new(),
80 eigenvalues: Vec::new(),
81 merges: Vec::new(),
82 modularity: 0.0,
83 });
84 }
85
86 let ecount = graph.ecount();
87 if let Some(w) = weights {
88 if w.len() != ecount {
89 return Err(IgraphError::InvalidArgument(format!(
90 "weights length ({}) differs from edge count ({ecount})",
91 w.len()
92 )));
93 }
94 for &wv in w {
95 if !wv.is_finite() {
96 return Err(IgraphError::InvalidArgument(
97 "edge weights must be finite".to_string(),
98 ));
99 }
100 }
101 }
102
103 let max_steps = match steps {
104 Some(s) => s as usize,
105 None => {
106 if n > 0 {
107 n - 1
108 } else {
109 0
110 }
111 }
112 };
113
114 let adj = build_adjacency(graph);
116
117 let (deg_or_strength, two_m) = compute_degrees(graph, weights, &adj);
119
120 if two_m <= 0.0 {
121 return Ok(LeadingEigenvectorResult {
122 membership: vec![0; n],
123 eigenvalues: Vec::new(),
124 merges: Vec::new(),
125 modularity: 0.0,
126 });
127 }
128
129 let cc = crate::algorithms::connectivity::components::connected_components(graph)?;
131 let mut membership: Vec<u32> = cc.membership.clone();
132 let mut communities = cc.count;
133
134 let mut eigenvalues_out = Vec::new();
135 let mut merges_out = Vec::new();
136
137 let mut to_split: VecDeque<u32> = VecDeque::new();
139 for c in 0..communities {
140 let size = membership.iter().filter(|&&m| m == c).count();
141 if size > 2 {
142 to_split.push_back(c);
143 }
144 }
145
146 for c in 1..communities {
148 merges_out.push((c - 1, c));
149 eigenvalues_out.push(f64::NAN);
150 }
151 let mut steps_taken = (communities as usize).saturating_sub(1);
152
153 let mut rng = SplitMix64::new(42);
154
155 while let Some(comm) = to_split.pop_back() {
156 if steps_taken >= max_steps {
157 break;
158 }
159
160 let idx: Vec<usize> = (0..n).filter(|&i| membership[i] == comm).collect();
162 let size = idx.len();
163
164 steps_taken += 1;
165
166 if size <= 2 {
167 continue;
168 }
169
170 let mut idx2 = vec![0usize; n];
172 for (pos, &v) in idx.iter().enumerate() {
173 idx2[v] = pos;
174 }
175
176 let matvec = |x: &[f64], y: &mut [f64]| {
178 modularity_matvec(
179 graph,
180 weights,
181 &adj,
182 °_or_strength,
183 two_m,
184 &membership,
185 comm,
186 &idx,
187 &idx2,
188 x,
189 y,
190 );
191 };
192
193 let mut start_vec = vec![0.0_f64; size];
195 for (i, sv) in start_vec.iter_mut().enumerate() {
196 let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
197 let perturb = (rng.gen_unit() - 0.5) * 0.2;
198 *sv = sign + perturb;
199 }
200 for i in (1..size).rev() {
202 let j = rng.gen_index(i + 1);
203 start_vec.swap(i, j);
204 }
205
206 let result = lanczos_largest(size, &matvec, &mut start_vec, 10000);
207
208 let eigenvalue = if result.eigenvalue.abs() < 1e-8 {
210 0.0
211 } else {
212 result.eigenvalue
213 };
214
215 let mut eigvec = result.eigenvector;
216 for v in &mut eigvec {
217 if v.abs() < 1e-8 {
218 *v = 0.0;
219 }
220 }
221
222 if let Some(first_nonzero) = eigvec.iter().find(|&&v| v != 0.0) {
224 if *first_nonzero < 0.0 {
225 for v in &mut eigvec {
226 *v = -*v;
227 }
228 }
229 }
230
231 eigenvalues_out.push(eigenvalue);
232
233 if eigenvalue <= 0.0 {
234 continue;
235 }
236
237 let neg_count = eigvec.iter().filter(|&&v| v < 0.0).count();
239 if neg_count == 0 || neg_count == size {
240 continue;
241 }
242
243 let mut sign_vec = vec![0.0_f64; size];
246 for (i, &v) in eigvec.iter().enumerate() {
247 sign_vec[i] = if v < 0.0 { -1.0 } else { 1.0 };
248 }
249 let mut bx = vec![0.0_f64; size];
250 matvec(&sign_vec, &mut bx);
251 let mod_increase: f64 = sign_vec.iter().zip(bx.iter()).map(|(s, b)| s * b).sum();
252 if mod_increase <= 1e-8 {
253 continue;
254 }
255
256 let new_comm = communities;
258 communities += 1;
259
260 for (j, &v) in eigvec.iter().enumerate() {
261 if v < 0.0 {
262 membership[idx[j]] = new_comm as u32;
263 }
264 }
265
266 merges_out.push((comm, new_comm as u32));
267
268 let pos_count = size - neg_count;
270 if neg_count > 1 {
271 to_split.push_back(new_comm as u32);
272 }
273 if pos_count > 1 {
274 to_split.push_back(comm);
275 }
276 }
277
278 let mod_val = compute_modularity(graph, weights, &membership, °_or_strength, two_m);
280
281 Ok(LeadingEigenvectorResult {
282 membership,
283 eigenvalues: eigenvalues_out,
284 merges: merges_out,
285 modularity: mod_val,
286 })
287}
288
289pub fn leading_eigenvector_weighted(
308 graph: &Graph,
309 weights: &[f64],
310 steps: Option<u32>,
311) -> IgraphResult<LeadingEigenvectorResult> {
312 leading_eigenvector(graph, Some(weights), steps)
313}
314
315type AdjList = Vec<Vec<(usize, usize)>>; fn build_adjacency(graph: &Graph) -> AdjList {
320 let n = graph.vcount() as usize;
321 let mut adj: AdjList = vec![Vec::new(); n];
322 for eid in 0..graph.ecount() {
323 #[allow(clippy::cast_possible_truncation)]
324 let eid32 = eid as u32;
325 let Ok(s) = graph.edge_source(eid32) else {
326 continue;
327 };
328 let Ok(t) = graph.edge_target(eid32) else {
329 continue;
330 };
331 let s = s as usize;
332 let t = t as usize;
333 adj[s].push((t, eid));
334 if s != t {
335 adj[t].push((s, eid));
336 }
337 }
338 adj
339}
340
341#[allow(clippy::cast_precision_loss)]
342fn compute_degrees(graph: &Graph, weights: Option<&[f64]>, adj: &AdjList) -> (Vec<f64>, f64) {
343 let n = graph.vcount() as usize;
344 let mut deg = vec![0.0_f64; n];
345 let mut total = 0.0_f64;
346
347 match weights {
348 None => {
349 for v in 0..n {
350 let d = adj[v].len() as f64;
351 deg[v] = d;
352 total += d;
353 }
354 }
355 Some(w) => {
356 for v in 0..n {
357 let mut s = 0.0_f64;
358 for &(_, eid) in &adj[v] {
359 s += w[eid];
360 }
361 deg[v] = s;
362 total += s;
363 }
364 }
365 }
366
367 (deg, total)
368}
369
370#[allow(clippy::too_many_arguments)]
377fn modularity_matvec(
378 _graph: &Graph,
379 weights: Option<&[f64]>,
380 adj: &AdjList,
381 deg: &[f64],
382 two_m: f64,
383 membership: &[u32],
384 comm: u32,
385 idx: &[usize],
386 idx2: &[usize],
387 x: &[f64],
388 y: &mut [f64],
389) {
390 let size = idx.len();
391 let inv_2m = 1.0 / two_m;
392 let mut tmp = vec![0.0_f64; size];
393
394 for j in 0..size {
396 let v = idx[j];
397 y[j] = 0.0;
398 tmp[j] = 0.0;
399 for &(nei, eid) in &adj[v] {
400 if membership[nei] == comm {
401 let w = match weights {
402 Some(wt) => wt[eid],
403 None => 1.0,
404 };
405 y[j] += x[idx2[nei]] * w;
406 tmp[j] += w;
407 }
408 }
409 }
410
411 let mut ktx = 0.0_f64;
413 let mut ktx2 = 0.0_f64;
414 for j in 0..size {
415 let v = idx[j];
416 ktx += x[j] * deg[v];
417 ktx2 += deg[v];
418 }
419 ktx *= inv_2m;
420 ktx2 *= inv_2m;
421
422 for j in 0..size {
424 let v = idx[j];
425 y[j] -= ktx * deg[v];
426 tmp[j] -= ktx2 * deg[v];
427 }
428
429 for j in 0..size {
432 y[j] -= tmp[j] * x[j];
433 }
434}
435
436fn compute_modularity(
437 graph: &Graph,
438 weights: Option<&[f64]>,
439 membership: &[u32],
440 deg: &[f64],
441 two_m: f64,
442) -> f64 {
443 if two_m <= 0.0 {
444 return 0.0;
445 }
446 let inv_2m = 1.0 / two_m;
447 let mut q = 0.0_f64;
448
449 for eid in 0..graph.ecount() {
450 #[allow(clippy::cast_possible_truncation)]
451 let eid32 = eid as u32;
452 let Ok(s) = graph.edge_source(eid32) else {
453 continue;
454 };
455 let Ok(t) = graph.edge_target(eid32) else {
456 continue;
457 };
458 let s = s as usize;
459 let t = t as usize;
460 if membership[s] == membership[t] {
461 let w = match weights {
462 Some(wt) => wt[eid],
463 None => 1.0,
464 };
465 if s == t {
466 q += w - deg[s] * deg[t] * inv_2m;
467 } else {
468 q += 2.0 * (w - deg[s] * deg[t] * inv_2m);
469 }
470 }
471 }
472
473 q * inv_2m
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn empty_graph() {
482 let g = Graph::with_vertices(0);
483 let r = leading_eigenvector(&g, None, None).unwrap();
484 assert!(r.membership.is_empty());
485 }
486
487 #[test]
488 fn single_vertex() {
489 let g = Graph::with_vertices(1);
490 let r = leading_eigenvector(&g, None, None).unwrap();
491 assert_eq!(r.membership, vec![0]);
492 }
493
494 #[test]
495 fn two_components() {
496 let mut g = Graph::with_vertices(4);
498 g.add_edge(0, 1).unwrap();
499 g.add_edge(2, 3).unwrap();
500 let r = leading_eigenvector(&g, None, None).unwrap();
501 assert_eq!(r.membership[0], r.membership[1]);
502 assert_eq!(r.membership[2], r.membership[3]);
503 assert_ne!(r.membership[0], r.membership[2]);
504 }
505
506 #[test]
507 fn barbell_finds_two_communities() {
508 let mut g = Graph::with_vertices(6);
509 g.add_edge(0, 1).unwrap();
511 g.add_edge(1, 2).unwrap();
512 g.add_edge(0, 2).unwrap();
513 g.add_edge(3, 4).unwrap();
515 g.add_edge(4, 5).unwrap();
516 g.add_edge(3, 5).unwrap();
517 g.add_edge(2, 3).unwrap();
519
520 let r = leading_eigenvector(&g, None, None).unwrap();
521 assert_eq!(r.membership[0], r.membership[1]);
523 assert_eq!(r.membership[0], r.membership[2]);
524 assert_eq!(r.membership[3], r.membership[4]);
525 assert_eq!(r.membership[3], r.membership[5]);
526 assert_ne!(r.membership[0], r.membership[3]);
528 }
529
530 #[test]
531 fn complete_graph_no_split() {
532 let mut g = Graph::with_vertices(5);
534 for i in 0..5u32 {
535 for j in (i + 1)..5 {
536 g.add_edge(i, j).unwrap();
537 }
538 }
539 let r = leading_eigenvector(&g, None, None).unwrap();
540 let c = r.membership[0];
542 for &m in &r.membership {
543 assert_eq!(m, c, "K5 should not be split");
544 }
545 }
546
547 #[test]
548 fn weighted_barbell() {
549 let mut g = Graph::with_vertices(6);
550 g.add_edge(0, 1).unwrap();
552 g.add_edge(1, 2).unwrap();
553 g.add_edge(0, 2).unwrap();
554 g.add_edge(3, 4).unwrap();
556 g.add_edge(4, 5).unwrap();
557 g.add_edge(3, 5).unwrap();
558 g.add_edge(2, 3).unwrap();
560
561 let weights = vec![5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 0.1];
562 let r = leading_eigenvector(&g, Some(&weights), None).unwrap();
563 assert_eq!(r.membership[0], r.membership[1]);
564 assert_eq!(r.membership[3], r.membership[4]);
565 assert_ne!(r.membership[0], r.membership[3]);
566 }
567
568 #[test]
569 fn steps_limit() {
570 let mut g = Graph::with_vertices(6);
571 g.add_edge(0, 1).unwrap();
572 g.add_edge(1, 2).unwrap();
573 g.add_edge(0, 2).unwrap();
574 g.add_edge(3, 4).unwrap();
575 g.add_edge(4, 5).unwrap();
576 g.add_edge(3, 5).unwrap();
577 g.add_edge(2, 3).unwrap();
578
579 let r = leading_eigenvector(&g, None, Some(0)).unwrap();
581 let c = r.membership[0];
583 for &m in &r.membership {
584 assert_eq!(m, c);
585 }
586 }
587
588 #[test]
589 fn modularity_is_positive_for_good_split() {
590 let mut g = Graph::with_vertices(6);
591 g.add_edge(0, 1).unwrap();
592 g.add_edge(1, 2).unwrap();
593 g.add_edge(0, 2).unwrap();
594 g.add_edge(3, 4).unwrap();
595 g.add_edge(4, 5).unwrap();
596 g.add_edge(3, 5).unwrap();
597 g.add_edge(2, 3).unwrap();
598
599 let r = leading_eigenvector(&g, None, None).unwrap();
600 assert!(
601 r.modularity > 0.0,
602 "modularity should be positive for barbell, got {}",
603 r.modularity
604 );
605 }
606}