Skip to main content

rust_igraph/algorithms/properties/
wiener_polarity_index.rs

1//! Wiener polarity index (ALGO-TR-035).
2//!
3//! The **Wiener polarity index** `W_p(G)` counts the number of
4//! unordered vertex pairs at distance exactly 3 in an undirected graph.
5//! Introduced by Wiener (1947) alongside the ordinary Wiener index.
6//!
7//! For directed graphs the shortest-path distance from `u` to `v`
8//! (not necessarily equal to `v` to `u`) is used; a pair `(u,v)` is
9//! counted if `d(u,v) = 3`.
10
11#![allow(
12    clippy::cast_possible_truncation,
13    clippy::cast_precision_loss,
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 Wiener polarity index of a graph.
23///
24/// Counts unordered vertex pairs `{u, v}` with shortest-path
25/// distance exactly 3. For directed graphs, counts ordered pairs
26/// `(u, v)` with `d(u,v) = 3`.
27///
28/// # Examples
29///
30/// ```
31/// use rust_igraph::{Graph, wiener_polarity_index};
32///
33/// // Path 0-1-2-3: only pair {0,3} is at distance 3
34/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3)], false, Some(4)).unwrap();
35/// assert_eq!(wiener_polarity_index(&g).unwrap(), 1);
36/// ```
37pub fn wiener_polarity_index(graph: &Graph) -> IgraphResult<u64> {
38    let n = graph.vcount() as usize;
39    if n < 4 {
40        return Ok(0);
41    }
42
43    let directed = graph.is_directed();
44    let adj = build_adj_list(graph, n);
45
46    let mut count: u64 = 0;
47
48    for src in 0..n {
49        let at3 = bfs_count_at_distance(&adj, n, src, 3);
50        count = count.saturating_add(at3 as u64);
51    }
52
53    if !directed {
54        count /= 2;
55    }
56
57    Ok(count)
58}
59
60/// Compute the number of vertex pairs at a given distance.
61///
62/// More general version: counts pairs at distance exactly `k`.
63/// For undirected graphs, counts unordered pairs; for directed,
64/// counts ordered pairs.
65///
66/// # Examples
67///
68/// ```
69/// use rust_igraph::{Graph, count_pairs_at_distance};
70///
71/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3),(3,4)], false, Some(5)).unwrap();
72/// // Distance 2: {0,2}, {1,3}, {2,4} = 3 pairs
73/// assert_eq!(count_pairs_at_distance(&g, 2).unwrap(), 3);
74/// ```
75pub fn count_pairs_at_distance(graph: &Graph, k: u32) -> IgraphResult<u64> {
76    let n = graph.vcount() as usize;
77    if k == 0 {
78        return Ok(n as u64);
79    }
80
81    let directed = graph.is_directed();
82    let adj = build_adj_list(graph, n);
83
84    let mut count: u64 = 0;
85
86    for src in 0..n {
87        let at_k = bfs_count_at_distance(&adj, n, src, k);
88        count = count.saturating_add(at_k as u64);
89    }
90
91    if !directed {
92        count /= 2;
93    }
94
95    Ok(count)
96}
97
98fn build_adj_list(graph: &Graph, n: usize) -> Vec<Vec<usize>> {
99    let mut adj = vec![Vec::new(); n];
100    for (u, v) in graph.edges() {
101        let ui = u as usize;
102        let vi = v as usize;
103        adj[ui].push(vi);
104        if !graph.is_directed() {
105            adj[vi].push(ui);
106        }
107    }
108    adj
109}
110
111fn bfs_count_at_distance(adj: &[Vec<usize>], n: usize, src: usize, target_dist: u32) -> usize {
112    let mut dist = vec![u32::MAX; n];
113    dist[src] = 0;
114    let mut queue = VecDeque::new();
115    queue.push_back(src);
116
117    let mut count: usize = 0;
118
119    while let Some(v) = queue.pop_front() {
120        let d = dist[v];
121        if d > target_dist {
122            break;
123        }
124        if d == target_dist {
125            count = count.saturating_add(1);
126            continue;
127        }
128        for &w in &adj[v] {
129            if dist[w] == u32::MAX {
130                dist[w] = d.saturating_add(1);
131                queue.push_back(w);
132            }
133        }
134    }
135
136    count
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    fn path4() -> Graph {
144        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
145    }
146
147    fn path5() -> Graph {
148        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap()
149    }
150
151    fn k3() -> Graph {
152        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
153    }
154
155    fn k4() -> Graph {
156        Graph::from_edges(
157            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
158            false,
159            Some(4),
160        )
161        .unwrap()
162    }
163
164    fn cycle5() -> Graph {
165        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
166    }
167
168    fn cycle6() -> Graph {
169        Graph::from_edges(
170            &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)],
171            false,
172            Some(6),
173        )
174        .unwrap()
175    }
176
177    fn star5() -> Graph {
178        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
179    }
180
181    fn petersen() -> Graph {
182        Graph::from_edges(
183            &[
184                (0, 1),
185                (1, 2),
186                (2, 3),
187                (3, 4),
188                (4, 0),
189                (0, 5),
190                (1, 6),
191                (2, 7),
192                (3, 8),
193                (4, 9),
194                (5, 7),
195                (7, 9),
196                (9, 6),
197                (6, 8),
198                (8, 5),
199            ],
200            false,
201            Some(10),
202        )
203        .unwrap()
204    }
205
206    // --- wiener_polarity_index ---
207
208    #[test]
209    fn wpi_empty() {
210        let g = Graph::with_vertices(0);
211        assert_eq!(wiener_polarity_index(&g).unwrap(), 0);
212    }
213
214    #[test]
215    fn wpi_single() {
216        let g = Graph::with_vertices(1);
217        assert_eq!(wiener_polarity_index(&g).unwrap(), 0);
218    }
219
220    #[test]
221    fn wpi_no_edges() {
222        let g = Graph::with_vertices(5);
223        assert_eq!(wiener_polarity_index(&g).unwrap(), 0);
224    }
225
226    #[test]
227    fn wpi_path4() {
228        // 0-1-2-3: only {0,3} at distance 3
229        assert_eq!(wiener_polarity_index(&path4()).unwrap(), 1);
230    }
231
232    #[test]
233    fn wpi_path5() {
234        // 0-1-2-3-4: pairs at distance 3: {0,3}, {1,4}
235        assert_eq!(wiener_polarity_index(&path5()).unwrap(), 2);
236    }
237
238    #[test]
239    fn wpi_k3() {
240        // All pairs at distance 1
241        assert_eq!(wiener_polarity_index(&k3()).unwrap(), 0);
242    }
243
244    #[test]
245    fn wpi_k4() {
246        // Complete graph: max distance = 1
247        assert_eq!(wiener_polarity_index(&k4()).unwrap(), 0);
248    }
249
250    #[test]
251    fn wpi_star5() {
252        // Star: max distance between leaves = 2, no distance-3 pairs
253        assert_eq!(wiener_polarity_index(&star5()).unwrap(), 0);
254    }
255
256    #[test]
257    fn wpi_cycle5() {
258        // C_5: distances are 1 or 2 only (diameter = 2)
259        assert_eq!(wiener_polarity_index(&cycle5()).unwrap(), 0);
260    }
261
262    #[test]
263    fn wpi_cycle6() {
264        // C_6: diameter = 3; exactly 3 pairs at distance 3: {0,3},{1,4},{2,5}
265        assert_eq!(wiener_polarity_index(&cycle6()).unwrap(), 3);
266    }
267
268    #[test]
269    fn wpi_petersen() {
270        // Petersen: diameter = 2, so no pairs at distance 3
271        assert_eq!(wiener_polarity_index(&petersen()).unwrap(), 0);
272    }
273
274    #[test]
275    fn wpi_two_components() {
276        // Two isolated edges: 0-1, 2-3 — no pairs at distance 3
277        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
278        assert_eq!(wiener_polarity_index(&g).unwrap(), 0);
279    }
280
281    // --- count_pairs_at_distance ---
282
283    #[test]
284    fn cpad_zero() {
285        let g = path4();
286        // k=0: each vertex is at distance 0 from itself → 4
287        assert_eq!(count_pairs_at_distance(&g, 0).unwrap(), 4);
288    }
289
290    #[test]
291    fn cpad_one_path4() {
292        // k=1: edges = 3
293        assert_eq!(count_pairs_at_distance(&path4(), 1).unwrap(), 3);
294    }
295
296    #[test]
297    fn cpad_two_path5() {
298        // 0-1-2-3-4: pairs at distance 2: {0,2},{1,3},{2,4} = 3
299        assert_eq!(count_pairs_at_distance(&path5(), 2).unwrap(), 3);
300    }
301
302    #[test]
303    fn cpad_three_path5() {
304        // {0,3},{1,4} = 2
305        assert_eq!(count_pairs_at_distance(&path5(), 3).unwrap(), 2);
306    }
307
308    #[test]
309    fn cpad_four_path5() {
310        // {0,4} = 1
311        assert_eq!(count_pairs_at_distance(&path5(), 4).unwrap(), 1);
312    }
313
314    #[test]
315    fn cpad_five_path5() {
316        // No pairs at distance 5 in a 5-vertex path
317        assert_eq!(count_pairs_at_distance(&path5(), 5).unwrap(), 0);
318    }
319
320    #[test]
321    fn cpad_one_k4() {
322        // K_4: 6 edges, all at distance 1
323        assert_eq!(count_pairs_at_distance(&k4(), 1).unwrap(), 6);
324    }
325
326    #[test]
327    fn cpad_two_cycle6() {
328        // C_6: distance-2 pairs: 6
329        assert_eq!(count_pairs_at_distance(&cycle6(), 2).unwrap(), 6);
330    }
331
332    #[test]
333    fn cpad_matches_wpi() {
334        // count_pairs_at_distance(g, 3) should equal wiener_polarity_index(g)
335        for g in &[path4(), path5(), k3(), k4(), cycle5(), cycle6(), star5()] {
336            let wpi = wiener_polarity_index(g).unwrap();
337            let cpad = count_pairs_at_distance(g, 3).unwrap();
338            assert_eq!(wpi, cpad);
339        }
340    }
341
342    #[test]
343    fn cpad_empty() {
344        let g = Graph::with_vertices(0);
345        assert_eq!(count_pairs_at_distance(&g, 1).unwrap(), 0);
346    }
347
348    // --- sum of all distances ---
349
350    #[test]
351    fn sum_distances_path4() {
352        // Wiener index of P_4: 1+2+3 + 1+2 + 1 = 10
353        let g = path4();
354        let mut total: u64 = 0;
355        for k in 1..4 {
356            total = total.saturating_add(count_pairs_at_distance(&g, k).unwrap() * u64::from(k));
357        }
358        assert_eq!(total, 10);
359    }
360}