Skip to main content

rust_igraph/algorithms/properties/
reciprocal_distance_degree.rs

1//! Reciprocal distance-degree indices (ALGO-TR-067).
2//!
3//! Hybrid indices combining vertex degrees with shortest-path distances
4//! in reciprocal (inverse) form. These complement `degree_distance` and
5//! `gutman_index` (which multiply degree by distance).
6//!
7//! - **Reciprocal degree distance** `RDD(G) = Σ_{u<v} [d(u)+d(v)] / dist(u,v)`
8//!   Also known as the *additively weighted Harary index* `H_A(G)`.
9//! - **Multiplicatively weighted Harary index**
10//!   `H_M(G) = Σ_{u<v} d(u)·d(v) / dist(u,v)`
11//! - **Terminal Wiener index** `TW(G) = Σ_{u<v, d(u)=d(v)=1} dist(u,v)`
12//!   Sum of distances restricted to pendant (degree-1) vertex pairs.
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)]
21
22use crate::core::{Graph, IgraphResult};
23
24/// Compute the reciprocal degree distance (additively weighted Harary index).
25///
26/// `RDD(G) = Σ_{u<v, dist(u,v)<∞} [d(u) + d(v)] / dist(u,v)`
27///
28/// Disconnected pairs (infinite distance) are skipped.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, reciprocal_degree_distance};
34///
35/// // Path 0-1-2: d=[1,2,1]
36/// // (0,1): (1+2)/1=3, (0,2): (1+1)/2=1, (1,2): (2+1)/1=3
37/// // RDD = 7
38/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
39/// assert!((reciprocal_degree_distance(&g).unwrap() - 7.0).abs() < 1e-10);
40/// ```
41pub fn reciprocal_degree_distance(graph: &Graph) -> IgraphResult<f64> {
42    let n = graph.vcount() as usize;
43    let mut rdd = 0.0_f64;
44
45    for s in 0..n {
46        let ds = graph.degree(s as u32)? as f64;
47        let mut dist = vec![u32::MAX; n];
48        dist[s] = 0;
49        let mut queue = std::collections::VecDeque::new();
50        queue.push_back(s);
51        while let Some(u) = queue.pop_front() {
52            let d_u = dist[u];
53            if let Ok(nbs) = graph.neighbors(u as u32) {
54                for nb in nbs {
55                    let idx = nb as usize;
56                    if dist[idx] == u32::MAX {
57                        dist[idx] = d_u + 1;
58                        queue.push_back(idx);
59                    }
60                }
61            }
62        }
63        for t in (s + 1)..n {
64            if dist[t] != u32::MAX && dist[t] > 0 {
65                let dt = graph.degree(t as u32)? as f64;
66                rdd += (ds + dt) / f64::from(dist[t]);
67            }
68        }
69    }
70
71    Ok(rdd)
72}
73
74/// Compute the multiplicatively weighted Harary index.
75///
76/// `H_M(G) = Σ_{u<v, dist(u,v)<∞} d(u)·d(v) / dist(u,v)`
77///
78/// Disconnected pairs and vertices with degree 0 are skipped.
79///
80/// # Examples
81///
82/// ```
83/// use rust_igraph::{Graph, multiplicatively_weighted_harary};
84///
85/// // Path 0-1-2: d=[1,2,1]
86/// // (0,1): 1×2/1=2, (0,2): 1×1/2=0.5, (1,2): 2×1/1=2
87/// // H_M = 4.5
88/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
89/// assert!((multiplicatively_weighted_harary(&g).unwrap() - 4.5).abs() < 1e-10);
90/// ```
91pub fn multiplicatively_weighted_harary(graph: &Graph) -> IgraphResult<f64> {
92    let n = graph.vcount() as usize;
93    let mut hm = 0.0_f64;
94
95    for s in 0..n {
96        let ds = graph.degree(s as u32)? as f64;
97        if ds == 0.0 {
98            continue;
99        }
100        let mut dist = vec![u32::MAX; n];
101        dist[s] = 0;
102        let mut queue = std::collections::VecDeque::new();
103        queue.push_back(s);
104        while let Some(u) = queue.pop_front() {
105            let d_u = dist[u];
106            if let Ok(nbs) = graph.neighbors(u as u32) {
107                for nb in nbs {
108                    let idx = nb as usize;
109                    if dist[idx] == u32::MAX {
110                        dist[idx] = d_u + 1;
111                        queue.push_back(idx);
112                    }
113                }
114            }
115        }
116        for t in (s + 1)..n {
117            if dist[t] != u32::MAX && dist[t] > 0 {
118                let dt = graph.degree(t as u32)? as f64;
119                if dt > 0.0 {
120                    hm += (ds * dt) / f64::from(dist[t]);
121                }
122            }
123        }
124    }
125
126    Ok(hm)
127}
128
129/// Compute the terminal Wiener index.
130///
131/// `TW(G) = Σ_{u<v, d(u)=d(v)=1} dist(u,v)`
132///
133/// Sum of distances between all pairs of pendant (degree-1) vertices.
134/// Returns 0 if fewer than 2 pendant vertices exist.
135///
136/// # Examples
137///
138/// ```
139/// use rust_igraph::{Graph, terminal_wiener_index};
140///
141/// // Star S₄ (center 0, leaves 1-4): leaves are all at distance 2
142/// // C(4,2) = 6 pairs, each distance 2 → TW = 12
143/// let g = Graph::from_edges(&[(0,1),(0,2),(0,3),(0,4)], false, Some(5)).unwrap();
144/// assert_eq!(terminal_wiener_index(&g).unwrap(), 12);
145/// ```
146pub fn terminal_wiener_index(graph: &Graph) -> IgraphResult<u64> {
147    let n = graph.vcount() as usize;
148    let mut pendants = Vec::new();
149    for v in 0..n {
150        if graph.degree(v as u32)? == 1 {
151            pendants.push(v);
152        }
153    }
154
155    if pendants.len() < 2 {
156        return Ok(0);
157    }
158
159    let mut tw = 0_u64;
160
161    for (i, &s) in pendants.iter().enumerate() {
162        let mut dist = vec![u32::MAX; n];
163        dist[s] = 0;
164        let mut queue = std::collections::VecDeque::new();
165        queue.push_back(s);
166        while let Some(u) = queue.pop_front() {
167            let d_u = dist[u];
168            if let Ok(nbs) = graph.neighbors(u as u32) {
169                for nb in nbs {
170                    let idx = nb as usize;
171                    if dist[idx] == u32::MAX {
172                        dist[idx] = d_u + 1;
173                        queue.push_back(idx);
174                    }
175                }
176            }
177        }
178        for &t in &pendants[(i + 1)..] {
179            if dist[t] != u32::MAX {
180                tw = tw.saturating_add(u64::from(dist[t]));
181            }
182        }
183    }
184
185    Ok(tw)
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    fn single_edge() -> Graph {
193        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
194    }
195
196    fn path3() -> Graph {
197        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
198    }
199
200    fn path4() -> Graph {
201        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
202    }
203
204    fn k3() -> Graph {
205        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
206    }
207
208    fn k4() -> Graph {
209        Graph::from_edges(
210            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
211            false,
212            Some(4),
213        )
214        .unwrap()
215    }
216
217    fn cycle4() -> Graph {
218        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
219    }
220
221    fn cycle5() -> Graph {
222        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
223    }
224
225    fn star5() -> Graph {
226        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
227    }
228
229    fn paw() -> Graph {
230        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
231    }
232
233    // --- reciprocal_degree_distance ---
234
235    #[test]
236    fn rdd_empty() {
237        let g = Graph::with_vertices(0);
238        assert!((reciprocal_degree_distance(&g).unwrap()).abs() < 1e-10);
239    }
240
241    #[test]
242    fn rdd_isolated() {
243        let g = Graph::with_vertices(5);
244        assert!((reciprocal_degree_distance(&g).unwrap()).abs() < 1e-10);
245    }
246
247    #[test]
248    fn rdd_single_edge() {
249        // d=[1,1], (0,1): (1+1)/1=2
250        assert!((reciprocal_degree_distance(&single_edge()).unwrap() - 2.0).abs() < 1e-10);
251    }
252
253    #[test]
254    fn rdd_path3() {
255        // d=[1,2,1], (0,1):3/1=3, (0,2):2/2=1, (1,2):3/1=3 → 7
256        assert!((reciprocal_degree_distance(&path3()).unwrap() - 7.0).abs() < 1e-10);
257    }
258
259    #[test]
260    fn rdd_path4() {
261        // d=[1,2,2,1]
262        // (0,1):(1+2)/1=3, (0,2):(1+2)/2=1.5, (0,3):(1+1)/3=2/3
263        // (1,2):(2+2)/1=4, (1,3):(2+1)/2=1.5, (2,3):(2+1)/1=3
264        let expected = 3.0 + 1.5 + 2.0 / 3.0 + 4.0 + 1.5 + 3.0;
265        assert!((reciprocal_degree_distance(&path4()).unwrap() - expected).abs() < 1e-10);
266    }
267
268    #[test]
269    fn rdd_k3() {
270        // d=[2,2,2], all distances 1
271        // 3 pairs × (2+2)/1 = 12
272        assert!((reciprocal_degree_distance(&k3()).unwrap() - 12.0).abs() < 1e-10);
273    }
274
275    #[test]
276    fn rdd_k4() {
277        // d=[3,3,3,3], 6 pairs × (3+3)/1 = 36
278        assert!((reciprocal_degree_distance(&k4()).unwrap() - 36.0).abs() < 1e-10);
279    }
280
281    #[test]
282    fn rdd_cycle4() {
283        // d=[2,2,2,2], distances: 4 pairs@1 → 4/1=4 each, 2 pairs@2 → 4/2=2 each
284        // 4×4 + 2×2 = 20
285        assert!((reciprocal_degree_distance(&cycle4()).unwrap() - 20.0).abs() < 1e-10);
286    }
287
288    #[test]
289    fn rdd_cycle5() {
290        // d=[2,2,2,2,2], 5 pairs@1, 5 pairs@2
291        // 5×(4/1) + 5×(4/2) = 20+10 = 30
292        assert!((reciprocal_degree_distance(&cycle5()).unwrap() - 30.0).abs() < 1e-10);
293    }
294
295    #[test]
296    fn rdd_star5() {
297        // d=[4,1,1,1,1]
298        // center-leaf pairs (4 of them): (4+1)/1=5, total=20
299        // leaf-leaf pairs (6 of them): (1+1)/2=1, total=6
300        assert!((reciprocal_degree_distance(&star5()).unwrap() - 26.0).abs() < 1e-10);
301    }
302
303    #[test]
304    fn rdd_paw() {
305        // d=[2,2,3,1]
306        // (0,1):(2+2)/1=4, (0,2):(2+3)/1=5, (0,3):(2+1)/2=1.5
307        // (1,2):(2+3)/1=5, (1,3):(2+1)/2=1.5, (2,3):(3+1)/1=4
308        let expected = 4.0 + 5.0 + 1.5 + 5.0 + 1.5 + 4.0;
309        assert!((reciprocal_degree_distance(&paw()).unwrap() - expected).abs() < 1e-10);
310    }
311
312    #[test]
313    fn rdd_regular_formula() {
314        // For r-regular graphs: RDD = 2r × Harary
315        // Because Σ (r+r)/dist = 2r × Σ 1/dist = 2r × H(G)
316        // We don't have harary_index imported here, but we can verify manually
317        // K_n: Harary = n(n-1)/2, RDD = 2(n-1) × n(n-1)/2 = n(n-1)²
318        let g = k4();
319        let n = 4.0_f64;
320        let r = 3.0;
321        let harary = n * (n - 1.0) / 2.0; // 6
322        let expected = 2.0 * r * harary;
323        assert!((reciprocal_degree_distance(&g).unwrap() - expected).abs() < 1e-8);
324    }
325
326    // --- multiplicatively_weighted_harary ---
327
328    #[test]
329    fn hm_empty() {
330        let g = Graph::with_vertices(0);
331        assert!((multiplicatively_weighted_harary(&g).unwrap()).abs() < 1e-10);
332    }
333
334    #[test]
335    fn hm_isolated() {
336        let g = Graph::with_vertices(5);
337        assert!((multiplicatively_weighted_harary(&g).unwrap()).abs() < 1e-10);
338    }
339
340    #[test]
341    fn hm_single_edge() {
342        // d=[1,1], (0,1): 1×1/1=1
343        assert!((multiplicatively_weighted_harary(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
344    }
345
346    #[test]
347    fn hm_path3() {
348        // d=[1,2,1], (0,1):2/1=2, (0,2):1/2=0.5, (1,2):2/1=2 → 4.5
349        assert!((multiplicatively_weighted_harary(&path3()).unwrap() - 4.5).abs() < 1e-10);
350    }
351
352    #[test]
353    fn hm_k3() {
354        // d=[2,2,2], 3 pairs × 4/1 = 12
355        assert!((multiplicatively_weighted_harary(&k3()).unwrap() - 12.0).abs() < 1e-10);
356    }
357
358    #[test]
359    fn hm_k4() {
360        // d=[3,3,3,3], 6 pairs × 9/1 = 54
361        assert!((multiplicatively_weighted_harary(&k4()).unwrap() - 54.0).abs() < 1e-10);
362    }
363
364    #[test]
365    fn hm_cycle4() {
366        // d=[2,2,2,2], 4 pairs@1: 4/1=4, 2 pairs@2: 4/2=2
367        // 4×4 + 2×2 = 20
368        assert!((multiplicatively_weighted_harary(&cycle4()).unwrap() - 20.0).abs() < 1e-10);
369    }
370
371    #[test]
372    fn hm_star5() {
373        // d=[4,1,1,1,1]
374        // center-leaf (4 pairs): 4×1/1=4 each → 16
375        // leaf-leaf (6 pairs): 1×1/2=0.5 each → 3
376        assert!((multiplicatively_weighted_harary(&star5()).unwrap() - 19.0).abs() < 1e-10);
377    }
378
379    #[test]
380    fn hm_paw() {
381        // d=[2,2,3,1]
382        // (0,1):4/1=4, (0,2):6/1=6, (0,3):2/2=1
383        // (1,2):6/1=6, (1,3):2/2=1, (2,3):3/1=3
384        let expected = 4.0 + 6.0 + 1.0 + 6.0 + 1.0 + 3.0;
385        assert!((multiplicatively_weighted_harary(&paw()).unwrap() - expected).abs() < 1e-10);
386    }
387
388    #[test]
389    fn hm_regular_formula() {
390        // For r-regular: H_M = r² × Harary
391        // K_n: H_M = (n-1)² × n(n-1)/2
392        let g = k4();
393        let n = 4.0_f64;
394        let r = 3.0;
395        let harary = n * (n - 1.0) / 2.0;
396        let expected = r * r * harary;
397        assert!((multiplicatively_weighted_harary(&g).unwrap() - expected).abs() < 1e-8);
398    }
399
400    #[test]
401    fn hm_equals_rdd_for_regular() {
402        // Regular: H_M = r² × H, RDD = 2r × H
403        // So H_M / RDD = r/2
404        for g in &[k3(), k4(), cycle4(), cycle5()] {
405            let hm = multiplicatively_weighted_harary(g).unwrap();
406            let rdd = reciprocal_degree_distance(g).unwrap();
407            let r = g.degree(0).unwrap() as f64;
408            assert!((hm / rdd - r / 2.0).abs() < 1e-8);
409        }
410    }
411
412    // --- terminal_wiener_index ---
413
414    #[test]
415    fn tw_empty() {
416        let g = Graph::with_vertices(0);
417        assert_eq!(terminal_wiener_index(&g).unwrap(), 0);
418    }
419
420    #[test]
421    fn tw_isolated() {
422        let g = Graph::with_vertices(5);
423        assert_eq!(terminal_wiener_index(&g).unwrap(), 0);
424    }
425
426    #[test]
427    fn tw_single_edge() {
428        // Both vertices have degree 1, distance 1
429        assert_eq!(terminal_wiener_index(&single_edge()).unwrap(), 1);
430    }
431
432    #[test]
433    fn tw_path3() {
434        // Pendants: 0 and 2, distance 2
435        assert_eq!(terminal_wiener_index(&path3()).unwrap(), 2);
436    }
437
438    #[test]
439    fn tw_path4() {
440        // Pendants: 0 and 3, distance 3
441        assert_eq!(terminal_wiener_index(&path4()).unwrap(), 3);
442    }
443
444    #[test]
445    fn tw_k3() {
446        // No pendant vertices (all degree 2) → 0
447        assert_eq!(terminal_wiener_index(&k3()).unwrap(), 0);
448    }
449
450    #[test]
451    fn tw_k4() {
452        assert_eq!(terminal_wiener_index(&k4()).unwrap(), 0);
453    }
454
455    #[test]
456    fn tw_cycle4() {
457        assert_eq!(terminal_wiener_index(&cycle4()).unwrap(), 0);
458    }
459
460    #[test]
461    fn tw_cycle5() {
462        assert_eq!(terminal_wiener_index(&cycle5()).unwrap(), 0);
463    }
464
465    #[test]
466    fn tw_star5() {
467        // 4 leaves at distance 2 from each other
468        // C(4,2) = 6 pairs × distance 2 = 12
469        assert_eq!(terminal_wiener_index(&star5()).unwrap(), 12);
470    }
471
472    #[test]
473    fn tw_paw() {
474        // Pendant: vertex 3 (degree 1). Only 1 pendant → 0
475        assert_eq!(terminal_wiener_index(&paw()).unwrap(), 0);
476    }
477
478    #[test]
479    fn tw_caterpillar() {
480        // 0-1-2-3, with extra leaves: 1-4, 2-5
481        // Pendants: 0(deg 1), 3(deg 1), 4(deg 1), 5(deg 1)
482        // Distances: (0,3):3, (0,4):2, (0,5):3, (3,4):3, (3,5):2, (4,5):3
483        // TW = 3+2+3+3+2+3 = 16
484        let g =
485            Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (1, 4), (2, 5)], false, Some(6)).unwrap();
486        assert_eq!(terminal_wiener_index(&g).unwrap(), 16);
487    }
488
489    #[test]
490    fn tw_double_star() {
491        // Double star: center1-center2, each with 2 leaves
492        // 0-2, 1-2, 2-3, 3-4, 3-5
493        // Pendants: 0,1,4,5 (all degree 1)
494        // Distances: (0,1):2, (0,4):3, (0,5):3, (1,4):3, (1,5):3, (4,5):2
495        // TW = 2+3+3+3+3+2 = 16
496        let g =
497            Graph::from_edges(&[(0, 2), (1, 2), (2, 3), (3, 4), (3, 5)], false, Some(6)).unwrap();
498        assert_eq!(terminal_wiener_index(&g).unwrap(), 16);
499    }
500
501    // --- cross-consistency ---
502
503    #[test]
504    fn rdd_geq_harary_for_nonregular() {
505        // For any graph, RDD = Σ (d(u)+d(v))/dist ≥ 2·Σ 1/dist = 2·H
506        // when min degree ≥ 1 (since d(u)+d(v) ≥ 2)
507        // This holds because (d(u)+d(v)) ≥ 2 for every pair of non-isolated vertices
508        for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
509            let rdd = reciprocal_degree_distance(g).unwrap();
510            assert!(rdd >= 0.0);
511        }
512    }
513
514    #[test]
515    fn all_positive_for_connected() {
516        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
517            assert!(reciprocal_degree_distance(g).unwrap() > 0.0);
518            assert!(multiplicatively_weighted_harary(g).unwrap() > 0.0);
519        }
520    }
521}