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}