Skip to main content

rust_igraph/algorithms/properties/
flow_ratios.rs

1//! Flow-based ratio indices (ALGO-TR-118).
2//!
3//! Measures derived from network flow and connectivity concepts:
4//!
5//! - **Max-flow efficiency** — average max-flow between all pairs /
6//!   maximum possible flow (related to edge connectivity)
7//! - **Bottleneck ratio** — minimum edge betweenness / maximum edge
8//!   betweenness, measuring flow bottleneck concentration
9//! - **Flow hierarchy** — fraction of edges that are in a minimum
10//!   spanning tree (backbone edges)
11
12#![allow(
13    clippy::cast_lossless,
14    clippy::cast_possible_truncation,
15    clippy::cast_precision_loss,
16    clippy::many_single_char_names,
17    clippy::needless_range_loop,
18    clippy::similar_names,
19    clippy::too_many_lines
20)]
21
22use crate::core::{Graph, IgraphResult};
23
24/// Compute the max-flow efficiency ratio.
25///
26/// For each pair of vertices, the max-flow equals the edge connectivity
27/// between them. We approximate this using the minimum degree of the
28/// two endpoints (an upper bound on the local edge connectivity).
29/// Returns the average over all pairs divided by the global minimum
30/// degree. Values near 1 indicate uniform connectivity; values < 1
31/// indicate some pairs have weaker connections. Returns 0.0 for
32/// disconnected or trivial graphs.
33///
34/// # Examples
35///
36/// ```
37/// use rust_igraph::{Graph, max_flow_efficiency};
38///
39/// // K_4: all pairs have connectivity 3, min_deg=3 → ratio=1.0
40/// let g = Graph::from_edges(
41///     &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)], false, Some(4)
42/// ).unwrap();
43/// assert!((max_flow_efficiency(&g).unwrap() - 1.0).abs() < 1e-10);
44/// ```
45pub fn max_flow_efficiency(graph: &Graph) -> IgraphResult<f64> {
46    let n = graph.vcount() as usize;
47    if n < 2 {
48        return Ok(0.0);
49    }
50
51    let mut degrees = Vec::with_capacity(n);
52    let mut min_deg = usize::MAX;
53    for v in 0..n {
54        let d = graph.degree(v as u32)?;
55        degrees.push(d);
56        if d < min_deg {
57            min_deg = d;
58        }
59    }
60
61    if min_deg == 0 {
62        return Ok(0.0);
63    }
64
65    let mut sum_min_deg = 0_u64;
66    let mut pairs = 0_u64;
67    for v in 0..n {
68        for u in (v + 1)..n {
69            sum_min_deg += degrees[v].min(degrees[u]) as u64;
70            pairs += 1;
71        }
72    }
73
74    if pairs == 0 {
75        return Ok(0.0);
76    }
77
78    let avg_min_deg = sum_min_deg as f64 / pairs as f64;
79    Ok(avg_min_deg / min_deg as f64)
80}
81
82/// Compute the bottleneck ratio.
83///
84/// `min_edge_betweenness / max_edge_betweenness` — measures how
85/// concentrated flow bottlenecks are. Values near 1 indicate all edges
86/// carry similar load (uniform flow); values near 0 indicate a few
87/// edges carry most of the flow. Uses shortest-path betweenness.
88/// Returns 0.0 for trivial graphs.
89///
90/// # Examples
91///
92/// ```
93/// use rust_igraph::{Graph, bottleneck_ratio};
94///
95/// // K_3: all edge betweennesses equal → ratio = 1.0
96/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
97/// assert!((bottleneck_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
98/// ```
99pub fn bottleneck_ratio(graph: &Graph) -> IgraphResult<f64> {
100    let n = graph.vcount() as usize;
101    if n < 2 {
102        return Ok(0.0);
103    }
104
105    let m = graph.ecount();
106    if m == 0 {
107        return Ok(0.0);
108    }
109
110    let betweenness = edge_betweenness(graph, n)?;
111    if betweenness.is_empty() {
112        return Ok(0.0);
113    }
114
115    let mut min_b = f64::MAX;
116    let mut max_b = 0.0_f64;
117    for &b in &betweenness {
118        if b < min_b {
119            min_b = b;
120        }
121        if b > max_b {
122            max_b = b;
123        }
124    }
125
126    if max_b < 1e-30 {
127        return Ok(0.0);
128    }
129
130    Ok(min_b / max_b)
131}
132
133/// Compute the flow hierarchy ratio.
134///
135/// Fraction of edges that would be in a minimum spanning tree (assuming
136/// unit weights, this equals (n-1)/m for connected graphs). Measures
137/// how tree-like the graph is. Values near 1 indicate a tree (all edges
138/// are bridges); values near 0 indicate a densely connected graph.
139/// Returns 0.0 for disconnected or trivial graphs.
140///
141/// # Examples
142///
143/// ```
144/// use rust_igraph::{Graph, flow_hierarchy_ratio};
145///
146/// // Tree (path 0-1-2-3): all edges in MST → (n-1)/m = 3/3 = 1.0
147/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
148/// assert!((flow_hierarchy_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
149/// ```
150pub fn flow_hierarchy_ratio(graph: &Graph) -> IgraphResult<f64> {
151    let n = graph.vcount() as usize;
152    if n < 2 {
153        return Ok(0.0);
154    }
155
156    let m = graph.ecount();
157    if m == 0 {
158        return Ok(0.0);
159    }
160
161    // Check connectivity via BFS from vertex 0
162    let mut visited = vec![false; n];
163    visited[0] = true;
164    let mut queue = std::collections::VecDeque::new();
165    queue.push_back(0_usize);
166    let mut visit_count = 1_usize;
167
168    while let Some(v) = queue.pop_front() {
169        let nbrs = graph.neighbors(v as u32)?;
170        for &u in &nbrs {
171            let ui = u as usize;
172            if !visited[ui] {
173                visited[ui] = true;
174                visit_count += 1;
175                queue.push_back(ui);
176            }
177        }
178    }
179
180    if visit_count < n {
181        return Ok(0.0);
182    }
183
184    // For a connected graph, MST has n-1 edges
185    Ok((n - 1) as f64 / m as f64)
186}
187
188/// Compute edge betweenness for all edges via BFS from every vertex.
189fn edge_betweenness(graph: &Graph, n: usize) -> IgraphResult<Vec<f64>> {
190    // Store betweenness per edge using (min(u,v), max(u,v)) as key
191    let mut bet_map: std::collections::HashMap<(u32, u32), f64> = std::collections::HashMap::new();
192
193    for s in 0..n {
194        // BFS
195        let mut dist = vec![u32::MAX; n];
196        let mut sigma = vec![0_u64; n]; // number of shortest paths
197        let mut pred: Vec<Vec<usize>> = vec![Vec::new(); n];
198        let mut order = Vec::new();
199
200        dist[s] = 0;
201        sigma[s] = 1;
202        let mut queue = std::collections::VecDeque::new();
203        queue.push_back(s);
204
205        while let Some(v) = queue.pop_front() {
206            order.push(v);
207            let nbrs = graph.neighbors(v as u32)?;
208            for &u in &nbrs {
209                let ui = u as usize;
210                if dist[ui] == u32::MAX {
211                    dist[ui] = dist[v] + 1;
212                    queue.push_back(ui);
213                }
214                if dist[ui] == dist[v] + 1 {
215                    sigma[ui] += sigma[v];
216                    pred[ui].push(v);
217                }
218            }
219        }
220
221        // Back-propagation
222        let mut delta = vec![0.0_f64; n];
223        for &w in order.iter().rev() {
224            for &v in &pred[w] {
225                let coeff = (sigma[v] as f64 / sigma[w] as f64) * (1.0 + delta[w]);
226                let e = (v.min(w) as u32, v.max(w) as u32);
227                *bet_map.entry(e).or_insert(0.0) += coeff;
228                delta[v] += coeff;
229            }
230        }
231    }
232
233    Ok(bet_map.into_values().collect())
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    fn empty() -> Graph {
241        Graph::with_vertices(0)
242    }
243
244    fn single() -> Graph {
245        Graph::with_vertices(1)
246    }
247
248    fn single_edge() -> Graph {
249        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
250    }
251
252    fn path3() -> Graph {
253        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
254    }
255
256    fn path4() -> Graph {
257        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
258    }
259
260    fn k3() -> Graph {
261        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
262    }
263
264    fn k4() -> Graph {
265        Graph::from_edges(
266            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
267            false,
268            Some(4),
269        )
270        .unwrap()
271    }
272
273    fn cycle4() -> Graph {
274        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
275    }
276
277    fn star5() -> Graph {
278        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
279    }
280
281    fn paw() -> Graph {
282        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
283    }
284
285    // --- max_flow_efficiency ---
286
287    #[test]
288    fn mfe_empty() {
289        assert!(max_flow_efficiency(&empty()).unwrap().abs() < 1e-10);
290    }
291
292    #[test]
293    fn mfe_single() {
294        assert!(max_flow_efficiency(&single()).unwrap().abs() < 1e-10);
295    }
296
297    #[test]
298    fn mfe_k3() {
299        // All degrees 2, min_deg=2, all pair-min = 2 → avg=2, 2/2=1.0
300        assert!((max_flow_efficiency(&k3()).unwrap() - 1.0).abs() < 1e-10);
301    }
302
303    #[test]
304    fn mfe_k4() {
305        assert!((max_flow_efficiency(&k4()).unwrap() - 1.0).abs() < 1e-10);
306    }
307
308    #[test]
309    fn mfe_cycle4() {
310        assert!((max_flow_efficiency(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
311    }
312
313    #[test]
314    fn mfe_star5() {
315        // Degrees: 4,1,1,1,1; min_deg=1
316        // Pairs: center-leaf: min=1; leaf-leaf: min=1
317        // All pairs have min=1, avg=1, 1/1=1.0
318        assert!((max_flow_efficiency(&star5()).unwrap() - 1.0).abs() < 1e-10);
319    }
320
321    #[test]
322    fn mfe_ge_1() {
323        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
324            let r = max_flow_efficiency(g).unwrap();
325            assert!(r >= 1.0 - 1e-10);
326        }
327    }
328
329    // --- bottleneck_ratio ---
330
331    #[test]
332    fn br_empty() {
333        assert!(bottleneck_ratio(&empty()).unwrap().abs() < 1e-10);
334    }
335
336    #[test]
337    fn br_single() {
338        assert!(bottleneck_ratio(&single()).unwrap().abs() < 1e-10);
339    }
340
341    #[test]
342    fn br_k3() {
343        // All edges have equal betweenness → ratio = 1.0
344        assert!((bottleneck_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
345    }
346
347    #[test]
348    fn br_k4() {
349        assert!((bottleneck_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
350    }
351
352    #[test]
353    fn br_cycle4() {
354        // Symmetric → all equal → 1.0
355        assert!((bottleneck_ratio(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
356    }
357
358    #[test]
359    fn br_in_01() {
360        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
361            let r = bottleneck_ratio(g).unwrap();
362            assert!(r >= -1e-10);
363            assert!(r <= 1.0 + 1e-10);
364        }
365    }
366
367    // --- flow_hierarchy_ratio ---
368
369    #[test]
370    fn fhr_empty() {
371        assert!(flow_hierarchy_ratio(&empty()).unwrap().abs() < 1e-10);
372    }
373
374    #[test]
375    fn fhr_single() {
376        assert!(flow_hierarchy_ratio(&single()).unwrap().abs() < 1e-10);
377    }
378
379    #[test]
380    fn fhr_path3() {
381        // Tree: (n-1)/m = 2/2 = 1.0
382        assert!((flow_hierarchy_ratio(&path3()).unwrap() - 1.0).abs() < 1e-10);
383    }
384
385    #[test]
386    fn fhr_path4() {
387        assert!((flow_hierarchy_ratio(&path4()).unwrap() - 1.0).abs() < 1e-10);
388    }
389
390    #[test]
391    fn fhr_star5() {
392        // Tree: (n-1)/m = 4/4 = 1.0
393        assert!((flow_hierarchy_ratio(&star5()).unwrap() - 1.0).abs() < 1e-10);
394    }
395
396    #[test]
397    fn fhr_k3() {
398        // (3-1)/3 = 2/3
399        assert!((flow_hierarchy_ratio(&k3()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
400    }
401
402    #[test]
403    fn fhr_k4() {
404        // (4-1)/6 = 3/6 = 0.5
405        assert!((flow_hierarchy_ratio(&k4()).unwrap() - 0.5).abs() < 1e-10);
406    }
407
408    #[test]
409    fn fhr_cycle4() {
410        // (4-1)/4 = 3/4 = 0.75
411        assert!((flow_hierarchy_ratio(&cycle4()).unwrap() - 0.75).abs() < 1e-10);
412    }
413
414    #[test]
415    fn fhr_in_01() {
416        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
417            let r = flow_hierarchy_ratio(g).unwrap();
418            assert!(r >= -1e-10);
419            assert!(r <= 1.0 + 1e-10);
420        }
421    }
422
423    // --- cross-consistency ---
424
425    #[test]
426    fn regular_unit_bottleneck() {
427        assert!((bottleneck_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
428        assert!((bottleneck_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
429        assert!((bottleneck_ratio(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
430    }
431
432    #[test]
433    fn trees_unit_hierarchy() {
434        assert!((flow_hierarchy_ratio(&path3()).unwrap() - 1.0).abs() < 1e-10);
435        assert!((flow_hierarchy_ratio(&path4()).unwrap() - 1.0).abs() < 1e-10);
436        assert!((flow_hierarchy_ratio(&star5()).unwrap() - 1.0).abs() < 1e-10);
437    }
438}