use crate::*;
#[test]
fn test_build_validates_config() {
assert!(ControllerConfig::builder()
.with_output_limits(-100.0, 100.0)
.build()
.is_ok());
assert!(ControllerConfig::builder()
.with_kp(f64::NAN)
.with_output_limits(-100.0, 100.0)
.build()
.is_err());
assert!(ControllerConfig::builder()
.with_ki(f64::INFINITY)
.with_output_limits(-100.0, 100.0)
.build()
.is_err());
assert!(ControllerConfig::builder()
.with_kd(f64::NAN)
.with_output_limits(-100.0, 100.0)
.build()
.is_err());
assert!(ControllerConfig::builder()
.with_output_limits(100.0, -100.0)
.build()
.is_err());
assert!(ControllerConfig::builder()
.with_output_limits(0.0, 0.0)
.build()
.is_err());
assert!(ControllerConfig::builder()
.with_deadband(f64::NAN)
.with_output_limits(-1.0, 1.0)
.build()
.is_err());
assert!(ControllerConfig::builder()
.with_derivative_filter_coeff(0.0)
.with_output_limits(-1.0, 1.0)
.build()
.is_err());
assert!(ControllerConfig::builder()
.with_derivative_filter_coeff(-1.0)
.with_output_limits(-1.0, 1.0)
.build()
.is_err());
assert!(ControllerConfig::builder()
.with_anti_windup_mode(AntiWindupMode::BackCalculation {
tracking_time: -1.0,
})
.with_output_limits(-1.0, 1.0)
.build()
.is_err());
assert!(ControllerConfig::builder()
.with_anti_windup_mode(AntiWindupMode::BackCalculation { tracking_time: 0.0 })
.with_output_limits(-1.0, 1.0)
.build()
.is_err());
}
#[test]
fn test_compute_validates_inputs() {
let config = ControllerConfig::builder()
.with_output_limits(-100.0, 100.0)
.build()
.unwrap();
let state = PidState::default();
assert!(pid_compute(&config, &state, f64::NAN, 0.1).is_err());
assert!(pid_compute(&config, &state, f64::INFINITY, 0.1).is_err());
assert!(pid_compute(&config, &state, 1.0, f64::NAN).is_err());
assert!(pid_compute(&config, &state, 1.0, -0.1).is_err());
assert!(pid_compute(&config, &state, 1.0, 0.0).is_err());
assert!(pid_compute(&config, &state, 1.0, 0.1).is_ok());
}
#[test]
fn test_first_run_returns_p_plus_i() {
let config = ControllerConfig::builder()
.with_kp(2.0)
.with_ki(1.0)
.with_kd(0.0)
.with_setpoint(10.0)
.with_output_limits(-100.0, 100.0)
.build()
.unwrap();
let state = PidState::default();
let (output, new_state) = pid_compute(&config, &state, 0.0, 0.1).unwrap();
assert!(
(output - 21.0).abs() < 1e-10,
"First run should return P+I, got {}",
output
);
assert!(!new_state.first_run);
assert!((new_state.integral_contribution - 1.0).abs() < 1e-10);
}
#[test]
fn test_pure_compute_with_injected_state() {
let config = ControllerConfig::builder()
.with_kp(1.0)
.with_ki(0.0)
.with_kd(1.0)
.with_setpoint(10.0)
.with_output_limits(-100.0, 100.0)
.with_derivative_mode(DerivativeMode::OnMeasurement)
.build()
.unwrap();
let state = PidState {
integral_contribution: 0.0,
prev_error: 5.0,
prev_measurement: 5.0,
prev_filtered_derivative: 0.0,
last_output: 5.0,
first_run: false,
};
let (output, new_state) = pid_compute(&config, &state, 7.0, 0.1).unwrap();
assert!(
(output - (-7.0)).abs() < 1e-10,
"Expected -7.0, got {}",
output
);
assert!((new_state.prev_measurement - 7.0).abs() < 1e-10);
assert!((new_state.prev_filtered_derivative - (-10.0)).abs() < 1e-10);
}
#[test]
fn test_derivative_kick_elimination() {
let config = ControllerConfig::builder()
.with_kp(0.0)
.with_ki(0.0)
.with_kd(10.0)
.with_setpoint(10.0)
.with_output_limits(-1000.0, 1000.0)
.with_derivative_mode(DerivativeMode::OnMeasurement)
.build()
.unwrap();
let mut state = PidState::default();
for _ in 0..5 {
let (_, new_state) = pid_compute(&config, &state, 5.0, 0.1).unwrap();
state = new_state;
}
let mut config2 = config.clone();
config2.setpoint = 100.0;
let (output, _) = pid_compute(&config2, &state, 5.0, 0.1).unwrap();
assert!(
output.abs() < 1.0,
"OnMeasurement derivative should not spike on setpoint change, got {}",
output
);
}
#[test]
fn test_derivative_filter() {
let config_filtered = ControllerConfig::builder()
.with_kp(0.0)
.with_ki(0.0)
.with_kd(1.0)
.with_setpoint(0.0)
.with_output_limits(-1000.0, 1000.0)
.with_derivative_mode(DerivativeMode::OnMeasurement)
.with_derivative_filter_coeff(2.0) .build()
.unwrap();
let config_less_filtered = ControllerConfig::builder()
.with_kp(0.0)
.with_ki(0.0)
.with_kd(1.0)
.with_setpoint(0.0)
.with_output_limits(-1000.0, 1000.0)
.with_derivative_mode(DerivativeMode::OnMeasurement)
.with_derivative_filter_coeff(100.0) .build()
.unwrap();
let mut state_f = PidState::default();
let mut state_u = PidState::default();
let mut outputs_filtered = Vec::new();
let mut outputs_unfiltered = Vec::new();
for i in 0..20 {
let pv = if i % 2 == 0 { 1.0 } else { -1.0 };
let (out_f, ns_f) = pid_compute(&config_filtered, &state_f, pv, 0.1).unwrap();
let (out_u, ns_u) = pid_compute(&config_less_filtered, &state_u, pv, 0.1).unwrap();
state_f = ns_f;
state_u = ns_u;
if i > 0 {
outputs_filtered.push(out_f);
outputs_unfiltered.push(out_u);
}
}
let var_f: f64 = {
let mean = outputs_filtered.iter().sum::<f64>() / outputs_filtered.len() as f64;
outputs_filtered
.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>()
/ outputs_filtered.len() as f64
};
let var_u: f64 = {
let mean = outputs_unfiltered.iter().sum::<f64>() / outputs_unfiltered.len() as f64;
outputs_unfiltered
.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>()
/ outputs_unfiltered.len() as f64
};
assert!(
var_f < var_u,
"Filtered variance ({}) should be less than unfiltered ({})",
var_f,
var_u
);
}