repose_motion_planning/lib.rs
1//! Planner-agnostic joint-space motion planning for `copper-rs`.
2//!
3//! Every planner — sampling-based (RRT*, PRM, ...), optimization-based
4//! (CHOMP/TrajOpt style), search-based, or direct — implements one trait,
5//! [`CuJointPlanner`], and runs inside one Copper task,
6//! [`CuJointPlannerTask`]. The task owns the anytime base/refine contract,
7//! publishing, seeding, and replay state, so a planner only has to answer:
8//! build itself from validated parameters ([`CuJointPlanner::new`]), start a
9//! job ([`CuJointPlanner::begin`]), do a bounded quantum of work
10//! ([`CuJointPlanner::step`]), and write its best path in place
11//! ([`CuJointPlanner::write_best`]).
12//!
13//! Requests and results are planner-neutral payloads:
14//! [`CuJointPlanRequest`] carries the planning scene, exclusions, start and
15//! goal; [`CuJointPlanPath`] carries the path, its cost, the scene revision it
16//! was planned against, and an explicit [`CuPlanStatus`] — failures such as an
17//! invalid start or an exhausted search are published, not silently dropped.
18//!
19//! Built-in planners, each with a `Cu<Planner>Params` struct and a task alias:
20//! - Sampling: [`CuJointRrtConnect`] ([`CuJointRrtConnectTask`];
21//! bidirectional, feasibility-first) and [`CuJointPrm`] ([`CuJointPrmTask`];
22//! probabilistic roadmap, improving).
23//! - Search: [`CuJointAStar`] ([`CuJointAStarTask`]; A* on a joint-space
24//! lattice, resolution-complete).
25//! - Optimization: [`CuJointPathOptimizer`] ([`CuJointPathOptimizerTask`];
26//! deterministic gradient, CHOMP-style) and [`CuJointStomp`]
27//! ([`CuJointStompTask`]; seeded stochastic rollouts).
28//! - Direct: [`CuJointDirect`] ([`CuJointDirectTask`]; straight joint line,
29//! one shot).
30//!
31//! Every planner task has the same node config, [`CuJointPlannerParams`];
32//! switching planners means changing the task type and the `params` block:
33//!
34//! ```ron
35//! (
36//! id: "planner",
37//! type: "repose_motion_planning::CuJointPrmTask<6>",
38//! config: {
39//! "chain": "igus_rebel",
40//! "collision_model": "igus_rebel",
41//! "params": {"connection_radius": 1.5, "max_nodes": 512},
42//! "refine_budget": 16, // optional; default per planner
43//! },
44//! )
45//! ```
46//!
47//! A plan-and-execute graph needs no application code:
48//!
49//! ```text
50//! joint state ─► CuJointGoalTask ─► CuJoint<Planner>Task ─► CuPlanToTrajectoryTask ─► follower
51//! ```
52//!
53//! [`CuJointGoalTask`] turns measured state and a configured goal into a
54//! request, and [`CuPlanToTrajectoryTask`] times each final path for
55//! `repose_trajectory::CuFollowTrajectoryTask`.
56//!
57//! RRT* lives in `repose-joint-rrt-star`. Every planner shares
58//! [`CuSceneValidity`] for collision and clearance queries.
59
60#![cfg_attr(not(feature = "std"), no_std)]
61
62#[cfg(not(feature = "std"))]
63extern crate alloc;
64
65/// Implements [`TypePath`] for a const-generic planner `$ty<N, M>` with the
66/// fixed path `$path` (const arguments are not part of the path).
67#[macro_export]
68macro_rules! impl_planner_type_path {
69 ($ty:ident, $path:literal) => {
70 impl<const N: usize, const M: usize> ::cu29::prelude::TypePath for $ty<N, M> {
71 fn type_path() -> &'static str {
72 $path
73 }
74 fn short_type_path() -> &'static str {
75 ::core::stringify!($ty)
76 }
77 fn type_ident() -> Option<&'static str> {
78 Some(::core::stringify!($ty))
79 }
80 fn crate_name() -> Option<&'static str> {
81 $path.split("::").next()
82 }
83 fn module_path() -> Option<&'static str> {
84 $path.rsplit_once("::").map(|(module, _)| module)
85 }
86 }
87 };
88}
89
90mod astar;
91mod common;
92mod direct;
93mod goal_task;
94mod optimizer;
95mod prm;
96mod rrt_connect;
97mod stomp;
98mod task;
99mod trajectory_task;
100mod validity;
101
102pub use astar::ASTAR_MAX_NODES;
103pub use astar::CuJointAStar;
104pub use astar::CuJointAStarParams;
105pub use astar::CuJointAStarTask;
106pub use direct::CuJointDirect;
107pub use direct::CuJointDirectParams;
108pub use direct::CuJointDirectTask;
109pub use goal_task::CuJointGoalParams;
110pub use goal_task::CuJointGoalTask;
111pub use optimizer::CuJointPathOptimizer;
112pub use optimizer::CuJointPathOptimizerParams;
113pub use optimizer::CuJointPathOptimizerTask;
114pub use prm::CuJointPrm;
115pub use prm::CuJointPrmParams;
116pub use prm::CuJointPrmTask;
117pub use prm::PRM_MAX_NEIGHBORS;
118pub use prm::PRM_MAX_NODES;
119pub use rrt_connect::CuJointRrtConnect;
120pub use rrt_connect::CuJointRrtConnectParams;
121pub use rrt_connect::CuJointRrtConnectTask;
122pub use rrt_connect::RRT_CONNECT_MAX_NODES;
123pub use stomp::CuJointStomp;
124pub use stomp::CuJointStompParams;
125pub use stomp::CuJointStompTask;
126pub use stomp::STOMP_MAX_ROLLOUTS;
127pub use task::CuJointPlannerDebugState;
128pub use task::CuJointPlannerParams;
129pub use task::CuJointPlannerTask;
130pub use task::planner_resources;
131pub use trajectory_task::CuPlanToTrajectoryTask;
132pub use validity::CuSceneValidity;
133pub use validity::joint_distance;
134
135use bincode::Decode;
136use bincode::Encode;
137use cu29::cutask_anytime::Quality;
138use cu29::prelude::*;
139use repose_collision::CuPlanningScene;
140use repose_collision::CuRobotCollisionModel;
141use repose_collision::CuSceneExclusions;
142use repose_collision::CuSceneRevision;
143use repose_kinematics::CuJointPosition;
144use repose_kinematics::CuJointPositionsPayload;
145use repose_kinematics::CuKinematicChain;
146use serde::Deserialize;
147use serde::Serialize;
148use serde::de::DeserializeOwned;
149
150/// Fixed capacity of waypoints in a published [`CuJointPlanPath`].
151pub const MAX_WAYPOINTS: usize = 32;
152/// Fixed capacity of scene obstacles in a [`CuJointPlanRequest`].
153pub const MAX_OBSTACLES: usize = 16;
154/// Fixed capacity of attached shapes in a [`CuJointPlanRequest`].
155pub const MAX_ATTACHED_SHAPES: usize = 8;
156/// Fixed capacity of stable-ID exclusions in a [`CuJointPlanRequest`].
157pub const MAX_EXCLUSIONS: usize = 16;
158
159/// Joint-space planning request (`CuMsgPayload`-compliant).
160#[derive(Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
161#[reflect(no_field_bounds, from_reflect = false)]
162pub struct CuJointPlanRequest<const N: usize> {
163 pub scene: CuPlanningScene<MAX_OBSTACLES, MAX_ATTACHED_SHAPES>,
164 pub exclusions: CuSceneExclusions<MAX_EXCLUSIONS>,
165 #[serde(with = "repose_kinematics::serde_arrays::position_array")]
166 pub start: [CuJointPosition; N],
167 #[serde(with = "repose_kinematics::serde_arrays::position_array")]
168 pub goal: [CuJointPosition; N],
169}
170
171impl<const N: usize> Default for CuJointPlanRequest<N> {
172 fn default() -> Self {
173 Self {
174 scene: CuPlanningScene::new(),
175 exclusions: CuSceneExclusions::new(),
176 start: [CuJointPosition::default(); N],
177 goal: [CuJointPosition::default(); N],
178 }
179 }
180}
181
182/// Outcome of a planning job, carried by [`CuJointPlanPath`].
183#[derive(
184 Default, Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
185)]
186#[serde(rename_all = "snake_case")]
187pub enum CuPlanStatus {
188 /// The job is running and has no collision-free path yet.
189 #[default]
190 Searching,
191 /// A collision-free path exists; more work may improve it.
192 Improving,
193 /// A collision-free path exists and further work cannot improve it.
194 Converged,
195 /// The planner gave up without a path (search budget or capacity spent,
196 /// optimizer stuck in collision, or no direct line).
197 Exhausted,
198 /// Parameters, scene, attached shapes, or exclusions are invalid.
199 InvalidRequest,
200 /// The start configuration is out of limits or in collision.
201 InvalidStart,
202 /// The goal configuration is out of limits or in collision.
203 InvalidGoal,
204 /// A path exists but does not fit [`MAX_WAYPOINTS`].
205 CapacityExceeded,
206}
207
208impl CuPlanStatus {
209 /// `true` when a collision-free path is available.
210 pub const fn has_path(self) -> bool {
211 matches!(self, Self::Improving | Self::Converged)
212 }
213
214 /// `true` when no further [`CuJointPlanner::step`] can change the outcome.
215 pub const fn is_final(self) -> bool {
216 !matches!(self, Self::Searching | Self::Improving)
217 }
218}
219
220/// Planned joint-space path (`CuMsgPayload`-compliant).
221#[derive(Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
222#[reflect(no_field_bounds, from_reflect = false)]
223pub struct CuJointPlanPath<const N: usize> {
224 pub waypoints: [CuJointPositionsPayload<N>; MAX_WAYPOINTS],
225 /// Number of valid waypoints in `waypoints[..len]`; `0` unless the status
226 /// has a path.
227 pub len: u32,
228 /// Dimensionless joint-space L2 path length under the planner's metric.
229 pub cost: f64,
230 /// Planner work spent on the job (samples, optimizer iterations, ...).
231 pub effort: u32,
232 pub status: CuPlanStatus,
233 /// Scene revision the path was planned against.
234 pub scene_revision: CuSceneRevision,
235 /// Collision exclusions captured for this plan.
236 pub exclusions: CuSceneExclusions<MAX_EXCLUSIONS>,
237}
238
239impl<const N: usize> Default for CuJointPlanPath<N> {
240 fn default() -> Self {
241 Self {
242 waypoints: core::array::from_fn(|_| CuJointPositionsPayload::default()),
243 len: 0,
244 cost: f64::INFINITY,
245 effort: 0,
246 status: CuPlanStatus::default(),
247 scene_revision: CuSceneRevision::INITIAL,
248 exclusions: CuSceneExclusions::new(),
249 }
250 }
251}
252
253impl<const N: usize> CuJointPlanPath<N> {
254 /// Valid waypoints.
255 pub fn waypoints(&self) -> &[CuJointPositionsPayload<N>] {
256 &self.waypoints[..(self.len as usize).min(MAX_WAYPOINTS)]
257 }
258}
259
260/// How a planner explores the configuration space.
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
262#[serde(rename_all = "snake_case")]
263pub enum CuPlannerKind {
264 /// Random sampling (RRT*, PRM, ...); results depend on the job seed.
265 Sampling,
266 /// Deterministic trajectory optimization (CHOMP, TrajOpt, ...).
267 Optimization,
268 /// Deterministic graph or lattice search (A*, ...).
269 Search,
270 /// A single deterministic construction (straight line, ...).
271 Direct,
272}
273
274/// A joint-space motion planner usable by [`CuJointPlannerTask`].
275///
276/// A planner is built once from robot models and its validated
277/// [`Params`](Self::Params) with [`new`](Self::new); invalid parameters fail
278/// there, not at the first job. Jobs are bounded and allocation-free: `begin`
279/// resets in place, `step(budget)` does at most `budget` units of work, and
280/// `write_best` copies the best path into a caller-owned payload.
281/// Deterministic planners ignore the seed; sampling planners must derive all
282/// randomness from it so replay reproduces every job.
283///
284/// `M` is the number of collision-model shapes (default `N`).
285///
286/// Planners implement [`TypePath`] so the generic planner task stays
287/// nameable and inspectable under Copper's runtime reflection; use
288/// [`impl_planner_type_path!`] for a const-generic planner.
289pub trait CuJointPlanner<const N: usize, const M: usize = N>: Sized + TypePath {
290 /// Exploration family, for diagnostics and documentation.
291 const KIND: CuPlannerKind;
292
293 /// Default `(base_budget, refine_budget)` work units of
294 /// [`CuJointPlannerTask`] when the node config leaves them out.
295 const DEFAULT_BUDGETS: (u32, u32) = (64, 32);
296
297 /// The planner's parameter struct: the `params` block of the planner
298 /// task's node config (`serde(default, deny_unknown_fields)`).
299 type Params: Clone + Default + DeserializeOwned;
300
301 /// Builds the planner for explicit robot models, validating `params`.
302 fn new(
303 chain: CuKinematicChain<N>,
304 collision_model: CuRobotCollisionModel<N, M>,
305 params: Self::Params,
306 ) -> CuResult<Self>;
307
308 /// Starts a new job for `request`, discarding the previous one.
309 fn begin(&mut self, request: &CuJointPlanRequest<N>, seed: u64) -> CuResult<CuPlanStatus>;
310
311 /// Runs one bounded quantum of at most `budget` work units.
312 fn step(&mut self, budget: u32) -> CuResult<CuPlanStatus>;
313
314 /// Writes the best path so far into `out` (waypoints, length, cost,
315 /// effort, scene revision, exclusions) and returns the job status. `out`
316 /// holds no waypoints unless the status has a path.
317 fn write_best(&self, out: &mut CuJointPlanPath<N>) -> CuPlanStatus;
318
319 /// Quality of the best path in `0.0..=1.0` (`0.0` without a path).
320 fn quality(&self) -> Quality;
321}