1use bincode::{Decode, Encode};
18use core::fmt::Debug;
19use cu_rng::prelude::*;
20use cu_spatial_payloads::{BBox2f, Point2f, Point2fSoa, Point3f, Point3fSoa};
21use cu29::prelude::*;
22use cu29::units::si::area::square_meter;
23use cu29::units::si::f32::{Area, Length, Ratio};
24use cu29::units::si::length::meter;
25use serde::{Deserialize, Serialize};
26
27pub const MAX_WAYPOINTS: usize = 32;
30
31pub const MAX_NODES: usize = 4096;
34
35pub const MAX_OBSTACLES: usize = 16;
39
40#[derive(
42 Default, Debug, Clone, Copy, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect,
43)]
44pub struct Obstacle {
45 pub center: Point2f,
46 pub radius: Length,
47}
48
49impl Obstacle {
50 pub const fn new(center: Point2f, radius: Length) -> Self {
51 Self { center, radius }
52 }
53}
54
55pub trait PointSet<P>: Default {
60 fn len(&self) -> usize;
61
62 fn is_empty(&self) -> bool {
63 self.len() == 0
64 }
65
66 fn clear(&mut self);
67
68 fn push(&mut self, point: P);
71
72 fn get(&self, index: usize) -> P;
75
76 fn distances_squared(&self, target: P, out: &mut [Area]);
81
82 fn compact(&mut self, destination: usize, source: usize);
85
86 fn truncate(&mut self, len: usize);
88}
89
90pub trait PlanPoint: Copy + Debug + PartialEq + 'static {
94 type Set: PointSet<Self>;
96
97 fn distance(self, other: Self) -> Length;
99
100 fn lerp(self, other: Self, ratio: Ratio) -> Self;
103
104 fn project(a: Self, b: Self, p: Self) -> (Area, Area);
108}
109
110macro_rules! impl_plan_point {
111 ($point:ty, $set:ty, $($axis:ident),+) => {
112 impl PointSet<$point> for $set {
113 fn len(&self) -> usize {
114 <$set>::len(self)
115 }
116
117 fn clear(&mut self) {
118 self.len = 0;
119 }
120
121 fn push(&mut self, point: $point) {
122 <$set>::push(self, point)
123 }
124
125 fn get(&self, index: usize) -> $point {
126 <$set>::get(self, index)
127 }
128
129 fn distances_squared(&self, target: $point, out: &mut [Area]) {
130 <$set>::distances_squared(self, target, out)
131 }
132
133 fn compact(&mut self, destination: usize, source: usize) {
134 debug_assert!(destination <= source);
135 $(self.$axis[destination] = self.$axis[source];)+
136 }
137
138 fn truncate(&mut self, len: usize) {
139 debug_assert!(len <= self.len);
140 self.len = len;
141 }
142 }
143
144 impl PlanPoint for $point {
145 type Set = $set;
146
147 fn distance(self, other: Self) -> Length {
148 <$point>::distance(self, other)
149 }
150
151 fn lerp(self, other: Self, ratio: Ratio) -> Self {
152 <$point>::lerp(self, other, ratio.raw())
153 }
154
155 fn project(a: Self, b: Self, p: Self) -> (Area, Area) {
156 let (mut dot, mut len_sq) = (0.0f32, 0.0f32);
157 $(
158 let along = (b.$axis - a.$axis).raw();
159 let to_point = (p.$axis - a.$axis).raw();
160 dot += to_point * along;
161 len_sq += along * along;
162 )+
163 (
164 Area::new::<square_meter>(dot),
165 Area::new::<square_meter>(len_sq),
166 )
167 }
168 }
169 };
170}
171
172impl_plan_point!(Point2f, Point2fSoa<MAX_NODES>, x, y);
173impl_plan_point!(Point3f, Point3fSoa<MAX_NODES>, x, y, z);
174
175pub trait Clearance {
182 type Point: PlanPoint;
184
185 fn clearance(&self, p: Self::Point) -> Length;
187
188 fn clearance_segment(&self, a: Self::Point, b: Self::Point) -> Length;
190}
191
192#[derive(Default, Debug, Clone, Encode, Decode, Serialize, Deserialize, Reflect)]
198pub struct World {
199 pub bounds: BBox2f,
200 pub obstacles: [Obstacle; MAX_OBSTACLES],
201 pub obstacle_count: u32,
203}
204
205impl World {
206 pub fn new(bounds: BBox2f, obstacles: &[Obstacle]) -> CuResult<Self> {
209 if obstacles.len() > MAX_OBSTACLES {
210 return Err(format!(
211 "rrt*: {} obstacles, the world holds at most {MAX_OBSTACLES}",
212 obstacles.len()
213 )
214 .into());
215 }
216 let mut world = Self {
217 bounds,
218 obstacles: [Obstacle::default(); MAX_OBSTACLES],
219 obstacle_count: obstacles.len() as u32,
220 };
221 world.obstacles[..obstacles.len()].copy_from_slice(obstacles);
222 Ok(world)
223 }
224
225 pub fn depot() -> Self {
230 let meters = Length::new::<meter>;
231 let point = Point2f::from_meters;
232 Self::new(
233 BBox2f::new(point(0.0, 0.0), point(10.0, 10.0)),
234 &[
235 Obstacle::new(point(3.0, 3.0), meters(1.2)),
236 Obstacle::new(point(6.0, 6.0), meters(1.5)),
237 Obstacle::new(point(7.0, 2.5), meters(1.0)),
238 Obstacle::new(point(2.5, 7.0), meters(1.0)),
239 Obstacle::new(point(5.0, 1.5), meters(0.8)),
240 ],
241 )
242 .expect("the depot obstacles fit MAX_OBSTACLES")
243 }
244
245 pub fn obstacles(&self) -> &[Obstacle] {
248 &self.obstacles[..(self.obstacle_count as usize).min(MAX_OBSTACLES)]
249 }
250
251 pub fn free_area(&self) -> Area {
254 let blocked: f32 = self
255 .obstacles()
256 .iter()
257 .map(|o| core::f32::consts::PI * o.radius.raw() * o.radius.raw())
258 .sum();
259 let width = (self.bounds.max.x - self.bounds.min.x).raw();
260 let height = (self.bounds.max.y - self.bounds.min.y).raw();
261 Area::new::<square_meter>((width * height - blocked).max(f32::EPSILON))
262 }
263}
264
265impl Clearance for World {
266 type Point = Point2f;
267
268 fn clearance(&self, p: Point2f) -> Length {
269 let b = &self.bounds;
270 let mut clearance = (p.x - b.min.x)
271 .raw()
272 .min((b.max.x - p.x).raw())
273 .min((p.y - b.min.y).raw())
274 .min((b.max.y - p.y).raw());
275 for o in self.obstacles() {
276 clearance = clearance.min(p.distance(o.center).raw() - o.radius.raw());
277 }
278 Length::new::<meter>(clearance)
279 }
280
281 fn clearance_segment(&self, a: Point2f, b: Point2f) -> Length {
282 let mut clearance = self.clearance(a).raw().min(self.clearance(b).raw());
285 for o in self.obstacles() {
286 clearance = clearance.min(distance_to_segment(a, b, o.center).raw() - o.radius.raw());
287 }
288 Length::new::<meter>(clearance)
289 }
290}
291
292pub trait RrtSpace: Clearance {
297 fn sample(&self, rng: &mut CuRng) -> Self::Point;
299
300 fn rrt_star_gamma(&self) -> Length;
307}
308
309impl RrtSpace for World {
310 fn sample(&self, rng: &mut CuRng) -> Point2f {
311 let b = &self.bounds;
312 Point2f::new(
313 b.min.x + (b.max.x - b.min.x) * rng.random::<f32>(),
314 b.min.y + (b.max.y - b.min.y) * rng.random::<f32>(),
315 )
316 }
317
318 fn rrt_star_gamma(&self) -> Length {
320 Length::new::<meter>(
321 2.0 * 1.5f32.sqrt() * (self.free_area().raw() / core::f32::consts::PI).sqrt(),
322 )
323 }
324}
325
326fn distance_to_segment<P: PlanPoint>(a: P, b: P, point: P) -> Length {
328 let (dot, len_sq) = P::project(a, b, point);
329 if len_sq.raw() <= f32::EPSILON {
330 return a.distance(point);
331 }
332 let along = ratio_of((dot.raw() / len_sq.raw()).clamp(0.0, 1.0));
333 a.lerp(b, along).distance(point)
334}
335
336pub(crate) fn meters(value: f32) -> Length {
338 Length::new::<meter>(value)
339}
340
341pub(crate) fn ratio_of(value: f32) -> Ratio {
343 Ratio::new::<cu29::units::si::ratio::ratio>(value)
344}
345
346fn shorter(a: Length, b: Length) -> Length {
349 if b < a { b } else { a }
350}
351
352#[derive(Debug, Clone, Copy, Reflect)]
354pub struct RrtParams {
355 pub step_size: Length,
357 pub goal_bias: Ratio,
359 pub goal_threshold: Length,
361 pub gamma: Length,
365 pub prune_interval: u32,
367 pub max_nodes: u32,
370}
371
372impl Default for RrtParams {
373 fn default() -> Self {
374 Self {
375 step_size: meters(0.8),
376 goal_bias: ratio_of(0.05),
377 goal_threshold: meters(0.5),
378 gamma: meters(0.0),
379 prune_interval: 512,
380 max_nodes: 4000,
381 }
382 }
383}
384
385#[derive(Debug, Clone)]
388struct TreeNode {
389 parent: Option<u32>,
391 cost: Length,
393 children: Vec<u32>,
394}
395
396pub struct RrtStar<S: RrtSpace = World> {
401 space: S,
402 params: RrtParams,
403 gamma: Length,
406 start: S::Point,
407 goal: S::Point,
408 positions: <S::Point as PlanPoint>::Set,
412 tree: Vec<TreeNode>,
415 best_goal: Option<u32>,
417 best_cost: Length,
419 iterations: u32,
420 rng: CuRng,
421 scratch_d2: Vec<Area>,
425 scratch_near: Vec<u32>,
426 scratch_stack: Vec<u32>,
427}
428
429impl<S: RrtSpace> RrtStar<S> {
430 pub fn new(space: S, params: RrtParams, start: S::Point, goal: S::Point, seed: u64) -> Self {
434 let mut planner = Self {
435 space,
436 params: RrtParams {
437 max_nodes: params.max_nodes.min(MAX_NODES as u32),
440 ..params
441 },
442 gamma: meters(0.0),
443 start,
444 goal,
445 positions: <S::Point as PlanPoint>::Set::default(),
446 tree: Vec::new(),
447 best_goal: None,
448 best_cost: meters(f32::INFINITY),
449 iterations: 0,
450 rng: CuRng::from_seed(seed),
451 scratch_d2: vec![Area::default(); MAX_NODES],
453 scratch_near: Vec::new(),
454 scratch_stack: Vec::new(),
455 };
456 planner.restart(start, goal, seed);
457 planner
458 }
459
460 pub fn reset(&mut self, space: S, start: S::Point, goal: S::Point, seed: u64) {
464 self.space = space;
465 self.restart(start, goal, seed);
466 }
467
468 fn restart(&mut self, start: S::Point, goal: S::Point, seed: u64) {
469 self.gamma = if self.params.gamma > meters(0.0) {
471 self.params.gamma
472 } else {
473 self.space.rrt_star_gamma()
474 };
475 self.start = start;
476 self.goal = goal;
477 self.tree.clear();
478 self.positions.clear();
479 self.positions.push(start);
480 self.tree.push(TreeNode {
481 parent: None,
482 cost: meters(0.0),
483 children: Vec::new(),
484 });
485 self.best_goal = None;
486 self.best_cost = meters(f32::INFINITY);
487 self.iterations = 0;
488 self.rng = CuRng::from_seed(seed);
489 }
490
491 pub fn grow(&mut self, iterations: u32) {
493 for _ in 0..iterations {
494 self.iterations += 1;
495 if self.tree.len() < self.params.max_nodes as usize {
496 self.step();
497 }
498 if self.params.prune_interval > 0
499 && self.iterations.is_multiple_of(self.params.prune_interval)
500 && self.best_goal.is_some()
501 {
502 self.prune();
503 }
504 }
505 }
506
507 pub fn best_cost(&self) -> Length {
509 self.best_cost
510 }
511
512 pub fn has_solution(&self) -> bool {
514 self.best_goal.is_some()
515 }
516
517 pub fn tree_size(&self) -> u32 {
518 self.tree.len() as u32
519 }
520
521 pub fn iterations(&self) -> u32 {
522 self.iterations
523 }
524
525 pub fn is_exhausted(&self) -> bool {
528 self.tree.len() >= self.params.max_nodes as usize
529 && (self.params.prune_interval == 0 || self.best_goal.is_none())
530 }
531
532 pub fn lower_bound(&self) -> Length {
534 self.start.distance(self.goal)
535 }
536
537 pub fn quality(&self) -> Ratio {
541 if !self.has_solution() {
542 return ratio_of(0.0);
543 }
544 let lower_bound = self.lower_bound();
545 if self.best_cost <= lower_bound {
546 return ratio_of(1.0);
549 }
550 ratio_of((lower_bound.raw() / self.best_cost.raw()).clamp(0.0, 1.0))
551 }
552
553 pub fn tree_path_len(&self) -> usize {
556 let Some(goal_node) = self.best_goal else {
557 return 0;
558 };
559 let mut len = 1; let mut cursor = Some(goal_node);
561 while let Some(index) = cursor {
562 len += 1;
563 cursor = self.tree[index as usize].parent;
564 }
565 len
566 }
567
568 pub fn write_path(&self, out: &mut [S::Point; MAX_WAYPOINTS]) -> Option<u32> {
582 let goal_node = self.best_goal?;
583 let mut chain = Vec::new();
584 let mut cursor = Some(goal_node);
585 while let Some(index) = cursor {
586 chain.push(self.positions.get(index as usize));
587 cursor = self.tree[index as usize].parent;
588 }
589 chain.reverse();
590 chain.push(self.goal);
591
592 let mut len = 0usize;
593 let mut at = 0usize;
594 loop {
595 if len == MAX_WAYPOINTS {
596 return None;
597 }
598 out[len] = chain[at];
599 len += 1;
600 if at == chain.len() - 1 {
601 return Some(len as u32);
602 }
603 let mut next = at + 1;
606 for candidate in (at + 2)..chain.len() {
607 if self.segment_free(chain[at], chain[candidate]) {
608 next = candidate;
609 }
610 }
611 at = next;
612 }
613 }
614
615 fn segment_free(&self, a: S::Point, b: S::Point) -> bool {
617 self.space.clearance_segment(a, b) > meters(0.0)
618 }
619
620 fn step(&mut self) {
623 let sample = self.sample();
624 let nearest = self.nearest(sample);
625 let from = self.positions.get(nearest as usize);
626 let new_pos = steer(from, sample, self.params.step_size);
627 if !self.segment_free(from, new_pos) {
628 return;
629 }
630
631 let radius = self.near_radius();
634 let radius_sq = Area::new::<square_meter>(radius.raw() * radius.raw());
635 let n = self.positions.len();
636 self.positions
637 .distances_squared(new_pos, &mut self.scratch_d2);
638 let mut near = core::mem::take(&mut self.scratch_near);
639 near.clear();
640 for index in 0..n {
641 if self.scratch_d2[index] <= radius_sq {
642 near.push(index as u32);
643 }
644 }
645
646 let mut parent = nearest;
648 let mut cost = self.tree[nearest as usize].cost + from.distance(new_pos);
649 for &index in near.iter() {
650 let candidate_pos = self.positions.get(index as usize);
651 let candidate_cost = self.tree[index as usize].cost + candidate_pos.distance(new_pos);
652 if candidate_cost < cost && self.segment_free(candidate_pos, new_pos) {
653 parent = index;
654 cost = candidate_cost;
655 }
656 }
657
658 let new_index = self.tree.len() as u32;
659 self.positions.push(new_pos);
660 self.tree.push(TreeNode {
661 parent: Some(parent),
662 cost,
663 children: Vec::new(),
664 });
665 self.tree[parent as usize].children.push(new_index);
666
667 for &index in near.iter() {
669 if index == parent {
670 continue;
671 }
672 let neighbor_pos = self.positions.get(index as usize);
673 let neighbor_cost = self.tree[index as usize].cost;
674 let rewired_cost = cost + neighbor_pos.distance(new_pos);
675 if rewired_cost < neighbor_cost
676 && !self.is_ancestor(index, new_index)
677 && self.segment_free(new_pos, neighbor_pos)
678 {
679 self.reparent(index, new_index, rewired_cost);
680 }
681 }
682 self.scratch_near = near;
683
684 let to_goal = new_pos.distance(self.goal);
686 if to_goal <= self.params.goal_threshold
687 && self.segment_free(new_pos, self.goal)
688 && cost + to_goal < self.best_cost
689 {
690 self.best_cost = cost + to_goal;
691 self.best_goal = Some(new_index);
692 }
693 if let Some(goal_node) = self.best_goal {
695 let cost = self.tree[goal_node as usize].cost;
696 let pos = self.positions.get(goal_node as usize);
697 self.best_cost = shorter(self.best_cost, cost + pos.distance(self.goal));
698 }
699 }
700
701 fn sample(&mut self) -> S::Point {
703 if self.rng.random::<f32>() < self.params.goal_bias.raw() {
704 return self.goal;
705 }
706 self.space.sample(&mut self.rng)
707 }
708
709 fn nearest(&mut self, point: S::Point) -> u32 {
714 let n = self.positions.len();
715 self.positions
716 .distances_squared(point, &mut self.scratch_d2);
717 let mut best = 0u32;
718 let mut best_distance = Area::new::<square_meter>(f32::INFINITY);
719 for index in 0..n {
720 let distance = self.scratch_d2[index];
721 if distance < best_distance {
722 best_distance = distance;
723 best = index as u32;
724 }
725 }
726 best
727 }
728
729 fn near_radius(&self) -> Length {
731 let n = (self.tree.len() as f32).max(2.0);
732 shorter(self.gamma * (n.ln() / n).sqrt(), self.params.step_size)
733 }
734
735 fn is_ancestor(&self, candidate: u32, node: u32) -> bool {
743 let mut cursor = self.tree[node as usize].parent;
744 while let Some(index) = cursor {
745 if index == candidate {
746 return true;
747 }
748 cursor = self.tree[index as usize].parent;
749 }
750 false
751 }
752
753 fn reparent(&mut self, node: u32, new_parent: u32, new_cost: Length) {
756 if let Some(old_parent) = self.tree[node as usize].parent {
757 self.tree[old_parent as usize]
758 .children
759 .retain(|&child| child != node);
760 }
761 self.tree[node as usize].parent = Some(new_parent);
762 self.tree[new_parent as usize].children.push(node);
763
764 let delta = new_cost - self.tree[node as usize].cost;
765 let mut stack = core::mem::take(&mut self.scratch_stack);
766 stack.clear();
767 stack.push(node);
768 while let Some(index) = stack.pop() {
769 self.tree[index as usize].cost += delta;
770 for i in 0..self.tree[index as usize].children.len() {
771 stack.push(self.tree[index as usize].children[i]);
772 }
773 }
774 self.scratch_stack = stack;
775 }
776
777 fn prune(&mut self) {
785 let mut protected = vec![false; self.tree.len()];
788 let mut cursor = self.best_goal;
789 while let Some(index) = cursor {
790 protected[index as usize] = true;
791 cursor = self.tree[index as usize].parent;
792 }
793
794 let mut keep = vec![false; self.tree.len()];
795 let mut stack = core::mem::take(&mut self.scratch_stack);
796 stack.clear();
797 stack.push(0);
798 keep[0] = true;
799 while let Some(index) = stack.pop() {
800 for i in 0..self.tree[index as usize].children.len() {
801 let child = self.tree[index as usize].children[i];
802 let cost = self.tree[child as usize].cost;
803 let pos = self.positions.get(child as usize);
804 if protected[child as usize] || cost + pos.distance(self.goal) <= self.best_cost {
805 keep[child as usize] = true;
806 stack.push(child);
807 }
808 }
809 }
810 self.scratch_stack = stack;
811
812 let mut remap = vec![u32::MAX; self.tree.len()];
813 let mut kept = Vec::with_capacity(self.tree.len());
814 for index in 0..self.tree.len() {
815 if keep[index] {
816 let destination = kept.len();
819 remap[index] = destination as u32;
820 self.positions.compact(destination, index);
821 kept.push(TreeNode {
822 parent: self.tree[index].parent,
823 cost: self.tree[index].cost,
824 children: Vec::new(),
825 });
826 }
827 }
828 self.positions.truncate(kept.len());
829 for node in kept.iter_mut() {
830 node.parent = node.parent.map(|parent| remap[parent as usize]);
831 }
832 for index in 0..kept.len() {
833 if let Some(parent) = kept[index].parent {
834 kept[parent as usize].children.push(index as u32);
835 }
836 }
837 self.best_goal = self.best_goal.map(|goal| remap[goal as usize]);
838 self.tree = kept;
839 }
840}
841
842fn steer<P: PlanPoint>(from: P, to: P, step_size: Length) -> P {
844 let distance = from.distance(to);
845 if distance <= step_size {
846 return to;
847 }
848 from.lerp(to, ratio_of(step_size.raw() / distance.raw()))
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854
855 fn start() -> Point2f {
856 Point2f::from_meters(0.5, 0.5)
857 }
858
859 fn goal() -> Point2f {
860 Point2f::from_meters(9.5, 9.5)
861 }
862
863 fn planner(seed: u64) -> RrtStar {
864 RrtStar::new(World::depot(), RrtParams::default(), start(), goal(), seed)
865 }
866
867 #[derive(Clone)]
871 struct Room {
872 bounds: cu_spatial_payloads::BBox3f,
873 center: Point3f,
874 radius: Length,
875 }
876
877 impl Room {
878 fn new() -> Self {
879 Self {
880 bounds: cu_spatial_payloads::BBox3f::new(
881 Point3f::from_meters(0.0, 0.0, 0.0),
882 Point3f::from_meters(10.0, 10.0, 10.0),
883 ),
884 center: Point3f::from_meters(5.0, 5.0, 5.0),
886 radius: Length::new::<meter>(1.5),
887 }
888 }
889 }
890
891 impl Clearance for Room {
892 type Point = Point3f;
893
894 fn clearance(&self, p: Point3f) -> Length {
895 let b = &self.bounds;
896 let walls = (p.x - b.min.x)
897 .raw()
898 .min((b.max.x - p.x).raw())
899 .min((p.y - b.min.y).raw())
900 .min((b.max.y - p.y).raw())
901 .min((p.z - b.min.z).raw())
902 .min((b.max.z - p.z).raw());
903 let sphere = p.distance(self.center).raw() - self.radius.raw();
904 Length::new::<meter>(walls.min(sphere))
905 }
906
907 fn clearance_segment(&self, a: Point3f, b: Point3f) -> Length {
908 let ends = self.clearance(a).raw().min(self.clearance(b).raw());
909 let sphere = distance_to_segment(a, b, self.center).raw() - self.radius.raw();
910 meters(ends.min(sphere))
911 }
912 }
913
914 impl RrtSpace for Room {
915 fn sample(&self, rng: &mut CuRng) -> Point3f {
916 let b = &self.bounds;
917 Point3f::new(
918 b.min.x + (b.max.x - b.min.x) * rng.random::<f32>(),
919 b.min.y + (b.max.y - b.min.y) * rng.random::<f32>(),
920 b.min.z + (b.max.z - b.min.z) * rng.random::<f32>(),
921 )
922 }
923
924 fn rrt_star_gamma(&self) -> Length {
926 let b = &self.bounds;
927 let side = |min: Length, max: Length| (max - min).raw();
928 let volume = side(b.min.x, b.max.x) * side(b.min.y, b.max.y) * side(b.min.z, b.max.z)
929 - 4.0 / 3.0 * core::f32::consts::PI * self.radius.raw().powi(3);
930 let zeta_3 = 4.0 / 3.0 * core::f32::consts::PI;
931 Length::new::<meter>(2.0 * (4.0f32 / 3.0).cbrt() * (volume / zeta_3).cbrt())
932 }
933 }
934
935 #[test]
939 fn the_same_planner_solves_a_3d_job() {
940 let room = Room::new();
941 let start = Point3f::from_meters(0.5, 0.5, 0.5);
942 let goal = Point3f::from_meters(9.5, 9.5, 9.5);
943 assert!(
944 room.clearance_segment(start, goal) <= meters(0.0),
945 "the straight line should be blocked, or the job is trivial"
946 );
947
948 let params = RrtParams {
949 step_size: meters(1.2),
950 ..Default::default()
951 };
952 let mut planner = RrtStar::new(room.clone(), params, start, goal, 5);
953 planner.grow(4000);
954 assert!(planner.has_solution(), "no 3D path found");
955 assert_eq!(planner.positions.len(), planner.tree.len());
956
957 let mut waypoints = [Point3f::default(); MAX_WAYPOINTS];
958 let len = planner.write_path(&mut waypoints).expect("the path fits");
959 assert!(len >= 2);
960 assert_eq!(waypoints[0], start);
961 assert_eq!(waypoints[(len - 1) as usize], goal);
962 for pair in waypoints[..len as usize].windows(2) {
963 assert!(
964 room.clearance_segment(pair[0], pair[1]) > meters(0.0),
965 "the published 3D path crosses the sphere"
966 );
967 }
968 assert!(planner.best_cost() >= planner.lower_bound());
969 }
970
971 #[test]
972 fn world_rejects_too_many_obstacles() {
973 let radius = Length::new::<meter>(0.1);
974 let bounds = BBox2f::new(
975 Point2f::from_meters(0.0, 0.0),
976 Point2f::from_meters(10.0, 10.0),
977 );
978 let too_many = [Obstacle::new(Point2f::from_meters(1.0, 1.0), radius); MAX_OBSTACLES + 1];
979 assert!(World::new(bounds, &too_many).is_err());
980 assert!(World::new(bounds, &too_many[..MAX_OBSTACLES]).is_ok());
981 }
982
983 #[test]
984 fn clearance_signs_match_the_geometry() {
985 let world = World::depot();
986 let point = Point2f::from_meters;
987 assert!(world.clearance_segment(point(1.0, 1.0), point(5.0, 5.0)) <= meters(0.0));
989 assert!(world.clearance_segment(point(0.2, 0.2), point(0.2, 9.8)) > meters(0.0));
991 assert!(world.clearance_segment(start(), point(11.0, 0.5)) <= meters(0.0));
993 assert!(world.clearance(point(3.0, 3.0)) < meters(0.0));
995 }
996
997 #[test]
998 fn refinement_only_improves_the_path() {
999 let mut planner = planner(42);
1000 planner.grow(400);
1001 assert!(planner.has_solution(), "no first path after the base block");
1002
1003 let mut previous = planner.best_cost();
1004 for _ in 0..16 {
1005 planner.grow(256);
1006 assert!(
1007 planner.best_cost() <= previous + meters(1e-4),
1008 "cost went up: {:?} then {:?}",
1009 previous,
1010 planner.best_cost()
1011 );
1012 previous = planner.best_cost();
1013 }
1014 assert!(planner.quality() > ratio_of(0.0) && planner.quality() <= ratio_of(1.0));
1015 assert!(planner.best_cost() >= planner.lower_bound());
1016 }
1017
1018 #[test]
1027 fn published_path_is_valid_at_every_stop_point() {
1028 let world = World::depot();
1029 let derived = world.rrt_star_gamma().raw();
1030 assert!(
1031 (12.0..13.0).contains(&derived),
1032 "gamma for the depot map should be near 12.4 m, got {derived}"
1033 );
1034
1035 let mut longest_tree_path = 0;
1036 let mut total_cost = [meters(0.0); 2];
1038 for (seed, index) in (1..40u64).flat_map(|seed| [(seed, 0usize), (seed, 1)]) {
1039 let params = RrtParams {
1040 gamma: [meters(0.0), meters(3.0)][index],
1041 ..Default::default()
1042 };
1043 let mut planner = RrtStar::new(World::depot(), params, start(), goal(), seed);
1044 planner.grow(400);
1045 for _ in 0..24 {
1046 planner.grow(256);
1047 let mut waypoints = [Point2f::default(); MAX_WAYPOINTS];
1048 let Some(len) = planner.write_path(&mut waypoints) else {
1049 panic!("seed {seed}: the shortcut path did not fit");
1050 };
1051 longest_tree_path = longest_tree_path.max(planner.tree_path_len());
1052 assert!(len >= 2, "a path has at least a start and a goal");
1053 assert_eq!(waypoints[0], start());
1054 assert_eq!(waypoints[(len - 1) as usize], goal());
1055 for pair in waypoints[..len as usize].windows(2) {
1056 assert!(
1057 world.clearance_segment(pair[0], pair[1]) > meters(0.0),
1058 "seed {seed}: published path crosses an obstacle"
1059 );
1060 }
1061 let published = waypoints[..len as usize]
1064 .windows(2)
1065 .fold(meters(0.0), |sum, pair| sum + pair[0].distance(pair[1]));
1066 assert!(
1067 published <= planner.best_cost() + meters(1e-3),
1068 "seed {seed}: shortcut path {:?} longer than the cost {:?}",
1069 published,
1070 planner.best_cost()
1071 );
1072 }
1073 total_cost[index] += planner.best_cost();
1074 }
1075 assert!(
1076 longest_tree_path > MAX_WAYPOINTS,
1077 "the tree path never outgrew MAX_WAYPOINTS, so the shortcut was never exercised"
1078 );
1079 assert!(
1080 total_cost[0] < total_cost[1],
1081 "the derived gamma should refine to a shorter path than a small one"
1082 );
1083 }
1084
1085 #[test]
1088 fn degenerate_job_reports_full_quality() {
1089 let mut planner = RrtStar::new(World::depot(), RrtParams::default(), start(), start(), 3);
1090 planner.grow(400);
1091 assert!(planner.has_solution());
1092 assert_eq!(planner.quality(), ratio_of(1.0));
1093 }
1094
1095 #[test]
1096 fn same_seed_replays_the_same_tree() {
1097 let (mut a, mut b) = (planner(11), planner(11));
1098 a.grow(600);
1099 b.grow(300);
1100 b.grow(300);
1101 assert_eq!(a.tree_size(), b.tree_size());
1102 assert_eq!(a.best_cost(), b.best_cost());
1103 }
1104
1105 #[test]
1106 fn pruning_keeps_the_best_path_reachable() {
1107 let mut planner = planner(3);
1108 planner.grow(1500);
1109 let cost_before = planner.best_cost();
1110 assert_eq!(planner.positions.len(), planner.tree.len());
1113 planner.prune();
1114 assert_eq!(planner.positions.len(), planner.tree.len());
1115 assert!(planner.has_solution(), "pruning dropped the goal node");
1116 for index in 0..planner.tree.len() {
1118 let mut cursor = Some(index as u32);
1119 let mut hops = 0;
1120 while let Some(current) = cursor {
1121 cursor = planner.tree[current as usize].parent;
1122 hops += 1;
1123 assert!(hops <= planner.tree.len(), "cycle in the tree");
1124 }
1125 }
1126 assert_eq!(planner.best_cost(), cost_before);
1127 }
1128
1129 #[test]
1132 fn max_nodes_is_clamped_to_the_soa_capacity() {
1133 let params = RrtParams {
1134 max_nodes: MAX_NODES as u32 * 4,
1135 prune_interval: 0,
1137 ..Default::default()
1138 };
1139 let mut planner = RrtStar::new(World::depot(), params, start(), goal(), 7);
1140 assert_eq!(planner.params.max_nodes, MAX_NODES as u32);
1141 planner.grow(MAX_NODES as u32 * 2);
1142 assert!(planner.tree.len() <= MAX_NODES);
1143 assert_eq!(planner.positions.len(), planner.tree.len());
1144 assert!(
1145 planner.is_exhausted(),
1146 "the tree should have filled the cap"
1147 );
1148 }
1149}