1use crate::CuJointPlanPath;
14use crate::CuJointPlanRequest;
15use crate::CuJointPlanner;
16use crate::CuJointPlannerTask;
17use crate::CuPlanStatus;
18use crate::CuPlannerKind;
19use crate::MAX_WAYPOINTS;
20use crate::common::check_endpoints;
21use crate::common::length_quality;
22use crate::common::path_length;
23use crate::common::write_header;
24use crate::validity::CuSceneValidity;
25use crate::validity::joint_distance;
26use bincode::Decode;
27use bincode::Encode;
28use cu_rng::prelude::*;
29use cu29::cutask_anytime::Quality;
30use cu29::prelude::*;
31use cu29::units::si::f64::Length;
32use cu29::units::si::length::meter;
33use repose_collision::CuRobotCollisionModel;
34use repose_kinematics::CuJointMetric;
35use repose_kinematics::CuJointPosition;
36use repose_kinematics::CuJointPositionsPayload;
37use repose_kinematics::CuJointResolution;
38use repose_kinematics::CuKinematicChain;
39use serde::Deserialize;
40use serde::Serialize;
41
42pub const STOMP_MAX_ROLLOUTS: usize = 16;
44
45#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
47#[serde(default, deny_unknown_fields)]
48pub struct CuJointStompParams {
49 pub waypoints: u32,
51 pub rollouts: u32,
53 pub noise: f64,
56 pub sensitivity: f64,
59 pub clearance: Length,
61 pub obstacle_weight: f64,
63 pub smoothness_weight: f64,
65 pub max_iterations: u32,
67 pub collision_check_step: CuJointResolution,
69 pub metric: CuJointMetric,
71}
72
73impl Default for CuJointStompParams {
74 fn default() -> Self {
75 Self {
76 waypoints: 12,
77 rollouts: 8,
78 noise: 0.15,
79 sensitivity: 10.0,
80 clearance: Length::new::<meter>(0.05),
81 obstacle_weight: 40.0,
82 smoothness_weight: 1.0,
83 max_iterations: 200,
84 collision_check_step: CuJointResolution::default(),
85 metric: CuJointMetric::default(),
86 }
87 }
88}
89
90impl CuJointStompParams {
91 pub fn validate(&self) -> CuResult<()> {
93 self.metric.validate()?;
94 self.collision_check_step.metric().validate()?;
95 let valid = {
96 let positive = |v: f64| v.is_finite() && v > 0.0;
97 (3..=MAX_WAYPOINTS as u32).contains(&self.waypoints)
98 && (2..=STOMP_MAX_ROLLOUTS as u32).contains(&self.rollouts)
99 && positive(self.noise)
100 && positive(self.sensitivity)
101 && positive(self.clearance.get::<meter>())
102 && self.obstacle_weight.is_finite()
103 && self.obstacle_weight >= 0.0
104 && self.smoothness_weight.is_finite()
105 && self.smoothness_weight >= 0.0
106 && self.max_iterations > 0
107 };
108 if valid {
109 Ok(())
110 } else {
111 Err(CuError::from(
112 "repose-motion-planning: invalid STOMP parameters",
113 ))
114 }
115 }
116}
117
118pub struct CuJointStomp<const N: usize, const M: usize = N> {
123 validity: CuSceneValidity<N, M>,
124 params: CuJointStompParams,
125 path: [[f64; N]; MAX_WAYPOINTS],
126 kinds: [CuJointPosition; N],
127 best: [[CuJointPosition; N]; MAX_WAYPOINTS],
128 count: usize,
129 best_cost: f64,
130 straight_cost: f64,
131 rng: CuRng,
132 iterations: u32,
133 status: CuPlanStatus,
134}
135
136fn normal(rng: &mut CuRng) -> f64 {
138 let sum: f64 = (0..4).map(|_| rng.random::<f64>()).sum();
139 (sum - 2.0) * libm::sqrt(3.0)
140}
141
142fn smooth<const N: usize>(values: &mut [[f64; N]], count: usize) {
144 let copy: [[f64; N]; MAX_WAYPOINTS] =
145 core::array::from_fn(|i| if i < count { values[i] } else { [0.0; N] });
146 for i in 1..count - 1 {
147 for j in 0..N {
148 values[i][j] = 0.25 * copy[i - 1][j] + 0.5 * copy[i][j] + 0.25 * copy[i + 1][j];
149 }
150 }
151 values[0] = [0.0; N];
152 values[count - 1] = [0.0; N];
153}
154
155pub type CuJointStompTask<const N: usize, const M: usize = N> =
157 CuJointPlannerTask<CuJointStomp<N, M>, N, M>;
158
159impl<const N: usize, const M: usize> CuJointStomp<N, M> {
160 pub const fn params(&self) -> &CuJointStompParams {
162 &self.params
163 }
164
165 fn build(
166 chain: CuKinematicChain<N>,
167 model: CuRobotCollisionModel<N, M>,
168 params: CuJointStompParams,
169 ) -> Self {
170 Self {
171 validity: CuSceneValidity::new(chain, model, params.collision_check_step),
172 params,
173 path: [[0.0; N]; MAX_WAYPOINTS],
174 kinds: [CuJointPosition::default(); N],
175 best: [[CuJointPosition::default(); N]; MAX_WAYPOINTS],
176 count: 0,
177 best_cost: f64::INFINITY,
178 straight_cost: 0.0,
179 rng: CuRng::from_seed(0),
180 iterations: 0,
181 status: CuPlanStatus::InvalidRequest,
182 }
183 }
184
185 pub const fn iterations(&self) -> u32 {
187 self.iterations
188 }
189
190 fn scale(&self, j: usize) -> f64 {
191 self.params.metric.scale(self.kinds[j])
192 }
193
194 fn configuration(&self, x: &[f64; N]) -> [CuJointPosition; N] {
195 core::array::from_fn(|j| self.kinds[j].with_si_value(x[j] * self.scale(j)))
196 }
197
198 fn clamp(&self, x: &mut [f64; N]) {
199 for (j, value) in x.iter_mut().enumerate() {
200 if matches!(self.kinds[j], CuJointPosition::Fixed) {
201 *value = 0.0;
202 continue;
203 }
204 let limits = self.validity.chain().joints()[j].limits;
205 let lo = limits.lower().si_value() / self.scale(j);
206 let hi = limits.upper().si_value() / self.scale(j);
207 *value = value.clamp(lo, hi);
208 }
209 }
210
211 fn obstacle_cost(&self, x: &[f64; N]) -> f64 {
212 let eps = self.params.clearance.get::<meter>();
213 match self.validity.clearance(&self.configuration(x)) {
214 Some(c) if c >= eps => 0.0,
215 Some(c) => self.params.obstacle_weight * (eps - c) * (eps - c) / eps,
216 None => self.params.obstacle_weight * eps,
217 }
218 }
219
220 fn record_if_better(&mut self) {
221 let mut q = [[CuJointPosition::default(); N]; MAX_WAYPOINTS];
222 for (slot, x) in q.iter_mut().zip(&self.path[..self.count]) {
223 *slot = self.configuration(x);
224 }
225 let free = (1..self.count - 1).all(|i| self.validity.is_configuration_free(&q[i]))
226 && (0..self.count - 1).all(|i| self.validity.is_segment_free(&q[i], &q[i + 1]));
227 if !free {
228 return;
229 }
230 let cost = path_length(&q[..self.count], self.params.metric);
231 if cost < self.best_cost {
232 self.best_cost = cost;
233 self.best = q;
234 }
235 }
236
237 #[allow(clippy::needless_range_loop)]
240 fn iterate(&mut self) {
241 let k = self.params.rollouts as usize;
242 let count = self.count;
243 let mut noise = [[[0.0_f64; N]; MAX_WAYPOINTS]; STOMP_MAX_ROLLOUTS];
244 let mut costs = [[0.0_f64; MAX_WAYPOINTS]; STOMP_MAX_ROLLOUTS];
245 for r in 0..k {
246 for i in 1..count - 1 {
247 for j in 0..N {
248 noise[r][i][j] = self.params.noise * normal(&mut self.rng);
249 }
250 }
251 smooth(&mut noise[r], count);
252 for i in 1..count - 1 {
253 let mut x = self.path[i];
254 for j in 0..N {
255 x[j] += noise[r][i][j];
256 }
257 self.clamp(&mut x);
258 let mut acceleration = 0.0;
259 for j in 0..N {
260 let a = (self.path[i - 1][j] + noise[r][i - 1][j]) - 2.0 * x[j]
261 + (self.path[i + 1][j] + noise[r][i + 1][j]);
262 acceleration += a * a;
263 }
264 costs[r][i] = self.obstacle_cost(&x) + self.params.smoothness_weight * acceleration;
265 }
266 }
267 let mut update = [[0.0_f64; N]; MAX_WAYPOINTS];
268 for i in 1..count - 1 {
269 let (lo, hi) = (0..k).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), r| {
270 (lo.min(costs[r][i]), hi.max(costs[r][i]))
271 });
272 let span = (hi - lo).max(1e-12);
273 let mut weights = [0.0_f64; STOMP_MAX_ROLLOUTS];
274 let mut total = 0.0;
275 for r in 0..k {
276 weights[r] = libm::exp(-self.params.sensitivity * (costs[r][i] - lo) / span);
277 total += weights[r];
278 }
279 for r in 0..k {
280 for j in 0..N {
281 update[i][j] += weights[r] / total * noise[r][i][j];
282 }
283 }
284 }
285 smooth(&mut update, count);
286 for i in 1..count - 1 {
287 let mut x = self.path[i];
288 for j in 0..N {
289 x[j] += update[i][j];
290 }
291 self.clamp(&mut x);
292 self.path[i] = x;
293 }
294 }
295}
296
297impl<const N: usize, const M: usize> CuJointPlanner<N, M> for CuJointStomp<N, M> {
298 const KIND: CuPlannerKind = CuPlannerKind::Optimization;
299
300 type Params = CuJointStompParams;
301
302 fn new(
303 chain: CuKinematicChain<N>,
304 collision_model: CuRobotCollisionModel<N, M>,
305 params: CuJointStompParams,
306 ) -> CuResult<Self> {
307 params.validate()?;
308 Ok(Self::build(chain, collision_model, params))
309 }
310
311 fn begin(&mut self, request: &CuJointPlanRequest<N>, seed: u64) -> CuResult<CuPlanStatus> {
312 self.rng.reseed(seed);
313 self.iterations = 0;
314 self.best_cost = f64::INFINITY;
315 self.count = self.params.waypoints as usize;
316 self.kinds = request.start;
317 self.straight_cost = joint_distance(&request.start, &request.goal, self.params.metric);
318 self.status = check_endpoints(&mut self.validity, request, self.params.metric);
319 if self.status != CuPlanStatus::Searching {
320 return Ok(self.status);
321 }
322 let last = (self.count - 1) as f64;
323 for i in 0..self.count {
324 let t = i as f64 / last;
325 for j in 0..N {
326 let a = self.params.metric.normalized(request.start[j]);
327 let b = self.params.metric.normalized(request.goal[j]);
328 self.path[i][j] = a + t * (b - a);
329 }
330 }
331 self.record_if_better();
333 if self.best_cost.is_finite() {
334 self.status = CuPlanStatus::Converged;
335 }
336 Ok(self.status)
337 }
338
339 fn step(&mut self, budget: u32) -> CuResult<CuPlanStatus> {
340 for _ in 0..budget {
341 if self.status.is_final() {
342 break;
343 }
344 self.iterate();
345 self.iterations += 1;
346 self.record_if_better();
347 let done = self.iterations >= self.params.max_iterations;
348 self.status = match (self.best_cost.is_finite(), done) {
349 (true, true) => CuPlanStatus::Converged,
350 (true, false) => CuPlanStatus::Improving,
351 (false, true) => CuPlanStatus::Exhausted,
352 (false, false) => CuPlanStatus::Searching,
353 };
354 }
355 Ok(self.status)
356 }
357
358 fn write_best(&self, out: &mut CuJointPlanPath<N>) -> CuPlanStatus {
359 write_header(out, &self.validity, self.status, self.iterations);
360 if self.status.has_path() {
361 for (slot, q) in out.waypoints.iter_mut().zip(&self.best[..self.count]) {
362 *slot = CuJointPositionsPayload { positions: *q };
363 }
364 out.len = self.count as u32;
365 out.cost = self.best_cost;
366 }
367 self.status
368 }
369
370 fn quality(&self) -> Quality {
371 length_quality(self.straight_cost, self.best_cost)
372 }
373}
374
375crate::impl_planner_type_path!(CuJointStomp, "repose_motion_planning::CuJointStomp");