Skip to main content

rust_igraph/algorithms/properties/
inverse_degree.rs

1//! Inverse degree and Zagreb coindex (ALGO-TR-056).
2//!
3//! - **Inverse degree index** `ID(G) = Σ_{v∈V, d(v)≥1} 1/d(v)`
4//!   (Also called the zeroth-order Randić index.) Sum of reciprocals of
5//!   non-zero vertex degrees. Introduced by Fajtlowicz (1987).
6//! - **First Zagreb coindex** `\bar{M}_1(G) = Σ_{(u,v)∉E, u≠v} (d(u)+d(v))`
7//!   Sum of degree sums over non-edges. Introduced by Došlić (2008).
8//!   Computed via the identity `\bar{M}_1 = 2m(n-1) - M_1`.
9//! - **Second Zagreb coindex** `\bar{M}_2(G) = Σ_{(u,v)∉E, u≠v} d(u)·d(v)`
10//!   Product of degrees over non-edges. Computed via
11//!   `\bar{M}_2 = 2m² - M_2` where `M_2` is the second Zagreb index.
12
13#![allow(
14    clippy::cast_possible_truncation,
15    clippy::cast_possible_wrap,
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 inverse degree index (zeroth-order Randić index).
25///
26/// `ID(G) = Σ_{v∈V, d(v)≥1} 1/d(v)`
27///
28/// Isolated vertices are excluded.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, inverse_degree_index};
34///
35/// // Star S_4: degrees [4,1,1,1,1] → ID = 1/4 + 4·1 = 4.25
36/// let g = Graph::from_edges(&[(0,1),(0,2),(0,3),(0,4)], false, Some(5)).unwrap();
37/// assert!((inverse_degree_index(&g).unwrap() - 4.25).abs() < 1e-10);
38/// ```
39pub fn inverse_degree_index(graph: &Graph) -> IgraphResult<f64> {
40    let mut id = 0.0_f64;
41
42    for v in 0..graph.vcount() {
43        let d = graph.degree(v)?;
44        if d > 0 {
45            id += 1.0 / d as f64;
46        }
47    }
48
49    Ok(id)
50}
51
52/// Compute the first Zagreb coindex.
53///
54/// `\bar{M}_1(G) = Σ_{(u,v)∉E, u≠v} (d(u) + d(v))`
55///
56/// Uses the identity: `\bar{M}_1 = 2m(n-1) - M_1` where `M_1 = Σ_v d(v)²`.
57///
58/// # Examples
59///
60/// ```
61/// use rust_igraph::{Graph, first_zagreb_coindex};
62///
63/// // Path 0-1-2: M₁ = 1+4+1 = 6, m=2, n=3
64/// // bar_M₁ = 2·2·2 - 6 = 2
65/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
66/// assert_eq!(first_zagreb_coindex(&g).unwrap(), 2);
67/// ```
68pub fn first_zagreb_coindex(graph: &Graph) -> IgraphResult<i64> {
69    let n = i64::from(graph.vcount());
70    let m = graph.ecount() as i64;
71
72    let mut m1: i64 = 0;
73    for v in 0..graph.vcount() {
74        let d = graph.degree(v)? as i64;
75        m1 = m1.saturating_add(d.saturating_mul(d));
76    }
77
78    Ok(2_i64
79        .saturating_mul(m)
80        .saturating_mul(n - 1)
81        .saturating_sub(m1))
82}
83
84/// Compute the second Zagreb coindex.
85///
86/// `\bar{M}_2(G) = Σ_{(u,v)∉E, u≠v} d(u) · d(v)`
87///
88/// Uses the identity: `\bar{M}_2 = 2m² - M_2` where
89/// `M_2 = Σ_{(u,v)∈E} d(u)·d(v)`.
90///
91/// # Examples
92///
93/// ```
94/// use rust_igraph::{Graph, second_zagreb_coindex};
95///
96/// // K_3: M₂ = 3·(2·2)=12, m=3 → bar_M₂ = 2·9-12 = 6
97/// // But K_3 has no non-edges, so bar_M₂ = 0. Let's check:
98/// // 2m² = 18, M₂ = 12 → 18-12 = 6? No, for K_3 there are no non-edges.
99/// // The identity is: Σ_{u<v} d(u)d(v) = M₂ + bar_M₂
100/// // Σ_{u<v} d(u)d(v) = (Σ d(v))²/2 - Σ d(v)²/2 = (2m)²/2 - M₁/2
101/// // = 2m² - M₁/2. So bar_M₂ = 2m² - M₁/2 - M₂.
102/// // For path 0-1-2: m=2, M₁=6, M₂=2+2=4
103/// // bar_M₂ = 2·4 - 6/2 - 4 = 8-3-4 = 1
104/// // Non-edge (0,2): d(0)·d(2) = 1·1 = 1. ✓
105/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
106/// assert_eq!(second_zagreb_coindex(&g).unwrap(), 1);
107/// ```
108pub fn second_zagreb_coindex(graph: &Graph) -> IgraphResult<i64> {
109    let mut sum_d: i64 = 0;
110    let mut sum_d2: i64 = 0;
111    for v in 0..graph.vcount() {
112        let d = graph.degree(v)? as i64;
113        sum_d = sum_d.saturating_add(d);
114        sum_d2 = sum_d2.saturating_add(d.saturating_mul(d));
115    }
116
117    let mut m2: i64 = 0;
118    for (u, v) in graph.edges() {
119        if u == v {
120            continue;
121        }
122        let du = graph.degree(u)? as i64;
123        let dv = graph.degree(v)? as i64;
124        m2 = m2.saturating_add(du.saturating_mul(dv));
125    }
126
127    // Σ_{u<v} d(u)d(v) = ((Σd)² - Σd²) / 2
128    let all_pairs_prod = (sum_d.saturating_mul(sum_d) - sum_d2) / 2;
129    Ok(all_pairs_prod - m2)
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    fn single_edge() -> Graph {
137        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
138    }
139
140    fn path3() -> Graph {
141        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
142    }
143
144    fn path4() -> Graph {
145        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
146    }
147
148    fn k3() -> Graph {
149        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
150    }
151
152    fn k4() -> Graph {
153        Graph::from_edges(
154            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
155            false,
156            Some(4),
157        )
158        .unwrap()
159    }
160
161    fn cycle4() -> Graph {
162        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
163    }
164
165    fn cycle5() -> Graph {
166        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
167    }
168
169    fn star5() -> Graph {
170        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
171    }
172
173    fn paw() -> Graph {
174        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
175    }
176
177    // --- inverse_degree_index ---
178
179    #[test]
180    fn id_empty() {
181        let g = Graph::with_vertices(0);
182        assert!((inverse_degree_index(&g).unwrap()).abs() < 1e-10);
183    }
184
185    #[test]
186    fn id_isolated() {
187        let g = Graph::with_vertices(5);
188        assert!((inverse_degree_index(&g).unwrap()).abs() < 1e-10);
189    }
190
191    #[test]
192    fn id_single_edge() {
193        assert!((inverse_degree_index(&single_edge()).unwrap() - 2.0).abs() < 1e-10);
194    }
195
196    #[test]
197    fn id_path3() {
198        // 1/1 + 1/2 + 1/1 = 2.5
199        assert!((inverse_degree_index(&path3()).unwrap() - 2.5).abs() < 1e-10);
200    }
201
202    #[test]
203    fn id_path4() {
204        // 1/1 + 1/2 + 1/2 + 1/1 = 3
205        assert!((inverse_degree_index(&path4()).unwrap() - 3.0).abs() < 1e-10);
206    }
207
208    #[test]
209    fn id_k3() {
210        assert!((inverse_degree_index(&k3()).unwrap() - 1.5).abs() < 1e-10);
211    }
212
213    #[test]
214    fn id_k4() {
215        assert!((inverse_degree_index(&k4()).unwrap() - 4.0 / 3.0).abs() < 1e-10);
216    }
217
218    #[test]
219    fn id_star5() {
220        assert!((inverse_degree_index(&star5()).unwrap() - 4.25).abs() < 1e-10);
221    }
222
223    #[test]
224    fn id_cycle4() {
225        // 4 · (1/2) = 2
226        assert!((inverse_degree_index(&cycle4()).unwrap() - 2.0).abs() < 1e-10);
227    }
228
229    #[test]
230    fn id_cycle5() {
231        // 5 · (1/2) = 2.5
232        assert!((inverse_degree_index(&cycle5()).unwrap() - 2.5).abs() < 1e-10);
233    }
234
235    #[test]
236    fn id_paw() {
237        // 1/2 + 1/2 + 1/3 + 1/1 = 7/3
238        assert!((inverse_degree_index(&paw()).unwrap() - 7.0 / 3.0).abs() < 1e-10);
239    }
240
241    #[test]
242    fn id_regular_is_n_over_r() {
243        for g in &[k3(), k4(), cycle4(), cycle5()] {
244            let n = f64::from(g.vcount());
245            let r = g.degree(0).unwrap() as f64;
246            assert!((inverse_degree_index(g).unwrap() - n / r).abs() < 1e-8);
247        }
248    }
249
250    #[test]
251    fn id_with_isolated() {
252        let g = Graph::from_edges(&[(0, 1)], false, Some(3)).unwrap();
253        assert!((inverse_degree_index(&g).unwrap() - 2.0).abs() < 1e-10);
254    }
255
256    // --- first_zagreb_coindex ---
257
258    #[test]
259    fn zco1_empty() {
260        let g = Graph::with_vertices(0);
261        assert_eq!(first_zagreb_coindex(&g).unwrap(), 0);
262    }
263
264    #[test]
265    fn zco1_single_edge() {
266        assert_eq!(first_zagreb_coindex(&single_edge()).unwrap(), 0);
267    }
268
269    #[test]
270    fn zco1_path3() {
271        assert_eq!(first_zagreb_coindex(&path3()).unwrap(), 2);
272    }
273
274    #[test]
275    fn zco1_path4() {
276        // m=3, n=4, M₁=1+4+4+1=10: 2·3·3-10=8
277        assert_eq!(first_zagreb_coindex(&path4()).unwrap(), 8);
278    }
279
280    #[test]
281    fn zco1_k3() {
282        assert_eq!(first_zagreb_coindex(&k3()).unwrap(), 0);
283    }
284
285    #[test]
286    fn zco1_k4() {
287        assert_eq!(first_zagreb_coindex(&k4()).unwrap(), 0);
288    }
289
290    #[test]
291    fn zco1_cycle4() {
292        // m=4, n=4, M₁=16: 2·4·3-16=8
293        assert_eq!(first_zagreb_coindex(&cycle4()).unwrap(), 8);
294    }
295
296    #[test]
297    fn zco1_cycle5() {
298        // m=5, n=5, M₁=20: 2·5·4-20=20
299        assert_eq!(first_zagreb_coindex(&cycle5()).unwrap(), 20);
300    }
301
302    #[test]
303    fn zco1_star5() {
304        // m=4, n=5, M₁=16+4=20: 2·4·4-20=12
305        assert_eq!(first_zagreb_coindex(&star5()).unwrap(), 12);
306    }
307
308    #[test]
309    fn zco1_paw() {
310        // m=4, n=4, M₁=4+4+9+1=18: 2·4·3-18=6
311        assert_eq!(first_zagreb_coindex(&paw()).unwrap(), 6);
312    }
313
314    #[test]
315    fn zco1_zero_for_complete() {
316        assert_eq!(first_zagreb_coindex(&k3()).unwrap(), 0);
317        assert_eq!(first_zagreb_coindex(&k4()).unwrap(), 0);
318    }
319
320    // --- second_zagreb_coindex ---
321
322    #[test]
323    fn zco2_empty() {
324        let g = Graph::with_vertices(0);
325        assert_eq!(second_zagreb_coindex(&g).unwrap(), 0);
326    }
327
328    #[test]
329    fn zco2_single_edge() {
330        assert_eq!(second_zagreb_coindex(&single_edge()).unwrap(), 0);
331    }
332
333    #[test]
334    fn zco2_path3() {
335        // Non-edge (0,2): 1·1 = 1
336        assert_eq!(second_zagreb_coindex(&path3()).unwrap(), 1);
337    }
338
339    #[test]
340    fn zco2_path4() {
341        // Non-edges: (0,2):1·2=2, (0,3):1·1=1, (1,3):2·1=2 → 5
342        assert_eq!(second_zagreb_coindex(&path4()).unwrap(), 5);
343    }
344
345    #[test]
346    fn zco2_k3() {
347        assert_eq!(second_zagreb_coindex(&k3()).unwrap(), 0);
348    }
349
350    #[test]
351    fn zco2_k4() {
352        assert_eq!(second_zagreb_coindex(&k4()).unwrap(), 0);
353    }
354
355    #[test]
356    fn zco2_cycle4() {
357        // Non-edges: (0,2):2·2=4, (1,3):2·2=4 → 8
358        assert_eq!(second_zagreb_coindex(&cycle4()).unwrap(), 8);
359    }
360
361    #[test]
362    fn zco2_cycle5() {
363        // Non-edges: 5 pairs at dist 2, each 2·2=4 → 20
364        assert_eq!(second_zagreb_coindex(&cycle5()).unwrap(), 20);
365    }
366
367    #[test]
368    fn zco2_star5() {
369        // Non-edges: C(4,2)=6 leaf-leaf pairs, each 1·1=1 → 6
370        assert_eq!(second_zagreb_coindex(&star5()).unwrap(), 6);
371    }
372
373    #[test]
374    fn zco2_paw() {
375        // degrees [2,2,3,1], edges: (0,1),(0,2),(1,2),(2,3)
376        // Non-edges: (0,3):2·1=2, (1,3):2·1=2 → 4
377        assert_eq!(second_zagreb_coindex(&paw()).unwrap(), 4);
378    }
379
380    #[test]
381    fn zco2_zero_for_complete() {
382        assert_eq!(second_zagreb_coindex(&k3()).unwrap(), 0);
383        assert_eq!(second_zagreb_coindex(&k4()).unwrap(), 0);
384    }
385
386    // --- cross-consistency ---
387
388    #[test]
389    fn coindices_nonneg() {
390        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
391            assert!(first_zagreb_coindex(g).unwrap() >= 0);
392            assert!(second_zagreb_coindex(g).unwrap() >= 0);
393        }
394    }
395
396    #[test]
397    fn id_geq_1_for_connected() {
398        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5()] {
399            assert!(inverse_degree_index(g).unwrap() >= 1.0 - 1e-10);
400        }
401    }
402}