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}