Skip to main content

rust_igraph/algorithms/properties/
hyperbolicity.rs

1//! Gromov δ-hyperbolicity (ALGO-TR-039).
2//!
3//! The **Gromov δ-hyperbolicity** of a graph measures how tree-like its
4//! metric structure is. For each 4-tuple of vertices `(u, v, w, x)`,
5//! form the three sums of pairwise distances:
6//!
7//!   `S1 = d(u,v) + d(w,x)`,  `S2 = d(u,w) + d(v,x)`,  `S3 = d(u,x) + d(v,w)`
8//!
9//! Sort these so that `S_max >= S_mid >= S_min`. The four-point
10//! condition gives `δ_4 = (S_max - S_mid) / 2`. The hyperbolicity
11//! is `δ = max δ_4` over all 4-tuples.
12//!
13//! A tree has `δ = 0`. Cycles `C_n` have `δ = ⌊n/4⌋` (for `n >= 4`).
14//! The value is always a non-negative half-integer, so we return
15//! `2δ` as a `u32` to avoid floating point.
16
17#![allow(
18    clippy::cast_possible_truncation,
19    clippy::cast_precision_loss,
20    clippy::many_single_char_names,
21    clippy::needless_range_loop,
22    clippy::too_many_lines
23)]
24
25use crate::core::Graph;
26use std::collections::VecDeque;
27
28/// Compute the Gromov δ-hyperbolicity of a graph.
29///
30/// Returns `2δ` as an integer (since δ is always a half-integer).
31/// To get the actual δ, divide by 2. Trees have `2δ = 0`.
32///
33/// Only considers the largest connected component if the graph is
34/// disconnected. Only feasible for small graphs — the brute-force
35/// algorithm is O(n^4).
36///
37/// # Examples
38///
39/// ```
40/// use rust_igraph::{Graph, hyperbolicity_twice};
41///
42/// // Tree: δ = 0
43/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
44/// assert_eq!(hyperbolicity_twice(&g).unwrap(), 0);
45///
46/// // C_4: δ = 1, so 2δ = 2
47/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3),(3,0)], false, Some(4)).unwrap();
48/// assert_eq!(hyperbolicity_twice(&g).unwrap(), 2);
49/// ```
50pub fn hyperbolicity_twice(graph: &Graph) -> Result<u32, crate::core::IgraphError> {
51    let n = graph.vcount() as usize;
52    if n < 4 {
53        return Ok(0);
54    }
55
56    let dist = all_pairs_bfs(graph, n);
57
58    let mut max_twice_delta: u32 = 0;
59
60    for u in 0..n {
61        for v in (u + 1)..n {
62            if dist[u * n + v] == u32::MAX {
63                continue;
64            }
65            for w in (v + 1)..n {
66                if dist[u * n + w] == u32::MAX || dist[v * n + w] == u32::MAX {
67                    continue;
68                }
69                for x in (w + 1)..n {
70                    if dist[u * n + x] == u32::MAX
71                        || dist[v * n + x] == u32::MAX
72                        || dist[w * n + x] == u32::MAX
73                    {
74                        continue;
75                    }
76
77                    let s1 = dist[u * n + v].saturating_add(dist[w * n + x]);
78                    let s2 = dist[u * n + w].saturating_add(dist[v * n + x]);
79                    let s3 = dist[u * n + x].saturating_add(dist[v * n + w]);
80
81                    let mut sums = [s1, s2, s3];
82                    sums.sort_unstable();
83
84                    let twice_delta = sums[2].saturating_sub(sums[1]);
85                    if twice_delta > max_twice_delta {
86                        max_twice_delta = twice_delta;
87                    }
88                }
89            }
90        }
91    }
92
93    Ok(max_twice_delta)
94}
95
96/// Compute δ-hyperbolicity as a floating-point value.
97///
98/// Convenience wrapper around [`hyperbolicity_twice`] that returns δ
99/// as `f64`.
100///
101/// # Examples
102///
103/// ```
104/// use rust_igraph::{Graph, hyperbolicity};
105///
106/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3),(3,0)], false, Some(4)).unwrap();
107/// assert!((hyperbolicity(&g).unwrap() - 1.0).abs() < 1e-10);
108/// ```
109pub fn hyperbolicity(graph: &Graph) -> Result<f64, crate::core::IgraphError> {
110    let twice = hyperbolicity_twice(graph)?;
111    Ok(f64::from(twice) / 2.0)
112}
113
114fn all_pairs_bfs(graph: &Graph, n: usize) -> Vec<u32> {
115    let adj = build_adj_list(graph, n);
116    let mut dist = vec![u32::MAX; n * n];
117
118    for src in 0..n {
119        bfs_distances(&adj, n, src, &mut dist);
120    }
121
122    dist
123}
124
125fn build_adj_list(graph: &Graph, n: usize) -> Vec<Vec<usize>> {
126    let mut adj = vec![Vec::new(); n];
127    for (u, v) in graph.edges() {
128        let ui = u as usize;
129        let vi = v as usize;
130        adj[ui].push(vi);
131        if !graph.is_directed() {
132            adj[vi].push(ui);
133        }
134    }
135    adj
136}
137
138fn bfs_distances(adj: &[Vec<usize>], n: usize, src: usize, dist: &mut [u32]) {
139    dist[src * n + src] = 0;
140    let mut queue = VecDeque::new();
141    queue.push_back(src);
142
143    while let Some(v) = queue.pop_front() {
144        let d = dist[src * n + v];
145        for &w in &adj[v] {
146            if dist[src * n + w] == u32::MAX {
147                dist[src * n + w] = d.saturating_add(1);
148                queue.push_back(w);
149            }
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    fn path4() -> Graph {
159        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
160    }
161
162    fn path5() -> Graph {
163        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap()
164    }
165
166    fn k4() -> Graph {
167        Graph::from_edges(
168            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
169            false,
170            Some(4),
171        )
172        .unwrap()
173    }
174
175    fn cycle4() -> Graph {
176        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
177    }
178
179    fn cycle5() -> Graph {
180        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
181    }
182
183    fn cycle6() -> Graph {
184        Graph::from_edges(
185            &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)],
186            false,
187            Some(6),
188        )
189        .unwrap()
190    }
191
192    fn cycle8() -> Graph {
193        Graph::from_edges(
194            &[
195                (0, 1),
196                (1, 2),
197                (2, 3),
198                (3, 4),
199                (4, 5),
200                (5, 6),
201                (6, 7),
202                (7, 0),
203            ],
204            false,
205            Some(8),
206        )
207        .unwrap()
208    }
209
210    fn star5() -> Graph {
211        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
212    }
213
214    fn petersen() -> Graph {
215        Graph::from_edges(
216            &[
217                (0, 1),
218                (1, 2),
219                (2, 3),
220                (3, 4),
221                (4, 0),
222                (0, 5),
223                (1, 6),
224                (2, 7),
225                (3, 8),
226                (4, 9),
227                (5, 7),
228                (7, 9),
229                (9, 6),
230                (6, 8),
231                (8, 5),
232            ],
233            false,
234            Some(10),
235        )
236        .unwrap()
237    }
238
239    // --- hyperbolicity_twice ---
240
241    #[test]
242    fn ht_empty() {
243        let g = Graph::with_vertices(0);
244        assert_eq!(hyperbolicity_twice(&g).unwrap(), 0);
245    }
246
247    #[test]
248    fn ht_small() {
249        let g = Graph::with_vertices(3);
250        assert_eq!(hyperbolicity_twice(&g).unwrap(), 0);
251    }
252
253    #[test]
254    fn ht_path4() {
255        // Trees: δ = 0
256        assert_eq!(hyperbolicity_twice(&path4()).unwrap(), 0);
257    }
258
259    #[test]
260    fn ht_path5() {
261        assert_eq!(hyperbolicity_twice(&path5()).unwrap(), 0);
262    }
263
264    #[test]
265    fn ht_star5() {
266        // Stars are trees: δ = 0
267        assert_eq!(hyperbolicity_twice(&star5()).unwrap(), 0);
268    }
269
270    #[test]
271    fn ht_k4() {
272        // Complete graph K_4: all distances = 1
273        // S1 = S2 = S3 = 2, so δ = 0
274        assert_eq!(hyperbolicity_twice(&k4()).unwrap(), 0);
275    }
276
277    #[test]
278    fn ht_cycle4() {
279        // C_4: δ = 1, 2δ = 2
280        assert_eq!(hyperbolicity_twice(&cycle4()).unwrap(), 2);
281    }
282
283    #[test]
284    fn ht_cycle5() {
285        // C_5: δ = 0.5, 2δ = 1
286        let ht = hyperbolicity_twice(&cycle5()).unwrap();
287        assert_eq!(ht, 1);
288    }
289
290    #[test]
291    fn ht_cycle6() {
292        // C_6: δ = 1, 2δ = 2
293        let ht = hyperbolicity_twice(&cycle6()).unwrap();
294        assert_eq!(ht, 2);
295    }
296
297    #[test]
298    fn ht_cycle8() {
299        // C_8: δ = 2, 2δ = 4
300        assert_eq!(hyperbolicity_twice(&cycle8()).unwrap(), 4);
301    }
302
303    #[test]
304    fn ht_petersen() {
305        // Petersen: diameter = 2, δ = 0.5, 2δ = 1
306        let ht = hyperbolicity_twice(&petersen()).unwrap();
307        assert_eq!(ht, 1);
308    }
309
310    // --- hyperbolicity ---
311
312    #[test]
313    fn h_path() {
314        let h = hyperbolicity(&path4()).unwrap();
315        assert!((h - 0.0).abs() < 1e-10);
316    }
317
318    #[test]
319    fn h_cycle4() {
320        let h = hyperbolicity(&cycle4()).unwrap();
321        assert!((h - 1.0).abs() < 1e-10);
322    }
323
324    #[test]
325    fn h_cycle8() {
326        let h = hyperbolicity(&cycle8()).unwrap();
327        assert!((h - 2.0).abs() < 1e-10);
328    }
329
330    // --- cross-consistency ---
331
332    #[test]
333    fn tree_hyperbolicity_zero() {
334        let trees = vec![
335            path4(),
336            path5(),
337            star5(),
338            Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (2, 4)], false, Some(5)).unwrap(),
339        ];
340        for g in &trees {
341            assert_eq!(hyperbolicity_twice(g).unwrap(), 0);
342        }
343    }
344
345    #[test]
346    fn hyperbolicity_non_negative() {
347        for g in &[path4(), k4(), cycle4(), cycle5(), star5(), petersen()] {
348            assert!(hyperbolicity(g).unwrap() >= 0.0);
349        }
350    }
351
352    #[test]
353    fn hyperbolicity_leq_half_diameter() {
354        // δ <= diameter / 2
355        for g in &[cycle4(), cycle5(), cycle6(), cycle8(), petersen()] {
356            let h = hyperbolicity(g).unwrap();
357            let diam = g.diameter().unwrap().unwrap_or(0);
358            assert!(
359                h <= f64::from(diam) / 2.0 + 1e-10,
360                "δ={h} > diameter/2={}",
361                f64::from(diam) / 2.0
362            );
363        }
364    }
365}