#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::needless_range_loop)]
use crate::core::scalar::ControlScalar;
use crate::data_driven::vrft::DataDrivenError;
pub struct CorrelationTuner<S, const DATA_LEN: usize> {
kp: S,
ki: S,
kd: S,
m: S,
dt: S,
mu: S,
}
impl<S: ControlScalar, const DATA_LEN: usize> CorrelationTuner<S, DATA_LEN> {
pub fn new(kp0: S, ki0: S, kd0: S, m: S, dt: S, mu: S) -> Result<Self, DataDrivenError> {
if m <= S::ZERO || m >= S::ONE {
return Err(DataDrivenError::InvalidParameter);
}
if dt <= S::ZERO {
return Err(DataDrivenError::InvalidParameter);
}
if mu <= S::ZERO {
return Err(DataDrivenError::InvalidParameter);
}
Ok(Self {
kp: kp0,
ki: ki0,
kd: kd0,
m,
dt,
mu,
})
}
pub fn step(
&mut self,
r_data: &[S; DATA_LEN],
y_data: &[S; DATA_LEN],
) -> Result<(), DataDrivenError> {
if DATA_LEN < 2 {
return Err(DataDrivenError::NotEnoughData);
}
let dt = self.dt;
let t_inv = S::ONE / S::from_f64(DATA_LEN as f64);
let mut g_p = S::ZERO;
let mut g_i = S::ZERO;
let mut g_d = S::ZERO;
let mut integral_e = S::ZERO;
let mut e_prev = S::ZERO;
for k in 0..DATA_LEN {
let e_k = r_data[k] - y_data[k];
integral_e += e_k * dt;
let deriv_e = if k == 0 { S::ZERO } else { (e_k - e_prev) / dt };
g_p += r_data[k] * e_k;
g_i += r_data[k] * integral_e;
g_d += r_data[k] * deriv_e;
e_prev = e_k;
}
g_p *= t_inv;
g_i *= t_inv;
g_d *= t_inv;
let mu = self.mu;
self.kp -= mu * g_p;
self.ki -= mu * g_i;
self.kd -= mu * g_d;
Ok(())
}
pub fn parameters(&self) -> (S, S, S) {
(self.kp, self.ki, self.kd)
}
pub fn correlation_criterion(&self, r: &[S; DATA_LEN], e: &[S; DATA_LEN]) -> S {
let t_inv = S::ONE / S::from_f64(DATA_LEN as f64);
let mut acc = S::ZERO;
for k in 0..DATA_LEN {
let v = r[k] * e[k];
acc += v * v;
}
acc * t_inv
}
pub fn reference_model_pole(&self) -> S {
self.m
}
pub fn step_size(&self) -> S {
self.mu
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn correlation_step_changes_parameters() {
const N: usize = 100;
let r_data = [1.0_f64; N];
let y_data = [0.8_f64; N];
let kp0 = 1.0_f64;
let ki0 = 0.0_f64;
let kd0 = 0.0_f64;
let mut tuner =
CorrelationTuner::<f64, N>::new(kp0, ki0, kd0, 0.8, 0.01, 0.1).expect("valid");
tuner.step(&r_data, &y_data).expect("step ok");
let (kp, ki, _kd) = tuner.parameters();
assert!(kp < kp0, "kp should decrease (was {kp0}, now {kp})");
assert!(ki < ki0, "ki should decrease (was {ki0}, now {ki})");
}
#[test]
fn correlation_correct_cross_correlation() {
const N: usize = 4;
let r = [1.0_f64, 2.0, 3.0, 4.0];
let e = [0.5_f64, 0.5, 0.5, 0.5];
let tuner = CorrelationTuner::<f64, N>::new(1.0, 0.0, 0.0, 0.5, 0.01, 0.1).expect("ok");
let j = tuner.correlation_criterion(&r, &e);
let expected = 1.875_f64;
assert!(
(j - expected).abs() < 1e-10,
"Expected J={expected}, got {j}"
);
}
#[test]
fn correlation_step_size_validation() {
assert_eq!(
CorrelationTuner::<f64, 100>::new(1.0, 0.0, 0.0, 0.5, 0.01, 0.0).err(),
Some(DataDrivenError::InvalidParameter)
);
assert_eq!(
CorrelationTuner::<f64, 100>::new(1.0, 0.0, 0.0, 0.5, 0.01, -0.1).err(),
Some(DataDrivenError::InvalidParameter)
);
assert_eq!(
CorrelationTuner::<f64, 100>::new(1.0, 0.0, 0.0, 1.0, 0.01, 0.1).err(),
Some(DataDrivenError::InvalidParameter)
);
assert!(CorrelationTuner::<f64, 100>::new(1.0, 0.0, 0.0, 0.5, 0.01, 0.1).is_ok());
}
#[test]
fn correlation_parameters_update_after_step() {
const N: usize = 50;
let r_data = [1.0_f64; N];
let y_data = [0.5_f64; N];
let mut tuner =
CorrelationTuner::<f64, N>::new(0.5, 0.1, 0.01, 0.7, 0.01, 0.5).expect("valid");
let (kp_before, ki_before, kd_before) = tuner.parameters();
tuner.step(&r_data, &y_data).expect("step ok");
let (kp_after, ki_after, kd_after) = tuner.parameters();
assert!((kp_after - kp_before).abs() > 1e-12, "kp should update");
assert!((ki_after - ki_before).abs() > 1e-12, "ki should update");
let _ = kd_before;
let _ = kd_after;
}
#[test]
fn correlation_criterion_decreases_over_multiple_steps() {
const N: usize = 200;
let r_data = [1.0_f64; N];
let y_data = [0.0_f64; N];
let mut tuner =
CorrelationTuner::<f64, N>::new(0.0, 0.0, 0.0, 0.9, 0.01, 0.01).expect("valid");
let e_data = {
let mut e = [0.0_f64; N];
for k in 0..N {
e[k] = r_data[k] - y_data[k];
}
e
};
let j_init = tuner.correlation_criterion(&r_data, &e_data);
for _ in 0..100 {
tuner.step(&r_data, &y_data).expect("step");
}
let (kp, ki, _kd) = tuner.parameters();
assert!(
kp.abs() > 1e-6 || ki.abs() > 1e-6,
"Parameters should be non-zero after steps"
);
assert!(j_init.is_finite(), "Criterion must be finite");
}
#[test]
fn correlation_f32_works() {
const N: usize = 60;
let r_data = [1.0_f32; N];
let y_data = [0.7_f32; N];
let mut tuner =
CorrelationTuner::<f32, N>::new(0.5, 0.1, 0.0, 0.8, 0.01, 0.2).expect("valid");
tuner.step(&r_data, &y_data).expect("step ok");
let (kp, ki, kd) = tuner.parameters();
assert!(kp.is_finite());
assert!(ki.is_finite());
assert!(kd.is_finite());
}
}