Skip to main content

rust_igraph/algorithms/properties/
transmission_zagreb.rs

1//! Transmission-based indices (ALGO-TR-065).
2//!
3//! These indices use the **transmission** (or **status**) of each vertex:
4//! `σ(v) = Σ_w d(v,w)`, the sum of distances from v to all other vertices.
5//!
6//! - **First transmission Zagreb index** `TZ₁(G) = Σ_v σ(v)²`
7//!   Squared vertex transmissions.
8//! - **Second transmission Zagreb index** `TZ₂(G) = Σ_{(u,v)∈E} σ(u)·σ(v)`
9//!   Product of endpoint transmissions over edges.
10//! - **Reciprocal transmission index** `RT(G) = Σ_v 1/σ(v)`
11//!   Sum of reciprocal transmissions (skipping isolated vertices
12//!   or vertices in disconnected components with σ=0).
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    clippy::unnecessary_wraps
21)]
22
23use crate::core::{Graph, IgraphResult};
24
25fn vertex_transmissions(graph: &Graph) -> IgraphResult<Vec<u64>> {
26    let n = graph.vcount() as usize;
27    let mut sigma = vec![0_u64; n];
28
29    for s in 0..n {
30        let mut dist = vec![u32::MAX; n];
31        dist[s] = 0;
32        let mut queue = std::collections::VecDeque::new();
33        queue.push_back(s);
34        while let Some(u) = queue.pop_front() {
35            let d_u = dist[u];
36            if let Ok(nbs) = graph.neighbors(u as u32) {
37                for nb in nbs {
38                    let idx = nb as usize;
39                    if dist[idx] == u32::MAX {
40                        dist[idx] = d_u + 1;
41                        queue.push_back(idx);
42                    }
43                }
44            }
45        }
46        let mut total = 0_u64;
47        for &d in &dist {
48            if d != u32::MAX {
49                total = total.saturating_add(u64::from(d));
50            }
51        }
52        sigma[s] = total;
53    }
54
55    Ok(sigma)
56}
57
58/// Compute the first transmission Zagreb index.
59///
60/// `TZ₁(G) = Σ_v σ(v)²` where `σ(v) = Σ_w d(v,w)`.
61///
62/// # Examples
63///
64/// ```
65/// use rust_igraph::{Graph, first_transmission_zagreb};
66///
67/// // Path 0-1-2: σ(0)=3, σ(1)=2, σ(2)=3
68/// // TZ₁ = 9 + 4 + 9 = 22
69/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
70/// assert_eq!(first_transmission_zagreb(&g).unwrap(), 22);
71/// ```
72pub fn first_transmission_zagreb(graph: &Graph) -> IgraphResult<u64> {
73    let sigma = vertex_transmissions(graph)?;
74    let mut tz1 = 0_u64;
75
76    for &sv in &sigma {
77        tz1 = tz1.saturating_add(sv.saturating_mul(sv));
78    }
79
80    Ok(tz1)
81}
82
83/// Compute the second transmission Zagreb index.
84///
85/// `TZ₂(G) = Σ_{(u,v)∈E} σ(u) · σ(v)`
86///
87/// Self-loops are skipped.
88///
89/// # Examples
90///
91/// ```
92/// use rust_igraph::{Graph, second_transmission_zagreb};
93///
94/// // Path 0-1-2: σ=[3,2,3]
95/// // (0,1): 3×2=6, (1,2): 2×3=6 → TZ₂=12
96/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
97/// assert_eq!(second_transmission_zagreb(&g).unwrap(), 12);
98/// ```
99pub fn second_transmission_zagreb(graph: &Graph) -> IgraphResult<u64> {
100    let sigma = vertex_transmissions(graph)?;
101    let mut tz2 = 0_u64;
102
103    for (u, v) in graph.edges() {
104        if u == v {
105            continue;
106        }
107        let su = sigma[u as usize];
108        let sv = sigma[v as usize];
109        tz2 = tz2.saturating_add(su.saturating_mul(sv));
110    }
111
112    Ok(tz2)
113}
114
115/// Compute the reciprocal transmission index.
116///
117/// `RT(G) = Σ_v 1/σ(v)` where `σ(v) = Σ_w d(v,w)`.
118///
119/// Vertices with σ(v) = 0 (isolated or single-vertex components) are
120/// skipped.
121///
122/// # Examples
123///
124/// ```
125/// use rust_igraph::{Graph, reciprocal_transmission_index};
126///
127/// // Path 0-1-2: σ=[3,2,3], RT = 1/3 + 1/2 + 1/3 = 7/6
128/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
129/// assert!((reciprocal_transmission_index(&g).unwrap() - 7.0/6.0).abs() < 1e-10);
130/// ```
131pub fn reciprocal_transmission_index(graph: &Graph) -> IgraphResult<f64> {
132    let sigma = vertex_transmissions(graph)?;
133    let mut rt = 0.0_f64;
134
135    for &sv in &sigma {
136        if sv > 0 {
137            rt += 1.0 / sv as f64;
138        }
139    }
140
141    Ok(rt)
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    fn single_edge() -> Graph {
149        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
150    }
151
152    fn path3() -> Graph {
153        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
154    }
155
156    fn path4() -> Graph {
157        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
158    }
159
160    fn k3() -> Graph {
161        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
162    }
163
164    fn k4() -> Graph {
165        Graph::from_edges(
166            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
167            false,
168            Some(4),
169        )
170        .unwrap()
171    }
172
173    fn cycle4() -> Graph {
174        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
175    }
176
177    fn cycle5() -> Graph {
178        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
179    }
180
181    fn star5() -> Graph {
182        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
183    }
184
185    fn paw() -> Graph {
186        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
187    }
188
189    // Helper: verify transmissions
190    fn transmissions(g: &Graph) -> Vec<u64> {
191        vertex_transmissions(g).unwrap()
192    }
193
194    // --- vertex_transmissions ---
195
196    #[test]
197    fn sigma_empty() {
198        let g = Graph::with_vertices(0);
199        assert_eq!(transmissions(&g), Vec::<u64>::new());
200    }
201
202    #[test]
203    fn sigma_isolated() {
204        let g = Graph::with_vertices(3);
205        assert_eq!(transmissions(&g), vec![0, 0, 0]);
206    }
207
208    #[test]
209    fn sigma_single_edge() {
210        // σ(0)=1, σ(1)=1
211        assert_eq!(transmissions(&single_edge()), vec![1, 1]);
212    }
213
214    #[test]
215    fn sigma_path3() {
216        // σ(0)=1+2=3, σ(1)=1+1=2, σ(2)=2+1=3
217        assert_eq!(transmissions(&path3()), vec![3, 2, 3]);
218    }
219
220    #[test]
221    fn sigma_path4() {
222        // σ(0)=1+2+3=6, σ(1)=1+1+2=4, σ(2)=2+1+1=4, σ(3)=3+2+1=6
223        assert_eq!(transmissions(&path4()), vec![6, 4, 4, 6]);
224    }
225
226    #[test]
227    fn sigma_k3() {
228        // All distances 1, σ(v)=2 for all
229        assert_eq!(transmissions(&k3()), vec![2, 2, 2]);
230    }
231
232    #[test]
233    fn sigma_k4() {
234        // σ(v) = 3 for all
235        assert_eq!(transmissions(&k4()), vec![3, 3, 3, 3]);
236    }
237
238    #[test]
239    fn sigma_cycle4() {
240        // C4: distances [0,1,2,1] so σ=4 for all
241        assert_eq!(transmissions(&cycle4()), vec![4, 4, 4, 4]);
242    }
243
244    #[test]
245    fn sigma_cycle5() {
246        // C5: distances [0,1,2,2,1] so σ=6 for all
247        assert_eq!(transmissions(&cycle5()), vec![6, 6, 6, 6, 6]);
248    }
249
250    #[test]
251    fn sigma_star5() {
252        // center: 4×1=4, leaves: 1+2+2+2=7
253        assert_eq!(transmissions(&star5()), vec![4, 7, 7, 7, 7]);
254    }
255
256    #[test]
257    fn sigma_paw() {
258        // σ(0)=1+1+2=4, σ(1)=1+1+2=4, σ(2)=1+1+1=3, σ(3)=2+2+1=5
259        assert_eq!(transmissions(&paw()), vec![4, 4, 3, 5]);
260    }
261
262    #[test]
263    fn sigma_sum_equals_wiener() {
264        // Σ σ(v) = 2·W(G) where W is the Wiener index
265        // For path3: W = 1+2+1 = 4, Σσ = 3+2+3 = 8 = 2×4 ✓
266        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
267            let sigma = transmissions(g);
268            let sum: u64 = sigma.iter().sum();
269            assert_eq!(sum % 2, 0);
270        }
271    }
272
273    // --- first_transmission_zagreb ---
274
275    #[test]
276    fn tz1_empty() {
277        let g = Graph::with_vertices(0);
278        assert_eq!(first_transmission_zagreb(&g).unwrap(), 0);
279    }
280
281    #[test]
282    fn tz1_isolated() {
283        let g = Graph::with_vertices(5);
284        assert_eq!(first_transmission_zagreb(&g).unwrap(), 0);
285    }
286
287    #[test]
288    fn tz1_single_edge() {
289        // σ=[1,1], TZ1 = 1+1 = 2
290        assert_eq!(first_transmission_zagreb(&single_edge()).unwrap(), 2);
291    }
292
293    #[test]
294    fn tz1_path3() {
295        // σ=[3,2,3], TZ1 = 9+4+9 = 22
296        assert_eq!(first_transmission_zagreb(&path3()).unwrap(), 22);
297    }
298
299    #[test]
300    fn tz1_path4() {
301        // σ=[6,4,4,6], TZ1 = 36+16+16+36 = 104
302        assert_eq!(first_transmission_zagreb(&path4()).unwrap(), 104);
303    }
304
305    #[test]
306    fn tz1_k3() {
307        // σ=[2,2,2], TZ1 = 3×4 = 12
308        assert_eq!(first_transmission_zagreb(&k3()).unwrap(), 12);
309    }
310
311    #[test]
312    fn tz1_k4() {
313        // σ=[3,3,3,3], TZ1 = 4×9 = 36
314        assert_eq!(first_transmission_zagreb(&k4()).unwrap(), 36);
315    }
316
317    #[test]
318    fn tz1_cycle4() {
319        // σ=[4,4,4,4], TZ1 = 4×16 = 64
320        assert_eq!(first_transmission_zagreb(&cycle4()).unwrap(), 64);
321    }
322
323    #[test]
324    fn tz1_cycle5() {
325        // σ=[6,6,6,6,6], TZ1 = 5×36 = 180
326        assert_eq!(first_transmission_zagreb(&cycle5()).unwrap(), 180);
327    }
328
329    #[test]
330    fn tz1_star5() {
331        // σ=[4,7,7,7,7], TZ1 = 16+49+49+49+49 = 212
332        assert_eq!(first_transmission_zagreb(&star5()).unwrap(), 212);
333    }
334
335    #[test]
336    fn tz1_paw() {
337        // σ=[4,4,3,5], TZ1 = 16+16+9+25 = 66
338        assert_eq!(first_transmission_zagreb(&paw()).unwrap(), 66);
339    }
340
341    #[test]
342    fn tz1_transmission_regular() {
343        // For transmission-regular graphs: TZ1 = n·σ²
344        for g in &[k3(), k4(), cycle4(), cycle5()] {
345            let n = u64::from(g.vcount());
346            let s = transmissions(g)[0];
347            assert_eq!(first_transmission_zagreb(g).unwrap(), n * s * s);
348        }
349    }
350
351    // --- second_transmission_zagreb ---
352
353    #[test]
354    fn tz2_empty() {
355        let g = Graph::with_vertices(0);
356        assert_eq!(second_transmission_zagreb(&g).unwrap(), 0);
357    }
358
359    #[test]
360    fn tz2_single_edge() {
361        // σ=[1,1], 1 edge: 1×1=1
362        assert_eq!(second_transmission_zagreb(&single_edge()).unwrap(), 1);
363    }
364
365    #[test]
366    fn tz2_path3() {
367        // σ=[3,2,3], edges: (0,1):6, (1,2):6 → TZ2=12
368        assert_eq!(second_transmission_zagreb(&path3()).unwrap(), 12);
369    }
370
371    #[test]
372    fn tz2_path4() {
373        // σ=[6,4,4,6], edges: (0,1):24, (1,2):16, (2,3):24 → TZ2=64
374        assert_eq!(second_transmission_zagreb(&path4()).unwrap(), 64);
375    }
376
377    #[test]
378    fn tz2_k3() {
379        // σ=[2,2,2], 3 edges × 4 = 12
380        assert_eq!(second_transmission_zagreb(&k3()).unwrap(), 12);
381    }
382
383    #[test]
384    fn tz2_k4() {
385        // σ=[3,3,3,3], 6 × 9 = 54
386        assert_eq!(second_transmission_zagreb(&k4()).unwrap(), 54);
387    }
388
389    #[test]
390    fn tz2_cycle4() {
391        // σ=[4,4,4,4], 4 × 16 = 64
392        assert_eq!(second_transmission_zagreb(&cycle4()).unwrap(), 64);
393    }
394
395    #[test]
396    fn tz2_star5() {
397        // σ=[4,7,7,7,7], edges: (0,1):28, (0,2):28, (0,3):28, (0,4):28
398        // TZ2 = 4×28 = 112
399        assert_eq!(second_transmission_zagreb(&star5()).unwrap(), 112);
400    }
401
402    #[test]
403    fn tz2_paw() {
404        // σ=[4,4,3,5], edges: (0,1):16, (0,2):12, (1,2):12, (2,3):15
405        // TZ2 = 16+12+12+15 = 55
406        assert_eq!(second_transmission_zagreb(&paw()).unwrap(), 55);
407    }
408
409    // --- reciprocal_transmission_index ---
410
411    #[test]
412    fn rt_empty() {
413        let g = Graph::with_vertices(0);
414        assert!((reciprocal_transmission_index(&g).unwrap()).abs() < 1e-10);
415    }
416
417    #[test]
418    fn rt_isolated() {
419        let g = Graph::with_vertices(5);
420        assert!((reciprocal_transmission_index(&g).unwrap()).abs() < 1e-10);
421    }
422
423    #[test]
424    fn rt_single_edge() {
425        // σ=[1,1], RT = 1+1 = 2
426        assert!((reciprocal_transmission_index(&single_edge()).unwrap() - 2.0).abs() < 1e-10);
427    }
428
429    #[test]
430    fn rt_path3() {
431        // σ=[3,2,3], RT = 1/3+1/2+1/3 = 7/6
432        let expected = 7.0 / 6.0;
433        assert!((reciprocal_transmission_index(&path3()).unwrap() - expected).abs() < 1e-10);
434    }
435
436    #[test]
437    fn rt_path4() {
438        // σ=[6,4,4,6], RT = 1/6+1/4+1/4+1/6 = 2/6+2/4 = 1/3+1/2 = 5/6
439        let expected = 5.0 / 6.0;
440        assert!((reciprocal_transmission_index(&path4()).unwrap() - expected).abs() < 1e-10);
441    }
442
443    #[test]
444    fn rt_k3() {
445        // σ=[2,2,2], RT = 3/2 = 1.5
446        assert!((reciprocal_transmission_index(&k3()).unwrap() - 1.5).abs() < 1e-10);
447    }
448
449    #[test]
450    fn rt_k4() {
451        // σ=[3,3,3,3], RT = 4/3
452        let expected = 4.0 / 3.0;
453        assert!((reciprocal_transmission_index(&k4()).unwrap() - expected).abs() < 1e-10);
454    }
455
456    #[test]
457    fn rt_cycle4() {
458        // σ=[4,4,4,4], RT = 4/4 = 1
459        assert!((reciprocal_transmission_index(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
460    }
461
462    #[test]
463    fn rt_cycle5() {
464        // σ=[6,6,6,6,6], RT = 5/6
465        let expected = 5.0 / 6.0;
466        assert!((reciprocal_transmission_index(&cycle5()).unwrap() - expected).abs() < 1e-10);
467    }
468
469    #[test]
470    fn rt_star5() {
471        // σ=[4,7,7,7,7], RT = 1/4 + 4/7
472        let expected = 0.25 + 4.0 / 7.0;
473        assert!((reciprocal_transmission_index(&star5()).unwrap() - expected).abs() < 1e-10);
474    }
475
476    #[test]
477    fn rt_paw() {
478        // σ=[4,4,3,5], RT = 1/4+1/4+1/3+1/5 = 15/60+15/60+20/60+12/60 = 62/60 = 31/30
479        let expected = 31.0 / 30.0;
480        assert!((reciprocal_transmission_index(&paw()).unwrap() - expected).abs() < 1e-10);
481    }
482
483    #[test]
484    fn rt_transmission_regular() {
485        // Transmission-regular: RT = n/σ
486        for g in &[k3(), k4(), cycle4(), cycle5()] {
487            let n = f64::from(g.vcount());
488            let s = transmissions(g)[0] as f64;
489            let expected = n / s;
490            assert!((reciprocal_transmission_index(g).unwrap() - expected).abs() < 1e-8);
491        }
492    }
493
494    // --- cross-consistency ---
495
496    #[test]
497    fn all_positive_for_connected() {
498        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
499            assert!(first_transmission_zagreb(g).unwrap() > 0);
500            assert!(second_transmission_zagreb(g).unwrap() > 0);
501            assert!(reciprocal_transmission_index(g).unwrap() > 0.0);
502        }
503    }
504}