1use crate::CuJointPlanPath;
12use crate::CuJointPlanRequest;
13use crate::CuJointPlanner;
14use crate::CuJointPlannerTask;
15use crate::CuPlanStatus;
16use crate::CuPlannerKind;
17use crate::common::check_endpoints;
18use crate::common::length_quality;
19use crate::common::sample_uniform;
20use crate::common::write_header;
21use crate::common::write_shortcut;
22use crate::validity::CuSceneValidity;
23use crate::validity::joint_distance;
24use bincode::Decode;
25use bincode::Encode;
26use cu_rng::prelude::*;
27use cu29::cutask_anytime::Quality;
28use cu29::prelude::*;
29use repose_collision::CuRobotCollisionModel;
30use repose_kinematics::CuJointMetric;
31use repose_kinematics::CuJointPosition;
32use repose_kinematics::CuJointResolution;
33use repose_kinematics::CuKinematicChain;
34use serde::Deserialize;
35use serde::Serialize;
36
37pub const PRM_MAX_NODES: usize = 1024;
39const PRM_MAX_PATH: usize = 256;
41pub const PRM_MAX_NEIGHBORS: usize = 10;
43
44#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
46#[serde(default, deny_unknown_fields)]
47pub struct CuJointPrmParams {
48 pub connection_radius: f64,
50 pub neighbors: u32,
52 pub max_nodes: u32,
54 pub collision_check_step: CuJointResolution,
56 pub metric: CuJointMetric,
58}
59
60impl Default for CuJointPrmParams {
61 fn default() -> Self {
62 Self {
63 connection_radius: 2.0,
64 neighbors: 8,
65 max_nodes: PRM_MAX_NODES as u32,
66 collision_check_step: CuJointResolution::default(),
67 metric: CuJointMetric::default(),
68 }
69 }
70}
71
72impl CuJointPrmParams {
73 pub fn validate(&self) -> CuResult<()> {
75 self.metric.validate()?;
76 self.collision_check_step.metric().validate()?;
77 let valid = {
78 self.connection_radius.is_finite()
79 && self.connection_radius > 0.0
80 && (1..=PRM_MAX_NEIGHBORS as u32).contains(&self.neighbors)
81 && (3..=PRM_MAX_NODES as u32).contains(&self.max_nodes)
82 };
83 if valid {
84 Ok(())
85 } else {
86 Err(CuError::from(
87 "repose-motion-planning: invalid PRM parameters",
88 ))
89 }
90 }
91}
92
93pub struct CuJointPrm<const N: usize, const M: usize = N> {
98 validity: CuSceneValidity<N, M>,
99 params: CuJointPrmParams,
100 nodes: [[CuJointPosition; N]; PRM_MAX_NODES],
101 edges: [[u16; PRM_MAX_NEIGHBORS]; PRM_MAX_NODES],
102 degree: [u8; PRM_MAX_NODES],
103 len: usize,
104 rng: CuRng,
105 straight_cost: f64,
106 samples: u32,
107 status: CuPlanStatus,
108 best: CuJointPlanPath<N>,
109}
110
111const START: usize = 0;
112const GOAL: usize = 1;
113
114pub type CuJointPrmTask<const N: usize, const M: usize = N> =
116 CuJointPlannerTask<CuJointPrm<N, M>, N, M>;
117
118impl<const N: usize, const M: usize> CuJointPrm<N, M> {
119 pub const fn params(&self) -> &CuJointPrmParams {
121 &self.params
122 }
123
124 fn build(
125 chain: CuKinematicChain<N>,
126 model: CuRobotCollisionModel<N, M>,
127 params: CuJointPrmParams,
128 ) -> Self {
129 Self {
130 validity: CuSceneValidity::new(chain, model, params.collision_check_step),
131 params,
132 nodes: [[CuJointPosition::default(); N]; PRM_MAX_NODES],
133 edges: [[0; PRM_MAX_NEIGHBORS]; PRM_MAX_NODES],
134 degree: [0; PRM_MAX_NODES],
135 len: 0,
136 rng: CuRng::from_seed(0),
137 straight_cost: 0.0,
138 samples: 0,
139 status: CuPlanStatus::InvalidRequest,
140 best: CuJointPlanPath::default(),
141 }
142 }
143
144 pub const fn roadmap_size(&self) -> usize {
146 self.len
147 }
148
149 fn link(&mut self, a: usize, b: usize) {
150 let cap = PRM_MAX_NEIGHBORS.min(self.params.neighbors as usize);
151 if (self.degree[a] as usize) < cap && (self.degree[b] as usize) < cap {
152 self.edges[a][self.degree[a] as usize] = b as u16;
153 self.degree[a] += 1;
154 self.edges[b][self.degree[b] as usize] = a as u16;
155 self.degree[b] += 1;
156 }
157 }
158
159 fn connect(&mut self, idx: usize) -> bool {
162 let k = (self.params.neighbors as usize).min(PRM_MAX_NEIGHBORS);
163 let mut nearest = [(usize::MAX, f64::INFINITY); PRM_MAX_NEIGHBORS];
164 for other in 0..self.len {
165 if other == idx {
166 continue;
167 }
168 let d = joint_distance(&self.nodes[idx], &self.nodes[other], self.params.metric);
169 if d > self.params.connection_radius || d >= nearest[k - 1].1 {
170 continue;
171 }
172 let mut slot = k - 1;
173 while slot > 0 && nearest[slot - 1].1 > d {
174 nearest[slot] = nearest[slot - 1];
175 slot -= 1;
176 }
177 nearest[slot] = (other, d);
178 }
179 let mut added = false;
180 for &(other, _) in nearest[..k].iter().take_while(|n| n.0 != usize::MAX) {
181 if self
182 .validity
183 .is_segment_free(&self.nodes[idx], &self.nodes[other])
184 {
185 let before = self.degree[idx];
186 self.link(idx, other);
187 added |= self.degree[idx] != before;
188 }
189 }
190 added
191 }
192
193 fn search(&mut self) {
195 let mut dist = [f64::INFINITY; PRM_MAX_NODES];
196 let mut prev = [u16::MAX; PRM_MAX_NODES];
197 let mut done = [false; PRM_MAX_NODES];
198 dist[START] = 0.0;
199 loop {
200 let mut current = usize::MAX;
201 for i in 0..self.len {
202 if !done[i]
203 && dist[i].is_finite()
204 && (current == usize::MAX || dist[i] < dist[current])
205 {
206 current = i;
207 }
208 }
209 if current == usize::MAX || current == GOAL {
210 break;
211 }
212 done[current] = true;
213 for e in 0..self.degree[current] as usize {
214 let next = self.edges[current][e] as usize;
215 let d = dist[current]
216 + joint_distance(&self.nodes[current], &self.nodes[next], self.params.metric);
217 if d < dist[next] {
218 dist[next] = d;
219 prev[next] = current as u16;
220 }
221 }
222 }
223 if !dist[GOAL].is_finite() || dist[GOAL] >= self.best.cost {
224 return;
225 }
226 let mut raw = [[CuJointPosition::default(); N]; PRM_MAX_PATH];
227 let mut len = 0;
228 let mut at = GOAL;
229 loop {
230 if len >= PRM_MAX_PATH {
231 if !self.best.cost.is_finite() {
232 self.status = CuPlanStatus::CapacityExceeded;
233 }
234 return;
235 }
236 raw[len] = self.nodes[at];
237 len += 1;
238 if at == START {
239 break;
240 }
241 at = prev[at] as usize;
242 }
243 raw[..len].reverse();
244 let mut candidate = CuJointPlanPath::default();
245 if write_shortcut(
246 &self.validity,
247 &raw[..len],
248 self.params.metric,
249 &mut candidate,
250 ) {
251 if candidate.cost < self.best.cost {
252 self.best = candidate;
253 }
254 } else if !self.best.cost.is_finite() {
255 self.status = CuPlanStatus::CapacityExceeded;
256 }
257 }
258
259 fn refresh_status(&mut self) {
260 if self.status.is_final() {
261 return;
262 }
263 let full = self.len >= self.params.max_nodes as usize;
264 let solved = self.best.cost.is_finite();
265 let optimal = solved && self.best.cost <= self.straight_cost + 1e-9;
266 self.status = match (solved, full || optimal) {
267 (true, true) => CuPlanStatus::Converged,
268 (true, false) => CuPlanStatus::Improving,
269 (false, true) => CuPlanStatus::Exhausted,
270 (false, false) => CuPlanStatus::Searching,
271 };
272 }
273}
274
275impl<const N: usize, const M: usize> CuJointPlanner<N, M> for CuJointPrm<N, M> {
276 const KIND: CuPlannerKind = CuPlannerKind::Sampling;
277
278 type Params = CuJointPrmParams;
279
280 fn new(
281 chain: CuKinematicChain<N>,
282 collision_model: CuRobotCollisionModel<N, M>,
283 params: CuJointPrmParams,
284 ) -> CuResult<Self> {
285 params.validate()?;
286 Ok(Self::build(chain, collision_model, params))
287 }
288
289 fn begin(&mut self, request: &CuJointPlanRequest<N>, seed: u64) -> CuResult<CuPlanStatus> {
290 self.rng.reseed(seed);
291 self.samples = 0;
292 self.len = 0;
293 self.degree = [0; PRM_MAX_NODES];
294 self.best = CuJointPlanPath::default();
295 self.straight_cost = joint_distance(&request.start, &request.goal, self.params.metric);
296 self.status = check_endpoints(&mut self.validity, request, self.params.metric);
297 if self.status != CuPlanStatus::Searching {
298 return Ok(self.status);
299 }
300 self.nodes[START] = request.start;
301 self.nodes[GOAL] = request.goal;
302 self.len = 2;
303 if self.validity.is_segment_free(&request.start, &request.goal) {
304 self.link(START, GOAL);
305 self.search();
306 }
307 self.refresh_status();
308 Ok(self.status)
309 }
310
311 fn step(&mut self, budget: u32) -> CuResult<CuPlanStatus> {
312 let mut changed = false;
313 for _ in 0..budget {
314 if self.status.is_final() || self.len >= self.params.max_nodes as usize {
315 break;
316 }
317 self.samples += 1;
318 let q = sample_uniform(self.validity.chain(), &mut self.rng);
319 if !self.validity.is_configuration_free(&q) {
320 continue;
321 }
322 let idx = self.len;
323 self.nodes[idx] = q;
324 self.degree[idx] = 0;
325 self.len += 1;
326 changed |= self.connect(idx);
327 }
328 if changed {
329 self.search();
330 }
331 self.refresh_status();
332 Ok(self.status)
333 }
334
335 fn write_best(&self, out: &mut CuJointPlanPath<N>) -> CuPlanStatus {
336 write_header(out, &self.validity, self.status, self.samples);
337 if self.status.has_path() {
338 out.waypoints = self.best.waypoints.clone();
339 out.len = self.best.len;
340 out.cost = self.best.cost;
341 }
342 self.status
343 }
344
345 fn quality(&self) -> Quality {
346 length_quality(
347 self.straight_cost,
348 if self.status.has_path() {
349 self.best.cost
350 } else {
351 f64::INFINITY
352 },
353 )
354 }
355}
356
357crate::impl_planner_type_path!(CuJointPrm, "repose_motion_planning::CuJointPrm");