use crate::diag::PathError;
use crate::path::OutOfRangeMode;
use crate::path::autodiff::Jet3;
use crate::path::spline::{SplineConfig, SplinePath};
use nalgebra::{DMatrix, DMatrixView};
use rayon::prelude::*;
use std::sync::Arc;
const EPS_RANGE: f64 = 1e-12;
pub type ParametricFn = Arc<dyn Fn(Jet3) -> Vec<Jet3> + Send + Sync>;
pub trait PathEvaluator2nd: Send + Sync {
fn dim(&self) -> usize;
fn evaluate_q(&self, s: &[f64], q: &mut [f64]) -> Result<(), PathError> {
let mut dq = vec![0.0; q.len()];
let mut ddq = vec![0.0; q.len()];
self.evaluate_up_to_2nd(s, q, &mut dq, &mut ddq)
}
fn evaluate_up_to_2nd(
&self,
s: &[f64],
q: &mut [f64],
dq: &mut [f64],
ddq: &mut [f64],
) -> Result<(), PathError>;
}
pub trait PathEvaluator3rd: PathEvaluator2nd {
fn evaluate_up_to_3rd(
&self,
s: &[f64],
q: &mut [f64],
dq: &mut [f64],
ddq: &mut [f64],
dddq: &mut [f64],
) -> Result<(), PathError>;
}
pub trait PathEvaluator: PathEvaluator3rd {}
impl<T: PathEvaluator3rd + ?Sized> PathEvaluator for T {}
#[derive(Debug)]
pub struct PathDerivatives {
pub q: DMatrix<f64>,
pub dq: Option<DMatrix<f64>>,
pub ddq: Option<DMatrix<f64>>,
pub dddq: Option<DMatrix<f64>>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Order {
Zero, Two, Three, }
pub struct Path {
dim: usize,
s_min: f64,
s_max: f64,
out_of_range_mode: OutOfRangeMode,
repr: PathRepr,
}
enum PathRepr {
Parametric(ParametricFn),
Spline(SplinePath),
Evaluator2nd(Arc<dyn PathEvaluator2nd>),
Evaluator3rd(Arc<dyn PathEvaluator3rd>),
}
impl Path {
pub fn from_parametric<F>(q_fn: F, s_min: f64, s_max: f64) -> Result<Self, PathError>
where
F: Fn(Jet3) -> Vec<Jet3> + Send + Sync + 'static,
{
validate_range(s_min, s_max)?;
let sample = q_fn(Jet3::constant((s_min + s_max) * 0.5));
if sample.is_empty() {
return Err(PathError::InvalidDimension { dim: 0 });
}
let dim = sample.len();
Ok(Self {
dim,
s_min,
s_max,
out_of_range_mode: OutOfRangeMode::Error,
repr: PathRepr::Parametric(Arc::new(q_fn)),
})
}
pub fn from_evaluator_2nd<E>(evaluator: E, s_min: f64, s_max: f64) -> Result<Self, PathError>
where
E: PathEvaluator2nd + 'static,
{
Self::from_shared_evaluator_2nd(Arc::new(evaluator), s_min, s_max)
}
pub fn from_shared_evaluator_2nd(
evaluator: Arc<dyn PathEvaluator2nd>,
s_min: f64,
s_max: f64,
) -> Result<Self, PathError> {
validate_range(s_min, s_max)?;
let dim = evaluator.dim();
if dim == 0 {
return Err(PathError::InvalidDimension { dim });
}
Ok(Self {
dim,
s_min,
s_max,
out_of_range_mode: OutOfRangeMode::Error,
repr: PathRepr::Evaluator2nd(evaluator),
})
}
pub fn from_evaluator_3rd<E>(evaluator: E, s_min: f64, s_max: f64) -> Result<Self, PathError>
where
E: PathEvaluator3rd + 'static,
{
Self::from_shared_evaluator_3rd(Arc::new(evaluator), s_min, s_max)
}
pub fn from_shared_evaluator_3rd(
evaluator: Arc<dyn PathEvaluator3rd>,
s_min: f64,
s_max: f64,
) -> Result<Self, PathError> {
validate_range(s_min, s_max)?;
let dim = evaluator.dim();
if dim == 0 {
return Err(PathError::InvalidDimension { dim });
}
Ok(Self {
dim,
s_min,
s_max,
out_of_range_mode: OutOfRangeMode::Error,
repr: PathRepr::Evaluator3rd(evaluator),
})
}
pub fn from_evaluator<E>(evaluator: E, s_min: f64, s_max: f64) -> Result<Self, PathError>
where
E: PathEvaluator3rd + 'static,
{
Self::from_evaluator_3rd(evaluator, s_min, s_max)
}
pub fn from_shared_evaluator(
evaluator: Arc<dyn PathEvaluator3rd>,
s_min: f64,
s_max: f64,
) -> Result<Self, PathError> {
Self::from_shared_evaluator_3rd(evaluator, s_min, s_max)
}
pub fn from_waypoints(waypoints: &DMatrix<f64>, cfg: SplineConfig) -> Result<Self, PathError> {
Self::from_waypoints_view(waypoints.as_view(), cfg)
}
pub fn from_waypoints_view(
waypoints: DMatrixView<'_, f64>,
cfg: SplineConfig,
) -> Result<Self, PathError> {
if waypoints.nrows() == 0 {
return Err(PathError::InvalidDimension {
dim: waypoints.nrows(),
});
}
if waypoints.ncols() < 2 {
return Err(PathError::NotEnoughWaypoints {
n: waypoints.ncols(),
});
}
if cfg.order < 3 {
return Err(PathError::InvalidOrder { order: cfg.order });
}
validate_range(cfg.s_min, cfg.s_max)?;
let spline = SplinePath::from_waypoints_view(waypoints, &cfg)?;
Ok(Self {
dim: waypoints.nrows(),
s_min: spline.s_min,
s_max: spline.s_max,
out_of_range_mode: spline.out_of_range_mode,
repr: PathRepr::Spline(spline),
})
}
#[inline(always)]
pub fn dim(&self) -> usize {
self.dim
}
#[inline(always)]
pub fn s_range(&self) -> (f64, f64) {
(self.s_min, self.s_max)
}
pub fn evaluate_q(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
self.evaluate_impl(s, Order::Zero)
}
pub fn evaluate_up_to_2nd(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
self.evaluate_impl(s, Order::Two)
}
pub fn evaluate_up_to_3rd(&self, s: &[f64]) -> Result<PathDerivatives, PathError> {
self.evaluate_impl(s, Order::Three)
}
fn evaluate_impl(&self, s: &[f64], order: Order) -> Result<PathDerivatives, PathError> {
let n = s.len();
let dim = self.dim;
let mut q = vec![0.0f64; dim * n];
let mut dq = (order != Order::Zero).then(|| vec![0.0f64; dim * n]);
let mut ddq = (order != Order::Zero).then(|| vec![0.0f64; dim * n]);
let mut dddq = (order == Order::Three).then(|| vec![0.0f64; dim * n]);
match &self.repr {
PathRepr::Parametric(eval_fn) => {
eval_parametric(
eval_fn,
self,
dim,
(s, &mut q, &mut dq, &mut ddq, &mut dddq),
)?;
}
PathRepr::Spline(spline) => {
eval_spline(spline, self, dim, (s, &mut q, &mut dq, &mut ddq, &mut dddq))?;
}
PathRepr::Evaluator2nd(evaluator) => {
eval_evaluator_2nd(
evaluator.as_ref(),
self,
dim,
(s, &mut q, &mut dq, &mut ddq, &mut dddq),
)?;
}
PathRepr::Evaluator3rd(evaluator) => {
eval_evaluator_3rd(
evaluator.as_ref(),
self,
dim,
(s, &mut q, &mut dq, &mut ddq, &mut dddq),
)?;
}
}
Ok(PathDerivatives {
q: DMatrix::from_vec(dim, n, q),
dq: dq.map(|v| DMatrix::from_vec(dim, n, v)),
ddq: ddq.map(|v| DMatrix::from_vec(dim, n, v)),
dddq: dddq.map(|v| DMatrix::from_vec(dim, n, v)),
})
}
#[inline(always)]
fn validate_s(&self, s: f64, index: usize) -> Result<f64, PathError> {
match self.out_of_range_mode {
OutOfRangeMode::Error => {
if s < self.s_min - EPS_RANGE || s > self.s_max + EPS_RANGE {
return Err(PathError::OutOfRangeS {
s_min: self.s_min,
s_max: self.s_max,
index,
value: s,
});
}
Ok(s.clamp(self.s_min, self.s_max))
}
OutOfRangeMode::Clamp => Ok(s.clamp(self.s_min, self.s_max)),
}
}
}
type EvalInput<'a> = (
&'a [f64], &'a mut [f64], &'a mut Option<Vec<f64>>, &'a mut Option<Vec<f64>>, &'a mut Option<Vec<f64>>, );
fn eval_parametric(
eval_fn: &ParametricFn,
path: &Path,
dim: usize,
input_eval: EvalInput,
) -> Result<(), PathError> {
let (s_values, q, dq, ddq, dddq) = input_eval;
let n = s_values.len();
let dq_chunks: Vec<Option<&mut [f64]>> = dq.as_deref_mut().map_or_else(
|| (0..n).map(|_| None).collect(),
|v| v.chunks_mut(dim).map(Some).collect(),
);
let ddq_chunks: Vec<Option<&mut [f64]>> = ddq.as_deref_mut().map_or_else(
|| (0..n).map(|_| None).collect(),
|v| v.chunks_mut(dim).map(Some).collect(),
);
let dddq_chunks: Vec<Option<&mut [f64]>> = dddq.as_deref_mut().map_or_else(
|| (0..n).map(|_| None).collect(),
|v| v.chunks_mut(dim).map(Some).collect(),
);
s_values
.par_iter()
.enumerate()
.zip(q.par_chunks_mut(dim))
.zip(dq_chunks.into_par_iter())
.zip(ddq_chunks.into_par_iter())
.zip(dddq_chunks.into_par_iter())
.map(
|(((((j, &s_raw), q_col), mut dq_col), mut ddq_col), mut dddq_col)| {
let s_curr = path.validate_s(s_raw, j)?;
let vals = eval_fn(Jet3::seed(s_curr));
if vals.len() != dim {
return Err(PathError::DimensionMismatch);
}
for (i, jet) in vals.iter().enumerate() {
q_col[i] = jet.v;
if let Some(ref mut b) = dq_col {
b[i] = jet.d1;
}
if let Some(ref mut b) = ddq_col {
b[i] = jet.d2;
}
if let Some(ref mut b) = dddq_col {
b[i] = jet.d3;
}
}
Ok(())
},
)
.collect()
}
fn eval_spline(
spline: &SplinePath,
path: &Path,
dim: usize,
input_eval: EvalInput,
) -> Result<(), PathError> {
let (s_values, q, dq, ddq, dddq) = input_eval;
match (dq.as_deref_mut(), ddq.as_deref_mut(), dddq.as_deref_mut()) {
(None, None, None) => s_values
.par_iter()
.enumerate()
.zip(q.par_chunks_mut(dim))
.map(|((j, &s_raw), q_col)| -> Result<(), PathError> {
let s_curr = path.validate_s(s_raw, j)?;
let (mut no_dq, mut no_ddq, mut no_dddq): ([f64; 0], [f64; 0], [f64; 0]) =
([], [], []);
spline.eval_at::<0>(s_curr, dim, q_col, &mut no_dq, &mut no_ddq, &mut no_dddq);
Ok(())
})
.collect(),
(Some(dq_buf), Some(ddq_buf), None) => {
let dq_chunks: Vec<&mut [f64]> = dq_buf.chunks_mut(dim).collect();
let ddq_chunks: Vec<&mut [f64]> = ddq_buf.chunks_mut(dim).collect();
s_values
.par_iter()
.enumerate()
.zip(q.par_chunks_mut(dim))
.zip(dq_chunks.into_par_iter())
.zip(ddq_chunks.into_par_iter())
.map(
|((((j, &s_raw), q_col), dq_col), ddq_col)| -> Result<(), PathError> {
let s_curr = path.validate_s(s_raw, j)?;
let mut s4 = [];
spline.eval_at::<2>(s_curr, dim, q_col, dq_col, ddq_col, &mut s4);
Ok(())
},
)
.collect()
}
(Some(dq_buf), Some(ddq_buf), Some(dddq_buf)) => {
let dq_chunks: Vec<&mut [f64]> = dq_buf.chunks_mut(dim).collect();
let ddq_chunks: Vec<&mut [f64]> = ddq_buf.chunks_mut(dim).collect();
let dddq_chunks: Vec<&mut [f64]> = dddq_buf.chunks_mut(dim).collect();
s_values
.par_iter()
.enumerate()
.zip(q.par_chunks_mut(dim))
.zip(dq_chunks.into_par_iter())
.zip(ddq_chunks.into_par_iter())
.zip(dddq_chunks.into_par_iter())
.map(
|(((((j, &s_raw), q_col), dq_col), ddq_col), dddq_col)| -> Result<(), PathError> {
let s_curr = path.validate_s(s_raw, j)?;
spline.eval_at::<3>(s_curr, dim, q_col, dq_col, ddq_col, dddq_col);
Ok(())
},
)
.collect()
}
_ => unreachable!("unexpected dq/ddq/dddq combination"),
}
}
fn eval_evaluator_2nd(
evaluator: &dyn PathEvaluator2nd,
path: &Path,
dim: usize,
input_eval: EvalInput,
) -> Result<(), PathError> {
let (s_values, q, dq, ddq, dddq) = input_eval;
let s_valid = s_values
.iter()
.enumerate()
.map(|(j, &s)| path.validate_s(s, j))
.collect::<Result<Vec<_>, _>>()?;
if evaluator.dim() != dim {
return Err(PathError::DimensionMismatch);
}
match (dq.as_deref_mut(), ddq.as_deref_mut(), dddq.as_deref_mut()) {
(None, None, None) => evaluator.evaluate_q(&s_valid, q),
(Some(dq_buf), Some(ddq_buf), None) => {
evaluator.evaluate_up_to_2nd(&s_valid, q, dq_buf, ddq_buf)
}
(Some(_), Some(_), Some(_)) => Err(PathError::UnsupportedDerivativeOrder {
requested: 3,
available: 2,
}),
_ => unreachable!("unexpected dq/ddq/dddq combination"),
}
}
fn eval_evaluator_3rd(
evaluator: &dyn PathEvaluator3rd,
path: &Path,
dim: usize,
input_eval: EvalInput,
) -> Result<(), PathError> {
let (s_values, q, dq, ddq, dddq) = input_eval;
let s_valid = s_values
.iter()
.enumerate()
.map(|(j, &s)| path.validate_s(s, j))
.collect::<Result<Vec<_>, _>>()?;
if evaluator.dim() != dim {
return Err(PathError::DimensionMismatch);
}
match (dq.as_deref_mut(), ddq.as_deref_mut(), dddq.as_deref_mut()) {
(None, None, None) => evaluator.evaluate_q(&s_valid, q),
(Some(dq_buf), Some(ddq_buf), None) => {
evaluator.evaluate_up_to_2nd(&s_valid, q, dq_buf, ddq_buf)
}
(Some(dq_buf), Some(ddq_buf), Some(dddq_buf)) => {
evaluator.evaluate_up_to_3rd(&s_valid, q, dq_buf, ddq_buf, dddq_buf)
}
_ => unreachable!("unexpected dq/ddq/dddq combination"),
}
}
fn validate_range(s_min: f64, s_max: f64) -> Result<(), PathError> {
if !s_min.is_finite() || !s_max.is_finite() || s_max <= s_min {
return Err(PathError::InvalidRange { s_min, s_max });
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::PathDerivatives;
use crate::path::{
Jet3, Path as PathModel, PathError, PathEvaluator2nd, PathEvaluator3rd, SplineConfig, cos,
exp, sin,
};
use nalgebra::{Const, DMatrix, DMatrixView, Dyn};
use plotters::prelude::*;
use rand::RngExt;
use std::error::Error;
use std::fs::create_dir_all;
use std::hint::black_box;
use std::path::Path as StdPath;
use std::time::Instant;
const DIM: usize = 6;
fn make_s(n: usize) -> DMatrix<f64> {
DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64)
}
fn make_parametric_path() -> Result<PathModel, PathError> {
PathModel::from_parametric(
|s: Jet3| {
vec![
sin(s),
cos(s),
exp(0.3 * s) - 1.0,
s + 0.1 * s * s - 0.01 * s * s * s * s,
sin(2.0 * s) + 0.15 * cos(3.0 * s),
sin(s) * cos(s),
]
},
0.0,
1.0,
)
}
struct PolynomialEvaluator;
struct QuadraticEvaluator2nd;
impl PathEvaluator2nd for PolynomialEvaluator {
fn dim(&self) -> usize {
2
}
fn evaluate_up_to_2nd(
&self,
s: &[f64],
q: &mut [f64],
dq: &mut [f64],
ddq: &mut [f64],
) -> Result<(), PathError> {
if q.len() != 2 * s.len() || dq.len() != q.len() || ddq.len() != q.len() {
return Err(PathError::DimensionMismatch);
}
for (j, &x) in s.iter().enumerate() {
let col = 2 * j;
q[col] = x * x * x;
dq[col] = 3.0 * x * x;
ddq[col] = 6.0 * x;
q[col + 1] = x * x + 1.0;
dq[col + 1] = 2.0 * x;
ddq[col + 1] = 2.0;
}
Ok(())
}
}
impl PathEvaluator3rd for PolynomialEvaluator {
fn evaluate_up_to_3rd(
&self,
s: &[f64],
q: &mut [f64],
dq: &mut [f64],
ddq: &mut [f64],
dddq: &mut [f64],
) -> Result<(), PathError> {
self.evaluate_up_to_2nd(s, q, dq, ddq)?;
if dddq.len() != 2 * s.len() {
return Err(PathError::DimensionMismatch);
}
for j in 0..s.len() {
let col = 2 * j;
dddq[col] = 6.0;
dddq[col + 1] = 0.0;
}
Ok(())
}
}
impl PathEvaluator2nd for QuadraticEvaluator2nd {
fn dim(&self) -> usize {
1
}
fn evaluate_up_to_2nd(
&self,
s: &[f64],
q: &mut [f64],
dq: &mut [f64],
ddq: &mut [f64],
) -> Result<(), PathError> {
if q.len() != s.len() || dq.len() != q.len() || ddq.len() != q.len() {
return Err(PathError::DimensionMismatch);
}
for (j, &x) in s.iter().enumerate() {
q[j] = x * x + 1.0;
dq[j] = 2.0 * x;
ddq[j] = 2.0;
}
Ok(())
}
}
fn make_waypoints(n_pts: usize) -> DMatrix<f64> {
let mut rng = rand::rng();
let mut waypoints = DMatrix::<f64>::zeros(DIM, n_pts);
for mut row in waypoints.row_iter_mut() {
row[0] = rng.random_range(-1.0..1.0);
for j in 1..n_pts {
let step = rng.random_range(-0.35..0.35);
row[j] = row[j - 1] + step;
}
}
waypoints
}
#[test]
fn test_waypoints_view_interpolates_padded_column_major() -> Result<(), PathError> {
const DIM_LOCAL: usize = 2;
const N_PTS: usize = 4;
const LEADING_DIM: usize = 3;
let data = [
0.0, 1.0, -99.0, 0.5, 1.5, -99.0, 1.0, 2.0, -99.0, 1.5, 2.5, -99.0,
];
let waypoints = DMatrixView::from_slice_with_strides_generic(
&data,
Dyn(DIM_LOCAL),
Dyn(N_PTS),
Const::<1>,
Dyn(LEADING_DIM),
);
let path = PathModel::from_waypoints_view(waypoints, SplineConfig::default())?;
let s = [0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0];
let out = path.evaluate_q(&s)?;
for j in 0..N_PTS {
assert!((out.q[(0, j)] - data[j * LEADING_DIM]).abs() < 1e-10);
assert!((out.q[(1, j)] - data[j * LEADING_DIM + 1]).abs() < 1e-10);
}
Ok(())
}
#[test]
fn test_evaluator_path_explicit_derivatives() -> Result<(), PathError> {
let path = PathModel::from_evaluator_3rd(PolynomialEvaluator, -1.0, 1.0)?;
let s = [-1.0, 0.0, 0.5];
let out = path.evaluate_up_to_3rd(&s)?;
let dq = out.dq.as_ref().unwrap();
let ddq = out.ddq.as_ref().unwrap();
let dddq = out.dddq.as_ref().unwrap();
for (j, &x) in s.iter().enumerate() {
assert!((out.q[(0, j)] - x.powi(3)).abs() < 1e-12);
assert!((dq[(0, j)] - 3.0 * x * x).abs() < 1e-12);
assert!((ddq[(0, j)] - 6.0 * x).abs() < 1e-12);
assert!((dddq[(0, j)] - 6.0).abs() < 1e-12);
assert!((out.q[(1, j)] - (x * x + 1.0)).abs() < 1e-12);
assert!((dq[(1, j)] - 2.0 * x).abs() < 1e-12);
assert!((ddq[(1, j)] - 2.0).abs() < 1e-12);
assert!(dddq[(1, j)].abs() < 1e-12);
}
let q_only = path.evaluate_q(&s)?;
assert!(q_only.dq.is_none());
assert!(q_only.ddq.is_none());
assert!(q_only.dddq.is_none());
assert!((q_only.q[(0, 2)] - 0.125).abs() < 1e-12);
Ok(())
}
#[test]
fn test_evaluator_path_2nd_does_not_require_3rd() -> Result<(), PathError> {
let path = PathModel::from_evaluator_2nd(QuadraticEvaluator2nd, -1.0, 1.0)?;
let s = [-1.0, 0.0, 0.5];
let out = path.evaluate_up_to_2nd(&s)?;
let dq = out.dq.as_ref().unwrap();
let ddq = out.ddq.as_ref().unwrap();
assert!(out.dddq.is_none());
assert!((out.q[(0, 2)] - 1.25).abs() < 1e-12);
assert!((dq[(0, 2)] - 1.0).abs() < 1e-12);
assert!((ddq[(0, 2)] - 2.0).abs() < 1e-12);
let err = path.evaluate_up_to_3rd(&s).unwrap_err();
match err {
PathError::UnsupportedDerivativeOrder {
requested: 3,
available: 2,
} => {}
other => panic!("unexpected error: {other}"),
}
Ok(())
}
#[test]
fn test_parametric_autodiff_dim6() -> Result<(), PathError> {
let path = make_parametric_path()?;
let n = 300;
let s = make_s(n);
let out = path.evaluate_up_to_3rd(s.as_slice())?;
let dq = out.dq.as_ref().unwrap();
let ddq = out.ddq.as_ref().unwrap();
let dddq = out.dddq.as_ref().unwrap();
s.as_slice().iter().enumerate().for_each(|(j, &x)| {
let e03x = (0.3 * x).exp();
let expected_q = [
x.sin(),
x.cos(),
e03x - 1.0,
x + 0.1 * x * x - 0.01 * x * x * x * x,
(2.0 * x).sin() + 0.15 * (3.0 * x).cos(),
x.sin() * x.cos(),
];
let expected_dq = [
x.cos(),
-x.sin(),
0.3 * e03x,
1.0 + 0.2 * x - 0.04 * x * x * x,
2.0 * (2.0 * x).cos() - 0.45 * (3.0 * x).sin(),
(2.0 * x).cos(),
];
let expected_ddq = [
-x.sin(),
-x.cos(),
0.09 * e03x,
0.2 - 0.12 * x * x,
-4.0 * (2.0 * x).sin() - 1.35 * (3.0 * x).cos(),
-2.0 * (2.0 * x).sin(),
];
let expected_dddq = [
-x.cos(),
x.sin(),
0.027 * e03x,
-0.24 * x,
-8.0 * (2.0 * x).cos() + 4.05 * (3.0 * x).sin(),
-4.0 * (2.0 * x).cos(),
];
for i in 0..DIM {
assert!(
(out.q[(i, j)] - expected_q[i]).abs() < 1e-10,
"q dim={i} idx={j}"
);
assert!(
(dq[(i, j)] - expected_dq[i]).abs() < 1e-10,
"dq dim={i} idx={j}"
);
assert!(
(ddq[(i, j)] - expected_ddq[i]).abs() < 1e-10,
"ddq dim={i} idx={j}"
);
assert!(
(dddq[(i, j)] - expected_dddq[i]).abs() < 1e-10,
"dddq dim={i} idx={j}"
);
}
});
Ok(())
}
#[test]
fn test_evaluate_q_only() -> Result<(), PathError> {
let path = make_parametric_path()?;
let n = 100;
let s = make_s(n);
let out = path.evaluate_q(s.as_slice())?;
assert!(out.dq.is_none());
assert!(out.ddq.is_none());
assert!(out.dddq.is_none());
for j in 0..n {
let x = s[(0, j)];
assert!((out.q[(0, j)] - x.sin()).abs() < 1e-10);
assert!((out.q[(1, j)] - x.cos()).abs() < 1e-10);
}
Ok(())
}
#[test]
fn test_evaluate_up_to_2nd() -> Result<(), PathError> {
let path = make_parametric_path()?;
let n = 100;
let s = make_s(n);
let out = path.evaluate_up_to_2nd(s.as_slice())?;
let dq = out.dq.as_ref().unwrap();
let ddq = out.ddq.as_ref().unwrap();
assert!(out.dddq.is_none());
for j in 0..n {
let x = s[(0, j)];
assert!((out.q[(0, j)] - x.sin()).abs() < 1e-10);
assert!((dq[(0, j)] - x.cos()).abs() < 1e-10);
assert!((ddq[(0, j)] - (-x.sin())).abs() < 1e-10);
}
Ok(())
}
#[test]
fn test_quintic_spline_interpolates_waypoints_dim6() -> Result<(), PathError> {
let n_pts = 25;
let waypoints = make_waypoints(n_pts);
let cfg = SplineConfig::default();
let path = PathModel::from_waypoints(&waypoints, cfg)?;
let s = DMatrix::<f64>::from_fn(1, n_pts, |_, j| j as f64 / (n_pts - 1) as f64);
let out = path.evaluate_up_to_3rd(s.as_slice())?;
let dq = out.dq.as_ref().unwrap();
let ddq = out.ddq.as_ref().unwrap();
let dddq = out.dddq.as_ref().unwrap();
for (i, j) in (0..DIM).flat_map(|i| (0..n_pts).map(move |j| (i, j))) {
assert!((out.q[(i, j)] - waypoints[(i, j)]).abs() < 1e-8);
assert!(dq[(i, j)].is_finite());
assert!(ddq[(i, j)].is_finite());
assert!(dddq[(i, j)].is_finite());
}
Ok(())
}
#[test]
fn test_s_out_of_range_error_dim6() -> Result<(), PathError> {
let waypoints = make_waypoints(12);
let path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
let s = DMatrix::<f64>::from_row_slice(1, 3, &[-0.1, 0.5, 1.1]);
let err = path.evaluate_up_to_3rd(s.as_slice()).unwrap_err();
match err {
PathError::OutOfRangeS { .. } => {}
_ => panic!("expected OutOfRangeS"),
}
Ok(())
}
#[test]
fn test_benchmark_parametric_and_spline_dim6() -> Result<(), PathError> {
let n_eval = 3000;
let n_repeat = 8;
let s = make_s(n_eval);
let start = Instant::now();
let param_path = make_parametric_path()?;
let tc_build_param = start.elapsed().as_secs_f64() * 1e3;
let start = Instant::now();
for _ in 0..n_repeat {
let out = param_path.evaluate_up_to_3rd(s.as_slice())?;
black_box(out.q[(0, 0)]);
}
let tc_eval_param = start.elapsed().as_secs_f64() * 1e3 / n_repeat as f64;
crate::verbosity_log!(
crate::diag::Verbosity::Summary,
"[bench][parametric][dim=6] build={tc_build_param:.3} ms eval={tc_eval_param:.3} ms (N={n_eval})"
);
let n_waypoints_list = [16usize, 32, 64, 128, 192, 256, 512, 1024];
for &n_pts in &n_waypoints_list {
let waypoints = make_waypoints(n_pts);
let start = Instant::now();
let spline_path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
let tc_build = start.elapsed().as_secs_f64() * 1e3;
let start = Instant::now();
for _ in 0..n_repeat {
let out = spline_path.evaluate_up_to_3rd(s.as_slice())?;
black_box(out.q[(0, 0)]);
}
let tc_eval = start.elapsed().as_secs_f64() * 1e3 / n_repeat as f64;
crate::verbosity_log!(
crate::diag::Verbosity::Summary,
"[bench][spline][dim=6][n_pts={n_pts}] build={tc_build:.3} ms eval={tc_eval:.3} ms"
);
}
Ok(())
}
#[test]
fn test_plot_parametric_and_spline_derivatives() -> Result<(), Box<dyn Error>> {
let dir = "data/path_plots";
create_dir_all(dir)?;
let n = 600;
let s = make_s(n);
let s_vec: Vec<f64> = (0..n).map(|j| s[(0, j)]).collect();
let param_path = make_parametric_path()?;
let param_out = param_path.evaluate_up_to_3rd(s.as_slice())?;
plot_grid_4x6(
&format!("{dir}/parametric_dim6_grid.png"),
"parametric dim=6",
&s_vec,
¶m_out,
None,
)?;
let n_pts = 10;
let waypoints = make_waypoints(n_pts);
let spline_path = PathModel::from_waypoints(&waypoints, SplineConfig::default())?;
let spline_out = spline_path.evaluate_up_to_3rd(s.as_slice())?;
let wp_s: Vec<f64> = (0..n_pts).map(|j| j as f64 / (n_pts - 1) as f64).collect();
plot_grid_4x6(
&format!("{dir}/spline_order5_dim6_grid.png"),
"spline order=5 dim=6",
&s_vec,
&spline_out,
Some((&wp_s, &waypoints)),
)?;
Ok(())
}
fn plot_grid_4x6(
file: &str,
title: &str,
s: &[f64],
data: &PathDerivatives,
waypoints: Option<(&[f64], &DMatrix<f64>)>,
) -> Result<(), Box<dyn Error>> {
if let Some(parent) = StdPath::new(file).parent() {
create_dir_all(parent)?;
}
let root = BitMapBackend::new(file, (2400, 1400)).into_drawing_area();
root.fill(&WHITE)?;
let empty = DMatrix::<f64>::zeros(0, 0);
let dq = data.dq.as_ref().unwrap_or(&empty);
let ddq = data.ddq.as_ref().unwrap_or(&empty);
let dddq = data.dddq.as_ref().unwrap_or(&empty);
let areas = root.split_evenly((4, DIM));
let mats = [&data.q, dq, ddq, dddq];
let row_names = ["q", "dq", "ddq", "dddq"];
for row in 0..4 {
for col in 0..DIM {
let area = &areas[row * DIM + col];
let series = mat_row(mats[row], col);
let (mut y_min, mut y_max) = min_max_slice(&series);
if (y_max - y_min).abs() < 1e-12 {
y_min -= 1.0;
y_max += 1.0;
} else {
let pad = 0.08 * (y_max - y_min);
y_min -= pad;
y_max += pad;
}
let mut chart = ChartBuilder::on(area)
.margin(8)
.caption(
format!("{} j{}", row_names[row], col + 1),
("sans-serif", 16),
)
.x_label_area_size(24)
.y_label_area_size(38)
.build_cartesian_2d(s[0]..s[s.len() - 1], y_min..y_max)?;
chart
.configure_mesh()
.x_desc(if row == 3 { "s" } else { "" })
.y_desc("")
.draw()?;
chart.draw_series(LineSeries::new(
(0..s.len()).map(|j| (s[j], series[j])),
&BLUE,
))?;
if row == 0
&& let Some((s_wp, q_wp)) = waypoints
{
chart.draw_series(
s_wp.iter()
.zip(q_wp.row(col).iter())
.map(|(&xs, &ys)| Circle::new((xs, ys), 2, RED.filled())),
)?;
}
}
}
root.titled(title, ("sans-serif", 28))?;
root.present()?;
Ok(())
}
fn mat_row(mat: &DMatrix<f64>, row: usize) -> Vec<f64> {
mat.row(row).iter().copied().collect()
}
fn min_max_slice(data: &[f64]) -> (f64, f64) {
data.iter()
.copied()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), v| {
(mn.min(v), mx.max(v))
})
}
}