Skip to main content

rust_igraph/algorithms/properties/
information_ratios.rs

1//! Information-theoretic ratio indices (ALGO-TR-115).
2//!
3//! Entropy-based measures of graph structure:
4//!
5//! - **Degree entropy ratio** — Shannon entropy of degree distribution
6//!   normalized by log(n)
7//! - **Edge distribution entropy** — entropy of the edge-endpoint degree
8//!   distribution normalized by log(2m)
9//! - **Structural information content** — log2 of the number of distinct
10//!   degree classes, normalized by log2(n)
11
12#![allow(
13    clippy::cast_lossless,
14    clippy::cast_possible_truncation,
15    clippy::cast_precision_loss,
16    clippy::many_single_char_names,
17    clippy::needless_range_loop,
18    clippy::similar_names,
19    clippy::too_many_lines
20)]
21
22use crate::core::{Graph, IgraphResult};
23
24/// Compute the degree entropy ratio.
25///
26/// Shannon entropy of the degree distribution divided by log(n).
27/// Measures how uniform the degree distribution is. Returns 1.0 for
28/// regular graphs, values < 1 for heterogeneous degree distributions.
29/// Returns 0.0 for trivial graphs.
30///
31/// # Examples
32///
33/// ```
34/// use rust_igraph::{Graph, degree_entropy_ratio};
35///
36/// // K_3: all degrees equal → H = log(1) = 0... wait, p=1 for one class
37/// // Actually all vertices have same degree → 1 class → H=0, but ratio = 0/log(3)?
38/// // Better: P(d=2) = 1 → H = -1*log(1) = 0 → ratio = 0
39/// // For non-trivial: cycle has uniform degrees too
40/// // Let's use star: center deg=4, leaves deg=1
41/// let star = Graph::from_edges(&[(0,1),(0,2),(0,3),(0,4)], false, Some(5)).unwrap();
42/// let r = degree_entropy_ratio(&star).unwrap();
43/// assert!(r > 0.0 && r < 1.0);
44/// ```
45pub fn degree_entropy_ratio(graph: &Graph) -> IgraphResult<f64> {
46    let n = graph.vcount() as usize;
47    if n < 2 {
48        return Ok(0.0);
49    }
50
51    let mut degrees = Vec::with_capacity(n);
52    for v in 0..n {
53        degrees.push(graph.degree(v as u32)?);
54    }
55
56    // Count degree frequencies
57    let max_deg = degrees.iter().copied().max().unwrap_or(0);
58    let mut freq = vec![0_u64; max_deg + 1];
59    for &d in &degrees {
60        freq[d] += 1;
61    }
62
63    let n_f = n as f64;
64    let mut entropy = 0.0_f64;
65    for &f in &freq {
66        if f > 0 {
67            let p = f as f64 / n_f;
68            entropy -= p * p.ln();
69        }
70    }
71
72    let max_entropy = n_f.ln();
73    if max_entropy < 1e-30 {
74        return Ok(0.0);
75    }
76
77    Ok(entropy / max_entropy)
78}
79
80/// Compute the edge distribution entropy.
81///
82/// For each edge (u,v), consider the pair (d(u), d(v)) as a sample from
83/// the joint degree distribution. Compute Shannon entropy of this
84/// distribution normalized by log(2m). Measures how diverse edge
85/// types are in terms of endpoint degrees. Returns 0.0 for edgeless
86/// or trivial graphs.
87///
88/// # Examples
89///
90/// ```
91/// use rust_igraph::{Graph, edge_distribution_entropy};
92///
93/// // K_3: all edges connect degree-2 vertices → 1 class → H=0
94/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
95/// assert!(edge_distribution_entropy(&g).unwrap().abs() < 1e-10);
96/// ```
97pub fn edge_distribution_entropy(graph: &Graph) -> IgraphResult<f64> {
98    let n = graph.vcount() as usize;
99    if n < 2 {
100        return Ok(0.0);
101    }
102
103    let m = graph.ecount();
104    if m == 0 {
105        return Ok(0.0);
106    }
107
108    let mut degrees = Vec::with_capacity(n);
109    for v in 0..n {
110        degrees.push(graph.degree(v as u32)?);
111    }
112
113    // Count edge type frequencies: key = (min_deg, max_deg)
114    let mut edge_types: std::collections::HashMap<(usize, usize), u64> =
115        std::collections::HashMap::new();
116
117    for v in 0..n {
118        let nbrs = graph.neighbors(v as u32)?;
119        for &u in &nbrs {
120            let ui = u as usize;
121            if ui > v {
122                let d1 = degrees[v].min(degrees[ui]);
123                let d2 = degrees[v].max(degrees[ui]);
124                *edge_types.entry((d1, d2)).or_insert(0) += 1;
125            }
126        }
127    }
128
129    let m_f = m as f64;
130    let mut entropy = 0.0_f64;
131    for &count in edge_types.values() {
132        if count > 0 {
133            let p = count as f64 / m_f;
134            entropy -= p * p.ln();
135        }
136    }
137
138    let max_entropy = m_f.ln();
139    if max_entropy < 1e-30 {
140        return Ok(0.0);
141    }
142
143    Ok(entropy / max_entropy)
144}
145
146/// Compute the structural information content.
147///
148/// `log2(k) / log2(n)` where k is the number of distinct degree values
149/// in the graph. Measures the structural diversity of the vertex roles
150/// by degree. Returns 1.0 when every vertex has a unique degree (e.g.
151/// path graphs for n≥3). Returns 0.0 for regular graphs or trivial
152/// graphs.
153///
154/// # Examples
155///
156/// ```
157/// use rust_igraph::{Graph, structural_information_content};
158///
159/// // K_4: all degrees 3 → k=1 → log2(1)/log2(4) = 0
160/// let g = Graph::from_edges(
161///     &[(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)], false, Some(4)
162/// ).unwrap();
163/// assert!(structural_information_content(&g).unwrap().abs() < 1e-10);
164/// ```
165pub fn structural_information_content(graph: &Graph) -> IgraphResult<f64> {
166    let n = graph.vcount() as usize;
167    if n < 2 {
168        return Ok(0.0);
169    }
170
171    let mut degrees = Vec::with_capacity(n);
172    for v in 0..n {
173        degrees.push(graph.degree(v as u32)?);
174    }
175
176    let mut seen = std::collections::HashSet::new();
177    for &d in &degrees {
178        seen.insert(d);
179    }
180
181    let k = seen.len();
182    if k <= 1 {
183        return Ok(0.0);
184    }
185
186    let log2_k = (k as f64).log2();
187    let log2_n = (n as f64).log2();
188
189    if log2_n < 1e-30 {
190        return Ok(0.0);
191    }
192
193    Ok(log2_k / log2_n)
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    fn empty() -> Graph {
201        Graph::with_vertices(0)
202    }
203
204    fn single() -> Graph {
205        Graph::with_vertices(1)
206    }
207
208    fn single_edge() -> Graph {
209        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
210    }
211
212    fn path3() -> Graph {
213        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
214    }
215
216    fn path4() -> Graph {
217        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
218    }
219
220    fn k3() -> Graph {
221        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
222    }
223
224    fn k4() -> Graph {
225        Graph::from_edges(
226            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
227            false,
228            Some(4),
229        )
230        .unwrap()
231    }
232
233    fn cycle4() -> Graph {
234        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
235    }
236
237    fn star5() -> Graph {
238        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
239    }
240
241    fn paw() -> Graph {
242        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
243    }
244
245    // --- degree_entropy_ratio ---
246
247    #[test]
248    fn der_empty() {
249        assert!(degree_entropy_ratio(&empty()).unwrap().abs() < 1e-10);
250    }
251
252    #[test]
253    fn der_single() {
254        assert!(degree_entropy_ratio(&single()).unwrap().abs() < 1e-10);
255    }
256
257    #[test]
258    fn der_k3() {
259        // All same degree → H=0 → ratio=0
260        assert!(degree_entropy_ratio(&k3()).unwrap().abs() < 1e-10);
261    }
262
263    #[test]
264    fn der_k4() {
265        assert!(degree_entropy_ratio(&k4()).unwrap().abs() < 1e-10);
266    }
267
268    #[test]
269    fn der_cycle4() {
270        assert!(degree_entropy_ratio(&cycle4()).unwrap().abs() < 1e-10);
271    }
272
273    #[test]
274    fn der_star5() {
275        // Two degree classes: 4(1 vertex) and 1(4 vertices)
276        // H = -(1/5)ln(1/5) - (4/5)ln(4/5)
277        // max = ln(5)
278        let r = degree_entropy_ratio(&star5()).unwrap();
279        let expected = (-(1.0_f64 / 5.0) * (1.0_f64 / 5.0).ln()
280            - (4.0_f64 / 5.0) * (4.0_f64 / 5.0).ln())
281            / 5.0_f64.ln();
282        assert!((r - expected).abs() < 1e-10);
283    }
284
285    #[test]
286    fn der_in_01() {
287        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
288            let r = degree_entropy_ratio(g).unwrap();
289            assert!(r >= -1e-10);
290            assert!(r <= 1.0 + 1e-10);
291        }
292    }
293
294    // --- edge_distribution_entropy ---
295
296    #[test]
297    fn ede_empty() {
298        assert!(edge_distribution_entropy(&empty()).unwrap().abs() < 1e-10);
299    }
300
301    #[test]
302    fn ede_single() {
303        assert!(edge_distribution_entropy(&single()).unwrap().abs() < 1e-10);
304    }
305
306    #[test]
307    fn ede_k3() {
308        // All edges (2,2) → 1 class → H=0
309        assert!(edge_distribution_entropy(&k3()).unwrap().abs() < 1e-10);
310    }
311
312    #[test]
313    fn ede_k4() {
314        // All edges (3,3) → 1 class → H=0
315        assert!(edge_distribution_entropy(&k4()).unwrap().abs() < 1e-10);
316    }
317
318    #[test]
319    fn ede_cycle4() {
320        // All edges (2,2) → 1 class → H=0
321        assert!(edge_distribution_entropy(&cycle4()).unwrap().abs() < 1e-10);
322    }
323
324    #[test]
325    fn ede_star5() {
326        // All edges (1,4) → 1 class → H=0
327        assert!(edge_distribution_entropy(&star5()).unwrap().abs() < 1e-10);
328    }
329
330    #[test]
331    fn ede_paw() {
332        // Edges: (0,1)→(2,2), (0,2)→(2,3), (1,2)→(2,3), (2,3)→(3,1)=(1,3)
333        // Types: (2,2)→1, (2,3)→2, (1,3)→1 → 3 classes, m=4
334        let r = edge_distribution_entropy(&paw()).unwrap();
335        assert!(r > 0.0);
336        assert!(r <= 1.0 + 1e-10);
337    }
338
339    #[test]
340    fn ede_in_01() {
341        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
342            let r = edge_distribution_entropy(g).unwrap();
343            assert!(r >= -1e-10);
344            assert!(r <= 1.0 + 1e-10);
345        }
346    }
347
348    // --- structural_information_content ---
349
350    #[test]
351    fn sic_empty() {
352        assert!(structural_information_content(&empty()).unwrap().abs() < 1e-10);
353    }
354
355    #[test]
356    fn sic_single() {
357        assert!(structural_information_content(&single()).unwrap().abs() < 1e-10);
358    }
359
360    #[test]
361    fn sic_k3() {
362        // 1 degree class → 0
363        assert!(structural_information_content(&k3()).unwrap().abs() < 1e-10);
364    }
365
366    #[test]
367    fn sic_k4() {
368        assert!(structural_information_content(&k4()).unwrap().abs() < 1e-10);
369    }
370
371    #[test]
372    fn sic_cycle4() {
373        assert!(structural_information_content(&cycle4()).unwrap().abs() < 1e-10);
374    }
375
376    #[test]
377    fn sic_star5() {
378        // 2 degree classes (1 and 4) → log2(2)/log2(5) = 1/log2(5)
379        let r = structural_information_content(&star5()).unwrap();
380        let expected = 1.0 / 5.0_f64.log2();
381        assert!((r - expected).abs() < 1e-10);
382    }
383
384    #[test]
385    fn sic_path4() {
386        // Degrees: 1,2,2,1 → 2 classes → log2(2)/log2(4) = 1/2 = 0.5
387        let r = structural_information_content(&path4()).unwrap();
388        assert!((r - 0.5).abs() < 1e-10);
389    }
390
391    #[test]
392    fn sic_paw() {
393        // Degrees: 2,2,3,1 → 3 classes → log2(3)/log2(4) = log2(3)/2
394        let r = structural_information_content(&paw()).unwrap();
395        let expected = 3.0_f64.log2() / 4.0_f64.log2();
396        assert!((r - expected).abs() < 1e-10);
397    }
398
399    #[test]
400    fn sic_in_01() {
401        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
402            let r = structural_information_content(g).unwrap();
403            assert!(r >= -1e-10);
404            assert!(r <= 1.0 + 1e-10);
405        }
406    }
407
408    // --- cross-consistency ---
409
410    #[test]
411    fn regular_zero_entropy() {
412        // Regular graphs have 1 degree class → entropy = 0
413        assert!(degree_entropy_ratio(&k3()).unwrap().abs() < 1e-10);
414        assert!(degree_entropy_ratio(&k4()).unwrap().abs() < 1e-10);
415        assert!(degree_entropy_ratio(&cycle4()).unwrap().abs() < 1e-10);
416    }
417
418    #[test]
419    fn regular_zero_sic() {
420        assert!(structural_information_content(&k3()).unwrap().abs() < 1e-10);
421        assert!(structural_information_content(&k4()).unwrap().abs() < 1e-10);
422        assert!(structural_information_content(&cycle4()).unwrap().abs() < 1e-10);
423    }
424
425    #[test]
426    fn regular_zero_edge_entropy() {
427        assert!(edge_distribution_entropy(&k3()).unwrap().abs() < 1e-10);
428        assert!(edge_distribution_entropy(&k4()).unwrap().abs() < 1e-10);
429        assert!(edge_distribution_entropy(&cycle4()).unwrap().abs() < 1e-10);
430    }
431}