Skip to main content

rust_igraph/algorithms/properties/
zagreb_connection.rs

1//! Zagreb connection indices (ALGO-TR-051).
2//!
3//! The **connection number** `τ(v)` of a vertex v is the number of
4//! vertices at distance exactly 2 from v (i.e., second neighbours).
5//!
6//! - **First Zagreb connection index** `ZC₁(G) = Σ_{v∈V} τ(v)²`
7//! - **Second Zagreb connection index** `ZC₂(G) = Σ_{(u,v)∈E} τ(u)·τ(v)`
8//! - **Modified first Zagreb connection** `ZC₁*(G) = Σ_{(u,v)∈E} (τ(u)+τ(v))`
9//!
10//! Introduced by Ali & Trinajstić (2018). These indices account for
11//! second-order connectivity and provide better predictive power than
12//! classical Zagreb indices for some molecular properties.
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};
23use std::collections::HashSet;
24
25fn connection_numbers(graph: &Graph) -> IgraphResult<Vec<f64>> {
26    let n = graph.vcount() as usize;
27    let mut tau = vec![0.0_f64; n];
28
29    for v in 0..graph.vcount() {
30        let mut dist1 = HashSet::new();
31        for nb in graph.neighbors(v)? {
32            dist1.insert(nb);
33        }
34        let mut dist2 = HashSet::new();
35        for &nb1 in &dist1 {
36            for nb2 in graph.neighbors(nb1)? {
37                if nb2 != v && !dist1.contains(&nb2) {
38                    dist2.insert(nb2);
39                }
40            }
41        }
42        tau[v as usize] = dist2.len() as f64;
43    }
44
45    Ok(tau)
46}
47
48/// Compute the first Zagreb connection index.
49///
50/// `ZC₁(G) = Σ_{v∈V} τ(v)²`
51///
52/// where `τ(v)` = number of vertices at distance exactly 2 from v.
53///
54/// # Examples
55///
56/// ```
57/// use rust_igraph::{Graph, first_zagreb_connection};
58///
59/// // Path 0-1-2: τ(0)=1, τ(1)=0, τ(2)=1
60/// // ZC₁ = 1+0+1 = 2
61/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
62/// assert!((first_zagreb_connection(&g).unwrap() - 2.0).abs() < 1e-10);
63/// ```
64pub fn first_zagreb_connection(graph: &Graph) -> IgraphResult<f64> {
65    let tau = connection_numbers(graph)?;
66    Ok(tau.iter().map(|t| t * t).sum())
67}
68
69/// Compute the second Zagreb connection index.
70///
71/// `ZC₂(G) = Σ_{(u,v)∈E} τ(u) · τ(v)`
72///
73/// # Examples
74///
75/// ```
76/// use rust_igraph::{Graph, second_zagreb_connection};
77///
78/// // Path 0-1-2: τ=[1,0,1]
79/// // (0,1): 1·0=0, (1,2): 0·1=0 → ZC₂=0
80/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
81/// assert!((second_zagreb_connection(&g).unwrap()).abs() < 1e-10);
82/// ```
83pub fn second_zagreb_connection(graph: &Graph) -> IgraphResult<f64> {
84    let tau = connection_numbers(graph)?;
85    let mut zc2 = 0.0_f64;
86    for (u, v) in graph.edges() {
87        if u == v {
88            continue;
89        }
90        zc2 += tau[u as usize] * tau[v as usize];
91    }
92    Ok(zc2)
93}
94
95/// Compute the modified first Zagreb connection index.
96///
97/// `ZC₁*(G) = Σ_{(u,v)∈E} (τ(u) + τ(v))`
98///
99/// # Examples
100///
101/// ```
102/// use rust_igraph::{Graph, modified_first_zagreb_connection};
103///
104/// // K_3: τ(v)=0 for all v (all pairs are direct neighbours)
105/// // ZC₁* = 0
106/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
107/// assert!((modified_first_zagreb_connection(&g).unwrap()).abs() < 1e-10);
108/// ```
109pub fn modified_first_zagreb_connection(graph: &Graph) -> IgraphResult<f64> {
110    let tau = connection_numbers(graph)?;
111    let mut zcs = 0.0_f64;
112    for (u, v) in graph.edges() {
113        if u == v {
114            continue;
115        }
116        zcs += tau[u as usize] + tau[v as usize];
117    }
118    Ok(zcs)
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    fn single_edge() -> Graph {
126        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
127    }
128
129    fn path3() -> Graph {
130        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
131    }
132
133    fn path4() -> Graph {
134        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
135    }
136
137    fn path5() -> Graph {
138        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap()
139    }
140
141    fn k3() -> Graph {
142        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
143    }
144
145    fn k4() -> Graph {
146        Graph::from_edges(
147            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
148            false,
149            Some(4),
150        )
151        .unwrap()
152    }
153
154    fn cycle4() -> Graph {
155        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
156    }
157
158    fn cycle5() -> Graph {
159        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
160    }
161
162    fn cycle6() -> Graph {
163        Graph::from_edges(
164            &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)],
165            false,
166            Some(6),
167        )
168        .unwrap()
169    }
170
171    fn star5() -> Graph {
172        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
173    }
174
175    fn paw() -> Graph {
176        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
177    }
178
179    // --- connection_numbers (helper) ---
180
181    #[test]
182    fn tau_empty() {
183        let g = Graph::with_vertices(0);
184        let tau = connection_numbers(&g).unwrap();
185        assert!(tau.is_empty());
186    }
187
188    #[test]
189    fn tau_single_vertex() {
190        let g = Graph::with_vertices(1);
191        let tau = connection_numbers(&g).unwrap();
192        assert!((tau[0]).abs() < 1e-10);
193    }
194
195    #[test]
196    fn tau_single_edge() {
197        // 0-1: τ(0)=0 (no vertex at dist 2), τ(1)=0
198        let tau = connection_numbers(&single_edge()).unwrap();
199        assert!((tau[0]).abs() < 1e-10);
200        assert!((tau[1]).abs() < 1e-10);
201    }
202
203    #[test]
204    fn tau_path3() {
205        // 0-1-2: τ(0)=1(v2), τ(1)=0, τ(2)=1(v0)
206        let tau = connection_numbers(&path3()).unwrap();
207        assert!((tau[0] - 1.0).abs() < 1e-10);
208        assert!((tau[1]).abs() < 1e-10);
209        assert!((tau[2] - 1.0).abs() < 1e-10);
210    }
211
212    #[test]
213    fn tau_path4() {
214        // 0-1-2-3: τ(0)=1(v2), τ(1)=1(v3), τ(2)=1(v0), τ(3)=1(v1)
215        let tau = connection_numbers(&path4()).unwrap();
216        assert!((tau[0] - 1.0).abs() < 1e-10);
217        assert!((tau[1] - 1.0).abs() < 1e-10);
218        assert!((tau[2] - 1.0).abs() < 1e-10);
219        assert!((tau[3] - 1.0).abs() < 1e-10);
220    }
221
222    #[test]
223    fn tau_path5() {
224        // 0-1-2-3-4: τ(0)=1, τ(1)=1, τ(2)=2, τ(3)=1, τ(4)=1
225        let tau = connection_numbers(&path5()).unwrap();
226        assert!((tau[0] - 1.0).abs() < 1e-10);
227        assert!((tau[1] - 1.0).abs() < 1e-10);
228        assert!((tau[2] - 2.0).abs() < 1e-10);
229        assert!((tau[3] - 1.0).abs() < 1e-10);
230        assert!((tau[4] - 1.0).abs() < 1e-10);
231    }
232
233    #[test]
234    fn tau_k3() {
235        // K_3: all vertices are adjacent, so τ(v)=0
236        let tau = connection_numbers(&k3()).unwrap();
237        for &t in &tau {
238            assert!((t).abs() < 1e-10);
239        }
240    }
241
242    #[test]
243    fn tau_k4() {
244        let tau = connection_numbers(&k4()).unwrap();
245        for &t in &tau {
246            assert!((t).abs() < 1e-10);
247        }
248    }
249
250    #[test]
251    fn tau_cycle4() {
252        // C_4 (0-1-2-3-0): each vertex has 2 neighbours and 1 vertex at dist 2
253        let tau = connection_numbers(&cycle4()).unwrap();
254        for &t in &tau {
255            assert!((t - 1.0).abs() < 1e-10);
256        }
257    }
258
259    #[test]
260    fn tau_cycle5() {
261        // C_5: each vertex has 2 neighbours at dist 1, 2 at dist 2
262        let tau = connection_numbers(&cycle5()).unwrap();
263        for &t in &tau {
264            assert!((t - 2.0).abs() < 1e-10);
265        }
266    }
267
268    #[test]
269    fn tau_cycle6() {
270        // C_6: each vertex: 2 at dist 1, 2 at dist 2, 1 at dist 3
271        let tau = connection_numbers(&cycle6()).unwrap();
272        for &t in &tau {
273            assert!((t - 2.0).abs() < 1e-10);
274        }
275    }
276
277    #[test]
278    fn tau_star5() {
279        // center τ(0) = 0 (all leaves adjacent to center)
280        // each leaf: τ(leaf) = 3 (other leaves at dist 2)
281        let tau = connection_numbers(&star5()).unwrap();
282        assert!((tau[0]).abs() < 1e-10);
283        for i in 1..5 {
284            assert!((tau[i] - 3.0).abs() < 1e-10);
285        }
286    }
287
288    #[test]
289    fn tau_paw() {
290        // 0-1, 1-2, 0-2, 2-3. degrees [2,2,3,1]
291        // τ(0): dist1={1,2}, dist2={3} → τ=1
292        // τ(1): dist1={0,2}, dist2={3} → τ=1
293        // τ(2): dist1={0,1,3}, dist2={} → τ=0
294        // τ(3): dist1={2}, dist2={0,1} → τ=2
295        let tau = connection_numbers(&paw()).unwrap();
296        assert!((tau[0] - 1.0).abs() < 1e-10);
297        assert!((tau[1] - 1.0).abs() < 1e-10);
298        assert!((tau[2]).abs() < 1e-10);
299        assert!((tau[3] - 2.0).abs() < 1e-10);
300    }
301
302    // --- first_zagreb_connection ---
303
304    #[test]
305    fn zc1_empty() {
306        let g = Graph::with_vertices(0);
307        assert!((first_zagreb_connection(&g).unwrap()).abs() < 1e-10);
308    }
309
310    #[test]
311    fn zc1_single_edge() {
312        assert!((first_zagreb_connection(&single_edge()).unwrap()).abs() < 1e-10);
313    }
314
315    #[test]
316    fn zc1_path3() {
317        // τ=[1,0,1], ZC₁=1+0+1=2
318        assert!((first_zagreb_connection(&path3()).unwrap() - 2.0).abs() < 1e-10);
319    }
320
321    #[test]
322    fn zc1_path4() {
323        // τ=[1,1,1,1], ZC₁=4
324        assert!((first_zagreb_connection(&path4()).unwrap() - 4.0).abs() < 1e-10);
325    }
326
327    #[test]
328    fn zc1_k3() {
329        // τ=[0,0,0], ZC₁=0
330        assert!((first_zagreb_connection(&k3()).unwrap()).abs() < 1e-10);
331    }
332
333    #[test]
334    fn zc1_k4() {
335        assert!((first_zagreb_connection(&k4()).unwrap()).abs() < 1e-10);
336    }
337
338    #[test]
339    fn zc1_cycle4() {
340        // τ=[1,1,1,1], ZC₁=4
341        assert!((first_zagreb_connection(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
342    }
343
344    #[test]
345    fn zc1_cycle5() {
346        // τ=[2,2,2,2,2], ZC₁=20
347        assert!((first_zagreb_connection(&cycle5()).unwrap() - 20.0).abs() < 1e-10);
348    }
349
350    #[test]
351    fn zc1_star5() {
352        // τ=[0,3,3,3,3], ZC₁=0+9+9+9+9=36
353        assert!((first_zagreb_connection(&star5()).unwrap() - 36.0).abs() < 1e-10);
354    }
355
356    #[test]
357    fn zc1_paw() {
358        // τ=[1,1,0,2], ZC₁=1+1+0+4=6
359        assert!((first_zagreb_connection(&paw()).unwrap() - 6.0).abs() < 1e-10);
360    }
361
362    // --- second_zagreb_connection ---
363
364    #[test]
365    fn zc2_empty() {
366        let g = Graph::with_vertices(0);
367        assert!((second_zagreb_connection(&g).unwrap()).abs() < 1e-10);
368    }
369
370    #[test]
371    fn zc2_single_edge() {
372        assert!((second_zagreb_connection(&single_edge()).unwrap()).abs() < 1e-10);
373    }
374
375    #[test]
376    fn zc2_path3() {
377        // τ=[1,0,1]: edges (0,1):0, (1,2):0 → 0
378        assert!((second_zagreb_connection(&path3()).unwrap()).abs() < 1e-10);
379    }
380
381    #[test]
382    fn zc2_path4() {
383        // τ=[1,1,1,1]: edges (0,1):1, (1,2):1, (2,3):1 → 3
384        assert!((second_zagreb_connection(&path4()).unwrap() - 3.0).abs() < 1e-10);
385    }
386
387    #[test]
388    fn zc2_k3() {
389        assert!((second_zagreb_connection(&k3()).unwrap()).abs() < 1e-10);
390    }
391
392    #[test]
393    fn zc2_cycle4() {
394        // τ=[1,1,1,1], each edge: 1·1=1, 4 edges → 4
395        assert!((second_zagreb_connection(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
396    }
397
398    #[test]
399    fn zc2_cycle5() {
400        // τ=[2,2,2,2,2], each edge: 4, 5 edges → 20
401        assert!((second_zagreb_connection(&cycle5()).unwrap() - 20.0).abs() < 1e-10);
402    }
403
404    #[test]
405    fn zc2_star5() {
406        // τ=[0,3,3,3,3], edges are (0,leaf): 0·3=0 → ZC₂=0
407        assert!((second_zagreb_connection(&star5()).unwrap()).abs() < 1e-10);
408    }
409
410    #[test]
411    fn zc2_paw() {
412        // τ=[1,1,0,2]
413        // (0,1): 1·1=1, (0,2): 1·0=0, (1,2): 1·0=0, (2,3): 0·2=0
414        // ZC₂ = 1
415        assert!((second_zagreb_connection(&paw()).unwrap() - 1.0).abs() < 1e-10);
416    }
417
418    // --- modified_first_zagreb_connection ---
419
420    #[test]
421    fn zcs_empty() {
422        let g = Graph::with_vertices(0);
423        assert!((modified_first_zagreb_connection(&g).unwrap()).abs() < 1e-10);
424    }
425
426    #[test]
427    fn zcs_single_edge() {
428        assert!((modified_first_zagreb_connection(&single_edge()).unwrap()).abs() < 1e-10);
429    }
430
431    #[test]
432    fn zcs_path3() {
433        // τ=[1,0,1]: (0,1):1+0=1, (1,2):0+1=1 → 2
434        assert!((modified_first_zagreb_connection(&path3()).unwrap() - 2.0).abs() < 1e-10);
435    }
436
437    #[test]
438    fn zcs_path4() {
439        // τ=[1,1,1,1]: each edge: 2, 3 edges → 6
440        assert!((modified_first_zagreb_connection(&path4()).unwrap() - 6.0).abs() < 1e-10);
441    }
442
443    #[test]
444    fn zcs_k3() {
445        assert!((modified_first_zagreb_connection(&k3()).unwrap()).abs() < 1e-10);
446    }
447
448    #[test]
449    fn zcs_cycle4() {
450        // τ=[1,1,1,1], each edge: 2, 4 edges → 8
451        assert!((modified_first_zagreb_connection(&cycle4()).unwrap() - 8.0).abs() < 1e-10);
452    }
453
454    #[test]
455    fn zcs_cycle5() {
456        // τ=[2,2,2,2,2], each edge: 4, 5 edges → 20
457        assert!((modified_first_zagreb_connection(&cycle5()).unwrap() - 20.0).abs() < 1e-10);
458    }
459
460    #[test]
461    fn zcs_star5() {
462        // τ=[0,3,3,3,3], edges (0,leaf): 0+3=3, 4 edges → 12
463        assert!((modified_first_zagreb_connection(&star5()).unwrap() - 12.0).abs() < 1e-10);
464    }
465
466    #[test]
467    fn zcs_paw() {
468        // τ=[1,1,0,2]
469        // (0,1):2, (0,2):1, (1,2):1, (2,3):2 → 6
470        assert!((modified_first_zagreb_connection(&paw()).unwrap() - 6.0).abs() < 1e-10);
471    }
472
473    // --- cross-consistency ---
474
475    #[test]
476    fn all_zero_for_complete() {
477        for g in &[k3(), k4()] {
478            assert!((first_zagreb_connection(g).unwrap()).abs() < 1e-10);
479            assert!((second_zagreb_connection(g).unwrap()).abs() < 1e-10);
480            assert!((modified_first_zagreb_connection(g).unwrap()).abs() < 1e-10);
481        }
482    }
483
484    #[test]
485    fn all_nonneg() {
486        for g in &[
487            single_edge(),
488            path3(),
489            path4(),
490            k3(),
491            cycle4(),
492            cycle5(),
493            star5(),
494            paw(),
495        ] {
496            assert!(first_zagreb_connection(g).unwrap() >= -1e-10);
497            assert!(second_zagreb_connection(g).unwrap() >= -1e-10);
498            assert!(modified_first_zagreb_connection(g).unwrap() >= -1e-10);
499        }
500    }
501
502    #[test]
503    fn zcs_equals_2sum_tau_deg() {
504        // ZC₁*(G) = Σ_{(u,v)∈E} (τ(u)+τ(v)) = Σ_v τ(v)·d(v)
505        for g in &[path3(), cycle4(), star5(), paw()] {
506            let tau = connection_numbers(g).unwrap();
507            let mut vertex_sum = 0.0_f64;
508            for v in 0..g.vcount() {
509                vertex_sum += tau[v as usize] * g.degree(v).unwrap() as f64;
510            }
511            let zcs = modified_first_zagreb_connection(g).unwrap();
512            assert!((zcs - vertex_sum).abs() < 1e-8);
513        }
514    }
515}