Skip to main content

rust_igraph/algorithms/properties/
neighborhood_zagreb.rs

1//! Neighborhood-degree based Zagreb indices (ALGO-TR-062).
2//!
3//! These indices use `S(v) = Σ_{u∈N(v)} d(u)`, the sum of degrees
4//! of all neighbors of vertex v (the "connection number" or
5//! "neighborhood degree sum").
6//!
7//! - **First neighborhood Zagreb index** `NM₁(G) = Σ_v S(v)²`
8//!   Introduced by Mondal et al. (2019). Vertex-level squared
9//!   neighborhood sums.
10//! - **Second neighborhood Zagreb index** `NM₂(G) = Σ_{(u,v)∈E} S(u)·S(v)`
11//!   Edge-level product of neighborhood sums.
12//! - **Neighborhood forgotten index** `NF(G) = Σ_v S(v)³`
13//!   Cube of neighborhood degree sums (analog of the forgotten
14//!   index but using S(v) instead of d(v)).
15
16#![allow(
17    clippy::cast_possible_truncation,
18    clippy::cast_precision_loss,
19    clippy::many_single_char_names,
20    clippy::needless_range_loop,
21    clippy::too_many_lines
22)]
23
24use crate::core::{Graph, IgraphResult};
25
26fn neighbor_degree_sums(graph: &Graph) -> IgraphResult<Vec<u64>> {
27    let n = graph.vcount();
28    let mut s = vec![0_u64; n as usize];
29
30    for v in 0..n {
31        let nbs = graph.neighbors(v)?;
32        let mut sum = 0_u64;
33        for nb in nbs {
34            sum = sum.saturating_add(graph.degree(nb)? as u64);
35        }
36        s[v as usize] = sum;
37    }
38
39    Ok(s)
40}
41
42/// Compute the first neighborhood Zagreb index.
43///
44/// `NM₁(G) = Σ_v S(v)²` where `S(v) = Σ_{u∈N(v)} d(u)`.
45///
46/// # Examples
47///
48/// ```
49/// use rust_igraph::{Graph, first_neighborhood_zagreb};
50///
51/// // K_3: each vertex has 2 neighbors of degree 2, S(v)=4
52/// // NM₁ = 3 × 16 = 48
53/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
54/// assert_eq!(first_neighborhood_zagreb(&g).unwrap(), 48);
55/// ```
56pub fn first_neighborhood_zagreb(graph: &Graph) -> IgraphResult<u64> {
57    let s = neighbor_degree_sums(graph)?;
58    let mut nm1 = 0_u64;
59
60    for &sv in &s {
61        nm1 = nm1.saturating_add(sv.saturating_mul(sv));
62    }
63
64    Ok(nm1)
65}
66
67/// Compute the second neighborhood Zagreb index.
68///
69/// `NM₂(G) = Σ_{(u,v)∈E} S(u) · S(v)`
70///
71/// Self-loops are skipped.
72///
73/// # Examples
74///
75/// ```
76/// use rust_igraph::{Graph, second_neighborhood_zagreb};
77///
78/// // K_3: S(v)=4 for all v, 3 edges → 3 × 16 = 48
79/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
80/// assert_eq!(second_neighborhood_zagreb(&g).unwrap(), 48);
81/// ```
82pub fn second_neighborhood_zagreb(graph: &Graph) -> IgraphResult<u64> {
83    let s = neighbor_degree_sums(graph)?;
84    let mut nm2 = 0_u64;
85
86    for (u, v) in graph.edges() {
87        if u == v {
88            continue;
89        }
90        let su = s[u as usize];
91        let sv = s[v as usize];
92        nm2 = nm2.saturating_add(su.saturating_mul(sv));
93    }
94
95    Ok(nm2)
96}
97
98/// Compute the neighborhood forgotten index.
99///
100/// `NF(G) = Σ_v S(v)³` where `S(v) = Σ_{u∈N(v)} d(u)`.
101///
102/// Vertex-level cubes of neighbor degree sums.
103///
104/// # Examples
105///
106/// ```
107/// use rust_igraph::{Graph, neighborhood_forgotten_index};
108///
109/// // K_3: S(v)=4 for all v, NF = 3 × 64 = 192
110/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
111/// assert_eq!(neighborhood_forgotten_index(&g).unwrap(), 192);
112/// ```
113pub fn neighborhood_forgotten_index(graph: &Graph) -> IgraphResult<u64> {
114    let s = neighbor_degree_sums(graph)?;
115    let mut nf = 0_u64;
116
117    for &sv in &s {
118        nf = nf.saturating_add(sv.saturating_mul(sv).saturating_mul(sv));
119    }
120
121    Ok(nf)
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    fn single_edge() -> Graph {
129        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
130    }
131
132    fn path3() -> Graph {
133        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
134    }
135
136    fn path4() -> Graph {
137        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
138    }
139
140    fn k3() -> Graph {
141        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
142    }
143
144    fn k4() -> Graph {
145        Graph::from_edges(
146            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
147            false,
148            Some(4),
149        )
150        .unwrap()
151    }
152
153    fn cycle4() -> Graph {
154        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
155    }
156
157    fn cycle5() -> Graph {
158        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
159    }
160
161    fn star5() -> Graph {
162        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
163    }
164
165    fn paw() -> Graph {
166        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
167    }
168
169    fn diamond() -> Graph {
170        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)], false, Some(4)).unwrap()
171    }
172
173    // Helper: compute S(v) for manual verification
174    fn compute_s(g: &Graph) -> Vec<u64> {
175        neighbor_degree_sums(g).unwrap()
176    }
177
178    // --- neighbor_degree_sums ---
179
180    #[test]
181    fn nds_single_edge() {
182        // v0: N={1}, d(1)=1 → S=1; v1: same
183        assert_eq!(compute_s(&single_edge()), vec![1, 1]);
184    }
185
186    #[test]
187    fn nds_path3() {
188        // v0: N={1}, d(1)=2 → S=2; v1: N={0,2}, d(0)+d(2)=1+1=2 → S=2; v2: N={1}, S=2
189        assert_eq!(compute_s(&path3()), vec![2, 2, 2]);
190    }
191
192    #[test]
193    fn nds_k3() {
194        // Each vertex: 2 neighbors of degree 2 → S=4
195        assert_eq!(compute_s(&k3()), vec![4, 4, 4]);
196    }
197
198    #[test]
199    fn nds_k4() {
200        // Each vertex: 3 neighbors of degree 3 → S=9
201        assert_eq!(compute_s(&k4()), vec![9, 9, 9, 9]);
202    }
203
204    #[test]
205    fn nds_star5() {
206        // center (d=4): 4 neighbors of d=1 → S=4
207        // leaf (d=1): 1 neighbor of d=4 → S=4
208        assert_eq!(compute_s(&star5()), vec![4, 4, 4, 4, 4]);
209    }
210
211    #[test]
212    fn nds_cycle4() {
213        // Each vertex: 2 neighbors of degree 2 → S=4
214        assert_eq!(compute_s(&cycle4()), vec![4, 4, 4, 4]);
215    }
216
217    #[test]
218    fn nds_paw() {
219        // degrees [2,2,3,1]
220        // v0: N={1,2}, d(1)+d(2)=2+3=5
221        // v1: N={0,2}, d(0)+d(2)=2+3=5
222        // v2: N={0,1,3}, d(0)+d(1)+d(3)=2+2+1=5
223        // v3: N={2}, d(2)=3
224        assert_eq!(compute_s(&paw()), vec![5, 5, 5, 3]);
225    }
226
227    // --- first_neighborhood_zagreb ---
228
229    #[test]
230    fn nm1_empty() {
231        let g = Graph::with_vertices(0);
232        assert_eq!(first_neighborhood_zagreb(&g).unwrap(), 0);
233    }
234
235    #[test]
236    fn nm1_isolated() {
237        let g = Graph::with_vertices(5);
238        assert_eq!(first_neighborhood_zagreb(&g).unwrap(), 0);
239    }
240
241    #[test]
242    fn nm1_single_edge() {
243        // S = [1,1], NM1 = 1+1 = 2
244        assert_eq!(first_neighborhood_zagreb(&single_edge()).unwrap(), 2);
245    }
246
247    #[test]
248    fn nm1_path3() {
249        // S = [2,2,2], NM1 = 4+4+4 = 12
250        assert_eq!(first_neighborhood_zagreb(&path3()).unwrap(), 12);
251    }
252
253    #[test]
254    fn nm1_path4() {
255        // degrees [1,2,2,1]
256        // S(0)=d(1)=2, S(1)=d(0)+d(2)=1+2=3, S(2)=d(1)+d(3)=2+1=3, S(3)=d(2)=2
257        // NM1 = 4+9+9+4 = 26
258        assert_eq!(first_neighborhood_zagreb(&path4()).unwrap(), 26);
259    }
260
261    #[test]
262    fn nm1_k3() {
263        // S = [4,4,4], NM1 = 48
264        assert_eq!(first_neighborhood_zagreb(&k3()).unwrap(), 48);
265    }
266
267    #[test]
268    fn nm1_k4() {
269        // S = [9,9,9,9], NM1 = 4×81 = 324
270        assert_eq!(first_neighborhood_zagreb(&k4()).unwrap(), 324);
271    }
272
273    #[test]
274    fn nm1_cycle4() {
275        // S = [4,4,4,4], NM1 = 4×16 = 64
276        assert_eq!(first_neighborhood_zagreb(&cycle4()).unwrap(), 64);
277    }
278
279    #[test]
280    fn nm1_cycle5() {
281        // S = [4,4,4,4,4], NM1 = 5×16 = 80
282        assert_eq!(first_neighborhood_zagreb(&cycle5()).unwrap(), 80);
283    }
284
285    #[test]
286    fn nm1_star5() {
287        // S = [4,4,4,4,4], NM1 = 5×16 = 80
288        assert_eq!(first_neighborhood_zagreb(&star5()).unwrap(), 80);
289    }
290
291    #[test]
292    fn nm1_paw() {
293        // S = [5,5,5,3], NM1 = 25+25+25+9 = 84
294        assert_eq!(first_neighborhood_zagreb(&paw()).unwrap(), 84);
295    }
296
297    #[test]
298    fn nm1_diamond() {
299        // degrees [3,3,2,2]
300        // S(0)=d(1)+d(2)+d(3)=3+2+2=7
301        // S(1)=d(0)+d(2)+d(3)=3+2+2=7
302        // S(2)=d(0)+d(1)=3+3=6
303        // S(3)=d(0)+d(1)=3+3=6
304        // NM1 = 49+49+36+36 = 170
305        assert_eq!(first_neighborhood_zagreb(&diamond()).unwrap(), 170);
306    }
307
308    #[test]
309    fn nm1_regular_formula() {
310        // r-regular: S(v) = r·r = r² for all v, NM1 = n·r⁴
311        for g in &[k3(), k4(), cycle4(), cycle5()] {
312            let n = u64::from(g.vcount());
313            let r = g.degree(0).unwrap() as u64;
314            assert_eq!(first_neighborhood_zagreb(g).unwrap(), n * r * r * r * r);
315        }
316    }
317
318    // --- second_neighborhood_zagreb ---
319
320    #[test]
321    fn nm2_empty() {
322        let g = Graph::with_vertices(0);
323        assert_eq!(second_neighborhood_zagreb(&g).unwrap(), 0);
324    }
325
326    #[test]
327    fn nm2_isolated() {
328        let g = Graph::with_vertices(5);
329        assert_eq!(second_neighborhood_zagreb(&g).unwrap(), 0);
330    }
331
332    #[test]
333    fn nm2_single_edge() {
334        // S=[1,1], edge: 1×1=1
335        assert_eq!(second_neighborhood_zagreb(&single_edge()).unwrap(), 1);
336    }
337
338    #[test]
339    fn nm2_path3() {
340        // S=[2,2,2], edges: (0,1):4, (1,2):4 → NM2=8
341        assert_eq!(second_neighborhood_zagreb(&path3()).unwrap(), 8);
342    }
343
344    #[test]
345    fn nm2_path4() {
346        // S=[2,3,3,2], edges: (0,1):6, (1,2):9, (2,3):6 → NM2=21
347        assert_eq!(second_neighborhood_zagreb(&path4()).unwrap(), 21);
348    }
349
350    #[test]
351    fn nm2_k3() {
352        // S=[4,4,4], 3 edges × 16 = 48
353        assert_eq!(second_neighborhood_zagreb(&k3()).unwrap(), 48);
354    }
355
356    #[test]
357    fn nm2_k4() {
358        // S=[9,9,9,9], 6 edges × 81 = 486
359        assert_eq!(second_neighborhood_zagreb(&k4()).unwrap(), 486);
360    }
361
362    #[test]
363    fn nm2_cycle4() {
364        // S=[4,4,4,4], 4 edges × 16 = 64
365        assert_eq!(second_neighborhood_zagreb(&cycle4()).unwrap(), 64);
366    }
367
368    #[test]
369    fn nm2_cycle5() {
370        // S=[4,4,4,4,4], 5 × 16 = 80
371        assert_eq!(second_neighborhood_zagreb(&cycle5()).unwrap(), 80);
372    }
373
374    #[test]
375    fn nm2_star5() {
376        // S=[4,4,4,4,4], 4 edges × 16 = 64
377        assert_eq!(second_neighborhood_zagreb(&star5()).unwrap(), 64);
378    }
379
380    #[test]
381    fn nm2_paw() {
382        // S=[5,5,5,3]
383        // (0,1):25, (0,2):25, (1,2):25, (2,3):15
384        // NM2 = 90
385        assert_eq!(second_neighborhood_zagreb(&paw()).unwrap(), 90);
386    }
387
388    #[test]
389    fn nm2_diamond() {
390        // S=[7,7,6,6]
391        // (0,1):49, (0,2):42, (0,3):42, (1,2):42, (1,3):42
392        // NM2 = 49+42+42+42+42 = 217
393        assert_eq!(second_neighborhood_zagreb(&diamond()).unwrap(), 217);
394    }
395
396    #[test]
397    fn nm2_regular_formula() {
398        // r-regular: NM2 = m·r⁴
399        for g in &[k3(), k4(), cycle4(), cycle5()] {
400            let m = g.ecount() as u64;
401            let r = g.degree(0).unwrap() as u64;
402            assert_eq!(second_neighborhood_zagreb(g).unwrap(), m * r * r * r * r);
403        }
404    }
405
406    // --- neighborhood_forgotten_index ---
407
408    #[test]
409    fn nf_empty() {
410        let g = Graph::with_vertices(0);
411        assert_eq!(neighborhood_forgotten_index(&g).unwrap(), 0);
412    }
413
414    #[test]
415    fn nf_isolated() {
416        let g = Graph::with_vertices(5);
417        assert_eq!(neighborhood_forgotten_index(&g).unwrap(), 0);
418    }
419
420    #[test]
421    fn nf_single_edge() {
422        // S=[1,1], NF = 1+1 = 2
423        assert_eq!(neighborhood_forgotten_index(&single_edge()).unwrap(), 2);
424    }
425
426    #[test]
427    fn nf_path3() {
428        // S=[2,2,2], NF = 3×8 = 24
429        assert_eq!(neighborhood_forgotten_index(&path3()).unwrap(), 24);
430    }
431
432    #[test]
433    fn nf_path4() {
434        // S=[2,3,3,2], NF = 8+27+27+8 = 70
435        assert_eq!(neighborhood_forgotten_index(&path4()).unwrap(), 70);
436    }
437
438    #[test]
439    fn nf_k3() {
440        // S=[4,4,4], NF = 3×64 = 192
441        assert_eq!(neighborhood_forgotten_index(&k3()).unwrap(), 192);
442    }
443
444    #[test]
445    fn nf_k4() {
446        // S=[9,9,9,9], NF = 4×729 = 2916
447        assert_eq!(neighborhood_forgotten_index(&k4()).unwrap(), 2916);
448    }
449
450    #[test]
451    fn nf_cycle4() {
452        // S=[4,4,4,4], NF = 4×64 = 256
453        assert_eq!(neighborhood_forgotten_index(&cycle4()).unwrap(), 256);
454    }
455
456    #[test]
457    fn nf_cycle5() {
458        // S=[4,4,4,4,4], NF = 5×64 = 320
459        assert_eq!(neighborhood_forgotten_index(&cycle5()).unwrap(), 320);
460    }
461
462    #[test]
463    fn nf_star5() {
464        // S=[4,4,4,4,4], NF = 5×64 = 320
465        assert_eq!(neighborhood_forgotten_index(&star5()).unwrap(), 320);
466    }
467
468    #[test]
469    fn nf_paw() {
470        // S=[5,5,5,3], NF = 125+125+125+27 = 402
471        assert_eq!(neighborhood_forgotten_index(&paw()).unwrap(), 402);
472    }
473
474    #[test]
475    fn nf_diamond() {
476        // S=[7,7,6,6], NF = 343+343+216+216 = 1118
477        assert_eq!(neighborhood_forgotten_index(&diamond()).unwrap(), 1118);
478    }
479
480    #[test]
481    fn nf_regular_formula() {
482        // r-regular: NF = n·r⁶
483        for g in &[k3(), k4(), cycle4(), cycle5()] {
484            let n = u64::from(g.vcount());
485            let r = g.degree(0).unwrap() as u64;
486            let r6 = r * r * r * r * r * r;
487            assert_eq!(neighborhood_forgotten_index(g).unwrap(), n * r6);
488        }
489    }
490
491    // --- cross-consistency ---
492
493    #[test]
494    fn nm1_nm2_coincide_for_regular() {
495        // For r-regular graphs: NM1/n = NM2/m since both equal r⁴·(n or m)
496        // Actually NM1 = n·r⁴ and NM2 = m·r⁴ and m=nr/2,
497        // so NM2 = NM1·r/2
498        for g in &[k3(), k4(), cycle4(), cycle5()] {
499            let nm1 = first_neighborhood_zagreb(g).unwrap();
500            let nm2 = second_neighborhood_zagreb(g).unwrap();
501            let r = g.degree(0).unwrap() as u64;
502            assert_eq!(nm2 * 2, nm1 * r);
503        }
504    }
505
506    #[test]
507    fn all_positive_for_nonempty_edges() {
508        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
509            assert!(first_neighborhood_zagreb(g).unwrap() > 0);
510            assert!(second_neighborhood_zagreb(g).unwrap() > 0);
511            assert!(neighborhood_forgotten_index(g).unwrap() > 0);
512        }
513    }
514}