use crate::error::FdarError;
use crate::matrix::FdMatrix;
use super::chi_squared::chi2_quantile;
use super::phase::SpmChart;
use super::stats::{hotelling_t2, spe_univariate};
#[derive(Debug, Clone, PartialEq)]
pub struct EwmaConfig {
pub lambda: f64,
pub ncomp: usize,
pub alpha: f64,
pub exact_covariance: bool,
}
impl Default for EwmaConfig {
fn default() -> Self {
Self {
lambda: 0.2,
ncomp: 5,
alpha: 0.05,
exact_covariance: false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct EwmaMonitorResult {
pub smoothed_scores: FdMatrix,
pub t2: Vec<f64>,
pub spe: Vec<f64>,
pub t2_limit: f64,
pub spe_limit: f64,
pub t2_alarm: Vec<bool>,
pub spe_alarm: Vec<bool>,
}
pub fn ewma_scores(scores: &FdMatrix, lambda: f64) -> Result<FdMatrix, FdarError> {
if lambda <= 0.0 || lambda > 1.0 {
return Err(FdarError::InvalidParameter {
parameter: "lambda",
message: format!("lambda must be in (0, 1], got {lambda}"),
});
}
let (n, ncomp) = scores.shape();
let mut smoothed = FdMatrix::zeros(n, ncomp);
if n > 0 {
for k in 0..ncomp {
smoothed[(0, k)] = lambda * scores[(0, k)];
}
}
for i in 1..n {
for k in 0..ncomp {
smoothed[(i, k)] = lambda * scores[(i, k)] + (1.0 - lambda) * smoothed[(i - 1, k)];
}
}
Ok(smoothed)
}
#[must_use = "monitoring result should not be discarded"]
pub fn spm_ewma_monitor(
chart: &SpmChart,
sequential_data: &FdMatrix,
argvals: &[f64],
config: &EwmaConfig,
) -> Result<EwmaMonitorResult, FdarError> {
let m = chart.fpca.mean.len();
if sequential_data.ncols() != m {
return Err(FdarError::InvalidDimension {
parameter: "sequential_data",
expected: format!("{m} columns"),
actual: format!("{} columns", sequential_data.ncols()),
});
}
let ncomp = chart.eigenvalues.len().min(config.ncomp);
if ncomp == 0 {
return Err(FdarError::InvalidParameter {
parameter: "ncomp",
message: "ncomp must be at least 1".to_string(),
});
}
let scores = chart.fpca.project(sequential_data)?;
let scores = if scores.ncols() > ncomp {
let n = scores.nrows();
let mut trunc = FdMatrix::zeros(n, ncomp);
for i in 0..n {
for k in 0..ncomp {
trunc[(i, k)] = scores[(i, k)];
}
}
trunc
} else {
scores
};
let actual_ncomp = scores.ncols();
let smoothed = ewma_scores(&scores, config.lambda)?;
let adj_eigenvalues: Vec<f64> = chart
.eigenvalues
.iter()
.take(actual_ncomp)
.map(|&ev| ev * config.lambda / (2.0 - config.lambda))
.collect();
let n = smoothed.nrows();
let t2 = if config.exact_covariance {
let lambda = config.lambda;
let one_minus_lambda = 1.0 - lambda;
let lambda_factor = lambda / (2.0 - lambda);
let mut stats = Vec::with_capacity(n);
for i in 0..n {
let t = (i + 1) as f64;
let time_factor = 1.0 - one_minus_lambda.powf(2.0 * t);
let mut t2_val = 0.0;
for l in 0..actual_ncomp {
let adj_ev = lambda_factor * time_factor * chart.eigenvalues[l];
let z = smoothed[(i, l)];
t2_val += z * z / adj_ev;
}
stats.push(t2_val);
}
stats
} else {
hotelling_t2(&smoothed, &adj_eigenvalues)?
};
let centered = {
let n = sequential_data.nrows();
let mut c = FdMatrix::zeros(n, m);
for i in 0..n {
for j in 0..m {
c[(i, j)] = sequential_data[(i, j)] - chart.fpca.mean[j];
}
}
c
};
let recon_centered = {
let n = scores.nrows();
let mut r = FdMatrix::zeros(n, m);
for i in 0..n {
for j in 0..m {
let mut val = 0.0;
for k in 0..actual_ncomp {
val += scores[(i, k)] * chart.fpca.rotation[(j, k)];
}
r[(i, j)] = val;
}
}
r
};
let spe = spe_univariate(¢ered, &recon_centered, argvals)?;
let t2_limit = chi2_quantile(1.0 - config.alpha, actual_ncomp);
let spe_limit = chart.spe_limit.ucl;
let t2_alarm: Vec<bool> = t2.iter().map(|&v| v > t2_limit).collect();
let spe_alarm: Vec<bool> = spe.iter().map(|&v| v > spe_limit).collect();
Ok(EwmaMonitorResult {
smoothed_scores: smoothed,
t2,
spe,
t2_limit,
spe_limit,
t2_alarm,
spe_alarm,
})
}