use crate::time::InstantLike;
use air_filters::iir::pt1::{Pt1Filter, Pt1FilterContext};
use air_filters::{CommonFilterConfig, Filter, FuncFilter};
use num_traits::FloatConst;
use core::time::Duration;
use num_traits::float::FloatCore;
#[cfg(feature = "std")]
use thiserror::Error;
#[cfg_attr(feature = "std", derive(Error))]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PidConfigError {
#[cfg_attr(
feature = "std",
error("Proportional gain is non-positive or non-finite")
)]
InvalidProportionalGain = 1,
#[cfg_attr(feature = "std", error("Integral gain is negative or non-finite"))]
InvalidIntegralGain = 2,
#[cfg_attr(feature = "std", error("Derivative gain is negative or non-finite"))]
InvalidDerivativeGain = 3,
#[cfg_attr(feature = "std", error("Sample time is zero or Duration::MAX"))]
InvalidSampleTime = 4,
#[cfg_attr(feature = "std", error("Output limits are flipped or NAN"))]
InvalidOutputLimits = 5,
#[cfg_attr(
feature = "std",
error("Filter time constant is non-positive or non-finite")
)]
InvalidFilterTimeConstant = 6,
}
#[derive(Copy, Clone, Debug)]
pub struct PidConfig<F: FloatCore> {
kp: F, ki: F, kd: F, filter_tc: F, sample_time: Duration, output_min: F, output_max: F, use_strict_causal_integrator: bool, use_derivative_on_measurement: bool, }
fn check_kp<F: FloatCore>(kp: F) -> Result<(), PidConfigError> {
if kp <= F::zero() || !kp.is_finite() {
return Err(PidConfigError::InvalidProportionalGain);
}
Ok(())
}
fn check_ki<F: FloatCore>(ki: F) -> Result<(), PidConfigError> {
if ki < F::zero() || !ki.is_finite() {
return Err(PidConfigError::InvalidIntegralGain);
}
Ok(())
}
fn check_kd<F: FloatCore>(kd: F) -> Result<(), PidConfigError> {
if kd < F::zero() || !kd.is_finite() {
return Err(PidConfigError::InvalidDerivativeGain);
}
Ok(())
}
fn check_filter_tc<F: FloatCore>(filter_tc: F) -> Result<(), PidConfigError> {
if filter_tc <= F::zero() || !filter_tc.is_finite() {
return Err(PidConfigError::InvalidFilterTimeConstant);
}
Ok(())
}
fn check_sample_time(sample_time: Duration) -> Result<(), PidConfigError> {
if sample_time.is_zero() || sample_time == Duration::MAX {
return Err(PidConfigError::InvalidSampleTime);
}
Ok(())
}
fn check_output_limits<F: FloatCore>(output_min: F, output_max: F) -> Result<(), PidConfigError> {
if output_min >= output_max || output_max.is_nan() || output_min.is_nan() {
return Err(PidConfigError::InvalidOutputLimits);
}
Ok(())
}
impl<F: FloatCore> Default for PidConfig<F> {
fn default() -> Self {
PidConfig {
kp: F::one(),
ki: F::from(0.01).unwrap(),
kd: F::zero(),
filter_tc: F::from(0.01).unwrap(),
sample_time: Duration::from_millis(10),
output_min: -F::infinity(),
output_max: F::infinity(),
use_strict_causal_integrator: false,
use_derivative_on_measurement: false,
}
}
}
impl<F: FloatCore> PidConfig<F> {
pub fn kp(&self) -> F {
self.kp
}
pub fn ki(&self) -> F {
self.ki / F::from(self.sample_time.as_secs_f64()).unwrap()
}
pub fn kd(&self) -> F {
self.kd * F::from(self.sample_time.as_secs_f64()).unwrap()
}
pub fn filter_tc(&self) -> F {
self.filter_tc
}
pub fn sample_time(&self) -> Duration {
self.sample_time
}
pub fn output_min(&self) -> F {
self.output_min
}
pub fn output_max(&self) -> F {
self.output_max
}
pub fn use_strict_causal_integrator(&self) -> bool {
self.use_strict_causal_integrator
}
pub fn use_derivative_on_measurement(&self) -> bool {
self.use_derivative_on_measurement
}
pub fn set_kp(&mut self, kp: F) -> Result<(), PidConfigError> {
check_kp(kp)?;
self.kp = kp;
Ok(())
}
pub fn set_ki(&mut self, ki: F) -> Result<(), PidConfigError> {
check_ki(ki)?;
self.ki = ki * F::from(self.sample_time.as_secs_f64()).unwrap();
Ok(())
}
pub fn set_kd(&mut self, kd: F) -> Result<(), PidConfigError> {
check_kd(kd)?;
self.kd = kd / F::from(self.sample_time.as_secs_f64()).unwrap();
Ok(())
}
pub fn set_filter_tc(&mut self, filter_tc: F) -> Result<(), PidConfigError> {
check_filter_tc(filter_tc)?;
self.filter_tc = filter_tc;
Ok(())
}
pub fn set_sample_time(&mut self, sample_time: Duration) -> Result<(), PidConfigError> {
check_sample_time(sample_time)?;
let ratio = F::from(sample_time.as_secs_f64() / self.sample_time.as_secs_f64()).unwrap();
self.ki = self.ki * ratio;
self.kd = self.kd / ratio;
self.sample_time = sample_time;
Ok(())
}
pub fn set_output_limits(
&mut self,
output_min: F,
output_max: F,
) -> Result<(), PidConfigError> {
check_output_limits(output_min, output_max)?;
self.output_min = output_min;
self.output_max = output_max;
Ok(())
}
pub fn set_use_strict_causal_integrator(&mut self, use_strict_causal_integrator: bool) {
self.use_strict_causal_integrator = use_strict_causal_integrator;
}
pub fn set_use_derivative_on_measurement(&mut self, use_derivative_on_measurement: bool) {
self.use_derivative_on_measurement = use_derivative_on_measurement;
}
}
#[derive(Debug)]
pub struct PidConfigBuilder<F: FloatCore> {
kp: F,
total_ki: F,
total_kd: F,
filter_tc: F,
sample_time: Duration,
output_min: F,
output_max: F,
use_strict_causal_integrator: bool,
use_derivative_on_measurement: bool,
}
impl<F: FloatCore> Default for PidConfigBuilder<F> {
fn default() -> Self {
Self {
kp: F::one(),
total_ki: F::from(0.01).unwrap(),
total_kd: F::zero(),
filter_tc: F::from(0.01).unwrap(),
sample_time: Duration::from_millis(10),
output_min: -F::infinity(),
output_max: F::infinity(),
use_strict_causal_integrator: false,
use_derivative_on_measurement: false,
}
}
}
impl<F: FloatCore> PidConfigBuilder<F> {
pub fn kp(mut self, kp: F) -> Self {
self.kp = kp;
self
}
pub fn ki(mut self, total_ki: F) -> Self {
self.total_ki = total_ki;
self
}
pub fn kd(mut self, total_kd: F) -> Self {
self.total_kd = total_kd;
self
}
pub fn filter_tc(mut self, filter_tc: F) -> Self {
self.filter_tc = filter_tc;
self
}
pub fn sample_time(mut self, sample_time: Duration) -> Self {
self.sample_time = sample_time;
self
}
pub fn output_limits(mut self, min: F, max: F) -> Self {
self.output_min = min;
self.output_max = max;
self
}
pub fn use_strict_causal_integrator(mut self, enabled: bool) -> Self {
self.use_strict_causal_integrator = enabled;
self
}
pub fn use_derivative_on_measurement(mut self, enabled: bool) -> Self {
self.use_derivative_on_measurement = enabled;
self
}
pub fn build(self) -> Result<PidConfig<F>, PidConfigError> {
check_kp(self.kp)?;
check_ki(self.total_ki)?;
check_kd(self.total_kd)?;
check_filter_tc(self.filter_tc)?;
check_sample_time(self.sample_time)?;
check_output_limits(self.output_min, self.output_max)?;
let delta_t = F::from(self.sample_time.as_secs_f64()).unwrap();
Ok(PidConfig {
kp: self.kp,
ki: self.total_ki * delta_t,
kd: self.total_kd / delta_t,
filter_tc: self.filter_tc,
sample_time: self.sample_time,
output_min: self.output_min,
output_max: self.output_max,
use_strict_causal_integrator: self.use_strict_causal_integrator,
use_derivative_on_measurement: self.use_derivative_on_measurement,
})
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum IntegratorActivity {
Inactive,
HoldIntegration,
Active,
}
trait PidAlgorithm<F: FloatCore> {
fn i_term(&self) -> F;
#[must_use]
fn eval_pid(
&mut self,
config: &PidConfig<F>,
error: F,
filtered_derivative: F,
feedforward: F,
integrator_activity: IntegratorActivity,
) -> (F, F, F) {
let mut i_term = self.i_term();
if !config.use_strict_causal_integrator {
i_term = self.compute_i_term(config, error, integrator_activity);
}
let mut output = config.kp * error + i_term + config.kd * filtered_derivative + feedforward;
output = output.clamp(config.output_min, config.output_max);
if config.use_strict_causal_integrator {
i_term = self.compute_i_term(config, error, integrator_activity);
}
(output, i_term, filtered_derivative)
}
#[must_use]
fn compute_i_term(
&self,
config: &PidConfig<F>,
error: F,
integrator_activity: IntegratorActivity,
) -> F {
match integrator_activity {
IntegratorActivity::Inactive => F::zero(),
IntegratorActivity::HoldIntegration => self.i_term(),
IntegratorActivity::Active => {
(self.i_term() + config.ki * error).clamp(config.output_min, config.output_max)
}
}
}
}
pub struct PidController<T: InstantLike, F: FloatCore> {
i_term: F,
prev_err: F,
prev_input: F,
prev_derivative: F,
output: F,
last_time: Option<T>,
is_active: bool,
is_initialized: bool,
integrator_activity: IntegratorActivity,
config: PidConfig<F>,
filter: Pt1Filter<F>,
}
impl<T: InstantLike, F: FloatCore + FloatConst> PidController<T, F> {
pub fn new_uninit(config: PidConfig<F>) -> Self {
let mut cfg = CommonFilterConfig::new();
let _ = cfg.set_cutoff_frequency_hz(F::one() / (F::TAU() * config.filter_tc()));
let _ = cfg
.set_sample_frequency_hz(F::one() / F::from(config.sample_time.as_secs_f64()).unwrap());
Self {
i_term: F::zero(),
prev_err: F::zero(),
prev_input: F::zero(),
prev_derivative: F::zero(),
output: F::zero(),
last_time: None,
is_active: true,
integrator_activity: IntegratorActivity::Active,
is_initialized: false,
config,
filter: Pt1Filter::new(cfg),
}
}
pub fn new(config: PidConfig<F>, timestamp: T, input: F, output: F) -> Self {
let mut cfg = CommonFilterConfig::new();
cfg.set_cutoff_frequency_hz(F::one() / (F::TAU() * config.filter_tc()))
.expect("Invalid filter time constant; This should have been caught by PidConfig validation");
cfg.set_sample_frequency_hz(F::one() / F::from(config.sample_time.as_secs_f64()).unwrap())
.expect("Invalid sample time; This should have been caught by PidConfig validation");
Self {
i_term: output.clamp(config.output_min, config.output_max),
prev_err: F::zero(),
prev_input: input,
prev_derivative: F::zero(),
output: output.clamp(config.output_min, config.output_max),
last_time: Some(timestamp),
is_active: true,
integrator_activity: IntegratorActivity::Active,
is_initialized: true,
config,
filter: Pt1Filter::new(cfg),
}
}
pub fn config(&self) -> &PidConfig<F> {
&self.config
}
pub fn config_mut(&mut self) -> &mut PidConfig<F> {
&mut self.config
}
pub fn compute(&mut self, input: F, setpoint: F, timestamp: T, feedforward: Option<F>) -> F {
if !self.is_active {
return self.output;
}
let error = setpoint - input;
if !self.is_initialized {
self.prev_input = input;
self.prev_err = error;
self.i_term = self.output;
self.i_term = self
.i_term
.clamp(self.config.output_min, self.config.output_max);
self.is_initialized = true;
} else {
let time_delta = timestamp.duration_since(self.last_time.unwrap());
if time_delta < self.config.sample_time {
return self.output;
}
}
let ff = feedforward.unwrap_or(F::zero());
let raw_deriv = if self.config.use_derivative_on_measurement {
self.prev_input - input
} else {
error - self.prev_err
};
let filtered_deriv = self.filter.apply(raw_deriv);
let config = self.config;
let integrator_activity = self.integrator_activity;
(self.output, self.i_term, self.prev_derivative) =
self.eval_pid(&config, error, filtered_deriv, ff, integrator_activity);
self.prev_err = error;
self.prev_input = input;
self.last_time = Some(timestamp);
self.output
}
pub fn output(&self) -> F {
self.output
}
pub fn last_time(&self) -> Option<T> {
self.last_time
}
pub fn is_active(&self) -> bool {
self.is_active
}
pub fn integrator_activity(&self) -> IntegratorActivity {
self.integrator_activity
}
pub fn is_initialized(&self) -> bool {
self.is_initialized
}
pub fn activate(&mut self) {
if !self.is_active {
self.is_initialized = false;
}
self.is_active = true;
}
pub fn deactivate(&mut self) {
self.is_active = false;
}
pub fn reset_integral(&mut self) {
self.i_term = F::zero();
}
pub fn set_integrator_activity(&mut self, activity: IntegratorActivity) {
let is_active = self.integrator_activity != IntegratorActivity::Inactive;
if is_active && activity == IntegratorActivity::Inactive {
self.reset_integral();
}
self.integrator_activity = activity;
}
}
impl<T: InstantLike, F: FloatCore> PidAlgorithm<F> for PidController<T, F> {
fn i_term(&self) -> F {
self.i_term
}
}
#[derive(Copy, Clone, Debug)]
pub struct PidContext<T: InstantLike, F: FloatCore> {
i_term: F,
prev_err: F,
prev_input: F,
prev_derivative: F,
filter_ctx: Pt1FilterContext<F>,
output: F,
last_time: Option<T>,
is_active: bool,
is_initialized: bool,
integrator_activity: IntegratorActivity,
}
impl<T: InstantLike, F: FloatCore> PidContext<T, F> {
pub fn new_uninit() -> Self {
Default::default()
}
pub fn new(timestamp: T, input: F, output: F) -> Self {
Self {
i_term: output,
prev_err: F::zero(),
prev_input: input,
prev_derivative: F::zero(),
filter_ctx: Pt1FilterContext::default(),
output,
last_time: Some(timestamp),
is_active: true,
integrator_activity: IntegratorActivity::Active,
is_initialized: true,
}
}
pub fn output(&self) -> F {
self.output
}
pub fn last_time(&self) -> Option<T> {
self.last_time
}
pub fn is_active(&self) -> bool {
self.is_active
}
pub fn integrator_activity(&self) -> IntegratorActivity {
self.integrator_activity
}
pub fn is_initialized(&self) -> bool {
self.is_initialized
}
pub fn activate(&mut self) {
if !self.is_active {
self.is_initialized = false;
}
self.is_active = true;
}
pub fn deactivate(&mut self) {
self.is_active = false;
}
pub fn reset_integral(&mut self) {
self.i_term = F::zero();
}
pub fn set_integrator_activity(&mut self, activity: IntegratorActivity) {
let is_active = self.integrator_activity != IntegratorActivity::Inactive;
if is_active && activity == IntegratorActivity::Inactive {
self.reset_integral();
}
self.integrator_activity = activity;
}
}
impl<T: InstantLike, F: FloatCore> Default for PidContext<T, F> {
fn default() -> Self {
Self {
i_term: F::zero(),
prev_err: F::zero(),
prev_input: F::zero(),
prev_derivative: F::zero(),
filter_ctx: Pt1FilterContext::default(),
output: F::zero(),
last_time: None,
is_active: true,
integrator_activity: IntegratorActivity::Active,
is_initialized: false,
}
}
}
impl<T: InstantLike, F: FloatCore> PidAlgorithm<F> for PidContext<T, F> {
fn i_term(&self) -> F {
self.i_term
}
}
pub struct FuncPidController<F: FloatCore> {
config: PidConfig<F>,
filter: Pt1Filter<F>,
}
impl<F: FloatCore + FloatConst> FuncPidController<F> {
pub fn new(config: PidConfig<F>) -> Self {
let mut cfg = CommonFilterConfig::new();
let _ = cfg.set_cutoff_frequency_hz(F::one() / (F::TAU() * config.filter_tc()));
let _ = cfg
.set_sample_frequency_hz(F::one() / F::from(config.sample_time.as_secs_f64()).unwrap());
FuncPidController {
filter: Pt1Filter::new(cfg),
config,
}
}
}
impl<F: FloatCore> FuncPidController<F> {
pub fn config(&self) -> &PidConfig<F> {
&self.config
}
pub fn config_mut(&mut self) -> &mut PidConfig<F> {
&mut self.config
}
pub fn compute<T: InstantLike>(
&self,
mut ctx: PidContext<T, F>,
input: F,
setpoint: F,
timestamp: T,
feedforward: Option<F>,
) -> (F, PidContext<T, F>) {
if !ctx.is_active {
return (ctx.output, ctx);
}
let error = setpoint - input;
if !ctx.is_initialized {
ctx.prev_input = input;
ctx.prev_err = error;
ctx.i_term = ctx.output;
ctx.i_term = ctx
.i_term
.clamp(self.config.output_min, self.config.output_max);
ctx.is_initialized = true;
} else {
let time_delta = timestamp.duration_since(ctx.last_time.unwrap());
if time_delta < self.config.sample_time {
return (ctx.output, ctx);
}
}
let ff = feedforward.unwrap_or(F::zero());
let raw_deriv = if self.config.use_derivative_on_measurement {
ctx.prev_input - input
} else {
error - ctx.prev_err
};
let (filtered_deriv, new_filter_ctx) =
self.filter.apply_stateless(raw_deriv, &ctx.filter_ctx);
ctx.filter_ctx = new_filter_ctx;
(ctx.output, ctx.i_term, ctx.prev_derivative) = ctx.eval_pid(
&self.config,
error,
filtered_deriv,
ff,
ctx.integrator_activity,
);
ctx.prev_input = input;
ctx.prev_err = error;
ctx.last_time = Some(timestamp);
(ctx.output, ctx)
}
}