1use super::chart::{
21 ChartPoint, ControlChart, ControlChartError, ControlLimits, Violation, ViolationType,
22 MAX_SUBGROUP_SIZE, MIN_SUBGROUP_SIZE,
23};
24use super::rules::{RuleSet, RunRule};
25
26pub fn range_chart_factors(n: usize) -> Result<(f64, f64, f64), ControlChartError> {
66 if !(MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE).contains(&n) {
67 return Err(ControlChartError::SubgroupSizeOutOfRange {
68 got: n,
69 min: MIN_SUBGROUP_SIZE,
70 max: MAX_SUBGROUP_SIZE,
71 });
72 }
73 Ok((A2[n - 2], D3[n - 2], D4[n - 2]))
74}
75
76const A2: [f64; 24] = [
80 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,
81 0.223, 0.212, 0.203, 0.194, 0.187, 0.180, 0.173, 0.167, 0.162, 0.157, 0.153,
82];
83
84const D3: [f64; 24] = [
88 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,
89 0.347, 0.363, 0.378, 0.391, 0.404, 0.415, 0.425, 0.435, 0.443, 0.452, 0.459,
90];
91
92const D4: [f64; 24] = [
96 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,
97 1.653, 1.637, 1.622, 1.609, 1.596, 1.585, 1.575, 1.565, 1.557, 1.548, 1.541,
98];
99
100const D2: [f64; 24] = [
111 std::f64::consts::FRAC_2_SQRT_PI,
114 1.6926,
115 2.0588,
116 2.3259,
117 2.5344,
118 2.7044,
119 2.8472,
120 2.9700,
121 3.0775,
122 3.1729,
123 3.2585,
124 3.3360,
125 3.4068,
126 3.4718,
127 3.5320,
128 3.5879,
129 3.6401,
130 3.6890,
131 3.7350,
132 3.7783,
133 3.8194,
134 3.8583,
135 3.8953,
136 3.9306,
137];
138
139const A3: [f64; 24] = [
143 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,
144 0.789, 0.763, 0.739, 0.718, 0.698, 0.680, 0.663, 0.647, 0.633, 0.619, 0.606,
145];
146
147const B3: [f64; 24] = [
151 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,
152 0.428, 0.448, 0.466, 0.482, 0.497, 0.510, 0.523, 0.534, 0.545, 0.555, 0.565,
153];
154
155const B4: [f64; 24] = [
159 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,
160 1.572, 1.552, 1.534, 1.518, 1.503, 1.490, 1.477, 1.466, 1.455, 1.445, 1.435,
161];
162
163const C4: [f64; 24] = [
174 0.7979, 0.8862, 0.9213, 0.9400, 0.9515, 0.9594, 0.9650, 0.9693, 0.9727, 0.9754, 0.9776, 0.9794,
175 0.9810, 0.9823, 0.9835, 0.9845, 0.9854, 0.9862, 0.9869, 0.9876, 0.9882, 0.9887, 0.9892, 0.9896,
176];
177
178fn equivalent_df(cv2: f64) -> f64 {
188 let a = 2.0 * cv2;
189 1.0 / a - 3.0 * a / 16.0 + 3.0 * a * a / 64.0 + 0.25
190}
191
192const E2: f64 = 2.660;
197
198const D4_MR: f64 = 3.267;
200
201#[derive(Debug, Clone)]
240pub struct XBarRChart {
241 subgroup_size: usize,
243 subgroups: Vec<Vec<f64>>,
245 xbar_points: Vec<ChartPoint>,
247 r_points: Vec<ChartPoint>,
249 xbar_limits: Option<ControlLimits>,
251 r_limits: Option<ControlLimits>,
253 rules: RuleSet,
255}
256
257impl XBarRChart {
258 #[must_use]
273 pub fn with_rules(mut self, rules: RuleSet) -> Self {
274 self.rules = rules;
275 self.recompute();
276 self
277 }
278
279 pub fn rules(&self) -> RuleSet {
281 self.rules
282 }
283 pub fn new(subgroup_size: usize) -> Result<Self, ControlChartError> {
294 if !(MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE).contains(&subgroup_size) {
295 return Err(ControlChartError::SubgroupSizeOutOfRange {
296 got: subgroup_size,
297 min: MIN_SUBGROUP_SIZE,
298 max: MAX_SUBGROUP_SIZE,
299 });
300 }
301 Ok(Self {
302 subgroup_size,
303 subgroups: Vec::new(),
304 xbar_points: Vec::new(),
305 r_points: Vec::new(),
306 xbar_limits: None,
307 r_limits: None,
308 rules: RuleSet::default(),
309 })
310 }
311
312 pub fn sigma_hat(&self) -> Option<f64> {
336 let r_bar = self.r_limits.as_ref()?.cl;
337 let d2 = D2[self.subgroup_size - 2];
338 (d2 > 0.0).then(|| r_bar / d2)
339 }
340
341 pub fn sigma_hat_df(&self) -> Option<f64> {
366 self.r_limits.as_ref()?;
367 let cv = (D4[self.subgroup_size - 2] - 1.0) / 3.0;
368 Some(equivalent_df(cv * cv / self.subgroups.len() as f64))
369 }
370
371 pub fn r_limits(&self) -> Option<ControlLimits> {
373 self.r_limits.clone()
374 }
375
376 pub fn r_points(&self) -> &[ChartPoint] {
378 &self.r_points
379 }
380
381 fn recompute(&mut self) {
383 if self.subgroups.is_empty() {
384 self.xbar_limits = None;
385 self.r_limits = None;
386 self.xbar_points.clear();
387 self.r_points.clear();
388 return;
389 }
390
391 let idx = self.subgroup_size - 2; let mut xbar_values = Vec::with_capacity(self.subgroups.len());
395 let mut r_values = Vec::with_capacity(self.subgroups.len());
396
397 for subgroup in &self.subgroups {
398 let mean_val = u_numflow::stats::mean(subgroup)
399 .expect("subgroup should be non-empty with finite values");
400 let range = subgroup_range(subgroup);
401 xbar_values.push(mean_val);
402 r_values.push(range);
403 }
404
405 let grand_mean = u_numflow::stats::mean(&xbar_values)
407 .expect("xbar_values should be non-empty with finite values");
408 let r_bar = u_numflow::stats::mean(&r_values)
409 .expect("r_values should be non-empty with finite values");
410
411 let a2 = A2[idx];
413 self.xbar_limits = Some(ControlLimits {
414 ucl: grand_mean + a2 * r_bar,
415 cl: grand_mean,
416 lcl: grand_mean - a2 * r_bar,
417 });
418
419 let d3 = D3[idx];
421 let d4 = D4[idx];
422 self.r_limits = Some(ControlLimits {
423 ucl: d4 * r_bar,
424 cl: r_bar,
425 lcl: d3 * r_bar,
426 });
427
428 self.xbar_points = xbar_values
430 .iter()
431 .enumerate()
432 .map(|(i, &v)| ChartPoint {
433 value: v,
434 index: i,
435 violations: Vec::new(),
436 })
437 .collect();
438
439 self.r_points = r_values
440 .iter()
441 .enumerate()
442 .map(|(i, &v)| ChartPoint {
443 value: v,
444 index: i,
445 violations: Vec::new(),
446 })
447 .collect();
448
449 if let Some(ref limits) = self.xbar_limits {
451 let violations = self.rules.check(&self.xbar_points, limits);
452 apply_violations(&mut self.xbar_points, &violations);
453 }
454
455 if let Some(ref limits) = self.r_limits {
457 let violations = self.rules.check(&self.r_points, limits);
458 apply_violations(&mut self.r_points, &violations);
459 }
460 }
461}
462
463fn check_sample(sample: &[f64], expected: usize) -> Result<(), ControlChartError> {
465 if sample.len() != expected {
466 return Err(ControlChartError::SampleLengthMismatch {
467 expected,
468 got: sample.len(),
469 });
470 }
471 if !sample.iter().all(|x| x.is_finite()) {
472 return Err(ControlChartError::NonFiniteValue);
473 }
474 Ok(())
475}
476
477impl ControlChart for XBarRChart {
478 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
480 check_sample(sample, self.subgroup_size)?;
481 self.subgroups.push(sample.to_vec());
482 self.recompute();
483 Ok(())
484 }
485
486 fn control_limits(&self) -> Option<ControlLimits> {
487 self.xbar_limits.clone()
488 }
489
490 fn is_in_control(&self) -> bool {
491 self.xbar_points.iter().all(|p| p.violations.is_empty())
492 && self.r_points.iter().all(|p| p.violations.is_empty())
493 }
494
495 fn violations(&self) -> Vec<Violation> {
496 collect_violations(&self.xbar_points)
497 .into_iter()
498 .chain(collect_violations(&self.r_points))
499 .collect()
500 }
501
502 fn points(&self) -> &[ChartPoint] {
503 &self.xbar_points
504 }
505}
506
507#[derive(Debug, Clone)]
529pub struct XBarSChart {
530 subgroup_size: usize,
532 subgroups: Vec<Vec<f64>>,
534 xbar_points: Vec<ChartPoint>,
536 s_points: Vec<ChartPoint>,
538 xbar_limits: Option<ControlLimits>,
540 s_limits: Option<ControlLimits>,
542 rules: RuleSet,
544}
545
546impl XBarSChart {
547 #[must_use]
562 pub fn with_rules(mut self, rules: RuleSet) -> Self {
563 self.rules = rules;
564 self.recompute();
565 self
566 }
567
568 pub fn rules(&self) -> RuleSet {
570 self.rules
571 }
572 pub fn new(subgroup_size: usize) -> Result<Self, ControlChartError> {
583 if !(MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE).contains(&subgroup_size) {
584 return Err(ControlChartError::SubgroupSizeOutOfRange {
585 got: subgroup_size,
586 min: MIN_SUBGROUP_SIZE,
587 max: MAX_SUBGROUP_SIZE,
588 });
589 }
590 Ok(Self {
591 subgroup_size,
592 subgroups: Vec::new(),
593 xbar_points: Vec::new(),
594 s_points: Vec::new(),
595 xbar_limits: None,
596 s_limits: None,
597 rules: RuleSet::default(),
598 })
599 }
600
601 pub fn sigma_hat(&self) -> Option<f64> {
621 let s_bar = self.s_limits.as_ref()?.cl;
622 let c4 = C4[self.subgroup_size - 2];
623 (c4 > 0.0).then(|| s_bar / c4)
624 }
625
626 pub fn sigma_hat_df(&self) -> Option<f64> {
634 self.s_limits.as_ref()?;
635 let c4 = C4[self.subgroup_size - 2];
636 Some(equivalent_df(
637 (1.0 - c4 * c4) / (c4 * c4 * self.subgroups.len() as f64),
638 ))
639 }
640
641 pub fn s_limits(&self) -> Option<ControlLimits> {
643 self.s_limits.clone()
644 }
645
646 pub fn s_points(&self) -> &[ChartPoint] {
648 &self.s_points
649 }
650
651 fn recompute(&mut self) {
653 if self.subgroups.is_empty() {
654 self.xbar_limits = None;
655 self.s_limits = None;
656 self.xbar_points.clear();
657 self.s_points.clear();
658 return;
659 }
660
661 let idx = self.subgroup_size - 2;
662
663 let mut xbar_values = Vec::with_capacity(self.subgroups.len());
665 let mut s_values = Vec::with_capacity(self.subgroups.len());
666
667 for subgroup in &self.subgroups {
668 let mean_val = u_numflow::stats::mean(subgroup)
669 .expect("subgroup should be non-empty with finite values");
670 let sd = u_numflow::stats::std_dev(subgroup)
671 .expect("subgroup should have >= 2 elements for std_dev");
672 xbar_values.push(mean_val);
673 s_values.push(sd);
674 }
675
676 let grand_mean = u_numflow::stats::mean(&xbar_values)
678 .expect("xbar_values should be non-empty with finite values");
679 let s_bar = u_numflow::stats::mean(&s_values)
680 .expect("s_values should be non-empty with finite values");
681
682 let a3 = A3[idx];
684 self.xbar_limits = Some(ControlLimits {
685 ucl: grand_mean + a3 * s_bar,
686 cl: grand_mean,
687 lcl: grand_mean - a3 * s_bar,
688 });
689
690 let b3 = B3[idx];
692 let b4 = B4[idx];
693 self.s_limits = Some(ControlLimits {
694 ucl: b4 * s_bar,
695 cl: s_bar,
696 lcl: b3 * s_bar,
697 });
698
699 self.xbar_points = xbar_values
701 .iter()
702 .enumerate()
703 .map(|(i, &v)| ChartPoint {
704 value: v,
705 index: i,
706 violations: Vec::new(),
707 })
708 .collect();
709
710 self.s_points = s_values
711 .iter()
712 .enumerate()
713 .map(|(i, &v)| ChartPoint {
714 value: v,
715 index: i,
716 violations: Vec::new(),
717 })
718 .collect();
719
720 if let Some(ref limits) = self.xbar_limits {
722 let violations = self.rules.check(&self.xbar_points, limits);
723 apply_violations(&mut self.xbar_points, &violations);
724 }
725
726 if let Some(ref limits) = self.s_limits {
728 let violations = self.rules.check(&self.s_points, limits);
729 apply_violations(&mut self.s_points, &violations);
730 }
731 }
732}
733
734impl ControlChart for XBarSChart {
735 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
737 check_sample(sample, self.subgroup_size)?;
738 self.subgroups.push(sample.to_vec());
739 self.recompute();
740 Ok(())
741 }
742
743 fn control_limits(&self) -> Option<ControlLimits> {
744 self.xbar_limits.clone()
745 }
746
747 fn is_in_control(&self) -> bool {
748 self.xbar_points.iter().all(|p| p.violations.is_empty())
749 && self.s_points.iter().all(|p| p.violations.is_empty())
750 }
751
752 fn violations(&self) -> Vec<Violation> {
753 collect_violations(&self.xbar_points)
754 .into_iter()
755 .chain(collect_violations(&self.s_points))
756 .collect()
757 }
758
759 fn points(&self) -> &[ChartPoint] {
760 &self.xbar_points
761 }
762}
763
764#[derive(Debug, Clone)]
801pub struct IndividualMRChart {
802 observations: Vec<f64>,
804 i_points: Vec<ChartPoint>,
806 mr_points: Vec<ChartPoint>,
808 i_limits: Option<ControlLimits>,
810 mr_limits: Option<ControlLimits>,
812 rules: RuleSet,
814}
815
816impl IndividualMRChart {
817 #[must_use]
832 pub fn with_rules(mut self, rules: RuleSet) -> Self {
833 self.rules = rules;
834 self.recompute();
835 self
836 }
837
838 pub fn rules(&self) -> RuleSet {
840 self.rules
841 }
842 pub fn new() -> Self {
844 Self {
845 observations: Vec::new(),
846 i_points: Vec::new(),
847 mr_points: Vec::new(),
848 i_limits: None,
849 mr_limits: None,
850 rules: RuleSet::default(),
851 }
852 }
853
854 pub fn sigma_hat(&self) -> Option<f64> {
878 let mr_bar = self.mr_limits.as_ref()?.cl;
879 Some(mr_bar / D2[0])
880 }
881
882 pub fn sigma_hat_df(&self) -> Option<f64> {
890 self.mr_limits.as_ref()?;
891 Some(0.62 * (self.observations.len() - 1) as f64)
892 }
893
894 pub fn mr_limits(&self) -> Option<ControlLimits> {
896 self.mr_limits.clone()
897 }
898
899 pub fn mr_points(&self) -> &[ChartPoint] {
901 &self.mr_points
902 }
903
904 fn recompute(&mut self) {
906 if self.observations.len() < 2 {
907 self.i_limits = None;
908 self.mr_limits = None;
909 self.i_points.clear();
910 self.mr_points.clear();
911 return;
912 }
913
914 let mr_values: Vec<f64> = self
916 .observations
917 .windows(2)
918 .map(|w| (w[1] - w[0]).abs())
919 .collect();
920
921 let x_bar = u_numflow::stats::mean(&self.observations)
923 .expect("observations should be non-empty with finite values");
924 let mr_bar = u_numflow::stats::mean(&mr_values)
925 .expect("mr_values should be non-empty with finite values");
926
927 self.i_limits = Some(ControlLimits {
929 ucl: x_bar + E2 * mr_bar,
930 cl: x_bar,
931 lcl: x_bar - E2 * mr_bar,
932 });
933
934 self.mr_limits = Some(ControlLimits {
936 ucl: D4_MR * mr_bar,
937 cl: mr_bar,
938 lcl: 0.0,
939 });
940
941 self.i_points = self
943 .observations
944 .iter()
945 .enumerate()
946 .map(|(i, &v)| ChartPoint {
947 value: v,
948 index: i,
949 violations: Vec::new(),
950 })
951 .collect();
952
953 self.mr_points = mr_values
955 .iter()
956 .enumerate()
957 .map(|(i, &v)| ChartPoint {
958 value: v,
959 index: i + 1,
960 violations: Vec::new(),
961 })
962 .collect();
963
964 if let Some(ref limits) = self.i_limits {
966 let violations = self.rules.check(&self.i_points, limits);
967 apply_violations(&mut self.i_points, &violations);
968 }
969
970 if let Some(ref limits) = self.mr_limits {
972 let violations = self.rules.check(&self.mr_points, limits);
973 apply_violations(&mut self.mr_points, &violations);
974 }
975 }
976}
977
978impl Default for IndividualMRChart {
979 fn default() -> Self {
980 Self::new()
981 }
982}
983
984impl ControlChart for IndividualMRChart {
985 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
987 check_sample(sample, 1)?;
988 self.observations.push(sample[0]);
989 self.recompute();
990 Ok(())
991 }
992
993 fn control_limits(&self) -> Option<ControlLimits> {
994 self.i_limits.clone()
995 }
996
997 fn is_in_control(&self) -> bool {
998 self.i_points.iter().all(|p| p.violations.is_empty())
999 && self.mr_points.iter().all(|p| p.violations.is_empty())
1000 }
1001
1002 fn violations(&self) -> Vec<Violation> {
1003 collect_violations(&self.i_points)
1004 .into_iter()
1005 .chain(collect_violations(&self.mr_points))
1006 .collect()
1007 }
1008
1009 fn points(&self) -> &[ChartPoint] {
1010 &self.i_points
1011 }
1012}
1013
1014fn subgroup_range(subgroup: &[f64]) -> f64 {
1024 let max_val =
1025 u_numflow::stats::max(subgroup).expect("subgroup should be non-empty without NaN");
1026 let min_val =
1027 u_numflow::stats::min(subgroup).expect("subgroup should be non-empty without NaN");
1028 max_val - min_val
1029}
1030
1031fn apply_violations(points: &mut [ChartPoint], violations: &[(usize, ViolationType)]) {
1033 for &(idx, vtype) in violations {
1034 if let Some(point) = points.iter_mut().find(|p| p.index == idx) {
1035 point.violations.push(vtype);
1036 }
1037 }
1038}
1039
1040fn collect_violations(points: &[ChartPoint]) -> Vec<Violation> {
1042 let mut result = Vec::new();
1043 for point in points {
1044 for &vtype in &point.violations {
1045 result.push(Violation {
1046 point_index: point.index,
1047 violation_type: vtype,
1048 });
1049 }
1050 }
1051 result
1052}
1053
1054#[cfg(test)]
1059mod tests {
1060 use super::*;
1061
1062 #[test]
1065 fn test_xbar_r_basic_limits() {
1066 let mut chart = XBarRChart::new(4).expect("subgroup size is in range");
1067 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]).unwrap();
1068 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]).unwrap();
1069 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]).unwrap();
1070 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]).unwrap();
1071 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]).unwrap();
1072
1073 let limits = chart.control_limits().expect("should have limits");
1074 let expected_grand_mean = (71.0 + 54.5 + 52.5 + 63.0 + 57.25) / 5.0;
1076 assert!(
1077 (limits.cl - expected_grand_mean).abs() < 0.1,
1078 "CL={}, expected ~{expected_grand_mean}",
1079 limits.cl
1080 );
1081
1082 assert!(limits.ucl > limits.cl);
1084 assert!(limits.cl > limits.lcl);
1085 }
1086
1087 #[test]
1088 fn test_xbar_r_rejects_wrong_size() {
1089 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1090 assert_eq!(
1091 chart.add_sample(&[1.0, 2.0, 3.0]),
1092 Err(ControlChartError::SampleLengthMismatch {
1093 expected: 5,
1094 got: 3
1095 })
1096 );
1097 assert!(chart.control_limits().is_none());
1098 }
1099
1100 #[test]
1101 fn test_xbar_r_rejects_nan() {
1102 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1103 assert_eq!(
1104 chart.add_sample(&[1.0, f64::NAN, 3.0]),
1105 Err(ControlChartError::NonFiniteValue)
1106 );
1107 assert!(chart.control_limits().is_none());
1108 }
1109
1110 #[test]
1111 fn test_xbar_r_r_chart_limits() {
1112 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1113 chart.add_sample(&[10.0, 12.0, 11.0, 13.0, 14.0]).unwrap();
1114 chart.add_sample(&[11.0, 13.0, 12.0, 10.0, 15.0]).unwrap();
1115 chart.add_sample(&[12.0, 11.0, 14.0, 13.0, 10.0]).unwrap();
1116
1117 let r_limits = chart.r_limits().expect("should have R limits");
1118 assert!(r_limits.ucl > r_limits.cl);
1119 assert!(r_limits.lcl >= 0.0);
1120 }
1121
1122 #[test]
1123 fn test_xbar_r_constant_subgroups() {
1124 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1126 chart.add_sample(&[10.0, 10.0, 10.0]).unwrap();
1127 chart.add_sample(&[10.0, 10.0, 10.0]).unwrap();
1128
1129 let limits = chart.control_limits().expect("should have limits");
1130 assert!((limits.cl - 10.0).abs() < f64::EPSILON);
1131 assert!((limits.ucl - 10.0).abs() < f64::EPSILON);
1132 assert!((limits.lcl - 10.0).abs() < f64::EPSILON);
1133 }
1134
1135 #[test]
1136 fn test_xbar_r_detects_out_of_control() {
1137 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1138 for _ in 0..5 {
1139 chart.add_sample(&[10.0, 10.5, 9.5]).unwrap();
1140 }
1141 chart.add_sample(&[50.0, 51.0, 49.0]).unwrap();
1143
1144 assert!(!chart.is_in_control());
1145 }
1146
1147 #[test]
1155 fn factor_tables_reproduce_the_published_table() {
1156 const PUB_A2: [f64; 24] = [
1161 1.880, 1.023, 0.729, 0.577, 0.483, 0.419, 0.373, 0.337, 0.308, 0.285, 0.266, 0.249,
1162 0.235, 0.223, 0.212, 0.203, 0.194, 0.187, 0.180, 0.173, 0.167, 0.162, 0.157, 0.153,
1163 ];
1164 const PUB_A3: [f64; 24] = [
1165 2.659, 1.954, 1.628, 1.427, 1.287, 1.182, 1.099, 1.032, 0.975, 0.927, 0.886, 0.850,
1166 0.817, 0.789, 0.763, 0.739, 0.718, 0.698, 0.680, 0.663, 0.647, 0.633, 0.619, 0.606,
1167 ];
1168 const PUB_C4: [f64; 24] = [
1169 0.7979, 0.8862, 0.9213, 0.9400, 0.9515, 0.9594, 0.9650, 0.9693, 0.9727, 0.9754, 0.9776,
1170 0.9794, 0.9810, 0.9823, 0.9835, 0.9845, 0.9854, 0.9862, 0.9869, 0.9876, 0.9882, 0.9887,
1171 0.9892, 0.9896,
1172 ];
1173 const PUB_B3: [f64; 24] = [
1174 0.0, 0.0, 0.0, 0.0, 0.030, 0.118, 0.185, 0.239, 0.284, 0.321, 0.354, 0.382, 0.406,
1175 0.428, 0.448, 0.466, 0.482, 0.497, 0.510, 0.523, 0.534, 0.545, 0.555, 0.565,
1176 ];
1177 const PUB_B4: [f64; 24] = [
1178 3.267, 2.568, 2.266, 2.089, 1.970, 1.882, 1.815, 1.761, 1.716, 1.679, 1.646, 1.618,
1179 1.594, 1.572, 1.552, 1.534, 1.518, 1.503, 1.490, 1.477, 1.466, 1.455, 1.445, 1.435,
1180 ];
1181 const PUB_D2: [f64; 24] = [
1182 1.128, 1.693, 2.059, 2.326, 2.534, 2.704, 2.847, 2.970, 3.078, 3.173, 3.258, 3.336,
1183 3.407, 3.472, 3.532, 3.588, 3.640, 3.689, 3.735, 3.778, 3.819, 3.858, 3.895, 3.931,
1184 ];
1185 const PUB_D3: [f64; 24] = [
1186 0.0, 0.0, 0.0, 0.0, 0.0, 0.076, 0.137, 0.184, 0.223, 0.256, 0.284, 0.307, 0.328, 0.347,
1187 0.363, 0.378, 0.392, 0.404, 0.415, 0.425, 0.435, 0.444, 0.452, 0.460,
1188 ];
1189 const PUB_D4: [f64; 24] = [
1190 3.266, 2.574, 2.281, 2.114, 2.003, 1.924, 1.863, 1.816, 1.777, 1.744, 1.716, 1.693,
1191 1.672, 1.653, 1.637, 1.622, 1.608, 1.596, 1.585, 1.575, 1.565, 1.556, 1.548, 1.540,
1192 ];
1193
1194 for (name, table, published, tol) in [
1199 ("A2", &A2, &PUB_A2, 5e-4),
1200 ("A3", &A3, &PUB_A3, 5e-4),
1201 ("c4", &C4, &PUB_C4, 5e-5),
1202 ("B3", &B3, &PUB_B3, 5e-4),
1203 ("B4", &B4, &PUB_B4, 5e-4),
1204 ("d2", &D2, &PUB_D2, 5.5e-4),
1205 ] {
1206 for (i, (got, want)) in table.iter().zip(published).enumerate() {
1207 assert!(
1208 (got - want).abs() <= tol,
1209 "{name}[n={}] = {got}, published {want}",
1210 i + 2
1211 );
1212 }
1213 }
1214
1215 for (name, table, published) in [("D3", &D3, &PUB_D3), ("D4", &D4, &PUB_D4)] {
1224 for (i, (got, want)) in table.iter().zip(published).enumerate() {
1225 assert!(
1226 (got - want).abs() <= 1e-3 + 1e-12,
1227 "{name}[n={}] = {got}, published {want}",
1228 i + 2
1229 );
1230 }
1231 }
1232 for i in 0..24 {
1233 if D3[i] > 0.0 {
1234 assert!(
1235 (D3[i] + D4[i] - 2.0).abs() < 1e-12,
1236 "D3 + D4 at n = {} is {}",
1237 i + 2,
1238 D3[i] + D4[i]
1239 );
1240 }
1241 }
1242 }
1243
1244 #[test]
1245 fn test_xbar_r_subgroup_size_below_range_is_an_error() {
1246 assert_eq!(
1247 XBarRChart::new(1).unwrap_err(),
1248 ControlChartError::SubgroupSizeOutOfRange {
1249 got: 1,
1250 min: 2,
1251 max: 25
1252 }
1253 );
1254 }
1255
1256 #[test]
1259 fn factor_tables_cover_the_declared_range() {
1260 let span = MAX_SUBGROUP_SIZE - MIN_SUBGROUP_SIZE + 1;
1261 for (name, len) in [
1262 ("A2", A2.len()),
1263 ("D3", D3.len()),
1264 ("D4", D4.len()),
1265 ("d2", D2.len()),
1266 ("A3", A3.len()),
1267 ("B3", B3.len()),
1268 ("B4", B4.len()),
1269 ("c4", C4.len()),
1270 ] {
1271 assert_eq!(
1272 len, span,
1273 "{name} does not cover {MIN_SUBGROUP_SIZE}..={MAX_SUBGROUP_SIZE}"
1274 );
1275 }
1276 for i in 1..span {
1278 assert!(D2[i] > D2[i - 1], "d2 not increasing at n={}", i + 2);
1279 assert!(C4[i] > C4[i - 1], "c4 not increasing at n={}", i + 2);
1280 assert!(A2[i] < A2[i - 1], "A2 not decreasing at n={}", i + 2);
1281 assert!(A3[i] < A3[i - 1], "A3 not decreasing at n={}", i + 2);
1282 assert!(D4[i] < D4[i - 1], "D4 not decreasing at n={}", i + 2);
1283 assert!(B4[i] < B4[i - 1], "B4 not decreasing at n={}", i + 2);
1284 assert!(D3[i] >= D3[i - 1], "D3 not non-decreasing at n={}", i + 2);
1285 assert!(B3[i] >= B3[i - 1], "B3 not non-decreasing at n={}", i + 2);
1286 }
1287 assert!(C4[span - 1] < 1.0 && C4[span - 1] > 0.98);
1289 }
1290
1291 #[test]
1292 fn test_xbar_r_subgroup_size_above_range_is_an_error() {
1293 assert_eq!(
1294 XBarRChart::new(26).unwrap_err(),
1295 ControlChartError::SubgroupSizeOutOfRange {
1296 got: 26,
1297 min: 2,
1298 max: 25
1299 }
1300 );
1301 }
1302
1303 #[test]
1304 fn test_subgroup_size_11_is_now_accepted() {
1305 let mut chart = XBarRChart::new(11).expect("11 is within 2..=25");
1308 for _ in 0..5 {
1309 chart
1310 .add_sample(&[10.0, 10.2, 9.8, 10.1, 9.9, 10.3, 9.7, 10.0, 10.1, 9.9, 10.0])
1311 .unwrap();
1312 }
1313 let limits = chart.control_limits().expect("limits from 5 subgroups");
1314 assert!(limits.ucl > limits.cl && limits.cl > limits.lcl);
1315 assert!(chart.sigma_hat().is_some_and(|s| s > 0.0));
1316 }
1317
1318 #[test]
1321 fn test_xbar_s_basic_limits() {
1322 let mut chart = XBarSChart::new(4).expect("subgroup size is in range");
1323 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]).unwrap();
1324 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]).unwrap();
1325 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]).unwrap();
1326 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]).unwrap();
1327 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]).unwrap();
1328
1329 let limits = chart.control_limits().expect("should have limits");
1330 assert!(limits.ucl > limits.cl);
1331 assert!(limits.cl > limits.lcl);
1332
1333 let s_limits = chart.s_limits().expect("should have S limits");
1334 assert!(s_limits.ucl > s_limits.cl);
1335 assert!(s_limits.lcl >= 0.0);
1336 }
1337
1338 #[test]
1339 fn test_xbar_s_rejects_wrong_size() {
1340 let mut chart = XBarSChart::new(5).expect("subgroup size is in range");
1341 assert_eq!(
1342 chart.add_sample(&[1.0, 2.0]),
1343 Err(ControlChartError::SampleLengthMismatch {
1344 expected: 5,
1345 got: 2
1346 })
1347 );
1348 assert!(chart.control_limits().is_none());
1349 }
1350
1351 #[test]
1352 fn test_xbar_s_in_control() {
1353 let mut chart = XBarSChart::new(4).expect("subgroup size is in range");
1354 for _ in 0..10 {
1355 chart.add_sample(&[10.0, 10.2, 9.8, 10.1]).unwrap();
1356 }
1357 assert!(chart.is_in_control());
1358 }
1359
1360 #[test]
1363 fn test_imr_basic_limits() {
1364 let mut chart = IndividualMRChart::new();
1365 let data = [10.0, 12.0, 11.0, 13.0, 10.0, 14.0, 11.0, 12.0, 13.0, 10.0];
1366 for &x in &data {
1367 chart.add_sample(&[x]).unwrap();
1368 }
1369
1370 let limits = chart.control_limits().expect("should have limits");
1371 assert!(limits.ucl > limits.cl);
1372 assert!(limits.cl > limits.lcl);
1373
1374 let mr_limits = chart.mr_limits().expect("should have MR limits");
1375 assert!(mr_limits.ucl > mr_limits.cl);
1376 assert!((mr_limits.lcl).abs() < f64::EPSILON);
1377 }
1378
1379 #[test]
1380 fn test_imr_needs_two_points() {
1381 let mut chart = IndividualMRChart::new();
1382 chart.add_sample(&[10.0]).unwrap();
1383 assert!(chart.control_limits().is_none());
1384 }
1385
1386 #[test]
1387 fn test_imr_center_line_is_mean() {
1388 let mut chart = IndividualMRChart::new();
1389 let data = [5.0, 10.0, 15.0, 20.0, 25.0];
1390 for &x in &data {
1391 chart.add_sample(&[x]).unwrap();
1392 }
1393 let limits = chart.control_limits().expect("should have limits");
1394 assert!((limits.cl - 15.0).abs() < f64::EPSILON);
1395 }
1396
1397 #[test]
1398 fn test_imr_mr_values() {
1399 let mut chart = IndividualMRChart::new();
1400 let data = [10.0, 12.0, 9.0];
1401 for &x in &data {
1402 chart.add_sample(&[x]).unwrap();
1403 }
1404 let mr_pts = chart.mr_points();
1406 assert_eq!(mr_pts.len(), 2);
1407 assert!((mr_pts[0].value - 2.0).abs() < f64::EPSILON);
1408 assert!((mr_pts[1].value - 3.0).abs() < f64::EPSILON);
1409 }
1410
1411 #[test]
1412 fn test_imr_rejects_multi_element_sample() {
1413 let mut chart = IndividualMRChart::new();
1414 assert_eq!(
1415 chart.add_sample(&[1.0, 2.0]),
1416 Err(ControlChartError::SampleLengthMismatch {
1417 expected: 1,
1418 got: 2
1419 })
1420 );
1421 assert!(chart.points().is_empty());
1422 }
1423
1424 #[test]
1425 fn test_imr_detects_out_of_control() {
1426 let mut chart = IndividualMRChart::new();
1427 for i in 0..10 {
1428 chart.add_sample(&[50.0 + (i as f64 % 3.0) * 0.5]).unwrap();
1429 }
1430 chart.add_sample(&[100.0]).unwrap();
1432
1433 assert!(!chart.is_in_control());
1434 }
1435
1436 #[test]
1437 fn test_imr_default() {
1438 let chart = IndividualMRChart::default();
1439 assert!(chart.points().is_empty());
1440 }
1441
1442 #[test]
1445 fn test_subgroup_range() {
1446 assert!((subgroup_range(&[1.0, 5.0, 3.0]) - 4.0).abs() < f64::EPSILON);
1447 assert!((subgroup_range(&[10.0, 10.0, 10.0])).abs() < f64::EPSILON);
1448 }
1449
1450 #[test]
1453 fn test_xbar_r_chart_factors_n5() {
1454 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1457 chart.add_sample(&[45.0, 47.0, 50.0, 53.0, 55.0]).unwrap();
1458
1459 let limits = chart.control_limits().expect("limits");
1460 assert!((limits.cl - 50.0).abs() < f64::EPSILON);
1461
1462 let r_limits = chart.r_limits().expect("R limits");
1463 assert!((r_limits.cl - 10.0).abs() < f64::EPSILON);
1464
1465 assert!((limits.ucl - 55.77).abs() < 0.01);
1467 assert!((limits.lcl - 44.23).abs() < 0.01);
1469 }
1470
1471 #[test]
1474 fn test_imr_e2_factor() {
1475 let mut chart = IndividualMRChart::new();
1478 chart.add_sample(&[95.0]).unwrap();
1479 chart.add_sample(&[105.0]).unwrap();
1480
1481 let limits = chart.control_limits().expect("limits");
1482 assert!((limits.cl - 100.0).abs() < f64::EPSILON);
1484 assert!((limits.ucl - 126.6).abs() < 0.1);
1487 assert!((limits.lcl - 73.4).abs() < 0.1);
1489 }
1490
1491 #[test]
1503 fn xbar_r_sigma_hat_is_r_bar_over_d2() {
1504 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1505 let groups = [
1506 [9.9, 10.1, 10.0, 9.8, 10.2],
1507 [10.3, 9.7, 10.0, 10.1, 9.9],
1508 [9.8, 10.2, 10.1, 9.9, 10.0],
1509 [10.5, 9.5, 10.0, 10.2, 9.8],
1510 ];
1511 for g in &groups {
1512 chart.add_sample(g).unwrap();
1513 }
1514 let r_bar = chart.r_limits().expect("limits").cl;
1515 let expected = r_bar / D2[5 - 2];
1516 let sigma = chart.sigma_hat().expect("sigma-hat");
1517 assert!(
1518 (sigma - expected).abs() < 1e-12,
1519 "sigma-hat must be R-bar/d2: got {sigma}, expected {expected}"
1520 );
1521 assert!(sigma > 0.0);
1522 }
1523
1524 #[test]
1525 fn xbar_s_sigma_hat_is_s_bar_over_c4() {
1526 let mut chart = XBarSChart::new(5).expect("subgroup size is in range");
1527 for g in &[
1528 [9.9, 10.1, 10.0, 9.8, 10.2],
1529 [10.3, 9.7, 10.0, 10.1, 9.9],
1530 [9.8, 10.2, 10.1, 9.9, 10.0],
1531 ] {
1532 chart.add_sample(g).unwrap();
1533 }
1534 let s_bar = chart.s_limits().expect("limits").cl;
1535 let expected = s_bar / C4[5 - 2];
1536 let sigma = chart.sigma_hat().expect("sigma-hat");
1537 assert!((sigma - expected).abs() < 1e-12);
1538 }
1539
1540 #[test]
1541 fn sigma_hat_is_none_without_limits() {
1542 assert!(XBarRChart::new(5).unwrap().sigma_hat().is_none());
1543 assert!(XBarSChart::new(5).unwrap().sigma_hat().is_none());
1544 assert!(XBarRChart::new(5).unwrap().sigma_hat_df().is_none());
1545 assert!(XBarSChart::new(5).unwrap().sigma_hat_df().is_none());
1546 assert!(IndividualMRChart::new().sigma_hat_df().is_none());
1547 }
1548
1549 #[test]
1553 fn equivalent_df_recovers_a_standard_deviations_df() {
1554 for (i, c4) in C4.iter().enumerate() {
1555 let v = (i + 1) as f64; let df = equivalent_df((1.0 - c4 * c4) / (c4 * c4));
1557 assert!(
1559 (df - v).abs() < 0.05 + 0.01 * v,
1560 "n = {}: {df} vs {v}",
1561 i + 2
1562 );
1563 }
1564 }
1565
1566 #[test]
1569 fn single_range_df_matches_patnaiks_table() {
1570 for (n, want) in [(2, 1.0), (3, 2.0), (4, 2.9), (5, 3.8), (6, 4.7)] {
1571 let cv = (D4[n - 2] - 1.0) / 3.0;
1572 let df = equivalent_df(cv * cv);
1573 assert!((df - want).abs() < 0.06, "n = {n}: {df} vs {want}");
1574 }
1575 }
1576
1577 #[test]
1580 fn chart_df_grow_with_subgroups_and_stay_under_pooled() {
1581 let mut r = XBarRChart::new(5).unwrap();
1582 let mut s = XBarSChart::new(5).unwrap();
1583 let mut last = 0.0;
1584 for k in 0..20 {
1585 let g = [
1586 10.0 + 0.1 * (k % 3) as f64,
1587 10.2,
1588 9.9,
1589 10.1,
1590 9.8 + 0.05 * k as f64,
1591 ];
1592 r.add_sample(&g).unwrap();
1593 s.add_sample(&g).unwrap();
1594 if let (Some(dr), Some(ds)) = (r.sigma_hat_df(), s.sigma_hat_df()) {
1595 let pooled = ((k + 1) * 4) as f64;
1596 assert!(
1597 dr > last && dr < pooled && ds < pooled + 0.5 && ds > dr,
1598 "k={k}: {dr} {ds}"
1599 );
1600 last = dr;
1601 }
1602 }
1603 let mut i = IndividualMRChart::new();
1604 for x in [1.0, 3.0, 2.0, 4.0, 3.5] {
1605 i.add_sample(&[x]).unwrap();
1606 }
1607 assert!((i.sigma_hat_df().unwrap() - 0.62 * 4.0).abs() < 1e-12);
1608 }
1609
1610 #[test]
1611 fn short_term_sigma_differs_from_overall_when_subgroups_drift() {
1612 use u_numflow::stats;
1613
1614 let groups = [
1617 [9.9, 10.1, 10.0, 9.8, 10.2],
1618 [10.3, 9.7, 10.0, 10.1, 9.9],
1619 [9.8, 10.2, 10.1, 9.9, 10.0],
1620 [10.5, 9.5, 10.0, 10.2, 9.8],
1621 [9.6, 10.4, 10.0, 9.9, 10.1],
1622 [10.1, 9.9, 10.0, 10.3, 9.7],
1623 ];
1624 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1625 for g in &groups {
1626 chart.add_sample(g).unwrap();
1627 }
1628 let sigma_within = chart.sigma_hat().expect("sigma-hat");
1629 let flat: Vec<f64> = groups.iter().flatten().copied().collect();
1630 let sigma_overall = stats::std_dev(&flat).expect("overall sigma");
1631
1632 assert!(
1633 (sigma_within - sigma_overall).abs() > 1e-6,
1634 "short-term and long-term sigma must differ on drifting subgroups: within={sigma_within}, overall={sigma_overall}"
1635 );
1636 }
1637}