1use crate::diag::PathError;
2use crate::path::OutOfRangeMode;
3use crate::path::autodiff::Jet3;
4use crate::path::spline::{SplineConfig, SplinePath};
5use nalgebra::DMatrix;
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>;
12
13#[derive(Debug)]
18pub struct PathDerivatives {
19 pub q: DMatrix<f64>,
20 pub dq: Option<DMatrix<f64>>,
21 pub ddq: Option<DMatrix<f64>>,
22 pub dddq: Option<DMatrix<f64>>,
23}
24
25#[derive(Clone, Copy, PartialEq, Eq)]
27enum Order {
28 Zero, Two, Three, }
32
33pub struct Path {
42 dim: usize,
43 s_min: f64,
44 s_max: f64,
45 out_of_range_mode: OutOfRangeMode,
46 repr: PathRepr,
47}
48
49enum PathRepr {
50 Parametric(ParametricFn),
52 Spline(SplinePath),
54}
55
56impl Path {
57 pub fn from_parametric<F>(q_fn: F, s_min: f64, s_max: f64) -> Result<Self, PathError>
72 where
73 F: Fn(Jet3) -> Vec<Jet3> + Send + Sync + 'static,
74 {
75 validate_range(s_min, s_max)?;
76
77 let sample = q_fn(Jet3::constant((s_min + s_max) * 0.5));
78 if sample.is_empty() {
79 return Err(PathError::InvalidDimension { dim: 0 });
80 }
81 let dim = sample.len();
82
83 Ok(Self {
84 dim,
85 s_min,
86 s_max,
87 out_of_range_mode: OutOfRangeMode::Error,
88 repr: PathRepr::Parametric(Arc::new(q_fn)),
89 })
90 }
91
92 pub fn from_waypoints(waypoints: &DMatrix<f64>, cfg: SplineConfig) -> Result<Self, PathError> {
110 if waypoints.nrows() == 0 {
111 return Err(PathError::InvalidDimension {
112 dim: waypoints.nrows(),
113 });
114 }
115 if waypoints.ncols() < 2 {
116 return Err(PathError::NotEnoughWaypoints {
117 n: waypoints.ncols(),
118 });
119 }
120 if cfg.order < 3 {
121 return Err(PathError::InvalidOrder { order: cfg.order });
122 }
123 validate_range(cfg.s_min, cfg.s_max)?;
124
125 let spline = SplinePath::from_waypoints(waypoints, &cfg)?;
126
127 Ok(Self {
128 dim: waypoints.nrows(),
129 s_min: spline.s_min,
130 s_max: spline.s_max,
131 out_of_range_mode: spline.out_of_range_mode,
132 repr: PathRepr::Spline(spline),
133 })
134 }
135
136 #[inline(always)]
138 pub fn dim(&self) -> usize {
139 self.dim
140 }
141
142 #[inline(always)]
144 pub fn s_range(&self) -> (f64, f64) {
145 (self.s_min, self.s_max)
146 }
147
148 pub fn evaluate_q(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
160 self.evaluate_impl(s, Order::Zero)
161 }
162
163 pub fn evaluate_up_to_2nd(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
172 self.evaluate_impl(s, Order::Two)
173 }
174
175 pub fn evaluate_up_to_3rd(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
211 self.evaluate_impl(s, Order::Three)
212 }
213
214 fn evaluate_impl(&self, s: &[f64], order: Order) -> Result<PathDerivatives, PathError> {
217 let n = s.len();
218 let dim = self.dim;
219
220 let mut q = vec![0.0f64; dim * n];
222 let mut dq = (order != Order::Zero).then(|| vec![0.0f64; dim * n]);
223 let mut ddq = (order != Order::Zero).then(|| vec![0.0f64; dim * n]);
224 let mut dddq = (order == Order::Three).then(|| vec![0.0f64; dim * n]);
225
226 match &self.repr {
227 PathRepr::Parametric(eval_fn) => {
228 eval_parametric(
229 eval_fn,
230 self,
231 dim,
232 (s, &mut q, &mut dq, &mut ddq, &mut dddq),
233 )?;
234 }
235 PathRepr::Spline(spline) => {
236 eval_spline(spline, self, dim, (s, &mut q, &mut dq, &mut ddq, &mut dddq))?;
237 }
238 }
239
240 Ok(PathDerivatives {
241 q: DMatrix::from_vec(dim, n, q),
242 dq: dq.map(|v| DMatrix::from_vec(dim, n, v)),
243 ddq: ddq.map(|v| DMatrix::from_vec(dim, n, v)),
244 dddq: dddq.map(|v| DMatrix::from_vec(dim, n, v)),
245 })
246 }
247
248 #[inline(always)]
249 fn validate_s(&self, s: f64, index: usize) -> Result<f64, PathError> {
250 match self.out_of_range_mode {
251 OutOfRangeMode::Error => {
252 if s < self.s_min - EPS_RANGE || s > self.s_max + EPS_RANGE {
253 return Err(PathError::OutOfRangeS {
254 s_min: self.s_min,
255 s_max: self.s_max,
256 index,
257 value: s,
258 });
259 }
260 Ok(s.clamp(self.s_min, self.s_max))
261 }
262 OutOfRangeMode::Clamp => Ok(s.clamp(self.s_min, self.s_max)),
263 }
264 }
265}
266
267type EvalInput<'a> = (
271 &'a [f64], &'a mut [f64], &'a mut Option<Vec<f64>>, &'a mut Option<Vec<f64>>, &'a mut Option<Vec<f64>>, );
277
278fn eval_parametric(
283 eval_fn: &ParametricFn,
284 path: &Path,
285 dim: usize,
286 input_eval: EvalInput,
287) -> Result<(), PathError> {
288 let (s_values, q, dq, ddq, dddq) = input_eval;
289 let n = s_values.len();
290
291 let dq_chunks: Vec<Option<&mut [f64]>> = dq.as_deref_mut().map_or_else(
296 || (0..n).map(|_| None).collect(),
297 |v| v.chunks_mut(dim).map(Some).collect(),
298 );
299 let ddq_chunks: Vec<Option<&mut [f64]>> = ddq.as_deref_mut().map_or_else(
300 || (0..n).map(|_| None).collect(),
301 |v| v.chunks_mut(dim).map(Some).collect(),
302 );
303 let dddq_chunks: Vec<Option<&mut [f64]>> = dddq.as_deref_mut().map_or_else(
304 || (0..n).map(|_| None).collect(),
305 |v| v.chunks_mut(dim).map(Some).collect(),
306 );
307
308 s_values
309 .par_iter()
310 .enumerate()
311 .zip(q.par_chunks_mut(dim))
312 .zip(dq_chunks.into_par_iter())
313 .zip(ddq_chunks.into_par_iter())
314 .zip(dddq_chunks.into_par_iter())
315 .map(
316 |(((((j, &s_raw), q_col), mut dq_col), mut ddq_col), mut dddq_col)| {
317 let s_curr = path.validate_s(s_raw, j)?;
318 let vals = eval_fn(Jet3::seed(s_curr));
319 if vals.len() != dim {
320 return Err(PathError::DimensionMismatch);
321 }
322 for (i, jet) in vals.iter().enumerate() {
323 q_col[i] = jet.v;
324 if let Some(ref mut b) = dq_col {
325 b[i] = jet.d1;
326 }
327 if let Some(ref mut b) = ddq_col {
328 b[i] = jet.d2;
329 }
330 if let Some(ref mut b) = dddq_col {
331 b[i] = jet.d3;
332 }
333 }
334 Ok(())
335 },
336 )
337 .collect()
338}
339
340fn eval_spline(
348 spline: &SplinePath,
349 path: &Path,
350 dim: usize,
351 input_eval: EvalInput,
352) -> Result<(), PathError> {
353 let (s_values, q, dq, ddq, dddq) = input_eval;
354 match (dq.as_deref_mut(), ddq.as_deref_mut(), dddq.as_deref_mut()) {
355 (None, None, None) => s_values
357 .par_iter()
358 .enumerate()
359 .zip(q.par_chunks_mut(dim))
360 .map(|((j, &s_raw), q_col)| -> Result<(), PathError> {
361 let s_curr = path.validate_s(s_raw, j)?;
362 let (mut no_dq, mut no_ddq, mut no_dddq): ([f64; 0], [f64; 0], [f64; 0]) =
365 ([], [], []);
366 spline.eval_at::<0>(s_curr, dim, q_col, &mut no_dq, &mut no_ddq, &mut no_dddq);
367 Ok(())
368 })
369 .collect(),
370 (Some(dq_buf), Some(ddq_buf), None) => {
372 let dq_chunks: Vec<&mut [f64]> = dq_buf.chunks_mut(dim).collect();
373 let ddq_chunks: Vec<&mut [f64]> = ddq_buf.chunks_mut(dim).collect();
374 s_values
375 .par_iter()
376 .enumerate()
377 .zip(q.par_chunks_mut(dim))
378 .zip(dq_chunks.into_par_iter())
379 .zip(ddq_chunks.into_par_iter())
380 .map(
381 |((((j, &s_raw), q_col), dq_col), ddq_col)| -> Result<(), PathError> {
382 let s_curr = path.validate_s(s_raw, j)?;
383 let mut s4 = [];
384 spline.eval_at::<2>(s_curr, dim, q_col, dq_col, ddq_col, &mut s4);
385 Ok(())
386 },
387 )
388 .collect()
389 }
390 (Some(dq_buf), Some(ddq_buf), Some(dddq_buf)) => {
392 let dq_chunks: Vec<&mut [f64]> = dq_buf.chunks_mut(dim).collect();
393 let ddq_chunks: Vec<&mut [f64]> = ddq_buf.chunks_mut(dim).collect();
394 let dddq_chunks: Vec<&mut [f64]> = dddq_buf.chunks_mut(dim).collect();
395 s_values
396 .par_iter()
397 .enumerate()
398 .zip(q.par_chunks_mut(dim))
399 .zip(dq_chunks.into_par_iter())
400 .zip(ddq_chunks.into_par_iter())
401 .zip(dddq_chunks.into_par_iter())
402 .map(
403 |(((((j, &s_raw), q_col), dq_col), ddq_col), dddq_col)| -> Result<(), PathError> {
404 let s_curr = path.validate_s(s_raw, j)?;
405 spline.eval_at::<3>(s_curr, dim, q_col, dq_col, ddq_col, dddq_col);
406 Ok(())
407 },
408 )
409 .collect()
410 }
411 _ => unreachable!("unexpected dq/ddq/dddq combination"),
413 }
414}
415
416fn validate_range(s_min: f64, s_max: f64) -> Result<(), PathError> {
419 if !s_min.is_finite() || !s_max.is_finite() || s_max <= s_min {
420 return Err(PathError::InvalidRange { s_min, s_max });
421 }
422 Ok(())
423}
424
425#[cfg(test)]
426mod tests {
427 use super::PathDerivatives;
428 use crate::path::{Jet3, Path as PathModel, PathError, SplineConfig, cos, exp, sin};
429 use nalgebra::DMatrix;
430 use plotters::prelude::*;
431 use rand::RngExt;
432 use std::error::Error;
433 use std::fs::create_dir_all;
434 use std::hint::black_box;
435 use std::path::Path as StdPath;
436 use std::time::Instant;
437
438 const DIM: usize = 6;
439
440 fn make_s(n: usize) -> DMatrix<f64> {
441 DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64)
442 }
443
444 fn make_parametric_path() -> Result<PathModel, PathError> {
445 PathModel::from_parametric(
446 |s: Jet3| {
447 vec![
448 sin(s),
449 cos(s),
450 exp(0.3 * s) - 1.0,
451 s + 0.1 * s * s - 0.01 * s * s * s * s,
452 sin(2.0 * s) + 0.15 * cos(3.0 * s),
453 sin(s) * cos(s),
454 ]
455 },
456 0.0,
457 1.0,
458 )
459 }
460
461 fn make_waypoints(n_pts: usize) -> DMatrix<f64> {
462 let mut rng = rand::rng();
463 let mut waypoints = DMatrix::<f64>::zeros(DIM, n_pts);
465 for mut row in waypoints.row_iter_mut() {
466 row[0] = rng.random_range(-1.0..1.0);
467 for j in 1..n_pts {
468 let step = rng.random_range(-0.35..0.35);
469 row[j] = row[j - 1] + step;
470 }
471 }
472 waypoints
473 }
474
475 #[test]
476 fn test_parametric_autodiff_dim6() -> Result<(), PathError> {
477 let path = make_parametric_path()?;
478
479 let n = 300;
480 let s = make_s(n);
481 let out = path.evaluate_up_to_3rd(s.as_slice())?;
482 let dq = out.dq.as_ref().unwrap();
483 let ddq = out.ddq.as_ref().unwrap();
484 let dddq = out.dddq.as_ref().unwrap();
485
486 s.as_slice().iter().enumerate().for_each(|(j, &x)| {
488 let e03x = (0.3 * x).exp();
489 let expected_q = [
490 x.sin(),
491 x.cos(),
492 e03x - 1.0,
493 x + 0.1 * x * x - 0.01 * x * x * x * x,
494 (2.0 * x).sin() + 0.15 * (3.0 * x).cos(),
495 x.sin() * x.cos(),
496 ];
497 let expected_dq = [
498 x.cos(),
499 -x.sin(),
500 0.3 * e03x,
501 1.0 + 0.2 * x - 0.04 * x * x * x,
502 2.0 * (2.0 * x).cos() - 0.45 * (3.0 * x).sin(),
503 (2.0 * x).cos(),
504 ];
505 let expected_ddq = [
506 -x.sin(),
507 -x.cos(),
508 0.09 * e03x,
509 0.2 - 0.12 * x * x,
510 -4.0 * (2.0 * x).sin() - 1.35 * (3.0 * x).cos(),
511 -2.0 * (2.0 * x).sin(),
512 ];
513 let expected_dddq = [
514 -x.cos(),
515 x.sin(),
516 0.027 * e03x,
517 -0.24 * x,
518 -8.0 * (2.0 * x).cos() + 4.05 * (3.0 * x).sin(),
519 -4.0 * (2.0 * x).cos(),
520 ];
521
522 for i in 0..DIM {
523 assert!(
524 (out.q[(i, j)] - expected_q[i]).abs() < 1e-10,
525 "q dim={i} idx={j}"
526 );
527 assert!(
528 (dq[(i, j)] - expected_dq[i]).abs() < 1e-10,
529 "dq dim={i} idx={j}"
530 );
531 assert!(
532 (ddq[(i, j)] - expected_ddq[i]).abs() < 1e-10,
533 "ddq dim={i} idx={j}"
534 );
535 assert!(
536 (dddq[(i, j)] - expected_dddq[i]).abs() < 1e-10,
537 "dddq dim={i} idx={j}"
538 );
539 }
540 });
541
542 Ok(())
543 }
544
545 #[test]
546 fn test_evaluate_q_only() -> Result<(), PathError> {
547 let path = make_parametric_path()?;
548 let n = 100;
549 let s = make_s(n);
550 let out = path.evaluate_q(s.as_slice())?;
551
552 assert!(out.dq.is_none());
553 assert!(out.ddq.is_none());
554 assert!(out.dddq.is_none());
555
556 for j in 0..n {
557 let x = s[(0, j)];
558 assert!((out.q[(0, j)] - x.sin()).abs() < 1e-10);
559 assert!((out.q[(1, j)] - x.cos()).abs() < 1e-10);
560 }
561
562 Ok(())
563 }
564
565 #[test]
566 fn test_evaluate_up_to_2nd() -> Result<(), PathError> {
567 let path = make_parametric_path()?;
568 let n = 100;
569 let s = make_s(n);
570 let out = path.evaluate_up_to_2nd(s.as_slice())?;
571 let dq = out.dq.as_ref().unwrap();
572 let ddq = out.ddq.as_ref().unwrap();
573
574 assert!(out.dddq.is_none());
575
576 for j in 0..n {
577 let x = s[(0, j)];
578 assert!((out.q[(0, j)] - x.sin()).abs() < 1e-10);
579 assert!((dq[(0, j)] - x.cos()).abs() < 1e-10);
580 assert!((ddq[(0, j)] - (-x.sin())).abs() < 1e-10);
581 }
582
583 Ok(())
584 }
585
586 #[test]
587 fn test_quintic_spline_interpolates_waypoints_dim6() -> Result<(), PathError> {
588 let n_pts = 25;
589 let waypoints = make_waypoints(n_pts);
590
591 let cfg = SplineConfig::default();
592 let path = PathModel::from_waypoints(&waypoints, cfg)?;
593
594 let s = DMatrix::<f64>::from_fn(1, n_pts, |_, j| j as f64 / (n_pts - 1) as f64);
595 let out = path.evaluate_up_to_3rd(s.as_slice())?;
596 let dq = out.dq.as_ref().unwrap();
597 let ddq = out.ddq.as_ref().unwrap();
598 let dddq = out.dddq.as_ref().unwrap();
599
600 for (i, j) in (0..DIM).flat_map(|i| (0..n_pts).map(move |j| (i, j))) {
603 assert!((out.q[(i, j)] - waypoints[(i, j)]).abs() < 1e-8);
604 assert!(dq[(i, j)].is_finite());
605 assert!(ddq[(i, j)].is_finite());
606 assert!(dddq[(i, j)].is_finite());
607 }
608
609 Ok(())
610 }
611
612 #[test]
613 fn test_s_out_of_range_error_dim6() -> Result<(), PathError> {
614 let waypoints = make_waypoints(12);
615 let path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
616 let s = DMatrix::<f64>::from_row_slice(1, 3, &[-0.1, 0.5, 1.1]);
617 let err = path.evaluate_up_to_3rd(s.as_slice()).unwrap_err();
618 match err {
619 PathError::OutOfRangeS { .. } => {}
620 _ => panic!("expected OutOfRangeS"),
621 }
622 Ok(())
623 }
624
625 #[test]
626 fn test_benchmark_parametric_and_spline_dim6() -> Result<(), PathError> {
627 let n_eval = 3000;
628 let n_repeat = 8;
629 let s = make_s(n_eval);
630
631 let start = Instant::now();
632 let param_path = make_parametric_path()?;
633 let tc_build_param = start.elapsed().as_secs_f64() * 1e3;
634
635 let start = Instant::now();
636 for _ in 0..n_repeat {
637 let out = param_path.evaluate_up_to_3rd(s.as_slice())?;
638 black_box(out.q[(0, 0)]);
639 }
640 let tc_eval_param = start.elapsed().as_secs_f64() * 1e3 / n_repeat as f64;
641 println!(
642 "[bench][parametric][dim=6] build={tc_build_param:.3} ms eval={tc_eval_param:.3} ms (N={n_eval})"
643 );
644
645 let n_waypoints_list = [16usize, 32, 64, 128, 192, 256, 512, 1024];
646 for &n_pts in &n_waypoints_list {
647 let waypoints = make_waypoints(n_pts);
648 let start = Instant::now();
649 let spline_path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
650 let tc_build = start.elapsed().as_secs_f64() * 1e3;
651
652 let start = Instant::now();
653 for _ in 0..n_repeat {
654 let out = spline_path.evaluate_up_to_3rd(s.as_slice())?;
655 black_box(out.q[(0, 0)]);
656 }
657 let tc_eval = start.elapsed().as_secs_f64() * 1e3 / n_repeat as f64;
658
659 println!(
660 "[bench][spline][dim=6][n_pts={n_pts}] build={tc_build:.3} ms eval={tc_eval:.3} ms"
661 );
662 }
663
664 Ok(())
665 }
666
667 #[test]
668 #[ignore = "plotting"]
669 fn test_plot_parametric_and_spline_derivatives() -> Result<(), Box<dyn Error>> {
670 let dir = "data/path_plots";
671 create_dir_all(dir)?;
672
673 let n = 600;
674 let s = make_s(n);
675 let s_vec: Vec<f64> = (0..n).map(|j| s[(0, j)]).collect();
676
677 let param_path = make_parametric_path()?;
678 let param_out = param_path.evaluate_up_to_3rd(s.as_slice())?;
679 plot_grid_4x6(
680 &format!("{dir}/parametric_dim6_grid.png"),
681 "parametric dim=6",
682 &s_vec,
683 ¶m_out,
684 None,
685 )?;
686
687 let n_pts = 10;
688 let waypoints = make_waypoints(n_pts);
689 let spline_path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
690 let spline_out = spline_path.evaluate_up_to_3rd(s.as_slice())?;
691 let wp_s: Vec<f64> = (0..n_pts).map(|j| j as f64 / (n_pts - 1) as f64).collect();
692 plot_grid_4x6(
693 &format!("{dir}/spline_order5_dim6_grid.png"),
694 "spline order=5 dim=6",
695 &s_vec,
696 &spline_out,
697 Some((&wp_s, &waypoints)),
698 )?;
699
700 Ok(())
701 }
702
703 fn plot_grid_4x6(
704 file: &str,
705 title: &str,
706 s: &[f64],
707 data: &PathDerivatives,
708 waypoints: Option<(&[f64], &DMatrix<f64>)>,
709 ) -> Result<(), Box<dyn Error>> {
710 if let Some(parent) = StdPath::new(file).parent() {
711 create_dir_all(parent)?;
712 }
713
714 let root = BitMapBackend::new(file, (2400, 1400)).into_drawing_area();
715 root.fill(&WHITE)?;
716
717 let empty = DMatrix::<f64>::zeros(0, 0);
718 let dq = data.dq.as_ref().unwrap_or(&empty);
719 let ddq = data.ddq.as_ref().unwrap_or(&empty);
720 let dddq = data.dddq.as_ref().unwrap_or(&empty);
721
722 let areas = root.split_evenly((4, DIM));
723 let mats = [&data.q, dq, ddq, dddq];
724 let row_names = ["q", "dq", "ddq", "dddq"];
725
726 for row in 0..4 {
727 for col in 0..DIM {
728 let area = &areas[row * DIM + col];
729 let series = mat_row(mats[row], col);
730 let (mut y_min, mut y_max) = min_max_slice(&series);
731 if (y_max - y_min).abs() < 1e-12 {
732 y_min -= 1.0;
733 y_max += 1.0;
734 } else {
735 let pad = 0.08 * (y_max - y_min);
736 y_min -= pad;
737 y_max += pad;
738 }
739
740 let mut chart = ChartBuilder::on(area)
741 .margin(8)
742 .caption(
743 format!("{} j{}", row_names[row], col + 1),
744 ("sans-serif", 16),
745 )
746 .x_label_area_size(24)
747 .y_label_area_size(38)
748 .build_cartesian_2d(s[0]..s[s.len() - 1], y_min..y_max)?;
749
750 chart
751 .configure_mesh()
752 .x_desc(if row == 3 { "s" } else { "" })
753 .y_desc("")
754 .draw()?;
755
756 chart.draw_series(LineSeries::new(
757 (0..s.len()).map(|j| (s[j], series[j])),
758 &BLUE,
759 ))?;
760
761 if row == 0
762 && let Some((s_wp, q_wp)) = waypoints
763 {
764 chart.draw_series(
765 s_wp.iter()
766 .zip(q_wp.row(col).iter())
767 .map(|(&xs, &ys)| Circle::new((xs, ys), 2, RED.filled())),
768 )?;
769 }
770 }
771 }
772
773 root.titled(title, ("sans-serif", 28))?;
774 root.present()?;
775 Ok(())
776 }
777
778 fn mat_row(mat: &DMatrix<f64>, row: usize) -> Vec<f64> {
779 mat.row(row).iter().copied().collect()
780 }
781
782 fn min_max_slice(data: &[f64]) -> (f64, f64) {
783 data.iter()
784 .copied()
785 .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), v| {
786 (mn.min(v), mx.max(v))
787 })
788 }
789}