use crate::core::scalar::ControlScalar;
use crate::core::transfer_fn::TransferFn;
use super::ImcError;
#[derive(Debug, Clone, Copy)]
pub struct SmithPredictorConfig<S: ControlScalar, const NM: usize> {
pub kp: S,
pub ki: S,
pub dt: S,
pub model_b: [S; NM],
pub model_a: [S; NM],
pub u_min: S,
pub u_max: S,
}
impl<S: ControlScalar, const NM: usize> SmithPredictorConfig<S, NM> {
pub fn new(kp: S, ki: S, dt: S, model_b: [S; NM], model_a: [S; NM]) -> Self {
let big = S::from_f64(1e9);
Self {
kp,
ki,
dt,
model_b,
model_a,
u_min: -big,
u_max: big,
}
}
pub fn with_limits(mut self, u_min: S, u_max: S) -> Self {
self.u_min = u_min;
self.u_max = u_max;
self
}
}
#[derive(Debug, Clone)]
pub struct SmithPredictor<S: ControlScalar, const NM: usize, const DELAY: usize> {
inner_controller: TransferFn<S, 1>,
model: TransferFn<S, NM>,
model_delayed: TransferFn<S, NM>,
delay_buffer: [S; DELAY],
delay_head: usize,
u_min: S,
u_max: S,
last_u: S,
}
impl<S: ControlScalar, const NM: usize, const DELAY: usize> SmithPredictor<S, NM, DELAY> {
pub fn new(cfg: &SmithPredictorConfig<S, NM>) -> Result<Self, ImcError> {
if cfg.dt <= S::ZERO {
return Err(ImcError::InvalidParameter("dt must be > 0"));
}
if cfg.u_min >= cfg.u_max {
return Err(ImcError::InvalidParameter("u_min must be < u_max"));
}
let ki_dt = cfg.ki * cfg.dt;
let inner_controller = TransferFn::<S, 1>::new([ki_dt], [-S::ONE]);
let model = TransferFn::<S, NM>::new(cfg.model_b, cfg.model_a);
let model_delayed = TransferFn::<S, NM>::new(cfg.model_b, cfg.model_a);
Ok(Self {
inner_controller,
model,
model_delayed,
delay_buffer: [S::ZERO; DELAY],
delay_head: 0,
u_min: cfg.u_min,
u_max: cfg.u_max,
last_u: S::ZERO,
})
}
pub fn update(&mut self, setpoint: S, plant_output: S, kp: S) -> Result<S, ImcError> {
let y_model_undelayed = self.model.process(self.last_u);
let y_model_before_delay = self.model_delayed.process(self.last_u);
let y_model_delayed = if DELAY == 0 {
y_model_before_delay
} else {
let old = self.delay_buffer[self.delay_head];
self.delay_buffer[self.delay_head] = y_model_before_delay;
self.delay_head = (self.delay_head + 1) % DELAY;
old
};
let predictor_correction = y_model_undelayed - y_model_delayed;
let error = setpoint - plant_output - predictor_correction;
let i_term = self.inner_controller.process(error);
let u_raw = kp * error + i_term;
let u_sat = u_raw.clamp_val(self.u_min, self.u_max);
self.last_u = u_sat;
Ok(u_sat)
}
pub fn reset(&mut self) {
self.inner_controller.reset();
self.model.reset();
self.model_delayed.reset();
self.delay_buffer = [S::ZERO; DELAY];
self.delay_head = 0;
self.last_u = S::ZERO;
}
pub fn delay_buffer(&self) -> &[S; DELAY] {
&self.delay_buffer
}
}
#[cfg(test)]
mod tests {
use super::*;
fn first_order_cfg(kp: f64, ki: f64, dt: f64) -> SmithPredictorConfig<f64, 1> {
SmithPredictorConfig::new(kp, ki, dt, [0.2], [-0.8])
}
#[test]
fn smith_construction_ok() {
let cfg = first_order_cfg(2.0, 0.5, 0.1);
let sp = SmithPredictor::<f64, 1, 5>::new(&cfg);
assert!(sp.is_ok(), "Valid config should construct OK");
}
#[test]
fn smith_construction_invalid_dt() {
let cfg = SmithPredictorConfig::<f64, 1>::new(1.0, 0.1, 0.0, [0.2], [-0.8]);
let sp = SmithPredictor::<f64, 1, 3>::new(&cfg);
assert!(
matches!(sp, Err(ImcError::InvalidParameter(_))),
"dt=0 must be rejected"
);
}
#[test]
fn smith_construction_invalid_limits() {
let cfg = SmithPredictorConfig::<f64, 1>::new(1.0, 0.1, 0.1, [0.2], [-0.8])
.with_limits(5.0, -5.0); let sp = SmithPredictor::<f64, 1, 2>::new(&cfg);
assert!(
matches!(sp, Err(ImcError::InvalidParameter(_))),
"Inverted limits must be rejected"
);
}
#[test]
fn smith_zero_delay_converges_to_setpoint() {
let cfg = first_order_cfg(3.0, 1.0, 0.05);
let kp = 3.0_f64;
let mut sp = SmithPredictor::<f64, 1, 0>::new(&cfg).unwrap();
let mut y = 0.0_f64;
let mut plant_state = TransferFn::<f64, 1>::new([0.2], [-0.8]);
let setpoint = 1.0_f64;
for _ in 0..400 {
let u = sp.update(setpoint, y, kp).unwrap();
y = plant_state.process(u);
}
let error = (y - setpoint).abs();
assert!(
error < 0.05,
"PI with zero delay should converge: e={:.4}, y={:.4}",
error,
y
);
}
#[test]
fn smith_delay_compensation_step_response() {
const D: usize = 5;
let cfg = first_order_cfg(2.5, 0.8, 0.05);
let kp = 2.5_f64;
let mut sp = SmithPredictor::<f64, 1, D>::new(&cfg).unwrap();
let mut plant_tf = TransferFn::<f64, 1>::new([0.2], [-0.8]);
let mut plant_delay: [f64; D] = [0.0; D];
let mut delay_head = 0_usize;
let setpoint = 1.0_f64;
let mut y_delayed = 0.0_f64;
for _ in 0..600 {
let u = sp.update(setpoint, y_delayed, kp).unwrap();
let y_plant_raw = plant_tf.process(u);
let oldest = plant_delay[delay_head];
plant_delay[delay_head] = y_plant_raw;
delay_head = (delay_head + 1) % D;
y_delayed = oldest;
}
let error = (y_delayed - setpoint).abs();
assert!(
error < 0.1,
"Smith predictor should compensate delay: e={:.4}, y={:.4}",
error,
y_delayed
);
}
#[test]
fn smith_delay_buffer_correct_size() {
let cfg = first_order_cfg(1.0, 0.2, 0.1);
let sp = SmithPredictor::<f64, 1, 8>::new(&cfg).unwrap();
assert_eq!(sp.delay_buffer().len(), 8);
}
#[test]
fn smith_reset_clears_state() {
let cfg = first_order_cfg(2.0, 0.5, 0.1);
let kp = 2.0_f64;
let mut sp = SmithPredictor::<f64, 1, 3>::new(&cfg).unwrap();
let mut plant = TransferFn::<f64, 1>::new([0.2], [-0.8]);
let mut y = 0.0_f64;
for _ in 0..100 {
let u = sp.update(1.0, y, kp).unwrap();
y = plant.process(u);
}
sp.reset();
let u_after = sp.update(0.0, 0.0, kp).unwrap();
assert!(
u_after.abs() < 1e-10,
"After reset, zero-input should give zero output, got {:.4e}",
u_after
);
}
#[test]
fn smith_saturation_respected() {
let cfg = SmithPredictorConfig::<f64, 1>::new(5.0, 1.0, 0.05, [0.2], [-0.8])
.with_limits(-0.5, 0.5);
let mut sp = SmithPredictor::<f64, 1, 2>::new(&cfg).unwrap();
let u = sp.update(1000.0, 0.0, 5.0).unwrap();
assert!(
u <= 0.5 + 1e-12,
"u should be saturated at 0.5, got {:.4}",
u
);
}
}