use super::{ControlError, ControlResult, SystemType, TransferFunction};
use scirs2_core::ndarray::Array1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TuningMethod {
ZieglerNichols,
CohenCoon,
Manual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AntiWindup {
None,
Clamp,
BackCalculation,
}
#[derive(Debug, Clone)]
pub struct PIDController {
kp: f64,
ki: f64,
kd: f64,
sample_time: f64,
integral: f64,
prev_error: f64,
output_limits: Option<(f64, f64)>,
integral_limits: Option<(f64, f64)>,
anti_windup: AntiWindup,
derivative_filter: f64,
filtered_derivative: f64,
}
impl PIDController {
pub fn new(kp: f64, ki: f64, kd: f64, sample_time: f64) -> Self {
Self {
kp,
ki,
kd,
sample_time,
integral: 0.0,
prev_error: 0.0,
output_limits: None,
integral_limits: None,
anti_windup: AntiWindup::None,
derivative_filter: 0.0,
filtered_derivative: 0.0,
}
}
pub fn set_output_limits(&mut self, min: f64, max: f64) -> ControlResult<()> {
if min >= max {
return Err(ControlError::InvalidParameters(
"Min limit must be less than max limit".to_string(),
));
}
self.output_limits = Some((min, max));
Ok(())
}
pub fn set_integral_limits(&mut self, min: f64, max: f64) -> ControlResult<()> {
if min >= max {
return Err(ControlError::InvalidParameters(
"Min limit must be less than max limit".to_string(),
));
}
self.integral_limits = Some((min, max));
Ok(())
}
pub fn set_anti_windup(&mut self, method: AntiWindup) {
self.anti_windup = method;
}
pub fn set_derivative_filter(&mut self, alpha: f64) -> ControlResult<()> {
if !(0.0..=1.0).contains(&alpha) {
return Err(ControlError::InvalidParameters(
"Derivative filter coefficient must be between 0 and 1".to_string(),
));
}
self.derivative_filter = alpha;
Ok(())
}
pub fn reset(&mut self) {
self.integral = 0.0;
self.prev_error = 0.0;
self.filtered_derivative = 0.0;
}
pub fn update(&mut self, error: f64) -> f64 {
let p_term = self.kp * error;
self.integral += error * self.sample_time;
if let Some((min, max)) = self.integral_limits {
self.integral = self.integral.clamp(min, max);
}
let i_term = self.ki * self.integral;
let raw_derivative = (error - self.prev_error) / self.sample_time;
self.filtered_derivative = self.derivative_filter * self.filtered_derivative
+ (1.0 - self.derivative_filter) * raw_derivative;
let d_term = self.kd * self.filtered_derivative;
let mut output = p_term + i_term + d_term;
if let Some((min, max)) = self.output_limits {
let unclamped_output = output;
output = output.clamp(min, max);
match self.anti_windup {
AntiWindup::Clamp => {
}
AntiWindup::BackCalculation => {
if output != unclamped_output {
let excess = unclamped_output - output;
self.integral -= excess / self.ki.max(1e-10);
}
}
AntiWindup::None => {}
}
}
self.prev_error = error;
output
}
pub fn gains(&self) -> (f64, f64, f64) {
(self.kp, self.ki, self.kd)
}
pub fn set_gains(&mut self, kp: f64, ki: f64, kd: f64) {
self.kp = kp;
self.ki = ki;
self.kd = kd;
}
pub fn tune_ziegler_nichols(
&mut self,
ultimate_gain: f64,
ultimate_period: f64,
) -> ControlResult<()> {
if ultimate_gain <= 0.0 || ultimate_period <= 0.0 {
return Err(ControlError::InvalidParameters(
"Ultimate gain and period must be positive".to_string(),
));
}
self.kp = 0.6 * ultimate_gain;
self.ki = 1.2 * ultimate_gain / ultimate_period;
self.kd = 0.075 * ultimate_gain * ultimate_period;
Ok(())
}
pub fn tune_cohen_coon(
&mut self,
process_gain: f64,
time_constant: f64,
dead_time: f64,
) -> ControlResult<()> {
if process_gain.abs() < 1e-10 {
return Err(ControlError::InvalidParameters(
"Process gain cannot be zero".to_string(),
));
}
if time_constant <= 0.0 || dead_time <= 0.0 {
return Err(ControlError::InvalidParameters(
"Time constant and dead time must be positive".to_string(),
));
}
let theta_tau = dead_time / time_constant;
self.kp = (1.0 / process_gain) * (time_constant / dead_time) * (1.0 + theta_tau / 3.0);
let ki_factor = (30.0 + 3.0 * theta_tau) / (9.0 + 20.0 * theta_tau);
self.ki = self.kp * ki_factor / time_constant;
let kd_factor = 4.0 / (11.0 + 2.0 * theta_tau);
self.kd = self.kp * kd_factor * dead_time;
Ok(())
}
pub fn to_transfer_function(&self) -> ControlResult<TransferFunction> {
let num = vec![self.kd, self.kp, self.ki];
let den = vec![1.0, 0.0];
TransferFunction::new(num, den)
}
pub fn simulate_step_response(
&mut self,
plant_tf: &TransferFunction,
setpoint: f64,
time_span: (f64, f64),
num_points: usize,
) -> ControlResult<(Vec<f64>, Vec<f64>, Vec<f64>)> {
let (t_start, t_end) = time_span;
let dt = (t_end - t_start) / (num_points - 1) as f64;
if dt <= 0.0 {
return Err(ControlError::InvalidParameters(
"Invalid time span or number of points".to_string(),
));
}
let mut time = Vec::with_capacity(num_points);
let mut output = Vec::with_capacity(num_points);
let mut control = Vec::with_capacity(num_points);
let mut y = 0.0; let mut u;
let poles = plant_tf.poles()?;
let dc_gain = plant_tf.dc_gain();
let tau = if !poles.is_empty() {
1.0 / poles[0].norm().max(0.1)
} else {
1.0
};
self.reset();
for i in 0..num_points {
let t = t_start + i as f64 * dt;
time.push(t);
let error = setpoint - y;
u = self.update(error);
let dydt = (dc_gain * u - y) / tau;
y += dydt * dt;
output.push(y);
control.push(u);
}
Ok((time, output, control))
}
}
pub fn tune_imc(
process_gain: f64,
time_constant: f64,
dead_time: f64,
desired_closed_loop_time_constant: f64,
) -> ControlResult<(f64, f64, f64)> {
if process_gain.abs() < 1e-10 {
return Err(ControlError::InvalidParameters(
"Process gain cannot be zero".to_string(),
));
}
if time_constant <= 0.0 || dead_time < 0.0 || desired_closed_loop_time_constant <= 0.0 {
return Err(ControlError::InvalidParameters(
"Invalid time constants".to_string(),
));
}
let lambda = desired_closed_loop_time_constant;
let kp = time_constant / (process_gain * (lambda + dead_time));
let ki = kp / time_constant;
let kd = 0.0;
Ok((kp, ki, kd))
}
#[derive(Debug, Clone)]
pub struct StepResponseMetrics {
pub rise_time: f64,
pub settling_time: f64,
pub overshoot_percent: f64,
pub peak_time: f64,
pub steady_state_error: f64,
}
pub fn analyze_step_response(
time: &[f64],
output: &[f64],
setpoint: f64,
) -> ControlResult<StepResponseMetrics> {
if time.len() != output.len() || time.is_empty() {
return Err(ControlError::InvalidParameters(
"Time and output arrays must have same non-zero length".to_string(),
));
}
let final_value = output[output.len() - 1];
let steady_state_error = (setpoint - final_value).abs();
let mut peak_value = f64::NEG_INFINITY;
let mut peak_time = 0.0;
for (i, &y) in output.iter().enumerate() {
if y > peak_value {
peak_value = y;
peak_time = time[i];
}
}
let overshoot_percent = if final_value.abs() > 1e-10 {
((peak_value - final_value) / final_value * 100.0).max(0.0)
} else {
0.0
};
let threshold_low = 0.1 * final_value;
let threshold_high = 0.9 * final_value;
let mut t_low = None;
let mut t_high = None;
for (i, &y) in output.iter().enumerate() {
if t_low.is_none() && y >= threshold_low {
t_low = Some(time[i]);
}
if y >= threshold_high {
t_high = Some(time[i]);
break;
}
}
let rise_time = match (t_low, t_high) {
(Some(tl), Some(th)) => th - tl,
_ => 0.0,
};
let tolerance = 0.02 * final_value.abs();
let mut settling_time = time[time.len() - 1];
for i in (0..output.len()).rev() {
if (output[i] - final_value).abs() > tolerance {
if i + 1 < time.len() {
settling_time = time[i + 1];
}
break;
}
}
Ok(StepResponseMetrics {
rise_time,
settling_time,
overshoot_percent,
peak_time,
steady_state_error,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pid_creation() {
let pid = PIDController::new(1.0, 0.5, 0.1, 0.01);
let (kp, ki, kd) = pid.gains();
assert_eq!(kp, 1.0);
assert_eq!(ki, 0.5);
assert_eq!(kd, 0.1);
}
#[test]
fn test_pid_update() {
let mut pid = PIDController::new(1.0, 0.0, 0.0, 0.01);
let output = pid.update(1.0);
assert!((output - 1.0).abs() < 1e-10);
}
#[test]
fn test_output_limits() {
let mut pid = PIDController::new(10.0, 0.0, 0.0, 0.01);
pid.set_output_limits(-5.0, 5.0)
.expect("test: valid output limits");
let output = pid.update(1.0);
assert!((-5.0..=5.0).contains(&output));
}
#[test]
fn test_pid_reset() {
let mut pid = PIDController::new(1.0, 1.0, 0.0, 0.01);
pid.update(1.0);
pid.update(1.0);
pid.reset();
let (_, _, _) = pid.gains();
let output = pid.update(0.0);
assert!((output).abs() < 1e-10);
}
#[test]
fn test_ziegler_nichols_tuning() {
let mut pid = PIDController::new(0.0, 0.0, 0.0, 0.01);
pid.tune_ziegler_nichols(4.0, 2.0)
.expect("test: valid Ziegler-Nichols tuning");
let (kp, ki, kd) = pid.gains();
assert!((kp - 2.4).abs() < 1e-10); assert!((ki - 2.4).abs() < 1e-10); assert!((kd - 0.6).abs() < 1e-10); }
#[test]
fn test_cohen_coon_tuning() {
let mut pid = PIDController::new(0.0, 0.0, 0.0, 0.01);
pid.tune_cohen_coon(1.0, 10.0, 1.0)
.expect("test: valid Cohen-Coon tuning");
let (kp, ki, kd) = pid.gains();
assert!(kp > 0.0);
assert!(ki > 0.0);
assert!(kd > 0.0);
}
#[test]
fn test_imc_tuning() {
let result = tune_imc(1.0, 10.0, 1.0, 5.0);
assert!(result.is_ok());
let (kp, ki, kd) = result.expect("test: valid IMC tuning");
assert!(kp > 0.0);
assert!(ki > 0.0);
assert_eq!(kd, 0.0); }
#[test]
fn test_analyze_step_response() {
let time = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
let output = vec![0.0, 0.5, 0.9, 1.1, 1.0, 1.0];
let setpoint = 1.0;
let metrics = analyze_step_response(&time, &output, setpoint)
.expect("test: valid step response analysis");
assert!(metrics.rise_time > 0.0);
assert!(metrics.overshoot_percent >= 0.0);
assert!(metrics.steady_state_error >= 0.0);
}
#[test]
fn test_pid_to_transfer_function() {
let pid = PIDController::new(1.0, 0.5, 0.1, 0.01);
let tf = pid.to_transfer_function();
assert!(tf.is_ok());
}
}