Skip to main content

rust_igraph/algorithms/properties/
szeged_edge.rs

1//! Edge-Szeged and Graovac-Ghorbani indices (ALGO-TR-059).
2//!
3//! Szeged-like indices using edge and vertex proximity counts.
4//!
5//! - **Edge-Szeged index** `Sz_e(G) = Σ_{(u,v)∈E} m_u(e) · m_v(e)`
6//!   where `m_u(e)` = number of edges closer to u than to v.
7//!   Introduced by Gutman & Ashrafi (2008).
8//! - **Edge-PI index** `PI_e(G) = Σ_{(u,v)∈E} [m_u(e) + m_v(e)]`
9//!   Edge version of the Padmakar-Ivan index.
10//! - **Graovac-Ghorbani index** `GG(G) = Σ_{(u,v)∈E} ln(n_u·n_v) / ln(n_u+n_v)`
11//!   where `n_u(e)` = vertices closer to u than v for edge (u,v).
12//!   Introduced by Graovac & Ghorbani (2010). Undefined (skipped)
13//!   when `n_u + n_v ≤ 1`.
14
15#![allow(
16    clippy::cast_possible_truncation,
17    clippy::cast_precision_loss,
18    clippy::many_single_char_names,
19    clippy::needless_range_loop,
20    clippy::too_many_lines
21)]
22
23use crate::core::{Graph, IgraphResult};
24
25fn all_pairs_bfs(graph: &Graph, n: usize) -> Vec<Vec<u32>> {
26    let mut dist = vec![vec![u32::MAX; n]; n];
27    for s in 0..n {
28        dist[s][s] = 0;
29        let mut queue = std::collections::VecDeque::new();
30        queue.push_back(s);
31        while let Some(u) = queue.pop_front() {
32            let d_u = dist[s][u];
33            if let Ok(nbs) = graph.neighbors(u as u32) {
34                for nb in nbs {
35                    let idx = nb as usize;
36                    if dist[s][idx] == u32::MAX {
37                        dist[s][idx] = d_u + 1;
38                        queue.push_back(idx);
39                    }
40                }
41            }
42        }
43    }
44    dist
45}
46
47/// Compute the edge-Szeged index.
48///
49/// `Sz_e(G) = Σ_{(u,v)∈E} m_u(e) · m_v(e)`
50///
51/// where `m_u(e)` is the number of edges whose midpoint is closer to u
52/// than to v. An edge (a,b) is closer to u when
53/// `dist(u,a) + dist(u,b) < dist(v,a) + dist(v,b)`.
54///
55/// # Examples
56///
57/// ```
58/// use rust_igraph::{Graph, edge_szeged_index};
59///
60/// // Path 0-1-2: 2 edges
61/// // edge (0,1): m_0=0, m_1=1 (edge (1,2) closer to 1) → 0
62/// // edge (1,2): m_1=1 (edge (0,1) closer to 1), m_2=0 → 0
63/// // Sz_e = 0
64/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
65/// assert_eq!(edge_szeged_index(&g).unwrap(), 0);
66/// ```
67pub fn edge_szeged_index(graph: &Graph) -> IgraphResult<u64> {
68    let n = graph.vcount() as usize;
69    if n == 0 {
70        return Ok(0);
71    }
72
73    let dist = all_pairs_bfs(graph, n);
74    let edges: Vec<(u32, u32)> = graph.edges().collect();
75    let m = edges.len();
76    let mut sz_e = 0_u64;
77
78    for &(u, v) in &edges {
79        let ui = u as usize;
80        let vi = v as usize;
81        let mut mu = 0_u64;
82        let mut mv = 0_u64;
83
84        for i in 0..m {
85            let (a, b) = edges[i];
86            let ai = a as usize;
87            let bi = b as usize;
88
89            if dist[ui][ai] == u32::MAX
90                || dist[ui][bi] == u32::MAX
91                || dist[vi][ai] == u32::MAX
92                || dist[vi][bi] == u32::MAX
93            {
94                continue;
95            }
96
97            let du = u64::from(dist[ui][ai]) + u64::from(dist[ui][bi]);
98            let dv = u64::from(dist[vi][ai]) + u64::from(dist[vi][bi]);
99
100            match du.cmp(&dv) {
101                std::cmp::Ordering::Less => mu += 1,
102                std::cmp::Ordering::Greater => mv += 1,
103                std::cmp::Ordering::Equal => {}
104            }
105        }
106
107        sz_e = sz_e.saturating_add(mu.saturating_mul(mv));
108    }
109
110    Ok(sz_e)
111}
112
113/// Compute the edge-PI index.
114///
115/// `PI_e(G) = Σ_{(u,v)∈E} [m_u(e) + m_v(e)]`
116///
117/// Edge version of the Padmakar-Ivan index. Uses the same proximity
118/// definition as the edge-Szeged index.
119///
120/// # Examples
121///
122/// ```
123/// use rust_igraph::{Graph, edge_pi_index};
124///
125/// // Path 0-1-2:
126/// // edge (0,1): m_0=0, m_1=1 → sum=1
127/// // edge (1,2): m_1=1, m_2=0 → sum=1
128/// // PI_e = 2
129/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
130/// assert_eq!(edge_pi_index(&g).unwrap(), 2);
131/// ```
132pub fn edge_pi_index(graph: &Graph) -> IgraphResult<u64> {
133    let n = graph.vcount() as usize;
134    if n == 0 {
135        return Ok(0);
136    }
137
138    let dist = all_pairs_bfs(graph, n);
139    let edges: Vec<(u32, u32)> = graph.edges().collect();
140    let m = edges.len();
141    let mut pi_e = 0_u64;
142
143    for &(u, v) in &edges {
144        let ui = u as usize;
145        let vi = v as usize;
146        let mut mu = 0_u64;
147        let mut mv = 0_u64;
148
149        for i in 0..m {
150            let (a, b) = edges[i];
151            let ai = a as usize;
152            let bi = b as usize;
153
154            if dist[ui][ai] == u32::MAX
155                || dist[ui][bi] == u32::MAX
156                || dist[vi][ai] == u32::MAX
157                || dist[vi][bi] == u32::MAX
158            {
159                continue;
160            }
161
162            let du = u64::from(dist[ui][ai]) + u64::from(dist[ui][bi]);
163            let dv = u64::from(dist[vi][ai]) + u64::from(dist[vi][bi]);
164
165            match du.cmp(&dv) {
166                std::cmp::Ordering::Less => mu += 1,
167                std::cmp::Ordering::Greater => mv += 1,
168                std::cmp::Ordering::Equal => {}
169            }
170        }
171
172        pi_e = pi_e.saturating_add(mu + mv);
173    }
174
175    Ok(pi_e)
176}
177
178/// Compute the Graovac-Ghorbani index.
179///
180/// `GG(G) = Σ_{(u,v)∈E} ln(n_u · n_v) / ln(n_u + n_v)`
181///
182/// where `n_u(e)` = number of vertices closer to u than to v.
183/// Terms where `n_u + n_v ≤ 1` or either count is zero are skipped.
184///
185/// # Examples
186///
187/// ```
188/// use rust_igraph::{Graph, graovac_ghorbani_index};
189///
190/// // K_3: all edges have n_u=1, n_v=1 → ln(1)/ln(2) = 0
191/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
192/// assert!((graovac_ghorbani_index(&g).unwrap()).abs() < 1e-10);
193/// ```
194pub fn graovac_ghorbani_index(graph: &Graph) -> IgraphResult<f64> {
195    let n = graph.vcount() as usize;
196    if n == 0 {
197        return Ok(0.0);
198    }
199
200    let dist = all_pairs_bfs(graph, n);
201    let mut gg = 0.0_f64;
202
203    for (u, v) in graph.edges() {
204        if u == v {
205            continue;
206        }
207        let ui = u as usize;
208        let vi = v as usize;
209        let mut nu = 0_u64;
210        let mut nv = 0_u64;
211
212        for w in 0..n {
213            if dist[ui][w] == u32::MAX || dist[vi][w] == u32::MAX {
214                continue;
215            }
216            match dist[ui][w].cmp(&dist[vi][w]) {
217                std::cmp::Ordering::Less => nu += 1,
218                std::cmp::Ordering::Greater => nv += 1,
219                std::cmp::Ordering::Equal => {}
220            }
221        }
222
223        if nu > 0 && nv > 0 {
224            let sum = nu + nv;
225            if sum > 1 {
226                let prod = (nu as f64) * (nv as f64);
227                gg += prod.ln() / (sum as f64).ln();
228            }
229        }
230    }
231
232    Ok(gg)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    fn single_edge() -> Graph {
240        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
241    }
242
243    fn path3() -> Graph {
244        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
245    }
246
247    fn path4() -> Graph {
248        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
249    }
250
251    fn k3() -> Graph {
252        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
253    }
254
255    fn k4() -> Graph {
256        Graph::from_edges(
257            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
258            false,
259            Some(4),
260        )
261        .unwrap()
262    }
263
264    fn cycle4() -> Graph {
265        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
266    }
267
268    fn cycle5() -> Graph {
269        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
270    }
271
272    fn star5() -> Graph {
273        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
274    }
275
276    fn paw() -> Graph {
277        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
278    }
279
280    // --- edge_szeged_index ---
281
282    #[test]
283    fn esz_empty() {
284        let g = Graph::with_vertices(0);
285        assert_eq!(edge_szeged_index(&g).unwrap(), 0);
286    }
287
288    #[test]
289    fn esz_single_edge() {
290        // Only 1 edge, no other edges to count → m_u=0, m_v=0 → 0
291        assert_eq!(edge_szeged_index(&single_edge()).unwrap(), 0);
292    }
293
294    #[test]
295    fn esz_path3() {
296        // edge (0,1): other edge (1,2) → du=0+1=1, dv=1+0=1 → tie, neither counted
297        // Actually: dist(0,1)+dist(0,2)=0+2=2 vs dist(1,1)+dist(1,2)=0+1=1
298        // Wait, m_u counts edges closer to u. For edge e=(0,1), checking edge f=(1,2):
299        // dist(0,1)+dist(0,2) = 1+2 = 3 (dist from 0 to endpoints of f)
300        // dist(1,1)+dist(1,2) = 0+1 = 1 (dist from 1 to endpoints of f)
301        // 1 < 3 → f closer to 1 → m_1 += 1
302        // So m_0=0, m_1=1 → 0·1 = 0
303        //
304        // edge (1,2): checking edge f=(0,1):
305        // dist(1,0)+dist(1,1) = 1+0 = 1
306        // dist(2,0)+dist(2,1) = 2+1 = 3
307        // 1 < 3 → f closer to 1 → m_1 += 1
308        // So m_1=1, m_2=0 → 1·0 = 0
309        // Sz_e = 0
310        assert_eq!(edge_szeged_index(&path3()).unwrap(), 0);
311    }
312
313    #[test]
314    fn esz_path4() {
315        // edges: (0,1), (1,2), (2,3)
316        // edge (0,1): check (1,2): d0=1+2=3, d1=0+1=1 → m1; check (2,3): d0=2+3=5, d1=1+2=3 → m1
317        // m_0=0, m_1=2 → 0
318        // edge (1,2): check (0,1): d1=1+0=1, d2=2+1=3 → m1; check (2,3): d1=1+2=3, d2=0+1=1 → m2
319        // m_1=1, m_2=1 → 1
320        // edge (2,3): check (0,1): d2=2+1=3, d3=3+2=5 → m2; check (1,2): d2=0+1=1, d3=1+2=3 → m2
321        // m_2=2, m_3=0 → 0
322        // Sz_e = 0 + 1 + 0 = 1
323        assert_eq!(edge_szeged_index(&path4()).unwrap(), 1);
324    }
325
326    #[test]
327    fn esz_k3() {
328        // All distances are 1. For any edge (u,v), other edge (a,b):
329        // du_e = dist(u,a)+dist(u,b), dv_e = dist(v,a)+dist(v,b)
330        // In K3, all non-self distances are 1. For edge (0,1), check edge (0,2):
331        // d0 = 0+1 = 1, d1 = 1+1 = 2 → closer to 0 → m_0++
332        // Check edge (1,2): d0 = 1+1 = 2, d1 = 0+1 = 1 → closer to 1 → m_1++
333        // m_0=1, m_1=1 → 1. By symmetry each edge contributes 1.
334        // Sz_e = 3
335        assert_eq!(edge_szeged_index(&k3()).unwrap(), 3);
336    }
337
338    #[test]
339    fn esz_k4() {
340        // For K4, for any edge (u,v), consider another edge (a,b):
341        // Case 1: u=a or u=b → du includes dist(u,u)=0, so du < dv (tie-break to u side)
342        // Actually let's compute precisely.
343        // For edge (0,1), check edge (0,2): d0=0+1=1, d1=1+1=2 → m0++
344        // check edge (0,3): d0=0+1=1, d1=1+1=2 → m0++
345        // check edge (1,2): d0=1+1=2, d1=0+1=1 → m1++
346        // check edge (1,3): d0=1+1=2, d1=0+1=1 → m1++
347        // check edge (2,3): d0=1+1=2, d1=1+1=2 → tie
348        // m_0=2, m_1=2 → 4. By symmetry, 6 edges × 4 = 24
349        assert_eq!(edge_szeged_index(&k4()).unwrap(), 24);
350    }
351
352    #[test]
353    fn esz_cycle4() {
354        // C4: edges (0,1),(1,2),(2,3),(3,0), distances: adjacent=1, opposite=2
355        // For edge (0,1), check (1,2): d0=1+2=3, d1=0+1=1 → m1
356        // check (2,3): d0=2+1=3, d1=1+2=3 → tie
357        // check (3,0): d0=0+1=1, d1=1+2=3 → m0
358        // m_0=1, m_1=1 → 1. By symmetry, 4 edges × 1 = 4
359        assert_eq!(edge_szeged_index(&cycle4()).unwrap(), 4);
360    }
361
362    #[test]
363    fn esz_star5() {
364        // Star: center=0, leaves 1-4. All edges (0,k).
365        // For edge (0,1), check edge (0,2): d0=0+1=1, d1=2+1=3 → m0
366        // check (0,3): same → m0. check (0,4): same → m0.
367        // m_0=3, m_1=0 → 0. By symmetry all 4 edges give 0.
368        assert_eq!(edge_szeged_index(&star5()).unwrap(), 0);
369    }
370
371    #[test]
372    fn esz_computes_ok() {
373        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
374            let _ = edge_szeged_index(g).unwrap();
375        }
376    }
377
378    // --- edge_pi_index ---
379
380    #[test]
381    fn epi_empty() {
382        let g = Graph::with_vertices(0);
383        assert_eq!(edge_pi_index(&g).unwrap(), 0);
384    }
385
386    #[test]
387    fn epi_single_edge() {
388        assert_eq!(edge_pi_index(&single_edge()).unwrap(), 0);
389    }
390
391    #[test]
392    fn epi_path3() {
393        // edge (0,1): m0=0, m1=1 → 1; edge (1,2): m1=1, m2=0 → 1
394        // PI_e = 2
395        assert_eq!(edge_pi_index(&path3()).unwrap(), 2);
396    }
397
398    #[test]
399    fn epi_path4() {
400        // edge (0,1): 0+2=2; edge (1,2): 1+1=2; edge (2,3): 2+0=2
401        // PI_e = 6
402        assert_eq!(edge_pi_index(&path4()).unwrap(), 6);
403    }
404
405    #[test]
406    fn epi_k3() {
407        // each edge: 1+1=2, 3 edges → 6
408        assert_eq!(edge_pi_index(&k3()).unwrap(), 6);
409    }
410
411    #[test]
412    fn epi_k4() {
413        // each edge: 2+2=4, 6 edges → 24
414        assert_eq!(edge_pi_index(&k4()).unwrap(), 24);
415    }
416
417    #[test]
418    fn epi_cycle4() {
419        // each edge: 1+1=2, 4 edges → 8
420        assert_eq!(edge_pi_index(&cycle4()).unwrap(), 8);
421    }
422
423    #[test]
424    fn epi_star5() {
425        // each edge: 3+0=3, 4 edges → 12
426        assert_eq!(edge_pi_index(&star5()).unwrap(), 12);
427    }
428
429    #[test]
430    fn epi_computes_ok() {
431        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
432            let _ = edge_pi_index(g).unwrap();
433        }
434    }
435
436    // --- graovac_ghorbani_index ---
437
438    #[test]
439    fn gg_empty() {
440        let g = Graph::with_vertices(0);
441        assert!((graovac_ghorbani_index(&g).unwrap()).abs() < 1e-10);
442    }
443
444    #[test]
445    fn gg_single_edge() {
446        // edge (0,1): n_0=1(vertex 0), n_1=1(vertex 1) → ln(1)/ln(2) = 0
447        assert!((graovac_ghorbani_index(&single_edge()).unwrap()).abs() < 1e-10);
448    }
449
450    #[test]
451    fn gg_path3() {
452        // edge (0,1): v0→n0(d=0<1), v1→n1(d=1>0), v2→n1(d=2>1)
453        // n_0=1, n_1=2 → ln(2)/ln(3)
454        // edge (1,2): v0→n1(d=1<2), v1→n1(d=0<1), v2→n2(d=2>0→wait: d(1,2)=1,d(2,2)=0)
455        // v0: d(1,0)=1 < d(2,0)=2 → n_1; v1: d(1,1)=0 < d(2,1)=1 → n_1; v2: d(1,2)=1 > d(2,2)=0 → n_2
456        // n_1=2, n_2=1 → ln(2)/ln(3)
457        // GG = 2·ln(2)/ln(3)
458        let expected = 2.0 * 2.0_f64.ln() / 3.0_f64.ln();
459        assert!((graovac_ghorbani_index(&path3()).unwrap() - expected).abs() < 1e-10);
460    }
461
462    #[test]
463    fn gg_k3() {
464        // For edge (0,1): n_0=1(v0), n_1=1(v1), vertex 2 equidistant → not counted
465        // ln(1·1)/ln(1+1) = ln(1)/ln(2) = 0. GG = 0
466        assert!((graovac_ghorbani_index(&k3()).unwrap()).abs() < 1e-10);
467    }
468
469    #[test]
470    fn gg_path4() {
471        // edge (0,1): n_0=1, n_1=3 → ln(3)/ln(4)
472        // edge (1,2): n_1=2, n_2=2 → ln(4)/ln(4) = 1
473        // edge (2,3): n_2=3, n_3=1 → ln(3)/ln(4)
474        // GG = 2·ln(3)/ln(4) + 1
475        let expected = 2.0 * 3.0_f64.ln() / 4.0_f64.ln() + 1.0;
476        assert!((graovac_ghorbani_index(&path4()).unwrap() - expected).abs() < 1e-10);
477    }
478
479    #[test]
480    fn gg_cycle4() {
481        // C4: for edge (0,1): v0 closer to 0, v3 closer to 0 (dist 1 vs 2),
482        // v1 closer to 1, v2 closer to 1 (dist 1 vs 2)
483        // Wait: v0=0 itself, dist(0,0)=0 < dist(1,0)=1 → n_0++
484        // v3: dist(0,3)=1, dist(1,3)=2 → n_0++
485        // v1: dist(0,1)=1, dist(1,1)=0 → n_1++
486        // v2: dist(0,2)=2, dist(1,2)=1 → n_1++
487        // n_0=2, n_1=2 → ln(4)/ln(4) = 1
488        // By symmetry all 4 edges give 1 → GG = 4
489        assert!((graovac_ghorbani_index(&cycle4()).unwrap() - 4.0).abs() < 1e-10);
490    }
491
492    #[test]
493    fn gg_k4() {
494        // For edge (0,1) in K4: all distances are 1 between non-identical vertices
495        // v0: dist(0,0)=0 < dist(1,0)=1 → n_0++
496        // v1: dist(0,1)=1 > dist(1,1)=0 → n_1++
497        // v2: dist(0,2)=1 = dist(1,2)=1 → tie
498        // v3: dist(0,3)=1 = dist(1,3)=1 → tie
499        // n_0=1, n_1=1 → ln(1)/ln(2) = 0
500        // GG = 0
501        assert!((graovac_ghorbani_index(&k4()).unwrap()).abs() < 1e-10);
502    }
503
504    #[test]
505    fn gg_star5() {
506        // edge (0,1): v0 closer to 0 (dist 0 vs 1), v1 closer to 1 (dist 1 vs 0)
507        // v2: dist(0,2)=1, dist(1,2)=2 → n_0++
508        // v3: dist(0,3)=1, dist(1,3)=2 → n_0++
509        // v4: dist(0,4)=1, dist(1,4)=2 → n_0++
510        // n_0=4, n_1=1 → ln(4)/ln(5)
511        // By symmetry, 4 edges × ln(4)/ln(5)
512        let expected = 4.0 * 4.0_f64.ln() / 5.0_f64.ln();
513        assert!((graovac_ghorbani_index(&star5()).unwrap() - expected).abs() < 1e-10);
514    }
515
516    #[test]
517    fn gg_nonneg() {
518        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
519            assert!(graovac_ghorbani_index(g).unwrap() >= -1e-10);
520        }
521    }
522
523    #[test]
524    fn gg_cycle5() {
525        // C5: for edge (0,1):
526        // v0: d(0,0)=0 < d(1,0)=1 → n_0
527        // v1: d(0,1)=1 > d(1,1)=0 → n_1
528        // v4: d(0,4)=1, d(1,4)=2 → n_0
529        // v2: d(0,2)=2, d(1,2)=1 → n_1
530        // v3: d(0,3)=2, d(1,3)=2 → tie
531        // n_0=2, n_1=2 → ln(4)/ln(4) = 1
532        // By symmetry, 5 edges → GG = 5
533        assert!((graovac_ghorbani_index(&cycle5()).unwrap() - 5.0).abs() < 1e-10);
534    }
535
536    #[test]
537    fn gg_paw() {
538        // paw: edges (0,1),(0,2),(1,2),(2,3), degrees [2,2,3,1]
539        // edge (0,1): v0→n0, v1→n1, v2: d(0,2)=1=d(1,2)=1→tie, v3: d(0,3)=2=d(1,3)=2→tie
540        // n_0=1, n_1=1 → ln(1)/ln(2) = 0
541        // edge (0,2): v0→n0, v2→n2, v1: d(0,1)=1=d(2,1)=1→tie, v3: d(0,3)=2,d(2,3)=1→n2
542        // n_0=1, n_2=2 → ln(2)/ln(3)
543        // edge (1,2): v1→n1, v2→n2, v0: d(1,0)=1=d(2,0)=1→tie, v3: d(1,3)=2,d(2,3)=1→n2
544        // n_1=1, n_2=2 → ln(2)/ln(3)
545        // edge (2,3): v2→n2, v3→n3, v0: d(2,0)=1,d(3,0)=2→n2, v1: d(2,1)=1,d(3,1)=2→n2
546        // n_2=3, n_3=1 → ln(3)/ln(4)
547        let expected = 2.0 * 2.0_f64.ln() / 3.0_f64.ln() + 3.0_f64.ln() / 4.0_f64.ln();
548        assert!((graovac_ghorbani_index(&paw()).unwrap() - expected).abs() < 1e-10);
549    }
550
551    // --- cross-consistency ---
552
553    #[test]
554    fn esz_trees_compute_ok() {
555        for g in &[single_edge(), path3(), path4(), star5()] {
556            let _ = edge_szeged_index(g).unwrap();
557        }
558    }
559}