Skip to main content

rust_igraph/algorithms/properties/
schultz_index.rs

1//! Schultz molecular topological index (ALGO-TR-053).
2//!
3//! **Schultz index** `S(G) = Σ_{u<v} (d(u) + d(v)) · dist(u,v)`
4//!
5//! Introduced by Schultz (1989). Measures molecular topology by
6//! weighting shortest-path distances with endpoint degree sums.
7//! Related to the Gutman index (which uses degree products instead
8//! of sums); both are defined in the `mostar_index` module.
9//!
10//! For disconnected graphs, infinite distances are excluded from
11//! the summation.
12
13#![allow(
14    clippy::cast_possible_truncation,
15    clippy::cast_precision_loss,
16    clippy::many_single_char_names,
17    clippy::needless_range_loop,
18    clippy::too_many_lines
19)]
20
21use crate::core::{Graph, IgraphResult};
22
23fn all_pairs_bfs(graph: &Graph, n: usize) -> Vec<u32> {
24    let mut dist = vec![u32::MAX; n * n];
25    for s in 0..n {
26        dist[s * n + s] = 0;
27        let mut queue = std::collections::VecDeque::new();
28        queue.push_back(s as u32);
29        while let Some(u) = queue.pop_front() {
30            let d_u = dist[s * n + u as usize];
31            if let Ok(nbs) = graph.neighbors(u) {
32                for nb in nbs {
33                    let idx = s * n + nb as usize;
34                    if dist[idx] == u32::MAX {
35                        dist[idx] = d_u + 1;
36                        queue.push_back(nb);
37                    }
38                }
39            }
40        }
41    }
42    dist
43}
44
45/// Compute the Schultz index (degree-distance index).
46///
47/// `S(G) = Σ_{u<v} (d(u) + d(v)) · dist(u,v)`
48///
49/// Only finite distances contribute. Returns 0 for graphs with
50/// fewer than 2 vertices.
51///
52/// # Examples
53///
54/// ```
55/// use rust_igraph::{Graph, schultz_index};
56///
57/// // Path 0-1-2: degrees [1,2,1], distances d(0,1)=1, d(0,2)=2, d(1,2)=1
58/// // S = (1+2)·1 + (1+1)·2 + (2+1)·1 = 3+4+3 = 10
59/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
60/// assert_eq!(schultz_index(&g).unwrap(), 10);
61/// ```
62pub fn schultz_index(graph: &Graph) -> IgraphResult<u64> {
63    let n = graph.vcount() as usize;
64    if n == 0 {
65        return Ok(0);
66    }
67
68    let dist = all_pairs_bfs(graph, n);
69    let mut deg = vec![0_usize; n];
70    for v in 0..n as u32 {
71        deg[v as usize] = graph.degree(v)?;
72    }
73
74    let mut s: u64 = 0;
75    for u in 0..n {
76        for v in (u + 1)..n {
77            let d = dist[u * n + v];
78            if d == u32::MAX {
79                continue;
80            }
81            let sum_deg = (deg[u] as u64).saturating_add(deg[v] as u64);
82            s = s.saturating_add(sum_deg.saturating_mul(u64::from(d)));
83        }
84    }
85
86    Ok(s)
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn single_edge() -> Graph {
94        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
95    }
96
97    fn path3() -> Graph {
98        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
99    }
100
101    fn path4() -> Graph {
102        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
103    }
104
105    fn path5() -> Graph {
106        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap()
107    }
108
109    fn k3() -> Graph {
110        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
111    }
112
113    fn k4() -> Graph {
114        Graph::from_edges(
115            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
116            false,
117            Some(4),
118        )
119        .unwrap()
120    }
121
122    fn cycle4() -> Graph {
123        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
124    }
125
126    fn cycle5() -> Graph {
127        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
128    }
129
130    fn cycle6() -> Graph {
131        Graph::from_edges(
132            &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 0)],
133            false,
134            Some(6),
135        )
136        .unwrap()
137    }
138
139    fn star5() -> Graph {
140        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
141    }
142
143    fn paw() -> Graph {
144        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
145    }
146
147    #[test]
148    fn schultz_empty() {
149        let g = Graph::with_vertices(0);
150        assert_eq!(schultz_index(&g).unwrap(), 0);
151    }
152
153    #[test]
154    fn schultz_single_vertex() {
155        let g = Graph::with_vertices(1);
156        assert_eq!(schultz_index(&g).unwrap(), 0);
157    }
158
159    #[test]
160    fn schultz_isolated() {
161        let g = Graph::with_vertices(5);
162        assert_eq!(schultz_index(&g).unwrap(), 0);
163    }
164
165    #[test]
166    fn schultz_single_edge() {
167        // degrees [1,1], dist=1: S = (1+1)·1 = 2
168        assert_eq!(schultz_index(&single_edge()).unwrap(), 2);
169    }
170
171    #[test]
172    fn schultz_path3() {
173        // degrees [1,2,1], dists: (0,1)=1,(0,2)=2,(1,2)=1
174        // S = (1+2)·1 + (1+1)·2 + (2+1)·1 = 3+4+3 = 10
175        assert_eq!(schultz_index(&path3()).unwrap(), 10);
176    }
177
178    #[test]
179    fn schultz_path4() {
180        // degrees [1,2,2,1]
181        // (0,1):(1+2)·1=3, (0,2):(1+2)·2=6, (0,3):(1+1)·3=6
182        // (1,2):(2+2)·1=4, (1,3):(2+1)·2=6, (2,3):(2+1)·1=3
183        // S = 3+6+6+4+6+3 = 28
184        assert_eq!(schultz_index(&path4()).unwrap(), 28);
185    }
186
187    #[test]
188    fn schultz_path5() {
189        // degrees [1,2,2,2,1]
190        // (0,1):(1+2)·1=3, (0,2):(1+2)·2=6, (0,3):(1+2)·3=9, (0,4):(1+1)·4=8
191        // (1,2):(2+2)·1=4, (1,3):(2+2)·2=8, (1,4):(2+1)·3=9
192        // (2,3):(2+2)·1=4, (2,4):(2+1)·2=6
193        // (3,4):(2+1)·1=3
194        // S = 3+6+9+8+4+8+9+4+6+3 = 60
195        assert_eq!(schultz_index(&path5()).unwrap(), 60);
196    }
197
198    #[test]
199    fn schultz_k3() {
200        // degrees [2,2,2], all dists=1, 3 pairs
201        // S = 3·(2+2)·1 = 12
202        assert_eq!(schultz_index(&k3()).unwrap(), 12);
203    }
204
205    #[test]
206    fn schultz_k4() {
207        // degrees [3,3,3,3], all dists=1, 6 pairs
208        // S = 6·(3+3)·1 = 36
209        assert_eq!(schultz_index(&k4()).unwrap(), 36);
210    }
211
212    #[test]
213    fn schultz_cycle4() {
214        // degrees [2,2,2,2]
215        // (0,1)=1,(0,2)=2,(0,3)=1,(1,2)=1,(1,3)=2,(2,3)=1
216        // 4 pairs at dist 1: 4·(2+2)·1=16
217        // 2 pairs at dist 2: 2·(2+2)·2=16
218        // S = 32
219        assert_eq!(schultz_index(&cycle4()).unwrap(), 32);
220    }
221
222    #[test]
223    fn schultz_cycle5() {
224        // degrees [2,2,2,2,2]
225        // 5 pairs at dist 1, 5 pairs at dist 2
226        // S = 5·4·1 + 5·4·2 = 20+40 = 60
227        assert_eq!(schultz_index(&cycle5()).unwrap(), 60);
228    }
229
230    #[test]
231    fn schultz_cycle6() {
232        // degrees [2,2,2,2,2,2], 15 pairs
233        // 6 at dist 1: 6·4·1=24
234        // 6 at dist 2: 6·4·2=48
235        // 3 at dist 3: 3·4·3=36
236        // S = 24+48+36 = 108
237        assert_eq!(schultz_index(&cycle6()).unwrap(), 108);
238    }
239
240    #[test]
241    fn schultz_star5() {
242        // degrees [4,1,1,1,1]
243        // (0,leaf): dist=1, 4 pairs, each (4+1)·1=5 → 20
244        // (leaf,leaf): dist=2, 6 pairs, each (1+1)·2=4 → 24
245        // S = 20+24 = 44
246        assert_eq!(schultz_index(&star5()).unwrap(), 44);
247    }
248
249    #[test]
250    fn schultz_paw() {
251        // degrees [2,2,3,1]
252        // (0,1):(2+2)·1=4, (0,2):(2+3)·1=5, (0,3):(2+1)·2=6
253        // (1,2):(2+3)·1=5, (1,3):(2+1)·2=6, (2,3):(3+1)·1=4
254        // S = 4+5+6+5+6+4 = 30
255        assert_eq!(schultz_index(&paw()).unwrap(), 30);
256    }
257
258    #[test]
259    fn schultz_disconnected() {
260        // Two isolated edges: 0-1 and 2-3
261        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
262        // Only finite pairs: (0,1) and (2,3)
263        // S = (1+1)·1 + (1+1)·1 = 4
264        assert_eq!(schultz_index(&g).unwrap(), 4);
265    }
266
267    #[test]
268    fn schultz_regular_formula() {
269        // r-regular: S = 2r · W(G) where W = Wiener index
270        for g in &[k3(), k4(), cycle4(), cycle5(), cycle6()] {
271            let r = g.degree(0).unwrap() as u64;
272            let n = g.vcount() as usize;
273            let dist = all_pairs_bfs(g, n);
274            let mut w: u64 = 0;
275            for u in 0..n {
276                for v in (u + 1)..n {
277                    let d = dist[u * n + v];
278                    if d != u32::MAX {
279                        w += u64::from(d);
280                    }
281                }
282            }
283            let expected = 2 * r * w;
284            assert_eq!(schultz_index(g).unwrap(), expected, "r={r}, W={w}");
285        }
286    }
287
288    #[test]
289    fn schultz_nonneg() {
290        for g in &[
291            single_edge(),
292            path3(),
293            path4(),
294            k3(),
295            k4(),
296            cycle4(),
297            star5(),
298            paw(),
299        ] {
300            assert!(schultz_index(g).unwrap() > 0);
301        }
302    }
303
304    #[test]
305    fn schultz_vs_gutman_regular() {
306        // For r-regular: S/Gut = 2r/(r²) = 2/r
307        // S = 2r·W, Gut = r²·W → S·r = 2·Gut
308        use crate::algorithms::properties::mostar_index::gutman_index;
309        for g in &[k3(), k4(), cycle4(), cycle5()] {
310            let r = g.degree(0).unwrap() as u64;
311            let s = schultz_index(g).unwrap();
312            let gut = gutman_index(g).unwrap();
313            assert_eq!(s * r, 2 * gut, "r={r}");
314        }
315    }
316
317    #[test]
318    fn schultz_geq_wiener_times_2() {
319        // S ≥ 2·W for any connected graph (since d(u)+d(v) ≥ 2 for all edges)
320        for g in &[single_edge(), path3(), k3(), cycle4(), star5()] {
321            let n = g.vcount() as usize;
322            let dist = all_pairs_bfs(g, n);
323            let mut w: u64 = 0;
324            for u in 0..n {
325                for v in (u + 1)..n {
326                    let d = dist[u * n + v];
327                    if d != u32::MAX {
328                        w += u64::from(d);
329                    }
330                }
331            }
332            assert!(schultz_index(g).unwrap() >= 2 * w);
333        }
334    }
335}