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> {
69 if !(MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE).contains(&n) {
70 return Err(ControlChartError::SubgroupSizeOutOfRange {
71 got: n,
72 min: MIN_SUBGROUP_SIZE,
73 max: MAX_SUBGROUP_SIZE,
74 });
75 }
76 Ok((A2[n - 2], D3[n - 2], D4[n - 2]))
77}
78
79const A2: [f64; 24] = [
83 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,
84 0.223, 0.212, 0.203, 0.194, 0.187, 0.180, 0.173, 0.167, 0.162, 0.157, 0.153,
85];
86
87const D3: [f64; 24] = [
91 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,
92 0.347, 0.363, 0.378, 0.391, 0.404, 0.415, 0.425, 0.435, 0.443, 0.452, 0.459,
93];
94
95const D4: [f64; 24] = [
99 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,
100 1.653, 1.637, 1.622, 1.609, 1.596, 1.585, 1.575, 1.565, 1.557, 1.548, 1.541,
101];
102
103const D2: [f64; 24] = [
114 std::f64::consts::FRAC_2_SQRT_PI,
117 1.6926,
118 2.0588,
119 2.3259,
120 2.5344,
121 2.7044,
122 2.8472,
123 2.9700,
124 3.0775,
125 3.1729,
126 3.2585,
127 3.3360,
128 3.4068,
129 3.4718,
130 3.5320,
131 3.5879,
132 3.6401,
133 3.6890,
134 3.7350,
135 3.7783,
136 3.8194,
137 3.8583,
138 3.8953,
139 3.9306,
140];
141
142const A3: [f64; 24] = [
146 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,
147 0.789, 0.763, 0.739, 0.718, 0.698, 0.680, 0.663, 0.647, 0.633, 0.619, 0.606,
148];
149
150const B3: [f64; 24] = [
154 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,
155 0.428, 0.448, 0.466, 0.482, 0.497, 0.510, 0.523, 0.534, 0.545, 0.555, 0.565,
156];
157
158const B4: [f64; 24] = [
162 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,
163 1.572, 1.552, 1.534, 1.518, 1.503, 1.490, 1.477, 1.466, 1.455, 1.445, 1.435,
164];
165
166const C4: [f64; 24] = [
177 0.7979, 0.8862, 0.9213, 0.9400, 0.9515, 0.9594, 0.9650, 0.9693, 0.9727, 0.9754, 0.9776, 0.9794,
178 0.9810, 0.9823, 0.9835, 0.9845, 0.9854, 0.9862, 0.9869, 0.9876, 0.9882, 0.9887, 0.9892, 0.9896,
179];
180
181const E2: f64 = 2.660;
186
187const D4_MR: f64 = 3.267;
189
190#[derive(Debug, Clone)]
229pub struct XBarRChart {
230 subgroup_size: usize,
232 subgroups: Vec<Vec<f64>>,
234 xbar_points: Vec<ChartPoint>,
236 r_points: Vec<ChartPoint>,
238 xbar_limits: Option<ControlLimits>,
240 r_limits: Option<ControlLimits>,
242 rules: RuleSet,
244}
245
246impl XBarRChart {
247 #[must_use]
262 pub fn with_rules(mut self, rules: RuleSet) -> Self {
263 self.rules = rules;
264 self.recompute();
265 self
266 }
267
268 pub fn rules(&self) -> RuleSet {
270 self.rules
271 }
272 pub fn new(subgroup_size: usize) -> Result<Self, ControlChartError> {
283 if !(MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE).contains(&subgroup_size) {
284 return Err(ControlChartError::SubgroupSizeOutOfRange {
285 got: subgroup_size,
286 min: MIN_SUBGROUP_SIZE,
287 max: MAX_SUBGROUP_SIZE,
288 });
289 }
290 Ok(Self {
291 subgroup_size,
292 subgroups: Vec::new(),
293 xbar_points: Vec::new(),
294 r_points: Vec::new(),
295 xbar_limits: None,
296 r_limits: None,
297 rules: RuleSet::default(),
298 })
299 }
300
301 pub fn sigma_hat(&self) -> Option<f64> {
325 let r_bar = self.r_limits.as_ref()?.cl;
326 let d2 = D2[self.subgroup_size - 2];
327 (d2 > 0.0).then(|| r_bar / d2)
328 }
329
330 pub fn r_limits(&self) -> Option<ControlLimits> {
332 self.r_limits.clone()
333 }
334
335 pub fn r_points(&self) -> &[ChartPoint] {
337 &self.r_points
338 }
339
340 fn recompute(&mut self) {
342 if self.subgroups.is_empty() {
343 self.xbar_limits = None;
344 self.r_limits = None;
345 self.xbar_points.clear();
346 self.r_points.clear();
347 return;
348 }
349
350 let idx = self.subgroup_size - 2; let mut xbar_values = Vec::with_capacity(self.subgroups.len());
354 let mut r_values = Vec::with_capacity(self.subgroups.len());
355
356 for subgroup in &self.subgroups {
357 let mean_val = u_numflow::stats::mean(subgroup)
358 .expect("subgroup should be non-empty with finite values");
359 let range = subgroup_range(subgroup);
360 xbar_values.push(mean_val);
361 r_values.push(range);
362 }
363
364 let grand_mean = u_numflow::stats::mean(&xbar_values)
366 .expect("xbar_values should be non-empty with finite values");
367 let r_bar = u_numflow::stats::mean(&r_values)
368 .expect("r_values should be non-empty with finite values");
369
370 let a2 = A2[idx];
372 self.xbar_limits = Some(ControlLimits {
373 ucl: grand_mean + a2 * r_bar,
374 cl: grand_mean,
375 lcl: grand_mean - a2 * r_bar,
376 });
377
378 let d3 = D3[idx];
380 let d4 = D4[idx];
381 self.r_limits = Some(ControlLimits {
382 ucl: d4 * r_bar,
383 cl: r_bar,
384 lcl: d3 * r_bar,
385 });
386
387 self.xbar_points = xbar_values
389 .iter()
390 .enumerate()
391 .map(|(i, &v)| ChartPoint {
392 value: v,
393 index: i,
394 violations: Vec::new(),
395 })
396 .collect();
397
398 self.r_points = r_values
399 .iter()
400 .enumerate()
401 .map(|(i, &v)| ChartPoint {
402 value: v,
403 index: i,
404 violations: Vec::new(),
405 })
406 .collect();
407
408 if let Some(ref limits) = self.xbar_limits {
410 let violations = self.rules.check(&self.xbar_points, limits);
411 apply_violations(&mut self.xbar_points, &violations);
412 }
413
414 if let Some(ref limits) = self.r_limits {
416 let violations = self.rules.check(&self.r_points, limits);
417 apply_violations(&mut self.r_points, &violations);
418 }
419 }
420}
421
422fn check_sample(sample: &[f64], expected: usize) -> Result<(), ControlChartError> {
424 if sample.len() != expected {
425 return Err(ControlChartError::SampleLengthMismatch {
426 expected,
427 got: sample.len(),
428 });
429 }
430 if !sample.iter().all(|x| x.is_finite()) {
431 return Err(ControlChartError::NonFiniteValue);
432 }
433 Ok(())
434}
435
436impl ControlChart for XBarRChart {
437 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
439 check_sample(sample, self.subgroup_size)?;
440 self.subgroups.push(sample.to_vec());
441 self.recompute();
442 Ok(())
443 }
444
445 fn control_limits(&self) -> Option<ControlLimits> {
446 self.xbar_limits.clone()
447 }
448
449 fn is_in_control(&self) -> bool {
450 self.xbar_points.iter().all(|p| p.violations.is_empty())
451 && self.r_points.iter().all(|p| p.violations.is_empty())
452 }
453
454 fn violations(&self) -> Vec<Violation> {
455 collect_violations(&self.xbar_points)
456 .into_iter()
457 .chain(collect_violations(&self.r_points))
458 .collect()
459 }
460
461 fn points(&self) -> &[ChartPoint] {
462 &self.xbar_points
463 }
464}
465
466#[derive(Debug, Clone)]
488pub struct XBarSChart {
489 subgroup_size: usize,
491 subgroups: Vec<Vec<f64>>,
493 xbar_points: Vec<ChartPoint>,
495 s_points: Vec<ChartPoint>,
497 xbar_limits: Option<ControlLimits>,
499 s_limits: Option<ControlLimits>,
501 rules: RuleSet,
503}
504
505impl XBarSChart {
506 #[must_use]
521 pub fn with_rules(mut self, rules: RuleSet) -> Self {
522 self.rules = rules;
523 self.recompute();
524 self
525 }
526
527 pub fn rules(&self) -> RuleSet {
529 self.rules
530 }
531 pub fn new(subgroup_size: usize) -> Result<Self, ControlChartError> {
542 if !(MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE).contains(&subgroup_size) {
543 return Err(ControlChartError::SubgroupSizeOutOfRange {
544 got: subgroup_size,
545 min: MIN_SUBGROUP_SIZE,
546 max: MAX_SUBGROUP_SIZE,
547 });
548 }
549 Ok(Self {
550 subgroup_size,
551 subgroups: Vec::new(),
552 xbar_points: Vec::new(),
553 s_points: Vec::new(),
554 xbar_limits: None,
555 s_limits: None,
556 rules: RuleSet::default(),
557 })
558 }
559
560 pub fn sigma_hat(&self) -> Option<f64> {
580 let s_bar = self.s_limits.as_ref()?.cl;
581 let c4 = C4[self.subgroup_size - 2];
582 (c4 > 0.0).then(|| s_bar / c4)
583 }
584
585 pub fn s_limits(&self) -> Option<ControlLimits> {
587 self.s_limits.clone()
588 }
589
590 pub fn s_points(&self) -> &[ChartPoint] {
592 &self.s_points
593 }
594
595 fn recompute(&mut self) {
597 if self.subgroups.is_empty() {
598 self.xbar_limits = None;
599 self.s_limits = None;
600 self.xbar_points.clear();
601 self.s_points.clear();
602 return;
603 }
604
605 let idx = self.subgroup_size - 2;
606
607 let mut xbar_values = Vec::with_capacity(self.subgroups.len());
609 let mut s_values = Vec::with_capacity(self.subgroups.len());
610
611 for subgroup in &self.subgroups {
612 let mean_val = u_numflow::stats::mean(subgroup)
613 .expect("subgroup should be non-empty with finite values");
614 let sd = u_numflow::stats::std_dev(subgroup)
615 .expect("subgroup should have >= 2 elements for std_dev");
616 xbar_values.push(mean_val);
617 s_values.push(sd);
618 }
619
620 let grand_mean = u_numflow::stats::mean(&xbar_values)
622 .expect("xbar_values should be non-empty with finite values");
623 let s_bar = u_numflow::stats::mean(&s_values)
624 .expect("s_values should be non-empty with finite values");
625
626 let a3 = A3[idx];
628 self.xbar_limits = Some(ControlLimits {
629 ucl: grand_mean + a3 * s_bar,
630 cl: grand_mean,
631 lcl: grand_mean - a3 * s_bar,
632 });
633
634 let b3 = B3[idx];
636 let b4 = B4[idx];
637 self.s_limits = Some(ControlLimits {
638 ucl: b4 * s_bar,
639 cl: s_bar,
640 lcl: b3 * s_bar,
641 });
642
643 self.xbar_points = xbar_values
645 .iter()
646 .enumerate()
647 .map(|(i, &v)| ChartPoint {
648 value: v,
649 index: i,
650 violations: Vec::new(),
651 })
652 .collect();
653
654 self.s_points = s_values
655 .iter()
656 .enumerate()
657 .map(|(i, &v)| ChartPoint {
658 value: v,
659 index: i,
660 violations: Vec::new(),
661 })
662 .collect();
663
664 if let Some(ref limits) = self.xbar_limits {
666 let violations = self.rules.check(&self.xbar_points, limits);
667 apply_violations(&mut self.xbar_points, &violations);
668 }
669
670 if let Some(ref limits) = self.s_limits {
672 let violations = self.rules.check(&self.s_points, limits);
673 apply_violations(&mut self.s_points, &violations);
674 }
675 }
676}
677
678impl ControlChart for XBarSChart {
679 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
681 check_sample(sample, self.subgroup_size)?;
682 self.subgroups.push(sample.to_vec());
683 self.recompute();
684 Ok(())
685 }
686
687 fn control_limits(&self) -> Option<ControlLimits> {
688 self.xbar_limits.clone()
689 }
690
691 fn is_in_control(&self) -> bool {
692 self.xbar_points.iter().all(|p| p.violations.is_empty())
693 && self.s_points.iter().all(|p| p.violations.is_empty())
694 }
695
696 fn violations(&self) -> Vec<Violation> {
697 collect_violations(&self.xbar_points)
698 .into_iter()
699 .chain(collect_violations(&self.s_points))
700 .collect()
701 }
702
703 fn points(&self) -> &[ChartPoint] {
704 &self.xbar_points
705 }
706}
707
708#[derive(Debug, Clone)]
745pub struct IndividualMRChart {
746 observations: Vec<f64>,
748 i_points: Vec<ChartPoint>,
750 mr_points: Vec<ChartPoint>,
752 i_limits: Option<ControlLimits>,
754 mr_limits: Option<ControlLimits>,
756 rules: RuleSet,
758}
759
760impl IndividualMRChart {
761 #[must_use]
776 pub fn with_rules(mut self, rules: RuleSet) -> Self {
777 self.rules = rules;
778 self.recompute();
779 self
780 }
781
782 pub fn rules(&self) -> RuleSet {
784 self.rules
785 }
786 pub fn new() -> Self {
788 Self {
789 observations: Vec::new(),
790 i_points: Vec::new(),
791 mr_points: Vec::new(),
792 i_limits: None,
793 mr_limits: None,
794 rules: RuleSet::default(),
795 }
796 }
797
798 pub fn sigma_hat(&self) -> Option<f64> {
822 let mr_bar = self.mr_limits.as_ref()?.cl;
823 Some(mr_bar / D2[0])
824 }
825
826 pub fn mr_limits(&self) -> Option<ControlLimits> {
828 self.mr_limits.clone()
829 }
830
831 pub fn mr_points(&self) -> &[ChartPoint] {
833 &self.mr_points
834 }
835
836 fn recompute(&mut self) {
838 if self.observations.len() < 2 {
839 self.i_limits = None;
840 self.mr_limits = None;
841 self.i_points.clear();
842 self.mr_points.clear();
843 return;
844 }
845
846 let mr_values: Vec<f64> = self
848 .observations
849 .windows(2)
850 .map(|w| (w[1] - w[0]).abs())
851 .collect();
852
853 let x_bar = u_numflow::stats::mean(&self.observations)
855 .expect("observations should be non-empty with finite values");
856 let mr_bar = u_numflow::stats::mean(&mr_values)
857 .expect("mr_values should be non-empty with finite values");
858
859 self.i_limits = Some(ControlLimits {
861 ucl: x_bar + E2 * mr_bar,
862 cl: x_bar,
863 lcl: x_bar - E2 * mr_bar,
864 });
865
866 self.mr_limits = Some(ControlLimits {
868 ucl: D4_MR * mr_bar,
869 cl: mr_bar,
870 lcl: 0.0,
871 });
872
873 self.i_points = self
875 .observations
876 .iter()
877 .enumerate()
878 .map(|(i, &v)| ChartPoint {
879 value: v,
880 index: i,
881 violations: Vec::new(),
882 })
883 .collect();
884
885 self.mr_points = mr_values
887 .iter()
888 .enumerate()
889 .map(|(i, &v)| ChartPoint {
890 value: v,
891 index: i + 1,
892 violations: Vec::new(),
893 })
894 .collect();
895
896 if let Some(ref limits) = self.i_limits {
898 let violations = self.rules.check(&self.i_points, limits);
899 apply_violations(&mut self.i_points, &violations);
900 }
901
902 if let Some(ref limits) = self.mr_limits {
904 let violations = self.rules.check(&self.mr_points, limits);
905 apply_violations(&mut self.mr_points, &violations);
906 }
907 }
908}
909
910impl Default for IndividualMRChart {
911 fn default() -> Self {
912 Self::new()
913 }
914}
915
916impl ControlChart for IndividualMRChart {
917 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
919 check_sample(sample, 1)?;
920 self.observations.push(sample[0]);
921 self.recompute();
922 Ok(())
923 }
924
925 fn control_limits(&self) -> Option<ControlLimits> {
926 self.i_limits.clone()
927 }
928
929 fn is_in_control(&self) -> bool {
930 self.i_points.iter().all(|p| p.violations.is_empty())
931 && self.mr_points.iter().all(|p| p.violations.is_empty())
932 }
933
934 fn violations(&self) -> Vec<Violation> {
935 collect_violations(&self.i_points)
936 .into_iter()
937 .chain(collect_violations(&self.mr_points))
938 .collect()
939 }
940
941 fn points(&self) -> &[ChartPoint] {
942 &self.i_points
943 }
944}
945
946fn subgroup_range(subgroup: &[f64]) -> f64 {
956 let max_val =
957 u_numflow::stats::max(subgroup).expect("subgroup should be non-empty without NaN");
958 let min_val =
959 u_numflow::stats::min(subgroup).expect("subgroup should be non-empty without NaN");
960 max_val - min_val
961}
962
963fn apply_violations(points: &mut [ChartPoint], violations: &[(usize, ViolationType)]) {
965 for &(idx, vtype) in violations {
966 if let Some(point) = points.iter_mut().find(|p| p.index == idx) {
967 point.violations.push(vtype);
968 }
969 }
970}
971
972fn collect_violations(points: &[ChartPoint]) -> Vec<Violation> {
974 let mut result = Vec::new();
975 for point in points {
976 for &vtype in &point.violations {
977 result.push(Violation {
978 point_index: point.index,
979 violation_type: vtype,
980 });
981 }
982 }
983 result
984}
985
986#[cfg(test)]
991mod tests {
992 use super::*;
993
994 #[test]
997 fn test_xbar_r_basic_limits() {
998 let mut chart = XBarRChart::new(4).expect("subgroup size is in range");
999 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]).unwrap();
1000 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]).unwrap();
1001 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]).unwrap();
1002 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]).unwrap();
1003 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]).unwrap();
1004
1005 let limits = chart.control_limits().expect("should have limits");
1006 let expected_grand_mean = (71.0 + 54.5 + 52.5 + 63.0 + 57.25) / 5.0;
1008 assert!(
1009 (limits.cl - expected_grand_mean).abs() < 0.1,
1010 "CL={}, expected ~{expected_grand_mean}",
1011 limits.cl
1012 );
1013
1014 assert!(limits.ucl > limits.cl);
1016 assert!(limits.cl > limits.lcl);
1017 }
1018
1019 #[test]
1020 fn test_xbar_r_rejects_wrong_size() {
1021 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1022 assert_eq!(
1023 chart.add_sample(&[1.0, 2.0, 3.0]),
1024 Err(ControlChartError::SampleLengthMismatch {
1025 expected: 5,
1026 got: 3
1027 })
1028 );
1029 assert!(chart.control_limits().is_none());
1030 }
1031
1032 #[test]
1033 fn test_xbar_r_rejects_nan() {
1034 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1035 assert_eq!(
1036 chart.add_sample(&[1.0, f64::NAN, 3.0]),
1037 Err(ControlChartError::NonFiniteValue)
1038 );
1039 assert!(chart.control_limits().is_none());
1040 }
1041
1042 #[test]
1043 fn test_xbar_r_r_chart_limits() {
1044 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1045 chart.add_sample(&[10.0, 12.0, 11.0, 13.0, 14.0]).unwrap();
1046 chart.add_sample(&[11.0, 13.0, 12.0, 10.0, 15.0]).unwrap();
1047 chart.add_sample(&[12.0, 11.0, 14.0, 13.0, 10.0]).unwrap();
1048
1049 let r_limits = chart.r_limits().expect("should have R limits");
1050 assert!(r_limits.ucl > r_limits.cl);
1051 assert!(r_limits.lcl >= 0.0);
1052 }
1053
1054 #[test]
1055 fn test_xbar_r_constant_subgroups() {
1056 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1058 chart.add_sample(&[10.0, 10.0, 10.0]).unwrap();
1059 chart.add_sample(&[10.0, 10.0, 10.0]).unwrap();
1060
1061 let limits = chart.control_limits().expect("should have limits");
1062 assert!((limits.cl - 10.0).abs() < f64::EPSILON);
1063 assert!((limits.ucl - 10.0).abs() < f64::EPSILON);
1064 assert!((limits.lcl - 10.0).abs() < f64::EPSILON);
1065 }
1066
1067 #[test]
1068 fn test_xbar_r_detects_out_of_control() {
1069 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1070 for _ in 0..5 {
1071 chart.add_sample(&[10.0, 10.5, 9.5]).unwrap();
1072 }
1073 chart.add_sample(&[50.0, 51.0, 49.0]).unwrap();
1075
1076 assert!(!chart.is_in_control());
1077 }
1078
1079 #[test]
1087 fn factor_tables_reproduce_the_published_astm_values() {
1088 const PUB_A2: [f64; 9] = [
1090 1.880, 1.023, 0.729, 0.577, 0.483, 0.419, 0.373, 0.337, 0.308,
1091 ];
1092 const PUB_D3: [f64; 9] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.076, 0.136, 0.184, 0.223];
1093 const PUB_D4: [f64; 9] = [
1094 3.267, 2.575, 2.282, 2.114, 2.004, 1.924, 1.864, 1.816, 1.777,
1095 ];
1096 const PUB_D2: [f64; 9] = [
1097 1.128, 1.693, 2.059, 2.326, 2.534, 2.704, 2.847, 2.970, 3.078,
1098 ];
1099 const PUB_A3: [f64; 9] = [
1100 2.659, 1.954, 1.628, 1.427, 1.287, 1.182, 1.099, 1.032, 0.975,
1101 ];
1102 const PUB_B3: [f64; 9] = [0.0, 0.0, 0.0, 0.0, 0.030, 0.118, 0.185, 0.239, 0.284];
1103 const PUB_B4: [f64; 9] = [
1104 3.267, 2.568, 2.266, 2.089, 1.970, 1.882, 1.815, 1.761, 1.716,
1105 ];
1106 const PUB_C4: [f64; 9] = [
1107 0.7979, 0.8862, 0.9213, 0.9400, 0.9515, 0.9594, 0.9650, 0.9693, 0.9727,
1108 ];
1109
1110 for (name, table, published, tol) in [
1111 ("A2", &A2[..9], &PUB_A2[..], 5e-4),
1112 ("D3", &D3[..9], &PUB_D3[..], 5e-4),
1113 ("D4", &D4[..9], &PUB_D4[..], 5e-4),
1114 ("d2", &D2[..9], &PUB_D2[..], 5e-4),
1115 ("A3", &A3[..9], &PUB_A3[..], 5e-4),
1116 ("B3", &B3[..9], &PUB_B3[..], 5e-4),
1117 ("B4", &B4[..9], &PUB_B4[..], 5e-4),
1118 ("c4", &C4[..9], &PUB_C4[..], 5e-5),
1119 ] {
1120 for (i, (got, want)) in table.iter().zip(published).enumerate() {
1121 assert!(
1122 (got - want).abs() < tol,
1123 "{name}[n={}] = {got}, published {want}",
1124 i + 2
1125 );
1126 }
1127 }
1128 }
1129
1130 #[test]
1133 fn factor_tables_cover_the_declared_range() {
1134 let span = MAX_SUBGROUP_SIZE - MIN_SUBGROUP_SIZE + 1;
1135 for (name, len) in [
1136 ("A2", A2.len()),
1137 ("D3", D3.len()),
1138 ("D4", D4.len()),
1139 ("d2", D2.len()),
1140 ("A3", A3.len()),
1141 ("B3", B3.len()),
1142 ("B4", B4.len()),
1143 ("c4", C4.len()),
1144 ] {
1145 assert_eq!(
1146 len, span,
1147 "{name} does not cover {MIN_SUBGROUP_SIZE}..={MAX_SUBGROUP_SIZE}"
1148 );
1149 }
1150 for i in 1..span {
1152 assert!(D2[i] > D2[i - 1], "d2 not increasing at n={}", i + 2);
1153 assert!(C4[i] > C4[i - 1], "c4 not increasing at n={}", i + 2);
1154 assert!(A2[i] < A2[i - 1], "A2 not decreasing at n={}", i + 2);
1155 assert!(A3[i] < A3[i - 1], "A3 not decreasing at n={}", i + 2);
1156 assert!(D4[i] < D4[i - 1], "D4 not decreasing at n={}", i + 2);
1157 assert!(B4[i] < B4[i - 1], "B4 not decreasing at n={}", i + 2);
1158 assert!(D3[i] >= D3[i - 1], "D3 not non-decreasing at n={}", i + 2);
1159 assert!(B3[i] >= B3[i - 1], "B3 not non-decreasing at n={}", i + 2);
1160 }
1161 assert!(C4[span - 1] < 1.0 && C4[span - 1] > 0.98);
1163 }
1164
1165 #[test]
1166 fn test_xbar_r_subgroup_size_below_range_is_an_error() {
1167 assert_eq!(
1168 XBarRChart::new(1).unwrap_err(),
1169 ControlChartError::SubgroupSizeOutOfRange {
1170 got: 1,
1171 min: 2,
1172 max: 25
1173 }
1174 );
1175 }
1176
1177 #[test]
1178 fn test_xbar_r_subgroup_size_above_range_is_an_error() {
1179 assert_eq!(
1180 XBarRChart::new(26).unwrap_err(),
1181 ControlChartError::SubgroupSizeOutOfRange {
1182 got: 26,
1183 min: 2,
1184 max: 25
1185 }
1186 );
1187 }
1188
1189 #[test]
1190 fn test_subgroup_size_11_is_now_accepted() {
1191 let mut chart = XBarRChart::new(11).expect("11 is within 2..=25");
1194 for _ in 0..5 {
1195 chart
1196 .add_sample(&[10.0, 10.2, 9.8, 10.1, 9.9, 10.3, 9.7, 10.0, 10.1, 9.9, 10.0])
1197 .unwrap();
1198 }
1199 let limits = chart.control_limits().expect("limits from 5 subgroups");
1200 assert!(limits.ucl > limits.cl && limits.cl > limits.lcl);
1201 assert!(chart.sigma_hat().is_some_and(|s| s > 0.0));
1202 }
1203
1204 #[test]
1207 fn test_xbar_s_basic_limits() {
1208 let mut chart = XBarSChart::new(4).expect("subgroup size is in range");
1209 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]).unwrap();
1210 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]).unwrap();
1211 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]).unwrap();
1212 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]).unwrap();
1213 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]).unwrap();
1214
1215 let limits = chart.control_limits().expect("should have limits");
1216 assert!(limits.ucl > limits.cl);
1217 assert!(limits.cl > limits.lcl);
1218
1219 let s_limits = chart.s_limits().expect("should have S limits");
1220 assert!(s_limits.ucl > s_limits.cl);
1221 assert!(s_limits.lcl >= 0.0);
1222 }
1223
1224 #[test]
1225 fn test_xbar_s_rejects_wrong_size() {
1226 let mut chart = XBarSChart::new(5).expect("subgroup size is in range");
1227 assert_eq!(
1228 chart.add_sample(&[1.0, 2.0]),
1229 Err(ControlChartError::SampleLengthMismatch {
1230 expected: 5,
1231 got: 2
1232 })
1233 );
1234 assert!(chart.control_limits().is_none());
1235 }
1236
1237 #[test]
1238 fn test_xbar_s_in_control() {
1239 let mut chart = XBarSChart::new(4).expect("subgroup size is in range");
1240 for _ in 0..10 {
1241 chart.add_sample(&[10.0, 10.2, 9.8, 10.1]).unwrap();
1242 }
1243 assert!(chart.is_in_control());
1244 }
1245
1246 #[test]
1249 fn test_imr_basic_limits() {
1250 let mut chart = IndividualMRChart::new();
1251 let data = [10.0, 12.0, 11.0, 13.0, 10.0, 14.0, 11.0, 12.0, 13.0, 10.0];
1252 for &x in &data {
1253 chart.add_sample(&[x]).unwrap();
1254 }
1255
1256 let limits = chart.control_limits().expect("should have limits");
1257 assert!(limits.ucl > limits.cl);
1258 assert!(limits.cl > limits.lcl);
1259
1260 let mr_limits = chart.mr_limits().expect("should have MR limits");
1261 assert!(mr_limits.ucl > mr_limits.cl);
1262 assert!((mr_limits.lcl).abs() < f64::EPSILON);
1263 }
1264
1265 #[test]
1266 fn test_imr_needs_two_points() {
1267 let mut chart = IndividualMRChart::new();
1268 chart.add_sample(&[10.0]).unwrap();
1269 assert!(chart.control_limits().is_none());
1270 }
1271
1272 #[test]
1273 fn test_imr_center_line_is_mean() {
1274 let mut chart = IndividualMRChart::new();
1275 let data = [5.0, 10.0, 15.0, 20.0, 25.0];
1276 for &x in &data {
1277 chart.add_sample(&[x]).unwrap();
1278 }
1279 let limits = chart.control_limits().expect("should have limits");
1280 assert!((limits.cl - 15.0).abs() < f64::EPSILON);
1281 }
1282
1283 #[test]
1284 fn test_imr_mr_values() {
1285 let mut chart = IndividualMRChart::new();
1286 let data = [10.0, 12.0, 9.0];
1287 for &x in &data {
1288 chart.add_sample(&[x]).unwrap();
1289 }
1290 let mr_pts = chart.mr_points();
1292 assert_eq!(mr_pts.len(), 2);
1293 assert!((mr_pts[0].value - 2.0).abs() < f64::EPSILON);
1294 assert!((mr_pts[1].value - 3.0).abs() < f64::EPSILON);
1295 }
1296
1297 #[test]
1298 fn test_imr_rejects_multi_element_sample() {
1299 let mut chart = IndividualMRChart::new();
1300 assert_eq!(
1301 chart.add_sample(&[1.0, 2.0]),
1302 Err(ControlChartError::SampleLengthMismatch {
1303 expected: 1,
1304 got: 2
1305 })
1306 );
1307 assert!(chart.points().is_empty());
1308 }
1309
1310 #[test]
1311 fn test_imr_detects_out_of_control() {
1312 let mut chart = IndividualMRChart::new();
1313 for i in 0..10 {
1314 chart.add_sample(&[50.0 + (i as f64 % 3.0) * 0.5]).unwrap();
1315 }
1316 chart.add_sample(&[100.0]).unwrap();
1318
1319 assert!(!chart.is_in_control());
1320 }
1321
1322 #[test]
1323 fn test_imr_default() {
1324 let chart = IndividualMRChart::default();
1325 assert!(chart.points().is_empty());
1326 }
1327
1328 #[test]
1331 fn test_subgroup_range() {
1332 assert!((subgroup_range(&[1.0, 5.0, 3.0]) - 4.0).abs() < f64::EPSILON);
1333 assert!((subgroup_range(&[10.0, 10.0, 10.0])).abs() < f64::EPSILON);
1334 }
1335
1336 #[test]
1339 fn test_xbar_r_chart_factors_n5() {
1340 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1343 chart.add_sample(&[45.0, 47.0, 50.0, 53.0, 55.0]).unwrap();
1344
1345 let limits = chart.control_limits().expect("limits");
1346 assert!((limits.cl - 50.0).abs() < f64::EPSILON);
1347
1348 let r_limits = chart.r_limits().expect("R limits");
1349 assert!((r_limits.cl - 10.0).abs() < f64::EPSILON);
1350
1351 assert!((limits.ucl - 55.77).abs() < 0.01);
1353 assert!((limits.lcl - 44.23).abs() < 0.01);
1355 }
1356
1357 #[test]
1360 fn test_imr_e2_factor() {
1361 let mut chart = IndividualMRChart::new();
1364 chart.add_sample(&[95.0]).unwrap();
1365 chart.add_sample(&[105.0]).unwrap();
1366
1367 let limits = chart.control_limits().expect("limits");
1368 assert!((limits.cl - 100.0).abs() < f64::EPSILON);
1370 assert!((limits.ucl - 126.6).abs() < 0.1);
1373 assert!((limits.lcl - 73.4).abs() < 0.1);
1375 }
1376
1377 #[test]
1386 fn test_d2_constants_montgomery_table_vi() {
1387 let expected: [(usize, f64); 9] = [
1389 (2, 1.128),
1390 (3, 1.693),
1391 (4, 2.059),
1392 (5, 2.326),
1393 (6, 2.534),
1394 (7, 2.704),
1395 (8, 2.847),
1396 (9, 2.970),
1397 (10, 3.078),
1398 ];
1399 for (n, d2_ref) in expected {
1400 let d2 = D2[n - 2];
1401 assert!(
1402 (d2 - d2_ref).abs() < 0.001,
1403 "d2(n={n}): expected {d2_ref}, got {d2}"
1404 );
1405 }
1406 }
1407
1408 #[test]
1417 fn test_c4_constants_montgomery_table_vi() {
1418 let expected: [(usize, f64); 6] = [
1420 (2, 0.7979),
1421 (3, 0.8862),
1422 (4, 0.9213),
1423 (5, 0.9400),
1424 (6, 0.9515),
1425 (7, 0.9594),
1426 ];
1427 for (n, c4_ref) in expected {
1428 let c4 = C4[n - 2];
1429 assert!(
1430 (c4 - c4_ref).abs() < 0.001,
1431 "c4(n={n}): expected {c4_ref}, got {c4}"
1432 );
1433 }
1434 }
1435
1436 #[test]
1444 fn xbar_r_sigma_hat_is_r_bar_over_d2() {
1445 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1446 let groups = [
1447 [9.9, 10.1, 10.0, 9.8, 10.2],
1448 [10.3, 9.7, 10.0, 10.1, 9.9],
1449 [9.8, 10.2, 10.1, 9.9, 10.0],
1450 [10.5, 9.5, 10.0, 10.2, 9.8],
1451 ];
1452 for g in &groups {
1453 chart.add_sample(g).unwrap();
1454 }
1455 let r_bar = chart.r_limits().expect("limits").cl;
1456 let expected = r_bar / D2[5 - 2];
1457 let sigma = chart.sigma_hat().expect("sigma-hat");
1458 assert!(
1459 (sigma - expected).abs() < 1e-12,
1460 "sigma-hat must be R-bar/d2: got {sigma}, expected {expected}"
1461 );
1462 assert!(sigma > 0.0);
1463 }
1464
1465 #[test]
1466 fn xbar_s_sigma_hat_is_s_bar_over_c4() {
1467 let mut chart = XBarSChart::new(5).expect("subgroup size is in range");
1468 for g in &[
1469 [9.9, 10.1, 10.0, 9.8, 10.2],
1470 [10.3, 9.7, 10.0, 10.1, 9.9],
1471 [9.8, 10.2, 10.1, 9.9, 10.0],
1472 ] {
1473 chart.add_sample(g).unwrap();
1474 }
1475 let s_bar = chart.s_limits().expect("limits").cl;
1476 let expected = s_bar / C4[5 - 2];
1477 let sigma = chart.sigma_hat().expect("sigma-hat");
1478 assert!((sigma - expected).abs() < 1e-12);
1479 }
1480
1481 #[test]
1482 fn sigma_hat_is_none_without_limits() {
1483 assert!(XBarRChart::new(5).unwrap().sigma_hat().is_none());
1484 assert!(XBarSChart::new(5).unwrap().sigma_hat().is_none());
1485 }
1486
1487 #[test]
1488 fn short_term_sigma_differs_from_overall_when_subgroups_drift() {
1489 use u_numflow::stats;
1490
1491 let groups = [
1494 [9.9, 10.1, 10.0, 9.8, 10.2],
1495 [10.3, 9.7, 10.0, 10.1, 9.9],
1496 [9.8, 10.2, 10.1, 9.9, 10.0],
1497 [10.5, 9.5, 10.0, 10.2, 9.8],
1498 [9.6, 10.4, 10.0, 9.9, 10.1],
1499 [10.1, 9.9, 10.0, 10.3, 9.7],
1500 ];
1501 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1502 for g in &groups {
1503 chart.add_sample(g).unwrap();
1504 }
1505 let sigma_within = chart.sigma_hat().expect("sigma-hat");
1506 let flat: Vec<f64> = groups.iter().flatten().copied().collect();
1507 let sigma_overall = stats::std_dev(&flat).expect("overall sigma");
1508
1509 assert!(
1510 (sigma_within - sigma_overall).abs() > 1e-6,
1511 "short-term and long-term sigma must differ on drifting subgroups: within={sigma_within}, overall={sigma_overall}"
1512 );
1513 }
1514}