Skip to main content

rust_igraph/algorithms/properties/
topological_indices.rs

1//! Chemical graph topological indices (ALGO-TR-040).
2//!
3//! Degree-based topological indices used in chemical graph theory and
4//! QSAR/QSPR modelling. All are defined for simple undirected graphs;
5//! directed graphs are treated as undirected (edge directions ignored).
6//!
7//! - **First Zagreb index** `M₁ = Σ_v deg(v)²`
8//! - **Second Zagreb index** `M₂ = Σ_{(u,v)∈E} deg(u)·deg(v)`
9//! - **Randić index** `R = Σ_{(u,v)∈E} 1/√(deg(u)·deg(v))`
10//! - **Atom-bond connectivity (ABC) index**
11//!   `ABC = Σ_{(u,v)∈E} √((deg(u)+deg(v)-2)/(deg(u)·deg(v)))`
12//! - **Harmonic index** `H = Σ_{(u,v)∈E} 2/(deg(u)+deg(v))`
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 first Zagreb index `M₁(G) = Σ deg(v)²`.
25///
26/// # Examples
27///
28/// ```
29/// use rust_igraph::{Graph, first_zagreb_index};
30///
31/// // Path 0-1-2: degrees [1, 2, 1], M₁ = 1+4+1 = 6
32/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
33/// assert_eq!(first_zagreb_index(&g).unwrap(), 6);
34/// ```
35pub fn first_zagreb_index(graph: &Graph) -> IgraphResult<u64> {
36    let n = graph.vcount() as usize;
37    let deg = compute_degrees(graph, n)?;
38
39    let mut m1: u64 = 0;
40    for &d in &deg {
41        m1 = m1.saturating_add((d as u64).saturating_mul(d as u64));
42    }
43
44    Ok(m1)
45}
46
47/// Compute the second Zagreb index `M₂(G) = Σ_{(u,v)∈E} deg(u)·deg(v)`.
48///
49/// # Examples
50///
51/// ```
52/// use rust_igraph::{Graph, second_zagreb_index};
53///
54/// // Path 0-1-2: edge (0,1): 1·2=2, edge (1,2): 2·1=2 → M₂ = 4
55/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
56/// assert_eq!(second_zagreb_index(&g).unwrap(), 4);
57/// ```
58pub fn second_zagreb_index(graph: &Graph) -> IgraphResult<u64> {
59    let n = graph.vcount() as usize;
60    let deg = compute_degrees(graph, n)?;
61
62    let mut m2: u64 = 0;
63    for (u, v) in graph.edges() {
64        let du = deg[u as usize] as u64;
65        let dv = deg[v as usize] as u64;
66        m2 = m2.saturating_add(du.saturating_mul(dv));
67    }
68
69    Ok(m2)
70}
71
72/// Compute the Randić connectivity index.
73///
74/// `R(G) = Σ_{(u,v)∈E} 1/√(deg(u)·deg(v))`
75///
76/// Isolated vertices and self-loops are ignored.
77///
78/// # Examples
79///
80/// ```
81/// use rust_igraph::{Graph, randic_index};
82///
83/// // Path 0-1-2: 1/√(1·2) + 1/√(2·1) = 2/√2 ≈ 1.414
84/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
85/// let r = randic_index(&g).unwrap();
86/// assert!((r - std::f64::consts::SQRT_2).abs() < 1e-10);
87/// ```
88pub fn randic_index(graph: &Graph) -> IgraphResult<f64> {
89    let n = graph.vcount() as usize;
90    let deg = compute_degrees(graph, n)?;
91
92    let mut r: f64 = 0.0;
93
94    for (u, v) in graph.edges() {
95        let ui = u as usize;
96        let vi = v as usize;
97        if ui == vi || deg[ui] == 0 || deg[vi] == 0 {
98            continue;
99        }
100        let product = (deg[ui] as f64) * (deg[vi] as f64);
101        r += 1.0 / product.sqrt();
102    }
103
104    Ok(r)
105}
106
107/// Compute the atom-bond connectivity (ABC) index.
108///
109/// `ABC(G) = Σ_{(u,v)∈E} √((deg(u)+deg(v)-2)/(deg(u)·deg(v)))`
110///
111/// # Examples
112///
113/// ```
114/// use rust_igraph::{Graph, abc_index};
115///
116/// // Single edge: √((1+1-2)/(1·1)) = 0
117/// let g = Graph::from_edges(&[(0,1)], false, Some(2)).unwrap();
118/// assert!((abc_index(&g).unwrap() - 0.0).abs() < 1e-10);
119/// ```
120pub fn abc_index(graph: &Graph) -> IgraphResult<f64> {
121    let n = graph.vcount() as usize;
122    let deg = compute_degrees(graph, n)?;
123
124    let mut abc: f64 = 0.0;
125
126    for (u, v) in graph.edges() {
127        let ui = u as usize;
128        let vi = v as usize;
129        if ui == vi || deg[ui] == 0 || deg[vi] == 0 {
130            continue;
131        }
132        let du = deg[ui] as f64;
133        let dv = deg[vi] as f64;
134        let numerator = du + dv - 2.0;
135        if numerator >= 0.0 {
136            abc += (numerator / (du * dv)).sqrt();
137        }
138    }
139
140    Ok(abc)
141}
142
143/// Compute the harmonic index.
144///
145/// `H(G) = Σ_{(u,v)∈E} 2/(deg(u)+deg(v))`
146///
147/// # Examples
148///
149/// ```
150/// use rust_igraph::{Graph, harmonic_graph_index};
151///
152/// // Path 0-1-2: 2/(1+2) + 2/(2+1) = 4/3 ≈ 1.333
153/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
154/// let h = harmonic_graph_index(&g).unwrap();
155/// assert!((h - 4.0/3.0).abs() < 1e-10);
156/// ```
157pub fn harmonic_graph_index(graph: &Graph) -> IgraphResult<f64> {
158    let n = graph.vcount() as usize;
159    let deg = compute_degrees(graph, n)?;
160
161    let mut h: f64 = 0.0;
162
163    for (u, v) in graph.edges() {
164        let ui = u as usize;
165        let vi = v as usize;
166        if ui == vi {
167            continue;
168        }
169        let sum_deg = deg[ui] as f64 + deg[vi] as f64;
170        if sum_deg > 0.0 {
171            h += 2.0 / sum_deg;
172        }
173    }
174
175    Ok(h)
176}
177
178fn compute_degrees(graph: &Graph, n: usize) -> IgraphResult<Vec<usize>> {
179    let mut deg = vec![0_usize; n];
180    for v in 0..n as u32 {
181        deg[v as usize] = graph.degree(v)?;
182    }
183    Ok(deg)
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    fn path3() -> Graph {
191        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
192    }
193
194    fn path4() -> Graph {
195        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
196    }
197
198    fn k3() -> Graph {
199        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
200    }
201
202    fn k4() -> Graph {
203        Graph::from_edges(
204            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
205            false,
206            Some(4),
207        )
208        .unwrap()
209    }
210
211    fn cycle4() -> Graph {
212        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
213    }
214
215    fn star5() -> Graph {
216        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
217    }
218
219    fn single_edge() -> Graph {
220        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
221    }
222
223    // --- first_zagreb_index ---
224
225    #[test]
226    fn fzi_empty() {
227        let g = Graph::with_vertices(0);
228        assert_eq!(first_zagreb_index(&g).unwrap(), 0);
229    }
230
231    #[test]
232    fn fzi_no_edges() {
233        let g = Graph::with_vertices(3);
234        assert_eq!(first_zagreb_index(&g).unwrap(), 0);
235    }
236
237    #[test]
238    fn fzi_single_edge() {
239        // degrees [1,1] → 1+1 = 2
240        assert_eq!(first_zagreb_index(&single_edge()).unwrap(), 2);
241    }
242
243    #[test]
244    fn fzi_path3() {
245        // degrees [1,2,1] → 1+4+1 = 6
246        assert_eq!(first_zagreb_index(&path3()).unwrap(), 6);
247    }
248
249    #[test]
250    fn fzi_path4() {
251        // degrees [1,2,2,1] → 1+4+4+1 = 10
252        assert_eq!(first_zagreb_index(&path4()).unwrap(), 10);
253    }
254
255    #[test]
256    fn fzi_k3() {
257        // all degree 2 → 3×4 = 12
258        assert_eq!(first_zagreb_index(&k3()).unwrap(), 12);
259    }
260
261    #[test]
262    fn fzi_k4() {
263        // all degree 3 → 4×9 = 36
264        assert_eq!(first_zagreb_index(&k4()).unwrap(), 36);
265    }
266
267    #[test]
268    fn fzi_cycle4() {
269        // all degree 2 → 4×4 = 16
270        assert_eq!(first_zagreb_index(&cycle4()).unwrap(), 16);
271    }
272
273    #[test]
274    fn fzi_star5() {
275        // center deg=4, leaves deg=1 → 16+1+1+1+1 = 20
276        assert_eq!(first_zagreb_index(&star5()).unwrap(), 20);
277    }
278
279    // --- second_zagreb_index ---
280
281    #[test]
282    fn szi_empty() {
283        let g = Graph::with_vertices(0);
284        assert_eq!(second_zagreb_index(&g).unwrap(), 0);
285    }
286
287    #[test]
288    fn szi_single_edge() {
289        // 1·1 = 1
290        assert_eq!(second_zagreb_index(&single_edge()).unwrap(), 1);
291    }
292
293    #[test]
294    fn szi_path3() {
295        // (0,1): 1·2=2, (1,2): 2·1=2 → 4
296        assert_eq!(second_zagreb_index(&path3()).unwrap(), 4);
297    }
298
299    #[test]
300    fn szi_k3() {
301        // 3 edges, each 2·2=4 → 12
302        assert_eq!(second_zagreb_index(&k3()).unwrap(), 12);
303    }
304
305    #[test]
306    fn szi_k4() {
307        // 6 edges, each 3·3=9 → 54
308        assert_eq!(second_zagreb_index(&k4()).unwrap(), 54);
309    }
310
311    #[test]
312    fn szi_star5() {
313        // 4 edges, each 4·1=4 → 16
314        assert_eq!(second_zagreb_index(&star5()).unwrap(), 16);
315    }
316
317    // --- randic_index ---
318
319    #[test]
320    fn ri_empty() {
321        let g = Graph::with_vertices(0);
322        assert!((randic_index(&g).unwrap() - 0.0).abs() < 1e-10);
323    }
324
325    #[test]
326    fn ri_single_edge() {
327        // 1/√(1·1) = 1
328        assert!((randic_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
329    }
330
331    #[test]
332    fn ri_path3() {
333        // 1/√2 + 1/√2 = √2
334        let r = randic_index(&path3()).unwrap();
335        assert!((r - std::f64::consts::SQRT_2).abs() < 1e-10);
336    }
337
338    #[test]
339    fn ri_k3() {
340        // 3 × 1/√(2·2) = 3/2 = 1.5
341        let r = randic_index(&k3()).unwrap();
342        assert!((r - 1.5).abs() < 1e-10);
343    }
344
345    #[test]
346    fn ri_k4() {
347        // 6 × 1/√(3·3) = 6/3 = 2.0
348        let r = randic_index(&k4()).unwrap();
349        assert!((r - 2.0).abs() < 1e-10);
350    }
351
352    // --- abc_index ---
353
354    #[test]
355    fn abc_single_edge() {
356        // √((1+1-2)/(1·1)) = 0
357        assert!((abc_index(&single_edge()).unwrap() - 0.0).abs() < 1e-10);
358    }
359
360    #[test]
361    fn abc_path3() {
362        // 2 edges: √((1+2-2)/(1·2)) = √(1/2) each → 2·√(0.5)
363        let a = abc_index(&path3()).unwrap();
364        assert!((a - 2.0 * (0.5_f64).sqrt()).abs() < 1e-10);
365    }
366
367    #[test]
368    fn abc_k3() {
369        // 3 edges: √((2+2-2)/(2·2)) = √(2/4) = √(0.5) each → 3·√(0.5)
370        let a = abc_index(&k3()).unwrap();
371        assert!((a - 3.0 * (0.5_f64).sqrt()).abs() < 1e-10);
372    }
373
374    // --- harmonic_graph_index ---
375
376    #[test]
377    fn hgi_empty() {
378        let g = Graph::with_vertices(0);
379        assert!((harmonic_graph_index(&g).unwrap() - 0.0).abs() < 1e-10);
380    }
381
382    #[test]
383    fn hgi_single_edge() {
384        // 2/(1+1) = 1
385        assert!((harmonic_graph_index(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
386    }
387
388    #[test]
389    fn hgi_path3() {
390        // 2/(1+2) + 2/(2+1) = 4/3
391        let h = harmonic_graph_index(&path3()).unwrap();
392        assert!((h - 4.0 / 3.0).abs() < 1e-10);
393    }
394
395    #[test]
396    fn hgi_k3() {
397        // 3 × 2/(2+2) = 3 × 0.5 = 1.5
398        let h = harmonic_graph_index(&k3()).unwrap();
399        assert!((h - 1.5).abs() < 1e-10);
400    }
401
402    #[test]
403    fn hgi_star5() {
404        // 4 × 2/(4+1) = 8/5 = 1.6
405        let h = harmonic_graph_index(&star5()).unwrap();
406        assert!((h - 1.6).abs() < 1e-10);
407    }
408
409    // --- cross-consistency ---
410
411    #[test]
412    fn m1_equals_sum_over_edges() {
413        // Alternative formula: M₁ = Σ_{(u,v)∈E} (deg(u)+deg(v))
414        for g in &[path3(), path4(), k3(), k4(), cycle4(), star5()] {
415            let m1 = first_zagreb_index(g).unwrap();
416            let n = g.vcount() as usize;
417            let deg: Vec<usize> = (0..n as u32).map(|v| g.degree(v).unwrap()).collect();
418
419            let mut seen = std::collections::HashSet::new();
420            let mut alt: u64 = 0;
421            for (u, v) in g.edges() {
422                let key = (u.min(v), u.max(v));
423                if seen.insert(key) {
424                    alt += deg[u as usize] as u64 + deg[v as usize] as u64;
425                }
426            }
427            assert_eq!(m1, alt, "M₁ edge sum formula mismatch");
428        }
429    }
430
431    #[test]
432    fn regular_graph_indices() {
433        // For r-regular graph with n vertices, m edges:
434        // M₁ = n·r², M₂ = m·r², R = m/r, H = m/r
435        let g = cycle4(); // 2-regular, n=4, m=4
436        assert_eq!(first_zagreb_index(&g).unwrap(), 16); // 4·4
437        assert_eq!(second_zagreb_index(&g).unwrap(), 16); // 4·4
438        let r = randic_index(&g).unwrap();
439        assert!((r - 2.0).abs() < 1e-10); // 4/2
440        let h = harmonic_graph_index(&g).unwrap();
441        assert!((h - 2.0).abs() < 1e-10); // 4 × 2/4 = 2
442    }
443}