ramp_maker/
trapezoidal.rs1use 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
15pub 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 pub fn new(target_accel: Num) -> Self {
96 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#[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 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 let two = Num::one() + Num::one();
158 let three = two + Num::one();
159 let one_five = three / two;
160
161 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 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
187pub 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 let two = Num::one() + Num::one();
220
221 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 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 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 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 if !around_start && !around_end {
340 assert_abs_diff_eq!(
341 accel.abs(),
342 target_accel,
343 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 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 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 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 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}