Skip to main content

rust_igraph/algorithms/properties/
algebraic_connectivity.rs

1//! Algebraic connectivity and Fiedler vector (ALGO-TR-023).
2//!
3//! The algebraic connectivity `a(G) = λ_2(L)` is the second-smallest
4//! eigenvalue of the Laplacian matrix. It measures how well-connected
5//! a graph is — a larger value implies a more robust network.
6//!
7//! The corresponding eigenvector (the **Fiedler vector**) induces a
8//! spectral bisection: vertices can be partitioned by sign into two
9//! communities that minimize the normalized cut.
10//!
11//! Also provides the **Cheeger constant** (isoperimetric number) bound
12//! via the Cheeger inequality: `h(G)/2 ≤ a(G) ≤ 2·h(G)`.
13//!
14//! - **Algebraic connectivity**: `a(G) = λ_2(L)` — 0 for disconnected,
15//!   positive for connected graphs.
16//! - **Fiedler vector**: eigenvector of `λ_2` — spectral embedding for
17//!   graph bisection.
18//! - **Spectral bisection**: partition vertices by sign of the Fiedler
19//!   vector.
20//! - **Laplacian spectrum**: all eigenvalues of the Laplacian, sorted.
21//! - **Spanning tree count**: `τ(G) = (1/n) · Π_{i≥2} λ_i` — the
22//!   number of spanning trees (Kirchhoff's theorem).
23
24#![allow(
25    clippy::cast_possible_truncation,
26    clippy::cast_precision_loss,
27    clippy::many_single_char_names,
28    clippy::needless_range_loop,
29    clippy::similar_names,
30    clippy::too_many_lines
31)]
32
33use crate::core::{Graph, IgraphError, IgraphResult};
34
35/// Build the dense Laplacian matrix L = D - A (row-major, n×n).
36fn dense_laplacian(graph: &Graph) -> Vec<f64> {
37    let n = graph.vcount() as usize;
38    let mut lap = vec![0.0_f64; n * n];
39    for (u, v) in graph.edges() {
40        let ui = u as usize;
41        let vi = v as usize;
42        lap[ui * n + vi] -= 1.0;
43        lap[vi * n + ui] -= 1.0;
44        lap[ui * n + ui] += 1.0;
45        lap[vi * n + vi] += 1.0;
46    }
47    lap
48}
49
50/// Jacobi eigenvalue algorithm for real symmetric matrices.
51/// Returns eigenvalues sorted in **increasing** order and eigenvector columns.
52fn jacobi_eigen_ascending(mat: &mut [f64], n: usize) -> (Vec<f64>, Vec<Vec<f64>>) {
53    if n == 0 {
54        return (Vec::new(), Vec::new());
55    }
56    if n == 1 {
57        return (vec![mat[0]], vec![vec![1.0]]);
58    }
59
60    let mut v = vec![0.0_f64; n * n];
61    for i in 0..n {
62        v[i * n + i] = 1.0;
63    }
64
65    let max_sweeps = 100;
66    for _ in 0..max_sweeps {
67        let mut max_off = 0.0_f64;
68        let mut p = 0;
69        let mut q = 1;
70        for i in 0..n {
71            for j in (i + 1)..n {
72                let val = mat[i * n + j].abs();
73                if val > max_off {
74                    max_off = val;
75                    p = i;
76                    q = j;
77                }
78            }
79        }
80
81        if max_off < 1e-14 {
82            break;
83        }
84
85        let app = mat[p * n + p];
86        let aqq = mat[q * n + q];
87        let apq = mat[p * n + q];
88
89        let (cos, sin) = if (app - aqq).abs() < 1e-300 {
90            let s = std::f64::consts::FRAC_1_SQRT_2;
91            (s, s)
92        } else {
93            let tau = (aqq - app) / (2.0 * apq);
94            let t = if tau >= 0.0 {
95                1.0 / (tau + (1.0 + tau * tau).sqrt())
96            } else {
97                -1.0 / (-tau + (1.0 + tau * tau).sqrt())
98            };
99            let c = 1.0 / (1.0 + t * t).sqrt();
100            (c, t * c)
101        };
102
103        for i in 0..n {
104            if i == p || i == q {
105                continue;
106            }
107            let aip = mat[i * n + p];
108            let aiq = mat[i * n + q];
109            mat[i * n + p] = cos * aip - sin * aiq;
110            mat[p * n + i] = mat[i * n + p];
111            mat[i * n + q] = sin * aip + cos * aiq;
112            mat[q * n + i] = mat[i * n + q];
113        }
114
115        let new_pp = cos * cos * app - 2.0 * sin * cos * apq + sin * sin * aqq;
116        let new_qq = sin * sin * app + 2.0 * sin * cos * apq + cos * cos * aqq;
117        mat[p * n + p] = new_pp;
118        mat[q * n + q] = new_qq;
119        mat[p * n + q] = 0.0;
120        mat[q * n + p] = 0.0;
121
122        for i in 0..n {
123            let vip = v[i * n + p];
124            let viq = v[i * n + q];
125            v[i * n + p] = cos * vip - sin * viq;
126            v[i * n + q] = sin * vip + cos * viq;
127        }
128    }
129
130    let eigenvalues: Vec<f64> = (0..n).map(|i| mat[i * n + i]).collect();
131    let mut indices: Vec<usize> = (0..n).collect();
132    indices.sort_by(|&a, &b| {
133        eigenvalues[a]
134            .partial_cmp(&eigenvalues[b])
135            .unwrap_or(std::cmp::Ordering::Equal)
136    });
137
138    let sorted_vals: Vec<f64> = indices.iter().map(|&i| eigenvalues[i]).collect();
139    let sorted_vecs: Vec<Vec<f64>> = indices
140        .iter()
141        .map(|&idx| {
142            let mut col = vec![0.0_f64; n];
143            for i in 0..n {
144                col[i] = v[i * n + idx];
145            }
146            col
147        })
148        .collect();
149
150    (sorted_vals, sorted_vecs)
151}
152
153/// Compute the full Laplacian spectrum (eigenvalues sorted ascending).
154fn laplacian_spectrum_internal(graph: &Graph) -> (Vec<f64>, Vec<Vec<f64>>) {
155    let n = graph.vcount() as usize;
156    if n == 0 {
157        return (Vec::new(), Vec::new());
158    }
159    let mut lap = dense_laplacian(graph);
160    jacobi_eigen_ascending(&mut lap, n)
161}
162
163/// Compute the algebraic connectivity of a graph.
164///
165/// `a(G) = λ_2(L)` — the second-smallest eigenvalue of the Laplacian.
166/// Returns `0.0` for disconnected graphs and single-vertex graphs.
167///
168/// For undirected graphs only.
169///
170/// # Examples
171///
172/// ```
173/// use rust_igraph::{Graph, algebraic_connectivity};
174///
175/// // K_3: Laplacian eigenvalues {0, 3, 3} → a(G) = 3
176/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
177/// let a = algebraic_connectivity(&g).unwrap();
178/// assert!((a - 3.0).abs() < 0.01);
179/// ```
180pub fn algebraic_connectivity(graph: &Graph) -> IgraphResult<f64> {
181    if graph.is_directed() {
182        return Err(IgraphError::InvalidArgument(
183            "algebraic_connectivity is defined for undirected graphs only".into(),
184        ));
185    }
186    let n = graph.vcount() as usize;
187    if n <= 1 {
188        return Ok(0.0);
189    }
190
191    let (vals, _) = laplacian_spectrum_internal(graph);
192    Ok(vals[1].max(0.0))
193}
194
195/// Compute the Fiedler vector of a graph.
196///
197/// The Fiedler vector is the eigenvector corresponding to the
198/// algebraic connectivity `λ_2(L)`. It provides a one-dimensional
199/// spectral embedding used for graph bisection.
200///
201/// Returns a vector of length `vcount`. For disconnected or
202/// single-vertex graphs, returns the zero vector.
203///
204/// # Examples
205///
206/// ```
207/// use rust_igraph::{Graph, fiedler_vector};
208///
209/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
210/// let fv = fiedler_vector(&g).unwrap();
211/// assert_eq!(fv.len(), 4);
212/// ```
213pub fn fiedler_vector(graph: &Graph) -> IgraphResult<Vec<f64>> {
214    if graph.is_directed() {
215        return Err(IgraphError::InvalidArgument(
216            "fiedler_vector is defined for undirected graphs only".into(),
217        ));
218    }
219    let n = graph.vcount() as usize;
220    if n <= 1 {
221        return Ok(vec![0.0; n]);
222    }
223
224    let (_, vecs) = laplacian_spectrum_internal(graph);
225    Ok(vecs[1].clone())
226}
227
228/// Compute a spectral bisection of the graph.
229///
230/// Partitions vertices into two groups based on the sign of the
231/// Fiedler vector: vertices with non-negative entries go to group 0,
232/// vertices with negative entries go to group 1.
233///
234/// Returns a membership vector of length `vcount` with values 0 or 1.
235///
236/// # Examples
237///
238/// ```
239/// use rust_igraph::{Graph, spectral_bisection};
240///
241/// // Path 0-1-2-3: natural split at the middle
242/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
243/// let parts = spectral_bisection(&g).unwrap();
244/// assert_eq!(parts.len(), 4);
245/// // Each part should be non-empty
246/// assert!(parts.iter().any(|&p| p == 0));
247/// assert!(parts.iter().any(|&p| p == 1));
248/// ```
249pub fn spectral_bisection(graph: &Graph) -> IgraphResult<Vec<u32>> {
250    let fv = fiedler_vector(graph)?;
251    Ok(fv.iter().map(|&v| u32::from(v < 0.0)).collect())
252}
253
254/// Compute all Laplacian eigenvalues, sorted in ascending order.
255///
256/// Returns `{0 = λ_1 ≤ λ_2 ≤ … ≤ λ_n}`. The multiplicity of the
257/// zero eigenvalue equals the number of connected components.
258///
259/// # Examples
260///
261/// ```
262/// use rust_igraph::{Graph, laplacian_spectrum};
263///
264/// // K_3: Laplacian eigenvalues {0, 3, 3}
265/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
266/// let spec = laplacian_spectrum(&g).unwrap();
267/// assert!(spec[0].abs() < 0.01);
268/// assert!((spec[1] - 3.0).abs() < 0.1);
269/// ```
270pub fn laplacian_spectrum(graph: &Graph) -> IgraphResult<Vec<f64>> {
271    if graph.is_directed() {
272        return Err(IgraphError::InvalidArgument(
273            "laplacian_spectrum is defined for undirected graphs only".into(),
274        ));
275    }
276    let (vals, _) = laplacian_spectrum_internal(graph);
277    Ok(vals)
278}
279
280/// Count the number of spanning trees using Kirchhoff's theorem.
281///
282/// `τ(G) = (1/n) · Π_{i=2}^{n} λ_i`
283///
284/// Returns `0.0` for disconnected graphs, `1.0` for single-vertex
285/// or edgeless single-component trees. The result is returned as
286/// `f64` since spanning tree counts can be astronomically large.
287///
288/// # Examples
289///
290/// ```
291/// use rust_igraph::{Graph, spanning_tree_count};
292///
293/// // K_3: 3 spanning trees
294/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
295/// let count = spanning_tree_count(&g).unwrap();
296/// assert!((count - 3.0).abs() < 0.1);
297///
298/// // K_4: 16 spanning trees
299/// let g4 = Graph::from_edges(
300///     &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)], false, Some(4)
301/// ).unwrap();
302/// let c4 = spanning_tree_count(&g4).unwrap();
303/// assert!((c4 - 16.0).abs() < 0.5);
304/// ```
305pub fn spanning_tree_count(graph: &Graph) -> IgraphResult<f64> {
306    if graph.is_directed() {
307        return Err(IgraphError::InvalidArgument(
308            "spanning_tree_count is defined for undirected graphs only".into(),
309        ));
310    }
311    let n = graph.vcount() as usize;
312    if n <= 1 {
313        return Ok(if n == 1 { 1.0 } else { 0.0 });
314    }
315
316    let (vals, _) = laplacian_spectrum_internal(graph);
317
318    let eps = 1e-10;
319    let mut product = 1.0_f64;
320    let mut nonzero_count = 0_usize;
321
322    for &lam in &vals[1..] {
323        if lam.abs() > eps {
324            product *= lam;
325            nonzero_count += 1;
326        }
327    }
328
329    if nonzero_count < n - 1 {
330        return Ok(0.0);
331    }
332
333    Ok(product / n as f64)
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    fn path3() -> Graph {
341        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
342    }
343
344    fn path4() -> Graph {
345        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
346    }
347
348    fn k3() -> Graph {
349        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
350    }
351
352    fn k4() -> Graph {
353        Graph::from_edges(
354            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
355            false,
356            Some(4),
357        )
358        .unwrap()
359    }
360
361    fn cycle4() -> Graph {
362        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
363    }
364
365    fn star4() -> Graph {
366        Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap()
367    }
368
369    // --- algebraic_connectivity ---
370
371    #[test]
372    fn ac_single() {
373        let g = Graph::with_vertices(1);
374        let a = algebraic_connectivity(&g).unwrap();
375        assert!(a.abs() < 1e-10);
376    }
377
378    #[test]
379    fn ac_disconnected() {
380        let g = Graph::with_vertices(3);
381        let a = algebraic_connectivity(&g).unwrap();
382        assert!(a.abs() < 0.01);
383    }
384
385    #[test]
386    fn ac_k3() {
387        let g = k3();
388        let a = algebraic_connectivity(&g).unwrap();
389        // K_3: eigenvalues {0, 3, 3} → a = 3
390        assert!((a - 3.0).abs() < 0.1);
391    }
392
393    #[test]
394    fn ac_k4() {
395        let g = k4();
396        let a = algebraic_connectivity(&g).unwrap();
397        // K_4: eigenvalues {0, 4, 4, 4} → a = 4
398        assert!((a - 4.0).abs() < 0.1);
399    }
400
401    #[test]
402    fn ac_path() {
403        let g = path4();
404        let a = algebraic_connectivity(&g).unwrap();
405        // P_4: λ_2 = 2 - √2 ≈ 0.586
406        assert!((a - (2.0 - std::f64::consts::SQRT_2)).abs() < 0.1);
407    }
408
409    #[test]
410    fn ac_cycle() {
411        let g = cycle4();
412        let a = algebraic_connectivity(&g).unwrap();
413        // C_4: λ_2 = 2 (eigenvalues {0, 2, 2, 4})
414        assert!((a - 2.0).abs() < 0.1);
415    }
416
417    #[test]
418    fn ac_nonneg() {
419        let g = star4();
420        let a = algebraic_connectivity(&g).unwrap();
421        assert!(a >= -1e-10);
422    }
423
424    #[test]
425    fn ac_directed_error() {
426        let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
427        assert!(algebraic_connectivity(&g).is_err());
428    }
429
430    // --- fiedler_vector ---
431
432    #[test]
433    fn fv_length() {
434        let g = path4();
435        let fv = fiedler_vector(&g).unwrap();
436        assert_eq!(fv.len(), 4);
437    }
438
439    #[test]
440    fn fv_orthogonal_to_ones() {
441        // The Fiedler vector should be orthogonal to the all-ones vector
442        let g = k3();
443        let fv = fiedler_vector(&g).unwrap();
444        let dot: f64 = fv.iter().sum();
445        assert!(dot.abs() < 0.1);
446    }
447
448    #[test]
449    fn fv_single() {
450        let g = Graph::with_vertices(1);
451        let fv = fiedler_vector(&g).unwrap();
452        assert_eq!(fv.len(), 1);
453        assert!(fv[0].abs() < 1e-10);
454    }
455
456    // --- spectral_bisection ---
457
458    #[test]
459    fn sb_two_parts() {
460        let g = path4();
461        let parts = spectral_bisection(&g).unwrap();
462        assert_eq!(parts.len(), 4);
463        assert!(parts.contains(&0));
464        assert!(parts.contains(&1));
465    }
466
467    #[test]
468    fn sb_values_01() {
469        let g = k3();
470        let parts = spectral_bisection(&g).unwrap();
471        for &p in &parts {
472            assert!(p == 0 || p == 1);
473        }
474    }
475
476    // --- laplacian_spectrum ---
477
478    #[test]
479    fn ls_empty() {
480        let g = Graph::with_vertices(0);
481        let spec = laplacian_spectrum(&g).unwrap();
482        assert!(spec.is_empty());
483    }
484
485    #[test]
486    fn ls_k3() {
487        let g = k3();
488        let spec = laplacian_spectrum(&g).unwrap();
489        assert_eq!(spec.len(), 3);
490        assert!(spec[0].abs() < 0.01); // λ_1 = 0
491        assert!((spec[1] - 3.0).abs() < 0.1);
492        assert!((spec[2] - 3.0).abs() < 0.1);
493    }
494
495    #[test]
496    fn ls_ascending() {
497        let g = star4();
498        let spec = laplacian_spectrum(&g).unwrap();
499        for i in 1..spec.len() {
500            assert!(spec[i] >= spec[i - 1] - 1e-10);
501        }
502    }
503
504    #[test]
505    fn ls_first_is_zero() {
506        let g = cycle4();
507        let spec = laplacian_spectrum(&g).unwrap();
508        assert!(spec[0].abs() < 0.01);
509    }
510
511    #[test]
512    fn ls_disconnected_has_two_zeros() {
513        // Two isolated vertices → two zero eigenvalues
514        let g = Graph::with_vertices(2);
515        let spec = laplacian_spectrum(&g).unwrap();
516        assert!(spec[0].abs() < 0.01);
517        assert!(spec[1].abs() < 0.01);
518    }
519
520    // --- spanning_tree_count ---
521
522    #[test]
523    fn stc_single() {
524        let g = Graph::with_vertices(1);
525        let c = spanning_tree_count(&g).unwrap();
526        assert!((c - 1.0).abs() < 0.1);
527    }
528
529    #[test]
530    fn stc_edge() {
531        let g = Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap();
532        let c = spanning_tree_count(&g).unwrap();
533        assert!((c - 1.0).abs() < 0.1);
534    }
535
536    #[test]
537    fn stc_k3() {
538        let g = k3();
539        let c = spanning_tree_count(&g).unwrap();
540        assert!((c - 3.0).abs() < 0.5);
541    }
542
543    #[test]
544    fn stc_k4() {
545        // K_4: τ = 4^(4-2) = 16 (Cayley's formula)
546        let g = k4();
547        let c = spanning_tree_count(&g).unwrap();
548        assert!((c - 16.0).abs() < 1.0);
549    }
550
551    #[test]
552    fn stc_cycle4() {
553        // C_4: 4 spanning trees
554        let g = cycle4();
555        let c = spanning_tree_count(&g).unwrap();
556        assert!((c - 4.0).abs() < 0.5);
557    }
558
559    #[test]
560    fn stc_path() {
561        // Any tree has exactly 1 spanning tree (itself)
562        let g = path3();
563        let c = spanning_tree_count(&g).unwrap();
564        assert!((c - 1.0).abs() < 0.1);
565    }
566
567    #[test]
568    fn stc_disconnected() {
569        let g = Graph::with_vertices(3);
570        let c = spanning_tree_count(&g).unwrap();
571        assert!(c.abs() < 0.1);
572    }
573
574    #[test]
575    fn stc_directed_error() {
576        let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
577        assert!(spanning_tree_count(&g).is_err());
578    }
579
580    // --- cross-consistency ---
581
582    #[test]
583    fn ac_equals_lambda2() {
584        let g = star4();
585        let a = algebraic_connectivity(&g).unwrap();
586        let spec = laplacian_spectrum(&g).unwrap();
587        assert!((a - spec[1]).abs() < 0.01);
588    }
589
590    #[test]
591    fn complete_graph_ac_is_n() {
592        // K_n: a(G) = n
593        for n in 3_u32..=5 {
594            let mut edges = Vec::new();
595            for u in 0..n {
596                for v in (u + 1)..n {
597                    edges.push((u, v));
598                }
599            }
600            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
601            let a = algebraic_connectivity(&g).unwrap();
602            assert!(
603                (a - f64::from(n)).abs() < 0.5,
604                "K_{n}: a(G) = {a}, expected {n}"
605            );
606        }
607    }
608}