#![deny(clippy::indexing_slicing)]
use crate::error::MotionError;
use crate::linear_algebra::{Matrix, Vector};
use crate::polynomial::{PiecewisePolynomial, Polynomial, endpoint_mapping_inverse};
use crate::scalar::Numeric;
const COEFFICIENTS_PER_SEGMENT: usize = 8;
const DERIVATIVES_PER_WAYPOINT: usize = 4;
const FREE_DERIVATIVES_PER_WAYPOINT: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoundaryDerivatives<const DIMENSION: usize, T: Numeric = f64> {
pub velocity: Vector<DIMENSION, T>,
pub acceleration: Vector<DIMENSION, T>,
pub jerk: Vector<DIMENSION, T>,
}
impl<const DIMENSION: usize, T: Numeric> Default for BoundaryDerivatives<DIMENSION, T> {
fn default() -> Self {
Self {
velocity: Vector::zeros(),
acceleration: Vector::zeros(),
jerk: Vector::zeros(),
}
}
}
impl<const DIMENSION: usize, T: Numeric> BoundaryDerivatives<DIMENSION, T> {
fn all_finite(&self) -> bool {
self.velocity.is_finite() && self.acceleration.is_finite() && self.jerk.is_finite()
}
fn at_order(&self, order: usize) -> Vector<DIMENSION, T> {
match order {
1 => self.velocity,
2 => self.acceleration,
_ => self.jerk,
}
}
}
enum EndpointDerivative<const DIMENSION: usize, T: Numeric> {
Fixed(Vector<DIMENSION, T>),
Free(usize),
}
fn ways_after_four_derivatives<T: Numeric>(index: usize) -> T {
let mut product = T::ONE;
for step in 0..4 {
product *= T::from_usize(index.saturating_sub(step));
}
product
}
fn snap_cost<T: Numeric>() -> Matrix<COEFFICIENTS_PER_SEGMENT, COEFFICIENTS_PER_SEGMENT, T> {
let mut cost = Matrix::zeros();
for row in 4..COEFFICIENTS_PER_SEGMENT {
for column in 4..COEFFICIENTS_PER_SEGMENT {
let value = ways_after_four_derivatives::<T>(row)
* ways_after_four_derivatives::<T>(column)
/ T::from_usize(row + column - 7);
if let Some(slot) = cost.get_mut(row, column) {
*slot = value;
}
}
}
cost
}
fn along_one_axis<const DIMENSION: usize, T: Numeric>(
block: &[Vector<DIMENSION, T>; DERIVATIVES_PER_WAYPOINT],
axis: usize,
) -> [T; DERIVATIVES_PER_WAYPOINT] {
let mut values = [T::ZERO; DERIVATIVES_PER_WAYPOINT];
for (slot, vector) in values.iter_mut().zip(block.iter()) {
*slot = vector.get(axis).copied().unwrap_or(T::ZERO);
}
values
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MinimumSnapPlanner<
const MAX_SEGMENTS: usize,
const MAX_FREE_DERIVATIVES: usize,
const DIMENSION: usize,
T: Numeric = f64,
> {
start: BoundaryDerivatives<DIMENSION, T>,
end: BoundaryDerivatives<DIMENSION, T>,
}
impl<
const MAX_SEGMENTS: usize,
const MAX_FREE_DERIVATIVES: usize,
const DIMENSION: usize,
T: Numeric,
> Default for MinimumSnapPlanner<MAX_SEGMENTS, MAX_FREE_DERIVATIVES, DIMENSION, T>
{
fn default() -> Self {
Self::new()
}
}
impl<
const MAX_SEGMENTS: usize,
const MAX_FREE_DERIVATIVES: usize,
const DIMENSION: usize,
T: Numeric,
> MinimumSnapPlanner<MAX_SEGMENTS, MAX_FREE_DERIVATIVES, DIMENSION, T>
{
#[inline]
#[must_use]
pub fn new() -> Self {
Self {
start: BoundaryDerivatives::default(),
end: BoundaryDerivatives::default(),
}
}
#[inline]
#[must_use]
pub fn with_start(mut self, boundary: BoundaryDerivatives<DIMENSION, T>) -> Self {
self.start = boundary;
self
}
#[inline]
#[must_use]
pub fn with_end(mut self, boundary: BoundaryDerivatives<DIMENSION, T>) -> Self {
self.end = boundary;
self
}
fn classify(
&self,
waypoint: usize,
order: usize,
waypoints: &[Vector<DIMENSION, T>],
segments: usize,
) -> EndpointDerivative<DIMENSION, T> {
if order == 0 {
return EndpointDerivative::Fixed(
waypoints.get(waypoint).copied().unwrap_or(Vector::zeros()),
);
}
if waypoint == 0 {
return EndpointDerivative::Fixed(self.start.at_order(order));
}
if waypoint == segments {
return EndpointDerivative::Fixed(self.end.at_order(order));
}
EndpointDerivative::Free(FREE_DERIVATIVES_PER_WAYPOINT * (waypoint - 1) + (order - 1))
}
pub fn plan(
&self,
waypoints: &[Vector<DIMENSION, T>],
durations: &[T],
) -> Result<
PiecewisePolynomial<MAX_SEGMENTS, COEFFICIENTS_PER_SEGMENT, DIMENSION, T>,
MotionError,
> {
if waypoints.len() < 2 {
return Err(MotionError::PathTooShort);
}
let segments = waypoints.len() - 1;
if segments > MAX_SEGMENTS {
return Err(MotionError::CapacityExceeded);
}
if durations.len() != segments {
return Err(MotionError::SegmentCountMismatch);
}
for duration in durations {
if !duration.is_finite() || *duration <= T::ZERO {
return Err(MotionError::DurationNotPositive);
}
}
if waypoints.iter().any(|waypoint| !waypoint.is_finite())
|| !self.start.all_finite()
|| !self.end.all_finite()
{
return Err(MotionError::NonFinite);
}
let free_count = FREE_DERIVATIVES_PER_WAYPOINT * (segments - 1);
if free_count > MAX_FREE_DERIVATIVES {
return Err(MotionError::WorkspaceTooSmall);
}
let cost = snap_cost::<T>();
let mut reduced = Matrix::<MAX_FREE_DERIVATIVES, MAX_FREE_DERIVATIVES, T>::zeros();
let mut known_side = Matrix::<MAX_FREE_DERIVATIVES, DIMENSION, T>::zeros();
for (segment, duration) in durations.iter().enumerate() {
let inverse_mapping = endpoint_mapping_inverse(*duration)?;
let in_endpoint_terms =
inverse_mapping.transpose() * cost.scale(duration.powi(-7)) * inverse_mapping;
for local_row in 0..COEFFICIENTS_PER_SEGMENT {
let row_waypoint = segment + local_row / DERIVATIVES_PER_WAYPOINT;
let row_order = local_row % DERIVATIVES_PER_WAYPOINT;
let EndpointDerivative::Free(free_row) =
self.classify(row_waypoint, row_order, waypoints, segments)
else {
continue;
};
for local_column in 0..COEFFICIENTS_PER_SEGMENT {
let column_waypoint = segment + local_column / DERIVATIVES_PER_WAYPOINT;
let column_order = local_column % DERIVATIVES_PER_WAYPOINT;
let entry = in_endpoint_terms
.get(local_row, local_column)
.copied()
.unwrap_or(T::ZERO);
match self.classify(column_waypoint, column_order, waypoints, segments) {
EndpointDerivative::Free(free_column) => {
if let Some(slot) = reduced.get_mut(free_row, free_column) {
*slot += entry;
}
}
EndpointDerivative::Fixed(value) => {
for axis in 0..DIMENSION {
let known = value.get(axis).copied().unwrap_or(T::ZERO);
if let Some(slot) = known_side.get_mut(free_row, axis) {
*slot -= entry * known;
}
}
}
}
}
}
}
let mut system = (reduced + reduced.transpose()).scale(T::HALF);
for row in free_count..MAX_FREE_DERIVATIVES {
if let Some(slot) = system.get_mut(row, row) {
*slot = T::ONE;
}
}
let solved = if free_count == 0 {
Matrix::<MAX_FREE_DERIVATIVES, DIMENSION, T>::zeros()
} else {
system.lu()?.solve_matrix::<DIMENSION>(known_side)
};
let mut blocks =
[[Vector::<DIMENSION, T>::zeros(); DERIVATIVES_PER_WAYPOINT]; MAX_SEGMENTS];
let mut last_block = [Vector::<DIMENSION, T>::zeros(); DERIVATIVES_PER_WAYPOINT];
for waypoint in 0..=segments {
let mut block = [Vector::<DIMENSION, T>::zeros(); DERIVATIVES_PER_WAYPOINT];
for (order, slot) in block.iter_mut().enumerate() {
*slot = match self.classify(waypoint, order, waypoints, segments) {
EndpointDerivative::Fixed(value) => value,
EndpointDerivative::Free(row) => {
Vector::from_fn(|axis| solved.get(row, axis).copied().unwrap_or(T::ZERO))
}
};
}
if waypoint == segments {
last_block = block;
} else if let Some(slot) = blocks.get_mut(waypoint) {
*slot = block;
}
}
let mut pieces =
[[Polynomial::<COEFFICIENTS_PER_SEGMENT, T>::zeros(); DIMENSION]; MAX_SEGMENTS];
let mut spans = [T::ZERO; MAX_SEGMENTS];
let empty = [Vector::<DIMENSION, T>::zeros(); DERIVATIVES_PER_WAYPOINT];
for segment in 0..segments {
let duration = durations.get(segment).copied().unwrap_or(T::ZERO);
let start_block = blocks.get(segment).copied().unwrap_or(empty);
let end_block = if segment + 1 == segments {
last_block
} else {
blocks.get(segment + 1).copied().unwrap_or(empty)
};
for axis in 0..DIMENSION {
let piece = Polynomial::<COEFFICIENTS_PER_SEGMENT, T>::from_endpoint_derivatives(
&along_one_axis(&start_block, axis),
&along_one_axis(&end_block, axis),
duration,
)?;
if let Some(slot) = pieces.get_mut(segment).and_then(|row| row.get_mut(axis)) {
*slot = piece;
}
}
if let Some(slot) = spans.get_mut(segment) {
*slot = duration;
}
}
Ok(PiecewisePolynomial::try_from_pieces(
pieces.get(..segments).unwrap_or(&[]),
spans.get(..segments).unwrap_or(&[]),
)?)
}
}
pub fn durations_from_average_speed<const DIMENSION: usize, T: Numeric>(
waypoints: &[Vector<DIMENSION, T>],
average_speed: T,
durations: &mut [T],
) -> Result<(), MotionError> {
if waypoints.len() < 2 {
return Err(MotionError::PathTooShort);
}
if durations.len() != waypoints.len() - 1 {
return Err(MotionError::SegmentCountMismatch);
}
if !average_speed.is_finite() || average_speed <= T::ZERO {
return Err(MotionError::DurationNotPositive);
}
if waypoints.iter().any(|waypoint| !waypoint.is_finite()) {
return Err(MotionError::NonFinite);
}
for (slot, pair) in durations.iter_mut().zip(waypoints.windows(2)) {
let distance = match (pair.first(), pair.get(1)) {
(Some(from), Some(to)) => (*to - *from).norm(),
_ => T::ZERO,
};
if distance <= T::ZERO {
return Err(MotionError::DurationNotPositive);
}
*slot = distance / average_speed;
}
Ok(())
}