1#![allow(dead_code)]
2
3use std::cmp::Ordering;
10use std::collections::{BinaryHeap, HashMap};
11
12use rand::Rng;
13
14use rust_robotics_core::{Path2D, PathPlanner, Point2D, RoboticsError};
15
16#[derive(Debug, Clone)]
22pub struct RRGNode {
23 pub x: f64,
24 pub y: f64,
25 pub neighbors: Vec<usize>,
27}
28
29impl RRGNode {
30 fn new(x: f64, y: f64) -> Self {
31 RRGNode {
32 x,
33 y,
34 neighbors: Vec::new(),
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
41pub struct CircleObstacle {
42 pub x: f64,
43 pub y: f64,
44 pub radius: f64,
45}
46
47impl CircleObstacle {
48 pub fn new(x: f64, y: f64, radius: f64) -> Self {
49 Self { x, y, radius }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct AreaBounds {
56 pub xmin: f64,
57 pub xmax: f64,
58 pub ymin: f64,
59 pub ymax: f64,
60}
61
62impl AreaBounds {
63 pub fn new(xmin: f64, xmax: f64, ymin: f64, ymax: f64) -> Self {
64 Self {
65 xmin,
66 xmax,
67 ymin,
68 ymax,
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct RRGConfig {
76 pub expand_dis: f64,
78 pub path_resolution: f64,
80 pub max_iter: usize,
82 pub robot_radius: f64,
84 pub rewire_radius: f64,
86}
87
88impl Default for RRGConfig {
89 fn default() -> Self {
90 Self {
91 expand_dis: 3.0,
92 path_resolution: 0.5,
93 max_iter: 500,
94 robot_radius: 0.8,
95 rewire_radius: 5.0,
96 }
97 }
98}
99
100pub struct RRGPlanner {
111 config: RRGConfig,
112 obstacles: Vec<CircleObstacle>,
113 rand_area: AreaBounds,
114 nodes: Vec<RRGNode>,
116}
117
118impl RRGPlanner {
119 pub fn new(obstacles: Vec<CircleObstacle>, rand_area: AreaBounds, config: RRGConfig) -> Self {
121 Self {
122 config,
123 obstacles,
124 rand_area,
125 nodes: Vec::new(),
126 }
127 }
128
129 fn plan_internal(&mut self, start: Point2D, goal: Point2D) -> Option<Path2D> {
134 self.nodes.clear();
135 self.nodes.push(RRGNode::new(start.x, start.y)); let mut rng = rand::rng();
138
139 for _ in 0..self.config.max_iter {
140 let rx = rng.random_range(self.rand_area.xmin..=self.rand_area.xmax);
142 let ry = rng.random_range(self.rand_area.ymin..=self.rand_area.ymax);
143
144 let nearest_idx = self.nearest_index(rx, ry);
146 let (nx, ny) = self.steer(nearest_idx, rx, ry);
147
148 if !self.segment_collision_free(
150 self.nodes[nearest_idx].x,
151 self.nodes[nearest_idx].y,
152 nx,
153 ny,
154 ) {
155 continue;
156 }
157
158 let new_idx = self.nodes.len();
159 self.nodes.push(RRGNode::new(nx, ny));
160
161 let near = self.nodes_within_radius(new_idx);
163 for ni in near {
164 let (ax, ay) = (self.nodes[new_idx].x, self.nodes[new_idx].y);
165 let (bx, by) = (self.nodes[ni].x, self.nodes[ni].y);
166 if self.segment_collision_free(ax, ay, bx, by) {
167 self.nodes[new_idx].neighbors.push(ni);
169 self.nodes[ni].neighbors.push(new_idx);
170 }
171 }
172 }
173
174 let goal_idx = self.attach_goal(goal.x, goal.y)?;
176
177 self.dijkstra(0, goal_idx, start, goal)
179 }
180
181 fn steer(&self, from_idx: usize, tx: f64, ty: f64) -> (f64, f64) {
183 let fx = self.nodes[from_idx].x;
184 let fy = self.nodes[from_idx].y;
185 let dx = tx - fx;
186 let dy = ty - fy;
187 let dist = (dx * dx + dy * dy).sqrt();
188 if dist <= self.config.expand_dis {
189 (tx, ty)
190 } else {
191 let scale = self.config.expand_dis / dist;
192 (fx + dx * scale, fy + dy * scale)
193 }
194 }
195
196 fn nearest_index(&self, x: f64, y: f64) -> usize {
197 self.nodes
198 .iter()
199 .enumerate()
200 .min_by(|(_, a), (_, b)| {
201 let da = (a.x - x).powi(2) + (a.y - y).powi(2);
202 let db = (b.x - x).powi(2) + (b.y - y).powi(2);
203 da.partial_cmp(&db).unwrap_or(Ordering::Equal)
204 })
205 .map(|(i, _)| i)
206 .unwrap_or(0)
207 }
208
209 fn nodes_within_radius(&self, exclude: usize) -> Vec<usize> {
211 let r2 = self.config.rewire_radius.powi(2);
212 let nx = self.nodes[exclude].x;
213 let ny = self.nodes[exclude].y;
214 self.nodes
215 .iter()
216 .enumerate()
217 .filter(|(i, node)| {
218 if *i == exclude {
219 return false;
220 }
221 let dx = node.x - nx;
222 let dy = node.y - ny;
223 dx * dx + dy * dy <= r2
224 })
225 .map(|(i, _)| i)
226 .collect()
227 }
228
229 fn segment_collision_free(&self, x1: f64, y1: f64, x2: f64, y2: f64) -> bool {
231 let dx = x2 - x1;
232 let dy = y2 - y1;
233 let dist = (dx * dx + dy * dy).sqrt();
234 let steps = ((dist / self.config.path_resolution).ceil() as usize).max(1);
235
236 for i in 0..=steps {
237 let t = i as f64 / steps as f64;
238 let px = x1 + t * dx;
239 let py = y1 + t * dy;
240 for obs in &self.obstacles {
241 let ex = px - obs.x;
242 let ey = py - obs.y;
243 let r = obs.radius + self.config.robot_radius;
244 if ex * ex + ey * ey <= r * r {
245 return false;
246 }
247 }
248 }
249 true
250 }
251
252 fn attach_goal(&mut self, gx: f64, gy: f64) -> Option<usize> {
256 let mut candidates: Vec<(usize, f64)> = self
258 .nodes
259 .iter()
260 .enumerate()
261 .map(|(i, n)| {
262 let dx = n.x - gx;
263 let dy = n.y - gy;
264 (i, dx * dx + dy * dy)
265 })
266 .collect();
267 candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
268
269 for (idx, _) in candidates {
270 if self.segment_collision_free(self.nodes[idx].x, self.nodes[idx].y, gx, gy) {
271 let goal_idx = self.nodes.len();
272 self.nodes.push(RRGNode::new(gx, gy));
273 self.nodes[goal_idx].neighbors.push(idx);
275 self.nodes[idx].neighbors.push(goal_idx);
276 return Some(goal_idx);
277 }
278 }
279 None
280 }
281
282 fn dijkstra(
284 &self,
285 src: usize,
286 dst: usize,
287 start_pt: Point2D,
288 goal_pt: Point2D,
289 ) -> Option<Path2D> {
290 let n = self.nodes.len();
291 let mut dist = vec![f64::INFINITY; n];
292 let mut prev: HashMap<usize, usize> = HashMap::new();
293 dist[src] = 0.0;
294
295 let mut heap: BinaryHeap<DijkstraItem> = BinaryHeap::new();
297 heap.push(DijkstraItem {
298 cost: 0.0,
299 idx: src,
300 });
301
302 while let Some(DijkstraItem { cost, idx }) = heap.pop() {
303 if idx == dst {
304 break;
305 }
306 if cost > dist[idx] {
307 continue;
308 }
309 for &nb in &self.nodes[idx].neighbors {
310 let dx = self.nodes[nb].x - self.nodes[idx].x;
311 let dy = self.nodes[nb].y - self.nodes[idx].y;
312 let edge_cost = (dx * dx + dy * dy).sqrt();
313 let new_cost = dist[idx] + edge_cost;
314 if new_cost < dist[nb] {
315 dist[nb] = new_cost;
316 prev.insert(nb, idx);
317 heap.push(DijkstraItem {
318 cost: new_cost,
319 idx: nb,
320 });
321 }
322 }
323 }
324
325 if dist[dst] == f64::INFINITY {
326 return None;
327 }
328
329 let mut path_pts: Vec<Point2D> = Vec::new();
331 let mut cur = dst;
332 loop {
333 path_pts.push(Point2D::new(self.nodes[cur].x, self.nodes[cur].y));
334 match prev.get(&cur) {
335 Some(&p) => cur = p,
336 None => break,
337 }
338 }
339 path_pts.reverse();
340
341 if let Some(first) = path_pts.first_mut() {
343 *first = start_pt;
344 }
345 if let Some(last) = path_pts.last_mut() {
346 *last = goal_pt;
347 }
348
349 Some(Path2D::from_points(path_pts))
350 }
351
352 pub fn get_graph(&self) -> &[RRGNode] {
354 &self.nodes
355 }
356}
357
358impl PathPlanner for RRGPlanner {
363 fn plan(&self, start: Point2D, goal: Point2D) -> Result<Path2D, RoboticsError> {
364 let mut planner = RRGPlanner {
367 config: self.config.clone(),
368 obstacles: self.obstacles.clone(),
369 rand_area: self.rand_area.clone(),
370 nodes: Vec::new(),
371 };
372 planner.plan_internal(start, goal).ok_or_else(|| {
373 RoboticsError::PlanningError("RRG: no path found within max_iter".to_string())
374 })
375 }
376}
377
378#[derive(Clone)]
383struct DijkstraItem {
384 cost: f64,
385 idx: usize,
386}
387
388impl PartialEq for DijkstraItem {
389 fn eq(&self, other: &Self) -> bool {
390 self.cost == other.cost && self.idx == other.idx
391 }
392}
393
394impl Eq for DijkstraItem {}
395
396impl Ord for DijkstraItem {
397 fn cmp(&self, other: &Self) -> Ordering {
398 other
400 .cost
401 .partial_cmp(&self.cost)
402 .unwrap_or(Ordering::Equal)
403 }
404}
405
406impl PartialOrd for DijkstraItem {
407 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
408 Some(self.cmp(other))
409 }
410}
411
412#[cfg(test)]
417mod tests {
418 use super::*;
419
420 fn default_bounds() -> AreaBounds {
421 AreaBounds::new(-2.0, 15.0, -2.0, 15.0)
422 }
423
424 #[test]
425 fn test_rrg_config_defaults() {
426 let cfg = RRGConfig::default();
427 assert_eq!(cfg.expand_dis, 3.0);
428 assert_eq!(cfg.path_resolution, 0.5);
429 assert_eq!(cfg.max_iter, 500);
430 assert_eq!(cfg.robot_radius, 0.8);
431 assert_eq!(cfg.rewire_radius, 5.0);
432 }
433
434 #[test]
435 fn test_rrg_finds_path_no_obstacles() {
436 let cfg = RRGConfig {
437 max_iter: 500,
438 ..Default::default()
439 };
440 let mut planner = RRGPlanner::new(vec![], default_bounds(), cfg);
441
442 let start = Point2D::new(0.0, 0.0);
443 let goal = Point2D::new(6.0, 10.0);
444
445 let path = planner.plan_internal(start, goal);
446 assert!(path.is_some(), "should find a path with no obstacles");
447
448 let path = path.unwrap();
449 let pts = &path.points;
450 assert!(!pts.is_empty());
451
452 let first = pts.first().unwrap();
453 let last = pts.last().unwrap();
454 assert!((first.x - 0.0).abs() < 1e-9 && (first.y - 0.0).abs() < 1e-9);
455 assert!((last.x - 6.0).abs() < 1e-9 && (last.y - 10.0).abs() < 1e-9);
456 println!(
457 "[no_obstacles] path length {} nodes, {} segments",
458 pts.len(),
459 pts.len().saturating_sub(1)
460 );
461 }
462
463 #[test]
464 fn test_rrg_finds_path_with_obstacles() {
465 let obstacles = vec![
466 CircleObstacle::new(5.0, 5.0, 1.0),
467 CircleObstacle::new(3.0, 6.0, 2.0),
468 CircleObstacle::new(3.0, 8.0, 2.0),
469 CircleObstacle::new(3.0, 10.0, 2.0),
470 CircleObstacle::new(7.0, 5.0, 2.0),
471 CircleObstacle::new(9.0, 5.0, 2.0),
472 ];
473 let cfg = RRGConfig {
474 max_iter: 2000,
475 ..Default::default()
476 };
477 let mut planner = RRGPlanner::new(obstacles, default_bounds(), cfg);
478
479 let start = Point2D::new(0.0, 0.0);
480 let goal = Point2D::new(6.0, 10.0);
481
482 let path = planner.plan_internal(start, goal);
483 assert!(path.is_some(), "should find a path even with obstacles");
484
485 let path = path.unwrap();
486 let pts = &path.points;
487 let first = pts.first().unwrap();
488 let last = pts.last().unwrap();
489 assert!((first.x - 0.0).abs() < 1e-9 && (first.y - 0.0).abs() < 1e-9);
490 assert!((last.x - 6.0).abs() < 1e-9 && (last.y - 10.0).abs() < 1e-9);
491 println!("[with_obstacles] path length {} nodes", pts.len());
492 }
493}