#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use core::fmt;
use num_traits::Float;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub enum SafetyStatus {
Normal,
OutputSaturated,
IntegralWindup,
DerivativeNoisy,
TimeStepExcessive,
MultipleFaults,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub enum AntiWindupMode {
Clamping,
BackCalculation,
Conditional,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub enum DerivativeMode {
None,
LowPass,
SimpleMovingAverage,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct PidConfig<T: Float> {
pub kp: T,
pub ki: T,
pub kd: T,
pub output_max: T,
pub output_min: T,
pub output_rate_limit: Option<T>,
pub p_limit: T,
pub i_limit: T,
pub d_limit: T,
pub antiwindup_mode: AntiWindupMode,
pub antiwindup_gain: T,
pub derivative_mode: DerivativeMode,
pub derivative_filter_coeff: T,
pub max_dt: T,
pub min_dt: T,
pub setpoint_ramping: bool,
pub setpoint_ramp_rate: T,
}
impl<T: Float> Default for PidConfig<T> {
fn default() -> Self {
Self {
kp: T::one(),
ki: T::zero(),
kd: T::zero(),
output_max: T::from(100.0).unwrap(),
output_min: T::from(-100.0).unwrap(),
output_rate_limit: None,
p_limit: T::infinity(),
i_limit: T::from(100.0).unwrap(),
d_limit: T::infinity(),
antiwindup_mode: AntiWindupMode::BackCalculation,
antiwindup_gain: T::one(),
derivative_mode: DerivativeMode::LowPass,
derivative_filter_coeff: T::from(0.2).unwrap(),
max_dt: T::from(1.0).unwrap(),
min_dt: T::from(0.0001).unwrap(),
setpoint_ramping: false,
setpoint_ramp_rate: T::from(10.0).unwrap(),
}
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct ControlOutput<T: Float> {
pub output: T,
pub p: T,
pub i: T,
pub d: T,
pub error: T,
pub effective_setpoint: T,
pub safety_status: SafetyStatus,
pub dt: T,
}
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct RobustPid<T: Float> {
config: PidConfig<T>,
setpoint: T,
internal_setpoint: T,
integral: T,
prev_measurement: Option<T>,
filtered_derivative: T,
prev_output: T,
prev_error: Option<T>,
update_count: u64,
saturation_count: u64,
max_error: T,
}
impl<T: Float> RobustPid<T> {
pub fn new(config: PidConfig<T>) -> Self {
Self {
internal_setpoint: T::zero(),
setpoint: T::zero(),
config,
integral: T::zero(),
prev_measurement: None,
filtered_derivative: T::zero(),
prev_output: T::zero(),
prev_error: None,
update_count: 0,
saturation_count: 0,
max_error: T::zero(),
}
}
pub fn with_gains(kp: T, ki: T, kd: T) -> Self {
let mut config = PidConfig::default();
config.kp = kp;
config.ki = ki;
config.kd = kd;
Self::new(config)
}
pub fn set_setpoint(&mut self, setpoint: T) {
self.setpoint = setpoint;
if !self.config.setpoint_ramping {
self.internal_setpoint = setpoint;
}
}
pub fn update_config(&mut self, config: PidConfig<T>) {
if config.ki != self.config.ki && config.ki != T::zero() {
self.integral = self.integral * (config.ki / self.config.ki);
}
self.config = config;
}
pub fn config(&self) -> &PidConfig<T> {
&self.config
}
pub fn update(&mut self, measurement: T, dt: T) -> ControlOutput<T> {
self.update_count += 1;
let dt = self.clamp_dt(dt);
if self.config.setpoint_ramping {
self.update_setpoint_ramp(dt);
}
let error = self.internal_setpoint - measurement;
self.update_with_error_and_measurement(error, measurement, dt)
}
pub fn update_from_error(&mut self, error: T, dt: T) -> ControlOutput<T> {
self.update_count += 1;
let dt = self.clamp_dt(dt);
let calculated_measurement = match self.prev_error {
Some(prev_err) => {
match self.prev_measurement {
Some(prev_meas) => prev_meas - (error - prev_err),
None => T::zero(), }
}
None => T::zero(), };
self.update_with_error_and_measurement(error, calculated_measurement, dt)
}
fn update_with_error_and_measurement(
&mut self,
error: T,
measurement: T,
dt: T,
) -> ControlOutput<T> {
if error.abs() > self.max_error {
self.max_error = error.abs();
}
let p_unbounded = error * self.config.kp;
let p = self.clamp(p_unbounded, self.config.p_limit);
let d = self.calculate_derivative(measurement, dt);
let tentative_output = p + self.integral + d;
let i = self.update_integral(error, dt, tentative_output);
let mut output = p + i + d;
output = self.clamp_output(output);
output = self.apply_rate_limit(output, dt);
let safety_status = self.determine_safety_status(output, error, dt);
self.prev_measurement = Some(measurement);
self.prev_error = Some(error);
self.prev_output = output;
ControlOutput {
output,
p,
i,
d,
error,
effective_setpoint: self.internal_setpoint,
safety_status,
dt,
}
}
fn calculate_derivative(&mut self, measurement: T, dt: T) -> T {
let raw_derivative = match self.prev_measurement {
Some(prev_measurement) => {
-(measurement - prev_measurement) / dt
}
None => T::zero(),
};
let derivative = match self.config.derivative_mode {
DerivativeMode::None => raw_derivative,
DerivativeMode::LowPass => {
let alpha = self.config.derivative_filter_coeff;
alpha * raw_derivative + (T::one() - alpha) * self.filtered_derivative
}
DerivativeMode::SimpleMovingAverage => {
let alpha = self.config.derivative_filter_coeff;
alpha * raw_derivative + (T::one() - alpha) * self.filtered_derivative
}
};
self.filtered_derivative = derivative;
let d_unbounded = derivative * self.config.kd;
self.clamp(d_unbounded, self.config.d_limit)
}
fn update_integral(&mut self, error: T, dt: T, tentative_output: T) -> T {
let error_contribution = error * self.config.ki * dt;
match self.config.antiwindup_mode {
AntiWindupMode::Clamping => {
let tentative_integral = self.integral + error_contribution;
self.integral = self.clamp(tentative_integral, self.config.i_limit);
}
AntiWindupMode::BackCalculation => {
let clamped_output = self.clamp_output(tentative_output);
let output_error = clamped_output - tentative_output;
let integral_correction = output_error * self.config.antiwindup_gain * dt;
let tentative_integral = self.integral + error_contribution + integral_correction;
self.integral = self.clamp(tentative_integral, self.config.i_limit);
}
AntiWindupMode::Conditional => {
let clamped_output = self.clamp_output(tentative_output);
if (tentative_output - clamped_output).abs() < T::epsilon() {
let tentative_integral = self.integral + error_contribution;
self.integral = self.clamp(tentative_integral, self.config.i_limit);
}
}
}
self.integral
}
fn update_setpoint_ramp(&mut self, dt: T) {
let error = self.setpoint - self.internal_setpoint;
let max_change = self.config.setpoint_ramp_rate * dt;
if error.abs() <= max_change {
self.internal_setpoint = self.setpoint;
} else {
let sign = if error > T::zero() {
T::one()
} else {
-T::one()
};
self.internal_setpoint = self.internal_setpoint + sign * max_change;
}
}
fn apply_rate_limit(&self, output: T, dt: T) -> T {
if let Some(rate_limit) = self.config.output_rate_limit {
let max_change = rate_limit * dt;
let change = output - self.prev_output;
if change.abs() > max_change {
let sign = if change > T::zero() {
T::one()
} else {
-T::one()
};
self.prev_output + sign * max_change
} else {
output
}
} else {
output
}
}
fn clamp(&self, value: T, limit: T) -> T {
let limit_abs = limit.abs();
if value > limit_abs {
limit_abs
} else if value < -limit_abs {
-limit_abs
} else {
value
}
}
fn clamp_output(&mut self, value: T) -> T {
let clamped = if value > self.config.output_max {
self.saturation_count += 1;
self.config.output_max
} else if value < self.config.output_min {
self.saturation_count += 1;
self.config.output_min
} else {
value
};
clamped
}
fn clamp_dt(&self, dt: T) -> T {
if dt < self.config.min_dt {
self.config.min_dt
} else if dt > self.config.max_dt {
self.config.max_dt
} else {
dt
}
}
fn determine_safety_status(&self, output: T, error: T, dt: T) -> SafetyStatus {
let mut fault_count = 0;
let mut status = SafetyStatus::Normal;
if (output - self.config.output_max).abs() < T::epsilon()
|| (output - self.config.output_min).abs() < T::epsilon()
{
status = SafetyStatus::OutputSaturated;
fault_count += 1;
}
if (self.integral.abs() - self.config.i_limit).abs() < T::epsilon() {
status = SafetyStatus::IntegralWindup;
fault_count += 1;
}
if error != T::zero()
&& self.filtered_derivative.abs() > error.abs() * T::from(10.0).unwrap()
{
status = SafetyStatus::DerivativeNoisy;
fault_count += 1;
}
if dt >= self.config.max_dt {
status = SafetyStatus::TimeStepExcessive;
fault_count += 1;
}
if fault_count > 1 {
SafetyStatus::MultipleFaults
} else {
status
}
}
pub fn reset(&mut self) {
self.integral = T::zero();
self.prev_measurement = None;
self.filtered_derivative = T::zero();
self.prev_output = T::zero();
self.prev_error = None;
self.internal_setpoint = self.setpoint;
}
pub fn reset_integral(&mut self) {
self.integral = T::zero();
}
pub fn preload_integral(&mut self, value: T) {
self.integral = self.clamp(value, self.config.i_limit);
}
pub fn get_integral_term(&self) -> T {
self.integral
}
pub fn diagnostics(&self) -> PidDiagnostics<T> {
PidDiagnostics {
update_count: self.update_count,
saturation_count: self.saturation_count,
saturation_ratio: if self.update_count > 0 {
T::from(self.saturation_count).unwrap() / T::from(self.update_count).unwrap()
} else {
T::zero()
},
max_error: self.max_error,
current_integral: self.integral,
current_derivative: self.filtered_derivative,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PidDiagnostics<T: Float> {
pub update_count: u64,
pub saturation_count: u64,
pub saturation_ratio: T,
pub max_error: T,
pub current_integral: T,
pub current_derivative: T,
}
impl<T: Float + fmt::Display> fmt::Display for PidDiagnostics<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"PID Diagnostics:\n\
Updates: {}\n\
Saturations: {} ({:.2}%)\n\
Max Error: {}\n\
Current Integral: {}\n\
Current Derivative: {}",
self.update_count,
self.saturation_count,
self.saturation_ratio * T::from(100.0).unwrap(),
self.max_error,
self.current_integral,
self.current_derivative
)
}
}
pub struct PidBuilder<T: Float> {
config: PidConfig<T>,
}
impl<T: Float> PidBuilder<T> {
pub fn new() -> Self {
Self {
config: PidConfig::default(),
}
}
pub fn gains(mut self, kp: T, ki: T, kd: T) -> Self {
self.config.kp = kp;
self.config.ki = ki;
self.config.kd = kd;
self
}
pub fn output_limits(mut self, min: T, max: T) -> Self {
self.config.output_min = min;
self.config.output_max = max;
self
}
pub fn term_limits(mut self, p_limit: T, i_limit: T, d_limit: T) -> Self {
self.config.p_limit = p_limit;
self.config.i_limit = i_limit;
self.config.d_limit = d_limit;
self
}
pub fn output_rate_limit(mut self, rate: T) -> Self {
self.config.output_rate_limit = Some(rate);
self
}
pub fn antiwindup(mut self, mode: AntiWindupMode, gain: T) -> Self {
self.config.antiwindup_mode = mode;
self.config.antiwindup_gain = gain;
self
}
pub fn derivative_filter(mut self, mode: DerivativeMode, coeff: T) -> Self {
self.config.derivative_mode = mode;
self.config.derivative_filter_coeff = coeff;
self
}
pub fn setpoint_ramping(mut self, enabled: bool, rate: T) -> Self {
self.config.setpoint_ramping = enabled;
self.config.setpoint_ramp_rate = rate;
self
}
pub fn time_limits(mut self, min_dt: T, max_dt: T) -> Self {
self.config.min_dt = min_dt;
self.config.max_dt = max_dt;
self
}
pub fn build(self) -> RobustPid<T> {
RobustPid::new(self.config)
}
}
impl<T: Float> Default for PidBuilder<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_proportional_only() {
let mut pid = PidBuilder::new()
.gains(2.0, 0.0, 0.0)
.output_limits(-100.0, 100.0)
.build();
pid.set_setpoint(10.0);
let output = pid.update(5.0, 1.0);
assert_eq!(output.p, 10.0); assert_eq!(output.i, 0.0);
assert_eq!(output.d, 0.0);
assert_eq!(output.output, 10.0);
}
#[test]
fn test_integral_accumulation() {
let mut pid = PidBuilder::new()
.gains(0.0, 1.0, 0.0)
.output_limits(-100.0, 100.0)
.build();
pid.set_setpoint(10.0);
let output1 = pid.update(8.0, 1.0); assert_eq!(output1.i, 2.0);
let output2 = pid.update(8.0, 1.0); assert_eq!(output2.i, 4.0);
}
#[test]
fn test_output_saturation() {
let mut pid = PidBuilder::new()
.gains(10.0, 0.0, 0.0)
.output_limits(-50.0, 50.0)
.build();
pid.set_setpoint(100.0);
let output = pid.update(0.0, 1.0);
assert_eq!(output.output, 50.0); assert!(matches!(
output.safety_status,
SafetyStatus::OutputSaturated | SafetyStatus::MultipleFaults
));
}
}