use crate::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_pid_control() {
let config = ControllerConfig::builder()
.with_kp(2.0)
.with_ki(0.5)
.with_kd(0.05)
.with_output_limits(-100.0, 100.0)
.with_setpoint(10.0)
.build()
.expect("Invalid PID config");
let mut controller = PidController::new(config);
let mut process_value = 0.0;
let dt = 0.1;
for _ in 0..200 {
let control_signal = controller.compute(process_value, dt).unwrap();
process_value += control_signal * dt * 0.1;
if process_value > 9.0 && process_value < 11.0 {
break;
}
}
assert!((process_value - 10.0).abs() < 1.0);
}
#[test]
fn test_anti_windup() {
let config_with_aw = ControllerConfig::builder()
.with_kp(0.5)
.with_ki(0.5)
.with_kd(0.0)
.with_setpoint(10.0)
.with_output_limits(-1.0, 1.0)
.with_anti_windup(true)
.build()
.expect("Invalid config");
let config_without_aw = ControllerConfig::builder()
.with_kp(0.5)
.with_ki(0.5)
.with_kd(0.0)
.with_setpoint(10.0)
.with_output_limits(-1.0, 1.0)
.with_anti_windup(false)
.build()
.expect("Invalid config");
let mut controller_with_aw = PidController::new(config_with_aw);
let mut controller_without_aw = PidController::new(config_without_aw);
for _ in 0..20 {
controller_with_aw.compute(0.0, 0.1).unwrap();
controller_without_aw.compute(0.0, 0.1).unwrap();
}
let recovery_with_aw = controller_with_aw.compute(9.5, 0.1).unwrap();
let recovery_without_aw = controller_without_aw.compute(9.5, 0.1).unwrap();
assert!(
recovery_with_aw < recovery_without_aw,
"Controller with anti-windup ({}) should recover faster (lower output) than without ({})",
recovery_with_aw,
recovery_without_aw
);
}
#[test]
fn test_back_calculation_anti_windup() {
let config_cond = ControllerConfig::builder()
.with_kp(0.5)
.with_ki(2.0)
.with_kd(0.0)
.with_setpoint(10.0)
.with_output_limits(-1.0, 1.0)
.with_anti_windup(true)
.build()
.expect("Invalid config");
let config_backcalc = ControllerConfig::builder()
.with_kp(0.5)
.with_ki(2.0)
.with_kd(0.0)
.with_setpoint(10.0)
.with_output_limits(-1.0, 1.0)
.with_anti_windup_mode(AntiWindupMode::BackCalculation { tracking_time: 0.1 })
.build()
.expect("Invalid config");
let mut ctrl_cond = PidController::new(config_cond);
let mut ctrl_bc = PidController::new(config_backcalc);
for _ in 0..30 {
ctrl_cond.compute(0.0, 0.1).unwrap();
ctrl_bc.compute(0.0, 0.1).unwrap();
}
let mut steps_cond = 0;
let mut steps_bc = 0;
let mut ctrl_cond_clone = PidController::new(
ControllerConfig::builder()
.with_kp(0.5)
.with_ki(2.0)
.with_kd(0.0)
.with_setpoint(10.0)
.with_output_limits(-1.0, 1.0)
.with_anti_windup(true)
.build()
.unwrap(),
);
let mut ctrl_bc_clone = PidController::new(
ControllerConfig::builder()
.with_kp(0.5)
.with_ki(2.0)
.with_kd(0.0)
.with_setpoint(10.0)
.with_output_limits(-1.0, 1.0)
.with_anti_windup_mode(AntiWindupMode::BackCalculation { tracking_time: 0.1 })
.build()
.unwrap(),
);
for _ in 0..30 {
ctrl_cond_clone.compute(0.0, 0.1).unwrap();
ctrl_bc_clone.compute(0.0, 0.1).unwrap();
}
for i in 1..=100 {
let out_cond = ctrl_cond_clone.compute(10.5, 0.1).unwrap();
if steps_cond == 0 && out_cond < 0.0 {
steps_cond = i;
}
let out_bc = ctrl_bc_clone.compute(10.5, 0.1).unwrap();
if steps_bc == 0 && out_bc < 0.0 {
steps_bc = i;
}
if steps_cond > 0 && steps_bc > 0 {
break;
}
}
assert!(
steps_bc <= steps_cond || steps_cond == 0,
"Back-calculation ({} steps) should recover at least as fast as conditional ({} steps)",
steps_bc,
steps_cond
);
}
#[test]
fn test_deadband() {
let config = ControllerConfig::builder()
.with_kp(1.0)
.with_ki(0.0)
.with_kd(0.0)
.with_deadband(5.0)
.with_setpoint(0.0)
.with_output_limits(-100.0, 100.0)
.build()
.expect("Invalid config");
let mut controller = PidController::new(config);
let output1 = controller.compute(-3.0, 0.1).unwrap();
assert_eq!(
output1, 0.0,
"Error within deadband should produce zero output"
);
let output2 = controller.compute(4.0, 0.1).unwrap();
assert_eq!(
output2, 0.0,
"Negative error within deadband should produce zero output"
);
let output3 = controller.compute(-15.0, 0.1).unwrap();
assert_eq!(
output3, 10.0,
"Error outside deadband should be reduced by deadband"
);
let output4 = controller.compute(25.0, 0.1).unwrap();
assert_eq!(
output4, -20.0,
"Negative error outside deadband should be reduced by deadband"
);
controller.set_deadband(10.0).unwrap();
let output5 = controller.compute(-15.0, 0.1).unwrap();
assert_eq!(output5, 5.0, "Output should reflect updated deadband value");
assert!(controller.set_deadband(f64::NAN).is_err());
assert!(controller.set_deadband(f64::INFINITY).is_err());
}
#[test]
fn test_thread_safe_controller() {
let config = ControllerConfig::builder()
.with_kp(1.0)
.with_ki(0.1)
.with_kd(0.0)
.with_output_limits(-10.0, 10.0)
.with_setpoint(10.0)
.build()
.expect("Invalid config");
let controller = ThreadSafePidController::new(config);
let thread_controller = controller.clone();
let handle = thread::spawn(move || {
for i in 0..100 {
let process_value = i as f64 * 0.1;
match thread_controller.compute(process_value, 0.01) {
Ok(_) => {}
Err(e) => panic!("Failed to compute: {:?}", e),
}
thread::sleep(Duration::from_millis(1));
}
});
for _ in 0..10 {
let _ = controller
.get_control_signal()
.expect("Failed to get control signal");
thread::sleep(Duration::from_millis(5));
}
handle.join().unwrap();
let stats = controller
.get_statistics()
.expect("Failed to get statistics");
assert!(stats.average_error > 0.0);
}
#[test]
fn test_parameter_validation() {
let config = ControllerConfig::builder()
.with_output_limits(-100.0, 100.0)
.build()
.expect("Invalid config");
let mut controller = PidController::new(config);
assert!(controller.set_setpoint(100.0).is_ok());
assert!(controller.set_setpoint(-100.0).is_ok());
assert!(controller.set_setpoint(f64::NAN).is_err());
assert!(controller.set_setpoint(f64::INFINITY).is_err());
assert!(controller.set_deadband(0.0).is_ok());
assert!(controller.set_deadband(10.0).is_ok());
assert!(controller.set_deadband(-5.0).is_ok());
assert!(controller.set_deadband(f64::NAN).is_err());
assert!(controller.set_deadband(f64::INFINITY).is_err());
assert!(controller.set_kp(1.0).is_ok());
assert!(controller.set_kp(-1.0).is_ok());
assert!(controller.set_kp(f64::NAN).is_err());
assert!(controller.set_kp(f64::INFINITY).is_err());
assert!(controller.set_ki(0.5).is_ok());
assert!(controller.set_ki(-0.5).is_ok());
assert!(controller.set_ki(f64::NAN).is_err());
assert!(controller.set_ki(f64::INFINITY).is_err());
assert!(controller.set_kd(0.1).is_ok());
assert!(controller.set_kd(-0.1).is_ok());
assert!(controller.set_kd(f64::NAN).is_err());
assert!(controller.set_kd(f64::INFINITY).is_err());
}
#[test]
fn test_negative_gains() {
let config = ControllerConfig::builder()
.with_kp(-2.0)
.with_ki(0.0)
.with_kd(0.0)
.with_setpoint(0.0)
.with_output_limits(-100.0, 100.0)
.build()
.expect("Invalid config");
let mut controller = PidController::new(config);
let init_output = controller.compute(0.0, 0.1).unwrap();
assert_eq!(
init_output, 0.0,
"First run with zero error should return 0.0"
);
let output = controller.compute(-5.0, 0.1).unwrap();
assert_eq!(
output, -10.0,
"With Kp=-2.0, setpoint=0.0, process_value=-5.0 should give output=-10.0, got {}",
output
);
controller.reset();
let first = controller.compute(5.0, 0.1).unwrap();
assert_eq!(first, 10.0, "First run P-only, got {}", first);
let second = controller.compute(5.0, 0.1).unwrap();
assert_eq!(second, 10.0, "Repeated measurement, P-only, got {}", second);
}
#[test]
fn test_cached_output() {
let config = ControllerConfig::builder()
.with_kp(2.0)
.with_ki(0.5)
.with_kd(0.1)
.with_setpoint(10.0)
.with_output_limits(-100.0, 100.0)
.build()
.expect("Invalid config");
let controller = ThreadSafePidController::new(config);
let cached = controller.get_control_signal().unwrap();
assert_eq!(cached, 0.0, "Initial cached output should be 0.0");
let output1 = controller.compute(5.0, 0.1).unwrap();
let cached1 = controller.get_control_signal().unwrap();
assert!(
(output1 - cached1).abs() < f64::EPSILON,
"Cached output ({}) should equal compute output ({})",
cached1,
output1
);
let output2 = controller.compute(7.0, 0.1).unwrap();
let cached2 = controller.get_control_signal().unwrap();
assert!(
(output2 - cached2).abs() < f64::EPSILON,
"Cached output ({}) should equal compute output ({})",
cached2,
output2
);
}