Skip to main content

u_analytics/spc/
variables.rs

1//! Variables control charts: X-bar-R, X-bar-S, and Individual-MR.
2//!
3//! These charts monitor continuous (variables) data from a process.
4//! Subgroup charts (X-bar-R, X-bar-S) track the mean and within-subgroup
5//! variation of small samples; the Individual-MR chart handles single
6//! observations.
7//!
8//! # Control Chart Factors
9//!
10//! All constants (A2, D3, D4, d2, A3, B3, B4, c4, E2) are sourced from
11//! ASTM E2587 — Standard Practice for Use of Control Charts in Statistical
12//! Process Control.
13//!
14//! # References
15//!
16//! - Montgomery, D.C. (2019). *Introduction to Statistical Quality Control*, 8th ed.
17//! - ASTM E2587 — Standard Practice for Use of Control Charts
18//! - Shewhart, W.A. (1931). *Economic Control of Quality of Manufactured Product*.
19
20use super::chart::{ChartPoint, ControlChart, ControlLimits, Violation, ViolationType};
21use super::rules::{NelsonRules, RunRule};
22
23// ---------------------------------------------------------------------------
24// Control chart factor tables (ASTM E2587), indexed by subgroup size n=2..10
25// Index 0 corresponds to n=2.
26// ---------------------------------------------------------------------------
27
28/// A2 factors for X-bar-R chart UCL/LCL computation.
29///
30/// UCL = X-double-bar + A2 * R-bar, LCL = X-double-bar - A2 * R-bar.
31const A2: [f64; 9] = [
32    1.880, 1.023, 0.729, 0.577, 0.483, 0.419, 0.373, 0.337, 0.308,
33];
34
35/// D3 factors for R chart lower control limit.
36///
37/// LCL_R = D3 * R-bar.
38const D3: [f64; 9] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.076, 0.136, 0.184, 0.223];
39
40/// D4 factors for R chart upper control limit.
41///
42/// UCL_R = D4 * R-bar.
43const D4: [f64; 9] = [
44    3.267, 2.575, 2.282, 2.114, 2.004, 1.924, 1.864, 1.816, 1.777,
45];
46
47/// d2 factors (mean of the range distribution) for estimating sigma from R-bar.
48///
49/// sigma-hat = R-bar / d2.
50#[allow(dead_code)]
51const D2: [f64; 9] = [
52    1.128, 1.693, 2.059, 2.326, 2.534, 2.704, 2.847, 2.970, 3.078,
53];
54
55/// A3 factors for X-bar-S chart UCL/LCL computation.
56///
57/// UCL = X-double-bar + A3 * S-bar, LCL = X-double-bar - A3 * S-bar.
58const A3: [f64; 9] = [
59    2.659, 1.954, 1.628, 1.427, 1.287, 1.182, 1.099, 1.032, 0.975,
60];
61
62/// B3 factors for S chart lower control limit.
63///
64/// LCL_S = B3 * S-bar.
65const B3: [f64; 9] = [0.0, 0.0, 0.0, 0.0, 0.030, 0.118, 0.185, 0.239, 0.284];
66
67/// B4 factors for S chart upper control limit.
68///
69/// UCL_S = B4 * S-bar.
70const B4: [f64; 9] = [
71    3.267, 2.568, 2.266, 2.089, 1.970, 1.882, 1.815, 1.761, 1.716,
72];
73
74/// c4 factors for unbiased estimation of sigma from S-bar.
75///
76/// sigma-hat = S-bar / c4.
77#[allow(dead_code)]
78const C4: [f64; 9] = [
79    0.7979, 0.8862, 0.9213, 0.9400, 0.9515, 0.9594, 0.9650, 0.9693, 0.9727,
80];
81
82/// E2 factor for Individual chart UCL/LCL.
83///
84/// UCL = X-bar + E2 * MR-bar, LCL = X-bar - E2 * MR-bar.
85/// E2 = 3 / d2(n=2) = 3 / 1.128 = 2.6596...
86const E2: f64 = 2.660;
87
88/// D4 factor for MR chart (n=2 moving range).
89const D4_MR: f64 = 3.267;
90
91// ---------------------------------------------------------------------------
92// X-bar-R Chart
93// ---------------------------------------------------------------------------
94
95/// X-bar and Range (X-bar-R) control chart.
96///
97/// Monitors the process mean (X-bar chart) and process variability (R chart)
98/// using subgroup ranges. Suitable for subgroup sizes n = 2..=10.
99///
100/// # Algorithm
101///
102/// 1. For each subgroup, compute the mean (X-bar) and range (R).
103/// 2. Compute the grand mean (X-double-bar) and average range (R-bar).
104/// 3. X-bar chart limits: CL = X-double-bar, UCL/LCL = CL +/- A2 * R-bar.
105/// 4. R chart limits: CL = R-bar, UCL = D4 * R-bar, LCL = D3 * R-bar.
106///
107/// # Examples
108///
109/// ```
110/// use u_analytics::spc::{XBarRChart, ControlChart};
111///
112/// let mut chart = XBarRChart::new(5);
113/// chart.add_sample(&[25.0, 26.0, 24.5, 25.5, 25.0]);
114/// chart.add_sample(&[25.2, 24.8, 25.1, 24.9, 25.3]);
115/// chart.add_sample(&[25.1, 25.0, 24.7, 25.3, 24.9]);
116///
117/// let limits = chart.control_limits().expect("should have limits after 3 samples");
118/// assert!(limits.ucl > limits.cl);
119/// assert!(limits.cl > limits.lcl);
120/// ```
121///
122/// # Reference
123///
124/// Montgomery, D.C. (2019). *Introduction to Statistical Quality Control*, 8th ed.,
125/// Chapter 6: Control Charts for Variables.
126pub struct XBarRChart {
127    /// Fixed subgroup size (2..=10).
128    subgroup_size: usize,
129    /// Stored subgroups.
130    subgroups: Vec<Vec<f64>>,
131    /// Computed X-bar chart points.
132    xbar_points: Vec<ChartPoint>,
133    /// Computed R chart points.
134    r_points: Vec<ChartPoint>,
135    /// X-bar chart control limits.
136    xbar_limits: Option<ControlLimits>,
137    /// R chart control limits.
138    r_limits: Option<ControlLimits>,
139}
140
141impl XBarRChart {
142    /// Create a new X-bar-R chart with the given subgroup size.
143    ///
144    /// # Panics
145    ///
146    /// Panics if `subgroup_size` is not in the range 2..=10.
147    pub fn new(subgroup_size: usize) -> Self {
148        assert!(
149            (2..=10).contains(&subgroup_size),
150            "subgroup_size must be 2..=10, got {subgroup_size}"
151        );
152        Self {
153            subgroup_size,
154            subgroups: Vec::new(),
155            xbar_points: Vec::new(),
156            r_points: Vec::new(),
157            xbar_limits: None,
158            r_limits: None,
159        }
160    }
161
162    /// Get the R chart control limits, or `None` if insufficient data.
163    pub fn r_limits(&self) -> Option<ControlLimits> {
164        self.r_limits.clone()
165    }
166
167    /// Get the R chart points.
168    pub fn r_points(&self) -> &[ChartPoint] {
169        &self.r_points
170    }
171
172    /// Recompute limits and points from stored subgroups.
173    fn recompute(&mut self) {
174        if self.subgroups.is_empty() {
175            self.xbar_limits = None;
176            self.r_limits = None;
177            self.xbar_points.clear();
178            self.r_points.clear();
179            return;
180        }
181
182        let idx = self.subgroup_size - 2; // Factor table index
183
184        // Compute subgroup means and ranges
185        let mut xbar_values = Vec::with_capacity(self.subgroups.len());
186        let mut r_values = Vec::with_capacity(self.subgroups.len());
187
188        for subgroup in &self.subgroups {
189            let mean_val = u_numflow::stats::mean(subgroup)
190                .expect("subgroup should be non-empty with finite values");
191            let range = subgroup_range(subgroup);
192            xbar_values.push(mean_val);
193            r_values.push(range);
194        }
195
196        // Grand mean and average range
197        let grand_mean = u_numflow::stats::mean(&xbar_values)
198            .expect("xbar_values should be non-empty with finite values");
199        let r_bar = u_numflow::stats::mean(&r_values)
200            .expect("r_values should be non-empty with finite values");
201
202        // X-bar chart limits
203        let a2 = A2[idx];
204        self.xbar_limits = Some(ControlLimits {
205            ucl: grand_mean + a2 * r_bar,
206            cl: grand_mean,
207            lcl: grand_mean - a2 * r_bar,
208        });
209
210        // R chart limits
211        let d3 = D3[idx];
212        let d4 = D4[idx];
213        self.r_limits = Some(ControlLimits {
214            ucl: d4 * r_bar,
215            cl: r_bar,
216            lcl: d3 * r_bar,
217        });
218
219        // Build points
220        self.xbar_points = xbar_values
221            .iter()
222            .enumerate()
223            .map(|(i, &v)| ChartPoint {
224                value: v,
225                index: i,
226                violations: Vec::new(),
227            })
228            .collect();
229
230        self.r_points = r_values
231            .iter()
232            .enumerate()
233            .map(|(i, &v)| ChartPoint {
234                value: v,
235                index: i,
236                violations: Vec::new(),
237            })
238            .collect();
239
240        // Apply Nelson rules to X-bar chart
241        if let Some(ref limits) = self.xbar_limits {
242            let nelson = NelsonRules;
243            let violations = nelson.check(&self.xbar_points, limits);
244            apply_violations(&mut self.xbar_points, &violations);
245        }
246
247        // Apply Nelson rules to R chart
248        if let Some(ref limits) = self.r_limits {
249            let nelson = NelsonRules;
250            let violations = nelson.check(&self.r_points, limits);
251            apply_violations(&mut self.r_points, &violations);
252        }
253    }
254}
255
256impl ControlChart for XBarRChart {
257    /// Add a subgroup sample. The sample length must equal the chart's subgroup size.
258    fn add_sample(&mut self, sample: &[f64]) {
259        if sample.len() != self.subgroup_size {
260            return;
261        }
262        if !sample.iter().all(|x| x.is_finite()) {
263            return;
264        }
265        self.subgroups.push(sample.to_vec());
266        self.recompute();
267    }
268
269    fn control_limits(&self) -> Option<ControlLimits> {
270        self.xbar_limits.clone()
271    }
272
273    fn is_in_control(&self) -> bool {
274        self.xbar_points.iter().all(|p| p.violations.is_empty())
275            && self.r_points.iter().all(|p| p.violations.is_empty())
276    }
277
278    fn violations(&self) -> Vec<Violation> {
279        collect_violations(&self.xbar_points)
280            .into_iter()
281            .chain(collect_violations(&self.r_points))
282            .collect()
283    }
284
285    fn points(&self) -> &[ChartPoint] {
286        &self.xbar_points
287    }
288}
289
290// ---------------------------------------------------------------------------
291// X-bar-S Chart
292// ---------------------------------------------------------------------------
293
294/// X-bar and Standard Deviation (X-bar-S) control chart.
295///
296/// Monitors the process mean (X-bar chart) and process variability (S chart)
297/// using subgroup standard deviations. Preferred over X-bar-R for larger
298/// subgroups where range is a less efficient estimator.
299///
300/// # Algorithm
301///
302/// 1. For each subgroup, compute the mean (X-bar) and sample standard deviation (S).
303/// 2. Compute the grand mean (X-double-bar) and average S (S-bar).
304/// 3. X-bar chart limits: CL = X-double-bar, UCL/LCL = CL +/- A3 * S-bar.
305/// 4. S chart limits: CL = S-bar, UCL = B4 * S-bar, LCL = B3 * S-bar.
306///
307/// # Reference
308///
309/// Montgomery, D.C. (2019). *Introduction to Statistical Quality Control*, 8th ed.,
310/// Chapter 6: Control Charts for Variables.
311pub struct XBarSChart {
312    /// Fixed subgroup size (2..=10).
313    subgroup_size: usize,
314    /// Stored subgroups.
315    subgroups: Vec<Vec<f64>>,
316    /// Computed X-bar chart points.
317    xbar_points: Vec<ChartPoint>,
318    /// Computed S chart points.
319    s_points: Vec<ChartPoint>,
320    /// X-bar chart control limits.
321    xbar_limits: Option<ControlLimits>,
322    /// S chart control limits.
323    s_limits: Option<ControlLimits>,
324}
325
326impl XBarSChart {
327    /// Create a new X-bar-S chart with the given subgroup size.
328    ///
329    /// # Panics
330    ///
331    /// Panics if `subgroup_size` is not in the range 2..=10.
332    pub fn new(subgroup_size: usize) -> Self {
333        assert!(
334            (2..=10).contains(&subgroup_size),
335            "subgroup_size must be 2..=10, got {subgroup_size}"
336        );
337        Self {
338            subgroup_size,
339            subgroups: Vec::new(),
340            xbar_points: Vec::new(),
341            s_points: Vec::new(),
342            xbar_limits: None,
343            s_limits: None,
344        }
345    }
346
347    /// Get the S chart control limits, or `None` if insufficient data.
348    pub fn s_limits(&self) -> Option<ControlLimits> {
349        self.s_limits.clone()
350    }
351
352    /// Get the S chart points.
353    pub fn s_points(&self) -> &[ChartPoint] {
354        &self.s_points
355    }
356
357    /// Recompute limits and points from stored subgroups.
358    fn recompute(&mut self) {
359        if self.subgroups.is_empty() {
360            self.xbar_limits = None;
361            self.s_limits = None;
362            self.xbar_points.clear();
363            self.s_points.clear();
364            return;
365        }
366
367        let idx = self.subgroup_size - 2;
368
369        // Compute subgroup means and standard deviations
370        let mut xbar_values = Vec::with_capacity(self.subgroups.len());
371        let mut s_values = Vec::with_capacity(self.subgroups.len());
372
373        for subgroup in &self.subgroups {
374            let mean_val = u_numflow::stats::mean(subgroup)
375                .expect("subgroup should be non-empty with finite values");
376            let sd = u_numflow::stats::std_dev(subgroup)
377                .expect("subgroup should have >= 2 elements for std_dev");
378            xbar_values.push(mean_val);
379            s_values.push(sd);
380        }
381
382        // Grand mean and average S
383        let grand_mean = u_numflow::stats::mean(&xbar_values)
384            .expect("xbar_values should be non-empty with finite values");
385        let s_bar = u_numflow::stats::mean(&s_values)
386            .expect("s_values should be non-empty with finite values");
387
388        // X-bar chart limits
389        let a3 = A3[idx];
390        self.xbar_limits = Some(ControlLimits {
391            ucl: grand_mean + a3 * s_bar,
392            cl: grand_mean,
393            lcl: grand_mean - a3 * s_bar,
394        });
395
396        // S chart limits
397        let b3 = B3[idx];
398        let b4 = B4[idx];
399        self.s_limits = Some(ControlLimits {
400            ucl: b4 * s_bar,
401            cl: s_bar,
402            lcl: b3 * s_bar,
403        });
404
405        // Build points
406        self.xbar_points = xbar_values
407            .iter()
408            .enumerate()
409            .map(|(i, &v)| ChartPoint {
410                value: v,
411                index: i,
412                violations: Vec::new(),
413            })
414            .collect();
415
416        self.s_points = s_values
417            .iter()
418            .enumerate()
419            .map(|(i, &v)| ChartPoint {
420                value: v,
421                index: i,
422                violations: Vec::new(),
423            })
424            .collect();
425
426        // Apply Nelson rules to X-bar chart
427        if let Some(ref limits) = self.xbar_limits {
428            let nelson = NelsonRules;
429            let violations = nelson.check(&self.xbar_points, limits);
430            apply_violations(&mut self.xbar_points, &violations);
431        }
432
433        // Apply Nelson rules to S chart
434        if let Some(ref limits) = self.s_limits {
435            let nelson = NelsonRules;
436            let violations = nelson.check(&self.s_points, limits);
437            apply_violations(&mut self.s_points, &violations);
438        }
439    }
440}
441
442impl ControlChart for XBarSChart {
443    /// Add a subgroup sample. The sample length must equal the chart's subgroup size.
444    fn add_sample(&mut self, sample: &[f64]) {
445        if sample.len() != self.subgroup_size {
446            return;
447        }
448        if !sample.iter().all(|x| x.is_finite()) {
449            return;
450        }
451        self.subgroups.push(sample.to_vec());
452        self.recompute();
453    }
454
455    fn control_limits(&self) -> Option<ControlLimits> {
456        self.xbar_limits.clone()
457    }
458
459    fn is_in_control(&self) -> bool {
460        self.xbar_points.iter().all(|p| p.violations.is_empty())
461            && self.s_points.iter().all(|p| p.violations.is_empty())
462    }
463
464    fn violations(&self) -> Vec<Violation> {
465        collect_violations(&self.xbar_points)
466            .into_iter()
467            .chain(collect_violations(&self.s_points))
468            .collect()
469    }
470
471    fn points(&self) -> &[ChartPoint] {
472        &self.xbar_points
473    }
474}
475
476// ---------------------------------------------------------------------------
477// Individual-MR Chart
478// ---------------------------------------------------------------------------
479
480/// Individual and Moving Range (I-MR) control chart.
481///
482/// Monitors individual observations (subgroup size = 1) using the moving range
483/// of consecutive observations to estimate process variability.
484///
485/// # Algorithm
486///
487/// 1. Compute moving ranges: MR_i = |x_i - x_{i-1}| for i >= 1.
488/// 2. Compute the mean of individual observations (X-bar) and the average
489///    moving range (MR-bar).
490/// 3. I chart limits: CL = X-bar, UCL/LCL = X-bar +/- E2 * MR-bar.
491/// 4. MR chart limits: CL = MR-bar, UCL = D4 * MR-bar, LCL = 0.
492///
493/// # Examples
494///
495/// ```
496/// use u_analytics::spc::{IndividualMRChart, ControlChart};
497///
498/// let mut chart = IndividualMRChart::new();
499/// for &x in &[25.0, 25.2, 24.8, 25.1, 24.9, 25.3, 25.0, 24.7] {
500///     chart.add_sample(&[x]);
501/// }
502///
503/// let limits = chart.control_limits().expect("should have limits after 2+ observations");
504/// assert!(limits.ucl > limits.cl);
505/// assert!(limits.cl > limits.lcl);
506/// ```
507///
508/// # Reference
509///
510/// Montgomery, D.C. (2019). *Introduction to Statistical Quality Control*, 8th ed.,
511/// Chapter 6: Control Charts for Variables.
512pub struct IndividualMRChart {
513    /// Individual observations.
514    observations: Vec<f64>,
515    /// Computed I chart points.
516    i_points: Vec<ChartPoint>,
517    /// Computed MR chart points.
518    mr_points: Vec<ChartPoint>,
519    /// I chart control limits.
520    i_limits: Option<ControlLimits>,
521    /// MR chart control limits.
522    mr_limits: Option<ControlLimits>,
523}
524
525impl IndividualMRChart {
526    /// Create a new Individual-MR chart.
527    pub fn new() -> Self {
528        Self {
529            observations: Vec::new(),
530            i_points: Vec::new(),
531            mr_points: Vec::new(),
532            i_limits: None,
533            mr_limits: None,
534        }
535    }
536
537    /// Get the MR chart control limits, or `None` if insufficient data.
538    pub fn mr_limits(&self) -> Option<ControlLimits> {
539        self.mr_limits.clone()
540    }
541
542    /// Get the MR chart points.
543    pub fn mr_points(&self) -> &[ChartPoint] {
544        &self.mr_points
545    }
546
547    /// Recompute limits and points from stored observations.
548    fn recompute(&mut self) {
549        if self.observations.len() < 2 {
550            self.i_limits = None;
551            self.mr_limits = None;
552            self.i_points.clear();
553            self.mr_points.clear();
554            return;
555        }
556
557        // Compute moving ranges
558        let mr_values: Vec<f64> = self
559            .observations
560            .windows(2)
561            .map(|w| (w[1] - w[0]).abs())
562            .collect();
563
564        // X-bar and MR-bar
565        let x_bar = u_numflow::stats::mean(&self.observations)
566            .expect("observations should be non-empty with finite values");
567        let mr_bar = u_numflow::stats::mean(&mr_values)
568            .expect("mr_values should be non-empty with finite values");
569
570        // I chart limits
571        self.i_limits = Some(ControlLimits {
572            ucl: x_bar + E2 * mr_bar,
573            cl: x_bar,
574            lcl: x_bar - E2 * mr_bar,
575        });
576
577        // MR chart limits (LCL is always 0 for n=2)
578        self.mr_limits = Some(ControlLimits {
579            ucl: D4_MR * mr_bar,
580            cl: mr_bar,
581            lcl: 0.0,
582        });
583
584        // Build I chart points
585        self.i_points = self
586            .observations
587            .iter()
588            .enumerate()
589            .map(|(i, &v)| ChartPoint {
590                value: v,
591                index: i,
592                violations: Vec::new(),
593            })
594            .collect();
595
596        // Build MR chart points (starts at index 1, since MR_0 is undefined)
597        self.mr_points = mr_values
598            .iter()
599            .enumerate()
600            .map(|(i, &v)| ChartPoint {
601                value: v,
602                index: i + 1,
603                violations: Vec::new(),
604            })
605            .collect();
606
607        // Apply Nelson rules to I chart
608        if let Some(ref limits) = self.i_limits {
609            let nelson = NelsonRules;
610            let violations = nelson.check(&self.i_points, limits);
611            apply_violations(&mut self.i_points, &violations);
612        }
613
614        // Apply Nelson rules to MR chart
615        if let Some(ref limits) = self.mr_limits {
616            let nelson = NelsonRules;
617            let violations = nelson.check(&self.mr_points, limits);
618            apply_violations(&mut self.mr_points, &violations);
619        }
620    }
621}
622
623impl Default for IndividualMRChart {
624    fn default() -> Self {
625        Self::new()
626    }
627}
628
629impl ControlChart for IndividualMRChart {
630    /// Add a single observation. The sample slice must contain exactly one element.
631    fn add_sample(&mut self, sample: &[f64]) {
632        if sample.len() != 1 {
633            return;
634        }
635        if !sample[0].is_finite() {
636            return;
637        }
638        self.observations.push(sample[0]);
639        self.recompute();
640    }
641
642    fn control_limits(&self) -> Option<ControlLimits> {
643        self.i_limits.clone()
644    }
645
646    fn is_in_control(&self) -> bool {
647        self.i_points.iter().all(|p| p.violations.is_empty())
648            && self.mr_points.iter().all(|p| p.violations.is_empty())
649    }
650
651    fn violations(&self) -> Vec<Violation> {
652        collect_violations(&self.i_points)
653            .into_iter()
654            .chain(collect_violations(&self.mr_points))
655            .collect()
656    }
657
658    fn points(&self) -> &[ChartPoint] {
659        &self.i_points
660    }
661}
662
663// ---------------------------------------------------------------------------
664// Helpers
665// ---------------------------------------------------------------------------
666
667/// Compute the range (max - min) of a subgroup.
668///
669/// # Panics
670///
671/// Uses `expect` — callers must ensure `subgroup` is non-empty with finite values.
672fn subgroup_range(subgroup: &[f64]) -> f64 {
673    let max_val =
674        u_numflow::stats::max(subgroup).expect("subgroup should be non-empty without NaN");
675    let min_val =
676        u_numflow::stats::min(subgroup).expect("subgroup should be non-empty without NaN");
677    max_val - min_val
678}
679
680/// Apply a list of violations to chart points, matching by index.
681fn apply_violations(points: &mut [ChartPoint], violations: &[(usize, ViolationType)]) {
682    for &(idx, vtype) in violations {
683        if let Some(point) = points.iter_mut().find(|p| p.index == idx) {
684            point.violations.push(vtype);
685        }
686    }
687}
688
689/// Collect all violations from chart points into a flat list.
690fn collect_violations(points: &[ChartPoint]) -> Vec<Violation> {
691    let mut result = Vec::new();
692    for point in points {
693        for &vtype in &point.violations {
694            result.push(Violation {
695                point_index: point.index,
696                violation_type: vtype,
697            });
698        }
699    }
700    result
701}
702
703// ---------------------------------------------------------------------------
704// Tests
705// ---------------------------------------------------------------------------
706
707#[cfg(test)]
708mod tests {
709    use super::*;
710
711    // --- XBarRChart ---
712
713    #[test]
714    fn test_xbar_r_basic_limits() {
715        let mut chart = XBarRChart::new(4);
716        chart.add_sample(&[72.0, 84.0, 79.0, 49.0]);
717        chart.add_sample(&[56.0, 87.0, 33.0, 42.0]);
718        chart.add_sample(&[55.0, 73.0, 22.0, 60.0]);
719        chart.add_sample(&[44.0, 80.0, 54.0, 74.0]);
720        chart.add_sample(&[97.0, 26.0, 48.0, 58.0]);
721
722        let limits = chart.control_limits().expect("should have limits");
723        // Subgroup means: 71.0, 54.5, 52.5, 63.0, 57.25
724        let expected_grand_mean = (71.0 + 54.5 + 52.5 + 63.0 + 57.25) / 5.0;
725        assert!(
726            (limits.cl - expected_grand_mean).abs() < 0.1,
727            "CL={}, expected ~{expected_grand_mean}",
728            limits.cl
729        );
730
731        // Verify UCL > CL > LCL
732        assert!(limits.ucl > limits.cl);
733        assert!(limits.cl > limits.lcl);
734    }
735
736    #[test]
737    fn test_xbar_r_rejects_wrong_size() {
738        let mut chart = XBarRChart::new(5);
739        chart.add_sample(&[1.0, 2.0, 3.0]); // Wrong size, should be ignored
740        assert!(chart.control_limits().is_none());
741    }
742
743    #[test]
744    fn test_xbar_r_rejects_nan() {
745        let mut chart = XBarRChart::new(3);
746        chart.add_sample(&[1.0, f64::NAN, 3.0]);
747        assert!(chart.control_limits().is_none());
748    }
749
750    #[test]
751    fn test_xbar_r_r_chart_limits() {
752        let mut chart = XBarRChart::new(5);
753        chart.add_sample(&[10.0, 12.0, 11.0, 13.0, 14.0]);
754        chart.add_sample(&[11.0, 13.0, 12.0, 10.0, 15.0]);
755        chart.add_sample(&[12.0, 11.0, 14.0, 13.0, 10.0]);
756
757        let r_limits = chart.r_limits().expect("should have R limits");
758        assert!(r_limits.ucl > r_limits.cl);
759        assert!(r_limits.lcl >= 0.0);
760    }
761
762    #[test]
763    fn test_xbar_r_constant_subgroups() {
764        // All identical values: R-bar = 0, limits collapse
765        let mut chart = XBarRChart::new(3);
766        chart.add_sample(&[10.0, 10.0, 10.0]);
767        chart.add_sample(&[10.0, 10.0, 10.0]);
768
769        let limits = chart.control_limits().expect("should have limits");
770        assert!((limits.cl - 10.0).abs() < f64::EPSILON);
771        assert!((limits.ucl - 10.0).abs() < f64::EPSILON);
772        assert!((limits.lcl - 10.0).abs() < f64::EPSILON);
773    }
774
775    #[test]
776    fn test_xbar_r_detects_out_of_control() {
777        let mut chart = XBarRChart::new(3);
778        for _ in 0..5 {
779            chart.add_sample(&[10.0, 10.5, 9.5]);
780        }
781        // Add an outlier subgroup
782        chart.add_sample(&[50.0, 51.0, 49.0]);
783
784        assert!(!chart.is_in_control());
785    }
786
787    #[test]
788    #[should_panic(expected = "subgroup_size must be 2..=10")]
789    fn test_xbar_r_invalid_size_1() {
790        let _ = XBarRChart::new(1);
791    }
792
793    #[test]
794    #[should_panic(expected = "subgroup_size must be 2..=10")]
795    fn test_xbar_r_invalid_size_11() {
796        let _ = XBarRChart::new(11);
797    }
798
799    // --- XBarSChart ---
800
801    #[test]
802    fn test_xbar_s_basic_limits() {
803        let mut chart = XBarSChart::new(4);
804        chart.add_sample(&[72.0, 84.0, 79.0, 49.0]);
805        chart.add_sample(&[56.0, 87.0, 33.0, 42.0]);
806        chart.add_sample(&[55.0, 73.0, 22.0, 60.0]);
807        chart.add_sample(&[44.0, 80.0, 54.0, 74.0]);
808        chart.add_sample(&[97.0, 26.0, 48.0, 58.0]);
809
810        let limits = chart.control_limits().expect("should have limits");
811        assert!(limits.ucl > limits.cl);
812        assert!(limits.cl > limits.lcl);
813
814        let s_limits = chart.s_limits().expect("should have S limits");
815        assert!(s_limits.ucl > s_limits.cl);
816        assert!(s_limits.lcl >= 0.0);
817    }
818
819    #[test]
820    fn test_xbar_s_rejects_wrong_size() {
821        let mut chart = XBarSChart::new(5);
822        chart.add_sample(&[1.0, 2.0]);
823        assert!(chart.control_limits().is_none());
824    }
825
826    #[test]
827    fn test_xbar_s_in_control() {
828        let mut chart = XBarSChart::new(4);
829        for _ in 0..10 {
830            chart.add_sample(&[10.0, 10.2, 9.8, 10.1]);
831        }
832        assert!(chart.is_in_control());
833    }
834
835    // --- IndividualMRChart ---
836
837    #[test]
838    fn test_imr_basic_limits() {
839        let mut chart = IndividualMRChart::new();
840        let data = [10.0, 12.0, 11.0, 13.0, 10.0, 14.0, 11.0, 12.0, 13.0, 10.0];
841        for &x in &data {
842            chart.add_sample(&[x]);
843        }
844
845        let limits = chart.control_limits().expect("should have limits");
846        assert!(limits.ucl > limits.cl);
847        assert!(limits.cl > limits.lcl);
848
849        let mr_limits = chart.mr_limits().expect("should have MR limits");
850        assert!(mr_limits.ucl > mr_limits.cl);
851        assert!((mr_limits.lcl).abs() < f64::EPSILON);
852    }
853
854    #[test]
855    fn test_imr_needs_two_points() {
856        let mut chart = IndividualMRChart::new();
857        chart.add_sample(&[10.0]);
858        assert!(chart.control_limits().is_none());
859    }
860
861    #[test]
862    fn test_imr_center_line_is_mean() {
863        let mut chart = IndividualMRChart::new();
864        let data = [5.0, 10.0, 15.0, 20.0, 25.0];
865        for &x in &data {
866            chart.add_sample(&[x]);
867        }
868        let limits = chart.control_limits().expect("should have limits");
869        assert!((limits.cl - 15.0).abs() < f64::EPSILON);
870    }
871
872    #[test]
873    fn test_imr_mr_values() {
874        let mut chart = IndividualMRChart::new();
875        let data = [10.0, 12.0, 9.0];
876        for &x in &data {
877            chart.add_sample(&[x]);
878        }
879        // MR values: |12-10| = 2, |9-12| = 3
880        let mr_pts = chart.mr_points();
881        assert_eq!(mr_pts.len(), 2);
882        assert!((mr_pts[0].value - 2.0).abs() < f64::EPSILON);
883        assert!((mr_pts[1].value - 3.0).abs() < f64::EPSILON);
884    }
885
886    #[test]
887    fn test_imr_rejects_multi_element_sample() {
888        let mut chart = IndividualMRChart::new();
889        chart.add_sample(&[1.0, 2.0]);
890        assert!(chart.points().is_empty());
891    }
892
893    #[test]
894    fn test_imr_detects_out_of_control() {
895        let mut chart = IndividualMRChart::new();
896        for i in 0..10 {
897            chart.add_sample(&[50.0 + (i as f64 % 3.0) * 0.5]);
898        }
899        // Add a far outlier
900        chart.add_sample(&[100.0]);
901
902        assert!(!chart.is_in_control());
903    }
904
905    #[test]
906    fn test_imr_default() {
907        let chart = IndividualMRChart::default();
908        assert!(chart.points().is_empty());
909    }
910
911    // --- Helper function tests ---
912
913    #[test]
914    fn test_subgroup_range() {
915        assert!((subgroup_range(&[1.0, 5.0, 3.0]) - 4.0).abs() < f64::EPSILON);
916        assert!((subgroup_range(&[10.0, 10.0, 10.0])).abs() < f64::EPSILON);
917    }
918
919    // --- Textbook verification: X-bar-R chart factors ---
920
921    #[test]
922    fn test_xbar_r_chart_factors_n5() {
923        // For n=5: A2=0.577, D3=0.0, D4=2.114
924        // Subgroup with mean=50, range=10
925        let mut chart = XBarRChart::new(5);
926        chart.add_sample(&[45.0, 47.0, 50.0, 53.0, 55.0]);
927
928        let limits = chart.control_limits().expect("limits");
929        assert!((limits.cl - 50.0).abs() < f64::EPSILON);
930
931        let r_limits = chart.r_limits().expect("R limits");
932        assert!((r_limits.cl - 10.0).abs() < f64::EPSILON);
933
934        // UCL = 50 + 0.577 * 10 = 55.77
935        assert!((limits.ucl - 55.77).abs() < 0.01);
936        // LCL = 50 - 0.577 * 10 = 44.23
937        assert!((limits.lcl - 44.23).abs() < 0.01);
938    }
939
940    // --- Textbook verification: I-MR chart ---
941
942    #[test]
943    fn test_imr_e2_factor() {
944        // E2 = 2.660
945        // Two points with X-bar = 100, MR = |105-95| = 10
946        let mut chart = IndividualMRChart::new();
947        chart.add_sample(&[95.0]);
948        chart.add_sample(&[105.0]);
949
950        let limits = chart.control_limits().expect("limits");
951        // X-bar = 100
952        assert!((limits.cl - 100.0).abs() < f64::EPSILON);
953        // MR-bar = 10
954        // UCL = 100 + 2.660 * 10 = 126.6
955        assert!((limits.ucl - 126.6).abs() < 0.1);
956        // LCL = 100 - 2.660 * 10 = 73.4
957        assert!((limits.lcl - 73.4).abs() < 0.1);
958    }
959}