1use crate::CuTrajectoryInterpolation;
23use crate::CuTrajectoryPayload;
24use crate::CuTrajectoryPoint;
25use crate::cartesian_blend::CuCartesianBlendPath;
26use crate::cartesian_blend::CuCartesianPathPoint;
27use crate::parameterize::CuJointTrajectoryLimits;
28use crate::parameterize::CuTimingParams;
29use crate::parameterize::CuTimingStatus;
30use crate::parameterize::CuTrajectoryProfile;
31use crate::parameterize::cubic_eval;
32use crate::parameterize::quintic_eval;
33use crate::quintic::cubic_limit_ratio;
34use crate::quintic::polynomial_extrema;
35use cu_spatial_payloads::Transform3D;
36use cu29::prelude::*;
37use cu29::units::si::f64::Time;
38use cu29::units::si::length::meter;
39use cu29::units::si::time::second;
40use repose_collision::CuCollisionEnv;
41use repose_collision::CuRobotCollisionModel;
42use repose_kinematics::CuJointAcceleration;
43use repose_kinematics::CuJointJerk;
44use repose_kinematics::CuJointPosition;
45use repose_kinematics::CuJointResolution;
46use repose_kinematics::CuJointVelocity;
47use repose_kinematics::CuKinematicChain;
48use serde::Deserialize;
49use serde::Serialize;
50
51#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
69#[reflect(no_field_bounds, from_reflect = false)]
70#[serde(deny_unknown_fields)]
72pub struct CuPathTimingParams<const N: usize> {
73 pub timing: CuTimingParams<N>,
75 #[serde(default)]
77 pub collision_step: CuJointResolution,
78}
79
80impl<const N: usize> Default for CuPathTimingParams<N> {
81 fn default() -> Self {
82 Self {
83 timing: CuTimingParams::default(),
84 collision_step: CuJointResolution::default(),
85 }
86 }
87}
88
89impl<const N: usize> CuPathTimingParams<N> {
90 pub fn validate(&self) -> CuResult<()> {
92 self.timing.validate()?;
93 self.collision_step.metric().validate()
94 }
95}
96
97pub fn parameterize_cartesian_path<
115 const N: usize,
116 const M: usize,
117 const CAP: usize,
118 const OUT: usize,
119>(
120 chain: &CuKinematicChain<N>,
121 model: &CuRobotCollisionModel<N, M>,
122 env: &CuCollisionEnv<'_>,
123 path: &CuCartesianBlendPath<N, CAP>,
124 params: &CuPathTimingParams<N>,
125 out: &mut CuTrajectoryPayload<N, OUT>,
126) -> CuResult<CuTimingStatus> {
127 out.clear();
128 out.interpolation = if params.timing.profile == CuTrajectoryProfile::SCurve {
129 CuTrajectoryInterpolation::Quintic
130 } else {
131 CuTrajectoryInterpolation::Cubic
132 };
133 let result = time_path(chain, model, env, path, params, out);
134 if !matches!(result, Ok(CuTimingStatus::Success)) {
135 out.clear();
136 }
137 result
138}
139
140#[derive(Debug, Clone, Copy, Default)]
143struct SpeedLimits {
144 vel: f64,
145 acc: f64,
146 jerk: f64,
147}
148
149#[derive(Debug, Clone, Copy, Default)]
151struct Span {
152 start: usize,
153 end: usize,
154 corner: Option<usize>,
155 limits: SpeedLimits,
156 speed: f64,
158}
159
160#[derive(Debug, Clone, Copy, Default)]
162struct Phase {
163 duration: f64,
164 jerk: f64,
165 acc: f64,
166}
167
168#[derive(Debug, Clone, Copy)]
170enum Motion {
171 Constant { speed: f64 },
172 Cubic { v0: f64, v1: f64 },
173 Quintic { v0: f64, v1: f64 },
174 Phased { v0: f64, phases: [Phase; 7] },
175}
176
177fn time_path<const N: usize, const M: usize, const CAP: usize, const OUT: usize>(
178 chain: &CuKinematicChain<N>,
179 model: &CuRobotCollisionModel<N, M>,
180 env: &CuCollisionEnv<'_>,
181 path: &CuCartesianBlendPath<N, CAP>,
182 config: &CuPathTimingParams<N>,
183 out: &mut CuTrajectoryPayload<N, OUT>,
184) -> CuResult<CuTimingStatus> {
185 let limits = &config.timing.limits;
186 let profile = config.timing.profile;
187 let count = path.len;
188 let dt = config.timing.sample_period.get::<second>();
189 if count < 2
190 || count > CAP
191 || !path.points[0].stop
192 || !path.points[count - 1].stop
193 || !dt.is_finite()
194 || dt <= 0.0
195 || config.collision_step.metric().validate().is_err()
196 {
197 return Ok(CuTimingStatus::InvalidInput);
198 }
199 let first = path.points[0].positions;
200 for (j, &joint) in first.iter().enumerate() {
201 let valid_kind = matches!(
202 (joint, limits.max_velocities[j]),
203 (CuJointPosition::Revolute(_), CuJointVelocity::Revolute(_))
204 | (CuJointPosition::Prismatic(_), CuJointVelocity::Prismatic(_))
205 | (CuJointPosition::Fixed, CuJointVelocity::Fixed)
206 ) && core::mem::discriminant(&limits.max_accelerations[j])
207 == core::mem::discriminant(&CuJointAcceleration::from_coordinate(joint, 0.0))
208 && (profile != CuTrajectoryProfile::SCurve
209 || matches!(joint, CuJointPosition::Fixed)
210 || limits.max_jerks.is_none_or(|jerks| {
211 core::mem::discriminant(&jerks[j])
212 == core::mem::discriminant(&CuJointJerk::from_coordinate(joint, 0.0))
213 }));
214 if !valid_kind {
215 return Ok(CuTimingStatus::InvalidInput);
216 }
217 if matches!(joint, CuJointPosition::Fixed) {
218 continue;
219 }
220 let v = limits.max_velocities[j].si_value();
221 let a = limits.acc(j);
222 let jerk = limits.jerk(j);
223 if !v.is_finite()
224 || v <= 0.0
225 || !a.is_finite()
226 || a <= 0.0
227 || (profile == CuTrajectoryProfile::SCurve && (jerk.is_nan() || jerk <= 0.0))
228 {
229 return Ok(CuTimingStatus::InvalidInput);
230 }
231 }
232
233 let points = path.points();
235 let mut s = [0.0_f64; CAP];
236 for i in 0..count {
237 let q = points[i].positions;
238 if !chain.is_within_limits(&q)
239 || (0..N).any(|j| core::mem::discriminant(&q[j]) != core::mem::discriminant(&first[j]))
240 {
241 return Ok(CuTimingStatus::InvalidInput);
242 }
243 if i > 0 {
244 let h = step(&points[i - 1].positions, &q, limits);
245 if !h.is_finite() || h <= 1e-15 {
246 return Ok(CuTimingStatus::InvalidInput);
247 }
248 s[i] = s[i - 1] + h;
249 }
250 }
251
252 let curvatures = spline_curvatures(points, &s);
256 let curve = Curve {
257 points,
258 s: &s,
259 curvatures: &curvatures,
260 };
261 for (i, _) in points.iter().enumerate().take(count - 1).skip(1) {
262 if points[i].stop {
263 continue;
264 }
265 let (_, slope, _) = curve.eval(i, 0.0);
266 if slope.iter().all(|m| libm::fabs(*m) <= 1e-12) {
267 return Ok(match points[i].zone {
268 Some(zone) => CuTimingStatus::NoContinuousMotion {
269 waypoint: zone.waypoint,
270 },
271 None => CuTimingStatus::UnexpectedStop { sample: i },
272 });
273 }
274 }
275
276 let metric = config.collision_step.metric();
278 for i in 0..count - 1 {
279 let h = s[i + 1] - s[i];
280 let (slope, _, _) = curve.bounds(i);
281 let mut checks = 1.0_f64;
282 for (j, &peak) in slope.iter().enumerate() {
283 checks = checks.max(libm::ceil(peak * h / metric.scale(first[j])));
284 }
285 if !checks.is_finite() || checks > 100_000.0 {
286 return Ok(CuTimingStatus::InvalidInput);
287 }
288 let zone = points[i + 1].zone;
289 let mut previous = points[i].positions;
290 for k in 1..=checks as usize {
291 let (q, _, _) = curve.eval(i, h * k as f64 / checks);
292 if !chain.is_within_limits(&q) {
293 return Ok(CuTimingStatus::InvalidInput);
294 }
295 if !model.is_configuration_collision_free(chain, &q, env)?
296 || !model.is_segment_collision_free(
297 chain,
298 &previous,
299 &q,
300 config.collision_step,
301 env,
302 )?
303 {
304 return Ok(CuTimingStatus::Collision { sample: i });
305 }
306 if let Some(zone) = zone {
307 let mut pose = Transform3D::<f64>::identity();
308 chain.forward_kinematics(&q, &mut pose)?;
309 let m = pose.to_matrix();
310 let c = zone.center.map(|c| c.get::<meter>());
311 let d = [m[0][3] - c[0], m[1][3] - c[1], m[2][3] - c[2]];
312 if libm::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]) > zone.radius.get::<meter>()
313 {
314 return Ok(CuTimingStatus::ZoneExceeded {
315 waypoint: zone.waypoint,
316 });
317 }
318 }
319 previous = q;
320 }
321 }
322
323 let mut spans = [Span::default(); CAP];
325 let mut nspans = 0;
326 let mut start = 0;
327 for i in 0..count - 1 {
328 let corner = corner_of(points, i);
329 let last = i + 1 == count - 1 || points[i + 1].stop || corner_of(points, i + 1) != corner;
330 if last {
331 spans[nspans] = Span {
332 start,
333 end: i + 1,
334 corner,
335 ..Span::default()
336 };
337 nspans += 1;
338 start = i + 1;
339 }
340 }
341 let spans = &mut spans[..nspans];
342 let scurve = profile == CuTrajectoryProfile::SCurve;
343 for p in 0..spans.len() {
351 let (lo, hi) = interior(spans, p);
352 let span = &mut spans[p];
353 let mut vel = f64::INFINITY;
354 let mut corner_vel = f64::INFINITY;
355 for i in lo..hi {
356 let (move_cap, corner_cap) = speed_caps(&curve.bounds(i), limits, &first, scurve);
357 vel = vel.min(move_cap);
358 corner_vel = corner_vel.min(corner_cap);
359 }
360 if !vel.is_finite() || vel <= 0.0 {
361 return Ok(CuTimingStatus::InvalidInput);
362 }
363 let mut acc = f64::INFINITY;
364 for i in span.start..span.end {
365 let bounds = curve.bounds(i);
366 let (slope, curvature, _) = bounds;
367 let v = vel.min(speed_caps(&bounds, limits, &first, scurve).0);
368 for j in 0..N {
369 if matches!(first[j], CuJointPosition::Fixed) {
370 continue;
371 }
372 let jmax = limits.jerk(j);
373 if slope[j] > 1e-300 {
374 acc = acc.min((limits.acc(j) - curvature[j] * v * v) / slope[j]);
375 }
376 if scurve && jmax.is_finite() && curvature[j] > 1e-300 {
377 acc = acc.min(jmax / (9.0 * curvature[j] * v));
378 }
379 }
380 }
381 if !acc.is_finite() || acc <= 0.0 {
382 return Ok(CuTimingStatus::InvalidInput);
383 }
384 let mut jerk = f64::INFINITY;
385 if scurve {
386 for i in span.start..span.end {
387 let bounds = curve.bounds(i);
388 let (slope, curvature, third) = bounds;
389 let v = vel.min(speed_caps(&bounds, limits, &first, scurve).0);
390 for j in 0..N {
391 let jmax = limits.jerk(j);
392 if matches!(first[j], CuJointPosition::Fixed)
393 || slope[j] <= 1e-300
394 || !jmax.is_finite()
395 {
396 continue;
397 }
398 let left = jmax - third[j] * v * v * v - 3.0 * curvature[j] * v * acc;
399 jerk = jerk.min(left / slope[j]);
400 }
401 }
402 if jerk.is_nan() || jerk <= 0.0 {
403 return Ok(CuTimingStatus::InvalidInput);
404 }
405 }
406 span.limits = SpeedLimits { vel, acc, jerk };
407 if span.corner.is_some() {
408 span.speed = corner_vel;
409 }
410 }
411 let mut p = 0;
414 while p < spans.len() {
415 if spans[p].corner.is_none() {
416 p += 1;
417 continue;
418 }
419 let first_corner = p;
420 while p + 1 < spans.len() && spans[p + 1].corner.is_some() {
421 p += 1;
422 }
423 if first_corner == 0
424 || p + 1 == spans.len()
425 || points[spans[first_corner].start].stop
426 || points[spans[p].end].stop
427 {
428 return Ok(CuTimingStatus::InvalidInput);
429 }
430 let speed = spans[first_corner..=p]
431 .iter()
432 .fold(f64::INFINITY, |v, span| v.min(span.speed))
433 .min(spans[first_corner - 1].limits.vel)
434 .min(spans[p + 1].limits.vel);
435 set_run_speed(spans, first_corner, speed);
436 p += 1;
437 }
438
439 let mut settled = false;
441 let mut blocked = None;
442 for _ in 0..400 {
443 settled = true;
444 for p in 0..spans.len() {
445 if spans[p].corner.is_some() {
446 continue;
447 }
448 let (v0, v1) = boundary_speeds(spans, p);
449 if plan_span(&curve, &s, spans, p, (v0, v1), config).is_some() {
450 continue;
451 }
452 settled = false;
453 let previous = (p > 0 && spans[p - 1].corner.is_some()).then(|| p - 1);
454 let next = (p + 1 < spans.len() && spans[p + 1].corner.is_some()).then_some(p + 1);
455 let lower = match (previous, next) {
457 (Some(before), Some(after)) => {
458 if v0 >= v1 {
459 before
460 } else {
461 after
462 }
463 }
464 (Some(corner), None) | (None, Some(corner)) => corner,
465 (None, None) => return Ok(CuTimingStatus::InvalidInput),
466 };
467 set_run_speed(spans, lower, spans[lower].speed * 0.9);
468 blocked = spans[lower].corner;
469 }
470 if settled {
471 break;
472 }
473 }
474 if !settled {
475 return Ok(match blocked {
476 Some(waypoint) => CuTimingStatus::NoContinuousMotion { waypoint },
477 None => CuTimingStatus::InvalidInput,
478 });
479 }
480
481 if OUT == 0 {
482 return Ok(CuTimingStatus::CapacityExceeded);
483 }
484 out.push(CuTrajectoryPoint {
485 time_from_start: Time::default(),
486 positions: first,
487 velocities: first.map(|q| CuJointVelocity::from_coordinate(q, 0.0)),
488 accelerations: first.map(|q| CuJointAcceleration::from_coordinate(q, 0.0)),
489 })?;
490 let mut elapsed = 0.0;
491 for p in 0..spans.len() {
492 let span = spans[p];
493 let length = s[span.end] - s[span.start];
494 let (v0, v1) = boundary_speeds(spans, p);
495 let (motion, duration) = if span.corner.is_some() {
496 (Motion::Constant { speed: span.speed }, length / span.speed)
497 } else {
498 match plan_span(&curve, &s, spans, p, (v0, v1), config) {
499 Some(planned) => planned,
500 None => return Ok(CuTimingStatus::InvalidInput),
501 }
502 };
503 let steps = libm::ceil(duration / dt).max(1.0);
504 let step_dt = duration / steps;
505 let mut phase_times = [0.0_f64; 6];
510 let mut phase_count = 0;
511 let mut knot_times = [0.0_f64; CAP];
512 let mut knot_count = 0;
513 if scurve {
514 if let Motion::Phased { phases, .. } = motion {
515 let mut at = 0.0;
516 for phase in &phases[..6] {
517 at += phase.duration;
518 phase_times[phase_count] = at;
519 phase_count += 1;
520 }
521 }
522 for knot in span.start + 1..span.end {
523 knot_times[knot_count] =
524 crossing_time(&motion, length, duration, s[knot] - s[span.start]);
525 knot_count += 1;
526 }
527 }
528 if !steps.is_finite()
529 || steps + (phase_count + knot_count) as f64 > OUT.saturating_sub(out.len()) as f64
530 {
531 return Ok(CuTimingStatus::CapacityExceeded);
532 }
533 let steps = steps as usize;
534 let (mut k, mut next_phase, mut next_knot) = (1, 0, 0);
535 let mut emitted = 0.0_f64;
536 let mut piece = span.start;
537 while k <= steps {
538 let grid = duration * k as f64 / steps as f64;
539 let mut extra = f64::INFINITY;
542 let mut from_phase = false;
543 loop {
544 let phase = phase_times
545 .get(next_phase)
546 .filter(|_| next_phase < phase_count);
547 let knot = knot_times.get(next_knot).filter(|_| next_knot < knot_count);
548 let (candidate, is_phase) = match (phase, knot) {
549 (Some(&p), Some(&q)) if p <= q => (p, true),
550 (_, Some(&q)) => (q, false),
551 (Some(&p), None) => (p, true),
552 (None, None) => break,
553 };
554 if candidate <= emitted + 0.1 * step_dt || candidate >= duration - 0.1 * step_dt {
555 if is_phase {
556 next_phase += 1;
557 } else {
558 next_knot += 1;
559 }
560 continue;
561 }
562 extra = candidate;
563 from_phase = is_phase;
564 break;
565 }
566 let mut take_extra = || {
567 if from_phase {
568 next_phase += 1;
569 } else {
570 next_knot += 1;
571 }
572 extra
573 };
574 let mut t = grid;
575 let mut last = false;
576 if extra < grid - 0.1 * step_dt {
577 t = take_extra();
578 } else {
579 if extra <= grid + 0.1 * step_dt {
580 t = take_extra();
581 }
582 last = k == steps;
583 k += 1;
584 }
585 emitted = t;
586 let (positions, velocities, accelerations) = if last {
587 let end = span.end - 1;
588 let (_, slope, curvature) = curve.eval(end, s[span.end] - s[end]);
589 let (_, _, acc) = motion.eval(length, duration, duration);
590 joint_state(points[span.end].positions, slope, curvature, v1, acc)
591 } else {
592 let (offset, speed, acc) = motion.eval(length, duration, t);
593 let at = s[span.start] + offset.clamp(0.0, length);
594 while piece + 1 < span.end && at > s[piece + 1] {
595 piece += 1;
596 }
597 let (q, slope, curvature) = curve.eval(piece, at - s[piece]);
598 joint_state(q, slope, curvature, speed.max(0.0), acc)
599 };
600 out.push(CuTrajectoryPoint {
601 time_from_start: Time::new::<second>(elapsed + t),
602 positions,
603 velocities,
604 accelerations,
605 })?;
606 }
607 elapsed += duration;
608 }
609
610 let ratio = if scurve {
616 out.quintic_limit_ratio(limits)
617 } else {
618 cubic_limit_ratio(out, limits)
619 };
620 match ratio {
621 Some(ratio) if ratio.is_finite() => {
622 if ratio > 1.0 {
623 out.stretch_time(ratio * (1.0 + 1e-9))?;
624 }
625 }
626 _ => return Ok(CuTimingStatus::InvalidInput),
627 }
628 Ok(CuTimingStatus::Success)
629}
630
631fn crossing_time(motion: &Motion, length: f64, duration: f64, target: f64) -> f64 {
634 let (mut lo, mut hi) = (0.0, duration);
635 for _ in 0..64 {
636 let mid = 0.5 * (lo + hi);
637 if motion.eval(length, duration, mid).0 < target {
638 lo = mid;
639 } else {
640 hi = mid;
641 }
642 }
643 hi
644}
645
646fn corner_of<const N: usize>(points: &[CuCartesianPathPoint<N>], i: usize) -> Option<usize> {
649 let zone = |k: usize| points.get(k).and_then(|p| p.zone).map(|z| z.waypoint);
650 zone(i + 1)
651 .or_else(|| zone(i).filter(|_| !points[i + 1].stop))
652 .or_else(|| zone(i + 2).filter(|_| !points[i].stop))
653}
654
655fn speed_caps<const N: usize>(
660 (slope, curvature, third): &([f64; N], [f64; N], [f64; N]),
661 limits: &CuJointTrajectoryLimits<N>,
662 first: &[CuJointPosition; N],
663 scurve: bool,
664) -> (f64, f64) {
665 let mut move_cap = f64::INFINITY;
666 let mut corner_cap = f64::INFINITY;
667 for j in 0..N {
668 if matches!(first[j], CuJointPosition::Fixed) {
669 continue;
670 }
671 let v = limits.max_velocities[j].si_value();
672 let a = limits.acc(j);
673 let jmax = limits.jerk(j);
674 if slope[j] > 1e-300 {
675 move_cap = move_cap.min(v / slope[j]);
676 corner_cap = corner_cap.min(v / slope[j]);
677 }
678 if curvature[j] > 1e-300 {
679 move_cap = move_cap.min(libm::sqrt(a / (2.0 * curvature[j])));
680 corner_cap = corner_cap.min(libm::sqrt(a / curvature[j]));
681 }
682 if scurve && jmax.is_finite() && third[j] > 1e-300 {
683 move_cap = move_cap.min(libm::cbrt(jmax / (3.0 * third[j])));
684 corner_cap = corner_cap.min(libm::cbrt(jmax / third[j]));
685 }
686 }
687 (move_cap, corner_cap)
688}
689
690const TRANSITION_PIECES: usize = 4;
699
700fn interior(spans: &[Span], p: usize) -> (usize, usize) {
704 let span = spans[p];
705 if span.corner.is_some() {
706 return (span.start, span.end);
707 }
708 let mut lo = span.start;
709 let mut hi = span.end;
710 if p > 0 && spans[p - 1].corner.is_some() {
711 lo += TRANSITION_PIECES;
712 }
713 if p + 1 < spans.len() && spans[p + 1].corner.is_some() {
714 hi = hi.saturating_sub(TRANSITION_PIECES);
715 }
716 if lo < hi {
717 (lo, hi)
718 } else {
719 (span.start, span.end)
720 }
721}
722
723fn set_run_speed(spans: &mut [Span], p: usize, speed: f64) {
725 let mut first = p;
726 while first > 0 && spans[first - 1].corner.is_some() {
727 first -= 1;
728 }
729 let mut last = p;
730 while last + 1 < spans.len() && spans[last + 1].corner.is_some() {
731 last += 1;
732 }
733 for span in &mut spans[first..=last] {
734 span.speed = speed;
735 }
736}
737
738fn boundary_speeds(spans: &[Span], p: usize) -> (f64, f64) {
740 if spans[p].corner.is_some() {
741 return (spans[p].speed, spans[p].speed);
742 }
743 let v0 = if p > 0 && spans[p - 1].corner.is_some() {
744 spans[p - 1].speed
745 } else {
746 0.0
747 };
748 let v1 = if p + 1 < spans.len() && spans[p + 1].corner.is_some() {
749 spans[p + 1].speed
750 } else {
751 0.0
752 };
753 (v0, v1)
754}
755
756fn step<const N: usize>(
760 a: &[CuJointPosition; N],
761 b: &[CuJointPosition; N],
762 limits: &CuJointTrajectoryLimits<N>,
763) -> f64 {
764 let mut sum = 0.0_f64;
765 for j in 0..N {
766 if !matches!(a[j], CuJointPosition::Fixed) {
767 let d = (b[j].si_value() - a[j].si_value()) / limits.max_velocities[j].si_value();
768 sum += d * d;
769 }
770 }
771 libm::sqrt(sum)
772}
773
774type JointState<const N: usize> = (
776 [CuJointPosition; N],
777 [CuJointVelocity; N],
778 [CuJointAcceleration; N],
779);
780
781fn joint_state<const N: usize>(
783 q: [CuJointPosition; N],
784 slope: [f64; N],
785 curvature: [f64; N],
786 speed: f64,
787 acc: f64,
788) -> JointState<N> {
789 (
790 q,
791 core::array::from_fn(|j| CuJointVelocity::from_coordinate(q[j], slope[j] * speed)),
792 core::array::from_fn(|j| {
793 CuJointAcceleration::from_coordinate(
794 q[j],
795 curvature[j] * speed * speed + slope[j] * acc,
796 )
797 }),
798 )
799}
800
801fn spline_curvatures<const N: usize, const CAP: usize>(
808 points: &[CuCartesianPathPoint<N>],
809 s: &[f64; CAP],
810) -> [[f64; N]; CAP] {
811 let count = points.len();
812 let mut curvatures = [[0.0_f64; N]; CAP];
813 let mut upper = [0.0_f64; CAP];
814 let mut rhs = [0.0_f64; CAP];
815 for (j, _) in points[0].positions.iter().enumerate() {
816 let y = |i: usize| points[i].positions[j].si_value();
817 for i in 0..count {
818 if i == 0 || i + 1 == count || points[i].stop {
819 upper[i] = 0.0;
820 rhs[i] = 0.0;
821 continue;
822 }
823 let (h0, h1) = (s[i] - s[i - 1], s[i + 1] - s[i]);
824 let diagonal = 2.0 * (h0 + h1) - h0 * upper[i - 1];
825 let slopes = (y(i + 1) - y(i)) / h1 - (y(i) - y(i - 1)) / h0;
826 upper[i] = h1 / diagonal;
827 rhs[i] = (6.0 * slopes - h0 * rhs[i - 1]) / diagonal;
828 }
829 let mut next = 0.0;
830 for i in (0..count).rev() {
831 next = rhs[i] - upper[i] * next;
832 curvatures[i][j] = next;
833 }
834 }
835 curvatures
836}
837
838struct Curve<'a, const N: usize, const CAP: usize> {
840 points: &'a [CuCartesianPathPoint<N>],
841 s: &'a [f64; CAP],
842 curvatures: &'a [[f64; N]; CAP],
843}
844
845impl<const N: usize, const CAP: usize> Curve<'_, N, CAP> {
846 fn coefficients(&self, i: usize, j: usize) -> (f64, f64, f64, f64, f64) {
849 let h = self.s[i + 1] - self.s[i];
850 let y0 = self.points[i].positions[j].si_value();
851 let y1 = self.points[i + 1].positions[j].si_value();
852 let (m0, m1) = (self.curvatures[i][j], self.curvatures[i + 1][j]);
853 (
854 h,
855 y0,
856 (y1 - y0) / h - h * (2.0 * m0 + m1) / 6.0,
857 m0,
858 (m1 - m0) / h,
859 )
860 }
861
862 fn eval(&self, i: usize, offset: f64) -> ([CuJointPosition; N], [f64; N], [f64; N]) {
864 let mut q = self.points[i].positions;
865 let mut slope = [0.0; N];
866 let mut curvature = [0.0; N];
867 for j in 0..N {
868 let (h, y0, b, m0, d) = self.coefficients(i, j);
869 let t = offset.clamp(0.0, h);
870 q[j] = q[j].with_si_value(y0 + t * (b + t * (0.5 * m0 + t * d / 6.0)));
871 slope[j] = b + t * (m0 + 0.5 * t * d);
872 curvature[j] = m0 + t * d;
873 }
874 (q, slope, curvature)
875 }
876
877 fn bounds(&self, i: usize) -> ([f64; N], [f64; N], [f64; N]) {
883 let mut slope = [0.0; N];
884 let mut curvature = [0.0; N];
885 let mut jerk = [0.0; N];
886 for j in 0..N {
887 let (h, _, b, m0, d) = self.coefficients(i, j);
888 let at = |t: f64| b + t * (m0 + 0.5 * t * d);
889 let mut peak = libm::fabs(b).max(libm::fabs(at(h)));
890 if libm::fabs(d) > 1e-300 {
891 let t = -m0 / d;
892 if t > 0.0 && t < h {
893 peak = peak.max(libm::fabs(at(t)));
894 }
895 }
896 slope[j] = peak;
897 curvature[j] = libm::fabs(m0).max(libm::fabs(m0 + h * d));
898 jerk[j] = libm::fabs(d);
899 }
900 (slope, curvature, jerk)
901 }
902}
903
904impl Motion {
905 fn eval(&self, length: f64, duration: f64, t: f64) -> (f64, f64, f64) {
907 let (offset, speed, acc, _) = self.eval_jerk(length, duration, t);
908 (offset, speed, acc)
909 }
910
911 fn eval_jerk(&self, length: f64, duration: f64, t: f64) -> (f64, f64, f64, f64) {
913 let tt = duration * duration;
914 match *self {
915 Motion::Constant { speed } => (speed * t, speed, 0.0, 0.0),
916 Motion::Cubic { v0, v1 } => {
917 let u = t / duration;
918 let (offset, speed) = cubic_eval(u, 0.0, length, v0, v1, duration);
919 let a = v0 * duration;
920 let b = 3.0 * length - 2.0 * a - v1 * duration;
921 let c = -2.0 * length + a + v1 * duration;
922 (
923 offset,
924 speed,
925 (2.0 * b + 6.0 * c * u) / tt,
926 6.0 * c / (tt * duration),
927 )
928 }
929 Motion::Quintic { v0, v1 } => {
930 let u = t / duration;
931 let (offset, speed) = quintic_eval(u, 0.0, length, v0, v1, duration);
932 let start = length - v0 * duration;
933 let end = (v1 - v0) * duration;
934 let a3 = 10.0 * start - 4.0 * end;
935 let a4 = -15.0 * start + 7.0 * end;
936 let a5 = 6.0 * start - 3.0 * end;
937 (
938 offset,
939 speed,
940 u * (6.0 * a3 + u * (12.0 * a4 + 20.0 * a5 * u)) / tt,
941 (6.0 * a3 + u * (24.0 * a4 + 60.0 * a5 * u)) / (tt * duration),
942 )
943 }
944 Motion::Phased { v0, phases } => {
945 let mut remaining = t;
946 let mut offset = 0.0;
947 let mut speed = v0;
948 let mut acc = 0.0;
949 let mut jerk = 0.0;
950 for phase in phases {
951 let tau = remaining.min(phase.duration);
952 offset += speed * tau
953 + 0.5 * phase.acc * tau * tau
954 + phase.jerk * tau * tau * tau / 6.0;
955 speed += phase.acc * tau + 0.5 * phase.jerk * tau * tau;
956 acc = phase.acc + phase.jerk * tau;
957 jerk = phase.jerk;
958 remaining -= tau;
959 if remaining <= 0.0 {
960 break;
961 }
962 }
963 (offset, speed, acc, jerk)
964 }
965 }
966 }
967}
968
969fn plan_span<const N: usize, const CAP: usize>(
973 curve: &Curve<'_, N, CAP>,
974 s: &[f64; CAP],
975 spans: &[Span],
976 p: usize,
977 (v0, v1): (f64, f64),
978 config: &CuPathTimingParams<N>,
979) -> Option<(Motion, f64)> {
980 let span = spans[p];
981 let length = s[span.end] - s[span.start];
982 let (motion, duration) = plan(config.timing.profile, length, v0, v1, &span.limits)?;
983 let (lo, hi) = interior(spans, p);
984 if (lo, hi) == (span.start, span.end) {
985 return Some((motion, duration));
986 }
987 let limits = &config.timing.limits;
988 let jerk_limited = config.timing.profile == CuTrajectoryProfile::SCurve;
989 let mut piece = span.start;
990 const SAMPLES: usize = 512;
993 for k in 0..=SAMPLES {
994 let t = duration * k as f64 / SAMPLES as f64;
995 let (offset, speed, acc, jerk) = motion.eval_jerk(length, duration, t);
996 let at = s[span.start] + offset.clamp(0.0, length);
997 while piece + 1 < span.end && at > s[piece + 1] {
998 piece += 1;
999 }
1000 if piece >= lo && piece < hi {
1001 continue;
1002 }
1003 let (_, slope, curvature) = curve.eval(piece, at - s[piece]);
1004 for j in 0..N {
1005 let (_, _, _, _, third) = curve.coefficients(piece, j);
1006 let velocity = libm::fabs(slope[j] * speed);
1007 let acceleration = libm::fabs(curvature[j] * speed * speed + slope[j] * acc);
1008 let joint_jerk = libm::fabs(
1009 third * speed * speed * speed + 3.0 * curvature[j] * speed * acc + slope[j] * jerk,
1010 );
1011 let tolerance = 1.0 + 1e-9;
1012 if velocity > limits.max_velocities[j].si_value() * tolerance
1013 || acceleration > limits.acc(j) * tolerance
1014 || (jerk_limited && joint_jerk > limits.jerk(j) * tolerance)
1015 {
1016 return None;
1017 }
1018 }
1019 }
1020 Some((motion, duration))
1021}
1022
1023fn plan(
1026 profile: CuTrajectoryProfile,
1027 length: f64,
1028 v0: f64,
1029 v1: f64,
1030 limits: &SpeedLimits,
1031) -> Option<(Motion, f64)> {
1032 match profile {
1033 CuTrajectoryProfile::Trapezoidal => plan_phased(length, v0, v1, limits, f64::INFINITY),
1034 CuTrajectoryProfile::SCurve => plan_phased(length, v0, v1, limits, limits.jerk),
1035 CuTrajectoryProfile::Cubic => {
1036 plan_polynomial(false, length, v0, v1, limits).map(|t| (Motion::Cubic { v0, v1 }, t))
1037 }
1038 CuTrajectoryProfile::MinimumJerk => {
1039 plan_polynomial(true, length, v0, v1, limits).map(|t| (Motion::Quintic { v0, v1 }, t))
1040 }
1041 }
1042}
1043
1044fn ramp(dv: f64, acc: f64, jerk: f64) -> (f64, f64, f64) {
1047 if dv <= 0.0 {
1048 (0.0, 0.0, 0.0)
1049 } else if !jerk.is_finite() {
1050 (0.0, dv / acc, acc)
1051 } else if dv >= acc * acc / jerk {
1052 (acc / jerk, dv / acc - acc / jerk, acc)
1053 } else {
1054 let tj = libm::sqrt(dv / jerk);
1055 (tj, 0.0, jerk * tj)
1056 }
1057}
1058
1059fn ramp_distance(from: f64, to: f64, acc: f64, jerk: f64) -> f64 {
1061 let (tj, ta, _) = ramp(libm::fabs(to - from), acc, jerk);
1062 0.5 * (from + to) * (2.0 * tj + ta)
1063}
1064
1065fn plan_phased(
1068 length: f64,
1069 v0: f64,
1070 v1: f64,
1071 limits: &SpeedLimits,
1072 jerk: f64,
1073) -> Option<(Motion, f64)> {
1074 let acc = limits.acc;
1075 let distance =
1076 |peak: f64| ramp_distance(v0, peak, acc, jerk) + ramp_distance(peak, v1, acc, jerk);
1077 let low = v0.max(v1);
1078 if low > limits.vel * (1.0 + 1e-9) || distance(low) > length * (1.0 + 1e-12) {
1079 return None;
1080 }
1081 let peak = if distance(limits.vel) <= length {
1082 limits.vel
1083 } else {
1084 let (mut lo, mut hi) = (low, limits.vel);
1085 for _ in 0..80 {
1086 let mid = 0.5 * (lo + hi);
1087 if distance(mid) <= length {
1088 lo = mid;
1089 } else {
1090 hi = mid;
1091 }
1092 }
1093 lo
1094 };
1095 if peak <= 0.0 {
1096 return None;
1097 }
1098 let cruise = (length - distance(peak)).max(0.0) / peak;
1099 let (uj, ua, up) = ramp(peak - v0, acc, jerk);
1100 let (dj, da, dp) = ramp(peak - v1, acc, jerk);
1101 let jerk = if jerk.is_finite() { jerk } else { 0.0 };
1102 let phases = [
1103 Phase {
1104 duration: uj,
1105 jerk,
1106 acc: 0.0,
1107 },
1108 Phase {
1109 duration: ua,
1110 jerk: 0.0,
1111 acc: up,
1112 },
1113 Phase {
1114 duration: uj,
1115 jerk: -jerk,
1116 acc: up,
1117 },
1118 Phase {
1119 duration: cruise,
1120 jerk: 0.0,
1121 acc: 0.0,
1122 },
1123 Phase {
1124 duration: dj,
1125 jerk: -jerk,
1126 acc: 0.0,
1127 },
1128 Phase {
1129 duration: da,
1130 jerk: 0.0,
1131 acc: -dp,
1132 },
1133 Phase {
1134 duration: dj,
1135 jerk,
1136 acc: -dp,
1137 },
1138 ];
1139 let duration = 2.0 * uj + ua + cruise + 2.0 * dj + da;
1140 Some((Motion::Phased { v0, phases }, duration))
1141}
1142
1143fn plan_polynomial(
1146 quintic: bool,
1147 length: f64,
1148 v0: f64,
1149 v1: f64,
1150 limits: &SpeedLimits,
1151) -> Option<f64> {
1152 let fits = |t: f64| {
1153 let (high, low, acc) = polynomial_extrema(quintic, length, v0, v1, t);
1154 high <= limits.vel * (1.0 + 1e-9) && acc <= limits.acc * (1.0 + 1e-9) && low >= 0.0
1155 };
1156 let mut t = length / limits.vel;
1157 let mut found = None;
1158 for _ in 0..400 {
1159 if fits(t) {
1160 found = Some(t);
1161 break;
1162 }
1163 t *= 1.05;
1164 }
1165 let mut hi = found?;
1166 let mut lo = hi / 1.05;
1167 for _ in 0..50 {
1168 let mid = 0.5 * (lo + hi);
1169 if fits(mid) {
1170 hi = mid;
1171 } else {
1172 lo = mid;
1173 }
1174 }
1175 Some(hi)
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180 use super::*;
1181 use crate::CuCartesianBlendParams;
1182 use crate::CuCartesianBlendStatus;
1183 use crate::CuCartesianWaypoint;
1184 use crate::CuTcpZone;
1185 use crate::blend_cartesian_path;
1186 use crate::test_support::limits;
1187 use crate::test_support::obstacle;
1188 use crate::test_support::q;
1189 use crate::test_support::robot;
1190 use crate::test_support::waypoints;
1191 use cu29::units::si::f64::Length;
1192 use cu29::units::si::length::meter;
1193
1194 const PROFILES: [CuTrajectoryProfile; 4] = [
1195 CuTrajectoryProfile::Trapezoidal,
1196 CuTrajectoryProfile::SCurve,
1197 CuTrajectoryProfile::MinimumJerk,
1198 CuTrajectoryProfile::Cubic,
1199 ];
1200
1201 fn timing(profile: CuTrajectoryProfile) -> CuPathTimingParams<3> {
1202 let jerk = CuJointJerk::from_coordinate(q(0.0, 0.0)[0], 4.0);
1203 CuPathTimingParams {
1204 timing: CuTimingParams {
1205 limits: limits().with_jerks([jerk; 3]),
1206 profile,
1207 sample_period: Time::new::<second>(0.02),
1208 },
1209 collision_step: CuCartesianBlendParams::default().collision_step,
1210 }
1211 }
1212
1213 fn timed<const K: usize>(
1214 points: &[[CuJointPosition; 3]; K],
1215 zones: [f64; K],
1216 profile: CuTrajectoryProfile,
1217 ) -> (CuTimingStatus, CuTrajectoryPayload<3, 512>) {
1218 let (chain, model) = robot();
1219 let env = CuCollisionEnv::EMPTY;
1220 let mut path = CuCartesianBlendPath::<3, 256>::default();
1221 assert_eq!(
1222 blend_cartesian_path(
1223 &chain,
1224 &model,
1225 &env,
1226 &waypoints(points, zones),
1227 &CuCartesianBlendParams::default(),
1228 &mut path
1229 )
1230 .unwrap(),
1231 CuCartesianBlendStatus::Success
1232 );
1233 let mut out = CuTrajectoryPayload::<3, 512>::new();
1234 let status =
1235 parameterize_cartesian_path(&chain, &model, &env, &path, &timing(profile), &mut out)
1236 .unwrap();
1237 (status, out)
1238 }
1239
1240 fn duration(out: &CuTrajectoryPayload<3, 512>) -> f64 {
1241 out.points().last().unwrap().time_from_start.get::<second>()
1242 }
1243
1244 fn at_rest(p: &CuTrajectoryPoint<3>, x: f64, y: f64) -> bool {
1245 (p.positions[0].si_value() - x).abs() < 1e-9
1246 && (p.positions[1].si_value() - y).abs() < 1e-9
1247 && p.velocities.iter().all(|v| v.si_value().abs() < 1e-9)
1248 }
1249
1250 fn assert_within_limits(out: &CuTrajectoryPayload<3, 512>) {
1252 for w in out.points().windows(2) {
1253 let dt = (w[1].time_from_start - w[0].time_from_start).get::<second>();
1254 assert!(dt > 0.0, "times must be strictly increasing");
1255 for j in 0..3 {
1256 let v = w[1].velocities[j].si_value();
1257 assert!(v.abs() <= 0.4 * (1.0 + 1e-6), "velocity {v}");
1258 let a = (v - w[0].velocities[j].si_value()) / dt;
1259 assert!(a.abs() <= 1.0 * (1.0 + 1e-3), "acceleration {a}");
1260 }
1261 }
1262 }
1263
1264 #[test]
1265 fn every_profile_flies_through_a_positive_zone() {
1266 let points = [q(0.0, 0.0), q(0.3, 0.0), q(0.3, 0.3)];
1267 for profile in PROFILES {
1268 let (status, out) = timed(&points, [0.0, 0.1, 0.0], profile);
1269 assert_eq!(status, CuTimingStatus::Success, "{profile:?}");
1270 assert_within_limits(&out);
1271 assert!(at_rest(&out.points()[0], 0.0, 0.0));
1272 assert!(at_rest(out.points().last().unwrap(), 0.3, 0.3));
1273 let interior = &out.points()[1..out.len() - 1];
1274 assert!(
1275 interior
1276 .iter()
1277 .all(|p| p.velocities.iter().any(|v| v.si_value().abs() > 1e-6)),
1278 "{profile:?} must not stop before the path end"
1279 );
1280 }
1281 }
1282
1283 #[test]
1284 fn every_profile_stops_exactly_at_a_zero_zone() {
1285 let points = [q(0.0, 0.0), q(0.3, 0.0), q(0.3, 0.3)];
1286 for profile in PROFILES {
1287 let (status, out) = timed(&points, [0.0; 3], profile);
1288 assert_eq!(status, CuTimingStatus::Success, "{profile:?}");
1289 assert_within_limits(&out);
1290 assert!(out.points().iter().any(|p| at_rest(p, 0.3, 0.0)));
1291 }
1292 }
1293
1294 #[test]
1295 fn flying_through_a_zone_is_faster_than_stopping() {
1296 let points = [q(0.0, 0.0), q(0.3, 0.0), q(0.3, 0.3)];
1297 for profile in PROFILES {
1298 let (_, blended) = timed(&points, [0.0, 0.1, 0.0], profile);
1299 let (_, stopped) = timed(&points, [0.0; 3], profile);
1300 assert!(
1301 duration(&blended) < duration(&stopped),
1302 "{profile:?}: {} >= {}",
1303 duration(&blended),
1304 duration(&stopped)
1305 );
1306 }
1307 }
1308
1309 #[test]
1310 fn zones_are_per_waypoint_on_one_path() {
1311 let points = [q(0.0, 0.0), q(0.3, 0.0), q(0.3, 0.3), q(0.0, 0.3)];
1312 for profile in PROFILES {
1313 let (status, out) = timed(&points, [0.0, 0.1, 0.0, 0.0], profile);
1314 assert_eq!(status, CuTimingStatus::Success, "{profile:?}");
1315 assert_within_limits(&out);
1316 assert!(!out.points().iter().any(|p| at_rest(p, 0.3, 0.0)));
1317 assert!(out.points().iter().any(|p| at_rest(p, 0.3, 0.3)));
1318 assert!(at_rest(out.points().last().unwrap(), 0.0, 0.3));
1319 }
1320 }
1321
1322 #[test]
1323 fn sample_cubic_is_continuous_across_stored_points() {
1324 let points = [q(0.0, 0.0), q(0.3, 0.0), q(0.3, 0.3)];
1325 let (status, out) = timed(&points, [0.0, 0.1, 0.0], CuTrajectoryProfile::Cubic);
1326 assert_eq!(status, CuTimingStatus::Success);
1327 for p in &out.points()[1..out.len() - 1] {
1328 let t = p.time_from_start.get::<second>();
1329 let mut before = (q(0.0, 0.0), [CuJointVelocity::default(); 3]);
1330 let mut after = before;
1331 out.sample_cubic(Time::new::<second>(t - 1e-7), &mut before.0, &mut before.1)
1332 .unwrap();
1333 out.sample_cubic(Time::new::<second>(t + 1e-7), &mut after.0, &mut after.1)
1334 .unwrap();
1335 for j in 0..3 {
1336 assert!((before.0[j].si_value() - after.0[j].si_value()).abs() < 1e-5);
1337 assert!((before.1[j].si_value() - after.1[j].si_value()).abs() < 1e-4);
1338 }
1339 }
1340 }
1341
1342 #[test]
1343 fn scurve_path_acceleration_is_continuous() {
1344 let points = [q(0.0, 0.0), q(0.3, 0.0)];
1345 let (status, out) = timed(&points, [0.0, 0.0], CuTrajectoryProfile::SCurve);
1346 assert_eq!(status, CuTimingStatus::Success);
1347 for w in out.points().windows(2) {
1350 let dt = (w[1].time_from_start - w[0].time_from_start).get::<second>();
1351 let jerk = (w[1].accelerations[0].si_value() - w[0].accelerations[0].si_value()) / dt;
1352 assert!(jerk.abs() <= 4.0 * (1.0 + 1e-6), "jerk {jerk}");
1353 }
1354 }
1355
1356 #[test]
1357 fn interpolated_path_is_rechecked_for_collisions() {
1358 let (chain, model) = robot();
1359 let points = [q(0.0, 0.0), q(0.3, 0.0), q(0.3, 0.3)];
1360 let mut path = CuCartesianBlendPath::<3, 256>::default();
1361 blend_cartesian_path(
1362 &chain,
1363 &model,
1364 &CuCollisionEnv::EMPTY,
1365 &waypoints(&points, [0.0, 0.1, 0.0]),
1366 &CuCartesianBlendParams::default(),
1367 &mut path,
1368 )
1369 .unwrap();
1370 let obstacles = [obstacle()];
1371 let env = CuCollisionEnv::new(&obstacles);
1372 let mut out = CuTrajectoryPayload::<3, 512>::new();
1373 let status = parameterize_cartesian_path(
1374 &chain,
1375 &model,
1376 &env,
1377 &path,
1378 &timing(CuTrajectoryProfile::Cubic),
1379 &mut out,
1380 )
1381 .unwrap();
1382 assert!(matches!(status, CuTimingStatus::Collision { .. }));
1383 assert!(out.is_empty());
1384 }
1385
1386 #[test]
1387 fn reversal_inside_a_zone_cannot_keep_moving() {
1388 let (chain, model) = robot();
1389 let env = CuCollisionEnv::EMPTY;
1390 let zone = CuTcpZone {
1391 waypoint: 1,
1392 center: [0.3, 0.0, 0.0].map(Length::new::<meter>),
1393 radius: Length::new::<meter>(0.1),
1394 };
1395 let mut path = CuCartesianBlendPath::<3, 8>::default();
1396 path.points[0] = CuCartesianPathPoint {
1397 positions: q(0.0, 0.0),
1398 stop: true,
1399 zone: None,
1400 };
1401 path.points[1] = CuCartesianPathPoint {
1402 positions: q(0.3, 0.0),
1403 stop: false,
1404 zone: Some(zone),
1405 };
1406 path.points[2] = CuCartesianPathPoint {
1407 positions: q(0.0, 0.0),
1408 stop: true,
1409 zone: None,
1410 };
1411 path.len = 3;
1412 let mut out = CuTrajectoryPayload::<3, 512>::new();
1413 let status = parameterize_cartesian_path(
1414 &chain,
1415 &model,
1416 &env,
1417 &path,
1418 &timing(CuTrajectoryProfile::Trapezoidal),
1419 &mut out,
1420 )
1421 .unwrap();
1422 assert_eq!(status, CuTimingStatus::NoContinuousMotion { waypoint: 1 });
1423 assert!(out.is_empty());
1424 }
1425
1426 #[test]
1427 fn invalid_period_and_small_capacity_leave_no_output() {
1428 let (chain, model) = robot();
1429 let env = CuCollisionEnv::EMPTY;
1430 let points = [q(0.0, 0.0), q(0.3, 0.0), q(0.3, 0.3)];
1431 let mut path = CuCartesianBlendPath::<3, 256>::default();
1432 blend_cartesian_path(
1433 &chain,
1434 &model,
1435 &env,
1436 &waypoints(&points, [0.0, 0.1, 0.0]),
1437 &CuCartesianBlendParams::default(),
1438 &mut path,
1439 )
1440 .unwrap();
1441 let mut config = timing(CuTrajectoryProfile::Cubic);
1442 config.timing.sample_period = Time::new::<second>(0.0);
1443 let mut out = CuTrajectoryPayload::<3, 512>::new();
1444 assert_eq!(
1445 parameterize_cartesian_path(&chain, &model, &env, &path, &config, &mut out).unwrap(),
1446 CuTimingStatus::InvalidInput
1447 );
1448 assert!(out.is_empty());
1449 let mut small = CuTrajectoryPayload::<3, 8>::new();
1450 assert_eq!(
1451 parameterize_cartesian_path(
1452 &chain,
1453 &model,
1454 &env,
1455 &path,
1456 &timing(CuTrajectoryProfile::Cubic),
1457 &mut small
1458 )
1459 .unwrap(),
1460 CuTimingStatus::CapacityExceeded
1461 );
1462 assert!(small.is_empty());
1463 }
1464
1465 #[test]
1466 fn rebel_flyby_is_timed_with_every_profile() {
1467 use cu29::units::si::angle::radian;
1468 use cu29::units::si::angular_velocity::radian_per_second;
1469 use cu29::units::si::f64::Angle;
1470 use cu29::units::si::f64::AngularVelocity;
1471 let chain = repose_kinematics::igus_rebel_chain();
1472 let model = repose_collision::igus_rebel_collision_model();
1473 let env = CuCollisionEnv::EMPTY;
1474 let to_q =
1475 |j1: f64| [j1, 0.75, -0.25, 0.0, -0.3, 0.0].map(|v| Angle::new::<radian>(v).into());
1476 let points = [to_q(-0.75), to_q(0.0), to_q(0.75)];
1477 let zones = [0.0, 0.03, 0.0].map(Length::new::<meter>);
1478 let waypoints: [CuCartesianWaypoint<6>; 3] =
1479 core::array::from_fn(|i| CuCartesianWaypoint::new(points[i], zones[i]));
1480 let mut blend = CuCartesianBlendParams::default();
1481 blend.ik.max_iterations = 256;
1482 let mut path = CuCartesianBlendPath::<6, 256>::default();
1483 assert_eq!(
1484 blend_cartesian_path(&chain, &model, &env, &waypoints, &blend, &mut path).unwrap(),
1485 CuCartesianBlendStatus::Success
1486 );
1487 let velocity = CuJointVelocity::Revolute(AngularVelocity::new::<radian_per_second>(1.0));
1488 for profile in PROFILES {
1489 let rev = points[0][0];
1490 let config = CuPathTimingParams {
1491 timing: CuTimingParams {
1492 limits: CuJointTrajectoryLimits::new(
1493 [velocity; 6],
1494 [CuJointAcceleration::from_coordinate(rev, 2.0); 6],
1495 )
1496 .with_jerks([CuJointJerk::from_coordinate(rev, 10.0); 6]),
1497 profile,
1498 sample_period: Time::new::<second>(0.02),
1499 },
1500 collision_step: blend.collision_step,
1501 };
1502 let mut out = CuTrajectoryPayload::<6, 1024>::new();
1503 assert_eq!(
1504 parameterize_cartesian_path(&chain, &model, &env, &path, &config, &mut out)
1505 .unwrap(),
1506 CuTimingStatus::Success,
1507 "{profile:?}"
1508 );
1509 }
1510 }
1511
1512 #[test]
1513 fn output_interpolation_matches_the_validated_reconstruction() {
1514 let points = [q(0.0, 0.0), q(0.3, 0.0)];
1515 for profile in PROFILES {
1516 let (status, out) = timed(&points, [0.0, 0.0], profile);
1517 assert_eq!(status, CuTimingStatus::Success);
1518 let expected = if profile == CuTrajectoryProfile::SCurve {
1519 CuTrajectoryInterpolation::Quintic
1520 } else {
1521 CuTrajectoryInterpolation::Cubic
1522 };
1523 assert_eq!(out.interpolation, expected, "{profile:?}");
1524 }
1525 }
1526}