rust_igraph/algorithms/properties/
graph_entropy.rs1#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
15
16use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
17
18pub fn degree_entropy(graph: &Graph) -> IgraphResult<f64> {
36 let nv = graph.vcount() as usize;
37 if nv == 0 {
38 return Ok(0.0);
39 }
40
41 let mut degrees = Vec::with_capacity(nv);
42 let mut max_deg = 0usize;
43 for v in 0..nv {
44 let d = graph.degree(v as VertexId)?;
45 if d > max_deg {
46 max_deg = d;
47 }
48 degrees.push(d);
49 }
50
51 let mut hist = vec![0usize; max_deg + 1];
53 for &d in °rees {
54 hist[d] += 1;
55 }
56
57 let n_f64 = nv as f64;
59 let mut entropy = 0.0;
60 for &count in &hist {
61 if count > 0 {
62 let p = count as f64 / n_f64;
63 entropy -= p * p.log2();
64 }
65 }
66
67 Ok(entropy)
68}
69
70pub fn edge_entropy(graph: &Graph) -> IgraphResult<f64> {
89 let nv = graph.vcount() as usize;
90 let ne = graph.ecount();
91 if ne == 0 {
92 return Ok(0.0);
93 }
94
95 let mut degrees = Vec::with_capacity(nv);
96 for v in 0..nv {
97 degrees.push(graph.degree(v as VertexId)?);
98 }
99
100 let mut weights = Vec::with_capacity(ne);
102 let mut total_weight = 0.0;
103 for (u, v) in graph.edges() {
104 let du = degrees[u as usize];
105 let dv = degrees[v as usize];
106 let w = if du > 0 && dv > 0 {
107 1.0 / (du as f64 * dv as f64)
108 } else {
109 0.0
110 };
111 weights.push(w);
112 total_weight += w;
113 }
114
115 if total_weight <= 0.0 {
116 return Ok(0.0);
117 }
118
119 let mut entropy = 0.0;
121 for &w in &weights {
122 if w > 0.0 {
123 let p = w / total_weight;
124 entropy -= p * p.log2();
125 }
126 }
127
128 Ok(entropy)
129}
130
131pub fn von_neumann_entropy(graph: &Graph) -> IgraphResult<f64> {
157 if graph.is_directed() {
158 return Err(IgraphError::InvalidArgument(
159 "von_neumann_entropy is defined for undirected graphs only".to_string(),
160 ));
161 }
162
163 let nv = graph.vcount() as usize;
164 let ne = graph.ecount();
165
166 if nv == 0 || ne == 0 {
167 return Ok(0.0);
168 }
169
170 let mut sum_deg_sq = 0.0_f64;
171 for v in 0..nv {
172 let d = graph.degree(v as VertexId)? as f64;
173 sum_deg_sq += d * d;
174 }
175
176 let two_m = 2.0 * ne as f64;
177
178 let entropy = 1.0 - 1.0 / nv as f64 - sum_deg_sq / (two_m * two_m);
183
184 Ok(entropy.max(0.0))
186}
187
188pub fn degree_structural_info(graph: &Graph) -> IgraphResult<f64> {
207 let nv = graph.vcount() as usize;
208 if nv <= 1 {
209 return Ok(0.0);
210 }
211
212 let mut max_deg = 0usize;
213 let mut degrees = Vec::with_capacity(nv);
214 for v in 0..nv {
215 let d = graph.degree(v as VertexId)?;
216 if d > max_deg {
217 max_deg = d;
218 }
219 degrees.push(d);
220 }
221
222 let mut hist = vec![0usize; max_deg + 1];
224 for &d in °rees {
225 hist[d] += 1;
226 }
227
228 let log_n_fact = log2_factorial(nv);
230 let mut sum_log_nk_fact = 0.0;
231 for &count in &hist {
232 if count > 1 {
233 sum_log_nk_fact += log2_factorial(count);
234 }
235 }
236
237 Ok(log_n_fact - sum_log_nk_fact)
238}
239
240fn log2_factorial(n: usize) -> f64 {
243 if n <= 1 {
245 return 0.0;
246 }
247 if n <= 20 {
248 let mut result = 0.0;
249 for i in 2..=n {
250 result += (i as f64).log2();
251 }
252 return result;
253 }
254 let nf = n as f64;
256 nf * nf.log2() - nf * std::f64::consts::E.log2()
257 + 0.5 * (2.0 * std::f64::consts::PI * nf).log2()
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn cycle4() -> Graph {
265 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
266 }
267
268 fn path4() -> Graph {
269 Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
270 }
271
272 fn star4() -> Graph {
273 Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap()
274 }
275
276 fn complete4() -> Graph {
277 Graph::from_edges(
278 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
279 false,
280 Some(4),
281 )
282 .unwrap()
283 }
284
285 #[test]
288 fn degree_entropy_regular() {
289 let g = cycle4();
290 let h = degree_entropy(&g).unwrap();
291 assert!(h.abs() < 1e-10);
293 }
294
295 #[test]
296 fn degree_entropy_star() {
297 let g = star4();
298 let h = degree_entropy(&g).unwrap();
299 let expected = -(3.0 / 4.0) * (3.0_f64 / 4.0).log2() - (1.0 / 4.0) * (1.0_f64 / 4.0).log2();
301 assert!((h - expected).abs() < 1e-10);
302 }
303
304 #[test]
305 fn degree_entropy_path() {
306 let g = path4();
307 let h = degree_entropy(&g).unwrap();
308 let expected = -2.0 * (0.5 * 0.5_f64.log2());
310 assert!((h - expected).abs() < 1e-10);
311 }
312
313 #[test]
314 fn degree_entropy_empty() {
315 let g = Graph::with_vertices(0);
316 let h = degree_entropy(&g).unwrap();
317 assert!(h.abs() < 1e-10);
318 }
319
320 #[test]
321 fn degree_entropy_isolated() {
322 let g = Graph::with_vertices(5);
323 let h = degree_entropy(&g).unwrap();
324 assert!(h.abs() < 1e-10);
326 }
327
328 #[test]
331 fn edge_entropy_regular() {
332 let g = cycle4();
333 let h = edge_entropy(&g).unwrap();
334 let expected = 4.0_f64.log2();
336 assert!((h - expected).abs() < 1e-10);
337 }
338
339 #[test]
340 fn edge_entropy_empty() {
341 let g = Graph::with_vertices(3);
342 let h = edge_entropy(&g).unwrap();
343 assert!(h.abs() < 1e-10);
344 }
345
346 #[test]
347 fn edge_entropy_positive() {
348 let g = star4();
349 let h = edge_entropy(&g).unwrap();
350 assert!(h > 0.0);
351 }
352
353 #[test]
356 fn vne_positive_for_complex() {
357 let g = complete4();
358 let h = von_neumann_entropy(&g).unwrap();
359 assert!(h > 0.0);
360 }
361
362 #[test]
363 fn vne_empty() {
364 let g = Graph::with_vertices(3);
365 let h = von_neumann_entropy(&g).unwrap();
366 assert!(h.abs() < 1e-10);
367 }
368
369 #[test]
370 fn vne_directed_error() {
371 let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
372 assert!(von_neumann_entropy(&g).is_err());
373 }
374
375 #[test]
376 fn vne_non_negative() {
377 let g = path4();
378 let h = von_neumann_entropy(&g).unwrap();
379 assert!(h >= 0.0);
380 }
381
382 #[test]
385 fn dsi_regular_zero() {
386 let g = cycle4();
387 let info = degree_structural_info(&g).unwrap();
388 assert!(info.abs() < 1e-10);
390 }
391
392 #[test]
393 fn dsi_star_positive() {
394 let g = star4();
395 let info = degree_structural_info(&g).unwrap();
396 let expected = log2_factorial(4) - log2_factorial(3);
398 assert!((info - expected).abs() < 1e-10);
399 }
400
401 #[test]
402 fn dsi_all_different() {
403 let g = path4();
405 let info = degree_structural_info(&g).unwrap();
406 let expected = log2_factorial(4) - 2.0 * log2_factorial(2);
407 assert!((info - expected).abs() < 1e-10);
408 }
409
410 #[test]
411 fn dsi_single_vertex() {
412 let g = Graph::with_vertices(1);
413 let info = degree_structural_info(&g).unwrap();
414 assert!(info.abs() < 1e-10);
415 }
416
417 #[test]
418 fn dsi_empty() {
419 let g = Graph::with_vertices(0);
420 let info = degree_structural_info(&g).unwrap();
421 assert!(info.abs() < 1e-10);
422 }
423}