1use crate::CuJointPlanPath;
21use crate::CuJointPlanRequest;
22use crate::CuJointPlanner;
23use crate::CuJointPlannerTask;
24use crate::CuPlanStatus;
25use crate::CuPlannerKind;
26use crate::MAX_WAYPOINTS;
27use crate::common::check_endpoints;
28use crate::common::write_header;
29use crate::validity::CuSceneValidity;
30use crate::validity::joint_distance;
31use bincode::Decode;
32use bincode::Encode;
33use cu29::cutask_anytime::Quality;
34use cu29::cutask_anytime::quality_from_f32;
35use cu29::prelude::*;
36use cu29::units::si::f64::Length;
37use cu29::units::si::length::meter;
38use repose_collision::CuRobotCollisionModel;
39use repose_kinematics::CuJointMetric;
40use repose_kinematics::CuJointPosition;
41use repose_kinematics::CuJointPositionsPayload;
42use repose_kinematics::CuJointResolution;
43use repose_kinematics::CuKinematicChain;
44use serde::Deserialize;
45use serde::Serialize;
46
47#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
49#[serde(default, deny_unknown_fields)]
50pub struct CuJointPathOptimizerParams {
51 pub waypoints: u32,
53 pub learning_rate: f64,
55 pub clearance: Length,
57 pub obstacle_weight: f64,
59 pub smoothness_weight: f64,
61 pub gradient_step: f64,
64 pub max_iterations: u32,
66 pub collision_check_step: CuJointResolution,
68 pub metric: CuJointMetric,
70}
71
72impl Default for CuJointPathOptimizerParams {
73 fn default() -> Self {
74 Self {
75 waypoints: 12,
76 learning_rate: 0.05,
77 clearance: Length::new::<meter>(0.05),
78 obstacle_weight: 40.0,
79 smoothness_weight: 1.0,
80 gradient_step: 1e-3,
81 max_iterations: 300,
82 collision_check_step: CuJointResolution::default(),
83 metric: CuJointMetric::default(),
84 }
85 }
86}
87
88impl CuJointPathOptimizerParams {
89 pub fn validate(&self) -> CuResult<()> {
91 self.metric.validate()?;
92 self.collision_check_step.metric().validate()?;
93 let valid = {
94 let positive = |v: f64| v.is_finite() && v > 0.0;
95 (3..=MAX_WAYPOINTS as u32).contains(&self.waypoints)
96 && positive(self.learning_rate)
97 && positive(self.clearance.get::<meter>())
98 && self.obstacle_weight.is_finite()
99 && self.obstacle_weight >= 0.0
100 && self.smoothness_weight.is_finite()
101 && self.smoothness_weight >= 0.0
102 && positive(self.gradient_step)
103 && self.max_iterations > 0
104 };
105 if valid {
106 Ok(())
107 } else {
108 Err(CuError::from(
109 "repose-motion-planning: invalid path optimizer parameters",
110 ))
111 }
112 }
113}
114
115#[derive(Clone)]
120pub struct CuJointPathOptimizer<const N: usize, const M: usize = N> {
121 validity: CuSceneValidity<N, M>,
122 params: CuJointPathOptimizerParams,
123 path: [[CuJointPosition; N]; MAX_WAYPOINTS],
124 best: [[CuJointPosition; N]; MAX_WAYPOINTS],
125 count: usize,
126 best_cost: f64,
127 straight_cost: f64,
128 iterations: u32,
129 status: CuPlanStatus,
130}
131
132pub type CuJointPathOptimizerTask<const N: usize, const M: usize = N> =
134 CuJointPlannerTask<CuJointPathOptimizer<N, M>, N, M>;
135
136impl<const N: usize, const M: usize> CuJointPathOptimizer<N, M> {
137 pub const fn params(&self) -> &CuJointPathOptimizerParams {
139 &self.params
140 }
141
142 fn build(
143 chain: CuKinematicChain<N>,
144 model: CuRobotCollisionModel<N, M>,
145 params: CuJointPathOptimizerParams,
146 ) -> Self {
147 Self {
148 validity: CuSceneValidity::new(chain, model, params.collision_check_step),
149 params,
150 path: [[CuJointPosition::default(); N]; MAX_WAYPOINTS],
151 best: [[CuJointPosition::default(); N]; MAX_WAYPOINTS],
152 count: 0,
153 best_cost: f64::INFINITY,
154 straight_cost: 0.0,
155 iterations: 0,
156 status: CuPlanStatus::InvalidRequest,
157 }
158 }
159
160 pub const fn iterations(&self) -> u32 {
162 self.iterations
163 }
164
165 fn penalty(&self, q: &[CuJointPosition; N]) -> f64 {
166 self.penalty_of(self.validity.clearance(q))
167 }
168
169 fn penalty_of(&self, clearance: Option<f64>) -> f64 {
170 let eps = self.params.clearance.get::<meter>();
171 match clearance {
172 Some(c) if c >= eps => 0.0,
173 Some(c) => self.params.obstacle_weight * (eps - c) * (eps - c) / eps,
174 None => self.params.obstacle_weight * eps,
176 }
177 }
178
179 fn path_cost(&self, path: &[[CuJointPosition; N]]) -> f64 {
180 path.windows(2)
181 .map(|w| joint_distance(&w[0], &w[1], self.params.metric))
182 .sum()
183 }
184
185 fn path_is_free(&self) -> bool {
186 (1..self.count - 1).all(|i| self.validity.is_configuration_free(&self.path[i]))
187 && (0..self.count - 1).all(|i| {
188 self.validity
189 .is_segment_free(&self.path[i], &self.path[i + 1])
190 })
191 }
192
193 fn record_if_better(&mut self) {
194 if !self.path_is_free() {
195 return;
196 }
197 let cost = self.path_cost(&self.path[..self.count]);
198 if cost < self.best_cost {
199 self.best_cost = cost;
200 self.best[..self.count].copy_from_slice(&self.path[..self.count]);
201 }
202 }
203
204 fn descend(&mut self) -> f64 {
207 let metric = self.params.metric;
208 let h = self.params.gradient_step;
209 let eps = self.params.clearance.get::<meter>();
210 let mut largest = 0.0_f64;
211 let mut next = self.path;
212 for (i, slot) in next.iter_mut().enumerate().take(self.count - 1).skip(1) {
213 let q = self.path[i];
214 let clearance = self.validity.clearance(&q);
215 let base = self.penalty_of(clearance);
216 let flat = clearance.is_some_and(|c| c > 2.0 * eps);
219 for j in 0..N {
220 if matches!(q[j], CuJointPosition::Fixed) {
221 continue;
222 }
223 let scale = metric.scale(q[j]);
224 let x = q[j].si_value() / scale;
225 let prev = self.path[i - 1][j].si_value() / scale;
226 let after = self.path[i + 1][j].si_value() / scale;
227 let mut gradient = self.params.smoothness_weight * (2.0 * x - prev - after);
228 if !flat {
229 let mut probe = q;
230 probe[j] = q[j].with_si_value((x + h) * scale);
231 gradient += (self.penalty(&probe) - base) / h;
232 }
233 let limits = self.validity.chain().joints()[j].limits;
234 let lo = limits.lower().si_value() / scale;
235 let hi = limits.upper().si_value() / scale;
236 let updated = (x - self.params.learning_rate * gradient).clamp(lo, hi);
237 largest = largest.max(libm::fabs(updated - x));
238 slot[j] = q[j].with_si_value(updated * scale);
239 }
240 }
241 self.path = next;
242 largest
243 }
244}
245
246impl<const N: usize, const M: usize> CuJointPlanner<N, M> for CuJointPathOptimizer<N, M> {
247 const KIND: CuPlannerKind = CuPlannerKind::Optimization;
248
249 type Params = CuJointPathOptimizerParams;
250
251 fn new(
252 chain: CuKinematicChain<N>,
253 collision_model: CuRobotCollisionModel<N, M>,
254 params: CuJointPathOptimizerParams,
255 ) -> CuResult<Self> {
256 params.validate()?;
257 Ok(Self::build(chain, collision_model, params))
258 }
259
260 fn begin(&mut self, request: &CuJointPlanRequest<N>, _seed: u64) -> CuResult<CuPlanStatus> {
261 self.iterations = 0;
262 self.best_cost = f64::INFINITY;
263 self.count = self.params.waypoints as usize;
264 self.straight_cost = joint_distance(&request.start, &request.goal, self.params.metric);
265 self.status = check_endpoints(&mut self.validity, request, self.params.metric);
266 if self.status == CuPlanStatus::Searching {
267 let last = (self.count - 1) as f64;
268 for i in 0..self.count {
269 for j in 0..N {
270 self.path[i][j] = request.start[j]
271 .interpolate(request.goal[j], i as f64 / last)
272 .map_err(|_| {
273 CuError::from("repose-motion-planning: joint kinds disagree")
274 })?;
275 }
276 }
277 self.record_if_better();
280 if self.best_cost.is_finite() {
281 self.status = CuPlanStatus::Converged;
282 }
283 }
284 Ok(self.status)
285 }
286
287 fn step(&mut self, budget: u32) -> CuResult<CuPlanStatus> {
288 for _ in 0..budget {
289 if self.status.is_final() {
290 break;
291 }
292 let moved = self.descend();
293 self.iterations += 1;
294 self.record_if_better();
295 let done = moved <= 1e-7 || self.iterations >= self.params.max_iterations;
296 self.status = match (self.best_cost.is_finite(), done) {
297 (true, true) => CuPlanStatus::Converged,
298 (true, false) => CuPlanStatus::Improving,
299 (false, true) => CuPlanStatus::Exhausted,
300 (false, false) => CuPlanStatus::Searching,
301 };
302 }
303 Ok(self.status)
304 }
305
306 fn write_best(&self, out: &mut CuJointPlanPath<N>) -> CuPlanStatus {
307 write_header(out, &self.validity, self.status, self.iterations);
308 if self.status.has_path() {
309 for (slot, q) in out.waypoints.iter_mut().zip(&self.best[..self.count]) {
310 *slot = CuJointPositionsPayload { positions: *q };
311 }
312 out.len = self.count as u32;
313 out.cost = self.best_cost;
314 }
315 self.status
316 }
317
318 fn quality(&self) -> Quality {
319 if !self.best_cost.is_finite() {
320 return quality_from_f32(0.0);
321 }
322 if self.best_cost <= 1e-9 {
323 return quality_from_f32(1.0);
324 }
325 quality_from_f32((self.straight_cost / self.best_cost).clamp(0.0, 1.0) as f32)
326 }
327}
328
329crate::impl_planner_type_path!(
330 CuJointPathOptimizer,
331 "repose_motion_planning::CuJointPathOptimizer"
332);