use bincode::{Decode, Encode};
use core::fmt::Debug;
use cu_rng::prelude::*;
use cu_spatial_payloads::{BBox2f, Point2f, Point2fSoa, Point3f, Point3fSoa};
use cu29::prelude::*;
use cu29::units::si::area::square_meter;
use cu29::units::si::f32::{Area, Length, Ratio};
use cu29::units::si::length::meter;
use serde::{Deserialize, Serialize};
pub const MAX_WAYPOINTS: usize = 32;
pub const MAX_NODES: usize = 4096;
pub const MAX_OBSTACLES: usize = 16;
#[derive(
Default, Debug, Clone, Copy, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect,
)]
pub struct Obstacle {
pub center: Point2f,
pub radius: Length,
}
impl Obstacle {
pub const fn new(center: Point2f, radius: Length) -> Self {
Self { center, radius }
}
}
pub trait PointSet<P>: Default {
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn clear(&mut self);
fn push(&mut self, point: P);
fn get(&self, index: usize) -> P;
fn distances_squared(&self, target: P, out: &mut [Area]);
fn compact(&mut self, destination: usize, source: usize);
fn truncate(&mut self, len: usize);
}
pub trait PlanPoint: Copy + Debug + PartialEq + 'static {
type Set: PointSet<Self>;
fn distance(self, other: Self) -> Length;
fn lerp(self, other: Self, ratio: Ratio) -> Self;
fn project(a: Self, b: Self, p: Self) -> (Area, Area);
}
macro_rules! impl_plan_point {
($point:ty, $set:ty, $($axis:ident),+) => {
impl PointSet<$point> for $set {
fn len(&self) -> usize {
<$set>::len(self)
}
fn clear(&mut self) {
self.len = 0;
}
fn push(&mut self, point: $point) {
<$set>::push(self, point)
}
fn get(&self, index: usize) -> $point {
<$set>::get(self, index)
}
fn distances_squared(&self, target: $point, out: &mut [Area]) {
<$set>::distances_squared(self, target, out)
}
fn compact(&mut self, destination: usize, source: usize) {
debug_assert!(destination <= source);
$(self.$axis[destination] = self.$axis[source];)+
}
fn truncate(&mut self, len: usize) {
debug_assert!(len <= self.len);
self.len = len;
}
}
impl PlanPoint for $point {
type Set = $set;
fn distance(self, other: Self) -> Length {
<$point>::distance(self, other)
}
fn lerp(self, other: Self, ratio: Ratio) -> Self {
<$point>::lerp(self, other, ratio.raw())
}
fn project(a: Self, b: Self, p: Self) -> (Area, Area) {
let (mut dot, mut len_sq) = (0.0f32, 0.0f32);
$(
let along = (b.$axis - a.$axis).raw();
let to_point = (p.$axis - a.$axis).raw();
dot += to_point * along;
len_sq += along * along;
)+
(
Area::new::<square_meter>(dot),
Area::new::<square_meter>(len_sq),
)
}
}
};
}
impl_plan_point!(Point2f, Point2fSoa<MAX_NODES>, x, y);
impl_plan_point!(Point3f, Point3fSoa<MAX_NODES>, x, y, z);
pub trait Clearance {
type Point: PlanPoint;
fn clearance(&self, p: Self::Point) -> Length;
fn clearance_segment(&self, a: Self::Point, b: Self::Point) -> Length;
}
#[derive(Default, Debug, Clone, Encode, Decode, Serialize, Deserialize, Reflect)]
pub struct World {
pub bounds: BBox2f,
pub obstacles: [Obstacle; MAX_OBSTACLES],
pub obstacle_count: u32,
}
impl World {
pub fn new(bounds: BBox2f, obstacles: &[Obstacle]) -> CuResult<Self> {
if obstacles.len() > MAX_OBSTACLES {
return Err(format!(
"rrt*: {} obstacles, the world holds at most {MAX_OBSTACLES}",
obstacles.len()
)
.into());
}
let mut world = Self {
bounds,
obstacles: [Obstacle::default(); MAX_OBSTACLES],
obstacle_count: obstacles.len() as u32,
};
world.obstacles[..obstacles.len()].copy_from_slice(obstacles);
Ok(world)
}
pub fn depot() -> Self {
let meters = Length::new::<meter>;
let point = Point2f::from_meters;
Self::new(
BBox2f::new(point(0.0, 0.0), point(10.0, 10.0)),
&[
Obstacle::new(point(3.0, 3.0), meters(1.2)),
Obstacle::new(point(6.0, 6.0), meters(1.5)),
Obstacle::new(point(7.0, 2.5), meters(1.0)),
Obstacle::new(point(2.5, 7.0), meters(1.0)),
Obstacle::new(point(5.0, 1.5), meters(0.8)),
],
)
.expect("the depot obstacles fit MAX_OBSTACLES")
}
pub fn obstacles(&self) -> &[Obstacle] {
&self.obstacles[..(self.obstacle_count as usize).min(MAX_OBSTACLES)]
}
pub fn free_area(&self) -> Area {
let blocked: f32 = self
.obstacles()
.iter()
.map(|o| core::f32::consts::PI * o.radius.raw() * o.radius.raw())
.sum();
let width = (self.bounds.max.x - self.bounds.min.x).raw();
let height = (self.bounds.max.y - self.bounds.min.y).raw();
Area::new::<square_meter>((width * height - blocked).max(f32::EPSILON))
}
}
impl Clearance for World {
type Point = Point2f;
fn clearance(&self, p: Point2f) -> Length {
let b = &self.bounds;
let mut clearance = (p.x - b.min.x)
.raw()
.min((b.max.x - p.x).raw())
.min((p.y - b.min.y).raw())
.min((b.max.y - p.y).raw());
for o in self.obstacles() {
clearance = clearance.min(p.distance(o.center).raw() - o.radius.raw());
}
Length::new::<meter>(clearance)
}
fn clearance_segment(&self, a: Point2f, b: Point2f) -> Length {
let mut clearance = self.clearance(a).raw().min(self.clearance(b).raw());
for o in self.obstacles() {
clearance = clearance.min(distance_to_segment(a, b, o.center).raw() - o.radius.raw());
}
Length::new::<meter>(clearance)
}
}
pub trait RrtSpace: Clearance {
fn sample(&self, rng: &mut CuRng) -> Self::Point;
fn rrt_star_gamma(&self) -> Length;
}
impl RrtSpace for World {
fn sample(&self, rng: &mut CuRng) -> Point2f {
let b = &self.bounds;
Point2f::new(
b.min.x + (b.max.x - b.min.x) * rng.random::<f32>(),
b.min.y + (b.max.y - b.min.y) * rng.random::<f32>(),
)
}
fn rrt_star_gamma(&self) -> Length {
Length::new::<meter>(
2.0 * 1.5f32.sqrt() * (self.free_area().raw() / core::f32::consts::PI).sqrt(),
)
}
}
fn distance_to_segment<P: PlanPoint>(a: P, b: P, point: P) -> Length {
let (dot, len_sq) = P::project(a, b, point);
if len_sq.raw() <= f32::EPSILON {
return a.distance(point);
}
let along = ratio_of((dot.raw() / len_sq.raw()).clamp(0.0, 1.0));
a.lerp(b, along).distance(point)
}
pub(crate) fn meters(value: f32) -> Length {
Length::new::<meter>(value)
}
pub(crate) fn ratio_of(value: f32) -> Ratio {
Ratio::new::<cu29::units::si::ratio::ratio>(value)
}
fn shorter(a: Length, b: Length) -> Length {
if b < a { b } else { a }
}
#[derive(Debug, Clone, Copy, Reflect)]
pub struct RrtParams {
pub step_size: Length,
pub goal_bias: Ratio,
pub goal_threshold: Length,
pub gamma: Length,
pub prune_interval: u32,
pub max_nodes: u32,
}
impl Default for RrtParams {
fn default() -> Self {
Self {
step_size: meters(0.8),
goal_bias: ratio_of(0.05),
goal_threshold: meters(0.5),
gamma: meters(0.0),
prune_interval: 512,
max_nodes: 4000,
}
}
}
#[derive(Debug, Clone)]
struct TreeNode {
parent: Option<u32>,
cost: Length,
children: Vec<u32>,
}
pub struct RrtStar<S: RrtSpace = World> {
space: S,
params: RrtParams,
gamma: Length,
start: S::Point,
goal: S::Point,
positions: <S::Point as PlanPoint>::Set,
tree: Vec<TreeNode>,
best_goal: Option<u32>,
best_cost: Length,
iterations: u32,
rng: CuRng,
scratch_d2: Vec<Area>,
scratch_near: Vec<u32>,
scratch_stack: Vec<u32>,
}
impl<S: RrtSpace> RrtStar<S> {
pub fn new(space: S, params: RrtParams, start: S::Point, goal: S::Point, seed: u64) -> Self {
let mut planner = Self {
space,
params: RrtParams {
max_nodes: params.max_nodes.min(MAX_NODES as u32),
..params
},
gamma: meters(0.0),
start,
goal,
positions: <S::Point as PlanPoint>::Set::default(),
tree: Vec::new(),
best_goal: None,
best_cost: meters(f32::INFINITY),
iterations: 0,
rng: CuRng::from_seed(seed),
scratch_d2: vec![Area::default(); MAX_NODES],
scratch_near: Vec::new(),
scratch_stack: Vec::new(),
};
planner.restart(start, goal, seed);
planner
}
pub fn reset(&mut self, space: S, start: S::Point, goal: S::Point, seed: u64) {
self.space = space;
self.restart(start, goal, seed);
}
fn restart(&mut self, start: S::Point, goal: S::Point, seed: u64) {
self.gamma = if self.params.gamma > meters(0.0) {
self.params.gamma
} else {
self.space.rrt_star_gamma()
};
self.start = start;
self.goal = goal;
self.tree.clear();
self.positions.clear();
self.positions.push(start);
self.tree.push(TreeNode {
parent: None,
cost: meters(0.0),
children: Vec::new(),
});
self.best_goal = None;
self.best_cost = meters(f32::INFINITY);
self.iterations = 0;
self.rng = CuRng::from_seed(seed);
}
pub fn grow(&mut self, iterations: u32) {
for _ in 0..iterations {
self.iterations += 1;
if self.tree.len() < self.params.max_nodes as usize {
self.step();
}
if self.params.prune_interval > 0
&& self.iterations.is_multiple_of(self.params.prune_interval)
&& self.best_goal.is_some()
{
self.prune();
}
}
}
pub fn best_cost(&self) -> Length {
self.best_cost
}
pub fn has_solution(&self) -> bool {
self.best_goal.is_some()
}
pub fn tree_size(&self) -> u32 {
self.tree.len() as u32
}
pub fn iterations(&self) -> u32 {
self.iterations
}
pub fn is_exhausted(&self) -> bool {
self.tree.len() >= self.params.max_nodes as usize
&& (self.params.prune_interval == 0 || self.best_goal.is_none())
}
pub fn lower_bound(&self) -> Length {
self.start.distance(self.goal)
}
pub fn quality(&self) -> Ratio {
if !self.has_solution() {
return ratio_of(0.0);
}
let lower_bound = self.lower_bound();
if self.best_cost <= lower_bound {
return ratio_of(1.0);
}
ratio_of((lower_bound.raw() / self.best_cost.raw()).clamp(0.0, 1.0))
}
pub fn tree_path_len(&self) -> usize {
let Some(goal_node) = self.best_goal else {
return 0;
};
let mut len = 1; let mut cursor = Some(goal_node);
while let Some(index) = cursor {
len += 1;
cursor = self.tree[index as usize].parent;
}
len
}
pub fn write_path(&self, out: &mut [S::Point; MAX_WAYPOINTS]) -> Option<u32> {
let goal_node = self.best_goal?;
let mut chain = Vec::new();
let mut cursor = Some(goal_node);
while let Some(index) = cursor {
chain.push(self.positions.get(index as usize));
cursor = self.tree[index as usize].parent;
}
chain.reverse();
chain.push(self.goal);
let mut len = 0usize;
let mut at = 0usize;
loop {
if len == MAX_WAYPOINTS {
return None;
}
out[len] = chain[at];
len += 1;
if at == chain.len() - 1 {
return Some(len as u32);
}
let mut next = at + 1;
for candidate in (at + 2)..chain.len() {
if self.segment_free(chain[at], chain[candidate]) {
next = candidate;
}
}
at = next;
}
}
fn segment_free(&self, a: S::Point, b: S::Point) -> bool {
self.space.clearance_segment(a, b) > meters(0.0)
}
fn step(&mut self) {
let sample = self.sample();
let nearest = self.nearest(sample);
let from = self.positions.get(nearest as usize);
let new_pos = steer(from, sample, self.params.step_size);
if !self.segment_free(from, new_pos) {
return;
}
let radius = self.near_radius();
let radius_sq = Area::new::<square_meter>(radius.raw() * radius.raw());
let n = self.positions.len();
self.positions
.distances_squared(new_pos, &mut self.scratch_d2);
let mut near = core::mem::take(&mut self.scratch_near);
near.clear();
for index in 0..n {
if self.scratch_d2[index] <= radius_sq {
near.push(index as u32);
}
}
let mut parent = nearest;
let mut cost = self.tree[nearest as usize].cost + from.distance(new_pos);
for &index in near.iter() {
let candidate_pos = self.positions.get(index as usize);
let candidate_cost = self.tree[index as usize].cost + candidate_pos.distance(new_pos);
if candidate_cost < cost && self.segment_free(candidate_pos, new_pos) {
parent = index;
cost = candidate_cost;
}
}
let new_index = self.tree.len() as u32;
self.positions.push(new_pos);
self.tree.push(TreeNode {
parent: Some(parent),
cost,
children: Vec::new(),
});
self.tree[parent as usize].children.push(new_index);
for &index in near.iter() {
if index == parent {
continue;
}
let neighbor_pos = self.positions.get(index as usize);
let neighbor_cost = self.tree[index as usize].cost;
let rewired_cost = cost + neighbor_pos.distance(new_pos);
if rewired_cost < neighbor_cost
&& !self.is_ancestor(index, new_index)
&& self.segment_free(new_pos, neighbor_pos)
{
self.reparent(index, new_index, rewired_cost);
}
}
self.scratch_near = near;
let to_goal = new_pos.distance(self.goal);
if to_goal <= self.params.goal_threshold
&& self.segment_free(new_pos, self.goal)
&& cost + to_goal < self.best_cost
{
self.best_cost = cost + to_goal;
self.best_goal = Some(new_index);
}
if let Some(goal_node) = self.best_goal {
let cost = self.tree[goal_node as usize].cost;
let pos = self.positions.get(goal_node as usize);
self.best_cost = shorter(self.best_cost, cost + pos.distance(self.goal));
}
}
fn sample(&mut self) -> S::Point {
if self.rng.random::<f32>() < self.params.goal_bias.raw() {
return self.goal;
}
self.space.sample(&mut self.rng)
}
fn nearest(&mut self, point: S::Point) -> u32 {
let n = self.positions.len();
self.positions
.distances_squared(point, &mut self.scratch_d2);
let mut best = 0u32;
let mut best_distance = Area::new::<square_meter>(f32::INFINITY);
for index in 0..n {
let distance = self.scratch_d2[index];
if distance < best_distance {
best_distance = distance;
best = index as u32;
}
}
best
}
fn near_radius(&self) -> Length {
let n = (self.tree.len() as f32).max(2.0);
shorter(self.gamma * (n.ln() / n).sqrt(), self.params.step_size)
}
fn is_ancestor(&self, candidate: u32, node: u32) -> bool {
let mut cursor = self.tree[node as usize].parent;
while let Some(index) = cursor {
if index == candidate {
return true;
}
cursor = self.tree[index as usize].parent;
}
false
}
fn reparent(&mut self, node: u32, new_parent: u32, new_cost: Length) {
if let Some(old_parent) = self.tree[node as usize].parent {
self.tree[old_parent as usize]
.children
.retain(|&child| child != node);
}
self.tree[node as usize].parent = Some(new_parent);
self.tree[new_parent as usize].children.push(node);
let delta = new_cost - self.tree[node as usize].cost;
let mut stack = core::mem::take(&mut self.scratch_stack);
stack.clear();
stack.push(node);
while let Some(index) = stack.pop() {
self.tree[index as usize].cost += delta;
for i in 0..self.tree[index as usize].children.len() {
stack.push(self.tree[index as usize].children[i]);
}
}
self.scratch_stack = stack;
}
fn prune(&mut self) {
let mut protected = vec![false; self.tree.len()];
let mut cursor = self.best_goal;
while let Some(index) = cursor {
protected[index as usize] = true;
cursor = self.tree[index as usize].parent;
}
let mut keep = vec![false; self.tree.len()];
let mut stack = core::mem::take(&mut self.scratch_stack);
stack.clear();
stack.push(0);
keep[0] = true;
while let Some(index) = stack.pop() {
for i in 0..self.tree[index as usize].children.len() {
let child = self.tree[index as usize].children[i];
let cost = self.tree[child as usize].cost;
let pos = self.positions.get(child as usize);
if protected[child as usize] || cost + pos.distance(self.goal) <= self.best_cost {
keep[child as usize] = true;
stack.push(child);
}
}
}
self.scratch_stack = stack;
let mut remap = vec![u32::MAX; self.tree.len()];
let mut kept = Vec::with_capacity(self.tree.len());
for index in 0..self.tree.len() {
if keep[index] {
let destination = kept.len();
remap[index] = destination as u32;
self.positions.compact(destination, index);
kept.push(TreeNode {
parent: self.tree[index].parent,
cost: self.tree[index].cost,
children: Vec::new(),
});
}
}
self.positions.truncate(kept.len());
for node in kept.iter_mut() {
node.parent = node.parent.map(|parent| remap[parent as usize]);
}
for index in 0..kept.len() {
if let Some(parent) = kept[index].parent {
kept[parent as usize].children.push(index as u32);
}
}
self.best_goal = self.best_goal.map(|goal| remap[goal as usize]);
self.tree = kept;
}
}
fn steer<P: PlanPoint>(from: P, to: P, step_size: Length) -> P {
let distance = from.distance(to);
if distance <= step_size {
return to;
}
from.lerp(to, ratio_of(step_size.raw() / distance.raw()))
}
#[cfg(test)]
mod tests {
use super::*;
fn start() -> Point2f {
Point2f::from_meters(0.5, 0.5)
}
fn goal() -> Point2f {
Point2f::from_meters(9.5, 9.5)
}
fn planner(seed: u64) -> RrtStar {
RrtStar::new(World::depot(), RrtParams::default(), start(), goal(), seed)
}
#[derive(Clone)]
struct Room {
bounds: cu_spatial_payloads::BBox3f,
center: Point3f,
radius: Length,
}
impl Room {
fn new() -> Self {
Self {
bounds: cu_spatial_payloads::BBox3f::new(
Point3f::from_meters(0.0, 0.0, 0.0),
Point3f::from_meters(10.0, 10.0, 10.0),
),
center: Point3f::from_meters(5.0, 5.0, 5.0),
radius: Length::new::<meter>(1.5),
}
}
}
impl Clearance for Room {
type Point = Point3f;
fn clearance(&self, p: Point3f) -> Length {
let b = &self.bounds;
let walls = (p.x - b.min.x)
.raw()
.min((b.max.x - p.x).raw())
.min((p.y - b.min.y).raw())
.min((b.max.y - p.y).raw())
.min((p.z - b.min.z).raw())
.min((b.max.z - p.z).raw());
let sphere = p.distance(self.center).raw() - self.radius.raw();
Length::new::<meter>(walls.min(sphere))
}
fn clearance_segment(&self, a: Point3f, b: Point3f) -> Length {
let ends = self.clearance(a).raw().min(self.clearance(b).raw());
let sphere = distance_to_segment(a, b, self.center).raw() - self.radius.raw();
meters(ends.min(sphere))
}
}
impl RrtSpace for Room {
fn sample(&self, rng: &mut CuRng) -> Point3f {
let b = &self.bounds;
Point3f::new(
b.min.x + (b.max.x - b.min.x) * rng.random::<f32>(),
b.min.y + (b.max.y - b.min.y) * rng.random::<f32>(),
b.min.z + (b.max.z - b.min.z) * rng.random::<f32>(),
)
}
fn rrt_star_gamma(&self) -> Length {
let b = &self.bounds;
let side = |min: Length, max: Length| (max - min).raw();
let volume = side(b.min.x, b.max.x) * side(b.min.y, b.max.y) * side(b.min.z, b.max.z)
- 4.0 / 3.0 * core::f32::consts::PI * self.radius.raw().powi(3);
let zeta_3 = 4.0 / 3.0 * core::f32::consts::PI;
Length::new::<meter>(2.0 * (4.0f32 / 3.0).cbrt() * (volume / zeta_3).cbrt())
}
}
#[test]
fn the_same_planner_solves_a_3d_job() {
let room = Room::new();
let start = Point3f::from_meters(0.5, 0.5, 0.5);
let goal = Point3f::from_meters(9.5, 9.5, 9.5);
assert!(
room.clearance_segment(start, goal) <= meters(0.0),
"the straight line should be blocked, or the job is trivial"
);
let params = RrtParams {
step_size: meters(1.2),
..Default::default()
};
let mut planner = RrtStar::new(room.clone(), params, start, goal, 5);
planner.grow(4000);
assert!(planner.has_solution(), "no 3D path found");
assert_eq!(planner.positions.len(), planner.tree.len());
let mut waypoints = [Point3f::default(); MAX_WAYPOINTS];
let len = planner.write_path(&mut waypoints).expect("the path fits");
assert!(len >= 2);
assert_eq!(waypoints[0], start);
assert_eq!(waypoints[(len - 1) as usize], goal);
for pair in waypoints[..len as usize].windows(2) {
assert!(
room.clearance_segment(pair[0], pair[1]) > meters(0.0),
"the published 3D path crosses the sphere"
);
}
assert!(planner.best_cost() >= planner.lower_bound());
}
#[test]
fn world_rejects_too_many_obstacles() {
let radius = Length::new::<meter>(0.1);
let bounds = BBox2f::new(
Point2f::from_meters(0.0, 0.0),
Point2f::from_meters(10.0, 10.0),
);
let too_many = [Obstacle::new(Point2f::from_meters(1.0, 1.0), radius); MAX_OBSTACLES + 1];
assert!(World::new(bounds, &too_many).is_err());
assert!(World::new(bounds, &too_many[..MAX_OBSTACLES]).is_ok());
}
#[test]
fn clearance_signs_match_the_geometry() {
let world = World::depot();
let point = Point2f::from_meters;
assert!(world.clearance_segment(point(1.0, 1.0), point(5.0, 5.0)) <= meters(0.0));
assert!(world.clearance_segment(point(0.2, 0.2), point(0.2, 9.8)) > meters(0.0));
assert!(world.clearance_segment(start(), point(11.0, 0.5)) <= meters(0.0));
assert!(world.clearance(point(3.0, 3.0)) < meters(0.0));
}
#[test]
fn refinement_only_improves_the_path() {
let mut planner = planner(42);
planner.grow(400);
assert!(planner.has_solution(), "no first path after the base block");
let mut previous = planner.best_cost();
for _ in 0..16 {
planner.grow(256);
assert!(
planner.best_cost() <= previous + meters(1e-4),
"cost went up: {:?} then {:?}",
previous,
planner.best_cost()
);
previous = planner.best_cost();
}
assert!(planner.quality() > ratio_of(0.0) && planner.quality() <= ratio_of(1.0));
assert!(planner.best_cost() >= planner.lower_bound());
}
#[test]
fn published_path_is_valid_at_every_stop_point() {
let world = World::depot();
let derived = world.rrt_star_gamma().raw();
assert!(
(12.0..13.0).contains(&derived),
"gamma for the depot map should be near 12.4 m, got {derived}"
);
let mut longest_tree_path = 0;
let mut total_cost = [meters(0.0); 2];
for (seed, index) in (1..40u64).flat_map(|seed| [(seed, 0usize), (seed, 1)]) {
let params = RrtParams {
gamma: [meters(0.0), meters(3.0)][index],
..Default::default()
};
let mut planner = RrtStar::new(World::depot(), params, start(), goal(), seed);
planner.grow(400);
for _ in 0..24 {
planner.grow(256);
let mut waypoints = [Point2f::default(); MAX_WAYPOINTS];
let Some(len) = planner.write_path(&mut waypoints) else {
panic!("seed {seed}: the shortcut path did not fit");
};
longest_tree_path = longest_tree_path.max(planner.tree_path_len());
assert!(len >= 2, "a path has at least a start and a goal");
assert_eq!(waypoints[0], start());
assert_eq!(waypoints[(len - 1) as usize], goal());
for pair in waypoints[..len as usize].windows(2) {
assert!(
world.clearance_segment(pair[0], pair[1]) > meters(0.0),
"seed {seed}: published path crosses an obstacle"
);
}
let published = waypoints[..len as usize]
.windows(2)
.fold(meters(0.0), |sum, pair| sum + pair[0].distance(pair[1]));
assert!(
published <= planner.best_cost() + meters(1e-3),
"seed {seed}: shortcut path {:?} longer than the cost {:?}",
published,
planner.best_cost()
);
}
total_cost[index] += planner.best_cost();
}
assert!(
longest_tree_path > MAX_WAYPOINTS,
"the tree path never outgrew MAX_WAYPOINTS, so the shortcut was never exercised"
);
assert!(
total_cost[0] < total_cost[1],
"the derived gamma should refine to a shorter path than a small one"
);
}
#[test]
fn degenerate_job_reports_full_quality() {
let mut planner = RrtStar::new(World::depot(), RrtParams::default(), start(), start(), 3);
planner.grow(400);
assert!(planner.has_solution());
assert_eq!(planner.quality(), ratio_of(1.0));
}
#[test]
fn same_seed_replays_the_same_tree() {
let (mut a, mut b) = (planner(11), planner(11));
a.grow(600);
b.grow(300);
b.grow(300);
assert_eq!(a.tree_size(), b.tree_size());
assert_eq!(a.best_cost(), b.best_cost());
}
#[test]
fn pruning_keeps_the_best_path_reachable() {
let mut planner = planner(3);
planner.grow(1500);
let cost_before = planner.best_cost();
assert_eq!(planner.positions.len(), planner.tree.len());
planner.prune();
assert_eq!(planner.positions.len(), planner.tree.len());
assert!(planner.has_solution(), "pruning dropped the goal node");
for index in 0..planner.tree.len() {
let mut cursor = Some(index as u32);
let mut hops = 0;
while let Some(current) = cursor {
cursor = planner.tree[current as usize].parent;
hops += 1;
assert!(hops <= planner.tree.len(), "cycle in the tree");
}
}
assert_eq!(planner.best_cost(), cost_before);
}
#[test]
fn max_nodes_is_clamped_to_the_soa_capacity() {
let params = RrtParams {
max_nodes: MAX_NODES as u32 * 4,
prune_interval: 0,
..Default::default()
};
let mut planner = RrtStar::new(World::depot(), params, start(), goal(), 7);
assert_eq!(planner.params.max_nodes, MAX_NODES as u32);
planner.grow(MAX_NODES as u32 * 2);
assert!(planner.tree.len() <= MAX_NODES);
assert_eq!(planner.positions.len(), planner.tree.len());
assert!(
planner.is_exhausted(),
"the tree should have filled the cap"
);
}
}