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(
164 graph: &Graph,
165 lengths: Option<&[f64]>,
166 weights: Option<&[f64]>,
167 mode: DijkstraMode,
168 r: f64,
169) -> IgraphResult<CommunityVoronoiResult> {
170 let n = graph.vcount();
171 let m = graph.ecount();
172
173 let mode = if graph.is_directed() {
175 mode
176 } else {
177 DijkstraMode::All
178 };
179
180 validate_inputs(graph, lengths, weights, m)?;
181
182 if m == 0 {
183 let membership: Vec<u32> = (0..n).collect();
185 let generators: Vec<VertexId> = (0..n).collect();
186 return Ok(CommunityVoronoiResult {
187 membership,
188 generators,
189 modularity: None,
190 });
191 }
192
193 let lrd = weighted_local_density(graph, weights)?;
195
196 let mut len_eff = ecc(graph, None, 3, true, true)?;
198 for v in &mut len_eff {
199 *v = 1.0 / *v;
202 }
203 if let Some(l) = lengths {
204 for (a, &b) in len_eff.iter_mut().zip(l.iter()) {
205 *a *= b;
206 }
207 }
208
209 if r < 0.0 {
210 let (minr, maxr) = estimate_minmax_r(graph, &lrd, &len_eff, mode)?;
213 let mut work = ModularityWork {
214 graph,
215 local_dens: &lrd,
216 lengths: &len_eff,
217 weights,
218 mode,
219 generators: Vec::new(),
220 membership: Vec::new(),
221 modularity: f64::NAN,
222 };
223 brent_opt(&mut work, minr, maxr)?;
224 Ok(CommunityVoronoiResult {
225 membership: work.membership,
226 generators: work.generators,
227 modularity: if work.modularity.is_nan() {
228 None
229 } else {
230 Some(work.modularity)
231 },
232 })
233 } else {
234 let generators = choose_generators(graph, &lrd, &len_eff, mode, r)?.0;
235 let part = voronoi(
236 graph,
237 Some(&len_eff),
238 mode,
239 &generators,
240 VoronoiTiebreaker::Random,
241 VORONOI_BRENT_SEED,
242 )?;
243 let membership = membership_to_flat(&part.membership);
244 let q = compute_modularity(graph, &membership, weights, mode)?;
245 Ok(CommunityVoronoiResult {
246 membership,
247 generators,
248 modularity: q,
249 })
250 }
251}
252
253fn membership_to_flat(m: &[Option<u32>]) -> Vec<u32> {
259 m.iter().map(|x| x.unwrap_or(u32::MAX)).collect()
260}
261
262fn compute_modularity(
265 graph: &Graph,
266 membership: &[u32],
267 weights: Option<&[f64]>,
268 mode: DijkstraMode,
269) -> IgraphResult<Option<f64>> {
270 let directed = graph.is_directed() && mode != DijkstraMode::All;
271 match (directed, weights) {
272 (false, None) => modularity(graph, membership, 1.0),
273 (false, Some(w)) => modularity_weighted(graph, membership, 1.0, w),
274 (true, None) => modularity_directed(graph, membership, 1.0),
275 (true, Some(w)) => modularity_weighted_directed(graph, membership, 1.0, w),
276 }
277}
278
279fn local_relative_density(graph: &Graph) -> IgraphResult<Vec<f64>> {
289 let n = graph.vcount();
290 let n_us = n as usize;
291 let mut res = vec![0.0_f64; n_us];
292
293 let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
296 for v in 0..n {
297 let mut neigh: Vec<VertexId> = graph
298 .neighbors(v)?
299 .into_iter()
300 .filter(|&u| u != v)
301 .collect();
302 neigh.sort_unstable();
303 neigh.dedup();
304 adj.push(neigh);
305 }
306
307 let mut nei_mask: Vec<u32> = vec![0; n_us];
311 let mut nei_done: Vec<u32> = vec![0; n_us];
312
313 for w in 0..n {
314 let w_us = w as usize;
315 let stamp = w + 1; let dw = adj[w_us].len();
317
318 for &u in &adj[w_us] {
319 nei_mask[u as usize] = stamp;
320 }
321 nei_mask[w_us] = stamp;
322
323 let mut int_count: u64 = dw as u64;
325 let mut ext_count: u64 = 0;
326 nei_done[w_us] = stamp;
327
328 for &v in &adj[w_us] {
329 if nei_done[v as usize] == stamp {
330 continue;
331 }
332 nei_done[v as usize] = stamp;
333 for &u in &adj[v as usize] {
334 if nei_mask[u as usize] == stamp {
335 int_count += 1;
336 } else {
337 ext_count += 1;
338 }
339 }
340 }
341 debug_assert!(int_count % 2 == 0);
343 int_count /= 2;
344
345 res[w_us] = if int_count == 0 {
346 0.0
347 } else {
348 int_count as f64 / (int_count + ext_count) as f64
349 };
350 }
351
352 Ok(res)
353}
354
355fn weighted_local_density(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<f64>> {
361 let mut res = local_relative_density(graph)?;
362 let n = graph.vcount() as usize;
363 let mut str_v = vec![0.0_f64; n];
364 if let Some(w) = weights {
365 for v in 0..graph.vcount() {
366 let mut s = 0.0_f64;
367 for eid in graph.incident(v)? {
368 s += w[eid as usize];
371 }
372 str_v[v as usize] = s;
373 }
374 } else {
375 for v in 0..graph.vcount() {
376 str_v[v as usize] = f64::from(u32::try_from(graph.degree(v)?).unwrap_or(u32::MAX));
379 }
380 }
381 for (a, b) in res.iter_mut().zip(str_v.iter()) {
382 *a *= *b;
383 }
384 Ok(res)
385}
386
387fn choose_generators(
390 graph: &Graph,
391 local_rel_dens: &[f64],
392 lengths: &[f64],
393 mode: DijkstraMode,
394 r: f64,
395) -> IgraphResult<(Vec<VertexId>, f64)> {
396 let n = graph.vcount();
397 let n_us = n as usize;
398
399 let mut ord: Vec<u32> = (0..n).collect();
402 ord.sort_by(|&a, &b| {
403 local_rel_dens[b as usize]
404 .partial_cmp(&local_rel_dens[a as usize])
405 .unwrap_or(Ordering::Equal)
406 .then(a.cmp(&b))
407 });
408
409 let mut excluded = vec![false; n_us];
410 let mut excluded_count: u32 = 0;
411 let mut generators: Vec<VertexId> = Vec::new();
412 let mut radius_max = f64::NEG_INFINITY;
413
414 let mut dist = vec![f64::INFINITY; n_us];
418 let mut epoch = vec![0u32; n_us];
419 let mut active = vec![false; n_us];
420 let mut current_epoch: u32 = 0;
421 let mut heap: BinaryHeap<HeapEntry> = BinaryHeap::new();
422
423 for &g in ord.iter().take(n_us) {
424 if excluded[g as usize] {
425 continue;
426 }
427 generators.push(g);
428
429 current_epoch = current_epoch.wrapping_add(1);
430 if current_epoch == 0 {
431 epoch.fill(0);
433 current_epoch = 1;
434 }
435 heap.clear();
436 dist[g as usize] = 0.0;
437 epoch[g as usize] = current_epoch;
438 active[g as usize] = true;
439 heap.push(HeapEntry { dist: 0.0, vid: g });
440
441 while let Some(HeapEntry { dist: d, vid }) = heap.pop() {
442 let v_us = vid as usize;
443 if !active[v_us] || d > dist[v_us] {
446 continue;
447 }
448 active[v_us] = false;
449
450 if d > r {
452 continue;
453 }
454
455 if !excluded[v_us] {
457 excluded[v_us] = true;
458 excluded_count += 1;
459 }
460 if d > radius_max {
461 radius_max = d;
462 }
463
464 for eid in incident_for_mode(graph, vid, mode)? {
465 let w = lengths[eid as usize];
466 if w.is_infinite() {
468 continue;
469 }
470 let (u1, u2) = graph.edge(eid)?;
471 let to = if u1 == vid { u2 } else { u1 };
472 let alt = d + w;
473 let to_us = to as usize;
474 if epoch[to_us] != current_epoch {
475 dist[to_us] = alt;
476 epoch[to_us] = current_epoch;
477 active[to_us] = true;
478 heap.push(HeapEntry { dist: alt, vid: to });
479 } else if active[to_us] && alt < dist[to_us] {
480 dist[to_us] = alt;
481 heap.push(HeapEntry { dist: alt, vid: to });
482 }
483 }
484 }
485
486 if excluded_count as usize == n_us {
487 break;
488 }
489 }
490
491 if radius_max == f64::NEG_INFINITY {
492 radius_max = 0.0;
493 }
494 Ok((generators, radius_max))
495}
496
497fn incident_for_mode(graph: &Graph, v: VertexId, mode: DijkstraMode) -> IgraphResult<Vec<EdgeId>> {
499 if !graph.is_directed() {
500 return graph.incident(v);
501 }
502 match mode {
503 DijkstraMode::Out => graph.incident(v),
504 DijkstraMode::In => graph.incident_in_pub(v),
505 DijkstraMode::All => {
506 let mut out = graph.incident(v)?;
507 out.extend(graph.incident_in_pub(v)?);
508 Ok(out)
509 }
510 }
511}
512
513fn estimate_minmax_r(
517 graph: &Graph,
518 local_rel_dens: &[f64],
519 lengths: &[f64],
520 mode: DijkstraMode,
521) -> IgraphResult<(f64, f64)> {
522 let mut min_r = f64::INFINITY;
523 for &v in lengths {
524 if v.is_finite() && v < min_r {
525 min_r = v;
526 }
527 }
528 if !min_r.is_finite() {
529 min_r = 0.0;
530 }
531 let (_, max_r) = choose_generators(graph, local_rel_dens, lengths, mode, f64::INFINITY)?;
532 Ok((min_r, max_r))
533}
534
535#[derive(Debug, Clone, Copy)]
537struct HeapEntry {
538 dist: f64,
539 vid: VertexId,
540}
541impl PartialEq for HeapEntry {
542 fn eq(&self, other: &Self) -> bool {
543 self.dist == other.dist && self.vid == other.vid
544 }
545}
546impl Eq for HeapEntry {}
547impl Ord for HeapEntry {
548 fn cmp(&self, other: &Self) -> Ordering {
549 other
551 .dist
552 .partial_cmp(&self.dist)
553 .unwrap_or(Ordering::Equal)
554 .then_with(|| other.vid.cmp(&self.vid))
555 }
556}
557impl PartialOrd for HeapEntry {
558 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
559 Some(self.cmp(other))
560 }
561}
562
563struct ModularityWork<'a> {
566 graph: &'a Graph,
567 local_dens: &'a [f64],
568 lengths: &'a [f64],
569 weights: Option<&'a [f64]>,
570 mode: DijkstraMode,
571 generators: Vec<VertexId>,
572 membership: Vec<u32>,
573 modularity: f64,
574}
575
576fn eval_modularity(work: &mut ModularityWork<'_>, r: f64) -> IgraphResult<f64> {
580 let (gens, _) = choose_generators(work.graph, work.local_dens, work.lengths, work.mode, r)?;
581 let part = voronoi(
582 work.graph,
583 Some(work.lengths),
584 work.mode,
585 &gens,
586 VoronoiTiebreaker::Random,
587 VORONOI_BRENT_SEED,
588 )?;
589 let membership = membership_to_flat(&part.membership);
590 let q = compute_modularity(work.graph, &membership, work.weights, work.mode)?
591 .unwrap_or(f64::NEG_INFINITY);
592 work.generators = gens;
593 work.membership = membership;
594 work.modularity = q;
595 Ok(q)
596}
597
598fn coeff2(x1: f64, x2: f64, x3: f64, f1: f64, f2: f64, f3: f64) -> f64 {
602 let num = x1 * (f3 - f2) + x2 * (f1 - f3) + x3 * (f2 - f1);
603 let denom = (x1 - x2) * (x1 - x3) * (x2 - x3);
604 num / denom
605}
606
607fn peakx(x1: f64, x2: f64, x3: f64, f1: f64, f2: f64, f3: f64) -> f64 {
609 let x1s = x1 * x1;
610 let x2s = x2 * x2;
611 let x3s = x3 * x3;
612 let num = f3 * (x1s - x2s) + f1 * (x2s - x3s) + f2 * (x3s - x1s);
613 let denom = f3 * (x1 - x2) + f1 * (x2 - x3) + f2 * (x3 - x1);
614 0.5 * num / denom
615}
616
617fn cmp_epsilon(a: f64, b: f64, eps: f64) -> Ordering {
618 let diff = (a - b).abs();
619 let tol = eps * (a.abs().max(b.abs())).max(1.0);
620 if diff <= tol {
621 Ordering::Equal
622 } else if a < b {
623 Ordering::Less
624 } else {
625 Ordering::Greater
626 }
627}
628
629fn brent_opt(work: &mut ModularityWork<'_>, x1_in: f64, x2_in: f64) -> IgraphResult<()> {
631 let lo = x1_in;
632 let hi = x2_in;
633
634 if !lo.is_finite() || !hi.is_finite() {
635 return Err(IgraphError::InvalidArgument(
636 "Voronoi radius search interval is non-finite".to_string(),
637 ));
638 }
639
640 let mut x1 = x1_in;
641 let mut x2 = x2_in;
642 let mut x3 = 0.6 * x1 + 0.4 * x2;
645
646 let mut f1 = eval_modularity(work, x1)?;
647 #[allow(clippy::float_cmp)]
650 if x1 == x2 {
651 return Ok(());
652 }
653 let mut f2 = eval_modularity(work, x2)?;
654 let mut f3 = eval_modularity(work, x3)?;
655
656 if f1 > f3 {
658 return Err(IgraphError::Internal(
659 "Voronoi auto-r optimizer did not converge (f1 > f3)",
660 ));
661 }
662
663 if f2 > f3 {
667 let mut bisected = false;
668 for _ in 0..10 {
669 x1 = x3;
670 f1 = f3;
671 x3 = 0.5 * (x1 + x2);
672 f3 = eval_modularity(work, x3)?;
673 if f3 >= f2 {
674 bisected = true;
675 break;
676 }
677 }
678 if !bisected {
679 let _ = eval_modularity(work, x2)?;
681 return Ok(());
682 }
683 }
684
685 for _ in 0..20 {
686 let new_x = peakx(x1, x2, x3, f1, f2, f3);
687 let new_f = eval_modularity(work, new_x)?;
688
689 let a1 = coeff2(x2, x3, new_x, f2, f3, new_f);
690 let a2 = coeff2(x1, x3, new_x, f1, f3, new_f);
691
692 if a1 >= 0.0 && a2 >= 0.0 {
694 break;
695 }
696
697 if a1 <= a2 {
698 x1 = x2;
700 x2 = x3;
701 x3 = new_x;
702 f1 = f2;
703 f2 = f3;
704 f3 = new_f;
705 } else {
706 x2 = x1;
708 x1 = x3;
709 x3 = new_x;
710 f2 = f1;
711 f1 = f3;
712 f3 = new_f;
713 }
714
715 if x3 < lo || x3 > hi {
717 return Err(IgraphError::Internal(
718 "Voronoi auto-r optimizer drifted outside initial interval",
719 ));
720 }
721
722 let eps = 1e-10;
726 if matches!(cmp_epsilon(f1, f3, eps), Ordering::Equal)
727 || matches!(cmp_epsilon(f2, f3, eps), Ordering::Equal)
728 {
729 break;
730 }
731 }
732 Ok(())
733}
734
735trait GraphIncidentInExt {
739 fn incident_in_pub(&self, v: VertexId) -> IgraphResult<Vec<EdgeId>>;
740}
741impl GraphIncidentInExt for Graph {
742 fn incident_in_pub(&self, v: VertexId) -> IgraphResult<Vec<EdgeId>> {
743 self.incident_in(v)
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750
751 fn k4() -> Graph {
752 let mut g = Graph::with_vertices(4);
753 for u in 0..4u32 {
754 for v in (u + 1)..4 {
755 g.add_edge(u, v).unwrap();
756 }
757 }
758 g
759 }
760
761 fn karate() -> Graph {
762 use std::fs::File;
763 use std::path::PathBuf;
764 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
765 path.push("fixtures/karate.edges");
766 crate::algorithms::io::read_edgelist(File::open(path).unwrap()).unwrap()
767 }
768
769 #[test]
770 fn null_graph_empty_membership() {
771 let g = Graph::with_vertices(0);
772 let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0).unwrap();
773 assert!(r.membership.is_empty());
774 assert!(r.generators.is_empty());
775 assert!(r.modularity.is_none());
776 }
777
778 #[test]
779 fn singleton_graph_one_community() {
780 let g = Graph::with_vertices(1);
781 let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0).unwrap();
782 assert_eq!(r.membership, vec![0]);
783 assert_eq!(r.generators, vec![0]);
784 }
785
786 #[test]
787 fn two_isolated_nodes_two_singleton_communities() {
788 let g = Graph::with_vertices(2);
789 let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0).unwrap();
790 assert_eq!(r.membership, vec![0, 1]);
791 assert_eq!(r.generators, vec![0, 1]);
792 }
793
794 #[test]
795 fn non_simple_graph_errors() {
796 let mut g = Graph::with_vertices(2);
797 g.add_edge(0, 1).unwrap();
798 g.add_edge(0, 1).unwrap(); let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0);
800 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
801 }
802
803 #[test]
804 fn self_loop_graph_errors() {
805 let mut g = Graph::with_vertices(2);
806 g.add_edge(0, 1).unwrap();
807 g.add_edge(0, 0).unwrap(); let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0);
809 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
810 }
811
812 #[test]
813 fn fixed_r_zero_gives_singleton_communities() {
814 let g = k4();
815 let r = community_voronoi(&g, None, None, DijkstraMode::All, 0.0).unwrap();
816 assert_eq!(r.generators.len(), 4);
819 let mut distinct: Vec<u32> = r.membership.clone();
821 distinct.sort_unstable();
822 distinct.dedup();
823 assert_eq!(distinct.len(), 4);
824 }
825
826 #[test]
827 fn karate_auto_r_yields_known_membership() {
828 let g = karate();
840 let r = community_voronoi(&g, None, None, DijkstraMode::All, -1.0).unwrap();
841 assert_eq!(r.membership.len(), 34);
842 assert!(r.generators.len() >= 2 && r.generators.len() <= 6);
843 let q = r.modularity.expect("karate is non-empty");
844 assert!(q > 0.30, "expected karate Q > 0.30, got {q}");
845 }
846
847 #[test]
848 fn karate_fixed_r_matches_modularity_call() {
849 let g = karate();
850 let r = community_voronoi(&g, None, None, DijkstraMode::All, 2.0).unwrap();
851 let q_direct = modularity(&g, &r.membership, 1.0).unwrap();
853 assert!((r.modularity.unwrap() - q_direct.unwrap()).abs() < 1e-9);
854 }
855
856 #[test]
857 fn lengths_size_mismatch_errors() {
858 let g = k4();
859 let bad = vec![1.0; g.ecount() + 1];
860 let r = community_voronoi(&g, Some(&bad), None, DijkstraMode::All, -1.0);
861 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
862 }
863
864 #[test]
865 fn weights_size_mismatch_errors() {
866 let g = k4();
867 let bad = vec![1.0; g.ecount() + 1];
868 let r = community_voronoi(&g, None, Some(&bad), DijkstraMode::All, -1.0);
869 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
870 }
871
872 #[test]
873 fn negative_length_errors() {
874 let g = k4();
875 let mut l = vec![1.0; g.ecount()];
876 l[0] = -1.0;
877 let r = community_voronoi(&g, Some(&l), None, DijkstraMode::All, -1.0);
878 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
879 }
880
881 #[test]
882 fn zero_weight_errors() {
883 let g = k4();
884 let mut w = vec![1.0; g.ecount()];
885 w[0] = 0.0;
886 let r = community_voronoi(&g, None, Some(&w), DijkstraMode::All, -1.0);
887 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
888 }
889
890 #[test]
891 fn nan_length_errors() {
892 let g = k4();
893 let mut l = vec![1.0; g.ecount()];
894 l[0] = f64::NAN;
895 let r = community_voronoi(&g, Some(&l), None, DijkstraMode::All, -1.0);
896 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
897 }
898
899 #[test]
900 fn local_relative_density_isolated_vertex_is_zero() {
901 let g = Graph::with_vertices(2);
902 let lrd = local_relative_density(&g).unwrap();
903 assert_eq!(lrd, vec![0.0, 0.0]);
904 }
905
906 #[test]
907 fn local_relative_density_triangle_is_one() {
908 let mut g = Graph::with_vertices(3);
909 g.add_edge(0, 1).unwrap();
910 g.add_edge(1, 2).unwrap();
911 g.add_edge(0, 2).unwrap();
912 let lrd = local_relative_density(&g).unwrap();
915 for v in lrd {
916 assert!((v - 1.0).abs() < 1e-12);
917 }
918 }
919}
920
921#[cfg(all(test, feature = "proptest-harness"))]
922mod proptests {
923 use super::*;
924 use proptest::prelude::*;
925
926 prop_compose! {
927 fn small_simple_graph()(n in 4u32..=10u32, edges_seed in any::<u64>()) -> Graph {
928 let mut g = Graph::with_vertices(n);
929 let mut rng = edges_seed;
930 let target_m = ((n * (n - 1)) / 2).min(n + 6) as usize;
931 let mut added = 0usize;
932 let mut guard = 0usize;
933 while added < target_m && guard < target_m * 8 + 4 {
934 rng = rng.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
935 let u = ((rng >> 33) % u64::from(n)) as u32;
936 rng = rng.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
937 let v = ((rng >> 33) % u64::from(n)) as u32;
938 guard += 1;
939 if u == v { continue; }
940 if g.add_edge(u, v).is_ok() {
941 added += 1;
942 }
943 }
944 g
945 }
946 }
947
948 proptest! {
949 #[test]
955 fn membership_size_matches_vertex_count(g in small_simple_graph()) {
956 if !crate::algorithms::properties::is_simple::is_simple(&g).unwrap() {
957 return Ok(());
958 }
959 let r = community_voronoi(&g, None, None, DijkstraMode::All, 1.0).unwrap();
960 prop_assert_eq!(r.membership.len() as u32, g.vcount());
961 }
962
963 #[test]
964 fn generators_subset_of_vertices(g in small_simple_graph()) {
965 if !crate::algorithms::properties::is_simple::is_simple(&g).unwrap() {
966 return Ok(());
967 }
968 let r = community_voronoi(&g, None, None, DijkstraMode::All, 1.0).unwrap();
969 for &g_id in &r.generators {
970 prop_assert!(g_id < g.vcount(), "generator {} out of range (n={})", g_id, g.vcount());
971 }
972 }
973 }
974}