Skip to main content

rust_igraph/algorithms/properties/
szeged_index.rs

1//! Distance-based topological indices (ALGO-TR-041).
2//!
3//! - **Szeged index** `Sz(G) = Σ_{(u,v)∈E} n_u(e)·n_v(e)`
4//!   where `n_u(e)` = number of vertices strictly closer to `u` than `v`.
5//! - **Revised Szeged index**
6//!   `Sz*(G) = Σ_{(u,v)∈E} (n_u + n₀/2)·(n_v + n₀/2)`
7//!   where `n₀` = vertices equidistant from `u` and `v`.
8//! - **Padmakar-Ivan (PI) index** `PI(G) = Σ_{(u,v)∈E} (n_u + n_v)`
9
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_precision_loss,
13    clippy::comparison_chain,
14    clippy::many_single_char_names,
15    clippy::needless_range_loop,
16    clippy::too_many_lines
17)]
18
19use crate::core::{Graph, IgraphResult};
20use std::collections::VecDeque;
21
22/// Compute the Szeged index of a graph.
23///
24/// `Sz(G) = Σ_{(u,v)∈E} n_u(e) · n_v(e)`
25///
26/// For each edge `(u, v)`, `n_u(e)` counts vertices strictly closer to
27/// `u` than to `v`, and `n_v(e)` counts vertices strictly closer to `v`
28/// than to `u`.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, szeged_index};
34///
35/// // Path 0-1-2: edge (0,1): n0=1, n1=2 → 2; edge (1,2): n1=2, n2=1 → 2
36/// // Sz = 2 + 2 = 4
37/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
38/// assert_eq!(szeged_index(&g).unwrap(), 4);
39/// ```
40pub fn szeged_index(graph: &Graph) -> IgraphResult<u64> {
41    let n = graph.vcount() as usize;
42    if n == 0 {
43        return Ok(0);
44    }
45
46    let dist = all_pairs_bfs(graph, n);
47    let mut sz: u64 = 0;
48
49    for (u, v) in graph.edges() {
50        let ui = u as usize;
51        let vi = v as usize;
52        if ui == vi {
53            continue;
54        }
55        let (nu, nv) = count_closer(&dist, n, ui, vi);
56        sz = sz.saturating_add((nu as u64).saturating_mul(nv as u64));
57    }
58
59    Ok(sz)
60}
61
62/// Compute the revised Szeged index of a graph.
63///
64/// `Sz*(G) = Σ_{(u,v)∈E} (n_u + n₀/2) · (n_v + n₀/2)`
65///
66/// Vertices equidistant from both endpoints are split evenly between
67/// the two sides.
68///
69/// # Examples
70///
71/// ```
72/// use rust_igraph::{Graph, revised_szeged_index};
73///
74/// // Single edge 0-1: n0=1, n1=1, equidistant=0 → (1)(1) = 1.0
75/// let g = Graph::from_edges(&[(0,1)], false, Some(2)).unwrap();
76/// assert!((revised_szeged_index(&g).unwrap() - 1.0).abs() < 1e-10);
77/// ```
78pub fn revised_szeged_index(graph: &Graph) -> IgraphResult<f64> {
79    let n = graph.vcount() as usize;
80    if n == 0 {
81        return Ok(0.0);
82    }
83
84    let dist = all_pairs_bfs(graph, n);
85    let mut sz_star: f64 = 0.0;
86
87    for (u, v) in graph.edges() {
88        let ui = u as usize;
89        let vi = v as usize;
90        if ui == vi {
91            continue;
92        }
93        let (nu, nv, n0) = count_closer_with_equidistant(&dist, n, ui, vi);
94        let su = nu as f64 + n0 as f64 / 2.0;
95        let sv = nv as f64 + n0 as f64 / 2.0;
96        sz_star += su * sv;
97    }
98
99    Ok(sz_star)
100}
101
102/// Compute the Padmakar-Ivan (PI) index of a graph.
103///
104/// `PI(G) = Σ_{(u,v)∈E} (n_u(e) + n_v(e))`
105///
106/// Equivalently, `PI(G) = Σ_{(u,v)∈E} (n - n₀(e))` where `n₀(e)` is
107/// the number of vertices equidistant from both endpoints (including
108/// unreachable vertices).
109///
110/// # Examples
111///
112/// ```
113/// use rust_igraph::{Graph, pi_index};
114///
115/// // Path 0-1-2: edge (0,1): nu=1,nv=2 → 3; edge (1,2): nu=2,nv=1 → 3
116/// // PI = 6
117/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
118/// assert_eq!(pi_index(&g).unwrap(), 6);
119/// ```
120pub fn pi_index(graph: &Graph) -> IgraphResult<u64> {
121    let n = graph.vcount() as usize;
122    if n == 0 {
123        return Ok(0);
124    }
125
126    let dist = all_pairs_bfs(graph, n);
127    let mut pi: u64 = 0;
128
129    for (u, v) in graph.edges() {
130        let ui = u as usize;
131        let vi = v as usize;
132        if ui == vi {
133            continue;
134        }
135        let (nu, nv) = count_closer(&dist, n, ui, vi);
136        pi = pi.saturating_add((nu as u64).saturating_add(nv as u64));
137    }
138
139    Ok(pi)
140}
141
142fn count_closer(dist: &[u32], n: usize, u: usize, v: usize) -> (usize, usize) {
143    let mut nu = 0_usize;
144    let mut nv = 0_usize;
145    for w in 0..n {
146        let du = dist[u * n + w];
147        let dv = dist[v * n + w];
148        if du == u32::MAX || dv == u32::MAX {
149            continue;
150        }
151        if du < dv {
152            nu += 1;
153        } else if dv < du {
154            nv += 1;
155        }
156    }
157    (nu, nv)
158}
159
160fn count_closer_with_equidistant(
161    dist: &[u32],
162    n: usize,
163    u: usize,
164    v: usize,
165) -> (usize, usize, usize) {
166    let mut nu = 0_usize;
167    let mut nv = 0_usize;
168    let mut n0 = 0_usize;
169    for w in 0..n {
170        let du = dist[u * n + w];
171        let dv = dist[v * n + w];
172        if du == u32::MAX || dv == u32::MAX {
173            continue;
174        }
175        if du < dv {
176            nu += 1;
177        } else if dv < du {
178            nv += 1;
179        } else {
180            n0 += 1;
181        }
182    }
183    (nu, nv, n0)
184}
185
186fn all_pairs_bfs(graph: &Graph, n: usize) -> Vec<u32> {
187    let adj = build_adj_list(graph, n);
188    let mut dist = vec![u32::MAX; n * n];
189    for src in 0..n {
190        bfs_distances(&adj, n, src, &mut dist);
191    }
192    dist
193}
194
195fn build_adj_list(graph: &Graph, n: usize) -> Vec<Vec<usize>> {
196    let mut adj = vec![Vec::new(); n];
197    for (u, v) in graph.edges() {
198        let ui = u as usize;
199        let vi = v as usize;
200        adj[ui].push(vi);
201        if !graph.is_directed() && ui != vi {
202            adj[vi].push(ui);
203        }
204    }
205    adj
206}
207
208fn bfs_distances(adj: &[Vec<usize>], n: usize, src: usize, dist: &mut [u32]) {
209    dist[src * n + src] = 0;
210    let mut queue = VecDeque::new();
211    queue.push_back(src);
212    while let Some(v) = queue.pop_front() {
213        let d = dist[src * n + v];
214        for &w in &adj[v] {
215            if dist[src * n + w] == u32::MAX {
216                dist[src * n + w] = d.saturating_add(1);
217                queue.push_back(w);
218            }
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    fn single_edge() -> Graph {
228        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
229    }
230
231    fn path3() -> Graph {
232        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
233    }
234
235    fn path4() -> Graph {
236        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
237    }
238
239    fn k3() -> Graph {
240        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
241    }
242
243    fn k4() -> Graph {
244        Graph::from_edges(
245            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
246            false,
247            Some(4),
248        )
249        .unwrap()
250    }
251
252    fn cycle4() -> Graph {
253        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
254    }
255
256    fn cycle6() -> Graph {
257        Graph::from_edges(
258            &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)],
259            false,
260            Some(6),
261        )
262        .unwrap()
263    }
264
265    fn star5() -> Graph {
266        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
267    }
268
269    // --- szeged_index ---
270
271    #[test]
272    fn sz_empty() {
273        let g = Graph::with_vertices(0);
274        assert_eq!(szeged_index(&g).unwrap(), 0);
275    }
276
277    #[test]
278    fn sz_no_edges() {
279        let g = Graph::with_vertices(3);
280        assert_eq!(szeged_index(&g).unwrap(), 0);
281    }
282
283    #[test]
284    fn sz_single_edge() {
285        // n_u=1, n_v=1 → 1
286        assert_eq!(szeged_index(&single_edge()).unwrap(), 1);
287    }
288
289    #[test]
290    fn sz_path3() {
291        // edge (0,1): closer to 0={0}, closer to 1={1,2} → 1·2 = 2
292        // edge (1,2): closer to 1={0,1}, closer to 2={2} → 2·1 = 2
293        // Sz = 4
294        assert_eq!(szeged_index(&path3()).unwrap(), 4);
295    }
296
297    #[test]
298    fn sz_path4() {
299        // edge (0,1): closer to 0={0}, closer to 1={2,3} → 1·2 = 2
300        // edge (1,2): closer to 1={0}, closer to 2={3} → 1·1 = 1 ... wait
301        // Actually: d(0,1)=1,d(0,2)=2 so d(w=0,u=1)=1 < d(w=0,v=2)=2 → w=0 closer to u=1
302        // d(w=3,u=1)=2, d(w=3,v=2)=1 → w=3 closer to v=2
303        // d(w=1,u=1)=0, d(w=1,v=2)=1 → w=1 closer to u=1
304        // d(w=2,u=1)=1, d(w=2,v=2)=0 → w=2 closer to v=2
305        // n_u=2, n_v=2 → 4
306        // edge (0,1): d(w=0,0)=0<d(w=0,1)=1 → closer to 0
307        //             d(w=1,0)=1>d(w=1,1)=0 → closer to 1
308        //             d(w=2,0)=2>d(w=2,1)=1 → closer to 1
309        //             d(w=3,0)=3>d(w=3,1)=2 → closer to 1
310        //             n0=1, n1=3 → 3
311        // edge (1,2): n1=2(w=0,w=1), n2=2(w=2,w=3) → 4
312        // edge (2,3): n2=3(w=0,w=1,w=2), n3=1(w=3) → 3
313        // Sz = 3 + 4 + 3 = 10
314        assert_eq!(szeged_index(&path4()).unwrap(), 10);
315    }
316
317    #[test]
318    fn sz_k3() {
319        // All distances = 1. For edge (0,1):
320        // d(w=0,0)=0 < d(w=0,1)=1 → closer to 0
321        // d(w=1,0)=1 > d(w=1,1)=0 → closer to 1
322        // d(w=2,0)=1 = d(w=2,1)=1 → equidistant
323        // n0=1, n1=1 → 1
324        // By symmetry all 3 edges give 1·1 = 1 → Sz = 3
325        assert_eq!(szeged_index(&k3()).unwrap(), 3);
326    }
327
328    #[test]
329    fn sz_k4() {
330        // For any edge (u,v) in K4: n_u=1(u itself), n_v=1(v itself),
331        // other 2 vertices equidistant → 1·1 = 1
332        // 6 edges → Sz = 6
333        assert_eq!(szeged_index(&k4()).unwrap(), 6);
334    }
335
336    #[test]
337    fn sz_cycle4() {
338        // C4: vertices 0-1-2-3-0
339        // edge (0,1): d(0,0)=0<d(0,1)=1 closer to 0
340        //             d(1,0)=1>d(1,1)=0 closer to 1
341        //             d(2,0)=2>d(2,1)=1 closer to 1
342        //             d(3,0)=1<d(3,1)=2 closer to 0
343        //             n0=2, n1=2 → 4
344        // By symmetry all 4 edges give 4 → Sz = 16
345        assert_eq!(szeged_index(&cycle4()).unwrap(), 16);
346    }
347
348    #[test]
349    fn sz_star5() {
350        // Star: center=0, leaves 1,2,3,4
351        // edge (0,1): closer to 0={0,2,3,4}(d to 0 < d to 1),
352        //             closer to 1={1} → 4·1 = 4
353        // By symmetry all 4 edges give 4 → Sz = 16
354        assert_eq!(szeged_index(&star5()).unwrap(), 16);
355    }
356
357    #[test]
358    fn sz_cycle6() {
359        // C6: for edge (0,1):
360        // d(w,0) vs d(w,1):
361        // w=0: 0<1 → closer to 0
362        // w=1: 1>0 → closer to 1
363        // w=2: 2>1 → closer to 1
364        // w=3: 3=3 (diameter vertex, equidistant) → tie  ... wait
365        // Actually in C6, d(3,0)=3, d(3,1)=2 → closer to 1
366        // w=4: d(4,0)=2, d(4,1)=3 → closer to 0
367        // w=5: d(5,0)=1, d(5,1)=2 → closer to 0
368        // n0=3, n1=3 → 9
369        // All 6 edges by symmetry → 54
370        assert_eq!(szeged_index(&cycle6()).unwrap(), 54);
371    }
372
373    // --- revised_szeged_index ---
374
375    #[test]
376    fn rsz_empty() {
377        let g = Graph::with_vertices(0);
378        assert!((revised_szeged_index(&g).unwrap() - 0.0).abs() < 1e-10);
379    }
380
381    #[test]
382    fn rsz_single_edge() {
383        // n_u=1, n_v=1, n_0=0 → (1)(1) = 1
384        assert!((revised_szeged_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
385    }
386
387    #[test]
388    fn rsz_k3() {
389        // For edge (0,1): n_u=1, n_v=1, n_0=1
390        // (1+0.5)(1+0.5) = 2.25
391        // 3 edges → 6.75
392        let r = revised_szeged_index(&k3()).unwrap();
393        assert!((r - 6.75).abs() < 1e-10);
394    }
395
396    #[test]
397    fn rsz_path3() {
398        // edge (0,1): closer to 0={0}, closer to 1={2}, equidist={1}(d=0,d=1 → not equal)
399        // Wait: d(w=1,u=0)=1, d(w=1,v=1)=0 → closer to 1
400        // So n_u=1, n_v=2, n_0=0 → (1)(2) = 2? No...
401        // edge(0,1): w=0: d(0,0)=0 < d(0,1)=1 → closer to 0 ✓
402        //            w=1: d(1,0)=1 > d(1,1)=0 → closer to 1 ✓
403        //            w=2: d(2,0)=2 > d(2,1)=1 → closer to 1 ✓
404        // n_u=1, n_v=2, n_0=0 → revised = (1)(2) = 2
405        // edge(1,2): w=0: d(0,1)=1 < d(0,2)=2 → closer to 1
406        //            w=1: d(1,1)=0 < d(1,2)=1 → closer to 1
407        //            w=2: d(2,1)=1 > d(2,2)=0 → closer to 2
408        // n_u=2, n_v=1, n_0=0 → revised = (2)(1) = 2
409        // Total = 4
410        let r = revised_szeged_index(&path3()).unwrap();
411        assert!((r - 4.0).abs() < 1e-10);
412    }
413
414    #[test]
415    fn rsz_equals_sz_for_trees() {
416        // Trees have no equidistant vertices (for endpoints of any edge
417        // in a tree, every vertex is strictly closer to one endpoint)
418        // So revised Szeged = Szeged for trees
419        for g in &[single_edge(), path3(), path4(), star5()] {
420            let sz = szeged_index(g).unwrap() as f64;
421            let rsz = revised_szeged_index(g).unwrap();
422            assert!(
423                (sz - rsz).abs() < 1e-10,
424                "Sz={sz}, Sz*={rsz} should be equal for trees"
425            );
426        }
427    }
428
429    #[test]
430    fn rsz_geq_sz() {
431        // Revised Szeged >= Szeged always
432        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), cycle6()] {
433            let sz = szeged_index(g).unwrap() as f64;
434            let rsz = revised_szeged_index(g).unwrap();
435            assert!(rsz >= sz - 1e-10, "Sz*={rsz} < Sz={sz}");
436        }
437    }
438
439    // --- pi_index ---
440
441    #[test]
442    fn pi_empty() {
443        let g = Graph::with_vertices(0);
444        assert_eq!(pi_index(&g).unwrap(), 0);
445    }
446
447    #[test]
448    fn pi_single_edge() {
449        // n_u=1, n_v=1 → 2
450        assert_eq!(pi_index(&single_edge()).unwrap(), 2);
451    }
452
453    #[test]
454    fn pi_path3() {
455        // edge(0,1): nu=1, nv=2 → 3
456        // edge(1,2): nu=2, nv=1 → 3
457        // PI = 6
458        assert_eq!(pi_index(&path3()).unwrap(), 6);
459    }
460
461    #[test]
462    fn pi_k3() {
463        // Each edge: nu=1, nv=1 → 2
464        // 3 edges → 6
465        assert_eq!(pi_index(&k3()).unwrap(), 6);
466    }
467
468    #[test]
469    fn pi_cycle4() {
470        // Each edge: nu=2, nv=2 → 4
471        // 4 edges → 16
472        assert_eq!(pi_index(&cycle4()).unwrap(), 16);
473    }
474
475    #[test]
476    fn pi_star5() {
477        // Each edge: nu=4, nv=1 → 5
478        // 4 edges → 20
479        assert_eq!(pi_index(&star5()).unwrap(), 20);
480    }
481
482    // --- cross-consistency ---
483
484    #[test]
485    fn szeged_leq_wiener_squared_over_m() {
486        // For connected graphs: Sz(G) >= W(G) (Klavžar-Rajapakse-Gutman)
487        // where W is the Wiener index
488        for g in &[
489            single_edge(),
490            path3(),
491            path4(),
492            k3(),
493            k4(),
494            cycle4(),
495            star5(),
496        ] {
497            let sz = szeged_index(g).unwrap() as f64;
498            let w = crate::algorithms::properties::distance_spectrum::wiener_index(g).unwrap();
499            assert!(
500                sz >= w - 1e-10,
501                "Szeged {sz} < Wiener {w}, violates Sz >= W"
502            );
503        }
504    }
505
506    #[test]
507    fn pi_equals_sum_nu_nv() {
508        // PI = Σ (nu + nv) and Sz = Σ nu·nv
509        // Verify PI and Sz are consistent
510        for g in &[path3(), k3(), cycle4(), star5()] {
511            let n = g.vcount() as usize;
512            let dist = all_pairs_bfs(g, n);
513            let mut sum_product: u64 = 0;
514            let mut sum_sum: u64 = 0;
515            for (u, v) in g.edges() {
516                let ui = u as usize;
517                let vi = v as usize;
518                if ui == vi {
519                    continue;
520                }
521                let (nu, nv) = count_closer(&dist, n, ui, vi);
522                sum_product += (nu as u64) * (nv as u64);
523                sum_sum += (nu as u64) + (nv as u64);
524            }
525            assert_eq!(sum_product, szeged_index(g).unwrap());
526            assert_eq!(sum_sum, pi_index(g).unwrap());
527        }
528    }
529
530    #[test]
531    fn szeged_equals_wiener_for_trees() {
532        // For trees, Szeged index = Wiener index
533        for g in &[single_edge(), path3(), path4(), star5()] {
534            let sz = szeged_index(g).unwrap() as f64;
535            let w = crate::algorithms::properties::distance_spectrum::wiener_index(g).unwrap();
536            assert!((sz - w).abs() < 1e-10, "Szeged {sz} != Wiener {w} for tree");
537        }
538    }
539}