1use crate::core::{Graph, IgraphError, IgraphResult};
18
19pub fn delaunay_graph(points: &[Vec<f64>]) -> IgraphResult<Graph> {
69 let n = points.len();
70 if n == 0 {
71 return Ok(Graph::with_vertices(0));
72 }
73
74 let dim = points[0].len();
75 if dim == 0 {
76 return Err(IgraphError::InvalidArgument(
77 "delaunay_graph: points must not be zero-dimensional".into(),
78 ));
79 }
80 for (i, row) in points.iter().enumerate() {
81 if row.len() != dim {
82 return Err(IgraphError::InvalidArgument(format!(
83 "delaunay_graph: point row {i} has dimension {} but expected {dim}",
84 row.len()
85 )));
86 }
87 for (j, &c) in row.iter().enumerate() {
88 if c.is_nan() || c.is_infinite() {
89 return Err(IgraphError::InvalidArgument(format!(
90 "delaunay_graph: coordinate [{i}][{j}] is not finite"
91 )));
92 }
93 }
94 }
95
96 if n == 1 {
97 return Ok(Graph::with_vertices(1));
98 }
99
100 match dim {
101 1 => delaunay_1d(points),
102 2 => {
103 if n == 2 {
104 let n_u32 = u32::try_from(n).map_err(|_| {
105 IgraphError::InvalidArgument("delaunay_graph: too many points".into())
106 })?;
107 let mut g = Graph::with_vertices(n_u32);
108 g.add_edge(0, 1)?;
109 return Ok(g);
110 }
111 delaunay_2d(points)
112 }
113 _ => Err(IgraphError::InvalidArgument(format!(
114 "delaunay_graph: {dim}D not supported (only 1D and 2D)"
115 ))),
116 }
117}
118
119fn delaunay_1d(points: &[Vec<f64>]) -> IgraphResult<Graph> {
120 let n = points.len();
121 let mut order: Vec<usize> = (0..n).collect();
122 order.sort_by(|&a, &b| {
123 points[a][0]
124 .partial_cmp(&points[b][0])
125 .unwrap_or(std::cmp::Ordering::Equal)
126 });
127
128 let n_u32 = u32::try_from(n)
129 .map_err(|_| IgraphError::InvalidArgument("delaunay_graph: too many points".into()))?;
130 let mut g = Graph::with_vertices(n_u32);
131
132 for w in order.windows(2) {
133 let (a, b) = (w[0], w[1]);
134 if (points[a][0] - points[b][0]).abs() < f64::EPSILON * 128.0 {
135 return Err(IgraphError::InvalidArgument(
136 "delaunay_graph: duplicate points".into(),
137 ));
138 }
139 #[allow(clippy::cast_possible_truncation)]
140 g.add_edge(a as u32, b as u32)?;
141 }
142 Ok(g)
143}
144
145fn is_collinear(points: &[Vec<f64>]) -> bool {
146 if points.len() < 3 {
147 return true;
148 }
149 let (x0, y0) = (points[0][0], points[0][1]);
150 let (x1, y1) = (points[1][0], points[1][1]);
151 let (dx, dy) = (x1 - x0, y1 - y0);
152 for p in points.iter().skip(2) {
153 let cross = dx * (p[1] - y0) - dy * (p[0] - x0);
154 if cross.abs() > 1e-10 {
155 return false;
156 }
157 }
158 true
159}
160
161fn delaunay_collinear(points: &[Vec<f64>]) -> IgraphResult<Graph> {
162 let n = points.len();
163 let n_u32 = u32::try_from(n)
164 .map_err(|_| IgraphError::InvalidArgument("delaunay_graph: too many points".into()))?;
165
166 let (x0, y0) = (points[0][0], points[0][1]);
168 let (x1, y1) = (points[1][0], points[1][1]);
169 let (dx, dy) = (x1 - x0, y1 - y0);
170 let len = (dx * dx + dy * dy).sqrt();
171 let (ux, uy) = if len > 1e-15 {
172 (dx / len, dy / len)
173 } else {
174 (1.0, 0.0)
175 };
176
177 let mut order: Vec<usize> = (0..n).collect();
178 order.sort_by(|&a, &b| {
179 let pa = ux * points[a][0] + uy * points[a][1];
180 let pb = ux * points[b][0] + uy * points[b][1];
181 pa.partial_cmp(&pb).unwrap_or(std::cmp::Ordering::Equal)
182 });
183
184 let mut g = Graph::with_vertices(n_u32);
185 for w in order.windows(2) {
186 #[allow(clippy::cast_possible_truncation)]
187 g.add_edge(w[0] as u32, w[1] as u32)?;
188 }
189 Ok(g)
190}
191
192#[derive(Clone, Copy)]
195struct Pt {
196 x: f64,
197 y: f64,
198}
199
200#[derive(Clone, Copy)]
201struct Triangle {
202 v: [usize; 3],
203 alive: bool,
204}
205
206struct Circumcircle {
207 cx: f64,
208 cy: f64,
209 r2: f64,
210}
211
212fn circumcircle(a: Pt, b: Pt, c: Pt) -> Circumcircle {
213 let ax = a.x - c.x;
214 let ay = a.y - c.y;
215 let bx = b.x - c.x;
216 let by = b.y - c.y;
217 let d = 2.0 * (ax * by - ay * bx);
218 if d.abs() < 1e-30 {
219 return Circumcircle {
220 cx: f64::INFINITY,
221 cy: f64::INFINITY,
222 r2: f64::INFINITY,
223 };
224 }
225 let a2 = ax * ax + ay * ay;
226 let b2 = bx * bx + by * by;
227 let ux = (a2 * by - b2 * ay) / d;
228 let uy = (ax * b2 - bx * a2) / d;
229 Circumcircle {
230 cx: ux + c.x,
231 cy: uy + c.y,
232 r2: ux * ux + uy * uy,
233 }
234}
235
236fn in_circumcircle(cc: &Circumcircle, p: Pt) -> bool {
237 if cc.r2.is_infinite() {
238 return false;
239 }
240 let dx = p.x - cc.cx;
241 let dy = p.y - cc.cy;
242 dx * dx + dy * dy < cc.r2
243}
244
245fn check_duplicate_2d(points: &[Vec<f64>]) -> IgraphResult<()> {
246 let n = points.len();
247 let mut sorted_indices: Vec<usize> = (0..n).collect();
248 sorted_indices.sort_by(|&a, &b| {
249 points[a][0]
250 .partial_cmp(&points[b][0])
251 .unwrap_or(std::cmp::Ordering::Equal)
252 .then_with(|| {
253 points[a][1]
254 .partial_cmp(&points[b][1])
255 .unwrap_or(std::cmp::Ordering::Equal)
256 })
257 });
258 for w in sorted_indices.windows(2) {
259 let (a, b) = (w[0], w[1]);
260 if (points[a][0] - points[b][0]).abs() < 1e-12
261 && (points[a][1] - points[b][1]).abs() < 1e-12
262 {
263 return Err(IgraphError::InvalidArgument(
264 "delaunay_graph: duplicate points".into(),
265 ));
266 }
267 }
268 Ok(())
269}
270
271#[allow(unknown_lints, clippy::manual_midpoint)]
272fn super_triangle_setup(pts: &[Pt], n: usize) -> (Vec<Pt>, Vec<Triangle>, Vec<Circumcircle>) {
273 let mut lo_x = f64::INFINITY;
274 let mut hi_x = f64::NEG_INFINITY;
275 let mut lo_y = f64::INFINITY;
276 let mut hi_y = f64::NEG_INFINITY;
277 for p in pts {
278 lo_x = lo_x.min(p.x);
279 hi_x = hi_x.max(p.x);
280 lo_y = lo_y.min(p.y);
281 hi_y = hi_y.max(p.y);
282 }
283 let cx = (lo_x + hi_x) / 2.0;
284 let cy = (lo_y + hi_y) / 2.0;
285
286 let big_r = if n <= 20 {
287 let mut max_r = 1.0_f64;
288 for i in 0..n {
289 for j in (i + 1)..n {
290 for k in (j + 1)..n {
291 let cc = circumcircle(pts[i], pts[j], pts[k]);
292 if !cc.r2.is_infinite() {
293 max_r = max_r.max(cc.r2.sqrt());
294 }
295 }
296 }
297 }
298 3.0 * max_r
299 } else {
300 let mut max_d2 = 1.0_f64;
301 for i in 0..n {
302 for j in (i + 1)..n {
303 let dx = pts[i].x - pts[j].x;
304 let dy = pts[i].y - pts[j].y;
305 max_d2 = max_d2.max(dx * dx + dy * dy);
306 }
307 }
308 100.0 * max_d2.sqrt()
309 };
310
311 let sqrt3 = 3.0_f64.sqrt();
312 let sp = [
313 Pt {
314 x: cx - big_r * sqrt3,
315 y: cy - big_r,
316 },
317 Pt {
318 x: cx + big_r * sqrt3,
319 y: cy - big_r,
320 },
321 Pt {
322 x: cx,
323 y: cy + 2.0 * big_r,
324 },
325 ];
326 let mut all_pts: Vec<Pt> = pts.to_vec();
327 all_pts.extend_from_slice(&sp);
328 let tri = Triangle {
329 v: [n, n + 1, n + 2],
330 alive: true,
331 };
332 let cc = circumcircle(sp[0], sp[1], sp[2]);
333 (all_pts, vec![tri], vec![cc])
334}
335
336fn bowyer_watson_insert(
337 i: usize,
338 all_pts: &[Pt],
339 triangles: &mut Vec<Triangle>,
340 circles: &mut Vec<Circumcircle>,
341) {
342 let p = all_pts[i];
343 let mut bad = Vec::new();
344 for (ti, tri) in triangles.iter().enumerate() {
345 if tri.alive && in_circumcircle(&circles[ti], p) {
346 bad.push(ti);
347 }
348 }
349
350 let mut boundary: Vec<[usize; 2]> = Vec::new();
351 for &ti in &bad {
352 let tri = &triangles[ti];
353 for edge_idx in 0..3 {
354 let e = [tri.v[edge_idx], tri.v[(edge_idx + 1) % 3]];
355 let shared = bad
356 .iter()
357 .any(|&oti| oti != ti && edge_in_triangle(e, &triangles[oti]));
358 if !shared {
359 boundary.push(e);
360 }
361 }
362 }
363
364 for &ti in &bad {
365 triangles[ti].alive = false;
366 }
367
368 for e in &boundary {
369 let new_tri = Triangle {
370 v: [i, e[0], e[1]],
371 alive: true,
372 };
373 let cc = circumcircle(all_pts[i], all_pts[e[0]], all_pts[e[1]]);
374 triangles.push(new_tri);
375 circles.push(cc);
376 }
377}
378
379fn collect_delaunay_edges(triangles: &[Triangle], n: usize) -> IgraphResult<Graph> {
380 let mut edge_set = std::collections::BTreeSet::new();
381 for tri in triangles {
382 if !tri.alive || tri.v[0] >= n || tri.v[1] >= n || tri.v[2] >= n {
383 continue;
384 }
385 for i in 0..3 {
386 let (a, b) = (tri.v[i], tri.v[(i + 1) % 3]);
387 let edge = if a < b { (a, b) } else { (b, a) };
388 edge_set.insert(edge);
389 }
390 }
391
392 let n_u32 = u32::try_from(n)
393 .map_err(|_| IgraphError::InvalidArgument("delaunay_graph: too many points".into()))?;
394 let mut g = Graph::with_vertices(n_u32);
395 for (a, b) in &edge_set {
396 #[allow(clippy::cast_possible_truncation)]
397 g.add_edge(*a as u32, *b as u32)?;
398 }
399 Ok(g)
400}
401
402fn delaunay_2d(points: &[Vec<f64>]) -> IgraphResult<Graph> {
403 let n = points.len();
404 check_duplicate_2d(points)?;
405
406 if is_collinear(points) {
407 return delaunay_collinear(points);
408 }
409
410 let pts: Vec<Pt> = points.iter().map(|p| Pt { x: p[0], y: p[1] }).collect();
411 let (all_pts, mut triangles, mut circles) = super_triangle_setup(&pts, n);
412
413 for i in 0..n {
414 bowyer_watson_insert(i, &all_pts, &mut triangles, &mut circles);
415 }
416
417 collect_delaunay_edges(&triangles, n)
418}
419
420fn edge_in_triangle(e: [usize; 2], tri: &Triangle) -> bool {
421 for i in 0..3 {
422 let te = [tri.v[i], tri.v[(i + 1) % 3]];
423 if (te[0] == e[0] && te[1] == e[1]) || (te[0] == e[1] && te[1] == e[0]) {
424 return true;
425 }
426 }
427 false
428}
429
430#[cfg(test)]
431#[allow(clippy::float_cmp)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn empty_points() {
437 let g = delaunay_graph(&[]).unwrap();
438 assert_eq!(g.vcount(), 0);
439 assert_eq!(g.ecount(), 0);
440 }
441
442 #[test]
443 fn single_point() {
444 let g = delaunay_graph(&[vec![1.0, 2.0]]).unwrap();
445 assert_eq!(g.vcount(), 1);
446 assert_eq!(g.ecount(), 0);
447 }
448
449 #[test]
450 fn two_points_2d() {
451 let pts = vec![vec![0.0, 0.0], vec![1.0, 0.0]];
452 let g = delaunay_graph(&pts).unwrap();
453 assert_eq!(g.vcount(), 2);
454 assert_eq!(g.ecount(), 1);
455 }
456
457 #[test]
458 fn triangle_2d() {
459 let pts = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![0.5, 1.0]];
460 let g = delaunay_graph(&pts).unwrap();
461 assert_eq!(g.vcount(), 3);
462 assert_eq!(g.ecount(), 3);
463 }
464
465 #[test]
466 fn square_2d_has_five_edges() {
467 let pts = vec![
471 vec![0.0, 0.0],
472 vec![1.0, 0.0],
473 vec![1.0, 1.0],
474 vec![0.0, 1.0],
475 ];
476 let g = delaunay_graph(&pts).unwrap();
477 assert_eq!(g.vcount(), 4);
478 assert_eq!(g.ecount(), 5);
479 }
480
481 #[test]
482 fn regular_pentagon_2d() {
483 let pts: Vec<Vec<f64>> = (0..5)
484 .map(|i| {
485 let angle = 2.0 * std::f64::consts::PI * f64::from(i) / 5.0;
486 vec![angle.cos(), angle.sin()]
487 })
488 .collect();
489 let g = delaunay_graph(&pts).unwrap();
490 assert_eq!(g.vcount(), 5);
491 assert_eq!(g.ecount(), 7);
495 }
496
497 #[test]
498 fn line_1d() {
499 let pts = vec![vec![3.0], vec![1.0], vec![4.0], vec![2.0]];
500 let g = delaunay_graph(&pts).unwrap();
501 assert_eq!(g.vcount(), 4);
502 assert_eq!(g.ecount(), 3);
503 assert!(g.get_eid(1, 3).is_ok());
506 assert!(g.get_eid(0, 3).is_ok() || g.get_eid(3, 0).is_ok());
507 assert!(g.get_eid(0, 2).is_ok() || g.get_eid(2, 0).is_ok());
508 }
509
510 #[test]
511 fn single_point_1d() {
512 let g = delaunay_graph(&[vec![5.0]]).unwrap();
513 assert_eq!(g.vcount(), 1);
514 assert_eq!(g.ecount(), 0);
515 }
516
517 #[test]
518 fn two_points_1d() {
519 let g = delaunay_graph(&[vec![2.0], vec![7.0]]).unwrap();
520 assert_eq!(g.vcount(), 2);
521 assert_eq!(g.ecount(), 1);
522 }
523
524 #[test]
525 fn duplicate_points_1d_error() {
526 let pts = vec![vec![1.0], vec![2.0], vec![1.0]];
527 assert!(delaunay_graph(&pts).is_err());
528 }
529
530 #[test]
531 fn duplicate_points_2d_error() {
532 let pts = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![1.0, 2.0]];
533 assert!(delaunay_graph(&pts).is_err());
534 }
535
536 #[test]
537 fn zero_dimensional_error() {
538 let pts: Vec<Vec<f64>> = vec![vec![]];
539 assert!(delaunay_graph(&pts).is_err());
540 }
541
542 #[test]
543 fn nan_coordinate_error() {
544 let pts = vec![vec![0.0, f64::NAN]];
545 assert!(delaunay_graph(&pts).is_err());
546 }
547
548 #[test]
549 fn infinite_coordinate_error() {
550 let pts = vec![vec![f64::INFINITY, 0.0]];
551 assert!(delaunay_graph(&pts).is_err());
552 }
553
554 #[test]
555 fn three_d_unsupported() {
556 let pts = vec![vec![0.0, 0.0, 0.0], vec![1.0, 0.0, 0.0]];
557 assert!(delaunay_graph(&pts).is_err());
558 }
559
560 #[test]
561 fn collinear_2d() {
562 let pts = vec![vec![0.0, 0.0], vec![1.0, 1.0], vec![2.0, 2.0]];
565 let g = delaunay_graph(&pts).unwrap();
566 assert_eq!(g.vcount(), 3);
567 assert_eq!(g.ecount(), 2);
568 }
569
570 #[test]
571 fn grid_3x3() {
572 let mut pts = Vec::new();
579 for i in 0..3 {
580 for j in 0..3 {
581 pts.push(vec![f64::from(i), f64::from(j)]);
582 }
583 }
584 let g = delaunay_graph(&pts).unwrap();
585 assert_eq!(g.vcount(), 9);
586 assert_eq!(g.ecount(), 16);
589 }
590
591 #[test]
592 fn cocircular_points() {
593 let pts = vec![
596 vec![0.0, 0.0],
597 vec![1.0, 0.0],
598 vec![1.0, 1.0],
599 vec![0.0, 1.0],
600 ];
601 let g = delaunay_graph(&pts).unwrap();
602 assert_eq!(g.vcount(), 4);
603 assert_eq!(g.ecount(), 5);
604 }
605}
606
607#[cfg(all(test, feature = "proptest-harness"))]
608mod proptests {
609 use super::*;
610 use proptest::prelude::*;
611
612 fn arb_points_2d(max_n: usize) -> impl Strategy<Value = Vec<Vec<f64>>> {
613 (3..=max_n).prop_flat_map(|n| {
614 proptest::collection::vec(proptest::collection::vec(-100.0_f64..100.0, 2..=2), n..=n)
615 })
616 }
617
618 proptest! {
619 #[test]
625 fn edge_count_in_planar_range(pts in arb_points_2d(8)) {
626 let mut sorted: Vec<(usize, &Vec<f64>)> = pts.iter().enumerate().collect();
628 sorted.sort_by(|(_, a), (_, b)| {
629 a[0].partial_cmp(&b[0]).unwrap()
630 .then_with(|| a[1].partial_cmp(&b[1]).unwrap())
631 });
632 for w in sorted.windows(2) {
633 let (_, a) = w[0];
634 let (_, b) = w[1];
635 if (a[0] - b[0]).abs() < 1e-10 && (a[1] - b[1]).abs() < 1e-10 {
636 return Ok(());
637 }
638 }
639
640 let g = delaunay_graph(&pts).unwrap();
641 let n = pts.len();
642 prop_assert_eq!(g.vcount() as usize, n);
643 if n >= 3 {
645 prop_assert!(g.ecount() as usize <= 3 * n - 6,
646 "too many edges: {} > {}", g.ecount(), 3 * n - 6);
647 }
648 prop_assert!(g.ecount() as usize >= n - 1,
650 "too few edges: {} < {}", g.ecount(), n - 1);
651 }
652 }
653}