rust_igraph/algorithms/properties/
smallworld_ratios.rs1#![allow(
13 clippy::cast_possible_truncation,
14 clippy::cast_precision_loss,
15 clippy::many_single_char_names,
16 clippy::needless_range_loop,
17 clippy::similar_names,
18 clippy::too_many_lines
19)]
20
21use crate::core::{Graph, IgraphResult};
22
23pub fn smallworld_sigma(graph: &Graph) -> IgraphResult<f64> {
48 let n = graph.vcount() as usize;
49 if n < 4 {
50 return Ok(0.0);
51 }
52
53 let (cc, apl) = clustering_and_apl(graph)?;
54 if apl < 1e-30 || cc < 1e-30 {
55 return Ok(0.0);
56 }
57
58 let mean_deg = 2.0 * graph.ecount() as f64 / n as f64;
59 if mean_deg < 1.0 + 1e-10 {
60 return Ok(0.0);
61 }
62
63 let c_rand = mean_deg / n as f64;
64 let l_rand = (n as f64).ln() / mean_deg.ln();
65
66 if c_rand < 1e-30 || l_rand < 1e-30 {
67 return Ok(0.0);
68 }
69
70 let gamma = cc / c_rand;
71 let lambda = apl / l_rand;
72
73 if lambda < 1e-30 {
74 return Ok(0.0);
75 }
76
77 Ok(gamma / lambda)
78}
79
80pub fn smallworld_omega(graph: &Graph) -> IgraphResult<f64> {
103 let n = graph.vcount() as usize;
104 if n < 4 {
105 return Ok(0.0);
106 }
107
108 let (cc, apl) = clustering_and_apl(graph)?;
109 if apl < 1e-30 {
110 return Ok(0.0);
111 }
112
113 let mean_deg = 2.0 * graph.ecount() as f64 / n as f64;
114 if mean_deg < 1.0 + 1e-10 {
115 return Ok(0.0);
116 }
117
118 let l_rand = (n as f64).ln() / mean_deg.ln();
119 let c_lattice = 0.75_f64;
120
121 let lambda_inv = l_rand / apl;
122 let gamma_lat = cc / c_lattice;
123
124 Ok(lambda_inv - gamma_lat)
125}
126
127pub fn clustering_path_ratio(graph: &Graph) -> IgraphResult<f64> {
145 let n = graph.vcount() as usize;
146 if n < 3 {
147 return Ok(0.0);
148 }
149
150 let (cc, apl) = clustering_and_apl(graph)?;
151 if apl < 1e-30 {
152 return Ok(0.0);
153 }
154
155 Ok(cc * n as f64 / apl)
156}
157
158pub fn navigability_ratio(graph: &Graph) -> IgraphResult<f64> {
178 let n = graph.vcount() as usize;
179 if n < 3 {
180 return Ok(0.0);
181 }
182
183 let (_, apl) = clustering_and_apl(graph)?;
184 if apl < 1e-30 {
185 return Ok(0.0);
186 }
187
188 Ok((n as f64).ln() / apl)
189}
190
191fn clustering_and_apl(graph: &Graph) -> IgraphResult<(f64, f64)> {
192 let n = graph.vcount() as usize;
193 if n < 2 {
194 return Ok((0.0, 0.0));
195 }
196
197 let mut total_triplets = 0_u64;
198 let mut closed_triplets = 0_u64;
199 let mut total_dist = 0_u64;
200 let mut dist_pairs = 0_u64;
201
202 for v in 0..n {
203 let neighbors = graph.neighbors(v as u32)?;
204 let d = neighbors.len();
205
206 if d >= 2 {
207 let possible = (d * (d - 1)) / 2;
208 total_triplets += possible as u64;
209
210 for i in 0..d {
211 for j in (i + 1)..d {
212 if graph.has_edge(neighbors[i], neighbors[j]) {
213 closed_triplets += 1;
214 }
215 }
216 }
217 }
218
219 let mut dist = vec![u32::MAX; n];
220 dist[v] = 0;
221 let mut queue = std::collections::VecDeque::new();
222 queue.push_back(v);
223 while let Some(curr) = queue.pop_front() {
224 let cd = dist[curr];
225 let nbrs = graph.neighbors(curr as u32)?;
226 for &u in &nbrs {
227 let ui = u as usize;
228 if dist[ui] == u32::MAX {
229 dist[ui] = cd + 1;
230 queue.push_back(ui);
231 }
232 }
233 }
234
235 for u in (v + 1)..n {
236 if dist[u] == u32::MAX {
237 return Ok((0.0, 0.0));
238 }
239 total_dist += u64::from(dist[u]);
240 dist_pairs += 1;
241 }
242 }
243
244 let cc = if total_triplets > 0 {
245 closed_triplets as f64 / total_triplets as f64
246 } else {
247 0.0
248 };
249
250 let apl = if dist_pairs > 0 {
251 total_dist as f64 / dist_pairs as f64
252 } else {
253 0.0
254 };
255
256 Ok((cc, apl))
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn path3() -> Graph {
264 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
265 }
266
267 fn k3() -> Graph {
268 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
269 }
270
271 fn k4() -> Graph {
272 Graph::from_edges(
273 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
274 false,
275 Some(4),
276 )
277 .unwrap()
278 }
279
280 fn cycle4() -> Graph {
281 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
282 }
283
284 fn star5() -> Graph {
285 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
286 }
287
288 fn paw() -> Graph {
289 Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
290 }
291
292 #[test]
295 fn sigma_empty() {
296 let g = Graph::with_vertices(0);
297 assert!(smallworld_sigma(&g).unwrap().abs() < 1e-10);
298 }
299
300 #[test]
301 fn sigma_small() {
302 let g = Graph::with_vertices(3);
303 assert!(smallworld_sigma(&g).unwrap().abs() < 1e-10);
304 }
305
306 #[test]
307 fn sigma_k4() {
308 let r = smallworld_sigma(&k4()).unwrap();
309 assert!(r > 1.0);
310 }
311
312 #[test]
313 fn sigma_cycle4() {
314 assert!(smallworld_sigma(&cycle4()).unwrap().abs() < 1e-10);
316 }
317
318 #[test]
319 fn sigma_star5() {
320 assert!(smallworld_sigma(&star5()).unwrap().abs() < 1e-10);
322 }
323
324 #[test]
325 fn sigma_paw() {
326 let r = smallworld_sigma(&paw()).unwrap();
327 assert!(r > 0.0);
328 }
329
330 #[test]
331 fn sigma_disconnected() {
332 let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
333 assert!(smallworld_sigma(&g).unwrap().abs() < 1e-10);
334 }
335
336 #[test]
339 fn omega_empty() {
340 let g = Graph::with_vertices(0);
341 assert!(smallworld_omega(&g).unwrap().abs() < 1e-10);
342 }
343
344 #[test]
345 fn omega_small() {
346 let g = Graph::with_vertices(3);
347 assert!(smallworld_omega(&g).unwrap().abs() < 1e-10);
348 }
349
350 #[test]
351 fn omega_k4() {
352 let w = smallworld_omega(&k4()).unwrap();
353 assert!(w > -2.0 && w < 2.0);
354 }
355
356 #[test]
357 fn omega_cycle4() {
358 let w = smallworld_omega(&cycle4()).unwrap();
359 assert!(w.is_finite());
360 }
361
362 #[test]
363 fn omega_disconnected() {
364 let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
365 assert!(smallworld_omega(&g).unwrap().abs() < 1e-10);
366 }
367
368 #[test]
371 fn cpr_empty() {
372 let g = Graph::with_vertices(0);
373 assert!(clustering_path_ratio(&g).unwrap().abs() < 1e-10);
374 }
375
376 #[test]
377 fn cpr_k3() {
378 assert!((clustering_path_ratio(&k3()).unwrap() - 3.0).abs() < 1e-10);
380 }
381
382 #[test]
383 fn cpr_k4() {
384 assert!((clustering_path_ratio(&k4()).unwrap() - 4.0).abs() < 1e-10);
386 }
387
388 #[test]
389 fn cpr_cycle4() {
390 assert!(clustering_path_ratio(&cycle4()).unwrap().abs() < 1e-10);
392 }
393
394 #[test]
395 fn cpr_star5() {
396 assert!(clustering_path_ratio(&star5()).unwrap().abs() < 1e-10);
398 }
399
400 #[test]
401 fn cpr_path3() {
402 assert!(clustering_path_ratio(&path3()).unwrap().abs() < 1e-10);
404 }
405
406 #[test]
407 fn cpr_disconnected() {
408 let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
409 assert!(clustering_path_ratio(&g).unwrap().abs() < 1e-10);
410 }
411
412 #[test]
413 fn cpr_nonneg() {
414 for g in &[path3(), k3(), k4(), cycle4(), star5(), paw()] {
415 assert!(clustering_path_ratio(g).unwrap() >= -1e-10);
416 }
417 }
418
419 #[test]
422 fn nr_empty() {
423 let g = Graph::with_vertices(0);
424 assert!(navigability_ratio(&g).unwrap().abs() < 1e-10);
425 }
426
427 #[test]
428 fn nr_k3() {
429 assert!((navigability_ratio(&k3()).unwrap() - 3.0_f64.ln()).abs() < 1e-10);
431 }
432
433 #[test]
434 fn nr_k4() {
435 assert!((navigability_ratio(&k4()).unwrap() - 4.0_f64.ln()).abs() < 1e-10);
437 }
438
439 #[test]
440 fn nr_cycle4() {
441 let expected = 4.0_f64.ln() / (4.0 / 3.0);
445 assert!((navigability_ratio(&cycle4()).unwrap() - expected).abs() < 1e-10);
446 }
447
448 #[test]
449 fn nr_disconnected() {
450 let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
451 assert!(navigability_ratio(&g).unwrap().abs() < 1e-10);
452 }
453
454 #[test]
455 fn nr_positive() {
456 for g in &[path3(), k3(), k4(), cycle4(), star5(), paw()] {
457 assert!(navigability_ratio(g).unwrap() > 0.0);
458 }
459 }
460
461 #[test]
464 fn complete_high_sigma() {
465 assert!(smallworld_sigma(&k4()).unwrap() > 1.0);
466 }
467
468 #[test]
469 fn triangle_free_zero_cpr() {
470 assert!(clustering_path_ratio(&cycle4()).unwrap().abs() < 1e-10);
471 assert!(clustering_path_ratio(&star5()).unwrap().abs() < 1e-10);
472 assert!(clustering_path_ratio(&path3()).unwrap().abs() < 1e-10);
473 }
474}