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}