Skip to main content

rust_igraph/algorithms/eigen/
adjacency.rs

1//! Eigenvalues of a graph's adjacency matrix.
2//!
3//! Convenience wrapper around [`super::eigen_matrix_symmetric`] that
4//! constructs the matrix-vector product `y = A·x` from the graph's
5//! adjacency structure.
6//!
7//! ```
8//! use rust_igraph::{Graph, eigen_adjacency, EigenWhich};
9//!
10//! // K_3 (complete graph on 3 vertices): adjacency eigenvalues are 2, -1, -1
11//! let mut g = Graph::with_vertices(3);
12//! g.add_edge(0, 1).unwrap();
13//! g.add_edge(0, 2).unwrap();
14//! g.add_edge(1, 2).unwrap();
15//!
16//! let result = eigen_adjacency(&g, 1, EigenWhich::LargestAlgebraic).unwrap();
17//! assert!((result.eigenvalues[0] - 2.0).abs() < 1e-6);
18//! ```
19
20use super::symmetric::{EigenDecomposition, EigenWhich, eigen_matrix_symmetric};
21use crate::core::error::{IgraphError, IgraphResult};
22use crate::core::graph::Graph;
23
24/// Compute selected eigenvalues of a graph's adjacency matrix.
25///
26/// For undirected graphs, the adjacency matrix is real symmetric and the
27/// eigenvalues are real. For directed graphs, the matrix-vector product
28/// uses the **symmetrized** adjacency `(A + A^T) / 2` so that the Lanczos
29/// solver applies; the eigenvalues correspond to this symmetrized form.
30///
31/// # Arguments
32///
33/// * `graph` — the graph whose adjacency spectrum to compute
34/// * `nev` — number of eigenvalues to compute (clamped to `vcount`)
35/// * `which` — which part of the spectrum to target
36///
37/// # Errors
38///
39/// Returns [`IgraphError::InvalidArgument`] if the graph has zero vertices.
40///
41/// # Examples
42///
43/// ```
44/// use rust_igraph::{Graph, eigen_adjacency, EigenWhich};
45///
46/// let mut g = Graph::with_vertices(4);
47/// g.add_edge(0, 1).unwrap();
48/// g.add_edge(1, 2).unwrap();
49/// g.add_edge(2, 3).unwrap();
50/// g.add_edge(3, 0).unwrap();
51///
52/// let result = eigen_adjacency(&g, 2, EigenWhich::LargestAlgebraic).unwrap();
53/// assert_eq!(result.eigenvalues.len(), 2);
54/// ```
55pub fn eigen_adjacency(
56    graph: &Graph,
57    nev: usize,
58    which: EigenWhich,
59) -> IgraphResult<EigenDecomposition> {
60    let n = graph.vcount() as usize;
61    if n == 0 {
62        return Err(IgraphError::InvalidArgument(
63            "eigen_adjacency: graph has zero vertices".into(),
64        ));
65    }
66
67    let directed = graph.is_directed();
68    let ecount = graph.ecount();
69
70    // Pre-build the edge list so the closure is allocation-free.
71    let ecount_u32 = u32::try_from(ecount).unwrap_or(u32::MAX);
72    let mut edges: Vec<(usize, usize)> = Vec::with_capacity(ecount);
73    for eid in 0..ecount_u32 {
74        if let Ok((u, v)) = graph.edge(eid) {
75            edges.push((u as usize, v as usize));
76        }
77    }
78
79    let matvec = |x: &[f64], y: &mut [f64]| {
80        for val in y.iter_mut() {
81            *val = 0.0;
82        }
83        for &(u, v) in &edges {
84            if directed {
85                // Symmetrized: (A + A^T) / 2
86                y[u] += x[v];
87                y[v] += x[u];
88            } else {
89                // Undirected: each edge (u,v) contributes A[u][v] and A[v][u]
90                y[u] += x[v];
91                y[v] += x[u];
92            }
93        }
94        if directed {
95            for val in y.iter_mut() {
96                *val *= 0.5;
97            }
98        }
99    };
100
101    eigen_matrix_symmetric(n, matvec, nev, which)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::core::graph::Graph;
108
109    #[test]
110    fn k3_largest_eigenvalue() {
111        let mut g = Graph::with_vertices(3);
112        g.add_edge(0, 1).unwrap();
113        g.add_edge(0, 2).unwrap();
114        g.add_edge(1, 2).unwrap();
115
116        let result = eigen_adjacency(&g, 1, EigenWhich::LargestAlgebraic).unwrap();
117        assert_eq!(result.eigenvalues.len(), 1);
118        assert!(
119            (result.eigenvalues[0] - 2.0).abs() < 1e-4,
120            "K3 largest eigenvalue should be 2.0, got {}",
121            result.eigenvalues[0]
122        );
123    }
124
125    #[test]
126    fn path_graph_eigenvalues() {
127        // Path P_3: vertices 0-1-2, eigenvalues are sqrt(2), 0, -sqrt(2)
128        let mut g = Graph::with_vertices(3);
129        g.add_edge(0, 1).unwrap();
130        g.add_edge(1, 2).unwrap();
131
132        let result = eigen_adjacency(&g, 1, EigenWhich::LargestAlgebraic).unwrap();
133        let expected = std::f64::consts::SQRT_2;
134        assert!(
135            (result.eigenvalues[0] - expected).abs() < 1e-4,
136            "P3 largest eigenvalue should be sqrt(2)={expected}, got {}",
137            result.eigenvalues[0]
138        );
139    }
140
141    #[test]
142    fn isolated_vertices() {
143        let g = Graph::with_vertices(5);
144        let result = eigen_adjacency(&g, 3, EigenWhich::LargestAlgebraic).unwrap();
145        for &ev in &result.eigenvalues {
146            assert!(
147                ev.abs() < 1e-6,
148                "isolated graph eigenvalue should be 0.0, got {ev}"
149            );
150        }
151    }
152
153    #[test]
154    fn empty_graph_error() {
155        let g = Graph::with_vertices(0);
156        assert!(eigen_adjacency(&g, 1, EigenWhich::LargestAlgebraic).is_err());
157    }
158
159    #[test]
160    fn star_graph_eigenvalues() {
161        // Star S_4 (center 0, leaves 1,2,3): eigenvalues are sqrt(3), 0, 0, -sqrt(3)
162        let mut g = Graph::with_vertices(4);
163        g.add_edge(0, 1).unwrap();
164        g.add_edge(0, 2).unwrap();
165        g.add_edge(0, 3).unwrap();
166
167        let result = eigen_adjacency(&g, 1, EigenWhich::LargestAlgebraic).unwrap();
168        let expected = 3.0_f64.sqrt();
169        assert!(
170            (result.eigenvalues[0] - expected).abs() < 1e-4,
171            "S4 largest eigenvalue should be sqrt(3)={expected}, got {}",
172            result.eigenvalues[0]
173        );
174    }
175
176    #[test]
177    fn cycle_graph_eigenvalues() {
178        // C_4: eigenvalues are 2, 0, 0, -2
179        let mut g = Graph::with_vertices(4);
180        g.add_edge(0, 1).unwrap();
181        g.add_edge(1, 2).unwrap();
182        g.add_edge(2, 3).unwrap();
183        g.add_edge(3, 0).unwrap();
184
185        let result = eigen_adjacency(&g, 1, EigenWhich::LargestAlgebraic).unwrap();
186        assert!(
187            (result.eigenvalues[0] - 2.0).abs() < 1e-4,
188            "C4 largest eigenvalue should be 2.0, got {}",
189            result.eigenvalues[0]
190        );
191    }
192}