1use std::cmp::Ordering;
17use std::collections::BinaryHeap;
18
19use crate::grid::GridMap;
20use rust_robotics_core::{Obstacles, Path2D, PathPlanner, Point2D, RoboticsError, RoboticsResult};
21
22#[derive(Debug, Clone)]
24pub struct AnyaConfig {
25 pub resolution: f64,
26 pub robot_radius: f64,
27 pub heuristic_weight: f64,
28}
29
30impl Default for AnyaConfig {
31 fn default() -> Self {
32 Self {
33 resolution: 1.0,
34 robot_radius: 0.5,
35 heuristic_weight: 1.0,
36 }
37 }
38}
39
40impl AnyaConfig {
41 pub fn validate(&self) -> RoboticsResult<()> {
42 if !self.resolution.is_finite() || self.resolution <= 0.0 {
43 return Err(RoboticsError::InvalidParameter(format!(
44 "resolution must be positive and finite, got {}",
45 self.resolution
46 )));
47 }
48 if !self.robot_radius.is_finite() || self.robot_radius < 0.0 {
49 return Err(RoboticsError::InvalidParameter(format!(
50 "robot_radius must be non-negative and finite, got {}",
51 self.robot_radius
52 )));
53 }
54 if !self.heuristic_weight.is_finite() || self.heuristic_weight <= 0.0 {
55 return Err(RoboticsError::InvalidParameter(format!(
56 "heuristic_weight must be positive and finite, got {}",
57 self.heuristic_weight
58 )));
59 }
60 Ok(())
61 }
62}
63
64#[derive(Debug)]
65struct DijkstraNode {
66 node_id: usize,
67 cost: f64,
68}
69
70impl Eq for DijkstraNode {}
71impl PartialEq for DijkstraNode {
72 fn eq(&self, other: &Self) -> bool {
73 self.cost == other.cost
74 }
75}
76impl Ord for DijkstraNode {
77 fn cmp(&self, other: &Self) -> Ordering {
78 other
79 .cost
80 .partial_cmp(&self.cost)
81 .unwrap_or(Ordering::Equal)
82 }
83}
84impl PartialOrd for DijkstraNode {
85 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
86 Some(self.cmp(other))
87 }
88}
89
90pub struct AnyaPlanner {
91 grid_map: GridMap,
92 #[allow(dead_code)]
93 config: AnyaConfig,
94}
95
96impl AnyaPlanner {
97 pub fn new(ox: &[f64], oy: &[f64], config: AnyaConfig) -> Self {
98 Self::try_new(ox, oy, config).expect(
99 "invalid Anya planner input: obstacle list must be non-empty and valid, and config values must be positive/finite",
100 )
101 }
102
103 pub fn try_new(ox: &[f64], oy: &[f64], config: AnyaConfig) -> RoboticsResult<Self> {
104 config.validate()?;
105 let grid_map = GridMap::try_new(ox, oy, config.resolution, config.robot_radius)?;
106 Ok(AnyaPlanner { grid_map, config })
107 }
108
109 pub fn from_obstacles(ox: &[f64], oy: &[f64], resolution: f64, robot_radius: f64) -> Self {
110 let config = AnyaConfig {
111 resolution,
112 robot_radius,
113 ..Default::default()
114 };
115 Self::new(ox, oy, config)
116 }
117
118 pub fn from_obstacle_points(obstacles: &Obstacles, config: AnyaConfig) -> RoboticsResult<Self> {
119 config.validate()?;
120 let grid_map = GridMap::from_obstacles(obstacles, config.resolution, config.robot_radius)?;
121 Ok(AnyaPlanner { grid_map, config })
122 }
123
124 #[deprecated(note = "use plan() or plan_xy() instead")]
125 pub fn planning(&self, sx: f64, sy: f64, gx: f64, gy: f64) -> Option<(Vec<f64>, Vec<f64>)> {
126 match self.plan_xy(sx, sy, gx, gy) {
127 Ok(path) => Some((path.x_coords(), path.y_coords())),
128 Err(_) => None,
129 }
130 }
131
132 pub fn plan(&self, start: Point2D, goal: Point2D) -> RoboticsResult<Path2D> {
133 self.plan_impl(start, goal)
134 }
135
136 pub fn plan_xy(&self, sx: f64, sy: f64, gx: f64, gy: f64) -> RoboticsResult<Path2D> {
137 self.plan_impl(Point2D::new(sx, sy), Point2D::new(gx, gy))
138 }
139
140 pub fn grid_map(&self) -> &GridMap {
141 &self.grid_map
142 }
143
144 fn line_of_sight(&self, x0: i32, y0: i32, x1: i32, y1: i32) -> bool {
146 if !self.grid_map.is_valid(x0, y0) || !self.grid_map.is_valid(x1, y1) {
147 return false;
148 }
149
150 if x0 == x1 && y0 == y1 {
151 return true;
152 }
153
154 let dx = x1 - x0;
155 let dy = y1 - y0;
156 let step_x = dx.signum();
157 let step_y = dy.signum();
158 let abs_dx = dx.abs() as f64;
159 let abs_dy = dy.abs() as f64;
160
161 let mut x = x0;
162 let mut y = y0;
163 let mut t_max_x = if step_x != 0 {
164 0.5 / abs_dx
165 } else {
166 f64::INFINITY
167 };
168 let mut t_max_y = if step_y != 0 {
169 0.5 / abs_dy
170 } else {
171 f64::INFINITY
172 };
173 let t_delta_x = if step_x != 0 {
174 1.0 / abs_dx
175 } else {
176 f64::INFINITY
177 };
178 let t_delta_y = if step_y != 0 {
179 1.0 / abs_dy
180 } else {
181 f64::INFINITY
182 };
183
184 while x != x1 || y != y1 {
185 let advance_x = t_max_x <= t_max_y;
186 let advance_y = t_max_y <= t_max_x;
187 let next_x = if advance_x { x + step_x } else { x };
188 let next_y = if advance_y { y + step_y } else { y };
189
190 if !self.grid_map.is_valid_step(x, y, next_x, next_y) {
191 return false;
192 }
193
194 x = next_x;
195 y = next_y;
196
197 if advance_x {
198 t_max_x += t_delta_x;
199 }
200 if advance_y {
201 t_max_y += t_delta_y;
202 }
203 }
204
205 true
206 }
207
208 fn find_all_free_cells(&self) -> Vec<(i32, i32)> {
218 let mut cells = Vec::new();
219 for ix in 0..self.grid_map.x_width {
220 for iy in 0..self.grid_map.y_width {
221 if self.grid_map.is_valid(ix, iy) {
222 cells.push((ix, iy));
223 }
224 }
225 }
226 cells
227 }
228
229 fn ensure_query_is_valid(&self, x: i32, y: i32, label: &str) -> RoboticsResult<()> {
230 if self.grid_map.is_valid(x, y) {
231 return Ok(());
232 }
233 Err(RoboticsError::PlanningError(format!(
234 "{} position is invalid",
235 label
236 )))
237 }
238
239 fn plan_impl(&self, start: Point2D, goal: Point2D) -> RoboticsResult<Path2D> {
240 let start_x = self.grid_map.calc_x_index(start.x);
241 let start_y = self.grid_map.calc_y_index(start.y);
242 let goal_x = self.grid_map.calc_x_index(goal.x);
243 let goal_y = self.grid_map.calc_y_index(goal.y);
244
245 self.ensure_query_is_valid(start_x, start_y, "Start")?;
246 self.ensure_query_is_valid(goal_x, goal_y, "Goal")?;
247
248 if self.line_of_sight(start_x, start_y, goal_x, goal_y) {
250 return Ok(Path2D::from_points(vec![
251 Point2D::new(
252 self.grid_map.calc_x_position(start_x),
253 self.grid_map.calc_y_position(start_y),
254 ),
255 Point2D::new(
256 self.grid_map.calc_x_position(goal_x),
257 self.grid_map.calc_y_position(goal_y),
258 ),
259 ]));
260 }
261
262 let mut nodes: Vec<(i32, i32)> = self.find_all_free_cells();
264
265 let start_pos = (start_x, start_y);
267 let goal_pos = (goal_x, goal_y);
268 if !nodes.contains(&start_pos) {
269 nodes.push(start_pos);
270 }
271 if !nodes.contains(&goal_pos) {
272 nodes.push(goal_pos);
273 }
274
275 let n = nodes.len();
276 let start_id = nodes.iter().position(|&p| p == start_pos).unwrap();
277 let goal_id = nodes.iter().position(|&p| p == goal_pos).unwrap();
278
279 let mut adj: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
281 for i in 0..n {
282 for j in (i + 1)..n {
283 let (x0, y0) = nodes[i];
284 let (x1, y1) = nodes[j];
285 if self.line_of_sight(x0, y0, x1, y1) {
286 let dist = (((x0 - x1).pow(2) + (y0 - y1).pow(2)) as f64).sqrt();
287 adj[i].push((j, dist));
288 adj[j].push((i, dist));
289 }
290 }
291 }
292
293 let mut dist = vec![f64::INFINITY; n];
295 let mut prev: Vec<Option<usize>> = vec![None; n];
296 let mut visited = vec![false; n];
297 dist[start_id] = 0.0;
298
299 let mut heap = BinaryHeap::new();
300 heap.push(DijkstraNode {
301 node_id: start_id,
302 cost: 0.0,
303 });
304
305 while let Some(current) = heap.pop() {
306 if current.node_id == goal_id {
307 break;
308 }
309 if visited[current.node_id] {
310 continue;
311 }
312 visited[current.node_id] = true;
313
314 for &(neighbor, weight) in &adj[current.node_id] {
315 let new_dist = dist[current.node_id] + weight;
316 if new_dist < dist[neighbor] {
317 dist[neighbor] = new_dist;
318 prev[neighbor] = Some(current.node_id);
319 heap.push(DijkstraNode {
320 node_id: neighbor,
321 cost: new_dist,
322 });
323 }
324 }
325 }
326
327 if dist[goal_id].is_infinite() {
328 return Err(RoboticsError::PlanningError("No path found".to_string()));
329 }
330
331 let mut path_indices = Vec::new();
333 let mut current = goal_id;
334 loop {
335 path_indices.push(current);
336 match prev[current] {
337 Some(p) => current = p,
338 None => break,
339 }
340 }
341 path_indices.reverse();
342
343 let points: Vec<Point2D> = path_indices
344 .iter()
345 .map(|&id| {
346 let (ix, iy) = nodes[id];
347 Point2D::new(
348 self.grid_map.calc_x_position(ix),
349 self.grid_map.calc_y_position(iy),
350 )
351 })
352 .collect();
353
354 Ok(Path2D::from_points(points))
355 }
356}
357
358impl PathPlanner for AnyaPlanner {
359 fn plan(&self, start: Point2D, goal: Point2D) -> Result<Path2D, RoboticsError> {
360 self.plan_impl(start, goal)
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use rust_robotics_core::Obstacles;
368
369 fn create_simple_obstacles() -> (Vec<f64>, Vec<f64>) {
370 let mut ox = Vec::new();
371 let mut oy = Vec::new();
372 for i in 0..21 {
373 ox.push(i as f64);
374 oy.push(0.0);
375 ox.push(i as f64);
376 oy.push(20.0);
377 ox.push(0.0);
378 oy.push(i as f64);
379 ox.push(20.0);
380 oy.push(i as f64);
381 }
382 for i in 5..15 {
383 ox.push(10.0);
384 oy.push(i as f64);
385 }
386 (ox, oy)
387 }
388
389 #[test]
390 fn test_anya_finds_path() {
391 let (ox, oy) = create_simple_obstacles();
392 let planner = AnyaPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
393 let result = planner.plan(Point2D::new(2.0, 10.0), Point2D::new(18.0, 10.0));
394 assert!(result.is_ok());
395 let path = result.unwrap();
396 assert!(!path.is_empty());
397 let xs = path.x_coords();
399 let ys = path.y_coords();
400 assert!((xs[0] - 2.0).abs() < 1.5);
401 assert!((ys[0] - 10.0).abs() < 1.5);
402 assert!((*xs.last().unwrap() - 18.0).abs() < 1.5);
403 assert!((*ys.last().unwrap() - 10.0).abs() < 1.5);
404 }
405
406 #[test]
407 fn test_anya_shorter_than_a_star() {
408 let (ox, oy) = create_simple_obstacles();
409 let anya_planner = AnyaPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
410 let a_star_planner = crate::a_star::AStarPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
411 let start = Point2D::new(2.0, 2.0);
412 let goal = Point2D::new(18.0, 18.0);
413 let anya_path = anya_planner.plan(start, goal).unwrap();
414 let a_star_path = a_star_planner.plan(start, goal).unwrap();
415 let anya_length = anya_path.total_length();
416 let a_star_length = a_star_path.total_length();
417 assert!(
418 anya_length <= a_star_length + 0.1,
419 "Anya path ({}) should not be longer than A* path ({})",
420 anya_length,
421 a_star_length
422 );
423 }
424
425 #[test]
426 fn test_anya_optimal_straight_line() {
427 let mut ox = Vec::new();
430 let mut oy = Vec::new();
431 for i in 0..21 {
432 ox.push(i as f64);
433 oy.push(0.0);
434 ox.push(i as f64);
435 oy.push(20.0);
436 ox.push(0.0);
437 oy.push(i as f64);
438 ox.push(20.0);
439 oy.push(i as f64);
440 }
441 let planner = AnyaPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
442 let start = Point2D::new(2.0, 2.0);
443 let goal = Point2D::new(18.0, 18.0);
444 let path = planner.plan(start, goal).unwrap();
445 let path_length = path.total_length();
446 let euclidean = ((18.0 - 2.0_f64).powi(2) + (18.0 - 2.0_f64).powi(2)).sqrt();
447 assert!(
448 (path_length - euclidean).abs() < 1.0,
449 "Straight-line path length ({}) should be close to euclidean distance ({})",
450 path_length,
451 euclidean
452 );
453 }
454
455 #[test]
456 fn test_anya_from_obstacle_points() {
457 let (ox, oy) = create_simple_obstacles();
458 let obstacles = Obstacles::try_from_xy(&ox, &oy).unwrap();
459 let planner = AnyaPlanner::from_obstacle_points(&obstacles, AnyaConfig::default()).unwrap();
460 let path = planner.plan_xy(2.0, 10.0, 18.0, 10.0).unwrap();
461 assert!(!path.is_empty());
462 }
463}