Skip to main content

rust_igraph/algorithms/properties/
exponential_indices.rs

1//! Exponential degree-based indices (ALGO-TR-073).
2//!
3//! Exponential versions of classical bond-additive indices, summing
4//! `exp(f(du,dv))` over edges. Well-studied in mathematical chemistry
5//! for their improved discriminating power:
6//!
7//! - **Exponential augmented Zagreb** `EAZ(G) = Σ_{(u,v)∈E} exp(du·dv/(du+dv-2))`
8//! - **Exponential Randić** `ER(G) = Σ_{(u,v)∈E} exp(1/√(du·dv))`
9//! - **Exponential atom-bond connectivity** `EABC(G) = Σ exp(√((du+dv-2)/(du·dv)))`
10//! - **Exponential geometric-arithmetic** `EGA(G) = Σ exp(2√(du·dv)/(du+dv))`
11
12#![allow(
13    clippy::cast_possible_truncation,
14    clippy::cast_precision_loss,
15    clippy::many_single_char_names,
16    clippy::needless_range_loop,
17    clippy::too_many_lines
18)]
19
20use crate::core::{Graph, IgraphResult};
21
22/// Compute the exponential augmented Zagreb index.
23///
24/// `EAZ(G) = Σ_{(u,v)∈E} exp(du·dv / (du+dv-2))`
25///
26/// Edges where `du + dv - 2 == 0` (i.e. both endpoints have degree 1,
27/// which requires a multi-edge or a single-edge graph with n=2) contribute
28/// `exp(+∞)` → skipped with a contribution of 0. Self-loops are skipped.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, exponential_augmented_zagreb};
34///
35/// // K_3: 3 edges, d=(2,2), each exp(4/2) = exp(2) → 3·exp(2)
36/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
37/// let expected = 3.0 * 2.0_f64.exp();
38/// assert!((exponential_augmented_zagreb(&g).unwrap() - expected).abs() < 1e-10);
39/// ```
40pub fn exponential_augmented_zagreb(graph: &Graph) -> IgraphResult<f64> {
41    let mut result = 0.0_f64;
42
43    for (u, v) in graph.edges() {
44        if u == v {
45            continue;
46        }
47        let du = graph.degree(u)? as f64;
48        let dv = graph.degree(v)? as f64;
49        let denom = du + dv - 2.0;
50        if denom > 0.0 {
51            result += (du * dv / denom).exp();
52        }
53    }
54
55    Ok(result)
56}
57
58/// Compute the exponential Randić index.
59///
60/// `ER(G) = Σ_{(u,v)∈E} exp(1 / √(du·dv))`
61///
62/// Edges with a degree-0 endpoint are skipped. Self-loops are skipped.
63///
64/// # Examples
65///
66/// ```
67/// use rust_igraph::{Graph, exponential_randic};
68///
69/// // K_3: 3 edges, d=(2,2), each exp(1/2) → 3·exp(0.5)
70/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
71/// let expected = 3.0 * 0.5_f64.exp();
72/// assert!((exponential_randic(&g).unwrap() - expected).abs() < 1e-10);
73/// ```
74pub fn exponential_randic(graph: &Graph) -> IgraphResult<f64> {
75    let mut result = 0.0_f64;
76
77    for (u, v) in graph.edges() {
78        if u == v {
79            continue;
80        }
81        let du = graph.degree(u)? as f64;
82        let dv = graph.degree(v)? as f64;
83        let product = du * dv;
84        if product > 0.0 {
85            result += (1.0 / product.sqrt()).exp();
86        }
87    }
88
89    Ok(result)
90}
91
92/// Compute the exponential atom-bond connectivity index.
93///
94/// `EABC(G) = Σ_{(u,v)∈E} exp(√((du+dv-2) / (du·dv)))`
95///
96/// Edges with a degree-0 endpoint or `du+dv-2 < 0` are skipped.
97/// Self-loops are skipped.
98///
99/// # Examples
100///
101/// ```
102/// use rust_igraph::{Graph, exponential_abc};
103///
104/// // K_3: 3 edges, d=(2,2), each exp(√(2/4)) = exp(1/√2) → 3·exp(1/√2)
105/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
106/// let expected = 3.0 * (1.0_f64 / 2.0_f64.sqrt()).exp();
107/// assert!((exponential_abc(&g).unwrap() - expected).abs() < 1e-10);
108/// ```
109pub fn exponential_abc(graph: &Graph) -> IgraphResult<f64> {
110    let mut result = 0.0_f64;
111
112    for (u, v) in graph.edges() {
113        if u == v {
114            continue;
115        }
116        let du = graph.degree(u)? as f64;
117        let dv = graph.degree(v)? as f64;
118        let product = du * dv;
119        let numer = du + dv - 2.0;
120        if product > 0.0 && numer >= 0.0 {
121            result += (numer / product).sqrt().exp();
122        }
123    }
124
125    Ok(result)
126}
127
128/// Compute the exponential geometric-arithmetic index.
129///
130/// `EGA(G) = Σ_{(u,v)∈E} exp(2√(du·dv) / (du+dv))`
131///
132/// Edges with `du+dv == 0` are skipped. Self-loops are skipped.
133///
134/// # Examples
135///
136/// ```
137/// use rust_igraph::{Graph, exponential_ga};
138///
139/// // K_3: 3 edges, d=(2,2), each exp(2·2/4) = exp(1) = e → 3e
140/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
141/// let expected = 3.0 * 1.0_f64.exp();
142/// assert!((exponential_ga(&g).unwrap() - expected).abs() < 1e-10);
143/// ```
144pub fn exponential_ga(graph: &Graph) -> IgraphResult<f64> {
145    let mut result = 0.0_f64;
146
147    for (u, v) in graph.edges() {
148        if u == v {
149            continue;
150        }
151        let du = graph.degree(u)? as f64;
152        let dv = graph.degree(v)? as f64;
153        let sum = du + dv;
154        if sum > 0.0 {
155            result += (2.0 * (du * dv).sqrt() / sum).exp();
156        }
157    }
158
159    Ok(result)
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    fn single_edge() -> Graph {
167        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
168    }
169
170    fn path3() -> Graph {
171        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
172    }
173
174    fn k3() -> Graph {
175        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
176    }
177
178    fn k4() -> Graph {
179        Graph::from_edges(
180            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
181            false,
182            Some(4),
183        )
184        .unwrap()
185    }
186
187    fn cycle4() -> Graph {
188        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
189    }
190
191    fn cycle5() -> Graph {
192        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
193    }
194
195    fn star5() -> Graph {
196        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
197    }
198
199    fn paw() -> Graph {
200        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
201    }
202
203    // --- exponential_augmented_zagreb ---
204
205    #[test]
206    fn eaz_empty() {
207        let g = Graph::with_vertices(0);
208        assert!(exponential_augmented_zagreb(&g).unwrap().abs() < 1e-10);
209    }
210
211    #[test]
212    fn eaz_isolated() {
213        let g = Graph::with_vertices(5);
214        assert!(exponential_augmented_zagreb(&g).unwrap().abs() < 1e-10);
215    }
216
217    #[test]
218    fn eaz_single_edge() {
219        // d=(1,1), du+dv-2=0 → skipped → 0
220        assert!(exponential_augmented_zagreb(&single_edge()).unwrap().abs() < 1e-10);
221    }
222
223    #[test]
224    fn eaz_k3() {
225        // d=(2,2), du*dv/(du+dv-2) = 4/2 = 2, exp(2)
226        let expected = 3.0 * 2.0_f64.exp();
227        assert!((exponential_augmented_zagreb(&k3()).unwrap() - expected).abs() < 1e-10);
228    }
229
230    #[test]
231    fn eaz_k4() {
232        // d=(3,3), 9/4 = 2.25, exp(2.25) per edge, 6 edges
233        let expected = 6.0 * 2.25_f64.exp();
234        assert!((exponential_augmented_zagreb(&k4()).unwrap() - expected).abs() < 1e-10);
235    }
236
237    #[test]
238    fn eaz_path3() {
239        // 2 edges d=(1,2): 1*2/(1+2-2) = 2/1 = 2, exp(2) each
240        let expected = 2.0 * 2.0_f64.exp();
241        assert!((exponential_augmented_zagreb(&path3()).unwrap() - expected).abs() < 1e-10);
242    }
243
244    #[test]
245    fn eaz_cycle4() {
246        // d=(2,2), 4/2=2, exp(2) per edge, 4 edges
247        let expected = 4.0 * 2.0_f64.exp();
248        assert!((exponential_augmented_zagreb(&cycle4()).unwrap() - expected).abs() < 1e-10);
249    }
250
251    #[test]
252    fn eaz_cycle5() {
253        let expected = 5.0 * 2.0_f64.exp();
254        assert!((exponential_augmented_zagreb(&cycle5()).unwrap() - expected).abs() < 1e-10);
255    }
256
257    #[test]
258    fn eaz_star5() {
259        // 4 edges d=(4,1): 4*1/(4+1-2) = 4/3, exp(4/3) each
260        let expected = 4.0 * (4.0_f64 / 3.0).exp();
261        assert!((exponential_augmented_zagreb(&star5()).unwrap() - expected).abs() < 1e-10);
262    }
263
264    #[test]
265    fn eaz_paw() {
266        // (0,1) d=(2,2): exp(2)
267        // (0,2) d=(2,3): exp(6/3)=exp(2)
268        // (1,2) d=(2,3): exp(2)
269        // (2,3) d=(3,1): exp(3/2)
270        let expected = 3.0 * 2.0_f64.exp() + 1.5_f64.exp();
271        assert!((exponential_augmented_zagreb(&paw()).unwrap() - expected).abs() < 1e-10);
272    }
273
274    // --- exponential_randic ---
275
276    #[test]
277    fn er_empty() {
278        let g = Graph::with_vertices(0);
279        assert!(exponential_randic(&g).unwrap().abs() < 1e-10);
280    }
281
282    #[test]
283    fn er_isolated() {
284        let g = Graph::with_vertices(5);
285        assert!(exponential_randic(&g).unwrap().abs() < 1e-10);
286    }
287
288    #[test]
289    fn er_single_edge() {
290        // d=(1,1), exp(1/1)=e
291        let expected = 1.0_f64.exp();
292        assert!((exponential_randic(&single_edge()).unwrap() - expected).abs() < 1e-10);
293    }
294
295    #[test]
296    fn er_k3() {
297        // d=(2,2), exp(1/2) per edge
298        let expected = 3.0 * 0.5_f64.exp();
299        assert!((exponential_randic(&k3()).unwrap() - expected).abs() < 1e-10);
300    }
301
302    #[test]
303    fn er_k4() {
304        // d=(3,3), exp(1/3) per edge
305        let expected = 6.0 * (1.0_f64 / 3.0).exp();
306        assert!((exponential_randic(&k4()).unwrap() - expected).abs() < 1e-10);
307    }
308
309    #[test]
310    fn er_path3() {
311        // d=(1,2): exp(1/√2)
312        let expected = 2.0 * (1.0 / 2.0_f64.sqrt()).exp();
313        assert!((exponential_randic(&path3()).unwrap() - expected).abs() < 1e-10);
314    }
315
316    #[test]
317    fn er_cycle4() {
318        // d=(2,2): exp(1/2) per edge
319        let expected = 4.0 * 0.5_f64.exp();
320        assert!((exponential_randic(&cycle4()).unwrap() - expected).abs() < 1e-10);
321    }
322
323    #[test]
324    fn er_cycle5() {
325        let expected = 5.0 * 0.5_f64.exp();
326        assert!((exponential_randic(&cycle5()).unwrap() - expected).abs() < 1e-10);
327    }
328
329    #[test]
330    fn er_star5() {
331        // d=(4,1): exp(1/2)
332        let expected = 4.0 * 0.5_f64.exp();
333        assert!((exponential_randic(&star5()).unwrap() - expected).abs() < 1e-10);
334    }
335
336    #[test]
337    fn er_paw() {
338        // (0,1) d=(2,2): exp(1/2)
339        // (0,2) d=(2,3): exp(1/√6)
340        // (1,2) d=(2,3): exp(1/√6)
341        // (2,3) d=(3,1): exp(1/√3)
342        let expected =
343            0.5_f64.exp() + 2.0 * (1.0 / 6.0_f64.sqrt()).exp() + (1.0 / 3.0_f64.sqrt()).exp();
344        assert!((exponential_randic(&paw()).unwrap() - expected).abs() < 1e-10);
345    }
346
347    // --- exponential_abc ---
348
349    #[test]
350    fn eabc_empty() {
351        let g = Graph::with_vertices(0);
352        assert!(exponential_abc(&g).unwrap().abs() < 1e-10);
353    }
354
355    #[test]
356    fn eabc_isolated() {
357        let g = Graph::with_vertices(5);
358        assert!(exponential_abc(&g).unwrap().abs() < 1e-10);
359    }
360
361    #[test]
362    fn eabc_single_edge() {
363        // d=(1,1), (1+1-2)/(1*1)=0, exp(0)=1
364        let expected = 1.0;
365        assert!((exponential_abc(&single_edge()).unwrap() - expected).abs() < 1e-10);
366    }
367
368    #[test]
369    fn eabc_k3() {
370        // d=(2,2), √(2/4)=1/√2, exp(1/√2)
371        let expected = 3.0 * (1.0 / 2.0_f64.sqrt()).exp();
372        assert!((exponential_abc(&k3()).unwrap() - expected).abs() < 1e-10);
373    }
374
375    #[test]
376    fn eabc_k4() {
377        // d=(3,3), √(4/9)=2/3, exp(2/3)
378        let expected = 6.0 * (2.0 / 3.0_f64).exp();
379        assert!((exponential_abc(&k4()).unwrap() - expected).abs() < 1e-10);
380    }
381
382    #[test]
383    fn eabc_path3() {
384        // d=(1,2): √((1+2-2)/(1*2))=√(1/2)=1/√2, exp(1/√2)
385        let expected = 2.0 * (1.0 / 2.0_f64.sqrt()).exp();
386        assert!((exponential_abc(&path3()).unwrap() - expected).abs() < 1e-10);
387    }
388
389    #[test]
390    fn eabc_cycle4() {
391        // d=(2,2): √(2/4)=1/√2, exp(1/√2)
392        let expected = 4.0 * (1.0 / 2.0_f64.sqrt()).exp();
393        assert!((exponential_abc(&cycle4()).unwrap() - expected).abs() < 1e-10);
394    }
395
396    #[test]
397    fn eabc_star5() {
398        // d=(4,1): √((4+1-2)/(4*1))=√(3/4)=√3/2, exp(√3/2)
399        let expected = 4.0 * (3.0_f64.sqrt() / 2.0).exp();
400        assert!((exponential_abc(&star5()).unwrap() - expected).abs() < 1e-10);
401    }
402
403    #[test]
404    fn eabc_paw() {
405        // (0,1) d=(2,2): exp(1/√2)
406        // (0,2) d=(2,3): √(3/6)=√(1/2)=1/√2, exp(1/√2)
407        // (1,2) d=(2,3): exp(1/√2)
408        // (2,3) d=(3,1): √(2/3), exp(√(2/3))
409        let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
410        let expected = 3.0 * inv_sqrt2.exp() + (2.0_f64 / 3.0).sqrt().exp();
411        assert!((exponential_abc(&paw()).unwrap() - expected).abs() < 1e-10);
412    }
413
414    // --- exponential_ga ---
415
416    #[test]
417    fn ega_empty() {
418        let g = Graph::with_vertices(0);
419        assert!(exponential_ga(&g).unwrap().abs() < 1e-10);
420    }
421
422    #[test]
423    fn ega_isolated() {
424        let g = Graph::with_vertices(5);
425        assert!(exponential_ga(&g).unwrap().abs() < 1e-10);
426    }
427
428    #[test]
429    fn ega_single_edge() {
430        // d=(1,1), 2√1/2=1, exp(1)=e
431        let expected = 1.0_f64.exp();
432        assert!((exponential_ga(&single_edge()).unwrap() - expected).abs() < 1e-10);
433    }
434
435    #[test]
436    fn ega_k3() {
437        // d=(2,2), 2·2/4=1, exp(1)=e
438        let expected = 3.0 * 1.0_f64.exp();
439        assert!((exponential_ga(&k3()).unwrap() - expected).abs() < 1e-10);
440    }
441
442    #[test]
443    fn ega_k4() {
444        // d=(3,3), 2·3/6=1, exp(1)=e
445        let expected = 6.0 * 1.0_f64.exp();
446        assert!((exponential_ga(&k4()).unwrap() - expected).abs() < 1e-10);
447    }
448
449    #[test]
450    fn ega_path3() {
451        // d=(1,2): 2√2/3, exp(2√2/3)
452        let expected = 2.0 * (2.0 * 2.0_f64.sqrt() / 3.0).exp();
453        assert!((exponential_ga(&path3()).unwrap() - expected).abs() < 1e-10);
454    }
455
456    #[test]
457    fn ega_cycle4() {
458        // d=(2,2): exp(1) per edge
459        let expected = 4.0 * 1.0_f64.exp();
460        assert!((exponential_ga(&cycle4()).unwrap() - expected).abs() < 1e-10);
461    }
462
463    #[test]
464    fn ega_cycle5() {
465        let expected = 5.0 * 1.0_f64.exp();
466        assert!((exponential_ga(&cycle5()).unwrap() - expected).abs() < 1e-10);
467    }
468
469    #[test]
470    fn ega_star5() {
471        // d=(4,1): 2·2/5=4/5, exp(4/5)
472        let expected = 4.0 * 0.8_f64.exp();
473        assert!((exponential_ga(&star5()).unwrap() - expected).abs() < 1e-10);
474    }
475
476    #[test]
477    fn ega_paw() {
478        // (0,1) d=(2,2): exp(1)
479        // (0,2) d=(2,3): 2√6/5, exp(2√6/5)
480        // (1,2) d=(2,3): exp(2√6/5)
481        // (2,3) d=(3,1): 2√3/4=√3/2, exp(√3/2)
482        let expected =
483            1.0_f64.exp() + 2.0 * (2.0 * 6.0_f64.sqrt() / 5.0).exp() + (3.0_f64.sqrt() / 2.0).exp();
484        assert!((exponential_ga(&paw()).unwrap() - expected).abs() < 1e-10);
485    }
486
487    // --- cross-consistency ---
488
489    #[test]
490    fn regular_ega_equals_m_times_e() {
491        // For r-regular, GA ratio = 1, so EGA = m·e
492        for g in &[k3(), k4(), cycle4(), cycle5()] {
493            let m = g.ecount() as f64;
494            let expected = m * 1.0_f64.exp();
495            assert!((exponential_ga(g).unwrap() - expected).abs() < 1e-8);
496        }
497    }
498
499    #[test]
500    fn regular_er() {
501        // For r-regular: ER = m·exp(1/r)
502        let g = &k4();
503        let m = g.ecount() as f64;
504        let expected = m * (1.0_f64 / 3.0).exp();
505        assert!((exponential_randic(g).unwrap() - expected).abs() < 1e-10);
506    }
507
508    #[test]
509    fn all_positive_for_nonempty() {
510        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
511            assert!(exponential_randic(g).unwrap() > 0.0);
512            assert!(exponential_abc(g).unwrap() > 0.0);
513            assert!(exponential_ga(g).unwrap() > 0.0);
514        }
515    }
516
517    #[test]
518    fn eaz_positive_for_connected_nonedge() {
519        // EAZ skips single-edge (denom=0) but positive for others
520        for g in &[path3(), k3(), k4(), cycle4(), star5(), paw()] {
521            assert!(exponential_augmented_zagreb(g).unwrap() > 0.0);
522        }
523    }
524
525    #[test]
526    fn ega_ge_m_for_nontrivial() {
527        // GA ratio ≤ 1, so EGA ≥ m·exp(0) = m... wait, GA ≤ 1 means
528        // 2√(du·dv)/(du+dv) ≤ 1, so exp(≤1) ≥ 1, so EGA ≥ m.
529        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
530            let m = g.ecount() as f64;
531            assert!(exponential_ga(g).unwrap() >= m - 1e-10);
532        }
533    }
534}