use crate::control::lti::state_space::{
ContinuousStateSpace, DiscreteStateSpace, DiscretizationMethod, StateSpaceError,
};
use crate::control::lti::{ContinuousTransferFunction, DiscreteTransferFunction, LtiError};
use crate::scalar::mul_add;
use crate::sparse::compensated::CompensatedField;
use core::fmt;
use faer::Mat;
use faer_traits::RealField;
use num_traits::Float;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AntiWindup<R> {
None,
Clamp {
low: R,
high: R,
},
BackCalculation {
gain: R,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct PidState<R> {
pub integrator: R,
pub derivative_state: R,
}
impl<R> Default for PidState<R>
where
R: Float,
{
fn default() -> Self {
Self {
integrator: R::zero(),
derivative_state: R::zero(),
}
}
}
impl<R> PidState<R>
where
R: Float,
{
pub fn reset(&mut self) {
self.integrator = R::zero();
self.derivative_state = R::zero();
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PidOutput<R> {
pub unsaturated: R,
pub saturated: R,
pub proportional: R,
pub integral: R,
pub derivative: R,
pub windup_error: R,
}
#[derive(Debug)]
pub enum PidError {
InvalidSampleTime,
InvalidDerivativeFilter,
InvalidIntegratorLimits,
InvalidClampLimits,
InvalidBackCalculationGain,
TrackingRequiredForBackCalculation,
NonlinearControllerExport,
Lti(LtiError),
StateSpace(StateSpaceError),
}
impl fmt::Display for PidError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
impl core::error::Error for PidError {}
impl From<LtiError> for PidError {
fn from(value: LtiError) -> Self {
Self::Lti(value)
}
}
impl From<StateSpaceError> for PidError {
fn from(value: StateSpaceError) -> Self {
Self::StateSpace(value)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Pid<R> {
kp: R,
ki: R,
kd: R,
derivative_filter: Option<R>,
anti_windup: AntiWindup<R>,
integrator_limits: Option<(R, R)>,
}
impl<R> Pid<R>
where
R: Float + RealField,
{
pub fn new(
kp: R,
ki: R,
kd: R,
derivative_filter: Option<R>,
anti_windup: AntiWindup<R>,
) -> Result<Self, PidError> {
validate_derivative_filter(kd, derivative_filter)?;
validate_anti_windup(anti_windup)?;
Ok(Self {
kp,
ki,
kd,
derivative_filter,
anti_windup,
integrator_limits: None,
})
}
pub fn with_integrator_limits(mut self, low: R, high: R) -> Result<Self, PidError> {
if !low.is_finite() || !high.is_finite() || low > high {
return Err(PidError::InvalidIntegratorLimits);
}
self.integrator_limits = Some((low, high));
Ok(self)
}
#[must_use]
pub fn kp(&self) -> R {
self.kp
}
#[must_use]
pub fn ki(&self) -> R {
self.ki
}
#[must_use]
pub fn kd(&self) -> R {
self.kd
}
#[must_use]
pub fn derivative_filter(&self) -> Option<R> {
self.derivative_filter
}
#[must_use]
pub fn anti_windup(&self) -> AntiWindup<R> {
self.anti_windup
}
#[must_use]
pub fn integrator_limits(&self) -> Option<(R, R)> {
self.integrator_limits
}
pub fn step(
&self,
state: &mut PidState<R>,
dt: R,
setpoint: R,
measurement: R,
) -> Result<PidOutput<R>, PidError> {
if matches!(self.anti_windup, AntiWindup::BackCalculation { .. }) {
return Err(PidError::TrackingRequiredForBackCalculation);
}
self.step_impl(state, dt, setpoint, measurement, None)
}
pub fn step_with_tracking(
&self,
state: &mut PidState<R>,
dt: R,
setpoint: R,
measurement: R,
u_applied: R,
) -> Result<PidOutput<R>, PidError> {
self.step_impl(state, dt, setpoint, measurement, Some(u_applied))
}
fn step_impl(
&self,
state: &mut PidState<R>,
dt: R,
setpoint: R,
measurement: R,
tracking: Option<R>,
) -> Result<PidOutput<R>, PidError> {
validate_dt(dt)?;
let error = setpoint - measurement;
let (proportional, derivative) = self.output_terms(error, state.derivative_state);
let integral = state.integrator;
let unsaturated = proportional + integral + derivative;
let (saturated, windup_error) = match (self.anti_windup, tracking) {
(AntiWindup::None, _) => (unsaturated, R::zero()),
(AntiWindup::Clamp { low, high }, _) => (unsaturated.max(low).min(high), R::zero()),
(AntiWindup::BackCalculation { .. }, Some(u_applied)) => {
(u_applied, u_applied - unsaturated)
}
(AntiWindup::BackCalculation { .. }, None) => {
return Err(PidError::TrackingRequiredForBackCalculation);
}
};
let next_derivative_state = self.next_derivative_state(error, dt, state.derivative_state);
let next_integrator = self.next_integrator(
dt,
error,
proportional,
derivative,
unsaturated,
saturated,
state.integrator,
);
state.derivative_state = next_derivative_state;
state.integrator = next_integrator;
Ok(PidOutput {
unsaturated,
saturated,
proportional,
integral,
derivative,
windup_error,
})
}
fn output_terms(&self, error: R, derivative_state: R) -> (R, R) {
let proportional = self.kp * error;
let derivative = match effective_derivative_filter(self.kd, self.derivative_filter) {
Some(filter) => self.kd * filter * (error - derivative_state),
None => R::zero(),
};
(proportional, derivative)
}
fn next_derivative_state(&self, error: R, dt: R, derivative_state: R) -> R {
match effective_derivative_filter(self.kd, self.derivative_filter) {
Some(filter) => {
let alpha = (-(filter * dt)).exp();
mul_add(alpha, derivative_state, (R::one() - alpha) * error)
}
None => derivative_state,
}
}
fn next_integrator(
&self,
dt: R,
error: R,
proportional: R,
derivative: R,
unsaturated: R,
saturated: R,
integrator: R,
) -> R {
let correction = match self.anti_windup {
AntiWindup::None => R::zero(),
AntiWindup::Clamp { .. } => R::zero(),
AntiWindup::BackCalculation { gain } => gain * (saturated - unsaturated),
};
let candidate = mul_add(dt, self.ki * error + correction, integrator);
let candidate = apply_integrator_limits(candidate, self.integrator_limits);
match self.anti_windup {
AntiWindup::Clamp { low, high } => {
let limited = candidate
.max(low - proportional - derivative)
.min(high - proportional - derivative);
apply_integrator_limits(limited, self.integrator_limits)
}
_ => candidate,
}
}
pub fn to_state_space_continuous(&self) -> Result<ContinuousStateSpace<R>, PidError> {
self.ensure_linear_export()?;
let (a, b, c, d) = self.continuous_core_matrices()?;
Ok(ContinuousStateSpace::new(a, b, c, d)?)
}
pub fn to_transfer_function_continuous(
&self,
) -> Result<ContinuousTransferFunction<R>, PidError> {
Ok(self.to_state_space_continuous()?.to_transfer_function()?)
}
pub fn to_state_space_discrete(&self, sample_time: R) -> Result<DiscreteStateSpace<R>, PidError>
where
R: CompensatedField,
{
self.ensure_linear_export()?;
validate_dt(sample_time)?;
Ok(self
.to_state_space_continuous()?
.discretize(sample_time, DiscretizationMethod::ZeroOrderHold)?)
}
pub fn to_transfer_function_discrete(
&self,
sample_time: R,
) -> Result<DiscreteTransferFunction<R>, PidError>
where
R: CompensatedField,
{
Ok(self
.to_state_space_discrete(sample_time)?
.to_transfer_function()?)
}
fn ensure_linear_export(&self) -> Result<(), PidError> {
if matches!(self.anti_windup, AntiWindup::None) {
Ok(())
} else {
Err(PidError::NonlinearControllerExport)
}
}
fn continuous_core_matrices(&self) -> Result<(Mat<R>, Mat<R>, Mat<R>, Mat<R>), PidError> {
let has_integrator = self.ki != R::zero();
let derivative_filter = effective_derivative_filter(self.kd, self.derivative_filter);
let has_derivative = derivative_filter.is_some();
let nstates = (has_integrator as usize) + (has_derivative as usize);
let mut a = Mat::<R>::zeros(nstates, nstates);
let mut b = Mat::<R>::zeros(nstates, 1);
let mut c = Mat::<R>::zeros(1, nstates);
let mut d = Mat::<R>::from_fn(1, 1, |_, _| self.kp);
let mut next_state = 0usize;
if has_integrator {
b[(next_state, 0)] = self.ki;
c[(0, next_state)] = R::one();
next_state += 1;
}
if let Some(filter) = derivative_filter {
a[(next_state, next_state)] = -filter;
b[(next_state, 0)] = filter;
c[(0, next_state)] = -(self.kd * filter);
d[(0, 0)] += self.kd * filter;
}
Ok((a, b, c, d))
}
}
fn validate_dt<R>(dt: R) -> Result<(), PidError>
where
R: Float,
{
if dt.is_finite() && dt > R::zero() {
Ok(())
} else {
Err(PidError::InvalidSampleTime)
}
}
fn validate_derivative_filter<R>(kd: R, derivative_filter: Option<R>) -> Result<(), PidError>
where
R: Float,
{
if kd == R::zero() {
return Ok(());
}
match derivative_filter {
Some(value) if value.is_finite() && value > R::zero() => Ok(()),
_ => Err(PidError::InvalidDerivativeFilter),
}
}
fn validate_anti_windup<R>(anti_windup: AntiWindup<R>) -> Result<(), PidError>
where
R: Float,
{
match anti_windup {
AntiWindup::None => Ok(()),
AntiWindup::Clamp { low, high } => {
if low.is_finite() && high.is_finite() && low <= high {
Ok(())
} else {
Err(PidError::InvalidClampLimits)
}
}
AntiWindup::BackCalculation { gain } => {
if gain.is_finite() && gain >= R::zero() {
Ok(())
} else {
Err(PidError::InvalidBackCalculationGain)
}
}
}
}
fn effective_derivative_filter<R>(kd: R, derivative_filter: Option<R>) -> Option<R>
where
R: Float,
{
if kd == R::zero() {
None
} else {
derivative_filter
}
}
fn apply_integrator_limits<R>(value: R, limits: Option<(R, R)>) -> R
where
R: Float,
{
match limits {
Some((low, high)) => value.max(low).min(high),
None => value,
}
}
#[cfg(test)]
mod test {
use super::{AntiWindup, Pid, PidError, PidState};
use faer::complex::Complex;
fn assert_close(lhs: f64, rhs: f64, tol: f64) {
let err = (lhs - rhs).abs();
assert!(err <= tol, "lhs={lhs}, rhs={rhs}, err={err}, tol={tol}");
}
#[test]
fn p_controller_uses_only_proportional_term() {
let pid = Pid::new(2.0, 0.0, 0.0, None, AntiWindup::None).unwrap();
let mut state = PidState::default();
let out = pid.step(&mut state, 0.1, 3.0, 1.0).unwrap();
assert_close(out.proportional, 4.0, 1.0e-12);
assert_close(out.integral, 0.0, 1.0e-12);
assert_close(out.derivative, 0.0, 1.0e-12);
assert_close(out.unsaturated, 4.0, 1.0e-12);
assert_close(out.saturated, 4.0, 1.0e-12);
assert_close(state.integrator, 0.0, 1.0e-12);
}
#[test]
fn pi_controller_accumulates_integral_state() {
let pid = Pid::new(1.0, 2.0, 0.0, None, AntiWindup::None).unwrap();
let mut state = PidState::default();
let out1 = pid.step(&mut state, 0.5, 1.0, 0.0).unwrap();
let out2 = pid.step(&mut state, 0.5, 1.0, 0.0).unwrap();
assert_close(out1.unsaturated, 1.0, 1.0e-12);
assert_close(state.integrator, 2.0, 1.0e-12);
assert_close(out2.integral, 1.0, 1.0e-12);
assert_close(out2.unsaturated, 2.0, 1.0e-12);
}
#[test]
fn pidf_derivative_filter_generates_finite_derivative_term() {
let pid = Pid::new(1.0, 0.0, 0.5, Some(10.0), AntiWindup::None).unwrap();
let mut state = PidState::default();
let out = pid.step(&mut state, 0.1, 1.0, 0.0).unwrap();
assert_close(out.proportional, 1.0, 1.0e-12);
assert_close(out.derivative, 5.0, 1.0e-12);
assert!(state.derivative_state > 0.0);
}
#[test]
fn clamp_mode_limits_output_and_integrator_growth() {
let pid = Pid::new(
0.0,
10.0,
0.0,
None,
AntiWindup::Clamp {
low: -1.0,
high: 1.0,
},
)
.unwrap();
let mut state = PidState::default();
let out1 = pid.step(&mut state, 1.0, 1.0, 0.0).unwrap();
let out2 = pid.step(&mut state, 1.0, 1.0, 0.0).unwrap();
assert_close(out1.saturated, 0.0, 1.0e-12);
assert_close(state.integrator, 1.0, 1.0e-12);
assert_close(out2.unsaturated, 1.0, 1.0e-12);
assert_close(out2.saturated, 1.0, 1.0e-12);
let _ = pid.step(&mut state, 1.0, 1.0, 0.0).unwrap();
assert_close(state.integrator, 1.0, 1.0e-12);
}
#[test]
fn back_calculation_uses_tracking_signal() {
let pid = Pid::new(
0.0,
1.0,
0.0,
None,
AntiWindup::BackCalculation { gain: 2.0 },
)
.unwrap();
let mut state = PidState {
integrator: 1.0,
derivative_state: 0.0,
};
let out = pid
.step_with_tracking(&mut state, 1.0, 1.0, 0.0, 0.0)
.unwrap();
assert_close(out.unsaturated, 1.0, 1.0e-12);
assert_close(out.saturated, 0.0, 1.0e-12);
assert_close(out.windup_error, -1.0, 1.0e-12);
assert_close(state.integrator, 0.0, 1.0e-12);
}
#[test]
fn step_requires_tracking_for_back_calculation() {
let pid = Pid::new(
0.0,
1.0,
0.0,
None,
AntiWindup::BackCalculation { gain: 1.0 },
)
.unwrap();
let mut state = PidState::default();
let err = pid.step(&mut state, 0.1, 1.0, 0.0).unwrap_err();
assert!(matches!(err, PidError::TrackingRequiredForBackCalculation));
}
#[test]
fn linear_export_matches_pi_transfer_function() {
let pid = Pid::new(2.0, 3.0, 0.0, None, AntiWindup::None).unwrap();
let tf = pid.to_transfer_function_continuous().unwrap();
let s = Complex::new(0.5, 0.25);
let expected = 2.0 + 3.0 / s;
let actual = tf.evaluate(s);
assert_close(actual.re, expected.re, 1.0e-10);
assert_close(actual.im, expected.im, 1.0e-10);
}
#[test]
fn linear_export_matches_pidf_transfer_function() {
let pid = Pid::new(1.0, 2.0, 3.0, Some(4.0), AntiWindup::None).unwrap();
let tf = pid.to_transfer_function_continuous().unwrap();
let s = Complex::new(0.7, -0.2);
let expected = 1.0 + 2.0 / s + 3.0 * (4.0 * s) / (s + 4.0);
let actual = tf.evaluate(s);
assert_close(actual.re, expected.re, 1.0e-10);
assert_close(actual.im, expected.im, 1.0e-10);
}
#[test]
fn discrete_linear_export_preserves_sample_time() {
let pid = Pid::new(1.0, 2.0, 0.5, Some(3.0), AntiWindup::None).unwrap();
let tf = pid.to_transfer_function_discrete(0.1).unwrap();
assert_close(tf.sample_time(), 0.1, 1.0e-12);
}
#[test]
fn nonlinear_modes_reject_linear_export() {
let pid = Pid::new(
1.0,
0.0,
0.0,
None,
AntiWindup::Clamp {
low: -1.0,
high: 1.0,
},
)
.unwrap();
let err = pid.to_transfer_function_continuous().unwrap_err();
assert!(matches!(err, PidError::NonlinearControllerExport));
}
#[test]
fn constructor_rejects_invalid_configuration() {
assert!(matches!(
Pid::new(0.0, 0.0, 1.0, None, AntiWindup::None).unwrap_err(),
PidError::InvalidDerivativeFilter
));
assert!(matches!(
Pid::new(
0.0,
0.0,
0.0,
None,
AntiWindup::Clamp {
low: 1.0,
high: -1.0
}
)
.unwrap_err(),
PidError::InvalidClampLimits
));
assert!(matches!(
Pid::new(
0.0,
0.0,
0.0,
None,
AntiWindup::BackCalculation { gain: -1.0 }
)
.unwrap_err(),
PidError::InvalidBackCalculationGain
));
}
}