Skip to main content

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}