Skip to main content

rust_igraph/algorithms/properties/
mostar_index.rs

1//! Mostar index and degree-distance index (ALGO-TR-042).
2//!
3//! - **Mostar index** `Mo(G) = Σ_{(u,v)∈E} |n_u(e) - n_v(e)|`
4//!   where `n_u(e)` = vertices strictly closer to `u` than `v`.
5//!   Measures peripherality — trees maximise it among graphs with
6//!   the same order and size; `Mo = 0` iff the graph is
7//!   distance-balanced.
8//! - **Degree-distance index** `DD(G) = Σ_{u≠v} (deg(u)+deg(v))·d(u,v)`
9//!   (Schultz-type invariant; equals the Schultz index for trees).
10//! - **Gutman index** `Gut(G) = Σ_{u<v} deg(u)·deg(v)·d(u,v)`
11
12#![allow(
13    clippy::cast_possible_truncation,
14    clippy::cast_precision_loss,
15    clippy::comparison_chain,
16    clippy::many_single_char_names,
17    clippy::needless_range_loop,
18    clippy::too_many_lines
19)]
20
21use crate::core::{Graph, IgraphResult};
22use std::collections::VecDeque;
23
24/// Compute the Mostar index of a graph.
25///
26/// `Mo(G) = Σ_{(u,v)∈E} |n_u(e) - n_v(e)|`
27///
28/// A graph is distance-balanced iff `Mo = 0`.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, mostar_index};
34///
35/// // Cycle C_4 is distance-balanced → Mo = 0
36/// let g = Graph::from_edges(&[(0,1),(1,2),(2,3),(3,0)], false, Some(4)).unwrap();
37/// assert_eq!(mostar_index(&g).unwrap(), 0);
38///
39/// // Path 0-1-2: edge(0,1) |1-2|=1, edge(1,2) |2-1|=1 → Mo = 2
40/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
41/// assert_eq!(mostar_index(&g).unwrap(), 2);
42/// ```
43pub fn mostar_index(graph: &Graph) -> IgraphResult<u64> {
44    let n = graph.vcount() as usize;
45    if n == 0 {
46        return Ok(0);
47    }
48
49    let dist = all_pairs_bfs(graph, n);
50    let mut mo: u64 = 0;
51
52    for (u, v) in graph.edges() {
53        let ui = u as usize;
54        let vi = v as usize;
55        if ui == vi {
56            continue;
57        }
58        let (nu, nv) = count_closer(&dist, n, ui, vi);
59        mo = mo.saturating_add(nu.abs_diff(nv) as u64);
60    }
61
62    Ok(mo)
63}
64
65/// Compute the degree-distance index (Schultz-type).
66///
67/// `DD(G) = Σ_{u≠v} (deg(u) + deg(v)) · d(u, v)`
68///
69/// For connected graphs only; disconnected pairs are skipped.
70///
71/// # Examples
72///
73/// ```
74/// use rust_igraph::{Graph, degree_distance};
75///
76/// // Path 0-1-2: degrees [1,2,1]
77/// // pairs: (0,1) d=1 (1+2)=3, (0,2) d=2 (1+1)=2·2=4,
78/// //        (1,0) d=1 3, (1,2) d=1 3, (2,0) d=2 4, (2,1) d=1 3
79/// // DD = 3+4+3+3+4+3 = 20
80/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
81/// assert_eq!(degree_distance(&g).unwrap(), 20);
82/// ```
83pub fn degree_distance(graph: &Graph) -> IgraphResult<u64> {
84    let n = graph.vcount() as usize;
85    if n == 0 {
86        return Ok(0);
87    }
88
89    let dist = all_pairs_bfs(graph, n);
90    let mut deg = vec![0_usize; n];
91    for v in 0..n as u32 {
92        deg[v as usize] = graph.degree(v)?;
93    }
94
95    let mut dd: u64 = 0;
96    for u in 0..n {
97        for v in 0..n {
98            if u == v {
99                continue;
100            }
101            let d = dist[u * n + v];
102            if d == u32::MAX {
103                continue;
104            }
105            let sum_deg = (deg[u] as u64).saturating_add(deg[v] as u64);
106            dd = dd.saturating_add(sum_deg.saturating_mul(u64::from(d)));
107        }
108    }
109
110    Ok(dd)
111}
112
113/// Compute the Gutman index.
114///
115/// `Gut(G) = Σ_{u<v} deg(u) · deg(v) · d(u, v)`
116///
117/// # Examples
118///
119/// ```
120/// use rust_igraph::{Graph, gutman_index};
121///
122/// // Path 0-1-2: degrees [1,2,1]
123/// // (0,1): 1·2·1=2, (0,2): 1·1·2=2, (1,2): 2·1·1=2
124/// // Gut = 6
125/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
126/// assert_eq!(gutman_index(&g).unwrap(), 6);
127/// ```
128pub fn gutman_index(graph: &Graph) -> IgraphResult<u64> {
129    let n = graph.vcount() as usize;
130    if n == 0 {
131        return Ok(0);
132    }
133
134    let dist = all_pairs_bfs(graph, n);
135    let mut deg = vec![0_usize; n];
136    for v in 0..n as u32 {
137        deg[v as usize] = graph.degree(v)?;
138    }
139
140    let mut gut: u64 = 0;
141    for u in 0..n {
142        for v in (u + 1)..n {
143            let d = dist[u * n + v];
144            if d == u32::MAX {
145                continue;
146            }
147            let prod = (deg[u] as u64).saturating_mul(deg[v] as u64);
148            gut = gut.saturating_add(prod.saturating_mul(u64::from(d)));
149        }
150    }
151
152    Ok(gut)
153}
154
155fn count_closer(dist: &[u32], n: usize, u: usize, v: usize) -> (usize, usize) {
156    let mut nu = 0_usize;
157    let mut nv = 0_usize;
158    for w in 0..n {
159        let du = dist[u * n + w];
160        let dv = dist[v * n + w];
161        if du == u32::MAX || dv == u32::MAX {
162            continue;
163        }
164        if du < dv {
165            nu += 1;
166        } else if dv < du {
167            nv += 1;
168        }
169    }
170    (nu, nv)
171}
172
173fn all_pairs_bfs(graph: &Graph, n: usize) -> Vec<u32> {
174    let adj = build_adj_list(graph, n);
175    let mut dist = vec![u32::MAX; n * n];
176    for src in 0..n {
177        bfs_distances(&adj, n, src, &mut dist);
178    }
179    dist
180}
181
182fn build_adj_list(graph: &Graph, n: usize) -> Vec<Vec<usize>> {
183    let mut adj = vec![Vec::new(); n];
184    for (u, v) in graph.edges() {
185        let ui = u as usize;
186        let vi = v as usize;
187        adj[ui].push(vi);
188        if !graph.is_directed() && ui != vi {
189            adj[vi].push(ui);
190        }
191    }
192    adj
193}
194
195fn bfs_distances(adj: &[Vec<usize>], n: usize, src: usize, dist: &mut [u32]) {
196    dist[src * n + src] = 0;
197    let mut queue = VecDeque::new();
198    queue.push_back(src);
199    while let Some(v) = queue.pop_front() {
200        let d = dist[src * n + v];
201        for &w in &adj[v] {
202            if dist[src * n + w] == u32::MAX {
203                dist[src * n + w] = d.saturating_add(1);
204                queue.push_back(w);
205            }
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    fn single_edge() -> Graph {
215        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
216    }
217
218    fn path3() -> Graph {
219        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
220    }
221
222    fn path4() -> Graph {
223        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
224    }
225
226    fn k3() -> Graph {
227        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
228    }
229
230    fn k4() -> Graph {
231        Graph::from_edges(
232            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
233            false,
234            Some(4),
235        )
236        .unwrap()
237    }
238
239    fn cycle4() -> Graph {
240        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
241    }
242
243    fn star5() -> Graph {
244        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
245    }
246
247    // --- mostar_index ---
248
249    #[test]
250    fn mo_empty() {
251        let g = Graph::with_vertices(0);
252        assert_eq!(mostar_index(&g).unwrap(), 0);
253    }
254
255    #[test]
256    fn mo_no_edges() {
257        let g = Graph::with_vertices(3);
258        assert_eq!(mostar_index(&g).unwrap(), 0);
259    }
260
261    #[test]
262    fn mo_single_edge() {
263        // n_u=1, n_v=1 → |1-1| = 0
264        assert_eq!(mostar_index(&single_edge()).unwrap(), 0);
265    }
266
267    #[test]
268    fn mo_path3() {
269        // edge(0,1): nu=1, nv=2 → |1-2|=1
270        // edge(1,2): nu=2, nv=1 → |2-1|=1
271        // Mo = 2
272        assert_eq!(mostar_index(&path3()).unwrap(), 2);
273    }
274
275    #[test]
276    fn mo_path4() {
277        // edge(0,1): nu=1, nv=3 → 2
278        // edge(1,2): nu=2, nv=2 → 0
279        // edge(2,3): nu=3, nv=1 → 2
280        // Mo = 4
281        assert_eq!(mostar_index(&path4()).unwrap(), 4);
282    }
283
284    #[test]
285    fn mo_k3() {
286        // Each edge: nu=1, nv=1 → 0
287        // Mo = 0 (K_n is distance-balanced)
288        assert_eq!(mostar_index(&k3()).unwrap(), 0);
289    }
290
291    #[test]
292    fn mo_cycle4() {
293        // C4: each edge nu=2, nv=2 → 0 (cycles are distance-balanced)
294        assert_eq!(mostar_index(&cycle4()).unwrap(), 0);
295    }
296
297    #[test]
298    fn mo_star5() {
299        // edge(0,i): nu=4, nv=1 → 3; 4 edges → Mo = 12
300        assert_eq!(mostar_index(&star5()).unwrap(), 12);
301    }
302
303    #[test]
304    fn mo_regular_distance_balanced() {
305        // Complete graphs and cycles are distance-balanced
306        assert_eq!(mostar_index(&k4()).unwrap(), 0);
307        assert_eq!(mostar_index(&cycle4()).unwrap(), 0);
308    }
309
310    // --- degree_distance ---
311
312    #[test]
313    fn dd_empty() {
314        let g = Graph::with_vertices(0);
315        assert_eq!(degree_distance(&g).unwrap(), 0);
316    }
317
318    #[test]
319    fn dd_single_edge() {
320        // (0,1) and (1,0): (1+1)·1 = 2, twice → 4
321        assert_eq!(degree_distance(&single_edge()).unwrap(), 4);
322    }
323
324    #[test]
325    fn dd_path3() {
326        // degrees [1,2,1]
327        // (0,1)+(1,0): 2·(1+2)·1 = 6
328        // (0,2)+(2,0): 2·(1+1)·2 = 8
329        // (1,2)+(2,1): 2·(2+1)·1 = 6
330        // DD = 20
331        assert_eq!(degree_distance(&path3()).unwrap(), 20);
332    }
333
334    #[test]
335    fn dd_k3() {
336        // degrees all 2, all distances 1
337        // 6 ordered pairs, each (2+2)·1 = 4 → 24
338        assert_eq!(degree_distance(&k3()).unwrap(), 24);
339    }
340
341    #[test]
342    fn dd_k4() {
343        // degrees all 3, all distances 1
344        // 12 ordered pairs, each (3+3)·1 = 6 → 72
345        assert_eq!(degree_distance(&k4()).unwrap(), 72);
346    }
347
348    #[test]
349    fn dd_star5() {
350        // degrees [4,1,1,1,1]
351        // center-leaf pairs: 4·(4+1)·1 = 20 (×2 for both directions = 40)
352        // leaf-leaf pairs: d=2, (1+1)·2=4; 12 ordered pairs → 48
353        // DD = 40 + 48 = 88
354        assert_eq!(degree_distance(&star5()).unwrap(), 88);
355    }
356
357    // --- gutman_index ---
358
359    #[test]
360    fn gut_empty() {
361        let g = Graph::with_vertices(0);
362        assert_eq!(gutman_index(&g).unwrap(), 0);
363    }
364
365    #[test]
366    fn gut_single_edge() {
367        // (0,1): 1·1·1 = 1
368        assert_eq!(gutman_index(&single_edge()).unwrap(), 1);
369    }
370
371    #[test]
372    fn gut_path3() {
373        // (0,1): 1·2·1=2, (0,2): 1·1·2=2, (1,2): 2·1·1=2 → 6
374        assert_eq!(gutman_index(&path3()).unwrap(), 6);
375    }
376
377    #[test]
378    fn gut_k3() {
379        // 3 pairs, each 2·2·1=4 → 12
380        assert_eq!(gutman_index(&k3()).unwrap(), 12);
381    }
382
383    #[test]
384    fn gut_k4() {
385        // 6 pairs, each 3·3·1=9 → 54
386        assert_eq!(gutman_index(&k4()).unwrap(), 54);
387    }
388
389    #[test]
390    fn gut_star5() {
391        // center-leaf: 4 pairs, 4·1·1=4 each → 16
392        // leaf-leaf: 6 pairs, 1·1·2=2 each → 12
393        // Gut = 28
394        assert_eq!(gutman_index(&star5()).unwrap(), 28);
395    }
396
397    // --- cross-consistency ---
398
399    #[test]
400    fn dd_is_twice_sum_unordered() {
401        // DD sums over ordered pairs = 2 × sum over unordered pairs
402        for g in &[path3(), k3(), k4(), star5()] {
403            let dd = degree_distance(g).unwrap();
404            let n = g.vcount() as usize;
405            let dist = all_pairs_bfs(g, n);
406            let mut deg = vec![0_usize; n];
407            for v in 0..n as u32 {
408                deg[v as usize] = g.degree(v).unwrap();
409            }
410            let mut half: u64 = 0;
411            for u in 0..n {
412                for v in (u + 1)..n {
413                    let d = dist[u * n + v];
414                    if d == u32::MAX {
415                        continue;
416                    }
417                    half += (deg[u] as u64 + deg[v] as u64) * u64::from(d);
418                }
419            }
420            assert_eq!(dd, 2 * half);
421        }
422    }
423
424    #[test]
425    fn gutman_leq_dd_div2_times_max_deg() {
426        // Gut(G) <= DD(G)/2 · Δ(G) for simple graphs (loose bound check)
427        for g in &[path3(), k3(), k4(), star5()] {
428            let gut = gutman_index(g).unwrap();
429            let dd = degree_distance(g).unwrap();
430            let max_d = u64::from(
431                crate::algorithms::properties::degree::max_degree(
432                    g,
433                    crate::algorithms::properties::degree::DegreeMode::All,
434                )
435                .unwrap(),
436            );
437            assert!(
438                gut <= dd / 2 * max_d + max_d,
439                "Gutman {gut} too large relative to DD {dd}"
440            );
441        }
442    }
443}