use crate::core::scalar::ControlScalar;
use super::FracError;
#[derive(Debug, Clone)]
pub struct GrunwaldLeibniz<S: ControlScalar, const N: usize> {
alpha: S,
sample_time: S,
weights: [S; N],
window: [S; N],
count: usize,
head: usize,
}
impl<S: ControlScalar, const N: usize> GrunwaldLeibniz<S, N> {
pub fn new(alpha: S, sample_time: S) -> Result<Self, FracError> {
if N == 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 weights = compute_gl_weights::<S, N>(alpha);
Ok(Self {
alpha,
sample_time,
weights,
window: [S::ZERO; N],
count: 0,
head: 0,
})
}
#[inline]
pub fn alpha(&self) -> S {
self.alpha
}
pub fn update(&mut self, x: S) -> S {
self.window[self.head] = x;
self.head = (self.head + 1) % N;
if self.count < N {
self.count += 1;
}
let mut acc = S::ZERO;
let effective = self.count;
for k in 0..effective {
let idx = (self.head + N - 1 - k) % N;
acc += self.weights[k] * self.window[idx];
}
let h_alpha = self.sample_time.powf(self.alpha);
if h_alpha.abs() < S::EPSILON {
S::ZERO
} else {
acc / h_alpha
}
}
pub fn reset(&mut self) {
self.window = [S::ZERO; N];
self.count = 0;
self.head = 0;
}
}
fn compute_gl_weights<S: ControlScalar, const N: usize>(alpha: S) -> [S; N] {
let mut w = [S::ZERO; N];
if N == 0 {
return w;
}
w[0] = S::ONE;
for k in 1..N {
let k_s = S::from_f64(k as f64);
let km1_s = S::from_f64((k - 1) as f64);
w[k] = w[k - 1] * (km1_s - alpha) / k_s;
}
w
}
#[derive(Debug, Clone)]
pub struct FracIntegrator<S: ControlScalar, const N: usize> {
inner: GrunwaldLeibniz<S, N>,
}
impl<S: ControlScalar, const N: usize> FracIntegrator<S, N> {
pub fn new(lambda: S, sample_time: S) -> Result<Self, FracError> {
let two = S::TWO;
if lambda <= S::ZERO || lambda >= two || !lambda.is_finite() {
return Err(FracError::InvalidOrder);
}
let alpha = S::ZERO - lambda; Ok(Self {
inner: GrunwaldLeibniz::new(alpha, sample_time)?,
})
}
#[inline]
pub fn update(&mut self, x: S) -> S {
self.inner.update(x)
}
#[inline]
pub fn reset(&mut self) {
self.inner.reset();
}
#[inline]
pub fn lambda(&self) -> S {
S::ZERO - self.inner.alpha()
}
}
#[derive(Debug, Clone)]
pub struct FracDifferentiator<S: ControlScalar, const N: usize> {
inner: GrunwaldLeibniz<S, N>,
}
impl<S: ControlScalar, const N: usize> FracDifferentiator<S, N> {
pub fn new(mu: S, sample_time: S) -> Result<Self, FracError> {
let two = S::TWO;
if mu <= S::ZERO || mu >= two || !mu.is_finite() {
return Err(FracError::InvalidOrder);
}
Ok(Self {
inner: GrunwaldLeibniz::new(mu, sample_time)?,
})
}
#[inline]
pub fn update(&mut self, x: S) -> S {
self.inner.update(x)
}
#[inline]
pub fn reset(&mut self) {
self.inner.reset();
}
#[inline]
pub fn mu(&self) -> S {
self.inner.alpha()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run_gl<const N: usize>(alpha: f64, h: f64, inputs: &[f64]) -> f64 {
let mut gl = GrunwaldLeibniz::<f64, N>::new(alpha, h).expect("valid GL");
let mut out = 0.0_f64;
for &x in inputs {
out = gl.update(x);
}
out
}
#[test]
fn grunwald_weights_alpha_one_standard_difference() {
let w = compute_gl_weights::<f64, 4>(1.0_f64);
assert!((w[0] - 1.0).abs() < 1e-12, "w[0]={}", w[0]);
assert!((w[1] - (-1.0)).abs() < 1e-12, "w[1]={}", w[1]);
assert!(w[2].abs() < 1e-12, "w[2]={}", w[2]);
assert!(w[3].abs() < 1e-12, "w[3]={}", w[3]);
}
#[test]
fn grunwald_weights_alpha_two_second_difference() {
let w = compute_gl_weights::<f64, 4>(2.0_f64);
assert!((w[0] - 1.0).abs() < 1e-12);
assert!((w[1] - (-2.0)).abs() < 1e-12);
assert!((w[2] - 1.0).abs() < 1e-12);
assert!(w[3].abs() < 1e-12);
}
#[test]
fn grunwald_weights_alpha_neg_one_cumsum() {
let w = compute_gl_weights::<f64, 5>(-1.0_f64);
for (i, &wi) in w.iter().enumerate() {
assert!((wi - 1.0).abs() < 1e-12, "w[{}]={} expected 1.0", i, wi);
}
}
#[test]
fn gl_alpha_one_recovers_standard_derivative() {
let h = 0.1_f64;
let n = 20_usize;
let inputs: Vec<f64> = (0..n).map(|i| i as f64 * h).collect();
let mut gl = GrunwaldLeibniz::<f64, 8>::new(1.0, h).expect("valid");
let mut last = 0.0_f64;
for &x in &inputs {
last = gl.update(x);
}
assert!((last - 1.0).abs() < 1e-10, "Expected ~1.0, got {}", last);
}
#[test]
fn gl_alpha_two_recovers_second_derivative_of_linear() {
let h = 0.1_f64;
let inputs: Vec<f64> = (0..10).map(|i| i as f64 * h).collect();
let mut gl = GrunwaldLeibniz::<f64, 4>::new(2.0, h).expect("valid");
let mut last = 0.0_f64;
for &x in &inputs {
last = gl.update(x);
}
assert!(
last.abs() < 1e-9,
"D^2 of linear should be ~0, got {}",
last
);
}
#[test]
fn gl_alpha_two_recovers_second_derivative_of_quadratic() {
let h = 0.01_f64;
let n = 40_usize;
let inputs: Vec<f64> = (0..n)
.map(|i| {
let t = i as f64 * h;
t * t / 2.0
})
.collect();
let mut gl = GrunwaldLeibniz::<f64, 32>::new(2.0, h).expect("valid");
let mut last = 0.0_f64;
for &x in &inputs {
last = gl.update(x);
}
assert!(
(last - 1.0).abs() < 0.02,
"D^2 of t^2/2 should be ~1.0, got {}",
last
);
}
#[test]
fn gl_neg_one_integrates_step() {
let h = 0.01_f64;
let n = 50_usize;
let mut gl = GrunwaldLeibniz::<f64, 64>::new(-1.0, h).expect("valid");
let mut last = 0.0_f64;
for _ in 0..n {
last = gl.update(1.0);
}
let expected = n as f64 * h; assert!(
(last - expected).abs() < 1e-10,
"Expected {}, got {}",
expected,
last
);
}
#[test]
fn gl_half_order_derivative_is_finite() {
let h = 0.01_f64;
let out = run_gl::<16>(0.5, h, &[0.0, 0.01, 0.02, 0.03, 0.04]);
assert!(out.is_finite(), "D^0.5 should be finite, got {}", out);
}
#[test]
fn gl_error_on_nan_alpha() {
let result = GrunwaldLeibniz::<f64, 4>::new(f64::NAN, 0.01);
assert!(matches!(result, Err(FracError::InvalidOrder)));
}
#[test]
fn gl_error_on_infinite_alpha() {
let result = GrunwaldLeibniz::<f64, 4>::new(f64::INFINITY, 0.01);
assert!(matches!(result, Err(FracError::InvalidOrder)));
}
#[test]
fn gl_error_on_zero_sample_time() {
let result = GrunwaldLeibniz::<f64, 4>::new(1.0, 0.0);
assert!(matches!(result, Err(FracError::InvalidSampleTime)));
}
#[test]
fn gl_error_on_negative_sample_time() {
let result = GrunwaldLeibniz::<f64, 4>::new(1.0, -0.01);
assert!(matches!(result, Err(FracError::InvalidSampleTime)));
}
#[test]
fn frac_integrator_lambda_one_matches_standard_integral() {
let h = 0.01_f64;
let n = 50_usize;
let mut fi = FracIntegrator::<f64, 64>::new(1.0, h).expect("valid");
let mut last = 0.0_f64;
for _ in 0..n {
last = fi.update(1.0);
}
let expected = n as f64 * h;
assert!(
(last - expected).abs() < 1e-10,
"Expected {}, got {}",
expected,
last
);
}
#[test]
fn frac_integrator_invalid_lambda_errors() {
assert!(matches!(
FracIntegrator::<f64, 4>::new(0.0, 0.01),
Err(FracError::InvalidOrder)
));
assert!(matches!(
FracIntegrator::<f64, 4>::new(2.0, 0.01),
Err(FracError::InvalidOrder)
));
assert!(matches!(
FracIntegrator::<f64, 4>::new(-0.5, 0.01),
Err(FracError::InvalidOrder)
));
}
#[test]
fn frac_differentiator_mu_one_matches_standard_derivative() {
let h = 0.1_f64;
let mut fd = FracDifferentiator::<f64, 8>::new(1.0, h).expect("valid");
let inputs: Vec<f64> = (0..15).map(|i| i as f64 * h).collect();
let mut last = 0.0_f64;
for &x in &inputs {
last = fd.update(x);
}
assert!((last - 1.0).abs() < 1e-10, "Expected ~1.0, got {}", last);
}
#[test]
fn frac_differentiator_invalid_mu_errors() {
assert!(matches!(
FracDifferentiator::<f64, 4>::new(0.0, 0.01),
Err(FracError::InvalidOrder)
));
assert!(matches!(
FracDifferentiator::<f64, 4>::new(2.0, 0.01),
Err(FracError::InvalidOrder)
));
}
#[test]
fn gl_reset_clears_window() {
let mut gl = GrunwaldLeibniz::<f64, 4>::new(1.0, 0.1).expect("valid");
for i in 0..4 {
gl.update(i as f64);
}
gl.reset();
let out = gl.update(0.0);
assert_eq!(out, 0.0, "After reset, output should be zero");
}
#[test]
fn frac_integrator_reset_works() {
let mut fi = FracIntegrator::<f64, 8>::new(1.0, 0.01).expect("valid");
for _ in 0..10 {
fi.update(1.0);
}
fi.reset();
let out = fi.update(0.0);
assert_eq!(out, 0.0);
}
}