Skip to main content

rust_igraph/algorithms/properties/
abc_variants.rs

1//! Atom-bond connectivity variants (ALGO-TR-055).
2//!
3//! Extended versions of the atom-bond connectivity index.
4//!
5//! - **Fourth ABC index** `ABC₄(G) = Σ_{(u,v)∈E} √((ε(u)+ε(v)-2) / (ε(u)·ε(v)))`
6//!   where `ε(v)` is the eccentricity of v. Introduced by Ghorbani & Hosseinzadeh (2010).
7//! - **Fifth geometric-arithmetic index** `GA₅(G) = Σ_{(u,v)∈E} 2√(ε(u)·ε(v)) / (ε(u)+ε(v))`
8//!   Uses eccentricities instead of degrees. Introduced by Graovac et al. (2011).
9//! - **Degree-sum index** `DS(G) = Σ_{(u,v)∈E} √(d(u)+d(v))`
10//!   Simple edge-weight using the square root of the degree sum.
11
12#![allow(
13    clippy::cast_possible_truncation,
14    clippy::cast_precision_loss,
15    clippy::many_single_char_names,
16    clippy::needless_range_loop,
17    clippy::too_many_lines
18)]
19
20use crate::core::{Graph, IgraphResult};
21
22fn eccentricities(graph: &Graph) -> IgraphResult<Vec<u32>> {
23    let n = graph.vcount() as usize;
24    let mut ecc = vec![0_u32; n];
25
26    for s in 0..n {
27        let mut dist = vec![u32::MAX; n];
28        dist[s] = 0;
29        let mut queue = std::collections::VecDeque::new();
30        queue.push_back(s as u32);
31        while let Some(u) = queue.pop_front() {
32            let d_u = dist[u as usize];
33            for nb in graph.neighbors(u)? {
34                let idx = nb as usize;
35                if dist[idx] == u32::MAX {
36                    dist[idx] = d_u + 1;
37                    queue.push_back(nb);
38                }
39            }
40        }
41        let mut max_d = 0_u32;
42        for &d in &dist {
43            if d != u32::MAX && d > max_d {
44                max_d = d;
45            }
46        }
47        ecc[s] = max_d;
48    }
49
50    Ok(ecc)
51}
52
53/// Compute the fourth atom-bond connectivity index.
54///
55/// `ABC₄(G) = Σ_{(u,v)∈E} √((ε(u)+ε(v)-2) / (ε(u)·ε(v)))`
56///
57/// where `ε(v)` is the eccentricity of vertex v. Self-loops and
58/// edges where `ε(u)·ε(v) = 0` are skipped.
59///
60/// # Examples
61///
62/// ```
63/// use rust_igraph::{Graph, fourth_abc_index};
64///
65/// // Path 0-1-2: eccentricities [2,1,2]
66/// // (0,1): √((2+1-2)/(2·1)) = √(1/2) = 1/√2
67/// // (1,2): √((1+2-2)/(1·2)) = √(1/2) = 1/√2
68/// // ABC₄ = 2/√2 = √2
69/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
70/// assert!((fourth_abc_index(&g).unwrap() - std::f64::consts::SQRT_2).abs() < 1e-10);
71/// ```
72pub fn fourth_abc_index(graph: &Graph) -> IgraphResult<f64> {
73    let n = graph.vcount();
74    if n == 0 {
75        return Ok(0.0);
76    }
77
78    let ecc = eccentricities(graph)?;
79    let mut abc4 = 0.0_f64;
80
81    for (u, v) in graph.edges() {
82        if u == v {
83            continue;
84        }
85        let eu = f64::from(ecc[u as usize]);
86        let ev = f64::from(ecc[v as usize]);
87        let prod = eu * ev;
88        if prod <= 0.0 {
89            continue;
90        }
91        let numer = eu + ev - 2.0;
92        if numer < 0.0 {
93            continue;
94        }
95        abc4 += (numer / prod).sqrt();
96    }
97
98    Ok(abc4)
99}
100
101/// Compute the fifth geometric-arithmetic index.
102///
103/// `GA₅(G) = Σ_{(u,v)∈E} 2√(ε(u)·ε(v)) / (ε(u)+ε(v))`
104///
105/// where `ε(v)` is the eccentricity. Self-loops and edges where
106/// `ε(u)+ε(v) = 0` are skipped.
107///
108/// # Examples
109///
110/// ```
111/// use rust_igraph::{Graph, fifth_ga_index};
112///
113/// // K_3: all eccentricities = 1
114/// // Each edge: 2√(1·1)/(1+1) = 1, 3 edges → GA₅ = 3
115/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
116/// assert!((fifth_ga_index(&g).unwrap() - 3.0).abs() < 1e-10);
117/// ```
118pub fn fifth_ga_index(graph: &Graph) -> IgraphResult<f64> {
119    let n = graph.vcount();
120    if n == 0 {
121        return Ok(0.0);
122    }
123
124    let ecc = eccentricities(graph)?;
125    let mut ga5 = 0.0_f64;
126
127    for (u, v) in graph.edges() {
128        if u == v {
129            continue;
130        }
131        let eu = f64::from(ecc[u as usize]);
132        let ev = f64::from(ecc[v as usize]);
133        let denom = eu + ev;
134        if denom <= 0.0 {
135            continue;
136        }
137        ga5 += 2.0 * (eu * ev).sqrt() / denom;
138    }
139
140    Ok(ga5)
141}
142
143/// Compute the degree-sum index.
144///
145/// `DS(G) = Σ_{(u,v)∈E} √(d(u) + d(v))`
146///
147/// Self-loops are skipped.
148///
149/// # Examples
150///
151/// ```
152/// use rust_igraph::{Graph, degree_sum_index};
153///
154/// // K_3: each edge √(2+2) = 2, 3 edges → DS = 6
155/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
156/// assert!((degree_sum_index(&g).unwrap() - 6.0).abs() < 1e-10);
157/// ```
158pub fn degree_sum_index(graph: &Graph) -> IgraphResult<f64> {
159    let mut ds = 0.0_f64;
160
161    for (u, v) in graph.edges() {
162        if u == v {
163            continue;
164        }
165        let du = graph.degree(u)? as f64;
166        let dv = graph.degree(v)? as f64;
167        ds += (du + dv).sqrt();
168    }
169
170    Ok(ds)
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 k3() -> Graph {
186        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
187    }
188
189    fn k4() -> Graph {
190        Graph::from_edges(
191            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
192            false,
193            Some(4),
194        )
195        .unwrap()
196    }
197
198    fn cycle4() -> Graph {
199        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
200    }
201
202    fn cycle5() -> Graph {
203        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
204    }
205
206    fn star5() -> Graph {
207        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
208    }
209
210    fn paw() -> Graph {
211        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
212    }
213
214    // --- eccentricities helper ---
215
216    #[test]
217    fn ecc_path3() {
218        let ecc = eccentricities(&path3()).unwrap();
219        assert_eq!(ecc, vec![2, 1, 2]);
220    }
221
222    #[test]
223    fn ecc_k3() {
224        let ecc = eccentricities(&k3()).unwrap();
225        assert_eq!(ecc, vec![1, 1, 1]);
226    }
227
228    #[test]
229    fn ecc_k4() {
230        let ecc = eccentricities(&k4()).unwrap();
231        assert_eq!(ecc, vec![1, 1, 1, 1]);
232    }
233
234    #[test]
235    fn ecc_cycle4() {
236        let ecc = eccentricities(&cycle4()).unwrap();
237        assert_eq!(ecc, vec![2, 2, 2, 2]);
238    }
239
240    #[test]
241    fn ecc_star5() {
242        let ecc = eccentricities(&star5()).unwrap();
243        assert_eq!(ecc[0], 1);
244        for i in 1..5 {
245            assert_eq!(ecc[i], 2);
246        }
247    }
248
249    // --- fourth_abc_index ---
250
251    #[test]
252    fn abc4_empty() {
253        let g = Graph::with_vertices(0);
254        assert!((fourth_abc_index(&g).unwrap()).abs() < 1e-10);
255    }
256
257    #[test]
258    fn abc4_single_edge() {
259        // eccentricities [1,1], (1+1-2)/(1·1) = 0 → √0 = 0
260        assert!((fourth_abc_index(&single_edge()).unwrap()).abs() < 1e-10);
261    }
262
263    #[test]
264    fn abc4_path3() {
265        // ecc [2,1,2]
266        // (0,1): √((2+1-2)/(2·1)) = √(1/2)
267        // (1,2): √((1+2-2)/(1·2)) = √(1/2)
268        // ABC₄ = 2·√(1/2) = √2
269        let expected = std::f64::consts::SQRT_2;
270        assert!((fourth_abc_index(&path3()).unwrap() - expected).abs() < 1e-10);
271    }
272
273    #[test]
274    fn abc4_k3() {
275        // ecc [1,1,1]
276        // each edge: √((1+1-2)/(1·1)) = 0
277        assert!((fourth_abc_index(&k3()).unwrap()).abs() < 1e-10);
278    }
279
280    #[test]
281    fn abc4_k4() {
282        // ecc [1,1,1,1], same as K_3
283        assert!((fourth_abc_index(&k4()).unwrap()).abs() < 1e-10);
284    }
285
286    #[test]
287    fn abc4_cycle4() {
288        // ecc [2,2,2,2]
289        // each edge: √((2+2-2)/(2·2)) = √(2/4) = √(1/2)
290        // 4 edges → 4·√(1/2) = 4/√2 = 2√2
291        let expected = 2.0 * std::f64::consts::SQRT_2;
292        assert!((fourth_abc_index(&cycle4()).unwrap() - expected).abs() < 1e-10);
293    }
294
295    #[test]
296    fn abc4_cycle5() {
297        // ecc [2,2,2,2,2]
298        // each edge: √((2+2-2)/(2·2)) = √(1/2)
299        // 5 edges → 5/√2
300        let expected = 5.0 / std::f64::consts::SQRT_2;
301        assert!((fourth_abc_index(&cycle5()).unwrap() - expected).abs() < 1e-10);
302    }
303
304    #[test]
305    fn abc4_star5() {
306        // ecc [1,2,2,2,2]
307        // (0,leaf): √((1+2-2)/(1·2)) = √(1/2)
308        // 4 edges → 4/√2 = 2√2
309        let expected = 2.0 * std::f64::consts::SQRT_2;
310        assert!((fourth_abc_index(&star5()).unwrap() - expected).abs() < 1e-10);
311    }
312
313    #[test]
314    fn abc4_paw() {
315        // ecc: v0→max(1,1,2)=2, v1→max(1,1,2)=2, v2→max(1,1,1)=1, v3→max(2,2,1)=2
316        let ecc = eccentricities(&paw()).unwrap();
317        assert_eq!(ecc, vec![2, 2, 1, 2]);
318        // (0,1): √((2+2-2)/(2·2)) = √(1/2)
319        // (0,2): √((2+1-2)/(2·1)) = √(1/2)
320        // (1,2): √((2+1-2)/(2·1)) = √(1/2)
321        // (2,3): √((1+2-2)/(1·2)) = √(1/2)
322        // ABC₄ = 4/√2 = 2√2
323        let expected = 2.0 * std::f64::consts::SQRT_2;
324        assert!((fourth_abc_index(&paw()).unwrap() - expected).abs() < 1e-10);
325    }
326
327    // --- fifth_ga_index ---
328
329    #[test]
330    fn ga5_empty() {
331        let g = Graph::with_vertices(0);
332        assert!((fifth_ga_index(&g).unwrap()).abs() < 1e-10);
333    }
334
335    #[test]
336    fn ga5_single_edge() {
337        // ecc [1,1]: 2√1/(1+1) = 1
338        assert!((fifth_ga_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
339    }
340
341    #[test]
342    fn ga5_path3() {
343        // ecc [2,1,2]
344        // (0,1): 2√(2·1)/3 = 2√2/3
345        // (1,2): 2√(1·2)/3 = 2√2/3
346        // GA₅ = 4√2/3
347        let expected = 4.0 * std::f64::consts::SQRT_2 / 3.0;
348        assert!((fifth_ga_index(&path3()).unwrap() - expected).abs() < 1e-10);
349    }
350
351    #[test]
352    fn ga5_k3() {
353        // ecc [1,1,1]: each edge 2·1/2 = 1, 3 edges → 3
354        assert!((fifth_ga_index(&k3()).unwrap() - 3.0).abs() < 1e-10);
355    }
356
357    #[test]
358    fn ga5_k4() {
359        // ecc [1,1,1,1]: each edge 1, 6 edges → 6
360        assert!((fifth_ga_index(&k4()).unwrap() - 6.0).abs() < 1e-10);
361    }
362
363    #[test]
364    fn ga5_cycle4() {
365        // ecc [2,2,2,2]: each edge 2·2/4 = 1, 4 edges → 4
366        assert!((fifth_ga_index(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
367    }
368
369    #[test]
370    fn ga5_cycle5() {
371        // ecc [2,2,2,2,2]: each edge 1, 5 edges → 5
372        assert!((fifth_ga_index(&cycle5()).unwrap() - 5.0).abs() < 1e-10);
373    }
374
375    #[test]
376    fn ga5_star5() {
377        // ecc [1,2,2,2,2]
378        // (0,leaf): 2√(1·2)/(1+2) = 2√2/3
379        // 4 edges → 8√2/3
380        let expected = 8.0 * std::f64::consts::SQRT_2 / 3.0;
381        assert!((fifth_ga_index(&star5()).unwrap() - expected).abs() < 1e-10);
382    }
383
384    #[test]
385    fn ga5_equal_ecc_gives_m() {
386        // When all ecc equal: each edge contributes 1 → GA₅ = m
387        for g in &[k3(), k4(), cycle4(), cycle5()] {
388            let m = g.ecount() as f64;
389            assert!((fifth_ga_index(g).unwrap() - m).abs() < 1e-10);
390        }
391    }
392
393    // --- degree_sum_index ---
394
395    #[test]
396    fn ds_empty() {
397        let g = Graph::with_vertices(0);
398        assert!((degree_sum_index(&g).unwrap()).abs() < 1e-10);
399    }
400
401    #[test]
402    fn ds_single_edge() {
403        // √(1+1) = √2
404        let expected = std::f64::consts::SQRT_2;
405        assert!((degree_sum_index(&single_edge()).unwrap() - expected).abs() < 1e-10);
406    }
407
408    #[test]
409    fn ds_path3() {
410        // (0,1): √(1+2)=√3, (1,2): √(2+1)=√3
411        // DS = 2√3
412        let expected = 2.0 * 3.0_f64.sqrt();
413        assert!((degree_sum_index(&path3()).unwrap() - expected).abs() < 1e-10);
414    }
415
416    #[test]
417    fn ds_k3() {
418        // each edge √(2+2)=2, 3 edges → 6
419        assert!((degree_sum_index(&k3()).unwrap() - 6.0).abs() < 1e-10);
420    }
421
422    #[test]
423    fn ds_k4() {
424        // each edge √(3+3)=√6, 6 edges → 6√6
425        let expected = 6.0 * 6.0_f64.sqrt();
426        assert!((degree_sum_index(&k4()).unwrap() - expected).abs() < 1e-10);
427    }
428
429    #[test]
430    fn ds_cycle4() {
431        // each edge √4=2, 4 edges → 8
432        assert!((degree_sum_index(&cycle4()).unwrap() - 8.0).abs() < 1e-10);
433    }
434
435    #[test]
436    fn ds_star5() {
437        // each edge √(4+1)=√5, 4 edges → 4√5
438        let expected = 4.0 * 5.0_f64.sqrt();
439        assert!((degree_sum_index(&star5()).unwrap() - expected).abs() < 1e-10);
440    }
441
442    #[test]
443    fn ds_paw() {
444        // degrees [2,2,3,1]
445        // (0,1):√4=2, (0,2):√5, (1,2):√5, (2,3):√4=2
446        // DS = 4 + 2√5
447        let expected = 4.0 + 2.0 * 5.0_f64.sqrt();
448        assert!((degree_sum_index(&paw()).unwrap() - expected).abs() < 1e-10);
449    }
450
451    #[test]
452    fn ds_regular_formula() {
453        // r-regular: DS = m·√(2r)
454        for g in &[k3(), k4(), cycle4(), cycle5()] {
455            let m = g.ecount() as f64;
456            let r = g.degree(0).unwrap() as f64;
457            let expected = m * (2.0 * r).sqrt();
458            assert!((degree_sum_index(g).unwrap() - expected).abs() < 1e-8);
459        }
460    }
461
462    // --- cross-consistency ---
463
464    #[test]
465    fn all_nonneg() {
466        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
467            assert!(fourth_abc_index(g).unwrap() >= -1e-10);
468            assert!(fifth_ga_index(g).unwrap() >= -1e-10);
469            assert!(degree_sum_index(g).unwrap() >= -1e-10);
470        }
471    }
472
473    #[test]
474    fn ga5_leq_m() {
475        // GA₅ ≤ m (AM-GM inequality: 2√(ab)/(a+b) ≤ 1)
476        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
477            let m = g.ecount() as f64;
478            assert!(fifth_ga_index(g).unwrap() <= m + 1e-10);
479        }
480    }
481}