rust_igraph/algorithms/properties/
core_periphery_ratios.rs1#![allow(
11 clippy::cast_possible_truncation,
12 clippy::cast_precision_loss,
13 clippy::many_single_char_names,
14 clippy::needless_range_loop,
15 clippy::similar_names,
16 clippy::too_many_lines
17)]
18
19use crate::core::{Graph, IgraphResult};
20
21pub fn core_ratio(graph: &Graph) -> IgraphResult<f64> {
39 let n = graph.vcount() as usize;
40 if n == 0 {
41 return Ok(0.0);
42 }
43
44 let cores = compute_coreness(graph)?;
45 let max_core = cores.iter().copied().max().unwrap_or(0);
46 if max_core == 0 {
47 return Ok(0.0);
48 }
49
50 let count = cores.iter().filter(|&&c| c == max_core).count();
51 Ok(count as f64 / n as f64)
52}
53
54pub fn core_density(graph: &Graph) -> IgraphResult<f64> {
72 let n = graph.vcount() as usize;
73 if n < 2 {
74 return Ok(0.0);
75 }
76
77 let cores = compute_coreness(graph)?;
78 let max_core = cores.iter().copied().max().unwrap_or(0);
79 if max_core == 0 {
80 return Ok(0.0);
81 }
82
83 let mut in_core = vec![false; n];
84 let mut core_size = 0_usize;
85 for (v, &c) in cores.iter().enumerate() {
86 if c == max_core {
87 in_core[v] = true;
88 core_size += 1;
89 }
90 }
91
92 if core_size < 2 {
93 return Ok(0.0);
94 }
95
96 let mut edges_in_core = 0_u64;
97 for v in 0..n {
98 if !in_core[v] {
99 continue;
100 }
101 let nbrs = graph.neighbors(v as u32)?;
102 for &u in &nbrs {
103 let ui = u as usize;
104 if in_core[ui] && ui > v {
105 edges_in_core += 1;
106 }
107 }
108 }
109
110 let max_edges = (core_size * (core_size - 1)) / 2;
111 if max_edges == 0 {
112 return Ok(0.0);
113 }
114
115 Ok(edges_in_core as f64 / max_edges as f64)
116}
117
118pub fn periphery_fraction(graph: &Graph) -> IgraphResult<f64> {
136 let n = graph.vcount() as usize;
137 if n == 0 {
138 return Ok(0.0);
139 }
140
141 let cores = compute_coreness(graph)?;
142 let max_core = cores.iter().copied().max().unwrap_or(0);
143 if max_core == 0 {
144 return Ok(0.0);
145 }
146
147 let count = cores.iter().filter(|&&c| c == 1).count();
148 Ok(count as f64 / n as f64)
149}
150
151pub fn core_periphery_gradient(graph: &Graph) -> IgraphResult<f64> {
170 let n = graph.vcount() as usize;
171 if n < 2 {
172 return Ok(0.0);
173 }
174
175 let cores = compute_coreness(graph)?;
176 let max_core = cores.iter().copied().max().unwrap_or(0);
177 if max_core <= 1 {
178 return Ok(0.0);
179 }
180
181 Ok((max_core - 1) as f64 / (n - 1) as f64)
182}
183
184fn compute_coreness(graph: &Graph) -> IgraphResult<Vec<usize>> {
186 let n = graph.vcount() as usize;
187 if n == 0 {
188 return Ok(Vec::new());
189 }
190
191 let mut deg = Vec::with_capacity(n);
192 for v in 0..n {
193 deg.push(graph.degree(v as u32)?);
194 }
195
196 let max_deg = deg.iter().copied().max().unwrap_or(0);
197
198 let mut bin = vec![0_usize; max_deg + 1];
200 for &d in ° {
201 bin[d] += 1;
202 }
203
204 let mut start = vec![0_usize; max_deg + 1];
205 let mut cumulative = 0_usize;
206 for d in 0..=max_deg {
207 start[d] = cumulative;
208 cumulative += bin[d];
209 }
210
211 let mut vert = vec![0_usize; n]; let mut pos = vec![0_usize; n]; for v in 0..n {
214 pos[v] = start[deg[v]];
215 vert[pos[v]] = v;
216 start[deg[v]] += 1;
217 }
218
219 let mut cumulative = 0_usize;
221 for d in 0..=max_deg {
222 let count = bin[d];
223 start[d] = cumulative;
224 cumulative += count;
225 }
226
227 let mut core = deg.clone();
228
229 for i in 0..n {
230 let v = vert[i];
231 let nbrs = graph.neighbors(v as u32)?;
232 for &u in &nbrs {
233 let ui = u as usize;
234 if core[ui] > core[v] {
235 let du = core[ui];
236 let pu = pos[ui];
237 let pw = start[du];
238 let w = vert[pw];
239
240 if ui != w {
241 vert[pu] = w;
242 vert[pw] = ui;
243 pos[w] = pu;
244 pos[ui] = pw;
245 }
246
247 start[du] += 1;
248 core[ui] -= 1;
249 }
250 }
251 }
252
253 Ok(core)
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 fn empty() -> Graph {
261 Graph::with_vertices(0)
262 }
263
264 fn single() -> Graph {
265 Graph::with_vertices(1)
266 }
267
268 fn isolated4() -> Graph {
269 Graph::with_vertices(4)
270 }
271
272 fn single_edge() -> Graph {
273 Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
274 }
275
276 fn path3() -> Graph {
277 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
278 }
279
280 fn k3() -> Graph {
281 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
282 }
283
284 fn k4() -> Graph {
285 Graph::from_edges(
286 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
287 false,
288 Some(4),
289 )
290 .unwrap()
291 }
292
293 fn cycle4() -> Graph {
294 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
295 }
296
297 fn star5() -> Graph {
298 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
299 }
300
301 fn paw() -> Graph {
302 Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
305 }
306
307 fn diamond() -> Graph {
308 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)], false, Some(4)).unwrap()
312 }
313
314 #[test]
317 fn cr_empty() {
318 assert!(core_ratio(&empty()).unwrap().abs() < 1e-10);
319 }
320
321 #[test]
322 fn cr_single() {
323 assert!(core_ratio(&single()).unwrap().abs() < 1e-10);
324 }
325
326 #[test]
327 fn cr_isolated() {
328 assert!(core_ratio(&isolated4()).unwrap().abs() < 1e-10);
329 }
330
331 #[test]
332 fn cr_single_edge() {
333 assert!((core_ratio(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
335 }
336
337 #[test]
338 fn cr_k3() {
339 assert!((core_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
341 }
342
343 #[test]
344 fn cr_k4() {
345 assert!((core_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
347 }
348
349 #[test]
350 fn cr_cycle4() {
351 assert!((core_ratio(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
353 }
354
355 #[test]
356 fn cr_star5() {
357 assert!((core_ratio(&star5()).unwrap() - 1.0).abs() < 1e-10);
359 }
360
361 #[test]
362 fn cr_paw() {
363 assert!((core_ratio(&paw()).unwrap() - 3.0 / 4.0).abs() < 1e-10);
365 }
366
367 #[test]
368 fn cr_in_01() {
369 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
370 let r = core_ratio(g).unwrap();
371 assert!(r >= -1e-10);
372 assert!(r <= 1.0 + 1e-10);
373 }
374 }
375
376 #[test]
379 fn cd_empty() {
380 assert!(core_density(&empty()).unwrap().abs() < 1e-10);
381 }
382
383 #[test]
384 fn cd_single() {
385 assert!(core_density(&single()).unwrap().abs() < 1e-10);
386 }
387
388 #[test]
389 fn cd_k4() {
390 assert!((core_density(&k4()).unwrap() - 1.0).abs() < 1e-10);
392 }
393
394 #[test]
395 fn cd_k3() {
396 assert!((core_density(&k3()).unwrap() - 1.0).abs() < 1e-10);
398 }
399
400 #[test]
401 fn cd_cycle4() {
402 assert!((core_density(&cycle4()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
405 }
406
407 #[test]
408 fn cd_paw() {
409 assert!((core_density(&paw()).unwrap() - 1.0).abs() < 1e-10);
412 }
413
414 #[test]
415 fn cd_diamond() {
416 assert!((core_density(&diamond()).unwrap() - 5.0 / 6.0).abs() < 1e-10);
418 }
419
420 #[test]
421 fn cd_in_01() {
422 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
423 let r = core_density(g).unwrap();
424 assert!(r >= -1e-10);
425 assert!(r <= 1.0 + 1e-10);
426 }
427 }
428
429 #[test]
432 fn pf_empty() {
433 assert!(periphery_fraction(&empty()).unwrap().abs() < 1e-10);
434 }
435
436 #[test]
437 fn pf_single() {
438 assert!(periphery_fraction(&single()).unwrap().abs() < 1e-10);
439 }
440
441 #[test]
442 fn pf_isolated() {
443 assert!(periphery_fraction(&isolated4()).unwrap().abs() < 1e-10);
444 }
445
446 #[test]
447 fn pf_single_edge() {
448 assert!((periphery_fraction(&single_edge()).unwrap() - 1.0).abs() < 1e-10);
450 }
451
452 #[test]
453 fn pf_star5() {
454 assert!((periphery_fraction(&star5()).unwrap() - 1.0).abs() < 1e-10);
456 }
457
458 #[test]
459 fn pf_path3() {
460 assert!((periphery_fraction(&path3()).unwrap() - 1.0).abs() < 1e-10);
462 }
463
464 #[test]
465 fn pf_k3() {
466 assert!(periphery_fraction(&k3()).unwrap().abs() < 1e-10);
468 }
469
470 #[test]
471 fn pf_k4() {
472 assert!(periphery_fraction(&k4()).unwrap().abs() < 1e-10);
474 }
475
476 #[test]
477 fn pf_paw() {
478 assert!((periphery_fraction(&paw()).unwrap() - 1.0 / 4.0).abs() < 1e-10);
480 }
481
482 #[test]
483 fn pf_in_01() {
484 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
485 let r = periphery_fraction(g).unwrap();
486 assert!(r >= -1e-10);
487 assert!(r <= 1.0 + 1e-10);
488 }
489 }
490
491 #[test]
494 fn cpg_empty() {
495 assert!(core_periphery_gradient(&empty()).unwrap().abs() < 1e-10);
496 }
497
498 #[test]
499 fn cpg_single() {
500 assert!(core_periphery_gradient(&single()).unwrap().abs() < 1e-10);
501 }
502
503 #[test]
504 fn cpg_isolated() {
505 assert!(core_periphery_gradient(&isolated4()).unwrap().abs() < 1e-10);
506 }
507
508 #[test]
509 fn cpg_single_edge() {
510 assert!(core_periphery_gradient(&single_edge()).unwrap().abs() < 1e-10);
512 }
513
514 #[test]
515 fn cpg_star5() {
516 assert!(core_periphery_gradient(&star5()).unwrap().abs() < 1e-10);
518 }
519
520 #[test]
521 fn cpg_k3() {
522 assert!((core_periphery_gradient(&k3()).unwrap() - 0.5).abs() < 1e-10);
524 }
525
526 #[test]
527 fn cpg_k4() {
528 assert!((core_periphery_gradient(&k4()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
530 }
531
532 #[test]
533 fn cpg_cycle4() {
534 assert!((core_periphery_gradient(&cycle4()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
536 }
537
538 #[test]
539 fn cpg_paw() {
540 assert!((core_periphery_gradient(&paw()).unwrap() - 1.0 / 3.0).abs() < 1e-10);
542 }
543
544 #[test]
545 fn cpg_nonneg() {
546 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
547 assert!(core_periphery_gradient(g).unwrap() >= -1e-10);
548 }
549 }
550
551 #[test]
554 fn complete_full_core() {
555 assert!((core_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
557 assert!((core_density(&k4()).unwrap() - 1.0).abs() < 1e-10);
558 }
559
560 #[test]
561 fn tree_all_periphery() {
562 assert!((periphery_fraction(&star5()).unwrap() - 1.0).abs() < 1e-10);
564 assert!((periphery_fraction(&path3()).unwrap() - 1.0).abs() < 1e-10);
565 }
566
567 #[test]
568 fn tree_zero_gradient() {
569 assert!(core_periphery_gradient(&star5()).unwrap().abs() < 1e-10);
571 assert!(core_periphery_gradient(&path3()).unwrap().abs() < 1e-10);
572 }
573}