Skip to main content

rust_igraph/algorithms/properties/
degree_eccentricity.rs

1//! Degree-eccentricity indices (ALGO-TR-058).
2//!
3//! Topological indices combining vertex degree with eccentricity.
4//!
5//! - **Lanzhou index** `Lz(G) = Σ_{v∈V} d(v)² · ε(v)`
6//!   Introduced by Vukičević et al. (2018). Product of squared degree
7//!   and eccentricity summed over all vertices.
8//! - **Degree-eccentricity index** `DE(G) = Σ_{v∈V} d(v) · ε(v)`
9//!   Product of degree and eccentricity summed over all vertices.
10//! - **Eccentric-distance sum** `ξ^d(G) = Σ_{v∈V} ε(v) · D(v)`
11//!   where `D(v) = Σ_{u∈V} dist(v,u)` is the distance sum (status) of v.
12//!   Introduced by Gupta et al. (2002).
13
14#![allow(
15    clippy::cast_possible_truncation,
16    clippy::cast_precision_loss,
17    clippy::many_single_char_names,
18    clippy::needless_range_loop,
19    clippy::too_many_lines
20)]
21
22use crate::core::{Graph, IgraphResult};
23
24fn bfs_ecc_and_dist_sum(graph: &Graph) -> IgraphResult<(Vec<u32>, Vec<u64>)> {
25    let n = graph.vcount() as usize;
26    let mut ecc = vec![0_u32; n];
27    let mut dist_sum = vec![0_u64; n];
28
29    for s in 0..n {
30        let mut dist = vec![u32::MAX; n];
31        dist[s] = 0;
32        let mut queue = std::collections::VecDeque::new();
33        queue.push_back(s as u32);
34        while let Some(u) = queue.pop_front() {
35            let d_u = dist[u as usize];
36            for nb in graph.neighbors(u)? {
37                let idx = nb as usize;
38                if dist[idx] == u32::MAX {
39                    dist[idx] = d_u + 1;
40                    queue.push_back(nb);
41                }
42            }
43        }
44        let mut max_d = 0_u32;
45        let mut sum = 0_u64;
46        for &d in &dist {
47            if d != u32::MAX {
48                if d > max_d {
49                    max_d = d;
50                }
51                sum += u64::from(d);
52            }
53        }
54        ecc[s] = max_d;
55        dist_sum[s] = sum;
56    }
57
58    Ok((ecc, dist_sum))
59}
60
61/// Compute the Lanzhou index.
62///
63/// `Lz(G) = Σ_{v∈V} d(v)² · ε(v)`
64///
65/// Returns 0 for empty graphs.
66///
67/// # Examples
68///
69/// ```
70/// use rust_igraph::{Graph, lanzhou_index};
71///
72/// // Path 0-1-2: degrees [1,2,1], eccentricities [2,1,2]
73/// // Lz = 1·2 + 4·1 + 1·2 = 8
74/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
75/// assert_eq!(lanzhou_index(&g).unwrap(), 8);
76/// ```
77pub fn lanzhou_index(graph: &Graph) -> IgraphResult<u64> {
78    let n = graph.vcount();
79    if n == 0 {
80        return Ok(0);
81    }
82
83    let (ecc, _) = bfs_ecc_and_dist_sum(graph)?;
84    let mut lz = 0_u64;
85
86    for v in 0..n {
87        let d = graph.degree(v)? as u64;
88        let e = u64::from(ecc[v as usize]);
89        lz = lz.saturating_add(d.saturating_mul(d).saturating_mul(e));
90    }
91
92    Ok(lz)
93}
94
95/// Compute the degree-eccentricity index.
96///
97/// `DE(G) = Σ_{v∈V} d(v) · ε(v)`
98///
99/// Returns 0 for empty graphs.
100///
101/// # Examples
102///
103/// ```
104/// use rust_igraph::{Graph, degree_eccentricity_index};
105///
106/// // Path 0-1-2: degrees [1,2,1], eccentricities [2,1,2]
107/// // DE = 1·2 + 2·1 + 1·2 = 6
108/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
109/// assert_eq!(degree_eccentricity_index(&g).unwrap(), 6);
110/// ```
111pub fn degree_eccentricity_index(graph: &Graph) -> IgraphResult<u64> {
112    let n = graph.vcount();
113    if n == 0 {
114        return Ok(0);
115    }
116
117    let (ecc, _) = bfs_ecc_and_dist_sum(graph)?;
118    let mut de = 0_u64;
119
120    for v in 0..n {
121        let d = graph.degree(v)? as u64;
122        let e = u64::from(ecc[v as usize]);
123        de = de.saturating_add(d.saturating_mul(e));
124    }
125
126    Ok(de)
127}
128
129/// Compute the eccentric-distance sum.
130///
131/// `ξ^d(G) = Σ_{v∈V} ε(v) · D(v)`
132///
133/// where `D(v) = Σ_{u} dist(v,u)` is the distance sum of v.
134/// Returns 0 for empty graphs. Disconnected components contribute
135/// only within their own component.
136///
137/// # Examples
138///
139/// ```
140/// use rust_igraph::{Graph, eccentric_distance_sum};
141///
142/// // Path 0-1-2: ecc [2,1,2], D [3,2,3]
143/// // ξ^d = 2·3 + 1·2 + 2·3 = 14
144/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
145/// assert_eq!(eccentric_distance_sum(&g).unwrap(), 14);
146/// ```
147pub fn eccentric_distance_sum(graph: &Graph) -> IgraphResult<u64> {
148    let n = graph.vcount();
149    if n == 0 {
150        return Ok(0);
151    }
152
153    let (ecc, dist_sum) = bfs_ecc_and_dist_sum(graph)?;
154    let mut eds = 0_u64;
155
156    for v in 0..n as usize {
157        let e = u64::from(ecc[v]);
158        eds = eds.saturating_add(e.saturating_mul(dist_sum[v]));
159    }
160
161    Ok(eds)
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    fn single_edge() -> Graph {
169        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
170    }
171
172    fn path3() -> Graph {
173        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
174    }
175
176    fn path4() -> Graph {
177        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
178    }
179
180    fn k3() -> Graph {
181        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
182    }
183
184    fn k4() -> Graph {
185        Graph::from_edges(
186            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
187            false,
188            Some(4),
189        )
190        .unwrap()
191    }
192
193    fn cycle4() -> Graph {
194        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
195    }
196
197    fn cycle5() -> Graph {
198        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
199    }
200
201    fn star5() -> Graph {
202        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
203    }
204
205    fn paw() -> Graph {
206        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
207    }
208
209    // --- bfs helper ---
210
211    #[test]
212    fn bfs_path3() {
213        let (ecc, ds) = bfs_ecc_and_dist_sum(&path3()).unwrap();
214        assert_eq!(ecc, vec![2, 1, 2]);
215        assert_eq!(ds, vec![3, 2, 3]);
216    }
217
218    #[test]
219    fn bfs_k3() {
220        let (ecc, ds) = bfs_ecc_and_dist_sum(&k3()).unwrap();
221        assert_eq!(ecc, vec![1, 1, 1]);
222        assert_eq!(ds, vec![2, 2, 2]);
223    }
224
225    #[test]
226    fn bfs_star5() {
227        let (ecc, ds) = bfs_ecc_and_dist_sum(&star5()).unwrap();
228        assert_eq!(ecc[0], 1);
229        for i in 1..5 {
230            assert_eq!(ecc[i], 2);
231        }
232        assert_eq!(ds[0], 4);
233        for i in 1..5 {
234            assert_eq!(ds[i], 7);
235        }
236    }
237
238    // --- lanzhou_index ---
239
240    #[test]
241    fn lz_empty() {
242        let g = Graph::with_vertices(0);
243        assert_eq!(lanzhou_index(&g).unwrap(), 0);
244    }
245
246    #[test]
247    fn lz_isolated() {
248        let g = Graph::with_vertices(5);
249        assert_eq!(lanzhou_index(&g).unwrap(), 0);
250    }
251
252    #[test]
253    fn lz_single_edge() {
254        // degrees [1,1], ecc [1,1]: 1·1 + 1·1 = 2
255        assert_eq!(lanzhou_index(&single_edge()).unwrap(), 2);
256    }
257
258    #[test]
259    fn lz_path3() {
260        // d²·ε: 1·2 + 4·1 + 1·2 = 8
261        assert_eq!(lanzhou_index(&path3()).unwrap(), 8);
262    }
263
264    #[test]
265    fn lz_path4() {
266        // degrees [1,2,2,1], ecc [3,2,2,3]
267        // 1·3 + 4·2 + 4·2 + 1·3 = 3+8+8+3 = 22
268        assert_eq!(lanzhou_index(&path4()).unwrap(), 22);
269    }
270
271    #[test]
272    fn lz_k3() {
273        // d²·ε: 3·(4·1) = 12
274        assert_eq!(lanzhou_index(&k3()).unwrap(), 12);
275    }
276
277    #[test]
278    fn lz_k4() {
279        // d²·ε: 4·(9·1) = 36
280        assert_eq!(lanzhou_index(&k4()).unwrap(), 36);
281    }
282
283    #[test]
284    fn lz_cycle4() {
285        // degrees all 2, ecc all 2: 4·(4·2) = 32
286        assert_eq!(lanzhou_index(&cycle4()).unwrap(), 32);
287    }
288
289    #[test]
290    fn lz_cycle5() {
291        // degrees all 2, ecc all 2: 5·(4·2) = 40
292        assert_eq!(lanzhou_index(&cycle5()).unwrap(), 40);
293    }
294
295    #[test]
296    fn lz_star5() {
297        // center: d=4, ecc=1: 16·1 = 16
298        // leaves: d=1, ecc=2: 4·(1·2) = 8
299        assert_eq!(lanzhou_index(&star5()).unwrap(), 24);
300    }
301
302    #[test]
303    fn lz_paw() {
304        // degrees [2,2,3,1], ecc [2,2,1,2]
305        // 4·2 + 4·2 + 9·1 + 1·2 = 8+8+9+2 = 27
306        assert_eq!(lanzhou_index(&paw()).unwrap(), 27);
307    }
308
309    // --- degree_eccentricity_index ---
310
311    #[test]
312    fn de_empty() {
313        let g = Graph::with_vertices(0);
314        assert_eq!(degree_eccentricity_index(&g).unwrap(), 0);
315    }
316
317    #[test]
318    fn de_isolated() {
319        let g = Graph::with_vertices(5);
320        assert_eq!(degree_eccentricity_index(&g).unwrap(), 0);
321    }
322
323    #[test]
324    fn de_single_edge() {
325        // d·ε: 1·1 + 1·1 = 2
326        assert_eq!(degree_eccentricity_index(&single_edge()).unwrap(), 2);
327    }
328
329    #[test]
330    fn de_path3() {
331        // 1·2 + 2·1 + 1·2 = 6
332        assert_eq!(degree_eccentricity_index(&path3()).unwrap(), 6);
333    }
334
335    #[test]
336    fn de_path4() {
337        // degrees [1,2,2,1], ecc [3,2,2,3]
338        // 1·3 + 2·2 + 2·2 + 1·3 = 3+4+4+3 = 14
339        assert_eq!(degree_eccentricity_index(&path4()).unwrap(), 14);
340    }
341
342    #[test]
343    fn de_k3() {
344        // 3·(2·1) = 6
345        assert_eq!(degree_eccentricity_index(&k3()).unwrap(), 6);
346    }
347
348    #[test]
349    fn de_k4() {
350        // 4·(3·1) = 12
351        assert_eq!(degree_eccentricity_index(&k4()).unwrap(), 12);
352    }
353
354    #[test]
355    fn de_cycle4() {
356        // 4·(2·2) = 16
357        assert_eq!(degree_eccentricity_index(&cycle4()).unwrap(), 16);
358    }
359
360    #[test]
361    fn de_cycle5() {
362        // 5·(2·2) = 20
363        assert_eq!(degree_eccentricity_index(&cycle5()).unwrap(), 20);
364    }
365
366    #[test]
367    fn de_star5() {
368        // center: 4·1 = 4, leaves: 4·(1·2) = 8
369        assert_eq!(degree_eccentricity_index(&star5()).unwrap(), 12);
370    }
371
372    #[test]
373    fn de_paw() {
374        // [2,2,3,1], ecc [2,2,1,2]
375        // 2·2 + 2·2 + 3·1 + 1·2 = 4+4+3+2 = 13
376        assert_eq!(degree_eccentricity_index(&paw()).unwrap(), 13);
377    }
378
379    #[test]
380    fn de_leq_lz() {
381        // DE = Σd·ε ≤ Σd²·ε = Lz when d ≥ 1
382        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
383            assert!(degree_eccentricity_index(g).unwrap() <= lanzhou_index(g).unwrap());
384        }
385    }
386
387    // --- eccentric_distance_sum ---
388
389    #[test]
390    fn eds_empty() {
391        let g = Graph::with_vertices(0);
392        assert_eq!(eccentric_distance_sum(&g).unwrap(), 0);
393    }
394
395    #[test]
396    fn eds_isolated() {
397        let g = Graph::with_vertices(5);
398        assert_eq!(eccentric_distance_sum(&g).unwrap(), 0);
399    }
400
401    #[test]
402    fn eds_single_edge() {
403        // ecc [1,1], D [1,1]: 1·1 + 1·1 = 2
404        assert_eq!(eccentric_distance_sum(&single_edge()).unwrap(), 2);
405    }
406
407    #[test]
408    fn eds_path3() {
409        // ecc [2,1,2], D [3,2,3]: 2·3 + 1·2 + 2·3 = 14
410        assert_eq!(eccentric_distance_sum(&path3()).unwrap(), 14);
411    }
412
413    #[test]
414    fn eds_path4() {
415        // ecc [3,2,2,3], D [6,4,4,6]: 3·6 + 2·4 + 2·4 + 3·6 = 18+8+8+18 = 52
416        assert_eq!(eccentric_distance_sum(&path4()).unwrap(), 52);
417    }
418
419    #[test]
420    fn eds_k3() {
421        // ecc [1,1,1], D [2,2,2]: 3·(1·2) = 6
422        assert_eq!(eccentric_distance_sum(&k3()).unwrap(), 6);
423    }
424
425    #[test]
426    fn eds_k4() {
427        // ecc [1,1,1,1], D [3,3,3,3]: 4·(1·3) = 12
428        assert_eq!(eccentric_distance_sum(&k4()).unwrap(), 12);
429    }
430
431    #[test]
432    fn eds_cycle4() {
433        // ecc [2,2,2,2], D [4,4,4,4]: 4·(2·4) = 32
434        assert_eq!(eccentric_distance_sum(&cycle4()).unwrap(), 32);
435    }
436
437    #[test]
438    fn eds_cycle5() {
439        // ecc [2,2,2,2,2], D [6,6,6,6,6]: 5·(2·6) = 60
440        assert_eq!(eccentric_distance_sum(&cycle5()).unwrap(), 60);
441    }
442
443    #[test]
444    fn eds_star5() {
445        // ecc [1,2,2,2,2], D [4,7,7,7,7]
446        // 1·4 + 4·(2·7) = 4+56 = 60
447        assert_eq!(eccentric_distance_sum(&star5()).unwrap(), 60);
448    }
449
450    #[test]
451    fn eds_paw() {
452        // ecc [2,2,1,2], D:
453        // D(0): 0+1+1+2=4, D(1): 1+0+1+2=4, D(2): 1+1+0+1=3, D(3): 2+2+1+0=5
454        // ξ^d = 2·4 + 2·4 + 1·3 + 2·5 = 8+8+3+10 = 29
455        assert_eq!(eccentric_distance_sum(&paw()).unwrap(), 29);
456    }
457
458    // --- cross-consistency ---
459
460    #[test]
461    fn all_nonneg() {
462        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
463            assert!(lanzhou_index(g).unwrap() > 0);
464            assert!(degree_eccentricity_index(g).unwrap() > 0);
465            assert!(eccentric_distance_sum(g).unwrap() > 0);
466        }
467    }
468
469    #[test]
470    fn lz_regular_formula() {
471        // r-regular, ε-uniform: Lz = n · r² · ε
472        for g in &[k3(), k4()] {
473            let n = u64::from(g.vcount());
474            let r = g.degree(0).unwrap() as u64;
475            let (ecc, _) = bfs_ecc_and_dist_sum(g).unwrap();
476            let e = u64::from(ecc[0]);
477            assert_eq!(lanzhou_index(g).unwrap(), n * r * r * e);
478        }
479    }
480
481    #[test]
482    fn de_regular_formula() {
483        // r-regular, ε-uniform: DE = n · r · ε
484        for g in &[k3(), k4()] {
485            let n = u64::from(g.vcount());
486            let r = g.degree(0).unwrap() as u64;
487            let (ecc, _) = bfs_ecc_and_dist_sum(g).unwrap();
488            let e = u64::from(ecc[0]);
489            assert_eq!(degree_eccentricity_index(g).unwrap(), n * r * e);
490        }
491    }
492}