1use std::cmp::Ordering;
8use std::collections::{BinaryHeap, HashMap};
9use std::f64::consts::PI;
10
11use crate::grid::GridMap;
12use crate::reeds_shepp_path;
13use rust_robotics_core::{RoboticsError, RoboticsResult};
14
15#[derive(Debug, Clone)]
17pub struct HybridAStarConfig {
18 pub xy_resolution: f64,
20 pub yaw_resolution: f64,
22 pub wheelbase: f64,
24 pub max_steer: f64,
26 pub n_steer: usize,
28 pub step_size: f64,
30 pub robot_radius: f64,
32 pub max_curvature: f64,
34 pub switch_back_cost: f64,
36 pub steer_cost: f64,
38 pub steer_change_cost: f64,
40 pub h_cost: f64,
42 pub analytic_expansion_interval: usize,
44}
45
46impl Default for HybridAStarConfig {
47 fn default() -> Self {
48 let wheelbase = 3.0;
49 let max_steer: f64 = 0.6;
50 let max_curvature = (max_steer).tan() / wheelbase;
51 Self {
52 xy_resolution: 2.0,
53 yaw_resolution: PI / 36.0, wheelbase,
55 max_steer,
56 n_steer: 20,
57 step_size: 1.5,
58 robot_radius: 1.5,
59 max_curvature,
60 switch_back_cost: 100.0,
61 steer_cost: 1.0,
62 steer_change_cost: 5.0,
63 h_cost: 1.0,
64 analytic_expansion_interval: 5,
65 }
66 }
67}
68
69impl HybridAStarConfig {
70 pub fn validate(&self) -> RoboticsResult<()> {
71 if !self.xy_resolution.is_finite() || self.xy_resolution <= 0.0 {
72 return Err(RoboticsError::InvalidParameter(format!(
73 "xy_resolution must be positive and finite, got {}",
74 self.xy_resolution
75 )));
76 }
77 if !self.yaw_resolution.is_finite() || self.yaw_resolution <= 0.0 {
78 return Err(RoboticsError::InvalidParameter(format!(
79 "yaw_resolution must be positive and finite, got {}",
80 self.yaw_resolution
81 )));
82 }
83 if !self.wheelbase.is_finite() || self.wheelbase <= 0.0 {
84 return Err(RoboticsError::InvalidParameter(format!(
85 "wheelbase must be positive and finite, got {}",
86 self.wheelbase
87 )));
88 }
89 if self.n_steer == 0 {
90 return Err(RoboticsError::InvalidParameter(
91 "n_steer must be at least 1".to_string(),
92 ));
93 }
94 Ok(())
95 }
96}
97
98#[derive(Debug, Clone)]
100struct HybridNode {
101 x_index: i32,
103 y_index: i32,
105 yaw_index: i32,
107 direction: bool,
109 x_list: Vec<f64>,
111 y_list: Vec<f64>,
113 yaw_list: Vec<f64>,
115 directions: Vec<i32>,
117 steer: f64,
119 cost: f64,
121 parent_index: Option<usize>,
123}
124
125#[derive(Debug)]
127struct PriorityNode {
128 index: usize,
129 cost: f64,
130}
131
132impl Eq for PriorityNode {}
133
134impl PartialEq for PriorityNode {
135 fn eq(&self, other: &Self) -> bool {
136 self.cost == other.cost
137 }
138}
139
140impl Ord for PriorityNode {
141 fn cmp(&self, other: &Self) -> Ordering {
142 other
143 .cost
144 .partial_cmp(&self.cost)
145 .unwrap_or(Ordering::Equal)
146 }
147}
148
149impl PartialOrd for PriorityNode {
150 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
151 Some(self.cmp(other))
152 }
153}
154
155#[derive(Debug, Clone)]
157pub struct HybridAStarPath {
158 pub x: Vec<f64>,
160 pub y: Vec<f64>,
162 pub yaw: Vec<f64>,
164 pub directions: Vec<i32>,
166}
167
168impl HybridAStarPath {
169 pub fn is_empty(&self) -> bool {
170 self.x.is_empty()
171 }
172
173 pub fn len(&self) -> usize {
174 self.x.len()
175 }
176}
177
178pub struct HybridAStarPlanner {
180 config: HybridAStarConfig,
181 grid_map: GridMap,
182 h_map: Vec<Vec<f64>>,
184 goal_x: i32,
185 goal_y: i32,
186}
187
188impl HybridAStarPlanner {
189 pub fn new(ox: &[f64], oy: &[f64], config: HybridAStarConfig) -> RoboticsResult<Self> {
191 config.validate()?;
192 let grid_map = GridMap::try_new(ox, oy, config.xy_resolution, config.robot_radius)?;
193
194 Ok(Self {
195 config,
196 grid_map,
197 h_map: Vec::new(),
198 goal_x: 0,
199 goal_y: 0,
200 })
201 }
202
203 pub fn plan(
205 &mut self,
206 sx: f64,
207 sy: f64,
208 syaw: f64,
209 gx: f64,
210 gy: f64,
211 gyaw: f64,
212 ) -> RoboticsResult<HybridAStarPath> {
213 self.goal_x = self.grid_map.calc_x_index(gx);
215 self.goal_y = self.grid_map.calc_y_index(gy);
216
217 self.h_map = self.compute_holonomic_heuristic();
219
220 let start_yaw_index = self.calc_yaw_index(syaw);
221 let start_node = HybridNode {
222 x_index: self.grid_map.calc_x_index(sx),
223 y_index: self.grid_map.calc_y_index(sy),
224 yaw_index: start_yaw_index,
225 direction: true,
226 x_list: vec![sx],
227 y_list: vec![sy],
228 yaw_list: vec![syaw],
229 directions: vec![1],
230 steer: 0.0,
231 cost: 0.0,
232 parent_index: None,
233 };
234
235 let goal_yaw_index = self.calc_yaw_index(gyaw);
236 let goal_node = HybridNode {
237 x_index: self.goal_x,
238 y_index: self.goal_y,
239 yaw_index: goal_yaw_index,
240 direction: true,
241 x_list: vec![gx],
242 y_list: vec![gy],
243 yaw_list: vec![gyaw],
244 directions: vec![1],
245 steer: 0.0,
246 cost: 0.0,
247 parent_index: None,
248 };
249
250 let mut open_set = BinaryHeap::new();
251 let mut closed_set: HashMap<i64, usize> = HashMap::new();
252 let mut node_storage: Vec<HybridNode> = Vec::new();
253
254 node_storage.push(start_node.clone());
255 let start_idx = 0;
256 let start_key = self.calc_hybrid_index(&node_storage[start_idx]);
257
258 let h = self.calc_heuristic(&node_storage[start_idx]);
259 open_set.push(PriorityNode {
260 index: start_idx,
261 cost: node_storage[start_idx].cost + h,
262 });
263
264 let mut open_map: HashMap<i64, usize> = HashMap::new();
265 open_map.insert(start_key, start_idx);
266
267 let mut iteration = 0;
268
269 while let Some(current_priority) = open_set.pop() {
270 let current_idx = current_priority.index;
271 let current_key = self.calc_hybrid_index(&node_storage[current_idx]);
272
273 if closed_set.contains_key(¤t_key) {
274 continue;
275 }
276
277 closed_set.insert(current_key, current_idx);
278 open_map.remove(¤t_key);
279
280 if self.is_goal(&node_storage[current_idx], &goal_node) {
282 return Ok(self.build_path(current_idx, &node_storage, None));
283 }
284
285 iteration += 1;
287 if iteration % self.config.analytic_expansion_interval == 0 {
288 if let Some(rs) = self.analytic_expansion(&node_storage[current_idx], &goal_node) {
289 return Ok(self.build_path(current_idx, &node_storage, Some(&rs)));
290 }
291 }
292
293 let neighbors = self.get_neighbors(&node_storage[current_idx], current_idx);
295 for neighbor in neighbors {
296 let neighbor_key = self.calc_hybrid_index(&neighbor);
297
298 if closed_set.contains_key(&neighbor_key) {
299 continue;
300 }
301
302 if let Some(&existing_idx) = open_map.get(&neighbor_key) {
303 if neighbor.cost < node_storage[existing_idx].cost {
304 node_storage.push(neighbor);
305 let new_idx = node_storage.len() - 1;
306 open_map.insert(neighbor_key, new_idx);
307 let h = self.calc_heuristic(&node_storage[new_idx]);
308 open_set.push(PriorityNode {
309 index: new_idx,
310 cost: node_storage[new_idx].cost + h,
311 });
312 }
313 } else {
314 node_storage.push(neighbor);
315 let new_idx = node_storage.len() - 1;
316 open_map.insert(neighbor_key, new_idx);
317 let h = self.calc_heuristic(&node_storage[new_idx]);
318 open_set.push(PriorityNode {
319 index: new_idx,
320 cost: node_storage[new_idx].cost + h,
321 });
322 }
323 }
324 }
325
326 Err(RoboticsError::PlanningError(
327 "Hybrid A*: no path found".to_string(),
328 ))
329 }
330
331 fn calc_yaw_index(&self, yaw: f64) -> i32 {
333 let normalized = pi_2_pi(yaw);
334 ((normalized + PI) / self.config.yaw_resolution).round() as i32
335 }
336
337 fn calc_hybrid_index(&self, node: &HybridNode) -> i64 {
339 let n_yaw = ((2.0 * PI) / self.config.yaw_resolution).round() as i64 + 1;
340 (node.yaw_index as i64)
341 + n_yaw * (node.y_index as i64)
342 + n_yaw * (self.grid_map.y_width as i64) * (node.x_index as i64)
343 }
344
345 fn is_goal(&self, node: &HybridNode, goal: &HybridNode) -> bool {
347 node.x_index == goal.x_index
348 && node.y_index == goal.y_index
349 && node.yaw_index == goal.yaw_index
350 }
351
352 fn calc_heuristic(&self, node: &HybridNode) -> f64 {
354 let ix = node.x_index;
355 let iy = node.y_index;
356
357 if ix >= 0 && ix < self.grid_map.x_width && iy >= 0 && iy < self.grid_map.y_width {
358 self.h_map[ix as usize][iy as usize] * self.config.h_cost
359 } else {
360 let dx = (node.x_index - self.goal_x) as f64;
362 let dy = (node.y_index - self.goal_y) as f64;
363 (dx * dx + dy * dy).sqrt() * self.config.h_cost
364 }
365 }
366
367 fn get_neighbors(&self, current: &HybridNode, parent_idx: usize) -> Vec<HybridNode> {
369 let mut neighbors = Vec::new();
370
371 let n_steer = self.config.n_steer as f64;
372 let steer_step = 2.0 * self.config.max_steer / n_steer;
373
374 for i in 0..=self.config.n_steer {
376 let steer = -self.config.max_steer + i as f64 * steer_step;
377
378 for &direction in &[1.0_f64, -1.0] {
380 let current_x = *current.x_list.last().unwrap();
381 let current_y = *current.y_list.last().unwrap();
382 let current_yaw = *current.yaw_list.last().unwrap();
383
384 if let Some(node) = self.simulate_motion(
385 current_x,
386 current_y,
387 current_yaw,
388 steer,
389 direction,
390 current,
391 parent_idx,
392 ) {
393 neighbors.push(node);
394 }
395 }
396 }
397
398 neighbors
399 }
400
401 #[allow(clippy::too_many_arguments)]
403 fn simulate_motion(
404 &self,
405 x: f64,
406 y: f64,
407 yaw: f64,
408 steer: f64,
409 direction: f64,
410 parent: &HybridNode,
411 parent_idx: usize,
412 ) -> Option<HybridNode> {
413 let arc_length = self.config.xy_resolution * 1.5;
414 let n_steps = (arc_length / self.config.step_size).ceil() as usize;
415 let d = direction * self.config.step_size;
416
417 let mut cx = x;
418 let mut cy = y;
419 let mut cyaw = yaw;
420
421 let mut x_list = vec![cx];
422 let mut y_list = vec![cy];
423 let mut yaw_list = vec![cyaw];
424
425 for _ in 0..n_steps {
426 cx += d * cyaw.cos();
427 cy += d * cyaw.sin();
428 cyaw += d * steer.tan() / self.config.wheelbase;
429 cyaw = pi_2_pi(cyaw);
430
431 x_list.push(cx);
432 y_list.push(cy);
433 yaw_list.push(cyaw);
434 }
435
436 if !self.verify_path(&x_list, &y_list) {
438 return None;
439 }
440
441 let dir_val = if direction > 0.0 { 1 } else { -1 };
442 let directions = vec![dir_val; x_list.len()];
443
444 let mut cost = parent.cost;
446
447 cost += arc_length;
449
450 cost += self.config.steer_cost * steer.abs();
452
453 cost += self.config.steer_change_cost * (steer - parent.steer).abs();
455
456 let parent_dir = if parent.direction { 1.0 } else { -1.0 };
458 if direction != parent_dir {
459 cost += self.config.switch_back_cost;
460 }
461
462 let x_idx = self.grid_map.calc_x_index(cx);
463 let y_idx = self.grid_map.calc_y_index(cy);
464 let yaw_idx = self.calc_yaw_index(cyaw);
465
466 Some(HybridNode {
467 x_index: x_idx,
468 y_index: y_idx,
469 yaw_index: yaw_idx,
470 direction: direction > 0.0,
471 x_list,
472 y_list,
473 yaw_list,
474 directions,
475 steer,
476 cost,
477 parent_index: Some(parent_idx),
478 })
479 }
480
481 fn verify_path(&self, x_list: &[f64], y_list: &[f64]) -> bool {
483 for (&x, &y) in x_list.iter().zip(y_list.iter()) {
484 let ix = self.grid_map.calc_x_index(x);
485 let iy = self.grid_map.calc_y_index(y);
486 if !self.grid_map.is_valid(ix, iy) {
487 return false;
488 }
489 }
490 true
491 }
492
493 fn analytic_expansion(&self, current: &HybridNode, goal: &HybridNode) -> Option<HybridNode> {
495 let sx = *current.x_list.last().unwrap();
496 let sy = *current.y_list.last().unwrap();
497 let syaw = *current.yaw_list.last().unwrap();
498
499 let gx = *goal.x_list.last().unwrap();
500 let gy = *goal.y_list.last().unwrap();
501 let gyaw = *goal.yaw_list.last().unwrap();
502
503 let result = reeds_shepp_path::reeds_shepp_path_planning(
504 sx,
505 sy,
506 syaw,
507 gx,
508 gy,
509 gyaw,
510 self.config.max_curvature,
511 self.config.step_size,
512 );
513
514 let (path_x, path_y, path_yaw, _ctypes, _lengths) = result?;
515
516 if path_x.is_empty() {
517 return None;
518 }
519
520 if !self.verify_path(&path_x, &path_y) {
522 return None;
523 }
524
525 let directions: Vec<i32> = path_yaw
527 .windows(2)
528 .map(|w| {
529 let diff = pi_2_pi(w[1] - w[0]);
530 if diff < -1e-6 {
531 -1
532 } else {
533 1
534 }
535 })
536 .chain(std::iter::once(1))
537 .collect();
538
539 let last_x = *path_x.last().unwrap();
540 let last_y = *path_y.last().unwrap();
541 let last_yaw = *path_yaw.last().unwrap();
542
543 Some(HybridNode {
544 x_index: self.grid_map.calc_x_index(last_x),
545 y_index: self.grid_map.calc_y_index(last_y),
546 yaw_index: self.calc_yaw_index(last_yaw),
547 direction: true,
548 x_list: path_x,
549 y_list: path_y,
550 yaw_list: path_yaw,
551 directions,
552 steer: 0.0,
553 cost: 0.0,
554 parent_index: None,
555 })
556 }
557
558 fn build_path(
560 &self,
561 final_idx: usize,
562 storage: &[HybridNode],
563 rs_segment: Option<&HybridNode>,
564 ) -> HybridAStarPath {
565 #[allow(clippy::type_complexity)]
566 let mut segments: Vec<(Vec<f64>, Vec<f64>, Vec<f64>, Vec<i32>)> = Vec::new();
567
568 let mut current_idx = Some(final_idx);
569 while let Some(idx) = current_idx {
570 let node = &storage[idx];
571 segments.push((
572 node.x_list.clone(),
573 node.y_list.clone(),
574 node.yaw_list.clone(),
575 node.directions.clone(),
576 ));
577 current_idx = node.parent_index;
578 }
579
580 segments.reverse();
581
582 let mut x = Vec::new();
583 let mut y = Vec::new();
584 let mut yaw = Vec::new();
585 let mut directions = Vec::new();
586
587 for (sx, sy, syaw, sd) in &segments {
588 x.extend_from_slice(sx);
589 y.extend_from_slice(sy);
590 yaw.extend_from_slice(syaw);
591 directions.extend_from_slice(sd);
592 }
593
594 if let Some(rs) = rs_segment {
596 x.extend_from_slice(&rs.x_list);
597 y.extend_from_slice(&rs.y_list);
598 yaw.extend_from_slice(&rs.yaw_list);
599 directions.extend_from_slice(&rs.directions);
600 }
601
602 HybridAStarPath {
603 x,
604 y,
605 yaw,
606 directions,
607 }
608 }
609
610 fn compute_holonomic_heuristic(&self) -> Vec<Vec<f64>> {
612 let xw = self.grid_map.x_width as usize;
613 let yw = self.grid_map.y_width as usize;
614
615 let mut cost_map = vec![vec![f64::INFINITY; yw]; xw];
616
617 let gx = self.goal_x;
618 let gy = self.goal_y;
619
620 if gx < 0 || gx >= self.grid_map.x_width || gy < 0 || gy >= self.grid_map.y_width {
621 return cost_map;
622 }
623
624 cost_map[gx as usize][gy as usize] = 0.0;
625
626 let mut open_set = BinaryHeap::new();
627 open_set.push(DijkstraNode {
628 x: gx,
629 y: gy,
630 cost: 0.0,
631 });
632
633 let motions: [(i32, i32, f64); 8] = [
634 (1, 0, 1.0),
635 (0, 1, 1.0),
636 (-1, 0, 1.0),
637 (0, -1, 1.0),
638 (-1, -1, std::f64::consts::SQRT_2),
639 (-1, 1, std::f64::consts::SQRT_2),
640 (1, -1, std::f64::consts::SQRT_2),
641 (1, 1, std::f64::consts::SQRT_2),
642 ];
643
644 while let Some(current) = open_set.pop() {
645 if current.cost > cost_map[current.x as usize][current.y as usize] {
646 continue;
647 }
648
649 for &(dx, dy, mc) in &motions {
650 let nx = current.x + dx;
651 let ny = current.y + dy;
652
653 if nx < 0 || nx >= self.grid_map.x_width || ny < 0 || ny >= self.grid_map.y_width {
654 continue;
655 }
656
657 if self.grid_map.obstacle_map[nx as usize][ny as usize] {
658 continue;
659 }
660
661 let new_cost = current.cost + mc;
662 if new_cost < cost_map[nx as usize][ny as usize] {
663 cost_map[nx as usize][ny as usize] = new_cost;
664 open_set.push(DijkstraNode {
665 x: nx,
666 y: ny,
667 cost: new_cost,
668 });
669 }
670 }
671 }
672
673 cost_map
674 }
675}
676
677#[derive(Debug)]
679struct DijkstraNode {
680 x: i32,
681 y: i32,
682 cost: f64,
683}
684
685impl Eq for DijkstraNode {}
686
687impl PartialEq for DijkstraNode {
688 fn eq(&self, other: &Self) -> bool {
689 self.cost == other.cost
690 }
691}
692
693impl Ord for DijkstraNode {
694 fn cmp(&self, other: &Self) -> Ordering {
695 other
696 .cost
697 .partial_cmp(&self.cost)
698 .unwrap_or(Ordering::Equal)
699 }
700}
701
702impl PartialOrd for DijkstraNode {
703 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
704 Some(self.cmp(other))
705 }
706}
707
708fn pi_2_pi(angle: f64) -> f64 {
710 let mut a = angle;
711 while a > PI {
712 a -= 2.0 * PI;
713 }
714 while a < -PI {
715 a += 2.0 * PI;
716 }
717 a
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723
724 fn create_boundary_obstacles(
726 x_min: f64,
727 x_max: f64,
728 y_min: f64,
729 y_max: f64,
730 step: f64,
731 ) -> (Vec<f64>, Vec<f64>) {
732 let mut ox = Vec::new();
733 let mut oy = Vec::new();
734
735 let mut x = x_min;
736 while x <= x_max {
737 ox.push(x);
738 oy.push(y_min);
739 ox.push(x);
740 oy.push(y_max);
741 x += step;
742 }
743
744 let mut y = y_min;
745 while y <= y_max {
746 ox.push(x_min);
747 oy.push(y);
748 ox.push(x_max);
749 oy.push(y);
750 y += step;
751 }
752
753 (ox, oy)
754 }
755
756 #[test]
757 fn test_open_space_path() {
758 let (ox, oy) = create_boundary_obstacles(0.0, 50.0, 0.0, 50.0, 1.0);
759
760 let config = HybridAStarConfig {
761 xy_resolution: 2.0,
762 yaw_resolution: PI / 18.0,
763 robot_radius: 1.0,
764 ..Default::default()
765 };
766
767 let mut planner = HybridAStarPlanner::new(&ox, &oy, config).unwrap();
768
769 let result = planner.plan(10.0, 10.0, 0.0, 40.0, 40.0, 0.0);
770 assert!(result.is_ok(), "Planning failed: {:?}", result.err());
771
772 let path = result.unwrap();
773 assert!(!path.is_empty());
774 assert_eq!(path.x.len(), path.y.len());
775 assert_eq!(path.x.len(), path.yaw.len());
776
777 let start_dist = ((path.x[0] - 10.0).powi(2) + (path.y[0] - 10.0).powi(2)).sqrt();
779 assert!(
780 start_dist < 5.0,
781 "Path start too far from start: {}",
782 start_dist
783 );
784
785 let last = path.x.len() - 1;
786 let goal_dist = ((path.x[last] - 40.0).powi(2) + (path.y[last] - 40.0).powi(2)).sqrt();
787 assert!(
788 goal_dist < 10.0,
789 "Path end too far from goal: {}",
790 goal_dist
791 );
792 }
793
794 #[test]
795 fn test_path_around_obstacle() {
796 let (mut ox, mut oy) = create_boundary_obstacles(0.0, 50.0, 0.0, 50.0, 1.0);
797
798 let mut y = 10.0;
800 while y <= 40.0 {
801 ox.push(25.0);
802 oy.push(y);
803 y += 1.0;
804 }
805
806 let config = HybridAStarConfig {
807 xy_resolution: 2.0,
808 yaw_resolution: PI / 18.0,
809 robot_radius: 1.0,
810 ..Default::default()
811 };
812
813 let mut planner = HybridAStarPlanner::new(&ox, &oy, config).unwrap();
814
815 let result = planner.plan(10.0, 25.0, 0.0, 40.0, 25.0, 0.0);
816 assert!(
817 result.is_ok(),
818 "Planning with obstacle failed: {:?}",
819 result.err()
820 );
821
822 let path = result.unwrap();
823 assert!(!path.is_empty());
824 assert!(path.len() > 5, "Path should have multiple waypoints");
825 }
826
827 #[test]
828 fn test_goal_tolerance() {
829 let (ox, oy) = create_boundary_obstacles(0.0, 50.0, 0.0, 50.0, 1.0);
830
831 let config = HybridAStarConfig {
832 xy_resolution: 2.0,
833 yaw_resolution: PI / 18.0,
834 robot_radius: 1.0,
835 ..Default::default()
836 };
837
838 let mut planner = HybridAStarPlanner::new(&ox, &oy, config).unwrap();
839
840 let result = planner.plan(10.0, 10.0, 0.0, 35.0, 35.0, PI / 2.0);
841 assert!(result.is_ok(), "Planning failed: {:?}", result.err());
842
843 let path = result.unwrap();
844 let last = path.x.len() - 1;
845 let goal_dist = ((path.x[last] - 35.0).powi(2) + (path.y[last] - 35.0).powi(2)).sqrt();
846 assert!(
847 goal_dist < 15.0,
848 "Path end too far from goal: dist={}",
849 goal_dist
850 );
851 }
852
853 #[test]
854 fn test_invalid_config() {
855 let (ox, oy) = create_boundary_obstacles(0.0, 50.0, 0.0, 50.0, 1.0);
856
857 let config = HybridAStarConfig {
858 xy_resolution: 0.0,
859 ..Default::default()
860 };
861
862 let result = HybridAStarPlanner::new(&ox, &oy, config);
863 assert!(result.is_err());
864 }
865
866 #[test]
867 fn test_path_result_consistency() {
868 let (ox, oy) = create_boundary_obstacles(0.0, 50.0, 0.0, 50.0, 1.0);
869
870 let config = HybridAStarConfig {
871 xy_resolution: 2.0,
872 yaw_resolution: PI / 18.0,
873 robot_radius: 1.0,
874 ..Default::default()
875 };
876
877 let mut planner = HybridAStarPlanner::new(&ox, &oy, config).unwrap();
878
879 let result = planner.plan(10.0, 10.0, 0.0, 30.0, 30.0, 0.0);
880 assert!(result.is_ok());
881
882 let path = result.unwrap();
883 assert_eq!(path.x.len(), path.y.len());
885 assert_eq!(path.x.len(), path.yaw.len());
886 assert_eq!(path.x.len(), path.directions.len());
887
888 for &yaw in &path.yaw {
890 assert!(
891 (-PI - 0.01..=PI + 0.01).contains(&yaw),
892 "Yaw out of range: {}",
893 yaw
894 );
895 }
896
897 for &d in &path.directions {
899 assert!(d == 1 || d == -1, "Invalid direction: {}", d);
900 }
901 }
902}