Skip to main content

rust_igraph/algorithms/properties/
general_randic.rs

1//! General Randić index and harmonic variants (ALGO-TR-050).
2//!
3//! - **General Randić index** `R_α(G) = Σ_{(u,v)∈E} (d_u · d_v)^α`
4//!   The classical Randić index is the special case α = −½.
5//!   The second Zagreb index is α = 1. The general form was introduced
6//!   by Bollobás & Erdős (1998).
7//! - **General sum-connectivity index** `χ_α(G) = Σ_{(u,v)∈E} (d_u + d_v)^α`
8//!   Generalisation of the sum-connectivity index (α = −½) and the
9//!   first Zagreb index (α = 1). Introduced by Zhou & Trinajstić (2010).
10//! - **Reciprocal Randić index** `RR(G) = Σ_{(u,v)∈E} √(d_u · d_v)`
11//!   The Randić index with α = +½ instead of −½. Measures branching
12//!   in the opposite direction.
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
24/// Compute the general Randić index `R_α(G)`.
25///
26/// `R_α(G) = Σ_{(u,v)∈E} (d_u · d_v)^α`
27///
28/// Special cases: α = −0.5 gives the classical Randić index;
29/// α = 1 gives the second Zagreb index `M₂`.
30///
31/// Self-loops and edges with `d_u · d_v = 0` are skipped.
32///
33/// # Examples
34///
35/// ```
36/// use rust_igraph::{Graph, general_randic_index};
37///
38/// // K_3: all degrees 2, α = 1 → 3 · (2·2)^1 = 12
39/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
40/// assert!((general_randic_index(&g, 1.0).unwrap() - 12.0).abs() < 1e-10);
41/// ```
42pub fn general_randic_index(graph: &Graph, alpha: f64) -> IgraphResult<f64> {
43    let n = graph.vcount();
44    if n == 0 {
45        return Ok(0.0);
46    }
47
48    let mut r = 0.0_f64;
49
50    for (u, v) in graph.edges() {
51        if u == v {
52            continue;
53        }
54        let du = graph.degree(u)? as f64;
55        let dv = graph.degree(v)? as f64;
56        let prod = du * dv;
57        if prod <= 0.0 {
58            continue;
59        }
60        r += prod.powf(alpha);
61    }
62
63    Ok(r)
64}
65
66/// Compute the general sum-connectivity index `χ_α(G)`.
67///
68/// `χ_α(G) = Σ_{(u,v)∈E} (d_u + d_v)^α`
69///
70/// Special cases: α = −0.5 gives the sum-connectivity index;
71/// α = 1 gives the first Zagreb index `M₁`.
72///
73/// Self-loops and edges with `d_u + d_v = 0` are skipped.
74///
75/// # Examples
76///
77/// ```
78/// use rust_igraph::{Graph, general_sum_connectivity_index};
79///
80/// // K_3: all degrees 2, α = 1 → 3 · (2+2)^1 = 12
81/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
82/// assert!((general_sum_connectivity_index(&g, 1.0).unwrap() - 12.0).abs() < 1e-10);
83/// ```
84pub fn general_sum_connectivity_index(graph: &Graph, alpha: f64) -> IgraphResult<f64> {
85    let n = graph.vcount();
86    if n == 0 {
87        return Ok(0.0);
88    }
89
90    let mut chi = 0.0_f64;
91
92    for (u, v) in graph.edges() {
93        if u == v {
94            continue;
95        }
96        let du = graph.degree(u)? as f64;
97        let dv = graph.degree(v)? as f64;
98        let sum_d = du + dv;
99        if sum_d <= 0.0 {
100            continue;
101        }
102        chi += sum_d.powf(alpha);
103    }
104
105    Ok(chi)
106}
107
108/// Compute the reciprocal Randić index.
109///
110/// `RR(G) = Σ_{(u,v)∈E} √(d_u · d_v)`
111///
112/// This is `R_{+½}(G)`, the general Randić index with α = +½.
113///
114/// # Examples
115///
116/// ```
117/// use rust_igraph::{Graph, reciprocal_randic_index};
118///
119/// // K_3: all degrees 2 → 3·√(2·2) = 3·2 = 6
120/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
121/// assert!((reciprocal_randic_index(&g).unwrap() - 6.0).abs() < 1e-10);
122/// ```
123pub fn reciprocal_randic_index(graph: &Graph) -> IgraphResult<f64> {
124    let n = graph.vcount();
125    if n == 0 {
126        return Ok(0.0);
127    }
128
129    let mut rr = 0.0_f64;
130
131    for (u, v) in graph.edges() {
132        if u == v {
133            continue;
134        }
135        let du = graph.degree(u)? as f64;
136        let dv = graph.degree(v)? as f64;
137        let prod = du * dv;
138        if prod <= 0.0 {
139            continue;
140        }
141        rr += prod.sqrt();
142    }
143
144    Ok(rr)
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    fn single_edge() -> Graph {
152        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
153    }
154
155    fn path3() -> Graph {
156        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
157    }
158
159    fn path4() -> Graph {
160        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
161    }
162
163    fn k3() -> Graph {
164        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
165    }
166
167    fn k4() -> Graph {
168        Graph::from_edges(
169            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
170            false,
171            Some(4),
172        )
173        .unwrap()
174    }
175
176    fn cycle4() -> Graph {
177        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
178    }
179
180    fn cycle5() -> Graph {
181        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
182    }
183
184    fn star5() -> Graph {
185        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
186    }
187
188    fn paw() -> Graph {
189        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
190    }
191
192    // --- general_randic_index ---
193
194    #[test]
195    fn gr_empty() {
196        let g = Graph::with_vertices(0);
197        assert!((general_randic_index(&g, -0.5).unwrap()).abs() < 1e-10);
198    }
199
200    #[test]
201    fn gr_single_vertex() {
202        let g = Graph::with_vertices(1);
203        assert!((general_randic_index(&g, 1.0).unwrap()).abs() < 1e-10);
204    }
205
206    #[test]
207    fn gr_single_edge() {
208        // α=1: (1·1)^1 = 1
209        assert!((general_randic_index(&single_edge(), 1.0).unwrap() - 1.0).abs() < 1e-10);
210    }
211
212    #[test]
213    fn gr_alpha_neg_half_is_randic() {
214        // classical Randić: Σ 1/√(d_u·d_v)
215        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
216            let gr = general_randic_index(g, -0.5).unwrap();
217            let mut expected = 0.0_f64;
218            for (u, v) in g.edges() {
219                if u == v {
220                    continue;
221                }
222                let du = g.degree(u).unwrap() as f64;
223                let dv = g.degree(v).unwrap() as f64;
224                expected += 1.0 / (du * dv).sqrt();
225            }
226            assert!((gr - expected).abs() < 1e-8);
227        }
228    }
229
230    #[test]
231    fn gr_alpha_1_is_second_zagreb() {
232        // α=1: Σ d_u·d_v = M₂
233        for g in &[single_edge(), path3(), k3(), k4(), star5()] {
234            let gr = general_randic_index(g, 1.0).unwrap();
235            let mut m2 = 0.0_f64;
236            for (u, v) in g.edges() {
237                if u == v {
238                    continue;
239                }
240                let du = g.degree(u).unwrap() as f64;
241                let dv = g.degree(v).unwrap() as f64;
242                m2 += du * dv;
243            }
244            assert!((gr - m2).abs() < 1e-8);
245        }
246    }
247
248    #[test]
249    fn gr_alpha_0_equals_m() {
250        // α=0: (d_u·d_v)^0 = 1 per edge → sum = m
251        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
252            let gr = general_randic_index(g, 0.0).unwrap();
253            assert!((gr - g.ecount() as f64).abs() < 1e-10);
254        }
255    }
256
257    #[test]
258    fn gr_k3() {
259        // α=1: 3·(2·2) = 12
260        assert!((general_randic_index(&k3(), 1.0).unwrap() - 12.0).abs() < 1e-10);
261    }
262
263    #[test]
264    fn gr_k4() {
265        // α=1: 6·(3·3) = 54
266        assert!((general_randic_index(&k4(), 1.0).unwrap() - 54.0).abs() < 1e-10);
267    }
268
269    #[test]
270    fn gr_star5_alpha1() {
271        // center=4, leaf=1: 4·(4·1)=16
272        assert!((general_randic_index(&star5(), 1.0).unwrap() - 16.0).abs() < 1e-10);
273    }
274
275    #[test]
276    fn gr_path3_alpha2() {
277        // (0,1): (1·2)²=4, (1,2): (2·1)²=4 → 8
278        assert!((general_randic_index(&path3(), 2.0).unwrap() - 8.0).abs() < 1e-10);
279    }
280
281    #[test]
282    fn gr_regular_formula() {
283        // r-regular: R_α = m · (r²)^α = m · r^(2α)
284        for g in &[k3(), k4(), cycle4(), cycle5()] {
285            let m = g.ecount() as f64;
286            let r = g.degree(0).unwrap() as f64;
287            for &alpha in &[-0.5_f64, 0.0, 0.5, 1.0, 2.0] {
288                let expected = m * r.powf(2.0 * alpha);
289                let actual = general_randic_index(g, alpha).unwrap();
290                assert!(
291                    (actual - expected).abs() < 1e-6,
292                    "alpha={alpha}, expected={expected}, got={actual}"
293                );
294            }
295        }
296    }
297
298    // --- general_sum_connectivity_index ---
299
300    #[test]
301    fn gs_empty() {
302        let g = Graph::with_vertices(0);
303        assert!((general_sum_connectivity_index(&g, -0.5).unwrap()).abs() < 1e-10);
304    }
305
306    #[test]
307    fn gs_alpha_neg_half_is_sci() {
308        // χ_{-½} = Σ 1/√(d_u+d_v) = SCI
309        for g in &[single_edge(), path3(), k3(), star5()] {
310            let gs = general_sum_connectivity_index(g, -0.5).unwrap();
311            let mut expected = 0.0_f64;
312            for (u, v) in g.edges() {
313                if u == v {
314                    continue;
315                }
316                let du = g.degree(u).unwrap() as f64;
317                let dv = g.degree(v).unwrap() as f64;
318                expected += 1.0 / (du + dv).sqrt();
319            }
320            assert!((gs - expected).abs() < 1e-8);
321        }
322    }
323
324    #[test]
325    fn gs_alpha_1_is_first_zagreb() {
326        // χ_1 = Σ (d_u+d_v) = M₁
327        for g in &[single_edge(), path3(), k3(), k4(), star5()] {
328            let gs = general_sum_connectivity_index(g, 1.0).unwrap();
329            let mut m1 = 0.0_f64;
330            for (u, v) in g.edges() {
331                if u == v {
332                    continue;
333                }
334                let du = g.degree(u).unwrap() as f64;
335                let dv = g.degree(v).unwrap() as f64;
336                m1 += du + dv;
337            }
338            assert!((gs - m1).abs() < 1e-8);
339        }
340    }
341
342    #[test]
343    fn gs_alpha_0_equals_m() {
344        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
345            let gs = general_sum_connectivity_index(g, 0.0).unwrap();
346            assert!((gs - g.ecount() as f64).abs() < 1e-10);
347        }
348    }
349
350    #[test]
351    fn gs_k3_alpha1() {
352        // 3·(2+2) = 12
353        assert!((general_sum_connectivity_index(&k3(), 1.0).unwrap() - 12.0).abs() < 1e-10);
354    }
355
356    #[test]
357    fn gs_k4_alpha1() {
358        // 6·(3+3) = 36
359        assert!((general_sum_connectivity_index(&k4(), 1.0).unwrap() - 36.0).abs() < 1e-10);
360    }
361
362    #[test]
363    fn gs_star5_alpha1() {
364        // 4·(4+1) = 20
365        assert!((general_sum_connectivity_index(&star5(), 1.0).unwrap() - 20.0).abs() < 1e-10);
366    }
367
368    #[test]
369    fn gs_regular_formula() {
370        // r-regular: χ_α = m · (2r)^α
371        for g in &[k3(), k4(), cycle4(), cycle5()] {
372            let m = g.ecount() as f64;
373            let r = g.degree(0).unwrap() as f64;
374            for &alpha in &[-0.5_f64, 0.0, 0.5, 1.0, 2.0] {
375                let expected = m * (2.0 * r).powf(alpha);
376                let actual = general_sum_connectivity_index(g, alpha).unwrap();
377                assert!((actual - expected).abs() < 1e-6);
378            }
379        }
380    }
381
382    // --- reciprocal_randic_index ---
383
384    #[test]
385    fn rr_empty() {
386        let g = Graph::with_vertices(0);
387        assert!((reciprocal_randic_index(&g).unwrap()).abs() < 1e-10);
388    }
389
390    #[test]
391    fn rr_single_edge() {
392        // √(1·1) = 1
393        assert!((reciprocal_randic_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
394    }
395
396    #[test]
397    fn rr_path3() {
398        // (0,1):√2, (1,2):√2 → 2√2
399        let expected = 2.0 * 2.0_f64.sqrt();
400        assert!((reciprocal_randic_index(&path3()).unwrap() - expected).abs() < 1e-10);
401    }
402
403    #[test]
404    fn rr_k3() {
405        // 3·√4 = 6
406        assert!((reciprocal_randic_index(&k3()).unwrap() - 6.0).abs() < 1e-10);
407    }
408
409    #[test]
410    fn rr_k4() {
411        // 6·√9 = 18
412        assert!((reciprocal_randic_index(&k4()).unwrap() - 18.0).abs() < 1e-10);
413    }
414
415    #[test]
416    fn rr_star5() {
417        // 4·√4 = 8
418        assert!((reciprocal_randic_index(&star5()).unwrap() - 8.0).abs() < 1e-10);
419    }
420
421    #[test]
422    fn rr_cycle4() {
423        // 4·√4 = 8
424        assert!((reciprocal_randic_index(&cycle4()).unwrap() - 8.0).abs() < 1e-10);
425    }
426
427    #[test]
428    fn rr_equals_gr_half() {
429        // RR = R_{+½}
430        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
431            let rr = reciprocal_randic_index(g).unwrap();
432            let gr = general_randic_index(g, 0.5).unwrap();
433            assert!((rr - gr).abs() < 1e-8);
434        }
435    }
436
437    #[test]
438    fn rr_regular_formula() {
439        // r-regular: RR = m·r
440        for g in &[k3(), k4(), cycle4(), cycle5()] {
441            let m = g.ecount() as f64;
442            let r = g.degree(0).unwrap() as f64;
443            assert!((reciprocal_randic_index(g).unwrap() - m * r).abs() < 1e-8);
444        }
445    }
446
447    // --- cross-consistency ---
448
449    #[test]
450    fn all_positive_for_connected() {
451        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
452            assert!(general_randic_index(g, 1.0).unwrap() > 0.0);
453            assert!(general_sum_connectivity_index(g, 1.0).unwrap() > 0.0);
454            assert!(reciprocal_randic_index(g).unwrap() > 0.0);
455        }
456    }
457
458    #[test]
459    fn gr_geq_rr_for_alpha_geq_half() {
460        // R_1 ≥ R_{½} since (d_u·d_v)^1 ≥ (d_u·d_v)^{½} when d_u·d_v ≥ 1
461        for g in &[single_edge(), path3(), k3(), k4(), star5()] {
462            let r1 = general_randic_index(g, 1.0).unwrap();
463            let rr = reciprocal_randic_index(g).unwrap();
464            assert!(r1 >= rr - 1e-8);
465        }
466    }
467
468    #[test]
469    fn paw_alpha1() {
470        // degrees [2,2,3,1]
471        // R_1: (0,1):4, (0,2):6, (1,2):6, (2,3):3 → 19
472        assert!((general_randic_index(&paw(), 1.0).unwrap() - 19.0).abs() < 1e-10);
473    }
474
475    #[test]
476    fn path4_alpha1() {
477        // degrees [1,2,2,1]
478        // R_1: (0,1):2, (1,2):4, (2,3):2 → 8
479        assert!((general_randic_index(&path4(), 1.0).unwrap() - 8.0).abs() < 1e-10);
480    }
481}