Skip to main content

rust_igraph/algorithms/properties/
degree_power_indices.rs

1//! Degree-power indices (ALGO-TR-077).
2//!
3//! Bond-additive indices that raise degree products/sums to general
4//! powers, generalizing many classical indices:
5//!
6//! - **General zeroth-order Randić** `⁰R_α(G) = Σ_v d(v)^α`
7//! - **General first Zagreb** `M₁^α(G) = Σ_v d(v)^α` (same as above
8//!   but conventional name uses α=2 for classical M₁)
9//! - **Variable sum exdeg** `SEI_a(G) = Σ_v d(v)·a^{d(v)}` for a>0
10//! - **Inverse degree power** `ID_k(G) = Σ_v 1/d(v)^k` for d(v)>0
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
22/// Compute the general zeroth-order Randić index.
23///
24/// `⁰R_α(G) = Σ_v d(v)^α`
25///
26/// For `α = 2` this equals the first Zagreb index.
27/// For `α = 3` this equals the forgotten index (F-index).
28/// For `α = -1` this equals the inverse degree index.
29/// Vertices with degree 0 are skipped when `α < 0`.
30///
31/// # Examples
32///
33/// ```
34/// use rust_igraph::{Graph, general_zeroth_order_randic};
35///
36/// // K_3: d=(2,2,2), α=2 → 3·4 = 12 (= first Zagreb index)
37/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
38/// assert!((general_zeroth_order_randic(&g, 2.0).unwrap() - 12.0).abs() < 1e-10);
39/// ```
40pub fn general_zeroth_order_randic(graph: &Graph, alpha: f64) -> IgraphResult<f64> {
41    let n = graph.vcount() as usize;
42    let mut result = 0.0_f64;
43
44    for v in 0..n {
45        let d = graph.degree(v as u32)?;
46        if d == 0 {
47            if alpha >= 0.0 {
48                // 0^α = 0 for α > 0, 0^0 = 1 by convention
49                if alpha == 0.0 {
50                    result += 1.0;
51                }
52            }
53            continue;
54        }
55        result += (d as f64).powf(alpha);
56    }
57
58    Ok(result)
59}
60
61/// Compute the variable sum exdeg index.
62///
63/// `SEI_a(G) = Σ_v d(v) · a^{d(v)}` for `a > 0, a ≠ 1`.
64///
65/// Returns 0.0 for empty graphs or when `a ≤ 0`.
66///
67/// # Examples
68///
69/// ```
70/// use rust_igraph::{Graph, variable_sum_exdeg};
71///
72/// // K_3: d=(2,2,2), a=2 → 3·2·4 = 24
73/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
74/// assert!((variable_sum_exdeg(&g, 2.0).unwrap() - 24.0).abs() < 1e-10);
75/// ```
76pub fn variable_sum_exdeg(graph: &Graph, a: f64) -> IgraphResult<f64> {
77    if a <= 0.0 {
78        return Ok(0.0);
79    }
80
81    let n = graph.vcount() as usize;
82    let mut result = 0.0_f64;
83
84    for v in 0..n {
85        let d = graph.degree(v as u32)?;
86        if d == 0 {
87            continue;
88        }
89        result += (d as f64) * a.powf(d as f64);
90    }
91
92    Ok(result)
93}
94
95/// Compute the inverse degree power index.
96///
97/// `ID_k(G) = Σ_v 1/d(v)^k` for `d(v) > 0`.
98///
99/// For `k = 1` this equals the inverse degree index.
100/// Vertices with degree 0 are skipped.
101///
102/// # Examples
103///
104/// ```
105/// use rust_igraph::{Graph, inverse_degree_power};
106///
107/// // K_3: d=(2,2,2), k=2 → 3·(1/4) = 0.75
108/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
109/// assert!((inverse_degree_power(&g, 2.0).unwrap() - 0.75).abs() < 1e-10);
110/// ```
111pub fn inverse_degree_power(graph: &Graph, k: f64) -> IgraphResult<f64> {
112    let n = graph.vcount() as usize;
113    let mut result = 0.0_f64;
114
115    for v in 0..n {
116        let d = graph.degree(v as u32)?;
117        if d == 0 {
118            continue;
119        }
120        result += 1.0 / (d as f64).powf(k);
121    }
122
123    Ok(result)
124}
125
126/// Compute the variable first Zagreb index.
127///
128/// `M₁^{(p)}(G) = Σ_{(u,v)∈E} (du^p + dv^p)` for real `p`.
129///
130/// Generalizes: `p=1` → first Zagreb, `p=2` → forgotten index.
131/// Edges with degree-0 endpoint are skipped when `p < 0`.
132/// Self-loops are skipped.
133///
134/// # Examples
135///
136/// ```
137/// use rust_igraph::{Graph, variable_first_zagreb};
138///
139/// // K_3: 3 edges, d=(2,2), p=1 → 3·(2+2) = 12
140/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
141/// assert!((variable_first_zagreb(&g, 1.0).unwrap() - 12.0).abs() < 1e-10);
142/// ```
143pub fn variable_first_zagreb(graph: &Graph, p: f64) -> IgraphResult<f64> {
144    let mut result = 0.0_f64;
145
146    for (u, v) in graph.edges() {
147        if u == v {
148            continue;
149        }
150        let du = graph.degree(u)? as f64;
151        let dv = graph.degree(v)? as f64;
152        if p < 0.0 && (du == 0.0 || dv == 0.0) {
153            continue;
154        }
155        result += du.powf(p) + dv.powf(p);
156    }
157
158    Ok(result)
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    fn single_edge() -> Graph {
166        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
167    }
168
169    fn path3() -> Graph {
170        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
171    }
172
173    fn k3() -> Graph {
174        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
175    }
176
177    fn k4() -> Graph {
178        Graph::from_edges(
179            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
180            false,
181            Some(4),
182        )
183        .unwrap()
184    }
185
186    fn cycle4() -> Graph {
187        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
188    }
189
190    fn star5() -> Graph {
191        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
192    }
193
194    fn paw() -> Graph {
195        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
196    }
197
198    // --- general_zeroth_order_randic ---
199
200    #[test]
201    fn gzr_empty() {
202        let g = Graph::with_vertices(0);
203        assert!(general_zeroth_order_randic(&g, 2.0).unwrap().abs() < 1e-10);
204    }
205
206    #[test]
207    fn gzr_isolated() {
208        let g = Graph::with_vertices(5);
209        assert!(general_zeroth_order_randic(&g, 2.0).unwrap().abs() < 1e-10);
210    }
211
212    #[test]
213    fn gzr_alpha2_is_first_zagreb() {
214        // α=2: Σ d² = first Zagreb index
215        // K_3: 3·4=12
216        assert!((general_zeroth_order_randic(&k3(), 2.0).unwrap() - 12.0).abs() < 1e-10);
217        // K_4: 4·9=36
218        assert!((general_zeroth_order_randic(&k4(), 2.0).unwrap() - 36.0).abs() < 1e-10);
219    }
220
221    #[test]
222    fn gzr_alpha3_is_forgotten() {
223        // α=3: Σ d³ = forgotten index
224        // K_3: 3·8=24
225        assert!((general_zeroth_order_randic(&k3(), 3.0).unwrap() - 24.0).abs() < 1e-10);
226    }
227
228    #[test]
229    fn gzr_alpha1_is_twice_edges() {
230        // α=1: Σ d = 2m
231        for g in &[k3(), k4(), cycle4(), star5(), paw()] {
232            let expected = 2.0 * g.ecount() as f64;
233            assert!((general_zeroth_order_randic(g, 1.0).unwrap() - expected).abs() < 1e-10);
234        }
235    }
236
237    #[test]
238    fn gzr_alpha_neg1_is_inverse_degree() {
239        // α=-1: Σ 1/d
240        // K_3: 3·(1/2) = 1.5
241        assert!((general_zeroth_order_randic(&k3(), -1.0).unwrap() - 1.5).abs() < 1e-10);
242    }
243
244    #[test]
245    fn gzr_single_edge() {
246        // d=(1,1), α=2: 1+1=2
247        assert!((general_zeroth_order_randic(&single_edge(), 2.0).unwrap() - 2.0).abs() < 1e-10);
248    }
249
250    #[test]
251    fn gzr_star5() {
252        // d=(4,1,1,1,1), α=2: 16+1+1+1+1=20
253        assert!((general_zeroth_order_randic(&star5(), 2.0).unwrap() - 20.0).abs() < 1e-10);
254    }
255
256    // --- variable_sum_exdeg ---
257
258    #[test]
259    fn vse_empty() {
260        let g = Graph::with_vertices(0);
261        assert!(variable_sum_exdeg(&g, 2.0).unwrap().abs() < 1e-10);
262    }
263
264    #[test]
265    fn vse_isolated() {
266        let g = Graph::with_vertices(5);
267        assert!(variable_sum_exdeg(&g, 2.0).unwrap().abs() < 1e-10);
268    }
269
270    #[test]
271    fn vse_k3_a2() {
272        // d=(2,2,2), a=2: 3·2·2²=24
273        assert!((variable_sum_exdeg(&k3(), 2.0).unwrap() - 24.0).abs() < 1e-10);
274    }
275
276    #[test]
277    fn vse_single_edge_a2() {
278        // d=(1,1), a=2: 2·1·2=4
279        assert!((variable_sum_exdeg(&single_edge(), 2.0).unwrap() - 4.0).abs() < 1e-10);
280    }
281
282    #[test]
283    fn vse_path3_a2() {
284        // d=(1,2,1): 1·2 + 2·4 + 1·2 = 2+8+2 = 12
285        assert!((variable_sum_exdeg(&path3(), 2.0).unwrap() - 12.0).abs() < 1e-10);
286    }
287
288    #[test]
289    fn vse_star5_a2() {
290        // d=(4,1,1,1,1): 4·16 + 4·(1·2) = 64+8 = 72
291        assert!((variable_sum_exdeg(&star5(), 2.0).unwrap() - 72.0).abs() < 1e-10);
292    }
293
294    #[test]
295    fn vse_k4_a3() {
296        // d=(3,3,3,3), a=3: 4·3·27=324
297        assert!((variable_sum_exdeg(&k4(), 3.0).unwrap() - 324.0).abs() < 1e-10);
298    }
299
300    #[test]
301    fn vse_invalid_a() {
302        // a ≤ 0 → 0
303        assert!(variable_sum_exdeg(&k3(), 0.0).unwrap().abs() < 1e-10);
304        assert!(variable_sum_exdeg(&k3(), -1.0).unwrap().abs() < 1e-10);
305    }
306
307    // --- inverse_degree_power ---
308
309    #[test]
310    fn idp_empty() {
311        let g = Graph::with_vertices(0);
312        assert!(inverse_degree_power(&g, 1.0).unwrap().abs() < 1e-10);
313    }
314
315    #[test]
316    fn idp_isolated() {
317        let g = Graph::with_vertices(5);
318        assert!(inverse_degree_power(&g, 1.0).unwrap().abs() < 1e-10);
319    }
320
321    #[test]
322    fn idp_k1_is_inverse_degree() {
323        // k=1: Σ 1/d
324        // K_3: 3·(1/2) = 1.5
325        assert!((inverse_degree_power(&k3(), 1.0).unwrap() - 1.5).abs() < 1e-10);
326    }
327
328    #[test]
329    fn idp_k2_k3() {
330        // k=2: Σ 1/d² = 3·(1/4) = 0.75
331        assert!((inverse_degree_power(&k3(), 2.0).unwrap() - 0.75).abs() < 1e-10);
332    }
333
334    #[test]
335    fn idp_single_edge() {
336        // d=(1,1), k=2: 2·(1/1)=2
337        assert!((inverse_degree_power(&single_edge(), 2.0).unwrap() - 2.0).abs() < 1e-10);
338    }
339
340    #[test]
341    fn idp_star5() {
342        // d=(4,1,1,1,1), k=1: 1/4+4·1 = 4.25
343        assert!((inverse_degree_power(&star5(), 1.0).unwrap() - 4.25).abs() < 1e-10);
344    }
345
346    #[test]
347    fn idp_paw() {
348        // d=(2,2,3,1), k=1: 1/2+1/2+1/3+1 = 2+1/3
349        let expected = 0.5 + 0.5 + 1.0 / 3.0 + 1.0;
350        assert!((inverse_degree_power(&paw(), 1.0).unwrap() - expected).abs() < 1e-10);
351    }
352
353    // --- variable_first_zagreb ---
354
355    #[test]
356    fn vfz_empty() {
357        let g = Graph::with_vertices(0);
358        assert!(variable_first_zagreb(&g, 1.0).unwrap().abs() < 1e-10);
359    }
360
361    #[test]
362    fn vfz_p1_is_m1() {
363        // p=1: Σ_edges (du+dv) = first Zagreb index
364        // K_3: 3·4=12
365        assert!((variable_first_zagreb(&k3(), 1.0).unwrap() - 12.0).abs() < 1e-10);
366    }
367
368    #[test]
369    fn vfz_p2_is_forgotten() {
370        // p=2: Σ_edges (du²+dv²) = forgotten index
371        // K_3: 3·(4+4)=24
372        assert!((variable_first_zagreb(&k3(), 2.0).unwrap() - 24.0).abs() < 1e-10);
373        // K_4: 6·(9+9)=108
374        assert!((variable_first_zagreb(&k4(), 2.0).unwrap() - 108.0).abs() < 1e-10);
375    }
376
377    #[test]
378    fn vfz_single_edge() {
379        // d=(1,1), p=1: 1+1=2
380        assert!((variable_first_zagreb(&single_edge(), 1.0).unwrap() - 2.0).abs() < 1e-10);
381    }
382
383    #[test]
384    fn vfz_path3() {
385        // 2 edges d=(1,2): (1+2)+(2+1)=6
386        assert!((variable_first_zagreb(&path3(), 1.0).unwrap() - 6.0).abs() < 1e-10);
387    }
388
389    #[test]
390    fn vfz_star5() {
391        // 4 edges d=(4,1): each (4+1)=5 → 4·5=20
392        assert!((variable_first_zagreb(&star5(), 1.0).unwrap() - 20.0).abs() < 1e-10);
393    }
394
395    #[test]
396    fn vfz_paw_p2() {
397        // (0,1) d=(2,2): 4+4=8
398        // (0,2) d=(2,3): 4+9=13
399        // (1,2) d=(2,3): 4+9=13
400        // (2,3) d=(3,1): 9+1=10
401        let expected = 8.0 + 13.0 + 13.0 + 10.0;
402        assert!((variable_first_zagreb(&paw(), 2.0).unwrap() - expected).abs() < 1e-10);
403    }
404
405    // --- cross-consistency ---
406
407    #[test]
408    fn gzr_alpha0_is_vertex_count_nonzero() {
409        // α=0: Σ d^0 = number of non-isolated vertices + isolated (0^0=1)
410        // But we define 0^0=1 and skip degree-0 with count
411        // Actually: for d>0, d^0=1; for d=0, we add 1 (α==0)
412        // So it's just n (vertex count)
413        let g = &k3();
414        assert!((general_zeroth_order_randic(g, 0.0).unwrap() - 3.0).abs() < 1e-10);
415
416        // With isolated vertices
417        let iso = Graph::with_vertices(5);
418        assert!((general_zeroth_order_randic(&iso, 0.0).unwrap() - 5.0).abs() < 1e-10);
419    }
420
421    #[test]
422    fn idp_k0_is_nonzero_count() {
423        // k=0: Σ 1/d^0 = Σ 1 = number of non-isolated vertices
424        assert!((inverse_degree_power(&k3(), 0.0).unwrap() - 3.0).abs() < 1e-10);
425        assert!((inverse_degree_power(&star5(), 0.0).unwrap() - 5.0).abs() < 1e-10);
426    }
427}