1pub mod rrt;
10
11pub use cu_spatial_payloads::{BBox2f, Point2f};
12pub use rrt::{
13 Clearance, MAX_NODES, MAX_OBSTACLES, MAX_WAYPOINTS, Obstacle, PlanPoint, PointSet, RrtParams,
14 RrtSpace, RrtStar, World,
15};
16
17use bincode::de::Decoder;
18use bincode::enc::Encoder;
19use bincode::error::{DecodeError, EncodeError};
20use bincode::{Decode, Encode};
21use cu_rng::prelude::*;
22use cu29::cutask_anytime::{AnytimeStatus, CuAnytimeTask, Quality, quality_from_f32};
23use cu29::prelude::*;
24use cu29::units::si::f32::Length;
25use serde::{Deserialize, Serialize};
26
27const CONVERGED_QUALITY: f32 = 0.999;
30
31fn converged(quality: Quality) -> bool {
33 quality.raw() >= CONVERGED_QUALITY
34}
35
36#[derive(Default, Debug, Clone, Encode, Decode, Serialize, Deserialize, Reflect)]
39pub struct PlanRequest {
40 pub world: World,
41 pub start: Point2f,
42 pub goal: Point2f,
43}
44
45#[derive(Default, Debug, Clone, Encode, Decode, Serialize, Deserialize, Reflect)]
50pub struct PlanPath {
51 pub waypoints: [Point2f; MAX_WAYPOINTS],
52 pub len: u32,
54 pub cost: Length,
58 pub iterations: u32,
60}
61
62#[derive(Default, Debug, Reflect)]
68pub struct PlannerDebugState {
69 pub iterations: u32,
70 pub tree_size: u32,
71 pub tree_path_len: u32,
73 pub best_cost: Length,
75 pub published_cost: Length,
77 pub published_quality: Quality,
78}
79
80mod planner_resources {
81 use super::*;
82 resources!({ rng => Owned<CuRng> });
83}
84
85#[derive(Reflect)]
95pub struct RrtStarPlanner {
96 params: RrtParams,
97 base_iterations: u32,
99 block_iterations: u32,
101 base_seed: u64,
104 job_counter: u64,
107 #[reflect(ignore)]
109 planner: Option<RrtStar>,
110 published_cost: Length,
113 published_quality: Quality,
114}
115
116impl Freezable for RrtStarPlanner {
119 fn freeze<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
120 Encode::encode(&self.base_seed, encoder)?;
121 Encode::encode(&self.job_counter, encoder)
122 }
123
124 fn thaw<D: Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
125 self.base_seed = Decode::decode(decoder)?;
126 self.job_counter = Decode::decode(decoder)?;
127 Ok(())
128 }
129}
130
131impl RrtStarPlanner {
132 fn publish(&mut self, output: &mut CuMsg<PlanPath>) -> Quality {
137 if let Some(planner) = self.planner.as_ref()
138 && planner.has_solution()
139 && planner.best_cost() < self.published_cost
140 {
141 let mut waypoints = [Point2f::default(); MAX_WAYPOINTS];
142 if let Some(len) = planner.write_path(&mut waypoints) {
145 output.set_payload(PlanPath {
146 waypoints,
147 len,
148 cost: planner.best_cost(),
149 iterations: planner.iterations(),
150 });
151 self.published_cost = planner.best_cost();
152 self.published_quality = planner.quality();
153 }
154 }
155 self.published_quality
156 }
157
158 fn debug_state(&self) -> PlannerDebugState {
160 let planner = self.planner.as_ref();
161 PlannerDebugState {
162 iterations: planner.map_or(0, RrtStar::iterations),
163 tree_size: planner.map_or(0, RrtStar::tree_size),
164 tree_path_len: planner.map_or(0, |p| p.tree_path_len() as u32),
165 best_cost: planner.map_or(rrt::meters(f32::INFINITY), RrtStar::best_cost),
166 published_cost: self.published_cost,
167 published_quality: self.published_quality,
168 }
169 }
170}
171
172impl CuAnytimeTask for RrtStarPlanner {
173 type Input<'m> = input_msg!(PlanRequest);
174 type Output<'m> = output_msg!(PlanPath);
175 type Resources<'r> = planner_resources::Resources;
176 type Quality = Quality;
177
178 fn register_debug_state_types(registry: &mut TypeRegistry) {
182 registry.register::<PlannerDebugState>();
183 }
184
185 fn debug_state_type_path() -> &'static str {
186 PlannerDebugState::type_path()
187 }
188
189 fn with_debug_state<R>(&self, f: impl FnOnce(&dyn bevy_reflect::Reflect) -> R) -> R {
190 f(&self.debug_state())
191 }
192
193 fn new(config: Option<&ComponentConfig>, resources: Self::Resources<'_>) -> CuResult<Self> {
194 let mut params = RrtParams::default();
195 let mut base_iterations = 400u32;
196 let mut block_iterations = 256u32;
197 if let Some(config) = config {
198 if let Some(value) = config.get::<f32>("step_size")? {
199 params.step_size = rrt::meters(value);
200 }
201 if let Some(value) = config.get::<f32>("goal_bias")? {
202 params.goal_bias = rrt::ratio_of(value);
203 }
204 if let Some(value) = config.get::<f32>("goal_threshold")? {
205 params.goal_threshold = rrt::meters(value);
206 }
207 if let Some(value) = config.get::<f32>("gamma")? {
208 params.gamma = rrt::meters(value);
209 }
210 if let Some(value) = config.get::<u32>("prune_interval")? {
211 params.prune_interval = value;
212 }
213 if let Some(value) = config.get::<u32>("max_nodes")? {
214 if value as usize > MAX_NODES {
215 warning!(
216 "rrt*: max_nodes {} exceeds the capacity {}, capping it",
217 value,
218 MAX_NODES as u32
219 );
220 }
221 params.max_nodes = value;
222 }
223 if let Some(value) = config.get::<u32>("base_iterations")? {
224 base_iterations = value;
225 }
226 if let Some(value) = config.get::<u32>("block_iterations")? {
227 block_iterations = value;
228 }
229 }
230 let Owned(mut rng) = resources.rng;
231 Ok(Self {
232 params,
233 base_iterations,
234 block_iterations,
235 base_seed: rng.random::<u64>(),
236 job_counter: 0,
237 planner: None,
238 published_cost: rrt::meters(f32::INFINITY),
239 published_quality: quality_from_f32(0.0),
240 })
241 }
242
243 fn base(
244 &mut self,
245 _ctx: &CuContext,
246 input: &Self::Input<'_>,
247 output: &mut Self::Output<'_>,
248 ) -> CuResult<AnytimeStatus<Quality>> {
249 let Some(request) = input.payload() else {
250 output.clear_payload();
254 self.published_cost = rrt::meters(f32::INFINITY);
255 self.published_quality = quality_from_f32(0.0);
256 return Ok(AnytimeStatus::Aborted);
257 };
258 self.job_counter = self.job_counter.wrapping_add(1);
259 let seed = self.base_seed.wrapping_add(self.job_counter);
262 let planner = match self.planner.as_mut() {
263 Some(planner) => {
265 planner.reset(request.world.clone(), request.start, request.goal, seed);
266 planner
267 }
268 None => self.planner.insert(RrtStar::new(
269 request.world.clone(),
270 self.params,
271 request.start,
272 request.goal,
273 seed,
274 )),
275 };
276 planner.grow(self.base_iterations);
277 self.published_cost = rrt::meters(f32::INFINITY);
278 self.published_quality = quality_from_f32(0.0);
279 output.clear_payload();
282 let quality = self.publish(output);
283 if converged(self.published_quality) {
284 return Ok(AnytimeStatus::Converged(quality));
287 }
288 Ok(AnytimeStatus::Improved(quality))
291 }
292
293 fn refine(
294 &mut self,
295 _ctx: &CuContext,
296 output: &mut Self::Output<'_>,
297 ) -> CuResult<AnytimeStatus<Quality>> {
298 let planner = self
299 .planner
300 .as_mut()
301 .ok_or("rrt*: refine() without a job from base()")?;
302 if planner.is_exhausted() {
303 return Ok(AnytimeStatus::Converged(self.published_quality));
304 }
305 planner.grow(self.block_iterations);
306 let quality = self.publish(output);
307 if converged(self.published_quality) {
308 return Ok(AnytimeStatus::Converged(quality));
310 }
311 Ok(AnytimeStatus::Improved(quality))
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 fn start() -> Point2f {
320 Point2f::from_meters(0.5, 0.5)
321 }
322
323 fn goal() -> Point2f {
324 Point2f::from_meters(9.5, 9.5)
325 }
326
327 fn test_resources(seed: u64) -> planner_resources::Resources {
329 planner_resources::Resources {
330 rng: Owned(CuRng::from_seed(seed)),
331 }
332 }
333
334 #[test]
338 fn refinement_only_commits_improvements() {
339 let ctx = CuContext::new_with_clock();
340 let mut task = RrtStarPlanner::new(None, test_resources(42)).unwrap();
341 let input = CuMsg::new(Some(PlanRequest {
342 world: World::depot(),
343 start: start(),
344 goal: goal(),
345 }));
346 let mut output = CuMsg::new(None);
347
348 task.start(&ctx).unwrap();
349 let status = task.base(&ctx, &input, &mut output).unwrap();
350 assert!(matches!(status, AnytimeStatus::Improved(_)));
351
352 let mut best = f32::INFINITY;
353 for _ in 0..24 {
354 if let AnytimeStatus::Aborted = task.refine(&ctx, &mut output).unwrap() {
355 panic!("the planner should not abort on a solvable map");
356 }
357 if let Some(path) = output.payload() {
358 assert!(
359 path.cost.raw() <= best + 1e-4,
360 "the output regressed: {best} then {}",
361 path.cost.raw()
362 );
363 best = path.cost.raw();
364 }
365 }
366 assert!(output.payload().is_some(), "no path after 24 quanta");
367
368 let state = task.debug_state();
369 assert_eq!(state.published_cost.raw(), best);
370 assert!(state.best_cost <= state.published_cost);
371 assert!(state.published_quality.raw() > 0.0);
372 assert!(state.iterations > 0 && state.tree_size > 0);
373 }
374
375 #[test]
378 fn missing_request_skips_the_job() {
379 let ctx = CuContext::new_with_clock();
380 let mut task = RrtStarPlanner::new(None, test_resources(42)).unwrap();
381 let input = CuMsg::new(Some(PlanRequest {
382 world: World::depot(),
383 start: start(),
384 goal: goal(),
385 }));
386 let mut output = CuMsg::new(None);
387
388 task.start(&ctx).unwrap();
389 task.base(&ctx, &input, &mut output).unwrap();
390 for _ in 0..24 {
391 task.refine(&ctx, &mut output).unwrap();
392 }
393 assert!(output.payload().is_some(), "no path to leak");
394
395 let empty = CuMsg::new(None);
396 let status = task.base(&ctx, &empty, &mut output).unwrap();
397 assert!(matches!(status, AnytimeStatus::Aborted));
398 assert!(output.payload().is_none(), "the old path leaked");
399 assert_eq!(task.debug_state().published_quality.raw(), 0.0);
400 }
401
402 #[test]
405 fn start_on_goal_converges_immediately() {
406 let ctx = CuContext::new_with_clock();
407 let mut task = RrtStarPlanner::new(None, test_resources(3)).unwrap();
408 let input = CuMsg::new(Some(PlanRequest {
409 world: World::depot(),
410 start: start(),
411 goal: start(),
412 }));
413 let mut output = CuMsg::new(None);
414
415 task.start(&ctx).unwrap();
416 let status = task.base(&ctx, &input, &mut output).unwrap();
417 assert!(matches!(status, AnytimeStatus::Converged(_)));
418 assert!(output.payload().is_some(), "a trivial path is still a path");
419 assert_eq!(task.debug_state().published_quality.raw(), 1.0);
420 }
421}