Skip to main content

u_analytics/spc/
attributes.rs

1//! Attributes control charts: P, NP, C, and U charts.
2//!
3//! These charts monitor discrete (count/proportion) data from a process.
4//! Unlike variables charts, attributes charts use the binomial or Poisson
5//! distribution to compute control limits.
6//!
7//! # Chart Selection Guide
8//!
9//! | Chart | Data Type | Sample Size |
10//! |-------|-----------|-------------|
11//! | P     | Proportion defective | Variable |
12//! | NP    | Count defective | Constant |
13//! | C     | Count of defects | Constant area |
14//! | U     | Defects per unit | Variable area |
15//!
16//! # References
17//!
18//! - Montgomery, D.C. (2019). *Introduction to Statistical Quality Control*, 8th ed.,
19//!   Chapter 7: Control Charts for Attributes.
20//! - ASTM E2587 — Standard Practice for Use of Control Charts
21
22/// A single data point on an attributes control chart.
23///
24/// Contains the computed statistic, its control limits (which may vary
25/// per point for charts with variable sample sizes), and an out-of-control flag.
26#[derive(Debug, Clone)]
27pub struct AttributeChartPoint {
28    /// The zero-based index of this point.
29    pub index: usize,
30    /// The computed statistic value (proportion, count, or rate).
31    pub value: f64,
32    /// Upper control limit for this point.
33    pub ucl: f64,
34    /// Center line for this point.
35    pub cl: f64,
36    /// Lower control limit for this point.
37    pub lcl: f64,
38    /// Whether this point is out of control (beyond UCL or below LCL).
39    pub out_of_control: bool,
40}
41
42// ---------------------------------------------------------------------------
43// P Chart
44// ---------------------------------------------------------------------------
45
46/// Proportion nonconforming (P) chart.
47///
48/// Monitors the fraction of defective items in samples that may have
49/// different sizes. Control limits vary per subgroup when sample sizes differ.
50///
51/// # Formulas
52///
53/// - CL = p-bar = total_defectives / total_inspected
54/// - UCL_i = p-bar + 3 * sqrt(p-bar * (1 - p-bar) / n_i)
55/// - LCL_i = max(0, p-bar - 3 * sqrt(p-bar * (1 - p-bar) / n_i))
56///
57/// # Examples
58///
59/// ```
60/// use u_analytics::spc::PChart;
61///
62/// let mut chart = PChart::new();
63/// chart.add_sample(3, 100);  // 3 defectives out of 100
64/// chart.add_sample(5, 100);
65/// chart.add_sample(2, 100);
66/// chart.add_sample(4, 100);
67///
68/// let p_bar = chart.p_bar().expect("should have p_bar after adding samples");
69/// assert!(p_bar > 0.0);
70/// ```
71///
72/// # Reference
73///
74/// Montgomery, D.C. (2019). *Introduction to Statistical Quality Control*, 8th ed.,
75/// Chapter 7, Section 7.3.
76pub struct PChart {
77    /// Stored samples as (defective_count, sample_size) pairs.
78    samples: Vec<(u64, u64)>,
79    /// Computed chart points.
80    chart_points: Vec<AttributeChartPoint>,
81    /// Overall proportion defective (p-bar).
82    p_bar: Option<f64>,
83}
84
85impl PChart {
86    /// Create a new P chart.
87    pub fn new() -> Self {
88        Self {
89            samples: Vec::new(),
90            chart_points: Vec::new(),
91            p_bar: None,
92        }
93    }
94
95    /// Add a sample with the number of defective items and the total sample size.
96    ///
97    /// Ignores samples where `defectives > sample_size` or `sample_size == 0`.
98    pub fn add_sample(&mut self, defectives: u64, sample_size: u64) {
99        if sample_size == 0 || defectives > sample_size {
100            return;
101        }
102        self.samples.push((defectives, sample_size));
103        self.recompute();
104    }
105
106    /// Get the overall proportion defective (p-bar), or `None` if no data.
107    pub fn p_bar(&self) -> Option<f64> {
108        self.p_bar
109    }
110
111    /// Get all chart points.
112    pub fn points(&self) -> &[AttributeChartPoint] {
113        &self.chart_points
114    }
115
116    /// Check if the process is in statistical control.
117    pub fn is_in_control(&self) -> bool {
118        self.chart_points.iter().all(|p| !p.out_of_control)
119    }
120
121    /// Recompute p-bar, control limits, and out-of-control flags.
122    fn recompute(&mut self) {
123        if self.samples.is_empty() {
124            self.p_bar = None;
125            self.chart_points.clear();
126            return;
127        }
128
129        let total_defectives: u64 = self.samples.iter().map(|&(d, _)| d).sum();
130        let total_inspected: u64 = self.samples.iter().map(|&(_, n)| n).sum();
131        let p_bar = total_defectives as f64 / total_inspected as f64;
132        self.p_bar = Some(p_bar);
133
134        self.chart_points = self
135            .samples
136            .iter()
137            .enumerate()
138            .map(|(i, &(defectives, sample_size))| {
139                let p = defectives as f64 / sample_size as f64;
140                let n = sample_size as f64;
141                let sigma = (p_bar * (1.0 - p_bar) / n).sqrt();
142                let ucl = p_bar + 3.0 * sigma;
143                let lcl = (p_bar - 3.0 * sigma).max(0.0);
144
145                AttributeChartPoint {
146                    index: i,
147                    value: p,
148                    ucl,
149                    cl: p_bar,
150                    lcl,
151                    out_of_control: p > ucl || p < lcl,
152                }
153            })
154            .collect();
155    }
156}
157
158impl Default for PChart {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164// ---------------------------------------------------------------------------
165// NP Chart
166// ---------------------------------------------------------------------------
167
168/// Count of nonconforming items (NP) chart.
169///
170/// Monitors the count of defective items in samples of constant size.
171/// Simpler than the P chart when sample sizes are uniform.
172///
173/// # Formulas
174///
175/// - CL = n * p-bar
176/// - UCL = n * p-bar + 3 * sqrt(n * p-bar * (1 - p-bar))
177/// - LCL = max(0, n * p-bar - 3 * sqrt(n * p-bar * (1 - p-bar)))
178///
179/// # Reference
180///
181/// Montgomery, D.C. (2019). *Introduction to Statistical Quality Control*, 8th ed.,
182/// Chapter 7, Section 7.3.
183pub struct NPChart {
184    /// Constant sample size.
185    sample_size: u64,
186    /// Defective counts per subgroup.
187    defective_counts: Vec<u64>,
188    /// Computed chart points.
189    chart_points: Vec<AttributeChartPoint>,
190    /// Control limits (constant for NP chart).
191    limits: Option<(f64, f64, f64)>, // (ucl, cl, lcl)
192}
193
194impl NPChart {
195    /// Create a new NP chart with a constant sample size.
196    ///
197    /// # Panics
198    ///
199    /// Panics if `sample_size == 0`.
200    pub fn new(sample_size: u64) -> Self {
201        assert!(sample_size > 0, "sample_size must be > 0");
202        Self {
203            sample_size,
204            defective_counts: Vec::new(),
205            chart_points: Vec::new(),
206            limits: None,
207        }
208    }
209
210    /// Add a defective count for one subgroup.
211    ///
212    /// Ignores values where `defectives > sample_size`.
213    pub fn add_sample(&mut self, defectives: u64) {
214        if defectives > self.sample_size {
215            return;
216        }
217        self.defective_counts.push(defectives);
218        self.recompute();
219    }
220
221    /// Get the control limits as `(ucl, cl, lcl)`, or `None` if no data.
222    pub fn control_limits(&self) -> Option<(f64, f64, f64)> {
223        self.limits
224    }
225
226    /// Get all chart points.
227    pub fn points(&self) -> &[AttributeChartPoint] {
228        &self.chart_points
229    }
230
231    /// Check if the process is in statistical control.
232    pub fn is_in_control(&self) -> bool {
233        self.chart_points.iter().all(|p| !p.out_of_control)
234    }
235
236    /// Recompute limits and points.
237    fn recompute(&mut self) {
238        if self.defective_counts.is_empty() {
239            self.limits = None;
240            self.chart_points.clear();
241            return;
242        }
243
244        let total_defectives: u64 = self.defective_counts.iter().sum();
245        let total_inspected = self.sample_size * self.defective_counts.len() as u64;
246        let p_bar = total_defectives as f64 / total_inspected as f64;
247        let n = self.sample_size as f64;
248
249        let np_bar = n * p_bar;
250        let sigma = (n * p_bar * (1.0 - p_bar)).sqrt();
251        let ucl = np_bar + 3.0 * sigma;
252        let lcl = (np_bar - 3.0 * sigma).max(0.0);
253
254        self.limits = Some((ucl, np_bar, lcl));
255
256        self.chart_points = self
257            .defective_counts
258            .iter()
259            .enumerate()
260            .map(|(i, &count)| {
261                let value = count as f64;
262                AttributeChartPoint {
263                    index: i,
264                    value,
265                    ucl,
266                    cl: np_bar,
267                    lcl,
268                    out_of_control: value > ucl || value < lcl,
269                }
270            })
271            .collect();
272    }
273}
274
275// ---------------------------------------------------------------------------
276// C Chart
277// ---------------------------------------------------------------------------
278
279/// Count of defects per unit (C) chart.
280///
281/// Monitors the total number of defects observed in a constant area of
282/// opportunity (inspection unit). Based on the Poisson distribution.
283///
284/// # Formulas
285///
286/// - CL = c-bar (mean defect count)
287/// - UCL = c-bar + 3 * sqrt(c-bar)
288/// - LCL = max(0, c-bar - 3 * sqrt(c-bar))
289///
290/// # Reference
291///
292/// Montgomery, D.C. (2019). *Introduction to Statistical Quality Control*, 8th ed.,
293/// Chapter 7, Section 7.4.
294pub struct CChart {
295    /// Defect counts per unit.
296    defect_counts: Vec<u64>,
297    /// Computed chart points.
298    chart_points: Vec<AttributeChartPoint>,
299    /// Control limits (constant for C chart).
300    limits: Option<(f64, f64, f64)>, // (ucl, cl, lcl)
301}
302
303impl CChart {
304    /// Create a new C chart.
305    pub fn new() -> Self {
306        Self {
307            defect_counts: Vec::new(),
308            chart_points: Vec::new(),
309            limits: None,
310        }
311    }
312
313    /// Add a defect count for one inspection unit.
314    pub fn add_sample(&mut self, defects: u64) {
315        self.defect_counts.push(defects);
316        self.recompute();
317    }
318
319    /// Get the control limits as `(ucl, cl, lcl)`, or `None` if no data.
320    pub fn control_limits(&self) -> Option<(f64, f64, f64)> {
321        self.limits
322    }
323
324    /// Get all chart points.
325    pub fn points(&self) -> &[AttributeChartPoint] {
326        &self.chart_points
327    }
328
329    /// Check if the process is in statistical control.
330    pub fn is_in_control(&self) -> bool {
331        self.chart_points.iter().all(|p| !p.out_of_control)
332    }
333
334    /// Recompute limits and points.
335    fn recompute(&mut self) {
336        if self.defect_counts.is_empty() {
337            self.limits = None;
338            self.chart_points.clear();
339            return;
340        }
341
342        let total: u64 = self.defect_counts.iter().sum();
343        let c_bar = total as f64 / self.defect_counts.len() as f64;
344        let sigma = c_bar.sqrt();
345        let ucl = c_bar + 3.0 * sigma;
346        let lcl = (c_bar - 3.0 * sigma).max(0.0);
347
348        self.limits = Some((ucl, c_bar, lcl));
349
350        self.chart_points = self
351            .defect_counts
352            .iter()
353            .enumerate()
354            .map(|(i, &count)| {
355                let value = count as f64;
356                AttributeChartPoint {
357                    index: i,
358                    value,
359                    ucl,
360                    cl: c_bar,
361                    lcl,
362                    out_of_control: value > ucl || value < lcl,
363                }
364            })
365            .collect();
366    }
367}
368
369impl Default for CChart {
370    fn default() -> Self {
371        Self::new()
372    }
373}
374
375// ---------------------------------------------------------------------------
376// U Chart
377// ---------------------------------------------------------------------------
378
379/// Defects per unit (U) chart.
380///
381/// Monitors the defect rate when the area of opportunity (inspection size)
382/// varies between subgroups. Control limits are computed individually for
383/// each subgroup based on its inspection size.
384///
385/// # Formulas
386///
387/// - CL = u-bar = total_defects / total_units
388/// - UCL_i = u-bar + 3 * sqrt(u-bar / n_i)
389/// - LCL_i = max(0, u-bar - 3 * sqrt(u-bar / n_i))
390///
391/// # Reference
392///
393/// Montgomery, D.C. (2019). *Introduction to Statistical Quality Control*, 8th ed.,
394/// Chapter 7, Section 7.4.
395pub struct UChart {
396    /// Stored samples as (defect_count, units_inspected) pairs.
397    samples: Vec<(u64, f64)>,
398    /// Computed chart points.
399    chart_points: Vec<AttributeChartPoint>,
400    /// Overall defect rate (u-bar).
401    u_bar: Option<f64>,
402}
403
404impl UChart {
405    /// Create a new U chart.
406    pub fn new() -> Self {
407        Self {
408            samples: Vec::new(),
409            chart_points: Vec::new(),
410            u_bar: None,
411        }
412    }
413
414    /// Add a sample with the number of defects and the number of units inspected.
415    ///
416    /// The `units_inspected` can be fractional (e.g., area or length).
417    /// Ignores samples where `units_inspected <= 0` or is not finite.
418    pub fn add_sample(&mut self, defects: u64, units_inspected: f64) {
419        if !units_inspected.is_finite() || units_inspected <= 0.0 {
420            return;
421        }
422        self.samples.push((defects, units_inspected));
423        self.recompute();
424    }
425
426    /// Get the overall defect rate (u-bar), or `None` if no data.
427    pub fn u_bar(&self) -> Option<f64> {
428        self.u_bar
429    }
430
431    /// Get all chart points.
432    pub fn points(&self) -> &[AttributeChartPoint] {
433        &self.chart_points
434    }
435
436    /// Check if the process is in statistical control.
437    pub fn is_in_control(&self) -> bool {
438        self.chart_points.iter().all(|p| !p.out_of_control)
439    }
440
441    /// Recompute u-bar, control limits, and out-of-control flags.
442    fn recompute(&mut self) {
443        if self.samples.is_empty() {
444            self.u_bar = None;
445            self.chart_points.clear();
446            return;
447        }
448
449        let total_defects: u64 = self.samples.iter().map(|&(d, _)| d).sum();
450        let total_units: f64 = self.samples.iter().map(|&(_, n)| n).sum();
451        let u_bar = total_defects as f64 / total_units;
452        self.u_bar = Some(u_bar);
453
454        self.chart_points = self
455            .samples
456            .iter()
457            .enumerate()
458            .map(|(i, &(defects, units))| {
459                let u = defects as f64 / units;
460                let sigma = (u_bar / units).sqrt();
461                let ucl = u_bar + 3.0 * sigma;
462                let lcl = (u_bar - 3.0 * sigma).max(0.0);
463
464                AttributeChartPoint {
465                    index: i,
466                    value: u,
467                    ucl,
468                    cl: u_bar,
469                    lcl,
470                    out_of_control: u > ucl || u < lcl,
471                }
472            })
473            .collect();
474    }
475}
476
477impl Default for UChart {
478    fn default() -> Self {
479        Self::new()
480    }
481}
482
483// ---------------------------------------------------------------------------
484// Tests
485// ---------------------------------------------------------------------------
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    // --- P Chart ---
492
493    #[test]
494    fn test_p_chart_basic() {
495        // Textbook example: 10 samples of size 100
496        let mut chart = PChart::new();
497        let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
498        for &d in &defectives {
499            chart.add_sample(d, 100);
500        }
501
502        let p_bar = chart.p_bar().expect("should have p_bar");
503        // p-bar = 55/1000 = 0.055
504        assert!(
505            (p_bar - 0.055).abs() < 1e-10,
506            "p_bar={p_bar}, expected 0.055"
507        );
508
509        // All 10 points should exist
510        assert_eq!(chart.points().len(), 10);
511
512        // Verify center line on all points
513        for pt in chart.points() {
514            assert!((pt.cl - 0.055).abs() < 1e-10);
515        }
516    }
517
518    #[test]
519    fn test_p_chart_limits() {
520        let mut chart = PChart::new();
521        // p-bar = 0.10, n = 100
522        // sigma = sqrt(0.1 * 0.9 / 100) = 0.03
523        // UCL = 0.10 + 0.09 = 0.19
524        // LCL = 0.10 - 0.09 = 0.01
525        chart.add_sample(10, 100);
526
527        let pt = &chart.points()[0];
528        assert!((pt.cl - 0.1).abs() < 1e-10);
529        assert!((pt.ucl - 0.19).abs() < 0.001);
530        assert!((pt.lcl - 0.01).abs() < 0.001);
531    }
532
533    #[test]
534    fn test_p_chart_variable_sample_sizes() {
535        let mut chart = PChart::new();
536        chart.add_sample(5, 100);
537        chart.add_sample(10, 200);
538        chart.add_sample(3, 50);
539
540        // p-bar = 18/350
541        let p_bar = chart.p_bar().expect("p_bar");
542        assert!((p_bar - 18.0 / 350.0).abs() < 1e-10);
543
544        // UCL should differ per point due to variable n
545        let pts = chart.points();
546        // Larger sample = tighter limits
547        assert!(pts[1].ucl - pts[1].cl < pts[0].ucl - pts[0].cl);
548    }
549
550    #[test]
551    fn test_p_chart_rejects_invalid() {
552        let mut chart = PChart::new();
553        chart.add_sample(5, 0); // Zero sample size
554        assert!(chart.p_bar().is_none());
555
556        chart.add_sample(10, 5); // Defectives > sample size
557        assert!(chart.p_bar().is_none());
558    }
559
560    #[test]
561    fn test_p_chart_lcl_clamped_to_zero() {
562        let mut chart = PChart::new();
563        // Very small p with small n → LCL would be negative
564        chart.add_sample(1, 10);
565        let pt = &chart.points()[0];
566        assert!(pt.lcl >= 0.0);
567    }
568
569    #[test]
570    fn test_p_chart_out_of_control() {
571        let mut chart = PChart::new();
572        // Establish baseline with many normal samples
573        for _ in 0..20 {
574            chart.add_sample(5, 100);
575        }
576        // Add an outlier
577        chart.add_sample(30, 100);
578
579        assert!(!chart.is_in_control());
580        let last = chart.points().last().expect("should have points");
581        assert!(last.out_of_control);
582    }
583
584    #[test]
585    fn test_p_chart_default() {
586        let chart = PChart::default();
587        assert!(chart.p_bar().is_none());
588        assert!(chart.points().is_empty());
589    }
590
591    // --- NP Chart ---
592
593    #[test]
594    fn test_np_chart_basic() {
595        let mut chart = NPChart::new(100);
596        let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
597        for &d in &defectives {
598            chart.add_sample(d);
599        }
600
601        let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
602        // np-bar = 55/10 = 5.5
603        assert!((cl - 5.5).abs() < 1e-10);
604        assert!(ucl > cl);
605        assert!(lcl < cl);
606        assert!(lcl >= 0.0);
607    }
608
609    #[test]
610    fn test_np_chart_rejects_invalid() {
611        let mut chart = NPChart::new(100);
612        chart.add_sample(101); // More defectives than sample size
613        assert!(chart.control_limits().is_none());
614    }
615
616    #[test]
617    #[should_panic(expected = "sample_size must be > 0")]
618    fn test_np_chart_zero_sample_size() {
619        let _ = NPChart::new(0);
620    }
621
622    #[test]
623    fn test_np_chart_out_of_control() {
624        let mut chart = NPChart::new(100);
625        for _ in 0..20 {
626            chart.add_sample(5);
627        }
628        chart.add_sample(30);
629
630        assert!(!chart.is_in_control());
631    }
632
633    #[test]
634    fn test_np_chart_limits_formula() {
635        // n=200, p-bar = 0.05 → np-bar = 10
636        // sigma = sqrt(200 * 0.05 * 0.95) = sqrt(9.5) ≈ 3.082
637        // UCL = 10 + 3*3.082 = 19.246
638        // LCL = 10 - 3*3.082 = 0.754
639        let mut chart = NPChart::new(200);
640        for _ in 0..10 {
641            chart.add_sample(10);
642        }
643
644        let (ucl, cl, lcl) = chart.control_limits().expect("limits");
645        assert!((cl - 10.0).abs() < 1e-10);
646        let expected_sigma = (200.0_f64 * 0.05 * 0.95).sqrt();
647        assert!((ucl - (10.0 + 3.0 * expected_sigma)).abs() < 0.01);
648        assert!((lcl - (10.0 - 3.0 * expected_sigma)).abs() < 0.01);
649    }
650
651    // --- C Chart ---
652
653    #[test]
654    fn test_c_chart_basic() {
655        let mut chart = CChart::new();
656        let counts = [3, 5, 4, 6, 2, 7, 3, 4, 5, 6];
657        for &c in &counts {
658            chart.add_sample(c);
659        }
660
661        let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
662        // c-bar = 45/10 = 4.5
663        assert!((cl - 4.5).abs() < 1e-10);
664        // UCL = 4.5 + 3*sqrt(4.5) = 4.5 + 6.364 = 10.864
665        let expected_ucl = 4.5 + 3.0 * 4.5_f64.sqrt();
666        assert!((ucl - expected_ucl).abs() < 0.01);
667        assert!(lcl >= 0.0);
668    }
669
670    #[test]
671    fn test_c_chart_out_of_control() {
672        let mut chart = CChart::new();
673        for _ in 0..20 {
674            chart.add_sample(5);
675        }
676        chart.add_sample(50); // Way out of control
677
678        assert!(!chart.is_in_control());
679    }
680
681    #[test]
682    fn test_c_chart_single_sample() {
683        let mut chart = CChart::new();
684        chart.add_sample(10);
685
686        let (_, cl, _) = chart.control_limits().expect("limits");
687        assert!((cl - 10.0).abs() < f64::EPSILON);
688    }
689
690    #[test]
691    fn test_c_chart_lcl_clamped() {
692        // c-bar = 1 → LCL = 1 - 3*1 = -2 → clamped to 0
693        let mut chart = CChart::new();
694        chart.add_sample(1);
695
696        let (_, _, lcl) = chart.control_limits().expect("limits");
697        assert!((lcl - 0.0).abs() < f64::EPSILON);
698    }
699
700    #[test]
701    fn test_c_chart_default() {
702        let chart = CChart::default();
703        assert!(chart.control_limits().is_none());
704        assert!(chart.points().is_empty());
705    }
706
707    // --- U Chart ---
708
709    #[test]
710    fn test_u_chart_basic() {
711        let mut chart = UChart::new();
712        // 5 samples, each inspecting 10 units
713        chart.add_sample(3, 10.0);
714        chart.add_sample(5, 10.0);
715        chart.add_sample(4, 10.0);
716        chart.add_sample(6, 10.0);
717        chart.add_sample(2, 10.0);
718
719        let u_bar = chart.u_bar().expect("should have u_bar");
720        // u-bar = 20/50 = 0.4
721        assert!((u_bar - 0.4).abs() < 1e-10);
722
723        assert_eq!(chart.points().len(), 5);
724    }
725
726    #[test]
727    fn test_u_chart_variable_units() {
728        let mut chart = UChart::new();
729        chart.add_sample(10, 5.0); // u = 2.0
730        chart.add_sample(20, 10.0); // u = 2.0
731        chart.add_sample(5, 2.5); // u = 2.0
732
733        let u_bar = chart.u_bar().expect("u_bar");
734        // u-bar = 35/17.5 = 2.0
735        assert!((u_bar - 2.0).abs() < 1e-10);
736
737        // Larger inspection area → tighter limits
738        let pts = chart.points();
739        let width_0 = pts[0].ucl - pts[0].cl; // n=5
740        let width_1 = pts[1].ucl - pts[1].cl; // n=10
741        assert!(width_1 < width_0, "larger n should have tighter limits");
742    }
743
744    #[test]
745    fn test_u_chart_rejects_invalid() {
746        let mut chart = UChart::new();
747        chart.add_sample(5, 0.0); // Zero units
748        assert!(chart.u_bar().is_none());
749
750        chart.add_sample(5, -1.0); // Negative units
751        assert!(chart.u_bar().is_none());
752
753        chart.add_sample(5, f64::NAN); // NaN units
754        assert!(chart.u_bar().is_none());
755
756        chart.add_sample(5, f64::INFINITY); // Infinite units
757        assert!(chart.u_bar().is_none());
758    }
759
760    #[test]
761    fn test_u_chart_out_of_control() {
762        let mut chart = UChart::new();
763        for _ in 0..20 {
764            chart.add_sample(4, 10.0);
765        }
766        chart.add_sample(50, 10.0); // Far outlier
767
768        assert!(!chart.is_in_control());
769    }
770
771    #[test]
772    fn test_u_chart_lcl_clamped() {
773        let mut chart = UChart::new();
774        // Small u-bar with small n → LCL would be negative
775        chart.add_sample(1, 1.0);
776        let pt = &chart.points()[0];
777        assert!(pt.lcl >= 0.0);
778    }
779
780    #[test]
781    fn test_u_chart_default() {
782        let chart = UChart::default();
783        assert!(chart.u_bar().is_none());
784        assert!(chart.points().is_empty());
785    }
786
787    #[test]
788    fn test_u_chart_limits_formula() {
789        // u-bar = 2.0, n = 4.0
790        // sigma = sqrt(2.0/4.0) = sqrt(0.5) ≈ 0.7071
791        // UCL = 2.0 + 3*0.7071 = 4.1213
792        // LCL = max(0, 2.0 - 2.1213) = 0.0 (clamped)
793        let mut chart = UChart::new();
794        chart.add_sample(8, 4.0);
795
796        let pt = &chart.points()[0];
797        assert!((pt.cl - 2.0).abs() < 1e-10);
798        let expected_sigma = (2.0_f64 / 4.0).sqrt();
799        assert!((pt.ucl - (2.0 + 3.0 * expected_sigma)).abs() < 0.001);
800    }
801
802    // --- Cross-chart consistency ---
803
804    #[test]
805    fn test_p_and_np_consistent() {
806        // P chart with constant n should give equivalent results to NP chart
807        let mut p_chart = PChart::new();
808        let mut np_chart = NPChart::new(100);
809
810        let defectives = [5, 8, 3, 6, 4];
811        for &d in &defectives {
812            p_chart.add_sample(d, 100);
813            np_chart.add_sample(d);
814        }
815
816        let p_bar = p_chart.p_bar().expect("p_bar");
817        let (_, np_cl, _) = np_chart.control_limits().expect("np limits");
818
819        // NP center line = n * p-bar
820        assert!(
821            (np_cl - 100.0 * p_bar).abs() < 1e-10,
822            "NP CL should equal n * p_bar"
823        );
824    }
825
826    #[test]
827    fn test_c_and_u_consistent_equal_units() {
828        // U chart with constant n=1 should give same limits as C chart
829        let mut c_chart = CChart::new();
830        let mut u_chart = UChart::new();
831
832        let defects = [3, 5, 4, 6, 2];
833        for &d in &defects {
834            c_chart.add_sample(d);
835            u_chart.add_sample(d, 1.0);
836        }
837
838        let (c_ucl, c_cl, c_lcl) = c_chart.control_limits().expect("C limits");
839        let u_bar = u_chart.u_bar().expect("u_bar");
840
841        assert!(
842            (c_cl - u_bar).abs() < 1e-10,
843            "C chart CL should equal U chart u-bar when n=1"
844        );
845
846        let u_pt = &u_chart.points()[0];
847        assert!((u_pt.ucl - c_ucl).abs() < 1e-10);
848        assert!((u_pt.lcl - c_lcl).abs() < 1e-10);
849    }
850}