Skip to main content

rust_robotics_planning/
voronoi_road_map.rs

1#![allow(dead_code)]
2
3//! Voronoi Road-Map path planning
4//!
5//! Constructs a roadmap from Voronoi vertices of obstacle configurations
6//! and searches for a path using Dijkstra's algorithm.
7
8use std::cmp::Ordering;
9use std::collections::{BinaryHeap, HashMap};
10
11// Parameters
12const N_KNN: usize = 10;
13const MAX_EDGE_LEN: f64 = 30.0;
14
15/// Node for Dijkstra search
16#[derive(Clone)]
17struct Node {
18    x: f64,
19    y: f64,
20    cost: f64,
21    parent: Option<usize>,
22}
23
24impl Node {
25    fn new(x: f64, y: f64) -> Self {
26        Node {
27            x,
28            y,
29            cost: f64::INFINITY,
30            parent: None,
31        }
32    }
33}
34
35/// Priority queue item for Dijkstra
36#[derive(Clone)]
37struct QueueItem {
38    cost: f64,
39    index: usize,
40}
41
42impl PartialEq for QueueItem {
43    fn eq(&self, other: &Self) -> bool {
44        self.cost == other.cost
45    }
46}
47
48impl Eq for QueueItem {}
49
50impl Ord for QueueItem {
51    fn cmp(&self, other: &Self) -> Ordering {
52        other
53            .cost
54            .partial_cmp(&self.cost)
55            .unwrap_or(Ordering::Equal)
56    }
57}
58
59impl PartialOrd for QueueItem {
60    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
61        Some(self.cmp(other))
62    }
63}
64
65/// Simple KDTree for nearest neighbor search
66struct KDTree {
67    points: Vec<(f64, f64)>,
68}
69
70impl KDTree {
71    fn new(points: Vec<(f64, f64)>) -> Self {
72        KDTree { points }
73    }
74
75    fn query_knn(&self, x: f64, y: f64, k: usize) -> Vec<(usize, f64)> {
76        let mut distances: Vec<(usize, f64)> = self
77            .points
78            .iter()
79            .enumerate()
80            .map(|(i, (px, py))| {
81                let d = ((x - px).powi(2) + (y - py).powi(2)).sqrt();
82                (i, d)
83            })
84            .collect();
85
86        distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
87        distances.truncate(k + 1);
88        distances
89    }
90
91    fn min_distance(&self, x: f64, y: f64) -> f64 {
92        self.points
93            .iter()
94            .map(|(px, py)| ((x - px).powi(2) + (y - py).powi(2)).sqrt())
95            .fold(f64::INFINITY, f64::min)
96    }
97}
98
99/// Compute circumcenter of three points
100fn circumcenter(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Option<(f64, f64)> {
101    let d = 2.0 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));
102
103    if d.abs() < 1e-10 {
104        return None;
105    }
106
107    let ux = ((x1 * x1 + y1 * y1) * (y2 - y3)
108        + (x2 * x2 + y2 * y2) * (y3 - y1)
109        + (x3 * x3 + y3 * y3) * (y1 - y2))
110        / d;
111    let uy = ((x1 * x1 + y1 * y1) * (x3 - x2)
112        + (x2 * x2 + y2 * y2) * (x1 - x3)
113        + (x3 * x3 + y3 * y3) * (x2 - x1))
114        / d;
115
116    Some((ux, uy))
117}
118
119/// Simple Voronoi diagram computation
120fn compute_voronoi_vertices(
121    ox: &[f64],
122    oy: &[f64],
123    min_x: f64,
124    max_x: f64,
125    min_y: f64,
126    max_y: f64,
127    robot_radius: f64,
128) -> Vec<(f64, f64)> {
129    let mut vertices = Vec::new();
130    let n = ox.len();
131
132    if n < 3 {
133        return vertices;
134    }
135
136    for i in 0..n {
137        for j in (i + 1)..n {
138            let mx = (ox[i] + ox[j]) / 2.0;
139            let my = (oy[i] + oy[j]) / 2.0;
140
141            let dist_i = ((mx - ox[i]).powi(2) + (my - oy[i]).powi(2)).sqrt();
142            let dist_j = ((mx - ox[j]).powi(2) + (my - oy[j]).powi(2)).sqrt();
143
144            if dist_i > robot_radius * 1.5
145                && dist_j > robot_radius * 1.5
146                && mx > min_x
147                && mx < max_x
148                && my > min_y
149                && my < max_y
150            {
151                vertices.push((mx, my));
152            }
153        }
154    }
155
156    for i in 0..n {
157        for j in (i + 1)..n {
158            for k in (j + 1)..n {
159                if let Some((cx, cy)) = circumcenter(ox[i], oy[i], ox[j], oy[j], ox[k], oy[k]) {
160                    if cx > min_x && cx < max_x && cy > min_y && cy < max_y {
161                        let min_dist = ox
162                            .iter()
163                            .zip(oy.iter())
164                            .map(|(&px, &py)| ((cx - px).powi(2) + (cy - py).powi(2)).sqrt())
165                            .fold(f64::INFINITY, f64::min);
166
167                        if min_dist > robot_radius * 1.5 {
168                            vertices.push((cx, cy));
169                        }
170                    }
171                }
172            }
173        }
174    }
175
176    let mut unique_vertices = Vec::new();
177    for (x, y) in vertices {
178        let is_dup = unique_vertices
179            .iter()
180            .any(|(vx, vy): &(f64, f64)| ((x - vx).powi(2) + (y - vy).powi(2)).sqrt() < 1.0);
181        if !is_dup {
182            unique_vertices.push((x, y));
183        }
184    }
185
186    unique_vertices
187}
188
189/// Voronoi Road-Map Planner
190pub struct VoronoiPlanner {
191    sample_x: Vec<f64>,
192    sample_y: Vec<f64>,
193    road_map: Vec<Vec<usize>>,
194}
195
196impl VoronoiPlanner {
197    pub fn new(
198        ox: &[f64],
199        oy: &[f64],
200        start: (f64, f64),
201        goal: (f64, f64),
202        robot_radius: f64,
203    ) -> Self {
204        let obstacle_tree = KDTree::new(ox.iter().zip(oy.iter()).map(|(&x, &y)| (x, y)).collect());
205
206        let min_x = ox.iter().cloned().fold(f64::INFINITY, f64::min);
207        let max_x = ox.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
208        let min_y = oy.iter().cloned().fold(f64::INFINITY, f64::min);
209        let max_y = oy.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
210
211        let voronoi_vertices =
212            compute_voronoi_vertices(ox, oy, min_x, max_x, min_y, max_y, robot_radius);
213
214        let mut sample_x: Vec<f64> = voronoi_vertices.iter().map(|(x, _)| *x).collect();
215        let mut sample_y: Vec<f64> = voronoi_vertices.iter().map(|(_, y)| *y).collect();
216
217        sample_x.push(start.0);
218        sample_y.push(start.1);
219        sample_x.push(goal.0);
220        sample_y.push(goal.1);
221
222        let road_map = Self::generate_road_map(&sample_x, &sample_y, robot_radius, &obstacle_tree);
223
224        VoronoiPlanner {
225            sample_x,
226            sample_y,
227            road_map,
228        }
229    }
230
231    fn generate_road_map(
232        sample_x: &[f64],
233        sample_y: &[f64],
234        robot_radius: f64,
235        obstacle_tree: &KDTree,
236    ) -> Vec<Vec<usize>> {
237        let sample_tree = KDTree::new(
238            sample_x
239                .iter()
240                .zip(sample_y.iter())
241                .map(|(&x, &y)| (x, y))
242                .collect(),
243        );
244
245        let mut road_map: Vec<Vec<usize>> = vec![Vec::new(); sample_x.len()];
246
247        for (i, (&x, &y)) in sample_x.iter().zip(sample_y.iter()).enumerate() {
248            let neighbors = sample_tree.query_knn(x, y, N_KNN);
249
250            for (j, dist) in neighbors {
251                if i == j {
252                    continue;
253                }
254
255                if dist > MAX_EDGE_LEN {
256                    continue;
257                }
258
259                if !Self::is_collision(x, y, sample_x[j], sample_y[j], robot_radius, obstacle_tree)
260                {
261                    road_map[i].push(j);
262                }
263            }
264        }
265
266        road_map
267    }
268
269    fn is_collision(
270        x1: f64,
271        y1: f64,
272        x2: f64,
273        y2: f64,
274        robot_radius: f64,
275        obstacle_tree: &KDTree,
276    ) -> bool {
277        let dx = x2 - x1;
278        let dy = y2 - y1;
279        let d = (dx * dx + dy * dy).sqrt();
280
281        if d == 0.0 {
282            return false;
283        }
284
285        let step = robot_radius;
286        let n_steps = (d / step).ceil() as usize;
287
288        for i in 0..=n_steps {
289            let t = i as f64 / n_steps as f64;
290            let x = x1 + t * dx;
291            let y = y1 + t * dy;
292
293            let min_dist = obstacle_tree.min_distance(x, y);
294            if min_dist <= robot_radius {
295                return true;
296            }
297        }
298
299        false
300    }
301
302    pub fn plan(&self) -> Option<(Vec<f64>, Vec<f64>)> {
303        let n = self.sample_x.len();
304        if n < 2 {
305            return None;
306        }
307
308        let start_idx = n - 2;
309        let goal_idx = n - 1;
310
311        let mut nodes: Vec<Node> = self
312            .sample_x
313            .iter()
314            .zip(self.sample_y.iter())
315            .map(|(&x, &y)| Node::new(x, y))
316            .collect();
317
318        nodes[start_idx].cost = 0.0;
319
320        let mut open_set = BinaryHeap::new();
321        open_set.push(QueueItem {
322            cost: 0.0,
323            index: start_idx,
324        });
325
326        let mut closed_set: HashMap<usize, bool> = HashMap::new();
327
328        while let Some(current) = open_set.pop() {
329            if current.index == goal_idx {
330                return Some(self.reconstruct_path(&nodes, goal_idx));
331            }
332
333            if closed_set.contains_key(&current.index) {
334                continue;
335            }
336            closed_set.insert(current.index, true);
337
338            for &neighbor_idx in &self.road_map[current.index] {
339                if closed_set.contains_key(&neighbor_idx) {
340                    continue;
341                }
342
343                let dx = nodes[neighbor_idx].x - nodes[current.index].x;
344                let dy = nodes[neighbor_idx].y - nodes[current.index].y;
345                let edge_cost = (dx * dx + dy * dy).sqrt();
346                let new_cost = nodes[current.index].cost + edge_cost;
347
348                if new_cost < nodes[neighbor_idx].cost {
349                    nodes[neighbor_idx].cost = new_cost;
350                    nodes[neighbor_idx].parent = Some(current.index);
351                    open_set.push(QueueItem {
352                        cost: new_cost,
353                        index: neighbor_idx,
354                    });
355                }
356            }
357        }
358
359        None
360    }
361
362    fn reconstruct_path(&self, nodes: &[Node], goal_idx: usize) -> (Vec<f64>, Vec<f64>) {
363        let mut path_x = Vec::new();
364        let mut path_y = Vec::new();
365
366        let mut current = goal_idx;
367        while let Some(parent) = nodes[current].parent {
368            path_x.push(nodes[current].x);
369            path_y.push(nodes[current].y);
370            current = parent;
371        }
372        path_x.push(nodes[current].x);
373        path_y.push(nodes[current].y);
374
375        path_x.reverse();
376        path_y.reverse();
377
378        (path_x, path_y)
379    }
380
381    pub fn get_samples(&self) -> (&[f64], &[f64]) {
382        (&self.sample_x, &self.sample_y)
383    }
384
385    pub fn get_edges(&self) -> Vec<((f64, f64), (f64, f64))> {
386        let mut edges = Vec::new();
387
388        for (i, neighbors) in self.road_map.iter().enumerate() {
389            for &j in neighbors {
390                if i < j {
391                    edges.push((
392                        (self.sample_x[i], self.sample_y[i]),
393                        (self.sample_x[j], self.sample_y[j]),
394                    ));
395                }
396            }
397        }
398
399        edges
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    const TEST_WORLD_SIZE: i32 = 24;
408    const TEST_BOUNDARY_STEP: usize = 2;
409    const TEST_START: (f64, f64) = (4.0, 4.0);
410    const TEST_GOAL: (f64, f64) = (20.0, 20.0);
411    const TEST_ROBOT_RADIUS: f64 = 2.0;
412
413    /// Build a lightweight rectangular boundary used by Voronoi planner tests.
414    fn make_boundary() -> (Vec<f64>, Vec<f64>) {
415        let mut ox = Vec::new();
416        let mut oy = Vec::new();
417
418        // bottom and top walls
419        for i in (0..=TEST_WORLD_SIZE).step_by(TEST_BOUNDARY_STEP) {
420            let v = i as f64;
421            ox.push(v);
422            oy.push(0.0);
423            ox.push(v);
424            oy.push(TEST_WORLD_SIZE as f64);
425        }
426        // left and right walls
427        for i in (0..=TEST_WORLD_SIZE).step_by(TEST_BOUNDARY_STEP) {
428            let v = i as f64;
429            ox.push(0.0);
430            oy.push(v);
431            ox.push(TEST_WORLD_SIZE as f64);
432            oy.push(v);
433        }
434
435        (ox, oy)
436    }
437
438    fn make_large_boundary() -> (Vec<f64>, Vec<f64>) {
439        let mut ox = Vec::new();
440        let mut oy = Vec::new();
441
442        for i in 0..=60 {
443            let v = i as f64;
444            ox.push(v);
445            oy.push(0.0);
446            ox.push(v);
447            oy.push(60.0);
448            ox.push(0.0);
449            oy.push(v);
450            ox.push(60.0);
451            oy.push(v);
452        }
453
454        (ox, oy)
455    }
456
457    #[test]
458    fn test_voronoi_planner_creation() {
459        let (ox, oy) = make_boundary();
460        let _planner = VoronoiPlanner::new(&ox, &oy, TEST_START, TEST_GOAL, TEST_ROBOT_RADIUS);
461    }
462
463    #[test]
464    fn test_voronoi_get_samples() {
465        let (ox, oy) = make_boundary();
466        let planner = VoronoiPlanner::new(&ox, &oy, TEST_START, TEST_GOAL, TEST_ROBOT_RADIUS);
467        let (sx, sy) = planner.get_samples();
468
469        // Must contain at least start and goal (the last two entries)
470        assert!(sx.len() >= 2);
471        assert_eq!(sx.len(), sy.len());
472
473        // Start and goal are appended at the end
474        let n = sx.len();
475        assert!((sx[n - 2] - TEST_START.0).abs() < 1e-9);
476        assert!((sy[n - 2] - TEST_START.1).abs() < 1e-9);
477        assert!((sx[n - 1] - TEST_GOAL.0).abs() < 1e-9);
478        assert!((sy[n - 1] - TEST_GOAL.1).abs() < 1e-9);
479    }
480
481    #[test]
482    fn test_voronoi_plan_simple() {
483        let (ox, oy) = make_boundary();
484        let planner = VoronoiPlanner::new(&ox, &oy, TEST_START, TEST_GOAL, TEST_ROBOT_RADIUS);
485        let result = planner.plan();
486
487        assert!(result.is_some(), "planner should find a path");
488        let (px, py) = result.unwrap();
489        assert!(px.len() >= 2);
490        assert_eq!(px.len(), py.len());
491
492        // Path should start near the configured start and end near the goal
493        assert!((px[0] - TEST_START.0).abs() < 1e-9);
494        assert!((py[0] - TEST_START.1).abs() < 1e-9);
495        assert!((*px.last().unwrap() - TEST_GOAL.0).abs() < 1e-9);
496        assert!((*py.last().unwrap() - TEST_GOAL.1).abs() < 1e-9);
497    }
498
499    #[test]
500    fn test_voronoi_get_edges() {
501        let (ox, oy) = make_boundary();
502        let planner = VoronoiPlanner::new(&ox, &oy, TEST_START, TEST_GOAL, TEST_ROBOT_RADIUS);
503        let edges = planner.get_edges();
504
505        assert!(!edges.is_empty(), "road map should contain edges");
506
507        // Every edge endpoint should be within the boundary
508        for ((x1, y1), (x2, y2)) in &edges {
509            assert!(*x1 >= 0.0 && *x1 <= TEST_WORLD_SIZE as f64);
510            assert!(*y1 >= 0.0 && *y1 <= TEST_WORLD_SIZE as f64);
511            assert!(*x2 >= 0.0 && *x2 <= TEST_WORLD_SIZE as f64);
512            assert!(*y2 >= 0.0 && *y2 <= TEST_WORLD_SIZE as f64);
513        }
514    }
515
516    #[test]
517    #[ignore = "long-running regression scenario"]
518    fn test_voronoi_plan_large_boundary() {
519        let (ox, oy) = make_large_boundary();
520        let planner = VoronoiPlanner::new(&ox, &oy, (10.0, 10.0), (50.0, 50.0), 5.0);
521        let result = planner.plan();
522
523        assert!(
524            result.is_some(),
525            "planner should find a path on the larger map"
526        );
527    }
528}