Skip to main content

rust_igraph/algorithms/properties/
reformulated_zagreb.rs

1//! Reformulated Zagreb indices (ALGO-TR-054).
2//!
3//! Edge-degree based variants of the Zagreb indices, where the
4//! **edge degree** `ε(e)` of edge `e=(u,v)` is `d(u)+d(v)-2`.
5//!
6//! - **First reformulated Zagreb** `EM₁(G) = Σ_{e∈E} ε(e)²`
7//! - **Second reformulated Zagreb** `EM₂(G) = Σ_{e~f} ε(e)·ε(f)`
8//!   where `e~f` means edges e and f share a vertex.
9//! - **Third Zagreb index** `M₃(G) = Σ_{(u,v)∈E} |d(u)-d(v)|`
10//!   (also called the irregularity index of edges).
11//!
12//! Introduced by Milićević, Nikolić & Trinajstić (2004).
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 first reformulated Zagreb index.
25///
26/// `EM₁(G) = Σ_{e∈E} ε(e)²` where `ε(e) = d(u) + d(v) - 2`.
27///
28/// Self-loops are skipped.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, first_reformulated_zagreb};
34///
35/// // K_3: each edge has ε = 2+2-2 = 2, EM₁ = 3·4 = 12
36/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
37/// assert_eq!(first_reformulated_zagreb(&g).unwrap(), 12);
38/// ```
39pub fn first_reformulated_zagreb(graph: &Graph) -> IgraphResult<u64> {
40    let mut em1: u64 = 0;
41
42    for (u, v) in graph.edges() {
43        if u == v {
44            continue;
45        }
46        let du = graph.degree(u)? as u64;
47        let dv = graph.degree(v)? as u64;
48        let eps = du + dv - 2;
49        em1 = em1.saturating_add(eps.saturating_mul(eps));
50    }
51
52    Ok(em1)
53}
54
55/// Compute the second reformulated Zagreb index.
56///
57/// `EM₂(G) = Σ_{e~f, e<f} ε(e) · ε(f)`
58///
59/// The sum is over all pairs of adjacent edges (sharing a vertex).
60/// For each vertex v, every pair of edges incident to v contributes
61/// one term. Self-loops are excluded.
62///
63/// # Examples
64///
65/// ```
66/// use rust_igraph::{Graph, second_reformulated_zagreb};
67///
68/// // Path 0-1-2: edges e₁=(0,1) ε=1, e₂=(1,2) ε=1
69/// // Only pair sharing vertex 1: ε·ε = 1 → EM₂ = 1
70/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
71/// assert_eq!(second_reformulated_zagreb(&g).unwrap(), 1);
72/// ```
73pub fn second_reformulated_zagreb(graph: &Graph) -> IgraphResult<u64> {
74    let n = graph.vcount() as usize;
75    let ecount = graph.ecount();
76
77    if ecount == 0 {
78        return Ok(0);
79    }
80
81    let edges: Vec<(u32, u32)> = graph.edges().collect();
82    let mut deg = vec![0_usize; n];
83    for v in 0..n as u32 {
84        deg[v as usize] = graph.degree(v)?;
85    }
86
87    let mut edge_deg = Vec::with_capacity(ecount);
88    for &(u, v) in &edges {
89        if u == v {
90            edge_deg.push(0_u64);
91            continue;
92        }
93        edge_deg.push((deg[u as usize] + deg[v as usize] - 2) as u64);
94    }
95
96    let mut incident: Vec<Vec<usize>> = vec![Vec::new(); n];
97    for (idx, &(u, v)) in edges.iter().enumerate() {
98        if u == v {
99            continue;
100        }
101        incident[u as usize].push(idx);
102        incident[v as usize].push(idx);
103    }
104
105    let mut em2: u64 = 0;
106    for v in 0..n {
107        let inc = &incident[v];
108        let k = inc.len();
109        for i in 0..k {
110            for j in (i + 1)..k {
111                em2 = em2.saturating_add(edge_deg[inc[i]].saturating_mul(edge_deg[inc[j]]));
112            }
113        }
114    }
115
116    Ok(em2)
117}
118
119/// Compute the third Zagreb index.
120///
121/// `M₃(G) = Σ_{(u,v)∈E} |d(u) - d(v)|`
122///
123/// Self-loops are skipped.
124///
125/// # Examples
126///
127/// ```
128/// use rust_igraph::{Graph, third_zagreb_index};
129///
130/// // Star S_4 (center=0): edges (0,1)..(0,4), degrees [4,1,1,1,1]
131/// // Each edge: |4-1|=3, 4 edges → M₃ = 12
132/// let g = Graph::from_edges(&[(0,1),(0,2),(0,3),(0,4)], false, Some(5)).unwrap();
133/// assert_eq!(third_zagreb_index(&g).unwrap(), 12);
134/// ```
135pub fn third_zagreb_index(graph: &Graph) -> IgraphResult<u64> {
136    let mut m3: u64 = 0;
137
138    for (u, v) in graph.edges() {
139        if u == v {
140            continue;
141        }
142        let du = graph.degree(u)?;
143        let dv = graph.degree(v)?;
144        m3 = m3.saturating_add(du.abs_diff(dv) as u64);
145    }
146
147    Ok(m3)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    fn single_edge() -> Graph {
155        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
156    }
157
158    fn path3() -> Graph {
159        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
160    }
161
162    fn path4() -> Graph {
163        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
164    }
165
166    fn k3() -> Graph {
167        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
168    }
169
170    fn k4() -> Graph {
171        Graph::from_edges(
172            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
173            false,
174            Some(4),
175        )
176        .unwrap()
177    }
178
179    fn cycle4() -> Graph {
180        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
181    }
182
183    fn cycle5() -> Graph {
184        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
185    }
186
187    fn star5() -> Graph {
188        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
189    }
190
191    fn paw() -> Graph {
192        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
193    }
194
195    // --- first_reformulated_zagreb ---
196
197    #[test]
198    fn em1_empty() {
199        let g = Graph::with_vertices(0);
200        assert_eq!(first_reformulated_zagreb(&g).unwrap(), 0);
201    }
202
203    #[test]
204    fn em1_single_vertex() {
205        let g = Graph::with_vertices(1);
206        assert_eq!(first_reformulated_zagreb(&g).unwrap(), 0);
207    }
208
209    #[test]
210    fn em1_single_edge() {
211        // ε = 1+1-2 = 0, EM₁ = 0
212        assert_eq!(first_reformulated_zagreb(&single_edge()).unwrap(), 0);
213    }
214
215    #[test]
216    fn em1_path3() {
217        // edges: (0,1) ε=1+2-2=1, (1,2) ε=2+1-2=1
218        // EM₁ = 1+1 = 2
219        assert_eq!(first_reformulated_zagreb(&path3()).unwrap(), 2);
220    }
221
222    #[test]
223    fn em1_path4() {
224        // edges: (0,1) ε=1+2-2=1, (1,2) ε=2+2-2=2, (2,3) ε=2+1-2=1
225        // EM₁ = 1+4+1 = 6
226        assert_eq!(first_reformulated_zagreb(&path4()).unwrap(), 6);
227    }
228
229    #[test]
230    fn em1_k3() {
231        // each edge ε=2+2-2=2, 3 edges
232        // EM₁ = 3·4 = 12
233        assert_eq!(first_reformulated_zagreb(&k3()).unwrap(), 12);
234    }
235
236    #[test]
237    fn em1_k4() {
238        // each edge ε=3+3-2=4, 6 edges
239        // EM₁ = 6·16 = 96
240        assert_eq!(first_reformulated_zagreb(&k4()).unwrap(), 96);
241    }
242
243    #[test]
244    fn em1_cycle4() {
245        // each edge ε=2+2-2=2, 4 edges
246        // EM₁ = 4·4 = 16
247        assert_eq!(first_reformulated_zagreb(&cycle4()).unwrap(), 16);
248    }
249
250    #[test]
251    fn em1_cycle5() {
252        // each edge ε=2+2-2=2, 5 edges
253        // EM₁ = 5·4 = 20
254        assert_eq!(first_reformulated_zagreb(&cycle5()).unwrap(), 20);
255    }
256
257    #[test]
258    fn em1_star5() {
259        // each edge ε=4+1-2=3, 4 edges
260        // EM₁ = 4·9 = 36
261        assert_eq!(first_reformulated_zagreb(&star5()).unwrap(), 36);
262    }
263
264    #[test]
265    fn em1_paw() {
266        // degrees [2,2,3,1]
267        // (0,1): ε=2+2-2=2, (0,2): ε=2+3-2=3, (1,2): ε=2+3-2=3, (2,3): ε=3+1-2=2
268        // EM₁ = 4+9+9+4 = 26
269        assert_eq!(first_reformulated_zagreb(&paw()).unwrap(), 26);
270    }
271
272    #[test]
273    fn em1_regular_formula() {
274        // r-regular: ε = 2r-2, EM₁ = m·(2r-2)²
275        for g in &[k3(), k4(), cycle4(), cycle5()] {
276            let m = g.ecount() as u64;
277            let r = g.degree(0).unwrap() as u64;
278            let eps = 2 * r - 2;
279            assert_eq!(first_reformulated_zagreb(g).unwrap(), m * eps * eps);
280        }
281    }
282
283    // --- second_reformulated_zagreb ---
284
285    #[test]
286    fn em2_empty() {
287        let g = Graph::with_vertices(0);
288        assert_eq!(second_reformulated_zagreb(&g).unwrap(), 0);
289    }
290
291    #[test]
292    fn em2_single_edge() {
293        // Only 1 edge, no adjacent pair → 0
294        assert_eq!(second_reformulated_zagreb(&single_edge()).unwrap(), 0);
295    }
296
297    #[test]
298    fn em2_path3() {
299        // e₁=(0,1) ε=1, e₂=(1,2) ε=1, adjacent at vertex 1
300        // EM₂ = 1·1 = 1
301        assert_eq!(second_reformulated_zagreb(&path3()).unwrap(), 1);
302    }
303
304    #[test]
305    fn em2_path4() {
306        // e₁=(0,1) ε=1, e₂=(1,2) ε=2, e₃=(2,3) ε=1
307        // Pairs: (e₁,e₂) at v1: 1·2=2, (e₂,e₃) at v2: 2·1=2
308        // EM₂ = 4
309        assert_eq!(second_reformulated_zagreb(&path4()).unwrap(), 4);
310    }
311
312    #[test]
313    fn em2_k3() {
314        // 3 edges, each ε=2. Each vertex has 2 incident edges.
315        // At each vertex: 1 pair → 2·2=4. 3 vertices → 12.
316        // But each edge pair is adjacent at exactly 1 vertex (they share exactly 1 vertex).
317        // There are 3 edge pairs, each contributing 2·2=4 → EM₂ = 12
318        assert_eq!(second_reformulated_zagreb(&k3()).unwrap(), 12);
319    }
320
321    #[test]
322    fn em2_k4() {
323        // 6 edges, each ε=4. Each vertex has degree 3 → C(3,2)=3 pairs per vertex.
324        // 4 vertices → 12 adjacent-edge pairs, each 4·4=16 → EM₂ = 192
325        assert_eq!(second_reformulated_zagreb(&k4()).unwrap(), 192);
326    }
327
328    #[test]
329    fn em2_cycle4() {
330        // 4 edges, each ε=2. Each vertex has degree 2 → C(2,2)=1 pair per vertex.
331        // 4 vertices → 4 pairs, each 2·2=4 → EM₂ = 16
332        assert_eq!(second_reformulated_zagreb(&cycle4()).unwrap(), 16);
333    }
334
335    #[test]
336    fn em2_cycle5() {
337        // 5 edges, each ε=2. 5 vertices, each C(2,2)=1 pair.
338        // 5 pairs, each 2·2=4 → EM₂ = 20
339        assert_eq!(second_reformulated_zagreb(&cycle5()).unwrap(), 20);
340    }
341
342    #[test]
343    fn em2_star5() {
344        // 4 edges, each ε=3. Center has C(4,2)=6 pairs.
345        // Leaves have degree 1, no pairs. 6 pairs, each 3·3=9 → EM₂ = 54
346        assert_eq!(second_reformulated_zagreb(&star5()).unwrap(), 54);
347    }
348
349    #[test]
350    fn em2_paw() {
351        // degrees [2,2,3,1], edges: (0,1)ε=2, (0,2)ε=3, (1,2)ε=3, (2,3)ε=2
352        // v0 (deg 2): pairs (e01,e02): 2·3=6
353        // v1 (deg 2): pairs (e01,e12): 2·3=6
354        // v2 (deg 3): pairs (e02,e12):3·3=9, (e02,e23):3·2=6, (e12,e23):3·2=6 → 21
355        // v3 (deg 1): no pairs
356        // EM₂ = 6+6+21 = 33
357        assert_eq!(second_reformulated_zagreb(&paw()).unwrap(), 33);
358    }
359
360    #[test]
361    fn em2_regular_formula() {
362        // r-regular: ε=2r-2 for all edges. Number of adjacent edge pairs = m·(r-1)
363        // (each edge has r-1 neighbors at each endpoint, but each pair counted once,
364        //  so total pairs = Σ_v C(deg(v),2) = n·C(r,2) = n·r(r-1)/2)
365        // Each pair contributes (2r-2)² → EM₂ = n·r(r-1)/2·(2r-2)²
366        for g in &[k3(), k4(), cycle4(), cycle5()] {
367            let n = u64::from(g.vcount());
368            let r = g.degree(0).unwrap() as u64;
369            let eps = 2 * r - 2;
370            let pairs = n * r * (r - 1) / 2;
371            assert_eq!(second_reformulated_zagreb(g).unwrap(), pairs * eps * eps);
372        }
373    }
374
375    // --- third_zagreb_index ---
376
377    #[test]
378    fn m3_empty() {
379        let g = Graph::with_vertices(0);
380        assert_eq!(third_zagreb_index(&g).unwrap(), 0);
381    }
382
383    #[test]
384    fn m3_single_edge() {
385        // |1-1| = 0
386        assert_eq!(third_zagreb_index(&single_edge()).unwrap(), 0);
387    }
388
389    #[test]
390    fn m3_path3() {
391        // (0,1):|1-2|=1, (1,2):|2-1|=1 → M₃ = 2
392        assert_eq!(third_zagreb_index(&path3()).unwrap(), 2);
393    }
394
395    #[test]
396    fn m3_path4() {
397        // (0,1):|1-2|=1, (1,2):|2-2|=0, (2,3):|2-1|=1 → M₃ = 2
398        assert_eq!(third_zagreb_index(&path4()).unwrap(), 2);
399    }
400
401    #[test]
402    fn m3_k3() {
403        // All degrees equal → M₃ = 0
404        assert_eq!(third_zagreb_index(&k3()).unwrap(), 0);
405    }
406
407    #[test]
408    fn m3_k4() {
409        assert_eq!(third_zagreb_index(&k4()).unwrap(), 0);
410    }
411
412    #[test]
413    fn m3_cycle4() {
414        assert_eq!(third_zagreb_index(&cycle4()).unwrap(), 0);
415    }
416
417    #[test]
418    fn m3_star5() {
419        // All edges: |4-1|=3, 4 edges → M₃ = 12
420        assert_eq!(third_zagreb_index(&star5()).unwrap(), 12);
421    }
422
423    #[test]
424    fn m3_paw() {
425        // (0,1):|2-2|=0, (0,2):|2-3|=1, (1,2):|2-3|=1, (2,3):|3-1|=2 → M₃ = 4
426        assert_eq!(third_zagreb_index(&paw()).unwrap(), 4);
427    }
428
429    #[test]
430    fn m3_zero_for_regular() {
431        for g in &[k3(), k4(), cycle4(), cycle5()] {
432            assert_eq!(third_zagreb_index(g).unwrap(), 0);
433        }
434    }
435
436    // --- cross-consistency ---
437
438    #[test]
439    fn all_compute_ok() {
440        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
441            first_reformulated_zagreb(g).unwrap();
442            second_reformulated_zagreb(g).unwrap();
443            third_zagreb_index(g).unwrap();
444        }
445    }
446
447    #[test]
448    fn em1_zero_iff_matching() {
449        // EM₁ = 0 iff every edge has ε = 0 iff d(u)+d(v) = 2 for all edges
450        // That means all edges are between degree-1 vertices → perfect matching
451        assert_eq!(first_reformulated_zagreb(&single_edge()).unwrap(), 0);
452        let matching = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
453        assert_eq!(first_reformulated_zagreb(&matching).unwrap(), 0);
454    }
455}