Skip to main content

pamoja_kit/
ramp.rs

1//! Easing a value toward a target at a limited rate.
2
3/// Moves a value toward a target by at most a fixed step each update.
4///
5/// Commanding an actuator straight to a new value can be harsh: a motor lurches, a valve
6/// slams, a lamp jumps. A [`Ramp`] limits how fast the commanded value may change, easing it
7/// toward the target by at most `max_step` per update and snapping to the target once it is
8/// within a step. It is the slew-rate limiter behind a smooth start and stop.
9///
10/// # Examples
11///
12/// ```
13/// use pamoja_kit::Ramp;
14///
15/// // Start at 0, move at most 2 per step, aim for 5.
16/// let mut ramp = Ramp::new(0.0, 2.0);
17/// assert_eq!(ramp.update(5.0), 2.0);
18/// assert_eq!(ramp.update(5.0), 4.0);
19/// assert_eq!(ramp.update(5.0), 5.0); // within a step: snaps to the target
20/// ```
21#[derive(Clone, Copy, Debug)]
22pub struct Ramp {
23    value: f32,
24    max_step: f32,
25}
26
27impl Ramp {
28    /// Creates a ramp starting at `start` that moves at most `max_step` per update.
29    ///
30    /// # Arguments
31    ///
32    /// * `start` - the initial value.
33    /// * `max_step` - the largest change allowed per update; its magnitude is used. A step
34    ///   that is not a number holds the ramp still, and an infinite one sets no limit.
35    ///
36    /// # Returns
37    ///
38    /// A ramp resting at `start`.
39    pub fn new(start: f32, max_step: f32) -> Self {
40        Self {
41            value: start,
42            max_step: step(max_step),
43        }
44    }
45
46    /// Moves toward `target` by at most the step and returns the new value.
47    ///
48    /// A target that is not a finite number is ignored and the value returned as it stood.
49    ///
50    /// # Arguments
51    ///
52    /// * `target` - the value being approached.
53    ///
54    /// # Returns
55    ///
56    /// The value after one limited step, equal to `target` once within a step of it.
57    pub fn update(&mut self, target: f32) -> f32 {
58        let step = self.max_step;
59        self.update_capped(target, step)
60    }
61
62    /// Moves toward `target` by at most `max_step` this update, overriding the fixed rate.
63    ///
64    /// This is the variable-rate cousin of [`update`](Ramp::update): a bounded-acceleration
65    /// limiter passes `accel * dt` as the step so the change allowed each update scales with
66    /// the time since the last one, rather than the constant step fixed at construction.
67    ///
68    /// # Arguments
69    ///
70    /// * `target` - the value being approached. One that is not a finite number is ignored.
71    /// * `max_step` - the largest change allowed this update; its magnitude is used. A step
72    ///   that is not a number holds the ramp still.
73    ///
74    /// # Returns
75    ///
76    /// The value after one limited step, equal to `target` once within `max_step` of it.
77    pub fn update_capped(&mut self, target: f32, max_step: f32) -> f32 {
78        if !target.is_finite() {
79            return self.value;
80        }
81        let limit = step(max_step);
82        let delta = target - self.value;
83        if delta > limit {
84            self.value += limit;
85        } else if delta < -limit {
86            self.value -= limit;
87        } else {
88            self.value = target;
89        }
90        self.value
91    }
92
93    /// Returns the current value.
94    pub fn value(&self) -> f32 {
95        self.value
96    }
97
98    /// Jumps directly to `value`, bypassing the rate limit.
99    ///
100    /// # Arguments
101    ///
102    /// * `value` - the new value. One that is not a finite number is ignored.
103    pub fn set(&mut self, value: f32) {
104        if value.is_finite() {
105            self.value = value;
106        }
107    }
108}
109
110// The magnitude of a step, taken by hand because `f32::abs` lives in `std`, with one that is
111// not a number read as zero so the ramp holds still rather than jumping.
112fn step(value: f32) -> f32 {
113    if value.is_nan() {
114        0.0
115    } else if value < 0.0 {
116        -value
117    } else {
118        value
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn it_climbs_toward_a_higher_target() {
128        let mut ramp = Ramp::new(0.0, 2.0);
129        assert_eq!(ramp.update(5.0), 2.0);
130        assert_eq!(ramp.update(5.0), 4.0);
131        assert_eq!(ramp.update(5.0), 5.0); // snaps within a step
132        assert_eq!(ramp.update(5.0), 5.0); // holds at the target
133    }
134
135    #[test]
136    fn it_falls_toward_a_lower_target() {
137        let mut ramp = Ramp::new(10.0, 3.0);
138        assert_eq!(ramp.update(0.0), 7.0);
139        assert_eq!(ramp.update(0.0), 4.0);
140        assert_eq!(ramp.update(0.0), 1.0);
141        assert_eq!(ramp.update(0.0), 0.0);
142    }
143
144    #[test]
145    fn a_negative_step_is_treated_as_its_magnitude() {
146        let mut ramp = Ramp::new(0.0, -2.0);
147        assert_eq!(ramp.update(10.0), 2.0);
148    }
149
150    #[test]
151    fn set_jumps_past_the_limit() {
152        let mut ramp = Ramp::new(0.0, 1.0);
153        ramp.set(100.0);
154        assert_eq!(ramp.value(), 100.0);
155    }
156
157    #[test]
158    fn update_capped_overrides_the_fixed_rate() {
159        // The construction step is 1.0, but each call here may move up to 0.25.
160        let mut ramp = Ramp::new(0.0, 1.0);
161        assert_eq!(ramp.update_capped(5.0, 0.25), 0.25);
162        assert_eq!(ramp.update_capped(5.0, 0.25), 0.5);
163        // A negative cap is treated as its magnitude.
164        assert_eq!(ramp.update_capped(5.0, -0.5), 1.0);
165    }
166
167    #[test]
168    fn a_target_that_is_not_a_number_is_ignored() {
169        let mut ramp = Ramp::new(0.0, 2.0);
170        ramp.update(5.0);
171        assert_eq!(ramp.update(f32::NAN), 2.0);
172        assert_eq!(ramp.update(f32::INFINITY), 2.0);
173        ramp.set(f32::NAN);
174        assert_eq!(ramp.value(), 2.0);
175        assert_eq!(ramp.update(5.0), 4.0);
176    }
177
178    #[test]
179    fn a_step_that_is_not_a_number_holds_still_and_an_infinite_one_sets_no_limit() {
180        let mut stuck = Ramp::new(0.0, f32::NAN);
181        assert_eq!(stuck.update(5.0), 0.0);
182        assert_eq!(stuck.update_capped(5.0, f32::NAN), 0.0);
183        let mut free = Ramp::new(0.0, f32::INFINITY);
184        assert_eq!(free.update(5.0), 5.0);
185    }
186}