use crate::core::scalar::ControlScalar;
use super::IlcError;
pub struct NormOptimalIlc<S, const TRIAL_LEN: usize> {
u_ff: [S; TRIAL_LEN],
e_prev: [S; TRIAL_LEN],
l_opt: S,
q_weight: S,
r_weight: S,
delta_u: [S; TRIAL_LEN],
trial: usize,
}
impl<S: ControlScalar, const TRIAL_LEN: usize> NormOptimalIlc<S, TRIAL_LEN> {
pub fn new(markov_gain: S, q: S, r: S) -> Result<Self, IlcError> {
if !markov_gain.is_finite() || markov_gain.abs() <= S::EPSILON {
return Err(IlcError::InvalidGain);
}
if !q.is_finite() || q <= S::ZERO {
return Err(IlcError::InvalidGain);
}
if !r.is_finite() || r <= S::ZERO {
return Err(IlcError::InvalidGain);
}
let g2 = markov_gain * markov_gain;
let l_opt = markov_gain * q / (r + g2 * q);
Ok(Self {
u_ff: [S::ZERO; TRIAL_LEN],
e_prev: [S::ZERO; TRIAL_LEN],
l_opt,
q_weight: q,
r_weight: r,
delta_u: [S::ZERO; TRIAL_LEN],
trial: 0,
})
}
pub fn update(&mut self, error: &[S; TRIAL_LEN]) -> Result<&[S; TRIAL_LEN], IlcError> {
for (n, &e) in error.iter().enumerate() {
let du = self.l_opt * e;
let new_u = self.u_ff[n] + du;
if !new_u.is_finite() {
return Err(IlcError::NotConverged);
}
self.delta_u[n] = du;
self.u_ff[n] = new_u;
self.e_prev[n] = e;
}
self.trial += 1;
Ok(&self.u_ff)
}
#[inline]
pub fn feedforward(&self) -> &[S; TRIAL_LEN] {
&self.u_ff
}
#[inline]
pub fn optimal_gain(&self) -> S {
self.l_opt
}
pub fn cost(&self, error: &[S; TRIAL_LEN]) -> S {
let mut e_sq = S::ZERO;
let mut du_sq = S::ZERO;
for (&e, &du) in error.iter().zip(self.delta_u.iter()) {
e_sq += e * e;
du_sq += du * du;
}
self.q_weight * e_sq + self.r_weight * du_sq
}
#[inline]
pub fn trial_count(&self) -> usize {
self.trial
}
pub fn reset(&mut self) {
self.u_ff = [S::ZERO; TRIAL_LEN];
self.e_prev = [S::ZERO; TRIAL_LEN];
self.delta_u = [S::ZERO; TRIAL_LEN];
self.trial = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
const N: usize = 20;
#[test]
fn optimal_gain_stability() {
let ilc: NormOptimalIlc<f64, N> = NormOptimalIlc::new(1.0, 1.0, 0.1).unwrap();
assert!(
ilc.optimal_gain() < 1.0,
"l_opt={} should be < 1/g=1.0",
ilc.optimal_gain()
);
let ilc2: NormOptimalIlc<f64, N> = NormOptimalIlc::new(2.0, 3.0, 1.0).unwrap();
assert!(
ilc2.optimal_gain() < 0.5,
"l_opt={} should be < 0.5",
ilc2.optimal_gain()
);
}
#[test]
fn cost_decreases_each_trial() {
const G: f64 = 1.0;
const REF: f64 = 1.0;
let mut ilc: NormOptimalIlc<f64, N> = NormOptimalIlc::new(G, 1.0, 0.5).unwrap();
let mut prev_cost = f64::MAX;
for k in 0..10_usize {
let u_ff = *ilc.feedforward();
let mut error = [0.0_f64; N];
for (n, e) in error.iter_mut().enumerate() {
*e = REF - G * u_ff[n];
}
let cost = ilc.cost(&error);
if k > 0 {
assert!(
cost <= prev_cost + 1e-12,
"cost did not decrease at trial {k}: {cost} > {prev_cost}"
);
}
prev_cost = cost;
ilc.update(&error).unwrap();
}
}
#[test]
fn high_q_approaches_one_over_g() {
let g = 1.0_f64;
let ilc: NormOptimalIlc<f64, N> = NormOptimalIlc::new(g, 1e8, 1.0).unwrap();
let expected = 1.0 / g;
assert!(
(ilc.optimal_gain() - expected).abs() < 1e-4,
"l_opt={} should approach 1/g={expected}",
ilc.optimal_gain()
);
}
#[test]
fn high_r_approaches_zero() {
let ilc: NormOptimalIlc<f64, N> = NormOptimalIlc::new(1.0, 1.0, 1e8).unwrap();
assert!(
ilc.optimal_gain() < 1e-4,
"l_opt={} should approach 0",
ilc.optimal_gain()
);
}
#[test]
fn convergence_within_ten_trials() {
const G: f64 = 1.0;
const REF: f64 = 1.0;
let mut ilc: NormOptimalIlc<f64, N> = NormOptimalIlc::new(G, 10.0, 0.1).unwrap();
for _ in 0..10 {
let u_ff = *ilc.feedforward();
let mut error = [0.0_f64; N];
for (n, e) in error.iter_mut().enumerate() {
*e = REF - G * u_ff[n];
}
ilc.update(&error).unwrap();
}
let u_ff = *ilc.feedforward();
let mut final_err = [0.0_f64; N];
for (n, e) in final_err.iter_mut().enumerate() {
*e = REF - G * u_ff[n];
}
let mut sq = 0.0_f64;
for &v in final_err.iter() {
sq += v * v;
}
let err_norm = sq.sqrt();
assert!(
err_norm < 0.1 * (N as f64).sqrt(),
"Error norm {err_norm} too large after 10 trials"
);
}
#[test]
fn invalid_params_rejected() {
assert!(NormOptimalIlc::<f64, N>::new(1.0, 0.0, 1.0).is_err());
assert!(NormOptimalIlc::<f64, N>::new(1.0, 1.0, 0.0).is_err());
assert!(NormOptimalIlc::<f64, N>::new(0.0, 1.0, 1.0).is_err());
assert!(NormOptimalIlc::<f64, N>::new(1.0, -1.0, 1.0).is_err());
assert!(NormOptimalIlc::<f64, N>::new(f64::NAN, 1.0, 1.0).is_err());
}
#[test]
fn reset_clears_state() {
let mut ilc: NormOptimalIlc<f64, N> = NormOptimalIlc::new(1.0, 1.0, 1.0).unwrap();
let err = [0.5_f64; N];
ilc.update(&err).unwrap();
assert_eq!(ilc.trial_count(), 1);
ilc.reset();
assert_eq!(ilc.trial_count(), 0);
for &v in ilc.feedforward().iter() {
assert_eq!(v, 0.0);
}
}
}