rust_igraph/algorithms/properties/
edge_distribution_entropy.rs1use std::collections::HashMap;
14
15use crate::core::{Graph, IgraphResult};
16
17pub fn edge_degree_entropy(graph: &Graph) -> IgraphResult<f64> {
36 let counts = degree_pair_counts(graph)?;
37 if counts.is_empty() {
38 return Ok(0.0);
39 }
40 let total: u64 = counts.values().sum();
41 if total == 0 {
42 return Ok(0.0);
43 }
44 #[allow(clippy::cast_precision_loss)]
45 let total_f = total as f64;
46 let mut entropy = 0.0_f64;
47 for &count in counts.values() {
48 if count > 0 {
49 #[allow(clippy::cast_precision_loss)]
50 let p = count as f64 / total_f;
51 entropy -= p * p.ln();
52 }
53 }
54 Ok(entropy)
55}
56
57pub fn edge_weight_balance(graph: &Graph) -> IgraphResult<f64> {
77 let counts = degree_pair_counts(graph)?;
78 let num_classes = counts.len();
79 if num_classes <= 1 {
80 return Ok(0.0);
81 }
82 let total: u64 = counts.values().sum();
83 if total == 0 {
84 return Ok(0.0);
85 }
86 #[allow(clippy::cast_precision_loss)]
87 let total_f = total as f64;
88 let mut entropy = 0.0_f64;
89 for &count in counts.values() {
90 if count > 0 {
91 #[allow(clippy::cast_precision_loss)]
92 let p = count as f64 / total_f;
93 entropy -= p * p.ln();
94 }
95 }
96 #[allow(clippy::cast_precision_loss)]
97 let max_entropy = (num_classes as f64).ln();
98 Ok(entropy / max_entropy)
99}
100
101pub fn degree_pair_concentration(graph: &Graph) -> IgraphResult<f64> {
119 let counts = degree_pair_counts(graph)?;
120 if counts.is_empty() {
121 return Ok(0.0);
122 }
123 let total: u64 = counts.values().sum();
124 if total == 0 {
125 return Ok(0.0);
126 }
127 let max_count = *counts.values().max().unwrap();
128 #[allow(clippy::cast_precision_loss)]
129 Ok(max_count as f64 / total as f64)
130}
131
132fn degree_pair_counts(graph: &Graph) -> IgraphResult<HashMap<(usize, usize), u64>> {
134 let mut counts: HashMap<(usize, usize), u64> = HashMap::new();
135 let n = graph.vcount();
136 if n == 0 {
137 return Ok(counts);
138 }
139 let mut degrees = vec![0usize; n as usize];
141 for v in 0..n {
142 degrees[v as usize] = graph.degree(v)?;
143 }
144 for (u, v) in graph.edges() {
146 let du = degrees[u as usize];
147 let dv = degrees[v as usize];
148 let key = if du <= dv { (du, dv) } else { (dv, du) };
149 *counts.entry(key).or_insert(0) += 1;
150 }
151 Ok(counts)
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
161 fn entropy_empty() {
162 let g = Graph::with_vertices(0);
163 assert!(edge_degree_entropy(&g).unwrap().abs() < 1e-12);
164 }
165
166 #[test]
167 fn entropy_edgeless() {
168 let g = Graph::with_vertices(5);
169 assert!(edge_degree_entropy(&g).unwrap().abs() < 1e-12);
170 }
171
172 #[test]
173 fn entropy_complete_graph() {
174 let g = Graph::from_edges(
176 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
177 false,
178 Some(4),
179 )
180 .unwrap();
181 assert!(edge_degree_entropy(&g).unwrap().abs() < 1e-10);
182 }
183
184 #[test]
185 fn entropy_star() {
186 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap();
188 assert!(edge_degree_entropy(&g).unwrap().abs() < 1e-10);
189 }
190
191 #[test]
192 fn entropy_mixed() {
193 let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap();
198 let h = edge_degree_entropy(&g).unwrap();
199 assert!(h > 0.0, "Mixed graph should have positive entropy, got {h}");
200 }
201
202 #[test]
205 fn balance_empty() {
206 let g = Graph::with_vertices(5);
207 assert!(edge_weight_balance(&g).unwrap().abs() < 1e-12);
208 }
209
210 #[test]
211 fn balance_single_class() {
212 let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap();
214 assert!(edge_weight_balance(&g).unwrap().abs() < 1e-10);
215 }
216
217 #[test]
218 fn balance_multiple_classes() {
219 let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap();
221 let b = edge_weight_balance(&g).unwrap();
222 assert!(b > 0.0, "Should be > 0, got {b}");
223 assert!(b <= 1.0, "Should be <= 1, got {b}");
224 }
225
226 #[test]
227 fn balance_uniform_is_one() {
228 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap();
236 let b = edge_weight_balance(&g).unwrap();
237 assert!(b > 0.0 && b <= 1.0, "Balance should be in (0,1], got {b}");
238 }
239
240 #[test]
243 fn concentration_empty() {
244 let g = Graph::with_vertices(5);
245 assert!(degree_pair_concentration(&g).unwrap().abs() < 1e-12);
246 }
247
248 #[test]
249 fn concentration_single_class() {
250 let g = Graph::from_edges(
252 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
253 false,
254 Some(4),
255 )
256 .unwrap();
257 assert!((degree_pair_concentration(&g).unwrap() - 1.0).abs() < 1e-10);
258 }
259
260 #[test]
261 fn concentration_mixed() {
262 let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap();
265 assert!((degree_pair_concentration(&g).unwrap() - 0.5).abs() < 1e-10);
266 }
267
268 #[test]
269 fn concentration_path() {
270 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap();
274 assert!((degree_pair_concentration(&g).unwrap() - 0.5).abs() < 1e-10);
275 }
276
277 #[test]
278 fn entropy_and_balance_consistency() {
279 let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap();
281 assert!(edge_degree_entropy(&g).unwrap().abs() < 1e-10);
282 assert!(edge_weight_balance(&g).unwrap().abs() < 1e-10);
283 assert!((degree_pair_concentration(&g).unwrap() - 1.0).abs() < 1e-10);
284 }
285}