use crate::error::FdarError;
use crate::matrix::FdMatrix;
use super::chi_squared::chi2_quantile;
use super::ewma::ewma_scores;
use super::phase::{center_data, centered_reconstruct, SpmChart};
use super::stats::{hotelling_t2, spe_univariate};
#[derive(Debug, Clone, PartialEq)]
pub struct MewmaConfig {
pub lambda: f64,
pub ncomp: usize,
pub alpha: f64,
pub asymptotic: bool,
}
impl Default for MewmaConfig {
fn default() -> Self {
Self {
lambda: 0.2,
ncomp: 5,
alpha: 0.05,
asymptotic: true,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct MewmaMonitorResult {
pub smoothed_scores: FdMatrix,
pub mewma_statistic: Vec<f64>,
pub ucl: f64,
pub alarm: Vec<bool>,
pub spe: Vec<f64>,
pub spe_limit: f64,
pub spe_alarm: Vec<bool>,
}
#[must_use = "monitoring result should not be discarded"]
pub fn spm_mewma_monitor(
chart: &SpmChart,
sequential_data: &FdMatrix,
argvals: &[f64],
config: &MewmaConfig,
) -> Result<MewmaMonitorResult, 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()),
});
}
if config.lambda <= 0.0 || config.lambda > 1.0 {
return Err(FdarError::InvalidParameter {
parameter: "lambda",
message: format!("lambda must be in (0, 1], got {}", config.lambda),
});
}
let ncomp = chart.eigenvalues.len().min(config.ncomp);
for l in 0..ncomp {
if chart.eigenvalues[l] <= 0.0 {
return Err(FdarError::InvalidParameter {
parameter: "eigenvalues",
message: format!(
"eigenvalue[{l}] = {} must be positive for MEWMA covariance",
chart.eigenvalues[l]
),
});
}
}
let n = sequential_data.nrows();
let all_scores = chart.fpca.project(sequential_data)?;
let scores = if all_scores.ncols() > ncomp {
let mut trunc = FdMatrix::zeros(n, ncomp);
for i in 0..n {
for k in 0..ncomp {
trunc[(i, k)] = all_scores[(i, k)];
}
}
trunc
} else {
all_scores
};
let actual_ncomp = scores.ncols();
let smoothed = ewma_scores(&scores, config.lambda)?;
let mewma_statistic = if config.asymptotic {
let adj_eigenvalues: Vec<f64> = chart
.eigenvalues
.iter()
.take(actual_ncomp)
.map(|&ev| ev * config.lambda / (2.0 - config.lambda))
.collect();
hotelling_t2(&smoothed, &adj_eigenvalues)?
} else {
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 = 0.0;
for l in 0..actual_ncomp {
let sigma_z_inv = 1.0 / (lambda_factor * time_factor * chart.eigenvalues[l]);
let z = smoothed[(i, l)];
t2 += z * z * sigma_z_inv;
}
stats.push(t2);
}
stats
};
let centered = center_data(sequential_data, &chart.fpca.mean);
let recon_centered = centered_reconstruct(&chart.fpca, &scores, actual_ncomp);
let spe = spe_univariate(¢ered, &recon_centered, argvals)?;
let ucl = chi2_quantile(1.0 - config.alpha, actual_ncomp);
let spe_limit = chart.spe_limit.ucl;
let alarm: Vec<bool> = mewma_statistic.iter().map(|&v| v > ucl).collect();
let spe_alarm: Vec<bool> = spe.iter().map(|&v| v > spe_limit).collect();
Ok(MewmaMonitorResult {
smoothed_scores: smoothed,
mewma_statistic,
ucl,
alarm,
spe,
spe_limit,
spe_alarm,
})
}