Skip to main content

rust_igraph/algorithms/properties/
path_ratios.rs

1//! Path-based ratio indices (ALGO-TR-101).
2//!
3//! Path and distance-based structural ratios:
4//!
5//! - **Diameter-radius ratio** — diameter / radius
6//! - **Average path fraction** — average shortest-path length / diameter
7//! - **Efficiency ratio** — global efficiency / max possible efficiency
8//! - **Compactness** — 1 - average distance / diameter
9
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_precision_loss,
13    clippy::many_single_char_names,
14    clippy::needless_range_loop,
15    clippy::similar_names,
16    clippy::too_many_lines
17)]
18
19use crate::core::{Graph, IgraphResult};
20
21/// Compute the diameter-radius ratio.
22///
23/// `diameter / radius` for connected graphs. A value of 1 means the
24/// graph is self-centered (diameter equals radius). Returns 0.0 for
25/// disconnected or trivial graphs.
26///
27/// # Examples
28///
29/// ```
30/// use rust_igraph::{Graph, diameter_radius_ratio};
31///
32/// // K_3: diameter=1, radius=1 → 1.0
33/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
34/// assert!((diameter_radius_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
35/// ```
36pub fn diameter_radius_ratio(graph: &Graph) -> IgraphResult<f64> {
37    let n = graph.vcount() as usize;
38    if n < 2 {
39        return Ok(0.0);
40    }
41
42    let dist = all_pairs_bfs(graph)?;
43
44    let mut max_ecc = 0_u32;
45    let mut min_ecc = u32::MAX;
46
47    for v in 0..n {
48        let mut ecc = 0_u32;
49        for u in 0..n {
50            if u == v {
51                continue;
52            }
53            let d = dist[v * n + u];
54            if d == u32::MAX {
55                return Ok(0.0);
56            }
57            if d > ecc {
58                ecc = d;
59            }
60        }
61        if ecc > max_ecc {
62            max_ecc = ecc;
63        }
64        if ecc < min_ecc {
65            min_ecc = ecc;
66        }
67    }
68
69    if min_ecc == 0 {
70        return Ok(0.0);
71    }
72
73    Ok(f64::from(max_ecc) / f64::from(min_ecc))
74}
75
76/// Compute the average path fraction.
77///
78/// `avg_dist / diameter` — how close the average shortest-path length
79/// is to the diameter. Values close to 1 indicate most paths are near
80/// the diameter; values close to 0 indicate most paths are short.
81/// Returns 0.0 for disconnected or trivial graphs.
82///
83/// # Examples
84///
85/// ```
86/// use rust_igraph::{Graph, avg_path_fraction};
87///
88/// // K_3: avg_dist=1, diameter=1 → 1.0
89/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
90/// assert!((avg_path_fraction(&g).unwrap() - 1.0).abs() < 1e-10);
91/// ```
92pub fn avg_path_fraction(graph: &Graph) -> IgraphResult<f64> {
93    let n = graph.vcount() as usize;
94    if n < 2 {
95        return Ok(0.0);
96    }
97
98    let dist = all_pairs_bfs(graph)?;
99
100    let mut sum = 0_u64;
101    let mut diameter = 0_u32;
102    let mut pair_count = 0_u64;
103
104    for v in 0..n {
105        for u in (v + 1)..n {
106            let d = dist[v * n + u];
107            if d == u32::MAX {
108                return Ok(0.0);
109            }
110            sum += u64::from(d);
111            pair_count += 1;
112            if d > diameter {
113                diameter = d;
114            }
115        }
116    }
117
118    if diameter == 0 || pair_count == 0 {
119        return Ok(0.0);
120    }
121
122    let avg_dist = sum as f64 / pair_count as f64;
123    Ok(avg_dist / f64::from(diameter))
124}
125
126/// Compute the efficiency ratio.
127///
128/// `global_efficiency / max_efficiency` where `global_efficiency` is the
129/// average inverse shortest-path length over all pairs, and
130/// `max_efficiency = 1` (the efficiency of a complete graph).
131/// Equivalent to just the global efficiency for simple graphs.
132/// Returns 0.0 for graphs with fewer than 2 vertices.
133///
134/// # Examples
135///
136/// ```
137/// use rust_igraph::{Graph, efficiency_ratio};
138///
139/// // K_3: all pairs at distance 1, eff = 1.0
140/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
141/// assert!((efficiency_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
142/// ```
143pub fn efficiency_ratio(graph: &Graph) -> IgraphResult<f64> {
144    let n = graph.vcount() as usize;
145    if n < 2 {
146        return Ok(0.0);
147    }
148
149    let dist = all_pairs_bfs(graph)?;
150
151    let mut sum_inv = 0.0_f64;
152    let mut pair_count = 0_u64;
153
154    for v in 0..n {
155        for u in (v + 1)..n {
156            let d = dist[v * n + u];
157            if d != u32::MAX && d > 0 {
158                sum_inv += 1.0 / f64::from(d);
159            }
160            pair_count += 1;
161        }
162    }
163
164    if pair_count == 0 {
165        return Ok(0.0);
166    }
167
168    Ok(sum_inv / pair_count as f64)
169}
170
171/// Compute the compactness of the graph.
172///
173/// `1 - avg_dist / diameter` — how compact the graph is relative to
174/// its diameter. A value of 0 means the average distance equals the
175/// diameter; close to 1 means most pairs are much closer than the
176/// diameter. Returns 0.0 for disconnected or trivial graphs.
177///
178/// # Examples
179///
180/// ```
181/// use rust_igraph::{Graph, graph_compactness};
182///
183/// // K_3: avg=1, diam=1 → 1-1/1 = 0.0
184/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
185/// assert!(graph_compactness(&g).unwrap().abs() < 1e-10);
186/// ```
187pub fn graph_compactness(graph: &Graph) -> IgraphResult<f64> {
188    let n = graph.vcount() as usize;
189    if n < 2 {
190        return Ok(0.0);
191    }
192
193    let dist = all_pairs_bfs(graph)?;
194
195    let mut sum = 0_u64;
196    let mut diameter = 0_u32;
197    let mut pair_count = 0_u64;
198
199    for v in 0..n {
200        for u in (v + 1)..n {
201            let d = dist[v * n + u];
202            if d == u32::MAX {
203                return Ok(0.0);
204            }
205            sum += u64::from(d);
206            pair_count += 1;
207            if d > diameter {
208                diameter = d;
209            }
210        }
211    }
212
213    if diameter == 0 || pair_count == 0 {
214        return Ok(0.0);
215    }
216
217    let avg_dist = sum as f64 / pair_count as f64;
218    Ok(1.0 - avg_dist / f64::from(diameter))
219}
220
221fn all_pairs_bfs(graph: &Graph) -> IgraphResult<Vec<u32>> {
222    let n = graph.vcount() as usize;
223    let mut dist = vec![u32::MAX; n * n];
224
225    for s in 0..n {
226        dist[s * n + s] = 0;
227        let mut queue = std::collections::VecDeque::new();
228        queue.push_back(s);
229        while let Some(v) = queue.pop_front() {
230            let d = dist[s * n + v];
231            let neighbors = graph.neighbors(v as u32)?;
232            for &u in &neighbors {
233                let ui = u as usize;
234                if dist[s * n + ui] == u32::MAX {
235                    dist[s * n + ui] = d + 1;
236                    queue.push_back(ui);
237                }
238            }
239        }
240    }
241
242    Ok(dist)
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    fn single_edge() -> Graph {
250        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
251    }
252
253    fn path3() -> Graph {
254        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
255    }
256
257    fn k3() -> Graph {
258        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
259    }
260
261    fn k4() -> Graph {
262        Graph::from_edges(
263            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
264            false,
265            Some(4),
266        )
267        .unwrap()
268    }
269
270    fn cycle4() -> Graph {
271        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
272    }
273
274    fn star5() -> Graph {
275        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
276    }
277
278    fn paw() -> Graph {
279        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
280    }
281
282    // --- diameter_radius_ratio ---
283
284    #[test]
285    fn drr_empty() {
286        let g = Graph::with_vertices(0);
287        assert!(diameter_radius_ratio(&g).unwrap().abs() < 1e-10);
288    }
289
290    #[test]
291    fn drr_single() {
292        let g = Graph::with_vertices(1);
293        assert!(diameter_radius_ratio(&g).unwrap().abs() < 1e-10);
294    }
295
296    #[test]
297    fn drr_single_edge() {
298        // diam=1, rad=1 → 1.0
299        assert!((diameter_radius_ratio(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
300    }
301
302    #[test]
303    fn drr_k3() {
304        // diam=1, rad=1 → 1.0
305        assert!((diameter_radius_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
306    }
307
308    #[test]
309    fn drr_k4() {
310        // diam=1, rad=1 → 1.0
311        assert!((diameter_radius_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
312    }
313
314    #[test]
315    fn drr_cycle4() {
316        // diam=2, rad=2 → 1.0
317        assert!((diameter_radius_ratio(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
318    }
319
320    #[test]
321    fn drr_path3() {
322        // ecc: [2,1,2], diam=2, rad=1 → 2.0
323        assert!((diameter_radius_ratio(&path3()).unwrap() - 2.0).abs() < 1e-10);
324    }
325
326    #[test]
327    fn drr_star5() {
328        // ecc: center=1, leaves=2, diam=2, rad=1 → 2.0
329        assert!((diameter_radius_ratio(&star5()).unwrap() - 2.0).abs() < 1e-10);
330    }
331
332    #[test]
333    fn drr_paw() {
334        // Distances from each vertex:
335        // v0: [0,1,1,2] ecc=2
336        // v1: [1,0,1,2] ecc=2
337        // v2: [1,1,0,1] ecc=1
338        // v3: [2,2,1,0] ecc=2
339        // diam=2, rad=1 → 2.0
340        assert!((diameter_radius_ratio(&paw()).unwrap() - 2.0).abs() < 1e-10);
341    }
342
343    #[test]
344    fn drr_disconnected() {
345        // Disconnected → 0.0
346        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
347        assert!(diameter_radius_ratio(&g).unwrap().abs() < 1e-10);
348    }
349
350    // --- avg_path_fraction ---
351
352    #[test]
353    fn apf_empty() {
354        let g = Graph::with_vertices(0);
355        assert!(avg_path_fraction(&g).unwrap().abs() < 1e-10);
356    }
357
358    #[test]
359    fn apf_single() {
360        let g = Graph::with_vertices(1);
361        assert!(avg_path_fraction(&g).unwrap().abs() < 1e-10);
362    }
363
364    #[test]
365    fn apf_k3() {
366        // avg=1, diam=1 → 1.0
367        assert!((avg_path_fraction(&k3()).unwrap() - 1.0).abs() < 1e-10);
368    }
369
370    #[test]
371    fn apf_k4() {
372        // avg=1, diam=1 → 1.0
373        assert!((avg_path_fraction(&k4()).unwrap() - 1.0).abs() < 1e-10);
374    }
375
376    #[test]
377    fn apf_path3() {
378        // pairs: (0,1)=1, (0,2)=2, (1,2)=1 → sum=4, avg=4/3
379        // diam=2 → 4/3/2 = 2/3
380        assert!((avg_path_fraction(&path3()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
381    }
382
383    #[test]
384    fn apf_cycle4() {
385        // pairs: (0,1)=1,(0,2)=2,(0,3)=1,(1,2)=1,(1,3)=2,(2,3)=1
386        // sum=8, avg=8/6=4/3, diam=2 → (4/3)/2 = 2/3
387        assert!((avg_path_fraction(&cycle4()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
388    }
389
390    #[test]
391    fn apf_disconnected() {
392        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
393        assert!(avg_path_fraction(&g).unwrap().abs() < 1e-10);
394    }
395
396    // --- efficiency_ratio ---
397
398    #[test]
399    fn er_empty() {
400        let g = Graph::with_vertices(0);
401        assert!(efficiency_ratio(&g).unwrap().abs() < 1e-10);
402    }
403
404    #[test]
405    fn er_single() {
406        let g = Graph::with_vertices(1);
407        assert!(efficiency_ratio(&g).unwrap().abs() < 1e-10);
408    }
409
410    #[test]
411    fn er_k3() {
412        // All pairs distance 1, inv = 1 each, avg=3/3=1.0
413        assert!((efficiency_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
414    }
415
416    #[test]
417    fn er_k4() {
418        // All pairs distance 1, inv = 1 each, avg=6/6=1.0
419        assert!((efficiency_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
420    }
421
422    #[test]
423    fn er_path3() {
424        // pairs: (0,1)=1→1, (0,2)=2→0.5, (1,2)=1→1 → sum=2.5/3
425        assert!((efficiency_ratio(&path3()).unwrap() - 2.5 / 3.0).abs() < 1e-10);
426    }
427
428    #[test]
429    fn er_cycle4() {
430        // pairs: (0,1)=1→1, (0,2)=2→0.5, (0,3)=1→1, (1,2)=1→1, (1,3)=2→0.5, (2,3)=1→1
431        // sum_inv = 5, pairs=6, eff=5/6
432        assert!((efficiency_ratio(&cycle4()).unwrap() - 5.0 / 6.0).abs() < 1e-10);
433    }
434
435    #[test]
436    fn er_isolated() {
437        let g = Graph::with_vertices(3);
438        // All pairs infinite → 0 inverse → eff=0
439        assert!(efficiency_ratio(&g).unwrap().abs() < 1e-10);
440    }
441
442    // --- graph_compactness ---
443
444    #[test]
445    fn gc_empty() {
446        let g = Graph::with_vertices(0);
447        assert!(graph_compactness(&g).unwrap().abs() < 1e-10);
448    }
449
450    #[test]
451    fn gc_single() {
452        let g = Graph::with_vertices(1);
453        assert!(graph_compactness(&g).unwrap().abs() < 1e-10);
454    }
455
456    #[test]
457    fn gc_k3() {
458        // avg=1, diam=1 → 1-1/1 = 0
459        assert!(graph_compactness(&k3()).unwrap().abs() < 1e-10);
460    }
461
462    #[test]
463    fn gc_k4() {
464        // avg=1, diam=1 → 0
465        assert!(graph_compactness(&k4()).unwrap().abs() < 1e-10);
466    }
467
468    #[test]
469    fn gc_path3() {
470        // avg=4/3, diam=2 → 1 - (4/3)/2 = 1 - 2/3 = 1/3
471        assert!((graph_compactness(&path3()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
472    }
473
474    #[test]
475    fn gc_cycle4() {
476        // avg=4/3, diam=2 → 1 - 2/3 = 1/3
477        assert!((graph_compactness(&cycle4()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
478    }
479
480    #[test]
481    fn gc_disconnected() {
482        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
483        assert!(graph_compactness(&g).unwrap().abs() < 1e-10);
484    }
485
486    // --- cross-consistency ---
487
488    #[test]
489    fn drr_ge1() {
490        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
491            let r = diameter_radius_ratio(g).unwrap();
492            assert!(r >= 1.0 - 1e-10 || r.abs() < 1e-10);
493        }
494    }
495
496    #[test]
497    fn apf_in_01() {
498        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
499            let r = avg_path_fraction(g).unwrap();
500            assert!(r >= -1e-10);
501            assert!(r <= 1.0 + 1e-10);
502        }
503    }
504
505    #[test]
506    fn er_in_01() {
507        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
508            let r = efficiency_ratio(g).unwrap();
509            assert!(r >= -1e-10);
510            assert!(r <= 1.0 + 1e-10);
511        }
512    }
513
514    #[test]
515    fn gc_in_01() {
516        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
517            let r = graph_compactness(g).unwrap();
518            assert!(r >= -1e-10);
519            assert!(r <= 1.0 + 1e-10);
520        }
521    }
522
523    #[test]
524    fn complete_graphs_self_centered() {
525        // Complete graphs: diameter = radius → ratio = 1
526        assert!((diameter_radius_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
527        assert!((diameter_radius_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
528    }
529
530    #[test]
531    fn complete_graphs_full_efficiency() {
532        assert!((efficiency_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
533        assert!((efficiency_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
534    }
535}