Skip to main content

rust_igraph/algorithms/properties/
robustness.rs

1//! Graph robustness and resilience metrics (ALGO-TR-027).
2//!
3//! Measures how resilient a graph is to node/edge removal, building on
4//! the existing flow-based connectivity functions.
5//!
6//! - **Vertex resilience**: `κ(G) / n` — fraction of vertices whose
7//!   removal is needed to disconnect the graph.
8//! - **Edge resilience**: `λ(G) / m` — fraction of edges whose removal
9//!   is needed to disconnect the graph.
10//! - **Toughness**: `min_{S} |S| / ω(G-S)` where `ω` is the number
11//!   of connected components after removing vertex set `S`. Measures
12//!   how many vertices must be removed per component created.
13//! - **Integrity**: `min_{S} (|S| + m(G-S))` where `m(G-S)` is the
14//!   order of the largest component after removing `S`.
15
16#![allow(
17    clippy::cast_possible_truncation,
18    clippy::cast_precision_loss,
19    clippy::many_single_char_names,
20    clippy::needless_range_loop,
21    clippy::too_many_lines
22)]
23
24use crate::core::{Graph, IgraphError, IgraphResult};
25use std::collections::VecDeque;
26
27/// Compute vertex resilience `κ(G) / n`.
28///
29/// The fraction of vertices that must be removed to disconnect the
30/// graph. Higher values indicate more robust graphs. Uses the
31/// flow-based `vertex_connectivity` from the flow module internally.
32///
33/// # Examples
34///
35/// ```
36/// use rust_igraph::{Graph, vertex_resilience};
37///
38/// // K_4: κ = 3, n = 4, resilience = 0.75
39/// let g = Graph::from_edges(
40///     &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)], false, Some(4)
41/// ).unwrap();
42/// let r = vertex_resilience(&g).unwrap();
43/// assert!((r - 0.75).abs() < 0.01);
44/// ```
45pub fn vertex_resilience(graph: &Graph) -> IgraphResult<f64> {
46    let n = graph.vcount() as usize;
47    if n == 0 {
48        return Ok(0.0);
49    }
50    let kappa = crate::algorithms::flow::vertex_connectivity::vertex_connectivity(graph, true)?;
51    Ok(kappa.max(0) as f64 / n as f64)
52}
53
54/// Compute edge resilience `λ(G) / m`.
55///
56/// The fraction of edges that must be removed to disconnect the graph.
57///
58/// # Examples
59///
60/// ```
61/// use rust_igraph::{Graph, edge_resilience};
62///
63/// // Path 0-1-2: λ = 1, m = 2, resilience = 0.5
64/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
65/// let r = edge_resilience(&g).unwrap();
66/// assert!((r - 0.5).abs() < 0.01);
67/// ```
68pub fn edge_resilience(graph: &Graph) -> IgraphResult<f64> {
69    let m = graph.ecount();
70    if m == 0 {
71        return Ok(0.0);
72    }
73    let lambda = crate::algorithms::flow::edge_connectivity::edge_connectivity(graph, true)?;
74    Ok(lambda.max(0) as f64 / m as f64)
75}
76
77/// Count connected components after removing a set of vertices.
78fn count_components_after_removal(graph: &Graph, removed: &[bool]) -> (usize, usize) {
79    let n = graph.vcount() as usize;
80    let mut visited = vec![false; n];
81    let mut num_components = 0_usize;
82    let mut largest_component = 0_usize;
83
84    for start in 0..n {
85        if visited[start] || removed[start] {
86            continue;
87        }
88        num_components += 1;
89        let mut size = 0_usize;
90        let mut queue = VecDeque::new();
91        queue.push_back(start as u32);
92        visited[start] = true;
93
94        while let Some(u) = queue.pop_front() {
95            size += 1;
96            if let Ok(nbrs) = graph.neighbors(u) {
97                for &v in &nbrs {
98                    let vi = v as usize;
99                    if !visited[vi] && !removed[vi] {
100                        visited[vi] = true;
101                        queue.push_back(v);
102                    }
103                }
104            }
105        }
106
107        if size > largest_component {
108            largest_component = size;
109        }
110    }
111
112    (num_components, largest_component)
113}
114
115/// Compute the toughness of a graph.
116///
117/// `τ(G) = min_{S ⊂ V, ω(G-S)>1} |S| / ω(G-S)`
118///
119/// where `ω(G-S)` is the number of connected components of `G` after
120/// removing vertex set `S`. A graph is `t`-tough if `τ(G) ≥ t`.
121///
122/// Returns `f64::INFINITY` for complete graphs (no vertex set
123/// disconnects them into multiple components). Returns `0.0` for
124/// disconnected graphs.
125///
126/// Brute-force — suitable for graphs with ≤ ~20 vertices.
127///
128/// # Examples
129///
130/// ```
131/// use rust_igraph::{Graph, graph_toughness};
132///
133/// // Path 0-1-2: removing {1} → 2 components → τ = 1/2
134/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
135/// let t = graph_toughness(&g).unwrap();
136/// assert!((t - 0.5).abs() < 0.01);
137/// ```
138pub fn graph_toughness(graph: &Graph) -> IgraphResult<f64> {
139    if graph.is_directed() {
140        return Err(IgraphError::InvalidArgument(
141            "graph_toughness is defined for undirected graphs only".into(),
142        ));
143    }
144    let n = graph.vcount() as usize;
145    if n <= 1 {
146        return Ok(0.0);
147    }
148
149    let (initial_comp, _) = count_components_after_removal(graph, &vec![false; n]);
150    if initial_comp > 1 {
151        return Ok(0.0);
152    }
153
154    let mut min_toughness = f64::INFINITY;
155    let mut removed = vec![false; n];
156
157    toughness_search(graph, n, 0, 0, &mut removed, &mut min_toughness);
158
159    Ok(min_toughness)
160}
161
162fn toughness_search(
163    graph: &Graph,
164    n: usize,
165    start: usize,
166    removed_count: usize,
167    removed: &mut Vec<bool>,
168    min_toughness: &mut f64,
169) {
170    if removed_count > 0 {
171        let (comp, _) = count_components_after_removal(graph, removed);
172        if comp > 1 {
173            let t = removed_count as f64 / comp as f64;
174            if t < *min_toughness {
175                *min_toughness = t;
176            }
177        }
178    }
179
180    let active_remaining = (start..n).filter(|&i| !removed[i]).count();
181    if active_remaining <= 1 {
182        return;
183    }
184
185    for i in start..n {
186        if removed[i] {
187            continue;
188        }
189        removed[i] = true;
190        toughness_search(graph, n, i + 1, removed_count + 1, removed, min_toughness);
191        removed[i] = false;
192    }
193}
194
195/// Compute the integrity of a graph.
196///
197/// `I(G) = min_{S ⊂ V} (|S| + m(G-S))`
198///
199/// where `m(G-S)` is the order (vertex count) of the largest connected
200/// component of `G - S`. Lower integrity means the graph is more
201/// vulnerable.
202///
203/// Brute-force — suitable for graphs with ≤ ~20 vertices.
204///
205/// # Examples
206///
207/// ```
208/// use rust_igraph::{Graph, graph_integrity};
209///
210/// // K_3: optimal is S=∅ → I = 0 + 3 = 3;
211/// //       or S={0} → I = 1 + 2 = 3. Min = 3.
212/// // Actually for S={0,1} → I = 2 + 1 = 3. Always 3 for K_3.
213/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
214/// let i = graph_integrity(&g).unwrap();
215/// assert!((i - 3.0).abs() < 0.01);
216/// ```
217pub fn graph_integrity(graph: &Graph) -> IgraphResult<f64> {
218    if graph.is_directed() {
219        return Err(IgraphError::InvalidArgument(
220            "graph_integrity is defined for undirected graphs only".into(),
221        ));
222    }
223    let n = graph.vcount() as usize;
224    if n == 0 {
225        return Ok(0.0);
226    }
227
228    let mut min_integrity = n as f64;
229    let mut removed = vec![false; n];
230
231    integrity_search(graph, n, 0, 0, &mut removed, &mut min_integrity);
232
233    Ok(min_integrity)
234}
235
236fn integrity_search(
237    graph: &Graph,
238    n: usize,
239    start: usize,
240    removed_count: usize,
241    removed: &mut Vec<bool>,
242    min_integrity: &mut f64,
243) {
244    let (_, largest) = count_components_after_removal(graph, removed);
245    let val = removed_count as f64 + largest as f64;
246    if val < *min_integrity {
247        *min_integrity = val;
248    }
249
250    if removed_count as f64 >= *min_integrity {
251        return;
252    }
253
254    for i in start..n {
255        if removed[i] {
256            continue;
257        }
258        removed[i] = true;
259        integrity_search(graph, n, i + 1, removed_count + 1, removed, min_integrity);
260        removed[i] = false;
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    fn path3() -> Graph {
269        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
270    }
271
272    fn path4() -> Graph {
273        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
274    }
275
276    fn k3() -> Graph {
277        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
278    }
279
280    fn k4() -> Graph {
281        Graph::from_edges(
282            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
283            false,
284            Some(4),
285        )
286        .unwrap()
287    }
288
289    fn cycle4() -> Graph {
290        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
291    }
292
293    fn star4() -> Graph {
294        Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap()
295    }
296
297    // --- vertex_resilience ---
298
299    #[test]
300    fn vr_k4() {
301        let g = k4();
302        let r = vertex_resilience(&g).unwrap();
303        assert!((r - 0.75).abs() < 0.01);
304    }
305
306    #[test]
307    fn vr_path() {
308        let g = path4();
309        let r = vertex_resilience(&g).unwrap();
310        assert!((r - 0.25).abs() < 0.01);
311    }
312
313    #[test]
314    fn vr_empty() {
315        let g = Graph::with_vertices(0);
316        let r = vertex_resilience(&g).unwrap();
317        assert!(r.abs() < 1e-10);
318    }
319
320    #[test]
321    fn vr_single() {
322        let g = Graph::with_vertices(1);
323        let r = vertex_resilience(&g).unwrap();
324        assert!(r.abs() < 1e-10);
325    }
326
327    // --- edge_resilience ---
328
329    #[test]
330    fn er_path3() {
331        let g = path3();
332        let r = edge_resilience(&g).unwrap();
333        assert!((r - 0.5).abs() < 0.01);
334    }
335
336    #[test]
337    fn er_k3() {
338        let g = k3();
339        let r = edge_resilience(&g).unwrap();
340        // λ = 2, m = 3, resilience = 2/3
341        assert!((r - 2.0 / 3.0).abs() < 0.01);
342    }
343
344    #[test]
345    fn er_empty() {
346        let g = Graph::with_vertices(1);
347        let r = edge_resilience(&g).unwrap();
348        assert!(r.abs() < 1e-10);
349    }
350
351    // --- graph_toughness ---
352
353    #[test]
354    fn gt_path3() {
355        let g = path3();
356        let t = graph_toughness(&g).unwrap();
357        // Removing {1} → 2 comp → τ = 1/2
358        assert!((t - 0.5).abs() < 0.01);
359    }
360
361    #[test]
362    fn gt_k3() {
363        let g = k3();
364        let t = graph_toughness(&g).unwrap();
365        // K_3: must remove 2 to disconnect → 2 leaves 1 vertex (1 comp)
366        // No S creates > 1 component while |S| < n-1
367        // Remove {0}: K_2 remains (1 comp). Remove {0,1}: 1 vertex (1 comp).
368        // Never > 1 component → toughness = ∞
369        assert!(t.is_infinite());
370    }
371
372    #[test]
373    fn gt_k4() {
374        let g = k4();
375        let t = graph_toughness(&g).unwrap();
376        assert!(t.is_infinite());
377    }
378
379    #[test]
380    fn gt_cycle4() {
381        let g = cycle4();
382        let t = graph_toughness(&g).unwrap();
383        // Removing 2 non-adjacent vertices → 2 components → τ = 2/2 = 1
384        // Removing 2 adjacent vertices → 2 isolated + path of 0 → 1 comp
385        // Wait: C_4 = 0-1-2-3-0. Remove {0,2}: 1,3 isolated → 2 comp → 2/2=1
386        // Remove {1}: 0-3-2 still connected → 1 comp
387        // So minimum is 1.0
388        assert!((t - 1.0).abs() < 0.01);
389    }
390
391    #[test]
392    fn gt_star4() {
393        let g = star4();
394        let t = graph_toughness(&g).unwrap();
395        // Remove {0} (center) → 3 isolated vertices → τ = 1/3
396        assert!((t - 1.0 / 3.0).abs() < 0.01);
397    }
398
399    #[test]
400    fn gt_disconnected() {
401        let g = Graph::with_vertices(3);
402        let t = graph_toughness(&g).unwrap();
403        assert!(t.abs() < 1e-10);
404    }
405
406    #[test]
407    fn gt_single() {
408        let g = Graph::with_vertices(1);
409        let t = graph_toughness(&g).unwrap();
410        assert!(t.abs() < 1e-10);
411    }
412
413    #[test]
414    fn gt_directed_error() {
415        let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
416        assert!(graph_toughness(&g).is_err());
417    }
418
419    // --- graph_integrity ---
420
421    #[test]
422    fn gi_k3() {
423        let g = k3();
424        let i = graph_integrity(&g).unwrap();
425        // S=∅: 0+3=3, S={v}: 1+2=3, S={v,w}: 2+1=3 → I=3
426        assert!((i - 3.0).abs() < 0.01);
427    }
428
429    #[test]
430    fn gi_path3() {
431        let g = path3();
432        let i = graph_integrity(&g).unwrap();
433        // S=∅: 0+3=3, S={1}: 1+1=2, S={0}: 1+2=3 → I=2
434        assert!((i - 2.0).abs() < 0.01);
435    }
436
437    #[test]
438    fn gi_path4() {
439        let g = path4();
440        let i = graph_integrity(&g).unwrap();
441        // S={1}: 1+2=3, S={1,2}: 2+1=3, S={2}: 1+2=3 → I=3
442        // Actually: S={1,2}: leaves {0} and {3} → largest=1 → 2+1=3
443        // S={1}: leaves {0} and {2,3} → largest=2 → 1+2=3
444        assert!((i - 3.0).abs() < 0.01);
445    }
446
447    #[test]
448    fn gi_star4() {
449        let g = star4();
450        let i = graph_integrity(&g).unwrap();
451        // S={0}: 1 + 1 = 2 (three isolated vertices, largest = 1)
452        assert!((i - 2.0).abs() < 0.01);
453    }
454
455    #[test]
456    fn gi_empty() {
457        let g = Graph::with_vertices(0);
458        let i = graph_integrity(&g).unwrap();
459        assert!(i.abs() < 1e-10);
460    }
461
462    #[test]
463    fn gi_single() {
464        let g = Graph::with_vertices(1);
465        let i = graph_integrity(&g).unwrap();
466        assert!((i - 1.0).abs() < 0.01);
467    }
468
469    #[test]
470    fn gi_directed_error() {
471        let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
472        assert!(graph_integrity(&g).is_err());
473    }
474
475    // --- cross-consistency ---
476
477    #[test]
478    fn resilience_bounded() {
479        let g = cycle4();
480        let vr = vertex_resilience(&g).unwrap();
481        let er = edge_resilience(&g).unwrap();
482        assert!((0.0..=1.0).contains(&vr));
483        assert!((0.0..=1.0).contains(&er));
484    }
485
486    #[test]
487    fn complete_graph_is_infinitely_tough() {
488        for n in 3_u32..=5 {
489            let mut edges = Vec::new();
490            for u in 0..n {
491                for v in (u + 1)..n {
492                    edges.push((u, v));
493                }
494            }
495            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
496            let t = graph_toughness(&g).unwrap();
497            assert!(t.is_infinite(), "K_{n} should have infinite toughness");
498        }
499    }
500}