Skip to main content

rust_igraph/algorithms/properties/
connectivity_ratios.rs

1//! Connectivity-based ratio indices (ALGO-TR-102).
2//!
3//! Ratios capturing connectivity structure:
4//!
5//! - **Component ratio** — number of components / number of vertices
6//! - **Largest component fraction** — largest component size / n
7//! - **Giant component gap** — (largest - 2nd largest) / n
8//! - **Vertex connectivity ratio** — vertex connectivity / (n - 1)
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 component ratio.
22///
23/// `num_components / n` — fraction of vertices that are component
24/// representatives. Equals 1.0 when every vertex is isolated,
25/// 1/n when the graph is connected. Returns 0.0 for empty graphs.
26///
27/// # Examples
28///
29/// ```
30/// use rust_igraph::{Graph, component_ratio};
31///
32/// // K_3: 1 component, 3 vertices → 1/3
33/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
34/// assert!((component_ratio(&g).unwrap() - 1.0/3.0).abs() < 1e-10);
35/// ```
36pub fn component_ratio(graph: &Graph) -> IgraphResult<f64> {
37    let n = graph.vcount() as usize;
38    if n == 0 {
39        return Ok(0.0);
40    }
41
42    let comp_sizes = component_sizes(graph)?;
43    Ok(comp_sizes.len() as f64 / n as f64)
44}
45
46/// Compute the largest component fraction.
47///
48/// `max_component_size / n` — how much of the graph is in the
49/// largest connected component. Equals 1.0 for connected graphs.
50/// Returns 0.0 for empty graphs.
51///
52/// # Examples
53///
54/// ```
55/// use rust_igraph::{Graph, largest_component_fraction};
56///
57/// // K_3: fully connected → 1.0
58/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
59/// assert!((largest_component_fraction(&g).unwrap() - 1.0).abs() < 1e-10);
60/// ```
61pub fn largest_component_fraction(graph: &Graph) -> IgraphResult<f64> {
62    let n = graph.vcount() as usize;
63    if n == 0 {
64        return Ok(0.0);
65    }
66
67    let comp_sizes = component_sizes(graph)?;
68    let max_size = comp_sizes.iter().copied().max().unwrap_or(0);
69    Ok(max_size as f64 / n as f64)
70}
71
72/// Compute the giant component gap.
73///
74/// `(largest - second_largest) / n` — the dominance of the largest
75/// component. Close to 1.0 means a single giant component dominates;
76/// close to 0 means two similarly-sized components exist.
77/// Returns 0.0 for graphs with fewer than 2 components or empty graphs.
78///
79/// # Examples
80///
81/// ```
82/// use rust_igraph::{Graph, giant_component_gap};
83///
84/// // K_3: single component → gap = (3 - 0) / 3 = 1.0
85/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
86/// assert!((giant_component_gap(&g).unwrap() - 1.0).abs() < 1e-10);
87/// ```
88pub fn giant_component_gap(graph: &Graph) -> IgraphResult<f64> {
89    let n = graph.vcount() as usize;
90    if n == 0 {
91        return Ok(0.0);
92    }
93
94    let mut comp_sizes = component_sizes(graph)?;
95    comp_sizes.sort_unstable_by(|a, b| b.cmp(a));
96
97    let largest = comp_sizes[0];
98    let second = if comp_sizes.len() > 1 {
99        comp_sizes[1]
100    } else {
101        0
102    };
103
104    Ok((largest - second) as f64 / n as f64)
105}
106
107/// Compute the vertex connectivity ratio.
108///
109/// `kappa / (n - 1)` where `kappa` is the vertex connectivity
110/// (minimum number of vertices whose removal disconnects the graph).
111/// Ranges from 0 (disconnected or tree) to 1 (complete graph).
112/// Returns 0.0 for graphs with fewer than 2 vertices.
113///
114/// For efficiency this uses an approximation: the minimum degree
115/// as an upper bound on vertex connectivity, since computing exact
116/// vertex connectivity is expensive. For simple undirected graphs,
117/// `kappa <= delta` (minimum degree) always holds, and for many
118/// graph families equality holds.
119///
120/// # Examples
121///
122/// ```
123/// use rust_igraph::{Graph, vertex_connectivity_ratio};
124///
125/// // K_4: min_degree=3, n-1=3 → 3/3 = 1.0
126/// let g = Graph::from_edges(
127///     &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)], false, Some(4)
128/// ).unwrap();
129/// assert!((vertex_connectivity_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
130/// ```
131pub fn vertex_connectivity_ratio(graph: &Graph) -> IgraphResult<f64> {
132    let n = graph.vcount() as usize;
133    if n < 2 {
134        return Ok(0.0);
135    }
136
137    let comp_sizes = component_sizes(graph)?;
138    if comp_sizes.len() > 1 {
139        return Ok(0.0);
140    }
141
142    let mut min_deg = usize::MAX;
143    for v in 0..n {
144        let d = graph.degree(v as u32)?;
145        if d < min_deg {
146            min_deg = d;
147        }
148    }
149
150    Ok(min_deg as f64 / (n - 1) as f64)
151}
152
153fn component_sizes(graph: &Graph) -> IgraphResult<Vec<usize>> {
154    let n = graph.vcount() as usize;
155    let mut visited = vec![false; n];
156    let mut sizes = Vec::new();
157    let mut queue = std::collections::VecDeque::new();
158
159    for start in 0..n {
160        if visited[start] {
161            continue;
162        }
163        visited[start] = true;
164        let mut size = 1_usize;
165        queue.push_back(start);
166        while let Some(v) = queue.pop_front() {
167            let neighbors = graph.neighbors(v as u32)?;
168            for &u in &neighbors {
169                let ui = u as usize;
170                if !visited[ui] {
171                    visited[ui] = true;
172                    size += 1;
173                    queue.push_back(ui);
174                }
175            }
176        }
177        sizes.push(size);
178    }
179
180    Ok(sizes)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    fn single_edge() -> Graph {
188        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
189    }
190
191    fn path3() -> Graph {
192        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
193    }
194
195    fn k3() -> Graph {
196        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
197    }
198
199    fn k4() -> Graph {
200        Graph::from_edges(
201            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
202            false,
203            Some(4),
204        )
205        .unwrap()
206    }
207
208    fn cycle4() -> Graph {
209        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
210    }
211
212    fn star5() -> Graph {
213        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
214    }
215
216    fn disconnected_2_2() -> Graph {
217        Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap()
218    }
219
220    fn disconnected_3_1() -> Graph {
221        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(4)).unwrap()
222    }
223
224    // --- component_ratio ---
225
226    #[test]
227    fn cr_empty() {
228        let g = Graph::with_vertices(0);
229        assert!(component_ratio(&g).unwrap().abs() < 1e-10);
230    }
231
232    #[test]
233    fn cr_single() {
234        let g = Graph::with_vertices(1);
235        assert!((component_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
236    }
237
238    #[test]
239    fn cr_isolated() {
240        let g = Graph::with_vertices(5);
241        assert!((component_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
242    }
243
244    #[test]
245    fn cr_k3() {
246        assert!((component_ratio(&k3()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
247    }
248
249    #[test]
250    fn cr_k4() {
251        assert!((component_ratio(&k4()).unwrap() - 0.25).abs() < 1e-10);
252    }
253
254    #[test]
255    fn cr_disconnected_2_2() {
256        // 2 components, 4 vertices → 0.5
257        assert!((component_ratio(&disconnected_2_2()).unwrap() - 0.5).abs() < 1e-10);
258    }
259
260    #[test]
261    fn cr_disconnected_3_1() {
262        // path(0-1-2) + isolated(3) → 2 components, 4 vertices → 0.5
263        assert!((component_ratio(&disconnected_3_1()).unwrap() - 0.5).abs() < 1e-10);
264    }
265
266    #[test]
267    fn cr_single_edge() {
268        assert!((component_ratio(&single_edge()).unwrap() - 0.5).abs() < 1e-10);
269    }
270
271    // --- largest_component_fraction ---
272
273    #[test]
274    fn lcf_empty() {
275        let g = Graph::with_vertices(0);
276        assert!(largest_component_fraction(&g).unwrap().abs() < 1e-10);
277    }
278
279    #[test]
280    fn lcf_single() {
281        let g = Graph::with_vertices(1);
282        assert!((largest_component_fraction(&g).unwrap() - 1.0).abs() < 1e-10);
283    }
284
285    #[test]
286    fn lcf_isolated() {
287        // 5 isolated vertices → largest = 1 → 1/5 = 0.2
288        let g = Graph::with_vertices(5);
289        assert!((largest_component_fraction(&g).unwrap() - 0.2).abs() < 1e-10);
290    }
291
292    #[test]
293    fn lcf_k3() {
294        assert!((largest_component_fraction(&k3()).unwrap() - 1.0).abs() < 1e-10);
295    }
296
297    #[test]
298    fn lcf_disconnected_2_2() {
299        // Two K_2's → largest = 2 → 2/4 = 0.5
300        assert!((largest_component_fraction(&disconnected_2_2()).unwrap() - 0.5).abs() < 1e-10);
301    }
302
303    #[test]
304    fn lcf_disconnected_3_1() {
305        // path(3) + isolated(1) → largest = 3 → 3/4 = 0.75
306        assert!((largest_component_fraction(&disconnected_3_1()).unwrap() - 0.75).abs() < 1e-10);
307    }
308
309    #[test]
310    fn lcf_star5() {
311        assert!((largest_component_fraction(&star5()).unwrap() - 1.0).abs() < 1e-10);
312    }
313
314    // --- giant_component_gap ---
315
316    #[test]
317    fn gcg_empty() {
318        let g = Graph::with_vertices(0);
319        assert!(giant_component_gap(&g).unwrap().abs() < 1e-10);
320    }
321
322    #[test]
323    fn gcg_single() {
324        let g = Graph::with_vertices(1);
325        assert!((giant_component_gap(&g).unwrap() - 1.0).abs() < 1e-10);
326    }
327
328    #[test]
329    fn gcg_k3() {
330        // Single component → gap = (3 - 0) / 3 = 1.0
331        assert!((giant_component_gap(&k3()).unwrap() - 1.0).abs() < 1e-10);
332    }
333
334    #[test]
335    fn gcg_disconnected_2_2() {
336        // Two equal components → gap = (2 - 2) / 4 = 0.0
337        assert!(giant_component_gap(&disconnected_2_2()).unwrap().abs() < 1e-10);
338    }
339
340    #[test]
341    fn gcg_disconnected_3_1() {
342        // 3 + 1 → gap = (3 - 1) / 4 = 0.5
343        assert!((giant_component_gap(&disconnected_3_1()).unwrap() - 0.5).abs() < 1e-10);
344    }
345
346    #[test]
347    fn gcg_isolated() {
348        // 5 isolated → all size 1 → gap = (1 - 1) / 5 = 0.0
349        let g = Graph::with_vertices(5);
350        assert!(giant_component_gap(&g).unwrap().abs() < 1e-10);
351    }
352
353    #[test]
354    fn gcg_star5() {
355        // Single component → gap = (5 - 0) / 5 = 1.0
356        assert!((giant_component_gap(&star5()).unwrap() - 1.0).abs() < 1e-10);
357    }
358
359    // --- vertex_connectivity_ratio ---
360
361    #[test]
362    fn vcr_empty() {
363        let g = Graph::with_vertices(0);
364        assert!(vertex_connectivity_ratio(&g).unwrap().abs() < 1e-10);
365    }
366
367    #[test]
368    fn vcr_single() {
369        let g = Graph::with_vertices(1);
370        assert!(vertex_connectivity_ratio(&g).unwrap().abs() < 1e-10);
371    }
372
373    #[test]
374    fn vcr_single_edge() {
375        // min_deg=1, n-1=1 → 1.0
376        assert!((vertex_connectivity_ratio(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
377    }
378
379    #[test]
380    fn vcr_k3() {
381        // min_deg=2, n-1=2 → 1.0
382        assert!((vertex_connectivity_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
383    }
384
385    #[test]
386    fn vcr_k4() {
387        // min_deg=3, n-1=3 → 1.0
388        assert!((vertex_connectivity_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
389    }
390
391    #[test]
392    fn vcr_cycle4() {
393        // min_deg=2, n-1=3 → 2/3
394        assert!((vertex_connectivity_ratio(&cycle4()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
395    }
396
397    #[test]
398    fn vcr_star5() {
399        // min_deg=1, n-1=4 → 0.25
400        assert!((vertex_connectivity_ratio(&star5()).unwrap() - 0.25).abs() < 1e-10);
401    }
402
403    #[test]
404    fn vcr_path3() {
405        // min_deg=1, n-1=2 → 0.5
406        assert!((vertex_connectivity_ratio(&path3()).unwrap() - 0.5).abs() < 1e-10);
407    }
408
409    #[test]
410    fn vcr_disconnected() {
411        // Disconnected → 0.0
412        assert!(
413            vertex_connectivity_ratio(&disconnected_2_2())
414                .unwrap()
415                .abs()
416                < 1e-10
417        );
418    }
419
420    #[test]
421    fn vcr_isolated() {
422        // Isolated vertices → disconnected → 0.0
423        let g = Graph::with_vertices(5);
424        assert!(vertex_connectivity_ratio(&g).unwrap().abs() < 1e-10);
425    }
426
427    // --- cross-consistency ---
428
429    #[test]
430    fn cr_in_01() {
431        for g in &[
432            single_edge(),
433            path3(),
434            k3(),
435            k4(),
436            cycle4(),
437            star5(),
438            disconnected_2_2(),
439        ] {
440            let r = component_ratio(g).unwrap();
441            assert!(r >= -1e-10);
442            assert!(r <= 1.0 + 1e-10);
443        }
444    }
445
446    #[test]
447    fn lcf_in_01() {
448        for g in &[
449            single_edge(),
450            path3(),
451            k3(),
452            k4(),
453            cycle4(),
454            star5(),
455            disconnected_2_2(),
456        ] {
457            let r = largest_component_fraction(g).unwrap();
458            assert!(r >= -1e-10);
459            assert!(r <= 1.0 + 1e-10);
460        }
461    }
462
463    #[test]
464    fn gcg_in_01() {
465        for g in &[
466            single_edge(),
467            path3(),
468            k3(),
469            k4(),
470            cycle4(),
471            star5(),
472            disconnected_2_2(),
473        ] {
474            let r = giant_component_gap(g).unwrap();
475            assert!(r >= -1e-10);
476            assert!(r <= 1.0 + 1e-10);
477        }
478    }
479
480    #[test]
481    fn vcr_in_01() {
482        for g in &[
483            single_edge(),
484            path3(),
485            k3(),
486            k4(),
487            cycle4(),
488            star5(),
489            disconnected_2_2(),
490        ] {
491            let r = vertex_connectivity_ratio(g).unwrap();
492            assert!(r >= -1e-10);
493            assert!(r <= 1.0 + 1e-10);
494        }
495    }
496
497    #[test]
498    fn connected_graphs_lcf_one() {
499        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
500            assert!((largest_component_fraction(g).unwrap() - 1.0).abs() < 1e-10);
501        }
502    }
503
504    #[test]
505    fn complete_graphs_vcr_one() {
506        assert!((vertex_connectivity_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
507        assert!((vertex_connectivity_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
508    }
509}