#![allow(clippy::needless_range_loop, clippy::doc_overindented_list_items)]
use crate::core::scalar::ControlScalar;
pub fn fit_percent<S: ControlScalar>(predicted: &[S], actual: &[S]) -> S {
let n = predicted.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] - predicted[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)
}
pub fn autocorrelation<S: ControlScalar, const L: usize>(signal: &[S]) -> [S; L] {
let n = signal.len();
let mut result = [S::ZERO; L];
if n == 0 {
return result;
}
let mut mean = S::ZERO;
for i in 0..n {
mean += signal[i];
}
mean = mean / S::from_f64(n as f64);
let mut r0 = S::ZERO;
for i in 0..n {
let d = signal[i] - mean;
r0 += d * d;
}
if r0 == S::ZERO {
return result;
}
for lag in 0..L {
let mut r_lag = S::ZERO;
let max_t = n.saturating_sub(lag);
for t in 0..max_t {
r_lag += (signal[t] - mean) * (signal[t + lag] - mean);
}
result[lag] = r_lag / r0;
}
result
}
#[derive(Debug, Clone, Copy)]
pub struct ResidualStats<S: ControlScalar> {
pub mean: S,
pub variance: S,
pub autocorr_lag1: S,
pub is_white: bool,
}
pub fn residual_analysis<S: ControlScalar>(
residuals: &[S],
max_lag: usize,
significance: S,
) -> ResidualStats<S> {
let n = residuals.len();
if n == 0 {
return ResidualStats {
mean: S::ZERO,
variance: S::ZERO,
autocorr_lag1: S::ZERO,
is_white: true,
};
}
let mut sum = S::ZERO;
for i in 0..n {
sum += residuals[i];
}
let mean = sum / S::from_f64(n as f64);
let mut var = S::ZERO;
for i in 0..n {
let d = residuals[i] - mean;
var += d * d;
}
let variance = if n > 1 {
var / S::from_f64((n - 1) as f64)
} else {
S::ZERO
};
let mut r0 = S::ZERO;
let mut r1 = S::ZERO;
for i in 0..n {
let d = residuals[i] - mean;
r0 += d * d;
}
for i in 0..(n.saturating_sub(1)) {
r1 += (residuals[i] - mean) * (residuals[i + 1] - mean);
}
let autocorr_lag1 = if r0 == S::ZERO { S::ZERO } else { r1 / r0 };
let is_white = whiteness_test(residuals, max_lag, significance);
ResidualStats {
mean,
variance,
autocorr_lag1,
is_white,
}
}
pub fn whiteness_test<S: ControlScalar>(residuals: &[S], max_lag: usize, significance: S) -> bool {
let n = residuals.len();
if n == 0 || max_lag == 0 {
return true;
}
let mut mean = S::ZERO;
for i in 0..n {
mean += residuals[i];
}
mean = mean / S::from_f64(n as f64);
let mut r0 = S::ZERO;
for i in 0..n {
let d = residuals[i] - mean;
r0 += d * d;
}
if r0 == S::ZERO {
return true;
}
let nf = S::from_f64(n as f64);
let mut q = S::ZERO;
for k in 1..=max_lag {
let max_t = n.saturating_sub(k);
let mut rk = S::ZERO;
for t in 0..max_t {
rk += (residuals[t] - mean) * (residuals[t + k] - mean);
}
let rk_norm = rk / r0;
let denom = nf - S::from_f64(k as f64);
if denom > S::ZERO {
q += rk_norm * rk_norm / denom;
}
}
q = q * nf * (nf + S::ONE);
let m = S::from_f64(max_lag as f64);
let alpha_f64 = significance.to_f64().clamp(1e-6, 0.5);
let z_alpha = normal_quantile_approx(S::from_f64(1.0 - alpha_f64));
let two_over_9m = S::from_f64(2.0) / (S::from_f64(9.0) * m);
let inner = S::ONE - two_over_9m + z_alpha * two_over_9m.sqrt();
let chi2_crit = if inner > S::ZERO {
m * inner * inner * inner
} else {
S::ZERO
};
q <= chi2_crit
}
fn normal_quantile_approx<S: ControlScalar>(p: S) -> S {
let p_f64 = p.to_f64().clamp(1e-9, 1.0 - 1e-9);
let z = if p_f64 <= 0.5 {
let t = (-2.0 * p_f64.ln()).sqrt();
let c0 = 2.515517_f64;
let c1 = 0.802853_f64;
let c2 = 0.010328_f64;
let d1 = 1.432788_f64;
let d2 = 0.189269_f64;
let d3 = 0.001308_f64;
-(t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t))
} else {
let q_val = 1.0 - p_f64;
let t = (-2.0 * q_val.ln()).sqrt();
let c0 = 2.515517_f64;
let c1 = 0.802853_f64;
let c2 = 0.010328_f64;
let d1 = 1.432788_f64;
let d2 = 0.189269_f64;
let d3 = 0.001308_f64;
t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t)
};
S::from_f64(z)
}
pub fn cross_correlation<S: ControlScalar, const L: usize>(
residuals: &[S],
inputs: &[S],
) -> [S; L] {
let n = residuals.len().min(inputs.len());
let mut result = [S::ZERO; L];
if n == 0 {
return result;
}
let mut mean_e = S::ZERO;
let mut mean_u = S::ZERO;
for i in 0..n {
mean_e += residuals[i];
mean_u += inputs[i];
}
mean_e = mean_e / S::from_f64(n as f64);
mean_u = mean_u / S::from_f64(n as f64);
let mut var_e = S::ZERO;
let mut var_u = S::ZERO;
for i in 0..n {
let de = residuals[i] - mean_e;
let du = inputs[i] - mean_u;
var_e += de * de;
var_u += du * du;
}
let norm = (var_e * var_u).sqrt();
if norm == S::ZERO {
return result;
}
for lag in 0..L {
let max_t = n.saturating_sub(lag);
let mut cross = S::ZERO;
for t in lag..n {
let t_u = t - lag;
if t_u < n {
cross += (residuals[t] - mean_e) * (inputs[t_u] - mean_u);
}
}
let mut c = S::ZERO;
for t in lag..max_t + lag {
if t < n && t >= lag {
c += (residuals[t] - mean_e) * (inputs[t - lag] - mean_u);
}
}
result[lag] = c / norm;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn white_noise_autocorrelation_near_zero() {
let mut lcg: u64 = 42;
let mut noise: heapless::Vec<f64, 1024> = heapless::Vec::new();
for _ in 0..1000 {
lcg = lcg
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let v = (lcg >> 33) as f64 / (u64::MAX >> 33) as f64 - 0.5;
let _ = noise.push(v);
}
let acf: [f64; 20] = autocorrelation(noise.as_slice());
assert!((acf[0] - 1.0).abs() < 1e-10, "ACF[0] = {}", acf[0]);
let bound = 3.0 / (1000.0_f64).sqrt();
for lag in 1..20 {
assert!(
acf[lag].abs() < bound,
"ACF[{lag}] = {:.4} exceeds bound {bound:.4}",
acf[lag]
);
}
}
#[test]
fn sine_autocorrelation_is_cosine() {
let n = 1000_usize;
let freq = 0.05; let mut sine: heapless::Vec<f64, 1024> = heapless::Vec::new();
for t in 0..n {
let _ = sine.push(libm::sin(2.0 * core::f64::consts::PI * freq * t as f64));
}
let acf: [f64; 50] = autocorrelation(sine.as_slice());
for lag in 0..50 {
let expected = libm::cos(2.0 * core::f64::consts::PI * freq * lag as f64);
assert!(
(acf[lag] - expected).abs() < 0.05,
"ACF[{lag}] = {:.4}, expected {expected:.4}",
acf[lag]
);
}
}
#[test]
fn whiteness_test_passes_for_white_noise() {
let mut lcg: u64 = 99;
let mut noise: heapless::Vec<f64, 2048> = heapless::Vec::new();
for _ in 0..2000 {
lcg = lcg
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let v = (lcg >> 33) as f64 / (u64::MAX >> 33) as f64 - 0.5;
let _ = noise.push(v);
}
let white = whiteness_test(noise.as_slice(), 20, 0.05_f64);
assert!(white, "Whiteness test should pass for white noise");
}
#[test]
fn whiteness_test_fails_for_highly_correlated_signal() {
let n = 2000_usize;
let mut sig: heapless::Vec<f64, 2048> = heapless::Vec::new();
for t in 0..n {
let _ = sig.push(libm::sin(2.0 * core::f64::consts::PI * 0.05 * t as f64));
}
let white = whiteness_test(sig.as_slice(), 20, 0.05_f64);
assert!(!white, "Whiteness test should fail for a sine wave signal");
}
#[test]
fn fit_percent_perfect() {
let y: [f64; 10] = [1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0];
let fp = fit_percent(&y, &y);
assert!((fp - 100.0).abs() < 1e-9, "FIT% = {fp}");
}
#[test]
fn fit_percent_mean_prediction_is_zero() {
let y: [f64; 6] = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let mean_pred: [f64; 6] = [3.5; 6];
let fp = fit_percent(&mean_pred, &y);
assert!(fp.abs() < 1e-9, "FIT% of mean prediction = {fp}");
}
#[test]
fn cross_correlation_independent_near_zero() {
let mut lcg: u64 = 7;
let mut sig1: heapless::Vec<f64, 1024> = heapless::Vec::new();
let mut sig2: heapless::Vec<f64, 1024> = heapless::Vec::new();
for _ in 0..1000 {
lcg = lcg
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let v1 = (lcg >> 33) as f64 / (u64::MAX >> 33) as f64 - 0.5;
lcg = lcg
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let v2 = (lcg >> 33) as f64 / (u64::MAX >> 33) as f64 - 0.5;
let _ = sig1.push(v1);
let _ = sig2.push(v2);
}
let cc: [f64; 10] = cross_correlation(sig1.as_slice(), sig2.as_slice());
let bound = 4.0 / (1000.0_f64).sqrt();
for lag in 0..10 {
assert!(
cc[lag].abs() < bound,
"cross_corr[{lag}] = {:.4} exceeds bound {bound:.4}",
cc[lag]
);
}
}
#[test]
fn residual_analysis_known_sequence() {
let seq = [2.0_f64; 100];
let stats = residual_analysis(&seq, 5, 0.05);
assert!((stats.mean - 2.0).abs() < 1e-10);
assert!(stats.variance.abs() < 1e-10);
}
}