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 s = graph.edge_source(eid32).unwrap() as usize;
326 let t = graph.edge_target(eid32).unwrap() as usize;
327 adj[s].push((t, eid));
328 if s != t {
329 adj[t].push((s, eid));
330 }
331 }
332 adj
333}
334
335#[allow(clippy::cast_precision_loss)]
336fn compute_degrees(graph: &Graph, weights: Option<&[f64]>, adj: &AdjList) -> (Vec<f64>, f64) {
337 let n = graph.vcount() as usize;
338 let mut deg = vec![0.0_f64; n];
339 let mut total = 0.0_f64;
340
341 match weights {
342 None => {
343 for v in 0..n {
344 let d = adj[v].len() as f64;
345 deg[v] = d;
346 total += d;
347 }
348 }
349 Some(w) => {
350 for v in 0..n {
351 let mut s = 0.0_f64;
352 for &(_, eid) in &adj[v] {
353 s += w[eid];
354 }
355 deg[v] = s;
356 total += s;
357 }
358 }
359 }
360
361 (deg, total)
362}
363
364#[allow(clippy::too_many_arguments)]
371fn modularity_matvec(
372 _graph: &Graph,
373 weights: Option<&[f64]>,
374 adj: &AdjList,
375 deg: &[f64],
376 two_m: f64,
377 membership: &[u32],
378 comm: u32,
379 idx: &[usize],
380 idx2: &[usize],
381 x: &[f64],
382 y: &mut [f64],
383) {
384 let size = idx.len();
385 let inv_2m = 1.0 / two_m;
386 let mut tmp = vec![0.0_f64; size];
387
388 for j in 0..size {
390 let v = idx[j];
391 y[j] = 0.0;
392 tmp[j] = 0.0;
393 for &(nei, eid) in &adj[v] {
394 if membership[nei] == comm {
395 let w = match weights {
396 Some(wt) => wt[eid],
397 None => 1.0,
398 };
399 y[j] += x[idx2[nei]] * w;
400 tmp[j] += w;
401 }
402 }
403 }
404
405 let mut ktx = 0.0_f64;
407 let mut ktx2 = 0.0_f64;
408 for j in 0..size {
409 let v = idx[j];
410 ktx += x[j] * deg[v];
411 ktx2 += deg[v];
412 }
413 ktx *= inv_2m;
414 ktx2 *= inv_2m;
415
416 for j in 0..size {
418 let v = idx[j];
419 y[j] -= ktx * deg[v];
420 tmp[j] -= ktx2 * deg[v];
421 }
422
423 for j in 0..size {
426 y[j] -= tmp[j] * x[j];
427 }
428}
429
430fn compute_modularity(
431 graph: &Graph,
432 weights: Option<&[f64]>,
433 membership: &[u32],
434 deg: &[f64],
435 two_m: f64,
436) -> f64 {
437 if two_m <= 0.0 {
438 return 0.0;
439 }
440 let inv_2m = 1.0 / two_m;
441 let mut q = 0.0_f64;
442
443 for eid in 0..graph.ecount() {
444 #[allow(clippy::cast_possible_truncation)]
445 let eid32 = eid as u32;
446 let s = graph.edge_source(eid32).unwrap() as usize;
447 let t = graph.edge_target(eid32).unwrap() as usize;
448 if membership[s] == membership[t] {
449 let w = match weights {
450 Some(wt) => wt[eid],
451 None => 1.0,
452 };
453 if s == t {
454 q += w - deg[s] * deg[t] * inv_2m;
455 } else {
456 q += 2.0 * (w - deg[s] * deg[t] * inv_2m);
457 }
458 }
459 }
460
461 q * inv_2m
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn empty_graph() {
470 let g = Graph::with_vertices(0);
471 let r = leading_eigenvector(&g, None, None).unwrap();
472 assert!(r.membership.is_empty());
473 }
474
475 #[test]
476 fn single_vertex() {
477 let g = Graph::with_vertices(1);
478 let r = leading_eigenvector(&g, None, None).unwrap();
479 assert_eq!(r.membership, vec![0]);
480 }
481
482 #[test]
483 fn two_components() {
484 let mut g = Graph::with_vertices(4);
486 g.add_edge(0, 1).unwrap();
487 g.add_edge(2, 3).unwrap();
488 let r = leading_eigenvector(&g, None, None).unwrap();
489 assert_eq!(r.membership[0], r.membership[1]);
490 assert_eq!(r.membership[2], r.membership[3]);
491 assert_ne!(r.membership[0], r.membership[2]);
492 }
493
494 #[test]
495 fn barbell_finds_two_communities() {
496 let mut g = Graph::with_vertices(6);
497 g.add_edge(0, 1).unwrap();
499 g.add_edge(1, 2).unwrap();
500 g.add_edge(0, 2).unwrap();
501 g.add_edge(3, 4).unwrap();
503 g.add_edge(4, 5).unwrap();
504 g.add_edge(3, 5).unwrap();
505 g.add_edge(2, 3).unwrap();
507
508 let r = leading_eigenvector(&g, None, None).unwrap();
509 assert_eq!(r.membership[0], r.membership[1]);
511 assert_eq!(r.membership[0], r.membership[2]);
512 assert_eq!(r.membership[3], r.membership[4]);
513 assert_eq!(r.membership[3], r.membership[5]);
514 assert_ne!(r.membership[0], r.membership[3]);
516 }
517
518 #[test]
519 fn complete_graph_no_split() {
520 let mut g = Graph::with_vertices(5);
522 for i in 0..5u32 {
523 for j in (i + 1)..5 {
524 g.add_edge(i, j).unwrap();
525 }
526 }
527 let r = leading_eigenvector(&g, None, None).unwrap();
528 let c = r.membership[0];
530 for &m in &r.membership {
531 assert_eq!(m, c, "K5 should not be split");
532 }
533 }
534
535 #[test]
536 fn weighted_barbell() {
537 let mut g = Graph::with_vertices(6);
538 g.add_edge(0, 1).unwrap();
540 g.add_edge(1, 2).unwrap();
541 g.add_edge(0, 2).unwrap();
542 g.add_edge(3, 4).unwrap();
544 g.add_edge(4, 5).unwrap();
545 g.add_edge(3, 5).unwrap();
546 g.add_edge(2, 3).unwrap();
548
549 let weights = vec![5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 0.1];
550 let r = leading_eigenvector(&g, Some(&weights), None).unwrap();
551 assert_eq!(r.membership[0], r.membership[1]);
552 assert_eq!(r.membership[3], r.membership[4]);
553 assert_ne!(r.membership[0], r.membership[3]);
554 }
555
556 #[test]
557 fn steps_limit() {
558 let mut g = Graph::with_vertices(6);
559 g.add_edge(0, 1).unwrap();
560 g.add_edge(1, 2).unwrap();
561 g.add_edge(0, 2).unwrap();
562 g.add_edge(3, 4).unwrap();
563 g.add_edge(4, 5).unwrap();
564 g.add_edge(3, 5).unwrap();
565 g.add_edge(2, 3).unwrap();
566
567 let r = leading_eigenvector(&g, None, Some(0)).unwrap();
569 let c = r.membership[0];
571 for &m in &r.membership {
572 assert_eq!(m, c);
573 }
574 }
575
576 #[test]
577 fn modularity_is_positive_for_good_split() {
578 let mut g = Graph::with_vertices(6);
579 g.add_edge(0, 1).unwrap();
580 g.add_edge(1, 2).unwrap();
581 g.add_edge(0, 2).unwrap();
582 g.add_edge(3, 4).unwrap();
583 g.add_edge(4, 5).unwrap();
584 g.add_edge(3, 5).unwrap();
585 g.add_edge(2, 3).unwrap();
586
587 let r = leading_eigenvector(&g, None, None).unwrap();
588 assert!(
589 r.modularity > 0.0,
590 "modularity should be positive for barbell, got {}",
591 r.modularity
592 );
593 }
594}