1use std::cmp::Ordering;
11use std::collections::{BinaryHeap, HashMap};
12
13use crate::grid::GridMap;
14use rust_robotics_core::{Obstacles, Path2D, PathPlanner, Point2D, RoboticsError, RoboticsResult};
15
16#[derive(Debug, Clone)]
18pub struct ARAStarConfig {
19 pub resolution: f64,
21 pub robot_radius: f64,
23 pub initial_epsilon: f64,
26 pub epsilon_step: f64,
28 pub final_epsilon: f64,
30}
31
32impl Default for ARAStarConfig {
33 fn default() -> Self {
34 Self {
35 resolution: 1.0,
36 robot_radius: 0.5,
37 initial_epsilon: 3.0,
38 epsilon_step: 0.5,
39 final_epsilon: 1.0,
40 }
41 }
42}
43
44impl ARAStarConfig {
45 pub fn validate(&self) -> RoboticsResult<()> {
47 if !self.resolution.is_finite() || self.resolution <= 0.0 {
48 return Err(RoboticsError::InvalidParameter(format!(
49 "resolution must be positive and finite, got {}",
50 self.resolution
51 )));
52 }
53 if !self.robot_radius.is_finite() || self.robot_radius < 0.0 {
54 return Err(RoboticsError::InvalidParameter(format!(
55 "robot_radius must be non-negative and finite, got {}",
56 self.robot_radius
57 )));
58 }
59 if !self.initial_epsilon.is_finite() || self.initial_epsilon < 1.0 {
60 return Err(RoboticsError::InvalidParameter(format!(
61 "initial_epsilon must be >= 1.0 and finite, got {}",
62 self.initial_epsilon
63 )));
64 }
65 if !self.epsilon_step.is_finite() || self.epsilon_step <= 0.0 {
66 return Err(RoboticsError::InvalidParameter(format!(
67 "epsilon_step must be positive and finite, got {}",
68 self.epsilon_step
69 )));
70 }
71 if !self.final_epsilon.is_finite() || self.final_epsilon < 1.0 {
72 return Err(RoboticsError::InvalidParameter(format!(
73 "final_epsilon must be >= 1.0 and finite, got {}",
74 self.final_epsilon
75 )));
76 }
77 if self.final_epsilon > self.initial_epsilon {
78 return Err(RoboticsError::InvalidParameter(format!(
79 "final_epsilon ({}) must be <= initial_epsilon ({})",
80 self.final_epsilon, self.initial_epsilon
81 )));
82 }
83 Ok(())
84 }
85}
86
87#[derive(Debug)]
90struct PriorityNode {
91 grid_idx: i32,
92 g: f64,
93 f: f64,
94}
95
96impl Eq for PriorityNode {}
97
98impl PartialEq for PriorityNode {
99 fn eq(&self, other: &Self) -> bool {
100 self.f == other.f
101 }
102}
103
104impl Ord for PriorityNode {
105 fn cmp(&self, other: &Self) -> Ordering {
106 other.f.partial_cmp(&self.f).unwrap_or(Ordering::Equal)
108 }
109}
110
111impl PartialOrd for PriorityNode {
112 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
113 Some(self.cmp(other))
114 }
115}
116
117pub struct ARAStarPlanner {
124 grid_map: GridMap,
125 config: ARAStarConfig,
126 motion: Vec<(i32, i32, f64)>,
127}
128
129impl ARAStarPlanner {
130 pub fn new(ox: &[f64], oy: &[f64], config: ARAStarConfig) -> Self {
132 Self::try_new(ox, oy, config).expect(
133 "invalid ARA* planner input: obstacles must be non-empty and valid, config values must be positive/finite",
134 )
135 }
136
137 pub fn try_new(ox: &[f64], oy: &[f64], config: ARAStarConfig) -> RoboticsResult<Self> {
139 config.validate()?;
140 let grid_map = GridMap::try_new(ox, oy, config.resolution, config.robot_radius)?;
141 Ok(Self {
142 grid_map,
143 motion: Self::motion_model(),
144 config,
145 })
146 }
147
148 pub fn from_obstacle_points(
150 obstacles: &Obstacles,
151 config: ARAStarConfig,
152 ) -> RoboticsResult<Self> {
153 config.validate()?;
154 let grid_map = GridMap::from_obstacles(obstacles, config.resolution, config.robot_radius)?;
155 Ok(Self {
156 grid_map,
157 motion: Self::motion_model(),
158 config,
159 })
160 }
161
162 pub fn plan(&self, start: Point2D, goal: Point2D) -> RoboticsResult<Path2D> {
165 let solutions = self.plan_anytime(start, goal);
166 solutions
167 .into_iter()
168 .last()
169 .map(|(_, path)| path)
170 .ok_or_else(|| RoboticsError::PlanningError("No path found".to_string()))
171 }
172
173 pub fn plan_anytime(&self, start: Point2D, goal: Point2D) -> Vec<(f64, Path2D)> {
177 let start_x = self.grid_map.calc_x_index(start.x);
178 let start_y = self.grid_map.calc_y_index(start.y);
179 let goal_x = self.grid_map.calc_x_index(goal.x);
180 let goal_y = self.grid_map.calc_y_index(goal.y);
181
182 if !self.grid_map.is_valid(start_x, start_y) || !self.grid_map.is_valid(goal_x, goal_y) {
183 return Vec::new();
184 }
185
186 let start_idx = self.grid_map.calc_index(start_x, start_y);
187 let goal_idx = self.grid_map.calc_index(goal_x, goal_y);
188
189 let mut g_table: HashMap<i32, f64> = HashMap::new();
191 let mut parent: HashMap<i32, Option<i32>> = HashMap::new();
193 let mut coords: HashMap<i32, (i32, i32)> = HashMap::new();
195
196 g_table.insert(start_idx, 0.0);
197 parent.insert(start_idx, None);
198 coords.insert(start_idx, (start_x, start_y));
199 coords.insert(goal_idx, (goal_x, goal_y));
200
201 let mut solutions: Vec<(f64, Path2D)> = Vec::new();
202 let mut epsilon = self.config.initial_epsilon;
203
204 loop {
205 if let Some(path) = self.weighted_a_star(
206 start_idx,
207 goal_idx,
208 start_x,
209 start_y,
210 goal_x,
211 goal_y,
212 epsilon,
213 &mut g_table,
214 &mut parent,
215 &mut coords,
216 ) {
217 solutions.push((epsilon, path));
218 }
219
220 if epsilon <= self.config.final_epsilon + f64::EPSILON {
221 break;
222 }
223 epsilon = (epsilon - self.config.epsilon_step).max(self.config.final_epsilon);
224 }
225
226 solutions
227 }
228
229 pub fn grid_map(&self) -> &GridMap {
231 &self.grid_map
232 }
233
234 fn motion_model() -> Vec<(i32, i32, f64)> {
237 vec![
238 (1, 0, 1.0),
239 (0, 1, 1.0),
240 (-1, 0, 1.0),
241 (0, -1, 1.0),
242 (-1, -1, std::f64::consts::SQRT_2),
243 (-1, 1, std::f64::consts::SQRT_2),
244 (1, -1, std::f64::consts::SQRT_2),
245 (1, 1, std::f64::consts::SQRT_2),
246 ]
247 }
248
249 fn heuristic(&self, x: i32, y: i32, gx: i32, gy: i32) -> f64 {
250 (((x - gx).pow(2) + (y - gy).pow(2)) as f64).sqrt()
251 }
252
253 #[allow(clippy::too_many_arguments)]
258 fn weighted_a_star(
259 &self,
260 start_idx: i32,
261 goal_idx: i32,
262 start_x: i32,
263 start_y: i32,
264 goal_x: i32,
265 goal_y: i32,
266 epsilon: f64,
267 g_table: &mut HashMap<i32, f64>,
268 parent: &mut HashMap<i32, Option<i32>>,
269 coords: &mut HashMap<i32, (i32, i32)>,
270 ) -> Option<Path2D> {
271 let start_g = *g_table.get(&start_idx).unwrap_or(&0.0);
272 let start_h = epsilon * self.heuristic(start_x, start_y, goal_x, goal_y);
273
274 let mut open: BinaryHeap<PriorityNode> = BinaryHeap::new();
275 let mut incons: HashMap<i32, f64> = HashMap::new();
277 let mut closed: HashMap<i32, f64> = HashMap::new();
278
279 open.push(PriorityNode {
280 grid_idx: start_idx,
281 g: start_g,
282 f: start_g + start_h,
283 });
284
285 for (&idx, &g) in g_table.iter() {
288 if idx == start_idx {
289 continue;
290 }
291 if let Some(&(x, y)) = coords.get(&idx) {
292 if self.grid_map.is_valid(x, y) {
293 let f = g + epsilon * self.heuristic(x, y, goal_x, goal_y);
294 open.push(PriorityNode {
295 grid_idx: idx,
296 g,
297 f,
298 });
299 }
300 }
301 }
302
303 while let Some(current) = open.pop() {
304 if let Some(&best_g) = g_table.get(¤t.grid_idx) {
306 if current.g > best_g + f64::EPSILON {
307 continue;
308 }
309 }
310 if closed.contains_key(¤t.grid_idx) {
311 continue;
312 }
313
314 closed.insert(current.grid_idx, current.g);
315
316 if current.grid_idx == goal_idx {
317 return Some(self.reconstruct_path(goal_idx, start_idx, parent, coords));
318 }
319
320 let (cx, cy) = match coords.get(¤t.grid_idx) {
321 Some(&c) => c,
322 None => continue,
323 };
324
325 for &(dx, dy, move_cost) in &self.motion {
326 if !self.grid_map.is_valid_offset(cx, cy, dx, dy) {
327 continue;
328 }
329
330 let nx = cx + dx;
331 let ny = cy + dy;
332 let n_idx = self.grid_map.calc_index(nx, ny);
333 let new_g = current.g + move_cost;
334
335 let old_g = g_table.get(&n_idx).copied().unwrap_or(f64::INFINITY);
336
337 if new_g < old_g - f64::EPSILON {
338 g_table.insert(n_idx, new_g);
339 coords.insert(n_idx, (nx, ny));
340 parent.insert(n_idx, Some(current.grid_idx));
341
342 if closed.contains_key(&n_idx) {
343 incons.insert(n_idx, new_g);
345 } else {
346 let f = new_g + epsilon * self.heuristic(nx, ny, goal_x, goal_y);
347 open.push(PriorityNode {
348 grid_idx: n_idx,
349 g: new_g,
350 f,
351 });
352 }
353 }
354 }
355 }
356
357 for (idx, g) in incons {
359 g_table.insert(idx, g);
360 }
361
362 None
363 }
364
365 fn reconstruct_path(
366 &self,
367 goal_idx: i32,
368 start_idx: i32,
369 parent: &HashMap<i32, Option<i32>>,
370 coords: &HashMap<i32, (i32, i32)>,
371 ) -> Path2D {
372 let mut points = Vec::new();
373 let mut current = goal_idx;
374
375 loop {
376 if let Some(&(x, y)) = coords.get(¤t) {
377 points.push(Point2D::new(
378 self.grid_map.calc_x_position(x),
379 self.grid_map.calc_y_position(y),
380 ));
381 }
382 if current == start_idx {
383 break;
384 }
385 match parent.get(¤t) {
386 Some(Some(p)) => current = *p,
387 _ => break,
388 }
389 }
390
391 points.reverse();
392 Path2D::from_points(points)
393 }
394}
395
396impl PathPlanner for ARAStarPlanner {
397 fn plan(&self, start: Point2D, goal: Point2D) -> Result<Path2D, RoboticsError> {
398 ARAStarPlanner::plan(self, start, goal)
399 }
400}
401
402#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn simple_obstacles() -> (Vec<f64>, Vec<f64>) {
409 let mut ox = Vec::new();
410 let mut oy = Vec::new();
411 for i in 0..=10 {
412 ox.push(i as f64);
413 oy.push(0.0);
414 ox.push(i as f64);
415 oy.push(10.0);
416 ox.push(0.0);
417 oy.push(i as f64);
418 ox.push(10.0);
419 oy.push(i as f64);
420 }
421 for i in 3..8 {
423 ox.push(5.0);
424 oy.push(i as f64);
425 }
426 (ox, oy)
427 }
428
429 #[test]
430 fn test_ara_star_finds_path() {
431 let (ox, oy) = simple_obstacles();
432 let planner = ARAStarPlanner::new(&ox, &oy, ARAStarConfig::default());
433
434 let path = planner.plan(Point2D::new(2.0, 2.0), Point2D::new(8.0, 8.0));
435 assert!(path.is_ok(), "ARA* should find a path: {:?}", path.err());
436 assert!(!path.unwrap().is_empty());
437 }
438
439 #[test]
440 fn test_plan_anytime_returns_multiple_solutions() {
441 let (ox, oy) = simple_obstacles();
442 let config = ARAStarConfig {
443 initial_epsilon: 3.0,
444 epsilon_step: 0.5,
445 final_epsilon: 1.0,
446 ..Default::default()
447 };
448 let planner = ARAStarPlanner::new(&ox, &oy, config);
449
450 let solutions = planner.plan_anytime(Point2D::new(2.0, 2.0), Point2D::new(8.0, 8.0));
451 assert!(
453 solutions.len() >= 2,
454 "plan_anytime should return at least 2 solutions, got {}",
455 solutions.len()
456 );
457 }
458
459 #[test]
460 fn test_first_solution_cost_not_less_than_final() {
461 let (ox, oy) = simple_obstacles();
462 let config = ARAStarConfig {
463 initial_epsilon: 3.0,
464 epsilon_step: 0.5,
465 final_epsilon: 1.0,
466 ..Default::default()
467 };
468 let planner = ARAStarPlanner::new(&ox, &oy, config);
469
470 let solutions = planner.plan_anytime(Point2D::new(2.0, 2.0), Point2D::new(8.0, 8.0));
471 assert!(solutions.len() >= 2, "need at least 2 solutions to compare");
472
473 let first_cost = solutions.first().unwrap().1.total_length();
474 let last_cost = solutions.last().unwrap().1.total_length();
475 println!("first_cost={first_cost:.4} last_cost={last_cost:.4}");
476
477 assert!(
479 first_cost >= last_cost - 1e-9,
480 "first solution ({first_cost}) should be >= final solution ({last_cost})"
481 );
482 }
483
484 #[test]
485 fn test_config_defaults() {
486 let cfg = ARAStarConfig::default();
487 assert_eq!(cfg.resolution, 1.0);
488 assert_eq!(cfg.robot_radius, 0.5);
489 assert_eq!(cfg.initial_epsilon, 3.0);
490 assert_eq!(cfg.epsilon_step, 0.5);
491 assert_eq!(cfg.final_epsilon, 1.0);
492 assert!(cfg.validate().is_ok());
493 }
494}