pub mod rrt;
pub use cu_spatial_payloads::{BBox2f, Point2f};
pub use rrt::{
Clearance, MAX_NODES, MAX_OBSTACLES, MAX_WAYPOINTS, Obstacle, PlanPoint, PointSet, RrtParams,
RrtSpace, RrtStar, World,
};
use bincode::de::Decoder;
use bincode::enc::Encoder;
use bincode::error::{DecodeError, EncodeError};
use bincode::{Decode, Encode};
use cu_rng::prelude::*;
use cu29::cutask_anytime::{AnytimeStatus, CuAnytimeTask, Quality, quality_from_f32};
use cu29::prelude::*;
use cu29::units::si::f32::Length;
use serde::{Deserialize, Serialize};
const CONVERGED_QUALITY: f32 = 0.999;
fn converged(quality: Quality) -> bool {
quality.raw() >= CONVERGED_QUALITY
}
#[derive(Default, Debug, Clone, Encode, Decode, Serialize, Deserialize, Reflect)]
pub struct PlanRequest {
pub world: World,
pub start: Point2f,
pub goal: Point2f,
}
#[derive(Default, Debug, Clone, Encode, Decode, Serialize, Deserialize, Reflect)]
pub struct PlanPath {
pub waypoints: [Point2f; MAX_WAYPOINTS],
pub len: u32,
pub cost: Length,
pub iterations: u32,
}
#[derive(Default, Debug, Reflect)]
pub struct PlannerDebugState {
pub iterations: u32,
pub tree_size: u32,
pub tree_path_len: u32,
pub best_cost: Length,
pub published_cost: Length,
pub published_quality: Quality,
}
mod planner_resources {
use super::*;
resources!({ rng => Owned<CuRng> });
}
#[derive(Reflect)]
pub struct RrtStarPlanner {
params: RrtParams,
base_iterations: u32,
block_iterations: u32,
base_seed: u64,
job_counter: u64,
#[reflect(ignore)]
planner: Option<RrtStar>,
published_cost: Length,
published_quality: Quality,
}
impl Freezable for RrtStarPlanner {
fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
Encode::encode(&self.base_seed, encoder)?;
Encode::encode(&self.job_counter, encoder)
}
fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
self.base_seed = Decode::decode(decoder)?;
self.job_counter = Decode::decode(decoder)?;
Ok(())
}
}
impl RrtStarPlanner {
fn publish(&mut self, output: &mut CuMsg<PlanPath>) -> Quality {
if let Some(planner) = self.planner.as_ref()
&& planner.has_solution()
&& planner.best_cost() < self.published_cost
{
let mut waypoints = [Point2f::default(); MAX_WAYPOINTS];
if let Some(len) = planner.write_path(&mut waypoints) {
output.set_payload(PlanPath {
waypoints,
len,
cost: planner.best_cost(),
iterations: planner.iterations(),
});
self.published_cost = planner.best_cost();
self.published_quality = planner.quality();
}
}
self.published_quality
}
fn debug_state(&self) -> PlannerDebugState {
let planner = self.planner.as_ref();
PlannerDebugState {
iterations: planner.map_or(0, RrtStar::iterations),
tree_size: planner.map_or(0, RrtStar::tree_size),
tree_path_len: planner.map_or(0, |p| p.tree_path_len() as u32),
best_cost: planner.map_or(rrt::meters(f32::INFINITY), RrtStar::best_cost),
published_cost: self.published_cost,
published_quality: self.published_quality,
}
}
}
impl CuAnytimeTask for RrtStarPlanner {
type Input<'m> = input_msg!(PlanRequest);
type Output<'m> = output_msg!(PlanPath);
type Resources<'r> = planner_resources::Resources;
type Quality = Quality;
fn register_debug_state_types(registry: &mut TypeRegistry) {
registry.register::<PlannerDebugState>();
}
fn debug_state_type_path() -> &'static str {
PlannerDebugState::type_path()
}
fn with_debug_state<R>(&self, f: impl FnOnce(&dyn bevy_reflect::Reflect) -> R) -> R {
f(&self.debug_state())
}
fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self> {
let mut params = RrtParams::default();
let mut base_iterations = 400u32;
let mut block_iterations = 256u32;
if let Some(config) = config {
if let Some(value) = config.get::<f32>("step_size")? {
params.step_size = rrt::meters(value);
}
if let Some(value) = config.get::<f32>("goal_bias")? {
params.goal_bias = rrt::ratio_of(value);
}
if let Some(value) = config.get::<f32>("goal_threshold")? {
params.goal_threshold = rrt::meters(value);
}
if let Some(value) = config.get::<f32>("gamma")? {
params.gamma = rrt::meters(value);
}
if let Some(value) = config.get::<u32>("prune_interval")? {
params.prune_interval = value;
}
if let Some(value) = config.get::<u32>("max_nodes")? {
if value as usize > MAX_NODES {
warning!(
"rrt*: max_nodes {} exceeds the capacity {}, capping it",
value,
MAX_NODES as u32
);
}
params.max_nodes = value;
}
if let Some(value) = config.get::<u32>("base_iterations")? {
base_iterations = value;
}
if let Some(value) = config.get::<u32>("block_iterations")? {
block_iterations = value;
}
}
let Owned(mut rng) = resources.rng;
Ok(Self {
params,
base_iterations,
block_iterations,
base_seed: rng.random::<u64>(),
job_counter: 0,
planner: None,
published_cost: rrt::meters(f32::INFINITY),
published_quality: quality_from_f32(0.0),
})
}
fn base(
&mut self,
_ctx: &CuContext,
input: &Self::Input<'_>,
output: &mut Self::Output<'_>,
) -> CuResult<AnytimeStatus<Quality>> {
let Some(request) = input.payload() else {
output.clear_payload();
self.published_cost = rrt::meters(f32::INFINITY);
self.published_quality = quality_from_f32(0.0);
return Ok(AnytimeStatus::Aborted);
};
self.job_counter = self.job_counter.wrapping_add(1);
let seed = self.base_seed.wrapping_add(self.job_counter);
let planner = match self.planner.as_mut() {
Some(planner) => {
planner.reset(request.world.clone(), request.start, request.goal, seed);
planner
}
None => self.planner.insert(RrtStar::new(
request.world.clone(),
self.params,
request.start,
request.goal,
seed,
)),
};
planner.grow(self.base_iterations);
self.published_cost = rrt::meters(f32::INFINITY);
self.published_quality = quality_from_f32(0.0);
output.clear_payload();
let quality = self.publish(output);
if converged(self.published_quality) {
return Ok(AnytimeStatus::Converged(quality));
}
Ok(AnytimeStatus::Improved(quality))
}
fn refine(
&mut self,
_ctx: &CuContext,
output: &mut Self::Output<'_>,
) -> CuResult<AnytimeStatus<Quality>> {
let planner = self
.planner
.as_mut()
.ok_or("rrt*: refine() without a job from base()")?;
if planner.is_exhausted() {
return Ok(AnytimeStatus::Converged(self.published_quality));
}
planner.grow(self.block_iterations);
let quality = self.publish(output);
if converged(self.published_quality) {
return Ok(AnytimeStatus::Converged(quality));
}
Ok(AnytimeStatus::Improved(quality))
}
}
#[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 test_resources(seed: u64) -> planner_resources::Resources {
planner_resources::Resources {
rng: Owned(CuRng::from_seed(seed)),
}
}
#[test]
fn refinement_only_commits_improvements() {
let ctx = CuContext::new_with_clock();
let mut task = RrtStarPlanner::new(None, test_resources(42)).unwrap();
let input = CuMsg::new(Some(PlanRequest {
world: World::depot(),
start: start(),
goal: goal(),
}));
let mut output = CuMsg::new(None);
task.start(&ctx).unwrap();
let status = task.base(&ctx, &input, &mut output).unwrap();
assert!(matches!(status, AnytimeStatus::Improved(_)));
let mut best = f32::INFINITY;
for _ in 0..24 {
if let AnytimeStatus::Aborted = task.refine(&ctx, &mut output).unwrap() {
panic!("the planner should not abort on a solvable map");
}
if let Some(path) = output.payload() {
assert!(
path.cost.raw() <= best + 1e-4,
"the output regressed: {best} then {}",
path.cost.raw()
);
best = path.cost.raw();
}
}
assert!(output.payload().is_some(), "no path after 24 quanta");
let state = task.debug_state();
assert_eq!(state.published_cost.raw(), best);
assert!(state.best_cost <= state.published_cost);
assert!(state.published_quality.raw() > 0.0);
assert!(state.iterations > 0 && state.tree_size > 0);
}
#[test]
fn missing_request_skips_the_job() {
let ctx = CuContext::new_with_clock();
let mut task = RrtStarPlanner::new(None, test_resources(42)).unwrap();
let input = CuMsg::new(Some(PlanRequest {
world: World::depot(),
start: start(),
goal: goal(),
}));
let mut output = CuMsg::new(None);
task.start(&ctx).unwrap();
task.base(&ctx, &input, &mut output).unwrap();
for _ in 0..24 {
task.refine(&ctx, &mut output).unwrap();
}
assert!(output.payload().is_some(), "no path to leak");
let empty = CuMsg::new(None);
let status = task.base(&ctx, &empty, &mut output).unwrap();
assert!(matches!(status, AnytimeStatus::Aborted));
assert!(output.payload().is_none(), "the old path leaked");
assert_eq!(task.debug_state().published_quality.raw(), 0.0);
}
#[test]
fn start_on_goal_converges_immediately() {
let ctx = CuContext::new_with_clock();
let mut task = RrtStarPlanner::new(None, test_resources(3)).unwrap();
let input = CuMsg::new(Some(PlanRequest {
world: World::depot(),
start: start(),
goal: start(),
}));
let mut output = CuMsg::new(None);
task.start(&ctx).unwrap();
let status = task.base(&ctx, &input, &mut output).unwrap();
assert!(matches!(status, AnytimeStatus::Converged(_)));
assert!(output.payload().is_some(), "a trivial path is still a path");
assert_eq!(task.debug_state().published_quality.raw(), 1.0);
}
}