1#![allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
32
33use std::cmp::Ordering;
34use std::collections::{BinaryHeap, VecDeque};
35
36use crate::algorithms::paths::dijkstra::DijkstraMode;
37use crate::core::graph::EdgeId;
38use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum VoronoiTiebreaker {
47 First,
50 Last,
53 Random,
62}
63
64#[derive(Debug, Clone, PartialEq)]
74pub struct VoronoiPartition {
75 pub membership: Vec<Option<u32>>,
77 pub distances: Vec<f64>,
80}
81
82struct SplitMix64(u64);
86
87impl SplitMix64 {
88 fn new(seed: u64) -> Self {
89 Self(seed)
90 }
91 fn next_u64(&mut self) -> u64 {
92 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
93 let mut z = self.0;
94 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
95 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
96 z ^ (z >> 31)
97 }
98 fn gen_unit(&mut self) -> f64 {
100 let bits = self.next_u64() >> 11;
101 #[allow(clippy::cast_precision_loss)]
102 let numerator = bits as f64;
103 numerator * (1.0_f64 / 9_007_199_254_740_992.0_f64)
104 }
105}
106
107fn incident_for_mode(graph: &Graph, v: VertexId, mode: DijkstraMode) -> IgraphResult<Vec<EdgeId>> {
109 if !graph.is_directed() {
110 return graph.incident(v);
111 }
112 match mode {
113 DijkstraMode::Out => graph.incident(v),
114 DijkstraMode::In => graph.incident_in(v),
115 DijkstraMode::All => {
116 let mut out = graph.incident(v)?;
117 out.extend(graph.incident_in(v)?);
118 Ok(out)
119 }
120 }
121}
122
123fn validate_weights(graph: &Graph, weights: &[f64]) -> IgraphResult<()> {
127 let m = graph.ecount();
128 if weights.len() != m {
129 return Err(IgraphError::InvalidArgument(format!(
130 "weights vector size ({}) differs from edge count ({})",
131 weights.len(),
132 m
133 )));
134 }
135 for (e, &w) in weights.iter().enumerate() {
136 if w.is_nan() {
137 return Err(IgraphError::InvalidArgument(format!(
138 "weight at edge {e} is NaN"
139 )));
140 }
141 if w < 0.0 {
142 return Err(IgraphError::InvalidArgument(format!(
143 "weight at edge {e} is negative ({w}); Voronoi requires non-negative weights"
144 )));
145 }
146 }
147 Ok(())
148}
149
150fn validate_generators(graph: &Graph, generators: &[VertexId]) -> IgraphResult<()> {
155 let n = graph.vcount();
156 for (i, &g) in generators.iter().enumerate() {
157 if g >= n {
158 return Err(IgraphError::InvalidArgument(format!(
159 "generator at index {i} is vertex {g}, out of range (vcount = {n})"
160 )));
161 }
162 }
163 Ok(())
164}
165
166fn apply_tiebreaker(
171 tiebreaker: VoronoiTiebreaker,
172 v_us: usize,
173 i: u32,
174 membership: &mut [Option<u32>],
175 tie_count: &mut [u32],
176 rng: &mut SplitMix64,
177) {
178 match tiebreaker {
179 VoronoiTiebreaker::First => { }
180 VoronoiTiebreaker::Last => membership[v_us] = Some(i),
181 VoronoiTiebreaker::Random => {
182 tie_count[v_us] = tie_count[v_us].saturating_add(1);
183 let k = tie_count[v_us];
187 if rng.gen_unit() < 1.0_f64 / f64::from(k) {
188 membership[v_us] = Some(i);
189 }
190 }
191 }
192}
193
194fn voronoi_bfs(
196 graph: &Graph,
197 generators: &[VertexId],
198 mode: DijkstraMode,
199 tiebreaker: VoronoiTiebreaker,
200 seed: u64,
201) -> IgraphResult<VoronoiPartition> {
202 let n = graph.vcount();
203 let n_us = n as usize;
204
205 let mut membership: Vec<Option<u32>> = vec![None; n_us];
206 let mut distances: Vec<f64> = vec![f64::INFINITY; n_us];
207 let mut tie_count: Vec<u32> = vec![0; n_us];
208 let mut already_counted: Vec<u32> = vec![0; n_us];
209 let mut q: VecDeque<(VertexId, u32)> = VecDeque::new();
210 let mut rng = SplitMix64::new(seed);
211
212 for (i, &g) in generators.iter().enumerate() {
213 let i_u32 = u32::try_from(i).map_err(|_| {
214 IgraphError::InvalidArgument(format!("too many generators (overflow at index {i})"))
215 })?;
216 let stamp = i_u32.checked_add(1).ok_or_else(|| {
217 IgraphError::InvalidArgument(format!("too many generators (overflow at index {i_u32})"))
218 })?;
219
220 q.clear();
221 already_counted[g as usize] = stamp;
222 q.push_back((g, 0));
223
224 while let Some((vid, dist)) = q.pop_front() {
225 let v_us = vid as usize;
226 let md = distances[v_us];
227 let dist_f = f64::from(dist);
228
229 if dist_f > md {
230 continue;
233 }
234 if dist_f < md {
235 distances[v_us] = dist_f;
236 membership[v_us] = Some(i_u32);
237 if matches!(tiebreaker, VoronoiTiebreaker::Random) {
238 tie_count[v_us] = 1;
239 }
240 } else {
241 apply_tiebreaker(
242 tiebreaker,
243 v_us,
244 i_u32,
245 &mut membership,
246 &mut tie_count,
247 &mut rng,
248 );
249 }
250
251 for eid in incident_for_mode(graph, vid, mode)? {
252 let nei = graph.edge_other(eid as EdgeId, vid)?;
253 if already_counted[nei as usize] == stamp {
254 continue;
255 }
256 already_counted[nei as usize] = stamp;
257 let next_dist = dist.checked_add(1).ok_or(IgraphError::Internal(
258 "voronoi BFS distance overflow (graph diameter exceeds u32::MAX)",
259 ))?;
260 q.push_back((nei, next_dist));
261 }
262 }
263 }
264
265 Ok(VoronoiPartition {
266 membership,
267 distances,
268 })
269}
270
271#[derive(Copy, Clone)]
274struct Frontier(f64, VertexId);
275
276impl PartialEq for Frontier {
277 fn eq(&self, other: &Self) -> bool {
278 self.0 == other.0 && self.1 == other.1
279 }
280}
281impl Eq for Frontier {}
282impl Ord for Frontier {
283 fn cmp(&self, other: &Self) -> Ordering {
284 other.0.total_cmp(&self.0).then(other.1.cmp(&self.1))
285 }
286}
287impl PartialOrd for Frontier {
288 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
289 Some(self.cmp(other))
290 }
291}
292
293fn voronoi_dijkstra(
295 graph: &Graph,
296 weights: &[f64],
297 generators: &[VertexId],
298 mode: DijkstraMode,
299 tiebreaker: VoronoiTiebreaker,
300 seed: u64,
301) -> IgraphResult<VoronoiPartition> {
302 let n = graph.vcount();
303 let n_us = n as usize;
304
305 let mut membership: Vec<Option<u32>> = vec![None; n_us];
306 let mut distances: Vec<f64> = vec![f64::INFINITY; n_us];
307 let mut tie_count: Vec<u32> = vec![0; n_us];
308 let mut rng = SplitMix64::new(seed);
309
310 let mut tentative: Vec<f64> = vec![f64::INFINITY; n_us];
315
316 for (i, &g) in generators.iter().enumerate() {
317 let i_u32 = u32::try_from(i).map_err(|_| {
318 IgraphError::InvalidArgument(format!("too many generators (overflow at index {i})"))
319 })?;
320
321 tentative.fill(f64::INFINITY);
327 let mut heap: BinaryHeap<Frontier> = BinaryHeap::new();
328 tentative[g as usize] = 0.0;
329 heap.push(Frontier(0.0, g));
330
331 while let Some(Frontier(dist, vid)) = heap.pop() {
332 let v_us = vid as usize;
333 if dist > tentative[v_us] {
335 continue;
336 }
337 let md = distances[v_us];
338
339 let cmp = cmp_epsilon(dist, md, EPSILON);
344 if cmp == Ordering::Greater {
345 continue;
347 }
348 if cmp == Ordering::Less {
349 distances[v_us] = dist;
350 membership[v_us] = Some(i_u32);
351 if matches!(tiebreaker, VoronoiTiebreaker::Random) {
352 tie_count[v_us] = 1;
353 }
354 } else {
355 apply_tiebreaker(
356 tiebreaker,
357 v_us,
358 i_u32,
359 &mut membership,
360 &mut tie_count,
361 &mut rng,
362 );
363 }
364
365 for eid in incident_for_mode(graph, vid, mode)? {
366 let w = weights[eid as usize];
367 if !w.is_finite() {
369 continue;
370 }
371 let nd = dist + w;
372 let other = graph.edge_other(eid as EdgeId, vid)?;
373 let other_us = other as usize;
374 if nd < tentative[other_us] {
375 tentative[other_us] = nd;
376 heap.push(Frontier(nd, other));
377 }
378 }
379 }
380 }
381
382 Ok(VoronoiPartition {
383 membership,
384 distances,
385 })
386}
387
388const EPSILON: f64 = 1e-10;
389
390fn cmp_epsilon(a: f64, b: f64, eps: f64) -> Ordering {
391 let diff = a - b;
392 let abs_a = a.abs();
393 let abs_b = b.abs();
394 let denom = if abs_a > abs_b { abs_a } else { abs_b };
395 if denom == 0.0 || denom.is_infinite() {
396 a.total_cmp(&b)
400 } else if diff.abs() <= eps * denom {
401 Ordering::Equal
402 } else if diff < 0.0 {
403 Ordering::Less
404 } else {
405 Ordering::Greater
406 }
407}
408
409pub fn voronoi(
459 graph: &Graph,
460 weights: Option<&[f64]>,
461 mode: DijkstraMode,
462 generators: &[VertexId],
463 tiebreaker: VoronoiTiebreaker,
464 seed: u64,
465) -> IgraphResult<VoronoiPartition> {
466 let n = graph.vcount();
467 let mode = if graph.is_directed() {
470 mode
471 } else {
472 DijkstraMode::All
473 };
474
475 validate_generators(graph, generators)?;
476 if let Some(w) = weights {
477 validate_weights(graph, w)?;
478 }
479
480 if n == 0 {
481 return Ok(VoronoiPartition {
482 membership: Vec::new(),
483 distances: Vec::new(),
484 });
485 }
486
487 if generators.is_empty() {
488 return Ok(VoronoiPartition {
490 membership: vec![None; n as usize],
491 distances: vec![f64::INFINITY; n as usize],
492 });
493 }
494
495 match weights {
496 None => voronoi_bfs(graph, generators, mode, tiebreaker, seed),
497 Some(w) => voronoi_dijkstra(graph, w, generators, mode, tiebreaker, seed),
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 fn path_n(n: u32) -> Graph {
506 let mut g = Graph::with_vertices(n);
507 for i in 0..n.saturating_sub(1) {
508 g.add_edge(i, i + 1).unwrap();
509 }
510 g
511 }
512
513 #[test]
514 fn empty_graph_returns_empty() {
515 let g = Graph::with_vertices(0);
516 let part = voronoi(
517 &g,
518 None,
519 DijkstraMode::Out,
520 &[],
521 VoronoiTiebreaker::First,
522 0,
523 )
524 .unwrap();
525 assert!(part.membership.is_empty());
526 assert!(part.distances.is_empty());
527 }
528
529 #[test]
530 fn no_generators_all_unreachable() {
531 let g = path_n(4);
532 let part = voronoi(
533 &g,
534 None,
535 DijkstraMode::Out,
536 &[],
537 VoronoiTiebreaker::First,
538 0,
539 )
540 .unwrap();
541 assert_eq!(part.membership, vec![None, None, None, None]);
542 assert!(part.distances.iter().all(|d| d.is_infinite()));
543 }
544
545 #[test]
546 fn single_generator_covers_connected_component() {
547 let g = path_n(5);
548 let part = voronoi(
549 &g,
550 None,
551 DijkstraMode::Out,
552 &[0],
553 VoronoiTiebreaker::First,
554 0,
555 )
556 .unwrap();
557 assert_eq!(
558 part.membership,
559 vec![Some(0), Some(0), Some(0), Some(0), Some(0)]
560 );
561 assert_eq!(part.distances, vec![0.0, 1.0, 2.0, 3.0, 4.0]);
562 }
563
564 #[test]
565 fn path_two_generators_first_keeps_tied_vertex_left() {
566 let g = path_n(5);
569 let part = voronoi(
570 &g,
571 None,
572 DijkstraMode::Out,
573 &[0, 4],
574 VoronoiTiebreaker::First,
575 0,
576 )
577 .unwrap();
578 assert_eq!(
579 part.membership,
580 vec![Some(0), Some(0), Some(0), Some(1), Some(1)]
581 );
582 assert_eq!(part.distances, vec![0.0, 1.0, 2.0, 1.0, 0.0]);
583 }
584
585 #[test]
586 fn path_two_generators_last_takes_tied_vertex() {
587 let g = path_n(5);
588 let part = voronoi(
589 &g,
590 None,
591 DijkstraMode::Out,
592 &[0, 4],
593 VoronoiTiebreaker::Last,
594 0,
595 )
596 .unwrap();
597 assert_eq!(
598 part.membership,
599 vec![Some(0), Some(0), Some(1), Some(1), Some(1)]
600 );
601 }
602
603 #[test]
604 fn random_tiebreaker_is_deterministic_for_fixed_seed() {
605 let g = path_n(5);
606 let part1 = voronoi(
607 &g,
608 None,
609 DijkstraMode::Out,
610 &[0, 4],
611 VoronoiTiebreaker::Random,
612 42,
613 )
614 .unwrap();
615 let part2 = voronoi(
616 &g,
617 None,
618 DijkstraMode::Out,
619 &[0, 4],
620 VoronoiTiebreaker::Random,
621 42,
622 )
623 .unwrap();
624 assert_eq!(part1, part2);
625 }
626
627 #[test]
628 fn disconnected_components_unreachable_get_none() {
629 let mut g = Graph::with_vertices(6);
631 g.add_edge(0, 1).unwrap();
632 g.add_edge(1, 2).unwrap();
633 g.add_edge(3, 4).unwrap();
634 g.add_edge(4, 5).unwrap();
635 let part = voronoi(
636 &g,
637 None,
638 DijkstraMode::Out,
639 &[0],
640 VoronoiTiebreaker::First,
641 0,
642 )
643 .unwrap();
644 assert_eq!(
645 part.membership,
646 vec![Some(0), Some(0), Some(0), None, None, None]
647 );
648 assert!(part.distances[3].is_infinite());
649 assert!(part.distances[4].is_infinite());
650 assert!(part.distances[5].is_infinite());
651 }
652
653 #[test]
654 fn weighted_dijkstra_assigns_closest() {
655 let mut g = Graph::with_vertices(3);
660 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(0, 2).unwrap(); let weights = [1.0, 2.0, 10.0];
664 let part = voronoi(
665 &g,
666 Some(&weights),
667 DijkstraMode::Out,
668 &[0, 2],
669 VoronoiTiebreaker::First,
670 0,
671 )
672 .unwrap();
673 assert_eq!(part.membership, vec![Some(0), Some(0), Some(1)]);
674 assert_eq!(part.distances, vec![0.0, 1.0, 0.0]);
675 }
676
677 #[test]
678 fn weighted_validates_negative_weight() {
679 let g = path_n(3);
680 let weights = [-1.0, 1.0];
681 let err = voronoi(
682 &g,
683 Some(&weights),
684 DijkstraMode::Out,
685 &[0],
686 VoronoiTiebreaker::First,
687 0,
688 )
689 .unwrap_err();
690 match err {
691 IgraphError::InvalidArgument(_) => (),
692 other => panic!("expected InvalidArgument, got {other:?}"),
693 }
694 }
695
696 #[test]
697 fn weighted_validates_nan_weight() {
698 let g = path_n(3);
699 let weights = [f64::NAN, 1.0];
700 let err = voronoi(
701 &g,
702 Some(&weights),
703 DijkstraMode::Out,
704 &[0],
705 VoronoiTiebreaker::First,
706 0,
707 )
708 .unwrap_err();
709 match err {
710 IgraphError::InvalidArgument(_) => (),
711 other => panic!("expected InvalidArgument, got {other:?}"),
712 }
713 }
714
715 #[test]
716 fn weighted_rejects_wrong_length_weight() {
717 let g = path_n(3);
718 let weights = [1.0, 1.0, 1.0]; let err = voronoi(
720 &g,
721 Some(&weights),
722 DijkstraMode::Out,
723 &[0],
724 VoronoiTiebreaker::First,
725 0,
726 )
727 .unwrap_err();
728 match err {
729 IgraphError::InvalidArgument(_) => (),
730 other => panic!("expected InvalidArgument, got {other:?}"),
731 }
732 }
733
734 #[test]
735 fn generator_out_of_range_errors() {
736 let g = path_n(3);
737 let err = voronoi(
738 &g,
739 None,
740 DijkstraMode::Out,
741 &[0, 5],
742 VoronoiTiebreaker::First,
743 0,
744 )
745 .unwrap_err();
746 match err {
747 IgraphError::InvalidArgument(_) => (),
748 other => panic!("expected InvalidArgument, got {other:?}"),
749 }
750 }
751
752 #[test]
753 fn all_vertices_as_generators_each_is_its_own_cell() {
754 let g = path_n(4);
755 let gens: Vec<VertexId> = (0..4).collect();
756 let part = voronoi(
757 &g,
758 None,
759 DijkstraMode::Out,
760 &gens,
761 VoronoiTiebreaker::First,
762 0,
763 )
764 .unwrap();
765 assert_eq!(part.membership, vec![Some(0), Some(1), Some(2), Some(3)]);
766 assert_eq!(part.distances, vec![0.0, 0.0, 0.0, 0.0]);
767 }
768
769 #[test]
770 fn directed_mode_out_vs_in() {
771 let mut g = Graph::new(3, true).unwrap();
775 g.add_edge(0, 1).unwrap();
776 g.add_edge(1, 2).unwrap();
777 let part_out = voronoi(
778 &g,
779 None,
780 DijkstraMode::Out,
781 &[2],
782 VoronoiTiebreaker::First,
783 0,
784 )
785 .unwrap();
786 assert_eq!(part_out.membership, vec![None, None, Some(0)]);
787 let part_in = voronoi(
788 &g,
789 None,
790 DijkstraMode::In,
791 &[2],
792 VoronoiTiebreaker::First,
793 0,
794 )
795 .unwrap();
796 assert_eq!(part_in.membership, vec![Some(0), Some(0), Some(0)]);
797 assert_eq!(part_in.distances, vec![2.0, 1.0, 0.0]);
798 }
799
800 #[test]
801 fn weighted_infinite_edge_skipped() {
802 let mut g = Graph::with_vertices(3);
805 g.add_edge(0, 1).unwrap();
806 g.add_edge(1, 2).unwrap();
807 g.add_edge(0, 2).unwrap();
808 let weights = [1.0, 2.0, f64::INFINITY];
809 let part = voronoi(
810 &g,
811 Some(&weights),
812 DijkstraMode::Out,
813 &[0],
814 VoronoiTiebreaker::First,
815 0,
816 )
817 .unwrap();
818 assert_eq!(part.membership, vec![Some(0), Some(0), Some(0)]);
819 assert_eq!(part.distances, vec![0.0, 1.0, 3.0]);
820 }
821}
822
823#[cfg(all(test, feature = "proptest-harness"))]
824mod proptests {
825 use super::*;
826 use crate::algorithms::paths::distances::distances;
827 use proptest::prelude::*;
828
829 prop_compose! {
830 fn small_undirected_graph()(n in 2u32..=10u32, edges_seed in any::<u64>()) -> Graph {
831 let mut g = Graph::with_vertices(n);
832 let mut rng = edges_seed;
833 let target_m = ((n * (n - 1)) / 2).min(n + 6) as usize;
834 let mut added = 0usize;
835 let mut guard = 0usize;
836 while added < target_m && guard < target_m * 8 + 4 {
837 rng = rng
838 .wrapping_mul(6_364_136_223_846_793_005)
839 .wrapping_add(1_442_695_040_888_963_407);
840 let u = ((rng >> 33) % u64::from(n)) as u32;
841 rng = rng
842 .wrapping_mul(6_364_136_223_846_793_005)
843 .wrapping_add(1_442_695_040_888_963_407);
844 let v = ((rng >> 33) % u64::from(n)) as u32;
845 guard += 1;
846 if u == v {
847 continue;
848 }
849 if g.add_edge(u, v).is_ok() {
850 added += 1;
851 }
852 }
853 g
854 }
855 }
856
857 proptest! {
858 #[test]
859 fn membership_indices_in_range(
860 g in small_undirected_graph(),
861 seed in any::<u64>(),
862 ) {
863 let n = g.vcount();
864 let k = (n as usize).div_ceil(3).max(1);
865 let gens: Vec<VertexId> = (0..k as u32).collect();
866 let part = voronoi(
867 &g, None, DijkstraMode::Out, &gens,
868 VoronoiTiebreaker::Random, seed,
869 ).unwrap();
870 prop_assert_eq!(part.membership.len(), n as usize);
871 for i in part.membership.iter().flatten() {
872 prop_assert!((*i as usize) < gens.len());
873 }
874 }
875
876 #[test]
877 fn unweighted_distance_matches_min_over_generators(
878 g in small_undirected_graph(),
879 seed in any::<u64>(),
880 ) {
881 let n = g.vcount();
882 let k = ((n as usize) / 2).max(1);
883 let gens: Vec<VertexId> = (0..k as u32).collect();
884 let part = voronoi(
885 &g, None, DijkstraMode::Out, &gens,
886 VoronoiTiebreaker::First, seed,
887 ).unwrap();
888 for v in 0..n {
889 let mut best = u32::MAX;
890 for &gid in &gens {
891 let d = distances(&g, gid).unwrap();
892 if let Some(dv) = d[v as usize] {
893 if dv < best {
894 best = dv;
895 }
896 }
897 }
898 if best == u32::MAX {
899 prop_assert!(part.distances[v as usize].is_infinite());
900 prop_assert!(part.membership[v as usize].is_none());
901 } else {
902 let got = part.distances[v as usize];
903 prop_assert!((got - f64::from(best)).abs() < 1e-9);
904 prop_assert!(part.membership[v as usize].is_some());
905 }
906 }
907 }
908
909 #[test]
910 fn tiebreaker_first_picks_earliest_at_ties(
911 g in small_undirected_graph(),
912 seed in any::<u64>(),
913 ) {
914 let n = g.vcount();
915 let k = ((n as usize) / 2).max(2);
916 let gens: Vec<VertexId> = (0..k as u32).collect();
917 let part = voronoi(
918 &g, None, DijkstraMode::Out, &gens,
919 VoronoiTiebreaker::First, seed,
920 ).unwrap();
921 for v in 0..n {
922 let mind = part.distances[v as usize];
923 if mind.is_infinite() {
924 continue;
925 }
926 let assigned = part.membership[v as usize].unwrap();
927 for (i, &gid) in gens.iter().enumerate() {
928 if i as u32 >= assigned {
929 break;
930 }
931 let d = distances(&g, gid).unwrap();
932 if let Some(dv) = d[v as usize] {
933 prop_assert!(f64::from(dv) > mind);
934 }
935 }
936 }
937 }
938 }
939}