rust_igraph/algorithms/properties/
normalized_laplacian.rs1#![allow(
20 clippy::cast_possible_truncation,
21 clippy::cast_precision_loss,
22 clippy::many_single_char_names,
23 clippy::needless_range_loop,
24 clippy::similar_names,
25 clippy::too_many_lines
26)]
27
28use crate::core::{Graph, IgraphError, IgraphResult};
29
30fn dense_normalized_laplacian(graph: &Graph) -> Vec<f64> {
34 let n = graph.vcount() as usize;
35 let mut lnorm = vec![0.0_f64; n * n];
36
37 let mut deg = vec![0_u32; n];
38 for (u, v) in graph.edges() {
39 deg[u as usize] += 1;
40 deg[v as usize] += 1;
41 }
42
43 let inv_sqrt_deg: Vec<f64> = deg
44 .iter()
45 .map(|&d| {
46 if d == 0 {
47 0.0
48 } else {
49 1.0 / (f64::from(d)).sqrt()
50 }
51 })
52 .collect();
53
54 for i in 0..n {
55 if deg[i] > 0 {
56 lnorm[i * n + i] = 1.0;
57 }
58 }
59
60 for (u, v) in graph.edges() {
61 let ui = u as usize;
62 let vi = v as usize;
63 let val = inv_sqrt_deg[ui] * inv_sqrt_deg[vi];
64 lnorm[ui * n + vi] -= val;
65 lnorm[vi * n + ui] -= val;
66 }
67
68 lnorm
69}
70
71fn jacobi_eigen_ascending(mat: &mut [f64], n: usize) -> (Vec<f64>, Vec<Vec<f64>>) {
74 if n == 0 {
75 return (Vec::new(), Vec::new());
76 }
77 if n == 1 {
78 return (vec![mat[0]], vec![vec![1.0]]);
79 }
80
81 let mut v = vec![0.0_f64; n * n];
82 for i in 0..n {
83 v[i * n + i] = 1.0;
84 }
85
86 let max_sweeps = 100;
87 for _ in 0..max_sweeps {
88 let mut max_off = 0.0_f64;
89 let mut p = 0;
90 let mut q = 1;
91 for i in 0..n {
92 for j in (i + 1)..n {
93 let val = mat[i * n + j].abs();
94 if val > max_off {
95 max_off = val;
96 p = i;
97 q = j;
98 }
99 }
100 }
101
102 if max_off < 1e-14 {
103 break;
104 }
105
106 let app = mat[p * n + p];
107 let aqq = mat[q * n + q];
108 let apq = mat[p * n + q];
109
110 let (cos, sin) = if (app - aqq).abs() < 1e-300 {
111 let s = std::f64::consts::FRAC_1_SQRT_2;
112 (s, s)
113 } else {
114 let tau = (aqq - app) / (2.0 * apq);
115 let t = if tau >= 0.0 {
116 1.0 / (tau + (1.0 + tau * tau).sqrt())
117 } else {
118 -1.0 / (-tau + (1.0 + tau * tau).sqrt())
119 };
120 let c = 1.0 / (1.0 + t * t).sqrt();
121 (c, t * c)
122 };
123
124 for i in 0..n {
125 if i == p || i == q {
126 continue;
127 }
128 let aip = mat[i * n + p];
129 let aiq = mat[i * n + q];
130 mat[i * n + p] = cos * aip - sin * aiq;
131 mat[p * n + i] = mat[i * n + p];
132 mat[i * n + q] = sin * aip + cos * aiq;
133 mat[q * n + i] = mat[i * n + q];
134 }
135
136 let new_pp = cos * cos * app - 2.0 * sin * cos * apq + sin * sin * aqq;
137 let new_qq = sin * sin * app + 2.0 * sin * cos * apq + cos * cos * aqq;
138 mat[p * n + p] = new_pp;
139 mat[q * n + q] = new_qq;
140 mat[p * n + q] = 0.0;
141 mat[q * n + p] = 0.0;
142
143 for i in 0..n {
144 let vip = v[i * n + p];
145 let viq = v[i * n + q];
146 v[i * n + p] = cos * vip - sin * viq;
147 v[i * n + q] = sin * vip + cos * viq;
148 }
149 }
150
151 let eigenvalues: Vec<f64> = (0..n).map(|i| mat[i * n + i]).collect();
152 let mut indices: Vec<usize> = (0..n).collect();
153 indices.sort_by(|&a, &b| {
154 eigenvalues[a]
155 .partial_cmp(&eigenvalues[b])
156 .unwrap_or(std::cmp::Ordering::Equal)
157 });
158
159 let sorted_vals: Vec<f64> = indices.iter().map(|&i| eigenvalues[i]).collect();
160 let sorted_vecs: Vec<Vec<f64>> = indices
161 .iter()
162 .map(|&idx| {
163 let mut col = vec![0.0_f64; n];
164 for i in 0..n {
165 col[i] = v[i * n + idx];
166 }
167 col
168 })
169 .collect();
170
171 (sorted_vals, sorted_vecs)
172}
173
174fn normalized_laplacian_internal(graph: &Graph) -> (Vec<f64>, Vec<Vec<f64>>) {
176 let n = graph.vcount() as usize;
177 if n == 0 {
178 return (Vec::new(), Vec::new());
179 }
180 let mut lnorm = dense_normalized_laplacian(graph);
181 jacobi_eigen_ascending(&mut lnorm, n)
182}
183
184pub fn normalized_laplacian_spectrum(graph: &Graph) -> IgraphResult<Vec<f64>> {
205 if graph.is_directed() {
206 return Err(IgraphError::InvalidArgument(
207 "normalized_laplacian_spectrum is defined for undirected graphs only".into(),
208 ));
209 }
210 let (vals, _) = normalized_laplacian_internal(graph);
211 Ok(vals)
212}
213
214pub fn normalized_algebraic_connectivity(graph: &Graph) -> IgraphResult<f64> {
232 if graph.is_directed() {
233 return Err(IgraphError::InvalidArgument(
234 "normalized_algebraic_connectivity is defined for undirected graphs only".into(),
235 ));
236 }
237 let n = graph.vcount() as usize;
238 if n <= 1 {
239 return Ok(0.0);
240 }
241
242 let (vals, _) = normalized_laplacian_internal(graph);
243 Ok(vals[1].max(0.0))
244}
245
246pub fn cheeger_bounds(graph: &Graph) -> IgraphResult<(f64, f64)> {
266 let mu2 = normalized_algebraic_connectivity(graph)?;
267 let lower = mu2 / 2.0;
268 let upper = (2.0 * mu2).sqrt();
269 Ok((lower, upper))
270}
271
272pub fn spectral_gap_ratio(graph: &Graph) -> IgraphResult<f64> {
289 if graph.is_directed() {
290 return Err(IgraphError::InvalidArgument(
291 "spectral_gap_ratio is defined for undirected graphs only".into(),
292 ));
293 }
294 let n = graph.vcount() as usize;
295 if n <= 1 {
296 return Ok(0.0);
297 }
298
299 let (vals, _) = normalized_laplacian_internal(graph);
300 let mu_n = vals[n - 1];
301 if mu_n.abs() < 1e-14 {
302 return Ok(0.0);
303 }
304 Ok(vals[1].max(0.0) / mu_n)
305}
306
307pub fn bipartiteness_ratio(graph: &Graph) -> IgraphResult<f64> {
331 if graph.is_directed() {
332 return Err(IgraphError::InvalidArgument(
333 "bipartiteness_ratio is defined for undirected graphs only".into(),
334 ));
335 }
336 let n = graph.vcount() as usize;
337 if n == 0 {
338 return Ok(1.0);
339 }
340
341 let (vals, _) = normalized_laplacian_internal(graph);
342 let mu_n = vals[n - 1];
343 Ok((2.0 - mu_n) / 2.0)
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 fn k3() -> Graph {
351 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
352 }
353
354 fn k4() -> Graph {
355 Graph::from_edges(
356 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
357 false,
358 Some(4),
359 )
360 .unwrap()
361 }
362
363 fn path4() -> Graph {
364 Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
365 }
366
367 fn cycle4() -> Graph {
368 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
369 }
370
371 fn star4() -> Graph {
372 Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap()
373 }
374
375 fn k22() -> Graph {
376 Graph::from_edges(&[(0, 2), (0, 3), (1, 2), (1, 3)], false, Some(4)).unwrap()
377 }
378
379 #[test]
382 fn nls_empty() {
383 let g = Graph::with_vertices(0);
384 let spec = normalized_laplacian_spectrum(&g).unwrap();
385 assert!(spec.is_empty());
386 }
387
388 #[test]
389 fn nls_single() {
390 let g = Graph::with_vertices(1);
391 let spec = normalized_laplacian_spectrum(&g).unwrap();
392 assert_eq!(spec.len(), 1);
393 assert!(spec[0].abs() < 1e-10);
394 }
395
396 #[test]
397 fn nls_k3() {
398 let g = k3();
399 let spec = normalized_laplacian_spectrum(&g).unwrap();
400 assert_eq!(spec.len(), 3);
401 assert!(spec[0].abs() < 0.01);
402 assert!((spec[1] - 1.5).abs() < 0.1);
403 assert!((spec[2] - 1.5).abs() < 0.1);
404 }
405
406 #[test]
407 fn nls_k4() {
408 let g = k4();
409 let spec = normalized_laplacian_spectrum(&g).unwrap();
410 assert!(spec[0].abs() < 0.01);
413 for i in 1..4 {
414 assert!((spec[i] - 4.0 / 3.0).abs() < 0.1);
415 }
416 }
417
418 #[test]
419 fn nls_in_range() {
420 let g = star4();
421 let spec = normalized_laplacian_spectrum(&g).unwrap();
422 for &v in &spec {
423 assert!((-0.01..=2.01).contains(&v), "eigenvalue {v} out of [0,2]");
424 }
425 }
426
427 #[test]
428 fn nls_ascending() {
429 let g = cycle4();
430 let spec = normalized_laplacian_spectrum(&g).unwrap();
431 for i in 1..spec.len() {
432 assert!(spec[i] >= spec[i - 1] - 1e-10);
433 }
434 }
435
436 #[test]
437 fn nls_first_is_zero() {
438 let g = path4();
439 let spec = normalized_laplacian_spectrum(&g).unwrap();
440 assert!(spec[0].abs() < 0.01);
441 }
442
443 #[test]
444 fn nls_bipartite_last_is_two() {
445 let g = k22();
447 let spec = normalized_laplacian_spectrum(&g).unwrap();
448 assert!((spec[spec.len() - 1] - 2.0).abs() < 0.01);
449 }
450
451 #[test]
452 fn nls_disconnected() {
453 let g = Graph::with_vertices(3);
454 let spec = normalized_laplacian_spectrum(&g).unwrap();
455 for &v in &spec {
456 assert!(v.abs() < 0.01);
457 }
458 }
459
460 #[test]
461 fn nls_directed_error() {
462 let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
463 assert!(normalized_laplacian_spectrum(&g).is_err());
464 }
465
466 #[test]
469 fn nac_k3() {
470 let g = k3();
471 let mu2 = normalized_algebraic_connectivity(&g).unwrap();
472 assert!((mu2 - 1.5).abs() < 0.1);
473 }
474
475 #[test]
476 fn nac_k4() {
477 let g = k4();
478 let mu2 = normalized_algebraic_connectivity(&g).unwrap();
479 assert!((mu2 - 4.0 / 3.0).abs() < 0.1);
480 }
481
482 #[test]
483 fn nac_disconnected() {
484 let g = Graph::with_vertices(3);
485 let mu2 = normalized_algebraic_connectivity(&g).unwrap();
486 assert!(mu2.abs() < 0.01);
487 }
488
489 #[test]
490 fn nac_single() {
491 let g = Graph::with_vertices(1);
492 let mu2 = normalized_algebraic_connectivity(&g).unwrap();
493 assert!(mu2.abs() < 1e-10);
494 }
495
496 #[test]
497 fn nac_cycle4() {
498 let g = cycle4();
499 let mu2 = normalized_algebraic_connectivity(&g).unwrap();
500 assert!((mu2 - 1.0).abs() < 0.1);
503 }
504
505 #[test]
508 fn cb_connected() {
509 let g = k3();
510 let (lo, hi) = cheeger_bounds(&g).unwrap();
511 assert!(lo >= 0.0);
512 assert!(lo <= hi + 1e-10);
513 }
514
515 #[test]
516 fn cb_disconnected() {
517 let g = Graph::with_vertices(3);
518 let (lo, hi) = cheeger_bounds(&g).unwrap();
519 assert!(lo.abs() < 0.01);
520 assert!(hi.abs() < 0.01);
521 }
522
523 #[test]
524 fn cb_single() {
525 let g = Graph::with_vertices(1);
526 let (lo, hi) = cheeger_bounds(&g).unwrap();
527 assert!(lo.abs() < 1e-10);
528 assert!(hi.abs() < 1e-10);
529 }
530
531 #[test]
534 fn sgr_k3() {
535 let g = k3();
536 let r = spectral_gap_ratio(&g).unwrap();
537 assert!((r - 1.0).abs() < 0.1);
539 }
540
541 #[test]
542 fn sgr_k4() {
543 let g = k4();
544 let r = spectral_gap_ratio(&g).unwrap();
545 assert!((r - 1.0).abs() < 0.1);
547 }
548
549 #[test]
550 fn sgr_path() {
551 let g = path4();
552 let r = spectral_gap_ratio(&g).unwrap();
553 assert!(r > 0.0 && r <= 1.0 + 1e-10);
554 }
555
556 #[test]
557 fn sgr_single() {
558 let g = Graph::with_vertices(1);
559 let r = spectral_gap_ratio(&g).unwrap();
560 assert!(r.abs() < 1e-10);
561 }
562
563 #[test]
564 fn sgr_disconnected() {
565 let g = Graph::with_vertices(3);
566 let r = spectral_gap_ratio(&g).unwrap();
567 assert!(r.abs() < 1e-10);
568 }
569
570 #[test]
573 fn br_bipartite() {
574 let g = k22();
575 let br = bipartiteness_ratio(&g).unwrap();
576 assert!(br.abs() < 0.01);
577 }
578
579 #[test]
580 fn br_path_bipartite() {
581 let g = path4();
582 let br = bipartiteness_ratio(&g).unwrap();
583 assert!(br.abs() < 0.01);
584 }
585
586 #[test]
587 fn br_k3_nonbipartite() {
588 let g = k3();
589 let br = bipartiteness_ratio(&g).unwrap();
590 assert!((br - 0.25).abs() < 0.05);
592 }
593
594 #[test]
595 fn br_k4_nonbipartite() {
596 let g = k4();
597 let br = bipartiteness_ratio(&g).unwrap();
598 assert!((br - 1.0 / 3.0).abs() < 0.05);
600 }
601
602 #[test]
603 fn br_nonneg() {
604 let g = cycle4();
605 let br = bipartiteness_ratio(&g).unwrap();
606 assert!(br >= -0.01);
607 }
608
609 #[test]
612 fn nac_equals_second_eigenvalue() {
613 let g = star4();
614 let mu2 = normalized_algebraic_connectivity(&g).unwrap();
615 let spec = normalized_laplacian_spectrum(&g).unwrap();
616 assert!((mu2 - spec[1]).abs() < 0.01);
617 }
618
619 #[test]
620 fn regular_graph_normalized_equals_scaled_combinatorial() {
621 let g = cycle4(); let spec = normalized_laplacian_spectrum(&g).unwrap();
624 assert!(spec[0].abs() < 0.01);
627 assert!((spec[1] - 1.0).abs() < 0.1);
628 assert!((spec[2] - 1.0).abs() < 0.1);
629 assert!((spec[3] - 2.0).abs() < 0.1);
630 }
631
632 #[test]
633 fn trace_equals_nontrivial_vertex_count() {
634 let g = star4();
636 let spec = normalized_laplacian_spectrum(&g).unwrap();
637 let trace: f64 = spec.iter().sum();
638 assert!((trace - 4.0).abs() < 0.1);
639 }
640}