use crate::core::scalar::ControlScalar;
use heapless::Vec as HVec;
use super::FracError;
pub fn design_tustin_frac<S: ControlScalar, const N: usize>(
alpha: S,
sample_time: S,
) -> Result<(HVec<S, N>, HVec<S, N>), FracError> {
let order = N.checked_sub(1).ok_or(FracError::WindowTooSmall)?;
if order == 0 {
return Err(FracError::WindowTooSmall);
}
if !alpha.is_finite() {
return Err(FracError::InvalidOrder);
}
if sample_time <= S::ZERO || !sample_time.is_finite() {
return Err(FracError::InvalidSampleTime);
}
let two_over_t = S::TWO / sample_time;
let scale = two_over_t.powf(alpha);
let num_raw = gl_weights_vec::<S, N>(alpha)?;
let mut num: HVec<S, N> = HVec::new();
for &w in num_raw.iter() {
num.push(scale * w).map_err(|_| FracError::WindowTooSmall)?;
}
let mut den: HVec<S, N> = HVec::new();
let mut v = S::ONE;
for k in 0..=order {
den.push(v).map_err(|_| FracError::WindowTooSmall)?;
if k < order {
let k_s = S::from_f64((k + 1) as f64);
let alpha_k = alpha - S::from_f64(k as f64);
v = v * alpha_k / k_s;
}
}
Ok((num, den))
}
fn gl_weights_vec<S: ControlScalar, const N: usize>(alpha: S) -> Result<HVec<S, N>, FracError> {
let mut w: HVec<S, N> = HVec::new();
let mut wk = S::ONE;
for k in 0..N {
w.push(wk).map_err(|_| FracError::WindowTooSmall)?;
let k_s = S::from_f64((k + 1) as f64);
let km1_s = S::from_f64(k as f64);
wk = wk * (km1_s - alpha) / k_s;
}
Ok(w)
}
#[derive(Debug, Clone)]
pub struct TustinFrac<S: ControlScalar, const N: usize> {
num: HVec<S, N>,
den: HVec<S, N>,
x_buf: [S; N],
y_buf: [S; N],
}
impl<S: ControlScalar, const N: usize> TustinFrac<S, N> {
pub fn new(alpha: S, sample_time: S) -> Result<Self, FracError> {
let (num, den) = design_tustin_frac::<S, N>(alpha, sample_time)?;
Ok(Self {
num,
den,
x_buf: [S::ZERO; N],
y_buf: [S::ZERO; N],
})
}
pub fn update(&mut self, x: S) -> S {
let order = self.num.len().min(self.den.len());
let mut y = self.num[0] * x;
for k in 1..order {
if k - 1 < N {
y += self.num[k] * self.x_buf[k - 1];
}
}
let d0 = self.den[0];
for k in 1..order {
if k - 1 < N {
y -= self.den[k] * self.y_buf[k - 1];
}
}
let y_out = if d0.abs() > S::EPSILON {
y / d0
} else {
S::ZERO
};
for k in (1..N).rev() {
self.x_buf[k] = self.x_buf[k - 1];
self.y_buf[k] = self.y_buf[k - 1];
}
self.x_buf[0] = x;
self.y_buf[0] = y_out;
y_out
}
pub fn reset(&mut self) {
self.x_buf = [S::ZERO; N];
self.y_buf = [S::ZERO; N];
}
pub fn num_coeffs(&self) -> &[S] {
&self.num
}
pub fn den_coeffs(&self) -> &[S] {
&self.den
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tustin_design_alpha_one_has_correct_num_length() {
let (num, den) = design_tustin_frac::<f64, 4>(1.0_f64, 0.01_f64).expect("valid");
assert_eq!(num.len(), 4);
assert_eq!(den.len(), 4);
}
#[test]
fn tustin_design_alpha_one_num_first_coeff_is_scaled() {
let h = 0.01_f64;
let (num, _) = design_tustin_frac::<f64, 4>(1.0, h).expect("valid");
let expected = (2.0 / h).powf(1.0); assert!(
(num[0] - expected).abs() < 1e-9,
"num[0]={} expected {}",
num[0],
expected
);
}
#[test]
fn tustin_design_error_on_zero_sample_time() {
let result = design_tustin_frac::<f64, 4>(1.0_f64, 0.0_f64);
assert!(matches!(result, Err(FracError::InvalidSampleTime)));
}
#[test]
fn tustin_design_error_on_nan_alpha() {
let result = design_tustin_frac::<f64, 4>(f64::NAN, 0.01_f64);
assert!(matches!(result, Err(FracError::InvalidOrder)));
}
#[test]
fn tustin_design_error_on_order_zero() {
let result = design_tustin_frac::<f64, 1>(1.0_f64, 0.01_f64);
assert!(matches!(result, Err(FracError::WindowTooSmall)));
}
#[test]
fn tustin_frac_output_is_finite_for_ramp() {
let mut f = TustinFrac::<f64, 8>::new(1.0_f64, 0.01_f64).expect("valid");
let h = 0.01_f64;
let mut last = 0.0_f64;
for i in 0..30 {
last = f.update(i as f64 * h);
}
assert!(last.is_finite(), "Output should be finite, got {}", last);
}
#[test]
fn tustin_frac_alpha_one_derivative_of_ramp_near_slope() {
let h = 0.01_f64;
let mut f = TustinFrac::<f64, 8>::new(1.0_f64, h).expect("valid");
let n_settle = 50_usize;
let n_avg = 20_usize;
for i in 0..n_settle {
f.update(i as f64 * h);
}
let mut sum = 0.0_f64;
for i in n_settle..(n_settle + n_avg) {
sum += f.update(i as f64 * h);
}
let avg = sum / n_avg as f64;
assert!(
(avg - 1.0).abs() < 0.05,
"Average Tustin s^1 output on ramp should equal slope 1.0; avg={}",
avg
);
}
#[test]
fn tustin_frac_alpha_half_finite_output() {
let mut f = TustinFrac::<f64, 8>::new(0.5_f64, 0.01_f64).expect("valid");
let h = 0.01_f64;
let mut last = 0.0_f64;
for i in 0..20 {
last = f.update(i as f64 * h);
}
assert!(
last.is_finite(),
"s^0.5 output should be finite, got {}",
last
);
}
#[test]
fn tustin_frac_reset_clears_state() {
let mut f = TustinFrac::<f64, 8>::new(1.0_f64, 0.01_f64).expect("valid");
for i in 0..20 {
f.update(i as f64 * 0.01);
}
f.reset();
let out = f.update(0.0);
assert_eq!(out, 0.0, "After reset and zero input, output should be 0");
}
#[test]
fn tustin_frac_alpha_one_gain_at_nyquist() {
let h = 0.1_f64;
let mut f = TustinFrac::<f64, 6>::new(1.0_f64, h).expect("valid");
let mut outputs = [0.0_f64; 20];
for (i, o) in outputs.iter_mut().enumerate() {
*o = f.update(if i == 0 { 0.0 } else { 1.0 });
}
for (i, &out) in outputs.iter().enumerate() {
assert!(out.is_finite(), "Output[{}]={} is not finite", i, out);
}
}
#[test]
fn tustin_frac_accessor_lengths() {
let f = TustinFrac::<f64, 6>::new(0.8_f64, 0.01_f64).expect("valid");
assert_eq!(f.num_coeffs().len(), 6);
assert_eq!(f.den_coeffs().len(), 6);
}
#[test]
fn tustin_frac_f32_works() {
let mut f = TustinFrac::<f32, 4>::new(0.5_f32, 0.01_f32).expect("valid");
let out = f.update(1.0_f32);
assert!(out.is_finite());
}
#[test]
fn tustin_frac_alpha_neg_one_monotone_on_step() {
let h = 0.1_f64;
let mut f = TustinFrac::<f64, 8>::new(-1.0_f64, h).expect("valid");
let mut prev = f64::NEG_INFINITY;
let mut monotone = true;
for _ in 0..15 {
let out = f.update(1.0);
if out < prev - 1e-9 {
monotone = false;
break;
}
prev = out;
}
assert!(monotone, "s^{{-1}} of unit step should be non-decreasing");
}
}