aquarium_control/utilities/sawtooth_profile.rs
1/* Copyright 2024 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9
10#[cfg(feature = "debug_sawtooth_profile")]
11use log::debug;
12
13use serde_derive::Deserialize;
14use std::fmt;
15
16/// Holds the data for the calculation of the saw tooth profile.
17/// The configuration is loaded from the .toml configuration file.
18/// This struct does not contain any implementation.
19#[derive(Deserialize, Clone)]
20pub struct SawToothProfileConfig {
21 /// Phase 1 is a ramp from zero to one.
22 /// The length describes the number of iterations necessary to reach the end of the ramp.
23 phase1_length: u32,
24
25 /// Phase 2 is a constant value of one.
26 /// The length describes for how many iterations this value is kept.
27 phase2_length: u32,
28
29 /// Phase 3 is a ramp from one to zero.
30 /// The length describes the number of iterations necessary to reach the end of the ramp.
31 phase3_length: u32,
32
33 /// Phase 4 is a constant value of zero.
34 /// The length describes for how many iterations this value is kept.
35 phase4_length: u32,
36}
37
38impl SawToothProfileConfig {
39 #[cfg(test)]
40 // Creates a new `SawToothProfileConfig` with specified phase lengths.
41 //
42 // This constructor is exclusively for use in test environments to quickly
43 // define and set up a sawtooth profile configuration without needing to
44 // load from a file or use default deserialization.
45 //
46 // # Arguments
47 // * `phase1_length` - The length of the first phase (ramp up from 0 to 1).
48 // * `phase2_length` - The length of the second phase (constant value of 1).
49 // * `phase3_length` - The length of the third phase (ramp down from 1 to 0).
50 // * `phase4_length` - The length of the fourth phase (constant value of 0).
51 //
52 // # Returns
53 // A new `SawToothProfileConfig` instance initialized with the given phase lengths.
54 pub fn new(
55 phase1_length: u32,
56 phase2_length: u32,
57 phase3_length: u32,
58 phase4_length: u32,
59 ) -> SawToothProfileConfig {
60 SawToothProfileConfig {
61 phase1_length,
62 phase2_length,
63 phase3_length,
64 phase4_length,
65 }
66 }
67}
68
69/// Contains the profile configuration and the implementation for the saw tooth profile.
70pub struct SawToothProfile {
71 /// This variable counts the number of iterations executed and is reset to zero when reaching the end of the profile.
72 counter: f32,
73
74 /// Variable with values from zero to one. This is the output of the saw tooth profile component.
75 pub level_normalized: f32,
76
77 /// This variable is set to the same value as the length of the first phase.
78 phase1_counter_limit: f32,
79
80 /// This variable is set to the cumulated length of the first and second phase.
81 phase2_counter_limit: f32,
82
83 /// This variable is set to the cumulated length of the first three phases.
84 phase3_counter_limit: f32,
85
86 /// This variable is set to the cumulated length of all phases.
87 phase4_counter_limit: f32,
88}
89
90impl fmt::Display for SawToothProfile {
91 /// Formats the `SawToothProfile` struct into a multi-line, human-readable string.
92 ///
93 /// This implementation is primarily for debugging and logging purposes, providing
94 /// a detailed view of the filter's current state, including its internal counter,
95 /// normalized output level, and the limits for each phase.
96 ///
97 /// # Arguments
98 /// * `f` - A mutable reference to the formatter, as required by the `fmt::Display` trait.
99 ///
100 /// # Returns
101 /// A `fmt::Result` indicating whether the formatting operation was successful.
102 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103 write!(
104 f,
105 "SawToothProfile:\n\
106 \tcounter={}\n\
107 \tlevel_normalized={}\n\
108 \tphase1_counter_limit={}\n\
109 \tphase2_counter_limit={}\n\
110 \tphase3_counter_limit={}\n\
111 \tphase4_counter_limit={}",
112 self.counter,
113 self.level_normalized,
114 self.phase1_counter_limit,
115 self.phase2_counter_limit,
116 self.phase3_counter_limit,
117 self.phase4_counter_limit
118 )
119 }
120}
121
122impl SawToothProfile {
123 /// Creates a new `SawToothProfile` instance based on the provided configuration.
124 ///
125 /// This constructor initializes the sawtooth wave generator, setting up its
126 /// internal counter and pre-calculating the limits for each phase based on
127 /// the lengths defined in the `SawToothProfileConfig`. The `level_normalized`
128 /// output is initially set to `0.0`.
129 ///
130 /// # Arguments
131 /// * `config` - A reference to a `SawToothProfileConfig` struct, which specifies
132 /// the desired lengths of the four phases (ramp up, constant high, ramp down, constant low)
133 /// in terms of iterations or time units.
134 ///
135 /// # Returns
136 /// A new `SawToothProfile` struct, ready to generate values.
137 pub fn new(config: &SawToothProfileConfig) -> SawToothProfile {
138 SawToothProfile {
139 counter: 0.0,
140 level_normalized: 0.0,
141 phase1_counter_limit: config.phase1_length as f32,
142 phase2_counter_limit: (config.phase1_length + config.phase2_length) as f32,
143 phase3_counter_limit: (config.phase1_length
144 + config.phase2_length
145 + config.phase3_length) as f32,
146 phase4_counter_limit: (config.phase1_length
147 + config.phase2_length
148 + config.phase3_length
149 + config.phase4_length) as f32,
150 }
151 }
152
153 /// Advances the internal state of the sawtooth profile and calculates its current normalized output level.
154 ///
155 /// This method increments an internal counter. When the counter reaches the end of a full cycle
156 /// (defined by the sum of all phase lengths), it resets to the beginning.
157 /// Based on the current value of the counter, the function then determines which phase of the
158 /// sawtooth profile is active and updates `level_normalized` accordingly. The output
159 /// `level_normalized` ranges from `0.0` to `1.0`.
160 ///
161 /// # Arguments
162 /// * `increment` - The step value by which the internal counter should be advanced in this execution.
163 /// This value determines the "speed" at which the profile progresses.
164 pub fn execute_with(&mut self, increment: f32) {
165 // If the counter exceeds the final limit, reset it. Otherwise, increment.
166 if self.counter >= (self.phase4_counter_limit - 3.0 * increment) {
167 #[cfg(feature = "debug_sawtooth_profile")]
168 debug!("saw tooth profile resetting phase");
169 self.counter = 0.0;
170 } else {
171 self.counter += increment;
172 }
173
174 // in the first phase, increase level to max.
175 if self.counter < self.phase1_counter_limit {
176 self.level_normalized = self.counter / self.phase1_counter_limit;
177 }
178 // for the next phase, keep the signal at 1
179 if self.counter >= self.phase1_counter_limit && self.counter <= self.phase2_counter_limit {
180 self.level_normalized = 1.0;
181 }
182 // for the next phase, ramp down from 1 to 0
183 if self.counter > self.phase2_counter_limit && self.counter < self.phase3_counter_limit {
184 self.level_normalized = 1.0
185 - (self.counter - self.phase2_counter_limit)
186 / (self.phase3_counter_limit - self.phase2_counter_limit);
187 }
188 // for the next phase, keep the signal at 0
189 if self.counter >= self.phase3_counter_limit && self.counter <= self.phase4_counter_limit {
190 self.level_normalized = 0.0;
191 }
192 }
193}
194
195#[cfg(test)]
196pub mod tests {
197 use crate::utilities::sawtooth_profile::SawToothProfile;
198 use crate::utilities::sawtooth_profile::SawToothProfileConfig;
199
200 // test case iterates through the profile:
201 // - beginning/end of each phase
202 // - middle of each phase
203 #[test]
204 pub fn test_saw_tooth_profile() {
205 let saw_tooth_profile_config = SawToothProfileConfig {
206 phase1_length: 10,
207 phase2_length: 2,
208 phase3_length: 10,
209 phase4_length: 2,
210 };
211 let mut saw_tooth_profile = SawToothProfile::new(&saw_tooth_profile_config);
212 println!("{}", saw_tooth_profile);
213
214 // initial calculation of normalized level
215 println!("{}", saw_tooth_profile);
216 assert_eq!(saw_tooth_profile.level_normalized, 0.0);
217 assert_eq!(saw_tooth_profile.counter, 0.0);
218
219 // iterate to the middle of the first interval
220 for _ in 0..5 {
221 saw_tooth_profile.execute_with(1.0);
222 }
223 println!("{}", saw_tooth_profile);
224 assert_eq!(saw_tooth_profile.level_normalized, 0.5);
225 assert_eq!(saw_tooth_profile.counter, 5.0);
226
227 // iterate to the end of the first interval
228 for _ in 0..5 {
229 saw_tooth_profile.execute_with(1.0);
230 }
231 println!("{}", saw_tooth_profile);
232 assert_eq!(saw_tooth_profile.level_normalized, 1.0);
233 assert_eq!(saw_tooth_profile.counter, 10.0);
234
235 // iterate to the middle of the second interval
236 saw_tooth_profile.execute_with(1.0);
237 println!("{}", saw_tooth_profile);
238 assert_eq!(saw_tooth_profile.level_normalized, 1.0);
239 assert_eq!(saw_tooth_profile.counter, 11.0);
240
241 // iterate to the end of the second interval
242 saw_tooth_profile.execute_with(1.0);
243 println!("{}", saw_tooth_profile);
244 assert_eq!(saw_tooth_profile.level_normalized, 1.0);
245 assert_eq!(saw_tooth_profile.counter, 12.0);
246
247 // iterate to the middle of the third interval
248 for _ in 0..5 {
249 saw_tooth_profile.execute_with(1.0);
250 }
251 println!("{}", saw_tooth_profile);
252 assert_eq!(saw_tooth_profile.level_normalized, 0.5);
253 assert_eq!(saw_tooth_profile.counter, 17.0);
254
255 // iterate to the end of the third interval
256 for _ in 0..5 {
257 saw_tooth_profile.execute_with(1.0);
258 }
259 println!("{}", saw_tooth_profile);
260 assert_eq!(saw_tooth_profile.level_normalized, 0.0);
261 assert_eq!(saw_tooth_profile.counter, 0.0);
262
263 // iterate to the middle of the fourth interval
264 saw_tooth_profile.execute_with(1.0);
265 println!("{}", saw_tooth_profile);
266 assert_eq!(saw_tooth_profile.level_normalized, 0.1);
267 assert_eq!(saw_tooth_profile.counter, 1.0);
268
269 // trigger start of new cycle
270 saw_tooth_profile.execute_with(1.0);
271 println!("{}", saw_tooth_profile);
272 assert_eq!(saw_tooth_profile.level_normalized, 0.2);
273 assert_eq!(saw_tooth_profile.counter, 2.0);
274
275 // check if the new cycle continues as expected
276 saw_tooth_profile.execute_with(1.0);
277 println!("{}", saw_tooth_profile);
278 assert_eq!(saw_tooth_profile.level_normalized, 0.3);
279 assert_eq!(saw_tooth_profile.counter, 3.0);
280 }
281}