use crate::error::FdarError;
use crate::matrix::FdMatrix;
use super::phase::{center_data, centered_reconstruct, SpmChart};
use super::stats::spe_univariate;
#[derive(Debug, Clone, PartialEq)]
pub struct CusumConfig {
pub k: f64,
pub h: f64,
pub ncomp: usize,
pub alpha: f64,
pub multivariate: bool,
pub restart: bool,
}
impl Default for CusumConfig {
fn default() -> Self {
Self {
k: 0.5,
h: 5.0,
ncomp: 5,
alpha: 0.05,
multivariate: true,
restart: false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct CusumMonitorResult {
pub cusum_statistic: Vec<f64>,
pub ucl: f64,
pub alarm: Vec<bool>,
pub scores: FdMatrix,
pub cusum_plus: Option<FdMatrix>,
pub cusum_minus: Option<FdMatrix>,
pub spe: Vec<f64>,
pub spe_limit: f64,
pub spe_alarm: Vec<bool>,
}
fn validate_config(config: &CusumConfig) -> Result<(), FdarError> {
if config.k <= 0.0 {
return Err(FdarError::InvalidParameter {
parameter: "k",
message: format!("k must be positive, got {}", config.k),
});
}
if config.h <= 0.0 {
return Err(FdarError::InvalidParameter {
parameter: "h",
message: format!("h must be positive, got {}", config.h),
});
}
Ok(())
}
fn validate_dimensions(chart: &SpmChart, sequential_data: &FdMatrix) -> Result<(), 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()),
});
}
Ok(())
}
fn project_and_standardize(
chart: &SpmChart,
sequential_data: &FdMatrix,
ncomp_requested: usize,
) -> Result<(FdMatrix, usize), FdarError> {
let all_scores = chart.fpca.project(sequential_data)?;
let ncomp = chart.eigenvalues.len().min(ncomp_requested);
let n = sequential_data.nrows();
let mut z = FdMatrix::zeros(n, ncomp);
for i in 0..n {
for l in 0..ncomp {
z[(i, l)] = all_scores[(i, l)] / chart.eigenvalues[l].sqrt();
}
}
Ok((z, ncomp))
}
fn mcusum_core(z: &FdMatrix, ncomp: usize, k: f64, h: f64, restart: bool) -> (Vec<f64>, Vec<bool>) {
let n = z.nrows();
let mut s = vec![0.0; ncomp];
let mut cusum_statistic = Vec::with_capacity(n);
let mut alarm = Vec::with_capacity(n);
for i in 0..n {
for l in 0..ncomp {
s[l] += z[(i, l)];
}
let c: f64 = s.iter().map(|&v| v * v).sum::<f64>().sqrt();
if c > k {
let scale = (c - k) / c;
for l in 0..ncomp {
s[l] *= scale;
}
} else {
s.fill(0.0);
}
let stat = s.iter().map(|&v| v * v).sum::<f64>().sqrt();
let is_alarm = stat > h;
cusum_statistic.push(stat);
alarm.push(is_alarm);
if restart && is_alarm {
s.fill(0.0);
}
}
(cusum_statistic, alarm)
}
fn univariate_cusum_core(
z: &FdMatrix,
ncomp: usize,
k: f64,
h: f64,
restart: bool,
) -> (Vec<f64>, Vec<bool>, FdMatrix, FdMatrix) {
let n = z.nrows();
let mut cp = vec![0.0; ncomp];
let mut cm = vec![0.0; ncomp];
let mut cusum_plus_mat = FdMatrix::zeros(n, ncomp);
let mut cusum_minus_mat = FdMatrix::zeros(n, ncomp);
let mut cusum_statistic = Vec::with_capacity(n);
let mut alarm = Vec::with_capacity(n);
for i in 0..n {
for l in 0..ncomp {
cp[l] = (cp[l] + z[(i, l)] - k).max(0.0);
cm[l] = (cm[l] - z[(i, l)] - k).max(0.0);
cusum_plus_mat[(i, l)] = cp[l];
cusum_minus_mat[(i, l)] = cm[l];
}
let mut max_val = 0.0_f64;
for l in 0..ncomp {
max_val = max_val.max(cp[l]).max(cm[l]);
}
let is_alarm = max_val > h;
cusum_statistic.push(max_val);
alarm.push(is_alarm);
if restart && is_alarm {
for l in 0..ncomp {
cp[l] = 0.0;
cm[l] = 0.0;
}
}
}
(cusum_statistic, alarm, cusum_plus_mat, cusum_minus_mat)
}
#[must_use = "monitoring result should not be discarded"]
pub fn spm_cusum_monitor(
chart: &SpmChart,
sequential_data: &FdMatrix,
argvals: &[f64],
config: &CusumConfig,
) -> Result<CusumMonitorResult, FdarError> {
validate_config(config)?;
validate_dimensions(chart, sequential_data)?;
let (z, ncomp) = project_and_standardize(chart, sequential_data, config.ncomp)?;
let n = sequential_data.nrows();
let mut raw_scores = FdMatrix::zeros(n, ncomp);
for i in 0..n {
for l in 0..ncomp {
raw_scores[(i, l)] = z[(i, l)] * chart.eigenvalues[l].sqrt();
}
}
let centered = center_data(sequential_data, &chart.fpca.mean);
let recon_centered = centered_reconstruct(&chart.fpca, &raw_scores, ncomp);
let spe = spe_univariate(¢ered, &recon_centered, argvals)?;
let spe_limit = chart.spe_limit.ucl;
let spe_alarm: Vec<bool> = spe.iter().map(|&v| v > spe_limit).collect();
if config.multivariate {
let (cusum_statistic, alarm) = mcusum_core(&z, ncomp, config.k, config.h, config.restart);
Ok(CusumMonitorResult {
cusum_statistic,
ucl: config.h,
alarm,
scores: z,
cusum_plus: None,
cusum_minus: None,
spe,
spe_limit,
spe_alarm,
})
} else {
let (cusum_statistic, alarm, cusum_plus_mat, cusum_minus_mat) =
univariate_cusum_core(&z, ncomp, config.k, config.h, config.restart);
Ok(CusumMonitorResult {
cusum_statistic,
ucl: config.h,
alarm,
scores: z,
cusum_plus: Some(cusum_plus_mat),
cusum_minus: Some(cusum_minus_mat),
spe,
spe_limit,
spe_alarm,
})
}
}
#[must_use = "monitoring result should not be discarded"]
pub fn spm_cusum_monitor_with_restart(
chart: &SpmChart,
sequential_data: &FdMatrix,
argvals: &[f64],
config: &CusumConfig,
) -> Result<CusumMonitorResult, FdarError> {
validate_config(config)?;
validate_dimensions(chart, sequential_data)?;
let (z, ncomp) = project_and_standardize(chart, sequential_data, config.ncomp)?;
let n = sequential_data.nrows();
let mut raw_scores = FdMatrix::zeros(n, ncomp);
for i in 0..n {
for l in 0..ncomp {
raw_scores[(i, l)] = z[(i, l)] * chart.eigenvalues[l].sqrt();
}
}
let centered = center_data(sequential_data, &chart.fpca.mean);
let recon_centered = centered_reconstruct(&chart.fpca, &raw_scores, ncomp);
let spe = spe_univariate(¢ered, &recon_centered, argvals)?;
let spe_limit = chart.spe_limit.ucl;
let spe_alarm: Vec<bool> = spe.iter().map(|&v| v > spe_limit).collect();
if config.multivariate {
let (cusum_statistic, alarm) = mcusum_core(&z, ncomp, config.k, config.h, true);
Ok(CusumMonitorResult {
cusum_statistic,
ucl: config.h,
alarm,
scores: z,
cusum_plus: None,
cusum_minus: None,
spe,
spe_limit,
spe_alarm,
})
} else {
let (cusum_statistic, alarm, cusum_plus_mat, cusum_minus_mat) =
univariate_cusum_core(&z, ncomp, config.k, config.h, true);
Ok(CusumMonitorResult {
cusum_statistic,
ucl: config.h,
alarm,
scores: z,
cusum_plus: Some(cusum_plus_mat),
cusum_minus: Some(cusum_minus_mat),
spe,
spe_limit,
spe_alarm,
})
}
}