#![allow(
clippy::needless_range_loop,
clippy::doc_overindented_list_items,
clippy::doc_lazy_continuation,
clippy::manual_memcpy
)]
use crate::core::scalar::ControlScalar;
use crate::sysid::SysIdError;
pub(crate) fn cholesky_solve_n<S: ControlScalar, const N: usize>(
a: &mut [[S; N]; N],
b: &mut [S; N],
) -> Result<(), SysIdError> {
for i in 0..N {
for j in 0..=i {
let mut s = a[i][j];
for k in 0..j {
s -= a[i][k] * a[j][k];
}
if i == j {
if s <= S::ZERO {
return Err(SysIdError::SingularMatrix);
}
a[i][j] = s.sqrt();
} else {
let diag = a[j][j];
if diag == S::ZERO {
return Err(SysIdError::SingularMatrix);
}
a[i][j] = s / diag;
}
}
}
for i in 0..N {
let mut s = b[i];
for k in 0..i {
s -= a[i][k] * b[k];
}
let diag = a[i][i];
if diag == S::ZERO {
return Err(SysIdError::SingularMatrix);
}
b[i] = s / diag;
}
let mut i = N;
while i > 0 {
i -= 1;
let mut s = b[i];
for k in (i + 1)..N {
s -= a[k][i] * b[k];
}
let diag = a[i][i];
if diag == S::ZERO {
return Err(SysIdError::SingularMatrix);
}
b[i] = s / diag;
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub struct ArxModel<S: ControlScalar, const NA: usize, const NB: usize> {
pub a: [S; NA],
pub b: [S; NB],
}
impl<S: ControlScalar, const NA: usize, const NB: usize> ArxModel<S, NA, NB> {
pub fn zeros() -> Self {
Self {
a: [S::ZERO; NA],
b: [S::ZERO; NB],
}
}
pub fn predict(&self, y_history: &[S; NA], u_history: &[S; NB]) -> S {
let mut y_hat = S::ZERO;
for i in 0..NA {
y_hat -= self.a[i] * y_history[i];
}
for i in 0..NB {
y_hat += self.b[i] * u_history[i];
}
y_hat
}
pub fn simulate<const M: usize>(
&self,
u_seq: &[S],
y0: &[S; NA],
) -> Result<[S; M], SysIdError> {
let needed = if NB > 0 { NB - 1 + M } else { M };
if u_seq.len() < needed {
return Err(SysIdError::InsufficientData);
}
let mut y_buf = *y0;
let mut out = [S::ZERO; M];
for step in 0..M {
let base = if NB > 0 { NB - 1 } else { 0 };
let mut u_hist = [S::ZERO; NB];
for j in 0..NB {
let idx = base + step;
if idx >= j {
u_hist[j] = u_seq[idx - j];
}
}
let y_new = self.predict(&y_buf, &u_hist);
out[step] = y_new;
let mut j = NA;
while j > 1 {
y_buf[j - 1] = y_buf[j - 2];
j -= 1;
}
if NA > 0 {
y_buf[0] = y_new;
}
}
Ok(out)
}
}
pub struct ArxIdentifier<
S: ControlScalar,
const NA: usize,
const NB: usize,
const P: usize,
const NK: usize,
> {
_phantom: core::marker::PhantomData<S>,
}
impl<S: ControlScalar, const NA: usize, const NB: usize, const P: usize, const NK: usize>
ArxIdentifier<S, NA, NB, P, NK>
{
pub fn fit(y: &[S], u: &[S]) -> Result<ArxModel<S, NA, NB>, SysIdError> {
let n = y.len();
if u.len() != n {
return Err(SysIdError::InvalidData);
}
for &v in y.iter().chain(u.iter()) {
if !v.is_finite() {
return Err(SysIdError::InvalidData);
}
}
let min_start = if NK + NB > NA { NK + NB } else { NA };
if n <= min_start {
return Err(SysIdError::InsufficientData);
}
if P == 0 {
return Ok(ArxModel::zeros());
}
let mut ata = [[S::ZERO; P]; P];
let mut aty = [S::ZERO; P];
for t in min_start..n {
let mut phi = [S::ZERO; P];
for i in 0..NA {
phi[i] = -y[t - 1 - i];
}
for i in 0..NB {
let delay = NK + i;
if delay <= t {
phi[NA + i] = u[t - delay];
}
}
let yt = y[t];
for i in 0..P {
for j in 0..P {
ata[i][j] += phi[i] * phi[j];
}
aty[i] += phi[i] * yt;
}
}
cholesky_solve_n::<S, P>(&mut ata, &mut aty)?;
let mut model = ArxModel::zeros();
for i in 0..NA {
model.a[i] = aty[i];
}
for i in 0..NB {
model.b[i] = aty[NA + i];
}
Ok(model)
}
}
#[derive(Debug, Clone)]
pub struct RecursiveArx<
S: ControlScalar,
const NA: usize,
const NB: usize,
const P: usize,
const NK: usize,
const UBN: usize,
> {
theta: [S; P],
cov: [[S; P]; P],
lambda: S,
y_buf: [S; NA],
u_buf: [S; UBN],
steps: u32,
}
impl<
S: ControlScalar,
const NA: usize,
const NB: usize,
const P: usize,
const NK: usize,
const UBN: usize,
> RecursiveArx<S, NA, NB, P, NK, UBN>
{
pub fn new(lambda: S, p_init: S) -> Self {
let mut cov = [[S::ZERO; P]; P];
for i in 0..P {
cov[i][i] = p_init;
}
Self {
theta: [S::ZERO; P],
cov,
lambda,
y_buf: [S::ZERO; NA],
u_buf: [S::ZERO; UBN],
steps: 0,
}
}
pub fn update(&mut self, y_new: S, u_new: S) -> Result<(), SysIdError> {
if !y_new.is_finite() || !u_new.is_finite() {
return Err(SysIdError::InvalidData);
}
{
let mut i = UBN;
while i > 1 {
self.u_buf[i - 1] = self.u_buf[i - 2];
i -= 1;
}
if UBN > 0 {
self.u_buf[0] = u_new;
}
}
let mut phi = [S::ZERO; P];
for i in 0..NA {
phi[i] = -self.y_buf[i];
}
for i in 0..NB {
let delay = NK + i;
if delay < UBN {
phi[NA + i] = self.u_buf[delay];
}
}
let mut p_phi = [S::ZERO; P];
for i in 0..P {
for j in 0..P {
p_phi[i] += self.cov[i][j] * phi[j];
}
}
let mut phi_t_p_phi = S::ZERO;
for i in 0..P {
phi_t_p_phi += phi[i] * p_phi[i];
}
let denom = self.lambda + phi_t_p_phi;
if denom == S::ZERO {
return Err(SysIdError::SingularMatrix);
}
let mut k = [S::ZERO; P];
for i in 0..P {
k[i] = p_phi[i] / denom;
}
let mut y_hat = S::ZERO;
for i in 0..P {
y_hat += phi[i] * self.theta[i];
}
let innovation = y_new - y_hat;
for i in 0..P {
self.theta[i] += k[i] * innovation;
}
let mut phi_t_p = [S::ZERO; P];
for j in 0..P {
for l in 0..P {
phi_t_p[j] += phi[l] * self.cov[l][j];
}
}
let lambda_inv = S::ONE / self.lambda;
for i in 0..P {
for j in 0..P {
self.cov[i][j] = (self.cov[i][j] - k[i] * phi_t_p[j]) * lambda_inv;
}
}
{
let mut i = NA;
while i > 1 {
self.y_buf[i - 1] = self.y_buf[i - 2];
i -= 1;
}
if NA > 0 {
self.y_buf[0] = y_new;
}
}
self.steps += 1;
Ok(())
}
pub fn model(&self) -> ArxModel<S, NA, NB> {
let mut model = ArxModel::zeros();
for i in 0..NA {
model.a[i] = self.theta[i];
}
for i in 0..NB {
model.b[i] = self.theta[NA + i];
}
model
}
pub fn steps(&self) -> u32 {
self.steps
}
}
pub fn fit_percent<S: ControlScalar>(model_output: &[S], actual: &[S]) -> S {
let n = model_output.len().min(actual.len());
if n == 0 {
return S::ZERO;
}
let mut sum = S::ZERO;
for i in 0..n {
sum += actual[i];
}
let mean = sum / S::from_f64(n as f64);
let mut num_sq = S::ZERO;
let mut den_sq = S::ZERO;
for i in 0..n {
let e = actual[i] - model_output[i];
num_sq += e * e;
let d = actual[i] - mean;
den_sq += d * d;
}
if den_sq == S::ZERO {
return S::ZERO;
}
let ratio = (num_sq / den_sq).sqrt();
S::from_f64(100.0) * (S::ONE - ratio)
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_arx1(n: usize) -> (heapless::Vec<f64, 4096>, heapless::Vec<f64, 4096>) {
let a_true = 0.7_f64;
let b_true = 0.3_f64;
let mut y: heapless::Vec<f64, 4096> = heapless::Vec::new();
let mut u: heapless::Vec<f64, 4096> = heapless::Vec::new();
let _ = y.push(0.0);
let _ = u.push(0.0);
for t in 1..n {
let ut = libm::sin(0.1 * t as f64) + libm::cos(0.07 * t as f64) * 0.5;
let yt = a_true * y[t - 1] + b_true * u[t - 1];
let _ = y.push(yt);
let _ = u.push(ut);
}
(y, u)
}
#[test]
fn batch_arx_identifies_first_order() {
let (y, u) = generate_arx1(800);
let model = ArxIdentifier::<f64, 1, 1, 2, 1>::fit(y.as_slice(), u.as_slice())
.expect("batch ARX fit should succeed");
let mut predicted: heapless::Vec<f64, 4096> = heapless::Vec::new();
for t in 1..y.len() {
let y_hist = [y[t - 1]];
let u_hist = [u[t - 1]];
let _ = predicted.push(model.predict(&y_hist, &u_hist));
}
let fp = fit_percent(predicted.as_slice(), &y.as_slice()[1..]);
assert!(
fp > 99.0,
"FIT% should exceed 99% for noiseless ARX-1, got {fp:.2}%"
);
}
#[test]
fn recursive_arx_identifies_first_order() {
let (y, u) = generate_arx1(2000);
let mut rls = RecursiveArx::<f64, 1, 1, 2, 1, 2>::new(0.99, 1e4);
for t in 1..y.len() {
rls.update(y[t], u[t]).expect("update should not error");
}
let model = rls.model();
let mut predicted: heapless::Vec<f64, 4096> = heapless::Vec::new();
for t in 1..y.len() {
let y_hist = [y[t - 1]];
let u_hist = [u[t - 1]];
let _ = predicted.push(model.predict(&y_hist, &u_hist));
}
let fp = fit_percent(predicted.as_slice(), &y.as_slice()[1..]);
assert!(
fp > 99.0,
"RLS ARX FIT% should exceed 99% for noiseless ARX-1, got {fp:.2}%"
);
}
#[test]
fn fit_percent_perfect_prediction() {
let y: [f64; 5] = [1.0, 2.0, 3.0, 4.0, 5.0];
let fp = fit_percent(&y, &y);
assert!(
(fp - 100.0).abs() < 1e-9,
"FIT% of perfect prediction should be 100%"
);
}
#[test]
fn fit_percent_zero_for_mean_prediction() {
let y: [f64; 5] = [1.0, 2.0, 3.0, 4.0, 5.0];
let mean = [3.0_f64; 5];
let fp = fit_percent(&mean, &y);
assert!(fp <= 1e-9, "FIT% of mean-level prediction should be ≤ 0");
}
#[test]
fn cholesky_solve_2x2() {
let mut a = [[4.0_f64, 2.0], [2.0, 3.0]];
let mut b = [8.0_f64, 7.0];
cholesky_solve_n::<f64, 2>(&mut a, &mut b).expect("should succeed");
assert!((b[0] - 1.25).abs() < 1e-10, "x[0]={}", b[0]);
assert!((b[1] - 1.5).abs() < 1e-10, "x[1]={}", b[1]);
}
#[test]
fn simulate_produces_finite_output() {
let model: ArxModel<f64, 1, 1> = ArxModel {
a: [-0.7],
b: [0.3],
};
let mut u_seq = [0.0_f64; 101];
for i in 1..=100 {
u_seq[i] = libm::sin(0.1 * i as f64);
}
let y0 = [0.0_f64];
let out: [f64; 100] = model
.simulate(&u_seq, &y0)
.expect("simulate should succeed");
for i in 0..100 {
assert!(out[i].is_finite(), "simulate output[{i}] is not finite");
}
}
}