rust_igraph/algorithms/properties/
spectral_gap_ratios.rs1#![allow(
13 clippy::cast_lossless,
14 clippy::cast_possible_truncation,
15 clippy::cast_precision_loss,
16 clippy::many_single_char_names,
17 clippy::needless_range_loop,
18 clippy::similar_names,
19 clippy::too_many_lines
20)]
21
22use crate::core::{Graph, IgraphResult};
23
24pub fn adjacency_spectral_gap_ratio(graph: &Graph) -> IgraphResult<f64> {
51 let n = graph.vcount() as usize;
52 if n < 2 {
53 return Ok(0.0);
54 }
55
56 let eigs = top_two_eigenvalues(graph)?;
57 let (lambda1, lambda2) = eigs;
58
59 if lambda1.abs() < 1e-12 {
60 return Ok(0.0);
61 }
62
63 Ok((lambda1 - lambda2) / lambda1)
64}
65
66pub fn spectral_radius_ratio(graph: &Graph) -> IgraphResult<f64> {
84 let n = graph.vcount() as usize;
85 if n < 2 {
86 return Ok(0.0);
87 }
88
89 let lambda1 = largest_eigenvalue(graph)?;
90 if lambda1.abs() < 1e-12 {
91 return Ok(0.0);
92 }
93
94 let mut max_deg = 0_usize;
95 for v in 0..n {
96 let d = graph.degree(v as u32)?;
97 if d > max_deg {
98 max_deg = d;
99 }
100 }
101
102 if max_deg == 0 {
103 return Ok(0.0);
104 }
105
106 let bound = ((max_deg as f64) * ((n - 1) as f64)).sqrt();
107 Ok(lambda1 / bound)
108}
109
110pub fn energy_ratio(graph: &Graph) -> IgraphResult<f64> {
131 let n = graph.vcount() as usize;
132 if n < 2 {
133 return Ok(0.0);
134 }
135
136 let m = graph.ecount();
137 if m == 0 {
138 return Ok(0.0);
139 }
140
141 let eigenvalues = all_eigenvalues(graph)?;
142 let energy: f64 = eigenvalues.iter().map(|e| e.abs()).sum();
143
144 let baseline = (n as f64) * (2.0 * m as f64 / n as f64).sqrt();
145 if baseline < 1e-12 {
146 return Ok(0.0);
147 }
148
149 Ok(energy / baseline)
150}
151
152fn largest_eigenvalue(graph: &Graph) -> IgraphResult<f64> {
154 let n = graph.vcount() as usize;
155 if n == 0 {
156 return Ok(0.0);
157 }
158
159 let mut x = vec![1.0_f64 / (n as f64).sqrt(); n];
160 let mut y = vec![0.0_f64; n];
161
162 for _ in 0..200 {
163 for i in 0..n {
164 y[i] = 0.0;
165 }
166 for v in 0..n {
167 let nbrs = graph.neighbors(v as u32)?;
168 for &u in &nbrs {
169 y[v] += x[u as usize];
170 }
171 }
172
173 let norm: f64 = y.iter().map(|v| v * v).sum::<f64>().sqrt();
174 if norm < 1e-30 {
175 return Ok(0.0);
176 }
177 for i in 0..n {
178 x[i] = y[i] / norm;
179 }
180 }
181
182 let mut lambda = 0.0_f64;
183 for v in 0..n {
184 let nbrs = graph.neighbors(v as u32)?;
185 let mut ax_v = 0.0_f64;
186 for &u in &nbrs {
187 ax_v += x[u as usize];
188 }
189 lambda += x[v] * ax_v;
190 }
191
192 Ok(lambda)
193}
194
195fn top_two_eigenvalues(graph: &Graph) -> IgraphResult<(f64, f64)> {
197 let n = graph.vcount() as usize;
198 if n < 2 {
199 return Ok((0.0, 0.0));
200 }
201
202 let lambda1 = largest_eigenvalue(graph)?;
203
204 let mut x1 = vec![1.0_f64 / (n as f64).sqrt(); n];
206 let mut y = vec![0.0_f64; n];
207
208 for _ in 0..200 {
209 for i in 0..n {
210 y[i] = 0.0;
211 }
212 for v in 0..n {
213 let nbrs = graph.neighbors(v as u32)?;
214 for &u in &nbrs {
215 y[v] += x1[u as usize];
216 }
217 }
218 let norm: f64 = y.iter().map(|v| v * v).sum::<f64>().sqrt();
219 if norm < 1e-30 {
220 return Ok((lambda1, 0.0));
221 }
222 for i in 0..n {
223 x1[i] = y[i] / norm;
224 }
225 }
226
227 let mut x2 = vec![0.0_f64; n];
229 x2[0] = -x1[1];
231 x2[1] = x1[0];
232 let norm: f64 = x2.iter().map(|v| v * v).sum::<f64>().sqrt();
233 if norm < 1e-30 {
234 for i in 0..n {
235 x2[i] = if i == 0 { 1.0 } else { 0.0 };
236 }
237 } else {
238 for i in 0..n {
239 x2[i] /= norm;
240 }
241 }
242
243 for _ in 0..300 {
244 for i in 0..n {
245 y[i] = 0.0;
246 }
247 for v in 0..n {
249 let nbrs = graph.neighbors(v as u32)?;
250 for &u in &nbrs {
251 y[v] += x2[u as usize];
252 }
253 }
254 let dot: f64 = x1.iter().zip(x2.iter()).map(|(a, b)| a * b).sum();
256 for i in 0..n {
257 y[i] -= lambda1 * dot * x1[i];
258 }
259
260 let norm: f64 = y.iter().map(|v| v * v).sum::<f64>().sqrt();
261 if norm < 1e-30 {
262 return Ok((lambda1, 0.0));
263 }
264 for i in 0..n {
265 x2[i] = y[i] / norm;
266 }
267 }
268
269 let mut ax2 = vec![0.0_f64; n];
271 for v in 0..n {
272 let nbrs = graph.neighbors(v as u32)?;
273 for &u in &nbrs {
274 ax2[v] += x2[u as usize];
275 }
276 }
277 let dot_x1_x2: f64 = x1.iter().zip(x2.iter()).map(|(a, b)| a * b).sum();
278 for i in 0..n {
279 ax2[i] -= lambda1 * dot_x1_x2 * x1[i];
280 }
281 let lambda2: f64 = x2.iter().zip(ax2.iter()).map(|(a, b)| a * b).sum();
282
283 Ok((lambda1, lambda2))
284}
285
286fn all_eigenvalues(graph: &Graph) -> IgraphResult<Vec<f64>> {
290 let n = graph.vcount() as usize;
291 if n == 0 {
292 return Ok(Vec::new());
293 }
294 if n == 1 {
295 return Ok(vec![0.0]);
296 }
297
298 let mut a = vec![0.0_f64; n * n];
300 for v in 0..n {
301 let nbrs = graph.neighbors(v as u32)?;
302 for &u in &nbrs {
303 a[v * n + u as usize] = 1.0;
304 }
305 }
306
307 let mut diag = vec![0.0_f64; n];
309 let mut offdiag = vec![0.0_f64; n];
310 tridiagonalize(&mut a, n, &mut diag, &mut offdiag);
311
312 tql2(&mut diag, &mut offdiag, n);
314
315 Ok(diag)
316}
317
318fn tridiagonalize(a: &mut [f64], n: usize, d: &mut [f64], e: &mut [f64]) {
320 for i in (1..n).rev() {
321 let l = i - 1;
322 let mut h = 0.0_f64;
323 let mut scale = 0.0_f64;
324
325 if l > 0 {
326 for k in 0..=l {
327 scale += a[i * n + k].abs();
328 }
329 if scale < 1e-30 {
330 e[i] = a[i * n + l];
331 } else {
332 for k in 0..=l {
333 a[i * n + k] /= scale;
334 h += a[i * n + k] * a[i * n + k];
335 }
336 let mut f = a[i * n + l];
337 let g = if f >= 0.0 { -h.sqrt() } else { h.sqrt() };
338 e[i] = scale * g;
339 h -= f * g;
340 a[i * n + l] = f - g;
341 f = 0.0;
342 for j in 0..=l {
343 a[j * n + i] = a[i * n + j] / h;
344 let mut g_val = 0.0_f64;
345 for k in 0..=j {
346 g_val += a[j * n + k] * a[i * n + k];
347 }
348 for k in (j + 1)..=l {
349 g_val += a[k * n + j] * a[i * n + k];
350 }
351 e[j] = g_val / h;
352 f += e[j] * a[i * n + j];
353 }
354 let hh = f / (h + h);
355 for j in 0..=l {
356 f = a[i * n + j];
357 let g_val = e[j] - hh * f;
358 e[j] = g_val;
359 for k in 0..=j {
360 a[j * n + k] -= f * e[k] + g_val * a[i * n + k];
361 }
362 }
363 }
364 } else {
365 e[i] = a[i * n + l];
366 }
367 d[i] = h;
368 }
369
370 d[0] = 0.0;
371 e[0] = 0.0;
372
373 for i in 0..n {
374 d[i] = a[i * n + i];
375 }
376}
377
378fn tql2(d: &mut [f64], e: &mut [f64], n: usize) {
380 for i in 1..n {
381 e[i - 1] = e[i];
382 }
383 e[n - 1] = 0.0;
384
385 let mut f = 0.0_f64;
386 let mut tst1 = 0.0_f64;
387 let eps = 1e-15_f64;
388
389 for l in 0..n {
390 tst1 = tst1.max(d[l].abs() + e[l].abs());
391 let mut m = l;
392 while m < n {
393 if e[m].abs() <= eps * tst1 {
394 break;
395 }
396 m += 1;
397 }
398
399 if m > l {
400 let mut iter_count = 0_u32;
401 loop {
402 iter_count += 1;
403 if iter_count > 300 {
404 break;
405 }
406
407 let mut g = d[l];
408 let mut p = (d[l + 1] - g) / (2.0 * e[l]);
409 let mut r = (p * p + 1.0_f64).sqrt();
410 if p < 0.0 {
411 r = -r;
412 }
413 d[l] = e[l] / (p + r);
414 d[l + 1] = e[l] * (p + r);
415 let dl1 = d[l + 1];
416 let mut h = g - d[l];
417 for i in (l + 2)..n {
418 d[i] -= h;
419 }
420 f += h;
421
422 p = d[m];
423 let mut c = 1.0_f64;
424 let mut c2 = c;
425 let mut c3 = c;
426 let el1 = e[l + 1];
427 let mut s = 0.0_f64;
428 let mut s2 = 0.0_f64;
429
430 let mut i = m;
431 while i > l {
432 i -= 1;
433 c3 = c2;
434 c2 = c;
435 s2 = s;
436 g = c * e[i];
437 h = c * p;
438 r = (p * p + e[i] * e[i]).sqrt();
439 e[i + 1] = s * r;
440 s = e[i] / r;
441 c = p / r;
442 p = c * d[i] - s * g;
443 d[i + 1] = h + s * (c * g + s * d[i]);
444 }
445
446 p = -s * s2 * c3 * el1 * e[l] / dl1;
447 e[l] = s * p;
448 d[l] = c * p;
449
450 if e[l].abs() <= eps * tst1 {
451 break;
452 }
453 }
454 }
455 d[l] += f;
456 e[l] = 0.0;
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463
464 fn empty() -> Graph {
465 Graph::with_vertices(0)
466 }
467
468 fn single() -> Graph {
469 Graph::with_vertices(1)
470 }
471
472 fn single_edge() -> Graph {
473 Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
474 }
475
476 fn path3() -> Graph {
477 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
478 }
479
480 fn k3() -> Graph {
481 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
482 }
483
484 fn k4() -> Graph {
485 Graph::from_edges(
486 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
487 false,
488 Some(4),
489 )
490 .unwrap()
491 }
492
493 fn cycle4() -> Graph {
494 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
495 }
496
497 fn star5() -> Graph {
498 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
499 }
500
501 fn paw() -> Graph {
502 Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
503 }
504
505 #[test]
508 fn sgr_empty() {
509 assert!(adjacency_spectral_gap_ratio(&empty()).unwrap().abs() < 1e-10);
510 }
511
512 #[test]
513 fn sgr_single() {
514 assert!(adjacency_spectral_gap_ratio(&single()).unwrap().abs() < 1e-10);
515 }
516
517 #[test]
518 fn sgr_single_edge() {
519 let r = adjacency_spectral_gap_ratio(&single_edge()).unwrap();
521 assert!((r - 2.0).abs() < 0.1);
522 }
523
524 #[test]
525 fn sgr_k3() {
526 let r = adjacency_spectral_gap_ratio(&k3()).unwrap();
528 assert!((r - 1.5).abs() < 0.1);
529 }
530
531 #[test]
532 fn sgr_k4() {
533 let r = adjacency_spectral_gap_ratio(&k4()).unwrap();
535 assert!((r - 4.0 / 3.0).abs() < 0.1);
536 }
537
538 #[test]
539 fn sgr_positive_connected() {
540 for g in &[single_edge(), k3(), k4(), cycle4(), star5(), paw()] {
541 let r = adjacency_spectral_gap_ratio(g).unwrap();
542 assert!(
543 r > 0.0,
544 "spectral gap ratio should be positive for connected graphs"
545 );
546 }
547 }
548
549 #[test]
552 fn srr_empty() {
553 assert!(spectral_radius_ratio(&empty()).unwrap().abs() < 1e-10);
554 }
555
556 #[test]
557 fn srr_single() {
558 assert!(spectral_radius_ratio(&single()).unwrap().abs() < 1e-10);
559 }
560
561 #[test]
562 fn srr_k3() {
563 let r = spectral_radius_ratio(&k3()).unwrap();
565 assert!((r - 1.0).abs() < 0.05);
566 }
567
568 #[test]
569 fn srr_single_edge() {
570 let r = spectral_radius_ratio(&single_edge()).unwrap();
572 assert!((r - 1.0).abs() < 0.05);
573 }
574
575 #[test]
576 fn srr_in_01() {
577 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
578 let r = spectral_radius_ratio(g).unwrap();
579 assert!(r >= -0.01);
580 assert!(r <= 1.01);
581 }
582 }
583
584 #[test]
587 fn er_empty() {
588 assert!(energy_ratio(&empty()).unwrap().abs() < 1e-10);
589 }
590
591 #[test]
592 fn er_single() {
593 assert!(energy_ratio(&single()).unwrap().abs() < 1e-10);
594 }
595
596 #[test]
597 fn er_k3() {
598 let r = energy_ratio(&k3()).unwrap();
602 assert!((r - 4.0 / (3.0 * 2.0_f64.sqrt())).abs() < 0.1);
603 }
604
605 #[test]
606 fn er_single_edge() {
607 let r = energy_ratio(&single_edge()).unwrap();
611 assert!((r - 1.0).abs() < 0.1);
612 }
613
614 #[test]
615 fn er_positive() {
616 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
617 let r = energy_ratio(g).unwrap();
618 assert!(r > 0.0);
619 }
620 }
621
622 #[test]
623 fn er_finite() {
624 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
625 assert!(energy_ratio(g).unwrap().is_finite());
626 }
627 }
628
629 #[test]
632 fn complete_graphs_high_gap() {
633 let r3 = adjacency_spectral_gap_ratio(&k3()).unwrap();
635 let r4 = adjacency_spectral_gap_ratio(&k4()).unwrap();
636 assert!(r3 > 1.0);
637 assert!(r4 > 1.0);
638 }
639
640 #[test]
641 fn all_indices_finite() {
642 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
643 assert!(adjacency_spectral_gap_ratio(g).unwrap().is_finite());
644 assert!(spectral_radius_ratio(g).unwrap().is_finite());
645 assert!(energy_ratio(g).unwrap().is_finite());
646 }
647 }
648}