use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, PartialEq)]
pub enum FeedforwardError {
InvalidParameter,
ZeroGain,
}
impl core::fmt::Display for FeedforwardError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidParameter => write!(f, "Invalid feedforward parameter"),
Self::ZeroGain => write!(f, "Plant gain b is zero; inversion not possible"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct InversionFeedforward<S: ControlScalar> {
a: S,
b: S,
y_prev: S,
}
impl<S: ControlScalar> InversionFeedforward<S> {
pub fn new(a: S, b: S) -> Result<Self, FeedforwardError> {
if b.abs() <= S::EPSILON {
return Err(FeedforwardError::ZeroGain);
}
Ok(Self {
a,
b,
y_prev: S::ZERO,
})
}
pub fn compute(&mut self, y_desired_next: S, y_current: S) -> Result<S, FeedforwardError> {
let u_ff = (y_desired_next - self.a * y_current) / self.b;
self.y_prev = y_current;
Ok(u_ff)
}
pub fn reset(&mut self, y0: S) {
self.y_prev = y0;
}
pub fn y_prev(&self) -> S {
self.y_prev
}
pub fn a(&self) -> S {
self.a
}
pub fn b(&self) -> S {
self.b
}
}
#[derive(Debug, Clone)]
pub struct PolynomialFeedforward<S: ControlScalar> {
kv: S,
ka: S,
}
impl<S: ControlScalar> PolynomialFeedforward<S> {
pub fn new(kv: S, ka: S) -> Self {
Self { kv, ka }
}
pub fn compute(&self, r_dot: S, r_ddot: S) -> S {
self.kv * r_dot + self.ka * r_ddot
}
pub fn set_gains(&mut self, kv: S, ka: S) {
self.kv = kv;
self.ka = ka;
}
pub fn kv(&self) -> S {
self.kv
}
pub fn ka(&self) -> S {
self.ka
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn perfect_tracking_first_order() {
let a = 0.9_f64;
let b = 0.5_f64;
let mut ff = InversionFeedforward::<f64>::new(a, b).expect("valid params");
let y_desired = 1.0_f64;
let mut y_current = 0.0_f64;
for _ in 0..20 {
let u_ff = ff.compute(y_desired, y_current).expect("compute ok");
y_current = a * y_current + b * u_ff;
}
assert!(
(y_current - y_desired).abs() < 1e-8,
"Inversion feedforward should achieve perfect tracking, got y={y_current:.8}"
);
}
#[test]
fn zero_b_returns_error() {
let result = InversionFeedforward::<f64>::new(0.9, 0.0);
assert!(
matches!(result, Err(FeedforwardError::ZeroGain)),
"b=0.0 should return ZeroGain error"
);
}
#[test]
fn near_zero_b_returns_error() {
let result = InversionFeedforward::<f64>::new(0.9, f64::EPSILON * 0.5);
assert!(
matches!(result, Err(FeedforwardError::ZeroGain)),
"b≈0 should return ZeroGain error"
);
}
#[test]
fn polynomial_linear_combination() {
let ff = PolynomialFeedforward::<f64>::new(2.0, 3.0);
let u_ff = ff.compute(1.0, 2.0);
assert!((u_ff - 8.0).abs() < 1e-10, "Expected 8.0, got {u_ff}");
}
#[test]
fn reset_sets_y_prev() {
let mut ff = InversionFeedforward::<f64>::new(0.5, 1.0).expect("valid params");
let _ = ff.compute(1.0, 0.7).expect("compute ok");
assert!(
(ff.y_prev() - 0.7).abs() < 1e-10,
"y_prev should be 0.7 after compute"
);
ff.reset(5.0);
assert!(
(ff.y_prev() - 5.0).abs() < 1e-10,
"y_prev should be 5.0 after reset, got {}",
ff.y_prev()
);
}
#[test]
fn inversion_feedforward_exact() {
let a = 0.5_f64;
let b = 1.0_f64;
let mut ff = InversionFeedforward::<f64>::new(a, b).expect("valid params");
let u_ff = ff.compute(1.0, 0.0).expect("compute ok");
assert!((u_ff - 1.0).abs() < 1e-10, "Expected u_ff=1.0, got {u_ff}");
let u_ff2 = ff.compute(2.0, 4.0).expect("compute ok");
assert!(u_ff2.abs() < 1e-10, "Expected u_ff=0.0, got {u_ff2}");
}
#[test]
fn polynomial_zero_gains() {
let ff = PolynomialFeedforward::<f64>::new(0.0, 0.0);
let u_ff = ff.compute(100.0, 200.0);
assert!(
u_ff.abs() < 1e-10,
"Zero gains should give zero output, got {u_ff}"
);
}
#[test]
fn polynomial_set_gains() {
let mut ff = PolynomialFeedforward::<f64>::new(1.0, 1.0);
ff.set_gains(2.0, 3.0);
assert!((ff.kv() - 2.0).abs() < 1e-10, "kv should be updated");
assert!((ff.ka() - 3.0).abs() < 1e-10, "ka should be updated");
let u_ff = ff.compute(1.0, 1.0);
assert!((u_ff - 5.0).abs() < 1e-10, "Expected 5.0, got {u_ff}");
}
#[test]
fn negative_b_inversion() {
let a = 0.8_f64;
let b = -0.5_f64;
let mut ff = InversionFeedforward::<f64>::new(a, b).expect("valid params");
let u_ff = ff.compute(1.0, 0.0).expect("compute ok");
assert!(
(u_ff - (-2.0)).abs() < 1e-10,
"Expected u_ff=-2.0, got {u_ff}"
);
}
}