pidgeon/
lib.rs

1// Pidgeon: A robust PID controller library written in Rust
2// Copyright (c) 2025 Security Union LLC
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10use std::sync::Arc;
11use std::sync::Mutex;
12
13#[cfg(feature = "wasm")]
14use web_time::{Duration, Instant};
15
16#[cfg(not(feature = "wasm"))]
17use std::time::{Duration, Instant};
18
19#[cfg(feature = "debugging")]
20mod debug;
21
22#[cfg(feature = "debugging")]
23pub use debug::{ControllerDebugger, DebugConfig};
24
25/// Configuration for a PID controller.
26///
27/// Uses a builder pattern to configure the controller parameters.
28#[derive(Debug, Clone)]
29pub struct ControllerConfig {
30    kp: f64,           // Proportional gain
31    ki: f64,           // Integral gain
32    kd: f64,           // Derivative gain
33    min_output: f64,   // Minimum output value
34    max_output: f64,   // Maximum output value
35    anti_windup: bool, // Whether to use anti-windup
36    setpoint: f64,     // Target value (optional, can be set during operation)
37    deadband: f64,     // Error deadband (errors within ±deadband are treated as zero)
38}
39
40/// Error type for PID controller validation.
41#[derive(Debug, Clone, PartialEq)]
42pub enum PidError {
43    /// Invalid parameter value (NaN, infinity, or out of allowed range)
44    InvalidParameter(&'static str),
45    /// Mutex was poisoned, indicating a panic in another thread
46    MutexPoisoned,
47}
48
49impl std::fmt::Display for PidError {
50    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
51        match self {
52            PidError::InvalidParameter(param) => write!(f, "Invalid parameter: {}", param),
53            PidError::MutexPoisoned => write!(f, "Mutex was poisoned"),
54        }
55    }
56}
57
58impl std::error::Error for PidError {}
59
60impl Default for ControllerConfig {
61    fn default() -> Self {
62        ControllerConfig {
63            kp: 1.0,
64            ki: 0.0,
65            kd: 0.0,
66            min_output: -f64::INFINITY,
67            max_output: f64::INFINITY,
68            anti_windup: true,
69            setpoint: 0.0,
70            deadband: 0.0,
71        }
72    }
73}
74
75impl ControllerConfig {
76    /// Create a new PID controller configuration with default values.
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Set the proportional gain (Kp).
82    ///
83    /// # Arguments
84    ///
85    /// * `kp` - Proportional gain coefficient
86    ///
87    /// # Returns
88    ///
89    /// * The updated configuration builder
90    ///
91    /// # Notes
92    ///
93    /// While typically positive, negative values are allowed for specialized
94    /// control applications.
95    ///
96    /// # Panics
97    ///
98    /// Panics if the value is NaN or infinity
99    pub fn with_kp(mut self, kp: f64) -> Self {
100        if !kp.is_finite() {
101            panic!("Kp must be a finite number, got: {}", kp);
102        }
103        self.kp = kp;
104        self
105    }
106
107    /// Set the integral gain (Ki).
108    ///
109    /// # Arguments
110    ///
111    /// * `ki` - Integral gain coefficient
112    ///
113    /// # Returns
114    ///
115    /// * The updated configuration builder
116    ///
117    /// # Notes
118    ///
119    /// While typically positive, negative values are allowed for specialized
120    /// control applications.
121    ///
122    /// # Panics
123    ///
124    /// Panics if the value is NaN or infinity
125    pub fn with_ki(mut self, ki: f64) -> Self {
126        if !ki.is_finite() {
127            panic!("Ki must be a finite number, got: {}", ki);
128        }
129        self.ki = ki;
130        self
131    }
132
133    /// Set the derivative gain (Kd).
134    ///
135    /// # Arguments
136    ///
137    /// * `kd` - Derivative gain coefficient
138    ///
139    /// # Returns
140    ///
141    /// * The updated configuration builder
142    ///
143    /// # Notes
144    ///
145    /// While typically positive, negative values are allowed for specialized
146    /// control applications.
147    ///
148    /// # Panics
149    ///
150    /// Panics if the value is NaN or infinity
151    pub fn with_kd(mut self, kd: f64) -> Self {
152        if !kd.is_finite() {
153            panic!("Kd must be a finite number, got: {}", kd);
154        }
155        self.kd = kd;
156        self
157    }
158
159    /// Set the output limits (min, max).
160    pub fn with_output_limits(mut self, min: f64, max: f64) -> Self {
161        self.min_output = min;
162        self.max_output = max;
163        self
164    }
165
166    /// Enable or disable anti-windup.
167    pub fn with_anti_windup(mut self, enable: bool) -> Self {
168        self.anti_windup = enable;
169        self
170    }
171
172    /// Set the initial setpoint (target value).
173    ///
174    /// # Arguments
175    ///
176    /// * `setpoint` - The target value for the controller
177    ///
178    /// # Returns
179    ///
180    /// * The updated configuration builder
181    ///
182    /// # Panics
183    ///
184    /// Panics if the setpoint is NaN or infinity
185    pub fn with_setpoint(mut self, setpoint: f64) -> Self {
186        if !setpoint.is_finite() {
187            panic!("Setpoint must be a finite number, got: {}", setpoint);
188        }
189        self.setpoint = setpoint;
190        self
191    }
192
193    /// Set the deadband value (errors within ±deadband will be treated as zero).
194    ///
195    /// A deadband is useful to prevent control output changes for very small errors,
196    /// which can reduce mechanical wear in physical systems and prevent oscillations
197    /// in systems with backlash or measurement noise.
198    ///
199    /// # Arguments
200    ///
201    /// * `deadband` - The absolute error threshold below which errors are treated as zero
202    ///
203    /// # Returns
204    ///
205    /// * The updated configuration builder
206    ///
207    /// # Panics
208    ///
209    /// Panics if:
210    /// - The deadband is negative (will convert to absolute value)
211    /// - The deadband is NaN or infinity
212    pub fn with_deadband(mut self, deadband: f64) -> Self {
213        if !deadband.is_finite() {
214            panic!("Deadband must be a finite number, got: {}", deadband);
215        }
216
217        self.deadband = deadband.abs(); // Ensure positive value
218        self
219    }
220}
221
222/// Statistics about the controller's performance.
223#[derive(Debug, Clone)]
224pub struct ControllerStatistics {
225    pub average_error: f64, // Average error over time
226    pub max_overshoot: f64, // Maximum overshoot
227    pub settling_time: f64, // Time to settle within acceptable range
228    pub rise_time: f64,     // Time to first reach the setpoint
229}
230
231/// A non-thread-safe PID controller implementation.
232///
233/// Please use ThreadSafePidController for multi-threaded applications.
234///
235/// This implementation follows the standard PID algorithm:
236/// u(t) = Kp * e(t) + Ki * ∫e(t)dt + Kd * de(t)/dt
237///
238/// Where:
239/// - u(t) is the control signal
240/// - e(t) is the error (setpoint - process_variable)
241/// - Kp, Ki, Kd are the proportional, integral, and derivative gains
242pub struct PidController {
243    config: ControllerConfig, // Controller configuration
244    integral: f64,            // Accumulated integral term
245    prev_error: f64,          // Previous error value (for derivative)
246    first_run: bool,          // Flag for first run
247
248    // Statistics tracking
249    start_time: Instant,
250    error_sum: f64,
251    error_count: u64,
252    max_error: f64,
253    reached_setpoint: bool,
254    rise_time: Option<Duration>,
255    settle_time: Option<Duration>,
256    settled_threshold: f64, // Error threshold for considering "settled"
257
258    // Debugging
259    #[cfg(feature = "debugging")]
260    debugger: Option<ControllerDebugger>,
261}
262
263impl PidController {
264    /// Create a new PID controller with the given configuration.
265    pub fn new(config: ControllerConfig) -> Self {
266        PidController {
267            config,
268            integral: 0.0,
269            prev_error: 0.0,
270            first_run: true,
271            start_time: Instant::now(),
272            error_sum: 0.0,
273            error_count: 0,
274            max_error: 0.0,
275            reached_setpoint: false,
276            rise_time: None,
277            settle_time: None,
278            settled_threshold: 0.05, // 5% of setpoint by default
279            #[cfg(feature = "debugging")]
280            debugger: None,
281        }
282    }
283
284    /// Compute the control output based on the process value and time step.
285    ///
286    /// # Arguments
287    /// * `process_value` - The current measured value of the process variable
288    /// * `dt` - Time step in seconds
289    ///
290    /// # Returns
291    /// The control output
292    ///
293    /// # Note
294    /// This method automatically calculates the error as (setpoint - process_value)
295    /// using the setpoint configured in the controller.
296    pub fn compute(&mut self, process_value: f64, dt: f64) -> f64 {
297        // Calculate error using the internal setpoint
298        let error = self.config.setpoint - process_value;
299
300        // Update stats first (using the original error)
301        self.update_statistics(error);
302
303        // Apply deadband - if error is within ±deadband, treat as zero
304        let working_error = if error.abs() <= self.config.deadband {
305            0.0
306        } else {
307            // Preserve sign but reduce magnitude by deadband
308            error - self.config.deadband * error.signum()
309        };
310
311        // Handle first run case
312        if self.first_run {
313            self.first_run = false;
314            self.prev_error = working_error;
315            return 0.0; // No control output on first run
316        }
317
318        // Calculate P term
319        let p_term = self.config.kp * working_error;
320
321        // Calculate I term
322        self.integral += working_error * dt;
323        let i_term = self.config.ki * self.integral;
324
325        // Calculate D term (using filtered derivative to reduce noise)
326        let d_term = if dt > 0.0 {
327            self.config.kd * ((working_error - self.prev_error) / dt)
328        } else {
329            0.0
330        };
331
332        // Store error for next iteration
333        self.prev_error = working_error;
334
335        // Sum the terms
336        let mut output = p_term + i_term + d_term;
337
338        // Apply output limits
339        if output > self.config.max_output {
340            output = self.config.max_output;
341
342            // Anti-windup: prevent integral from growing when saturated
343            if self.config.anti_windup {
344                self.integral -= working_error * dt;
345            }
346        } else if output < self.config.min_output {
347            output = self.config.min_output;
348
349            // Anti-windup: prevent integral from growing when saturated
350            if self.config.anti_windup {
351                self.integral -= working_error * dt;
352            }
353        }
354
355        // Debugging
356        #[cfg(feature = "debugging")]
357        if let Some(ref mut debugger) = self.debugger {
358            debugger.log_pid_state(working_error, p_term, i_term, d_term, output, dt);
359        }
360
361        output
362    }
363
364    /// Reset the controller to its initial state.
365    pub fn reset(&mut self) {
366        self.integral = 0.0;
367        self.prev_error = 0.0;
368        self.first_run = true;
369        self.start_time = Instant::now();
370        self.error_sum = 0.0;
371        self.error_count = 0;
372        self.max_error = 0.0;
373        self.reached_setpoint = false;
374        self.rise_time = None;
375        self.settle_time = None;
376    }
377
378    /// Set the proportional gain (Kp).
379    ///
380    /// # Arguments
381    ///
382    /// * `kp` - Proportional gain coefficient
383    ///
384    /// # Returns
385    ///
386    /// Result indicating success or validation error
387    ///
388    /// # Notes
389    ///
390    /// While typically positive, negative values are allowed for specialized applications.
391    pub fn set_kp(&mut self, kp: f64) -> Result<(), PidError> {
392        if !kp.is_finite() {
393            return Err(PidError::InvalidParameter("kp must be a finite number"));
394        }
395        self.config.kp = kp;
396        Ok(())
397    }
398
399    /// Set the integral gain (Ki).
400    ///
401    /// # Arguments
402    ///
403    /// * `ki` - Integral gain coefficient
404    ///
405    /// # Returns
406    ///
407    /// Result indicating success or validation error
408    ///
409    /// # Notes
410    ///
411    /// While typically positive, negative values are allowed for specialized applications.
412    pub fn set_ki(&mut self, ki: f64) -> Result<(), PidError> {
413        if !ki.is_finite() {
414            return Err(PidError::InvalidParameter("ki must be a finite number"));
415        }
416        self.config.ki = ki;
417        Ok(())
418    }
419
420    /// Set the derivative gain (Kd).
421    ///
422    /// # Arguments
423    ///
424    /// * `kd` - Derivative gain coefficient
425    ///
426    /// # Returns
427    ///
428    /// Result indicating success or validation error
429    ///
430    /// # Notes
431    ///
432    /// While typically positive, negative values are allowed for specialized applications.
433    pub fn set_kd(&mut self, kd: f64) -> Result<(), PidError> {
434        if !kd.is_finite() {
435            return Err(PidError::InvalidParameter("kd must be a finite number"));
436        }
437        self.config.kd = kd;
438        Ok(())
439    }
440
441    /// Set the output limits.
442    pub fn set_output_limits(&mut self, min: f64, max: f64) {
443        self.config.min_output = min;
444        self.config.max_output = max;
445    }
446
447    /// Enable or disable anti-windup.
448    pub fn set_anti_windup(&mut self, enable: bool) {
449        self.config.anti_windup = enable;
450    }
451
452    /// Set the setpoint (target value).
453    ///
454    /// # Arguments
455    ///
456    /// * `setpoint` - The target value for the controller
457    ///
458    /// # Returns
459    ///
460    /// Result indicating success or validation error
461    pub fn set_setpoint(&mut self, setpoint: f64) -> Result<(), PidError> {
462        if !setpoint.is_finite() {
463            return Err(PidError::InvalidParameter(
464                "setpoint must be a finite number",
465            ));
466        }
467
468        self.config.setpoint = setpoint;
469        Ok(())
470    }
471
472    /// Get the setpoint (target value).
473    pub fn setpoint(&self) -> f64 {
474        self.config.setpoint
475    }
476
477    /// Get the controller statistics.
478    pub fn get_statistics(&self) -> ControllerStatistics {
479        let avg_error = if self.error_count > 0 {
480            self.error_sum / self.error_count as f64
481        } else {
482            0.0
483        };
484
485        let settling_time = match self.settle_time {
486            Some(time) => time.as_secs_f64(),
487            None => (Instant::now() - self.start_time).as_secs_f64(),
488        };
489
490        let rise_time = match self.rise_time {
491            Some(time) => time.as_secs_f64(),
492            None => f64::NAN,
493        };
494
495        ControllerStatistics {
496            average_error: avg_error,
497            max_overshoot: self.max_error,
498            settling_time,
499            rise_time,
500        }
501    }
502
503    /// Set the error threshold for considering the system "settled".
504    pub fn set_settled_threshold(&mut self, threshold: f64) {
505        self.settled_threshold = threshold;
506    }
507
508    /// Set the deadband value.
509    ///
510    /// # Arguments
511    ///
512    /// * `deadband` - The absolute error threshold below which errors are treated as zero
513    ///
514    /// # Returns
515    ///
516    /// Result indicating success or validation error
517    pub fn set_deadband(&mut self, deadband: f64) -> Result<(), PidError> {
518        if !deadband.is_finite() {
519            return Err(PidError::InvalidParameter(
520                "deadband must be a finite number",
521            ));
522        }
523
524        self.config.deadband = deadband.abs(); // Ensure positive value
525        Ok(())
526    }
527
528    // Private method to update statistics
529    fn update_statistics(&mut self, error: f64) {
530        // Update error tracking
531        self.error_sum += error.abs();
532        self.error_count += 1;
533
534        // Track max error magnitude (potential overshoot)
535        if error.abs() > self.max_error {
536            self.max_error = error.abs();
537        }
538
539        // Check if we've reached the setpoint for the first time
540        if !self.reached_setpoint && error.abs() <= self.settled_threshold {
541            self.reached_setpoint = true;
542            self.rise_time = Some(Instant::now() - self.start_time);
543        }
544
545        // Check if we've settled (stable within threshold)
546        if self.reached_setpoint
547            && error.abs() <= self.settled_threshold
548            && self.settle_time.is_none()
549        {
550            self.settle_time = Some(Instant::now() - self.start_time);
551        } else if error.abs() > self.settled_threshold {
552            // If we go outside the threshold after settling, reset the settle time
553            self.settle_time = None;
554        }
555    }
556
557    #[cfg(feature = "debugging")]
558    pub fn with_debugging(mut self, debug_config: DebugConfig) -> Self {
559        self.debugger = Some(ControllerDebugger::new(debug_config));
560        self
561    }
562}
563
564/// Thread-safe version of the PID controller.
565///
566/// This controller can be safely shared between threads, such as a sensor
567/// reading thread and a control output thread.
568pub struct ThreadSafePidController {
569    controller: Arc<Mutex<PidController>>,
570}
571
572impl Clone for ThreadSafePidController {
573    fn clone(&self) -> Self {
574        ThreadSafePidController {
575            controller: Arc::clone(&self.controller),
576        }
577    }
578}
579
580impl ThreadSafePidController {
581    /// Create a new thread-safe PID controller.
582    pub fn new(config: ControllerConfig) -> Self {
583        ThreadSafePidController {
584            controller: Arc::new(Mutex::new(PidController::new(config))),
585        }
586    }
587
588    /// Update the controller with the current process value and compute the control output.
589    ///
590    /// # Parameters
591    ///
592    /// * `process_value` - The current measured value
593    /// * `dt` - Time delta in seconds since the last update
594    ///
595    /// # Description
596    ///
597    /// This method automatically calculates the error as (setpoint - process_value)
598    /// using the internally configured setpoint.
599    ///
600    /// # Time Delta (dt)
601    ///
602    /// The `dt` parameter represents the time elapsed since the last controller update, in seconds.
603    /// This is crucial for properly scaling the integral and derivative terms of the PID algorithm.
604    ///
605    /// Common ways to determine `dt`:
606    ///
607    /// 1. Fixed time step: If your control loop runs at a fixed frequency (e.g., 100Hz),
608    ///    you can use a constant value (`dt = 0.01` for 100Hz).
609    ///
610    /// 2. Measured time: Calculate the actual elapsed time between calls:
611    ///    ```
612    ///    let mut last_update_time = std::time::Instant::now(); // In the real world, this would be the last time the controller was updated
613    ///    let now = std::time::Instant::now();
614    ///    let dt = now.duration_since(last_update_time).as_secs_f64();
615    ///    last_update_time = now;
616    ///    ```
617    ///
618    /// Choose the approach that best fits your application's requirements for timing accuracy.
619    pub fn compute(&self, process_value: f64, dt: f64) -> Result<f64, PidError> {
620        let mut controller = self
621            .controller
622            .lock()
623            .map_err(|_| PidError::MutexPoisoned)?;
624        Ok(controller.compute(process_value, dt))
625    }
626
627    /// Reset the controller state.
628    pub fn reset(&self) -> Result<(), PidError> {
629        let mut controller = self
630            .controller
631            .lock()
632            .map_err(|_| PidError::MutexPoisoned)?;
633        controller.reset();
634        Ok(())
635    }
636
637    /// Get the current control signal.
638    pub fn get_control_signal(&self) -> Result<f64, PidError> {
639        let controller = self
640            .controller
641            .lock()
642            .map_err(|_| PidError::MutexPoisoned)?;
643
644        // If first run, return 0.0 (no control signal yet)
645        if controller.first_run {
646            return Ok(0.0);
647        }
648
649        // For getting the last control signal, we don't want to modify state
650        // So we create a simplified version without updating internal state
651        let p_term = controller.config.kp * controller.prev_error;
652        let i_term = controller.config.ki * controller.integral;
653        let d_term = 0.0; // No derivative term when just retrieving the signal
654
655        let mut output = p_term + i_term + d_term;
656
657        // Apply output limits
658        if output > controller.config.max_output {
659            output = controller.config.max_output;
660        } else if output < controller.config.min_output {
661            output = controller.config.min_output;
662        }
663
664        Ok(output)
665    }
666
667    /// Set the proportional gain (Kp).
668    pub fn set_kp(&self, kp: f64) -> Result<(), PidError> {
669        let mut controller = self
670            .controller
671            .lock()
672            .map_err(|_| PidError::MutexPoisoned)?;
673        controller.set_kp(kp)
674    }
675
676    /// Set the integral gain (Ki).
677    pub fn set_ki(&self, ki: f64) -> Result<(), PidError> {
678        let mut controller = self
679            .controller
680            .lock()
681            .map_err(|_| PidError::MutexPoisoned)?;
682        controller.set_ki(ki)
683    }
684
685    /// Set the derivative gain (Kd).
686    pub fn set_kd(&self, kd: f64) -> Result<(), PidError> {
687        let mut controller = self
688            .controller
689            .lock()
690            .map_err(|_| PidError::MutexPoisoned)?;
691        controller.set_kd(kd)
692    }
693
694    /// Set the output limits.
695    pub fn set_output_limits(&self, min: f64, max: f64) -> Result<(), PidError> {
696        let mut controller = self
697            .controller
698            .lock()
699            .map_err(|_| PidError::MutexPoisoned)?;
700        controller.set_output_limits(min, max);
701        Ok(())
702    }
703
704    /// Set the setpoint (target value).
705    ///
706    /// # Arguments
707    ///
708    /// * `setpoint` - The target value for the controller
709    ///
710    /// # Returns
711    ///
712    /// Result indicating success or validation error
713    pub fn set_setpoint(&self, setpoint: f64) -> Result<(), PidError> {
714        let mut controller = self
715            .controller
716            .lock()
717            .map_err(|_| PidError::MutexPoisoned)?;
718        controller.set_setpoint(setpoint)
719    }
720
721    /// Get the controller statistics.
722    pub fn get_statistics(&self) -> Result<ControllerStatistics, PidError> {
723        let controller = self
724            .controller
725            .lock()
726            .map_err(|_| PidError::MutexPoisoned)?;
727        Ok(controller.get_statistics())
728    }
729
730    /// Set the deadband value.
731    ///
732    /// # Arguments
733    ///
734    /// * `deadband` - The absolute error threshold below which errors are treated as zero
735    ///
736    /// # Returns
737    ///
738    /// Result indicating success or validation error
739    pub fn set_deadband(&self, deadband: f64) -> Result<(), PidError> {
740        let mut controller = self
741            .controller
742            .lock()
743            .map_err(|_| PidError::MutexPoisoned)?;
744        controller.set_deadband(deadband)
745    }
746
747    /// Configure debugging if the debugging feature is enabled
748    #[cfg(feature = "debugging")]
749    pub fn with_debugging(self, debug_config: DebugConfig) -> Result<Self, PidError> {
750        // First get a lock on the controller
751        let lock = self
752            .controller
753            .lock()
754            .map_err(|_| PidError::MutexPoisoned)?;
755
756        // Clone the debug_config before putting it inside the controller
757        // to avoid ownership issues
758        let debug_config_clone = debug_config.clone();
759
760        // Create a new controller with debugging by cloning the current controller
761        // and adding the debugger
762        #[allow(unused_mut)]
763        let mut pid_controller = PidController {
764            config: lock.config.clone(),
765            integral: lock.integral,
766            prev_error: lock.prev_error,
767            first_run: lock.first_run,
768            start_time: lock.start_time,
769            error_sum: lock.error_sum,
770            error_count: lock.error_count,
771            max_error: lock.max_error,
772            reached_setpoint: lock.reached_setpoint,
773            rise_time: lock.rise_time,
774            settle_time: lock.settle_time,
775            settled_threshold: lock.settled_threshold,
776            debugger: Some(ControllerDebugger::new(debug_config_clone)),
777        };
778
779        // Create a new ThreadSafePidController with the modified controller
780        Ok(ThreadSafePidController {
781            controller: Arc::new(Mutex::new(pid_controller)),
782        })
783    }
784}
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789    use std::thread;
790    use std::time::Duration;
791
792    #[test]
793    fn test_pid_control() {
794        // Create PID controller with test configuration
795        let config = ControllerConfig::new()
796            .with_kp(2.0) // Increase proportional gain
797            .with_ki(0.5) // Increase integral gain
798            .with_kd(0.05)
799            .with_output_limits(-100.0, 100.0)
800            .with_setpoint(10.0); // Target is 10.0
801
802        let mut controller = PidController::new(config);
803
804        // Test scenario: Start at 0, target is 10
805        let mut process_value = 0.0;
806        let dt = 0.1; // 100ms time step
807
808        // Run for 200 iterations (20 seconds simulated time)
809        for _ in 0..200 {
810            // Compute control signal using process value
811            let control_signal = controller.compute(process_value, dt);
812
813            // Simple process model: value changes proportionally to control signal
814            process_value += control_signal * dt * 0.1;
815
816            // Ensure we're approaching the setpoint
817            if process_value > 9.0 && process_value < 11.0 {
818                break;
819            }
820        }
821
822        // Verify the controller moved the process value close to the setpoint
823        assert!((process_value - 10.0).abs() < 1.0);
824    }
825
826    #[test]
827    fn test_anti_windup() {
828        println!("\n--- Testing Anti-Windup Effect ---");
829
830        // Create two controllers - one with anti-windup and one without
831        let config_with_aw = ControllerConfig::new()
832            .with_kp(0.5)
833            .with_ki(0.5)
834            .with_kd(0.0)
835            .with_setpoint(10.0)
836            .with_output_limits(-1.0, 1.0) // Restrictive limits to cause saturation
837            .with_anti_windup(true); // Enable anti-windup
838
839        let config_without_aw = ControllerConfig::new()
840            .with_kp(0.5)
841            .with_ki(0.5)
842            .with_kd(0.0)
843            .with_setpoint(10.0)
844            .with_output_limits(-1.0, 1.0) // Same restrictive limits
845            .with_anti_windup(false); // Disable anti-windup
846
847        let mut controller_with_aw = PidController::new(config_with_aw);
848        let mut controller_without_aw = PidController::new(config_without_aw);
849
850        // Skip first run to initialize both controllers
851        controller_with_aw.compute(0.0, 0.1);
852        controller_without_aw.compute(0.0, 0.1);
853
854        // Run both controllers with large error to cause saturation
855        // Process value of 0.0 will give error = 10.0 - 0.0 = 10.0
856        let dt = 0.1;
857        let process_value = 0.0;
858
859        println!("Running controllers with large error (10.0) for 20 iterations");
860
861        // Run for multiple iterations to accumulate integral term
862        for i in 0..20 {
863            let out_with_aw = controller_with_aw.compute(process_value, dt);
864            let out_without_aw = controller_without_aw.compute(process_value, dt);
865
866            // Both should quickly saturate at the maximum output
867            if i > 2 {
868                assert_eq!(
869                    out_with_aw, 1.0,
870                    "Controller with anti-windup should saturate"
871                );
872                assert_eq!(
873                    out_without_aw, 1.0,
874                    "Controller without anti-windup should saturate"
875                );
876            }
877
878            if i % 5 == 0 {
879                println!(
880                    "Iteration {}: With AW = {:.4}, Without AW = {:.4}",
881                    i, out_with_aw, out_without_aw
882                );
883            }
884        }
885
886        // After saturation period, the controller with anti-windup should have
887        // a smaller integral term than the one without anti-windup
888        let integral_with_aw = controller_with_aw.integral;
889        let integral_without_aw = controller_without_aw.integral;
890
891        println!("Integral term after saturation:");
892        println!("  With anti-windup: {:.4}", integral_with_aw);
893        println!("  Without anti-windup: {:.4}", integral_without_aw);
894
895        // The key test: anti-windup should limit integral growth during saturation
896        assert!(
897            integral_with_aw < integral_without_aw,
898            "Anti-windup should limit integral term during saturation"
899        );
900
901        // Now test recovery: suddenly change process value to be very close to setpoint
902        // and see how controllers respond
903        println!("\nTesting recovery by setting process value to 9.5");
904        let recovery_value = 9.5; // Close to setpoint of 10.0
905
906        let recovery_with_aw = controller_with_aw.compute(recovery_value, dt);
907        let recovery_without_aw = controller_without_aw.compute(recovery_value, dt);
908
909        println!("Recovery outputs:");
910        println!("  With anti-windup: {:.4}", recovery_with_aw);
911        println!("  Without anti-windup: {:.4}", recovery_without_aw);
912
913        // The controller without anti-windup should have a larger output due to
914        // the accumulated integral term
915        assert!(
916            recovery_with_aw < recovery_without_aw,
917            "Controller with anti-windup should recover faster"
918        );
919    }
920
921    #[test]
922    fn test_deadband() {
923        // Create a controller with deadband of 5.0
924        let config = ControllerConfig::new()
925            .with_kp(1.0) // P-only controller for clear results
926            .with_ki(0.0)
927            .with_kd(0.0)
928            .with_deadband(5.0)
929            .with_setpoint(0.0) // With setpoint=0, process_value directly maps to -error
930            .with_output_limits(-100.0, 100.0);
931
932        let mut controller = PidController::new(config);
933
934        // Test 1: Process value creating error within deadband should result in zero output
935        // Process = -3.0 -> Error = 0.0 - (-3.0) = 3.0 -> Within deadband
936        let process_small_error = -3.0;
937        let output1 = controller.compute(process_small_error, 0.1);
938        assert_eq!(
939            output1, 0.0,
940            "Error within deadband should produce zero output"
941        );
942
943        // Test with process value creating negative error within deadband
944        // Process = 4.0 -> Error = 0.0 - 4.0 = -4.0 -> Within deadband
945        let process_small_negative_error = 4.0;
946        let output2 = controller.compute(process_small_negative_error, 0.1);
947        assert_eq!(
948            output2, 0.0,
949            "Negative error within deadband should produce zero output"
950        );
951
952        // Test 2: Process value creating error outside deadband should be affected by deadband
953        // Process = -15.0 -> Error = 0.0 - (-15.0) = 15.0 -> Outside deadband
954        // With Kp=1.0, output should be (error - deadband) * Kp = (15 - 5) * 1 = 10
955        let process_large_error = -15.0;
956        let output3 = controller.compute(process_large_error, 0.1);
957        assert_eq!(
958            output3, 10.0,
959            "Error outside deadband should be reduced by deadband"
960        );
961
962        // Test with process value creating negative error outside deadband
963        // Process = 25.0 -> Error = 0.0 - 25.0 = -25.0 -> Outside deadband
964        // With Kp=1.0, output should be (error + deadband) * Kp = (-25 + 5) * 1 = -20
965        let process_large_negative_error = 25.0;
966        let output4 = controller.compute(process_large_negative_error, 0.1);
967        assert_eq!(
968            output4, -20.0,
969            "Negative error outside deadband should be reduced by deadband"
970        );
971
972        // Test 3: Dynamically changing deadband
973        controller.set_deadband(10.0).unwrap();
974
975        // Now compute with the same process value but with updated deadband
976        // Process = -15.0 -> Error = 0.0 - (-15.0) = 15.0 -> Outside deadband
977        // With new deadband=10.0: (15 - 10) * 1 = 5
978        let output5 = controller.compute(-15.0, 0.1);
979        assert_eq!(output5, 5.0, "Output should reflect updated deadband value");
980
981        // Test 4: Invalid deadband values
982        assert!(controller.set_deadband(f64::NAN).is_err());
983        assert!(controller.set_deadband(f64::INFINITY).is_err());
984    }
985
986    #[test]
987    fn test_thread_safe_controller() {
988        // Create thread-safe controller
989        let config = ControllerConfig::new()
990            .with_kp(1.0)
991            .with_ki(0.1)
992            .with_kd(0.0)
993            .with_output_limits(-10.0, 10.0)
994            .with_setpoint(10.0);
995
996        let controller = ThreadSafePidController::new(config);
997
998        // Clone controller for thread
999        let thread_controller = controller.clone();
1000
1001        // Start a thread that updates the controller rapidly
1002        let handle = thread::spawn(move || {
1003            for i in 0..100 {
1004                let process_value = i as f64 * 0.1;
1005                // Handle the Result correctly
1006                match thread_controller.compute(process_value, 0.01) {
1007                    Ok(_) => {}
1008                    Err(e) => panic!("Failed to compute: {:?}", e),
1009                }
1010                thread::sleep(Duration::from_millis(1));
1011            }
1012        });
1013
1014        // Meanwhile, read from the controller in the main thread
1015        for _ in 0..10 {
1016            // Handle the Result correctly
1017            let _ = controller
1018                .get_control_signal()
1019                .expect("Failed to get control signal");
1020            thread::sleep(Duration::from_millis(5));
1021        }
1022
1023        // Wait for thread to complete
1024        handle.join().unwrap();
1025
1026        // Check stats - should show that updates happened
1027        let stats = controller
1028            .get_statistics()
1029            .expect("Failed to get statistics");
1030        assert!(stats.average_error > 0.0);
1031    }
1032
1033    #[test]
1034    fn test_parameter_validation() {
1035        let mut controller = PidController::new(ControllerConfig::default());
1036
1037        // Test setpoint validation
1038        assert!(controller.set_setpoint(100.0).is_ok());
1039        assert!(controller.set_setpoint(-100.0).is_ok());
1040        assert!(controller.set_setpoint(f64::NAN).is_err());
1041        assert!(controller.set_setpoint(f64::INFINITY).is_err());
1042
1043        // Test deadband validation
1044        assert!(controller.set_deadband(0.0).is_ok());
1045        assert!(controller.set_deadband(10.0).is_ok());
1046        assert!(controller.set_deadband(-5.0).is_ok()); // Should accept and convert to abs
1047        assert!(controller.set_deadband(f64::NAN).is_err());
1048        assert!(controller.set_deadband(f64::INFINITY).is_err());
1049
1050        // Test gain constants validation
1051        assert!(controller.set_kp(1.0).is_ok());
1052        assert!(controller.set_kp(-1.0).is_ok()); // Negative values allowed
1053        assert!(controller.set_kp(f64::NAN).is_err());
1054        assert!(controller.set_kp(f64::INFINITY).is_err());
1055
1056        assert!(controller.set_ki(0.5).is_ok());
1057        assert!(controller.set_ki(-0.5).is_ok()); // Negative values allowed
1058        assert!(controller.set_ki(f64::NAN).is_err());
1059        assert!(controller.set_ki(f64::INFINITY).is_err());
1060
1061        assert!(controller.set_kd(0.1).is_ok());
1062        assert!(controller.set_kd(-0.1).is_ok()); // Negative values allowed
1063        assert!(controller.set_kd(f64::NAN).is_err());
1064        assert!(controller.set_kd(f64::INFINITY).is_err());
1065    }
1066
1067    #[test]
1068    fn test_negative_gains() {
1069        // Create a simple P-only controller with negative gain for clear results
1070        let config = ControllerConfig::new()
1071            .with_kp(-2.0) // Negative proportional gain
1072            .with_ki(0.0) // No integral gain for simplicity
1073            .with_kd(0.0) // No derivative gain for simplicity
1074            .with_setpoint(0.0) // Setpoint of 0.0 means error = 0.0 - process_value
1075            .with_output_limits(-100.0, 100.0); // Ensure we don't hit limits
1076
1077        let mut controller = PidController::new(config);
1078
1079        // First call will initialize but not produce output due to first_run flag
1080        let init_output = controller.compute(0.0, 0.1);
1081        assert_eq!(init_output, 0.0, "First run should return 0.0");
1082
1083        // For process_value = -5.0, error = 0.0 - (-5.0) = 5.0
1084        // With Kp = -2.0, that gives output = -2.0 * 5.0 = -10.0
1085        let process_value_negative = -5.0;
1086        let output_for_negative_process = controller.compute(process_value_negative, 0.1);
1087        assert_eq!(
1088            output_for_negative_process, -10.0,
1089            "With Kp=-2.0, setpoint=0.0, process_value=-5.0 should give output=-10.0, got {}",
1090            output_for_negative_process
1091        );
1092
1093        // Reset controller for clean test
1094        controller.reset();
1095        let _ = controller.compute(0.0, 0.1); // Skip first run
1096
1097        // For process_value = 5.0, error = 0.0 - 5.0 = -5.0
1098        // With Kp = -2.0, that gives output = -2.0 * (-5.0) = 10.0
1099        let process_value_positive = 5.0;
1100        let output_for_positive_process = controller.compute(process_value_positive, 0.1);
1101        assert_eq!(
1102            output_for_positive_process, 10.0,
1103            "With Kp=-2.0, setpoint=0.0, process_value=5.0 should give output=10.0, got {}",
1104            output_for_positive_process
1105        );
1106    }
1107
1108    #[test]
1109    fn test_steady_state_precision() {
1110        // Create a PID controller with settings like our drone example
1111        let config = ControllerConfig::new()
1112            .with_kp(10.0)
1113            .with_ki(2.0)
1114            .with_kd(8.0)
1115            .with_output_limits(0.0, 100.0)
1116            .with_setpoint(10.0)
1117            .with_anti_windup(true);
1118
1119        let mut controller = PidController::new(config);
1120
1121        // Initial conditions
1122        let mut value = 0.0;
1123        let dt = 0.05;
1124
1125        println!(
1126            "Testing controller precision with default settled_threshold: {}",
1127            controller.settled_threshold
1128        );
1129
1130        // Run 200 iterations (10 seconds) without disturbances
1131        for i in 0..200 {
1132            let control_signal = controller.compute(value, dt);
1133
1134            // Simple plant model (similar to drone without gravity/wind)
1135            value += control_signal * dt * 0.01;
1136
1137            // Print every 20 iterations
1138            if i % 20 == 0 {
1139                println!(
1140                    "Iteration {}: Value = {:.6}, Error = {:.6}, Control = {:.6}",
1141                    i,
1142                    value,
1143                    10.0 - value,
1144                    control_signal
1145                );
1146            }
1147        }
1148
1149        // Check final value - how close to setpoint?
1150        let final_error = (10.0 - value).abs();
1151        println!("Final value: {:.6}, Error: {:.6}", value, final_error);
1152
1153        // Now let's check if it's related to settled_threshold
1154        println!("\nResetting and using smaller settled_threshold...");
1155        controller.reset();
1156        controller.set_settled_threshold(0.001);
1157        value = 0.0;
1158
1159        // Run again with smaller settled_threshold
1160        for i in 0..200 {
1161            let control_signal = controller.compute(value, dt);
1162            value += control_signal * dt * 0.01;
1163
1164            if i % 20 == 0 {
1165                println!(
1166                    "Iteration {}: Value = {:.6}, Error = {:.6}, Control = {:.6}",
1167                    i,
1168                    value,
1169                    10.0 - value,
1170                    control_signal
1171                );
1172            }
1173        }
1174
1175        // Check final value again
1176        let final_error_2 = (10.0 - value).abs();
1177        println!(
1178            "Final value with smaller threshold: {:.6}, Error: {:.6}",
1179            value, final_error_2
1180        );
1181    }
1182
1183    #[test]
1184    fn test_deadband_effect() {
1185        println!("\n--- Testing Deadband Effect ---");
1186
1187        // Create controller with zero deadband
1188        let config = ControllerConfig::new()
1189            .with_kp(10.0)
1190            .with_ki(2.0)
1191            .with_kd(0.0) // No derivative to simplify
1192            .with_output_limits(0.0, 100.0)
1193            .with_setpoint(10.0)
1194            .with_deadband(0.0); // Zero deadband
1195
1196        let mut controller = PidController::new(config);
1197
1198        // Initial state
1199        let mut value = 9.95; // Start very close to setpoint
1200        let dt = 0.05;
1201
1202        println!("Starting with process value: {:.6}", value);
1203
1204        // Run 100 iterations with zero deadband
1205        for i in 0..100 {
1206            let control_signal = controller.compute(value, dt);
1207            value += control_signal * dt * 0.01;
1208
1209            if i % 10 == 0 || i > 90 {
1210                println!(
1211                    "Iteration {}: Value = {:.6}, Error = {:.6}, Integral = {:.6}",
1212                    i,
1213                    value,
1214                    10.0 - value,
1215                    controller.integral
1216                );
1217            }
1218        }
1219
1220        // Final value with zero deadband
1221        let final_value_no_deadband = value;
1222        println!(
1223            "Final value with zero deadband: {:.6}",
1224            final_value_no_deadband
1225        );
1226
1227        // Now test with small deadband
1228        let config = ControllerConfig::new()
1229            .with_kp(10.0)
1230            .with_ki(2.0)
1231            .with_kd(0.0)
1232            .with_output_limits(0.0, 100.0)
1233            .with_setpoint(10.0)
1234            .with_deadband(0.05); // 0.05 deadband
1235
1236        let mut controller = PidController::new(config);
1237
1238        // Reset simulation
1239        value = 9.95;
1240
1241        println!(
1242            "\nStarting with process value: {:.6} and deadband: 0.05",
1243            value
1244        );
1245
1246        // Run 100 iterations with deadband
1247        for i in 0..100 {
1248            let control_signal = controller.compute(value, dt);
1249            value += control_signal * dt * 0.01;
1250
1251            if i % 10 == 0 || i > 90 {
1252                // Get the raw error and the working error after deadband
1253                let error = 10.0 - value;
1254                let working_error = if error.abs() <= 0.05 {
1255                    0.0
1256                } else {
1257                    error - 0.05 * error.signum()
1258                };
1259
1260                println!(
1261                    "Iteration {}: Value = {:.6}, Raw Error = {:.6}, Working Error = {:.6}",
1262                    i, value, error, working_error
1263                );
1264            }
1265        }
1266
1267        // Final value with deadband
1268        let final_value_with_deadband = value;
1269        println!(
1270            "Final value with 0.05 deadband: {:.6}",
1271            final_value_with_deadband
1272        );
1273
1274        // Compare results
1275        println!("\nComparing results:");
1276        println!("Without deadband: {:.6}", final_value_no_deadband);
1277        println!("With 0.05 deadband: {:.6}", final_value_with_deadband);
1278        println!(
1279            "Difference: {:.6}",
1280            (final_value_no_deadband - final_value_with_deadband).abs()
1281        );
1282    }
1283
1284    #[test]
1285    fn test_anti_windup_effect_on_precision() {
1286        println!("\n--- Testing Anti-Windup Effect on Precision ---");
1287
1288        // Controller with anti-windup enabled
1289        let config_with_aw = ControllerConfig::new()
1290            .with_kp(10.0)
1291            .with_ki(2.0)
1292            .with_kd(0.0)
1293            .with_output_limits(0.0, 100.0)
1294            .with_setpoint(10.0)
1295            .with_deadband(0.0)
1296            .with_anti_windup(true);
1297
1298        // Controller with anti-windup disabled
1299        let config_without_aw = ControllerConfig::new()
1300            .with_kp(10.0)
1301            .with_ki(2.0)
1302            .with_kd(0.0)
1303            .with_output_limits(0.0, 100.0)
1304            .with_setpoint(10.0)
1305            .with_deadband(0.0)
1306            .with_anti_windup(false);
1307
1308        let mut controller_with_aw = PidController::new(config_with_aw);
1309        let mut controller_without_aw = PidController::new(config_without_aw);
1310
1311        // Initial state
1312        let mut value_with_aw = 0.0;
1313        let mut value_without_aw = 0.0;
1314        let dt = 0.05;
1315
1316        // Run 200 iterations (10 seconds)
1317        for i in 0..200 {
1318            let control_with_aw = controller_with_aw.compute(value_with_aw, dt);
1319            let control_without_aw = controller_without_aw.compute(value_without_aw, dt);
1320
1321            // Update process values
1322            value_with_aw += control_with_aw * dt * 0.01;
1323            value_without_aw += control_without_aw * dt * 0.01;
1324
1325            if i % 20 == 0 {
1326                println!(
1327                    "Iteration {}: With AW = {:.6}, Without AW = {:.6}, Diff = {:.6}",
1328                    i,
1329                    value_with_aw,
1330                    value_without_aw,
1331                    (value_with_aw - value_without_aw).abs()
1332                );
1333            }
1334        }
1335
1336        // Compare final results
1337        println!("\nFinal values after 200 iterations:");
1338        println!(
1339            "With anti-windup: {:.6}, Error = {:.6}",
1340            value_with_aw,
1341            (10.0 - value_with_aw).abs()
1342        );
1343        println!(
1344            "Without anti-windup: {:.6}, Error = {:.6}",
1345            value_without_aw,
1346            (10.0 - value_without_aw).abs()
1347        );
1348    }
1349
1350    #[test]
1351    fn test_gravity_compensation() {
1352        println!("\n--- Testing Gravity Effect on Steady-State ---");
1353
1354        // Create controller with typical drone parameters
1355        let config = ControllerConfig::new()
1356            .with_kp(10.0)
1357            .with_ki(2.0)
1358            .with_kd(8.0)
1359            .with_output_limits(0.0, 100.0)
1360            .with_setpoint(10.0)
1361            .with_deadband(0.0);
1362
1363        let mut controller = PidController::new(config);
1364
1365        // Simulation parameters - similar to drone example
1366        let mut altitude = 0.0;
1367        let mut velocity = 0.0;
1368        let dt = 0.05;
1369        let drone_mass = 1.2; // kg
1370        let gravity = 9.81; // m/s²
1371        let max_thrust = 30.0; // Newtons
1372
1373        // Run simulation with gravity
1374        println!("Running drone simulation with gravity");
1375        for i in 0..200 {
1376            // Get control signal (0-100%)
1377            let control_signal_percent = controller.compute(altitude, dt);
1378
1379            // Convert to thrust in Newtons
1380            let thrust = control_signal_percent * max_thrust / 100.0;
1381
1382            // Calculate net force with gravity
1383            let net_force = thrust - (drone_mass * gravity);
1384
1385            // Calculate acceleration
1386            let acceleration = net_force / drone_mass;
1387
1388            // Update velocity and position
1389            velocity += acceleration * dt;
1390            altitude += velocity * dt;
1391
1392            if i % 20 == 0 {
1393                let hover_thrust_pct = gravity * drone_mass * 100.0 / max_thrust;
1394                println!(
1395                    "Iteration {}: Alt = {:.4}, Vel = {:.4}, Thrust% = {:.2}, Hover% = {:.2}",
1396                    i, altitude, velocity, control_signal_percent, hover_thrust_pct
1397                );
1398            }
1399        }
1400
1401        // Final position
1402        println!(
1403            "\nFinal altitude: {:.6}, Error: {:.6}",
1404            altitude,
1405            (10.0 - altitude).abs()
1406        );
1407        println!(
1408            "Exact hover thrust percentage: {:.6}%",
1409            gravity * drone_mass * 100.0 / max_thrust
1410        );
1411    }
1412
1413    #[test]
1414    fn test_close_to_setpoint_behavior() {
1415        println!("\n--- Testing Behavior Near Setpoint ---");
1416
1417        // Create a controller with zero deadband to isolate just the physics effects
1418        let config = ControllerConfig::new()
1419            .with_kp(10.0)
1420            .with_ki(2.0)
1421            .with_kd(0.0) // No derivative term to simplify analysis
1422            .with_output_limits(0.0, 100.0)
1423            .with_setpoint(10.0)
1424            .with_deadband(0.0);
1425
1426        let mut controller = PidController::new(config);
1427
1428        // Simulate with physics similar to the drone example
1429        let mut altitude = 9.95; // Start very close to setpoint but just below
1430        let mut velocity = 0.0;
1431        let dt = 0.05;
1432        let drone_mass = 1.2; // kg
1433        let gravity = 9.81; // m/s²
1434        let max_thrust = 30.0; // Newtons
1435
1436        // Calculate exact hover thrust
1437        let hover_thrust_pct = gravity * drone_mass * 100.0 / max_thrust;
1438
1439        println!("Starting conditions:");
1440        println!("Altitude: 9.95 meters (error = 0.05)");
1441        println!("Velocity: 0.0 m/s");
1442        println!("Exact hover thrust: {:.4}%", hover_thrust_pct);
1443
1444        println!("\nStep | Altitude | Error | Control | P Term | I Term | Net Force | Accel");
1445        println!("-----|----------|-------|---------|--------|--------|-----------|------");
1446
1447        // Run a short simulation to observe the behavior
1448        // We'll track the integral term and P term separately
1449        for i in 0..50 {
1450            // Get raw error for diagnostics
1451            let error = 10.0 - altitude;
1452
1453            // Compute control signal and output components
1454            controller.integral = 0.0; // Reset integral term to isolate P component
1455            let p_only = controller.compute(altitude, dt);
1456
1457            // Reset and compute with P+I
1458            controller.integral = error * i as f64 * dt; // Simulate accumulated integral term
1459            let control_signal = controller.compute(altitude, dt);
1460            let i_term = control_signal - p_only;
1461
1462            // Get actual thrust
1463            let thrust = control_signal * max_thrust / 100.0;
1464
1465            // Calculate forces
1466            let weight_force = drone_mass * gravity;
1467            let net_force = thrust - weight_force; // Simplified physics (no drag)
1468            let acceleration = net_force / drone_mass;
1469
1470            // Print the step details
1471            if i % 5 == 0 || i < 5 {
1472                println!(
1473                    "{:4} | {:8.5} | {:5.5} | {:7.4} | {:6.4} | {:6.4} | {:9.5} | {:6.4}",
1474                    i, altitude, error, control_signal, p_only, i_term, net_force, acceleration
1475                );
1476            }
1477
1478            // Update state for next iteration
1479            velocity += acceleration * dt;
1480            altitude += velocity * dt;
1481        }
1482
1483        // Now run a more complete simulation with proper integral term accumulation
1484        println!("\nRunning full simulation with integral term accumulation:");
1485
1486        // Reset with a new config
1487        let config2 = ControllerConfig::new()
1488            .with_kp(10.0)
1489            .with_ki(2.0)
1490            .with_kd(0.0)
1491            .with_output_limits(0.0, 100.0)
1492            .with_setpoint(10.0)
1493            .with_deadband(0.0);
1494
1495        controller = PidController::new(config2);
1496        altitude = 9.95;
1497        velocity = 0.0;
1498
1499        println!("\nStep | Altitude | Error | Control | Integral | P Term | I Term");
1500        println!("-----|----------|-------|---------|----------|--------|-------");
1501
1502        for i in 0..100 {
1503            // Compute control signal
1504            let error_before = 10.0 - altitude;
1505            let control_signal = controller.compute(altitude, dt);
1506
1507            // Convert to thrust and calculate physics
1508            let thrust = control_signal * max_thrust / 100.0;
1509            let weight_force = drone_mass * gravity;
1510            let net_force = thrust - weight_force;
1511            let acceleration = net_force / drone_mass;
1512
1513            // Print diagnostics
1514            if i % 10 == 0 || !(5..=95).contains(&i) {
1515                let p_term = 10.0 * error_before; // Kp * error
1516                let i_term = 2.0 * controller.integral; // Ki * integral
1517
1518                println!(
1519                    "{:4} | {:8.5} | {:5.5} | {:7.4} | {:8.5} | {:6.4} | {:6.4}",
1520                    i, altitude, error_before, control_signal, controller.integral, p_term, i_term
1521                );
1522            }
1523
1524            // Update state for next iteration
1525            velocity += acceleration * dt;
1526            altitude += velocity * dt;
1527        }
1528
1529        println!("\nFinal altitude: {:.6}", altitude);
1530        println!("Difference from setpoint: {:.6}", (10.0 - altitude).abs());
1531
1532        // Now run with various integral gain values
1533        println!("\nTesting different integral gains (Ki):");
1534
1535        for ki in [0.5, 1.0, 2.0, 4.0, 8.0].iter() {
1536            let config = ControllerConfig::new()
1537                .with_kp(10.0)
1538                .with_ki(*ki)
1539                .with_kd(0.0)
1540                .with_output_limits(0.0, 100.0)
1541                .with_setpoint(10.0)
1542                .with_deadband(0.0);
1543
1544            let mut controller = PidController::new(config);
1545            let mut altitude = 9.95;
1546            let mut velocity = 0.0;
1547
1548            // Run simulation for 200 steps
1549            for _ in 0..200 {
1550                let control_signal = controller.compute(altitude, dt);
1551                let thrust = control_signal * max_thrust / 100.0;
1552                let weight_force = drone_mass * gravity;
1553                let net_force = thrust - weight_force;
1554                let acceleration = net_force / drone_mass;
1555
1556                velocity += acceleration * dt;
1557                altitude += velocity * dt;
1558            }
1559
1560            println!(
1561                "Ki = {:.1}: Final altitude = {:.6}, Error = {:.6}",
1562                ki,
1563                altitude,
1564                (10.0 - altitude).abs()
1565            );
1566        }
1567    }
1568
1569    #[test]
1570    fn test_drone_simulation_converging_point() {
1571        println!("\n--- Testing Drone Simulation Convergence ---");
1572
1573        // Create controller identical to the drone simulation
1574        let config = ControllerConfig::new()
1575            .with_kp(10.0)
1576            .with_ki(2.0)
1577            .with_kd(8.0)
1578            .with_output_limits(0.0, 100.0)
1579            .with_setpoint(10.0)
1580            .with_deadband(0.0);
1581
1582        let mut controller = PidController::new(config);
1583
1584        // Drone parameters (matching the example)
1585        let mut altitude = 0.0;
1586        let mut velocity: f64 = 0.0;
1587        let dt = 0.05;
1588        let drone_mass = 1.2;
1589        let gravity = 9.81;
1590        let max_thrust = 30.0;
1591        let motor_response_delay = 0.1;
1592        let drag_coefficient = 0.3;
1593
1594        // Additional variables matching the drone example
1595        let mut commanded_thrust = 0.0;
1596        let mut thrust;
1597
1598        println!("Running simulation with full physics model:");
1599
1600        // Headers
1601        println!("Step | Altitude | Error | Control | Actual Thrust | Net Force | Velocity");
1602        println!("-----|----------|-------|---------|--------------|-----------|----------");
1603
1604        // Our goal is to see at what altitude the system stabilizes
1605        for i in 0..1200 {
1606            // 60 seconds of simulation (at 20Hz)
1607            if i % 200 == 0 {
1608                println!("--- {:1} seconds of simulation ---", i / 20);
1609            }
1610
1611            // Get control signal
1612            let control_signal = controller.compute(altitude, dt);
1613
1614            // Apply motor response delay
1615            commanded_thrust =
1616                commanded_thrust + (control_signal - commanded_thrust) * dt / motor_response_delay;
1617
1618            // Convert to actual thrust
1619            thrust = commanded_thrust * max_thrust / 100.0;
1620
1621            // Calculate forces
1622            let weight_force = drone_mass * gravity;
1623            let drag_force = drag_coefficient * velocity.abs() * velocity;
1624            let net_force = thrust - weight_force - drag_force;
1625            let acceleration = net_force / drone_mass;
1626
1627            // Update state
1628            velocity += acceleration * dt;
1629            altitude += velocity * dt;
1630
1631            // Constrain to ground
1632            if altitude < 0.0 {
1633                altitude = 0.0;
1634                velocity = 0.0;
1635            }
1636
1637            // Print every 100 steps (5 seconds) or when near 10m
1638            if i % 100 == 0 || (altitude > 9.9 && altitude < 10.0 && i % 20 == 0) {
1639                println!(
1640                    "{:4} | {:8.5} | {:6.5} | {:7.4} | {:12.6} | {:9.5} | {:8.6}",
1641                    i,
1642                    altitude,
1643                    10.0 - altitude,
1644                    control_signal,
1645                    thrust,
1646                    net_force,
1647                    velocity
1648                );
1649            }
1650
1651            // Check if altitude has reached a stable value (very small velocity)
1652            if i > 400 && altitude > 9.9 && velocity.abs() < 0.001 {
1653                println!(
1654                    "System stabilized at altitude {:.6} meters at step {}",
1655                    altitude, i
1656                );
1657                println!("Error from setpoint: {:.6} meters", (10.0 - altitude).abs());
1658                break;
1659            }
1660        }
1661
1662        // Now let's test with motor response delay disabled
1663        println!("\nTesting with instant motor response (no delay):");
1664
1665        // Recreate config since previous one was moved
1666        let config2 = ControllerConfig::new()
1667            .with_kp(10.0)
1668            .with_ki(2.0)
1669            .with_kd(8.0)
1670            .with_output_limits(0.0, 100.0)
1671            .with_setpoint(10.0)
1672            .with_deadband(0.0);
1673
1674        controller = PidController::new(config2);
1675        altitude = 0.0;
1676        velocity = 0.0;
1677
1678        for i in 0..1200 {
1679            let control_signal = controller.compute(altitude, dt);
1680
1681            // Skip motor response delay
1682            thrust = control_signal * max_thrust / 100.0;
1683
1684            let weight_force = drone_mass * gravity;
1685            let drag_force = drag_coefficient * velocity.abs() * velocity;
1686            let net_force = thrust - weight_force - drag_force;
1687            let acceleration = net_force / drone_mass;
1688
1689            velocity += acceleration * dt;
1690            altitude += velocity * dt;
1691
1692            if altitude < 0.0 {
1693                altitude = 0.0;
1694                velocity = 0.0;
1695            }
1696
1697            if i % 200 == 0 {
1698                println!(
1699                    "Step {:4}: Altitude = {:.6}, Error = {:.6}",
1700                    i,
1701                    altitude,
1702                    (10.0 - altitude).abs()
1703                );
1704            }
1705
1706            if i > 400 && altitude > 9.9 && velocity.abs() < 0.001 {
1707                println!(
1708                    "System stabilized at altitude {:.6} meters at step {}",
1709                    altitude, i
1710                );
1711                println!("Error from setpoint: {:.6} meters", (10.0 - altitude).abs());
1712                break;
1713            }
1714        }
1715
1716        // Finally, let's test with very high Ki to see if that helps converge exactly to 10.0
1717        println!("\nTesting with high integral gain (Ki = 10.0):");
1718
1719        let high_ki_config = ControllerConfig::new()
1720            .with_kp(10.0)
1721            .with_ki(10.0) // Increased from 2.0 to 10.0
1722            .with_kd(8.0)
1723            .with_output_limits(0.0, 100.0)
1724            .with_setpoint(10.0)
1725            .with_deadband(0.0);
1726
1727        controller = PidController::new(high_ki_config);
1728        altitude = 0.0;
1729        velocity = 0.0;
1730        commanded_thrust = 0.0;
1731
1732        for i in 0..1200 {
1733            let control_signal = controller.compute(altitude, dt);
1734
1735            // Apply motor response delay
1736            commanded_thrust =
1737                commanded_thrust + (control_signal - commanded_thrust) * dt / motor_response_delay;
1738
1739            thrust = commanded_thrust * max_thrust / 100.0;
1740
1741            let weight_force = drone_mass * gravity;
1742            let drag_force = drag_coefficient * velocity.abs() * velocity;
1743            let net_force = thrust - weight_force - drag_force;
1744            let acceleration = net_force / drone_mass;
1745
1746            velocity += acceleration * dt;
1747            altitude += velocity * dt;
1748
1749            if altitude < 0.0 {
1750                altitude = 0.0;
1751                velocity = 0.0;
1752            }
1753
1754            if i % 200 == 0 {
1755                println!(
1756                    "Step {:4}: Altitude = {:.6}, Error = {:.6}",
1757                    i,
1758                    altitude,
1759                    (10.0 - altitude).abs()
1760                );
1761            }
1762
1763            if i > 400 && altitude > 9.9 && velocity.abs() < 0.001 {
1764                println!(
1765                    "System stabilized at altitude {:.6} meters at step {}",
1766                    altitude, i
1767                );
1768                println!("Error from setpoint: {:.6} meters", (10.0 - altitude).abs());
1769                break;
1770            }
1771        }
1772    }
1773}