use crate::error::FdarError;
use crate::function_on_scalar::{fosr, FosrResult};
use crate::matrix::FdMatrix;
use crate::regression::{fdata_to_pc_1d, FpcaResult};
use crate::spm::control::{t2_control_limit, ControlLimit};
use crate::spm::stats::hotelling_t2;
#[derive(Debug, Clone, PartialEq)]
pub struct ProfileMonitorConfig {
pub fosr_lambda: f64,
pub ncomp: usize,
pub alpha: f64,
pub window_size: usize,
pub step_size: usize,
}
impl Default for ProfileMonitorConfig {
fn default() -> Self {
Self {
fosr_lambda: 1e-4,
ncomp: 3,
alpha: 0.05,
window_size: 20,
step_size: 1,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ProfileChart {
pub reference_fosr: FosrResult,
pub beta_fpca: FpcaResult,
pub eigenvalues: Vec<f64>,
pub t2_limit: ControlLimit,
pub lag1_autocorrelation: f64,
pub effective_n_windows: f64,
pub config: ProfileMonitorConfig,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ProfileMonitorResult {
pub betas: FdMatrix,
pub t2: Vec<f64>,
pub t2_alarm: Vec<bool>,
pub beta_scores: FdMatrix,
}
#[must_use = "expensive computation whose result should not be discarded"]
pub fn profile_phase1(
y_curves: &FdMatrix,
predictors: &FdMatrix,
argvals: &[f64],
config: &ProfileMonitorConfig,
) -> Result<ProfileChart, FdarError> {
let (n, m) = y_curves.shape();
if predictors.nrows() != n {
return Err(FdarError::InvalidDimension {
parameter: "predictors",
expected: format!("{n} rows"),
actual: format!("{} rows", predictors.nrows()),
});
}
if argvals.len() != m {
return Err(FdarError::InvalidDimension {
parameter: "argvals",
expected: format!("{m}"),
actual: format!("{}", argvals.len()),
});
}
if config.step_size == 0 {
return Err(FdarError::InvalidParameter {
parameter: "step_size",
message: "step_size must be at least 1".to_string(),
});
}
if config.window_size < 3 {
return Err(FdarError::InvalidParameter {
parameter: "window_size",
message: format!("window_size must be >= 3, got {}", config.window_size),
});
}
if config.window_size > n {
return Err(FdarError::InvalidParameter {
parameter: "window_size",
message: format!(
"window_size ({}) exceeds data size ({n})",
config.window_size
),
});
}
let expected_n_windows = if n >= config.window_size {
(n - config.window_size) / config.step_size + 1
} else {
0
};
if expected_n_windows < 4 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "enough data for at least 4 windows".to_string(),
actual: format!(
"{expected_n_windows} windows (n={n}, window_size={}, step_size={})",
config.window_size, config.step_size
),
});
}
let reference_fosr = fosr(y_curves, predictors, config.fosr_lambda)?;
let beta_vecs = rolling_betas(y_curves, predictors, config)?;
let n_windows = beta_vecs.nrows();
if n_windows < 4 {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: "enough data for at least 4 windows".to_string(),
actual: format!("{n_windows} windows"),
});
}
let ncomp = config.ncomp.min(n_windows - 1).min(beta_vecs.ncols());
let beta_m = beta_vecs.ncols();
let beta_argvals: Vec<f64> = (0..beta_m)
.map(|j| j as f64 / (beta_m - 1).max(1) as f64)
.collect();
let beta_fpca = fdata_to_pc_1d(&beta_vecs, ncomp, &beta_argvals)?;
let actual_ncomp = beta_fpca.scores.ncols();
let eigenvalues: Vec<f64> = beta_fpca
.singular_values
.iter()
.take(actual_ncomp)
.map(|&sv| sv * sv / (n_windows as f64 - 1.0))
.collect();
let phase1_t2 = hotelling_t2(&beta_fpca.scores, &eigenvalues)?;
let lag1_autocorrelation = if phase1_t2.len() > 2 {
let n_t2 = phase1_t2.len();
let mean_t2 = phase1_t2.iter().sum::<f64>() / n_t2 as f64;
let var_t2: f64 = phase1_t2
.iter()
.map(|&v| (v - mean_t2).powi(2))
.sum::<f64>()
/ (n_t2 - 1) as f64;
if var_t2 > 0.0 {
let cov1: f64 = (0..n_t2 - 1)
.map(|i| (phase1_t2[i] - mean_t2) * (phase1_t2[i + 1] - mean_t2))
.sum::<f64>()
/ (n_t2 - 1) as f64;
(cov1 / var_t2).clamp(-1.0, 1.0)
} else {
0.0
}
} else {
0.0
};
let effective_n_windows = if lag1_autocorrelation.abs() > 0.01 {
let bartlett_factor = (1.0 + 2.0 * lag1_autocorrelation.abs()).max(1.0);
(n_windows as f64 / bartlett_factor).max(2.0)
} else {
n_windows as f64
};
let t2_limit = t2_control_limit(actual_ncomp, config.alpha)?;
Ok(ProfileChart {
reference_fosr,
beta_fpca,
eigenvalues,
t2_limit,
lag1_autocorrelation,
effective_n_windows,
config: config.clone(),
})
}
#[must_use = "monitoring result should not be discarded"]
pub fn profile_monitor(
chart: &ProfileChart,
new_y: &FdMatrix,
new_predictors: &FdMatrix,
_argvals: &[f64],
config: &ProfileMonitorConfig,
) -> Result<ProfileMonitorResult, FdarError> {
if config.step_size == 0 {
return Err(FdarError::InvalidParameter {
parameter: "step_size",
message: "step_size must be at least 1".to_string(),
});
}
let n = new_y.nrows();
if new_predictors.nrows() != n {
return Err(FdarError::InvalidDimension {
parameter: "new_predictors",
expected: format!("{n} rows"),
actual: format!("{} rows", new_predictors.nrows()),
});
}
let expected_m = chart.reference_fosr.beta.ncols();
if new_y.ncols() != expected_m {
return Err(FdarError::InvalidDimension {
parameter: "new_y",
expected: format!("{expected_m} columns (grid points)"),
actual: format!("{} columns", new_y.ncols()),
});
}
let beta_vecs = rolling_betas(new_y, new_predictors, config)?;
let beta_scores = chart.beta_fpca.project(&beta_vecs)?;
let t2 = hotelling_t2(&beta_scores, &chart.eigenvalues)?;
let t2_alarm: Vec<bool> = t2.iter().map(|&v| v > chart.t2_limit.ucl).collect();
Ok(ProfileMonitorResult {
betas: beta_vecs,
t2,
t2_alarm,
beta_scores,
})
}
fn rolling_betas(
y_curves: &FdMatrix,
predictors: &FdMatrix,
config: &ProfileMonitorConfig,
) -> Result<FdMatrix, FdarError> {
let n = y_curves.nrows();
let m = y_curves.ncols();
let p = predictors.ncols();
if config.window_size < p + 2 {
return Err(FdarError::InvalidParameter {
parameter: "window_size",
message: format!(
"window_size ({}) must be >= p + 2 = {} for FOSR fitting",
config.window_size,
p + 2
),
});
}
let mut windows = Vec::new();
let mut start = 0;
while start + config.window_size <= n {
windows.push(start);
start += config.step_size;
}
if windows.is_empty() {
return Err(FdarError::InvalidDimension {
parameter: "data",
expected: format!(
"at least {} observations for one window",
config.window_size
),
actual: format!("{n} observations"),
});
}
let beta_len = p * m;
let n_windows = windows.len();
let mut beta_mat = FdMatrix::zeros(n_windows, beta_len);
for (w_idx, &win_start) in windows.iter().enumerate() {
let mut y_window = FdMatrix::zeros(config.window_size, m);
let mut pred_window = FdMatrix::zeros(config.window_size, p);
for i in 0..config.window_size {
for j in 0..m {
y_window[(i, j)] = y_curves[(win_start + i, j)];
}
for j in 0..p {
pred_window[(i, j)] = predictors[(win_start + i, j)];
}
}
let fosr_result = fosr(&y_window, &pred_window, config.fosr_lambda)?;
for j in 0..p {
for t in 0..m {
beta_mat[(w_idx, j * m + t)] = fosr_result.beta[(j, t)];
}
}
}
Ok(beta_mat)
}