1use std::cmp::Ordering;
22use std::collections::{BinaryHeap, HashMap};
23
24use crate::grid::{GridMap, Node};
25use rust_robotics_core::{Obstacles, Path2D, PathPlanner, Point2D, RoboticsError, RoboticsResult};
26
27#[derive(Debug, Clone)]
29pub struct EnhancedLazyThetaStarConfig {
30 pub resolution: f64,
31 pub robot_radius: f64,
32 pub heuristic_weight: f64,
33}
34
35impl Default for EnhancedLazyThetaStarConfig {
36 fn default() -> Self {
37 Self {
38 resolution: 1.0,
39 robot_radius: 0.5,
40 heuristic_weight: 1.0,
41 }
42 }
43}
44
45impl EnhancedLazyThetaStarConfig {
46 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.heuristic_weight.is_finite() || self.heuristic_weight <= 0.0 {
60 return Err(RoboticsError::InvalidParameter(format!(
61 "heuristic_weight must be positive and finite, got {}",
62 self.heuristic_weight
63 )));
64 }
65 Ok(())
66 }
67}
68
69#[derive(Debug)]
70struct PriorityNode {
71 x: i32,
72 y: i32,
73 #[allow(dead_code)]
74 cost: f64,
75 priority: f64,
76 index: usize,
77}
78impl Eq for PriorityNode {}
79impl PartialEq for PriorityNode {
80 fn eq(&self, other: &Self) -> bool {
81 self.priority == other.priority
82 }
83}
84impl Ord for PriorityNode {
85 fn cmp(&self, other: &Self) -> Ordering {
86 other
87 .priority
88 .partial_cmp(&self.priority)
89 .unwrap_or(Ordering::Equal)
90 }
91}
92impl PartialOrd for PriorityNode {
93 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
94 Some(self.cmp(other))
95 }
96}
97
98pub struct EnhancedLazyThetaStarPlanner {
99 grid_map: GridMap,
100 config: EnhancedLazyThetaStarConfig,
101 motion: Vec<(i32, i32, f64)>,
102}
103
104impl EnhancedLazyThetaStarPlanner {
105 pub fn new(ox: &[f64], oy: &[f64], config: EnhancedLazyThetaStarConfig) -> Self {
106 Self::try_new(ox, oy, config).expect("invalid Enhanced Lazy Theta* planner input")
107 }
108
109 pub fn try_new(
110 ox: &[f64],
111 oy: &[f64],
112 config: EnhancedLazyThetaStarConfig,
113 ) -> RoboticsResult<Self> {
114 config.validate()?;
115 let grid_map = GridMap::try_new(ox, oy, config.resolution, config.robot_radius)?;
116 let motion = Self::get_motion_model();
117 Ok(Self {
118 grid_map,
119 config,
120 motion,
121 })
122 }
123
124 pub fn from_obstacles(ox: &[f64], oy: &[f64], resolution: f64, robot_radius: f64) -> Self {
125 let config = EnhancedLazyThetaStarConfig {
126 resolution,
127 robot_radius,
128 ..Default::default()
129 };
130 Self::new(ox, oy, config)
131 }
132
133 pub fn from_obstacle_points(
134 obstacles: &Obstacles,
135 config: EnhancedLazyThetaStarConfig,
136 ) -> RoboticsResult<Self> {
137 config.validate()?;
138 let grid_map = GridMap::from_obstacles(obstacles, config.resolution, config.robot_radius)?;
139 let motion = Self::get_motion_model();
140 Ok(Self {
141 grid_map,
142 config,
143 motion,
144 })
145 }
146
147 #[deprecated(note = "use plan() or plan_xy() instead")]
148 pub fn planning(&self, sx: f64, sy: f64, gx: f64, gy: f64) -> Option<(Vec<f64>, Vec<f64>)> {
149 match self.plan_xy(sx, sy, gx, gy) {
150 Ok(path) => Some((path.x_coords(), path.y_coords())),
151 Err(_) => None,
152 }
153 }
154
155 pub fn plan(&self, start: Point2D, goal: Point2D) -> RoboticsResult<Path2D> {
156 self.plan_impl(start, goal)
157 }
158
159 pub fn plan_xy(&self, sx: f64, sy: f64, gx: f64, gy: f64) -> RoboticsResult<Path2D> {
160 self.plan_impl(Point2D::new(sx, sy), Point2D::new(gx, gy))
161 }
162
163 pub fn grid_map(&self) -> &GridMap {
164 &self.grid_map
165 }
166
167 fn calc_heuristic(&self, n1_x: i32, n1_y: i32, n2_x: i32, n2_y: i32) -> f64 {
168 self.config.heuristic_weight * (((n1_x - n2_x).pow(2) + (n1_y - n2_y).pow(2)) as f64).sqrt()
169 }
170
171 fn get_motion_model() -> Vec<(i32, i32, f64)> {
172 vec![
173 (1, 0, 1.0),
174 (0, 1, 1.0),
175 (-1, 0, 1.0),
176 (0, -1, 1.0),
177 (-1, -1, std::f64::consts::SQRT_2),
178 (-1, 1, std::f64::consts::SQRT_2),
179 (1, -1, std::f64::consts::SQRT_2),
180 (1, 1, std::f64::consts::SQRT_2),
181 ]
182 }
183
184 fn line_of_sight(&self, x0: i32, y0: i32, x1: i32, y1: i32) -> bool {
185 if !self.grid_map.is_valid(x0, y0) || !self.grid_map.is_valid(x1, y1) {
186 return false;
187 }
188 if x0 == x1 && y0 == y1 {
189 return true;
190 }
191 let dx = x1 - x0;
192 let dy = y1 - y0;
193 let step_x = dx.signum();
194 let step_y = dy.signum();
195 let abs_dx = dx.abs() as f64;
196 let abs_dy = dy.abs() as f64;
197 let mut x = x0;
198 let mut y = y0;
199 let mut t_max_x = if step_x != 0 {
200 0.5 / abs_dx
201 } else {
202 f64::INFINITY
203 };
204 let mut t_max_y = if step_y != 0 {
205 0.5 / abs_dy
206 } else {
207 f64::INFINITY
208 };
209 let t_delta_x = if step_x != 0 {
210 1.0 / abs_dx
211 } else {
212 f64::INFINITY
213 };
214 let t_delta_y = if step_y != 0 {
215 1.0 / abs_dy
216 } else {
217 f64::INFINITY
218 };
219 while x != x1 || y != y1 {
220 let advance_x = t_max_x <= t_max_y;
221 let advance_y = t_max_y <= t_max_x;
222 let next_x = if advance_x { x + step_x } else { x };
223 let next_y = if advance_y { y + step_y } else { y };
224 if !self.grid_map.is_valid_step(x, y, next_x, next_y) {
225 return false;
226 }
227 x = next_x;
228 y = next_y;
229 if advance_x {
230 t_max_x += t_delta_x;
231 }
232 if advance_y {
233 t_max_y += t_delta_y;
234 }
235 }
236 true
237 }
238
239 fn euclidean_distance(&self, x1: i32, y1: i32, x2: i32, y2: i32) -> f64 {
240 (((x1 - x2).pow(2) + (y1 - y2).pow(2)) as f64).sqrt()
241 }
242
243 fn build_path(&self, goal_index: usize, node_storage: &[Node]) -> Path2D {
244 let mut points = Vec::new();
245 let mut current_index = Some(goal_index);
246 while let Some(index) = current_index {
247 let node = &node_storage[index];
248 points.push(Point2D::new(
249 self.grid_map.calc_x_position(node.x),
250 self.grid_map.calc_y_position(node.y),
251 ));
252 current_index = node.parent_index;
253 }
254 points.reverse();
255 Path2D::from_points(points)
256 }
257
258 fn ensure_query_is_valid(&self, x: i32, y: i32, label: &str) -> RoboticsResult<()> {
259 if self.grid_map.is_valid(x, y) {
260 return Ok(());
261 }
262 Err(RoboticsError::PlanningError(format!(
263 "{} position is invalid",
264 label
265 )))
266 }
267
268 fn enhanced_best_parent(
271 &self,
272 x: i32,
273 y: i32,
274 closed_set: &HashMap<i32, usize>,
275 node_storage: &[Node],
276 ) -> Option<(usize, f64)> {
277 let mut best: Option<(usize, f64)> = None;
278 const MAX_ANCESTOR_HOPS: usize = 6;
279
280 let offsets: [(i32, i32); 24] = [
284 (1, 0),
286 (0, 1),
287 (-1, 0),
288 (0, -1),
289 (1, 1),
290 (1, -1),
291 (-1, 1),
292 (-1, -1),
293 (2, 0),
295 (0, 2),
296 (-2, 0),
297 (0, -2),
298 (2, 1),
299 (2, -1),
300 (-2, 1),
301 (-2, -1),
302 (1, 2),
303 (1, -2),
304 (-1, 2),
305 (-1, -2),
306 (2, 2),
307 (2, -2),
308 (-2, 2),
309 (-2, -2),
310 ];
311
312 for &(dx, dy) in &offsets {
313 let nx = x + dx;
314 let ny = y + dy;
315 if !self.grid_map.is_valid(nx, ny) {
316 continue;
317 }
318 let neighbor_grid_index = self.grid_map.calc_index(nx, ny);
319 if let Some(&neighbor_storage_index) = closed_set.get(&neighbor_grid_index) {
320 let neighbor = &node_storage[neighbor_storage_index];
321
322 if self.line_of_sight(neighbor.x, neighbor.y, x, y) {
324 let dist = self.euclidean_distance(neighbor.x, neighbor.y, x, y);
325 let g_via = neighbor.cost + dist;
326 if best.map_or(true, |(_, best_g)| g_via < best_g) {
327 best = Some((neighbor_storage_index, g_via));
328 }
329 }
330
331 let mut ancestor_idx = neighbor.parent_index;
333 let mut hops = 0;
334 while let Some(a_idx) = ancestor_idx {
335 if hops >= MAX_ANCESTOR_HOPS {
336 break;
337 }
338 let ancestor = &node_storage[a_idx];
339 if self.line_of_sight(ancestor.x, ancestor.y, x, y) {
340 let dist = self.euclidean_distance(ancestor.x, ancestor.y, x, y);
341 let g_via = ancestor.cost + dist;
342 if best.map_or(true, |(_, best_g)| g_via < best_g) {
343 best = Some((a_idx, g_via));
344 }
345 }
346 ancestor_idx = ancestor.parent_index;
347 hops += 1;
348 }
349 }
350 }
351
352 best
353 }
354
355 fn plan_impl(&self, start: Point2D, goal: Point2D) -> RoboticsResult<Path2D> {
356 let start_x = self.grid_map.calc_x_index(start.x);
357 let start_y = self.grid_map.calc_y_index(start.y);
358 let goal_x = self.grid_map.calc_x_index(goal.x);
359 let goal_y = self.grid_map.calc_y_index(goal.y);
360
361 self.ensure_query_is_valid(start_x, start_y, "Start")?;
362 self.ensure_query_is_valid(goal_x, goal_y, "Goal")?;
363
364 let mut open_set = BinaryHeap::new();
365 let mut closed_set: HashMap<i32, usize> = HashMap::new();
366 let mut node_storage: Vec<Node> = Vec::new();
367 let mut g_values: HashMap<i32, f64> = HashMap::new();
368 let mut best_index: HashMap<i32, usize> = HashMap::new();
369
370 node_storage.push(Node::new(start_x, start_y, 0.0, None));
371 let start_index = 0;
372 let start_grid_index = self.grid_map.calc_index(start_x, start_y);
373 g_values.insert(start_grid_index, 0.0);
374 best_index.insert(start_grid_index, start_index);
375
376 open_set.push(PriorityNode {
377 x: start_x,
378 y: start_y,
379 cost: 0.0,
380 priority: self.calc_heuristic(start_x, start_y, goal_x, goal_y),
381 index: start_index,
382 });
383
384 while let Some(current) = open_set.pop() {
385 let current_grid_index = self.grid_map.calc_index(current.x, current.y);
386
387 if closed_set.contains_key(¤t_grid_index) {
388 continue;
389 }
390
391 let mut corrected_index = current.index;
395 let current_node = &node_storage[current.index];
396 if let Some(parent_idx) = current_node.parent_index {
397 let parent_node = &node_storage[parent_idx];
398 let needs_correction =
400 !self.line_of_sight(parent_node.x, parent_node.y, current.x, current.y);
401
402 if needs_correction {
403 if let Some((best_parent_idx, best_g)) =
405 self.enhanced_best_parent(current.x, current.y, &closed_set, &node_storage)
406 {
407 node_storage.push(Node::new(
408 current.x,
409 current.y,
410 best_g,
411 Some(best_parent_idx),
412 ));
413 corrected_index = node_storage.len() - 1;
414 g_values.insert(current_grid_index, best_g);
415 best_index.insert(current_grid_index, corrected_index);
416 }
417 } else {
418 if let Some((better_idx, better_g)) =
422 self.enhanced_best_parent(current.x, current.y, &closed_set, &node_storage)
423 {
424 if better_g < current_node.cost {
425 node_storage.push(Node::new(
426 current.x,
427 current.y,
428 better_g,
429 Some(better_idx),
430 ));
431 corrected_index = node_storage.len() - 1;
432 g_values.insert(current_grid_index, better_g);
433 best_index.insert(current_grid_index, corrected_index);
434 }
435 }
436 }
437 }
438
439 if current.x == goal_x && current.y == goal_y {
440 return Ok(self.build_path(corrected_index, &node_storage));
441 }
442
443 closed_set.insert(current_grid_index, corrected_index);
444
445 let current_cost = node_storage[corrected_index].cost;
446 let current_parent = node_storage[corrected_index].parent_index;
447
448 for &(dx, dy, _) in &self.motion {
449 let new_x = current.x + dx;
450 let new_y = current.y + dy;
451 let new_grid_index = self.grid_map.calc_index(new_x, new_y);
452 if !self.grid_map.is_valid_offset(current.x, current.y, dx, dy) {
453 continue;
454 }
455 if closed_set.contains_key(&new_grid_index) {
456 continue;
457 }
458
459 let (new_cost, new_parent_index) = if let Some(p_idx) = current_parent {
461 let parent_node = &node_storage[p_idx];
462 let dist = self.euclidean_distance(parent_node.x, parent_node.y, new_x, new_y);
463 let cost_via_parent = parent_node.cost + dist;
464
465 let dist_via_current =
466 self.euclidean_distance(current.x, current.y, new_x, new_y);
467 let cost_via_current = current_cost + dist_via_current;
468
469 if cost_via_parent < cost_via_current {
470 (cost_via_parent, Some(p_idx))
471 } else {
472 (cost_via_current, Some(corrected_index))
473 }
474 } else {
475 let dist = self.euclidean_distance(current.x, current.y, new_x, new_y);
476 (current_cost + dist, Some(corrected_index))
477 };
478
479 let existing_g = g_values
480 .get(&new_grid_index)
481 .copied()
482 .unwrap_or(f64::INFINITY);
483 if new_cost < existing_g {
484 g_values.insert(new_grid_index, new_cost);
485 node_storage.push(Node::new(new_x, new_y, new_cost, new_parent_index));
486 let new_index = node_storage.len() - 1;
487 best_index.insert(new_grid_index, new_index);
488 let priority = new_cost + self.calc_heuristic(new_x, new_y, goal_x, goal_y);
489 open_set.push(PriorityNode {
490 x: new_x,
491 y: new_y,
492 cost: new_cost,
493 priority,
494 index: new_index,
495 });
496 }
497 }
498 }
499
500 Err(RoboticsError::PlanningError("No path found".to_string()))
501 }
502}
503
504impl PathPlanner for EnhancedLazyThetaStarPlanner {
505 fn plan(&self, start: Point2D, goal: Point2D) -> Result<Path2D, RoboticsError> {
506 self.plan_impl(start, goal)
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use rust_robotics_core::Obstacles;
514
515 fn create_simple_obstacles() -> (Vec<f64>, Vec<f64>) {
516 let mut ox = Vec::new();
517 let mut oy = Vec::new();
518 for i in 0..21 {
519 ox.push(i as f64);
520 oy.push(0.0);
521 ox.push(i as f64);
522 oy.push(20.0);
523 ox.push(0.0);
524 oy.push(i as f64);
525 ox.push(20.0);
526 oy.push(i as f64);
527 }
528 for i in 5..15 {
529 ox.push(10.0);
530 oy.push(i as f64);
531 }
532 (ox, oy)
533 }
534
535 #[test]
536 fn test_enhanced_finds_path() {
537 let (ox, oy) = create_simple_obstacles();
538 let planner = EnhancedLazyThetaStarPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
539 let result = planner.plan(Point2D::new(2.0, 10.0), Point2D::new(18.0, 10.0));
540 assert!(result.is_ok());
541 assert!(!result.unwrap().is_empty());
542 }
543
544 #[test]
545 fn test_enhanced_shorter_than_a_star() {
546 let (ox, oy) = create_simple_obstacles();
547 let planner = EnhancedLazyThetaStarPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
548 let a_star = crate::a_star::AStarPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
549 let start = Point2D::new(2.0, 2.0);
550 let goal = Point2D::new(18.0, 18.0);
551 let enhanced_path = planner.plan(start, goal).unwrap();
552 let a_star_path = a_star.plan(start, goal).unwrap();
553 assert!(
554 enhanced_path.total_length() <= a_star_path.total_length() + 0.1,
555 "Enhanced ({:.2}) should not be longer than A* ({:.2})",
556 enhanced_path.total_length(),
557 a_star_path.total_length()
558 );
559 }
560
561 #[test]
562 fn test_enhanced_at_least_as_good_as_lazy_theta() {
563 let (ox, oy) = create_simple_obstacles();
564 let enhanced = EnhancedLazyThetaStarPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
565 let lazy = crate::lazy_theta_star::LazyThetaStarPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
566 let theta = crate::theta_star::ThetaStarPlanner::from_obstacles(&ox, &oy, 1.0, 0.5);
567
568 let start = Point2D::new(2.0, 10.0);
569 let goal = Point2D::new(18.0, 10.0);
570
571 let enhanced_len = enhanced.plan(start, goal).unwrap().total_length();
572 let lazy_len = lazy.plan(start, goal).unwrap().total_length();
573 let theta_len = theta.plan(start, goal).unwrap().total_length();
574
575 let best_existing = lazy_len.min(theta_len);
577 assert!(
578 enhanced_len <= best_existing + 0.5,
579 "Enhanced ({:.2}) should be at least as good as best existing ({:.2}, lazy={:.2}, theta={:.2})",
580 enhanced_len,
581 best_existing,
582 lazy_len,
583 theta_len
584 );
585 }
586
587 #[test]
588 fn test_enhanced_from_obstacle_points() {
589 let (ox, oy) = create_simple_obstacles();
590 let obstacles = Obstacles::try_from_xy(&ox, &oy).unwrap();
591 let planner = EnhancedLazyThetaStarPlanner::from_obstacle_points(
592 &obstacles,
593 EnhancedLazyThetaStarConfig::default(),
594 )
595 .unwrap();
596 let path = planner.plan_xy(2.0, 10.0, 18.0, 10.0).unwrap();
597 assert!(!path.is_empty());
598 }
599
600 #[test]
601 fn test_enhanced_rejects_invalid_config() {
602 let (ox, oy) = create_simple_obstacles();
603 let config = EnhancedLazyThetaStarConfig {
604 heuristic_weight: 0.0,
605 ..Default::default()
606 };
607 assert!(EnhancedLazyThetaStarPlanner::try_new(&ox, &oy, config).is_err());
608 }
609}