copp\path/path_core.rs
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
11/// Shared analytic path function used by [`Path::from_parametric`].
12///
13/// The input is a seeded [`Jet3`] scalar representing path parameter `s`, and
14/// the returned vector contains one [`Jet3`] per path dimension.
15pub type ParametricFn = Arc<dyn Fn(Jet3) -> Vec<Jet3> + Send + Sync>;
16
17/// User-provided path evaluator with explicit derivatives up to second order.
18///
19/// Implement this trait when path derivatives are already available from an
20/// external model, library, or hand-written analytic formula. Unlike
21/// [`Path::from_parametric`], no automatic differentiation is performed: the
22/// evaluator writes `q`, `dq`, and `ddq` directly into pre-allocated
23/// column-major buffers.
24///
25/// Buffer layout is always `dim x s.len()` in column-major order:
26/// `buffer[row + col * dim]` corresponds to path dimension `row` at sample
27/// `s[col]`. Empty `s` slices are valid no-ops.
28///
29/// See [`Path::from_evaluator_2nd`] for a complete constructor example.
30pub trait PathEvaluator2nd: Send + Sync {
31 /// Return the path dimension.
32 ///
33 /// The dimension must remain stable for the lifetime of the evaluator and
34 /// must be greater than zero.
35 fn dim(&self) -> usize;
36
37 /// Evaluate position only.
38 ///
39 /// The default implementation calls [`PathEvaluator2nd::evaluate_up_to_2nd`]
40 /// with temporary derivative buffers. Override this method if computing
41 /// only `q` is substantially cheaper for the evaluator.
42 fn evaluate_q(&self, s: &[f64], q: &mut [f64]) -> Result<(), PathError> {
43 let mut dq = vec![0.0; q.len()];
44 let mut ddq = vec![0.0; q.len()];
45 self.evaluate_up_to_2nd(s, q, &mut dq, &mut ddq)
46 }
47
48 /// Evaluate `q`, `dq`, and `ddq` at all supplied path parameters.
49 ///
50 /// All output buffers have length `dim() * s.len()` and use column-major
51 /// layout.
52 fn evaluate_up_to_2nd(
53 &self,
54 s: &[f64],
55 q: &mut [f64],
56 dq: &mut [f64],
57 ddq: &mut [f64],
58 ) -> Result<(), PathError>;
59}
60
61/// User-provided path evaluator with explicit derivatives up to third order.
62///
63/// This extends [`PathEvaluator2nd`] with jerk-level path derivatives. Use it
64/// when the path will be sampled by TOPP3/COPP3 workflows or any API that calls
65/// [`Path::evaluate_up_to_3rd`].
66///
67/// See [`Path::from_evaluator_3rd`] for a complete constructor example.
68pub trait PathEvaluator3rd: PathEvaluator2nd {
69 /// Evaluate `q`, `dq`, `ddq`, and `dddq` at all supplied path parameters.
70 ///
71 /// All output buffers have length `dim() * s.len()` and use column-major
72 /// layout.
73 fn evaluate_up_to_3rd(
74 &self,
75 s: &[f64],
76 q: &mut [f64],
77 dq: &mut [f64],
78 ddq: &mut [f64],
79 dddq: &mut [f64],
80 ) -> Result<(), PathError>;
81}
82
83/// Compatibility alias for third-order explicit path evaluators.
84///
85/// New code should prefer [`PathEvaluator2nd`] or [`PathEvaluator3rd`] to make
86/// the supported derivative order explicit.
87pub trait PathEvaluator: PathEvaluator3rd {}
88
89impl<T: PathEvaluator3rd + ?Sized> PathEvaluator for T {}
90
91/// Output of path evaluation.
92///
93/// `dq`, `ddq`, `dddq` are `None` when the evaluation did not request them
94/// (e.g. `evaluate_q` only fills `q`; `evaluate_up_to_2nd` fills `q/dq/ddq`).
95#[derive(Debug)]
96pub struct PathDerivatives {
97 /// Position samples with shape `(dim, s.len())`.
98 pub q: DMatrix<f64>,
99 /// First derivative samples `dq/ds`, populated by second- and third-order evaluation.
100 pub dq: Option<DMatrix<f64>>,
101 /// Second derivative samples `d^2q/ds^2`, populated by second- and third-order evaluation.
102 pub ddq: Option<DMatrix<f64>>,
103 /// Third derivative samples `d^3q/ds^3`, populated only by third-order evaluation.
104 pub dddq: Option<DMatrix<f64>>,
105}
106
107/// How many derivative orders to compute.
108#[derive(Clone, Copy, PartialEq, Eq)]
109enum Order {
110 Zero, // q only
111 Two, // q, dq, ddq
112 Three, // q, dq, ddq, dddq
113}
114
115/// Unified path abstraction over parametric, spline, and evaluator representations.
116///
117/// Construct via [`Path::from_parametric`](crate::path::Path::from_parametric),
118/// [`Path::from_waypoints`](crate::path::Path::from_waypoints),
119/// [`Path::from_evaluator_2nd`](crate::path::Path::from_evaluator_2nd), or
120/// [`Path::from_evaluator_3rd`](crate::path::Path::from_evaluator_3rd), then
121/// query a batch of parameter values with the `evaluate_*` family of methods.
122///
123/// The valid parameter domain is `[s_min, s_max]` (set at construction time).
124/// Out-of-range behaviour is controlled by [`OutOfRangeMode`](crate::path::OutOfRangeMode): the default is to
125/// return an error; it can be changed to silent clamping.
126pub struct Path {
127 dim: usize,
128 s_min: f64,
129 s_max: f64,
130 out_of_range_mode: OutOfRangeMode,
131 repr: PathRepr,
132}
133
134enum PathRepr {
135 /// Closure-based path evaluated via third-order forward AD.
136 Parametric(ParametricFn),
137 /// Piecewise-polynomial path built from waypoints.
138 Spline(SplinePath),
139 /// User-provided path evaluator with explicit derivatives up to second order.
140 Evaluator2nd(Arc<dyn PathEvaluator2nd>),
141 /// User-provided path evaluator with explicit derivatives up to third order.
142 Evaluator3rd(Arc<dyn PathEvaluator3rd>),
143}
144
145impl Path {
146 /// Build a parametric path from an analytic closure.
147 ///
148 /// Derivatives up to third order are computed automatically via [`Jet3`](crate::path::Jet3)
149 /// forward-mode AD. The closure only needs to express `q(s)` symbolically;
150 /// no manual differentiation is required.
151 ///
152 /// # Arguments
153 /// - `q_fn` : closure mapping scalar `s` to a `dim`-dimensional position vector
154 /// - `s_min` : lower bound of the path parameter
155 /// - `s_max` : upper bound of the path parameter (`s_max > s_min` required)
156 ///
157 /// # Errors
158 /// - [`PathError::InvalidRange`](crate::diag::PathError::InvalidRange) : `s_min >= s_max` or either value is non-finite
159 /// - [`PathError::InvalidDimension`](crate::diag::PathError::InvalidDimension) : closure returned an empty vector
160 ///
161 /// # Example
162 /// The example below builds a two-dimensional analytic path and evaluates
163 /// derivatives produced by automatic differentiation.
164 ///
165 /// ```rust
166 /// # fn main() -> Result<(), copp::diag::CoppError> {
167 /// use copp::path::autodiff::Jet3;
168 /// use copp::path::{cos, sin, Path};
169 ///
170 /// let path = Path::from_parametric(|s: Jet3| vec![sin(s), cos(s)], 0.0, 1.0)?;
171 ///
172 /// let s = [0.0, 0.25, 0.5, 0.75, 1.0];
173 /// let out = path.evaluate_up_to_3rd(s.as_slice())?;
174 /// assert_eq!(out.q.shape(), (2, 5));
175 /// assert!(out.dddq.is_some());
176 /// # Ok(())
177 /// # }
178 /// ```
179 pub fn from_parametric<F>(q_fn: F, s_min: f64, s_max: f64) -> Result<Self, PathError>
180 where
181 F: Fn(Jet3) -> Vec<Jet3> + Send + Sync + 'static,
182 {
183 validate_range(s_min, s_max)?;
184
185 let sample = q_fn(Jet3::constant((s_min + s_max) * 0.5));
186 if sample.is_empty() {
187 return Err(PathError::InvalidDimension { dim: 0 });
188 }
189 let dim = sample.len();
190
191 Ok(Self {
192 dim,
193 s_min,
194 s_max,
195 out_of_range_mode: OutOfRangeMode::Error,
196 repr: PathRepr::Parametric(Arc::new(q_fn)),
197 })
198 }
199
200 /// Build a path from an evaluator that provides explicit derivatives up to second order.
201 ///
202 /// Use this constructor when derivatives are already available from an
203 /// external source and automatic differentiation is not desired. The
204 /// evaluator is owned by the returned [`Path`] through an internal
205 /// [`Arc`], so the path can be passed around without borrowing the original
206 /// value.
207 ///
208 /// # Arguments
209 /// - `evaluator`: object that writes column-major derivative buffers
210 /// - `s_min`: lower bound of the path parameter
211 /// - `s_max`: upper bound of the path parameter (`s_max > s_min` required)
212 ///
213 /// # Errors
214 /// - [`PathError::InvalidRange`](crate::diag::PathError::InvalidRange): `s_min >= s_max` or either value is non-finite
215 /// - [`PathError::InvalidDimension`](crate::diag::PathError::InvalidDimension): evaluator dimension is zero
216 ///
217 /// # Example
218 /// The example below wraps a two-dimensional external evaluator that writes
219 /// explicit `q/dq/ddq` buffers in column-major order.
220 ///
221 /// ```rust
222 /// # fn main() -> Result<(), copp::diag::CoppError> {
223 /// use copp::diag::PathError;
224 /// use copp::path::{Path, PathEvaluator2nd};
225 ///
226 /// struct NormalizedEvaluator2nd;
227 ///
228 /// impl PathEvaluator2nd for NormalizedEvaluator2nd {
229 /// fn dim(&self) -> usize {
230 /// 2
231 /// }
232 ///
233 /// fn evaluate_up_to_2nd(
234 /// &self,
235 /// s: &[f64],
236 /// q: &mut [f64],
237 /// dq: &mut [f64],
238 /// ddq: &mut [f64],
239 /// ) -> Result<(), PathError> {
240 /// for (col, &sj) in s.iter().enumerate() {
241 /// let row0 = 2 * col;
242 /// q[row0] = 0.5 * sj * sj;
243 /// q[row0 + 1] = sj;
244 /// dq[row0] = sj;
245 /// dq[row0 + 1] = 1.0;
246 /// ddq[row0] = 1.0;
247 /// ddq[row0 + 1] = 0.0;
248 /// }
249 /// Ok(())
250 /// }
251 /// }
252 ///
253 /// let path = Path::from_evaluator_2nd(NormalizedEvaluator2nd, 0.0, 1.0)?;
254 /// let out = path.evaluate_up_to_2nd(&[0.0, 0.5])?;
255 /// assert_eq!(out.q.shape(), (2, 2));
256 /// assert_eq!(out.q[(0, 1)], 0.125);
257 /// assert!(out.dddq.is_none());
258 /// # Ok(())
259 /// # }
260 /// ```
261 pub fn from_evaluator_2nd<E>(evaluator: E, s_min: f64, s_max: f64) -> Result<Self, PathError>
262 where
263 E: PathEvaluator2nd + 'static,
264 {
265 Self::from_shared_evaluator_2nd(Arc::new(evaluator), s_min, s_max)
266 }
267
268 /// Build a path from a shared explicit-derivative evaluator up to second order.
269 ///
270 /// This is the same representation as [`Path::from_evaluator_2nd`], but accepts
271 /// an already shared evaluator. It is useful when multiple paths or
272 /// application components need to hold the same evaluator object.
273 ///
274 /// # Errors
275 /// - [`PathError::InvalidRange`](crate::diag::PathError::InvalidRange): `s_min >= s_max` or either value is non-finite
276 /// - [`PathError::InvalidDimension`](crate::diag::PathError::InvalidDimension): evaluator dimension is zero
277 pub fn from_shared_evaluator_2nd(
278 evaluator: Arc<dyn PathEvaluator2nd>,
279 s_min: f64,
280 s_max: f64,
281 ) -> Result<Self, PathError> {
282 validate_range(s_min, s_max)?;
283
284 let dim = evaluator.dim();
285 if dim == 0 {
286 return Err(PathError::InvalidDimension { dim });
287 }
288
289 Ok(Self {
290 dim,
291 s_min,
292 s_max,
293 out_of_range_mode: OutOfRangeMode::Error,
294 repr: PathRepr::Evaluator2nd(evaluator),
295 })
296 }
297
298 /// Build a path from an evaluator that provides explicit derivatives up to third order.
299 ///
300 /// This is the constructor to use when the path will be evaluated by
301 /// third-order APIs such as TOPP3/COPP3 sampling. For TOPP2/COPP2-only
302 /// usage, [`Path::from_evaluator_2nd`] avoids requiring a third derivative
303 /// implementation.
304 ///
305 /// # Errors
306 /// - [`PathError::InvalidRange`](crate::diag::PathError::InvalidRange): `s_min >= s_max` or either value is non-finite
307 /// - [`PathError::InvalidDimension`](crate::diag::PathError::InvalidDimension): evaluator dimension is zero
308 ///
309 /// # Example
310 /// The example below extends a two-dimensional explicit evaluator to third
311 /// order by writing jerk samples into the `dddq` buffer.
312 ///
313 /// ```rust
314 /// # fn main() -> Result<(), copp::diag::CoppError> {
315 /// use copp::diag::PathError;
316 /// use copp::path::{Path, PathEvaluator2nd, PathEvaluator3rd};
317 ///
318 /// struct NormalizedEvaluator3rd;
319 ///
320 /// impl PathEvaluator2nd for NormalizedEvaluator3rd {
321 /// fn dim(&self) -> usize {
322 /// 2
323 /// }
324 ///
325 /// fn evaluate_up_to_2nd(
326 /// &self,
327 /// s: &[f64],
328 /// q: &mut [f64],
329 /// dq: &mut [f64],
330 /// ddq: &mut [f64],
331 /// ) -> Result<(), PathError> {
332 /// for (col, &sj) in s.iter().enumerate() {
333 /// let row0 = 2 * col;
334 /// q[row0] = 0.5 * sj * sj;
335 /// q[row0 + 1] = sj;
336 /// dq[row0] = sj;
337 /// dq[row0 + 1] = 1.0;
338 /// ddq[row0] = 1.0;
339 /// ddq[row0 + 1] = 0.0;
340 /// }
341 /// Ok(())
342 /// }
343 /// }
344 ///
345 /// impl PathEvaluator3rd for NormalizedEvaluator3rd {
346 /// fn evaluate_up_to_3rd(
347 /// &self,
348 /// s: &[f64],
349 /// q: &mut [f64],
350 /// dq: &mut [f64],
351 /// ddq: &mut [f64],
352 /// dddq: &mut [f64],
353 /// ) -> Result<(), PathError> {
354 /// self.evaluate_up_to_2nd(s, q, dq, ddq)?;
355 /// dddq.fill(0.0);
356 /// Ok(())
357 /// }
358 /// }
359 ///
360 /// let path = Path::from_evaluator_3rd(NormalizedEvaluator3rd, 0.0, 1.0)?;
361 /// let out = path.evaluate_up_to_3rd(&[0.0, 0.5])?;
362 /// assert_eq!(out.q.shape(), (2, 2));
363 /// assert_eq!(out.q[(0, 1)], 0.125);
364 /// assert!(out.dddq.is_some());
365 /// # Ok(())
366 /// # }
367 /// ```
368 pub fn from_evaluator_3rd<E>(evaluator: E, s_min: f64, s_max: f64) -> Result<Self, PathError>
369 where
370 E: PathEvaluator3rd + 'static,
371 {
372 Self::from_shared_evaluator_3rd(Arc::new(evaluator), s_min, s_max)
373 }
374
375 /// Build a path from a shared explicit-derivative evaluator up to third order.
376 ///
377 /// # Errors
378 /// - [`PathError::InvalidRange`](crate::diag::PathError::InvalidRange): `s_min >= s_max` or either value is non-finite
379 /// - [`PathError::InvalidDimension`](crate::diag::PathError::InvalidDimension): evaluator dimension is zero
380 pub fn from_shared_evaluator_3rd(
381 evaluator: Arc<dyn PathEvaluator3rd>,
382 s_min: f64,
383 s_max: f64,
384 ) -> Result<Self, PathError> {
385 validate_range(s_min, s_max)?;
386
387 let dim = evaluator.dim();
388 if dim == 0 {
389 return Err(PathError::InvalidDimension { dim });
390 }
391
392 Ok(Self {
393 dim,
394 s_min,
395 s_max,
396 out_of_range_mode: OutOfRangeMode::Error,
397 repr: PathRepr::Evaluator3rd(evaluator),
398 })
399 }
400
401 /// Build a path from a third-order explicit-derivative evaluator.
402 ///
403 /// This compatibility constructor is equivalent to
404 /// [`Path::from_evaluator_3rd`]. New code should prefer
405 /// [`Path::from_evaluator_2nd`] or [`Path::from_evaluator_3rd`] to make the
406 /// supported derivative order explicit.
407 pub fn from_evaluator<E>(evaluator: E, s_min: f64, s_max: f64) -> Result<Self, PathError>
408 where
409 E: PathEvaluator3rd + 'static,
410 {
411 Self::from_evaluator_3rd(evaluator, s_min, s_max)
412 }
413
414 /// Build a path from a shared third-order explicit-derivative evaluator.
415 ///
416 /// This compatibility constructor is equivalent to
417 /// [`Path::from_shared_evaluator_3rd`].
418 pub fn from_shared_evaluator(
419 evaluator: Arc<dyn PathEvaluator3rd>,
420 s_min: f64,
421 s_max: f64,
422 ) -> Result<Self, PathError> {
423 Self::from_shared_evaluator_3rd(evaluator, s_min, s_max)
424 }
425
426 /// Build a spline path by interpolating a waypoint matrix.
427 ///
428 /// Internally solves the Hermite spline system with an O(N) block-Thomas
429 /// algorithm; all dimensions are solved in parallel.
430 /// The default configuration ([`SplineConfig::default`](crate::path::SplineConfig::default)) uses a quintic
431 /// (order-5) spline with `s in [0, 1]`.
432 ///
433 /// # Arguments
434 /// - `waypoints` : matrix of shape `(dim, n_points)`; each column is one waypoint
435 /// - `cfg` : spline configuration (order, parameter range, boundary derivatives, out-of-range mode)
436 ///
437 /// # Errors
438 /// - [`PathError::InvalidDimension`](crate::diag::PathError::InvalidDimension) : `waypoints` has zero rows
439 /// - [`PathError::NotEnoughWaypoints`](crate::diag::PathError::NotEnoughWaypoints) : fewer than 2 columns
440 /// - [`PathError::InvalidOrder`](crate::diag::PathError::InvalidOrder) : `order < 3`
441 /// - [`PathError::InvalidRange`](crate::diag::PathError::InvalidRange) : invalid parameter range
442 /// - [`PathError::SingularSystem`](crate::diag::PathError::SingularSystem) : spline system is singular (extremely rare)
443 ///
444 /// # Example
445 /// The example below builds a spline through two-dimensional waypoints and
446 /// evaluates position samples.
447 ///
448 /// ```rust
449 /// # fn main() -> Result<(), copp::diag::CoppError> {
450 /// use copp::path::{Path, SplineConfig};
451 /// use nalgebra::DMatrix;
452 ///
453 /// let waypoints = DMatrix::from_row_slice(
454 /// 2,
455 /// 5,
456 /// &[
457 /// 0.0, 0.25, 0.5, 0.75, 1.0,
458 /// 0.0, 0.1, -0.1, 0.2, 0.0,
459 /// ],
460 /// );
461 /// let path = Path::from_waypoints(&waypoints, SplineConfig::default())?;
462 ///
463 /// let s = [0.0, 0.5, 1.0];
464 /// let out = path.evaluate_q(s.as_slice())?;
465 /// assert_eq!(out.q.shape(), (2, 3));
466 /// assert!(out.dq.is_none());
467 /// # Ok(())
468 /// # }
469 /// ```
470 pub fn from_waypoints(waypoints: &DMatrix<f64>, cfg: SplineConfig) -> Result<Self, PathError> {
471 Self::from_waypoints_view(waypoints.as_view(), cfg)
472 }
473
474 /// Build a spline path from a borrowed waypoint matrix view.
475 ///
476 /// This accepts nalgebra views such as `waypoints.as_view()` and compatible
477 /// strided column-major views. See [`Path::from_waypoints`] for the full
478 /// interpolation semantics, configuration, and error conditions.
479 pub fn from_waypoints_view(
480 waypoints: DMatrixView<'_, f64>,
481 cfg: SplineConfig,
482 ) -> Result<Self, PathError> {
483 if waypoints.nrows() == 0 {
484 return Err(PathError::InvalidDimension {
485 dim: waypoints.nrows(),
486 });
487 }
488 if waypoints.ncols() < 2 {
489 return Err(PathError::NotEnoughWaypoints {
490 n: waypoints.ncols(),
491 });
492 }
493 if cfg.order < 3 {
494 return Err(PathError::InvalidOrder { order: cfg.order });
495 }
496 validate_range(cfg.s_min, cfg.s_max)?;
497
498 let spline = SplinePath::from_waypoints_view(waypoints, &cfg)?;
499
500 Ok(Self {
501 dim: waypoints.nrows(),
502 s_min: spline.s_min,
503 s_max: spline.s_max,
504 out_of_range_mode: spline.out_of_range_mode,
505 repr: PathRepr::Spline(spline),
506 })
507 }
508
509 /// Returns the spatial dimension (number of joints) of the path.
510 #[inline(always)]
511 pub fn dim(&self) -> usize {
512 self.dim
513 }
514
515 /// Returns the valid parameter range `(s_min, s_max)`.
516 #[inline(always)]
517 pub fn s_range(&self) -> (f64, f64) {
518 (self.s_min, self.s_max)
519 }
520
521 /// Evaluate position `q` only at the query points (cheapest; no derivatives).
522 ///
523 /// # Arguments
524 /// - `s` : one-dimensional parameter samples (length `N`)
525 ///
526 /// # Returns
527 /// [`PathDerivatives`](crate::path::PathDerivatives) with `dq / ddq / dddq` all `None`;
528 /// `q` has shape `(dim, N)`.
529 ///
530 /// # Errors
531 /// - [`PathError::OutOfRangeS`] : a query value is out of range (only in `Error` mode)
532 pub fn evaluate_q(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
533 self.evaluate_impl(s, Order::Zero)
534 }
535
536 /// Evaluate position, velocity, and acceleration (`q`, `dq`, `ddq`); jerk is not computed.
537 ///
538 /// # Arguments
539 /// - `s` : one-dimensional parameter samples (length `N`)
540 ///
541 /// # Returns
542 /// [`PathDerivatives`](crate::path::PathDerivatives) with `dddq = None`;
543 /// `q / dq / ddq` each have shape `(dim, N)`.
544 pub fn evaluate_up_to_2nd(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
545 self.evaluate_impl(s, Order::Two)
546 }
547
548 /// Evaluate position and all three derivative orders (`q`, `dq`, `ddq`, `dddq`).
549 ///
550 /// This is the most expensive evaluation method. If jerk is not needed,
551 /// prefer [`evaluate_up_to_2nd`].
552 ///
553 /// # Arguments
554 /// - `s` : one-dimensional parameter samples (length `N`)
555 ///
556 /// # Returns
557 /// [`PathDerivatives`](crate::path::PathDerivatives) with all four fields populated; each matrix has shape `(dim, N)`.
558 ///
559 /// # Errors
560 /// - [`PathError::OutOfRangeS`] : a query value is out of range
561 ///
562 /// # Example
563 /// The example below evaluates an analytic path up to jerk using automatic
564 /// differentiation.
565 ///
566 /// ```rust, no_run
567 /// use copp::path::{Path, sin, cos};
568 /// use copp::path::autodiff::Jet3;
569 ///
570 /// let path = Path::from_parametric(
571 /// |s: Jet3| vec![sin(s), cos(s)],
572 /// 0.0, 1.0,
573 /// ).unwrap();
574 ///
575 /// let s = [0.0, 0.25, 0.5, 0.75, 1.0];
576 /// let out = path.evaluate_up_to_3rd(&s).unwrap();
577 ///
578 /// let dq = out.dq.as_ref().unwrap();
579 /// let dddq = out.dddq.as_ref().unwrap();
580 /// // dim 0 is sin(s); its first derivative is cos(s)
581 /// assert!((dq[(0, 0)] - 1.0_f64.cos()).abs() < 1e-10);
582 /// // dim 1 is cos(s); its third derivative is sin(s)
583 /// assert!((dddq[(1, 0)] - 0.0_f64.sin()).abs() < 1e-10);
584 /// ```
585 ///
586 /// [`evaluate_up_to_2nd`]: Path::evaluate_up_to_2nd
587 pub fn evaluate_up_to_3rd(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
588 self.evaluate_impl(s, Order::Three)
589 }
590
591 // ── internal ─────────────────────────────────────────────────────────────
592
593 fn evaluate_impl(&self, s: &[f64], order: Order) -> Result<PathDerivatives, PathError> {
594 let n = s.len();
595 let dim = self.dim;
596
597 // Allocate output buffers; skip higher-order buffers when not needed.
598 let mut q = vec![0.0f64; dim * n];
599 let mut dq = (order != Order::Zero).then(|| vec![0.0f64; dim * n]);
600 let mut ddq = (order != Order::Zero).then(|| vec![0.0f64; dim * n]);
601 let mut dddq = (order == Order::Three).then(|| vec![0.0f64; dim * n]);
602
603 match &self.repr {
604 PathRepr::Parametric(eval_fn) => {
605 eval_parametric(
606 eval_fn,
607 self,
608 dim,
609 (s, &mut q, &mut dq, &mut ddq, &mut dddq),
610 )?;
611 }
612 PathRepr::Spline(spline) => {
613 eval_spline(spline, self, dim, (s, &mut q, &mut dq, &mut ddq, &mut dddq))?;
614 }
615 PathRepr::Evaluator2nd(evaluator) => {
616 eval_evaluator_2nd(
617 evaluator.as_ref(),
618 self,
619 dim,
620 (s, &mut q, &mut dq, &mut ddq, &mut dddq),
621 )?;
622 }
623 PathRepr::Evaluator3rd(evaluator) => {
624 eval_evaluator_3rd(
625 evaluator.as_ref(),
626 self,
627 dim,
628 (s, &mut q, &mut dq, &mut ddq, &mut dddq),
629 )?;
630 }
631 }
632
633 Ok(PathDerivatives {
634 q: DMatrix::from_vec(dim, n, q),
635 dq: dq.map(|v| DMatrix::from_vec(dim, n, v)),
636 ddq: ddq.map(|v| DMatrix::from_vec(dim, n, v)),
637 dddq: dddq.map(|v| DMatrix::from_vec(dim, n, v)),
638 })
639 }
640
641 #[inline(always)]
642 fn validate_s(&self, s: f64, index: usize) -> Result<f64, PathError> {
643 match self.out_of_range_mode {
644 OutOfRangeMode::Error => {
645 if s < self.s_min - EPS_RANGE || s > self.s_max + EPS_RANGE {
646 return Err(PathError::OutOfRangeS {
647 s_min: self.s_min,
648 s_max: self.s_max,
649 index,
650 value: s,
651 });
652 }
653 Ok(s.clamp(self.s_min, self.s_max))
654 }
655 OutOfRangeMode::Clamp => Ok(s.clamp(self.s_min, self.s_max)),
656 }
657 }
658}
659
660// ── free evaluation functions ─────────────────────────────────────────────────
661
662/// The input of evaluation functions.
663type EvalInput<'a> = (
664 &'a [f64], // s
665 &'a mut [f64], // q
666 &'a mut Option<Vec<f64>>, // dq
667 &'a mut Option<Vec<f64>>, // ddq
668 &'a mut Option<Vec<f64>>, // dddq
669);
670
671/// Evaluate a parametric path into pre-allocated column-major buffers.
672///
673/// Per-column parallelism via Rayon: validate + AD-evaluate + write in one pass.
674/// No intermediate `Vec<Vec<Jet3>>` allocation; results go directly into output buffers.
675fn eval_parametric(
676 eval_fn: &ParametricFn,
677 path: &Path,
678 dim: usize,
679 input_eval: EvalInput,
680) -> Result<(), PathError> {
681 let (s_values, q, dq, ddq, dddq) = input_eval;
682 let n = s_values.len();
683
684 // Chunk each output buffer by `dim` so column j maps to slice [j*dim .. (j+1)*dim].
685 // When a derivative level is not requested (`None`), we still need an
686 // `IndexedParallelIterator` of the same length for `zip`; a Vec<None> is the
687 // simplest way to satisfy Rayon's type constraints here.
688 let dq_chunks: Vec<Option<&mut [f64]>> = dq.as_deref_mut().map_or_else(
689 || (0..n).map(|_| None).collect(),
690 |v| v.chunks_mut(dim).map(Some).collect(),
691 );
692 let ddq_chunks: Vec<Option<&mut [f64]>> = ddq.as_deref_mut().map_or_else(
693 || (0..n).map(|_| None).collect(),
694 |v| v.chunks_mut(dim).map(Some).collect(),
695 );
696 let dddq_chunks: Vec<Option<&mut [f64]>> = dddq.as_deref_mut().map_or_else(
697 || (0..n).map(|_| None).collect(),
698 |v| v.chunks_mut(dim).map(Some).collect(),
699 );
700
701 s_values
702 .par_iter()
703 .enumerate()
704 .zip(q.par_chunks_mut(dim))
705 .zip(dq_chunks.into_par_iter())
706 .zip(ddq_chunks.into_par_iter())
707 .zip(dddq_chunks.into_par_iter())
708 .map(
709 |(((((j, &s_raw), q_col), mut dq_col), mut ddq_col), mut dddq_col)| {
710 let s_curr = path.validate_s(s_raw, j)?;
711 let vals = eval_fn(Jet3::seed(s_curr));
712 if vals.len() != dim {
713 return Err(PathError::DimensionMismatch);
714 }
715 for (i, jet) in vals.iter().enumerate() {
716 q_col[i] = jet.v;
717 if let Some(ref mut b) = dq_col {
718 b[i] = jet.d1;
719 }
720 if let Some(ref mut b) = ddq_col {
721 b[i] = jet.d2;
722 }
723 if let Some(ref mut b) = dddq_col {
724 b[i] = jet.d3;
725 }
726 }
727 Ok(())
728 },
729 )
730 .collect()
731}
732
733/// Evaluate spline representation into pre-allocated column-major buffers.
734///
735/// Dispatches to [`SplinePath::eval_at`](crate::path::spline::SplinePath::eval_at)`::<ORDER>` with the minimum derivative
736/// order that satisfies the request: zero run-time branching per sample:
737/// - [`Order::Zero`](Order::Zero) => `eval_at::<0>` (q only)
738/// - [`Order::Two`](Order::Two) => `eval_at::<2>` (q, dq, ddq)
739/// - [`Order::Three`](Order::Three) => `eval_at::<3>` (q, dq, ddq, dddq)
740fn eval_spline(
741 spline: &SplinePath,
742 path: &Path,
743 dim: usize,
744 input_eval: EvalInput,
745) -> Result<(), PathError> {
746 let (s_values, q, dq, ddq, dddq) = input_eval;
747 match (dq.as_deref_mut(), ddq.as_deref_mut(), dddq.as_deref_mut()) {
748 // ── Order::Zero: q only ───────────────────────────────────────────
749 (None, None, None) => s_values
750 .par_iter()
751 .enumerate()
752 .zip(q.par_chunks_mut(dim))
753 .map(|((j, &s_raw), q_col)| -> Result<(), PathError> {
754 let s_curr = path.validate_s(s_raw, j)?;
755 // eval_at::<0> only writes q_col; the derivative slices are never
756 // accessed, so zero-length arrays satisfy the borrow checker.
757 let (mut no_dq, mut no_ddq, mut no_dddq): ([f64; 0], [f64; 0], [f64; 0]) =
758 ([], [], []);
759 spline.eval_at::<0>(s_curr, dim, q_col, &mut no_dq, &mut no_ddq, &mut no_dddq);
760 Ok(())
761 })
762 .collect(),
763 // ── Order::Two: q, dq, ddq ────────────────────────────────────────
764 (Some(dq_buf), Some(ddq_buf), None) => {
765 let dq_chunks: Vec<&mut [f64]> = dq_buf.chunks_mut(dim).collect();
766 let ddq_chunks: Vec<&mut [f64]> = ddq_buf.chunks_mut(dim).collect();
767 s_values
768 .par_iter()
769 .enumerate()
770 .zip(q.par_chunks_mut(dim))
771 .zip(dq_chunks.into_par_iter())
772 .zip(ddq_chunks.into_par_iter())
773 .map(
774 |((((j, &s_raw), q_col), dq_col), ddq_col)| -> Result<(), PathError> {
775 let s_curr = path.validate_s(s_raw, j)?;
776 let mut s4 = [];
777 spline.eval_at::<2>(s_curr, dim, q_col, dq_col, ddq_col, &mut s4);
778 Ok(())
779 },
780 )
781 .collect()
782 }
783 // ── Order::Three: q, dq, ddq, dddq ───────────────────────────────
784 (Some(dq_buf), Some(ddq_buf), Some(dddq_buf)) => {
785 let dq_chunks: Vec<&mut [f64]> = dq_buf.chunks_mut(dim).collect();
786 let ddq_chunks: Vec<&mut [f64]> = ddq_buf.chunks_mut(dim).collect();
787 let dddq_chunks: Vec<&mut [f64]> = dddq_buf.chunks_mut(dim).collect();
788 s_values
789 .par_iter()
790 .enumerate()
791 .zip(q.par_chunks_mut(dim))
792 .zip(dq_chunks.into_par_iter())
793 .zip(ddq_chunks.into_par_iter())
794 .zip(dddq_chunks.into_par_iter())
795 .map(
796 |(((((j, &s_raw), q_col), dq_col), ddq_col), dddq_col)| -> Result<(), PathError> {
797 let s_curr = path.validate_s(s_raw, j)?;
798 spline.eval_at::<3>(s_curr, dim, q_col, dq_col, ddq_col, dddq_col);
799 Ok(())
800 },
801 )
802 .collect()
803 }
804 // Unreachable: evaluate_impl only produces the three patterns above.
805 _ => unreachable!("unexpected dq/ddq/dddq combination"),
806 }
807}
808
809// ── helpers ───────────────────────────────────────────────────────────────────
810
811/// Evaluate a user-provided second-order explicit-derivative evaluator.
812///
813/// The evaluator receives already validated/clamped path parameters. It is
814/// called once for the whole batch so custom evaluators can use their own
815/// vectorized implementation.
816fn eval_evaluator_2nd(
817 evaluator: &dyn PathEvaluator2nd,
818 path: &Path,
819 dim: usize,
820 input_eval: EvalInput,
821) -> Result<(), PathError> {
822 let (s_values, q, dq, ddq, dddq) = input_eval;
823 let s_valid = s_values
824 .iter()
825 .enumerate()
826 .map(|(j, &s)| path.validate_s(s, j))
827 .collect::<Result<Vec<_>, _>>()?;
828
829 if evaluator.dim() != dim {
830 return Err(PathError::DimensionMismatch);
831 }
832
833 match (dq.as_deref_mut(), ddq.as_deref_mut(), dddq.as_deref_mut()) {
834 (None, None, None) => evaluator.evaluate_q(&s_valid, q),
835 (Some(dq_buf), Some(ddq_buf), None) => {
836 evaluator.evaluate_up_to_2nd(&s_valid, q, dq_buf, ddq_buf)
837 }
838 (Some(_), Some(_), Some(_)) => Err(PathError::UnsupportedDerivativeOrder {
839 requested: 3,
840 available: 2,
841 }),
842 // Unreachable: evaluate_impl only produces the three patterns above.
843 _ => unreachable!("unexpected dq/ddq/dddq combination"),
844 }
845}
846
847/// Evaluate a user-provided third-order explicit-derivative evaluator.
848///
849/// The evaluator receives already validated/clamped path parameters. It is
850/// called once for the whole batch so custom evaluators can use their own
851/// vectorized implementation.
852fn eval_evaluator_3rd(
853 evaluator: &dyn PathEvaluator3rd,
854 path: &Path,
855 dim: usize,
856 input_eval: EvalInput,
857) -> Result<(), PathError> {
858 let (s_values, q, dq, ddq, dddq) = input_eval;
859 let s_valid = s_values
860 .iter()
861 .enumerate()
862 .map(|(j, &s)| path.validate_s(s, j))
863 .collect::<Result<Vec<_>, _>>()?;
864
865 if evaluator.dim() != dim {
866 return Err(PathError::DimensionMismatch);
867 }
868
869 match (dq.as_deref_mut(), ddq.as_deref_mut(), dddq.as_deref_mut()) {
870 (None, None, None) => evaluator.evaluate_q(&s_valid, q),
871 (Some(dq_buf), Some(ddq_buf), None) => {
872 evaluator.evaluate_up_to_2nd(&s_valid, q, dq_buf, ddq_buf)
873 }
874 (Some(dq_buf), Some(ddq_buf), Some(dddq_buf)) => {
875 evaluator.evaluate_up_to_3rd(&s_valid, q, dq_buf, ddq_buf, dddq_buf)
876 }
877 // Unreachable: evaluate_impl only produces the three patterns above.
878 _ => unreachable!("unexpected dq/ddq/dddq combination"),
879 }
880}
881
882fn validate_range(s_min: f64, s_max: f64) -> Result<(), PathError> {
883 if !s_min.is_finite() || !s_max.is_finite() || s_max <= s_min {
884 return Err(PathError::InvalidRange { s_min, s_max });
885 }
886 Ok(())
887}
888
889#[cfg(test)]
890mod tests {
891 use super::PathDerivatives;
892 use crate::path::{
893 Jet3, Path as PathModel, PathError, PathEvaluator2nd, PathEvaluator3rd, SplineConfig, cos,
894 exp, sin,
895 };
896 use nalgebra::{Const, DMatrix, DMatrixView, Dyn};
897 use plotters::prelude::*;
898 use rand::RngExt;
899 use std::error::Error;
900 use std::fs::create_dir_all;
901 use std::hint::black_box;
902 use std::path::Path as StdPath;
903 use std::time::Instant;
904
905 const DIM: usize = 6;
906
907 fn make_s(n: usize) -> DMatrix<f64> {
908 DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64)
909 }
910
911 fn make_parametric_path() -> Result<PathModel, PathError> {
912 PathModel::from_parametric(
913 |s: Jet3| {
914 vec![
915 sin(s),
916 cos(s),
917 exp(0.3 * s) - 1.0,
918 s + 0.1 * s * s - 0.01 * s * s * s * s,
919 sin(2.0 * s) + 0.15 * cos(3.0 * s),
920 sin(s) * cos(s),
921 ]
922 },
923 0.0,
924 1.0,
925 )
926 }
927
928 struct PolynomialEvaluator;
929 struct QuadraticEvaluator2nd;
930
931 impl PathEvaluator2nd for PolynomialEvaluator {
932 fn dim(&self) -> usize {
933 2
934 }
935
936 fn evaluate_up_to_2nd(
937 &self,
938 s: &[f64],
939 q: &mut [f64],
940 dq: &mut [f64],
941 ddq: &mut [f64],
942 ) -> Result<(), PathError> {
943 if q.len() != 2 * s.len() || dq.len() != q.len() || ddq.len() != q.len() {
944 return Err(PathError::DimensionMismatch);
945 }
946
947 for (j, &x) in s.iter().enumerate() {
948 let col = 2 * j;
949 q[col] = x * x * x;
950 dq[col] = 3.0 * x * x;
951 ddq[col] = 6.0 * x;
952
953 q[col + 1] = x * x + 1.0;
954 dq[col + 1] = 2.0 * x;
955 ddq[col + 1] = 2.0;
956 }
957 Ok(())
958 }
959 }
960
961 impl PathEvaluator3rd for PolynomialEvaluator {
962 fn evaluate_up_to_3rd(
963 &self,
964 s: &[f64],
965 q: &mut [f64],
966 dq: &mut [f64],
967 ddq: &mut [f64],
968 dddq: &mut [f64],
969 ) -> Result<(), PathError> {
970 self.evaluate_up_to_2nd(s, q, dq, ddq)?;
971 if dddq.len() != 2 * s.len() {
972 return Err(PathError::DimensionMismatch);
973 }
974
975 for j in 0..s.len() {
976 let col = 2 * j;
977 dddq[col] = 6.0;
978 dddq[col + 1] = 0.0;
979 }
980 Ok(())
981 }
982 }
983
984 impl PathEvaluator2nd for QuadraticEvaluator2nd {
985 fn dim(&self) -> usize {
986 1
987 }
988
989 fn evaluate_up_to_2nd(
990 &self,
991 s: &[f64],
992 q: &mut [f64],
993 dq: &mut [f64],
994 ddq: &mut [f64],
995 ) -> Result<(), PathError> {
996 if q.len() != s.len() || dq.len() != q.len() || ddq.len() != q.len() {
997 return Err(PathError::DimensionMismatch);
998 }
999
1000 for (j, &x) in s.iter().enumerate() {
1001 q[j] = x * x + 1.0;
1002 dq[j] = 2.0 * x;
1003 ddq[j] = 2.0;
1004 }
1005 Ok(())
1006 }
1007 }
1008
1009 fn make_waypoints(n_pts: usize) -> DMatrix<f64> {
1010 let mut rng = rand::rng();
1011 // Random-walk waypoints: each row is one DOF, each column is a waypoint.
1012 let mut waypoints = DMatrix::<f64>::zeros(DIM, n_pts);
1013 for mut row in waypoints.row_iter_mut() {
1014 row[0] = rng.random_range(-1.0..1.0);
1015 for j in 1..n_pts {
1016 let step = rng.random_range(-0.35..0.35);
1017 row[j] = row[j - 1] + step;
1018 }
1019 }
1020 waypoints
1021 }
1022
1023 #[test]
1024 fn test_waypoints_view_interpolates_padded_column_major() -> Result<(), PathError> {
1025 const DIM_LOCAL: usize = 2;
1026 const N_PTS: usize = 4;
1027 const LEADING_DIM: usize = 3;
1028 let data = [
1029 0.0, 1.0, -99.0, //
1030 0.5, 1.5, -99.0, //
1031 1.0, 2.0, -99.0, //
1032 1.5, 2.5, -99.0,
1033 ];
1034 let waypoints = DMatrixView::from_slice_with_strides_generic(
1035 &data,
1036 Dyn(DIM_LOCAL),
1037 Dyn(N_PTS),
1038 Const::<1>,
1039 Dyn(LEADING_DIM),
1040 );
1041
1042 let path = PathModel::from_waypoints_view(waypoints, SplineConfig::default())?;
1043 let s = [0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0];
1044 let out = path.evaluate_q(&s)?;
1045
1046 for j in 0..N_PTS {
1047 assert!((out.q[(0, j)] - data[j * LEADING_DIM]).abs() < 1e-10);
1048 assert!((out.q[(1, j)] - data[j * LEADING_DIM + 1]).abs() < 1e-10);
1049 }
1050
1051 Ok(())
1052 }
1053
1054 #[test]
1055 fn test_evaluator_path_explicit_derivatives() -> Result<(), PathError> {
1056 let path = PathModel::from_evaluator_3rd(PolynomialEvaluator, -1.0, 1.0)?;
1057 let s = [-1.0, 0.0, 0.5];
1058
1059 let out = path.evaluate_up_to_3rd(&s)?;
1060 let dq = out.dq.as_ref().unwrap();
1061 let ddq = out.ddq.as_ref().unwrap();
1062 let dddq = out.dddq.as_ref().unwrap();
1063
1064 for (j, &x) in s.iter().enumerate() {
1065 assert!((out.q[(0, j)] - x.powi(3)).abs() < 1e-12);
1066 assert!((dq[(0, j)] - 3.0 * x * x).abs() < 1e-12);
1067 assert!((ddq[(0, j)] - 6.0 * x).abs() < 1e-12);
1068 assert!((dddq[(0, j)] - 6.0).abs() < 1e-12);
1069
1070 assert!((out.q[(1, j)] - (x * x + 1.0)).abs() < 1e-12);
1071 assert!((dq[(1, j)] - 2.0 * x).abs() < 1e-12);
1072 assert!((ddq[(1, j)] - 2.0).abs() < 1e-12);
1073 assert!(dddq[(1, j)].abs() < 1e-12);
1074 }
1075
1076 let q_only = path.evaluate_q(&s)?;
1077 assert!(q_only.dq.is_none());
1078 assert!(q_only.ddq.is_none());
1079 assert!(q_only.dddq.is_none());
1080 assert!((q_only.q[(0, 2)] - 0.125).abs() < 1e-12);
1081
1082 Ok(())
1083 }
1084
1085 #[test]
1086 fn test_evaluator_path_2nd_does_not_require_3rd() -> Result<(), PathError> {
1087 let path = PathModel::from_evaluator_2nd(QuadraticEvaluator2nd, -1.0, 1.0)?;
1088 let s = [-1.0, 0.0, 0.5];
1089
1090 let out = path.evaluate_up_to_2nd(&s)?;
1091 let dq = out.dq.as_ref().unwrap();
1092 let ddq = out.ddq.as_ref().unwrap();
1093
1094 assert!(out.dddq.is_none());
1095 assert!((out.q[(0, 2)] - 1.25).abs() < 1e-12);
1096 assert!((dq[(0, 2)] - 1.0).abs() < 1e-12);
1097 assert!((ddq[(0, 2)] - 2.0).abs() < 1e-12);
1098
1099 let err = path.evaluate_up_to_3rd(&s).unwrap_err();
1100 match err {
1101 PathError::UnsupportedDerivativeOrder {
1102 requested: 3,
1103 available: 2,
1104 } => {}
1105 other => panic!("unexpected error: {other}"),
1106 }
1107
1108 Ok(())
1109 }
1110
1111 #[test]
1112 fn test_parametric_autodiff_dim6() -> Result<(), PathError> {
1113 let path = make_parametric_path()?;
1114
1115 let n = 300;
1116 let s = make_s(n);
1117 let out = path.evaluate_up_to_3rd(s.as_slice())?;
1118 let dq = out.dq.as_ref().unwrap();
1119 let ddq = out.ddq.as_ref().unwrap();
1120 let dddq = out.dddq.as_ref().unwrap();
1121
1122 // Build expected values for all query points and check all 4 derivative orders.
1123 s.as_slice().iter().enumerate().for_each(|(j, &x)| {
1124 let e03x = (0.3 * x).exp();
1125 let expected_q = [
1126 x.sin(),
1127 x.cos(),
1128 e03x - 1.0,
1129 x + 0.1 * x * x - 0.01 * x * x * x * x,
1130 (2.0 * x).sin() + 0.15 * (3.0 * x).cos(),
1131 x.sin() * x.cos(),
1132 ];
1133 let expected_dq = [
1134 x.cos(),
1135 -x.sin(),
1136 0.3 * e03x,
1137 1.0 + 0.2 * x - 0.04 * x * x * x,
1138 2.0 * (2.0 * x).cos() - 0.45 * (3.0 * x).sin(),
1139 (2.0 * x).cos(),
1140 ];
1141 let expected_ddq = [
1142 -x.sin(),
1143 -x.cos(),
1144 0.09 * e03x,
1145 0.2 - 0.12 * x * x,
1146 -4.0 * (2.0 * x).sin() - 1.35 * (3.0 * x).cos(),
1147 -2.0 * (2.0 * x).sin(),
1148 ];
1149 let expected_dddq = [
1150 -x.cos(),
1151 x.sin(),
1152 0.027 * e03x,
1153 -0.24 * x,
1154 -8.0 * (2.0 * x).cos() + 4.05 * (3.0 * x).sin(),
1155 -4.0 * (2.0 * x).cos(),
1156 ];
1157
1158 for i in 0..DIM {
1159 assert!(
1160 (out.q[(i, j)] - expected_q[i]).abs() < 1e-10,
1161 "q dim={i} idx={j}"
1162 );
1163 assert!(
1164 (dq[(i, j)] - expected_dq[i]).abs() < 1e-10,
1165 "dq dim={i} idx={j}"
1166 );
1167 assert!(
1168 (ddq[(i, j)] - expected_ddq[i]).abs() < 1e-10,
1169 "ddq dim={i} idx={j}"
1170 );
1171 assert!(
1172 (dddq[(i, j)] - expected_dddq[i]).abs() < 1e-10,
1173 "dddq dim={i} idx={j}"
1174 );
1175 }
1176 });
1177
1178 Ok(())
1179 }
1180
1181 #[test]
1182 fn test_evaluate_q_only() -> Result<(), PathError> {
1183 let path = make_parametric_path()?;
1184 let n = 100;
1185 let s = make_s(n);
1186 let out = path.evaluate_q(s.as_slice())?;
1187
1188 assert!(out.dq.is_none());
1189 assert!(out.ddq.is_none());
1190 assert!(out.dddq.is_none());
1191
1192 for j in 0..n {
1193 let x = s[(0, j)];
1194 assert!((out.q[(0, j)] - x.sin()).abs() < 1e-10);
1195 assert!((out.q[(1, j)] - x.cos()).abs() < 1e-10);
1196 }
1197
1198 Ok(())
1199 }
1200
1201 #[test]
1202 fn test_evaluate_up_to_2nd() -> Result<(), PathError> {
1203 let path = make_parametric_path()?;
1204 let n = 100;
1205 let s = make_s(n);
1206 let out = path.evaluate_up_to_2nd(s.as_slice())?;
1207 let dq = out.dq.as_ref().unwrap();
1208 let ddq = out.ddq.as_ref().unwrap();
1209
1210 assert!(out.dddq.is_none());
1211
1212 for j in 0..n {
1213 let x = s[(0, j)];
1214 assert!((out.q[(0, j)] - x.sin()).abs() < 1e-10);
1215 assert!((dq[(0, j)] - x.cos()).abs() < 1e-10);
1216 assert!((ddq[(0, j)] - (-x.sin())).abs() < 1e-10);
1217 }
1218
1219 Ok(())
1220 }
1221
1222 #[test]
1223 fn test_quintic_spline_interpolates_waypoints_dim6() -> Result<(), PathError> {
1224 let n_pts = 25;
1225 let waypoints = make_waypoints(n_pts);
1226
1227 let cfg = SplineConfig::default();
1228 let path = PathModel::from_waypoints(&waypoints, cfg)?;
1229
1230 let s = DMatrix::<f64>::from_fn(1, n_pts, |_, j| j as f64 / (n_pts - 1) as f64);
1231 let out = path.evaluate_up_to_3rd(s.as_slice())?;
1232 let dq = out.dq.as_ref().unwrap();
1233 let ddq = out.ddq.as_ref().unwrap();
1234 let dddq = out.dddq.as_ref().unwrap();
1235
1236 // The spline must interpolate every waypoint exactly (up to floating-point rounding)
1237 // and all derivatives must be finite (no blowup).
1238 for (i, j) in (0..DIM).flat_map(|i| (0..n_pts).map(move |j| (i, j))) {
1239 assert!((out.q[(i, j)] - waypoints[(i, j)]).abs() < 1e-8);
1240 assert!(dq[(i, j)].is_finite());
1241 assert!(ddq[(i, j)].is_finite());
1242 assert!(dddq[(i, j)].is_finite());
1243 }
1244
1245 Ok(())
1246 }
1247
1248 #[test]
1249 fn test_s_out_of_range_error_dim6() -> Result<(), PathError> {
1250 let waypoints = make_waypoints(12);
1251 let path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
1252 let s = DMatrix::<f64>::from_row_slice(1, 3, &[-0.1, 0.5, 1.1]);
1253 let err = path.evaluate_up_to_3rd(s.as_slice()).unwrap_err();
1254 match err {
1255 PathError::OutOfRangeS { .. } => {}
1256 _ => panic!("expected OutOfRangeS"),
1257 }
1258 Ok(())
1259 }
1260
1261 #[test]
1262 fn test_benchmark_parametric_and_spline_dim6() -> Result<(), PathError> {
1263 let n_eval = 3000;
1264 let n_repeat = 8;
1265 let s = make_s(n_eval);
1266
1267 let start = Instant::now();
1268 let param_path = make_parametric_path()?;
1269 let tc_build_param = start.elapsed().as_secs_f64() * 1e3;
1270
1271 let start = Instant::now();
1272 for _ in 0..n_repeat {
1273 let out = param_path.evaluate_up_to_3rd(s.as_slice())?;
1274 black_box(out.q[(0, 0)]);
1275 }
1276 let tc_eval_param = start.elapsed().as_secs_f64() * 1e3 / n_repeat as f64;
1277 crate::verbosity_log!(
1278 crate::diag::Verbosity::Summary,
1279 "[bench][parametric][dim=6] build={tc_build_param:.3} ms eval={tc_eval_param:.3} ms (N={n_eval})"
1280 );
1281
1282 let n_waypoints_list = [16usize, 32, 64, 128, 192, 256, 512, 1024];
1283 for &n_pts in &n_waypoints_list {
1284 let waypoints = make_waypoints(n_pts);
1285 let start = Instant::now();
1286 let spline_path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
1287 let tc_build = start.elapsed().as_secs_f64() * 1e3;
1288
1289 let start = Instant::now();
1290 for _ in 0..n_repeat {
1291 let out = spline_path.evaluate_up_to_3rd(s.as_slice())?;
1292 black_box(out.q[(0, 0)]);
1293 }
1294 let tc_eval = start.elapsed().as_secs_f64() * 1e3 / n_repeat as f64;
1295
1296 crate::verbosity_log!(
1297 crate::diag::Verbosity::Summary,
1298 "[bench][spline][dim=6][n_pts={n_pts}] build={tc_build:.3} ms eval={tc_eval:.3} ms"
1299 );
1300 }
1301
1302 Ok(())
1303 }
1304
1305 #[test]
1306 fn test_plot_parametric_and_spline_derivatives() -> Result<(), Box<dyn Error>> {
1307 let dir = "data/path_plots";
1308 create_dir_all(dir)?;
1309
1310 let n = 600;
1311 let s = make_s(n);
1312 let s_vec: Vec<f64> = (0..n).map(|j| s[(0, j)]).collect();
1313
1314 let param_path = make_parametric_path()?;
1315 let param_out = param_path.evaluate_up_to_3rd(s.as_slice())?;
1316 plot_grid_4x6(
1317 &format!("{dir}/parametric_dim6_grid.png"),
1318 "parametric dim=6",
1319 &s_vec,
1320 ¶m_out,
1321 None,
1322 )?;
1323
1324 let n_pts = 10;
1325 let waypoints = make_waypoints(n_pts);
1326 let spline_path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
1327 let spline_out = spline_path.evaluate_up_to_3rd(s.as_slice())?;
1328 let wp_s: Vec<f64> = (0..n_pts).map(|j| j as f64 / (n_pts - 1) as f64).collect();
1329 plot_grid_4x6(
1330 &format!("{dir}/spline_order5_dim6_grid.png"),
1331 "spline order=5 dim=6",
1332 &s_vec,
1333 &spline_out,
1334 Some((&wp_s, &waypoints)),
1335 )?;
1336
1337 Ok(())
1338 }
1339
1340 fn plot_grid_4x6(
1341 file: &str,
1342 title: &str,
1343 s: &[f64],
1344 data: &PathDerivatives,
1345 waypoints: Option<(&[f64], &DMatrix<f64>)>,
1346 ) -> Result<(), Box<dyn Error>> {
1347 if let Some(parent) = StdPath::new(file).parent() {
1348 create_dir_all(parent)?;
1349 }
1350
1351 let root = BitMapBackend::new(file, (2400, 1400)).into_drawing_area();
1352 root.fill(&WHITE)?;
1353
1354 let empty = DMatrix::<f64>::zeros(0, 0);
1355 let dq = data.dq.as_ref().unwrap_or(&empty);
1356 let ddq = data.ddq.as_ref().unwrap_or(&empty);
1357 let dddq = data.dddq.as_ref().unwrap_or(&empty);
1358
1359 let areas = root.split_evenly((4, DIM));
1360 let mats = [&data.q, dq, ddq, dddq];
1361 let row_names = ["q", "dq", "ddq", "dddq"];
1362
1363 for row in 0..4 {
1364 for col in 0..DIM {
1365 let area = &areas[row * DIM + col];
1366 let series = mat_row(mats[row], col);
1367 let (mut y_min, mut y_max) = min_max_slice(&series);
1368 if (y_max - y_min).abs() < 1e-12 {
1369 y_min -= 1.0;
1370 y_max += 1.0;
1371 } else {
1372 let pad = 0.08 * (y_max - y_min);
1373 y_min -= pad;
1374 y_max += pad;
1375 }
1376
1377 let mut chart = ChartBuilder::on(area)
1378 .margin(8)
1379 .caption(
1380 format!("{} j{}", row_names[row], col + 1),
1381 ("sans-serif", 16),
1382 )
1383 .x_label_area_size(24)
1384 .y_label_area_size(38)
1385 .build_cartesian_2d(s[0]..s[s.len() - 1], y_min..y_max)?;
1386
1387 chart
1388 .configure_mesh()
1389 .x_desc(if row == 3 { "s" } else { "" })
1390 .y_desc("")
1391 .draw()?;
1392
1393 chart.draw_series(LineSeries::new(
1394 (0..s.len()).map(|j| (s[j], series[j])),
1395 &BLUE,
1396 ))?;
1397
1398 if row == 0
1399 && let Some((s_wp, q_wp)) = waypoints
1400 {
1401 chart.draw_series(
1402 s_wp.iter()
1403 .zip(q_wp.row(col).iter())
1404 .map(|(&xs, &ys)| Circle::new((xs, ys), 2, RED.filled())),
1405 )?;
1406 }
1407 }
1408 }
1409
1410 root.titled(title, ("sans-serif", 28))?;
1411 root.present()?;
1412 Ok(())
1413 }
1414
1415 fn mat_row(mat: &DMatrix<f64>, row: usize) -> Vec<f64> {
1416 mat.row(row).iter().copied().collect()
1417 }
1418
1419 fn min_max_slice(data: &[f64]) -> (f64, f64) {
1420 data.iter()
1421 .copied()
1422 .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), v| {
1423 (mn.min(v), mx.max(v))
1424 })
1425 }
1426}