1use crate::error::FdarError;
12use crate::function_on_scalar::{fosr, predict_fosr, FosrResult};
13use crate::matrix::FdMatrix;
14use crate::regression::{fdata_to_pc_1d, FpcaResult};
15
16use super::control::{spe_control_limit, t2_control_limit, ControlLimit};
17use super::stats::{hotelling_t2, spe_univariate};
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct FrccConfig {
22 pub ncomp: usize,
24 pub fosr_lambda: f64,
26 pub alpha: f64,
28 pub tuning_fraction: f64,
30 pub seed: u64,
32}
33
34impl Default for FrccConfig {
35 fn default() -> Self {
36 Self {
37 ncomp: 5,
38 fosr_lambda: 1e-4,
39 alpha: 0.05,
40 tuning_fraction: 0.5,
41 seed: 42,
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq)]
48#[non_exhaustive]
49pub struct FrccChart {
50 pub fosr: FosrResult,
52 pub residual_fpca: FpcaResult,
54 pub eigenvalues: Vec<f64>,
56 pub t2_limit: ControlLimit,
58 pub spe_limit: ControlLimit,
60 pub config: FrccConfig,
62}
63
64#[derive(Debug, Clone, PartialEq)]
66#[non_exhaustive]
67pub struct FrccMonitorResult {
68 pub t2: Vec<f64>,
70 pub spe: Vec<f64>,
72 pub t2_alarm: Vec<bool>,
74 pub spe_alarm: Vec<bool>,
76 pub residual_scores: FdMatrix,
78}
79
80fn split_indices(n: usize, tuning_fraction: f64, seed: u64) -> (Vec<usize>, Vec<usize>) {
82 let n_tune = ((n as f64 * tuning_fraction).round() as usize)
83 .max(2)
84 .min(n - 1);
85
86 let mut indices: Vec<usize> = (0..n).collect();
87 let mut rng_state: u64 = seed;
88 for i in (1..n).rev() {
89 rng_state = rng_state
90 .wrapping_mul(6_364_136_223_846_793_005)
91 .wrapping_add(1);
92 let j = (rng_state >> 33) as usize % (i + 1);
93 indices.swap(i, j);
94 }
95
96 let tune_indices: Vec<usize> = indices[..n_tune].to_vec();
97 let cal_indices: Vec<usize> = indices[n_tune..].to_vec();
98 (tune_indices, cal_indices)
99}
100
101fn compute_residuals(observed: &FdMatrix, predicted: &FdMatrix) -> FdMatrix {
103 let (n, m) = observed.shape();
104 let mut residuals = FdMatrix::zeros(n, m);
105 for i in 0..n {
106 for j in 0..m {
107 residuals[(i, j)] = observed[(i, j)] - predicted[(i, j)];
108 }
109 }
110 residuals
111}
112
113fn centered_reconstruct(fpca: &FpcaResult, scores: &FdMatrix, ncomp: usize) -> FdMatrix {
115 let n = scores.nrows();
116 let m = fpca.mean.len();
117 let ncomp = ncomp.min(fpca.rotation.ncols()).min(scores.ncols());
118
119 let mut recon = FdMatrix::zeros(n, m);
120 for i in 0..n {
121 for j in 0..m {
122 let mut val = 0.0;
123 for k in 0..ncomp {
124 val += scores[(i, k)] * fpca.rotation[(j, k)];
125 }
126 recon[(i, j)] = val;
127 }
128 }
129 recon
130}
131
132fn center_data(data: &FdMatrix, mean: &[f64]) -> FdMatrix {
134 let (n, m) = data.shape();
135 let mut centered = FdMatrix::zeros(n, m);
136 for i in 0..n {
137 for j in 0..m {
138 centered[(i, j)] = data[(i, j)] - mean[j];
139 }
140 }
141 centered
142}
143
144#[must_use = "expensive computation whose result should not be discarded"]
162pub fn frcc_phase1(
163 y_curves: &FdMatrix,
164 predictors: &FdMatrix,
165 argvals: &[f64],
166 config: &FrccConfig,
167) -> Result<FrccChart, FdarError> {
168 let (n, m) = y_curves.shape();
169 let p = predictors.ncols();
170
171 if n < 6 {
172 return Err(FdarError::InvalidDimension {
173 parameter: "y_curves",
174 expected: "at least 6 observations".to_string(),
175 actual: format!("{n} observations"),
176 });
177 }
178 if predictors.nrows() != n {
179 return Err(FdarError::InvalidDimension {
180 parameter: "predictors",
181 expected: format!("{n} rows"),
182 actual: format!("{} rows", predictors.nrows()),
183 });
184 }
185 if argvals.len() != m {
186 return Err(FdarError::InvalidDimension {
187 parameter: "argvals",
188 expected: format!("{m}"),
189 actual: format!("{}", argvals.len()),
190 });
191 }
192
193 let (tune_idx, cal_idx) = split_indices(n, config.tuning_fraction, config.seed);
195
196 let tune_y = crate::cv::subset_rows(y_curves, &tune_idx);
197 let tune_x = crate::cv::subset_rows(predictors, &tune_idx);
198 let cal_y = crate::cv::subset_rows(y_curves, &cal_idx);
199 let cal_x = crate::cv::subset_rows(predictors, &cal_idx);
200
201 let n_tune = tune_y.nrows();
202 let n_cal = cal_y.nrows();
203
204 if n_tune < p + 2 {
206 return Err(FdarError::InvalidDimension {
207 parameter: "y_curves",
208 expected: format!("tuning set with at least {} observations", p + 2),
209 actual: format!("{n_tune} observations in tuning set"),
210 });
211 }
212
213 let fosr_lambda = if config.fosr_lambda < 0.0 {
215 1e-4
216 } else {
217 config.fosr_lambda
218 };
219 let fosr_result = fosr(&tune_y, &tune_x, fosr_lambda)?;
220
221 let cal_predicted = predict_fosr(&fosr_result, &cal_x);
223 let cal_residuals = compute_residuals(&cal_y, &cal_predicted);
224
225 let ncomp = config.ncomp.min(n_cal - 1).min(m);
227 let residual_fpca = fdata_to_pc_1d(&cal_residuals, ncomp)?;
228 let actual_ncomp = residual_fpca.scores.ncols();
229
230 let eigenvalues: Vec<f64> = residual_fpca
232 .singular_values
233 .iter()
234 .take(actual_ncomp)
235 .map(|&sv| sv * sv / (n_cal as f64 - 1.0))
236 .collect();
237
238 let _t2_cal = hotelling_t2(&residual_fpca.scores, &eigenvalues)?;
240
241 let cal_resid_centered = center_data(&cal_residuals, &residual_fpca.mean);
243 let cal_resid_recon = centered_reconstruct(&residual_fpca, &residual_fpca.scores, actual_ncomp);
244 let spe_cal = spe_univariate(&cal_resid_centered, &cal_resid_recon, argvals)?;
245
246 let t2_limit = t2_control_limit(actual_ncomp, config.alpha)?;
248 let spe_limit = spe_control_limit(&spe_cal, config.alpha)?;
249
250 Ok(FrccChart {
251 fosr: fosr_result,
252 residual_fpca,
253 eigenvalues,
254 t2_limit,
255 spe_limit,
256 config: config.clone(),
257 })
258}
259
260#[must_use = "monitoring result should not be discarded"]
277pub fn frcc_monitor(
278 chart: &FrccChart,
279 new_y: &FdMatrix,
280 new_predictors: &FdMatrix,
281 argvals: &[f64],
282) -> Result<FrccMonitorResult, FdarError> {
283 let m = chart.residual_fpca.mean.len();
284 if new_y.ncols() != m {
285 return Err(FdarError::InvalidDimension {
286 parameter: "new_y",
287 expected: format!("{m} columns"),
288 actual: format!("{} columns", new_y.ncols()),
289 });
290 }
291 if new_y.nrows() != new_predictors.nrows() {
292 return Err(FdarError::InvalidDimension {
293 parameter: "new_predictors",
294 expected: format!("{} rows", new_y.nrows()),
295 actual: format!("{} rows", new_predictors.nrows()),
296 });
297 }
298
299 let ncomp = chart.eigenvalues.len();
300
301 let predicted = predict_fosr(&chart.fosr, new_predictors);
303 let residuals = compute_residuals(new_y, &predicted);
304
305 let residual_scores = chart.residual_fpca.project(&residuals)?;
307
308 let t2 = hotelling_t2(&residual_scores, &chart.eigenvalues)?;
310
311 let resid_centered = center_data(&residuals, &chart.residual_fpca.mean);
313 let resid_recon = centered_reconstruct(&chart.residual_fpca, &residual_scores, ncomp);
314 let spe = spe_univariate(&resid_centered, &resid_recon, argvals)?;
315
316 let t2_alarm: Vec<bool> = t2.iter().map(|&v| v > chart.t2_limit.ucl).collect();
318 let spe_alarm: Vec<bool> = spe.iter().map(|&v| v > chart.spe_limit.ucl).collect();
319
320 Ok(FrccMonitorResult {
321 t2,
322 spe,
323 t2_alarm,
324 spe_alarm,
325 residual_scores,
326 })
327}