1use ndarray::{Array1, ArrayView1};
20use rand::rngs::StdRng;
21use rand::{Rng, SeedableRng};
22use std::fmt::Debug;
23
24use crate::distance::EuclideanDistance;
25use crate::error::{SpatialError, SpatialResult};
26use crate::kdtree::KDTree;
27use crate::pathplanning::astar::Path;
28type CollisionCheckFn = Box<dyn Fn(&Array1<f64>, &Array1<f64>) -> bool>;
32
33#[derive(Clone, Debug)]
35pub struct RRTConfig {
36 pub max_iterations: usize,
38 pub step_size: f64,
40 pub goal_bias: f64,
42 pub seed: Option<u64>,
44 pub use_rrt_star: bool,
46 pub neighborhood_radius: Option<f64>,
48 pub bidirectional: bool,
50}
51
52impl Default for RRTConfig {
53 fn default() -> Self {
54 RRTConfig {
55 max_iterations: 10000,
56 step_size: 0.5,
57 goal_bias: 0.05,
58 seed: None,
59 use_rrt_star: false,
60 neighborhood_radius: None,
61 bidirectional: false,
62 }
63 }
64}
65
66#[derive(Clone, Debug)]
68struct RRTNode {
69 position: Array1<f64>,
71 parent: Option<usize>,
73 cost: f64,
75}
76
77pub struct RRTPlanner {
79 config: RRTConfig,
81 collision_checker: Option<CollisionCheckFn>,
83 rng: StdRng,
85 dimension: usize,
87 bounds: Option<(Array1<f64>, Array1<f64>)>,
89}
90
91impl Debug for RRTPlanner {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("RRTPlanner")
94 .field("config", &self.config)
95 .field("dimension", &self.dimension)
96 .field("bounds", &self.bounds)
97 .field("collision_checker", &"<function>")
98 .finish()
99 }
100}
101
102impl Clone for RRTPlanner {
103 fn clone(&self) -> Self {
104 Self {
105 config: self.config.clone(),
106 collision_checker: None, rng: StdRng::seed_from_u64(rand::random()), dimension: self.dimension,
109 bounds: self.bounds.clone(),
110 }
111 }
112}
113
114impl RRTPlanner {
115 pub fn new(config: RRTConfig, dimension: usize) -> Self {
117 let seed = config.seed.unwrap_or_else(rand::random);
118 let rng = StdRng::seed_from_u64(seed);
119
120 RRTPlanner {
121 config,
122 collision_checker: None,
123 rng,
124 dimension,
125 bounds: None,
126 }
127 }
128
129 pub fn with_collision_checker<F>(mut self, collisionchecker: F) -> Self
133 where
134 F: Fn(&Array1<f64>, &Array1<f64>) -> bool + 'static,
135 {
136 self.collision_checker = Some(Box::new(collisionchecker));
137 self
138 }
139
140 pub fn with_bounds(
142 mut self,
143 min_bounds: Array1<f64>,
144 max_bounds: Array1<f64>,
145 ) -> SpatialResult<Self> {
146 if min_bounds.len() != self.dimension || max_bounds.len() != self.dimension {
147 return Err(SpatialError::DimensionError(format!(
148 "Bounds dimensions ({}, {}) don't match planner dimension ({})",
149 min_bounds.len(),
150 max_bounds.len(),
151 self.dimension
152 )));
153 }
154
155 for i in 0..self.dimension {
157 if min_bounds[i] >= max_bounds[i] {
158 return Err(SpatialError::ValueError(format!(
159 "Min bound {} is not less than max bound {} at index {}",
160 min_bounds[i], max_bounds[i], i
161 )));
162 }
163 }
164
165 self.bounds = Some((min_bounds, max_bounds));
166 Ok(self)
167 }
168
169 fn sample_random_point(&mut self) -> SpatialResult<Array1<f64>> {
171 let (min_bounds, max_bounds) = self.bounds.as_ref().ok_or_else(|| {
172 SpatialError::ValueError("Bounds must be set before sampling".to_string())
173 })?;
174 let mut point = Array1::zeros(self.dimension);
175
176 for i in 0..self.dimension {
177 point[i] = self.rng.gen_range(min_bounds[i]..max_bounds[i]);
178 }
179
180 Ok(point)
181 }
182
183 fn sample_with_goal_bias(&mut self, goal: &ArrayView1<f64>) -> SpatialResult<Array1<f64>> {
185 if self.rng.gen_range(0.0..1.0) < self.config.goal_bias {
186 Ok(goal.to_owned())
187 } else {
188 self.sample_random_point()
189 }
190 }
191
192 fn find_nearest_node(
194 &self,
195 point: &ArrayView1<f64>,
196 _nodes: &[RRTNode],
197 kdtree: &KDTree<f64, EuclideanDistance<f64>>,
198 ) -> SpatialResult<usize> {
199 let point_vec = point.to_vec();
200 let (indices, _) = kdtree.query(point_vec.as_slice(), 1)?;
201 Ok(indices[0])
202 }
203
204 fn steer(&self, nearest: &ArrayView1<f64>, randompoint: &ArrayView1<f64>) -> Array1<f64> {
206 let mut direction = randompoint - nearest;
207 let norm = direction.iter().map(|&x| x * x).sum::<f64>().sqrt();
208
209 if norm < 1e-10 {
210 return nearest.to_owned();
211 }
212
213 if norm > self.config.step_size {
215 direction *= self.config.step_size / norm;
216 }
217
218 nearest + direction
219 }
220
221 fn is_valid_connection(&self, from: &ArrayView1<f64>, to: &ArrayView1<f64>) -> bool {
223 if let Some(ref collision_checker) = self.collision_checker {
224 !collision_checker(&from.to_owned(), &to.to_owned())
225 } else {
226 true }
228 }
229
230 pub fn find_path(
232 &mut self,
233 start: ArrayView1<f64>,
234 goal: ArrayView1<f64>,
235 goal_threshold: f64,
236 ) -> SpatialResult<Option<Path<Array1<f64>>>> {
237 if start.len() != self.dimension || goal.len() != self.dimension {
238 return Err(SpatialError::DimensionError(format!(
239 "Start or goal dimensions ({}, {}) don't match planner dimension ({})",
240 start.len(),
241 goal.len(),
242 self.dimension
243 )));
244 }
245
246 if self.bounds.is_none() {
247 return Err(SpatialError::ValueError(
248 "Bounds must be set before planning".to_string(),
249 ));
250 }
251
252 if self.config.bidirectional {
253 self.find_path_bidirectional(start, goal, goal_threshold)
254 } else if self.config.use_rrt_star {
255 self.find_path_rrt_star(start, goal, goal_threshold)
256 } else {
257 self.find_path_basic_rrt(start, goal, goal_threshold)
258 }
259 }
260
261 fn find_path_basic_rrt(
263 &mut self,
264 start: ArrayView1<f64>,
265 goal: ArrayView1<f64>,
266 goal_threshold: f64,
267 ) -> SpatialResult<Option<Path<Array1<f64>>>> {
268 let mut nodes = vec![RRTNode {
270 position: start.to_owned(),
271 parent: None,
272 cost: 0.0,
273 }];
274
275 for _ in 0..self.config.max_iterations {
276 let randompoint = self.sample_with_goal_bias(&goal)?;
278
279 let points: Vec<_> = nodes.iter().map(|node| node.position.clone()).collect();
281 let points_array = ndarray::stack(
282 ndarray::Axis(0),
283 &points.iter().map(|p| p.view()).collect::<Vec<_>>(),
284 )
285 .expect("Failed to stack points");
286 let kdtree = KDTree::<f64, EuclideanDistance<f64>>::new(&points_array)
287 .expect("Failed to build KDTree");
288
289 let nearest_idx = self.find_nearest_node(&randompoint.view(), &nodes, &kdtree)?;
291
292 let nearest_position = nodes[nearest_idx].position.clone();
294 let nearest_cost = nodes[nearest_idx].cost;
295
296 let new_point = self.steer(&nearest_position.view(), &randompoint.view());
298
299 if self.is_valid_connection(&nearest_position.view(), &new_point.view()) {
301 let new_node = RRTNode {
303 position: new_point.clone(),
304 parent: Some(nearest_idx),
305 cost: nearest_cost
306 + euclidean_distance(&nearest_position.view(), &new_point.view()),
307 };
308 nodes.push(new_node);
309
310 if euclidean_distance(&new_point.view(), &goal) <= goal_threshold {
312 return Ok(Some(RRTPlanner::extract_path(&nodes, nodes.len() - 1)));
314 }
315 }
316 }
317
318 Ok(None)
320 }
321
322 fn find_path_rrt_star(
324 &mut self,
325 start: ArrayView1<f64>,
326 goal: ArrayView1<f64>,
327 goal_threshold: f64,
328 ) -> SpatialResult<Option<Path<Array1<f64>>>> {
329 let mut nodes = vec![RRTNode {
331 position: start.to_owned(),
332 parent: None,
333 cost: 0.0,
334 }];
335
336 let mut goalidx: Option<usize> = None;
338 let neighborhood_radius = self
339 .config
340 .neighborhood_radius
341 .unwrap_or(self.config.step_size * 2.0);
342
343 for _ in 0..self.config.max_iterations {
344 let randompoint = self.sample_with_goal_bias(&goal)?;
346
347 let points: Vec<_> = nodes.iter().map(|node| node.position.clone()).collect();
349 let points_array = ndarray::stack(
350 ndarray::Axis(0),
351 &points.iter().map(|p| p.view()).collect::<Vec<_>>(),
352 )
353 .expect("Failed to stack points");
354 let kdtree = KDTree::<f64, EuclideanDistance<f64>>::new(&points_array)
355 .expect("Failed to build KDTree");
356
357 let nearest_idx = self.find_nearest_node(&randompoint.view(), &nodes, &kdtree)?;
359
360 let nearest_position = nodes[nearest_idx].position.clone();
362
363 let new_point = self.steer(&nearest_position.view(), &randompoint.view());
365
366 if self.is_valid_connection(&nearest_position.view(), &new_point.view()) {
368 let (parent_idx, cost_from_parent) = self.find_best_parent(
370 &new_point,
371 &nodes,
372 &kdtree,
373 nearest_idx,
374 neighborhood_radius,
375 );
376
377 let new_node_idx = nodes.len();
379 let parent_cost = nodes[parent_idx].cost;
380 let new_node = RRTNode {
381 position: new_point.clone(),
382 parent: Some(parent_idx),
383 cost: parent_cost + cost_from_parent,
384 };
385 nodes.push(new_node);
386
387 self.rewire_tree(&mut nodes, new_node_idx, &kdtree, neighborhood_radius);
389
390 let dist_to_goal = euclidean_distance(&new_point.view(), &goal);
392 if dist_to_goal <= goal_threshold {
393 let new_cost = nodes[new_node_idx].cost + dist_to_goal;
395 if let Some(idx) = goalidx {
396 if new_cost < nodes[idx].cost {
397 goalidx = Some(new_node_idx);
398 }
399 } else {
400 goalidx = Some(new_node_idx);
401 }
402 }
403 }
404 }
405
406 if let Some(idx) = goalidx {
408 Ok(Some(RRTPlanner::extract_path(&nodes, idx)))
409 } else {
410 Ok(None)
411 }
412 }
413
414 fn find_best_parent(
416 &self,
417 new_point: &Array1<f64>,
418 nodes: &[RRTNode],
419 kdtree: &KDTree<f64, EuclideanDistance<f64>>,
420 nearest_idx: usize,
421 radius: f64,
422 ) -> (usize, f64) {
423 let mut best_parent_idx = nearest_idx;
424 let mut best_cost = nodes[nearest_idx].cost
425 + euclidean_distance(&nodes[nearest_idx].position.view(), &new_point.view());
426
427 let (near_indices, near_distances) = kdtree
429 .query_radius(new_point.as_slice().unwrap(), radius)
430 .expect("KDTree query failed");
431
432 for (_idx, &nodeidx) in near_indices.iter().enumerate() {
434 let node = &nodes[nodeidx];
435 let dist = near_distances[_idx];
436
437 let cost_from_start = node.cost + dist;
439
440 if cost_from_start < best_cost
442 && self.is_valid_connection(&node.position.view(), &new_point.view())
443 {
444 best_parent_idx = nodeidx;
445 best_cost = cost_from_start;
446 }
447 }
448
449 let cost_from_parent =
451 euclidean_distance(&nodes[best_parent_idx].position.view(), &new_point.view());
452 (best_parent_idx, cost_from_parent)
453 }
454
455 fn rewire_tree(
457 &self,
458 nodes: &mut [RRTNode],
459 new_node_idx: usize,
460 kdtree: &KDTree<f64, EuclideanDistance<f64>>,
461 radius: f64,
462 ) {
463 let new_point = nodes[new_node_idx].position.clone();
465 let new_cost = nodes[new_node_idx].cost;
466
467 let (near_indices, near_distances) = kdtree
469 .query_radius(new_point.as_slice().unwrap(), radius)
470 .expect("KDTree query failed");
471
472 for (_idx, &nodeidx) in near_indices.iter().enumerate() {
474 if nodeidx == new_node_idx {
476 continue;
477 }
478
479 let dist = near_distances[_idx];
480 let cost_through_new = new_cost + dist;
481
482 let node_position = nodes[nodeidx].position.clone();
484
485 if cost_through_new < nodes[nodeidx].cost
487 && self.is_valid_connection(&new_point.view(), &node_position.view())
488 {
489 nodes[nodeidx].parent = Some(new_node_idx);
490 nodes[nodeidx].cost = cost_through_new;
491
492 RRTPlanner::update_subtree_costs(nodes, nodeidx);
494 }
495 }
496 }
497
498 #[allow(clippy::only_used_in_recursion)]
500 fn update_subtree_costs(nodes: &mut [RRTNode], nodeidx: usize) {
501 let children: Vec<usize> = nodes
503 .iter()
504 .enumerate()
505 .filter(|(_, node)| node.parent == Some(nodeidx))
506 .map(|(idx, _)| idx)
507 .collect();
508
509 for &child_idx in &children {
511 let parent_cost = nodes[nodeidx].cost;
513 let parent_position = nodes[nodeidx].position.clone();
514 let child_position = nodes[child_idx].position.clone();
515
516 let edge_cost = euclidean_distance(&parent_position.view(), &child_position.view());
517 nodes[child_idx].cost = parent_cost + edge_cost;
518
519 Self::update_subtree_costs(nodes, child_idx);
521 }
522 }
523
524 fn find_path_bidirectional(
526 &mut self,
527 start: ArrayView1<f64>,
528 goal: ArrayView1<f64>,
529 goal_threshold: f64,
530 ) -> SpatialResult<Option<Path<Array1<f64>>>> {
531 let mut start_tree = vec![RRTNode {
533 position: start.to_owned(),
534 parent: None,
535 cost: 0.0,
536 }];
537
538 let mut goal_tree = vec![RRTNode {
539 position: goal.to_owned(),
540 parent: None,
541 cost: 0.0,
542 }];
543
544 let mut tree_a = &mut start_tree;
546 let mut tree_b = &mut goal_tree;
547 let mut a_is_start = true;
548
549 let mut connection: Option<(usize, usize)> = None;
551
552 for _ in 0..self.config.max_iterations {
553 std::mem::swap(&mut tree_a, &mut tree_b);
555 a_is_start = !a_is_start;
556
557 let target = if a_is_start {
559 goal.to_owned()
560 } else {
561 start.to_owned()
562 };
563 let randompoint = self.sample_with_goal_bias(&target.view())?;
564
565 let points_a: Vec<_> = tree_a.iter().map(|node| node.position.clone()).collect();
567 let points_array_a = ndarray::stack(
568 ndarray::Axis(0),
569 &points_a.iter().map(|p| p.view()).collect::<Vec<_>>(),
570 )
571 .expect("Failed to stack points");
572 let kdtree_a = KDTree::<f64, EuclideanDistance<f64>>::new(&points_array_a)
573 .expect("Failed to build KDTree");
574
575 let nearest_idxa = self.find_nearest_node(&randompoint.view(), tree_a, &kdtree_a)?;
577
578 let nearest_position = tree_a[nearest_idxa].position.clone();
580 let nearest_cost = tree_a[nearest_idxa].cost;
581
582 let new_point = self.steer(&nearest_position.view(), &randompoint.view());
584
585 if self.is_valid_connection(&nearest_position.view(), &new_point.view()) {
587 let new_cost =
589 nearest_cost + euclidean_distance(&nearest_position.view(), &new_point.view());
590 let new_node_idx_a = tree_a.len();
591 tree_a.push(RRTNode {
592 position: new_point.clone(),
593 parent: Some(nearest_idxa),
594 cost: new_cost,
595 });
596
597 let points_b: Vec<_> = tree_b.iter().map(|node| node.position.clone()).collect();
599 let points_array_b = ndarray::stack(
600 ndarray::Axis(0),
601 &points_b.iter().map(|p| p.view()).collect::<Vec<_>>(),
602 )
603 .expect("Failed to stack points");
604 let kdtree_b = KDTree::<f64, EuclideanDistance<f64>>::new(&points_array_b)
605 .expect("Failed to build KDTree");
606
607 let nearest_idxb = self.find_nearest_node(&new_point.view(), tree_b, &kdtree_b)?;
609
610 let nearest_position_b = tree_b[nearest_idxb].position.clone();
612
613 let dist_between_trees =
615 euclidean_distance(&new_point.view(), &nearest_position_b.view());
616 if dist_between_trees <= goal_threshold
617 && self.is_valid_connection(&new_point.view(), &nearest_position_b.view())
618 {
619 connection = if a_is_start {
621 Some((new_node_idx_a, nearest_idxb))
622 } else {
623 Some((nearest_idxb, new_node_idx_a))
624 };
625 break;
626 }
627 }
628 }
629
630 if let Some((start_idx, goalidx)) = connection {
632 let path = self.extract_bidirectional_path(&start_tree, &goal_tree, start_idx, goalidx);
633 Ok(Some(path))
634 } else {
635 Ok(None)
636 }
637 }
638
639 fn extract_bidirectional_path(
641 &self,
642 start_tree: &[RRTNode],
643 goal_tree: &[RRTNode],
644 start_idx: usize,
645 goalidx: usize,
646 ) -> Path<Array1<f64>> {
647 let mut forward_path = Vec::new();
649 let mut current_idx = Some(start_idx);
650 while let Some(_idx) = current_idx {
651 forward_path.push(start_tree[_idx].position.clone());
652 current_idx = start_tree[_idx].parent;
653 }
654 forward_path.reverse(); let mut backward_path = Vec::new();
658 let mut current_idx = Some(goalidx);
659 while let Some(_idx) = current_idx {
660 backward_path.push(goal_tree[_idx].position.clone());
661 current_idx = goal_tree[_idx].parent;
662 }
663 let mut full_path = forward_path;
667 full_path.extend(backward_path);
668
669 let mut total_cost = 0.0;
671 for i in 1..full_path.len() {
672 total_cost += euclidean_distance(&full_path[i - 1].view(), &full_path[i].view());
673 }
674
675 Path::new(full_path, total_cost)
676 }
677
678 fn extract_path(nodes: &[RRTNode], goalidx: usize) -> Path<Array1<f64>> {
680 let mut path = Vec::new();
681 let mut current_idx = Some(goalidx);
682 let cost = nodes[goalidx].cost;
683
684 while let Some(_idx) = current_idx {
685 path.push(nodes[_idx].position.clone());
686 current_idx = nodes[_idx].parent;
687 }
688
689 path.reverse();
691
692 Path::new(path, cost)
693 }
694}
695
696#[allow(dead_code)]
698fn euclidean_distance(a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> f64 {
699 let mut sum = 0.0;
700 for i in 0..a.len() {
701 let diff = a[i] - b[i];
702 sum += diff * diff;
703 }
704 sum.sqrt()
705}
706
707#[derive(Clone)]
709pub struct RRT2DPlanner {
710 planner: RRTPlanner,
712 obstacles: Vec<Vec<[f64; 2]>>,
714 _collision_step_size: f64,
716}
717
718impl RRT2DPlanner {
719 pub fn new(
721 config: RRTConfig,
722 obstacles: Vec<Vec<[f64; 2]>>,
723 min_bounds: [f64; 2],
724 max_bounds: [f64; 2],
725 collision_step_size: f64,
726 ) -> SpatialResult<Self> {
727 let mut planner = RRTPlanner::new(config, 2);
728 planner = planner.with_bounds(ndarray::arr1(&min_bounds), ndarray::arr1(&max_bounds))?;
729
730 let obstacles_clone = obstacles.clone();
731 planner = planner.with_collision_checker(move |from, to| {
732 Self::check_collision_with_obstacles(from, to, &obstacles_clone, collision_step_size)
733 });
734
735 Ok(RRT2DPlanner {
736 planner,
737 obstacles: obstacles.clone(),
738 _collision_step_size: collision_step_size,
739 })
740 }
741
742 pub fn find_path(
744 &mut self,
745 start: [f64; 2],
746 goal: [f64; 2],
747 goal_threshold: f64,
748 ) -> SpatialResult<Option<Path<[f64; 2]>>> {
749 let start_arr = ndarray::arr1(&start);
750 let goal_arr = ndarray::arr1(&goal);
751
752 for obstacle in &self.obstacles {
754 if Self::point_in_polygon(&start, obstacle) {
755 return Err(SpatialError::ValueError(
756 "Start point is inside an obstacle".to_string(),
757 ));
758 }
759 if Self::point_in_polygon(&goal, obstacle) {
760 return Err(SpatialError::ValueError(
761 "Goal point is inside an obstacle".to_string(),
762 ));
763 }
764 }
765
766 let result = self
768 .planner
769 .find_path(start_arr.view(), goal_arr.view(), goal_threshold)?;
770
771 if let Some(path) = result {
773 let nodes: Vec<[f64; 2]> = path.nodes.iter().map(|p| [p[0], p[1]]).collect();
774 Ok(Some(Path::new(nodes, path.cost)))
775 } else {
776 Ok(None)
777 }
778 }
779
780 fn check_collision_with_obstacles(
782 from: &Array1<f64>,
783 to: &Array1<f64>,
784 obstacles: &[Vec<[f64; 2]>],
785 step_size: f64,
786 ) -> bool {
787 let from_point = [from[0], from[1]];
788 let to_point = [to[0], to[1]];
789
790 for obstacle in obstacles {
792 if Self::point_in_polygon(&from_point, obstacle)
793 || Self::point_in_polygon(&to_point, obstacle)
794 {
795 return true;
796 }
797 }
798
799 let dx = to[0] - from[0];
801 let dy = to[1] - from[1];
802 let distance = (dx * dx + dy * dy).sqrt();
803
804 if distance < 1e-6 {
805 return false; }
807
808 let steps = (distance / step_size).ceil() as usize;
809
810 for i in 1..steps {
811 let t = i as f64 / steps as f64;
812 let x = from[0] + dx * t;
813 let y = from[1] + dy * t;
814 let point = [x, y];
815
816 for obstacle in obstacles {
817 if Self::point_in_polygon(&point, obstacle) {
818 return true;
819 }
820 }
821 }
822
823 false
824 }
825
826 fn point_in_polygon(point: &[f64; 2], polygon: &[[f64; 2]]) -> bool {
828 if polygon.len() < 3 {
829 return false;
830 }
831
832 let mut inside = false;
833 let mut j = polygon.len() - 1;
834
835 for i in 0..polygon.len() {
836 let xi = polygon[i][0];
837 let yi = polygon[i][1];
838 let xj = polygon[j][0];
839 let yj = polygon[j][1];
840
841 let intersect = ((yi > point[1]) != (yj > point[1]))
842 && (point[0] < (xj - xi) * (point[1] - yi) / (yj - yi) + xi);
843
844 if intersect {
845 inside = !inside;
846 }
847
848 j = i;
849 }
850
851 inside
852 }
853}
854
855#[cfg(test)]
856mod tests {
857 use super::*;
858
859 #[test]
860 fn test_rrt_empty_space() {
861 let config = RRTConfig {
863 max_iterations: 1000,
864 step_size: 0.5,
865 goal_bias: 0.1,
866 seed: Some(42), use_rrt_star: false,
868 neighborhood_radius: None,
869 bidirectional: false,
870 };
871
872 let mut planner = RRT2DPlanner::new(
873 config,
874 vec![], [0.0, 0.0], [10.0, 10.0], 0.1, )
879 .unwrap();
880
881 let start = [1.0, 1.0];
883 let goal = [9.0, 9.0];
884 let goal_threshold = 0.5;
885
886 let path = planner.find_path(start, goal, goal_threshold).unwrap();
887
888 assert!(path.is_some());
890 let path = path.unwrap();
891
892 assert_eq!(path.nodes[0], start);
894 let last_node = path.nodes.last().unwrap();
895 let dist_to_goal =
896 ((last_node[0] - goal[0]).powi(2) + (last_node[1] - goal[1]).powi(2)).sqrt();
897 assert!(dist_to_goal <= goal_threshold);
898 }
899
900 #[test]
901 fn test_rrt_star_optimization() {
902 let config = RRTConfig {
904 max_iterations: 1000,
905 step_size: 0.5,
906 goal_bias: 0.1,
907 seed: Some(42), use_rrt_star: true,
909 neighborhood_radius: Some(1.0),
910 bidirectional: false,
911 };
912
913 let mut planner = RRT2DPlanner::new(
914 config,
915 vec![], [0.0, 0.0], [10.0, 10.0], 0.1, )
920 .unwrap();
921
922 let start = [1.0, 1.0];
924 let goal = [9.0, 9.0];
925 let goal_threshold = 0.5;
926
927 let path = planner.find_path(start, goal, goal_threshold).unwrap();
928
929 assert!(path.is_some());
931 let path = path.unwrap();
932
933 assert_eq!(path.nodes[0], start);
935 let last_node = path.nodes.last().unwrap();
936 let dist_to_goal =
937 ((last_node[0] - goal[0]).powi(2) + (last_node[1] - goal[1]).powi(2)).sqrt();
938 assert!(dist_to_goal <= goal_threshold);
939
940 let direct_distance = ((goal[0] - start[0]).powi(2) + (goal[1] - start[1]).powi(2)).sqrt();
943 assert!(path.cost <= direct_distance * 1.5);
944 }
945
946 #[test]
947 fn test_rrt_bidirectional() {
948 let config = RRTConfig {
950 max_iterations: 1000,
951 step_size: 0.5,
952 goal_bias: 0.1,
953 seed: Some(42), use_rrt_star: false,
955 neighborhood_radius: None,
956 bidirectional: true,
957 };
958
959 let mut planner = RRT2DPlanner::new(
960 config,
961 vec![], [0.0, 0.0], [10.0, 10.0], 0.1, )
966 .unwrap();
967
968 let start = [1.0, 1.0];
970 let goal = [9.0, 9.0];
971 let goal_threshold = 0.5;
972
973 let path = planner.find_path(start, goal, goal_threshold).unwrap();
974
975 assert!(path.is_some());
977 let path = path.unwrap();
978
979 assert_eq!(path.nodes[0], start);
981 let last_node = path.nodes.last().unwrap();
982 let dist_to_goal =
983 ((last_node[0] - goal[0]).powi(2) + (last_node[1] - goal[1]).powi(2)).sqrt();
984 assert!(dist_to_goal <= goal_threshold);
985 }
986
987 #[test]
988 fn test_rrt_with_obstacles() {
989 let config = RRTConfig {
991 max_iterations: 2000,
992 step_size: 0.3,
993 goal_bias: 0.1,
994 seed: Some(42), use_rrt_star: false,
996 neighborhood_radius: None,
997 bidirectional: false,
998 };
999
1000 let obstacles = vec![vec![[4.0, 0.0], [5.0, 0.0], [5.0, 8.0], [4.0, 8.0]]];
1002
1003 let mut planner = RRT2DPlanner::new(
1004 config,
1005 obstacles,
1006 [0.0, 0.0], [10.0, 10.0], 0.1, )
1010 .unwrap();
1011
1012 let start = [2.0, 5.0];
1014 let goal = [7.0, 5.0];
1015 let goal_threshold = 0.5;
1016
1017 let path = planner.find_path(start, goal, goal_threshold).unwrap();
1018
1019 assert!(path.is_some());
1021 let path = path.unwrap();
1022
1023 assert_eq!(path.nodes[0], start);
1025 let last_node = path.nodes.last().unwrap();
1026 let dist_to_goal =
1027 ((last_node[0] - goal[0]).powi(2) + (last_node[1] - goal[1]).powi(2)).sqrt();
1028 assert!(dist_to_goal <= goal_threshold);
1029
1030 for node in &path.nodes {
1033 assert!(!(node[0] >= 4.0 && node[0] <= 5.0 && node[1] >= 0.0 && node[1] <= 8.0));
1034 }
1035 }
1036}