use core::ops;
use az::Az as _;
use num_traits::{clamp_max, clamp_min};
use crate::{
util::traits::{Ceil, Sqrt},
MotionProfile,
};
pub struct Trapezoidal<Num = DefaultNum> {
delay_min: Option<Num>,
delay_initial: Num,
delay_prev: Num,
target_accel: Num,
steps_left: u32,
}
impl<Num> Trapezoidal<Num>
where
Num: Copy
+ num_traits::One
+ ops::Add<Output = Num>
+ ops::Div<Output = Num>
+ Sqrt,
{
pub fn new(target_accel: Num) -> Self {
let two = Num::one() + Num::one();
let initial_delay = Num::one() / (two * target_accel).sqrt();
Self {
delay_min: None,
delay_initial: initial_delay,
delay_prev: initial_delay,
target_accel,
steps_left: 0,
}
}
}
#[cfg(test)]
impl Default for Trapezoidal<f32> {
fn default() -> Self {
Self::new(6000.0)
}
}
impl<Num> MotionProfile for Trapezoidal<Num>
where
Num: Copy
+ PartialOrd
+ az::Cast<u32>
+ num_traits::Zero
+ num_traits::One
+ num_traits::Inv<Output = Num>
+ ops::Add<Output = Num>
+ ops::Sub<Output = Num>
+ ops::Mul<Output = Num>
+ ops::Div<Output = Num>
+ Ceil,
{
type Velocity = Num;
type Delay = Num;
fn enter_position_mode(
&mut self,
max_velocity: Self::Velocity,
num_steps: u32,
) {
self.delay_min = if max_velocity.is_zero() {
None
} else {
Some(max_velocity.inv())
};
self.steps_left = num_steps;
}
fn next_delay(&mut self) -> Option<Self::Delay> {
let mode = RampMode::compute(self);
let two = Num::one() + Num::one();
let three = two + Num::one();
let one_five = three / two;
let q = self.target_accel * self.delay_prev * self.delay_prev;
let addend = one_five * q * q;
let delay_next = match mode {
RampMode::Idle => {
return None;
}
RampMode::RampUp { delay_min } => {
let delay_next = self.delay_prev * (Num::one() - q + addend);
clamp_min(delay_next, delay_min)
}
RampMode::Plateau => self.delay_prev,
RampMode::RampDown => self.delay_prev * (Num::one() + q + addend),
};
let delay_next = clamp_max(delay_next, self.delay_initial);
self.delay_prev = delay_next;
self.steps_left = self.steps_left.saturating_sub(1);
Some(delay_next)
}
}
pub type DefaultNum = fixed::FixedU64<typenum::U32>;
enum RampMode<Num> {
Idle,
RampUp { delay_min: Num },
Plateau,
RampDown,
}
impl<Num> RampMode<Num>
where
Num: Copy
+ PartialOrd
+ az::Cast<u32>
+ num_traits::One
+ num_traits::Inv<Output = Num>
+ ops::Add<Output = Num>
+ ops::Div<Output = Num>
+ Ceil,
{
fn compute(profile: &Trapezoidal<Num>) -> Self {
let no_steps_left = profile.steps_left == 0;
let not_moving = profile.delay_prev >= profile.delay_initial;
if no_steps_left && not_moving {
return Self::Idle;
}
let two = Num::one() + Num::one();
let velocity = profile.delay_prev.inv();
let steps_to_stop =
(velocity * velocity) / (two * profile.target_accel);
let steps_to_stop = steps_to_stop.ceil().az::<u32>();
let target_step_is_close = profile.steps_left <= steps_to_stop;
if target_step_is_close {
return Self::RampDown;
}
let delay_min = match profile.delay_min {
Some(delay_min) => delay_min,
None => {
return if not_moving {
Self::Idle
} else {
Self::RampDown
};
}
};
let above_max_velocity = profile.delay_prev < delay_min;
let reached_max_velocity = profile.delay_prev == delay_min;
if above_max_velocity {
Self::RampDown
} else if reached_max_velocity {
Self::Plateau
} else {
Self::RampUp { delay_min }
}
}
}
#[cfg(test)]
mod tests {
use approx::{assert_abs_diff_eq, AbsDiffEq as _};
use crate::{MotionProfile as _, Trapezoidal};
const MIN_VELOCITY: f32 = 110.0;
#[test]
fn trapezoidal_should_pass_motion_profile_tests() {
crate::util::testing::test::<Trapezoidal<f32>>();
}
#[test]
fn trapezoidal_should_generate_actual_trapezoidal_ramp() {
let mut trapezoidal = Trapezoidal::new(6000.0);
let mut mode = Mode::RampUp;
let mut ramped_up = false;
let mut plateaued = false;
let mut ramped_down = false;
trapezoidal.enter_position_mode(1000.0, 200);
for (i, accel) in trapezoidal.accelerations().enumerate() {
println!("{}: {}, {:?}", i, accel, mode);
match mode {
Mode::RampUp => {
ramped_up = true;
if i > 0 && accel == 0.0 {
mode = Mode::Plateau;
} else {
assert!(accel > 0.0);
}
}
Mode::Plateau => {
plateaued = true;
if accel < 0.0 {
mode = Mode::RampDown;
} else {
assert_eq!(accel, 0.0);
}
}
Mode::RampDown => {
ramped_down = true;
assert!(accel <= 0.0);
}
}
}
assert!(ramped_up);
assert!(plateaued);
assert!(ramped_down);
}
#[test]
fn trapezoidal_should_generate_ramp_with_approximate_target_acceleration() {
let target_accel = 6000.0;
let mut trapezoidal = Trapezoidal::new(target_accel);
let num_steps = 100;
trapezoidal.enter_position_mode(1000.0, num_steps);
for (i, accel) in trapezoidal.accelerations::<f32>().enumerate() {
println!("{}: {}, {}", i, target_accel, accel);
let around_start = i < 5;
let around_end = i as u32 > num_steps - 5;
if !around_start && !around_end {
assert_abs_diff_eq!(
accel.abs(),
target_accel,
epsilon = target_accel * 0.05,
);
}
}
}
#[test]
fn trapezoidal_should_come_to_stop_with_last_step() {
let mut trapezoidal = Trapezoidal::new(6000.0);
let max_velocity = 1000.0;
trapezoidal.enter_position_mode(max_velocity, 10_000);
for velocity in trapezoidal.velocities() {
if max_velocity.abs_diff_eq(&velocity, 0.001) {
break;
}
}
let mut last_velocity = None;
trapezoidal.enter_position_mode(max_velocity, 0);
for velocity in trapezoidal.velocities() {
println!("Velocity: {}", velocity);
last_velocity = Some(velocity);
}
let last_velocity = last_velocity.unwrap();
println!("Velocity on last step: {}", last_velocity);
assert!(last_velocity <= MIN_VELOCITY);
}
#[test]
fn trapezoidal_should_adapt_to_changes_in_max_velocity() {
let mut trapezoidal = Trapezoidal::new(6000.0);
let mut accelerated = false;
let mut prev_velocity = None;
let max_velocity = 1000.0;
trapezoidal.enter_position_mode(max_velocity, 10_000);
loop {
let velocity = trapezoidal.velocities().next().unwrap();
if max_velocity.abs_diff_eq(&velocity, 0.001) {
break;
}
if let Some(prev_velocity) = prev_velocity {
assert!(velocity > prev_velocity);
accelerated = true
}
prev_velocity = Some(velocity);
}
assert!(accelerated);
let mut decelerated = false;
let mut prev_velocity = None;
let max_velocity = 500.0;
trapezoidal.enter_position_mode(max_velocity, 10_000);
loop {
let velocity = trapezoidal.velocities().next().unwrap();
if max_velocity.abs_diff_eq(&velocity, 0.001) {
break;
}
if let Some(prev_velocity) = prev_velocity {
assert!(velocity < prev_velocity);
decelerated = true
}
prev_velocity = Some(velocity);
}
assert!(decelerated);
let mut decelerated = false;
let mut prev_velocity = None;
let max_velocity = 0.0;
trapezoidal.enter_position_mode(max_velocity, 10_000);
loop {
let velocity = trapezoidal.velocities().next().unwrap();
if velocity <= MIN_VELOCITY {
break;
}
if let Some(prev_velocity) = prev_velocity {
assert!(velocity < prev_velocity);
decelerated = true
}
prev_velocity = Some(velocity);
}
assert!(decelerated);
}
#[derive(Debug, Eq, PartialEq)]
enum Mode {
RampUp,
Plateau,
RampDown,
}
}