ramp_maker/
trapezoidal.rs

1//! Trapezoidal motion profile
2//!
3//! See [`Trapezoidal`].
4
5use core::ops;
6
7use az::Az as _;
8use num_traits::{clamp_max, clamp_min};
9
10use crate::{
11    util::traits::{Ceil, Sqrt},
12    MotionProfile,
13};
14
15/// Trapezoidal motion profile
16///
17/// Generates an approximation of a trapezoidal ramp, following the algorithm
18/// laid out here:
19/// [http://hwml.com/LeibRamp.htm](http://hwml.com/LeibRamp.htm)
20///
21/// A PDF version of that page is available:
22/// [http://hwml.com/LeibRamp.pdf](http://hwml.com/LeibRamp.pdf)
23///
24/// This implementation makes the following simplifications:
25/// - The unit of time used is left to the user (see "Unit of Time" below), so
26///   the frequency variable `F` is ignored.
27/// - The initial velocity `v0` is assumed to be zero, meaning you can't
28///   construct an instance of this struct to take over an ongoing movement.
29///
30/// Create an instance of this struct using [`Trapezoidal::new`], then use the
31/// API defined by [`MotionProfile`] (which this struct implements) to generate
32/// the acceleration ramp.
33///
34/// # Acceleration Ramp
35///
36/// This struct will generate a trapezoidal acceleration ramp with the following
37/// attributes:
38/// - The velocity will always be equal to or less than the maximum velocity
39///   passed to the constructor.
40/// - While ramping up or down, the acceleration will be an approximation
41///   of the target acceleration passed to the constructor.
42///
43/// # Unit of Time
44///
45/// This code is agnostic on which unit of time is used. If you provide the
46/// target acceleration and maximum velocity in steps per second, the unit of
47/// the delay returned will be seconds.
48///
49/// This allows you to pass the parameters in steps per number of timer counts
50/// for the timer you're using, completely eliminating any conversion overhead
51/// for the delay.
52///
53/// # Type Parameter
54///
55/// The type parameter `Num` defines the type that is used to represent the
56/// target acceleration, maximum velocity, and delays per step. It is set to a
57/// 64-bit fixed-point number type by default.
58///
59/// You can override the default with `f32`, `f64`, or any other type from the
60/// `fixed` crate. Please note that this code uses a very naive approach
61/// regarding its use of numeric types, which does not play well lower-accuracy
62/// fixed-point types. Please be very careful when using any other other type
63/// than the default or a floating-point type. The code might not even generate
64/// a proper trapezoidal ramp, if accuracy is too low!
65///
66/// Please note that you need to enable support for `f32`/`f64` explicitly.
67/// Check out the section on Cargo features from the documentation in the root
68/// module for more information.
69pub struct Trapezoidal<Num = DefaultNum> {
70    delay_min: Option<Num>,
71    delay_initial: Num,
72    delay_prev: Num,
73
74    target_accel: Num,
75    steps_left: u32,
76}
77
78impl<Num> Trapezoidal<Num>
79where
80    Num: Copy
81        + num_traits::One
82        + ops::Add<Output = Num>
83        + ops::Div<Output = Num>
84        + Sqrt,
85{
86    /// Create a new instance of `Trapezoidal`
87    ///
88    /// Accepts the target acceleration in steps per (unit of time)^2 as an
89    /// argument. It must not be zero. See the struct documentation for
90    /// information about units of time.
91    ///
92    /// # Panics
93    ///
94    /// Panics, if `target_accel` is zero.
95    pub fn new(target_accel: Num) -> Self {
96        // Based on equation [17] in the referenced paper.
97        let two = Num::one() + Num::one();
98        let initial_delay = Num::one() / (two * target_accel).sqrt();
99
100        Self {
101            delay_min: None,
102            delay_initial: initial_delay,
103            delay_prev: initial_delay,
104
105            target_accel,
106            steps_left: 0,
107        }
108    }
109}
110
111// Needed for the `MotionProfile` test suite in `crate::util::testing`.
112#[cfg(test)]
113impl Default for Trapezoidal<f32> {
114    fn default() -> Self {
115        Self::new(6000.0)
116    }
117}
118
119impl<Num> MotionProfile for Trapezoidal<Num>
120where
121    Num: Copy
122        + PartialOrd
123        + az::Cast<u32>
124        + num_traits::Zero
125        + num_traits::One
126        + num_traits::Inv<Output = Num>
127        + ops::Add<Output = Num>
128        + ops::Sub<Output = Num>
129        + ops::Mul<Output = Num>
130        + ops::Div<Output = Num>
131        + Ceil,
132{
133    type Velocity = Num;
134    type Delay = Num;
135
136    fn enter_position_mode(
137        &mut self,
138        max_velocity: Self::Velocity,
139        num_steps: u32,
140    ) {
141        // Based on equation [7] in the reference paper.
142        self.delay_min = if max_velocity.is_zero() {
143            None
144        } else {
145            Some(max_velocity.inv())
146        };
147
148        self.steps_left = num_steps;
149    }
150
151    fn next_delay(&mut self) -> Option<Self::Delay> {
152        let mode = RampMode::compute(self);
153
154        // Compute some basic numbers we're going to need for the following
155        // calculations. All of this is statically known, so let's hope it
156        // optimizes out.
157        let two = Num::one() + Num::one();
158        let three = two + Num::one();
159        let one_five = three / two;
160
161        // Compute the delay for the next step. See [22] in the referenced
162        // paper.
163        let q = self.target_accel * self.delay_prev * self.delay_prev;
164        let addend = one_five * q * q;
165        let delay_next = match mode {
166            RampMode::Idle => {
167                return None;
168            }
169            RampMode::RampUp { delay_min } => {
170                let delay_next = self.delay_prev * (Num::one() - q + addend);
171                clamp_min(delay_next, delay_min)
172            }
173            RampMode::Plateau => self.delay_prev,
174            RampMode::RampDown => self.delay_prev * (Num::one() + q + addend),
175        };
176
177        // See the explanation following [20] in the referenced paper.
178        let delay_next = clamp_max(delay_next, self.delay_initial);
179
180        self.delay_prev = delay_next;
181        self.steps_left = self.steps_left.saturating_sub(1);
182
183        Some(delay_next)
184    }
185}
186
187/// The default numeric type used by [`Trapezoidal`]
188pub type DefaultNum = fixed::FixedU64<typenum::U32>;
189
190enum RampMode<Num> {
191    Idle,
192    RampUp { delay_min: Num },
193    Plateau,
194    RampDown,
195}
196
197impl<Num> RampMode<Num>
198where
199    Num: Copy
200        + PartialOrd
201        + az::Cast<u32>
202        + num_traits::One
203        + num_traits::Inv<Output = Num>
204        + ops::Add<Output = Num>
205        + ops::Div<Output = Num>
206        + Ceil,
207{
208    fn compute(profile: &Trapezoidal<Num>) -> Self {
209        let no_steps_left = profile.steps_left == 0;
210        let not_moving = profile.delay_prev >= profile.delay_initial;
211
212        if no_steps_left && not_moving {
213            return Self::Idle;
214        }
215
216        // Compute some basic numbers we're going to need for the following
217        // calculations. All of this is statically known, so let's hope it
218        // optimizes out.
219        let two = Num::one() + Num::one();
220
221        // Compute the number of steps needed to come to a stop. We'll compare
222        // that to the number of steps left to the target step below, to
223        // determine whether we need to decelerate.
224        let velocity = profile.delay_prev.inv();
225        let steps_to_stop =
226            (velocity * velocity) / (two * profile.target_accel);
227        let steps_to_stop = steps_to_stop.ceil().az::<u32>();
228
229        let target_step_is_close = profile.steps_left <= steps_to_stop;
230        if target_step_is_close {
231            return Self::RampDown;
232        }
233
234        let delay_min = match profile.delay_min {
235            Some(delay_min) => delay_min,
236            None => {
237                // No minimum delay means someone set max velocity to zero.
238                return if not_moving {
239                    Self::Idle
240                } else {
241                    Self::RampDown
242                };
243            }
244        };
245
246        let above_max_velocity = profile.delay_prev < delay_min;
247        let reached_max_velocity = profile.delay_prev == delay_min;
248
249        if above_max_velocity {
250            Self::RampDown
251        } else if reached_max_velocity {
252            Self::Plateau
253        } else {
254            Self::RampUp { delay_min }
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use approx::{assert_abs_diff_eq, AbsDiffEq as _};
262
263    use crate::{MotionProfile as _, Trapezoidal};
264
265    // The minimum velocity that is acceptable for the last step, if the goal is
266    // to reach stand-still. No idea if this value is appropriate, but it
267    // matches what the algorithm produces.
268    const MIN_VELOCITY: f32 = 110.0;
269
270    #[test]
271    fn trapezoidal_should_pass_motion_profile_tests() {
272        crate::util::testing::test::<Trapezoidal<f32>>();
273    }
274
275    #[test]
276    fn trapezoidal_should_generate_actual_trapezoidal_ramp() {
277        let mut trapezoidal = Trapezoidal::new(6000.0);
278
279        let mut mode = Mode::RampUp;
280
281        let mut ramped_up = false;
282        let mut plateaued = false;
283        let mut ramped_down = false;
284
285        trapezoidal.enter_position_mode(1000.0, 200);
286        for (i, accel) in trapezoidal.accelerations().enumerate() {
287            println!("{}: {}, {:?}", i, accel, mode);
288
289            match mode {
290                Mode::RampUp => {
291                    ramped_up = true;
292
293                    if i > 0 && accel == 0.0 {
294                        mode = Mode::Plateau;
295                    } else {
296                        assert!(accel > 0.0);
297                    }
298                }
299                Mode::Plateau => {
300                    plateaued = true;
301
302                    if accel < 0.0 {
303                        mode = Mode::RampDown;
304                    } else {
305                        assert_eq!(accel, 0.0);
306                    }
307                }
308                Mode::RampDown => {
309                    ramped_down = true;
310
311                    assert!(accel <= 0.0);
312                }
313            }
314        }
315
316        assert!(ramped_up);
317        assert!(plateaued);
318        assert!(ramped_down);
319    }
320
321    #[test]
322    fn trapezoidal_should_generate_ramp_with_approximate_target_acceleration() {
323        let target_accel = 6000.0;
324        let mut trapezoidal = Trapezoidal::new(target_accel);
325
326        // Make the ramp so short that it becomes triangular. This makes testing
327        // a bit easier, as we don't have to deal with the plateau.
328        let num_steps = 100;
329        trapezoidal.enter_position_mode(1000.0, num_steps);
330
331        for (i, accel) in trapezoidal.accelerations::<f32>().enumerate() {
332            println!("{}: {}, {}", i, target_accel, accel);
333
334            let around_start = i < 5;
335            let around_end = i as u32 > num_steps - 5;
336
337            // There are some inaccuracies at various points, which we
338            // accept. The rest of the ramp is much more accurate.
339            if !around_start && !around_end {
340                assert_abs_diff_eq!(
341                    accel.abs(),
342                    target_accel,
343                    // It's much more accurate for the most part, but can be
344                    // quite inaccurate at the beginning and end.
345                    epsilon = target_accel * 0.05,
346                );
347            }
348        }
349    }
350
351    #[test]
352    fn trapezoidal_should_come_to_stop_with_last_step() {
353        let mut trapezoidal = Trapezoidal::new(6000.0);
354
355        let max_velocity = 1000.0;
356
357        // Accelerate to maximum velocity.
358        trapezoidal.enter_position_mode(max_velocity, 10_000);
359        for velocity in trapezoidal.velocities() {
360            if max_velocity.abs_diff_eq(&velocity, 0.001) {
361                break;
362            }
363        }
364
365        let mut last_velocity = None;
366
367        trapezoidal.enter_position_mode(max_velocity, 0);
368        for velocity in trapezoidal.velocities() {
369            println!("Velocity: {}", velocity);
370            last_velocity = Some(velocity);
371        }
372
373        let last_velocity = last_velocity.unwrap();
374        println!("Velocity on last step: {}", last_velocity);
375        assert!(last_velocity <= MIN_VELOCITY);
376    }
377
378    #[test]
379    fn trapezoidal_should_adapt_to_changes_in_max_velocity() {
380        let mut trapezoidal = Trapezoidal::new(6000.0);
381
382        let mut accelerated = false;
383
384        // Accelerate to maximum velocity.
385        let mut prev_velocity = None;
386        let max_velocity = 1000.0;
387        trapezoidal.enter_position_mode(max_velocity, 10_000);
388        loop {
389            let velocity = trapezoidal.velocities().next().unwrap();
390
391            if max_velocity.abs_diff_eq(&velocity, 0.001) {
392                break;
393            }
394
395            if let Some(prev_velocity) = prev_velocity {
396                assert!(velocity > prev_velocity);
397                accelerated = true
398            }
399            prev_velocity = Some(velocity);
400        }
401
402        assert!(accelerated);
403
404        let mut decelerated = false;
405
406        // Decelerate to a lower maximum velocity.
407        let mut prev_velocity = None;
408        let max_velocity = 500.0;
409        trapezoidal.enter_position_mode(max_velocity, 10_000);
410        loop {
411            let velocity = trapezoidal.velocities().next().unwrap();
412
413            if max_velocity.abs_diff_eq(&velocity, 0.001) {
414                break;
415            }
416
417            if let Some(prev_velocity) = prev_velocity {
418                assert!(velocity < prev_velocity);
419                decelerated = true
420            }
421            prev_velocity = Some(velocity);
422        }
423
424        assert!(decelerated);
425
426        let mut decelerated = false;
427
428        // Decelerate to stand-still.
429        let mut prev_velocity = None;
430        let max_velocity = 0.0;
431        trapezoidal.enter_position_mode(max_velocity, 10_000);
432        loop {
433            let velocity = trapezoidal.velocities().next().unwrap();
434
435            if velocity <= MIN_VELOCITY {
436                break;
437            }
438
439            if let Some(prev_velocity) = prev_velocity {
440                assert!(velocity < prev_velocity);
441                decelerated = true
442            }
443            prev_velocity = Some(velocity);
444        }
445
446        assert!(decelerated);
447    }
448
449    #[derive(Debug, Eq, PartialEq)]
450    enum Mode {
451        RampUp,
452        Plateau,
453        RampDown,
454    }
455}