rust_igraph/algorithms/properties/
centrality_diversity.rs1#![allow(
14 clippy::cast_lossless,
15 clippy::cast_possible_truncation,
16 clippy::cast_precision_loss,
17 clippy::many_single_char_names,
18 clippy::needless_range_loop,
19 clippy::similar_names,
20 clippy::too_many_lines
21)]
22
23use crate::core::{Graph, IgraphResult};
24
25pub fn centrality_entropy(graph: &Graph) -> IgraphResult<f64> {
47 let n = graph.vcount() as usize;
48 if n < 2 {
49 return Ok(0.0);
50 }
51
52 let mut degrees = Vec::with_capacity(n);
53 let mut sum = 0_u64;
54 for v in 0..n {
55 let d = graph.degree(v as u32)?;
56 degrees.push(d);
57 sum += d as u64;
58 }
59
60 if sum == 0 {
61 return Ok(0.0);
62 }
63
64 let sum_f = sum as f64;
65 let mut entropy = 0.0_f64;
66 for &d in °rees {
67 if d > 0 {
68 let p = d as f64 / sum_f;
69 entropy -= p * p.ln();
70 }
71 }
72
73 let max_entropy = (n as f64).ln();
75 if max_entropy > 0.0 {
76 Ok(entropy / max_entropy)
77 } else {
78 Ok(0.0)
79 }
80}
81
82pub fn centrality_divergence(graph: &Graph) -> IgraphResult<f64> {
104 let n = graph.vcount() as usize;
105 if n < 3 {
106 return Ok(0.0);
107 }
108
109 let mut degrees = Vec::with_capacity(n);
111 let mut deg_sum = 0_u64;
112 for v in 0..n {
113 let d = graph.degree(v as u32)?;
114 degrees.push(d as f64);
115 deg_sum += d as u64;
116 }
117
118 if deg_sum == 0 {
119 return Ok(0.0);
120 }
121
122 let bc = crate::algorithms::properties::betweenness::betweenness(graph)?;
124
125 let bc_sum: f64 = bc.iter().sum();
126
127 if bc_sum <= 0.0 {
129 return Ok(0.0);
130 }
131
132 let deg_sum_f = deg_sum as f64;
134 let p: Vec<f64> = degrees.iter().map(|&d| d / deg_sum_f).collect();
135 let q: Vec<f64> = bc.iter().map(|&b| b / bc_sum).collect();
136
137 let mut jsd = 0.0_f64;
139 for i in 0..n {
140 let m_i = f64::midpoint(p[i], q[i]);
141 if m_i > 0.0 {
142 if p[i] > 0.0 {
143 jsd += p[i] * (p[i] / m_i).ln();
144 }
145 if q[i] > 0.0 {
146 jsd += q[i] * (q[i] / m_i).ln();
147 }
148 }
149 }
150 jsd /= 2.0;
151
152 Ok(jsd)
153}
154
155pub fn centrality_rank_correlation(graph: &Graph) -> IgraphResult<f64> {
177 let n = graph.vcount() as usize;
178 if n < 3 {
179 return Ok(0.0);
180 }
181
182 let mut degrees = Vec::with_capacity(n);
184 for v in 0..n {
185 degrees.push(graph.degree(v as u32)? as f64);
186 }
187
188 let bc = crate::algorithms::properties::betweenness::betweenness(graph)?;
190
191 let deg_ranks = compute_ranks(°rees);
193 let bc_ranks = compute_ranks(&bc);
194
195 Ok(pearson_correlation(°_ranks, &bc_ranks))
197}
198
199fn compute_ranks(values: &[f64]) -> Vec<f64> {
201 let n = values.len();
202 let mut indexed: Vec<(usize, f64)> = values.iter().copied().enumerate().collect();
203 indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
204
205 let mut ranks = vec![0.0_f64; n];
206 let mut i = 0;
207 while i < n {
208 let mut j = i;
209 while j < n && (indexed[j].1 - indexed[i].1).abs() < 1e-12 {
211 j += 1;
212 }
213 let avg_rank = (i + 1 + j) as f64 / 2.0;
216 for k in i..j {
217 ranks[indexed[k].0] = avg_rank;
218 }
219 i = j;
220 }
221 ranks
222}
223
224fn pearson_correlation(x: &[f64], y: &[f64]) -> f64 {
226 let n = x.len();
227 if n == 0 {
228 return 0.0;
229 }
230
231 let mean_x: f64 = x.iter().sum::<f64>() / n as f64;
232 let mean_y: f64 = y.iter().sum::<f64>() / n as f64;
233
234 let mut cov = 0.0_f64;
235 let mut var_x = 0.0_f64;
236 let mut var_y = 0.0_f64;
237
238 for i in 0..n {
239 let dx = x[i] - mean_x;
240 let dy = y[i] - mean_y;
241 cov += dx * dy;
242 var_x += dx * dx;
243 var_y += dy * dy;
244 }
245
246 let denom = (var_x * var_y).sqrt();
247 if denom < 1e-15 {
248 return 0.0;
249 }
250
251 cov / denom
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn entropy_empty_graph() {
260 let g = Graph::with_vertices(0);
261 assert!(centrality_entropy(&g).unwrap().abs() < 1e-12);
262 }
263
264 #[test]
265 fn entropy_single_vertex() {
266 let g = Graph::with_vertices(1);
267 assert!(centrality_entropy(&g).unwrap().abs() < 1e-12);
268 }
269
270 #[test]
271 fn entropy_regular_graph_is_one() {
272 let g = Graph::from_edges(
274 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
275 false,
276 Some(4),
277 )
278 .unwrap();
279 let h = centrality_entropy(&g).unwrap();
280 assert!((h - 1.0).abs() < 1e-10, "K4 entropy = {h}, expected 1.0");
281 }
282
283 #[test]
284 fn entropy_star_is_low() {
285 let g =
287 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4), (0, 5)], false, Some(6)).unwrap();
288 let h = centrality_entropy(&g).unwrap();
289 assert!(h < 0.95, "Star entropy = {h}, should be < 0.95");
291 assert!(h > 0.0, "Star entropy should be positive");
292 }
293
294 #[test]
295 fn divergence_empty() {
296 let g = Graph::with_vertices(2);
297 assert!(centrality_divergence(&g).unwrap().abs() < 1e-12);
298 }
299
300 #[test]
301 fn divergence_complete_graph() {
302 let g = Graph::from_edges(
304 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
305 false,
306 Some(4),
307 )
308 .unwrap();
309 let jsd = centrality_divergence(&g).unwrap();
310 assert!(jsd.abs() < 1e-12);
311 }
312
313 #[test]
314 fn divergence_path_graph() {
315 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap();
317 let jsd = centrality_divergence(&g).unwrap();
318 assert!(jsd > 0.0, "Path JSD should be positive, got {jsd}");
319 assert!(
320 jsd < std::f64::consts::LN_2,
321 "JSD should be < ln(2), got {jsd}"
322 );
323 }
324
325 #[test]
326 fn rank_correlation_empty() {
327 let g = Graph::with_vertices(2);
328 assert!(centrality_rank_correlation(&g).unwrap().abs() < 1e-12);
329 }
330
331 #[test]
332 fn rank_correlation_path() {
333 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap();
335 let rho = centrality_rank_correlation(&g).unwrap();
336 assert!(
337 rho > 0.5,
338 "Path rank correlation should be > 0.5, got {rho}"
339 );
340 }
341
342 #[test]
343 fn rank_correlation_star() {
344 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap();
346 let rho = centrality_rank_correlation(&g).unwrap();
347 assert!(
349 rho > 0.8,
350 "Star rank correlation should be > 0.8, got {rho}"
351 );
352 }
353
354 #[test]
355 fn rank_correlation_edgeless() {
356 let g = Graph::with_vertices(5);
358 let rho = centrality_rank_correlation(&g).unwrap();
359 assert!(rho.abs() < 1e-12);
360 }
361
362 #[test]
363 fn compute_ranks_basic() {
364 let values = vec![3.0, 1.0, 2.0, 1.0];
365 let ranks = compute_ranks(&values);
366 assert!((ranks[0] - 4.0).abs() < 1e-10); assert!((ranks[1] - 1.5).abs() < 1e-10); assert!((ranks[2] - 3.0).abs() < 1e-10); assert!((ranks[3] - 1.5).abs() < 1e-10); }
372}