use heapless::Deque;
use crate::core::scalar::ControlScalar;
use crate::core::transfer_fn::TransferFn;
use super::ImcError;
pub const MAX_HORIZON: usize = 32;
#[derive(Debug, Clone, Copy)]
pub struct PfcConfig<S: ControlScalar, const NP: usize> {
pub model_b: [S; NP],
pub model_a: [S; NP],
pub horizon: usize,
pub reference_time_constant: S,
pub u_min: S,
pub u_max: S,
}
impl<S: ControlScalar, const NP: usize> PfcConfig<S, NP> {
pub fn new(
model_b: [S; NP],
model_a: [S; NP],
horizon: usize,
reference_time_constant: S,
) -> Self {
let big = S::from_f64(1e9);
Self {
model_b,
model_a,
horizon,
reference_time_constant,
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 PfcController<S: ControlScalar, const NP: usize> {
plant_model: TransferFn<S, NP>,
g_coefficient: S,
coincidence_horizon: usize,
reference_time_constant: S,
u_history: Deque<S, MAX_HORIZON>,
u_min: S,
u_max: S,
model_b: [S; NP],
model_a: [S; NP],
}
impl<S: ControlScalar, const NP: usize> PfcController<S, NP> {
pub fn new(cfg: &PfcConfig<S, NP>) -> Result<Self, ImcError> {
if cfg.horizon == 0 || cfg.horizon > MAX_HORIZON {
return Err(ImcError::InvalidParameter(
"horizon must be in [1, MAX_HORIZON]",
));
}
if cfg.reference_time_constant <= S::ZERO {
return Err(ImcError::InvalidParameter(
"reference_time_constant must be > 0",
));
}
if cfg.u_min >= cfg.u_max {
return Err(ImcError::InvalidParameter("u_min must be < u_max"));
}
let g_coefficient = Self::compute_g(cfg.model_b, cfg.model_a, cfg.horizon);
if g_coefficient.abs() < S::from_f64(1e-12) {
return Err(ImcError::NumericalError(
"step-response coefficient G is near zero; horizon may be too short",
));
}
let plant_model = TransferFn::<S, NP>::new(cfg.model_b, cfg.model_a);
Ok(Self {
plant_model,
g_coefficient,
coincidence_horizon: cfg.horizon,
reference_time_constant: cfg.reference_time_constant,
u_history: Deque::new(),
u_min: cfg.u_min,
u_max: cfg.u_max,
model_b: cfg.model_b,
model_a: cfg.model_a,
})
}
fn compute_g(b: [S; NP], a: [S; NP], horizon: usize) -> S {
let mut tf = TransferFn::<S, NP>::new(b, a);
let mut y = S::ZERO;
for _ in 0..horizon {
y = tf.process(S::ONE);
}
y
}
fn compute_free_response(&self) -> S {
let mut model_clone = self.plant_model;
let mut y = S::ZERO;
for _ in 0..self.coincidence_horizon {
y = model_clone.process(S::ZERO);
}
y
}
fn reference_at_horizon(&self, y_sp: S, y0: S) -> S {
let h = S::from_f64(self.coincidence_horizon as f64);
let decay = (-h / self.reference_time_constant).exp();
y_sp + (y0 - y_sp) * decay
}
pub fn update(&mut self, setpoint: S, measurement: S) -> Result<S, ImcError> {
let y_ref_h = self.reference_at_horizon(setpoint, measurement);
let f = self.compute_free_response();
let u_raw = (y_ref_h - f) / self.g_coefficient;
let u_sat = u_raw.clamp_val(self.u_min, self.u_max);
let _ = self.plant_model.process(u_sat);
if self.u_history.is_full() {
let _ = self.u_history.pop_front();
}
let push_result = self.u_history.push_back(u_sat);
if push_result.is_err() {
return Err(ImcError::NumericalError("u_history buffer overflow"));
}
Ok(u_sat)
}
pub fn reset(&mut self) {
self.plant_model.reset();
self.u_history.clear();
}
#[inline]
pub fn g_coefficient(&self) -> S {
self.g_coefficient
}
#[inline]
pub fn coincidence_horizon(&self) -> usize {
self.coincidence_horizon
}
pub fn set_horizon(&mut self, new_horizon: usize) -> Result<(), ImcError> {
if new_horizon == 0 || new_horizon > MAX_HORIZON {
return Err(ImcError::InvalidParameter(
"horizon must be in [1, MAX_HORIZON]",
));
}
let g = Self::compute_g(self.model_b, self.model_a, new_horizon);
if g.abs() < S::from_f64(1e-12) {
return Err(ImcError::NumericalError(
"new G is near zero; choose a longer horizon",
));
}
self.coincidence_horizon = new_horizon;
self.g_coefficient = g;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn unity_dc_cfg(horizon: usize, tau_ref: f64) -> PfcConfig<f64, 1> {
PfcConfig::new([0.2], [-0.8], horizon, tau_ref)
}
#[test]
fn pfc_construction_ok() {
let cfg = unity_dc_cfg(10, 5.0);
let ctrl = PfcController::<f64, 1>::new(&cfg);
assert!(ctrl.is_ok(), "Valid config should construct OK");
}
#[test]
fn pfc_construction_invalid_horizon_zero() {
let cfg = unity_dc_cfg(0, 5.0);
let ctrl = PfcController::<f64, 1>::new(&cfg);
assert!(
matches!(ctrl, Err(ImcError::InvalidParameter(_))),
"horizon=0 must be rejected"
);
}
#[test]
fn pfc_construction_invalid_horizon_too_large() {
let cfg = unity_dc_cfg(MAX_HORIZON + 1, 5.0);
let ctrl = PfcController::<f64, 1>::new(&cfg);
assert!(
matches!(ctrl, Err(ImcError::InvalidParameter(_))),
"horizon>MAX_HORIZON must be rejected"
);
}
#[test]
fn pfc_construction_invalid_tau_ref_zero() {
let cfg = unity_dc_cfg(10, 0.0);
let ctrl = PfcController::<f64, 1>::new(&cfg);
assert!(
matches!(ctrl, Err(ImcError::InvalidParameter(_))),
"tau_ref=0 must be rejected"
);
}
#[test]
fn pfc_construction_invalid_limits() {
let cfg = unity_dc_cfg(10, 5.0).with_limits(1.0, -1.0);
let ctrl = PfcController::<f64, 1>::new(&cfg);
assert!(
matches!(ctrl, Err(ImcError::InvalidParameter(_))),
"Inverted limits must be rejected"
);
}
#[test]
fn pfc_step_reference_tracking() {
let cfg = unity_dc_cfg(8, 6.0);
let mut ctrl = PfcController::<f64, 1>::new(&cfg).unwrap();
let mut plant = TransferFn::<f64, 1>::new([0.2], [-0.8]);
let mut y = 0.0_f64;
let setpoint = 1.0_f64;
let mut last_u = 0.0_f64;
for _ in 0..500 {
let u = ctrl.update(setpoint, y).unwrap();
y = plant.process(u);
last_u = u;
}
let error = (y - setpoint).abs();
assert!(
error < 0.02,
"PFC should track step reference: e={:.4}, y={:.4}, u={:.4}",
error,
y,
last_u
);
}
#[test]
fn pfc_shorter_horizon_faster_response() {
let cfg_fast = PfcConfig::<f64, 1>::new([0.2], [-0.8], 5, 3.0);
let cfg_slow = PfcConfig::<f64, 1>::new([0.2], [-0.8], 20, 15.0);
let mut ctrl_fast = PfcController::<f64, 1>::new(&cfg_fast).unwrap();
let mut ctrl_slow = PfcController::<f64, 1>::new(&cfg_slow).unwrap();
let mut plant_fast = TransferFn::<f64, 1>::new([0.2], [-0.8]);
let mut plant_slow = TransferFn::<f64, 1>::new([0.2], [-0.8]);
let setpoint = 1.0_f64;
let mut y_fast = 0.0_f64;
let mut y_slow = 0.0_f64;
let mut steps_fast_90 = usize::MAX;
let mut steps_slow_90 = usize::MAX;
for step in 0..300_usize {
let u_f = ctrl_fast.update(setpoint, y_fast).unwrap();
y_fast = plant_fast.process(u_f);
if y_fast >= 0.9 * setpoint && steps_fast_90 == usize::MAX {
steps_fast_90 = step;
}
let u_s = ctrl_slow.update(setpoint, y_slow).unwrap();
y_slow = plant_slow.process(u_s);
if y_slow >= 0.9 * setpoint && steps_slow_90 == usize::MAX {
steps_slow_90 = step;
}
}
assert!(
steps_fast_90 <= steps_slow_90,
"Shorter horizon/τ should reach 90% faster: fast={} slow={}",
steps_fast_90,
steps_slow_90
);
}
#[test]
fn pfc_saturation_respected() {
let cfg = unity_dc_cfg(10, 5.0).with_limits(-0.1, 0.1);
let mut ctrl = PfcController::<f64, 1>::new(&cfg).unwrap();
let u = ctrl.update(100.0, 0.0).unwrap();
assert!(
u <= 0.1 + 1e-12,
"u should be saturated at u_max=0.1, got {:.6}",
u
);
}
#[test]
fn pfc_reset_clears_state() {
let cfg = unity_dc_cfg(10, 5.0);
let mut ctrl = PfcController::<f64, 1>::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 = ctrl.update(1.0, y).unwrap();
y = plant.process(u);
}
ctrl.reset();
let u_post = ctrl.update(0.0, 0.0).unwrap();
assert!(
u_post.abs() < 1e-10,
"After reset on zero input, output should be zero: {:.4e}",
u_post
);
}
#[test]
fn pfc_g_coefficient_positive() {
let cfg = unity_dc_cfg(10, 5.0);
let ctrl = PfcController::<f64, 1>::new(&cfg).unwrap();
assert!(
ctrl.g_coefficient() > 0.0,
"G should be positive: {}",
ctrl.g_coefficient()
);
}
#[test]
fn pfc_set_horizon_works() {
let cfg = unity_dc_cfg(10, 5.0);
let mut ctrl = PfcController::<f64, 1>::new(&cfg).unwrap();
assert_eq!(ctrl.coincidence_horizon(), 10);
ctrl.set_horizon(20).unwrap();
assert_eq!(ctrl.coincidence_horizon(), 20);
}
#[test]
fn pfc_set_horizon_invalid() {
let cfg = unity_dc_cfg(10, 5.0);
let mut ctrl = PfcController::<f64, 1>::new(&cfg).unwrap();
assert!(ctrl.set_horizon(0).is_err());
assert!(ctrl.set_horizon(MAX_HORIZON + 1).is_err());
}
#[test]
fn pfc_second_order_plant() {
let cfg = PfcConfig::<f64, 2>::new([0.1, 0.05], [-1.5, 0.7], 12, 8.0);
let mut ctrl = PfcController::<f64, 2>::new(&cfg).unwrap();
let mut plant = TransferFn::<f64, 2>::new([0.1, 0.05], [-1.5, 0.7]);
let mut y = 0.0_f64;
for _ in 0..300 {
let u = ctrl.update(1.0, y).unwrap();
y = plant.process(u);
}
assert!(
y.is_finite(),
"Second-order PFC response should be finite: y={:.4}",
y
);
}
}