Skip to main content

rust_igraph/algorithms/properties/
hyper_wiener.rs

1//! Hyper-Wiener index and Harary index (ALGO-TR-044).
2//!
3//! - **Hyper-Wiener index** `WW(G) = ½ Σ_{u<v} d(u,v) + ½ Σ_{u<v} d(u,v)²`
4//!   Introduced by Randić (1993); generalises the Wiener index for
5//!   acyclic graphs and is widely used in QSAR/QSPR studies.
6//! - **Harary index** `H(G) = Σ_{u<v} 1/d(u,v)`
7//!   The reciprocal-distance sum; measures graph compactness.
8//!   Named after Frank Harary.
9
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_precision_loss,
13    clippy::many_single_char_names,
14    clippy::needless_range_loop,
15    clippy::too_many_lines
16)]
17
18use crate::core::{Graph, IgraphResult};
19use std::collections::VecDeque;
20
21/// Compute the hyper-Wiener index of a graph.
22///
23/// `WW(G) = ½ Σ_{u<v} d(u,v) + ½ Σ_{u<v} d(u,v)²`
24///
25/// Only finite distances contribute. Returns 0.0 for graphs with
26/// fewer than 2 vertices or no edges.
27///
28/// # Examples
29///
30/// ```
31/// use rust_igraph::{Graph, hyper_wiener_index};
32///
33/// // Path 0-1-2: distances 1,2,1
34/// // W = 1+2+1 = 4, but u<v only: d(0,1)=1, d(0,2)=2, d(1,2)=1
35/// // WW = ½(1+2+1) + ½(1+4+1) = 2 + 3 = 5
36/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
37/// assert!((hyper_wiener_index(&g).unwrap() - 5.0).abs() < 1e-10);
38/// ```
39pub fn hyper_wiener_index(graph: &Graph) -> IgraphResult<f64> {
40    let n = graph.vcount() as usize;
41    if n < 2 {
42        return Ok(0.0);
43    }
44
45    let dist = all_pairs_bfs(graph, n);
46    let mut sum_d = 0.0_f64;
47    let mut sum_d2 = 0.0_f64;
48
49    for u in 0..n {
50        for v in (u + 1)..n {
51            let d = dist[u * n + v];
52            if d == u32::MAX {
53                continue;
54            }
55            let df = f64::from(d);
56            sum_d += df;
57            sum_d2 += df * df;
58        }
59    }
60
61    Ok(0.5 * sum_d + 0.5 * sum_d2)
62}
63
64/// Compute the Harary index of a graph.
65///
66/// `H(G) = Σ_{u<v} 1 / d(u, v)`
67///
68/// Disconnected pairs (infinite distance) are skipped.
69/// Returns 0.0 for graphs with fewer than 2 vertices.
70///
71/// # Examples
72///
73/// ```
74/// use rust_igraph::{Graph, harary_index};
75///
76/// // Path 0-1-2: d(0,1)=1, d(0,2)=2, d(1,2)=1
77/// // H = 1/1 + 1/2 + 1/1 = 2.5
78/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
79/// assert!((harary_index(&g).unwrap() - 2.5).abs() < 1e-10);
80/// ```
81pub fn harary_index(graph: &Graph) -> IgraphResult<f64> {
82    let n = graph.vcount() as usize;
83    if n < 2 {
84        return Ok(0.0);
85    }
86
87    let dist = all_pairs_bfs(graph, n);
88    let mut h = 0.0_f64;
89
90    for u in 0..n {
91        for v in (u + 1)..n {
92            let d = dist[u * n + v];
93            if d != u32::MAX && d > 0 {
94                h += 1.0 / f64::from(d);
95            }
96        }
97    }
98
99    Ok(h)
100}
101
102fn all_pairs_bfs(graph: &Graph, n: usize) -> Vec<u32> {
103    let adj = build_adj_list(graph, n);
104    let mut dist = vec![u32::MAX; n * n];
105    for src in 0..n {
106        bfs_distances(&adj, n, src, &mut dist);
107    }
108    dist
109}
110
111fn build_adj_list(graph: &Graph, n: usize) -> Vec<Vec<usize>> {
112    let mut adj = vec![Vec::new(); n];
113    for (u, v) in graph.edges() {
114        let ui = u as usize;
115        let vi = v as usize;
116        adj[ui].push(vi);
117        if !graph.is_directed() && ui != vi {
118            adj[vi].push(ui);
119        }
120    }
121    adj
122}
123
124fn bfs_distances(adj: &[Vec<usize>], n: usize, src: usize, dist: &mut [u32]) {
125    dist[src * n + src] = 0;
126    let mut queue = VecDeque::new();
127    queue.push_back(src);
128    while let Some(v) = queue.pop_front() {
129        let d = dist[src * n + v];
130        for &w in &adj[v] {
131            if dist[src * n + w] == u32::MAX {
132                dist[src * n + w] = d.saturating_add(1);
133                queue.push_back(w);
134            }
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    fn single_edge() -> Graph {
144        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
145    }
146
147    fn path3() -> Graph {
148        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
149    }
150
151    fn path4() -> Graph {
152        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
153    }
154
155    fn path5() -> Graph {
156        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap()
157    }
158
159    fn k3() -> Graph {
160        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
161    }
162
163    fn k4() -> Graph {
164        Graph::from_edges(
165            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
166            false,
167            Some(4),
168        )
169        .unwrap()
170    }
171
172    fn cycle4() -> Graph {
173        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
174    }
175
176    fn cycle5() -> Graph {
177        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
178    }
179
180    fn star5() -> Graph {
181        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
182    }
183
184    // --- hyper_wiener_index ---
185
186    #[test]
187    fn hww_empty() {
188        let g = Graph::with_vertices(0);
189        assert!((hyper_wiener_index(&g).unwrap() - 0.0).abs() < 1e-10);
190    }
191
192    #[test]
193    fn hww_single_vertex() {
194        let g = Graph::with_vertices(1);
195        assert!((hyper_wiener_index(&g).unwrap() - 0.0).abs() < 1e-10);
196    }
197
198    #[test]
199    fn hww_no_edges() {
200        let g = Graph::with_vertices(3);
201        assert!((hyper_wiener_index(&g).unwrap() - 0.0).abs() < 1e-10);
202    }
203
204    #[test]
205    fn hww_single_edge() {
206        // d(0,1)=1, WW = ½·1 + ½·1 = 1
207        assert!((hyper_wiener_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
208    }
209
210    #[test]
211    fn hww_path3() {
212        // d(0,1)=1, d(0,2)=2, d(1,2)=1
213        // WW = ½(1+2+1) + ½(1+4+1) = 2 + 3 = 5
214        assert!((hyper_wiener_index(&path3()).unwrap() - 5.0).abs() < 1e-10);
215    }
216
217    #[test]
218    fn hww_path4() {
219        // d(0,1)=1, d(0,2)=2, d(0,3)=3, d(1,2)=1, d(1,3)=2, d(2,3)=1
220        // sum_d = 1+2+3+1+2+1 = 10
221        // sum_d2 = 1+4+9+1+4+1 = 20
222        // WW = ½·10 + ½·20 = 5 + 10 = 15
223        assert!((hyper_wiener_index(&path4()).unwrap() - 15.0).abs() < 1e-10);
224    }
225
226    #[test]
227    fn hww_path5() {
228        // distances: 1,2,3,4,1,2,3,1,2,1
229        // sum_d = 1+2+3+4+1+2+3+1+2+1 = 20
230        // sum_d2 = 1+4+9+16+1+4+9+1+4+1 = 50
231        // WW = ½·20 + ½·50 = 10 + 25 = 35
232        assert!((hyper_wiener_index(&path5()).unwrap() - 35.0).abs() < 1e-10);
233    }
234
235    #[test]
236    fn hww_k3() {
237        // all distances 1, 3 pairs
238        // WW = ½·3 + ½·3 = 3
239        assert!((hyper_wiener_index(&k3()).unwrap() - 3.0).abs() < 1e-10);
240    }
241
242    #[test]
243    fn hww_k4() {
244        // all distances 1, 6 pairs
245        // WW = ½·6 + ½·6 = 6
246        assert!((hyper_wiener_index(&k4()).unwrap() - 6.0).abs() < 1e-10);
247    }
248
249    #[test]
250    fn hww_cycle4() {
251        // d: 1,2,1,1,1,2 → wait, let me list u<v pairs:
252        // (0,1)=1, (0,2)=2, (0,3)=1, (1,2)=1, (1,3)=2, (2,3)=1
253        // sum_d = 1+2+1+1+2+1 = 8
254        // sum_d2 = 1+4+1+1+4+1 = 12
255        // WW = 4 + 6 = 10
256        assert!((hyper_wiener_index(&cycle4()).unwrap() - 10.0).abs() < 1e-10);
257    }
258
259    #[test]
260    fn hww_cycle5() {
261        // (0,1)=1,(0,2)=2,(0,3)=2,(0,4)=1,(1,2)=1,(1,3)=2,(1,4)=2,(2,3)=1,(2,4)=2,(3,4)=1
262        // sum_d = 1+2+2+1+1+2+2+1+2+1 = 15
263        // sum_d2 = 1+4+4+1+1+4+4+1+4+1 = 25
264        // WW = 7.5 + 12.5 = 20
265        assert!((hyper_wiener_index(&cycle5()).unwrap() - 20.0).abs() < 1e-10);
266    }
267
268    #[test]
269    fn hww_star5() {
270        // center 0, leaves 1-4
271        // (0,i)=1 for i=1..4 (4 pairs, d=1)
272        // (i,j)=2 for i<j in 1..4 (6 pairs, d=2)
273        // sum_d = 4·1 + 6·2 = 16
274        // sum_d2 = 4·1 + 6·4 = 28
275        // WW = 8 + 14 = 22
276        assert!((hyper_wiener_index(&star5()).unwrap() - 22.0).abs() < 1e-10);
277    }
278
279    #[test]
280    fn hww_with_isolated() {
281        // 0-1 plus isolated vertex 2: only pair (0,1) contributes
282        let g = Graph::from_edges(&[(0, 1)], false, Some(3)).unwrap();
283        assert!((hyper_wiener_index(&g).unwrap() - 1.0).abs() < 1e-10);
284    }
285
286    #[test]
287    fn hww_complete_formula() {
288        // For K_n: all distances=1, C(n,2) pairs
289        // WW = ½·C(n,2) + ½·C(n,2) = C(n,2)
290        for n in 2_u32..=6 {
291            let edges: Vec<(u32, u32)> = (0..n)
292                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
293                .collect();
294            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
295            let pairs = f64::from(n) * f64::from(n - 1) / 2.0;
296            assert!((hyper_wiener_index(&g).unwrap() - pairs).abs() < 1e-10);
297        }
298    }
299
300    #[test]
301    fn hww_geq_wiener() {
302        // WW(G) >= W(G) for connected graphs (since d² >= d for d >= 1)
303        for g in &[
304            single_edge(),
305            path3(),
306            path4(),
307            k3(),
308            k4(),
309            cycle4(),
310            star5(),
311        ] {
312            let ww = hyper_wiener_index(g).unwrap();
313            let n = g.vcount() as usize;
314            let dist = all_pairs_bfs(g, n);
315            let mut w = 0.0_f64;
316            for u in 0..n {
317                for v in (u + 1)..n {
318                    let d = dist[u * n + v];
319                    if d != u32::MAX {
320                        w += f64::from(d);
321                    }
322                }
323            }
324            assert!(ww >= w, "WW={ww} < W={w}");
325        }
326    }
327
328    #[test]
329    fn hww_tree_formula() {
330        // For trees: WW = ½ W + ½ Σ d² (simple identity check)
331        for g in &[single_edge(), path3(), path4(), path5(), star5()] {
332            let n = g.vcount() as usize;
333            let dist = all_pairs_bfs(g, n);
334            let mut w = 0.0_f64;
335            let mut sq = 0.0_f64;
336            for u in 0..n {
337                for v in (u + 1)..n {
338                    let d = dist[u * n + v];
339                    if d != u32::MAX {
340                        let df = f64::from(d);
341                        w += df;
342                        sq += df * df;
343                    }
344                }
345            }
346            let ww = hyper_wiener_index(g).unwrap();
347            assert!((ww - (0.5 * w + 0.5 * sq)).abs() < 1e-10);
348        }
349    }
350
351    #[test]
352    fn hww_two_components() {
353        // Two edges 0-1 and 2-3: pairs (0,1) and (2,3) contribute
354        // WW = 2 · (½·1 + ½·1) = 2
355        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
356        assert!((hyper_wiener_index(&g).unwrap() - 2.0).abs() < 1e-10);
357    }
358
359    // --- harary_index ---
360
361    #[test]
362    fn hi_empty() {
363        let g = Graph::with_vertices(0);
364        assert!((harary_index(&g).unwrap() - 0.0).abs() < 1e-10);
365    }
366
367    #[test]
368    fn hi_single_vertex() {
369        let g = Graph::with_vertices(1);
370        assert!((harary_index(&g).unwrap() - 0.0).abs() < 1e-10);
371    }
372
373    #[test]
374    fn hi_no_edges() {
375        let g = Graph::with_vertices(3);
376        assert!((harary_index(&g).unwrap() - 0.0).abs() < 1e-10);
377    }
378
379    #[test]
380    fn hi_single_edge() {
381        // H = 1/1 = 1
382        assert!((harary_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
383    }
384
385    #[test]
386    fn hi_path3() {
387        // 1/1 + 1/2 + 1/1 = 2.5
388        assert!((harary_index(&path3()).unwrap() - 2.5).abs() < 1e-10);
389    }
390
391    #[test]
392    fn hi_path4() {
393        // 1/1 + 1/2 + 1/3 + 1/1 + 1/2 + 1/1 = 3 + 1 + 1/3 = 13/3
394        let h = harary_index(&path4()).unwrap();
395        assert!((h - 13.0 / 3.0).abs() < 1e-10);
396    }
397
398    #[test]
399    fn hi_k3() {
400        // 3 pairs, all d=1 → H=3
401        assert!((harary_index(&k3()).unwrap() - 3.0).abs() < 1e-10);
402    }
403
404    #[test]
405    fn hi_k4() {
406        // 6 pairs, all d=1 → H=6
407        assert!((harary_index(&k4()).unwrap() - 6.0).abs() < 1e-10);
408    }
409
410    #[test]
411    fn hi_cycle4() {
412        // (0,1)=1,(0,2)=2,(0,3)=1,(1,2)=1,(1,3)=2,(2,3)=1
413        // H = 4·(1/1) + 2·(1/2) = 4 + 1 = 5
414        assert!((harary_index(&cycle4()).unwrap() - 5.0).abs() < 1e-10);
415    }
416
417    #[test]
418    fn hi_cycle5() {
419        // 5 edges d=1, 5 pairs d=2
420        // H = 5·1 + 5·0.5 = 7.5
421        assert!((harary_index(&cycle5()).unwrap() - 7.5).abs() < 1e-10);
422    }
423
424    #[test]
425    fn hi_star5() {
426        // (0,i):d=1 → 4 pairs → 4
427        // (i,j):d=2 → 6 pairs → 3
428        // H = 7
429        assert!((harary_index(&star5()).unwrap() - 7.0).abs() < 1e-10);
430    }
431
432    #[test]
433    fn hi_complete_formula() {
434        // K_n: all d=1, C(n,2) pairs → H = C(n,2)
435        for n in 2_u32..=6 {
436            let edges: Vec<(u32, u32)> = (0..n)
437                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
438                .collect();
439            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
440            let pairs = f64::from(n) * f64::from(n - 1) / 2.0;
441            assert!((harary_index(&g).unwrap() - pairs).abs() < 1e-10);
442        }
443    }
444
445    #[test]
446    fn hi_with_isolated() {
447        let g = Graph::from_edges(&[(0, 1)], false, Some(3)).unwrap();
448        assert!((harary_index(&g).unwrap() - 1.0).abs() < 1e-10);
449    }
450
451    #[test]
452    fn hi_two_components() {
453        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
454        assert!((harary_index(&g).unwrap() - 2.0).abs() < 1e-10);
455    }
456
457    #[test]
458    fn hi_positive_for_connected() {
459        for g in &[
460            single_edge(),
461            path3(),
462            path4(),
463            k3(),
464            k4(),
465            cycle4(),
466            star5(),
467        ] {
468            assert!(harary_index(g).unwrap() > 0.0);
469        }
470    }
471
472    #[test]
473    fn hi_leq_pairs() {
474        // H(G) <= C(n,2) since 1/d <= 1 for d >= 1
475        for g in &[path3(), path4(), k3(), k4(), cycle4(), star5()] {
476            let n = f64::from(g.vcount());
477            let max = n * (n - 1.0) / 2.0;
478            assert!(harary_index(g).unwrap() <= max + 1e-10);
479        }
480    }
481
482    #[test]
483    fn hi_path5() {
484        // d: 1,2,3,4,1,2,3,1,2,1
485        // H = 1 + 1/2 + 1/3 + 1/4 + 1 + 1/2 + 1/3 + 1 + 1/2 + 1
486        //   = 4 + 3/2 + 2/3 + 1/4
487        //   = 4 + 1.5 + 0.6667 + 0.25 = 6.4167
488        let h = harary_index(&path5()).unwrap();
489        let expected = 4.0 + 1.5 + 2.0 / 3.0 + 0.25;
490        assert!((h - expected).abs() < 1e-10);
491    }
492
493    #[test]
494    fn hi_diamond() {
495        // K4 minus edge (2,3): vertices 0,1,2,3
496        // edges: 0-1,0-2,0-3,1-2,1-3
497        // d(0,1)=1,d(0,2)=1,d(0,3)=1,d(1,2)=1,d(1,3)=1,d(2,3)=2
498        // H = 5·1 + 1·0.5 = 5.5
499        let g =
500            Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)], false, Some(4)).unwrap();
501        assert!((harary_index(&g).unwrap() - 5.5).abs() < 1e-10);
502    }
503
504    // --- cross-consistency ---
505
506    #[test]
507    fn hww_equals_wiener_for_d1() {
508        // When all distances are 1 (complete graph): WW = W = C(n,2)
509        for n in 2_u32..=5 {
510            let edges: Vec<(u32, u32)> = (0..n)
511                .flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
512                .collect();
513            let g = Graph::from_edges(&edges, false, Some(n)).unwrap();
514            let h = harary_index(&g).unwrap();
515            let ww = hyper_wiener_index(&g).unwrap();
516            assert!((h - ww).abs() < 1e-10);
517        }
518    }
519
520    #[test]
521    fn harary_geq_wiener_reciprocal() {
522        // H(G) >= n(n-1)/(2W) by AM-HM inequality on distances (connected)
523        for g in &[path3(), path4(), k3(), k4(), cycle4(), star5()] {
524            let n = g.vcount() as usize;
525            let dist = all_pairs_bfs(g, n);
526            let mut w = 0.0_f64;
527            for u in 0..n {
528                for v in (u + 1)..n {
529                    let d = dist[u * n + v];
530                    if d != u32::MAX {
531                        w += f64::from(d);
532                    }
533                }
534            }
535            let pairs = n as f64 * (n as f64 - 1.0) / 2.0;
536            let h = harary_index(g).unwrap();
537            // AM-HM: mean(d) >= pairs/H → H >= pairs²/sum_d
538            assert!(
539                h >= pairs * pairs / w - 1e-10,
540                "H={h} < pairs²/W={}, n={n}",
541                pairs * pairs / w
542            );
543        }
544    }
545}