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