1use crate::CuJointPlanPath;
14use crate::CuJointPlanRequest;
15use crate::CuJointPlanner;
16use crate::CuJointPlannerTask;
17use crate::CuPlanStatus;
18use crate::CuPlannerKind;
19use crate::common::check_endpoints;
20use crate::common::length_quality;
21use crate::common::write_header;
22use crate::common::write_shortcut;
23use crate::validity::CuSceneValidity;
24use crate::validity::joint_distance;
25use bincode::Decode;
26use bincode::Encode;
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 ASTAR_MAX_NODES: usize = 2048;
39const ASTAR_MAX_PATH: usize = 512;
41const HASH_SLOTS: usize = 2 * ASTAR_MAX_NODES;
42const EMPTY: u16 = u16::MAX;
43
44#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
46#[serde(default, deny_unknown_fields)]
47pub struct CuJointAStarParams {
48 pub resolution: f64,
50 pub max_nodes: u32,
52 pub collision_check_step: CuJointResolution,
54 pub metric: CuJointMetric,
56}
57
58impl Default for CuJointAStarParams {
59 fn default() -> Self {
60 Self {
61 resolution: 0.25,
62 max_nodes: ASTAR_MAX_NODES as u32,
63 collision_check_step: CuJointResolution::default(),
64 metric: CuJointMetric::default(),
65 }
66 }
67}
68
69impl CuJointAStarParams {
70 pub fn validate(&self) -> CuResult<()> {
72 self.metric.validate()?;
73 self.collision_check_step.metric().validate()?;
74 let valid = {
75 self.resolution.is_finite()
76 && self.resolution > 0.0
77 && (2..=ASTAR_MAX_NODES as u32).contains(&self.max_nodes)
78 };
79 if valid {
80 Ok(())
81 } else {
82 Err(CuError::from(
83 "repose-motion-planning: invalid A* parameters",
84 ))
85 }
86 }
87}
88
89#[derive(Clone)]
94pub struct CuJointAStar<const N: usize, const M: usize = N> {
95 validity: CuSceneValidity<N, M>,
96 params: CuJointAStarParams,
97 start: [CuJointPosition; N],
98 goal: [CuJointPosition; N],
99 cells: [[i16; N]; ASTAR_MAX_NODES],
100 cost: [f64; ASTAR_MAX_NODES],
101 estimate: [f64; ASTAR_MAX_NODES],
102 parent: [u16; ASTAR_MAX_NODES],
103 closed: [bool; ASTAR_MAX_NODES],
104 table: [u16; HASH_SLOTS],
105 len: usize,
106 open: usize,
107 goal_reach: f64,
108 straight_cost: f64,
109 expansions: u32,
110 status: CuPlanStatus,
111 best: CuJointPlanPath<N>,
112}
113
114pub type CuJointAStarTask<const N: usize, const M: usize = N> =
116 CuJointPlannerTask<CuJointAStar<N, M>, N, M>;
117
118impl<const N: usize, const M: usize> CuJointAStar<N, M> {
119 pub const fn params(&self) -> &CuJointAStarParams {
121 &self.params
122 }
123
124 fn build(
125 chain: CuKinematicChain<N>,
126 model: CuRobotCollisionModel<N, M>,
127 params: CuJointAStarParams,
128 ) -> Self {
129 Self {
130 validity: CuSceneValidity::new(chain, model, params.collision_check_step),
131 params,
132 start: [CuJointPosition::default(); N],
133 goal: [CuJointPosition::default(); N],
134 cells: [[0; N]; ASTAR_MAX_NODES],
135 cost: [0.0; ASTAR_MAX_NODES],
136 estimate: [0.0; ASTAR_MAX_NODES],
137 parent: [EMPTY; ASTAR_MAX_NODES],
138 closed: [false; ASTAR_MAX_NODES],
139 table: [EMPTY; HASH_SLOTS],
140 len: 0,
141 open: 0,
142 goal_reach: 0.0,
143 straight_cost: 0.0,
144 expansions: 0,
145 status: CuPlanStatus::InvalidRequest,
146 best: CuJointPlanPath::default(),
147 }
148 }
149
150 pub const fn lattice_size(&self) -> usize {
152 self.len
153 }
154
155 fn configuration(&self, cell: &[i16; N]) -> [CuJointPosition; N] {
156 let metric = self.params.metric;
157 core::array::from_fn(|j| {
158 let q = self.start[j];
159 let offset = cell[j] as f64 * self.params.resolution * metric.scale(q);
160 q.with_si_value(q.si_value() + offset)
161 })
162 }
163
164 fn hash(cell: &[i16; N]) -> usize {
165 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
166 for &c in cell {
167 h ^= c as u16 as u64;
168 h = h.wrapping_mul(0x0100_0000_01b3);
169 }
170 (h as usize) % HASH_SLOTS
171 }
172
173 fn find(&self, cell: &[i16; N]) -> Option<usize> {
174 let mut slot = Self::hash(cell);
175 loop {
176 match self.table[slot] {
177 EMPTY => return None,
178 idx if self.cells[idx as usize] == *cell => return Some(idx as usize),
179 _ => slot = (slot + 1) % HASH_SLOTS,
180 }
181 }
182 }
183
184 fn insert(&mut self, cell: [i16; N], cost: f64, parent: u16) -> usize {
185 let idx = self.len;
186 self.cells[idx] = cell;
187 self.cost[idx] = cost;
188 self.estimate[idx] =
189 cost + joint_distance(&self.configuration(&cell), &self.goal, self.params.metric);
190 self.parent[idx] = parent;
191 self.closed[idx] = false;
192 let mut slot = Self::hash(&cell);
193 while self.table[slot] != EMPTY {
194 slot = (slot + 1) % HASH_SLOTS;
195 }
196 self.table[slot] = idx as u16;
197 self.len += 1;
198 self.open += 1;
199 idx
200 }
201
202 fn finish(&mut self, last: usize) {
203 let mut raw = [[CuJointPosition::default(); N]; ASTAR_MAX_PATH];
204 let mut len = 0;
205 raw[len] = self.goal;
206 len += 1;
207 let mut at = last;
208 loop {
209 if len >= ASTAR_MAX_PATH {
210 self.status = CuPlanStatus::CapacityExceeded;
211 return;
212 }
213 raw[len] = self.configuration(&self.cells[at]);
214 len += 1;
215 if self.parent[at] == EMPTY {
216 break;
217 }
218 at = self.parent[at] as usize;
219 }
220 raw[..len].reverse();
221 self.status = if write_shortcut(
222 &self.validity,
223 &raw[..len],
224 self.params.metric,
225 &mut self.best,
226 ) {
227 CuPlanStatus::Converged
228 } else {
229 CuPlanStatus::CapacityExceeded
230 };
231 }
232
233 fn expand(&mut self) {
234 let mut current = usize::MAX;
235 for i in 0..self.len {
236 if !self.closed[i]
237 && (current == usize::MAX || self.estimate[i] < self.estimate[current])
238 {
239 current = i;
240 }
241 }
242 if current == usize::MAX {
243 self.status = CuPlanStatus::Exhausted;
244 return;
245 }
246 self.closed[current] = true;
247 self.open -= 1;
248 self.expansions += 1;
249 let cell = self.cells[current];
250 let q = self.configuration(&cell);
251 if joint_distance(&q, &self.goal, self.params.metric) <= self.goal_reach
252 && self.validity.is_segment_free(&q, &self.goal)
253 {
254 self.finish(current);
255 return;
256 }
257 for j in 0..N {
258 if matches!(self.start[j], CuJointPosition::Fixed) {
259 continue;
260 }
261 for direction in [-1i16, 1] {
262 let mut next = cell;
263 next[j] = match next[j].checked_add(direction) {
264 Some(v) => v,
265 None => continue,
266 };
267 let cost = self.cost[current] + self.params.resolution;
268 match self.find(&next) {
269 Some(idx) if self.closed[idx] || cost >= self.cost[idx] => {}
270 Some(idx) => {
271 let q_next = self.configuration(&next);
272 if self.validity.is_segment_free(&q, &q_next) {
273 self.estimate[idx] += cost - self.cost[idx];
274 self.cost[idx] = cost;
275 self.parent[idx] = current as u16;
276 }
277 }
278 None => {
279 if self.len >= self.params.max_nodes as usize {
280 continue;
281 }
282 let q_next = self.configuration(&next);
283 if self.validity.is_segment_free(&q, &q_next) {
284 self.insert(next, cost, current as u16);
285 }
286 }
287 }
288 }
289 }
290 if self.open == 0 {
291 self.status = CuPlanStatus::Exhausted;
292 }
293 }
294}
295
296impl<const N: usize, const M: usize> CuJointPlanner<N, M> for CuJointAStar<N, M> {
297 const KIND: CuPlannerKind = CuPlannerKind::Search;
298
299 type Params = CuJointAStarParams;
300
301 fn new(
302 chain: CuKinematicChain<N>,
303 collision_model: CuRobotCollisionModel<N, M>,
304 params: CuJointAStarParams,
305 ) -> CuResult<Self> {
306 params.validate()?;
307 Ok(Self::build(chain, collision_model, params))
308 }
309
310 fn begin(&mut self, request: &CuJointPlanRequest<N>, _seed: u64) -> CuResult<CuPlanStatus> {
311 self.start = request.start;
312 self.goal = request.goal;
313 self.expansions = 0;
314 self.len = 0;
315 self.open = 0;
316 self.table = [EMPTY; HASH_SLOTS];
317 self.best = CuJointPlanPath::default();
318 self.straight_cost = joint_distance(&request.start, &request.goal, self.params.metric);
319 let active = self
320 .start
321 .iter()
322 .filter(|q| !matches!(q, CuJointPosition::Fixed))
323 .count()
324 .max(1);
325 self.goal_reach = self.params.resolution * libm::sqrt(active as f64);
328 self.status = check_endpoints(&mut self.validity, request, self.params.metric);
329 if self.status == CuPlanStatus::Searching {
330 self.insert([0; N], 0.0, EMPTY);
331 }
332 Ok(self.status)
333 }
334
335 fn step(&mut self, budget: u32) -> CuResult<CuPlanStatus> {
336 for _ in 0..budget {
337 if self.status.is_final() {
338 break;
339 }
340 self.expand();
341 }
342 Ok(self.status)
343 }
344
345 fn write_best(&self, out: &mut CuJointPlanPath<N>) -> CuPlanStatus {
346 write_header(out, &self.validity, self.status, self.expansions);
347 if self.status.has_path() {
348 out.waypoints = self.best.waypoints.clone();
349 out.len = self.best.len;
350 out.cost = self.best.cost;
351 }
352 self.status
353 }
354
355 fn quality(&self) -> Quality {
356 length_quality(
357 self.straight_cost,
358 if self.status.has_path() {
359 self.best.cost
360 } else {
361 f64::INFINITY
362 },
363 )
364 }
365}
366
367crate::impl_planner_type_path!(CuJointAStar, "repose_motion_planning::CuJointAStar");