rust_igraph/algorithms/properties/
distance_profile.rs1use crate::core::{Graph, IgraphResult};
14
15pub fn hop_entropy(graph: &Graph) -> IgraphResult<f64> {
35 let hist = distance_histogram(graph)?;
36 if hist.is_empty() {
37 return Ok(0.0);
38 }
39 let num_bins = hist.len();
40 if num_bins <= 1 {
41 return Ok(0.0);
42 }
43
44 let total: u64 = hist.iter().sum();
45 if total == 0 {
46 return Ok(0.0);
47 }
48
49 #[allow(clippy::cast_precision_loss)]
50 let total_f = total as f64;
51 let mut entropy = 0.0_f64;
52 let mut non_empty = 0usize;
53 for &count in &hist {
54 if count > 0 {
55 #[allow(clippy::cast_precision_loss)]
56 let p = count as f64 / total_f;
57 entropy -= p * p.ln();
58 non_empty += 1;
59 }
60 }
61
62 if non_empty <= 1 {
63 return Ok(0.0);
64 }
65
66 #[allow(clippy::cast_precision_loss)]
67 let max_entropy = (non_empty as f64).ln();
68 Ok(entropy / max_entropy)
69}
70
71pub fn distance_gini(graph: &Graph) -> IgraphResult<f64> {
89 let distances = all_finite_distances(graph)?;
90 let n = distances.len();
91 if n < 2 {
92 return Ok(0.0);
93 }
94
95 let mut sorted = distances;
96 sorted.sort_unstable();
97
98 #[allow(clippy::cast_precision_loss)]
99 let n_f = n as f64;
100 let mean: f64 = sorted.iter().map(|&d| f64::from(d)).sum::<f64>() / n_f;
101 if mean < 1e-15 {
102 return Ok(0.0);
103 }
104
105 let mut weighted_sum = 0.0_f64;
107 for (i, &d) in sorted.iter().enumerate() {
108 #[allow(clippy::cast_precision_loss)]
109 let rank = (i + 1) as f64;
110 weighted_sum += rank * f64::from(d);
111 }
112 let total_sum = mean * n_f;
113 let gini = (2.0 * weighted_sum) / (n_f * total_sum) - (n_f + 1.0) / n_f;
114 Ok(gini)
115}
116
117pub fn reach_decay(graph: &Graph) -> IgraphResult<f64> {
143 let n = graph.vcount();
144 if n < 2 {
145 return Ok(0.0);
146 }
147
148 let mut diameter: u32 = 0;
150 let n_us = n as usize;
151 let mut all_dists: Vec<Vec<Option<u32>>> = Vec::with_capacity(n_us);
152 for v in 0..n {
153 let dists = graph.distances(v)?;
154 for &d in &dists {
155 if let Some(dist) = d {
156 if dist > diameter {
157 diameter = dist;
158 }
159 }
160 }
161 all_dists.push(dists);
162 }
163
164 if diameter == 0 {
165 return Ok(0.0);
166 }
167
168 let half_diam = diameter / 2;
169 if half_diam == 0 {
170 return Ok(0.0);
171 }
172
173 let mut total_fraction = 0.0_f64;
175 let others = f64::from(n - 1);
176 for dists in &all_dists {
177 let reachable = dists
178 .iter()
179 .filter(|&&d| matches!(d, Some(dist) if dist > 0 && dist <= half_diam))
180 .count();
181 #[allow(clippy::cast_precision_loss)]
182 let frac = reachable as f64 / others;
183 total_fraction += frac;
184 }
185
186 Ok(total_fraction / f64::from(n))
187}
188
189fn distance_histogram(graph: &Graph) -> IgraphResult<Vec<u64>> {
192 let n = graph.vcount();
193 if n < 2 {
194 return Ok(Vec::new());
195 }
196
197 let mut max_dist: u32 = 0;
198 let mut pairs: Vec<u32> = Vec::new();
199
200 for v in 0..n {
201 let dists = graph.distances(v)?;
202 for (u, &d) in dists.iter().enumerate() {
203 #[allow(clippy::cast_possible_truncation)]
204 let u_u32 = u as u32;
205 if u_u32 > v {
206 if let Some(dist) = d {
207 if dist > 0 {
208 pairs.push(dist);
209 if dist > max_dist {
210 max_dist = dist;
211 }
212 }
213 }
214 }
215 }
216 }
217
218 if max_dist == 0 {
219 return Ok(Vec::new());
220 }
221
222 let mut hist = vec![0u64; max_dist as usize];
223 for &d in &pairs {
224 hist[(d - 1) as usize] += 1;
225 }
226 Ok(hist)
227}
228
229fn all_finite_distances(graph: &Graph) -> IgraphResult<Vec<u32>> {
231 let n = graph.vcount();
232 if n < 2 {
233 return Ok(Vec::new());
234 }
235
236 let mut distances = Vec::new();
237 for v in 0..n {
238 let dists = graph.distances(v)?;
239 for (u, &d) in dists.iter().enumerate() {
240 #[allow(clippy::cast_possible_truncation)]
241 if (u as u32) > v {
242 if let Some(dist) = d {
243 if dist > 0 {
244 distances.push(dist);
245 }
246 }
247 }
248 }
249 }
250 Ok(distances)
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
260 fn hop_entropy_empty() {
261 let g = Graph::with_vertices(0);
262 assert!(hop_entropy(&g).unwrap().abs() < 1e-12);
263 }
264
265 #[test]
266 fn hop_entropy_edgeless() {
267 let g = Graph::with_vertices(5);
268 assert!(hop_entropy(&g).unwrap().abs() < 1e-12);
269 }
270
271 #[test]
272 fn hop_entropy_complete() {
273 let g = Graph::from_edges(
275 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
276 false,
277 Some(4),
278 )
279 .unwrap();
280 assert!(hop_entropy(&g).unwrap().abs() < 1e-10);
281 }
282
283 #[test]
284 fn hop_entropy_path() {
285 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap();
287 let h = hop_entropy(&g).unwrap();
288 assert!(h > 0.0, "Path should have positive hop entropy, got {h}");
289 assert!(h <= 1.0, "Should be <= 1, got {h}");
290 }
291
292 #[test]
295 fn gini_empty() {
296 let g = Graph::with_vertices(0);
297 assert!(distance_gini(&g).unwrap().abs() < 1e-12);
298 }
299
300 #[test]
301 fn gini_complete() {
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 assert!(distance_gini(&g).unwrap().abs() < 1e-10);
310 }
311
312 #[test]
313 fn gini_path() {
314 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap();
316 let gini = distance_gini(&g).unwrap();
317 assert!(gini > 0.0, "Path should have positive Gini, got {gini}");
318 assert!(gini < 1.0, "Gini should be < 1, got {gini}");
319 }
320
321 #[test]
324 fn reach_empty() {
325 let g = Graph::with_vertices(0);
326 assert!(reach_decay(&g).unwrap().abs() < 1e-12);
327 }
328
329 #[test]
330 fn reach_path() {
331 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap();
334 let r = reach_decay(&g).unwrap();
335 assert!(r > 0.0, "Path should have positive reach, got {r}");
336 assert!(r <= 1.0, "Reach should be <= 1, got {r}");
337 }
338
339 #[test]
340 fn reach_star() {
341 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap();
346 let r = reach_decay(&g).unwrap();
347 assert!((r - 0.4).abs() < 1e-10, "Star reach should be 0.4, got {r}");
348 }
349}