1#![allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
37
38use std::cmp::Ordering;
39use std::collections::BinaryHeap;
40
41use crate::algorithms::community::modularity::{
42 modularity, modularity_directed, modularity_weighted, modularity_weighted_directed,
43};
44use crate::algorithms::paths::dijkstra::DijkstraMode;
45use crate::algorithms::paths::voronoi::{VoronoiTiebreaker, voronoi};
46use crate::algorithms::properties::ecc::ecc;
47use crate::algorithms::properties::is_simple::is_simple;
48use crate::core::graph::EdgeId;
49use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
50
51const VORONOI_BRENT_SEED: u64 = 42;
52
53#[derive(Debug, Clone, PartialEq)]
69pub struct CommunityVoronoiResult {
70 pub membership: Vec<u32>,
71 pub generators: Vec<VertexId>,
72 pub modularity: Option<f64>,
73}
74
75fn validate_inputs(
107 graph: &Graph,
108 lengths: Option<&[f64]>,
109 weights: Option<&[f64]>,
110 m: usize,
111) -> IgraphResult<()> {
112 if let Some(l) = lengths {
113 if l.len() != m {
114 return Err(IgraphError::InvalidArgument(format!(
115 "lengths vector size ({}) differs from edge count ({})",
116 l.len(),
117 m
118 )));
119 }
120 for (e, &v) in l.iter().enumerate() {
121 if v.is_nan() {
122 return Err(IgraphError::InvalidArgument(format!(
123 "length at edge {e} is NaN"
124 )));
125 }
126 if v < 0.0 {
127 return Err(IgraphError::InvalidArgument(format!(
128 "length at edge {e} is negative ({v}); Voronoi requires non-negative lengths"
129 )));
130 }
131 }
132 }
133 if let Some(w) = weights {
134 if w.len() != m {
135 return Err(IgraphError::InvalidArgument(format!(
136 "weights vector size ({}) differs from edge count ({})",
137 w.len(),
138 m
139 )));
140 }
141 for (e, &v) in w.iter().enumerate() {
142 if v.is_nan() {
143 return Err(IgraphError::InvalidArgument(format!(
144 "weight at edge {e} is NaN"
145 )));
146 }
147 if v <= 0.0 {
148 return Err(IgraphError::InvalidArgument(format!(
149 "weight at edge {e} is non-positive ({v}); Voronoi requires positive weights"
150 )));
151 }
152 }
153 }
154 if !is_simple(graph)? {
155 return Err(IgraphError::InvalidArgument(
156 "the graph must be simple (no self-loops, no multi-edges) for Voronoi communities"
157 .to_string(),
158 ));
159 }
160 Ok(())
161}
162
163pub fn community_voronoi(
189 graph: &Graph,
190 lengths: Option<&[f64]>,
191 weights: Option<&[f64]>,
192 mode: DijkstraMode,
193 r: f64,
194) -> IgraphResult<CommunityVoronoiResult> {
195 let n = graph.vcount();
196 let m = graph.ecount();
197
198 let mode = if graph.is_directed() {
200 mode
201 } else {
202 DijkstraMode::All
203 };
204
205 validate_inputs(graph, lengths, weights, m)?;
206
207 if m == 0 {
208 let membership: Vec<u32> = (0..n).collect();
210 let generators: Vec<VertexId> = (0..n).collect();
211 return Ok(CommunityVoronoiResult {
212 membership,
213 generators,
214 modularity: None,
215 });
216 }
217
218 let lrd = weighted_local_density(graph, weights)?;
220
221 let mut len_eff = ecc(graph, None, 3, true, true)?;
223 for v in &mut len_eff {
224 *v = 1.0 / *v;
227 }
228 if let Some(l) = lengths {
229 for (a, &b) in len_eff.iter_mut().zip(l.iter()) {
230 *a *= b;
231 }
232 }
233
234 if r < 0.0 {
235 let (minr, maxr) = estimate_minmax_r(graph, &lrd, &len_eff, mode)?;
238 let mut work = ModularityWork {
239 graph,
240 local_dens: &lrd,
241 lengths: &len_eff,
242 weights,
243 mode,
244 generators: Vec::new(),
245 membership: Vec::new(),
246 modularity: f64::NAN,
247 };
248 brent_opt(&mut work, minr, maxr)?;
249 Ok(CommunityVoronoiResult {
250 membership: work.membership,
251 generators: work.generators,
252 modularity: if work.modularity.is_nan() {
253 None
254 } else {
255 Some(work.modularity)
256 },
257 })
258 } else {
259 let generators = choose_generators(graph, &lrd, &len_eff, mode, r)?.0;
260 let part = voronoi(
261 graph,
262 Some(&len_eff),
263 mode,
264 &generators,
265 VoronoiTiebreaker::Random,
266 VORONOI_BRENT_SEED,
267 )?;
268 let membership = membership_to_flat(&part.membership);
269 let q = compute_modularity(graph, &membership, weights, mode)?;
270 Ok(CommunityVoronoiResult {
271 membership,
272 generators,
273 modularity: q,
274 })
275 }
276}
277
278fn membership_to_flat(m: &[Option<u32>]) -> Vec<u32> {
284 m.iter().map(|x| x.unwrap_or(u32::MAX)).collect()
285}
286
287fn compute_modularity(
290 graph: &Graph,
291 membership: &[u32],
292 weights: Option<&[f64]>,
293 mode: DijkstraMode,
294) -> IgraphResult<Option<f64>> {
295 let directed = graph.is_directed() && mode != DijkstraMode::All;
296 match (directed, weights) {
297 (false, None) => modularity(graph, membership, 1.0),
298 (false, Some(w)) => modularity_weighted(graph, membership, 1.0, w),
299 (true, None) => modularity_directed(graph, membership, 1.0),
300 (true, Some(w)) => modularity_weighted_directed(graph, membership, 1.0, w),
301 }
302}
303
304fn local_relative_density(graph: &Graph) -> IgraphResult<Vec<f64>> {
314 let n = graph.vcount();
315 let n_us = n as usize;
316 let mut res = vec![0.0_f64; n_us];
317
318 let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
321 for v in 0..n {
322 let mut neigh: Vec<VertexId> = graph
323 .neighbors(v)?
324 .into_iter()
325 .filter(|&u| u != v)
326 .collect();
327 neigh.sort_unstable();
328 neigh.dedup();
329 adj.push(neigh);
330 }
331
332 let mut nei_mask: Vec<u32> = vec![0; n_us];
336 let mut nei_done: Vec<u32> = vec![0; n_us];
337
338 for w in 0..n {
339 let w_us = w as usize;
340 let stamp = w + 1; let dw = adj[w_us].len();
342
343 for &u in &adj[w_us] {
344 nei_mask[u as usize] = stamp;
345 }
346 nei_mask[w_us] = stamp;
347
348 let mut int_count: u64 = dw as u64;
350 let mut ext_count: u64 = 0;
351 nei_done[w_us] = stamp;
352
353 for &v in &adj[w_us] {
354 if nei_done[v as usize] == stamp {
355 continue;
356 }
357 nei_done[v as usize] = stamp;
358 for &u in &adj[v as usize] {
359 if nei_mask[u as usize] == stamp {
360 int_count += 1;
361 } else {
362 ext_count += 1;
363 }
364 }
365 }
366 debug_assert!(int_count % 2 == 0);
368 int_count /= 2;
369
370 res[w_us] = if int_count == 0 {
371 0.0
372 } else {
373 int_count as f64 / (int_count + ext_count) as f64
374 };
375 }
376
377 Ok(res)
378}
379
380fn weighted_local_density(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<f64>> {
386 let mut res = local_relative_density(graph)?;
387 let n = graph.vcount() as usize;
388 let mut str_v = vec![0.0_f64; n];
389 if let Some(w) = weights {
390 for v in 0..graph.vcount() {
391 let mut s = 0.0_f64;
392 for eid in graph.incident(v)? {
393 s += w[eid as usize];
396 }
397 str_v[v as usize] = s;
398 }
399 } else {
400 for v in 0..graph.vcount() {
401 str_v[v as usize] = f64::from(u32::try_from(graph.degree(v)?).unwrap_or(u32::MAX));
404 }
405 }
406 for (a, b) in res.iter_mut().zip(str_v.iter()) {
407 *a *= *b;
408 }
409 Ok(res)
410}
411
412fn choose_generators(
415 graph: &Graph,
416 local_rel_dens: &[f64],
417 lengths: &[f64],
418 mode: DijkstraMode,
419 r: f64,
420) -> IgraphResult<(Vec<VertexId>, f64)> {
421 let n = graph.vcount();
422 let n_us = n as usize;
423
424 let mut ord: Vec<u32> = (0..n).collect();
427 ord.sort_by(|&a, &b| {
428 local_rel_dens[b as usize]
429 .partial_cmp(&local_rel_dens[a as usize])
430 .unwrap_or(Ordering::Equal)
431 .then(a.cmp(&b))
432 });
433
434 let mut excluded = vec![false; n_us];
435 let mut excluded_count: u32 = 0;
436 let mut generators: Vec<VertexId> = Vec::new();
437 let mut radius_max = f64::NEG_INFINITY;
438
439 let mut dist = vec![f64::INFINITY; n_us];
443 let mut epoch = vec![0u32; n_us];
444 let mut active = vec![false; n_us];
445 let mut current_epoch: u32 = 0;
446 let mut heap: BinaryHeap<HeapEntry> = BinaryHeap::new();
447
448 for &g in ord.iter().take(n_us) {
449 if excluded[g as usize] {
450 continue;
451 }
452 generators.push(g);
453
454 current_epoch = current_epoch.wrapping_add(1);
455 if current_epoch == 0 {
456 epoch.fill(0);
458 current_epoch = 1;
459 }
460 heap.clear();
461 dist[g as usize] = 0.0;
462 epoch[g as usize] = current_epoch;
463 active[g as usize] = true;
464 heap.push(HeapEntry { dist: 0.0, vid: g });
465
466 while let Some(HeapEntry { dist: d, vid }) = heap.pop() {
467 let v_us = vid as usize;
468 if !active[v_us] || d > dist[v_us] {
471 continue;
472 }
473 active[v_us] = false;
474
475 if d > r {
477 continue;
478 }
479
480 if !excluded[v_us] {
482 excluded[v_us] = true;
483 excluded_count += 1;
484 }
485 if d > radius_max {
486 radius_max = d;
487 }
488
489 for eid in incident_for_mode(graph, vid, mode)? {
490 let w = lengths[eid as usize];
491 if w.is_infinite() {
493 continue;
494 }
495 let (u1, u2) = graph.edge(eid)?;
496 let to = if u1 == vid { u2 } else { u1 };
497 let alt = d + w;
498 let to_us = to as usize;
499 if epoch[to_us] != current_epoch {
500 dist[to_us] = alt;
501 epoch[to_us] = current_epoch;
502 active[to_us] = true;
503 heap.push(HeapEntry { dist: alt, vid: to });
504 } else if active[to_us] && alt < dist[to_us] {
505 dist[to_us] = alt;
506 heap.push(HeapEntry { dist: alt, vid: to });
507 }
508 }
509 }
510
511 if excluded_count as usize == n_us {
512 break;
513 }
514 }
515
516 if radius_max == f64::NEG_INFINITY {
517 radius_max = 0.0;
518 }
519 Ok((generators, radius_max))
520}
521
522fn incident_for_mode(graph: &Graph, v: VertexId, mode: DijkstraMode) -> IgraphResult<Vec<EdgeId>> {
524 if !graph.is_directed() {
525 return graph.incident(v);
526 }
527 match mode {
528 DijkstraMode::Out => graph.incident(v),
529 DijkstraMode::In => graph.incident_in_pub(v),
530 DijkstraMode::All => {
531 let mut out = graph.incident(v)?;
532 out.extend(graph.incident_in_pub(v)?);
533 Ok(out)
534 }
535 }
536}
537
538fn estimate_minmax_r(
542 graph: &Graph,
543 local_rel_dens: &[f64],
544 lengths: &[f64],
545 mode: DijkstraMode,
546) -> IgraphResult<(f64, f64)> {
547 let mut min_r = f64::INFINITY;
548 for &v in lengths {
549 if v.is_finite() && v < min_r {
550 min_r = v;
551 }
552 }
553 if !min_r.is_finite() {
554 min_r = 0.0;
555 }
556 let (_, max_r) = choose_generators(graph, local_rel_dens, lengths, mode, f64::INFINITY)?;
557 Ok((min_r, max_r))
558}
559
560#[derive(Debug, Clone, Copy)]
562struct HeapEntry {
563 dist: f64,
564 vid: VertexId,
565}
566impl PartialEq for HeapEntry {
567 fn eq(&self, other: &Self) -> bool {
568 self.dist == other.dist && self.vid == other.vid
569 }
570}
571impl Eq for HeapEntry {}
572impl Ord for HeapEntry {
573 fn cmp(&self, other: &Self) -> Ordering {
574 other
576 .dist
577 .partial_cmp(&self.dist)
578 .unwrap_or(Ordering::Equal)
579 .then_with(|| other.vid.cmp(&self.vid))
580 }
581}
582impl PartialOrd for HeapEntry {
583 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
584 Some(self.cmp(other))
585 }
586}
587
588struct ModularityWork<'a> {
591 graph: &'a Graph,
592 local_dens: &'a [f64],
593 lengths: &'a [f64],
594 weights: Option<&'a [f64]>,
595 mode: DijkstraMode,
596 generators: Vec<VertexId>,
597 membership: Vec<u32>,
598 modularity: f64,
599}
600
601fn eval_modularity(work: &mut ModularityWork<'_>, r: f64) -> IgraphResult<f64> {
605 let (gens, _) = choose_generators(work.graph, work.local_dens, work.lengths, work.mode, r)?;
606 let part = voronoi(
607 work.graph,
608 Some(work.lengths),
609 work.mode,
610 &gens,
611 VoronoiTiebreaker::Random,
612 VORONOI_BRENT_SEED,
613 )?;
614 let membership = membership_to_flat(&part.membership);
615 let q = compute_modularity(work.graph, &membership, work.weights, work.mode)?
616 .unwrap_or(f64::NEG_INFINITY);
617 work.generators = gens;
618 work.membership = membership;
619 work.modularity = q;
620 Ok(q)
621}
622
623fn coeff2(x1: f64, x2: f64, x3: f64, f1: f64, f2: f64, f3: f64) -> f64 {
627 let num = x1 * (f3 - f2) + x2 * (f1 - f3) + x3 * (f2 - f1);
628 let denom = (x1 - x2) * (x1 - x3) * (x2 - x3);
629 num / denom
630}
631
632fn peakx(x1: f64, x2: f64, x3: f64, f1: f64, f2: f64, f3: f64) -> f64 {
634 let x1s = x1 * x1;
635 let x2s = x2 * x2;
636 let x3s = x3 * x3;
637 let num = f3 * (x1s - x2s) + f1 * (x2s - x3s) + f2 * (x3s - x1s);
638 let denom = f3 * (x1 - x2) + f1 * (x2 - x3) + f2 * (x3 - x1);
639 0.5 * num / denom
640}
641
642fn cmp_epsilon(a: f64, b: f64, eps: f64) -> Ordering {
643 let diff = (a - b).abs();
644 let tol = eps * (a.abs().max(b.abs())).max(1.0);
645 if diff <= tol {
646 Ordering::Equal
647 } else if a < b {
648 Ordering::Less
649 } else {
650 Ordering::Greater
651 }
652}
653
654fn brent_opt(work: &mut ModularityWork<'_>, x1_in: f64, x2_in: f64) -> IgraphResult<()> {
656 let lo = x1_in;
657 let hi = x2_in;
658
659 if !lo.is_finite() || !hi.is_finite() {
660 return Err(IgraphError::InvalidArgument(
661 "Voronoi radius search interval is non-finite".to_string(),
662 ));
663 }
664
665 let mut x1 = x1_in;
666 let mut x2 = x2_in;
667 let mut x3 = 0.6 * x1 + 0.4 * x2;
670
671 let mut f1 = eval_modularity(work, x1)?;
672 #[allow(clippy::float_cmp)]
675 if x1 == x2 {
676 return Ok(());
677 }
678 let mut f2 = eval_modularity(work, x2)?;
679 let mut f3 = eval_modularity(work, x3)?;
680
681 if f1 > f3 {
683 return Err(IgraphError::Internal(
684 "Voronoi auto-r optimizer did not converge (f1 > f3)",
685 ));
686 }
687
688 if f2 > f3 {
692 let mut bisected = false;
693 for _ in 0..10 {
694 x1 = x3;
695 f1 = f3;
696 x3 = 0.5 * (x1 + x2);
697 f3 = eval_modularity(work, x3)?;
698 if f3 >= f2 {
699 bisected = true;
700 break;
701 }
702 }
703 if !bisected {
704 let _ = eval_modularity(work, x2)?;
706 return Ok(());
707 }
708 }
709
710 for _ in 0..20 {
711 let new_x = peakx(x1, x2, x3, f1, f2, f3);
712 let new_f = eval_modularity(work, new_x)?;
713
714 let a1 = coeff2(x2, x3, new_x, f2, f3, new_f);
715 let a2 = coeff2(x1, x3, new_x, f1, f3, new_f);
716
717 if a1 >= 0.0 && a2 >= 0.0 {
719 break;
720 }
721
722 if a1 <= a2 {
723 x1 = x2;
725 x2 = x3;
726 x3 = new_x;
727 f1 = f2;
728 f2 = f3;
729 f3 = new_f;
730 } else {
731 x2 = x1;
733 x1 = x3;
734 x3 = new_x;
735 f2 = f1;
736 f1 = f3;
737 f3 = new_f;
738 }
739
740 if x3 < lo || x3 > hi {
742 return Err(IgraphError::Internal(
743 "Voronoi auto-r optimizer drifted outside initial interval",
744 ));
745 }
746
747 let eps = 1e-10;
751 if matches!(cmp_epsilon(f1, f3, eps), Ordering::Equal)
752 || matches!(cmp_epsilon(f2, f3, eps), Ordering::Equal)
753 {
754 break;
755 }
756 }
757 Ok(())
758}
759
760trait GraphIncidentInExt {
764 fn incident_in_pub(&self, v: VertexId) -> IgraphResult<Vec<EdgeId>>;
765}
766impl GraphIncidentInExt for Graph {
767 fn incident_in_pub(&self, v: VertexId) -> IgraphResult<Vec<EdgeId>> {
768 self.incident_in(v)
769 }
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775
776 fn k4() -> Graph {
777 let mut g = Graph::with_vertices(4);
778 for u in 0..4u32 {
779 for v in (u + 1)..4 {
780 g.add_edge(u, v).unwrap();
781 }
782 }
783 g
784 }
785
786 fn karate() -> Graph {
787 use std::fs::File;
788 use std::path::PathBuf;
789 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
790 path.push("fixtures/karate.edges");
791 crate::algorithms::io::read_edgelist(File::open(path).unwrap()).unwrap()
792 }
793
794 #[test]
795 fn null_graph_empty_membership() {
796 let g = Graph::with_vertices(0);
797 let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0).unwrap();
798 assert!(r.membership.is_empty());
799 assert!(r.generators.is_empty());
800 assert!(r.modularity.is_none());
801 }
802
803 #[test]
804 fn singleton_graph_one_community() {
805 let g = Graph::with_vertices(1);
806 let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0).unwrap();
807 assert_eq!(r.membership, vec![0]);
808 assert_eq!(r.generators, vec![0]);
809 }
810
811 #[test]
812 fn two_isolated_nodes_two_singleton_communities() {
813 let g = Graph::with_vertices(2);
814 let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0).unwrap();
815 assert_eq!(r.membership, vec![0, 1]);
816 assert_eq!(r.generators, vec![0, 1]);
817 }
818
819 #[test]
820 fn non_simple_graph_errors() {
821 let mut g = Graph::with_vertices(2);
822 g.add_edge(0, 1).unwrap();
823 g.add_edge(0, 1).unwrap(); let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0);
825 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
826 }
827
828 #[test]
829 fn self_loop_graph_errors() {
830 let mut g = Graph::with_vertices(2);
831 g.add_edge(0, 1).unwrap();
832 g.add_edge(0, 0).unwrap(); let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0);
834 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
835 }
836
837 #[test]
838 fn fixed_r_zero_gives_singleton_communities() {
839 let g = k4();
840 let r = community_voronoi(&g, None, None, DijkstraMode::All, 0.0).unwrap();
841 assert_eq!(r.generators.len(), 4);
844 let mut distinct: Vec<u32> = r.membership.clone();
846 distinct.sort_unstable();
847 distinct.dedup();
848 assert_eq!(distinct.len(), 4);
849 }
850
851 #[test]
852 fn karate_auto_r_yields_known_membership() {
853 let g = karate();
865 let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0).unwrap();
866 assert_eq!(r.membership.len(), 34);
867 assert!(r.generators.len() >= 2 && r.generators.len() <= 6);
868 let q = r.modularity.expect("karate is non-empty");
869 assert!(q > 0.30, "expected karate Q > 0.30, got {q}");
870 }
871
872 #[test]
873 fn karate_fixed_r_matches_modularity_call() {
874 let g = karate();
875 let r = community_voronoi(&g, None, None, DijkstraMode::All, 2.0).unwrap();
876 let q_direct = modularity(&g, &r.membership, 1.0).unwrap();
878 assert!((r.modularity.unwrap() - q_direct.unwrap()).abs() < 1e-9);
879 }
880
881 #[test]
882 fn lengths_size_mismatch_errors() {
883 let g = k4();
884 let bad = vec![1.0; g.ecount() + 1];
885 let r = community_voronoi(&g, Some(&bad), None, DijkstraMode::All, -1.0);
886 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
887 }
888
889 #[test]
890 fn weights_size_mismatch_errors() {
891 let g = k4();
892 let bad = vec![1.0; g.ecount() + 1];
893 let r = community_voronoi(&g, None, Some(&bad), DijkstraMode::All, -1.0);
894 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
895 }
896
897 #[test]
898 fn negative_length_errors() {
899 let g = k4();
900 let mut l = vec![1.0; g.ecount()];
901 l[0] = -1.0;
902 let r = community_voronoi(&g, Some(&l), None, DijkstraMode::All, -1.0);
903 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
904 }
905
906 #[test]
907 fn zero_weight_errors() {
908 let g = k4();
909 let mut w = vec![1.0; g.ecount()];
910 w[0] = 0.0;
911 let r = community_voronoi(&g, None, Some(&w), DijkstraMode::All, -1.0);
912 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
913 }
914
915 #[test]
916 fn nan_length_errors() {
917 let g = k4();
918 let mut l = vec![1.0; g.ecount()];
919 l[0] = f64::NAN;
920 let r = community_voronoi(&g, Some(&l), None, DijkstraMode::All, -1.0);
921 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
922 }
923
924 #[test]
925 fn local_relative_density_isolated_vertex_is_zero() {
926 let g = Graph::with_vertices(2);
927 let lrd = local_relative_density(&g).unwrap();
928 assert_eq!(lrd, vec![0.0, 0.0]);
929 }
930
931 #[test]
932 fn local_relative_density_triangle_is_one() {
933 let mut g = Graph::with_vertices(3);
934 g.add_edge(0, 1).unwrap();
935 g.add_edge(1, 2).unwrap();
936 g.add_edge(0, 2).unwrap();
937 let lrd = local_relative_density(&g).unwrap();
940 for v in lrd {
941 assert!((v - 1.0).abs() < 1e-12);
942 }
943 }
944}
945
946#[cfg(all(test, feature = "proptest-harness"))]
947mod proptests {
948 use super::*;
949 use proptest::prelude::*;
950
951 prop_compose! {
952 fn small_simple_graph()(n in 4u32..=10u32, edges_seed in any::<u64>()) -> Graph {
953 let mut g = Graph::with_vertices(n);
954 let mut rng = edges_seed;
955 let target_m = ((n * (n - 1)) / 2).min(n + 6) as usize;
956 let mut added = 0usize;
957 let mut guard = 0usize;
958 while added < target_m && guard < target_m * 8 + 4 {
959 rng = rng.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
960 let u = ((rng >> 33) % u64::from(n)) as u32;
961 rng = rng.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
962 let v = ((rng >> 33) % u64::from(n)) as u32;
963 guard += 1;
964 if u == v { continue; }
965 if g.add_edge(u, v).is_ok() {
966 added += 1;
967 }
968 }
969 g
970 }
971 }
972
973 proptest! {
974 #[test]
980 fn membership_size_matches_vertex_count(g in small_simple_graph()) {
981 if !crate::algorithms::properties::is_simple::is_simple(&g).unwrap() {
982 return Ok(());
983 }
984 let r = community_voronoi(&g, None, None, DijkstraMode::All, 1.0).unwrap();
985 prop_assert_eq!(r.membership.len() as u32, g.vcount());
986 }
987
988 #[test]
989 fn generators_subset_of_vertices(g in small_simple_graph()) {
990 if !crate::algorithms::properties::is_simple::is_simple(&g).unwrap() {
991 return Ok(());
992 }
993 let r = community_voronoi(&g, None, None, DijkstraMode::All, 1.0).unwrap();
994 for &g_id in &r.generators {
995 prop_assert!(g_id < g.vcount(), "generator {} out of range (n={})", g_id, g.vcount());
996 }
997 }
998 }
999}