1use bincode::Decode;
11use bincode::Encode;
12use cu29::prelude::*;
13use cu29::units::si::f64::Time;
14use cu29::units::si::time::second;
15use repose_kinematics::CuJointAcceleration;
16use repose_kinematics::CuJointJerk;
17use repose_kinematics::CuJointPosition;
18use repose_kinematics::CuJointVelocity;
19use serde::Deserialize;
20use serde::Serialize;
21
22use crate::CuTrajectoryInterpolation;
23use crate::CuTrajectoryPayload;
24use crate::CuTrajectoryPoint;
25use crate::quintic::polynomial_extrema;
26
27#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Encode, Decode, Reflect)]
46#[reflect(no_field_bounds, from_reflect = false)]
47#[serde(deny_unknown_fields)]
48pub struct CuJointTrajectoryLimits<const N: usize> {
49 #[serde(with = "repose_kinematics::serde_arrays::velocity_array")]
50 pub max_velocities: [CuJointVelocity; N],
51 #[serde(with = "repose_kinematics::serde_arrays::acceleration_array")]
52 pub max_accelerations: [CuJointAcceleration; N],
53 #[serde(default, with = "crate::serde_arrays::option_jerk_array")]
55 pub max_jerks: Option<[CuJointJerk; N]>,
56}
57
58impl<const N: usize> Default for CuJointTrajectoryLimits<N> {
59 fn default() -> Self {
62 Self::new(
63 [CuJointVelocity::default(); N],
64 [CuJointAcceleration::default(); N],
65 )
66 }
67}
68
69impl<const N: usize> CuJointTrajectoryLimits<N> {
70 pub const fn new(
73 max_velocities: [CuJointVelocity; N],
74 max_accelerations: [CuJointAcceleration; N],
75 ) -> Self {
76 Self {
77 max_velocities,
78 max_accelerations,
79 max_jerks: None,
80 }
81 }
82
83 pub const fn with_jerks(mut self, max_jerks: [CuJointJerk; N]) -> Self {
87 self.max_jerks = Some(max_jerks);
88 self
89 }
90
91 pub(crate) fn acc(&self, j: usize) -> f64 {
93 self.max_accelerations[j].si_value()
94 }
95
96 pub(crate) fn jerk(&self, j: usize) -> f64 {
98 match self.max_jerks {
99 Some(jerks) => match jerks[j] {
100 CuJointJerk::Fixed => f64::INFINITY,
101 jerk => jerk.si_value(),
102 },
103 None => f64::INFINITY,
104 }
105 }
106
107 pub fn validate(&self) -> CuResult<()> {
112 for j in 0..N {
113 let vel = self.max_velocities[j];
114 let acc = self.max_accelerations[j];
115 let jerk = self.max_jerks.map(|jerks| jerks[j]);
116 let kinds_agree = match vel {
117 CuJointVelocity::Revolute(_) => {
118 matches!(acc, CuJointAcceleration::Revolute(_))
119 && jerk.is_none_or(|k| matches!(k, CuJointJerk::Revolute(_)))
120 }
121 CuJointVelocity::Prismatic(_) => {
122 matches!(acc, CuJointAcceleration::Prismatic(_))
123 && jerk.is_none_or(|k| matches!(k, CuJointJerk::Prismatic(_)))
124 }
125 CuJointVelocity::Fixed => true,
126 };
127 if !kinds_agree {
128 return Err(CuError::from(
129 "Trajectory limit kinds disagree across velocity, acceleration and jerk",
130 ));
131 }
132 if matches!(vel, CuJointVelocity::Fixed) {
133 continue;
134 }
135 let (v, a) = (vel.si_value(), acc.si_value());
136 if !v.is_finite() || v <= 0.0 || !a.is_finite() || a <= 0.0 {
137 return Err(CuError::from(
138 "Trajectory velocity and acceleration limits must be positive and finite",
139 ));
140 }
141 let k = self.jerk(j);
142 if k.is_nan() || k <= 0.0 {
143 return Err(CuError::from("Trajectory jerk limits must be positive"));
144 }
145 }
146 Ok(())
147 }
148}
149
150#[derive(
155 Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, Reflect,
156)]
157#[serde(rename_all = "snake_case")]
158pub enum CuTrajectoryProfile {
159 #[default]
162 Trapezoidal,
163 SCurve,
169 MinimumJerk,
172 Cubic,
175}
176
177fn default_sample_period() -> Time {
178 Time::new::<second>(0.02)
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
198#[reflect(no_field_bounds, from_reflect = false)]
199#[serde(deny_unknown_fields)]
201pub struct CuTimingParams<const N: usize> {
202 pub limits: CuJointTrajectoryLimits<N>,
204 #[serde(default)]
206 pub profile: CuTrajectoryProfile,
207 #[serde(default = "default_sample_period")]
209 pub sample_period: Time,
210}
211
212impl<const N: usize> Default for CuTimingParams<N> {
213 fn default() -> Self {
214 Self {
215 limits: CuJointTrajectoryLimits::default(),
216 profile: CuTrajectoryProfile::default(),
217 sample_period: default_sample_period(),
218 }
219 }
220}
221
222impl<const N: usize> CuTimingParams<N> {
223 pub fn validate(&self) -> CuResult<()> {
225 self.limits.validate()?;
226 let dt = self.sample_period.get::<second>();
227 if !dt.is_finite() || dt <= 0.0 {
228 return Err(CuError::from(
229 "Trajectory sample period must be positive and finite",
230 ));
231 }
232 Ok(())
233 }
234}
235
236#[derive(Default, Debug, Clone, Copy)]
245pub struct CuTimingOverrides<'a, const N: usize> {
246 pub segment_durations: Option<&'a [Time]>,
247 pub waypoint_velocities: Option<&'a [[CuJointVelocity; N]]>,
248}
249
250#[derive(
255 Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Encode, Decode, Reflect,
256)]
257#[serde(rename_all = "snake_case")]
258pub enum CuTimingStatus {
259 Success,
261 #[default]
263 EmptyPath,
264 InvalidInput,
271 CapacityExceeded,
273 Collision { sample: usize },
276 ZoneExceeded { waypoint: usize },
278 NoContinuousMotion { waypoint: usize },
280 UnexpectedStop { sample: usize },
283}
284
285impl CuTimingStatus {
286 pub const fn is_success(&self) -> bool {
288 matches!(self, Self::Success)
289 }
290}
291
292pub fn parameterize<const N: usize, const MAX_POINTS: usize>(
381 waypoints: &[[CuJointPosition; N]],
382 params: &CuTimingParams<N>,
383 overrides: CuTimingOverrides<'_, N>,
384 out: &mut CuTrajectoryPayload<N, MAX_POINTS>,
385) -> CuResult<CuTimingStatus> {
386 out.clear();
387 out.interpolation = CuTrajectoryInterpolation::Cubic;
388 let limits = ¶ms.limits;
389 let profile = params.profile;
390 let timing = &overrides;
391 if waypoints.is_empty() {
392 return Ok(CuTimingStatus::EmptyPath);
393 }
394 let dt = params.sample_period.get::<second>();
395 if !dt.is_finite() || dt <= 0.0 {
396 return Ok(CuTimingStatus::InvalidInput);
397 }
398 validate_waypoints(waypoints)?;
399 if !limits_valid(waypoints[0], limits) {
400 return Ok(CuTimingStatus::InvalidInput);
401 }
402 if profile == CuTrajectoryProfile::SCurve && !jerks_valid(waypoints[0], limits) {
403 return Ok(CuTimingStatus::InvalidInput);
404 }
405 let last = waypoints.len() - 1;
406 if let Some(durations) = timing.segment_durations {
407 if durations.len() != last {
408 return Ok(CuTimingStatus::InvalidInput);
409 }
410 let mut s = 0;
411 while s < last {
412 let span = durations[s].get::<second>();
413 if !span.is_finite() || span <= 0.0 {
414 return Ok(CuTimingStatus::InvalidInput);
415 }
416 s += 1;
417 }
418 }
419 if timing.waypoint_velocities.is_some_and(|vias| {
420 vias.len() != waypoints.len()
421 || !vias_valid(waypoints, vias)
422 || !vias_within_limits(waypoints, vias, limits)
423 }) {
424 return Ok(CuTimingStatus::InvalidInput);
425 }
426 let rest_only = matches!(
427 profile,
428 CuTrajectoryProfile::Trapezoidal | CuTrajectoryProfile::SCurve
429 );
430 let all_zero_via = !vias_nonzero(timing);
431 if rest_only && !all_zero_via {
432 return Ok(CuTimingStatus::InvalidInput);
433 }
434 if waypoints.len() == 1 {
435 let solo = waypoint_velocity(timing, 0, waypoints[0]);
436 return push_single_point(out, Time::default(), waypoints[0], solo);
437 }
438 let mut total = 0.0;
439 let mut s = 0;
440 while s < last {
441 let minimum = segment_minimum(
442 waypoints[s],
443 waypoints[s + 1],
444 limits,
445 profile,
446 all_zero_via,
447 );
448 let duration = match timing.segment_durations {
449 Some(fixed) => {
450 let exact = fixed[s].get::<second>();
451 if rest_only && exact < minimum && minimum - exact > 1e-9 * minimum.max(1.0) {
452 return Ok(CuTimingStatus::InvalidInput);
453 }
454 exact
455 }
456 None => minimum,
457 };
458 total += duration;
459 s += 1;
460 }
461 if total <= 0.0 {
462 let solo = waypoint_velocity(timing, 0, waypoints[0]);
463 return push_single_point(out, Time::default(), waypoints[0], solo);
464 }
465 if matches!(
466 profile,
467 CuTrajectoryProfile::MinimumJerk | CuTrajectoryProfile::Cubic
468 ) && !segments_within_limits(waypoints, limits, profile, timing, all_zero_via)
469 {
470 return Ok(CuTimingStatus::InvalidInput);
471 }
472 if !try_push(
473 out,
474 CuTrajectoryPoint {
475 time_from_start: Time::default(),
476 positions: waypoints[0],
477 velocities: waypoint_velocity(timing, 0, waypoints[0]),
478 accelerations: zero_accelerations(waypoints[0]),
479 },
480 ) {
481 return Ok(CuTimingStatus::CapacityExceeded);
482 }
483 let mut k = 1u64;
484 loop {
485 let t = k as f64 * dt;
486 if t >= total {
487 break;
488 }
489 let at = locate_segment(t, waypoints, limits, profile, timing, all_zero_via);
490 let (positions, velocities) = eval_segment(&at, t, waypoints, limits, profile, timing);
491 if !try_push(
492 out,
493 CuTrajectoryPoint {
494 time_from_start: Time::new::<second>(t),
495 positions,
496 velocities,
497 accelerations: zero_accelerations(waypoints[0]),
498 },
499 ) {
500 return Ok(CuTimingStatus::CapacityExceeded);
501 }
502 k += 1;
503 }
504 if !try_push(
505 out,
506 CuTrajectoryPoint {
507 time_from_start: Time::new::<second>(total),
508 positions: waypoints[last],
509 velocities: waypoint_velocity(timing, last, waypoints[last]),
510 accelerations: zero_accelerations(waypoints[0]),
511 },
512 ) {
513 return Ok(CuTimingStatus::CapacityExceeded);
514 }
515 Ok(CuTimingStatus::Success)
516}
517
518fn validate_waypoints<const N: usize>(waypoints: &[[CuJointPosition; N]]) -> CuResult<()> {
520 let mut i = 0;
521 while i < waypoints.len() {
522 let mut j = 0;
523 while j < N {
524 if core::mem::discriminant(&waypoints[i][j])
525 != core::mem::discriminant(&waypoints[0][j])
526 || !waypoints[i][j].si_value().is_finite()
527 {
528 return Err(CuError::from(
529 "Waypoint joint kinds must match and be finite",
530 ));
531 }
532 j += 1;
533 }
534 i += 1;
535 }
536 Ok(())
537}
538
539fn limits_valid<const N: usize>(
541 reference: [CuJointPosition; N],
542 limits: &CuJointTrajectoryLimits<N>,
543) -> bool {
544 let mut j = 0;
545 while j < N {
546 let acc = limits.acc(j);
547 if !acc.is_finite() {
548 return false;
549 }
550 let vel = limits.max_velocities[j];
551 if core::mem::discriminant(&vel)
552 != core::mem::discriminant(&coordinate_velocity_kind(reference[j]))
553 || core::mem::discriminant(&limits.max_accelerations[j])
554 != core::mem::discriminant(&CuJointAcceleration::from_coordinate(reference[j], 0.0))
555 {
556 return false;
557 }
558 match reference[j] {
559 CuJointPosition::Fixed => {
560 if acc < 0.0 {
561 return false;
562 }
563 }
564 _ => {
565 if !vel.si_value().is_finite() || vel.si_value() <= 0.0 || acc <= 0.0 {
566 return false;
567 }
568 }
569 }
570 j += 1;
571 }
572 true
573}
574
575fn jerks_valid<const N: usize>(
582 reference: [CuJointPosition; N],
583 limits: &CuJointTrajectoryLimits<N>,
584) -> bool {
585 let mut j = 0;
586 while j < N {
587 if matches!(reference[j], CuJointPosition::Fixed) {
588 j += 1;
589 continue;
590 }
591 if let Some(jerks) = limits.max_jerks
592 && core::mem::discriminant(&jerks[j])
593 != core::mem::discriminant(&CuJointJerk::from_coordinate(reference[j], 0.0))
594 {
595 return false;
596 }
597 let jerk = limits.jerk(j);
598 if jerk <= 0.0 || jerk.is_nan() {
599 return false;
600 }
601 j += 1;
602 }
603 true
604}
605
606fn vias_valid<const N: usize>(
608 waypoints: &[[CuJointPosition; N]],
609 vias: &[[CuJointVelocity; N]],
610) -> bool {
611 let mut i = 0;
612 while i < waypoints.len() {
613 let mut j = 0;
614 while j < N {
615 if core::mem::discriminant(&vias[i][j])
616 != core::mem::discriminant(&coordinate_velocity_kind(waypoints[i][j]))
617 || !vias[i][j].si_value().is_finite()
618 {
619 return false;
620 }
621 j += 1;
622 }
623 i += 1;
624 }
625 true
626}
627
628fn vias_nonzero<const N: usize>(timing: &CuTimingOverrides<'_, N>) -> bool {
630 match timing.waypoint_velocities {
631 Some(vias) => {
632 let mut i = 0;
633 while i < vias.len() {
634 let mut j = 0;
635 while j < N {
636 if vias[i][j].si_value() != 0.0 {
637 return true;
638 }
639 j += 1;
640 }
641 i += 1;
642 }
643 false
644 }
645 None => false,
646 }
647}
648
649fn waypoint_velocity<const N: usize>(
651 timing: &CuTimingOverrides<'_, N>,
652 index: usize,
653 reference: [CuJointPosition; N],
654) -> [CuJointVelocity; N] {
655 match timing.waypoint_velocities {
656 Some(vias) => vias[index],
657 None => core::array::from_fn(|j| CuJointVelocity::from_coordinate(reference[j], 0.0)),
658 }
659}
660
661fn zero_accelerations<const N: usize>(reference: [CuJointPosition; N]) -> [CuJointAcceleration; N] {
663 reference.map(|q| CuJointAcceleration::from_coordinate(q, 0.0))
664}
665
666fn coordinate_velocity_kind(q: CuJointPosition) -> CuJointVelocity {
668 CuJointVelocity::from_coordinate(q, 0.0)
669}
670
671fn joint_profile_duration(distance: f64, max_vel: f64, max_acc: f64) -> f64 {
673 if distance <= 0.0 {
674 return 0.0;
675 }
676 let accel_time = max_vel / max_acc;
677 if distance >= max_vel * accel_time {
678 distance / max_vel + accel_time
679 } else {
680 2.0 * libm::sqrt(distance / max_acc)
681 }
682}
683
684fn scurve_joint_duration(distance: f64, max_vel: f64, max_acc: f64, max_jerk: f64) -> f64 {
689 if distance <= 0.0 {
690 return 0.0;
691 }
692 if !max_jerk.is_finite() {
693 return joint_profile_duration(distance, max_vel, max_acc);
694 }
695 let (jerk_time, accel_time, cruise_time) = scurve_shape(distance, max_vel, max_acc, max_jerk);
696 4.0 * jerk_time + 2.0 * accel_time + cruise_time
697}
698
699fn scurve_shape(distance: f64, max_vel: f64, max_acc: f64, max_jerk: f64) -> (f64, f64, f64) {
708 let jerk_full = max_acc / max_jerk;
709 let accel_full = max_vel / max_acc - jerk_full;
710 if accel_full >= 0.0 {
711 let reach = max_vel * (accel_full + 2.0 * jerk_full);
712 if distance >= reach {
713 return (jerk_full, accel_full, (distance - reach) / max_vel);
714 }
715 let discriminant = jerk_full * jerk_full + 4.0 * distance / max_acc;
716 let accel = 0.5 * (libm::sqrt(discriminant) - 3.0 * jerk_full);
717 if accel >= 0.0 {
718 return (jerk_full, accel, 0.0);
719 }
720 return (libm::cbrt(distance / (2.0 * max_jerk)), 0.0, 0.0);
721 }
722 let jerk = libm::sqrt(max_vel / max_jerk);
723 if distance >= 2.0 * max_vel * jerk {
724 return (jerk, 0.0, distance / max_vel - 2.0 * jerk);
725 }
726 (libm::cbrt(distance / (2.0 * max_jerk)), 0.0, 0.0)
727}
728
729fn trapezoid_eval(offset: f64, distance: f64, max_vel: f64, max_acc: f64) -> (f64, f64) {
734 if distance <= 0.0 || offset <= 0.0 {
735 return (0.0, 0.0);
736 }
737 let accel_time = max_vel / max_acc;
738 if distance >= max_vel * accel_time {
739 let cruise = distance / max_vel - accel_time;
740 let total = distance / max_vel + accel_time;
741 let accel_distance = 0.5 * max_vel * accel_time;
742 if offset < accel_time {
743 (0.5 * max_acc * offset * offset, max_acc * offset)
744 } else if offset < accel_time + cruise {
745 (accel_distance + max_vel * (offset - accel_time), max_vel)
746 } else if offset < total {
747 let remaining = total - offset;
748 (
749 distance - 0.5 * max_acc * remaining * remaining,
750 max_acc * remaining,
751 )
752 } else {
753 (distance, 0.0)
754 }
755 } else {
756 let half = libm::sqrt(distance / max_acc);
757 let total = 2.0 * half;
758 if offset < half {
759 (0.5 * max_acc * offset * offset, max_acc * offset)
760 } else if offset < total {
761 let remaining = total - offset;
762 (
763 distance - 0.5 * max_acc * remaining * remaining,
764 max_acc * remaining,
765 )
766 } else {
767 (distance, 0.0)
768 }
769 }
770}
771
772fn scurve_eval(
777 offset: f64,
778 distance: f64,
779 max_vel: f64,
780 max_acc: f64,
781 max_jerk: f64,
782) -> (f64, f64) {
783 if distance <= 0.0 || offset <= 0.0 {
784 return (0.0, 0.0);
785 }
786 let (jerk_time, accel_time, cruise_time) = scurve_shape(distance, max_vel, max_acc, max_jerk);
787 let total = 4.0 * jerk_time + 2.0 * accel_time + cruise_time;
788 if offset >= total {
789 return (distance, 0.0);
790 }
791 let mut remaining = offset;
792 let mut position = 0.0;
793 let mut velocity = 0.0;
794 let mut acceleration = 0.0;
795 let mut phase = 0;
796 while phase < 7 {
797 let (jerk, duration) = match phase {
798 0 => (max_jerk, jerk_time),
799 1 => (0.0, accel_time),
800 2 => (-max_jerk, jerk_time),
801 3 => (0.0, cruise_time),
802 4 => (-max_jerk, jerk_time),
803 5 => (0.0, accel_time),
804 _ => (max_jerk, jerk_time),
805 };
806 let step = if remaining < duration {
807 remaining
808 } else {
809 duration
810 };
811 position +=
812 velocity * step + 0.5 * acceleration * step * step + jerk * step * step * step / 6.0;
813 velocity += acceleration * step + 0.5 * jerk * step * step;
814 acceleration += jerk * step;
815 remaining -= step;
816 if remaining <= 0.0 {
817 break;
818 }
819 phase += 1;
820 }
821 (position, velocity)
822}
823
824struct JointPrimitive {
826 distance: f64,
827 max_vel: f64,
828 max_acc: f64,
829 max_jerk: f64,
830}
831
832fn stretched_eval(
839 tau: f64,
840 effective: f64,
841 own: f64,
842 prim: &JointPrimitive,
843 jerk_limited: bool,
844) -> (f64, f64) {
845 if effective <= 0.0 || own <= 0.0 {
846 return (0.0, 0.0);
847 }
848 let scale = own / effective;
849 let (offset, vel) = if jerk_limited && prim.max_jerk.is_finite() {
850 scurve_eval(
851 tau * scale,
852 prim.distance,
853 prim.max_vel,
854 prim.max_acc,
855 prim.max_jerk,
856 )
857 } else {
858 trapezoid_eval(tau * scale, prim.distance, prim.max_vel, prim.max_acc)
859 };
860 (offset, vel * scale)
861}
862
863pub(crate) fn quintic_eval(
871 u: f64,
872 q0: f64,
873 q1: f64,
874 v0: f64,
875 v1: f64,
876 duration: f64,
877) -> (f64, f64) {
878 if duration <= 0.0 {
879 return (q0, v0);
880 }
881 let distance = q1 - q0;
882 let start_term = distance - v0 * duration;
883 let end_term = (v1 - v0) * duration;
884 let a3 = 10.0 * start_term - 4.0 * end_term;
885 let a4 = -15.0 * start_term + 7.0 * end_term;
886 let a5 = 6.0 * start_term - 3.0 * end_term;
887 let u2 = u * u;
888 let u3 = u2 * u;
889 let u4 = u3 * u;
890 let position = q0 + v0 * duration * u + a3 * u3 + a4 * u4 + a5 * u4 * u;
891 let velocity = v0 + (3.0 * a3 * u2 + 4.0 * a4 * u3 + 5.0 * a5 * u4) / duration;
892 (position, velocity)
893}
894
895pub(crate) fn cubic_eval(u: f64, q0: f64, q1: f64, v0: f64, v1: f64, duration: f64) -> (f64, f64) {
902 if duration <= 0.0 {
903 return (q0, v0);
904 }
905 let u2 = u * u;
906 let u3 = u2 * u;
907 let h00 = 2.0 * u3 - 3.0 * u2 + 1.0;
908 let h10 = u3 - 2.0 * u2 + u;
909 let h01 = -2.0 * u3 + 3.0 * u2;
910 let h11 = u3 - u2;
911 let position = h00 * q0 + h10 * duration * v0 + h01 * q1 + h11 * duration * v1;
912 let dh00 = 6.0 * u2 - 6.0 * u;
913 let dh10 = 3.0 * u2 - 4.0 * u + 1.0;
914 let dh01 = -6.0 * u2 + 6.0 * u;
915 let dh11 = 3.0 * u2 - 2.0 * u;
916 let velocity = (dh00 * q0 + dh10 * duration * v0 + dh01 * q1 + dh11 * duration * v1) / duration;
917 (position, velocity)
918}
919
920fn segment_minimum<const N: usize>(
927 from: [CuJointPosition; N],
928 to: [CuJointPosition; N],
929 limits: &CuJointTrajectoryLimits<N>,
930 profile: CuTrajectoryProfile,
931 all_zero_via: bool,
932) -> f64 {
933 match profile {
934 CuTrajectoryProfile::MinimumJerk if all_zero_via => {
935 minjerk_segment_duration(&from, &to, limits)
936 }
937 CuTrajectoryProfile::MinimumJerk | CuTrajectoryProfile::Cubic => {
938 segment_duration(&from, &to, limits)
939 }
940 CuTrajectoryProfile::Trapezoidal => segment_duration(&from, &to, limits),
941 CuTrajectoryProfile::SCurve => scurve_segment_duration(&from, &to, limits),
942 }
943}
944
945fn segment_duration<const N: usize>(
947 from: &[CuJointPosition; N],
948 to: &[CuJointPosition; N],
949 limits: &CuJointTrajectoryLimits<N>,
950) -> f64 {
951 let mut duration = 0.0;
952 let mut j = 0;
953 while j < N {
954 let distance = (to[j].si_value() - from[j].si_value()).abs();
955 let leg =
956 joint_profile_duration(distance, limits.max_velocities[j].si_value(), limits.acc(j));
957 if leg > duration {
958 duration = leg;
959 }
960 j += 1;
961 }
962 duration
963}
964
965fn scurve_segment_duration<const N: usize>(
967 from: &[CuJointPosition; N],
968 to: &[CuJointPosition; N],
969 limits: &CuJointTrajectoryLimits<N>,
970) -> f64 {
971 let mut duration = 0.0;
972 let mut j = 0;
973 while j < N {
974 let distance = (to[j].si_value() - from[j].si_value()).abs();
975 let leg = scurve_joint_duration(
976 distance,
977 limits.max_velocities[j].si_value(),
978 limits.acc(j),
979 limits.jerk(j),
980 );
981 if leg > duration {
982 duration = leg;
983 }
984 j += 1;
985 }
986 duration
987}
988
989fn minjerk_segment_duration<const N: usize>(
995 from: &[CuJointPosition; N],
996 to: &[CuJointPosition; N],
997 limits: &CuJointTrajectoryLimits<N>,
998) -> f64 {
999 let mut duration = 0.0;
1000 let mut j = 0;
1001 while j < N {
1002 let distance = (to[j].si_value() - from[j].si_value()).abs();
1003 if distance > 0.0 {
1004 let vel = limits.max_velocities[j].si_value();
1005 let acc = limits.acc(j);
1006 let vel_time = 1.875 * distance / vel;
1007 let acc_time = libm::sqrt((10.0 / libm::sqrt(3.0)) * distance / acc);
1008 if vel_time > duration {
1009 duration = vel_time;
1010 }
1011 if acc_time > duration {
1012 duration = acc_time;
1013 }
1014 }
1015 j += 1;
1016 }
1017 duration
1018}
1019
1020fn effective_duration<const N: usize>(
1022 seg: usize,
1023 waypoints: &[[CuJointPosition; N]],
1024 limits: &CuJointTrajectoryLimits<N>,
1025 profile: CuTrajectoryProfile,
1026 timing: &CuTimingOverrides<'_, N>,
1027 all_zero_via: bool,
1028) -> f64 {
1029 match timing.segment_durations {
1030 Some(fixed) => fixed[seg].get::<second>(),
1031 None => segment_minimum(
1032 waypoints[seg],
1033 waypoints[seg + 1],
1034 limits,
1035 profile,
1036 all_zero_via,
1037 ),
1038 }
1039}
1040
1041fn locate_segment<const N: usize>(
1043 t: f64,
1044 waypoints: &[[CuJointPosition; N]],
1045 limits: &CuJointTrajectoryLimits<N>,
1046 profile: CuTrajectoryProfile,
1047 timing: &CuTimingOverrides<'_, N>,
1048 all_zero_via: bool,
1049) -> SegmentAt {
1050 let mut seg = 0;
1051 let mut seg_start = 0.0;
1052 while seg + 2 < waypoints.len() {
1053 let duration = effective_duration(seg, waypoints, limits, profile, timing, all_zero_via);
1054 if t < seg_start + duration {
1055 break;
1056 }
1057 seg_start += duration;
1058 seg += 1;
1059 }
1060 let seg_duration = effective_duration(seg, waypoints, limits, profile, timing, all_zero_via);
1061 SegmentAt {
1062 seg,
1063 start: seg_start,
1064 duration: seg_duration,
1065 }
1066}
1067
1068struct SegmentAt {
1070 seg: usize,
1071 start: f64,
1072 duration: f64,
1073}
1074
1075fn eval_segment<const N: usize>(
1081 at: &SegmentAt,
1082 t: f64,
1083 waypoints: &[[CuJointPosition; N]],
1084 limits: &CuJointTrajectoryLimits<N>,
1085 profile: CuTrajectoryProfile,
1086 timing: &CuTimingOverrides<'_, N>,
1087) -> ([CuJointPosition; N], [CuJointVelocity; N]) {
1088 let from = waypoints[at.seg];
1089 let to = waypoints[at.seg + 1];
1090 let start_vel = waypoint_velocity(timing, at.seg, from);
1091 let end_vel = waypoint_velocity(timing, at.seg + 1, to);
1092 let tau = (t - at.start).clamp(0.0, at.duration);
1093 let mut positions = [CuJointPosition::Fixed; N];
1094 let mut velocities = [CuJointVelocity::Fixed; N];
1095 let mut j = 0;
1096 while j < N {
1097 let start = from[j].si_value();
1098 let displacement = to[j].si_value() - start;
1099 let prim = JointPrimitive {
1100 distance: displacement.abs(),
1101 max_vel: limits.max_velocities[j].si_value(),
1102 max_acc: limits.acc(j),
1103 max_jerk: limits.jerk(j),
1104 };
1105 match profile {
1106 CuTrajectoryProfile::Trapezoidal | CuTrajectoryProfile::SCurve => {
1107 let sign = if displacement >= 0.0 { 1.0 } else { -1.0 };
1108 let jerk_limited = profile == CuTrajectoryProfile::SCurve;
1109 let own = if jerk_limited {
1110 scurve_joint_duration(prim.distance, prim.max_vel, prim.max_acc, prim.max_jerk)
1111 } else {
1112 joint_profile_duration(prim.distance, prim.max_vel, prim.max_acc)
1113 };
1114 let (offset, vel) = stretched_eval(tau, at.duration, own, &prim, jerk_limited);
1115 positions[j] = from[j].with_si_value(start + sign * offset);
1116 velocities[j] = CuJointVelocity::from_coordinate(from[j], sign * vel);
1117 }
1118 CuTrajectoryProfile::MinimumJerk | CuTrajectoryProfile::Cubic => {
1119 let u = if at.duration > 0.0 {
1120 (tau / at.duration).clamp(0.0, 1.0)
1121 } else {
1122 0.0
1123 };
1124 let (position, vel) = if profile == CuTrajectoryProfile::MinimumJerk {
1125 quintic_eval(
1126 u,
1127 start,
1128 start + displacement,
1129 start_vel[j].si_value(),
1130 end_vel[j].si_value(),
1131 at.duration,
1132 )
1133 } else {
1134 cubic_eval(
1135 u,
1136 start,
1137 start + displacement,
1138 start_vel[j].si_value(),
1139 end_vel[j].si_value(),
1140 at.duration,
1141 )
1142 };
1143 positions[j] = from[j].with_si_value(position);
1144 velocities[j] = CuJointVelocity::from_coordinate(from[j], vel);
1145 }
1146 }
1147 j += 1;
1148 }
1149 (positions, velocities)
1150}
1151
1152fn vias_within_limits<const N: usize>(
1154 waypoints: &[[CuJointPosition; N]],
1155 vias: &[[CuJointVelocity; N]],
1156 limits: &CuJointTrajectoryLimits<N>,
1157) -> bool {
1158 let mut i = 0;
1159 while i < vias.len() {
1160 let mut j = 0;
1161 while j < N {
1162 if matches!(waypoints[i][j], CuJointPosition::Fixed) {
1163 j += 1;
1164 continue;
1165 }
1166 let bound = limits.max_velocities[j].si_value() * (1.0 + 1e-9) + 1e-12;
1167 if vias[i][j].si_value().abs() > bound {
1168 return false;
1169 }
1170 j += 1;
1171 }
1172 i += 1;
1173 }
1174 true
1175}
1176
1177fn segments_within_limits<const N: usize>(
1185 waypoints: &[[CuJointPosition; N]],
1186 limits: &CuJointTrajectoryLimits<N>,
1187 profile: CuTrajectoryProfile,
1188 timing: &CuTimingOverrides<'_, N>,
1189 all_zero_via: bool,
1190) -> bool {
1191 let quintic = profile == CuTrajectoryProfile::MinimumJerk;
1192 let mut seg = 0;
1193 while seg + 1 < waypoints.len() {
1194 let duration = effective_duration(seg, waypoints, limits, profile, timing, all_zero_via);
1195 if duration <= 0.0 {
1196 seg += 1;
1197 continue;
1198 }
1199 let start_vel = waypoint_velocity(timing, seg, waypoints[seg]);
1200 let end_vel = waypoint_velocity(timing, seg + 1, waypoints[seg + 1]);
1201 let mut j = 0;
1202 while j < N {
1203 if matches!(waypoints[seg][j], CuJointPosition::Fixed) {
1204 j += 1;
1205 continue;
1206 }
1207 let distance = waypoints[seg + 1][j].si_value() - waypoints[seg][j].si_value();
1208 let (high, low, _) = polynomial_extrema(
1209 quintic,
1210 distance,
1211 start_vel[j].si_value(),
1212 end_vel[j].si_value(),
1213 duration,
1214 );
1215 let bound = limits.max_velocities[j].si_value() * (1.0 + 1e-9) + 1e-12;
1216 if !high.is_finite() || !low.is_finite() || high > bound || low < -bound {
1217 return false;
1218 }
1219 j += 1;
1220 }
1221 seg += 1;
1222 }
1223 true
1224}
1225
1226fn push_single_point<const N: usize, const MAX_POINTS: usize>(
1228 out: &mut CuTrajectoryPayload<N, MAX_POINTS>,
1229 time_from_start: Time,
1230 q: [CuJointPosition; N],
1231 velocities: [CuJointVelocity; N],
1232) -> CuResult<CuTimingStatus> {
1233 match out.push(CuTrajectoryPoint {
1234 time_from_start,
1235 positions: q,
1236 velocities,
1237 accelerations: zero_accelerations(q),
1238 }) {
1239 Ok(()) => Ok(CuTimingStatus::Success),
1240 Err(_) => Ok(CuTimingStatus::CapacityExceeded),
1241 }
1242}
1243
1244fn try_push<const N: usize, const MAX_POINTS: usize>(
1246 out: &mut CuTrajectoryPayload<N, MAX_POINTS>,
1247 point: CuTrajectoryPoint<N>,
1248) -> bool {
1249 out.push(point).is_ok()
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use super::*;
1255 use cu29::units::si::angle::radian;
1256 use cu29::units::si::angular_velocity::radian_per_second;
1257 use cu29::units::si::f64::Angle;
1258 use cu29::units::si::f64::AngularVelocity;
1259
1260 fn rev(q: f64) -> CuJointPosition {
1261 CuJointPosition::Revolute(Angle::new::<radian>(q))
1262 }
1263
1264 fn rev_acc(a: f64) -> CuJointAcceleration {
1265 CuJointAcceleration::from_coordinate(rev(0.0), a)
1266 }
1267
1268 fn rev_limits(v: f64, a: f64) -> CuJointTrajectoryLimits<1> {
1269 CuJointTrajectoryLimits::new(
1270 [CuJointVelocity::Revolute(AngularVelocity::new::<
1271 radian_per_second,
1272 >(v))],
1273 [rev_acc(a)],
1274 )
1275 }
1276
1277 fn rev_limits_jerk(v: f64, a: f64, j: f64) -> CuJointTrajectoryLimits<1> {
1278 rev_limits(v, a).with_jerks([CuJointJerk::from_coordinate(rev(0.0), j)])
1279 }
1280
1281 fn trapezoid<const N: usize, const M: usize>(
1283 waypoints: &[[CuJointPosition; N]],
1284 limits: &CuJointTrajectoryLimits<N>,
1285 sample_period: Time,
1286 out: &mut CuTrajectoryPayload<N, M>,
1287 ) -> CuResult<CuTimingStatus> {
1288 let params = CuTimingParams {
1289 limits: *limits,
1290 sample_period,
1291 ..Default::default()
1292 };
1293 parameterize(waypoints, ¶ms, CuTimingOverrides::default(), out)
1294 }
1295
1296 fn with_profile<const N: usize, const M: usize>(
1297 waypoints: &[[CuJointPosition; N]],
1298 limits: &CuJointTrajectoryLimits<N>,
1299 profile: CuTrajectoryProfile,
1300 overrides: &CuTimingOverrides<'_, N>,
1301 sample_period: Time,
1302 out: &mut CuTrajectoryPayload<N, M>,
1303 ) -> CuResult<CuTimingStatus> {
1304 let params = CuTimingParams {
1305 limits: *limits,
1306 profile,
1307 sample_period,
1308 };
1309 parameterize(waypoints, ¶ms, *overrides, out)
1310 }
1311
1312 fn zero_vel() -> CuJointVelocity {
1313 CuJointVelocity::Revolute(AngularVelocity::new::<radian_per_second>(0.0))
1314 }
1315
1316 fn rev_vel(v: f64) -> CuJointVelocity {
1317 CuJointVelocity::Revolute(AngularVelocity::new::<radian_per_second>(v))
1318 }
1319
1320 fn max_sampled_velocity<const M: usize>(traj: &CuTrajectoryPayload<1, M>) -> (f64, f64) {
1321 let mut lo = f64::INFINITY;
1322 let mut hi = f64::NEG_INFINITY;
1323 let mut i = 0;
1324 while i < traj.len() {
1325 let v = traj.points()[i].velocities[0].si_value();
1326 lo = lo.min(v);
1327 hi = hi.max(v);
1328 i += 1;
1329 }
1330 (lo, hi)
1331 }
1332
1333 fn assert_strictly_increasing<const M: usize>(traj: &CuTrajectoryPayload<1, M>) {
1334 let mut i = 1;
1335 while i < traj.len() {
1336 assert!(
1337 traj.points()[i].time_from_start > traj.points()[i - 1].time_from_start,
1338 "times must be strictly increasing"
1339 );
1340 i += 1;
1341 }
1342 }
1343
1344 #[test]
1345 fn test_empty_path_yields_empty_status() {
1346 let mut out = CuTrajectoryPayload::<1>::new();
1347 let status = trapezoid(
1348 &[],
1349 &rev_limits(1.0, 1.0),
1350 Time::new::<second>(0.01),
1351 &mut out,
1352 )
1353 .unwrap();
1354 assert_eq!(status, CuTimingStatus::EmptyPath);
1355 assert!(out.is_empty());
1356 }
1357
1358 #[test]
1359 fn test_single_waypoint_is_zero_velocity_point() {
1360 let mut out = CuTrajectoryPayload::<1>::new();
1361 let status = trapezoid(
1362 &[[rev(0.5)]],
1363 &rev_limits(1.0, 1.0),
1364 Time::new::<second>(0.01),
1365 &mut out,
1366 )
1367 .unwrap();
1368 assert_eq!(status, CuTimingStatus::Success);
1369 assert_eq!(out.len(), 1);
1370 assert_eq!(out.points()[0].time_from_start.get::<second>(), 0.0);
1371 assert_eq!(out.points()[0].positions[0].si_value(), 0.5);
1372 assert_eq!(out.points()[0].velocities[0].si_value(), 0.0);
1373 }
1374
1375 #[test]
1376 fn test_trapezoidal_duration_and_velocity_limits() {
1377 let mut out = CuTrajectoryPayload::<1, 512>::new();
1378 let status = trapezoid(
1379 &[[rev(0.0)], [rev(1.0)]],
1380 &rev_limits(0.5, 1.0),
1381 Time::new::<second>(0.02),
1382 &mut out,
1383 )
1384 .unwrap();
1385 assert_eq!(status, CuTimingStatus::Success);
1386 assert!((out.duration().get::<second>() - 2.5).abs() < 1e-9);
1388 let (lo, hi) = max_sampled_velocity(&out);
1389 assert!(hi <= 0.5 + 1e-9, "velocity limit exceeded: {hi}");
1390 assert!(lo >= -1e-9, "unexpected reverse motion: {lo}");
1391 assert_eq!(out.points()[0].velocities[0].si_value(), 0.0);
1392 let last = out.len() - 1;
1393 assert_eq!(out.points()[last].positions[0].si_value(), 1.0);
1394 assert_eq!(out.points()[last].velocities[0].si_value(), 0.0);
1395 }
1396
1397 #[test]
1398 fn test_triangular_duration_matches_formula() {
1399 let mut out = CuTrajectoryPayload::<1, 256>::new();
1400 let status = trapezoid(
1401 &[[rev(0.0)], [rev(0.1)]],
1402 &rev_limits(1.0, 1.0),
1403 Time::new::<second>(0.005),
1404 &mut out,
1405 )
1406 .unwrap();
1407 assert_eq!(status, CuTimingStatus::Success);
1408 let expected = 2.0 * (0.1f64).sqrt();
1410 assert!((out.duration().get::<second>() - expected).abs() < 1e-9);
1411 let (_, peak) = max_sampled_velocity(&out);
1412 assert!((peak - (0.1f64).sqrt()).abs() < 0.02);
1413 }
1414
1415 #[test]
1416 fn test_slow_joint_dominates_synchronized_duration() {
1417 let slow = CuJointTrajectoryLimits::new(
1418 [
1419 CuJointVelocity::Revolute(AngularVelocity::new::<radian_per_second>(2.0)),
1420 CuJointVelocity::Revolute(AngularVelocity::new::<radian_per_second>(0.25)),
1421 ],
1422 [rev_acc(2.0), rev_acc(0.5)],
1423 );
1424 let mut out = CuTrajectoryPayload::<2, 512>::new();
1425 let status = trapezoid(
1426 &[[rev(0.0), rev(0.0)], [rev(0.1), rev(1.0)]],
1427 &slow,
1428 Time::new::<second>(0.05),
1429 &mut out,
1430 )
1431 .unwrap();
1432 assert_eq!(status, CuTimingStatus::Success);
1433 assert!((out.duration().get::<second>() - 4.5).abs() < 1e-9);
1435 let last = out.len() - 1;
1436 assert!((out.points()[last].positions[0].si_value() - 0.1).abs() < 1e-9);
1437 assert!((out.points()[last].positions[1].si_value() - 1.0).abs() < 1e-9);
1438 }
1439
1440 #[test]
1441 fn test_capacity_exceeded_status() {
1442 let mut out = CuTrajectoryPayload::<1, 3>::new();
1443 let status = trapezoid(
1444 &[[rev(0.0)], [rev(1.0)]],
1445 &rev_limits(0.5, 1.0),
1446 Time::new::<second>(0.01),
1447 &mut out,
1448 )
1449 .unwrap();
1450 assert_eq!(status, CuTimingStatus::CapacityExceeded);
1451 assert_eq!(out.len(), 3);
1452 }
1453
1454 #[test]
1455 fn test_invalid_limits_rejected() {
1456 let wps = [[rev(0.0)], [rev(1.0)]];
1457 let mut out = CuTrajectoryPayload::<1>::new();
1458 let zero_vel = CuJointTrajectoryLimits::new(
1459 [CuJointVelocity::Revolute(AngularVelocity::new::<
1460 radian_per_second,
1461 >(0.0))],
1462 [rev_acc(1.0)],
1463 );
1464 let status = trapezoid(&wps, &zero_vel, Time::new::<second>(0.01), &mut out).unwrap();
1465 assert_eq!(status, CuTimingStatus::InvalidInput);
1466 let neg_acc = rev_limits(1.0, -2.0);
1467 let status = trapezoid(&wps, &neg_acc, Time::new::<second>(0.01), &mut out).unwrap();
1468 assert_eq!(status, CuTimingStatus::InvalidInput);
1469 let status = trapezoid(
1470 &wps,
1471 &rev_limits(1.0, 1.0),
1472 Time::new::<second>(0.0),
1473 &mut out,
1474 )
1475 .unwrap();
1476 assert_eq!(status, CuTimingStatus::InvalidInput);
1477 }
1478
1479 #[test]
1480 fn test_profile_default_is_trapezoidal() {
1481 assert_eq!(
1482 CuTrajectoryProfile::default(),
1483 CuTrajectoryProfile::Trapezoidal
1484 );
1485 }
1486
1487 #[test]
1488 fn test_default_profile_matches_legacy_entry_point() {
1489 let wps = [[rev(0.0)], [rev(0.5)], [rev(1.0)]];
1490 let limits = rev_limits(0.5, 1.0);
1491 let dt = Time::new::<second>(0.01);
1492 let mut legacy = CuTrajectoryPayload::<1, 512>::new();
1493 let mut profiled = CuTrajectoryPayload::<1, 512>::new();
1494 let legacy_status = trapezoid(&wps, &limits, dt, &mut legacy).unwrap();
1495 let profiled_status = with_profile(
1496 &wps,
1497 &limits,
1498 CuTrajectoryProfile::Trapezoidal,
1499 &CuTimingOverrides::default(),
1500 dt,
1501 &mut profiled,
1502 )
1503 .unwrap();
1504 assert_eq!(legacy_status, profiled_status);
1505 assert_eq!(legacy.points(), profiled.points());
1506 }
1507
1508 #[test]
1509 fn test_cubic_honors_via_velocity() {
1510 let wps = [[rev(0.0)], [rev(1.0)], [rev(2.0)]];
1511 let limits = rev_limits(2.0, 5.0);
1512 let vias = [[zero_vel()], [rev_vel(0.8)], [zero_vel()]];
1513 let fixed = [Time::new::<second>(1.0), Time::new::<second>(1.0)];
1514 let timing = CuTimingOverrides {
1515 segment_durations: Some(fixed.as_slice()),
1516 waypoint_velocities: Some(vias.as_slice()),
1517 };
1518 let mut out = CuTrajectoryPayload::<1, 512>::new();
1519 let status = with_profile(
1520 &wps,
1521 &limits,
1522 CuTrajectoryProfile::Cubic,
1523 &timing,
1524 Time::new::<second>(0.01),
1525 &mut out,
1526 )
1527 .unwrap();
1528 assert_eq!(status, CuTimingStatus::Success);
1529 assert!((out.duration().get::<second>() - 2.0).abs() < 1e-9);
1530 let mid = out
1531 .points()
1532 .iter()
1533 .find(|p| (p.time_from_start.get::<second>() - 1.0).abs() < 1e-9)
1534 .expect("resampled interior waypoint");
1535 assert!((mid.positions[0].si_value() - 1.0).abs() < 1e-9);
1536 assert!((mid.velocities[0].si_value() - 0.8).abs() < 1e-9);
1537 assert_eq!(out.points()[0].velocities[0].si_value(), 0.0);
1538 let last = out.len() - 1;
1539 assert_eq!(out.points()[last].positions[0].si_value(), 2.0);
1540 assert_eq!(out.points()[last].velocities[0].si_value(), 0.0);
1541 assert_strictly_increasing(&out);
1542 }
1543
1544 #[test]
1545 fn test_minimum_jerk_honors_via_and_zero_end_acceleration() {
1546 let wps = [[rev(0.0)], [rev(1.0)], [rev(2.0)]];
1547 let limits = rev_limits(2.0, 5.0);
1548 let vias = [[zero_vel()], [rev_vel(0.8)], [zero_vel()]];
1549 let fixed = [Time::new::<second>(1.0), Time::new::<second>(1.0)];
1550 let timing = CuTimingOverrides {
1551 segment_durations: Some(fixed.as_slice()),
1552 waypoint_velocities: Some(vias.as_slice()),
1553 };
1554 let mut out = CuTrajectoryPayload::<1, 2048>::new();
1555 let status = with_profile(
1556 &wps,
1557 &limits,
1558 CuTrajectoryProfile::MinimumJerk,
1559 &timing,
1560 Time::new::<second>(0.002),
1561 &mut out,
1562 )
1563 .unwrap();
1564 assert_eq!(status, CuTimingStatus::Success);
1565 assert!((out.duration().get::<second>() - 2.0).abs() < 1e-9);
1566 let mid = out
1567 .points()
1568 .iter()
1569 .find(|p| (p.time_from_start.get::<second>() - 1.0).abs() < 1e-9)
1570 .expect("resampled interior waypoint");
1571 assert!((mid.velocities[0].si_value() - 0.8).abs() < 1e-9);
1572 let dt = 0.002;
1575 let accel_start = (out.points()[1].velocities[0].si_value()
1576 - out.points()[0].velocities[0].si_value())
1577 / dt;
1578 assert!(accel_start.abs() < 0.5, "start accel: {accel_start}");
1579 let last = out.len() - 1;
1580 let accel_end = (out.points()[last].velocities[0].si_value()
1581 - out.points()[last - 1].velocities[0].si_value())
1582 / dt;
1583 assert!(accel_end.abs() < 0.5, "end accel: {accel_end}");
1584 assert_strictly_increasing(&out);
1585 }
1586
1587 #[test]
1588 fn test_minimum_jerk_derived_duration_matches_quintic_factors() {
1589 let wps = [[rev(0.0)], [rev(1.0)]];
1590 let limits = rev_limits(1.0, 1.0);
1591 let mut out = CuTrajectoryPayload::<1, 512>::new();
1592 let status = with_profile(
1593 &wps,
1594 &limits,
1595 CuTrajectoryProfile::MinimumJerk,
1596 &CuTimingOverrides::default(),
1597 Time::new::<second>(0.005),
1598 &mut out,
1599 )
1600 .unwrap();
1601 assert_eq!(status, CuTimingStatus::Success);
1602 let expected = 1.875f64.max((10.0f64 / 3.0f64.sqrt()).sqrt());
1604 assert!((out.duration().get::<second>() - expected).abs() < 1e-9);
1605 let (lo, hi) = max_sampled_velocity(&out);
1606 assert!(hi <= 1.0 + 1e-9, "velocity limit exceeded: {hi}");
1607 assert!(lo >= -1e-9, "unexpected reverse motion: {lo}");
1608 assert_strictly_increasing(&out);
1609 }
1610
1611 #[test]
1612 fn test_scurve_respects_velocity_acceleration_and_rest() {
1613 let wps = [[rev(0.0)], [rev(1.0)]];
1614 let limits = rev_limits_jerk(0.5, 1.0, 2.0);
1615 let mut out = CuTrajectoryPayload::<1, 1024>::new();
1616 let status = with_profile(
1617 &wps,
1618 &limits,
1619 CuTrajectoryProfile::SCurve,
1620 &CuTimingOverrides::default(),
1621 Time::new::<second>(0.005),
1622 &mut out,
1623 )
1624 .unwrap();
1625 assert_eq!(status, CuTimingStatus::Success);
1626 assert!((out.duration().get::<second>() - 3.0).abs() < 1e-9);
1628 let (lo, hi) = max_sampled_velocity(&out);
1629 assert!(hi <= 0.5 + 1e-9, "velocity limit exceeded: {hi}");
1630 assert!(lo >= -1e-9, "unexpected reverse motion: {lo}");
1631 assert_eq!(out.points()[0].velocities[0].si_value(), 0.0);
1632 let last = out.len() - 1;
1633 assert_eq!(out.points()[last].positions[0].si_value(), 1.0);
1634 assert_eq!(out.points()[last].velocities[0].si_value(), 0.0);
1635 let mut peak_accel: f64 = 0.0;
1636 let mut i = 1;
1637 while i + 2 < out.len() {
1638 let gap = out.points()[i + 1].time_from_start.get::<second>()
1639 - out.points()[i - 1].time_from_start.get::<second>();
1640 if (gap - 0.01).abs() < 1e-12 {
1641 let accel = (out.points()[i + 1].velocities[0].si_value()
1642 - out.points()[i - 1].velocities[0].si_value())
1643 / gap;
1644 peak_accel = peak_accel.max(accel.abs());
1645 }
1646 i += 1;
1647 }
1648 assert!(
1649 peak_accel <= 1.0 + 0.05,
1650 "accel limit exceeded: {peak_accel}"
1651 );
1652 assert_strictly_increasing(&out);
1653 }
1654
1655 #[test]
1656 fn test_scurve_infinite_jerk_matches_trapezoidal_timing() {
1657 let wps = [[rev(0.0)], [rev(1.0)]];
1658 let limits = rev_limits(0.5, 1.0);
1659 assert_eq!(limits.max_jerks, None);
1660 let mut out = CuTrajectoryPayload::<1, 256>::new();
1661 let status = with_profile(
1662 &wps,
1663 &limits,
1664 CuTrajectoryProfile::SCurve,
1665 &CuTimingOverrides::default(),
1666 Time::new::<second>(0.02),
1667 &mut out,
1668 )
1669 .unwrap();
1670 assert_eq!(status, CuTimingStatus::Success);
1671 assert!((out.duration().get::<second>() - 2.5).abs() < 1e-9);
1673 let (_, peak) = max_sampled_velocity(&out);
1674 assert!(peak <= 0.5 + 1e-9, "velocity limit exceeded: {peak}");
1675 }
1676
1677 #[test]
1678 fn test_scurve_rejects_non_positive_jerk() {
1679 let wps = [[rev(0.0)], [rev(1.0)]];
1680 for bad in [0.0, -1.0, f64::NAN, f64::NEG_INFINITY] {
1681 let limits = rev_limits_jerk(0.5, 1.0, bad);
1682 let mut out = CuTrajectoryPayload::<1>::new();
1683 let status = with_profile(
1684 &wps,
1685 &limits,
1686 CuTrajectoryProfile::SCurve,
1687 &CuTimingOverrides::default(),
1688 Time::new::<second>(0.01),
1689 &mut out,
1690 )
1691 .unwrap();
1692 assert_eq!(status, CuTimingStatus::InvalidInput, "jerk: {bad}");
1693 }
1694 }
1695
1696 #[test]
1697 fn test_rest_only_profiles_reject_nonzero_via() {
1698 let wps = [[rev(0.0)], [rev(1.0)]];
1699 let limits = rev_limits(1.0, 1.0);
1700 let vias = [[zero_vel()], [rev_vel(0.5)]];
1701 let timing = CuTimingOverrides {
1702 segment_durations: None,
1703 waypoint_velocities: Some(vias.as_slice()),
1704 };
1705 for profile in [
1706 CuTrajectoryProfile::Trapezoidal,
1707 CuTrajectoryProfile::SCurve,
1708 ] {
1709 let mut out = CuTrajectoryPayload::<1>::new();
1710 let status = with_profile(
1711 &wps,
1712 &limits,
1713 profile,
1714 &timing,
1715 Time::new::<second>(0.01),
1716 &mut out,
1717 )
1718 .unwrap();
1719 assert_eq!(status, CuTimingStatus::InvalidInput);
1720 }
1721 }
1722
1723 #[test]
1724 fn test_timing_shape_violations_rejected() {
1725 let wps = [[rev(0.0)], [rev(1.0)], [rev(2.0)]];
1726 let limits = rev_limits(1.0, 1.0);
1727 let dt = Time::new::<second>(0.01);
1728 let one = [Time::new::<second>(1.0)];
1730 let short_timing = CuTimingOverrides {
1731 segment_durations: Some(one.as_slice()),
1732 waypoint_velocities: None,
1733 };
1734 let vias = [[zero_vel()], [zero_vel()]];
1736 let via_timing = CuTimingOverrides {
1737 segment_durations: None,
1738 waypoint_velocities: Some(vias.as_slice()),
1739 };
1740 let short = [Time::new::<second>(0.5), Time::new::<second>(0.5)];
1742 let fast_timing = CuTimingOverrides {
1743 segment_durations: Some(short.as_slice()),
1744 waypoint_velocities: None,
1745 };
1746 let zero = [Time::new::<second>(0.0), Time::new::<second>(1.0)];
1748 let zero_timing = CuTimingOverrides {
1749 segment_durations: Some(zero.as_slice()),
1750 waypoint_velocities: None,
1751 };
1752 for timing in [short_timing, via_timing, fast_timing, zero_timing] {
1753 let mut out = CuTrajectoryPayload::<1>::new();
1754 let status = with_profile(
1755 &wps,
1756 &limits,
1757 CuTrajectoryProfile::Trapezoidal,
1758 &timing,
1759 dt,
1760 &mut out,
1761 )
1762 .unwrap();
1763 assert_eq!(status, CuTimingStatus::InvalidInput);
1764 }
1765 }
1766
1767 #[test]
1768 fn test_fixed_durations_produce_exact_total() {
1769 let wps = [[rev(0.0)], [rev(1.0)], [rev(1.5)]];
1770 let limits = rev_limits(3.0, 5.0);
1771 let fixed = [Time::new::<second>(0.7), Time::new::<second>(1.3)];
1772 let timing = CuTimingOverrides {
1773 segment_durations: Some(fixed.as_slice()),
1774 waypoint_velocities: None,
1775 };
1776 for profile in [CuTrajectoryProfile::Cubic, CuTrajectoryProfile::MinimumJerk] {
1777 let mut out = CuTrajectoryPayload::<1, 512>::new();
1778 let status = with_profile(
1779 &wps,
1780 &limits,
1781 profile,
1782 &timing,
1783 Time::new::<second>(0.01),
1784 &mut out,
1785 )
1786 .unwrap();
1787 assert_eq!(status, CuTimingStatus::Success);
1788 assert!((out.duration().get::<second>() - 2.0).abs() < 1e-9);
1789 let last = out.len() - 1;
1790 assert_eq!(out.points()[last].positions[0].si_value(), 1.5);
1791 assert_eq!(out.points()[last].velocities[0].si_value(), 0.0);
1792 assert_strictly_increasing(&out);
1793 }
1794 }
1795
1796 #[test]
1797 fn test_cubic_velocity_violation_rejected() {
1798 let wps = [[rev(0.0)], [rev(1.0)]];
1799 let limits = rev_limits(0.5, 5.0);
1800 let mut out = CuTrajectoryPayload::<1, 512>::new();
1801 let status = with_profile(
1804 &wps,
1805 &limits,
1806 CuTrajectoryProfile::Cubic,
1807 &CuTimingOverrides::default(),
1808 Time::new::<second>(0.01),
1809 &mut out,
1810 )
1811 .unwrap();
1812 assert_eq!(status, CuTimingStatus::InvalidInput);
1813 assert!(out.is_empty());
1814 }
1815
1816 #[test]
1817 fn test_parameterize_tags_cubic_interpolation() {
1818 let mut out = CuTrajectoryPayload::<1, 512>::new();
1819 trapezoid(
1820 &[[rev(0.0)], [rev(1.0)]],
1821 &rev_limits(0.5, 1.0),
1822 Time::new::<second>(0.02),
1823 &mut out,
1824 )
1825 .unwrap();
1826 assert_eq!(out.interpolation, CuTrajectoryInterpolation::Cubic);
1827 }
1828
1829 #[test]
1830 fn test_params_validate() {
1831 assert!(CuTimingParams::<1>::default().validate().is_err());
1832 let mut params = CuTimingParams {
1833 limits: rev_limits(0.5, 1.0),
1834 ..Default::default()
1835 };
1836 assert!(params.validate().is_ok());
1837 assert_eq!(params.sample_period.get::<second>(), 0.02);
1838 params.sample_period = Time::new::<second>(0.0);
1839 assert!(params.validate().is_err());
1840 params.sample_period = Time::new::<second>(0.01);
1841 params.limits = rev_limits_jerk(0.5, 1.0, -1.0);
1842 assert!(params.validate().is_err());
1843 params.limits = rev_limits(0.5, 1.0);
1844 params.limits.max_accelerations = [CuJointAcceleration::Fixed];
1845 assert!(params.validate().is_err());
1846 }
1847
1848 #[cfg(feature = "std")]
1849 #[test]
1850 fn test_params_from_ron() {
1851 let parse = |text: &str| {
1852 let cfg: ComponentConfig =
1853 ron::from_str(&format!("#![enable(unwrap_newtypes)] {text}")).unwrap();
1854 cfg.deserialize_into::<CuTimingParams<1>>()
1855 };
1856 let params = parse(
1857 r#"{"limits": {"max_velocities": [{"revolute": 0.5}], "max_accelerations": [{"revolute": 1.0}]}}"#,
1858 )
1859 .unwrap();
1860 assert_eq!(params.limits, rev_limits(0.5, 1.0));
1861 assert_eq!(params.profile, CuTrajectoryProfile::Trapezoidal);
1862 assert_eq!(params.sample_period.get::<second>(), 0.02);
1863 let params = parse(
1864 r#"{"limits": {"max_velocities": [{"revolute": 0.5}], "max_accelerations": [{"revolute": 1.0}],
1865 "max_jerks": [{"revolute": 2.0}]}, "profile": "s_curve", "sample_period": 0.01}"#,
1866 )
1867 .unwrap();
1868 assert_eq!(params.limits, rev_limits_jerk(0.5, 1.0, 2.0));
1869 assert_eq!(params.profile, CuTrajectoryProfile::SCurve);
1870 assert_eq!(params.sample_period.get::<second>(), 0.01);
1871 assert!(
1872 parse(
1873 r#"{"limits": {"max_velocities": [{"revolute": 0.5}], "max_accelerations": [{"revolute": 1.0}]},
1874 "sample_period_s": 0.01}"#
1875 )
1876 .is_err()
1877 );
1878 assert!(parse(r#"{"profile": "cubic"}"#).is_err());
1879 }
1880}