Skip to main content

rust_igraph/algorithms/properties/
eccentric_connectivity.rs

1//! Eccentric connectivity index and total eccentricity (ALGO-TR-045).
2//!
3//! - **Eccentric connectivity index** `ξ^c(G) = Σ_v deg(v) · ecc(v)`
4//!   where `ecc(v) = max_w d(v, w)` is the eccentricity of vertex `v`.
5//!   Introduced by Sharma, Goswami & Madan (1997); widely used in
6//!   QSAR studies.
7//! - **Total eccentricity** `ζ(G) = Σ_v ecc(v)`.
8//! - **Connective eccentricity index** `ξ^ce(G) = Σ_v deg(v) / ecc(v)`.
9//!
10//! Disconnected vertices have `ecc = 0` within their component if
11//! isolated (single vertex); otherwise eccentricity is computed
12//! within the reachable set. Isolated vertices are skipped for
13//! the connective eccentricity index (division by zero).
14
15#![allow(
16    clippy::cast_possible_truncation,
17    clippy::cast_precision_loss,
18    clippy::many_single_char_names,
19    clippy::needless_range_loop,
20    clippy::too_many_lines
21)]
22
23use crate::core::{Graph, IgraphResult};
24use std::collections::VecDeque;
25
26/// Compute the eccentric connectivity index.
27///
28/// `ξ^c(G) = Σ_v deg(v) · ecc(v)`
29///
30/// Isolated vertices contribute 0. For disconnected graphs,
31/// eccentricity is computed within the reachable set of each vertex.
32///
33/// # Examples
34///
35/// ```
36/// use rust_igraph::{Graph, eccentric_connectivity_index};
37///
38/// // Path 0-1-2: deg=[1,2,1], ecc=[2,1,2]
39/// // ξ^c = 1·2 + 2·1 + 1·2 = 6
40/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
41/// assert_eq!(eccentric_connectivity_index(&g).unwrap(), 6);
42/// ```
43pub fn eccentric_connectivity_index(graph: &Graph) -> IgraphResult<u64> {
44    let n = graph.vcount() as usize;
45    if n == 0 {
46        return Ok(0);
47    }
48
49    let ecc = compute_eccentricities(graph, n);
50    let mut xi: u64 = 0;
51
52    for v in 0..n as u32 {
53        let deg = graph.degree(v)? as u64;
54        xi = xi.saturating_add(deg.saturating_mul(u64::from(ecc[v as usize])));
55    }
56
57    Ok(xi)
58}
59
60/// Compute the total eccentricity.
61///
62/// `ζ(G) = Σ_v ecc(v)`
63///
64/// # Examples
65///
66/// ```
67/// use rust_igraph::{Graph, total_eccentricity};
68///
69/// // Path 0-1-2: ecc=[2,1,2] → ζ = 5
70/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
71/// assert_eq!(total_eccentricity(&g).unwrap(), 5);
72/// ```
73pub fn total_eccentricity(graph: &Graph) -> IgraphResult<u64> {
74    let n = graph.vcount() as usize;
75    if n == 0 {
76        return Ok(0);
77    }
78
79    let ecc = compute_eccentricities(graph, n);
80    let mut total: u64 = 0;
81    for &e in &ecc {
82        total = total.saturating_add(u64::from(e));
83    }
84
85    Ok(total)
86}
87
88/// Compute the connective eccentricity index.
89///
90/// `ξ^ce(G) = Σ_v deg(v) / ecc(v)`
91///
92/// Vertices with eccentricity 0 (isolated) are skipped.
93///
94/// # Examples
95///
96/// ```
97/// use rust_igraph::{Graph, connective_eccentricity_index};
98///
99/// // Path 0-1-2: deg=[1,2,1], ecc=[2,1,2]
100/// // ξ^ce = 1/2 + 2/1 + 1/2 = 3.0
101/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
102/// assert!((connective_eccentricity_index(&g).unwrap() - 3.0).abs() < 1e-10);
103/// ```
104pub fn connective_eccentricity_index(graph: &Graph) -> IgraphResult<f64> {
105    let n = graph.vcount() as usize;
106    if n == 0 {
107        return Ok(0.0);
108    }
109
110    let ecc = compute_eccentricities(graph, n);
111    let mut xi = 0.0_f64;
112
113    for v in 0..n as u32 {
114        let e = ecc[v as usize];
115        if e == 0 {
116            continue;
117        }
118        let deg = graph.degree(v)? as f64;
119        xi += deg / f64::from(e);
120    }
121
122    Ok(xi)
123}
124
125fn compute_eccentricities(graph: &Graph, n: usize) -> Vec<u32> {
126    let adj = build_adj_list(graph, n);
127    let mut ecc = vec![0_u32; n];
128
129    for src in 0..n {
130        let dist = bfs_from(&adj, n, src);
131        let mut max_d = 0_u32;
132        for &d in &dist {
133            if d != u32::MAX && d > max_d {
134                max_d = d;
135            }
136        }
137        ecc[src] = max_d;
138    }
139
140    ecc
141}
142
143fn build_adj_list(graph: &Graph, n: usize) -> Vec<Vec<usize>> {
144    let mut adj = vec![Vec::new(); n];
145    for (u, v) in graph.edges() {
146        let ui = u as usize;
147        let vi = v as usize;
148        adj[ui].push(vi);
149        if !graph.is_directed() && ui != vi {
150            adj[vi].push(ui);
151        }
152    }
153    adj
154}
155
156fn bfs_from(adj: &[Vec<usize>], n: usize, src: usize) -> Vec<u32> {
157    let mut dist = vec![u32::MAX; n];
158    dist[src] = 0;
159    let mut queue = VecDeque::new();
160    queue.push_back(src);
161    while let Some(v) = queue.pop_front() {
162        let d = dist[v];
163        for &w in &adj[v] {
164            if dist[w] == u32::MAX {
165                dist[w] = d.saturating_add(1);
166                queue.push_back(w);
167            }
168        }
169    }
170    dist
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    fn single_edge() -> Graph {
178        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
179    }
180
181    fn path3() -> Graph {
182        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
183    }
184
185    fn path4() -> Graph {
186        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
187    }
188
189    fn path5() -> Graph {
190        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap()
191    }
192
193    fn k3() -> Graph {
194        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
195    }
196
197    fn k4() -> Graph {
198        Graph::from_edges(
199            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
200            false,
201            Some(4),
202        )
203        .unwrap()
204    }
205
206    fn cycle4() -> Graph {
207        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
208    }
209
210    fn cycle5() -> Graph {
211        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
212    }
213
214    fn star5() -> Graph {
215        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
216    }
217
218    // --- eccentric_connectivity_index ---
219
220    #[test]
221    fn eci_empty() {
222        let g = Graph::with_vertices(0);
223        assert_eq!(eccentric_connectivity_index(&g).unwrap(), 0);
224    }
225
226    #[test]
227    fn eci_single_vertex() {
228        let g = Graph::with_vertices(1);
229        assert_eq!(eccentric_connectivity_index(&g).unwrap(), 0);
230    }
231
232    #[test]
233    fn eci_no_edges() {
234        let g = Graph::with_vertices(3);
235        assert_eq!(eccentric_connectivity_index(&g).unwrap(), 0);
236    }
237
238    #[test]
239    fn eci_single_edge() {
240        // deg=[1,1], ecc=[1,1] → ξ = 1·1 + 1·1 = 2
241        assert_eq!(eccentric_connectivity_index(&single_edge()).unwrap(), 2);
242    }
243
244    #[test]
245    fn eci_path3() {
246        // deg=[1,2,1], ecc=[2,1,2] → ξ = 2 + 2 + 2 = 6
247        assert_eq!(eccentric_connectivity_index(&path3()).unwrap(), 6);
248    }
249
250    #[test]
251    fn eci_path4() {
252        // deg=[1,2,2,1], ecc=[3,2,2,3]
253        // ξ = 1·3 + 2·2 + 2·2 + 1·3 = 3+4+4+3 = 14
254        assert_eq!(eccentric_connectivity_index(&path4()).unwrap(), 14);
255    }
256
257    #[test]
258    fn eci_path5() {
259        // deg=[1,2,2,2,1], ecc=[4,3,2,3,4]
260        // ξ = 4 + 6 + 4 + 6 + 4 = 24
261        assert_eq!(eccentric_connectivity_index(&path5()).unwrap(), 24);
262    }
263
264    #[test]
265    fn eci_k3() {
266        // deg=[2,2,2], ecc=[1,1,1] → ξ = 6
267        assert_eq!(eccentric_connectivity_index(&k3()).unwrap(), 6);
268    }
269
270    #[test]
271    fn eci_k4() {
272        // deg=[3,3,3,3], ecc=[1,1,1,1] → ξ = 12
273        assert_eq!(eccentric_connectivity_index(&k4()).unwrap(), 12);
274    }
275
276    #[test]
277    fn eci_cycle4() {
278        // deg=[2,2,2,2], ecc=[2,2,2,2] → ξ = 16
279        assert_eq!(eccentric_connectivity_index(&cycle4()).unwrap(), 16);
280    }
281
282    #[test]
283    fn eci_cycle5() {
284        // deg=[2,2,2,2,2], ecc=[2,2,2,2,2] → ξ = 20
285        assert_eq!(eccentric_connectivity_index(&cycle5()).unwrap(), 20);
286    }
287
288    #[test]
289    fn eci_star5() {
290        // deg=[4,1,1,1,1], ecc=[1,2,2,2,2]
291        // ξ = 4·1 + 1·2 + 1·2 + 1·2 + 1·2 = 4+8 = 12
292        assert_eq!(eccentric_connectivity_index(&star5()).unwrap(), 12);
293    }
294
295    #[test]
296    fn eci_with_isolated() {
297        // 0-1 plus isolated 2: deg=[1,1,0], ecc=[1,1,0]
298        // ξ = 1 + 1 + 0 = 2
299        let g = Graph::from_edges(&[(0, 1)], false, Some(3)).unwrap();
300        assert_eq!(eccentric_connectivity_index(&g).unwrap(), 2);
301    }
302
303    #[test]
304    fn eci_two_components() {
305        // 0-1 and 2-3
306        // deg=[1,1,1,1], ecc=[1,1,1,1]
307        // ξ = 4
308        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
309        assert_eq!(eccentric_connectivity_index(&g).unwrap(), 4);
310    }
311
312    #[test]
313    fn eci_complete_formula() {
314        // K_n: all deg=n-1, all ecc=1 → ξ = n(n-1)
315        for n in 2_u32..=6 {
316            let edges: Vec<(u32, u32)> = (0..n)
317                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
318                .collect();
319            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
320            assert_eq!(
321                eccentric_connectivity_index(&g).unwrap(),
322                u64::from(n) * u64::from(n - 1)
323            );
324        }
325    }
326
327    #[test]
328    fn eci_diamond() {
329        // K4 minus (2,3): edges 0-1,0-2,0-3,1-2,1-3
330        // deg=[3,3,2,2], ecc=[1,1,2,2]
331        // ξ = 3+3+4+4 = 14
332        let g =
333            Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)], false, Some(4)).unwrap();
334        assert_eq!(eccentric_connectivity_index(&g).unwrap(), 14);
335    }
336
337    // --- total_eccentricity ---
338
339    #[test]
340    fn te_empty() {
341        let g = Graph::with_vertices(0);
342        assert_eq!(total_eccentricity(&g).unwrap(), 0);
343    }
344
345    #[test]
346    fn te_single_vertex() {
347        let g = Graph::with_vertices(1);
348        assert_eq!(total_eccentricity(&g).unwrap(), 0);
349    }
350
351    #[test]
352    fn te_single_edge() {
353        assert_eq!(total_eccentricity(&single_edge()).unwrap(), 2);
354    }
355
356    #[test]
357    fn te_path3() {
358        // ecc=[2,1,2] → 5
359        assert_eq!(total_eccentricity(&path3()).unwrap(), 5);
360    }
361
362    #[test]
363    fn te_path4() {
364        // ecc=[3,2,2,3] → 10
365        assert_eq!(total_eccentricity(&path4()).unwrap(), 10);
366    }
367
368    #[test]
369    fn te_k3() {
370        // ecc=[1,1,1] → 3
371        assert_eq!(total_eccentricity(&k3()).unwrap(), 3);
372    }
373
374    #[test]
375    fn te_k4() {
376        // ecc=[1,1,1,1] → 4
377        assert_eq!(total_eccentricity(&k4()).unwrap(), 4);
378    }
379
380    #[test]
381    fn te_cycle4() {
382        // ecc=[2,2,2,2] → 8
383        assert_eq!(total_eccentricity(&cycle4()).unwrap(), 8);
384    }
385
386    #[test]
387    fn te_cycle5() {
388        // ecc=[2,2,2,2,2] → 10
389        assert_eq!(total_eccentricity(&cycle5()).unwrap(), 10);
390    }
391
392    #[test]
393    fn te_star5() {
394        // ecc=[1,2,2,2,2] → 9
395        assert_eq!(total_eccentricity(&star5()).unwrap(), 9);
396    }
397
398    #[test]
399    fn te_complete_formula() {
400        // K_n: all ecc=1 → ζ = n
401        for n in 2_u32..=6 {
402            let edges: Vec<(u32, u32)> = (0..n)
403                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
404                .collect();
405            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
406            assert_eq!(total_eccentricity(&g).unwrap(), u64::from(n));
407        }
408    }
409
410    // --- connective_eccentricity_index ---
411
412    #[test]
413    fn cei_empty() {
414        let g = Graph::with_vertices(0);
415        assert!((connective_eccentricity_index(&g).unwrap() - 0.0).abs() < 1e-10);
416    }
417
418    #[test]
419    fn cei_single_vertex() {
420        let g = Graph::with_vertices(1);
421        assert!((connective_eccentricity_index(&g).unwrap() - 0.0).abs() < 1e-10);
422    }
423
424    #[test]
425    fn cei_single_edge() {
426        // deg=[1,1], ecc=[1,1] → ξ^ce = 1+1 = 2
427        assert!((connective_eccentricity_index(&single_edge()).unwrap() - 2.0).abs() < 1e-10);
428    }
429
430    #[test]
431    fn cei_path3() {
432        // deg=[1,2,1], ecc=[2,1,2] → 0.5+2+0.5 = 3
433        assert!((connective_eccentricity_index(&path3()).unwrap() - 3.0).abs() < 1e-10);
434    }
435
436    #[test]
437    fn cei_path4() {
438        // deg=[1,2,2,1], ecc=[3,2,2,3]
439        // 1/3 + 1 + 1 + 1/3 = 8/3
440        let c = connective_eccentricity_index(&path4()).unwrap();
441        assert!((c - 8.0 / 3.0).abs() < 1e-10);
442    }
443
444    #[test]
445    fn cei_k3() {
446        // deg=[2,2,2], ecc=[1,1,1] → 2+2+2 = 6
447        assert!((connective_eccentricity_index(&k3()).unwrap() - 6.0).abs() < 1e-10);
448    }
449
450    #[test]
451    fn cei_k4() {
452        // deg=[3,3,3,3], ecc=[1,1,1,1] → 12
453        assert!((connective_eccentricity_index(&k4()).unwrap() - 12.0).abs() < 1e-10);
454    }
455
456    #[test]
457    fn cei_cycle4() {
458        // deg=[2,2,2,2], ecc=[2,2,2,2] → 1+1+1+1 = 4
459        assert!((connective_eccentricity_index(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
460    }
461
462    #[test]
463    fn cei_star5() {
464        // deg=[4,1,1,1,1], ecc=[1,2,2,2,2]
465        // 4/1 + 1/2 + 1/2 + 1/2 + 1/2 = 6
466        assert!((connective_eccentricity_index(&star5()).unwrap() - 6.0).abs() < 1e-10);
467    }
468
469    #[test]
470    fn cei_with_isolated() {
471        // 0-1 plus isolated 2 (ecc=0, skipped)
472        // deg=[1,1,0], ecc=[1,1,0] → 1+1 = 2
473        let g = Graph::from_edges(&[(0, 1)], false, Some(3)).unwrap();
474        assert!((connective_eccentricity_index(&g).unwrap() - 2.0).abs() < 1e-10);
475    }
476
477    #[test]
478    fn cei_complete_formula() {
479        // K_n: all deg=n-1, all ecc=1 → ξ^ce = n(n-1)
480        for n in 2_u32..=6 {
481            let edges: Vec<(u32, u32)> = (0..n)
482                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
483                .collect();
484            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
485            assert!(
486                (connective_eccentricity_index(&g).unwrap() - f64::from(n) * f64::from(n - 1))
487                    .abs()
488                    < 1e-10
489            );
490        }
491    }
492
493    // --- cross-consistency ---
494
495    #[test]
496    fn eci_geq_total_ecc() {
497        // ξ^c(G) >= ζ(G) since deg(v) >= 1 for non-isolated v
498        // (for connected graphs with n >= 2)
499        for g in &[
500            single_edge(),
501            path3(),
502            path4(),
503            k3(),
504            k4(),
505            cycle4(),
506            star5(),
507        ] {
508            let xi = eccentric_connectivity_index(g).unwrap();
509            let te = total_eccentricity(g).unwrap();
510            assert!(xi >= te, "ξ^c={xi} < ζ={te}");
511        }
512    }
513
514    #[test]
515    fn eci_equals_2m_for_kn() {
516        // For K_n: ξ^c = Σ (n-1)·1 = n(n-1) = 2m
517        for n in 2_u32..=6 {
518            let edges: Vec<(u32, u32)> = (0..n)
519                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
520                .collect();
521            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
522            assert_eq!(
523                eccentric_connectivity_index(&g).unwrap(),
524                2 * g.ecount() as u64
525            );
526        }
527    }
528
529    #[test]
530    fn cei_leq_eci_for_ecc_geq_1() {
531        // ξ^ce(G) <= ξ^c(G) when all ecc >= 1 (deg/ecc <= deg·ecc)
532        for g in &[
533            single_edge(),
534            path3(),
535            path4(),
536            k3(),
537            k4(),
538            cycle4(),
539            star5(),
540        ] {
541            let xi = eccentric_connectivity_index(g).unwrap() as f64;
542            let ce = connective_eccentricity_index(g).unwrap();
543            assert!(ce <= xi + 1e-10, "ξ^ce={ce} > ξ^c={xi}");
544        }
545    }
546
547    #[test]
548    fn eci_path_formula() {
549        // For P_n (n >= 2): ecc(i) = max(i, n-1-i) for i=0..n-1
550        // Verify against direct formula
551        for n in 2_u32..=8 {
552            let edges: Vec<(u32, u32)> = (0..n - 1).map(|i| (i, i + 1)).collect();
553            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
554
555            let mut expected: u64 = 0;
556            for i in 0..n {
557                let ecc = std::cmp::max(i, n - 1 - i);
558                let deg = if i == 0 || i == n - 1 { 1_u64 } else { 2_u64 };
559                expected += deg * u64::from(ecc);
560            }
561            assert_eq!(eccentric_connectivity_index(&g).unwrap(), expected);
562        }
563    }
564
565    #[test]
566    fn te_path_formula() {
567        // For P_n: total ecc = Σ max(i, n-1-i)
568        for n in 2_u32..=8 {
569            let edges: Vec<(u32, u32)> = (0..n - 1).map(|i| (i, i + 1)).collect();
570            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
571
572            let mut expected: u64 = 0;
573            for i in 0..n {
574                expected += u64::from(std::cmp::max(i, n - 1 - i));
575            }
576            assert_eq!(total_eccentricity(&g).unwrap(), expected);
577        }
578    }
579}