use crate::error::FdarError;
use crate::matrix::FdMatrix;
use super::chi_squared::chi2_quantile;
use super::phase::SpmChart;
#[derive(Debug, Clone, PartialEq)]
pub struct AmewmaConfig {
pub lambda_min: f64,
pub lambda_max: f64,
pub lambda_init: f64,
pub eta: f64,
pub ncomp: usize,
pub alpha: f64,
pub error_clip: Option<f64>,
}
impl Default for AmewmaConfig {
fn default() -> Self {
Self {
lambda_min: 0.05,
lambda_max: 0.95,
lambda_init: 0.2,
eta: 0.1,
ncomp: 5,
alpha: 0.05,
error_clip: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct AmewmaMonitorResult {
pub smoothed_scores: FdMatrix,
pub t2_statistic: Vec<f64>,
pub lambda_t: Vec<f64>,
pub ucl: f64,
pub alarm: Vec<bool>,
}
#[must_use = "monitoring result should not be discarded"]
pub fn spm_amewma_monitor(
chart: &SpmChart,
sequential_data: &FdMatrix,
_argvals: &[f64],
config: &AmewmaConfig,
) -> Result<AmewmaMonitorResult, 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_min <= 0.0 || config.lambda_min > 1.0 {
return Err(FdarError::InvalidParameter {
parameter: "lambda_min",
message: format!("lambda_min must be in (0, 1], got {}", config.lambda_min),
});
}
if config.lambda_max <= 0.0 || config.lambda_max > 1.0 {
return Err(FdarError::InvalidParameter {
parameter: "lambda_max",
message: format!("lambda_max must be in (0, 1], got {}", config.lambda_max),
});
}
if config.lambda_min > config.lambda_max {
return Err(FdarError::InvalidParameter {
parameter: "lambda_min",
message: format!(
"lambda_min ({}) must be <= lambda_max ({})",
config.lambda_min, config.lambda_max
),
});
}
if config.lambda_init < config.lambda_min || config.lambda_init > config.lambda_max {
return Err(FdarError::InvalidParameter {
parameter: "lambda_init",
message: format!(
"lambda_init ({}) must be in [lambda_min, lambda_max] = [{}, {}]",
config.lambda_init, config.lambda_min, config.lambda_max
),
});
}
if config.eta <= 0.0 || config.eta > 1.0 {
return Err(FdarError::InvalidParameter {
parameter: "eta",
message: format!("eta must be in (0, 1], got {}", config.eta),
});
}
if let Some(clip) = config.error_clip {
if clip <= 0.0 {
return Err(FdarError::InvalidParameter {
parameter: "error_clip",
message: format!("error_clip must be positive, got {clip}"),
});
}
}
let ncomp = chart.eigenvalues.len().min(config.ncomp);
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 mut z = FdMatrix::zeros(n, actual_ncomp);
for i in 0..n {
for l in 0..actual_ncomp {
let ev = chart.eigenvalues[l];
z[(i, l)] = if ev > 0.0 {
scores[(i, l)] / ev.sqrt()
} else {
0.0
};
}
}
let mut smoothed = FdMatrix::zeros(n, actual_ncomp);
let mut t2_statistic = Vec::with_capacity(n);
let mut lambda_vals = Vec::with_capacity(n);
let mut q_prev = config.lambda_init;
let mut lambda_prev = config.lambda_init;
let mut sum_lambda_sq_prod = 0.0_f64;
for i in 0..n {
let lambda_cur = lambda_prev;
for l in 0..actual_ncomp {
let s_prev = if i > 0 { smoothed[(i - 1, l)] } else { 0.0 };
smoothed[(i, l)] = lambda_cur * z[(i, l)] + (1.0 - lambda_cur) * s_prev;
}
sum_lambda_sq_prod =
lambda_cur * lambda_cur + (1.0 - lambda_cur).powi(2) * sum_lambda_sq_prod;
let var_factor = sum_lambda_sq_prod.max(1e-15);
let mut t2 = 0.0;
for l in 0..actual_ncomp {
t2 += smoothed[(i, l)] * smoothed[(i, l)] / var_factor;
}
t2_statistic.push(t2);
lambda_vals.push(lambda_cur);
let mut e_sq_sum = 0.0;
for l in 0..actual_ncomp {
e_sq_sum += z[(i, l)] * z[(i, l)];
}
let e_sq_avg = e_sq_sum / actual_ncomp as f64;
let e_sq_avg = if let Some(clip) = config.error_clip {
e_sq_avg.min(clip)
} else {
e_sq_avg
};
let q_t = config.eta * e_sq_avg + (1.0 - config.eta) * q_prev;
lambda_prev = config.lambda_min.max(config.lambda_max.min(q_t));
q_prev = q_t.clamp(0.0, config.lambda_max * 2.0);
}
let ucl = chi2_quantile(1.0 - config.alpha, actual_ncomp);
let alarm: Vec<bool> = t2_statistic.iter().map(|&v| v > ucl).collect();
Ok(AmewmaMonitorResult {
smoothed_scores: smoothed,
t2_statistic,
lambda_t: lambda_vals,
ucl,
alarm,
})
}