1use crate::diag::PathError;
2use crate::path::OutOfRangeMode;
3use crate::path::autodiff::Jet3;
4use crate::path::spline::{SplineConfig, SplinePath};
5use nalgebra::{DMatrix, DMatrixView};
6use rayon::prelude::*;
7use std::sync::Arc;
8
9const EPS_RANGE: f64 = 1e-12;
10
11pub type ParametricFn = Arc<dyn Fn(Jet3) -> Vec<Jet3> + Send + Sync>;
16
17pub trait PathEvaluator2nd: Send + Sync {
29 fn dim(&self) -> usize;
34
35 fn evaluate_q(&self, s: &[f64], q: &mut [f64]) -> Result<(), PathError> {
41 let mut dq = vec![0.0; q.len()];
42 let mut ddq = vec![0.0; q.len()];
43 self.evaluate_up_to_2nd(s, q, &mut dq, &mut ddq)
44 }
45
46 fn evaluate_up_to_2nd(
51 &self,
52 s: &[f64],
53 q: &mut [f64],
54 dq: &mut [f64],
55 ddq: &mut [f64],
56 ) -> Result<(), PathError>;
57}
58
59pub trait PathEvaluator3rd: PathEvaluator2nd {
65 fn evaluate_up_to_3rd(
70 &self,
71 s: &[f64],
72 q: &mut [f64],
73 dq: &mut [f64],
74 ddq: &mut [f64],
75 dddq: &mut [f64],
76 ) -> Result<(), PathError>;
77}
78
79pub trait PathEvaluator: PathEvaluator3rd {}
84
85impl<T: PathEvaluator3rd + ?Sized> PathEvaluator for T {}
86
87#[derive(Debug)]
92pub struct PathDerivatives {
93 pub q: DMatrix<f64>,
95 pub dq: Option<DMatrix<f64>>,
97 pub ddq: Option<DMatrix<f64>>,
99 pub dddq: Option<DMatrix<f64>>,
101}
102
103#[derive(Clone, Copy, PartialEq, Eq)]
105enum Order {
106 Zero, Two, Three, }
110
111pub struct Path {
123 dim: usize,
124 s_min: f64,
125 s_max: f64,
126 out_of_range_mode: OutOfRangeMode,
127 repr: PathRepr,
128}
129
130enum PathRepr {
131 Parametric(ParametricFn),
133 Spline(SplinePath),
135 Evaluator2nd(Arc<dyn PathEvaluator2nd>),
137 Evaluator3rd(Arc<dyn PathEvaluator3rd>),
139}
140
141impl Path {
142 pub fn from_parametric<F>(q_fn: F, s_min: f64, s_max: f64) -> Result<Self, PathError>
157 where
158 F: Fn(Jet3) -> Vec<Jet3> + Send + Sync + 'static,
159 {
160 validate_range(s_min, s_max)?;
161
162 let sample = q_fn(Jet3::constant((s_min + s_max) * 0.5));
163 if sample.is_empty() {
164 return Err(PathError::InvalidDimension { dim: 0 });
165 }
166 let dim = sample.len();
167
168 Ok(Self {
169 dim,
170 s_min,
171 s_max,
172 out_of_range_mode: OutOfRangeMode::Error,
173 repr: PathRepr::Parametric(Arc::new(q_fn)),
174 })
175 }
176
177 pub fn from_evaluator_2nd<E>(evaluator: E, s_min: f64, s_max: f64) -> Result<Self, PathError>
194 where
195 E: PathEvaluator2nd + 'static,
196 {
197 Self::from_shared_evaluator_2nd(Arc::new(evaluator), s_min, s_max)
198 }
199
200 pub fn from_shared_evaluator_2nd(
210 evaluator: Arc<dyn PathEvaluator2nd>,
211 s_min: f64,
212 s_max: f64,
213 ) -> Result<Self, PathError> {
214 validate_range(s_min, s_max)?;
215
216 let dim = evaluator.dim();
217 if dim == 0 {
218 return Err(PathError::InvalidDimension { dim });
219 }
220
221 Ok(Self {
222 dim,
223 s_min,
224 s_max,
225 out_of_range_mode: OutOfRangeMode::Error,
226 repr: PathRepr::Evaluator2nd(evaluator),
227 })
228 }
229
230 pub fn from_evaluator_3rd<E>(evaluator: E, s_min: f64, s_max: f64) -> Result<Self, PathError>
241 where
242 E: PathEvaluator3rd + 'static,
243 {
244 Self::from_shared_evaluator_3rd(Arc::new(evaluator), s_min, s_max)
245 }
246
247 pub fn from_shared_evaluator_3rd(
253 evaluator: Arc<dyn PathEvaluator3rd>,
254 s_min: f64,
255 s_max: f64,
256 ) -> Result<Self, PathError> {
257 validate_range(s_min, s_max)?;
258
259 let dim = evaluator.dim();
260 if dim == 0 {
261 return Err(PathError::InvalidDimension { dim });
262 }
263
264 Ok(Self {
265 dim,
266 s_min,
267 s_max,
268 out_of_range_mode: OutOfRangeMode::Error,
269 repr: PathRepr::Evaluator3rd(evaluator),
270 })
271 }
272
273 pub fn from_evaluator<E>(evaluator: E, s_min: f64, s_max: f64) -> Result<Self, PathError>
280 where
281 E: PathEvaluator3rd + 'static,
282 {
283 Self::from_evaluator_3rd(evaluator, s_min, s_max)
284 }
285
286 pub fn from_shared_evaluator(
291 evaluator: Arc<dyn PathEvaluator3rd>,
292 s_min: f64,
293 s_max: f64,
294 ) -> Result<Self, PathError> {
295 Self::from_shared_evaluator_3rd(evaluator, s_min, s_max)
296 }
297
298 pub fn from_waypoints(waypoints: &DMatrix<f64>, cfg: SplineConfig) -> Result<Self, PathError> {
316 Self::from_waypoints_view(waypoints.as_view(), cfg)
317 }
318
319 pub fn from_waypoints_view(
325 waypoints: DMatrixView<'_, f64>,
326 cfg: SplineConfig,
327 ) -> Result<Self, PathError> {
328 if waypoints.nrows() == 0 {
329 return Err(PathError::InvalidDimension {
330 dim: waypoints.nrows(),
331 });
332 }
333 if waypoints.ncols() < 2 {
334 return Err(PathError::NotEnoughWaypoints {
335 n: waypoints.ncols(),
336 });
337 }
338 if cfg.order < 3 {
339 return Err(PathError::InvalidOrder { order: cfg.order });
340 }
341 validate_range(cfg.s_min, cfg.s_max)?;
342
343 let spline = SplinePath::from_waypoints_view(waypoints, &cfg)?;
344
345 Ok(Self {
346 dim: waypoints.nrows(),
347 s_min: spline.s_min,
348 s_max: spline.s_max,
349 out_of_range_mode: spline.out_of_range_mode,
350 repr: PathRepr::Spline(spline),
351 })
352 }
353
354 #[inline(always)]
356 pub fn dim(&self) -> usize {
357 self.dim
358 }
359
360 #[inline(always)]
362 pub fn s_range(&self) -> (f64, f64) {
363 (self.s_min, self.s_max)
364 }
365
366 pub fn evaluate_q(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
378 self.evaluate_impl(s, Order::Zero)
379 }
380
381 pub fn evaluate_up_to_2nd(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
390 self.evaluate_impl(s, Order::Two)
391 }
392
393 pub fn evaluate_up_to_3rd(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
430 self.evaluate_impl(s, Order::Three)
431 }
432
433 fn evaluate_impl(&self, s: &[f64], order: Order) -> Result<PathDerivatives, PathError> {
436 let n = s.len();
437 let dim = self.dim;
438
439 let mut q = vec![0.0f64; dim * n];
441 let mut dq = (order != Order::Zero).then(|| vec![0.0f64; dim * n]);
442 let mut ddq = (order != Order::Zero).then(|| vec![0.0f64; dim * n]);
443 let mut dddq = (order == Order::Three).then(|| vec![0.0f64; dim * n]);
444
445 match &self.repr {
446 PathRepr::Parametric(eval_fn) => {
447 eval_parametric(
448 eval_fn,
449 self,
450 dim,
451 (s, &mut q, &mut dq, &mut ddq, &mut dddq),
452 )?;
453 }
454 PathRepr::Spline(spline) => {
455 eval_spline(spline, self, dim, (s, &mut q, &mut dq, &mut ddq, &mut dddq))?;
456 }
457 PathRepr::Evaluator2nd(evaluator) => {
458 eval_evaluator_2nd(
459 evaluator.as_ref(),
460 self,
461 dim,
462 (s, &mut q, &mut dq, &mut ddq, &mut dddq),
463 )?;
464 }
465 PathRepr::Evaluator3rd(evaluator) => {
466 eval_evaluator_3rd(
467 evaluator.as_ref(),
468 self,
469 dim,
470 (s, &mut q, &mut dq, &mut ddq, &mut dddq),
471 )?;
472 }
473 }
474
475 Ok(PathDerivatives {
476 q: DMatrix::from_vec(dim, n, q),
477 dq: dq.map(|v| DMatrix::from_vec(dim, n, v)),
478 ddq: ddq.map(|v| DMatrix::from_vec(dim, n, v)),
479 dddq: dddq.map(|v| DMatrix::from_vec(dim, n, v)),
480 })
481 }
482
483 #[inline(always)]
484 fn validate_s(&self, s: f64, index: usize) -> Result<f64, PathError> {
485 match self.out_of_range_mode {
486 OutOfRangeMode::Error => {
487 if s < self.s_min - EPS_RANGE || s > self.s_max + EPS_RANGE {
488 return Err(PathError::OutOfRangeS {
489 s_min: self.s_min,
490 s_max: self.s_max,
491 index,
492 value: s,
493 });
494 }
495 Ok(s.clamp(self.s_min, self.s_max))
496 }
497 OutOfRangeMode::Clamp => Ok(s.clamp(self.s_min, self.s_max)),
498 }
499 }
500}
501
502type EvalInput<'a> = (
506 &'a [f64], &'a mut [f64], &'a mut Option<Vec<f64>>, &'a mut Option<Vec<f64>>, &'a mut Option<Vec<f64>>, );
512
513fn eval_parametric(
518 eval_fn: &ParametricFn,
519 path: &Path,
520 dim: usize,
521 input_eval: EvalInput,
522) -> Result<(), PathError> {
523 let (s_values, q, dq, ddq, dddq) = input_eval;
524 let n = s_values.len();
525
526 let dq_chunks: Vec<Option<&mut [f64]>> = dq.as_deref_mut().map_or_else(
531 || (0..n).map(|_| None).collect(),
532 |v| v.chunks_mut(dim).map(Some).collect(),
533 );
534 let ddq_chunks: Vec<Option<&mut [f64]>> = ddq.as_deref_mut().map_or_else(
535 || (0..n).map(|_| None).collect(),
536 |v| v.chunks_mut(dim).map(Some).collect(),
537 );
538 let dddq_chunks: Vec<Option<&mut [f64]>> = dddq.as_deref_mut().map_or_else(
539 || (0..n).map(|_| None).collect(),
540 |v| v.chunks_mut(dim).map(Some).collect(),
541 );
542
543 s_values
544 .par_iter()
545 .enumerate()
546 .zip(q.par_chunks_mut(dim))
547 .zip(dq_chunks.into_par_iter())
548 .zip(ddq_chunks.into_par_iter())
549 .zip(dddq_chunks.into_par_iter())
550 .map(
551 |(((((j, &s_raw), q_col), mut dq_col), mut ddq_col), mut dddq_col)| {
552 let s_curr = path.validate_s(s_raw, j)?;
553 let vals = eval_fn(Jet3::seed(s_curr));
554 if vals.len() != dim {
555 return Err(PathError::DimensionMismatch);
556 }
557 for (i, jet) in vals.iter().enumerate() {
558 q_col[i] = jet.v;
559 if let Some(ref mut b) = dq_col {
560 b[i] = jet.d1;
561 }
562 if let Some(ref mut b) = ddq_col {
563 b[i] = jet.d2;
564 }
565 if let Some(ref mut b) = dddq_col {
566 b[i] = jet.d3;
567 }
568 }
569 Ok(())
570 },
571 )
572 .collect()
573}
574
575fn eval_spline(
583 spline: &SplinePath,
584 path: &Path,
585 dim: usize,
586 input_eval: EvalInput,
587) -> Result<(), PathError> {
588 let (s_values, q, dq, ddq, dddq) = input_eval;
589 match (dq.as_deref_mut(), ddq.as_deref_mut(), dddq.as_deref_mut()) {
590 (None, None, None) => s_values
592 .par_iter()
593 .enumerate()
594 .zip(q.par_chunks_mut(dim))
595 .map(|((j, &s_raw), q_col)| -> Result<(), PathError> {
596 let s_curr = path.validate_s(s_raw, j)?;
597 let (mut no_dq, mut no_ddq, mut no_dddq): ([f64; 0], [f64; 0], [f64; 0]) =
600 ([], [], []);
601 spline.eval_at::<0>(s_curr, dim, q_col, &mut no_dq, &mut no_ddq, &mut no_dddq);
602 Ok(())
603 })
604 .collect(),
605 (Some(dq_buf), Some(ddq_buf), None) => {
607 let dq_chunks: Vec<&mut [f64]> = dq_buf.chunks_mut(dim).collect();
608 let ddq_chunks: Vec<&mut [f64]> = ddq_buf.chunks_mut(dim).collect();
609 s_values
610 .par_iter()
611 .enumerate()
612 .zip(q.par_chunks_mut(dim))
613 .zip(dq_chunks.into_par_iter())
614 .zip(ddq_chunks.into_par_iter())
615 .map(
616 |((((j, &s_raw), q_col), dq_col), ddq_col)| -> Result<(), PathError> {
617 let s_curr = path.validate_s(s_raw, j)?;
618 let mut s4 = [];
619 spline.eval_at::<2>(s_curr, dim, q_col, dq_col, ddq_col, &mut s4);
620 Ok(())
621 },
622 )
623 .collect()
624 }
625 (Some(dq_buf), Some(ddq_buf), Some(dddq_buf)) => {
627 let dq_chunks: Vec<&mut [f64]> = dq_buf.chunks_mut(dim).collect();
628 let ddq_chunks: Vec<&mut [f64]> = ddq_buf.chunks_mut(dim).collect();
629 let dddq_chunks: Vec<&mut [f64]> = dddq_buf.chunks_mut(dim).collect();
630 s_values
631 .par_iter()
632 .enumerate()
633 .zip(q.par_chunks_mut(dim))
634 .zip(dq_chunks.into_par_iter())
635 .zip(ddq_chunks.into_par_iter())
636 .zip(dddq_chunks.into_par_iter())
637 .map(
638 |(((((j, &s_raw), q_col), dq_col), ddq_col), dddq_col)| -> Result<(), PathError> {
639 let s_curr = path.validate_s(s_raw, j)?;
640 spline.eval_at::<3>(s_curr, dim, q_col, dq_col, ddq_col, dddq_col);
641 Ok(())
642 },
643 )
644 .collect()
645 }
646 _ => unreachable!("unexpected dq/ddq/dddq combination"),
648 }
649}
650
651fn eval_evaluator_2nd(
659 evaluator: &dyn PathEvaluator2nd,
660 path: &Path,
661 dim: usize,
662 input_eval: EvalInput,
663) -> Result<(), PathError> {
664 let (s_values, q, dq, ddq, dddq) = input_eval;
665 let s_valid = s_values
666 .iter()
667 .enumerate()
668 .map(|(j, &s)| path.validate_s(s, j))
669 .collect::<Result<Vec<_>, _>>()?;
670
671 if evaluator.dim() != dim {
672 return Err(PathError::DimensionMismatch);
673 }
674
675 match (dq.as_deref_mut(), ddq.as_deref_mut(), dddq.as_deref_mut()) {
676 (None, None, None) => evaluator.evaluate_q(&s_valid, q),
677 (Some(dq_buf), Some(ddq_buf), None) => {
678 evaluator.evaluate_up_to_2nd(&s_valid, q, dq_buf, ddq_buf)
679 }
680 (Some(_), Some(_), Some(_)) => Err(PathError::UnsupportedDerivativeOrder {
681 requested: 3,
682 available: 2,
683 }),
684 _ => unreachable!("unexpected dq/ddq/dddq combination"),
686 }
687}
688
689fn eval_evaluator_3rd(
695 evaluator: &dyn PathEvaluator3rd,
696 path: &Path,
697 dim: usize,
698 input_eval: EvalInput,
699) -> Result<(), PathError> {
700 let (s_values, q, dq, ddq, dddq) = input_eval;
701 let s_valid = s_values
702 .iter()
703 .enumerate()
704 .map(|(j, &s)| path.validate_s(s, j))
705 .collect::<Result<Vec<_>, _>>()?;
706
707 if evaluator.dim() != dim {
708 return Err(PathError::DimensionMismatch);
709 }
710
711 match (dq.as_deref_mut(), ddq.as_deref_mut(), dddq.as_deref_mut()) {
712 (None, None, None) => evaluator.evaluate_q(&s_valid, q),
713 (Some(dq_buf), Some(ddq_buf), None) => {
714 evaluator.evaluate_up_to_2nd(&s_valid, q, dq_buf, ddq_buf)
715 }
716 (Some(dq_buf), Some(ddq_buf), Some(dddq_buf)) => {
717 evaluator.evaluate_up_to_3rd(&s_valid, q, dq_buf, ddq_buf, dddq_buf)
718 }
719 _ => unreachable!("unexpected dq/ddq/dddq combination"),
721 }
722}
723
724fn validate_range(s_min: f64, s_max: f64) -> Result<(), PathError> {
725 if !s_min.is_finite() || !s_max.is_finite() || s_max <= s_min {
726 return Err(PathError::InvalidRange { s_min, s_max });
727 }
728 Ok(())
729}
730
731#[cfg(test)]
732mod tests {
733 use super::PathDerivatives;
734 use crate::path::{
735 Jet3, Path as PathModel, PathError, PathEvaluator2nd, PathEvaluator3rd, SplineConfig, cos,
736 exp, sin,
737 };
738 use nalgebra::{Const, DMatrix, DMatrixView, Dyn};
739 use plotters::prelude::*;
740 use rand::RngExt;
741 use std::error::Error;
742 use std::fs::create_dir_all;
743 use std::hint::black_box;
744 use std::path::Path as StdPath;
745 use std::time::Instant;
746
747 const DIM: usize = 6;
748
749 fn make_s(n: usize) -> DMatrix<f64> {
750 DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64)
751 }
752
753 fn make_parametric_path() -> Result<PathModel, PathError> {
754 PathModel::from_parametric(
755 |s: Jet3| {
756 vec![
757 sin(s),
758 cos(s),
759 exp(0.3 * s) - 1.0,
760 s + 0.1 * s * s - 0.01 * s * s * s * s,
761 sin(2.0 * s) + 0.15 * cos(3.0 * s),
762 sin(s) * cos(s),
763 ]
764 },
765 0.0,
766 1.0,
767 )
768 }
769
770 struct PolynomialEvaluator;
771 struct QuadraticEvaluator2nd;
772
773 impl PathEvaluator2nd for PolynomialEvaluator {
774 fn dim(&self) -> usize {
775 2
776 }
777
778 fn evaluate_up_to_2nd(
779 &self,
780 s: &[f64],
781 q: &mut [f64],
782 dq: &mut [f64],
783 ddq: &mut [f64],
784 ) -> Result<(), PathError> {
785 if q.len() != 2 * s.len() || dq.len() != q.len() || ddq.len() != q.len() {
786 return Err(PathError::DimensionMismatch);
787 }
788
789 for (j, &x) in s.iter().enumerate() {
790 let col = 2 * j;
791 q[col] = x * x * x;
792 dq[col] = 3.0 * x * x;
793 ddq[col] = 6.0 * x;
794
795 q[col + 1] = x * x + 1.0;
796 dq[col + 1] = 2.0 * x;
797 ddq[col + 1] = 2.0;
798 }
799 Ok(())
800 }
801 }
802
803 impl PathEvaluator3rd for PolynomialEvaluator {
804 fn evaluate_up_to_3rd(
805 &self,
806 s: &[f64],
807 q: &mut [f64],
808 dq: &mut [f64],
809 ddq: &mut [f64],
810 dddq: &mut [f64],
811 ) -> Result<(), PathError> {
812 self.evaluate_up_to_2nd(s, q, dq, ddq)?;
813 if dddq.len() != 2 * s.len() {
814 return Err(PathError::DimensionMismatch);
815 }
816
817 for j in 0..s.len() {
818 let col = 2 * j;
819 dddq[col] = 6.0;
820 dddq[col + 1] = 0.0;
821 }
822 Ok(())
823 }
824 }
825
826 impl PathEvaluator2nd for QuadraticEvaluator2nd {
827 fn dim(&self) -> usize {
828 1
829 }
830
831 fn evaluate_up_to_2nd(
832 &self,
833 s: &[f64],
834 q: &mut [f64],
835 dq: &mut [f64],
836 ddq: &mut [f64],
837 ) -> Result<(), PathError> {
838 if q.len() != s.len() || dq.len() != q.len() || ddq.len() != q.len() {
839 return Err(PathError::DimensionMismatch);
840 }
841
842 for (j, &x) in s.iter().enumerate() {
843 q[j] = x * x + 1.0;
844 dq[j] = 2.0 * x;
845 ddq[j] = 2.0;
846 }
847 Ok(())
848 }
849 }
850
851 fn make_waypoints(n_pts: usize) -> DMatrix<f64> {
852 let mut rng = rand::rng();
853 let mut waypoints = DMatrix::<f64>::zeros(DIM, n_pts);
855 for mut row in waypoints.row_iter_mut() {
856 row[0] = rng.random_range(-1.0..1.0);
857 for j in 1..n_pts {
858 let step = rng.random_range(-0.35..0.35);
859 row[j] = row[j - 1] + step;
860 }
861 }
862 waypoints
863 }
864
865 #[test]
866 fn test_waypoints_view_interpolates_padded_column_major() -> Result<(), PathError> {
867 const DIM_LOCAL: usize = 2;
868 const N_PTS: usize = 4;
869 const LEADING_DIM: usize = 3;
870 let data = [
871 0.0, 1.0, -99.0, 0.5, 1.5, -99.0, 1.0, 2.0, -99.0, 1.5, 2.5, -99.0,
875 ];
876 let waypoints = DMatrixView::from_slice_with_strides_generic(
877 &data,
878 Dyn(DIM_LOCAL),
879 Dyn(N_PTS),
880 Const::<1>,
881 Dyn(LEADING_DIM),
882 );
883
884 let path = PathModel::from_waypoints_view(waypoints, SplineConfig::default())?;
885 let s = [0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0];
886 let out = path.evaluate_q(&s)?;
887
888 for j in 0..N_PTS {
889 assert!((out.q[(0, j)] - data[j * LEADING_DIM]).abs() < 1e-10);
890 assert!((out.q[(1, j)] - data[j * LEADING_DIM + 1]).abs() < 1e-10);
891 }
892
893 Ok(())
894 }
895
896 #[test]
897 fn test_evaluator_path_explicit_derivatives() -> Result<(), PathError> {
898 let path = PathModel::from_evaluator_3rd(PolynomialEvaluator, -1.0, 1.0)?;
899 let s = [-1.0, 0.0, 0.5];
900
901 let out = path.evaluate_up_to_3rd(&s)?;
902 let dq = out.dq.as_ref().unwrap();
903 let ddq = out.ddq.as_ref().unwrap();
904 let dddq = out.dddq.as_ref().unwrap();
905
906 for (j, &x) in s.iter().enumerate() {
907 assert!((out.q[(0, j)] - x.powi(3)).abs() < 1e-12);
908 assert!((dq[(0, j)] - 3.0 * x * x).abs() < 1e-12);
909 assert!((ddq[(0, j)] - 6.0 * x).abs() < 1e-12);
910 assert!((dddq[(0, j)] - 6.0).abs() < 1e-12);
911
912 assert!((out.q[(1, j)] - (x * x + 1.0)).abs() < 1e-12);
913 assert!((dq[(1, j)] - 2.0 * x).abs() < 1e-12);
914 assert!((ddq[(1, j)] - 2.0).abs() < 1e-12);
915 assert!(dddq[(1, j)].abs() < 1e-12);
916 }
917
918 let q_only = path.evaluate_q(&s)?;
919 assert!(q_only.dq.is_none());
920 assert!(q_only.ddq.is_none());
921 assert!(q_only.dddq.is_none());
922 assert!((q_only.q[(0, 2)] - 0.125).abs() < 1e-12);
923
924 Ok(())
925 }
926
927 #[test]
928 fn test_evaluator_path_2nd_does_not_require_3rd() -> Result<(), PathError> {
929 let path = PathModel::from_evaluator_2nd(QuadraticEvaluator2nd, -1.0, 1.0)?;
930 let s = [-1.0, 0.0, 0.5];
931
932 let out = path.evaluate_up_to_2nd(&s)?;
933 let dq = out.dq.as_ref().unwrap();
934 let ddq = out.ddq.as_ref().unwrap();
935
936 assert!(out.dddq.is_none());
937 assert!((out.q[(0, 2)] - 1.25).abs() < 1e-12);
938 assert!((dq[(0, 2)] - 1.0).abs() < 1e-12);
939 assert!((ddq[(0, 2)] - 2.0).abs() < 1e-12);
940
941 let err = path.evaluate_up_to_3rd(&s).unwrap_err();
942 match err {
943 PathError::UnsupportedDerivativeOrder {
944 requested: 3,
945 available: 2,
946 } => {}
947 other => panic!("unexpected error: {other}"),
948 }
949
950 Ok(())
951 }
952
953 #[test]
954 fn test_parametric_autodiff_dim6() -> Result<(), PathError> {
955 let path = make_parametric_path()?;
956
957 let n = 300;
958 let s = make_s(n);
959 let out = path.evaluate_up_to_3rd(s.as_slice())?;
960 let dq = out.dq.as_ref().unwrap();
961 let ddq = out.ddq.as_ref().unwrap();
962 let dddq = out.dddq.as_ref().unwrap();
963
964 s.as_slice().iter().enumerate().for_each(|(j, &x)| {
966 let e03x = (0.3 * x).exp();
967 let expected_q = [
968 x.sin(),
969 x.cos(),
970 e03x - 1.0,
971 x + 0.1 * x * x - 0.01 * x * x * x * x,
972 (2.0 * x).sin() + 0.15 * (3.0 * x).cos(),
973 x.sin() * x.cos(),
974 ];
975 let expected_dq = [
976 x.cos(),
977 -x.sin(),
978 0.3 * e03x,
979 1.0 + 0.2 * x - 0.04 * x * x * x,
980 2.0 * (2.0 * x).cos() - 0.45 * (3.0 * x).sin(),
981 (2.0 * x).cos(),
982 ];
983 let expected_ddq = [
984 -x.sin(),
985 -x.cos(),
986 0.09 * e03x,
987 0.2 - 0.12 * x * x,
988 -4.0 * (2.0 * x).sin() - 1.35 * (3.0 * x).cos(),
989 -2.0 * (2.0 * x).sin(),
990 ];
991 let expected_dddq = [
992 -x.cos(),
993 x.sin(),
994 0.027 * e03x,
995 -0.24 * x,
996 -8.0 * (2.0 * x).cos() + 4.05 * (3.0 * x).sin(),
997 -4.0 * (2.0 * x).cos(),
998 ];
999
1000 for i in 0..DIM {
1001 assert!(
1002 (out.q[(i, j)] - expected_q[i]).abs() < 1e-10,
1003 "q dim={i} idx={j}"
1004 );
1005 assert!(
1006 (dq[(i, j)] - expected_dq[i]).abs() < 1e-10,
1007 "dq dim={i} idx={j}"
1008 );
1009 assert!(
1010 (ddq[(i, j)] - expected_ddq[i]).abs() < 1e-10,
1011 "ddq dim={i} idx={j}"
1012 );
1013 assert!(
1014 (dddq[(i, j)] - expected_dddq[i]).abs() < 1e-10,
1015 "dddq dim={i} idx={j}"
1016 );
1017 }
1018 });
1019
1020 Ok(())
1021 }
1022
1023 #[test]
1024 fn test_evaluate_q_only() -> Result<(), PathError> {
1025 let path = make_parametric_path()?;
1026 let n = 100;
1027 let s = make_s(n);
1028 let out = path.evaluate_q(s.as_slice())?;
1029
1030 assert!(out.dq.is_none());
1031 assert!(out.ddq.is_none());
1032 assert!(out.dddq.is_none());
1033
1034 for j in 0..n {
1035 let x = s[(0, j)];
1036 assert!((out.q[(0, j)] - x.sin()).abs() < 1e-10);
1037 assert!((out.q[(1, j)] - x.cos()).abs() < 1e-10);
1038 }
1039
1040 Ok(())
1041 }
1042
1043 #[test]
1044 fn test_evaluate_up_to_2nd() -> Result<(), PathError> {
1045 let path = make_parametric_path()?;
1046 let n = 100;
1047 let s = make_s(n);
1048 let out = path.evaluate_up_to_2nd(s.as_slice())?;
1049 let dq = out.dq.as_ref().unwrap();
1050 let ddq = out.ddq.as_ref().unwrap();
1051
1052 assert!(out.dddq.is_none());
1053
1054 for j in 0..n {
1055 let x = s[(0, j)];
1056 assert!((out.q[(0, j)] - x.sin()).abs() < 1e-10);
1057 assert!((dq[(0, j)] - x.cos()).abs() < 1e-10);
1058 assert!((ddq[(0, j)] - (-x.sin())).abs() < 1e-10);
1059 }
1060
1061 Ok(())
1062 }
1063
1064 #[test]
1065 fn test_quintic_spline_interpolates_waypoints_dim6() -> Result<(), PathError> {
1066 let n_pts = 25;
1067 let waypoints = make_waypoints(n_pts);
1068
1069 let cfg = SplineConfig::default();
1070 let path = PathModel::from_waypoints(&waypoints, cfg)?;
1071
1072 let s = DMatrix::<f64>::from_fn(1, n_pts, |_, j| j as f64 / (n_pts - 1) as f64);
1073 let out = path.evaluate_up_to_3rd(s.as_slice())?;
1074 let dq = out.dq.as_ref().unwrap();
1075 let ddq = out.ddq.as_ref().unwrap();
1076 let dddq = out.dddq.as_ref().unwrap();
1077
1078 for (i, j) in (0..DIM).flat_map(|i| (0..n_pts).map(move |j| (i, j))) {
1081 assert!((out.q[(i, j)] - waypoints[(i, j)]).abs() < 1e-8);
1082 assert!(dq[(i, j)].is_finite());
1083 assert!(ddq[(i, j)].is_finite());
1084 assert!(dddq[(i, j)].is_finite());
1085 }
1086
1087 Ok(())
1088 }
1089
1090 #[test]
1091 fn test_s_out_of_range_error_dim6() -> Result<(), PathError> {
1092 let waypoints = make_waypoints(12);
1093 let path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
1094 let s = DMatrix::<f64>::from_row_slice(1, 3, &[-0.1, 0.5, 1.1]);
1095 let err = path.evaluate_up_to_3rd(s.as_slice()).unwrap_err();
1096 match err {
1097 PathError::OutOfRangeS { .. } => {}
1098 _ => panic!("expected OutOfRangeS"),
1099 }
1100 Ok(())
1101 }
1102
1103 #[test]
1104 fn test_benchmark_parametric_and_spline_dim6() -> Result<(), PathError> {
1105 let n_eval = 3000;
1106 let n_repeat = 8;
1107 let s = make_s(n_eval);
1108
1109 let start = Instant::now();
1110 let param_path = make_parametric_path()?;
1111 let tc_build_param = start.elapsed().as_secs_f64() * 1e3;
1112
1113 let start = Instant::now();
1114 for _ in 0..n_repeat {
1115 let out = param_path.evaluate_up_to_3rd(s.as_slice())?;
1116 black_box(out.q[(0, 0)]);
1117 }
1118 let tc_eval_param = start.elapsed().as_secs_f64() * 1e3 / n_repeat as f64;
1119 crate::verbosity_log!(
1120 crate::diag::Verbosity::Summary,
1121 "[bench][parametric][dim=6] build={tc_build_param:.3} ms eval={tc_eval_param:.3} ms (N={n_eval})"
1122 );
1123
1124 let n_waypoints_list = [16usize, 32, 64, 128, 192, 256, 512, 1024];
1125 for &n_pts in &n_waypoints_list {
1126 let waypoints = make_waypoints(n_pts);
1127 let start = Instant::now();
1128 let spline_path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
1129 let tc_build = start.elapsed().as_secs_f64() * 1e3;
1130
1131 let start = Instant::now();
1132 for _ in 0..n_repeat {
1133 let out = spline_path.evaluate_up_to_3rd(s.as_slice())?;
1134 black_box(out.q[(0, 0)]);
1135 }
1136 let tc_eval = start.elapsed().as_secs_f64() * 1e3 / n_repeat as f64;
1137
1138 crate::verbosity_log!(
1139 crate::diag::Verbosity::Summary,
1140 "[bench][spline][dim=6][n_pts={n_pts}] build={tc_build:.3} ms eval={tc_eval:.3} ms"
1141 );
1142 }
1143
1144 Ok(())
1145 }
1146
1147 #[test]
1148 fn test_plot_parametric_and_spline_derivatives() -> Result<(), Box<dyn Error>> {
1149 let dir = "data/path_plots";
1150 create_dir_all(dir)?;
1151
1152 let n = 600;
1153 let s = make_s(n);
1154 let s_vec: Vec<f64> = (0..n).map(|j| s[(0, j)]).collect();
1155
1156 let param_path = make_parametric_path()?;
1157 let param_out = param_path.evaluate_up_to_3rd(s.as_slice())?;
1158 plot_grid_4x6(
1159 &format!("{dir}/parametric_dim6_grid.png"),
1160 "parametric dim=6",
1161 &s_vec,
1162 ¶m_out,
1163 None,
1164 )?;
1165
1166 let n_pts = 10;
1167 let waypoints = make_waypoints(n_pts);
1168 let spline_path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
1169 let spline_out = spline_path.evaluate_up_to_3rd(s.as_slice())?;
1170 let wp_s: Vec<f64> = (0..n_pts).map(|j| j as f64 / (n_pts - 1) as f64).collect();
1171 plot_grid_4x6(
1172 &format!("{dir}/spline_order5_dim6_grid.png"),
1173 "spline order=5 dim=6",
1174 &s_vec,
1175 &spline_out,
1176 Some((&wp_s, &waypoints)),
1177 )?;
1178
1179 Ok(())
1180 }
1181
1182 fn plot_grid_4x6(
1183 file: &str,
1184 title: &str,
1185 s: &[f64],
1186 data: &PathDerivatives,
1187 waypoints: Option<(&[f64], &DMatrix<f64>)>,
1188 ) -> Result<(), Box<dyn Error>> {
1189 if let Some(parent) = StdPath::new(file).parent() {
1190 create_dir_all(parent)?;
1191 }
1192
1193 let root = BitMapBackend::new(file, (2400, 1400)).into_drawing_area();
1194 root.fill(&WHITE)?;
1195
1196 let empty = DMatrix::<f64>::zeros(0, 0);
1197 let dq = data.dq.as_ref().unwrap_or(&empty);
1198 let ddq = data.ddq.as_ref().unwrap_or(&empty);
1199 let dddq = data.dddq.as_ref().unwrap_or(&empty);
1200
1201 let areas = root.split_evenly((4, DIM));
1202 let mats = [&data.q, dq, ddq, dddq];
1203 let row_names = ["q", "dq", "ddq", "dddq"];
1204
1205 for row in 0..4 {
1206 for col in 0..DIM {
1207 let area = &areas[row * DIM + col];
1208 let series = mat_row(mats[row], col);
1209 let (mut y_min, mut y_max) = min_max_slice(&series);
1210 if (y_max - y_min).abs() < 1e-12 {
1211 y_min -= 1.0;
1212 y_max += 1.0;
1213 } else {
1214 let pad = 0.08 * (y_max - y_min);
1215 y_min -= pad;
1216 y_max += pad;
1217 }
1218
1219 let mut chart = ChartBuilder::on(area)
1220 .margin(8)
1221 .caption(
1222 format!("{} j{}", row_names[row], col + 1),
1223 ("sans-serif", 16),
1224 )
1225 .x_label_area_size(24)
1226 .y_label_area_size(38)
1227 .build_cartesian_2d(s[0]..s[s.len() - 1], y_min..y_max)?;
1228
1229 chart
1230 .configure_mesh()
1231 .x_desc(if row == 3 { "s" } else { "" })
1232 .y_desc("")
1233 .draw()?;
1234
1235 chart.draw_series(LineSeries::new(
1236 (0..s.len()).map(|j| (s[j], series[j])),
1237 &BLUE,
1238 ))?;
1239
1240 if row == 0
1241 && let Some((s_wp, q_wp)) = waypoints
1242 {
1243 chart.draw_series(
1244 s_wp.iter()
1245 .zip(q_wp.row(col).iter())
1246 .map(|(&xs, &ys)| Circle::new((xs, ys), 2, RED.filled())),
1247 )?;
1248 }
1249 }
1250 }
1251
1252 root.titled(title, ("sans-serif", 28))?;
1253 root.present()?;
1254 Ok(())
1255 }
1256
1257 fn mat_row(mat: &DMatrix<f64>, row: usize) -> Vec<f64> {
1258 mat.row(row).iter().copied().collect()
1259 }
1260
1261 fn min_max_slice(data: &[f64]) -> (f64, f64) {
1262 data.iter()
1263 .copied()
1264 .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), v| {
1265 (mn.min(v), mx.max(v))
1266 })
1267 }
1268}