fdars_core/spm/
control.rs1use super::chi_squared::chi2_quantile;
8use crate::error::FdarError;
9
10#[derive(Debug, Clone, PartialEq)]
12#[non_exhaustive]
13pub struct ControlLimit {
14 pub ucl: f64,
16 pub alpha: f64,
18 pub description: String,
20}
21
22pub fn t2_control_limit(ncomp: usize, alpha: f64) -> Result<ControlLimit, FdarError> {
34 if ncomp == 0 {
35 return Err(FdarError::InvalidParameter {
36 parameter: "ncomp",
37 message: "ncomp must be at least 1".to_string(),
38 });
39 }
40 if alpha <= 0.0 || alpha >= 1.0 {
41 return Err(FdarError::InvalidParameter {
42 parameter: "alpha",
43 message: format!("alpha must be in (0, 1), got {alpha}"),
44 });
45 }
46
47 let ucl = chi2_quantile(1.0 - alpha, ncomp);
48
49 Ok(ControlLimit {
50 ucl,
51 alpha,
52 description: format!("T2 ~ chi2({ncomp}), alpha={alpha}"),
53 })
54}
55
56pub fn spe_control_limit(spe_values: &[f64], alpha: f64) -> Result<ControlLimit, FdarError> {
73 let n = spe_values.len();
74 if n < 2 {
75 return Err(FdarError::InvalidDimension {
76 parameter: "spe_values",
77 expected: "at least 2 values".to_string(),
78 actual: format!("{n} values"),
79 });
80 }
81 if alpha <= 0.0 || alpha >= 1.0 {
82 return Err(FdarError::InvalidParameter {
83 parameter: "alpha",
84 message: format!("alpha must be in (0, 1), got {alpha}"),
85 });
86 }
87
88 let mean: f64 = spe_values.iter().sum::<f64>() / n as f64;
89 let var: f64 = spe_values.iter().map(|&s| (s - mean).powi(2)).sum::<f64>() / (n as f64 - 1.0);
90
91 if mean <= 0.0 || var <= 0.0 {
92 return Err(FdarError::ComputationFailed {
93 operation: "spe_control_limit",
94 detail: format!(
95 "SPE mean={mean:.6}, var={var:.6}; cannot estimate chi-squared parameters from non-positive moments"
96 ),
97 });
98 }
99
100 let a = var / (2.0 * mean);
101 let b = 2.0 * mean * mean / var;
102 let b_int = b.round().max(1.0) as usize;
103
104 let ucl = a * chi2_quantile(1.0 - alpha, b_int);
105
106 Ok(ControlLimit {
107 ucl,
108 alpha,
109 description: format!("SPE ~ {a:.4} * chi2({b_int}), alpha={alpha}"),
110 })
111}