Skip to main content

rust_igraph/algorithms/properties/
degree_distance_ratios.rs

1//! Degree-distance combined ratio indices (ALGO-TR-103).
2//!
3//! Measures combining degree and distance information:
4//!
5//! - **Degree-distance correlation** — Pearson correlation between
6//!   vertex degree and eccentricity
7//! - **Local efficiency ratio** — mean local efficiency / global efficiency
8//! - **Degree-closeness ratio** — mean ratio of degree to closeness rank
9//! - **Transmission ratio** — mean transmission / max transmission
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 degree-distance correlation.
23///
24/// Pearson correlation coefficient between vertex degree and
25/// eccentricity (maximum shortest-path distance from the vertex).
26/// Returns 0.0 for disconnected, trivial, or constant-degree-and-eccentricity
27/// graphs.
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, degree_distance_correlation};
33///
34/// // K_3: all same degree and eccentricity → 0.0
35/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
36/// assert!(degree_distance_correlation(&g).unwrap().abs() < 1e-10);
37/// ```
38pub fn degree_distance_correlation(graph: &Graph) -> IgraphResult<f64> {
39    let n = graph.vcount() as usize;
40    if n < 2 {
41        return Ok(0.0);
42    }
43
44    let dist = all_pairs_bfs(graph)?;
45
46    let mut degrees = Vec::with_capacity(n);
47    let mut eccs = Vec::with_capacity(n);
48
49    for v in 0..n {
50        degrees.push(graph.degree(v as u32)?);
51
52        let mut ecc = 0_u32;
53        for u in 0..n {
54            if u == v {
55                continue;
56            }
57            let d = dist[v * n + u];
58            if d == u32::MAX {
59                return Ok(0.0);
60            }
61            if d > ecc {
62                ecc = d;
63            }
64        }
65        eccs.push(ecc);
66    }
67
68    let mean_deg = degrees.iter().sum::<usize>() as f64 / n as f64;
69    let mean_ecc = f64::from(eccs.iter().sum::<u32>()) / n as f64;
70
71    let mut cov = 0.0_f64;
72    let mut var_deg = 0.0_f64;
73    let mut var_ecc = 0.0_f64;
74
75    for v in 0..n {
76        let dd = degrees[v] as f64 - mean_deg;
77        let de = f64::from(eccs[v]) - mean_ecc;
78        cov += dd * de;
79        var_deg += dd * dd;
80        var_ecc += de * de;
81    }
82
83    if var_deg < 1e-30 || var_ecc < 1e-30 {
84        return Ok(0.0);
85    }
86
87    Ok(cov / (var_deg.sqrt() * var_ecc.sqrt()))
88}
89
90/// Compute the local efficiency ratio.
91///
92/// `mean_local_eff / global_eff` — how the average vertex's local
93/// efficiency compares to the global efficiency. Returns 0.0 if
94/// global efficiency is zero or for trivial graphs.
95///
96/// Local efficiency of vertex v is the average inverse distance
97/// between v's neighbors (using full-graph distances).
98///
99/// # Examples
100///
101/// ```
102/// use rust_igraph::{Graph, local_efficiency_ratio};
103///
104/// // K_3: global_eff=1.0, each vertex's neighbors form K_2 → local_eff=1.0 → ratio=1.0
105/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
106/// assert!((local_efficiency_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
107/// ```
108pub fn local_efficiency_ratio(graph: &Graph) -> IgraphResult<f64> {
109    let n = graph.vcount() as usize;
110    if n < 2 {
111        return Ok(0.0);
112    }
113
114    let dist = all_pairs_bfs(graph)?;
115
116    let mut global_sum_inv = 0.0_f64;
117    let mut global_pairs = 0_u64;
118    for v in 0..n {
119        for u in (v + 1)..n {
120            let d = dist[v * n + u];
121            if d != u32::MAX && d > 0 {
122                global_sum_inv += 1.0 / f64::from(d);
123            }
124            global_pairs += 1;
125        }
126    }
127
128    if global_pairs == 0 {
129        return Ok(0.0);
130    }
131    let global_eff = global_sum_inv / global_pairs as f64;
132    if global_eff < 1e-30 {
133        return Ok(0.0);
134    }
135
136    let mut local_eff_sum = 0.0_f64;
137    for v in 0..n {
138        let neighbors = graph.neighbors(v as u32)?;
139        let nn = neighbors.len();
140        if nn < 2 {
141            continue;
142        }
143
144        let mut sub_sum_inv = 0.0_f64;
145        let mut sub_pairs = 0_u64;
146        for i in 0..nn {
147            let ni = neighbors[i] as usize;
148            for j in (i + 1)..nn {
149                let nj = neighbors[j] as usize;
150                let d = dist[ni * n + nj];
151                if d != u32::MAX && d > 0 {
152                    sub_sum_inv += 1.0 / f64::from(d);
153                }
154                sub_pairs += 1;
155            }
156        }
157
158        if sub_pairs > 0 {
159            local_eff_sum += sub_sum_inv / sub_pairs as f64;
160        }
161    }
162
163    let mean_local_eff = local_eff_sum / n as f64;
164    Ok(mean_local_eff / global_eff)
165}
166
167/// Compute the transmission ratio.
168///
169/// `mean_transmission / max_transmission` where transmission of a
170/// vertex is the sum of distances from it to all other vertices.
171/// Returns 0.0 for disconnected or trivial graphs.
172///
173/// # Examples
174///
175/// ```
176/// use rust_igraph::{Graph, transmission_ratio};
177///
178/// // K_3: all transmissions = 2, ratio = 1.0
179/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
180/// assert!((transmission_ratio(&g).unwrap() - 1.0).abs() < 1e-10);
181/// ```
182pub fn transmission_ratio(graph: &Graph) -> IgraphResult<f64> {
183    let n = graph.vcount() as usize;
184    if n < 2 {
185        return Ok(0.0);
186    }
187
188    let dist = all_pairs_bfs(graph)?;
189
190    let mut transmissions = Vec::with_capacity(n);
191    for v in 0..n {
192        let mut t = 0_u64;
193        for u in 0..n {
194            if u == v {
195                continue;
196            }
197            let d = dist[v * n + u];
198            if d == u32::MAX {
199                return Ok(0.0);
200            }
201            t += u64::from(d);
202        }
203        transmissions.push(t);
204    }
205
206    let max_t = transmissions.iter().copied().max().unwrap_or(0);
207    if max_t == 0 {
208        return Ok(0.0);
209    }
210
211    let mean_t = transmissions.iter().sum::<u64>() as f64 / n as f64;
212    Ok(mean_t / max_t as f64)
213}
214
215/// Compute the degree-closeness correlation.
216///
217/// Pearson correlation between vertex degree and closeness centrality
218/// (reciprocal of mean distance). Returns 0.0 for disconnected or
219/// trivial graphs, or when degree or closeness has zero variance.
220///
221/// # Examples
222///
223/// ```
224/// use rust_igraph::{Graph, degree_closeness_correlation};
225///
226/// // K_3: constant degree and closeness → 0.0
227/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
228/// assert!(degree_closeness_correlation(&g).unwrap().abs() < 1e-10);
229/// ```
230pub fn degree_closeness_correlation(graph: &Graph) -> IgraphResult<f64> {
231    let n = graph.vcount() as usize;
232    if n < 2 {
233        return Ok(0.0);
234    }
235
236    let dist = all_pairs_bfs(graph)?;
237
238    let mut degrees = Vec::with_capacity(n);
239    let mut closeness_vals = Vec::with_capacity(n);
240
241    for v in 0..n {
242        degrees.push(graph.degree(v as u32)?);
243
244        let mut sum_d = 0_u64;
245        for u in 0..n {
246            if u == v {
247                continue;
248            }
249            let d = dist[v * n + u];
250            if d == u32::MAX {
251                return Ok(0.0);
252            }
253            sum_d += u64::from(d);
254        }
255        if sum_d == 0 {
256            closeness_vals.push(0.0);
257        } else {
258            closeness_vals.push((n - 1) as f64 / sum_d as f64);
259        }
260    }
261
262    let mean_deg = degrees.iter().sum::<usize>() as f64 / n as f64;
263    let mean_close: f64 = closeness_vals.iter().sum::<f64>() / n as f64;
264
265    let mut cov = 0.0_f64;
266    let mut var_deg = 0.0_f64;
267    let mut var_close = 0.0_f64;
268
269    for v in 0..n {
270        let dd = degrees[v] as f64 - mean_deg;
271        let dc = closeness_vals[v] - mean_close;
272        cov += dd * dc;
273        var_deg += dd * dd;
274        var_close += dc * dc;
275    }
276
277    if var_deg < 1e-30 || var_close < 1e-30 {
278        return Ok(0.0);
279    }
280
281    Ok(cov / (var_deg.sqrt() * var_close.sqrt()))
282}
283
284fn all_pairs_bfs(graph: &Graph) -> IgraphResult<Vec<u32>> {
285    let n = graph.vcount() as usize;
286    let mut dist = vec![u32::MAX; n * n];
287
288    for s in 0..n {
289        dist[s * n + s] = 0;
290        let mut queue = std::collections::VecDeque::new();
291        queue.push_back(s);
292        while let Some(v) = queue.pop_front() {
293            let d = dist[s * n + v];
294            let neighbors = graph.neighbors(v as u32)?;
295            for &u in &neighbors {
296                let ui = u as usize;
297                if dist[s * n + ui] == u32::MAX {
298                    dist[s * n + ui] = d + 1;
299                    queue.push_back(ui);
300                }
301            }
302        }
303    }
304
305    Ok(dist)
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    fn single_edge() -> Graph {
313        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
314    }
315
316    fn path3() -> Graph {
317        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
318    }
319
320    fn k3() -> Graph {
321        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
322    }
323
324    fn k4() -> Graph {
325        Graph::from_edges(
326            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
327            false,
328            Some(4),
329        )
330        .unwrap()
331    }
332
333    fn cycle4() -> Graph {
334        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
335    }
336
337    fn star5() -> Graph {
338        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
339    }
340
341    fn paw() -> Graph {
342        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
343    }
344
345    // --- degree_distance_correlation ---
346
347    #[test]
348    fn ddc_empty() {
349        let g = Graph::with_vertices(0);
350        assert!(degree_distance_correlation(&g).unwrap().abs() < 1e-10);
351    }
352
353    #[test]
354    fn ddc_single() {
355        let g = Graph::with_vertices(1);
356        assert!(degree_distance_correlation(&g).unwrap().abs() < 1e-10);
357    }
358
359    #[test]
360    fn ddc_k3() {
361        // Regular, constant eccentricity → 0
362        assert!(degree_distance_correlation(&k3()).unwrap().abs() < 1e-10);
363    }
364
365    #[test]
366    fn ddc_k4() {
367        assert!(degree_distance_correlation(&k4()).unwrap().abs() < 1e-10);
368    }
369
370    #[test]
371    fn ddc_cycle4() {
372        // Regular, constant eccentricity → 0
373        assert!(degree_distance_correlation(&cycle4()).unwrap().abs() < 1e-10);
374    }
375
376    #[test]
377    fn ddc_star5() {
378        // Center: deg=4, ecc=1; Leaves: deg=1, ecc=2
379        // Higher degree → lower eccentricity → negative correlation
380        let r = degree_distance_correlation(&star5()).unwrap();
381        assert!(r < -0.5);
382    }
383
384    #[test]
385    fn ddc_path3() {
386        // deg: [1,2,1], ecc: [2,1,2] → negative correlation
387        let r = degree_distance_correlation(&path3()).unwrap();
388        assert!(r < -0.5);
389    }
390
391    #[test]
392    fn ddc_disconnected() {
393        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
394        assert!(degree_distance_correlation(&g).unwrap().abs() < 1e-10);
395    }
396
397    #[test]
398    fn ddc_in_range() {
399        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
400            let r = degree_distance_correlation(g).unwrap();
401            assert!(r >= -1.0 - 1e-10);
402            assert!(r <= 1.0 + 1e-10);
403        }
404    }
405
406    // --- local_efficiency_ratio ---
407
408    #[test]
409    fn ler_empty() {
410        let g = Graph::with_vertices(0);
411        assert!(local_efficiency_ratio(&g).unwrap().abs() < 1e-10);
412    }
413
414    #[test]
415    fn ler_single() {
416        let g = Graph::with_vertices(1);
417        assert!(local_efficiency_ratio(&g).unwrap().abs() < 1e-10);
418    }
419
420    #[test]
421    fn ler_k3() {
422        // global=1.0, each vertex's neighbors form K_2, local_eff=1.0 → ratio=1.0
423        assert!((local_efficiency_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
424    }
425
426    #[test]
427    fn ler_k4() {
428        // global=1.0, each vertex's neighbors form K_3, local_eff=1.0 → ratio=1.0
429        assert!((local_efficiency_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
430    }
431
432    #[test]
433    fn ler_star5() {
434        // Center: neighbors={1,2,3,4}, all at dist 2 → local_eff=0.5
435        // Leaves: 1 neighbor → local_eff=0
436        // mean_local=0.1, global=0.7 → ratio=1/7
437        let r = local_efficiency_ratio(&star5()).unwrap();
438        assert!(r > 0.0);
439        assert!(r < 0.5);
440    }
441
442    #[test]
443    fn ler_single_edge() {
444        // Each vertex has 1 neighbor → no subgraph pairs → local_eff = 0
445        // But global eff = 1.0 → ratio = 0
446        assert!(local_efficiency_ratio(&single_edge()).unwrap().abs() < 1e-10);
447    }
448
449    #[test]
450    fn ler_nonneg() {
451        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
452            assert!(local_efficiency_ratio(g).unwrap() >= -1e-10);
453        }
454    }
455
456    // --- transmission_ratio ---
457
458    #[test]
459    fn tr_empty() {
460        let g = Graph::with_vertices(0);
461        assert!(transmission_ratio(&g).unwrap().abs() < 1e-10);
462    }
463
464    #[test]
465    fn tr_single() {
466        let g = Graph::with_vertices(1);
467        assert!(transmission_ratio(&g).unwrap().abs() < 1e-10);
468    }
469
470    #[test]
471    fn tr_k3() {
472        // All transmissions = 2 → ratio = 1.0
473        assert!((transmission_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
474    }
475
476    #[test]
477    fn tr_k4() {
478        // All transmissions = 3 → ratio = 1.0
479        assert!((transmission_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
480    }
481
482    #[test]
483    fn tr_cycle4() {
484        // All transmissions = 1+2+1=4 → ratio = 1.0
485        assert!((transmission_ratio(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
486    }
487
488    #[test]
489    fn tr_path3() {
490        // Trans: [1+2, 1+1, 2+1] = [3, 2, 3]
491        // mean=8/3, max=3 → 8/9
492        assert!((transmission_ratio(&path3()).unwrap() - 8.0 / 9.0).abs() < 1e-10);
493    }
494
495    #[test]
496    fn tr_star5() {
497        // Center: 1+1+1+1=4; Leaf: 1+2+2+2=7
498        // Trans: [4,7,7,7,7], mean=32/5, max=7 → 32/35
499        assert!((transmission_ratio(&star5()).unwrap() - 32.0 / 35.0).abs() < 1e-10);
500    }
501
502    #[test]
503    fn tr_disconnected() {
504        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
505        assert!(transmission_ratio(&g).unwrap().abs() < 1e-10);
506    }
507
508    #[test]
509    fn tr_in_01() {
510        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
511            let r = transmission_ratio(g).unwrap();
512            assert!(r >= -1e-10);
513            assert!(r <= 1.0 + 1e-10);
514        }
515    }
516
517    // --- degree_closeness_correlation ---
518
519    #[test]
520    fn dcc_empty() {
521        let g = Graph::with_vertices(0);
522        assert!(degree_closeness_correlation(&g).unwrap().abs() < 1e-10);
523    }
524
525    #[test]
526    fn dcc_single() {
527        let g = Graph::with_vertices(1);
528        assert!(degree_closeness_correlation(&g).unwrap().abs() < 1e-10);
529    }
530
531    #[test]
532    fn dcc_k3() {
533        // Constant → 0
534        assert!(degree_closeness_correlation(&k3()).unwrap().abs() < 1e-10);
535    }
536
537    #[test]
538    fn dcc_k4() {
539        assert!(degree_closeness_correlation(&k4()).unwrap().abs() < 1e-10);
540    }
541
542    #[test]
543    fn dcc_cycle4() {
544        assert!(degree_closeness_correlation(&cycle4()).unwrap().abs() < 1e-10);
545    }
546
547    #[test]
548    fn dcc_star5() {
549        // Center: deg=4, closeness=1.0 (all at dist 1)
550        // Leaf: deg=1, closeness=4/(1+2+2+2)=4/7
551        // Higher degree → higher closeness → positive correlation
552        let r = degree_closeness_correlation(&star5()).unwrap();
553        assert!(r > 0.5);
554    }
555
556    #[test]
557    fn dcc_path3() {
558        // deg: [1,2,1], closeness: [2/(1+2)=2/3, 2/(1+1)=1, 2/(2+1)=2/3]
559        // Higher degree → higher closeness → positive
560        let r = degree_closeness_correlation(&path3()).unwrap();
561        assert!(r > 0.5);
562    }
563
564    #[test]
565    fn dcc_disconnected() {
566        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
567        assert!(degree_closeness_correlation(&g).unwrap().abs() < 1e-10);
568    }
569
570    #[test]
571    fn dcc_in_range() {
572        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
573            let r = degree_closeness_correlation(g).unwrap();
574            assert!(r >= -1.0 - 1e-10);
575            assert!(r <= 1.0 + 1e-10);
576        }
577    }
578
579    // --- cross-consistency ---
580
581    #[test]
582    fn regular_graphs_zero_corr() {
583        // Regular graphs have constant degree → zero degree-distance correlation
584        assert!(degree_distance_correlation(&k3()).unwrap().abs() < 1e-10);
585        assert!(degree_distance_correlation(&k4()).unwrap().abs() < 1e-10);
586        assert!(degree_distance_correlation(&cycle4()).unwrap().abs() < 1e-10);
587    }
588
589    #[test]
590    fn regular_graphs_unit_transmission() {
591        // Regular + vertex-transitive → all same transmission → ratio=1
592        assert!((transmission_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
593        assert!((transmission_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
594        assert!((transmission_ratio(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
595    }
596}