Skip to main content

rust_igraph/algorithms/properties/
resilience_ratios.rs

1//! Resilience-based ratio indices (ALGO-TR-106).
2//!
3//! Ratios capturing network resilience and vulnerability:
4//!
5//! - **Vertex connectivity ratio** — `min_degree / (n-1)` proxy for connected,
6//!   0 for disconnected
7//! - **Edge connectivity ratio** — `min_degree / (n-1)` edge analog
8//! - **Diameter vulnerability** — max increase in diameter when removing
9//!   one vertex, normalized
10//! - **Neighbor degree disparity** — mean `knn(v) / degree(v)` over
11//!   vertices with `degree >= 1`
12
13#![allow(
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 vertex connectivity ratio.
25///
26/// For connected graphs: `min_degree / (n-1)`. For disconnected
27/// graphs: 0.0. This is an upper bound proxy for the actual vertex
28/// connectivity κ(G) / (n-1). Returns 0.0 for trivial graphs.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, vertex_conn_ratio};
34///
35/// // K_4: min_degree=3, n-1=3 → 1.0
36/// let g = Graph::from_edges(
37///     &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)], false, Some(4)
38/// ).unwrap();
39/// assert!((vertex_conn_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
40/// ```
41pub fn vertex_conn_ratio(graph: &Graph) -> IgraphResult<f64> {
42    let n = graph.vcount() as usize;
43    if n < 2 {
44        return Ok(0.0);
45    }
46
47    if !is_connected_bfs(graph)? {
48        return Ok(0.0);
49    }
50
51    let mut min_deg = usize::MAX;
52    for v in 0..n {
53        let d = graph.degree(v as u32)?;
54        if d < min_deg {
55            min_deg = d;
56        }
57    }
58
59    Ok(min_deg as f64 / (n - 1) as f64)
60}
61
62/// Compute the edge connectivity ratio.
63///
64/// For connected graphs: `min_degree / (n-1)`. This is the same
65/// formula as vertex connectivity ratio since both use `min_degree`
66/// as a proxy (Whitney's theorem: κ(G) ≤ κ'(G) ≤ δ(G)). Returns
67/// 0.0 for disconnected or trivial graphs.
68///
69/// # Examples
70///
71/// ```
72/// use rust_igraph::{Graph, edge_conn_ratio};
73///
74/// // Cycle_4: min_degree=2, n-1=3 → 2/3
75/// let g = Graph::from_edges(
76///     &[(0,1),(1,2),(2,3),(3,0)], false, Some(4)
77/// ).unwrap();
78/// assert!((edge_conn_ratio(&g).unwrap() - 2.0/3.0).abs() < 1e-10);
79/// ```
80pub fn edge_conn_ratio(graph: &Graph) -> IgraphResult<f64> {
81    let n = graph.vcount() as usize;
82    if n < 2 {
83        return Ok(0.0);
84    }
85
86    if !is_connected_bfs(graph)? {
87        return Ok(0.0);
88    }
89
90    let mut min_deg = usize::MAX;
91    for v in 0..n {
92        let d = graph.degree(v as u32)?;
93        if d < min_deg {
94            min_deg = d;
95        }
96    }
97
98    Ok(min_deg as f64 / (n - 1) as f64)
99}
100
101/// Compute the diameter vulnerability.
102///
103/// For each vertex v, compute the diameter of G - v (graph with v
104/// removed). The vulnerability is the maximum increase in diameter
105/// over all removals, normalized by the original diameter. Returns
106/// 0.0 for disconnected, trivial graphs, or graphs with zero diameter.
107///
108/// # Examples
109///
110/// ```
111/// use rust_igraph::{Graph, diameter_vulnerability};
112///
113/// // K_3: removing any vertex leaves K_2, diameter unchanged (1→1) → 0.0
114/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
115/// assert!(diameter_vulnerability(&g).unwrap().abs() < 1e-10);
116/// ```
117pub fn diameter_vulnerability(graph: &Graph) -> IgraphResult<f64> {
118    let n = graph.vcount() as usize;
119    if n < 3 {
120        return Ok(0.0);
121    }
122
123    let orig_diam = compute_diameter(graph)?;
124    if orig_diam == 0 || orig_diam == u32::MAX {
125        return Ok(0.0);
126    }
127
128    let mut max_increase = 0_i64;
129
130    for removed in 0..n {
131        let sub_diam = compute_diameter_without(graph, removed)?;
132        if sub_diam == u32::MAX {
133            // subgraph disconnected — treat as infinite increase
134            // but we can cap it reasonably
135            let increase = i64::from(orig_diam);
136            if increase > max_increase {
137                max_increase = increase;
138            }
139        } else {
140            let increase = i64::from(sub_diam) - i64::from(orig_diam);
141            if increase > max_increase {
142                max_increase = increase;
143            }
144        }
145    }
146
147    if max_increase <= 0 {
148        return Ok(0.0);
149    }
150
151    Ok(max_increase as f64 / f64::from(orig_diam))
152}
153
154/// Compute the average neighbor degree ratio.
155///
156/// Mean of `knn(v) / degree(v)` over all vertices with `degree >= 1`,
157/// where `knn(v)` is the average degree of v's neighbors. This
158/// measures the tendency for high-degree vertices to connect to
159/// other high-degree vertices. Returns 0.0 for graphs with no
160/// edges.
161///
162/// # Examples
163///
164/// ```
165/// use rust_igraph::{Graph, neighbor_degree_disparity};
166///
167/// // K_3: each vertex has degree 2, neighbors have degree 2 → knn/d = 1.0
168/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
169/// assert!((neighbor_degree_disparity(&g).unwrap() - 1.0).abs() < 1e-10);
170/// ```
171pub fn neighbor_degree_disparity(graph: &Graph) -> IgraphResult<f64> {
172    let n = graph.vcount() as usize;
173    if n == 0 {
174        return Ok(0.0);
175    }
176
177    let mut degrees = Vec::with_capacity(n);
178    for v in 0..n {
179        degrees.push(graph.degree(v as u32)?);
180    }
181
182    let mut sum = 0.0_f64;
183    let mut count = 0_usize;
184
185    for v in 0..n {
186        let dv = degrees[v];
187        if dv == 0 {
188            continue;
189        }
190
191        let neighbors = graph.neighbors(v as u32)?;
192        let knn: f64 = neighbors
193            .iter()
194            .map(|&u| degrees[u as usize] as f64)
195            .sum::<f64>()
196            / dv as f64;
197        sum += knn / dv as f64;
198        count += 1;
199    }
200
201    if count == 0 {
202        return Ok(0.0);
203    }
204
205    Ok(sum / count as f64)
206}
207
208fn is_connected_bfs(graph: &Graph) -> IgraphResult<bool> {
209    let n = graph.vcount() as usize;
210    if n <= 1 {
211        return Ok(true);
212    }
213
214    let mut visited = vec![false; n];
215    visited[0] = true;
216    let mut queue = std::collections::VecDeque::new();
217    queue.push_back(0_usize);
218    let mut seen = 1_usize;
219
220    while let Some(v) = queue.pop_front() {
221        let neighbors = graph.neighbors(v as u32)?;
222        for &u in &neighbors {
223            let ui = u as usize;
224            if !visited[ui] {
225                visited[ui] = true;
226                seen += 1;
227                queue.push_back(ui);
228            }
229        }
230    }
231
232    Ok(seen == n)
233}
234
235fn compute_diameter(graph: &Graph) -> IgraphResult<u32> {
236    let n = graph.vcount() as usize;
237    if n < 2 {
238        return Ok(0);
239    }
240
241    let mut max_d = 0_u32;
242    for s in 0..n {
243        let mut dist = vec![u32::MAX; n];
244        dist[s] = 0;
245        let mut queue = std::collections::VecDeque::new();
246        queue.push_back(s);
247        while let Some(v) = queue.pop_front() {
248            let d = dist[v];
249            let neighbors = graph.neighbors(v as u32)?;
250            for &u in &neighbors {
251                let ui = u as usize;
252                if dist[ui] == u32::MAX {
253                    dist[ui] = d + 1;
254                    queue.push_back(ui);
255                }
256            }
257        }
258        for u in 0..n {
259            if dist[u] == u32::MAX {
260                return Ok(u32::MAX);
261            }
262            if dist[u] > max_d {
263                max_d = dist[u];
264            }
265        }
266    }
267
268    Ok(max_d)
269}
270
271fn compute_diameter_without(graph: &Graph, removed: usize) -> IgraphResult<u32> {
272    let n = graph.vcount() as usize;
273    if n < 3 {
274        return Ok(0);
275    }
276
277    let mut max_d = 0_u32;
278    for s in 0..n {
279        if s == removed {
280            continue;
281        }
282        let mut dist = vec![u32::MAX; n];
283        dist[s] = 0;
284        let mut queue = std::collections::VecDeque::new();
285        queue.push_back(s);
286        while let Some(v) = queue.pop_front() {
287            let d = dist[v];
288            let neighbors = graph.neighbors(v as u32)?;
289            for &u in &neighbors {
290                let ui = u as usize;
291                if ui == removed {
292                    continue;
293                }
294                if dist[ui] == u32::MAX {
295                    dist[ui] = d + 1;
296                    queue.push_back(ui);
297                }
298            }
299        }
300        for u in 0..n {
301            if u == removed {
302                continue;
303            }
304            if dist[u] == u32::MAX {
305                return Ok(u32::MAX);
306            }
307            if dist[u] > max_d {
308                max_d = dist[u];
309            }
310        }
311    }
312
313    Ok(max_d)
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    fn single_edge() -> Graph {
321        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
322    }
323
324    fn path3() -> Graph {
325        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
326    }
327
328    fn k3() -> Graph {
329        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
330    }
331
332    fn k4() -> Graph {
333        Graph::from_edges(
334            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
335            false,
336            Some(4),
337        )
338        .unwrap()
339    }
340
341    fn cycle4() -> Graph {
342        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
343    }
344
345    fn star5() -> Graph {
346        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
347    }
348
349    fn paw() -> Graph {
350        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
351    }
352
353    // --- vertex_conn_ratio ---
354
355    #[test]
356    fn vcr_empty() {
357        let g = Graph::with_vertices(0);
358        assert!(vertex_conn_ratio(&g).unwrap().abs() < 1e-10);
359    }
360
361    #[test]
362    fn vcr_single() {
363        let g = Graph::with_vertices(1);
364        assert!(vertex_conn_ratio(&g).unwrap().abs() < 1e-10);
365    }
366
367    #[test]
368    fn vcr_k3() {
369        // min_deg=2, n-1=2 → 1.0
370        assert!((vertex_conn_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
371    }
372
373    #[test]
374    fn vcr_k4() {
375        // min_deg=3, n-1=3 → 1.0
376        assert!((vertex_conn_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
377    }
378
379    #[test]
380    fn vcr_cycle4() {
381        // min_deg=2, n-1=3 → 2/3
382        assert!((vertex_conn_ratio(&cycle4()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
383    }
384
385    #[test]
386    fn vcr_star5() {
387        // min_deg=1, n-1=4 → 1/4
388        assert!((vertex_conn_ratio(&star5()).unwrap() - 0.25).abs() < 1e-10);
389    }
390
391    #[test]
392    fn vcr_disconnected() {
393        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
394        assert!(vertex_conn_ratio(&g).unwrap().abs() < 1e-10);
395    }
396
397    #[test]
398    fn vcr_in_01() {
399        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
400            let r = vertex_conn_ratio(g).unwrap();
401            assert!(r >= -1e-10);
402            assert!(r <= 1.0 + 1e-10);
403        }
404    }
405
406    // --- edge_conn_ratio ---
407
408    #[test]
409    fn ecr_empty() {
410        let g = Graph::with_vertices(0);
411        assert!(edge_conn_ratio(&g).unwrap().abs() < 1e-10);
412    }
413
414    #[test]
415    fn ecr_single() {
416        let g = Graph::with_vertices(1);
417        assert!(edge_conn_ratio(&g).unwrap().abs() < 1e-10);
418    }
419
420    #[test]
421    fn ecr_k3() {
422        assert!((edge_conn_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
423    }
424
425    #[test]
426    fn ecr_k4() {
427        assert!((edge_conn_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
428    }
429
430    #[test]
431    fn ecr_cycle4() {
432        assert!((edge_conn_ratio(&cycle4()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
433    }
434
435    #[test]
436    fn ecr_star5() {
437        assert!((edge_conn_ratio(&star5()).unwrap() - 0.25).abs() < 1e-10);
438    }
439
440    #[test]
441    fn ecr_disconnected() {
442        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
443        assert!(edge_conn_ratio(&g).unwrap().abs() < 1e-10);
444    }
445
446    // --- diameter_vulnerability ---
447
448    #[test]
449    fn dv_empty() {
450        let g = Graph::with_vertices(0);
451        assert!(diameter_vulnerability(&g).unwrap().abs() < 1e-10);
452    }
453
454    #[test]
455    fn dv_single() {
456        let g = Graph::with_vertices(1);
457        assert!(diameter_vulnerability(&g).unwrap().abs() < 1e-10);
458    }
459
460    #[test]
461    fn dv_two() {
462        assert!(diameter_vulnerability(&single_edge()).unwrap().abs() < 1e-10);
463    }
464
465    #[test]
466    fn dv_k3() {
467        // Removing any vertex: diameter stays 1 → 0
468        assert!(diameter_vulnerability(&k3()).unwrap().abs() < 1e-10);
469    }
470
471    #[test]
472    fn dv_k4() {
473        // Removing any vertex: K_3 → diameter 1 (same as K_4) → 0
474        assert!(diameter_vulnerability(&k4()).unwrap().abs() < 1e-10);
475    }
476
477    #[test]
478    fn dv_path3() {
479        // Original diameter=2. Remove v1: disconnected → vulnerability = 2/2 = 1.0
480        assert!((diameter_vulnerability(&path3()).unwrap() - 1.0).abs() < 1e-10);
481    }
482
483    #[test]
484    fn dv_cycle4() {
485        // diameter=2. Remove any vertex: path of 3, diameter=2 → no increase → 0
486        assert!(diameter_vulnerability(&cycle4()).unwrap().abs() < 1e-10);
487    }
488
489    #[test]
490    fn dv_star5() {
491        // diameter=2. Remove center: 4 isolated vertices → disconnected → increase = 2/2 = 1.0
492        assert!((diameter_vulnerability(&star5()).unwrap() - 1.0).abs() < 1e-10);
493    }
494
495    #[test]
496    fn dv_nonneg() {
497        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
498            assert!(diameter_vulnerability(g).unwrap() >= -1e-10);
499        }
500    }
501
502    // --- neighbor_degree_disparity ---
503
504    #[test]
505    fn andr_empty() {
506        let g = Graph::with_vertices(0);
507        assert!(neighbor_degree_disparity(&g).unwrap().abs() < 1e-10);
508    }
509
510    #[test]
511    fn andr_single() {
512        let g = Graph::with_vertices(1);
513        assert!(neighbor_degree_disparity(&g).unwrap().abs() < 1e-10);
514    }
515
516    #[test]
517    fn andr_no_edges() {
518        let g = Graph::with_vertices(5);
519        assert!(neighbor_degree_disparity(&g).unwrap().abs() < 1e-10);
520    }
521
522    #[test]
523    fn andr_k3() {
524        // Each vertex: degree=2, neighbors have degree=2 → knn/d = 2/2 = 1.0
525        assert!((neighbor_degree_disparity(&k3()).unwrap() - 1.0).abs() < 1e-10);
526    }
527
528    #[test]
529    fn andr_k4() {
530        // Each vertex: degree=3, neighbors have degree=3 → knn/d = 3/3 = 1.0
531        assert!((neighbor_degree_disparity(&k4()).unwrap() - 1.0).abs() < 1e-10);
532    }
533
534    #[test]
535    fn andr_cycle4() {
536        // Each vertex: degree=2, neighbors have degree=2 → knn/d = 2/2 = 1.0
537        assert!((neighbor_degree_disparity(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
538    }
539
540    #[test]
541    fn andr_star5() {
542        // Center: d=4, knn=4*1/4=1 → knn/d = 1/4 = 0.25
543        // Leaf: d=1, knn=4/1=4 → knn/d = 4/1 = 4.0
544        // avg = (0.25 + 4*4.0) / 5 = 16.25/5 = 3.25
545        let r = neighbor_degree_disparity(&star5()).unwrap();
546        assert!((r - 3.25).abs() < 1e-10);
547    }
548
549    #[test]
550    fn andr_single_edge() {
551        // Both have d=1, knn=1 → knn/d = 1.0
552        assert!((neighbor_degree_disparity(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
553    }
554
555    #[test]
556    fn andr_positive() {
557        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
558            assert!(neighbor_degree_disparity(g).unwrap() > 0.0);
559        }
560    }
561
562    // --- cross-consistency ---
563
564    #[test]
565    fn complete_max_connectivity() {
566        assert!((vertex_conn_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
567        assert!((vertex_conn_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
568        assert!((edge_conn_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
569        assert!((edge_conn_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
570    }
571
572    #[test]
573    fn complete_zero_vulnerability() {
574        assert!(diameter_vulnerability(&k3()).unwrap().abs() < 1e-10);
575        assert!(diameter_vulnerability(&k4()).unwrap().abs() < 1e-10);
576    }
577
578    #[test]
579    fn regular_unit_neighbor_ratio() {
580        assert!((neighbor_degree_disparity(&k3()).unwrap() - 1.0).abs() < 1e-10);
581        assert!((neighbor_degree_disparity(&k4()).unwrap() - 1.0).abs() < 1e-10);
582        assert!((neighbor_degree_disparity(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
583    }
584}