Skip to main content

rust_igraph/algorithms/properties/
graph_connectivity_ratios.rs

1//! Graph connectivity ratio indices (ALGO-TR-096).
2//!
3//! Measures of how well-connected a graph is relative to its size:
4//!
5//! - **Circuit rank ratio** — cyclomatic number normalized by edges
6//! - **Meshedness coefficient** — circuit rank relative to max planar
7//! - **Edge surplus ratio** — fraction of edges beyond a spanning tree
8//! - **Connectivity index** — average degree divided by max possible degree
9
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_possible_wrap,
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 circuit rank ratio of the graph.
23///
24/// `CRR = (m - n + c) / m`
25///
26/// where c is the number of connected components. The circuit rank
27/// (cyclomatic number) `m - n + c` counts the number of independent
28/// cycles. Dividing by m gives the fraction of edges that are
29/// "redundant" beyond a spanning forest. Returns 0.0 for graphs
30/// with no edges.
31///
32/// # Examples
33///
34/// ```
35/// use rust_igraph::{Graph, circuit_rank_ratio};
36///
37/// // K_3: m=3, n=3, c=1 → circuit_rank=1, CRR=1/3
38/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
39/// assert!((circuit_rank_ratio(&g).unwrap() - 1.0/3.0).abs() < 1e-10);
40/// ```
41pub fn circuit_rank_ratio(graph: &Graph) -> IgraphResult<f64> {
42    let m = graph.ecount() as i64;
43    if m == 0 {
44        return Ok(0.0);
45    }
46
47    let n = i64::from(graph.vcount());
48    let c = count_components(graph)? as i64;
49    let circuit_rank = (m - n + c).max(0);
50
51    Ok(circuit_rank as f64 / m as f64)
52}
53
54/// Compute the meshedness coefficient of the graph.
55///
56/// `MC = (m - n + 1) / (2n - 5)`
57///
58/// For connected graphs, the circuit rank divided by the maximum
59/// possible circuit rank of a planar graph on n vertices. Ranges
60/// from 0 (tree) to values above 1 for dense/non-planar graphs.
61/// Returns 0.0 for disconnected graphs, graphs with fewer than 3
62/// vertices, or edgeless graphs.
63///
64/// # Examples
65///
66/// ```
67/// use rust_igraph::{Graph, meshedness_coefficient};
68///
69/// // K_3: m=3, n=3, MC = (3-3+1)/(2·3-5) = 1/1 = 1.0
70/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
71/// assert!((meshedness_coefficient(&g).unwrap() - 1.0).abs() < 1e-10);
72/// ```
73pub fn meshedness_coefficient(graph: &Graph) -> IgraphResult<f64> {
74    let n = i64::from(graph.vcount());
75    let m = graph.ecount() as i64;
76
77    if n < 3 || m == 0 {
78        return Ok(0.0);
79    }
80
81    let c = count_components(graph)? as i64;
82    if c > 1 {
83        return Ok(0.0);
84    }
85
86    let denom = 2 * n - 5;
87    if denom <= 0 {
88        return Ok(0.0);
89    }
90
91    let circuit_rank = (m - n + 1).max(0);
92    Ok(circuit_rank as f64 / denom as f64)
93}
94
95/// Compute the edge surplus ratio.
96///
97/// `ESR = (m - n + c) / (n·(n-1)/2 - n + c)`
98///
99/// The fraction of the available "surplus" edge slots that are
100/// actually used, where the denominator is the maximum possible
101/// circuit rank (for a complete graph). Zero for forests, 1.0 for
102/// complete graphs. Returns 0.0 for graphs with fewer than 3
103/// vertices or when the denominator is zero.
104///
105/// # Examples
106///
107/// ```
108/// use rust_igraph::{Graph, edge_surplus_ratio};
109///
110/// // K_3: circuit_rank=1, max_circuit_rank = 3-3+1 = 1 → ESR=1.0
111/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
112/// assert!((edge_surplus_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
113/// ```
114pub fn edge_surplus_ratio(graph: &Graph) -> IgraphResult<f64> {
115    let n = i64::from(graph.vcount());
116    let m = graph.ecount() as i64;
117
118    if n < 2 || m == 0 {
119        return Ok(0.0);
120    }
121
122    let c = count_components(graph)? as i64;
123    let circuit_rank = (m - n + c).max(0);
124
125    let max_m = n * (n - 1) / 2;
126    let max_circuit_rank = (max_m - n + c).max(0);
127
128    if max_circuit_rank == 0 {
129        return Ok(0.0);
130    }
131
132    Ok(circuit_rank as f64 / max_circuit_rank as f64)
133}
134
135/// Compute the connectivity index of the graph.
136///
137/// `CI = (2m/n) / (n - 1)`
138///
139/// Average degree divided by the maximum possible average degree
140/// (for a simple graph). Equals the graph density for simple
141/// undirected graphs. Returns 0.0 for graphs with fewer than 2
142/// vertices.
143///
144/// # Examples
145///
146/// ```
147/// use rust_igraph::{Graph, connectivity_index};
148///
149/// // K_3: avg_deg=2, max_avg_deg=2, CI=1.0
150/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
151/// assert!((connectivity_index(&g).unwrap() - 1.0).abs() < 1e-10);
152/// ```
153pub fn connectivity_index(graph: &Graph) -> IgraphResult<f64> {
154    let n = u64::from(graph.vcount());
155    if n < 2 {
156        return Ok(0.0);
157    }
158
159    let m = graph.ecount() as f64;
160    let avg_deg = 2.0 * m / n as f64;
161    let max_avg_deg = (n - 1) as f64;
162
163    Ok(avg_deg / max_avg_deg)
164}
165
166fn count_components(graph: &Graph) -> IgraphResult<usize> {
167    let n = graph.vcount() as usize;
168    if n == 0 {
169        return Ok(0);
170    }
171
172    let mut visited = vec![false; n];
173    let mut count = 0_usize;
174
175    for start in 0..n {
176        if visited[start] {
177            continue;
178        }
179        count += 1;
180        let mut stack = vec![start];
181        visited[start] = true;
182        while let Some(v) = stack.pop() {
183            let neighbors = graph.neighbors(v as u32)?;
184            for &u in &neighbors {
185                let ui = u as usize;
186                if !visited[ui] {
187                    visited[ui] = true;
188                    stack.push(ui);
189                }
190            }
191        }
192    }
193
194    Ok(count)
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    fn single_edge() -> Graph {
202        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
203    }
204
205    fn path3() -> Graph {
206        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
207    }
208
209    fn k3() -> Graph {
210        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
211    }
212
213    fn k4() -> Graph {
214        Graph::from_edges(
215            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
216            false,
217            Some(4),
218        )
219        .unwrap()
220    }
221
222    fn cycle4() -> Graph {
223        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
224    }
225
226    fn star5() -> Graph {
227        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
228    }
229
230    fn paw() -> Graph {
231        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
232    }
233
234    fn disconnected() -> Graph {
235        // Two components: {0,1} and {2,3}
236        Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap()
237    }
238
239    // --- circuit_rank_ratio ---
240
241    #[test]
242    fn crr_empty() {
243        let g = Graph::with_vertices(0);
244        assert!(circuit_rank_ratio(&g).unwrap().abs() < 1e-10);
245    }
246
247    #[test]
248    fn crr_isolated() {
249        let g = Graph::with_vertices(5);
250        assert!(circuit_rank_ratio(&g).unwrap().abs() < 1e-10);
251    }
252
253    #[test]
254    fn crr_single_edge() {
255        // m=1, n=2, c=1 → cr=0, CRR=0
256        assert!(circuit_rank_ratio(&single_edge()).unwrap().abs() < 1e-10);
257    }
258
259    #[test]
260    fn crr_path3() {
261        // m=2, n=3, c=1 → cr=0, CRR=0 (tree)
262        assert!(circuit_rank_ratio(&path3()).unwrap().abs() < 1e-10);
263    }
264
265    #[test]
266    fn crr_k3() {
267        // m=3, n=3, c=1 → cr=1, CRR=1/3
268        assert!((circuit_rank_ratio(&k3()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
269    }
270
271    #[test]
272    fn crr_k4() {
273        // m=6, n=4, c=1 → cr=3, CRR=3/6=0.5
274        assert!((circuit_rank_ratio(&k4()).unwrap() - 0.5).abs() < 1e-10);
275    }
276
277    #[test]
278    fn crr_cycle4() {
279        // m=4, n=4, c=1 → cr=1, CRR=1/4=0.25
280        assert!((circuit_rank_ratio(&cycle4()).unwrap() - 0.25).abs() < 1e-10);
281    }
282
283    #[test]
284    fn crr_star5() {
285        // m=4, n=5, c=1 → cr=0, CRR=0 (tree)
286        assert!(circuit_rank_ratio(&star5()).unwrap().abs() < 1e-10);
287    }
288
289    #[test]
290    fn crr_paw() {
291        // m=4, n=4, c=1 → cr=1, CRR=1/4=0.25
292        assert!((circuit_rank_ratio(&paw()).unwrap() - 0.25).abs() < 1e-10);
293    }
294
295    #[test]
296    fn crr_disconnected() {
297        // m=2, n=4, c=2 → cr=2-4+2=0, CRR=0 (forest)
298        assert!(circuit_rank_ratio(&disconnected()).unwrap().abs() < 1e-10);
299    }
300
301    // --- meshedness_coefficient ---
302
303    #[test]
304    fn mc_empty() {
305        let g = Graph::with_vertices(0);
306        assert!(meshedness_coefficient(&g).unwrap().abs() < 1e-10);
307    }
308
309    #[test]
310    fn mc_too_small() {
311        let g = Graph::with_vertices(2);
312        assert!(meshedness_coefficient(&g).unwrap().abs() < 1e-10);
313    }
314
315    #[test]
316    fn mc_path3() {
317        // Tree → cr=0 → MC=0
318        assert!(meshedness_coefficient(&path3()).unwrap().abs() < 1e-10);
319    }
320
321    #[test]
322    fn mc_k3() {
323        // m=3, n=3, cr=1, denom=2·3-5=1, MC=1.0
324        assert!((meshedness_coefficient(&k3()).unwrap() - 1.0).abs() < 1e-10);
325    }
326
327    #[test]
328    fn mc_k4() {
329        // m=6, n=4, cr=3, denom=2·4-5=3, MC=3/3=1.0
330        assert!((meshedness_coefficient(&k4()).unwrap() - 1.0).abs() < 1e-10);
331    }
332
333    #[test]
334    fn mc_cycle4() {
335        // m=4, n=4, cr=1, denom=3, MC=1/3
336        assert!((meshedness_coefficient(&cycle4()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
337    }
338
339    #[test]
340    fn mc_star5() {
341        // Tree → cr=0 → MC=0
342        assert!(meshedness_coefficient(&star5()).unwrap().abs() < 1e-10);
343    }
344
345    #[test]
346    fn mc_paw() {
347        // m=4, n=4, cr=1, denom=3, MC=1/3
348        assert!((meshedness_coefficient(&paw()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
349    }
350
351    #[test]
352    fn mc_disconnected() {
353        // c > 1 → MC=0
354        assert!(meshedness_coefficient(&disconnected()).unwrap().abs() < 1e-10);
355    }
356
357    // --- edge_surplus_ratio ---
358
359    #[test]
360    fn esr_empty() {
361        let g = Graph::with_vertices(0);
362        assert!(edge_surplus_ratio(&g).unwrap().abs() < 1e-10);
363    }
364
365    #[test]
366    fn esr_single_vertex() {
367        let g = Graph::with_vertices(1);
368        assert!(edge_surplus_ratio(&g).unwrap().abs() < 1e-10);
369    }
370
371    #[test]
372    fn esr_single_edge() {
373        // m=1, n=2, c=1, cr=0, max_cr=1-2+1=0 → denom=0 → 0.0
374        assert!(edge_surplus_ratio(&single_edge()).unwrap().abs() < 1e-10);
375    }
376
377    #[test]
378    fn esr_path3() {
379        // Tree → cr=0 → ESR=0
380        assert!(edge_surplus_ratio(&path3()).unwrap().abs() < 1e-10);
381    }
382
383    #[test]
384    fn esr_k3() {
385        // cr=1, max_m=3, max_cr=3-3+1=1, ESR=1/1=1.0
386        assert!((edge_surplus_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
387    }
388
389    #[test]
390    fn esr_k4() {
391        // cr=3, max_m=6, max_cr=6-4+1=3, ESR=3/3=1.0
392        assert!((edge_surplus_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
393    }
394
395    #[test]
396    fn esr_cycle4() {
397        // cr=1, max_m=6, max_cr=6-4+1=3, ESR=1/3
398        assert!((edge_surplus_ratio(&cycle4()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
399    }
400
401    #[test]
402    fn esr_star5() {
403        // Tree → cr=0 → ESR=0
404        assert!(edge_surplus_ratio(&star5()).unwrap().abs() < 1e-10);
405    }
406
407    #[test]
408    fn esr_paw() {
409        // cr=1, max_m=6, max_cr=6-4+1=3, ESR=1/3
410        assert!((edge_surplus_ratio(&paw()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
411    }
412
413    // --- connectivity_index ---
414
415    #[test]
416    fn ci_empty() {
417        let g = Graph::with_vertices(0);
418        assert!(connectivity_index(&g).unwrap().abs() < 1e-10);
419    }
420
421    #[test]
422    fn ci_single() {
423        let g = Graph::with_vertices(1);
424        assert!(connectivity_index(&g).unwrap().abs() < 1e-10);
425    }
426
427    #[test]
428    fn ci_single_edge() {
429        // avg_deg=1, max=1, CI=1.0
430        assert!((connectivity_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
431    }
432
433    #[test]
434    fn ci_k3() {
435        // avg_deg=2, max=2, CI=1.0
436        assert!((connectivity_index(&k3()).unwrap() - 1.0).abs() < 1e-10);
437    }
438
439    #[test]
440    fn ci_k4() {
441        // avg_deg=3, max=3, CI=1.0
442        assert!((connectivity_index(&k4()).unwrap() - 1.0).abs() < 1e-10);
443    }
444
445    #[test]
446    fn ci_cycle4() {
447        // avg_deg=2, max=3, CI=2/3
448        assert!((connectivity_index(&cycle4()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
449    }
450
451    #[test]
452    fn ci_star5() {
453        // avg_deg=8/5=1.6, max=4, CI=1.6/4=0.4
454        assert!((connectivity_index(&star5()).unwrap() - 0.4).abs() < 1e-10);
455    }
456
457    #[test]
458    fn ci_path3() {
459        // avg_deg=4/3, max=2, CI=(4/3)/2=2/3
460        assert!((connectivity_index(&path3()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
461    }
462
463    #[test]
464    fn ci_paw() {
465        // avg_deg=8/4=2, max=3, CI=2/3
466        assert!((connectivity_index(&paw()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
467    }
468
469    #[test]
470    fn ci_isolated() {
471        let g = Graph::with_vertices(5);
472        assert!(connectivity_index(&g).unwrap().abs() < 1e-10);
473    }
474
475    // --- cross-consistency ---
476
477    #[test]
478    fn crr_in_01() {
479        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
480            let r = circuit_rank_ratio(g).unwrap();
481            assert!(r >= -1e-10);
482            assert!(r <= 1.0 + 1e-10);
483        }
484    }
485
486    #[test]
487    fn esr_in_01() {
488        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
489            let r = edge_surplus_ratio(g).unwrap();
490            assert!(r >= -1e-10);
491            assert!(r <= 1.0 + 1e-10);
492        }
493    }
494
495    #[test]
496    fn ci_in_01() {
497        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
498            let r = connectivity_index(g).unwrap();
499            assert!(r >= -1e-10);
500            assert!(r <= 1.0 + 1e-10);
501        }
502    }
503
504    #[test]
505    fn complete_graphs_all_one() {
506        // Complete graphs: CRR=0.5(K4), ESR=1.0, CI=1.0
507        assert!((edge_surplus_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
508        assert!((edge_surplus_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
509        assert!((connectivity_index(&k3()).unwrap() - 1.0).abs() < 1e-10);
510        assert!((connectivity_index(&k4()).unwrap() - 1.0).abs() < 1e-10);
511    }
512
513    #[test]
514    fn trees_zero_surplus() {
515        // Trees: CRR=0, ESR=0
516        assert!(circuit_rank_ratio(&path3()).unwrap().abs() < 1e-10);
517        assert!(circuit_rank_ratio(&star5()).unwrap().abs() < 1e-10);
518        assert!(edge_surplus_ratio(&path3()).unwrap().abs() < 1e-10);
519        assert!(edge_surplus_ratio(&star5()).unwrap().abs() < 1e-10);
520    }
521
522    #[test]
523    fn mc_nonneg() {
524        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
525            assert!(meshedness_coefficient(g).unwrap() >= -1e-10);
526        }
527    }
528}