#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::needless_range_loop)]
use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataDrivenError {
NotEnoughData,
SingularMatrix,
NotTuned,
InvalidParameter,
}
impl core::fmt::Display for DataDrivenError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotEnoughData => f.write_str("not enough data points"),
Self::SingularMatrix => f.write_str("singular or ill-conditioned matrix"),
Self::NotTuned => f.write_str("tuner has not been run yet; call tune() first"),
Self::InvalidParameter => f.write_str("parameter out of valid range"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DataDrivenError {}
fn gaussian_solve_3x3<S: ControlScalar>(
mut a: [[S; 3]; 3],
mut b: [S; 3],
) -> Result<[S; 3], DataDrivenError> {
let mut anorm = S::ZERO;
for row in &a {
let mut row_sum = S::ZERO;
for &v in row {
let av = if v < S::ZERO { -v } else { v };
row_sum += av;
}
if row_sum > anorm {
anorm = row_sum;
}
}
let thresh = if anorm == S::ZERO {
S::EPSILON
} else {
S::from_f64(1e-12) * anorm
};
for col in 0..3_usize {
let mut pivot_row = col;
let mut pivot_val = {
let v = a[col][col];
if v < S::ZERO {
-v
} else {
v
}
};
for row in (col + 1)..3 {
let v = a[row][col];
let av = if v < S::ZERO { -v } else { v };
if av > pivot_val {
pivot_val = av;
pivot_row = row;
}
}
if pivot_val < thresh {
return Err(DataDrivenError::SingularMatrix);
}
if pivot_row != col {
a.swap(pivot_row, col);
b.swap(pivot_row, col);
}
for row in (col + 1)..3 {
let factor = a[row][col] / a[col][col];
for k in col..3 {
let sub = factor * a[col][k];
a[row][k] -= sub;
}
let sub = factor * b[col];
b[row] -= sub;
}
}
let mut x = [S::ZERO; 3];
let mut i = 3_usize;
while i > 0 {
i -= 1;
if {
let v = a[i][i];
if v < S::ZERO {
-v
} else {
v
}
} < thresh
{
return Err(DataDrivenError::SingularMatrix);
}
let mut s = b[i];
for j in (i + 1)..3 {
s -= a[i][j] * x[j];
}
x[i] = s / a[i][i];
}
Ok(x)
}
#[derive(Debug, Clone, PartialEq)]
pub struct VrftPid<S, const DATA_LEN: usize> {
m: S,
dt: S,
kp: S,
ki: S,
kd: S,
tuned: bool,
}
impl<S: ControlScalar, const DATA_LEN: usize> VrftPid<S, DATA_LEN> {
pub fn new(m: S, dt: S) -> Result<Self, DataDrivenError> {
if m <= S::ZERO || m >= S::ONE {
return Err(DataDrivenError::InvalidParameter);
}
if dt <= S::ZERO {
return Err(DataDrivenError::InvalidParameter);
}
Ok(Self {
m,
dt,
kp: S::ZERO,
ki: S::ZERO,
kd: S::ZERO,
tuned: false,
})
}
pub fn tune(
&mut self,
u_data: &[S; DATA_LEN],
y_data: &[S; DATA_LEN],
) -> Result<(), DataDrivenError> {
if DATA_LEN < 3 {
return Err(DataDrivenError::NotEnoughData);
}
let m = self.m;
let dt = self.dt;
let one_minus_m = S::ONE - m;
let mut g = [[S::ZERO; 3]; 3];
let mut h = [S::ZERO; 3];
let mut integral_e = S::ZERO; let mut e_prev = {
let r0 = y_data[0] / one_minus_m;
r0 - y_data[0]
};
for k in 1..DATA_LEN {
let r_k = (y_data[k] - m * y_data[k - 1]) / one_minus_m;
let e_k = r_k - y_data[k];
integral_e += e_k * dt;
let deriv_e = (e_k - e_prev) / dt;
let phi = [e_k, integral_e, deriv_e];
for i in 0..3 {
for j in 0..3 {
g[i][j] += phi[i] * phi[j];
}
h[i] += phi[i] * u_data[k];
}
e_prev = e_k;
}
let theta = gaussian_solve_3x3(g, h)?;
self.kp = theta[0];
self.ki = theta[1];
self.kd = theta[2];
self.tuned = true;
Ok(())
}
pub fn kp(&self) -> Result<S, DataDrivenError> {
if self.tuned {
Ok(self.kp)
} else {
Err(DataDrivenError::NotTuned)
}
}
pub fn ki(&self) -> Result<S, DataDrivenError> {
if self.tuned {
Ok(self.ki)
} else {
Err(DataDrivenError::NotTuned)
}
}
pub fn kd(&self) -> Result<S, DataDrivenError> {
if self.tuned {
Ok(self.kd)
} else {
Err(DataDrivenError::NotTuned)
}
}
pub fn is_tuned(&self) -> bool {
self.tuned
}
pub fn reference_model_pole(&self) -> S {
self.m
}
}
#[cfg(test)]
mod tests {
use super::*;
fn simulate_pid_plant<const N: usize>(
a: f64,
b: f64,
kp: f64,
ki: f64,
kd: f64,
dt: f64,
) -> ([f64; N], [f64; N]) {
let mut u = [0.0_f64; N];
let mut y = [0.0_f64; N];
let r = 1.0_f64;
let mut integral = 0.0_f64;
let mut e_prev = 0.0_f64;
for k in 1..N {
y[k] = a * y[k - 1] + b * u[k - 1];
let e = r - y[k];
integral += e * dt;
let deriv = (e - e_prev) / dt;
u[k] = kp * e + ki * integral + kd * deriv;
u[k] = u[k].clamp(-10.0, 10.0); e_prev = e;
}
(u, y)
}
#[test]
fn vrft_recovers_pid_gains_approx() {
let kp_true = 1.5_f64;
let ki_true = 0.1_f64;
let kd_true = 0.05_f64;
let dt = 0.01_f64;
let (u_data, y_data) = simulate_pid_plant::<200>(0.8, 0.2, kp_true, ki_true, kd_true, dt);
let m = 0.905_f64;
let mut tuner = VrftPid::<f64, 200>::new(m, dt).expect("valid params");
tuner.tune(&u_data, &y_data).expect("tune succeeds");
let kp_est = tuner.kp().expect("tuned");
let ki_est = tuner.ki().expect("tuned");
let kd_est = tuner.kd().expect("tuned");
assert!(kp_est.is_finite(), "kp must be finite, got {kp_est}");
assert!(ki_est.is_finite(), "ki must be finite, got {ki_est}");
assert!(kd_est.is_finite(), "kd must be finite, got {kd_est}");
assert!(
kp_est.abs() > 1e-6 || ki_est.abs() > 1e-6 || kd_est.abs() > 1e-6,
"At least one gain must be non-zero: kp={kp_est}, ki={ki_est}, kd={kd_est}"
);
}
#[test]
fn vrft_m_out_of_range_returns_error() {
assert!(
matches!(
VrftPid::<f64, 100>::new(0.0, 0.01),
Err(DataDrivenError::InvalidParameter)
),
"m=0.0 should be rejected"
);
assert!(
matches!(
VrftPid::<f64, 100>::new(1.0, 0.01),
Err(DataDrivenError::InvalidParameter)
),
"m=1.0 should be rejected"
);
assert!(
matches!(
VrftPid::<f64, 100>::new(-0.5, 0.01),
Err(DataDrivenError::InvalidParameter)
),
"m=-0.5 should be rejected"
);
assert!(
matches!(
VrftPid::<f64, 100>::new(1.5, 0.01),
Err(DataDrivenError::InvalidParameter)
),
"m=1.5 should be rejected"
);
assert!(VrftPid::<f64, 100>::new(0.5, 0.01).is_ok());
}
#[test]
fn vrft_not_tuned_returns_error() {
let tuner = VrftPid::<f64, 100>::new(0.8, 0.01).expect("valid");
assert!(
matches!(tuner.kp(), Err(DataDrivenError::NotTuned)),
"kp() should return NotTuned before tuning"
);
assert!(
matches!(tuner.ki(), Err(DataDrivenError::NotTuned)),
"ki() should return NotTuned before tuning"
);
assert!(
matches!(tuner.kd(), Err(DataDrivenError::NotTuned)),
"kd() should return NotTuned before tuning"
);
assert!(!tuner.is_tuned());
}
#[test]
fn vrft_regressor_integral_column_monotone() {
const N: usize = 50;
let mut u_data = [1.0_f64; N];
let mut y_data = [0.0_f64; N];
for k in 0..N {
y_data[k] = 0.01 * (k as f64) * 0.01;
u_data[k] = 1.0;
}
let m = 0.8_f64;
let dt = 0.01_f64;
let one_minus_m = 1.0 - m;
let mut integral = 0.0_f64;
let mut e_prev = y_data[0] / one_minus_m - y_data[0];
let mut integrals = [0.0_f64; N];
for k in 1..N {
let r_k = (y_data[k] - m * y_data[k - 1]) / one_minus_m;
let e_k = r_k - y_data[k];
integral += e_k * dt;
integrals[k] = integral;
e_prev = e_k;
}
let _ = e_prev;
let first = integrals[1];
let last = integrals[N - 1];
assert!(
(last - first).abs() > 1e-10,
"Integral should vary: first={first}, last={last}"
);
}
#[test]
fn vrft_least_squares_residual_improvement() {
const N: usize = 150;
let dt = 0.01_f64;
let (u_data, y_data) = simulate_pid_plant::<N>(0.7, 0.3, 2.0, 0.2, 0.1, dt);
let m = 0.85_f64;
let mut tuner = VrftPid::<f64, N>::new(m, dt).expect("valid");
tuner.tune(&u_data, &y_data).expect("tune");
let kp = tuner.kp().expect("kp");
let ki = tuner.ki().expect("ki");
let kd = tuner.kd().expect("kd");
let one_minus_m = 1.0 - m;
let mut residual_sq = 0.0_f64;
let mut integral_e = 0.0_f64;
let mut e_prev = y_data[0] / one_minus_m - y_data[0];
for k in 1..N {
let r_k = (y_data[k] - m * y_data[k - 1]) / one_minus_m;
let e_k = r_k - y_data[k];
integral_e += e_k * dt;
let deriv_e = (e_k - e_prev) / dt;
let u_hat = kp * e_k + ki * integral_e + kd * deriv_e;
let diff = u_data[k] - u_hat;
residual_sq += diff * diff;
e_prev = e_k;
}
let u_norm_sq: f64 = u_data.iter().skip(1).map(|&v| v * v).sum();
assert!(
residual_sq <= u_norm_sq + 1e-6,
"Residual {residual_sq:.4} should be ≤ baseline {u_norm_sq:.4}"
);
}
#[test]
fn vrft_accessor_after_tune() {
const N: usize = 100;
let dt = 0.01_f64;
let (u_data, y_data) = simulate_pid_plant::<N>(0.8, 0.2, 1.0, 0.05, 0.02, dt);
let mut tuner = VrftPid::<f64, N>::new(0.9, dt).expect("valid");
assert!(!tuner.is_tuned());
tuner.tune(&u_data, &y_data).expect("tune");
assert!(tuner.is_tuned());
assert_eq!(tuner.reference_model_pole(), 0.9_f64);
assert!(tuner.kp().is_ok());
assert!(tuner.ki().is_ok());
assert!(tuner.kd().is_ok());
}
#[test]
fn vrft_dt_invalid_returns_error() {
assert_eq!(
VrftPid::<f64, 100>::new(0.8, 0.0),
Err(DataDrivenError::InvalidParameter)
);
assert_eq!(
VrftPid::<f64, 100>::new(0.8, -0.01),
Err(DataDrivenError::InvalidParameter)
);
}
#[test]
fn vrft_f32_works() {
const N: usize = 80;
let mut u_data = [0.0_f32; N];
let mut y_data = [0.0_f32; N];
for k in 0..N {
let t = k as f32 * 0.01;
u_data[k] = (t * 10.0).sin();
y_data[k] = 0.8 * (if k > 0 { y_data[k - 1] } else { 0.0 })
+ 0.2 * (if k > 0 { u_data[k - 1] } else { 0.0 });
}
let mut tuner = VrftPid::<f32, N>::new(0.85_f32, 0.01_f32).expect("valid");
let _ = tuner.tune(&u_data, &y_data);
}
}