Skip to main content

rust_igraph/algorithms/properties/
index_entropy.rs

1//! Entropy-based topological indices (ALGO-TR-076).
2//!
3//! Shannon entropy applied to the probability distributions induced by
4//! classical topological indices. For a bond-additive index
5//! `I(G) = Σ f(du,dv)`, define `p_e = f(du,dv) / I(G)`, then:
6//!
7//! `ENT_I(G) = -Σ p_e · ln(p_e)`
8//!
9//! - **First Zagreb entropy** weights by `(du + dv)`
10//! - **Second Zagreb entropy** weights by `(du · dv)`
11//! - **Randić entropy** weights by `1/√(du·dv)`
12//! - **ABC entropy** weights by `√((du+dv-2)/(du·dv))`
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
24fn shannon_entropy(weights: &[f64]) -> f64 {
25    let total: f64 = weights.iter().sum();
26    if total <= 0.0 {
27        return 0.0;
28    }
29    let mut h = 0.0_f64;
30    for &w in weights {
31        if w > 0.0 {
32            let p = w / total;
33            h -= p * p.ln();
34        }
35    }
36    h
37}
38
39/// Compute the first Zagreb entropy.
40///
41/// `ENT_M1(G) = -Σ_e p_e·ln(p_e)` where `p_e = (du+dv)/M1(G)`.
42///
43/// Returns 0.0 for edgeless graphs.
44///
45/// # Examples
46///
47/// ```
48/// use rust_igraph::{Graph, first_zagreb_entropy};
49///
50/// // K_3: all weights equal (4,4,4) → ln(3)
51/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
52/// assert!((first_zagreb_entropy(&g).unwrap() - 3.0_f64.ln()).abs() < 1e-10);
53/// ```
54pub fn first_zagreb_entropy(graph: &Graph) -> IgraphResult<f64> {
55    let mut weights = Vec::new();
56
57    for (u, v) in graph.edges() {
58        if u == v {
59            continue;
60        }
61        let du = graph.degree(u)? as f64;
62        let dv = graph.degree(v)? as f64;
63        weights.push(du + dv);
64    }
65
66    Ok(shannon_entropy(&weights))
67}
68
69/// Compute the second Zagreb entropy.
70///
71/// `ENT_M2(G) = -Σ_e p_e·ln(p_e)` where `p_e = (du·dv)/M2(G)`.
72///
73/// Returns 0.0 for edgeless graphs.
74///
75/// # Examples
76///
77/// ```
78/// use rust_igraph::{Graph, second_zagreb_entropy};
79///
80/// // K_3: all weights equal (4,4,4) → ln(3)
81/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
82/// assert!((second_zagreb_entropy(&g).unwrap() - 3.0_f64.ln()).abs() < 1e-10);
83/// ```
84pub fn second_zagreb_entropy(graph: &Graph) -> IgraphResult<f64> {
85    let mut weights = Vec::new();
86
87    for (u, v) in graph.edges() {
88        if u == v {
89            continue;
90        }
91        let du = graph.degree(u)? as f64;
92        let dv = graph.degree(v)? as f64;
93        let product = du * dv;
94        if product > 0.0 {
95            weights.push(product);
96        }
97    }
98
99    Ok(shannon_entropy(&weights))
100}
101
102/// Compute the Randić entropy.
103///
104/// `ENT_R(G) = -Σ_e p_e·ln(p_e)` where `p_e = (1/√(du·dv))/R(G)`.
105///
106/// Returns 0.0 for edgeless graphs.
107///
108/// # Examples
109///
110/// ```
111/// use rust_igraph::{Graph, randic_entropy};
112///
113/// // K_3: all weights equal → ln(3)
114/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
115/// assert!((randic_entropy(&g).unwrap() - 3.0_f64.ln()).abs() < 1e-10);
116/// ```
117pub fn randic_entropy(graph: &Graph) -> IgraphResult<f64> {
118    let mut weights = Vec::new();
119
120    for (u, v) in graph.edges() {
121        if u == v {
122            continue;
123        }
124        let du = graph.degree(u)? as f64;
125        let dv = graph.degree(v)? as f64;
126        let product = du * dv;
127        if product > 0.0 {
128            weights.push(1.0 / product.sqrt());
129        }
130    }
131
132    Ok(shannon_entropy(&weights))
133}
134
135/// Compute the ABC entropy.
136///
137/// `ENT_ABC(G) = -Σ_e p_e·ln(p_e)` where
138/// `p_e = √((du+dv-2)/(du·dv)) / ABC(G)`.
139///
140/// Returns 0.0 for edgeless graphs.
141///
142/// # Examples
143///
144/// ```
145/// use rust_igraph::{Graph, abc_entropy};
146///
147/// // K_3: all weights equal → ln(3)
148/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
149/// assert!((abc_entropy(&g).unwrap() - 3.0_f64.ln()).abs() < 1e-10);
150/// ```
151pub fn abc_entropy(graph: &Graph) -> IgraphResult<f64> {
152    let mut weights = Vec::new();
153
154    for (u, v) in graph.edges() {
155        if u == v {
156            continue;
157        }
158        let du = graph.degree(u)? as f64;
159        let dv = graph.degree(v)? as f64;
160        let product = du * dv;
161        let numer = du + dv - 2.0;
162        if product > 0.0 && numer > 0.0 {
163            weights.push((numer / product).sqrt());
164        }
165    }
166
167    Ok(shannon_entropy(&weights))
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    fn single_edge() -> Graph {
175        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
176    }
177
178    fn path3() -> Graph {
179        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
180    }
181
182    fn k3() -> Graph {
183        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
184    }
185
186    fn k4() -> Graph {
187        Graph::from_edges(
188            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
189            false,
190            Some(4),
191        )
192        .unwrap()
193    }
194
195    fn cycle4() -> Graph {
196        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
197    }
198
199    fn cycle5() -> Graph {
200        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
201    }
202
203    fn star5() -> Graph {
204        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
205    }
206
207    fn paw() -> Graph {
208        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
209    }
210
211    // --- first_zagreb_entropy ---
212
213    #[test]
214    fn fze_empty() {
215        let g = Graph::with_vertices(0);
216        assert!(first_zagreb_entropy(&g).unwrap().abs() < 1e-10);
217    }
218
219    #[test]
220    fn fze_isolated() {
221        let g = Graph::with_vertices(5);
222        assert!(first_zagreb_entropy(&g).unwrap().abs() < 1e-10);
223    }
224
225    #[test]
226    fn fze_single_edge() {
227        // 1 edge → 1 weight → entropy 0 (ln(1)=0)
228        assert!(first_zagreb_entropy(&single_edge()).unwrap().abs() < 1e-10);
229    }
230
231    #[test]
232    fn fze_k3() {
233        // 3 equal weights → ln(3)
234        assert!((first_zagreb_entropy(&k3()).unwrap() - 3.0_f64.ln()).abs() < 1e-10);
235    }
236
237    #[test]
238    fn fze_k4() {
239        // 6 equal weights → ln(6)
240        assert!((first_zagreb_entropy(&k4()).unwrap() - 6.0_f64.ln()).abs() < 1e-10);
241    }
242
243    #[test]
244    fn fze_cycle4() {
245        // 4 equal weights → ln(4)
246        assert!((first_zagreb_entropy(&cycle4()).unwrap() - 4.0_f64.ln()).abs() < 1e-10);
247    }
248
249    #[test]
250    fn fze_cycle5() {
251        // 5 equal weights → ln(5)
252        assert!((first_zagreb_entropy(&cycle5()).unwrap() - 5.0_f64.ln()).abs() < 1e-10);
253    }
254
255    #[test]
256    fn fze_path3() {
257        // 2 edges, d=(1,2): weights 3,3 → equal → ln(2)
258        assert!((first_zagreb_entropy(&path3()).unwrap() - 2.0_f64.ln()).abs() < 1e-10);
259    }
260
261    #[test]
262    fn fze_star5() {
263        // 4 edges, d=(4,1): all weights 5 → ln(4)
264        assert!((first_zagreb_entropy(&star5()).unwrap() - 4.0_f64.ln()).abs() < 1e-10);
265    }
266
267    #[test]
268    fn fze_paw() {
269        // (0,1)d=(2,2):4, (0,2)d=(2,3):5, (1,2)d=(2,3):5, (2,3)d=(3,1):4
270        // total=18, p=4/18,5/18,5/18,4/18
271        let total = 18.0_f64;
272        let expected =
273            -2.0 * (4.0 / total) * (4.0 / total).ln() - 2.0 * (5.0 / total) * (5.0 / total).ln();
274        assert!((first_zagreb_entropy(&paw()).unwrap() - expected).abs() < 1e-10);
275    }
276
277    // --- second_zagreb_entropy ---
278
279    #[test]
280    fn sze_empty() {
281        let g = Graph::with_vertices(0);
282        assert!(second_zagreb_entropy(&g).unwrap().abs() < 1e-10);
283    }
284
285    #[test]
286    fn sze_single_edge() {
287        assert!(second_zagreb_entropy(&single_edge()).unwrap().abs() < 1e-10);
288    }
289
290    #[test]
291    fn sze_k3() {
292        assert!((second_zagreb_entropy(&k3()).unwrap() - 3.0_f64.ln()).abs() < 1e-10);
293    }
294
295    #[test]
296    fn sze_k4() {
297        assert!((second_zagreb_entropy(&k4()).unwrap() - 6.0_f64.ln()).abs() < 1e-10);
298    }
299
300    #[test]
301    fn sze_cycle4() {
302        assert!((second_zagreb_entropy(&cycle4()).unwrap() - 4.0_f64.ln()).abs() < 1e-10);
303    }
304
305    #[test]
306    fn sze_path3() {
307        // d=(1,2): weights 2,2 → ln(2)
308        assert!((second_zagreb_entropy(&path3()).unwrap() - 2.0_f64.ln()).abs() < 1e-10);
309    }
310
311    #[test]
312    fn sze_star5() {
313        // d=(4,1): weights all 4 → ln(4)
314        assert!((second_zagreb_entropy(&star5()).unwrap() - 4.0_f64.ln()).abs() < 1e-10);
315    }
316
317    #[test]
318    fn sze_paw() {
319        // (0,1):4, (0,2):6, (1,2):6, (2,3):3
320        let total = 19.0_f64;
321        let expected = -(4.0 / total) * (4.0 / total).ln()
322            - 2.0 * (6.0 / total) * (6.0 / total).ln()
323            - (3.0 / total) * (3.0 / total).ln();
324        assert!((second_zagreb_entropy(&paw()).unwrap() - expected).abs() < 1e-10);
325    }
326
327    // --- randic_entropy ---
328
329    #[test]
330    fn re_empty() {
331        let g = Graph::with_vertices(0);
332        assert!(randic_entropy(&g).unwrap().abs() < 1e-10);
333    }
334
335    #[test]
336    fn re_single_edge() {
337        assert!(randic_entropy(&single_edge()).unwrap().abs() < 1e-10);
338    }
339
340    #[test]
341    fn re_k3() {
342        assert!((randic_entropy(&k3()).unwrap() - 3.0_f64.ln()).abs() < 1e-10);
343    }
344
345    #[test]
346    fn re_k4() {
347        assert!((randic_entropy(&k4()).unwrap() - 6.0_f64.ln()).abs() < 1e-10);
348    }
349
350    #[test]
351    fn re_cycle4() {
352        assert!((randic_entropy(&cycle4()).unwrap() - 4.0_f64.ln()).abs() < 1e-10);
353    }
354
355    #[test]
356    fn re_path3() {
357        // d=(1,2): weights 1/√2, 1/√2 → equal → ln(2)
358        assert!((randic_entropy(&path3()).unwrap() - 2.0_f64.ln()).abs() < 1e-10);
359    }
360
361    #[test]
362    fn re_star5() {
363        // d=(4,1): all 1/2 → ln(4)
364        assert!((randic_entropy(&star5()).unwrap() - 4.0_f64.ln()).abs() < 1e-10);
365    }
366
367    #[test]
368    fn re_paw() {
369        // (0,1):1/2, (0,2):1/√6, (1,2):1/√6, (2,3):1/√3
370        let w0 = 0.5;
371        let w1 = 1.0 / 6.0_f64.sqrt();
372        let w3 = 1.0 / 3.0_f64.sqrt();
373        let total = w0 + 2.0 * w1 + w3;
374        let expected = -(w0 / total) * (w0 / total).ln()
375            - 2.0 * (w1 / total) * (w1 / total).ln()
376            - (w3 / total) * (w3 / total).ln();
377        assert!((randic_entropy(&paw()).unwrap() - expected).abs() < 1e-10);
378    }
379
380    // --- abc_entropy ---
381
382    #[test]
383    fn abce_empty() {
384        let g = Graph::with_vertices(0);
385        assert!(abc_entropy(&g).unwrap().abs() < 1e-10);
386    }
387
388    #[test]
389    fn abce_single_edge() {
390        // d=(1,1): numer=0 → skipped → 0
391        assert!(abc_entropy(&single_edge()).unwrap().abs() < 1e-10);
392    }
393
394    #[test]
395    fn abce_k3() {
396        assert!((abc_entropy(&k3()).unwrap() - 3.0_f64.ln()).abs() < 1e-10);
397    }
398
399    #[test]
400    fn abce_k4() {
401        assert!((abc_entropy(&k4()).unwrap() - 6.0_f64.ln()).abs() < 1e-10);
402    }
403
404    #[test]
405    fn abce_cycle4() {
406        assert!((abc_entropy(&cycle4()).unwrap() - 4.0_f64.ln()).abs() < 1e-10);
407    }
408
409    #[test]
410    fn abce_path3() {
411        // d=(1,2): √(1/2) each → equal → ln(2)
412        assert!((abc_entropy(&path3()).unwrap() - 2.0_f64.ln()).abs() < 1e-10);
413    }
414
415    #[test]
416    fn abce_star5() {
417        // d=(4,1): √(3/4) each → equal → ln(4)
418        assert!((abc_entropy(&star5()).unwrap() - 4.0_f64.ln()).abs() < 1e-10);
419    }
420
421    #[test]
422    fn abce_paw() {
423        // (0,1):√(2/4)=1/√2, (0,2):√(3/6)=1/√2, (1,2):1/√2, (2,3):√(2/3)
424        let w0 = 1.0 / 2.0_f64.sqrt();
425        let w3 = (2.0_f64 / 3.0).sqrt();
426        let total = 3.0 * w0 + w3;
427        let expected = -3.0 * (w0 / total) * (w0 / total).ln() - (w3 / total) * (w3 / total).ln();
428        assert!((abc_entropy(&paw()).unwrap() - expected).abs() < 1e-10);
429    }
430
431    // --- cross-consistency ---
432
433    #[test]
434    fn regular_all_equal_ln_m() {
435        // Regular graphs: all edge weights equal → entropy = ln(m)
436        for g in &[k3(), k4(), cycle4(), cycle5()] {
437            let ln_m = (g.ecount() as f64).ln();
438            assert!((first_zagreb_entropy(g).unwrap() - ln_m).abs() < 1e-10);
439            assert!((second_zagreb_entropy(g).unwrap() - ln_m).abs() < 1e-10);
440            assert!((randic_entropy(g).unwrap() - ln_m).abs() < 1e-10);
441            assert!((abc_entropy(g).unwrap() - ln_m).abs() < 1e-10);
442        }
443    }
444
445    #[test]
446    fn entropy_nonnegative() {
447        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
448            assert!(first_zagreb_entropy(g).unwrap() >= -1e-10);
449            assert!(second_zagreb_entropy(g).unwrap() >= -1e-10);
450            assert!(randic_entropy(g).unwrap() >= -1e-10);
451            assert!(abc_entropy(g).unwrap() >= -1e-10);
452        }
453    }
454
455    #[test]
456    fn entropy_le_ln_m() {
457        // Shannon entropy ≤ ln(m) always (maximum for uniform distribution)
458        for g in &[path3(), k3(), k4(), cycle4(), star5(), paw()] {
459            let ln_m = (g.ecount() as f64).ln();
460            assert!(first_zagreb_entropy(g).unwrap() <= ln_m + 1e-10);
461            assert!(second_zagreb_entropy(g).unwrap() <= ln_m + 1e-10);
462            assert!(randic_entropy(g).unwrap() <= ln_m + 1e-10);
463        }
464    }
465}