Skip to main content

rust_igraph/algorithms/properties/
hyper_zagreb.rs

1//! Hyper-Zagreb and redefined Zagreb indices (ALGO-TR-060).
2//!
3//! - **First hyper-Zagreb index** `HM₁(G) = Σ_{(u,v)∈E} (d(u) + d(v))²`
4//!   Introduced by Shirdel et al. (2013). Square of degree sum over edges.
5//! - **Second hyper-Zagreb index** `HM₂(G) = Σ_{(u,v)∈E} (d(u) · d(v))²`
6//!   Square of degree product over edges.
7//! - **First redefined Zagreb index** `ReZG₁(G) = Σ_{(u,v)∈E} (d(u)+d(v)) / (d(u)·d(v))`
8//!   Introduced by Ranjini et al. (2013).
9
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_precision_loss,
13    clippy::many_single_char_names,
14    clippy::needless_range_loop,
15    clippy::too_many_lines
16)]
17
18use crate::core::{Graph, IgraphResult};
19
20/// Compute the first hyper-Zagreb index.
21///
22/// `HM₁(G) = Σ_{(u,v)∈E} (d(u) + d(v))²`
23///
24/// Self-loops are skipped.
25///
26/// # Examples
27///
28/// ```
29/// use rust_igraph::{Graph, first_hyper_zagreb};
30///
31/// // K_3: each edge (2+2)²=16, 3 edges → 48
32/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
33/// assert_eq!(first_hyper_zagreb(&g).unwrap(), 48);
34/// ```
35pub fn first_hyper_zagreb(graph: &Graph) -> IgraphResult<u64> {
36    let mut hm1 = 0_u64;
37
38    for (u, v) in graph.edges() {
39        if u == v {
40            continue;
41        }
42        let du = graph.degree(u)? as u64;
43        let dv = graph.degree(v)? as u64;
44        let s = du + dv;
45        hm1 = hm1.saturating_add(s.saturating_mul(s));
46    }
47
48    Ok(hm1)
49}
50
51/// Compute the second hyper-Zagreb index.
52///
53/// `HM₂(G) = Σ_{(u,v)∈E} (d(u) · d(v))²`
54///
55/// Self-loops are skipped.
56///
57/// # Examples
58///
59/// ```
60/// use rust_igraph::{Graph, second_hyper_zagreb};
61///
62/// // K_3: each edge (2·2)²=16, 3 edges → 48
63/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
64/// assert_eq!(second_hyper_zagreb(&g).unwrap(), 48);
65/// ```
66pub fn second_hyper_zagreb(graph: &Graph) -> IgraphResult<u64> {
67    let mut hm2 = 0_u64;
68
69    for (u, v) in graph.edges() {
70        if u == v {
71            continue;
72        }
73        let du = graph.degree(u)? as u64;
74        let dv = graph.degree(v)? as u64;
75        let p = du.saturating_mul(dv);
76        hm2 = hm2.saturating_add(p.saturating_mul(p));
77    }
78
79    Ok(hm2)
80}
81
82/// Compute the first redefined Zagreb index.
83///
84/// `ReZG₁(G) = Σ_{(u,v)∈E} (d(u) + d(v)) / (d(u) · d(v))`
85///
86/// Edges where either endpoint has degree 0 or self-loops are skipped.
87///
88/// # Examples
89///
90/// ```
91/// use rust_igraph::{Graph, first_redefined_zagreb};
92///
93/// // K_3: each edge (2+2)/(2·2)=1, 3 edges → 3
94/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
95/// assert!((first_redefined_zagreb(&g).unwrap() - 3.0).abs() < 1e-10);
96/// ```
97pub fn first_redefined_zagreb(graph: &Graph) -> IgraphResult<f64> {
98    let mut rezg1 = 0.0_f64;
99
100    for (u, v) in graph.edges() {
101        if u == v {
102            continue;
103        }
104        let du = graph.degree(u)? as f64;
105        let dv = graph.degree(v)? as f64;
106        let prod = du * dv;
107        if prod > 0.0 {
108            rezg1 += (du + dv) / prod;
109        }
110    }
111
112    Ok(rezg1)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    fn single_edge() -> Graph {
120        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
121    }
122
123    fn path3() -> Graph {
124        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
125    }
126
127    fn path4() -> Graph {
128        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
129    }
130
131    fn k3() -> Graph {
132        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
133    }
134
135    fn k4() -> Graph {
136        Graph::from_edges(
137            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
138            false,
139            Some(4),
140        )
141        .unwrap()
142    }
143
144    fn cycle4() -> Graph {
145        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
146    }
147
148    fn cycle5() -> Graph {
149        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
150    }
151
152    fn star5() -> Graph {
153        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
154    }
155
156    fn paw() -> Graph {
157        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
158    }
159
160    // --- first_hyper_zagreb ---
161
162    #[test]
163    fn hm1_empty() {
164        let g = Graph::with_vertices(0);
165        assert_eq!(first_hyper_zagreb(&g).unwrap(), 0);
166    }
167
168    #[test]
169    fn hm1_isolated() {
170        let g = Graph::with_vertices(5);
171        assert_eq!(first_hyper_zagreb(&g).unwrap(), 0);
172    }
173
174    #[test]
175    fn hm1_single_edge() {
176        // (1+1)² = 4
177        assert_eq!(first_hyper_zagreb(&single_edge()).unwrap(), 4);
178    }
179
180    #[test]
181    fn hm1_path3() {
182        // (0,1): (1+2)²=9, (1,2): (2+1)²=9 → 18
183        assert_eq!(first_hyper_zagreb(&path3()).unwrap(), 18);
184    }
185
186    #[test]
187    fn hm1_path4() {
188        // (0,1): (1+2)²=9, (1,2): (2+2)²=16, (2,3): (2+1)²=9 → 34
189        assert_eq!(first_hyper_zagreb(&path4()).unwrap(), 34);
190    }
191
192    #[test]
193    fn hm1_k3() {
194        // 3 × (2+2)² = 3×16 = 48
195        assert_eq!(first_hyper_zagreb(&k3()).unwrap(), 48);
196    }
197
198    #[test]
199    fn hm1_k4() {
200        // 6 × (3+3)² = 6×36 = 216
201        assert_eq!(first_hyper_zagreb(&k4()).unwrap(), 216);
202    }
203
204    #[test]
205    fn hm1_cycle4() {
206        // 4 × (2+2)² = 4×16 = 64
207        assert_eq!(first_hyper_zagreb(&cycle4()).unwrap(), 64);
208    }
209
210    #[test]
211    fn hm1_cycle5() {
212        // 5 × (2+2)² = 5×16 = 80
213        assert_eq!(first_hyper_zagreb(&cycle5()).unwrap(), 80);
214    }
215
216    #[test]
217    fn hm1_star5() {
218        // 4 × (4+1)² = 4×25 = 100
219        assert_eq!(first_hyper_zagreb(&star5()).unwrap(), 100);
220    }
221
222    #[test]
223    fn hm1_paw() {
224        // degrees [2,2,3,1]
225        // (0,1):(2+2)²=16, (0,2):(2+3)²=25, (1,2):(2+3)²=25, (2,3):(3+1)²=16
226        // HM₁ = 16+25+25+16 = 82
227        assert_eq!(first_hyper_zagreb(&paw()).unwrap(), 82);
228    }
229
230    #[test]
231    fn hm1_regular_formula() {
232        // r-regular: HM₁ = m·(2r)² = 4r²m
233        for g in &[k3(), k4(), cycle4(), cycle5()] {
234            let m = g.ecount() as u64;
235            let r = g.degree(0).unwrap() as u64;
236            assert_eq!(first_hyper_zagreb(g).unwrap(), 4 * r * r * m);
237        }
238    }
239
240    // --- second_hyper_zagreb ---
241
242    #[test]
243    fn hm2_empty() {
244        let g = Graph::with_vertices(0);
245        assert_eq!(second_hyper_zagreb(&g).unwrap(), 0);
246    }
247
248    #[test]
249    fn hm2_single_edge() {
250        // (1·1)² = 1
251        assert_eq!(second_hyper_zagreb(&single_edge()).unwrap(), 1);
252    }
253
254    #[test]
255    fn hm2_path3() {
256        // (0,1):(1·2)²=4, (1,2):(2·1)²=4 → 8
257        assert_eq!(second_hyper_zagreb(&path3()).unwrap(), 8);
258    }
259
260    #[test]
261    fn hm2_path4() {
262        // (0,1):(1·2)²=4, (1,2):(2·2)²=16, (2,3):(2·1)²=4 → 24
263        assert_eq!(second_hyper_zagreb(&path4()).unwrap(), 24);
264    }
265
266    #[test]
267    fn hm2_k3() {
268        // 3 × (2·2)² = 3×16 = 48
269        assert_eq!(second_hyper_zagreb(&k3()).unwrap(), 48);
270    }
271
272    #[test]
273    fn hm2_k4() {
274        // 6 × (3·3)² = 6×81 = 486
275        assert_eq!(second_hyper_zagreb(&k4()).unwrap(), 486);
276    }
277
278    #[test]
279    fn hm2_cycle4() {
280        // 4 × (2·2)² = 4×16 = 64
281        assert_eq!(second_hyper_zagreb(&cycle4()).unwrap(), 64);
282    }
283
284    #[test]
285    fn hm2_cycle5() {
286        // 5 × (2·2)² = 5×16 = 80
287        assert_eq!(second_hyper_zagreb(&cycle5()).unwrap(), 80);
288    }
289
290    #[test]
291    fn hm2_star5() {
292        // 4 × (4·1)² = 4×16 = 64
293        assert_eq!(second_hyper_zagreb(&star5()).unwrap(), 64);
294    }
295
296    #[test]
297    fn hm2_paw() {
298        // (0,1):(2·2)²=16, (0,2):(2·3)²=36, (1,2):(2·3)²=36, (2,3):(3·1)²=9
299        // HM₂ = 16+36+36+9 = 97
300        assert_eq!(second_hyper_zagreb(&paw()).unwrap(), 97);
301    }
302
303    #[test]
304    fn hm2_regular_formula() {
305        // r-regular: HM₂ = m·r⁴
306        for g in &[k3(), k4(), cycle4(), cycle5()] {
307            let m = g.ecount() as u64;
308            let r = g.degree(0).unwrap() as u64;
309            assert_eq!(second_hyper_zagreb(g).unwrap(), m * r * r * r * r);
310        }
311    }
312
313    #[test]
314    fn hm1_k3_eq_hm2_k3() {
315        // For K_3: HM₁ = HM₂ = 48 (coincidence since (2+2)² = (2·2)²)
316        assert_eq!(
317            first_hyper_zagreb(&k3()).unwrap(),
318            second_hyper_zagreb(&k3()).unwrap()
319        );
320    }
321
322    // --- first_redefined_zagreb ---
323
324    #[test]
325    fn rezg1_empty() {
326        let g = Graph::with_vertices(0);
327        assert!((first_redefined_zagreb(&g).unwrap()).abs() < 1e-10);
328    }
329
330    #[test]
331    fn rezg1_single_edge() {
332        // (1+1)/(1·1) = 2
333        assert!((first_redefined_zagreb(&single_edge()).unwrap() - 2.0).abs() < 1e-10);
334    }
335
336    #[test]
337    fn rezg1_path3() {
338        // (0,1):(1+2)/(1·2) = 3/2, (1,2): same → 3
339        assert!((first_redefined_zagreb(&path3()).unwrap() - 3.0).abs() < 1e-10);
340    }
341
342    #[test]
343    fn rezg1_path4() {
344        // (0,1):(1+2)/(1·2)=3/2, (1,2):(2+2)/(2·2)=1, (2,3):3/2 → 4
345        assert!((first_redefined_zagreb(&path4()).unwrap() - 4.0).abs() < 1e-10);
346    }
347
348    #[test]
349    fn rezg1_k3() {
350        // 3 × (2+2)/(2·2) = 3×1 = 3
351        assert!((first_redefined_zagreb(&k3()).unwrap() - 3.0).abs() < 1e-10);
352    }
353
354    #[test]
355    fn rezg1_k4() {
356        // 6 × (3+3)/(3·3) = 6×6/9 = 4
357        assert!((first_redefined_zagreb(&k4()).unwrap() - 4.0).abs() < 1e-10);
358    }
359
360    #[test]
361    fn rezg1_cycle4() {
362        // 4 × 1 = 4
363        assert!((first_redefined_zagreb(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
364    }
365
366    #[test]
367    fn rezg1_cycle5() {
368        // 5 × 1 = 5
369        assert!((first_redefined_zagreb(&cycle5()).unwrap() - 5.0).abs() < 1e-10);
370    }
371
372    #[test]
373    fn rezg1_star5() {
374        // 4 × (4+1)/(4·1) = 4×5/4 = 5
375        assert!((first_redefined_zagreb(&star5()).unwrap() - 5.0).abs() < 1e-10);
376    }
377
378    #[test]
379    fn rezg1_paw() {
380        // (0,1):(2+2)/(2·2)=1, (0,2):(2+3)/(2·3)=5/6, (1,2):5/6, (2,3):(3+1)/(3·1)=4/3
381        // ReZG₁ = 1 + 5/6 + 5/6 + 4/3 = 1 + 10/6 + 4/3 = 1 + 5/3 + 4/3 = 1 + 3 = 4
382        assert!((first_redefined_zagreb(&paw()).unwrap() - 4.0).abs() < 1e-10);
383    }
384
385    #[test]
386    fn rezg1_regular_formula() {
387        // r-regular: ReZG₁ = m · 2r/r² = m · 2/r
388        for g in &[k3(), k4(), cycle4(), cycle5()] {
389            let m = g.ecount() as f64;
390            let r = g.degree(0).unwrap() as f64;
391            let expected = m * 2.0 / r;
392            assert!((first_redefined_zagreb(g).unwrap() - expected).abs() < 1e-8);
393        }
394    }
395
396    // --- cross-consistency ---
397
398    #[test]
399    fn hm1_geq_hm2_for_bipartite() {
400        // For trees (bipartite): not universally true. Just check both compute.
401        for g in &[single_edge(), path3(), path4(), star5()] {
402            let _ = first_hyper_zagreb(g).unwrap();
403            let _ = second_hyper_zagreb(g).unwrap();
404        }
405    }
406
407    #[test]
408    fn rezg1_positive() {
409        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
410            assert!(first_redefined_zagreb(g).unwrap() > 0.0);
411        }
412    }
413
414    #[test]
415    fn rezg1_identity() {
416        // ReZG₁ = Σ (1/d(u) + 1/d(v)) = 2·Σ_v 1/d(v) = 2·ID(G)? No:
417        // ReZG₁ = Σ_{edges} (d(u)+d(v))/(d(u)·d(v)) = Σ_{edges} (1/d(v) + 1/d(u))
418        // For a vertex v with degree d, it appears in d edges, so
419        // ReZG₁ = Σ_v d(v)·(1/d(v)) · (count of reciprocals from neighbors)
420        // Actually: ReZG₁ = Σ_{(u,v)∈E} 1/d(u) + 1/d(v)
421        // Each vertex v contributes 1/d(v) for each of its d(v) edges = 1.
422        // So ReZG₁ = n (for graphs without isolated vertices)!
423        // Wait, that would mean ReZG₁ = |V with d>0|.
424        // Let's verify: path3 has 3 non-isolated vertices → ReZG₁ should be 3. ✓
425        // K3: 3. ✓. K4: 4. ✓. C4: 4. ✓. C5: 5. ✓. star5: 5. ✓. paw: 4. ✓.
426        for g in &[path3(), k3(), k4(), cycle4(), cycle5(), star5(), paw()] {
427            let n_nonisolated = (0..g.vcount())
428                .filter(|&v| g.degree(v).unwrap() > 0)
429                .count() as f64;
430            assert!((first_redefined_zagreb(g).unwrap() - n_nonisolated).abs() < 1e-8);
431        }
432    }
433}