use crate::core::scalar::ControlScalar;
use crate::core::signal::{ControlOutput, Feedback, Setpoint};
use crate::core::traits::Controller;
use crate::pid::standard::{Pid, PidConfig};
pub struct CascadePid<S: ControlScalar> {
outer: Pid<S>,
inner: Pid<S>,
}
impl<S: ControlScalar> CascadePid<S> {
pub fn new(outer_config: PidConfig<S>, inner_config: PidConfig<S>) -> Self {
Self {
outer: outer_config.build(),
inner: inner_config.build(),
}
}
pub fn update(
&mut self,
outer_setpoint: &Setpoint<S>,
outer_feedback: &Feedback<S>,
inner_feedback: &Feedback<S>,
dt: S,
) -> ControlOutput<S> {
let outer_out = self.outer.update(outer_setpoint, outer_feedback, dt);
let inner_sp = Setpoint::new(outer_out.value());
self.inner.update(&inner_sp, inner_feedback, dt)
}
pub fn update_multirate(
&mut self,
outer_setpoint: &Setpoint<S>,
outer_feedback: &Feedback<S>,
inner_feedback: &Feedback<S>,
outer_dt: S,
inner_dt: S,
) -> ControlOutput<S> {
let outer_out = self.outer.update(outer_setpoint, outer_feedback, outer_dt);
let inner_sp = Setpoint::new(outer_out.value());
self.inner.update(&inner_sp, inner_feedback, inner_dt)
}
pub fn reset(&mut self) {
self.outer.reset();
self.inner.reset();
}
pub fn outer(&self) -> &Pid<S> {
&self.outer
}
pub fn inner(&self) -> &Pid<S> {
&self.inner
}
pub fn outer_mut(&mut self) -> &mut Pid<S> {
&mut self.outer
}
pub fn inner_mut(&mut self) -> &mut Pid<S> {
&mut self.inner
}
pub fn is_saturated(&self) -> bool {
self.inner.is_saturated()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pid::anti_windup::AntiWindupMethod;
#[test]
fn cascade_basic_operation() {
let outer = PidConfig::p(1.0_f64);
let inner = PidConfig::p(2.0_f64);
let mut cascade = CascadePid::new(outer, inner);
let sp = Setpoint::new(10.0);
let outer_fb = Feedback::new(8.0);
let inner_fb = Feedback::new(0.0);
let out = cascade.update(&sp, &outer_fb, &inner_fb, 0.01);
assert!((out.value() - 4.0).abs() < 1e-10);
}
#[test]
fn cascade_reset() {
let outer = PidConfig::pi(1.0_f64, 1.0);
let inner = PidConfig::pi(1.0_f64, 1.0);
let mut cascade = CascadePid::new(outer, inner);
for _ in 0..10 {
cascade.update(
&Setpoint::new(10.0),
&Feedback::new(0.0),
&Feedback::new(0.0),
0.01,
);
}
cascade.reset();
let out = cascade.update(
&Setpoint::new(0.0),
&Feedback::new(0.0),
&Feedback::new(0.0),
0.01,
);
assert_eq!(out.value(), 0.0);
}
#[test]
fn cascade_converges_simulated_plant() {
let outer = PidConfig::pi(5.0_f64, 2.0)
.with_limits(-50.0, 50.0)
.with_anti_windup(AntiWindupMethod::Clamping);
let inner = PidConfig::pi(2.0_f64, 10.0)
.with_limits(-100.0, 100.0)
.with_anti_windup(AntiWindupMethod::Clamping);
let mut cascade = CascadePid::new(outer, inner);
let mut pos = 0.0_f64;
let mut vel = 0.0_f64;
let dt = 0.001;
let target_pos = 1.0;
for _ in 0..20_000 {
let sp = Setpoint::new(target_pos);
let outer_fb = Feedback::new(pos);
let inner_fb = Feedback::new(vel);
let u = cascade.update(&sp, &outer_fb, &inner_fb, dt);
let dv = u.value() - vel;
vel += dv * dt;
pos += vel * dt;
}
assert!(
(pos - target_pos).abs() < 0.15,
"Should converge: pos={:.4}",
pos
);
}
}