use crate::core::matrix::{matvec, outer, Matrix};
use crate::core::scalar::ControlScalar;
#[derive(Debug)]
pub struct SelfTuningRegulator<S: ControlScalar, const NA: usize, const NB: usize, const NP: usize>
{
pub theta: [S; NP],
pub p: Matrix<S, NP, NP>,
pub lambda: S,
y_hist: [S; NA],
u_hist: [S; NB],
pub u_limit: S,
pub desired_poles: [S; NA],
pub mode: StrMode,
pub b_min: S,
sample_count: u32,
last_prediction_error: S,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StrMode {
MinimumVariance,
PolePlacement,
}
impl<S: ControlScalar, const NA: usize, const NB: usize, const NP: usize>
SelfTuningRegulator<S, NA, NB, NP>
{
pub fn new(lambda: S, p0: S, u_limit: S, b_min: S) -> Self {
debug_assert_eq!(NP, NA + NB, "NP must equal NA + NB");
Self {
theta: [S::ZERO; NP],
p: Matrix::<S, NP, NP>::identity().scale(p0),
lambda,
y_hist: [S::ZERO; NA],
u_hist: [S::ZERO; NB],
u_limit,
desired_poles: [S::ZERO; NA],
mode: StrMode::MinimumVariance,
b_min,
sample_count: 0,
last_prediction_error: S::ZERO,
}
}
pub fn update_estimate(&mut self, y: S, u_prev: S) {
for i in (1..NB).rev() {
self.u_hist[i] = self.u_hist[i - 1];
}
if NB > 0 {
self.u_hist[0] = u_prev;
}
let phi = self.build_regressor();
let y_hat: S = phi
.iter()
.zip(self.theta.iter())
.map(|(&p, &t)| p * t)
.fold(S::ZERO, |a, b| a + b);
let error = y - y_hat;
let p_phi = matvec(&self.p, &phi);
let phi_p_phi: S = phi
.iter()
.zip(p_phi.iter())
.map(|(&a, &b)| a * b)
.fold(S::ZERO, |acc, x| acc + x);
let denom = self.lambda + phi_p_phi;
if denom.abs() >= S::EPSILON {
let k: [S; NP] = core::array::from_fn(|i| p_phi[i] / denom);
for (i, &ki) in k.iter().enumerate() {
self.theta[i] += ki * error;
}
let k_phi_t: Matrix<S, NP, NP> = outer(&k, &phi);
let mut kp = Matrix::<S, NP, NP>::zeros();
for r in 0..NP {
for c in 0..NP {
kp.data[r][c] = k_phi_t.data[r]
.iter()
.zip(self.p.data.iter())
.map(|(&kf, pr)| kf * pr[c])
.fold(S::ZERO, |acc, x| acc + x);
}
}
self.p = self.p.sub_mat(&kp).scale(S::ONE / self.lambda);
}
for i in (1..NA).rev() {
self.y_hist[i] = self.y_hist[i - 1];
}
if NA > 0 {
self.y_hist[0] = y;
}
self.last_prediction_error = error;
self.sample_count += 1;
}
pub fn compute_control(&self, r: S) -> S {
let u = match self.mode {
StrMode::MinimumVariance => self.minimum_variance_law(r),
StrMode::PolePlacement => self.pole_placement_law(r),
};
u.clamp_val(-self.u_limit, self.u_limit)
}
fn minimum_variance_law(&self, r: S) -> S {
let mut ar_sum = S::ZERO;
for i in 0..NA {
ar_sum += self.theta[i] * self.y_hist[i];
}
let b1 = if NB > 0 { self.theta[NA] } else { S::ONE };
let b1_guarded = if b1.abs() < self.b_min {
if b1 >= S::ZERO {
self.b_min
} else {
-self.b_min
}
} else {
b1
};
let mut bu_sum = S::ZERO;
for j in 1..NB {
bu_sum += self.theta[NA + j] * self.u_hist[j];
}
(r + ar_sum - bu_sum) / b1_guarded
}
fn pole_placement_law(&self, r: S) -> S {
if NA == 0 || NB == 0 {
return self.minimum_variance_law(r);
}
let pc1 = if NA > 0 {
self.desired_poles[0]
} else {
S::ZERO
};
let a1 = if NA > 0 { self.theta[0] } else { S::ZERO };
let b1 = if NB > 0 { self.theta[NA] } else { S::ONE };
let b1_guarded = if b1.abs() < self.b_min {
if b1 >= S::ZERO {
self.b_min
} else {
-self.b_min
}
} else {
b1
};
let r0 = (-pc1 - a1) / b1_guarded;
let t0 = -pc1;
let y_now = if NA > 0 { self.y_hist[0] } else { S::ZERO };
t0 * r - r0 * y_now
}
fn build_regressor(&self) -> [S; NP] {
let mut phi = [S::ZERO; NP];
for (phi_i, &y_i) in phi.iter_mut().zip(self.y_hist.iter()).take(NA) {
*phi_i = -y_i;
}
for (phi_j, &u_j) in phi[NA..].iter_mut().zip(self.u_hist.iter()).take(NB) {
*phi_j = u_j;
}
phi
}
pub fn parameters(&self) -> &[S; NP] {
&self.theta
}
pub fn a_params(&self) -> &[S] {
&self.theta[..NA]
}
pub fn b_params(&self) -> &[S] {
&self.theta[NA..]
}
pub fn reset(&mut self, p0: S) {
self.theta = [S::ZERO; NP];
self.p = Matrix::<S, NP, NP>::identity().scale(p0);
self.y_hist = [S::ZERO; NA];
self.u_hist = [S::ZERO; NB];
self.sample_count = 0;
self.last_prediction_error = S::ZERO;
}
pub fn sample_count(&self) -> u32 {
self.sample_count
}
pub fn prediction_error(&self, _y: S) -> S {
self.last_prediction_error
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identifies_first_order_plant() {
let mut str_ctrl = SelfTuningRegulator::<f64, 1, 1, 2>::new(1.0, 1e6, 100.0, 0.01);
let mut y = 0.0_f64;
let mut u = 0.0_f64;
for k in 1..=500 {
let y_next = 0.8 * y + 0.5 * u;
str_ctrl.update_estimate(y_next, u);
u = if k % 10 < 5 { 1.0 } else { -1.0 };
y = y_next;
}
let a1 = str_ctrl.a_params()[0];
let b1 = str_ctrl.b_params()[0];
assert!(
(a1 - (-0.8)).abs() < 0.05,
"a1 est={:.4} (expected -0.8)",
a1
);
assert!((b1 - 0.5).abs() < 0.05, "b1 est={:.4} (expected 0.5)", b1);
}
#[test]
fn mv_control_tracks_reference() {
let mut str_ctrl = SelfTuningRegulator::<f64, 1, 1, 2>::new(0.98, 1e6, 50.0, 0.05);
let mut y = 0.0_f64;
let mut u = 0.0_f64;
let r = 1.0_f64;
for _ in 0..1000 {
let y_next = 0.7 * y + 0.8 * u;
str_ctrl.update_estimate(y_next, u);
u = str_ctrl.compute_control(r);
y = y_next;
}
assert!(
(y - r).abs() < 0.2,
"STR MV tracking: y={:.4}, r={:.4}",
y,
r
);
}
#[test]
fn reset_clears_state() {
let mut str_ctrl = SelfTuningRegulator::<f64, 1, 1, 2>::new(1.0, 1e4, 100.0, 0.01);
let mut y = 0.0_f64;
let u = 1.0_f64;
for _ in 0..100 {
let y_next = 0.8 * y + 0.5 * u;
str_ctrl.update_estimate(y_next, u);
y = y_next;
}
str_ctrl.reset(1e4);
assert_eq!(str_ctrl.sample_count(), 0);
assert!((str_ctrl.parameters()[0]).abs() < 1e-10);
}
#[test]
fn control_saturates() {
let str_ctrl = SelfTuningRegulator::<f64, 1, 1, 2>::new(1.0, 1e4, 5.0, 0.01);
let u = str_ctrl.compute_control(1000.0);
assert!(u.abs() <= 5.0 + 1e-9, "u={}", u);
}
#[test]
fn prediction_error_zero_on_exact_model() {
let mut str_ctrl = SelfTuningRegulator::<f64, 1, 1, 2>::new(1.0, 1e6, 100.0, 0.01);
let mut y = 0.0_f64;
let mut u = 0.0_f64;
for k in 1..=800 {
let y_next = 0.5 * y + 1.0 * u;
str_ctrl.update_estimate(y_next, u);
u = if k % 7 < 3 { 1.0 } else { -1.0 };
y = y_next;
}
let e = str_ctrl.prediction_error(y);
assert!(e.abs() < 0.2, "prediction error={:.4}", e);
}
#[test]
fn second_order_identification() {
let mut str_ctrl = SelfTuningRegulator::<f64, 2, 2, 4>::new(0.99, 1e5, 100.0, 0.01);
let mut y = [0.0_f64; 3];
let mut u = [0.0_f64; 3];
for k in 1..=2000 {
let y_next = 1.3 * y[0] - 0.4 * y[1] + 0.5 * u[0] + 0.2 * u[1];
str_ctrl.update_estimate(y_next, u[0]);
u[1] = u[0];
u[0] = if k % 13 < 6 { 1.5 } else { -1.0 };
y[2] = y[1];
y[1] = y[0];
y[0] = y_next;
}
let params = str_ctrl.parameters();
assert!((params[0] - (-1.3)).abs() < 0.1, "a1={:.4}", params[0]);
assert!((params[1] - 0.4).abs() < 0.1, "a2={:.4}", params[1]);
assert!((params[2] - 0.5).abs() < 0.1, "b1={:.4}", params[2]);
assert!((params[3] - 0.2).abs() < 0.1, "b2={:.4}", params[3]);
}
}