#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::needless_range_loop)]
use crate::core::scalar::ControlScalar;
use crate::data_driven::vrft::DataDrivenError;
#[derive(Debug, Clone, PartialEq)]
pub struct FritTuner<S, const DATA_LEN: usize> {
kp: S,
ki: S,
kd: S,
m: S,
dt: S,
mu: S,
iteration: usize,
}
impl<S: ControlScalar, const DATA_LEN: usize> FritTuner<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,
iteration: 0,
})
}
pub fn step(
&mut self,
r_data: &[S; DATA_LEN],
u_data: &[S; DATA_LEN],
y_data: &[S; DATA_LEN],
) -> Result<S, DataDrivenError> {
if DATA_LEN < 2 {
return Err(DataDrivenError::NotEnoughData);
}
let (cost, grad_kp, grad_ki, grad_kd) =
self.compute_cost_and_gradient(r_data, u_data, y_data);
let mu = self.mu;
self.kp -= mu * grad_kp;
self.ki -= mu * grad_ki;
self.kd -= mu * grad_kd;
self.iteration += 1;
Ok(cost)
}
pub fn frit_cost(
&self,
r_data: &[S; DATA_LEN],
u_data: &[S; DATA_LEN],
y_data: &[S; DATA_LEN],
) -> Result<S, DataDrivenError> {
if DATA_LEN < 2 {
return Err(DataDrivenError::NotEnoughData);
}
let (cost, _, _, _) = self.compute_cost_and_gradient(r_data, u_data, y_data);
Ok(cost)
}
fn compute_cost_and_gradient(
&self,
r_data: &[S; DATA_LEN],
u_data: &[S; DATA_LEN],
y_data: &[S; DATA_LEN],
) -> (S, S, S, S) {
let m = self.m;
let one_minus_m = S::ONE - m;
let dt = self.dt;
let t_inv = S::ONE / S::from_f64(DATA_LEN as f64);
let kp = self.kp;
let ki = self.ki;
let kd = self.kd;
let mut cost = S::ZERO;
let mut grad_kp = S::ZERO;
let mut grad_ki = S::ZERO;
let mut grad_kd = S::ZERO;
let mut dy = S::ZERO; let mut s_kp = S::ZERO; let mut s_ki = S::ZERO; let mut s_kd = 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 };
let u_theta = kp * e_k + ki * integral_e + kd * deriv_e;
let v_k = u_data[k] - u_theta;
dy = m * dy + one_minus_m * v_k;
s_kp = m * s_kp + one_minus_m * (-e_k);
s_ki = m * s_ki + one_minus_m * (-integral_e);
s_kd = m * s_kd + one_minus_m * (-deriv_e);
cost += dy * dy;
grad_kp += dy * s_kp;
grad_ki += dy * s_ki;
grad_kd += dy * s_kd;
e_prev = e_k;
}
let two_t_inv = S::TWO * t_inv;
(
cost * t_inv,
grad_kp * two_t_inv,
grad_ki * two_t_inv,
grad_kd * two_t_inv,
)
}
pub fn parameters(&self) -> (S, S, S) {
(self.kp, self.ki, self.kd)
}
pub fn iteration(&self) -> usize {
self.iteration
}
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_closed_loop_data<const N: usize>(
a: f64,
b: f64,
kp0: f64,
r_val: f64,
) -> ([f64; N], [f64; N], [f64; N]) {
let r_data = [r_val; N];
let mut u_data = [0.0_f64; N];
let mut y_data = [0.0_f64; N];
for k in 1..N {
y_data[k] = a * y_data[k - 1] + b * u_data[k - 1];
let e = r_data[k] - y_data[k];
u_data[k] = kp0 * e;
u_data[k] = u_data[k].clamp(-10.0, 10.0);
}
let _ = r_data[0];
(r_data, u_data, y_data)
}
#[test]
fn frit_cost_is_computable() {
const N: usize = 100;
let (r_data, u_data, y_data) = generate_closed_loop_data::<N>(0.7, 0.3, 1.0, 1.0);
let tuner = FritTuner::<f64, N>::new(1.0, 0.0, 0.0, 0.8, 0.01, 0.1).expect("valid");
let cost = tuner.frit_cost(&r_data, &u_data, &y_data).expect("cost ok");
assert!(cost.is_finite(), "FRIT cost must be finite, got {cost}");
assert!(cost >= 0.0, "FRIT cost must be non-negative, got {cost}");
}
#[test]
fn frit_parameters_update_after_step() {
const N: usize = 80;
let (r_data, u_data, y_data) = generate_closed_loop_data::<N>(0.7, 0.3, 1.0, 1.0);
let mut tuner = FritTuner::<f64, N>::new(1.0, 0.1, 0.02, 0.8, 0.01, 0.5).expect("valid");
let (kp0, ki0, kd0) = tuner.parameters();
tuner.step(&r_data, &u_data, &y_data).expect("step ok");
let (kp1, ki1, kd1) = tuner.parameters();
let changed =
(kp1 - kp0).abs() > 1e-14 || (ki1 - ki0).abs() > 1e-14 || (kd1 - kd0).abs() > 1e-14;
assert!(changed, "Parameters should update after a step");
}
#[test]
fn frit_step_size_effect() {
const N: usize = 100;
let (r_data, u_data, y_data) = generate_closed_loop_data::<N>(0.7, 0.3, 0.5, 1.0);
let mut tuner_small =
FritTuner::<f64, N>::new(1.0, 0.0, 0.0, 0.8, 0.01, 0.01).expect("valid");
let mut tuner_large =
FritTuner::<f64, N>::new(1.0, 0.0, 0.0, 0.8, 0.01, 1.0).expect("valid");
tuner_small.step(&r_data, &u_data, &y_data).expect("step");
tuner_large.step(&r_data, &u_data, &y_data).expect("step");
let (kp_small, _, _) = tuner_small.parameters();
let (kp_large, _, _) = tuner_large.parameters();
let change_small = (kp_small - 1.0_f64).abs();
let change_large = (kp_large - 1.0_f64).abs();
assert!(
change_large >= change_small,
"Larger mu should produce larger change: small={change_small}, large={change_large}"
);
}
#[test]
fn frit_cost_does_not_increase_unboundedly_over_iterations() {
const N: usize = 120;
let (r_data, u_data, y_data) = generate_closed_loop_data::<N>(0.7, 0.3, 1.0, 1.0);
let mut tuner = FritTuner::<f64, N>::new(1.0, 0.0, 0.0, 0.85, 0.01, 0.05).expect("valid");
let cost0 = tuner
.frit_cost(&r_data, &u_data, &y_data)
.expect("initial cost");
let mut final_cost = cost0;
for _ in 0..10 {
final_cost = tuner.step(&r_data, &u_data, &y_data).expect("step");
}
assert!(
final_cost.is_finite(),
"Cost must remain finite, got {final_cost}"
);
assert!(
final_cost <= cost0 * 100.0 + 1.0,
"Cost should stay bounded: initial={cost0}, final={final_cost}"
);
}
#[test]
fn frit_iteration_counter() {
const N: usize = 60;
let (r_data, u_data, y_data) = generate_closed_loop_data::<N>(0.8, 0.2, 1.0, 1.0);
let mut tuner = FritTuner::<f64, N>::new(0.5, 0.0, 0.0, 0.9, 0.01, 0.1).expect("valid");
assert_eq!(tuner.iteration(), 0);
for i in 1..=5 {
tuner.step(&r_data, &u_data, &y_data).expect("step");
assert_eq!(
tuner.iteration(),
i,
"iteration counter mismatch at step {i}"
);
}
}
#[test]
fn frit_invalid_params_rejected() {
assert_eq!(
FritTuner::<f64, 100>::new(1.0, 0.0, 0.0, 0.0, 0.01, 0.1),
Err(DataDrivenError::InvalidParameter)
);
assert_eq!(
FritTuner::<f64, 100>::new(1.0, 0.0, 0.0, 1.0, 0.01, 0.1),
Err(DataDrivenError::InvalidParameter)
);
assert_eq!(
FritTuner::<f64, 100>::new(1.0, 0.0, 0.0, 0.8, 0.0, 0.1),
Err(DataDrivenError::InvalidParameter)
);
assert_eq!(
FritTuner::<f64, 100>::new(1.0, 0.0, 0.0, 0.8, 0.01, 0.0),
Err(DataDrivenError::InvalidParameter)
);
assert!(FritTuner::<f64, 100>::new(1.0, 0.0, 0.0, 0.8, 0.01, 0.1).is_ok());
}
#[test]
fn frit_f32_works() {
const N: usize = 50;
let r_data = [1.0_f32; N];
let u_data = [0.5_f32; N];
let y_data = {
let mut y = [0.0_f32; N];
for k in 1..N {
y[k] = 0.7 * y[k - 1] + 0.3 * u_data[k - 1];
}
y
};
let mut tuner = FritTuner::<f32, N>::new(1.0, 0.0, 0.0, 0.8, 0.01, 0.05).expect("valid");
let cost = tuner.step(&r_data, &u_data, &y_data).expect("step");
assert!(cost.is_finite());
}
}