use crate::core::scalar::ControlScalar;
use crate::core::transfer_fn::TransferFn;
use super::ImcError;
#[derive(Debug, Clone, Copy)]
pub struct ImcConfig<S: ControlScalar, const NP: usize> {
pub model_b: [S; NP],
pub model_a: [S; NP],
pub filter_lambda: S,
pub u_min: S,
pub u_max: S,
}
impl<S: ControlScalar, const NP: usize> ImcConfig<S, NP> {
pub fn new(model_b: [S; NP], model_a: [S; NP], filter_lambda: S) -> Self {
let big = S::from_f64(1e9);
Self {
model_b,
model_a,
filter_lambda,
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, Copy)]
pub struct ImcController<S: ControlScalar, const NP: usize> {
plant_model: TransferFn<S, NP>,
q_filter: TransferFn<S, 1>,
mismatch: S,
u_min: S,
u_max: S,
}
impl<S: ControlScalar, const NP: usize> ImcController<S, NP> {
pub fn new(cfg: &ImcConfig<S, NP>) -> Result<Self, ImcError> {
let lambda = cfg.filter_lambda;
if lambda <= S::ZERO || lambda >= S::ONE {
return Err(ImcError::InvalidParameter(
"filter_lambda must be in (0, 1)",
));
}
if cfg.u_min >= cfg.u_max {
return Err(ImcError::InvalidParameter("u_min must be < u_max"));
}
let q_filter = TransferFn::<S, 1>::new([S::ONE - lambda], [-lambda]);
let plant_model = TransferFn::<S, NP>::new(cfg.model_b, cfg.model_a);
Ok(Self {
plant_model,
q_filter,
mismatch: S::ZERO,
u_min: cfg.u_min,
u_max: cfg.u_max,
})
}
pub fn update(&mut self, reference: S, plant_output: S) -> Result<S, ImcError> {
let augmented_error = reference - self.mismatch;
let u_raw = self.q_filter.process(augmented_error);
let u_saturated = u_raw.clamp_val(self.u_min, self.u_max);
let y_model = self.plant_model.process(u_saturated);
self.mismatch = plant_output - y_model;
Ok(u_saturated)
}
pub fn reset(&mut self) {
self.q_filter.reset();
self.plant_model.reset();
self.mismatch = S::ZERO;
}
#[inline]
pub fn mismatch(&self) -> S {
self.mismatch
}
pub fn equivalent_dc_gain(&self, cfg: &ImcConfig<S, NP>) -> Result<S, ImcError> {
let sum_b: S = cfg.model_b.iter().copied().fold(S::ZERO, |acc, x| acc + x);
let sum_a: S = cfg.model_a.iter().copied().fold(S::ZERO, |acc, x| acc + x);
let plant_dc = sum_b / (S::ONE + sum_a);
let q_dc = S::ONE; let denom = S::ONE - q_dc * plant_dc;
if denom.abs() < S::EPSILON * S::from_f64(1e6) {
return Err(ImcError::NumericalError(
"equivalent_dc_gain: near-zero denominator",
));
}
Ok(q_dc / denom)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn first_order_plant_cfg(lambda: f64) -> ImcConfig<f64, 1> {
ImcConfig::new([0.5], [-0.5], lambda)
}
#[test]
fn imc_controller_construction_valid() {
let cfg = first_order_plant_cfg(0.8);
let ctrl = ImcController::<f64, 1>::new(&cfg);
assert!(ctrl.is_ok(), "Valid config should construct OK");
}
#[test]
fn imc_controller_construction_invalid_lambda_zero() {
let cfg = ImcConfig::<f64, 1>::new([0.5], [-0.5], 0.0);
let ctrl = ImcController::<f64, 1>::new(&cfg);
assert!(
matches!(ctrl, Err(ImcError::InvalidParameter(_))),
"λ=0 must be rejected"
);
}
#[test]
fn imc_controller_construction_invalid_lambda_one() {
let cfg = ImcConfig::<f64, 1>::new([0.5], [-0.5], 1.0);
let ctrl = ImcController::<f64, 1>::new(&cfg);
assert!(
matches!(ctrl, Err(ImcError::InvalidParameter(_))),
"λ=1 must be rejected"
);
}
#[test]
fn imc_controller_construction_invalid_limits() {
let cfg = ImcConfig::<f64, 1>::new([0.5], [-0.5], 0.8).with_limits(1.0, -1.0); let ctrl = ImcController::<f64, 1>::new(&cfg);
assert!(
matches!(ctrl, Err(ImcError::InvalidParameter(_))),
"Inverted limits must be rejected"
);
}
#[test]
fn imc_perfect_model_zero_steady_state_error() {
let cfg = first_order_plant_cfg(0.7);
let mut ctrl = ImcController::<f64, 1>::new(&cfg).unwrap();
let mut plant_sim = TransferFn::<f64, 1>::new([0.5], [-0.5]);
let setpoint = 1.0_f64;
let mut y_plant = 0.0_f64;
let mut u = 0.0_f64;
for _ in 0..500 {
u = ctrl.update(setpoint, y_plant).unwrap();
y_plant = plant_sim.process(u);
}
let error = (y_plant - setpoint).abs();
assert!(
error < 1e-4,
"Steady-state error should be near zero with perfect model, got e={:.6}, y={:.6}, u={:.6}",
error, y_plant, u
);
}
#[test]
fn imc_imperfect_model_bounded_response() {
let cfg = ImcConfig::<f64, 1>::new([0.5], [-0.5], 0.9);
let mut ctrl = ImcController::<f64, 1>::new(&cfg).unwrap();
let mut plant_sim = TransferFn::<f64, 1>::new([0.6], [-0.5]);
let setpoint = 1.0_f64;
let mut y_plant = 0.0_f64;
let mut last_u = 0.0_f64;
for _ in 0..800 {
let u = ctrl.update(setpoint, y_plant).unwrap();
y_plant = plant_sim.process(u);
last_u = u;
}
assert!(
y_plant > 0.5 && y_plant < 2.0,
"With 20% mismatch, output should be bounded: y={:.4}, u={:.4}",
y_plant,
last_u
);
}
#[test]
fn imc_reset_clears_state() {
let cfg = first_order_plant_cfg(0.8);
let mut ctrl = ImcController::<f64, 1>::new(&cfg).unwrap();
let mut plant_sim = TransferFn::<f64, 1>::new([0.5], [-0.5]);
let mut y = 0.0_f64;
for _ in 0..100 {
let u = ctrl.update(1.0, y).unwrap();
y = plant_sim.process(u);
}
ctrl.reset();
let u_post = ctrl.update(0.0, 0.0).unwrap();
assert!(
u_post.abs() < 1e-10,
"After reset, output on zero input should be zero, got {:.6e}",
u_post
);
}
#[test]
fn imc_saturation_respected() {
let cfg = ImcConfig::<f64, 1>::new([0.5], [-0.5], 0.5).with_limits(-0.1, 0.1);
let mut ctrl = ImcController::<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 imc_second_order_plant() {
let cfg = ImcConfig::<f64, 2>::new([0.25, 0.25], [-1.0, 0.0], 0.85);
let mut ctrl = ImcController::<f64, 2>::new(&cfg).unwrap();
let mut plant_sim = TransferFn::<f64, 2>::new([0.25, 0.25], [-1.0, 0.0]);
let mut y = 0.0_f64;
let mut last_u = 0.0_f64;
for _ in 0..600 {
let u = ctrl.update(1.0, y).unwrap();
y = plant_sim.process(u);
last_u = u;
}
assert!(
y.is_finite() && last_u.is_finite(),
"Second-order plant response should be finite: y={:.4}, u={:.4}",
y,
last_u
);
}
}