Skip to main content

rust_igraph/algorithms/properties/
bridge_ratios.rs

1//! Bridge and articulation-point ratio indices (ALGO-TR-111).
2//!
3//! Measures of structural vulnerability via bridges and cut vertices:
4//!
5//! - **Bridge ratio** — fraction of edges that are bridges
6//! - **Articulation ratio** — fraction of vertices that are cut vertices
7//! - **Biconnected ratio** — fraction of edges in the largest
8//!   biconnected component
9//! - **Leaf ratio** — fraction of vertices with degree 1 (pendant vertices)
10
11#![allow(
12    clippy::cast_possible_truncation,
13    clippy::cast_precision_loss,
14    clippy::many_single_char_names,
15    clippy::needless_range_loop,
16    clippy::similar_names,
17    clippy::too_many_lines
18)]
19
20use crate::core::{Graph, IgraphResult};
21
22/// Compute the bridge ratio.
23///
24/// Fraction of edges that are bridges (whose removal disconnects the
25/// graph). Uses Tarjan's bridge-finding algorithm in O(V+E). Trees
26/// have bridge ratio 1.0; biconnected graphs have 0.0. Returns 0.0
27/// for graphs with no edges.
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, bridge_edge_ratio};
33///
34/// // Path 0-1-2: both edges are bridges → 1.0
35/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
36/// assert!((bridge_edge_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
37/// ```
38pub fn bridge_edge_ratio(graph: &Graph) -> IgraphResult<f64> {
39    let m = graph.ecount();
40    if m == 0 {
41        return Ok(0.0);
42    }
43
44    let bridges = count_bridges(graph)?;
45    Ok(bridges as f64 / m as f64)
46}
47
48/// Compute the articulation ratio.
49///
50/// Fraction of vertices that are articulation points (cut vertices
51/// whose removal disconnects the graph). Uses a DFS-based algorithm
52/// in O(V+E). Returns 0.0 for graphs with fewer than 3 vertices or
53/// no edges.
54///
55/// # Examples
56///
57/// ```
58/// use rust_igraph::{Graph, articulation_ratio};
59///
60/// // Path 0-1-2: vertex 1 is the only cut vertex → 1/3
61/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
62/// assert!((articulation_ratio(&g).unwrap() - 1.0/3.0).abs() < 1e-10);
63/// ```
64pub fn articulation_ratio(graph: &Graph) -> IgraphResult<f64> {
65    let n = graph.vcount() as usize;
66    if n < 3 || graph.ecount() == 0 {
67        return Ok(0.0);
68    }
69
70    let cut_vertices = count_articulation_points(graph)?;
71    Ok(cut_vertices as f64 / n as f64)
72}
73
74/// Compute the biconnected ratio.
75///
76/// Fraction of edges belonging to the largest biconnected component.
77/// A biconnected component is a maximal subgraph with no cut vertices.
78/// Higher values indicate the graph is dominated by a single robust
79/// block. Returns 0.0 for graphs with no edges.
80///
81/// # Examples
82///
83/// ```
84/// use rust_igraph::{Graph, biconnected_ratio};
85///
86/// // K_4: entire graph is biconnected → 1.0
87/// let g = Graph::from_edges(
88///     &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)], false, Some(4)
89/// ).unwrap();
90/// assert!((biconnected_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
91/// ```
92pub fn biconnected_ratio(graph: &Graph) -> IgraphResult<f64> {
93    let m = graph.ecount();
94    if m == 0 {
95        return Ok(0.0);
96    }
97
98    let max_block_edges = largest_block_edge_count(graph)?;
99    Ok(max_block_edges as f64 / m as f64)
100}
101
102/// Compute the leaf ratio.
103///
104/// Fraction of vertices with degree exactly 1 (pendant/leaf vertices).
105/// These are the most vulnerable vertices — removing their single edge
106/// isolates them. Returns 0.0 for empty or edgeless graphs.
107///
108/// # Examples
109///
110/// ```
111/// use rust_igraph::{Graph, leaf_ratio};
112///
113/// // Star_5: 4 leaves out of 5 vertices → 4/5
114/// let g = Graph::from_edges(
115///     &[(0,1),(0,2),(0,3),(0,4)], false, Some(5)
116/// ).unwrap();
117/// assert!((leaf_ratio(&g).unwrap() - 4.0/5.0).abs() < 1e-10);
118/// ```
119pub fn leaf_ratio(graph: &Graph) -> IgraphResult<f64> {
120    let n = graph.vcount() as usize;
121    if n == 0 {
122        return Ok(0.0);
123    }
124
125    let mut leaves = 0_u64;
126    for v in 0..n {
127        if graph.degree(v as u32)? == 1 {
128            leaves += 1;
129        }
130    }
131
132    Ok(leaves as f64 / n as f64)
133}
134
135/// Count bridges using iterative Tarjan's algorithm.
136fn count_bridges(graph: &Graph) -> IgraphResult<u64> {
137    let n = graph.vcount() as usize;
138    if n == 0 {
139        return Ok(0);
140    }
141
142    let mut disc = vec![0_u32; n];
143    let mut low = vec![0_u32; n];
144    let mut visited = vec![false; n];
145    let mut timer = 1_u32;
146    let mut bridges = 0_u64;
147
148    for start in 0..n {
149        if visited[start] {
150            continue;
151        }
152
153        // Iterative DFS with stack: (vertex, parent, neighbor_index)
154        let mut stack: Vec<(usize, usize, usize)> = Vec::new();
155        visited[start] = true;
156        disc[start] = timer;
157        low[start] = timer;
158        timer += 1;
159        stack.push((start, usize::MAX, 0));
160
161        while let Some((v, parent, idx)) = stack.last_mut() {
162            let v = *v;
163            let parent = *parent;
164            let nbrs = graph.neighbors(v as u32)?;
165
166            if *idx < nbrs.len() {
167                let u = nbrs[*idx] as usize;
168                *idx += 1;
169
170                if !visited[u] {
171                    visited[u] = true;
172                    disc[u] = timer;
173                    low[u] = timer;
174                    timer += 1;
175                    stack.push((u, v, 0));
176                } else if u != parent && disc[u] < low[v] {
177                    let len = stack.len();
178                    let cv = stack[len - 1].0;
179                    if disc[u] < low[cv] {
180                        low[cv] = disc[u];
181                    }
182                }
183            } else {
184                // All neighbors processed, backtrack
185                let cv = v;
186                stack.pop();
187                if let Some(top) = stack.last_mut() {
188                    let pv = top.0;
189                    if low[cv] < low[pv] {
190                        low[pv] = low[cv];
191                    }
192                    if low[cv] > disc[pv] {
193                        bridges += 1;
194                    }
195                }
196            }
197        }
198    }
199
200    Ok(bridges)
201}
202
203/// Count articulation points using iterative Tarjan's algorithm.
204fn count_articulation_points(graph: &Graph) -> IgraphResult<u64> {
205    let n = graph.vcount() as usize;
206    if n == 0 {
207        return Ok(0);
208    }
209
210    let mut disc = vec![0_u32; n];
211    let mut low = vec![0_u32; n];
212    let mut visited = vec![false; n];
213    let mut is_cut = vec![false; n];
214    let mut timer = 1_u32;
215
216    for start in 0..n {
217        if visited[start] {
218            continue;
219        }
220
221        visited[start] = true;
222        disc[start] = timer;
223        low[start] = timer;
224        timer += 1;
225
226        // For the root, count children in DFS tree
227        let mut root_children = 0_u32;
228        let mut stack: Vec<(usize, usize, usize)> = Vec::new();
229        stack.push((start, usize::MAX, 0));
230
231        while let Some((v, parent, idx)) = stack.last_mut() {
232            let v = *v;
233            let parent = *parent;
234            let nbrs = graph.neighbors(v as u32)?;
235
236            if *idx < nbrs.len() {
237                let u = nbrs[*idx] as usize;
238                *idx += 1;
239
240                if !visited[u] {
241                    visited[u] = true;
242                    disc[u] = timer;
243                    low[u] = timer;
244                    timer += 1;
245
246                    if v == start {
247                        root_children += 1;
248                    }
249
250                    stack.push((u, v, 0));
251                } else if u != parent {
252                    let len = stack.len();
253                    let cv = stack[len - 1].0;
254                    if disc[u] < low[cv] {
255                        low[cv] = disc[u];
256                    }
257                }
258            } else {
259                let cv = v;
260                stack.pop();
261                if let Some(top) = stack.last_mut() {
262                    let pv = top.0;
263                    if low[cv] < low[pv] {
264                        low[pv] = low[cv];
265                    }
266                    // Non-root: pv is cut vertex if low[cv] >= disc[pv]
267                    if pv != start && low[cv] >= disc[pv] {
268                        is_cut[pv] = true;
269                    }
270                }
271            }
272        }
273
274        // Root is cut vertex if it has >1 children in DFS tree
275        if root_children > 1 {
276            is_cut[start] = true;
277        }
278    }
279
280    Ok(is_cut.iter().filter(|&&c| c).count() as u64)
281}
282
283/// Find edge count of the largest biconnected component.
284fn largest_block_edge_count(graph: &Graph) -> IgraphResult<u64> {
285    let n = graph.vcount() as usize;
286    if n == 0 {
287        return Ok(0);
288    }
289
290    let mut disc = vec![0_u32; n];
291    let mut low = vec![0_u32; n];
292    let mut visited = vec![false; n];
293    let mut timer = 1_u32;
294    let mut max_block = 0_u64;
295
296    // Edge stack for biconnected component decomposition
297    let mut edge_stack: Vec<(usize, usize)> = Vec::new();
298
299    for start in 0..n {
300        if visited[start] {
301            continue;
302        }
303
304        visited[start] = true;
305        disc[start] = timer;
306        low[start] = timer;
307        timer += 1;
308
309        let mut stack: Vec<(usize, usize, usize)> = Vec::new();
310        stack.push((start, usize::MAX, 0));
311
312        while let Some((v, parent, idx)) = stack.last_mut() {
313            let v = *v;
314            let parent = *parent;
315            let nbrs = graph.neighbors(v as u32)?;
316
317            if *idx < nbrs.len() {
318                let u = nbrs[*idx] as usize;
319                *idx += 1;
320
321                if !visited[u] {
322                    visited[u] = true;
323                    disc[u] = timer;
324                    low[u] = timer;
325                    timer += 1;
326                    edge_stack.push((v, u));
327                    stack.push((u, v, 0));
328                } else if u != parent && disc[u] < disc[v] {
329                    edge_stack.push((v, u));
330                    let len = stack.len();
331                    let cv = stack[len - 1].0;
332                    if disc[u] < low[cv] {
333                        low[cv] = disc[u];
334                    }
335                }
336            } else {
337                let cv = v;
338                stack.pop();
339                if let Some(top) = stack.last_mut() {
340                    let pv = top.0;
341                    if low[cv] < low[pv] {
342                        low[pv] = low[cv];
343                    }
344                    // Extract biconnected component
345                    if low[cv] >= disc[pv] {
346                        let mut block_edges = 0_u64;
347                        while let Some(&(a, b)) = edge_stack.last() {
348                            edge_stack.pop();
349                            block_edges += 1;
350                            if (a == pv && b == cv) || (a == cv && b == pv) {
351                                break;
352                            }
353                        }
354                        if block_edges > max_block {
355                            max_block = block_edges;
356                        }
357                    }
358                }
359            }
360        }
361    }
362
363    Ok(max_block)
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    fn empty() -> Graph {
371        Graph::with_vertices(0)
372    }
373
374    fn single() -> Graph {
375        Graph::with_vertices(1)
376    }
377
378    fn single_edge() -> Graph {
379        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
380    }
381
382    fn path3() -> Graph {
383        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
384    }
385
386    fn path4() -> Graph {
387        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
388    }
389
390    fn k3() -> Graph {
391        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
392    }
393
394    fn k4() -> Graph {
395        Graph::from_edges(
396            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
397            false,
398            Some(4),
399        )
400        .unwrap()
401    }
402
403    fn cycle4() -> Graph {
404        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
405    }
406
407    fn star5() -> Graph {
408        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
409    }
410
411    fn paw() -> Graph {
412        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
413    }
414
415    fn bowtie() -> Graph {
416        // Two triangles sharing vertex 2: {0,1,2} and {2,3,4}
417        Graph::from_edges(
418            &[(0, 1), (1, 2), (0, 2), (2, 3), (3, 4), (2, 4)],
419            false,
420            Some(5),
421        )
422        .unwrap()
423    }
424
425    // --- bridge_ratio ---
426
427    #[test]
428    fn br_empty() {
429        assert!(bridge_edge_ratio(&empty()).unwrap().abs() < 1e-10);
430    }
431
432    #[test]
433    fn br_single() {
434        assert!(bridge_edge_ratio(&single()).unwrap().abs() < 1e-10);
435    }
436
437    #[test]
438    fn br_single_edge() {
439        // The single edge is a bridge → 1.0
440        assert!((bridge_edge_ratio(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
441    }
442
443    #[test]
444    fn br_path3() {
445        // Both edges are bridges → 1.0
446        assert!((bridge_edge_ratio(&path3()).unwrap() - 1.0).abs() < 1e-10);
447    }
448
449    #[test]
450    fn br_path4() {
451        // All 3 edges are bridges → 1.0
452        assert!((bridge_edge_ratio(&path4()).unwrap() - 1.0).abs() < 1e-10);
453    }
454
455    #[test]
456    fn br_k3() {
457        // No bridges in a cycle → 0.0
458        assert!(bridge_edge_ratio(&k3()).unwrap().abs() < 1e-10);
459    }
460
461    #[test]
462    fn br_k4() {
463        assert!(bridge_edge_ratio(&k4()).unwrap().abs() < 1e-10);
464    }
465
466    #[test]
467    fn br_cycle4() {
468        assert!(bridge_edge_ratio(&cycle4()).unwrap().abs() < 1e-10);
469    }
470
471    #[test]
472    fn br_star5() {
473        // All 4 edges are bridges → 1.0
474        assert!((bridge_edge_ratio(&star5()).unwrap() - 1.0).abs() < 1e-10);
475    }
476
477    #[test]
478    fn br_paw() {
479        // Edge (2,3) is a bridge, edges in triangle are not → 1/4
480        assert!((bridge_edge_ratio(&paw()).unwrap() - 1.0 / 4.0).abs() < 1e-10);
481    }
482
483    #[test]
484    fn br_bowtie() {
485        // No bridges (each edge is in a triangle) → 0.0
486        assert!(bridge_edge_ratio(&bowtie()).unwrap().abs() < 1e-10);
487    }
488
489    #[test]
490    fn br_in_01() {
491        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
492            let r = bridge_edge_ratio(g).unwrap();
493            assert!(r >= -1e-10);
494            assert!(r <= 1.0 + 1e-10);
495        }
496    }
497
498    // --- articulation_ratio ---
499
500    #[test]
501    fn ar_empty() {
502        assert!(articulation_ratio(&empty()).unwrap().abs() < 1e-10);
503    }
504
505    #[test]
506    fn ar_single() {
507        assert!(articulation_ratio(&single()).unwrap().abs() < 1e-10);
508    }
509
510    #[test]
511    fn ar_single_edge() {
512        // n=2, < 3 → 0.0
513        assert!(articulation_ratio(&single_edge()).unwrap().abs() < 1e-10);
514    }
515
516    #[test]
517    fn ar_path3() {
518        // Vertex 1 is cut vertex → 1/3
519        assert!((articulation_ratio(&path3()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
520    }
521
522    #[test]
523    fn ar_path4() {
524        // Vertices 1,2 are cut vertices → 2/4 = 0.5
525        assert!((articulation_ratio(&path4()).unwrap() - 0.5).abs() < 1e-10);
526    }
527
528    #[test]
529    fn ar_k3() {
530        // No cut vertices → 0.0
531        assert!(articulation_ratio(&k3()).unwrap().abs() < 1e-10);
532    }
533
534    #[test]
535    fn ar_k4() {
536        assert!(articulation_ratio(&k4()).unwrap().abs() < 1e-10);
537    }
538
539    #[test]
540    fn ar_cycle4() {
541        assert!(articulation_ratio(&cycle4()).unwrap().abs() < 1e-10);
542    }
543
544    #[test]
545    fn ar_star5() {
546        // Center (vertex 0) is cut vertex → 1/5
547        assert!((articulation_ratio(&star5()).unwrap() - 1.0 / 5.0).abs() < 1e-10);
548    }
549
550    #[test]
551    fn ar_paw() {
552        // Vertex 2 is cut vertex (removing it disconnects 3) → 1/4
553        assert!((articulation_ratio(&paw()).unwrap() - 1.0 / 4.0).abs() < 1e-10);
554    }
555
556    #[test]
557    fn ar_bowtie() {
558        // Vertex 2 is the only cut vertex → 1/5
559        assert!((articulation_ratio(&bowtie()).unwrap() - 1.0 / 5.0).abs() < 1e-10);
560    }
561
562    #[test]
563    fn ar_in_01() {
564        for g in &[path3(), k3(), k4(), cycle4(), star5(), paw(), bowtie()] {
565            let r = articulation_ratio(g).unwrap();
566            assert!(r >= -1e-10);
567            assert!(r <= 1.0 + 1e-10);
568        }
569    }
570
571    // --- biconnected_ratio ---
572
573    #[test]
574    fn bcr_empty() {
575        assert!(biconnected_ratio(&empty()).unwrap().abs() < 1e-10);
576    }
577
578    #[test]
579    fn bcr_single_edge() {
580        // Single edge is its own block → 1/1 = 1.0
581        assert!((biconnected_ratio(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
582    }
583
584    #[test]
585    fn bcr_path3() {
586        // Two blocks of 1 edge each → max=1, total=2 → 0.5
587        assert!((biconnected_ratio(&path3()).unwrap() - 0.5).abs() < 1e-10);
588    }
589
590    #[test]
591    fn bcr_k3() {
592        // Entire graph is one block → 1.0
593        assert!((biconnected_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
594    }
595
596    #[test]
597    fn bcr_k4() {
598        assert!((biconnected_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
599    }
600
601    #[test]
602    fn bcr_cycle4() {
603        assert!((biconnected_ratio(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
604    }
605
606    #[test]
607    fn bcr_paw() {
608        // Two blocks: triangle {0,1,2} (3 edges) and bridge {2,3} (1 edge)
609        // Max = 3, total = 4 → 3/4
610        assert!((biconnected_ratio(&paw()).unwrap() - 3.0 / 4.0).abs() < 1e-10);
611    }
612
613    #[test]
614    fn bcr_bowtie() {
615        // Two blocks: {0,1,2} (3 edges) and {2,3,4} (3 edges) → max=3, total=6 → 0.5
616        assert!((biconnected_ratio(&bowtie()).unwrap() - 0.5).abs() < 1e-10);
617    }
618
619    #[test]
620    fn bcr_in_01() {
621        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
622            let r = biconnected_ratio(g).unwrap();
623            assert!(r >= -1e-10);
624            assert!(r <= 1.0 + 1e-10);
625        }
626    }
627
628    // --- leaf_ratio ---
629
630    #[test]
631    fn lr_empty() {
632        assert!(leaf_ratio(&empty()).unwrap().abs() < 1e-10);
633    }
634
635    #[test]
636    fn lr_single() {
637        // degree 0, not a leaf
638        assert!(leaf_ratio(&single()).unwrap().abs() < 1e-10);
639    }
640
641    #[test]
642    fn lr_single_edge() {
643        // Both vertices have degree 1 → 1.0
644        assert!((leaf_ratio(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
645    }
646
647    #[test]
648    fn lr_path3() {
649        // Vertices 0,2 have degree 1 → 2/3
650        assert!((leaf_ratio(&path3()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
651    }
652
653    #[test]
654    fn lr_k3() {
655        // All degree 2 → 0.0
656        assert!(leaf_ratio(&k3()).unwrap().abs() < 1e-10);
657    }
658
659    #[test]
660    fn lr_k4() {
661        assert!(leaf_ratio(&k4()).unwrap().abs() < 1e-10);
662    }
663
664    #[test]
665    fn lr_cycle4() {
666        assert!(leaf_ratio(&cycle4()).unwrap().abs() < 1e-10);
667    }
668
669    #[test]
670    fn lr_star5() {
671        // 4 leaves out of 5 → 4/5
672        assert!((leaf_ratio(&star5()).unwrap() - 4.0 / 5.0).abs() < 1e-10);
673    }
674
675    #[test]
676    fn lr_paw() {
677        // Vertex 3 has degree 1 → 1/4
678        assert!((leaf_ratio(&paw()).unwrap() - 1.0 / 4.0).abs() < 1e-10);
679    }
680
681    #[test]
682    fn lr_in_01() {
683        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
684            let r = leaf_ratio(g).unwrap();
685            assert!(r >= -1e-10);
686            assert!(r <= 1.0 + 1e-10);
687        }
688    }
689
690    // --- cross-consistency ---
691
692    #[test]
693    fn tree_all_bridges() {
694        // Trees: all edges are bridges
695        assert!((bridge_edge_ratio(&path3()).unwrap() - 1.0).abs() < 1e-10);
696        assert!((bridge_edge_ratio(&star5()).unwrap() - 1.0).abs() < 1e-10);
697        assert!((bridge_edge_ratio(&path4()).unwrap() - 1.0).abs() < 1e-10);
698    }
699
700    #[test]
701    fn biconnected_no_bridges() {
702        // Biconnected graphs: no bridges
703        assert!(bridge_edge_ratio(&k3()).unwrap().abs() < 1e-10);
704        assert!(bridge_edge_ratio(&k4()).unwrap().abs() < 1e-10);
705        assert!(bridge_edge_ratio(&cycle4()).unwrap().abs() < 1e-10);
706    }
707
708    #[test]
709    fn biconnected_no_cut_vertices() {
710        assert!(articulation_ratio(&k3()).unwrap().abs() < 1e-10);
711        assert!(articulation_ratio(&k4()).unwrap().abs() < 1e-10);
712        assert!(articulation_ratio(&cycle4()).unwrap().abs() < 1e-10);
713    }
714
715    #[test]
716    fn biconnected_full_block() {
717        // Biconnected: entire graph is one block → ratio = 1.0
718        assert!((biconnected_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
719        assert!((biconnected_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
720        assert!((biconnected_ratio(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
721    }
722}