1use super::chart::{
21 ChartPoint, ControlChart, ControlChartError, ControlLimits, Violation, ViolationType,
22 MAX_SUBGROUP_SIZE, MIN_SUBGROUP_SIZE,
23};
24use super::rules::{RuleSet, RunRule};
25
26const A2: [f64; 24] = [
50 1.880, 1.023, 0.729, 0.577, 0.483, 0.419, 0.373, 0.337, 0.308, 0.285, 0.266, 0.249, 0.235,
51 0.223, 0.212, 0.203, 0.194, 0.187, 0.180, 0.173, 0.167, 0.162, 0.157, 0.153,
52];
53
54const D3: [f64; 24] = [
58 0.000, 0.000, 0.000, 0.000, 0.000, 0.076, 0.136, 0.184, 0.223, 0.256, 0.283, 0.307, 0.328,
59 0.347, 0.363, 0.378, 0.391, 0.404, 0.415, 0.425, 0.435, 0.443, 0.452, 0.459,
60];
61
62const D4: [f64; 24] = [
66 3.267, 2.575, 2.282, 2.114, 2.004, 1.924, 1.864, 1.816, 1.777, 1.744, 1.717, 1.693, 1.672,
67 1.653, 1.637, 1.622, 1.609, 1.596, 1.585, 1.575, 1.565, 1.557, 1.548, 1.541,
68];
69
70const D2: [f64; 24] = [
81 std::f64::consts::FRAC_2_SQRT_PI,
84 1.6926,
85 2.0588,
86 2.3259,
87 2.5344,
88 2.7044,
89 2.8472,
90 2.9700,
91 3.0775,
92 3.1729,
93 3.2585,
94 3.3360,
95 3.4068,
96 3.4718,
97 3.5320,
98 3.5879,
99 3.6401,
100 3.6890,
101 3.7350,
102 3.7783,
103 3.8194,
104 3.8583,
105 3.8953,
106 3.9306,
107];
108
109const A3: [f64; 24] = [
113 2.659, 1.954, 1.628, 1.427, 1.287, 1.182, 1.099, 1.032, 0.975, 0.927, 0.886, 0.850, 0.817,
114 0.789, 0.763, 0.739, 0.718, 0.698, 0.680, 0.663, 0.647, 0.633, 0.619, 0.606,
115];
116
117const B3: [f64; 24] = [
121 0.000, 0.000, 0.000, 0.000, 0.030, 0.118, 0.185, 0.239, 0.284, 0.321, 0.354, 0.382, 0.406,
122 0.428, 0.448, 0.466, 0.482, 0.497, 0.510, 0.523, 0.534, 0.545, 0.555, 0.565,
123];
124
125const B4: [f64; 24] = [
129 3.267, 2.568, 2.266, 2.089, 1.970, 1.882, 1.815, 1.761, 1.716, 1.679, 1.646, 1.618, 1.594,
130 1.572, 1.552, 1.534, 1.518, 1.503, 1.490, 1.477, 1.466, 1.455, 1.445, 1.435,
131];
132
133const C4: [f64; 24] = [
144 0.7979, 0.8862, 0.9213, 0.9400, 0.9515, 0.9594, 0.9650, 0.9693, 0.9727, 0.9754, 0.9776, 0.9794,
145 0.9810, 0.9823, 0.9835, 0.9845, 0.9854, 0.9862, 0.9869, 0.9876, 0.9882, 0.9887, 0.9892, 0.9896,
146];
147
148const E2: f64 = 2.660;
153
154const D4_MR: f64 = 3.267;
156
157#[derive(Debug, Clone)]
196pub struct XBarRChart {
197 subgroup_size: usize,
199 subgroups: Vec<Vec<f64>>,
201 xbar_points: Vec<ChartPoint>,
203 r_points: Vec<ChartPoint>,
205 xbar_limits: Option<ControlLimits>,
207 r_limits: Option<ControlLimits>,
209 rules: RuleSet,
211}
212
213impl XBarRChart {
214 #[must_use]
229 pub fn with_rules(mut self, rules: RuleSet) -> Self {
230 self.rules = rules;
231 self.recompute();
232 self
233 }
234
235 pub fn rules(&self) -> RuleSet {
237 self.rules
238 }
239 pub fn new(subgroup_size: usize) -> Result<Self, ControlChartError> {
250 if !(MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE).contains(&subgroup_size) {
251 return Err(ControlChartError::SubgroupSizeOutOfRange {
252 got: subgroup_size,
253 min: MIN_SUBGROUP_SIZE,
254 max: MAX_SUBGROUP_SIZE,
255 });
256 }
257 Ok(Self {
258 subgroup_size,
259 subgroups: Vec::new(),
260 xbar_points: Vec::new(),
261 r_points: Vec::new(),
262 xbar_limits: None,
263 r_limits: None,
264 rules: RuleSet::default(),
265 })
266 }
267
268 pub fn sigma_hat(&self) -> Option<f64> {
292 let r_bar = self.r_limits.as_ref()?.cl;
293 let d2 = D2[self.subgroup_size - 2];
294 (d2 > 0.0).then(|| r_bar / d2)
295 }
296
297 pub fn r_limits(&self) -> Option<ControlLimits> {
299 self.r_limits.clone()
300 }
301
302 pub fn r_points(&self) -> &[ChartPoint] {
304 &self.r_points
305 }
306
307 fn recompute(&mut self) {
309 if self.subgroups.is_empty() {
310 self.xbar_limits = None;
311 self.r_limits = None;
312 self.xbar_points.clear();
313 self.r_points.clear();
314 return;
315 }
316
317 let idx = self.subgroup_size - 2; let mut xbar_values = Vec::with_capacity(self.subgroups.len());
321 let mut r_values = Vec::with_capacity(self.subgroups.len());
322
323 for subgroup in &self.subgroups {
324 let mean_val = u_numflow::stats::mean(subgroup)
325 .expect("subgroup should be non-empty with finite values");
326 let range = subgroup_range(subgroup);
327 xbar_values.push(mean_val);
328 r_values.push(range);
329 }
330
331 let grand_mean = u_numflow::stats::mean(&xbar_values)
333 .expect("xbar_values should be non-empty with finite values");
334 let r_bar = u_numflow::stats::mean(&r_values)
335 .expect("r_values should be non-empty with finite values");
336
337 let a2 = A2[idx];
339 self.xbar_limits = Some(ControlLimits {
340 ucl: grand_mean + a2 * r_bar,
341 cl: grand_mean,
342 lcl: grand_mean - a2 * r_bar,
343 });
344
345 let d3 = D3[idx];
347 let d4 = D4[idx];
348 self.r_limits = Some(ControlLimits {
349 ucl: d4 * r_bar,
350 cl: r_bar,
351 lcl: d3 * r_bar,
352 });
353
354 self.xbar_points = xbar_values
356 .iter()
357 .enumerate()
358 .map(|(i, &v)| ChartPoint {
359 value: v,
360 index: i,
361 violations: Vec::new(),
362 })
363 .collect();
364
365 self.r_points = r_values
366 .iter()
367 .enumerate()
368 .map(|(i, &v)| ChartPoint {
369 value: v,
370 index: i,
371 violations: Vec::new(),
372 })
373 .collect();
374
375 if let Some(ref limits) = self.xbar_limits {
377 let violations = self.rules.check(&self.xbar_points, limits);
378 apply_violations(&mut self.xbar_points, &violations);
379 }
380
381 if let Some(ref limits) = self.r_limits {
383 let violations = self.rules.check(&self.r_points, limits);
384 apply_violations(&mut self.r_points, &violations);
385 }
386 }
387}
388
389fn check_sample(sample: &[f64], expected: usize) -> Result<(), ControlChartError> {
391 if sample.len() != expected {
392 return Err(ControlChartError::SampleLengthMismatch {
393 expected,
394 got: sample.len(),
395 });
396 }
397 if !sample.iter().all(|x| x.is_finite()) {
398 return Err(ControlChartError::NonFiniteValue);
399 }
400 Ok(())
401}
402
403impl ControlChart for XBarRChart {
404 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
406 check_sample(sample, self.subgroup_size)?;
407 self.subgroups.push(sample.to_vec());
408 self.recompute();
409 Ok(())
410 }
411
412 fn control_limits(&self) -> Option<ControlLimits> {
413 self.xbar_limits.clone()
414 }
415
416 fn is_in_control(&self) -> bool {
417 self.xbar_points.iter().all(|p| p.violations.is_empty())
418 && self.r_points.iter().all(|p| p.violations.is_empty())
419 }
420
421 fn violations(&self) -> Vec<Violation> {
422 collect_violations(&self.xbar_points)
423 .into_iter()
424 .chain(collect_violations(&self.r_points))
425 .collect()
426 }
427
428 fn points(&self) -> &[ChartPoint] {
429 &self.xbar_points
430 }
431}
432
433#[derive(Debug, Clone)]
455pub struct XBarSChart {
456 subgroup_size: usize,
458 subgroups: Vec<Vec<f64>>,
460 xbar_points: Vec<ChartPoint>,
462 s_points: Vec<ChartPoint>,
464 xbar_limits: Option<ControlLimits>,
466 s_limits: Option<ControlLimits>,
468 rules: RuleSet,
470}
471
472impl XBarSChart {
473 #[must_use]
488 pub fn with_rules(mut self, rules: RuleSet) -> Self {
489 self.rules = rules;
490 self.recompute();
491 self
492 }
493
494 pub fn rules(&self) -> RuleSet {
496 self.rules
497 }
498 pub fn new(subgroup_size: usize) -> Result<Self, ControlChartError> {
509 if !(MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE).contains(&subgroup_size) {
510 return Err(ControlChartError::SubgroupSizeOutOfRange {
511 got: subgroup_size,
512 min: MIN_SUBGROUP_SIZE,
513 max: MAX_SUBGROUP_SIZE,
514 });
515 }
516 Ok(Self {
517 subgroup_size,
518 subgroups: Vec::new(),
519 xbar_points: Vec::new(),
520 s_points: Vec::new(),
521 xbar_limits: None,
522 s_limits: None,
523 rules: RuleSet::default(),
524 })
525 }
526
527 pub fn sigma_hat(&self) -> Option<f64> {
547 let s_bar = self.s_limits.as_ref()?.cl;
548 let c4 = C4[self.subgroup_size - 2];
549 (c4 > 0.0).then(|| s_bar / c4)
550 }
551
552 pub fn s_limits(&self) -> Option<ControlLimits> {
554 self.s_limits.clone()
555 }
556
557 pub fn s_points(&self) -> &[ChartPoint] {
559 &self.s_points
560 }
561
562 fn recompute(&mut self) {
564 if self.subgroups.is_empty() {
565 self.xbar_limits = None;
566 self.s_limits = None;
567 self.xbar_points.clear();
568 self.s_points.clear();
569 return;
570 }
571
572 let idx = self.subgroup_size - 2;
573
574 let mut xbar_values = Vec::with_capacity(self.subgroups.len());
576 let mut s_values = Vec::with_capacity(self.subgroups.len());
577
578 for subgroup in &self.subgroups {
579 let mean_val = u_numflow::stats::mean(subgroup)
580 .expect("subgroup should be non-empty with finite values");
581 let sd = u_numflow::stats::std_dev(subgroup)
582 .expect("subgroup should have >= 2 elements for std_dev");
583 xbar_values.push(mean_val);
584 s_values.push(sd);
585 }
586
587 let grand_mean = u_numflow::stats::mean(&xbar_values)
589 .expect("xbar_values should be non-empty with finite values");
590 let s_bar = u_numflow::stats::mean(&s_values)
591 .expect("s_values should be non-empty with finite values");
592
593 let a3 = A3[idx];
595 self.xbar_limits = Some(ControlLimits {
596 ucl: grand_mean + a3 * s_bar,
597 cl: grand_mean,
598 lcl: grand_mean - a3 * s_bar,
599 });
600
601 let b3 = B3[idx];
603 let b4 = B4[idx];
604 self.s_limits = Some(ControlLimits {
605 ucl: b4 * s_bar,
606 cl: s_bar,
607 lcl: b3 * s_bar,
608 });
609
610 self.xbar_points = xbar_values
612 .iter()
613 .enumerate()
614 .map(|(i, &v)| ChartPoint {
615 value: v,
616 index: i,
617 violations: Vec::new(),
618 })
619 .collect();
620
621 self.s_points = s_values
622 .iter()
623 .enumerate()
624 .map(|(i, &v)| ChartPoint {
625 value: v,
626 index: i,
627 violations: Vec::new(),
628 })
629 .collect();
630
631 if let Some(ref limits) = self.xbar_limits {
633 let violations = self.rules.check(&self.xbar_points, limits);
634 apply_violations(&mut self.xbar_points, &violations);
635 }
636
637 if let Some(ref limits) = self.s_limits {
639 let violations = self.rules.check(&self.s_points, limits);
640 apply_violations(&mut self.s_points, &violations);
641 }
642 }
643}
644
645impl ControlChart for XBarSChart {
646 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
648 check_sample(sample, self.subgroup_size)?;
649 self.subgroups.push(sample.to_vec());
650 self.recompute();
651 Ok(())
652 }
653
654 fn control_limits(&self) -> Option<ControlLimits> {
655 self.xbar_limits.clone()
656 }
657
658 fn is_in_control(&self) -> bool {
659 self.xbar_points.iter().all(|p| p.violations.is_empty())
660 && self.s_points.iter().all(|p| p.violations.is_empty())
661 }
662
663 fn violations(&self) -> Vec<Violation> {
664 collect_violations(&self.xbar_points)
665 .into_iter()
666 .chain(collect_violations(&self.s_points))
667 .collect()
668 }
669
670 fn points(&self) -> &[ChartPoint] {
671 &self.xbar_points
672 }
673}
674
675#[derive(Debug, Clone)]
712pub struct IndividualMRChart {
713 observations: Vec<f64>,
715 i_points: Vec<ChartPoint>,
717 mr_points: Vec<ChartPoint>,
719 i_limits: Option<ControlLimits>,
721 mr_limits: Option<ControlLimits>,
723 rules: RuleSet,
725}
726
727impl IndividualMRChart {
728 #[must_use]
743 pub fn with_rules(mut self, rules: RuleSet) -> Self {
744 self.rules = rules;
745 self.recompute();
746 self
747 }
748
749 pub fn rules(&self) -> RuleSet {
751 self.rules
752 }
753 pub fn new() -> Self {
755 Self {
756 observations: Vec::new(),
757 i_points: Vec::new(),
758 mr_points: Vec::new(),
759 i_limits: None,
760 mr_limits: None,
761 rules: RuleSet::default(),
762 }
763 }
764
765 pub fn sigma_hat(&self) -> Option<f64> {
789 let mr_bar = self.mr_limits.as_ref()?.cl;
790 Some(mr_bar / D2[0])
791 }
792
793 pub fn mr_limits(&self) -> Option<ControlLimits> {
795 self.mr_limits.clone()
796 }
797
798 pub fn mr_points(&self) -> &[ChartPoint] {
800 &self.mr_points
801 }
802
803 fn recompute(&mut self) {
805 if self.observations.len() < 2 {
806 self.i_limits = None;
807 self.mr_limits = None;
808 self.i_points.clear();
809 self.mr_points.clear();
810 return;
811 }
812
813 let mr_values: Vec<f64> = self
815 .observations
816 .windows(2)
817 .map(|w| (w[1] - w[0]).abs())
818 .collect();
819
820 let x_bar = u_numflow::stats::mean(&self.observations)
822 .expect("observations should be non-empty with finite values");
823 let mr_bar = u_numflow::stats::mean(&mr_values)
824 .expect("mr_values should be non-empty with finite values");
825
826 self.i_limits = Some(ControlLimits {
828 ucl: x_bar + E2 * mr_bar,
829 cl: x_bar,
830 lcl: x_bar - E2 * mr_bar,
831 });
832
833 self.mr_limits = Some(ControlLimits {
835 ucl: D4_MR * mr_bar,
836 cl: mr_bar,
837 lcl: 0.0,
838 });
839
840 self.i_points = self
842 .observations
843 .iter()
844 .enumerate()
845 .map(|(i, &v)| ChartPoint {
846 value: v,
847 index: i,
848 violations: Vec::new(),
849 })
850 .collect();
851
852 self.mr_points = mr_values
854 .iter()
855 .enumerate()
856 .map(|(i, &v)| ChartPoint {
857 value: v,
858 index: i + 1,
859 violations: Vec::new(),
860 })
861 .collect();
862
863 if let Some(ref limits) = self.i_limits {
865 let violations = self.rules.check(&self.i_points, limits);
866 apply_violations(&mut self.i_points, &violations);
867 }
868
869 if let Some(ref limits) = self.mr_limits {
871 let violations = self.rules.check(&self.mr_points, limits);
872 apply_violations(&mut self.mr_points, &violations);
873 }
874 }
875}
876
877impl Default for IndividualMRChart {
878 fn default() -> Self {
879 Self::new()
880 }
881}
882
883impl ControlChart for IndividualMRChart {
884 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
886 check_sample(sample, 1)?;
887 self.observations.push(sample[0]);
888 self.recompute();
889 Ok(())
890 }
891
892 fn control_limits(&self) -> Option<ControlLimits> {
893 self.i_limits.clone()
894 }
895
896 fn is_in_control(&self) -> bool {
897 self.i_points.iter().all(|p| p.violations.is_empty())
898 && self.mr_points.iter().all(|p| p.violations.is_empty())
899 }
900
901 fn violations(&self) -> Vec<Violation> {
902 collect_violations(&self.i_points)
903 .into_iter()
904 .chain(collect_violations(&self.mr_points))
905 .collect()
906 }
907
908 fn points(&self) -> &[ChartPoint] {
909 &self.i_points
910 }
911}
912
913fn subgroup_range(subgroup: &[f64]) -> f64 {
923 let max_val =
924 u_numflow::stats::max(subgroup).expect("subgroup should be non-empty without NaN");
925 let min_val =
926 u_numflow::stats::min(subgroup).expect("subgroup should be non-empty without NaN");
927 max_val - min_val
928}
929
930fn apply_violations(points: &mut [ChartPoint], violations: &[(usize, ViolationType)]) {
932 for &(idx, vtype) in violations {
933 if let Some(point) = points.iter_mut().find(|p| p.index == idx) {
934 point.violations.push(vtype);
935 }
936 }
937}
938
939fn collect_violations(points: &[ChartPoint]) -> Vec<Violation> {
941 let mut result = Vec::new();
942 for point in points {
943 for &vtype in &point.violations {
944 result.push(Violation {
945 point_index: point.index,
946 violation_type: vtype,
947 });
948 }
949 }
950 result
951}
952
953#[cfg(test)]
958mod tests {
959 use super::*;
960
961 #[test]
964 fn test_xbar_r_basic_limits() {
965 let mut chart = XBarRChart::new(4).expect("subgroup size is in range");
966 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]).unwrap();
967 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]).unwrap();
968 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]).unwrap();
969 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]).unwrap();
970 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]).unwrap();
971
972 let limits = chart.control_limits().expect("should have limits");
973 let expected_grand_mean = (71.0 + 54.5 + 52.5 + 63.0 + 57.25) / 5.0;
975 assert!(
976 (limits.cl - expected_grand_mean).abs() < 0.1,
977 "CL={}, expected ~{expected_grand_mean}",
978 limits.cl
979 );
980
981 assert!(limits.ucl > limits.cl);
983 assert!(limits.cl > limits.lcl);
984 }
985
986 #[test]
987 fn test_xbar_r_rejects_wrong_size() {
988 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
989 assert_eq!(
990 chart.add_sample(&[1.0, 2.0, 3.0]),
991 Err(ControlChartError::SampleLengthMismatch {
992 expected: 5,
993 got: 3
994 })
995 );
996 assert!(chart.control_limits().is_none());
997 }
998
999 #[test]
1000 fn test_xbar_r_rejects_nan() {
1001 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1002 assert_eq!(
1003 chart.add_sample(&[1.0, f64::NAN, 3.0]),
1004 Err(ControlChartError::NonFiniteValue)
1005 );
1006 assert!(chart.control_limits().is_none());
1007 }
1008
1009 #[test]
1010 fn test_xbar_r_r_chart_limits() {
1011 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1012 chart.add_sample(&[10.0, 12.0, 11.0, 13.0, 14.0]).unwrap();
1013 chart.add_sample(&[11.0, 13.0, 12.0, 10.0, 15.0]).unwrap();
1014 chart.add_sample(&[12.0, 11.0, 14.0, 13.0, 10.0]).unwrap();
1015
1016 let r_limits = chart.r_limits().expect("should have R limits");
1017 assert!(r_limits.ucl > r_limits.cl);
1018 assert!(r_limits.lcl >= 0.0);
1019 }
1020
1021 #[test]
1022 fn test_xbar_r_constant_subgroups() {
1023 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1025 chart.add_sample(&[10.0, 10.0, 10.0]).unwrap();
1026 chart.add_sample(&[10.0, 10.0, 10.0]).unwrap();
1027
1028 let limits = chart.control_limits().expect("should have limits");
1029 assert!((limits.cl - 10.0).abs() < f64::EPSILON);
1030 assert!((limits.ucl - 10.0).abs() < f64::EPSILON);
1031 assert!((limits.lcl - 10.0).abs() < f64::EPSILON);
1032 }
1033
1034 #[test]
1035 fn test_xbar_r_detects_out_of_control() {
1036 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1037 for _ in 0..5 {
1038 chart.add_sample(&[10.0, 10.5, 9.5]).unwrap();
1039 }
1040 chart.add_sample(&[50.0, 51.0, 49.0]).unwrap();
1042
1043 assert!(!chart.is_in_control());
1044 }
1045
1046 #[test]
1054 fn factor_tables_reproduce_the_published_astm_values() {
1055 const PUB_A2: [f64; 9] = [
1057 1.880, 1.023, 0.729, 0.577, 0.483, 0.419, 0.373, 0.337, 0.308,
1058 ];
1059 const PUB_D3: [f64; 9] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.076, 0.136, 0.184, 0.223];
1060 const PUB_D4: [f64; 9] = [
1061 3.267, 2.575, 2.282, 2.114, 2.004, 1.924, 1.864, 1.816, 1.777,
1062 ];
1063 const PUB_D2: [f64; 9] = [
1064 1.128, 1.693, 2.059, 2.326, 2.534, 2.704, 2.847, 2.970, 3.078,
1065 ];
1066 const PUB_A3: [f64; 9] = [
1067 2.659, 1.954, 1.628, 1.427, 1.287, 1.182, 1.099, 1.032, 0.975,
1068 ];
1069 const PUB_B3: [f64; 9] = [0.0, 0.0, 0.0, 0.0, 0.030, 0.118, 0.185, 0.239, 0.284];
1070 const PUB_B4: [f64; 9] = [
1071 3.267, 2.568, 2.266, 2.089, 1.970, 1.882, 1.815, 1.761, 1.716,
1072 ];
1073 const PUB_C4: [f64; 9] = [
1074 0.7979, 0.8862, 0.9213, 0.9400, 0.9515, 0.9594, 0.9650, 0.9693, 0.9727,
1075 ];
1076
1077 for (name, table, published, tol) in [
1078 ("A2", &A2[..9], &PUB_A2[..], 5e-4),
1079 ("D3", &D3[..9], &PUB_D3[..], 5e-4),
1080 ("D4", &D4[..9], &PUB_D4[..], 5e-4),
1081 ("d2", &D2[..9], &PUB_D2[..], 5e-4),
1082 ("A3", &A3[..9], &PUB_A3[..], 5e-4),
1083 ("B3", &B3[..9], &PUB_B3[..], 5e-4),
1084 ("B4", &B4[..9], &PUB_B4[..], 5e-4),
1085 ("c4", &C4[..9], &PUB_C4[..], 5e-5),
1086 ] {
1087 for (i, (got, want)) in table.iter().zip(published).enumerate() {
1088 assert!(
1089 (got - want).abs() < tol,
1090 "{name}[n={}] = {got}, published {want}",
1091 i + 2
1092 );
1093 }
1094 }
1095 }
1096
1097 #[test]
1100 fn factor_tables_cover_the_declared_range() {
1101 let span = MAX_SUBGROUP_SIZE - MIN_SUBGROUP_SIZE + 1;
1102 for (name, len) in [
1103 ("A2", A2.len()),
1104 ("D3", D3.len()),
1105 ("D4", D4.len()),
1106 ("d2", D2.len()),
1107 ("A3", A3.len()),
1108 ("B3", B3.len()),
1109 ("B4", B4.len()),
1110 ("c4", C4.len()),
1111 ] {
1112 assert_eq!(
1113 len, span,
1114 "{name} does not cover {MIN_SUBGROUP_SIZE}..={MAX_SUBGROUP_SIZE}"
1115 );
1116 }
1117 for i in 1..span {
1119 assert!(D2[i] > D2[i - 1], "d2 not increasing at n={}", i + 2);
1120 assert!(C4[i] > C4[i - 1], "c4 not increasing at n={}", i + 2);
1121 assert!(A2[i] < A2[i - 1], "A2 not decreasing at n={}", i + 2);
1122 assert!(A3[i] < A3[i - 1], "A3 not decreasing at n={}", i + 2);
1123 assert!(D4[i] < D4[i - 1], "D4 not decreasing at n={}", i + 2);
1124 assert!(B4[i] < B4[i - 1], "B4 not decreasing at n={}", i + 2);
1125 assert!(D3[i] >= D3[i - 1], "D3 not non-decreasing at n={}", i + 2);
1126 assert!(B3[i] >= B3[i - 1], "B3 not non-decreasing at n={}", i + 2);
1127 }
1128 assert!(C4[span - 1] < 1.0 && C4[span - 1] > 0.98);
1130 }
1131
1132 #[test]
1133 fn test_xbar_r_subgroup_size_below_range_is_an_error() {
1134 assert_eq!(
1135 XBarRChart::new(1).unwrap_err(),
1136 ControlChartError::SubgroupSizeOutOfRange {
1137 got: 1,
1138 min: 2,
1139 max: 25
1140 }
1141 );
1142 }
1143
1144 #[test]
1145 fn test_xbar_r_subgroup_size_above_range_is_an_error() {
1146 assert_eq!(
1147 XBarRChart::new(26).unwrap_err(),
1148 ControlChartError::SubgroupSizeOutOfRange {
1149 got: 26,
1150 min: 2,
1151 max: 25
1152 }
1153 );
1154 }
1155
1156 #[test]
1157 fn test_subgroup_size_11_is_now_accepted() {
1158 let mut chart = XBarRChart::new(11).expect("11 is within 2..=25");
1161 for _ in 0..5 {
1162 chart
1163 .add_sample(&[10.0, 10.2, 9.8, 10.1, 9.9, 10.3, 9.7, 10.0, 10.1, 9.9, 10.0])
1164 .unwrap();
1165 }
1166 let limits = chart.control_limits().expect("limits from 5 subgroups");
1167 assert!(limits.ucl > limits.cl && limits.cl > limits.lcl);
1168 assert!(chart.sigma_hat().is_some_and(|s| s > 0.0));
1169 }
1170
1171 #[test]
1174 fn test_xbar_s_basic_limits() {
1175 let mut chart = XBarSChart::new(4).expect("subgroup size is in range");
1176 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]).unwrap();
1177 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]).unwrap();
1178 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]).unwrap();
1179 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]).unwrap();
1180 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]).unwrap();
1181
1182 let limits = chart.control_limits().expect("should have limits");
1183 assert!(limits.ucl > limits.cl);
1184 assert!(limits.cl > limits.lcl);
1185
1186 let s_limits = chart.s_limits().expect("should have S limits");
1187 assert!(s_limits.ucl > s_limits.cl);
1188 assert!(s_limits.lcl >= 0.0);
1189 }
1190
1191 #[test]
1192 fn test_xbar_s_rejects_wrong_size() {
1193 let mut chart = XBarSChart::new(5).expect("subgroup size is in range");
1194 assert_eq!(
1195 chart.add_sample(&[1.0, 2.0]),
1196 Err(ControlChartError::SampleLengthMismatch {
1197 expected: 5,
1198 got: 2
1199 })
1200 );
1201 assert!(chart.control_limits().is_none());
1202 }
1203
1204 #[test]
1205 fn test_xbar_s_in_control() {
1206 let mut chart = XBarSChart::new(4).expect("subgroup size is in range");
1207 for _ in 0..10 {
1208 chart.add_sample(&[10.0, 10.2, 9.8, 10.1]).unwrap();
1209 }
1210 assert!(chart.is_in_control());
1211 }
1212
1213 #[test]
1216 fn test_imr_basic_limits() {
1217 let mut chart = IndividualMRChart::new();
1218 let data = [10.0, 12.0, 11.0, 13.0, 10.0, 14.0, 11.0, 12.0, 13.0, 10.0];
1219 for &x in &data {
1220 chart.add_sample(&[x]).unwrap();
1221 }
1222
1223 let limits = chart.control_limits().expect("should have limits");
1224 assert!(limits.ucl > limits.cl);
1225 assert!(limits.cl > limits.lcl);
1226
1227 let mr_limits = chart.mr_limits().expect("should have MR limits");
1228 assert!(mr_limits.ucl > mr_limits.cl);
1229 assert!((mr_limits.lcl).abs() < f64::EPSILON);
1230 }
1231
1232 #[test]
1233 fn test_imr_needs_two_points() {
1234 let mut chart = IndividualMRChart::new();
1235 chart.add_sample(&[10.0]).unwrap();
1236 assert!(chart.control_limits().is_none());
1237 }
1238
1239 #[test]
1240 fn test_imr_center_line_is_mean() {
1241 let mut chart = IndividualMRChart::new();
1242 let data = [5.0, 10.0, 15.0, 20.0, 25.0];
1243 for &x in &data {
1244 chart.add_sample(&[x]).unwrap();
1245 }
1246 let limits = chart.control_limits().expect("should have limits");
1247 assert!((limits.cl - 15.0).abs() < f64::EPSILON);
1248 }
1249
1250 #[test]
1251 fn test_imr_mr_values() {
1252 let mut chart = IndividualMRChart::new();
1253 let data = [10.0, 12.0, 9.0];
1254 for &x in &data {
1255 chart.add_sample(&[x]).unwrap();
1256 }
1257 let mr_pts = chart.mr_points();
1259 assert_eq!(mr_pts.len(), 2);
1260 assert!((mr_pts[0].value - 2.0).abs() < f64::EPSILON);
1261 assert!((mr_pts[1].value - 3.0).abs() < f64::EPSILON);
1262 }
1263
1264 #[test]
1265 fn test_imr_rejects_multi_element_sample() {
1266 let mut chart = IndividualMRChart::new();
1267 assert_eq!(
1268 chart.add_sample(&[1.0, 2.0]),
1269 Err(ControlChartError::SampleLengthMismatch {
1270 expected: 1,
1271 got: 2
1272 })
1273 );
1274 assert!(chart.points().is_empty());
1275 }
1276
1277 #[test]
1278 fn test_imr_detects_out_of_control() {
1279 let mut chart = IndividualMRChart::new();
1280 for i in 0..10 {
1281 chart.add_sample(&[50.0 + (i as f64 % 3.0) * 0.5]).unwrap();
1282 }
1283 chart.add_sample(&[100.0]).unwrap();
1285
1286 assert!(!chart.is_in_control());
1287 }
1288
1289 #[test]
1290 fn test_imr_default() {
1291 let chart = IndividualMRChart::default();
1292 assert!(chart.points().is_empty());
1293 }
1294
1295 #[test]
1298 fn test_subgroup_range() {
1299 assert!((subgroup_range(&[1.0, 5.0, 3.0]) - 4.0).abs() < f64::EPSILON);
1300 assert!((subgroup_range(&[10.0, 10.0, 10.0])).abs() < f64::EPSILON);
1301 }
1302
1303 #[test]
1306 fn test_xbar_r_chart_factors_n5() {
1307 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1310 chart.add_sample(&[45.0, 47.0, 50.0, 53.0, 55.0]).unwrap();
1311
1312 let limits = chart.control_limits().expect("limits");
1313 assert!((limits.cl - 50.0).abs() < f64::EPSILON);
1314
1315 let r_limits = chart.r_limits().expect("R limits");
1316 assert!((r_limits.cl - 10.0).abs() < f64::EPSILON);
1317
1318 assert!((limits.ucl - 55.77).abs() < 0.01);
1320 assert!((limits.lcl - 44.23).abs() < 0.01);
1322 }
1323
1324 #[test]
1327 fn test_imr_e2_factor() {
1328 let mut chart = IndividualMRChart::new();
1331 chart.add_sample(&[95.0]).unwrap();
1332 chart.add_sample(&[105.0]).unwrap();
1333
1334 let limits = chart.control_limits().expect("limits");
1335 assert!((limits.cl - 100.0).abs() < f64::EPSILON);
1337 assert!((limits.ucl - 126.6).abs() < 0.1);
1340 assert!((limits.lcl - 73.4).abs() < 0.1);
1342 }
1343
1344 #[test]
1353 fn test_d2_constants_montgomery_table_vi() {
1354 let expected: [(usize, f64); 9] = [
1356 (2, 1.128),
1357 (3, 1.693),
1358 (4, 2.059),
1359 (5, 2.326),
1360 (6, 2.534),
1361 (7, 2.704),
1362 (8, 2.847),
1363 (9, 2.970),
1364 (10, 3.078),
1365 ];
1366 for (n, d2_ref) in expected {
1367 let d2 = D2[n - 2];
1368 assert!(
1369 (d2 - d2_ref).abs() < 0.001,
1370 "d2(n={n}): expected {d2_ref}, got {d2}"
1371 );
1372 }
1373 }
1374
1375 #[test]
1384 fn test_c4_constants_montgomery_table_vi() {
1385 let expected: [(usize, f64); 6] = [
1387 (2, 0.7979),
1388 (3, 0.8862),
1389 (4, 0.9213),
1390 (5, 0.9400),
1391 (6, 0.9515),
1392 (7, 0.9594),
1393 ];
1394 for (n, c4_ref) in expected {
1395 let c4 = C4[n - 2];
1396 assert!(
1397 (c4 - c4_ref).abs() < 0.001,
1398 "c4(n={n}): expected {c4_ref}, got {c4}"
1399 );
1400 }
1401 }
1402
1403 #[test]
1411 fn xbar_r_sigma_hat_is_r_bar_over_d2() {
1412 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1413 let groups = [
1414 [9.9, 10.1, 10.0, 9.8, 10.2],
1415 [10.3, 9.7, 10.0, 10.1, 9.9],
1416 [9.8, 10.2, 10.1, 9.9, 10.0],
1417 [10.5, 9.5, 10.0, 10.2, 9.8],
1418 ];
1419 for g in &groups {
1420 chart.add_sample(g).unwrap();
1421 }
1422 let r_bar = chart.r_limits().expect("limits").cl;
1423 let expected = r_bar / D2[5 - 2];
1424 let sigma = chart.sigma_hat().expect("sigma-hat");
1425 assert!(
1426 (sigma - expected).abs() < 1e-12,
1427 "sigma-hat must be R-bar/d2: got {sigma}, expected {expected}"
1428 );
1429 assert!(sigma > 0.0);
1430 }
1431
1432 #[test]
1433 fn xbar_s_sigma_hat_is_s_bar_over_c4() {
1434 let mut chart = XBarSChart::new(5).expect("subgroup size is in range");
1435 for g in &[
1436 [9.9, 10.1, 10.0, 9.8, 10.2],
1437 [10.3, 9.7, 10.0, 10.1, 9.9],
1438 [9.8, 10.2, 10.1, 9.9, 10.0],
1439 ] {
1440 chart.add_sample(g).unwrap();
1441 }
1442 let s_bar = chart.s_limits().expect("limits").cl;
1443 let expected = s_bar / C4[5 - 2];
1444 let sigma = chart.sigma_hat().expect("sigma-hat");
1445 assert!((sigma - expected).abs() < 1e-12);
1446 }
1447
1448 #[test]
1449 fn sigma_hat_is_none_without_limits() {
1450 assert!(XBarRChart::new(5).unwrap().sigma_hat().is_none());
1451 assert!(XBarSChart::new(5).unwrap().sigma_hat().is_none());
1452 }
1453
1454 #[test]
1455 fn short_term_sigma_differs_from_overall_when_subgroups_drift() {
1456 use u_numflow::stats;
1457
1458 let groups = [
1461 [9.9, 10.1, 10.0, 9.8, 10.2],
1462 [10.3, 9.7, 10.0, 10.1, 9.9],
1463 [9.8, 10.2, 10.1, 9.9, 10.0],
1464 [10.5, 9.5, 10.0, 10.2, 9.8],
1465 [9.6, 10.4, 10.0, 9.9, 10.1],
1466 [10.1, 9.9, 10.0, 10.3, 9.7],
1467 ];
1468 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1469 for g in &groups {
1470 chart.add_sample(g).unwrap();
1471 }
1472 let sigma_within = chart.sigma_hat().expect("sigma-hat");
1473 let flat: Vec<f64> = groups.iter().flatten().copied().collect();
1474 let sigma_overall = stats::std_dev(&flat).expect("overall sigma");
1475
1476 assert!(
1477 (sigma_within - sigma_overall).abs() > 1e-6,
1478 "short-term and long-term sigma must differ on drifting subgroups: within={sigma_within}, overall={sigma_overall}"
1479 );
1480 }
1481}