Skip to main content

rust_igraph/algorithms/properties/
nirmala_index.rs

1//! Nirmala indices (ALGO-TR-064).
2//!
3//! - **Nirmala index** `N(G) = Σ_{(u,v)∈E} √(d(u) + d(v))`
4//!   Introduced by Kulli (2021). Square root of degree sum over edges.
5//! - **First inverse Nirmala index** `IN₁(G) = Σ_{(u,v)∈E} 1/√(d(u)+d(v))`
6//!   Reciprocal square root of degree sum.
7//! - **Second inverse Nirmala index** `IN₂(G) = Σ_{(u,v)∈E} 1/√(d(u)·d(v))`
8//!   Reciprocal square root of degree product (equals the Randić index).
9
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_precision_loss,
13    clippy::many_single_char_names,
14    clippy::needless_range_loop,
15    clippy::too_many_lines
16)]
17
18use crate::core::{Graph, IgraphResult};
19
20/// Compute the Nirmala index.
21///
22/// `N(G) = Σ_{(u,v)∈E} √(d(u) + d(v))`
23///
24/// Self-loops are skipped.
25///
26/// # Examples
27///
28/// ```
29/// use rust_igraph::{Graph, nirmala_index};
30///
31/// // K_3: 3 edges × √(2+2) = 3×2 = 6
32/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
33/// assert!((nirmala_index(&g).unwrap() - 6.0).abs() < 1e-10);
34/// ```
35pub fn nirmala_index(graph: &Graph) -> IgraphResult<f64> {
36    let mut n_idx = 0.0_f64;
37
38    for (u, v) in graph.edges() {
39        if u == v {
40            continue;
41        }
42        let du = graph.degree(u)? as f64;
43        let dv = graph.degree(v)? as f64;
44        n_idx += (du + dv).sqrt();
45    }
46
47    Ok(n_idx)
48}
49
50/// Compute the first inverse Nirmala index.
51///
52/// `IN₁(G) = Σ_{(u,v)∈E} 1/√(d(u) + d(v))`
53///
54/// Self-loops and edges where both endpoints have degree 0 are skipped.
55///
56/// # Examples
57///
58/// ```
59/// use rust_igraph::{Graph, first_inverse_nirmala};
60///
61/// // K_3: 3 edges × 1/√4 = 3×0.5 = 1.5
62/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
63/// assert!((first_inverse_nirmala(&g).unwrap() - 1.5).abs() < 1e-10);
64/// ```
65pub fn first_inverse_nirmala(graph: &Graph) -> IgraphResult<f64> {
66    let mut in1 = 0.0_f64;
67
68    for (u, v) in graph.edges() {
69        if u == v {
70            continue;
71        }
72        let du = graph.degree(u)? as f64;
73        let dv = graph.degree(v)? as f64;
74        let s = du + dv;
75        if s > 0.0 {
76            in1 += 1.0 / s.sqrt();
77        }
78    }
79
80    Ok(in1)
81}
82
83/// Compute the second inverse Nirmala index.
84///
85/// `IN₂(G) = Σ_{(u,v)∈E} 1/√(d(u) · d(v))`
86///
87/// This equals the Randić connectivity index `R(G)`.
88/// Self-loops and edges where either endpoint has degree 0 are skipped.
89///
90/// # Examples
91///
92/// ```
93/// use rust_igraph::{Graph, second_inverse_nirmala};
94///
95/// // K_3: 3 edges × 1/√(2·2) = 3×0.5 = 1.5
96/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
97/// assert!((second_inverse_nirmala(&g).unwrap() - 1.5).abs() < 1e-10);
98/// ```
99pub fn second_inverse_nirmala(graph: &Graph) -> IgraphResult<f64> {
100    let mut in2 = 0.0_f64;
101
102    for (u, v) in graph.edges() {
103        if u == v {
104            continue;
105        }
106        let du = graph.degree(u)? as f64;
107        let dv = graph.degree(v)? as f64;
108        let p = du * dv;
109        if p > 0.0 {
110            in2 += 1.0 / p.sqrt();
111        }
112    }
113
114    Ok(in2)
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    fn single_edge() -> Graph {
122        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
123    }
124
125    fn path3() -> Graph {
126        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
127    }
128
129    fn path4() -> Graph {
130        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
131    }
132
133    fn k3() -> Graph {
134        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
135    }
136
137    fn k4() -> Graph {
138        Graph::from_edges(
139            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
140            false,
141            Some(4),
142        )
143        .unwrap()
144    }
145
146    fn cycle4() -> Graph {
147        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
148    }
149
150    fn cycle5() -> Graph {
151        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
152    }
153
154    fn star5() -> Graph {
155        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
156    }
157
158    fn paw() -> Graph {
159        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
160    }
161
162    fn diamond() -> Graph {
163        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)], false, Some(4)).unwrap()
164    }
165
166    // --- nirmala_index ---
167
168    #[test]
169    fn nirmala_empty() {
170        let g = Graph::with_vertices(0);
171        assert!((nirmala_index(&g).unwrap()).abs() < 1e-10);
172    }
173
174    #[test]
175    fn nirmala_isolated() {
176        let g = Graph::with_vertices(5);
177        assert!((nirmala_index(&g).unwrap()).abs() < 1e-10);
178    }
179
180    #[test]
181    fn nirmala_single_edge() {
182        // √(1+1) = √2
183        let expected = 2.0_f64.sqrt();
184        assert!((nirmala_index(&single_edge()).unwrap() - expected).abs() < 1e-10);
185    }
186
187    #[test]
188    fn nirmala_path3() {
189        // (0,1):√(1+2)=√3, (1,2):√3 → 2√3
190        let expected = 2.0 * 3.0_f64.sqrt();
191        assert!((nirmala_index(&path3()).unwrap() - expected).abs() < 1e-10);
192    }
193
194    #[test]
195    fn nirmala_path4() {
196        // (0,1):√3, (1,2):√4=2, (2,3):√3 → 2√3 + 2
197        let expected = 2.0 * 3.0_f64.sqrt() + 2.0;
198        assert!((nirmala_index(&path4()).unwrap() - expected).abs() < 1e-10);
199    }
200
201    #[test]
202    fn nirmala_k3() {
203        // 3 × √(2+2) = 3×2 = 6
204        assert!((nirmala_index(&k3()).unwrap() - 6.0).abs() < 1e-10);
205    }
206
207    #[test]
208    fn nirmala_k4() {
209        // 6 × √(3+3) = 6√6
210        let expected = 6.0 * 6.0_f64.sqrt();
211        assert!((nirmala_index(&k4()).unwrap() - expected).abs() < 1e-10);
212    }
213
214    #[test]
215    fn nirmala_cycle4() {
216        // 4 × √4 = 4×2 = 8
217        assert!((nirmala_index(&cycle4()).unwrap() - 8.0).abs() < 1e-10);
218    }
219
220    #[test]
221    fn nirmala_cycle5() {
222        // 5 × 2 = 10
223        assert!((nirmala_index(&cycle5()).unwrap() - 10.0).abs() < 1e-10);
224    }
225
226    #[test]
227    fn nirmala_star5() {
228        // 4 × √(4+1) = 4√5
229        let expected = 4.0 * 5.0_f64.sqrt();
230        assert!((nirmala_index(&star5()).unwrap() - expected).abs() < 1e-10);
231    }
232
233    #[test]
234    fn nirmala_paw() {
235        // (0,1):√4=2, (0,2):√5, (1,2):√5, (2,3):√4=2
236        let expected = 4.0 + 2.0 * 5.0_f64.sqrt();
237        assert!((nirmala_index(&paw()).unwrap() - expected).abs() < 1e-10);
238    }
239
240    #[test]
241    fn nirmala_diamond() {
242        // (0,1):√6, (0,2):√5, (0,3):√5, (1,2):√5, (1,3):√5
243        let expected = 6.0_f64.sqrt() + 4.0 * 5.0_f64.sqrt();
244        assert!((nirmala_index(&diamond()).unwrap() - expected).abs() < 1e-10);
245    }
246
247    #[test]
248    fn nirmala_regular_formula() {
249        // r-regular: N = m·√(2r)
250        for g in &[k3(), k4(), cycle4(), cycle5()] {
251            let m = g.ecount() as f64;
252            let r = g.degree(0).unwrap() as f64;
253            let expected = m * (2.0 * r).sqrt();
254            assert!((nirmala_index(g).unwrap() - expected).abs() < 1e-8);
255        }
256    }
257
258    // --- first_inverse_nirmala ---
259
260    #[test]
261    fn in1_empty() {
262        let g = Graph::with_vertices(0);
263        assert!((first_inverse_nirmala(&g).unwrap()).abs() < 1e-10);
264    }
265
266    #[test]
267    fn in1_single_edge() {
268        // 1/√2
269        let expected = 1.0 / 2.0_f64.sqrt();
270        assert!((first_inverse_nirmala(&single_edge()).unwrap() - expected).abs() < 1e-10);
271    }
272
273    #[test]
274    fn in1_path3() {
275        // 2/√3
276        let expected = 2.0 / 3.0_f64.sqrt();
277        assert!((first_inverse_nirmala(&path3()).unwrap() - expected).abs() < 1e-10);
278    }
279
280    #[test]
281    fn in1_k3() {
282        // 3 × 1/√4 = 3/2 = 1.5
283        assert!((first_inverse_nirmala(&k3()).unwrap() - 1.5).abs() < 1e-10);
284    }
285
286    #[test]
287    fn in1_k4() {
288        // 6/√6 = √6
289        let expected = 6.0_f64.sqrt();
290        assert!((first_inverse_nirmala(&k4()).unwrap() - expected).abs() < 1e-10);
291    }
292
293    #[test]
294    fn in1_cycle4() {
295        // 4 × 1/√4 = 4/2 = 2
296        assert!((first_inverse_nirmala(&cycle4()).unwrap() - 2.0).abs() < 1e-10);
297    }
298
299    #[test]
300    fn in1_cycle5() {
301        // 5/2 = 2.5
302        assert!((first_inverse_nirmala(&cycle5()).unwrap() - 2.5).abs() < 1e-10);
303    }
304
305    #[test]
306    fn in1_star5() {
307        // 4/√5
308        let expected = 4.0 / 5.0_f64.sqrt();
309        assert!((first_inverse_nirmala(&star5()).unwrap() - expected).abs() < 1e-10);
310    }
311
312    #[test]
313    fn in1_paw() {
314        // (0,1):1/2, (0,2):1/√5, (1,2):1/√5, (2,3):1/2
315        let expected = 1.0 + 2.0 / 5.0_f64.sqrt();
316        assert!((first_inverse_nirmala(&paw()).unwrap() - expected).abs() < 1e-10);
317    }
318
319    #[test]
320    fn in1_regular_formula() {
321        // r-regular: IN₁ = m/√(2r)
322        for g in &[k3(), k4(), cycle4(), cycle5()] {
323            let m = g.ecount() as f64;
324            let r = g.degree(0).unwrap() as f64;
325            let expected = m / (2.0 * r).sqrt();
326            assert!((first_inverse_nirmala(g).unwrap() - expected).abs() < 1e-8);
327        }
328    }
329
330    // --- second_inverse_nirmala ---
331
332    #[test]
333    fn in2_empty() {
334        let g = Graph::with_vertices(0);
335        assert!((second_inverse_nirmala(&g).unwrap()).abs() < 1e-10);
336    }
337
338    #[test]
339    fn in2_single_edge() {
340        // 1/√(1·1) = 1
341        assert!((second_inverse_nirmala(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
342    }
343
344    #[test]
345    fn in2_path3() {
346        // (0,1):1/√2, (1,2):1/√2 → 2/√2 = √2
347        let expected = 2.0_f64.sqrt();
348        assert!((second_inverse_nirmala(&path3()).unwrap() - expected).abs() < 1e-10);
349    }
350
351    #[test]
352    fn in2_k3() {
353        // 3 × 1/√(2·2) = 3/2 = 1.5
354        assert!((second_inverse_nirmala(&k3()).unwrap() - 1.5).abs() < 1e-10);
355    }
356
357    #[test]
358    fn in2_k4() {
359        // 6 × 1/√9 = 6/3 = 2
360        assert!((second_inverse_nirmala(&k4()).unwrap() - 2.0).abs() < 1e-10);
361    }
362
363    #[test]
364    fn in2_cycle4() {
365        // 4/2 = 2
366        assert!((second_inverse_nirmala(&cycle4()).unwrap() - 2.0).abs() < 1e-10);
367    }
368
369    #[test]
370    fn in2_cycle5() {
371        // 5/2 = 2.5
372        assert!((second_inverse_nirmala(&cycle5()).unwrap() - 2.5).abs() < 1e-10);
373    }
374
375    #[test]
376    fn in2_star5() {
377        // 4 × 1/√4 = 4/2 = 2
378        assert!((second_inverse_nirmala(&star5()).unwrap() - 2.0).abs() < 1e-10);
379    }
380
381    #[test]
382    fn in2_paw() {
383        // (0,1):1/2, (0,2):1/√6, (1,2):1/√6, (2,3):1/√3
384        let expected = 0.5 + 2.0 / 6.0_f64.sqrt() + 1.0 / 3.0_f64.sqrt();
385        assert!((second_inverse_nirmala(&paw()).unwrap() - expected).abs() < 1e-10);
386    }
387
388    #[test]
389    fn in2_equals_randic() {
390        // IN₂ = Randić index = Σ 1/√(d(u)·d(v))
391        // Verify against known Randić values for regular graphs
392        // r-regular: R = m/r
393        for g in &[k3(), k4(), cycle4(), cycle5()] {
394            let m = g.ecount() as f64;
395            let r = g.degree(0).unwrap() as f64;
396            assert!((second_inverse_nirmala(g).unwrap() - m / r).abs() < 1e-8);
397        }
398    }
399
400    #[test]
401    fn in2_regular_formula() {
402        // r-regular: IN₂ = m/r
403        for g in &[k3(), k4(), cycle4(), cycle5()] {
404            let m = g.ecount() as f64;
405            let r = g.degree(0).unwrap() as f64;
406            let expected = m / r;
407            assert!((second_inverse_nirmala(g).unwrap() - expected).abs() < 1e-8);
408        }
409    }
410
411    // --- cross-consistency ---
412
413    #[test]
414    fn nirmala_times_in1_geq_m() {
415        // By Cauchy-Schwarz: N·IN₁ ≥ m²/m = m
416        // Actually: (Σ √s)·(Σ 1/√s) ≥ m² by Cauchy-Schwarz (with f=√s, g=1/√s)
417        // Wait: Σ(√s · 1/√s) = m, so by C-S: (Σ √s²)(Σ 1/s) ≥ m²? No.
418        // Correct: Cauchy-Schwarz: (Σ √s)(Σ 1/√s) ≥ (Σ 1)² = m²
419        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
420            let n_val = nirmala_index(g).unwrap();
421            let in1_val = first_inverse_nirmala(g).unwrap();
422            let m = g.ecount() as f64;
423            assert!(n_val * in1_val >= m * m - 1e-8);
424        }
425    }
426
427    #[test]
428    fn all_positive() {
429        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
430            assert!(nirmala_index(g).unwrap() > 0.0);
431            assert!(first_inverse_nirmala(g).unwrap() > 0.0);
432            assert!(second_inverse_nirmala(g).unwrap() > 0.0);
433        }
434    }
435}