use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MfcError {
ZeroAlpha,
NonPositiveDt,
WindowTooSmall,
}
impl core::fmt::Display for MfcError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
MfcError::ZeroAlpha => f.write_str("alpha must be non-zero"),
MfcError::NonPositiveDt => f.write_str("dt must be positive"),
MfcError::WindowTooSmall => f.write_str("window size must be >= 2"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SlidingWindow<S: ControlScalar, const N: usize> {
data: [S; N],
head: usize,
count: usize,
}
impl<S: ControlScalar, const N: usize> SlidingWindow<S, N> {
pub fn new() -> Self {
Self {
data: [S::ZERO; N],
head: 0,
count: 0,
}
}
pub fn push(&mut self, value: S) {
self.data[self.head] = value;
self.head = (self.head + 1) % N;
if self.count < N {
self.count += 1;
}
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn get(&self, age: usize) -> Option<S> {
if age >= self.count {
return None;
}
let idx = (self.head + N - 1 - age) % N;
Some(self.data[idx])
}
pub fn oldest(&self) -> Option<S> {
self.get(self.count.saturating_sub(1))
}
pub fn newest(&self) -> Option<S> {
self.get(0)
}
pub fn mean(&self) -> Option<S> {
if self.count == 0 {
return None;
}
let mut sum = S::ZERO;
for i in 0..self.count {
sum += self.data[i];
}
Some(sum / S::from_f64(self.count as f64))
}
pub fn reset(&mut self) {
self.data = [S::ZERO; N];
self.head = 0;
self.count = 0;
}
}
impl<S: ControlScalar, const N: usize> Default for SlidingWindow<S, N> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub struct AlgebraicFEstimator<S: ControlScalar, const W: usize> {
y_buf: SlidingWindow<S, W>,
u_buf: SlidingWindow<S, W>,
alpha: S,
dt: S,
f_hat: S,
}
impl<S: ControlScalar, const W: usize> AlgebraicFEstimator<S, W> {
pub fn new(alpha: S, dt: S) -> Result<Self, MfcError> {
if alpha == S::ZERO {
return Err(MfcError::ZeroAlpha);
}
if dt <= S::ZERO {
return Err(MfcError::NonPositiveDt);
}
if W < 2 {
return Err(MfcError::WindowTooSmall);
}
Ok(Self {
y_buf: SlidingWindow::new(),
u_buf: SlidingWindow::new(),
alpha,
dt,
f_hat: S::ZERO,
})
}
pub fn update(&mut self, y: S, u: S) {
self.y_buf.push(y);
self.u_buf.push(u);
let count = self.y_buf.len();
if count < 2 {
return;
}
let y_new = match self.y_buf.newest() {
Some(v) => v,
None => return,
};
let y_old = match self.y_buf.oldest() {
Some(v) => v,
None => return,
};
let delta_y = y_new - y_old;
let t_span = S::from_f64((count - 1) as f64) * self.dt;
if t_span <= S::ZERO {
return;
}
let dy_dt = delta_y / t_span;
let u_mean = self.u_buf.mean().unwrap_or(S::ZERO);
self.f_hat = dy_dt - self.alpha * u_mean;
}
pub fn f_hat(&self) -> S {
self.f_hat
}
pub fn reset(&mut self) {
self.y_buf.reset();
self.u_buf.reset();
self.f_hat = S::ZERO;
}
}
#[derive(Debug, Clone, Copy)]
pub struct IPid<S: ControlScalar, const W: usize> {
alpha: S,
kp: S,
ki: S,
kd: S,
dt: S,
estimator: AlgebraicFEstimator<S, W>,
integral: S,
prev_error: S,
has_prev: bool,
integral_limit: Option<S>,
}
impl<S: ControlScalar, const W: usize> IPid<S, W> {
pub fn new(
alpha: S,
kp: S,
ki: S,
kd: S,
dt: S,
integral_limit: Option<S>,
) -> Result<Self, MfcError> {
let estimator = AlgebraicFEstimator::new(alpha, dt)?;
Ok(Self {
alpha,
kp,
ki,
kd,
dt,
estimator,
integral: S::ZERO,
prev_error: S::ZERO,
has_prev: false,
integral_limit,
})
}
pub fn reset(&mut self) {
self.integral = S::ZERO;
self.prev_error = S::ZERO;
self.has_prev = false;
self.estimator.reset();
}
pub fn update(&mut self, y: S, r: S, dr: S, u_prev: S) -> S {
self.estimator.update(y, u_prev);
let f_hat = self.estimator.f_hat();
let error = r - y;
let raw_integral = self.integral + error * self.dt;
self.integral = match self.integral_limit {
Some(lim) => raw_integral.saturate(lim),
None => raw_integral,
};
let derivative = if self.has_prev {
(error - self.prev_error) / self.dt
} else {
S::ZERO
};
self.prev_error = error;
self.has_prev = true;
let pid_term = self.kp * error + self.ki * self.integral + self.kd * derivative;
(dr - f_hat + pid_term) / self.alpha
}
pub fn f_hat(&self) -> S {
self.estimator.f_hat()
}
pub fn integral_state(&self) -> S {
self.integral
}
}
#[derive(Debug, Clone, Copy)]
pub struct IP<S: ControlScalar, const W: usize> {
alpha: S,
kp: S,
estimator: AlgebraicFEstimator<S, W>,
}
impl<S: ControlScalar, const W: usize> IP<S, W> {
pub fn new(alpha: S, kp: S, dt: S) -> Result<Self, MfcError> {
let estimator = AlgebraicFEstimator::new(alpha, dt)?;
Ok(Self {
alpha,
kp,
estimator,
})
}
pub fn update(&mut self, y: S, r: S, dr: S, u_prev: S) -> S {
self.estimator.update(y, u_prev);
let f_hat = self.estimator.f_hat();
let error = r - y;
(dr - f_hat + self.kp * error) / self.alpha
}
pub fn f_hat(&self) -> S {
self.estimator.f_hat()
}
pub fn reset(&mut self) {
self.estimator.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
const DT: f64 = 0.005;
fn step_plant(y: f64, u: f64, d: f64) -> f64 {
y + DT * (-2.0 * y + 3.0 * u + d)
}
#[test]
fn mfc_error_zero_alpha() {
let res = AlgebraicFEstimator::<f64, 10>::new(0.0, DT);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), MfcError::ZeroAlpha);
}
#[test]
fn mfc_error_bad_dt() {
let res = AlgebraicFEstimator::<f64, 10>::new(3.0, -0.001);
assert!(res.is_err());
}
#[test]
fn sliding_window_push_and_get() {
let mut w = SlidingWindow::<f64, 4>::new();
w.push(1.0);
w.push(2.0);
w.push(3.0);
assert_eq!(w.len(), 3);
assert_eq!(w.newest(), Some(3.0));
assert_eq!(w.oldest(), Some(1.0));
}
#[test]
fn sliding_window_wraps_correctly() {
let mut w = SlidingWindow::<f64, 3>::new();
w.push(10.0);
w.push(20.0);
w.push(30.0);
w.push(40.0);
assert_eq!(w.len(), 3);
assert_eq!(w.newest(), Some(40.0));
assert_eq!(w.oldest(), Some(20.0));
}
#[test]
fn sliding_window_mean() {
let mut w = SlidingWindow::<f64, 4>::new();
w.push(1.0);
w.push(2.0);
w.push(3.0);
w.push(4.0);
let m = w.mean().expect("mean of non-empty window");
assert!((m - 2.5).abs() < 1e-12, "mean={}", m);
}
#[test]
fn ipid_tracks_reference_with_disturbance() {
let mut ctrl = IPid::<f64, 20>::new(
3.0, 5.0, 1.0, 0.1, DT,
Some(50.0), )
.expect("valid params");
let r = 2.0_f64;
let d = 1.0_f64;
let mut y = 0.0_f64;
let mut u = 0.0_f64;
for _ in 0..3000 {
let u_new = ctrl.update(y, r, 0.0, u);
y = step_plant(y, u, d);
u = u_new;
}
assert!(
(y - r).abs() < 0.1,
"output y={:.4} should track r={}",
y,
r
);
}
#[test]
fn ip_produces_finite_output() {
let mut ctrl = IP::<f64, 8>::new(3.0, 5.0, DT).expect("valid params");
let mut y = 0.0_f64;
let mut u = 0.0_f64;
for _ in 0..100 {
let u_new = ctrl.update(y, 1.0, 0.0, u);
y = step_plant(y, u, 0.0);
u = u_new;
assert!(u.is_finite(), "u should be finite: {}", u);
}
}
#[test]
fn ipid_reset_clears_state() {
let mut ctrl = IPid::<f64, 10>::new(1.0, 2.0, 0.5, 0.1, DT, None).expect("valid params");
for _ in 0..50 {
let _ = ctrl.update(0.5, 1.0, 0.0, 0.0);
}
ctrl.reset();
assert_eq!(ctrl.integral_state(), 0.0);
assert_eq!(ctrl.f_hat(), 0.0);
}
#[test]
fn ipid_f32_compiles() {
let mut ctrl =
IPid::<f32, 6>::new(1.0_f32, 2.0, 0.1, 0.01, 0.005, None).expect("f32 valid");
let u = ctrl.update(0.0_f32, 1.0_f32, 0.0_f32, 0.0_f32);
assert!(u.is_finite());
}
}