1use std::time::Duration;
19
20use clarabel::algebra::CscMatrix;
21use clarabel::solver::{DefaultSettings, DefaultSolver, IPSolver, SolverStatus, SupportedConeT};
22use deke_types::glam::DVec3;
23use deke_types::{
24 ContinuousFKChain, DekeError, DekeResult, Retimer, SRobotPath, SRobotQ, SRobotTraj, Validator,
25};
26
27use crate::constraints::{JointLimits, LinearConstraints};
28use crate::diagnostic::LinearRetimerDiagnostic;
29use crate::error::LinearError;
30
31const LIMIT_MARGIN: f64 = 0.97;
35
36const SMOOTH_STEP: f64 = 1e-4;
40
41type SolvedProfile<const N: usize> = (Vec<f64>, usize, Vec<SRobotQ<N, f64>>, f64, f64);
44
45#[derive(Clone, Debug)]
47pub struct ConstantSpeedRetimer<'a, const N: usize, FK> {
48 fk: &'a FK,
49}
50
51impl<'a, const N: usize, FK> ConstantSpeedRetimer<'a, N, FK>
52where
53 FK: ContinuousFKChain<N, f64>,
54{
55 pub fn new(fk: &'a FK) -> Self {
56 Self { fk }
57 }
58
59 pub(crate) fn retime_path(
60 &self,
61 c: &LinearConstraints<N>,
62 path: &SRobotPath<N, f64>,
63 run_idx: usize,
64 ) -> Result<(SRobotTraj<N, f64>, LinearRetimerDiagnostic), LinearError> {
65 let raw: Vec<SRobotQ<N, f64>> = path.iter().copied().collect();
66 let dt = c.output_dt.as_secs_f64().max(1e-6);
67
68 let (knots, s) = smooth_path(self.fk, &raw, c.corner_smoothing)?;
73 let nb = knots.len();
74 let total = if nb == 0 { 0.0 } else { s[nb - 1] };
75 if nb < 2 || total < 1e-9 {
76 let traj = SRobotTraj::new(c.output_dt, path.clone());
77 return Ok((traj, degenerate_diag(nb, dt, total, c.tcp.speed)));
78 }
79
80 let secant: Vec<[f64; N]> = (0..nb - 1)
85 .map(|b| {
86 let ds = (s[b + 1] - s[b]).max(1e-12);
87 std::array::from_fn(|j| (knots[b + 1].0[j] - knots[b].0[j]) / ds)
88 })
89 .collect();
90
91 let v_cmd = c.tcp.speed.max(1e-9);
92 let bin_of = |sx: f64| -> usize {
93 let sx = sx.clamp(0.0, total);
94 let mut b = 0;
95 while b + 1 < nb - 1 && s[b + 1] <= sx {
96 b += 1;
97 }
98 b
99 };
100 let proj = |b: usize, lim: &SRobotQ<N, f64>| -> f64 {
102 (0..N)
103 .map(|j| lim.0[j] / secant[b][j].abs().max(1e-12))
104 .fold(f64::INFINITY, f64::min)
105 };
106
107 if c.forbid_interior_dips {
111 let mut worst: Option<(usize, f64)> = None;
112 #[allow(clippy::needless_range_loop)]
113 for b in 1..nb - 1 {
114 let g = proj(b, &c.joint.v_max);
115 if g < v_cmd * (1.0 - 1e-3) && worst.is_none_or(|(_, gw)| g < gw) {
116 worst = Some((b, g));
117 }
118 }
119 if let Some((b, g)) = worst {
120 return Err(LinearError::SpeedDipRequired {
121 run: run_idx,
122 s: s[b],
123 feasible_speed: g,
124 commanded: v_cmd,
125 });
126 }
127 }
128
129 let mg = LIMIT_MARGIN;
133 let a_eff = (0..nb - 1)
134 .map(|b| proj(b, &c.joint.a_max))
135 .fold(f64::INFINITY, f64::min)
136 .min(c.tcp.accel.unwrap_or(f64::INFINITY))
137 .max(1e-6);
138 let j_eff = (0..nb - 1)
139 .map(|b| proj(b, &c.joint.j_max))
140 .fold(f64::INFINITY, f64::min)
141 .min(c.tcp.jerk.unwrap_or(f64::INFINITY))
142 .max(1e-6);
143 let ramp_t = v_cmd / a_eff + a_eff / j_eff;
144 let mut kk =
145 ((total / (v_cmd * dt)).ceil() as usize + (4.0 * ramp_t / dt) as usize + 64).max(8);
146
147 let mut a_der = mg;
156 let mut j_der = mg;
157 let mut solved: Option<SolvedProfile<N>> = None;
158 for _tcp_iter in 0..5 {
159 let mut sigma: Option<Vec<f64>> = None;
164 for _grow in 0..6 {
165 let mut sg: Vec<f64> = (0..kk)
166 .map(|k| k as f64 / (kk - 1) as f64 * total)
167 .collect();
168 let mut prev_bins: Vec<usize> = Vec::new();
169 let mut feasible = true;
170 for _pass in 0..4 {
171 let bins: Vec<usize> = sg.iter().map(|&sx| bin_of(sx)).collect();
172 if bins == prev_bins {
173 break;
174 }
175 let cap_v: Vec<f64> = bins
176 .iter()
177 .map(|&b| (mg * proj(b, &c.joint.v_max)).min(v_cmd) * dt)
178 .collect();
179 let cap_a: Vec<f64> = bins
180 .iter()
181 .map(|&b| {
182 (mg * proj(b, &c.joint.a_max))
183 .min(c.tcp.accel.map_or(f64::INFINITY, |t| a_der * t))
184 * dt
185 * dt
186 })
187 .collect();
188 let cap_j: Vec<f64> = bins
189 .iter()
190 .map(|&b| {
191 (mg * proj(b, &c.joint.j_max))
192 .min(c.tcp.jerk.map_or(f64::INFINITY, |t| j_der * t))
193 * dt
194 * dt
195 * dt
196 })
197 .collect();
198 match solve_sigma(kk, total, &cap_v, &cap_a, &cap_j) {
199 Some(next) => sg = next,
200 None => {
201 feasible = false;
202 break;
203 }
204 }
205 prev_bins = bins;
206 }
207 if feasible {
208 sigma = Some(sg);
209 break;
210 }
211 kk = (kk as f64 * 1.6) as usize + 16;
212 }
213 let sigma = sigma.ok_or(LinearError::Stalled {
214 run: run_idx,
215 s: 0.0,
216 })?;
217
218 let recon = |sx: f64| -> SRobotQ<N, f64> {
221 let b = bin_of(sx);
222 SRobotQ(std::array::from_fn(|j| {
223 knots[b].0[j] + secant[b][j] * (sx - s[b])
224 }))
225 };
226 let mut end = kk;
227 while end > 2 && (total - sigma[end - 2]).abs() < 1e-9 {
228 end -= 1;
229 }
230 let samples: Vec<SRobotQ<N, f64>> = sigma[..end].iter().map(|&sx| recon(sx)).collect();
231
232 let (a_over, j_over) = tcp_fd_ratios(self.fk, &samples, c.tcp.accel, c.tcp.jerk, dt)?;
235 let tol = 1.0 + 1e-6;
236 let converged = a_over <= tol && j_over <= tol;
237 solved = Some((sigma, end, samples, a_over, j_over));
238 if converged {
239 break;
240 }
241 if a_over > tol {
244 a_der /= a_over * 1.01;
245 }
246 if j_over > tol {
247 j_der /= j_over * 1.01;
248 }
249 }
250 let (sigma, end, samples, a_over, j_over) = solved.ok_or(LinearError::Stalled {
251 run: run_idx,
252 s: 0.0,
253 })?;
254
255 if let Some((kind, joint, value, limit, idx)) = verify_fd(&samples, &c.joint, dt) {
259 return Err(LinearError::LimitExceeded {
260 run: run_idx,
261 s: sigma.get(idx).copied().unwrap_or(total),
262 joint,
263 kind,
264 value,
265 limit,
266 });
267 }
268 if a_over > 1.0 + 1e-6 {
272 let limit = c
273 .tcp
274 .accel
275 .expect("accel cap set when its ratio is positive");
276 return Err(LinearError::TcpLimitExceeded {
277 run: run_idx,
278 kind: "acceleration",
279 value: a_over * limit,
280 limit,
281 });
282 }
283 if j_over > 1.0 + 1e-6 {
284 let limit = c.tcp.jerk.expect("jerk cap set when its ratio is positive");
285 return Err(LinearError::TcpLimitExceeded {
286 run: run_idx,
287 kind: "jerk",
288 value: j_over * limit,
289 limit,
290 });
291 }
292
293 let out_samples = samples.len();
294 let (_, pk_a, pk_j) = fd_peaks(&samples, dt);
295 let peak_speed = (1..end)
296 .map(|k| (sigma[k] - sigma[k - 1]) / dt)
297 .fold(0.0, f64::max);
298 let path_out = SRobotPath::try_new(samples).map_err(LinearError::from)?;
299 let traj = SRobotTraj::new(c.output_dt, path_out);
300 Ok((
301 traj,
302 LinearRetimerDiagnostic {
303 output_samples: out_samples,
304 duration: Duration::from_secs_f64((out_samples.saturating_sub(1)) as f64 * dt),
305 arc_length: total,
306 commanded_speed: c.tcp.speed,
307 peak_speed,
308 peak_joint_accel: pk_a,
309 peak_joint_jerk: pk_j,
310 },
311 ))
312 }
313}
314
315impl<'a, const N: usize, FK> Retimer<N, f64> for ConstantSpeedRetimer<'a, N, FK>
316where
317 FK: ContinuousFKChain<N, f64>,
318{
319 type Diagnostic = LinearRetimerDiagnostic;
320 type Constraints = LinearConstraints<N>;
321
322 fn retime<V: Validator<N, (), f64>>(
323 &self,
324 constraints: &Self::Constraints,
325 path: &SRobotPath<N, f64>,
326 validator: &V,
327 ctx: &V::Context<'_>,
328 ) -> (DekeResult<SRobotTraj<N, f64>>, Self::Diagnostic) {
329 match self.retime_path(constraints, path, 0) {
330 Ok((traj, diag)) => {
331 let samples: Vec<SRobotQ<N, f64>> = traj.path().iter().copied().collect();
332 if let Err(e) = validator.validate_motion(&samples, ctx) {
333 return (Err(e), diag);
334 }
335 (Ok(traj), diag)
336 }
337 Err(e) => (
338 Err(e.into()),
339 LinearRetimerDiagnostic {
340 output_samples: 0,
341 duration: Duration::ZERO,
342 arc_length: 0.0,
343 commanded_speed: constraints.tcp.speed,
344 peak_speed: 0.0,
345 peak_joint_accel: 0.0,
346 peak_joint_jerk: 0.0,
347 },
348 ),
349 }
350 }
351}
352
353fn degenerate_diag(nb: usize, dt: f64, total: f64, speed: f64) -> LinearRetimerDiagnostic {
354 LinearRetimerDiagnostic {
355 output_samples: nb,
356 duration: Duration::from_secs_f64((nb.saturating_sub(1)) as f64 * dt),
357 arc_length: total,
358 commanded_speed: speed,
359 peak_speed: 0.0,
360 peak_joint_accel: 0.0,
361 peak_joint_jerk: 0.0,
362 }
363}
364
365fn smooth_path<const N: usize, FK: ContinuousFKChain<N, f64>>(
370 fk: &FK,
371 raw: &[SRobotQ<N, f64>],
372 res: Option<f64>,
373) -> Result<(Vec<SRobotQ<N, f64>>, Vec<f64>), LinearError> {
374 let arc = |pts: &[SRobotQ<N, f64>]| -> Result<Vec<f64>, LinearError> {
375 let pos: Vec<DVec3> = pts
376 .iter()
377 .map(|q| fk.fk_end(q).map(|t| t.translation))
378 .collect::<Result<_, DekeError>>()?;
379 let mut s = vec![0.0f64; pts.len()];
380 for i in 1..pts.len() {
381 s[i] = s[i - 1] + pos[i].distance(pos[i - 1]);
382 }
383 Ok(s)
384 };
385 let s_raw = arc(raw)?;
386 let mut sd: Vec<f64> = Vec::with_capacity(raw.len());
387 let mut qd: Vec<SRobotQ<N, f64>> = Vec::with_capacity(raw.len());
388 for i in 0..raw.len() {
389 if qd.is_empty() || s_raw[i] - sd[sd.len() - 1] > 1e-9 {
390 sd.push(s_raw[i]);
391 qd.push(raw[i]);
392 }
393 }
394 let n = qd.len();
395 let step = match res {
396 Some(r) if r > 0.0 && n >= 3 => r.min(SMOOTH_STEP),
397 _ => {
398 let s = arc(&qd)?;
399 return Ok((qd, s));
400 }
401 };
402 let h: Vec<f64> = (0..n - 1).map(|i| (sd[i + 1] - sd[i]).max(1e-12)).collect();
403 let mm = natural_cubic(&h, &qd);
404 let mut out = vec![qd[0]];
405 for i in 0..n - 1 {
406 let k = ((h[i] / step).ceil() as usize).max(1);
407 for ss in 1..=k {
408 let uu = sd[i] + h[i] * (ss as f64) / (k as f64);
409 let a = (sd[i + 1] - uu) / h[i];
410 let b = (uu - sd[i]) / h[i];
411 out.push(SRobotQ(std::array::from_fn(|j| {
412 qd[i].0[j] * a
413 + qd[i + 1].0[j] * b
414 + (mm[i][j] * (a * a * a - a) + mm[i + 1][j] * (b * b * b - b))
415 * (h[i] * h[i] / 6.0)
416 })));
417 }
418 }
419 let s = arc(&out)?;
420 Ok((out, s))
421}
422
423#[allow(clippy::needless_range_loop)]
426fn natural_cubic<const N: usize>(h: &[f64], y: &[SRobotQ<N, f64>]) -> Vec<[f64; N]> {
427 let n = y.len();
428 let mut cp = vec![0.0f64; n];
429 let mut dp = vec![[0.0f64; N]; n];
430 for i in 1..n - 1 {
431 let (a, b, cc) = (h[i - 1], 2.0 * (h[i - 1] + h[i]), h[i]);
432 let denom = b - a * cp[i - 1];
433 cp[i] = cc / denom;
434 for j in 0..N {
435 let rhs =
436 ((y[i + 1].0[j] - y[i].0[j]) / h[i] - (y[i].0[j] - y[i - 1].0[j]) / h[i - 1]) * 6.0;
437 dp[i][j] = (rhs - dp[i - 1][j] * a) / denom;
438 }
439 }
440 let mut mm = vec![[0.0f64; N]; n];
441 for i in (1..n - 1).rev() {
442 for j in 0..N {
443 mm[i][j] = dp[i][j] - mm[i + 1][j] * cp[i];
444 }
445 }
446 mm
447}
448
449#[allow(clippy::needless_range_loop, clippy::field_reassign_with_default)]
453fn solve_sigma(
454 n: usize,
455 total: f64,
456 cap_v: &[f64],
457 cap_a: &[f64],
458 cap_j: &[f64],
459) -> Option<Vec<f64>> {
460 let inv = 1.0 / total;
463 let mut t: Vec<(usize, usize, f64)> = Vec::new();
464 let mut b: Vec<f64> = Vec::new();
465 let mut row = 0usize;
466 let eq = |t: &mut Vec<(usize, usize, f64)>,
467 b: &mut Vec<f64>,
468 row: &mut usize,
469 e: &[(usize, f64)],
470 rhs: f64| {
471 for &(c, v) in e {
472 t.push((*row, c, v));
473 }
474 b.push(rhs);
475 *row += 1;
476 };
477 eq(&mut t, &mut b, &mut row, &[(0, 1.0)], 0.0);
478 eq(&mut t, &mut b, &mut row, &[(n - 1, 1.0)], 1.0);
479 eq(&mut t, &mut b, &mut row, &[(1, 1.0), (0, -1.0)], 0.0);
480 eq(&mut t, &mut b, &mut row, &[(2, 1.0), (1, -1.0)], 0.0);
481 eq(
482 &mut t,
483 &mut b,
484 &mut row,
485 &[(n - 1, 1.0), (n - 2, -1.0)],
486 0.0,
487 );
488 eq(
489 &mut t,
490 &mut b,
491 &mut row,
492 &[(n - 2, 1.0), (n - 3, -1.0)],
493 0.0,
494 );
495 let n_eq = row;
496 for k in 1..n {
497 t.push((row, k - 1, 1.0));
498 t.push((row, k, -1.0));
499 b.push(0.0);
500 row += 1;
501 t.push((row, k, 1.0));
502 t.push((row, k - 1, -1.0));
503 b.push(cap_v[k] * inv);
504 row += 1;
505 }
506 for k in 2..n {
507 for sgn in [1.0, -1.0] {
508 t.push((row, k, sgn));
509 t.push((row, k - 1, -2.0 * sgn));
510 t.push((row, k - 2, sgn));
511 b.push(cap_a[k] * inv);
512 row += 1;
513 }
514 }
515 for k in 3..n {
516 for sgn in [1.0, -1.0] {
517 t.push((row, k, sgn));
518 t.push((row, k - 1, -3.0 * sgn));
519 t.push((row, k - 2, 3.0 * sgn));
520 t.push((row, k - 3, -sgn));
521 b.push(cap_j[k] * inv);
522 row += 1;
523 }
524 }
525 let m = row;
526 let mut scale = vec![1.0f64; m];
530 for r in n_eq..m {
531 if b[r].abs() > 1e-12 {
532 scale[r] = 1.0 / b[r].abs();
533 }
534 }
535 for entry in t.iter_mut() {
536 entry.2 *= scale[entry.0];
537 }
538 for r in 0..m {
539 b[r] *= scale[r];
540 }
541 let a = triplets_to_csc(m, n, t);
542 let p = CscMatrix::new(n, n, vec![0; n + 1], vec![], vec![]);
543 let q = vec![-1.0f64; n];
544 let cones = [
545 SupportedConeT::ZeroConeT(n_eq),
546 SupportedConeT::NonnegativeConeT(m - n_eq),
547 ];
548 let mut set = DefaultSettings::default();
549 set.verbose = false;
550 set.max_iter = 200;
551 let mut solver = DefaultSolver::new(&p, &q, &a, &b, &cones, set).ok()?;
552 solver.solve();
553 match solver.solution.status {
554 SolverStatus::Solved | SolverStatus::AlmostSolved => {
555 Some(solver.solution.x.iter().map(|x| x * total).collect())
556 }
557 _ => None,
558 }
559}
560
561fn triplets_to_csc(m: usize, n: usize, mut t: Vec<(usize, usize, f64)>) -> CscMatrix<f64> {
563 t.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
564 let mut colptr = vec![0usize; n + 1];
565 let mut rowval = Vec::with_capacity(t.len());
566 let mut nzval = Vec::with_capacity(t.len());
567 for &(r, c, v) in &t {
568 colptr[c + 1] += 1;
569 rowval.push(r);
570 nzval.push(v);
571 }
572 for c in 0..n {
573 colptr[c + 1] += colptr[c];
574 }
575 CscMatrix::new(m, n, colptr, rowval, nzval)
576}
577
578fn verify_fd<const N: usize>(
582 q: &[SRobotQ<N, f64>],
583 lim: &JointLimits<N>,
584 dt: f64,
585) -> Option<(&'static str, usize, f64, f64, usize)> {
586 let n = q.len();
587 let mut worst: Option<(f64, &'static str, usize, f64, f64, usize)> = None;
588 let mut consider = |val: f64, limit: f64, kind: &'static str, j: usize, idx: usize| {
589 if val > limit * (1.0 + 1e-6) {
590 let r = val / limit;
591 if worst.is_none_or(|w| r > w.0) {
592 worst = Some((r, kind, j, val, limit, idx));
593 }
594 }
595 };
596 for i in 1..n {
597 for j in 0..N {
598 consider(
599 (q[i].0[j] - q[i - 1].0[j]).abs() / dt,
600 lim.v_max.0[j],
601 "velocity",
602 j,
603 i,
604 );
605 }
606 }
607 for i in 2..n {
608 for j in 0..N {
609 let a = (q[i].0[j] - 2.0 * q[i - 1].0[j] + q[i - 2].0[j]).abs() / (dt * dt);
610 consider(a, lim.a_max.0[j], "acceleration", j, i);
611 }
612 }
613 for i in 3..n {
614 for j in 0..N {
615 let jk = (q[i].0[j] - 3.0 * q[i - 1].0[j] + 3.0 * q[i - 2].0[j] - q[i - 3].0[j]).abs()
616 / (dt * dt * dt);
617 consider(jk, lim.j_max.0[j], "jerk", j, i);
618 }
619 }
620 worst.map(|(_, kind, j, val, limit, idx)| (kind, j, val, limit, idx))
621}
622
623fn tcp_fd_ratios<const N: usize, FK: ContinuousFKChain<N, f64>>(
627 fk: &FK,
628 q: &[SRobotQ<N, f64>],
629 accel_cap: Option<f64>,
630 jerk_cap: Option<f64>,
631 dt: f64,
632) -> Result<(f64, f64), LinearError> {
633 if (accel_cap.is_none() && jerk_cap.is_none()) || q.len() < 3 {
634 return Ok((0.0, 0.0));
635 }
636 let pos: Vec<DVec3> = q
637 .iter()
638 .map(|qi| fk.fk_end(qi).map(|t| t.translation))
639 .collect::<Result<_, DekeError>>()?;
640 let sp: Vec<f64> = (0..pos.len() - 1)
641 .map(|i| pos[i + 1].distance(pos[i]) / dt)
642 .collect();
643 let a_ratio = accel_cap.map_or(0.0, |a| {
644 (1..sp.len()).fold(0.0f64, |w, i| w.max((sp[i] - sp[i - 1]).abs() / dt)) / a
645 });
646 let j_ratio = jerk_cap.map_or(0.0, |j| {
647 (2..sp.len()).fold(0.0f64, |w, i| {
648 w.max((sp[i] - 2.0 * sp[i - 1] + sp[i - 2]).abs() / (dt * dt))
649 }) / j
650 });
651 Ok((a_ratio, j_ratio))
652}
653
654fn fd_peaks<const N: usize>(q: &[SRobotQ<N, f64>], dt: f64) -> (f64, f64, f64) {
656 let n = q.len();
657 let (mut v, mut a, mut jk) = (0.0f64, 0.0f64, 0.0f64);
658 for i in 1..n {
659 for j in 0..N {
660 v = v.max((q[i].0[j] - q[i - 1].0[j]).abs() / dt);
661 }
662 }
663 for i in 2..n {
664 for j in 0..N {
665 a = a.max((q[i].0[j] - 2.0 * q[i - 1].0[j] + q[i - 2].0[j]).abs() / (dt * dt));
666 }
667 }
668 for i in 3..n {
669 for j in 0..N {
670 jk = jk.max(
671 (q[i].0[j] - 3.0 * q[i - 1].0[j] + 3.0 * q[i - 2].0[j] - q[i - 3].0[j]).abs()
672 / (dt * dt * dt),
673 );
674 }
675 }
676 (v, a, jk)
677}