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
178const E2: f64 = 2.660;
183
184const D4_MR: f64 = 3.267;
186
187#[derive(Debug, Clone)]
226pub struct XBarRChart {
227 subgroup_size: usize,
229 subgroups: Vec<Vec<f64>>,
231 xbar_points: Vec<ChartPoint>,
233 r_points: Vec<ChartPoint>,
235 xbar_limits: Option<ControlLimits>,
237 r_limits: Option<ControlLimits>,
239 rules: RuleSet,
241}
242
243impl XBarRChart {
244 #[must_use]
259 pub fn with_rules(mut self, rules: RuleSet) -> Self {
260 self.rules = rules;
261 self.recompute();
262 self
263 }
264
265 pub fn rules(&self) -> RuleSet {
267 self.rules
268 }
269 pub fn new(subgroup_size: usize) -> Result<Self, ControlChartError> {
280 if !(MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE).contains(&subgroup_size) {
281 return Err(ControlChartError::SubgroupSizeOutOfRange {
282 got: subgroup_size,
283 min: MIN_SUBGROUP_SIZE,
284 max: MAX_SUBGROUP_SIZE,
285 });
286 }
287 Ok(Self {
288 subgroup_size,
289 subgroups: Vec::new(),
290 xbar_points: Vec::new(),
291 r_points: Vec::new(),
292 xbar_limits: None,
293 r_limits: None,
294 rules: RuleSet::default(),
295 })
296 }
297
298 pub fn sigma_hat(&self) -> Option<f64> {
322 let r_bar = self.r_limits.as_ref()?.cl;
323 let d2 = D2[self.subgroup_size - 2];
324 (d2 > 0.0).then(|| r_bar / d2)
325 }
326
327 pub fn r_limits(&self) -> Option<ControlLimits> {
329 self.r_limits.clone()
330 }
331
332 pub fn r_points(&self) -> &[ChartPoint] {
334 &self.r_points
335 }
336
337 fn recompute(&mut self) {
339 if self.subgroups.is_empty() {
340 self.xbar_limits = None;
341 self.r_limits = None;
342 self.xbar_points.clear();
343 self.r_points.clear();
344 return;
345 }
346
347 let idx = self.subgroup_size - 2; let mut xbar_values = Vec::with_capacity(self.subgroups.len());
351 let mut r_values = Vec::with_capacity(self.subgroups.len());
352
353 for subgroup in &self.subgroups {
354 let mean_val = u_numflow::stats::mean(subgroup)
355 .expect("subgroup should be non-empty with finite values");
356 let range = subgroup_range(subgroup);
357 xbar_values.push(mean_val);
358 r_values.push(range);
359 }
360
361 let grand_mean = u_numflow::stats::mean(&xbar_values)
363 .expect("xbar_values should be non-empty with finite values");
364 let r_bar = u_numflow::stats::mean(&r_values)
365 .expect("r_values should be non-empty with finite values");
366
367 let a2 = A2[idx];
369 self.xbar_limits = Some(ControlLimits {
370 ucl: grand_mean + a2 * r_bar,
371 cl: grand_mean,
372 lcl: grand_mean - a2 * r_bar,
373 });
374
375 let d3 = D3[idx];
377 let d4 = D4[idx];
378 self.r_limits = Some(ControlLimits {
379 ucl: d4 * r_bar,
380 cl: r_bar,
381 lcl: d3 * r_bar,
382 });
383
384 self.xbar_points = xbar_values
386 .iter()
387 .enumerate()
388 .map(|(i, &v)| ChartPoint {
389 value: v,
390 index: i,
391 violations: Vec::new(),
392 })
393 .collect();
394
395 self.r_points = r_values
396 .iter()
397 .enumerate()
398 .map(|(i, &v)| ChartPoint {
399 value: v,
400 index: i,
401 violations: Vec::new(),
402 })
403 .collect();
404
405 if let Some(ref limits) = self.xbar_limits {
407 let violations = self.rules.check(&self.xbar_points, limits);
408 apply_violations(&mut self.xbar_points, &violations);
409 }
410
411 if let Some(ref limits) = self.r_limits {
413 let violations = self.rules.check(&self.r_points, limits);
414 apply_violations(&mut self.r_points, &violations);
415 }
416 }
417}
418
419fn check_sample(sample: &[f64], expected: usize) -> Result<(), ControlChartError> {
421 if sample.len() != expected {
422 return Err(ControlChartError::SampleLengthMismatch {
423 expected,
424 got: sample.len(),
425 });
426 }
427 if !sample.iter().all(|x| x.is_finite()) {
428 return Err(ControlChartError::NonFiniteValue);
429 }
430 Ok(())
431}
432
433impl ControlChart for XBarRChart {
434 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
436 check_sample(sample, self.subgroup_size)?;
437 self.subgroups.push(sample.to_vec());
438 self.recompute();
439 Ok(())
440 }
441
442 fn control_limits(&self) -> Option<ControlLimits> {
443 self.xbar_limits.clone()
444 }
445
446 fn is_in_control(&self) -> bool {
447 self.xbar_points.iter().all(|p| p.violations.is_empty())
448 && self.r_points.iter().all(|p| p.violations.is_empty())
449 }
450
451 fn violations(&self) -> Vec<Violation> {
452 collect_violations(&self.xbar_points)
453 .into_iter()
454 .chain(collect_violations(&self.r_points))
455 .collect()
456 }
457
458 fn points(&self) -> &[ChartPoint] {
459 &self.xbar_points
460 }
461}
462
463#[derive(Debug, Clone)]
485pub struct XBarSChart {
486 subgroup_size: usize,
488 subgroups: Vec<Vec<f64>>,
490 xbar_points: Vec<ChartPoint>,
492 s_points: Vec<ChartPoint>,
494 xbar_limits: Option<ControlLimits>,
496 s_limits: Option<ControlLimits>,
498 rules: RuleSet,
500}
501
502impl XBarSChart {
503 #[must_use]
518 pub fn with_rules(mut self, rules: RuleSet) -> Self {
519 self.rules = rules;
520 self.recompute();
521 self
522 }
523
524 pub fn rules(&self) -> RuleSet {
526 self.rules
527 }
528 pub fn new(subgroup_size: usize) -> Result<Self, ControlChartError> {
539 if !(MIN_SUBGROUP_SIZE..=MAX_SUBGROUP_SIZE).contains(&subgroup_size) {
540 return Err(ControlChartError::SubgroupSizeOutOfRange {
541 got: subgroup_size,
542 min: MIN_SUBGROUP_SIZE,
543 max: MAX_SUBGROUP_SIZE,
544 });
545 }
546 Ok(Self {
547 subgroup_size,
548 subgroups: Vec::new(),
549 xbar_points: Vec::new(),
550 s_points: Vec::new(),
551 xbar_limits: None,
552 s_limits: None,
553 rules: RuleSet::default(),
554 })
555 }
556
557 pub fn sigma_hat(&self) -> Option<f64> {
577 let s_bar = self.s_limits.as_ref()?.cl;
578 let c4 = C4[self.subgroup_size - 2];
579 (c4 > 0.0).then(|| s_bar / c4)
580 }
581
582 pub fn s_limits(&self) -> Option<ControlLimits> {
584 self.s_limits.clone()
585 }
586
587 pub fn s_points(&self) -> &[ChartPoint] {
589 &self.s_points
590 }
591
592 fn recompute(&mut self) {
594 if self.subgroups.is_empty() {
595 self.xbar_limits = None;
596 self.s_limits = None;
597 self.xbar_points.clear();
598 self.s_points.clear();
599 return;
600 }
601
602 let idx = self.subgroup_size - 2;
603
604 let mut xbar_values = Vec::with_capacity(self.subgroups.len());
606 let mut s_values = Vec::with_capacity(self.subgroups.len());
607
608 for subgroup in &self.subgroups {
609 let mean_val = u_numflow::stats::mean(subgroup)
610 .expect("subgroup should be non-empty with finite values");
611 let sd = u_numflow::stats::std_dev(subgroup)
612 .expect("subgroup should have >= 2 elements for std_dev");
613 xbar_values.push(mean_val);
614 s_values.push(sd);
615 }
616
617 let grand_mean = u_numflow::stats::mean(&xbar_values)
619 .expect("xbar_values should be non-empty with finite values");
620 let s_bar = u_numflow::stats::mean(&s_values)
621 .expect("s_values should be non-empty with finite values");
622
623 let a3 = A3[idx];
625 self.xbar_limits = Some(ControlLimits {
626 ucl: grand_mean + a3 * s_bar,
627 cl: grand_mean,
628 lcl: grand_mean - a3 * s_bar,
629 });
630
631 let b3 = B3[idx];
633 let b4 = B4[idx];
634 self.s_limits = Some(ControlLimits {
635 ucl: b4 * s_bar,
636 cl: s_bar,
637 lcl: b3 * s_bar,
638 });
639
640 self.xbar_points = xbar_values
642 .iter()
643 .enumerate()
644 .map(|(i, &v)| ChartPoint {
645 value: v,
646 index: i,
647 violations: Vec::new(),
648 })
649 .collect();
650
651 self.s_points = s_values
652 .iter()
653 .enumerate()
654 .map(|(i, &v)| ChartPoint {
655 value: v,
656 index: i,
657 violations: Vec::new(),
658 })
659 .collect();
660
661 if let Some(ref limits) = self.xbar_limits {
663 let violations = self.rules.check(&self.xbar_points, limits);
664 apply_violations(&mut self.xbar_points, &violations);
665 }
666
667 if let Some(ref limits) = self.s_limits {
669 let violations = self.rules.check(&self.s_points, limits);
670 apply_violations(&mut self.s_points, &violations);
671 }
672 }
673}
674
675impl ControlChart for XBarSChart {
676 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
678 check_sample(sample, self.subgroup_size)?;
679 self.subgroups.push(sample.to_vec());
680 self.recompute();
681 Ok(())
682 }
683
684 fn control_limits(&self) -> Option<ControlLimits> {
685 self.xbar_limits.clone()
686 }
687
688 fn is_in_control(&self) -> bool {
689 self.xbar_points.iter().all(|p| p.violations.is_empty())
690 && self.s_points.iter().all(|p| p.violations.is_empty())
691 }
692
693 fn violations(&self) -> Vec<Violation> {
694 collect_violations(&self.xbar_points)
695 .into_iter()
696 .chain(collect_violations(&self.s_points))
697 .collect()
698 }
699
700 fn points(&self) -> &[ChartPoint] {
701 &self.xbar_points
702 }
703}
704
705#[derive(Debug, Clone)]
742pub struct IndividualMRChart {
743 observations: Vec<f64>,
745 i_points: Vec<ChartPoint>,
747 mr_points: Vec<ChartPoint>,
749 i_limits: Option<ControlLimits>,
751 mr_limits: Option<ControlLimits>,
753 rules: RuleSet,
755}
756
757impl IndividualMRChart {
758 #[must_use]
773 pub fn with_rules(mut self, rules: RuleSet) -> Self {
774 self.rules = rules;
775 self.recompute();
776 self
777 }
778
779 pub fn rules(&self) -> RuleSet {
781 self.rules
782 }
783 pub fn new() -> Self {
785 Self {
786 observations: Vec::new(),
787 i_points: Vec::new(),
788 mr_points: Vec::new(),
789 i_limits: None,
790 mr_limits: None,
791 rules: RuleSet::default(),
792 }
793 }
794
795 pub fn sigma_hat(&self) -> Option<f64> {
819 let mr_bar = self.mr_limits.as_ref()?.cl;
820 Some(mr_bar / D2[0])
821 }
822
823 pub fn mr_limits(&self) -> Option<ControlLimits> {
825 self.mr_limits.clone()
826 }
827
828 pub fn mr_points(&self) -> &[ChartPoint] {
830 &self.mr_points
831 }
832
833 fn recompute(&mut self) {
835 if self.observations.len() < 2 {
836 self.i_limits = None;
837 self.mr_limits = None;
838 self.i_points.clear();
839 self.mr_points.clear();
840 return;
841 }
842
843 let mr_values: Vec<f64> = self
845 .observations
846 .windows(2)
847 .map(|w| (w[1] - w[0]).abs())
848 .collect();
849
850 let x_bar = u_numflow::stats::mean(&self.observations)
852 .expect("observations should be non-empty with finite values");
853 let mr_bar = u_numflow::stats::mean(&mr_values)
854 .expect("mr_values should be non-empty with finite values");
855
856 self.i_limits = Some(ControlLimits {
858 ucl: x_bar + E2 * mr_bar,
859 cl: x_bar,
860 lcl: x_bar - E2 * mr_bar,
861 });
862
863 self.mr_limits = Some(ControlLimits {
865 ucl: D4_MR * mr_bar,
866 cl: mr_bar,
867 lcl: 0.0,
868 });
869
870 self.i_points = self
872 .observations
873 .iter()
874 .enumerate()
875 .map(|(i, &v)| ChartPoint {
876 value: v,
877 index: i,
878 violations: Vec::new(),
879 })
880 .collect();
881
882 self.mr_points = mr_values
884 .iter()
885 .enumerate()
886 .map(|(i, &v)| ChartPoint {
887 value: v,
888 index: i + 1,
889 violations: Vec::new(),
890 })
891 .collect();
892
893 if let Some(ref limits) = self.i_limits {
895 let violations = self.rules.check(&self.i_points, limits);
896 apply_violations(&mut self.i_points, &violations);
897 }
898
899 if let Some(ref limits) = self.mr_limits {
901 let violations = self.rules.check(&self.mr_points, limits);
902 apply_violations(&mut self.mr_points, &violations);
903 }
904 }
905}
906
907impl Default for IndividualMRChart {
908 fn default() -> Self {
909 Self::new()
910 }
911}
912
913impl ControlChart for IndividualMRChart {
914 fn add_sample(&mut self, sample: &[f64]) -> Result<(), ControlChartError> {
916 check_sample(sample, 1)?;
917 self.observations.push(sample[0]);
918 self.recompute();
919 Ok(())
920 }
921
922 fn control_limits(&self) -> Option<ControlLimits> {
923 self.i_limits.clone()
924 }
925
926 fn is_in_control(&self) -> bool {
927 self.i_points.iter().all(|p| p.violations.is_empty())
928 && self.mr_points.iter().all(|p| p.violations.is_empty())
929 }
930
931 fn violations(&self) -> Vec<Violation> {
932 collect_violations(&self.i_points)
933 .into_iter()
934 .chain(collect_violations(&self.mr_points))
935 .collect()
936 }
937
938 fn points(&self) -> &[ChartPoint] {
939 &self.i_points
940 }
941}
942
943fn subgroup_range(subgroup: &[f64]) -> f64 {
953 let max_val =
954 u_numflow::stats::max(subgroup).expect("subgroup should be non-empty without NaN");
955 let min_val =
956 u_numflow::stats::min(subgroup).expect("subgroup should be non-empty without NaN");
957 max_val - min_val
958}
959
960fn apply_violations(points: &mut [ChartPoint], violations: &[(usize, ViolationType)]) {
962 for &(idx, vtype) in violations {
963 if let Some(point) = points.iter_mut().find(|p| p.index == idx) {
964 point.violations.push(vtype);
965 }
966 }
967}
968
969fn collect_violations(points: &[ChartPoint]) -> Vec<Violation> {
971 let mut result = Vec::new();
972 for point in points {
973 for &vtype in &point.violations {
974 result.push(Violation {
975 point_index: point.index,
976 violation_type: vtype,
977 });
978 }
979 }
980 result
981}
982
983#[cfg(test)]
988mod tests {
989 use super::*;
990
991 #[test]
994 fn test_xbar_r_basic_limits() {
995 let mut chart = XBarRChart::new(4).expect("subgroup size is in range");
996 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]).unwrap();
997 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]).unwrap();
998 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]).unwrap();
999 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]).unwrap();
1000 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]).unwrap();
1001
1002 let limits = chart.control_limits().expect("should have limits");
1003 let expected_grand_mean = (71.0 + 54.5 + 52.5 + 63.0 + 57.25) / 5.0;
1005 assert!(
1006 (limits.cl - expected_grand_mean).abs() < 0.1,
1007 "CL={}, expected ~{expected_grand_mean}",
1008 limits.cl
1009 );
1010
1011 assert!(limits.ucl > limits.cl);
1013 assert!(limits.cl > limits.lcl);
1014 }
1015
1016 #[test]
1017 fn test_xbar_r_rejects_wrong_size() {
1018 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1019 assert_eq!(
1020 chart.add_sample(&[1.0, 2.0, 3.0]),
1021 Err(ControlChartError::SampleLengthMismatch {
1022 expected: 5,
1023 got: 3
1024 })
1025 );
1026 assert!(chart.control_limits().is_none());
1027 }
1028
1029 #[test]
1030 fn test_xbar_r_rejects_nan() {
1031 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1032 assert_eq!(
1033 chart.add_sample(&[1.0, f64::NAN, 3.0]),
1034 Err(ControlChartError::NonFiniteValue)
1035 );
1036 assert!(chart.control_limits().is_none());
1037 }
1038
1039 #[test]
1040 fn test_xbar_r_r_chart_limits() {
1041 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1042 chart.add_sample(&[10.0, 12.0, 11.0, 13.0, 14.0]).unwrap();
1043 chart.add_sample(&[11.0, 13.0, 12.0, 10.0, 15.0]).unwrap();
1044 chart.add_sample(&[12.0, 11.0, 14.0, 13.0, 10.0]).unwrap();
1045
1046 let r_limits = chart.r_limits().expect("should have R limits");
1047 assert!(r_limits.ucl > r_limits.cl);
1048 assert!(r_limits.lcl >= 0.0);
1049 }
1050
1051 #[test]
1052 fn test_xbar_r_constant_subgroups() {
1053 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1055 chart.add_sample(&[10.0, 10.0, 10.0]).unwrap();
1056 chart.add_sample(&[10.0, 10.0, 10.0]).unwrap();
1057
1058 let limits = chart.control_limits().expect("should have limits");
1059 assert!((limits.cl - 10.0).abs() < f64::EPSILON);
1060 assert!((limits.ucl - 10.0).abs() < f64::EPSILON);
1061 assert!((limits.lcl - 10.0).abs() < f64::EPSILON);
1062 }
1063
1064 #[test]
1065 fn test_xbar_r_detects_out_of_control() {
1066 let mut chart = XBarRChart::new(3).expect("subgroup size is in range");
1067 for _ in 0..5 {
1068 chart.add_sample(&[10.0, 10.5, 9.5]).unwrap();
1069 }
1070 chart.add_sample(&[50.0, 51.0, 49.0]).unwrap();
1072
1073 assert!(!chart.is_in_control());
1074 }
1075
1076 #[test]
1084 fn factor_tables_reproduce_the_published_table() {
1085 const PUB_A2: [f64; 24] = [
1090 1.880, 1.023, 0.729, 0.577, 0.483, 0.419, 0.373, 0.337, 0.308, 0.285, 0.266, 0.249,
1091 0.235, 0.223, 0.212, 0.203, 0.194, 0.187, 0.180, 0.173, 0.167, 0.162, 0.157, 0.153,
1092 ];
1093 const PUB_A3: [f64; 24] = [
1094 2.659, 1.954, 1.628, 1.427, 1.287, 1.182, 1.099, 1.032, 0.975, 0.927, 0.886, 0.850,
1095 0.817, 0.789, 0.763, 0.739, 0.718, 0.698, 0.680, 0.663, 0.647, 0.633, 0.619, 0.606,
1096 ];
1097 const PUB_C4: [f64; 24] = [
1098 0.7979, 0.8862, 0.9213, 0.9400, 0.9515, 0.9594, 0.9650, 0.9693, 0.9727, 0.9754, 0.9776,
1099 0.9794, 0.9810, 0.9823, 0.9835, 0.9845, 0.9854, 0.9862, 0.9869, 0.9876, 0.9882, 0.9887,
1100 0.9892, 0.9896,
1101 ];
1102 const PUB_B3: [f64; 24] = [
1103 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,
1104 0.428, 0.448, 0.466, 0.482, 0.497, 0.510, 0.523, 0.534, 0.545, 0.555, 0.565,
1105 ];
1106 const PUB_B4: [f64; 24] = [
1107 3.267, 2.568, 2.266, 2.089, 1.970, 1.882, 1.815, 1.761, 1.716, 1.679, 1.646, 1.618,
1108 1.594, 1.572, 1.552, 1.534, 1.518, 1.503, 1.490, 1.477, 1.466, 1.455, 1.445, 1.435,
1109 ];
1110 const PUB_D2: [f64; 24] = [
1111 1.128, 1.693, 2.059, 2.326, 2.534, 2.704, 2.847, 2.970, 3.078, 3.173, 3.258, 3.336,
1112 3.407, 3.472, 3.532, 3.588, 3.640, 3.689, 3.735, 3.778, 3.819, 3.858, 3.895, 3.931,
1113 ];
1114 const PUB_D3: [f64; 24] = [
1115 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,
1116 0.363, 0.378, 0.392, 0.404, 0.415, 0.425, 0.435, 0.444, 0.452, 0.460,
1117 ];
1118 const PUB_D4: [f64; 24] = [
1119 3.266, 2.574, 2.281, 2.114, 2.003, 1.924, 1.863, 1.816, 1.777, 1.744, 1.716, 1.693,
1120 1.672, 1.653, 1.637, 1.622, 1.608, 1.596, 1.585, 1.575, 1.565, 1.556, 1.548, 1.540,
1121 ];
1122
1123 for (name, table, published, tol) in [
1128 ("A2", &A2, &PUB_A2, 5e-4),
1129 ("A3", &A3, &PUB_A3, 5e-4),
1130 ("c4", &C4, &PUB_C4, 5e-5),
1131 ("B3", &B3, &PUB_B3, 5e-4),
1132 ("B4", &B4, &PUB_B4, 5e-4),
1133 ("d2", &D2, &PUB_D2, 5.5e-4),
1134 ] {
1135 for (i, (got, want)) in table.iter().zip(published).enumerate() {
1136 assert!(
1137 (got - want).abs() <= tol,
1138 "{name}[n={}] = {got}, published {want}",
1139 i + 2
1140 );
1141 }
1142 }
1143
1144 for (name, table, published) in [("D3", &D3, &PUB_D3), ("D4", &D4, &PUB_D4)] {
1153 for (i, (got, want)) in table.iter().zip(published).enumerate() {
1154 assert!(
1155 (got - want).abs() <= 1e-3 + 1e-12,
1156 "{name}[n={}] = {got}, published {want}",
1157 i + 2
1158 );
1159 }
1160 }
1161 for i in 0..24 {
1162 if D3[i] > 0.0 {
1163 assert!(
1164 (D3[i] + D4[i] - 2.0).abs() < 1e-12,
1165 "D3 + D4 at n = {} is {}",
1166 i + 2,
1167 D3[i] + D4[i]
1168 );
1169 }
1170 }
1171 }
1172
1173 #[test]
1174 fn test_xbar_r_subgroup_size_below_range_is_an_error() {
1175 assert_eq!(
1176 XBarRChart::new(1).unwrap_err(),
1177 ControlChartError::SubgroupSizeOutOfRange {
1178 got: 1,
1179 min: 2,
1180 max: 25
1181 }
1182 );
1183 }
1184
1185 #[test]
1188 fn factor_tables_cover_the_declared_range() {
1189 let span = MAX_SUBGROUP_SIZE - MIN_SUBGROUP_SIZE + 1;
1190 for (name, len) in [
1191 ("A2", A2.len()),
1192 ("D3", D3.len()),
1193 ("D4", D4.len()),
1194 ("d2", D2.len()),
1195 ("A3", A3.len()),
1196 ("B3", B3.len()),
1197 ("B4", B4.len()),
1198 ("c4", C4.len()),
1199 ] {
1200 assert_eq!(
1201 len, span,
1202 "{name} does not cover {MIN_SUBGROUP_SIZE}..={MAX_SUBGROUP_SIZE}"
1203 );
1204 }
1205 for i in 1..span {
1207 assert!(D2[i] > D2[i - 1], "d2 not increasing at n={}", i + 2);
1208 assert!(C4[i] > C4[i - 1], "c4 not increasing at n={}", i + 2);
1209 assert!(A2[i] < A2[i - 1], "A2 not decreasing at n={}", i + 2);
1210 assert!(A3[i] < A3[i - 1], "A3 not decreasing at n={}", i + 2);
1211 assert!(D4[i] < D4[i - 1], "D4 not decreasing at n={}", i + 2);
1212 assert!(B4[i] < B4[i - 1], "B4 not decreasing at n={}", i + 2);
1213 assert!(D3[i] >= D3[i - 1], "D3 not non-decreasing at n={}", i + 2);
1214 assert!(B3[i] >= B3[i - 1], "B3 not non-decreasing at n={}", i + 2);
1215 }
1216 assert!(C4[span - 1] < 1.0 && C4[span - 1] > 0.98);
1218 }
1219
1220 #[test]
1221 fn test_xbar_r_subgroup_size_above_range_is_an_error() {
1222 assert_eq!(
1223 XBarRChart::new(26).unwrap_err(),
1224 ControlChartError::SubgroupSizeOutOfRange {
1225 got: 26,
1226 min: 2,
1227 max: 25
1228 }
1229 );
1230 }
1231
1232 #[test]
1233 fn test_subgroup_size_11_is_now_accepted() {
1234 let mut chart = XBarRChart::new(11).expect("11 is within 2..=25");
1237 for _ in 0..5 {
1238 chart
1239 .add_sample(&[10.0, 10.2, 9.8, 10.1, 9.9, 10.3, 9.7, 10.0, 10.1, 9.9, 10.0])
1240 .unwrap();
1241 }
1242 let limits = chart.control_limits().expect("limits from 5 subgroups");
1243 assert!(limits.ucl > limits.cl && limits.cl > limits.lcl);
1244 assert!(chart.sigma_hat().is_some_and(|s| s > 0.0));
1245 }
1246
1247 #[test]
1250 fn test_xbar_s_basic_limits() {
1251 let mut chart = XBarSChart::new(4).expect("subgroup size is in range");
1252 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]).unwrap();
1253 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]).unwrap();
1254 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]).unwrap();
1255 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]).unwrap();
1256 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]).unwrap();
1257
1258 let limits = chart.control_limits().expect("should have limits");
1259 assert!(limits.ucl > limits.cl);
1260 assert!(limits.cl > limits.lcl);
1261
1262 let s_limits = chart.s_limits().expect("should have S limits");
1263 assert!(s_limits.ucl > s_limits.cl);
1264 assert!(s_limits.lcl >= 0.0);
1265 }
1266
1267 #[test]
1268 fn test_xbar_s_rejects_wrong_size() {
1269 let mut chart = XBarSChart::new(5).expect("subgroup size is in range");
1270 assert_eq!(
1271 chart.add_sample(&[1.0, 2.0]),
1272 Err(ControlChartError::SampleLengthMismatch {
1273 expected: 5,
1274 got: 2
1275 })
1276 );
1277 assert!(chart.control_limits().is_none());
1278 }
1279
1280 #[test]
1281 fn test_xbar_s_in_control() {
1282 let mut chart = XBarSChart::new(4).expect("subgroup size is in range");
1283 for _ in 0..10 {
1284 chart.add_sample(&[10.0, 10.2, 9.8, 10.1]).unwrap();
1285 }
1286 assert!(chart.is_in_control());
1287 }
1288
1289 #[test]
1292 fn test_imr_basic_limits() {
1293 let mut chart = IndividualMRChart::new();
1294 let data = [10.0, 12.0, 11.0, 13.0, 10.0, 14.0, 11.0, 12.0, 13.0, 10.0];
1295 for &x in &data {
1296 chart.add_sample(&[x]).unwrap();
1297 }
1298
1299 let limits = chart.control_limits().expect("should have limits");
1300 assert!(limits.ucl > limits.cl);
1301 assert!(limits.cl > limits.lcl);
1302
1303 let mr_limits = chart.mr_limits().expect("should have MR limits");
1304 assert!(mr_limits.ucl > mr_limits.cl);
1305 assert!((mr_limits.lcl).abs() < f64::EPSILON);
1306 }
1307
1308 #[test]
1309 fn test_imr_needs_two_points() {
1310 let mut chart = IndividualMRChart::new();
1311 chart.add_sample(&[10.0]).unwrap();
1312 assert!(chart.control_limits().is_none());
1313 }
1314
1315 #[test]
1316 fn test_imr_center_line_is_mean() {
1317 let mut chart = IndividualMRChart::new();
1318 let data = [5.0, 10.0, 15.0, 20.0, 25.0];
1319 for &x in &data {
1320 chart.add_sample(&[x]).unwrap();
1321 }
1322 let limits = chart.control_limits().expect("should have limits");
1323 assert!((limits.cl - 15.0).abs() < f64::EPSILON);
1324 }
1325
1326 #[test]
1327 fn test_imr_mr_values() {
1328 let mut chart = IndividualMRChart::new();
1329 let data = [10.0, 12.0, 9.0];
1330 for &x in &data {
1331 chart.add_sample(&[x]).unwrap();
1332 }
1333 let mr_pts = chart.mr_points();
1335 assert_eq!(mr_pts.len(), 2);
1336 assert!((mr_pts[0].value - 2.0).abs() < f64::EPSILON);
1337 assert!((mr_pts[1].value - 3.0).abs() < f64::EPSILON);
1338 }
1339
1340 #[test]
1341 fn test_imr_rejects_multi_element_sample() {
1342 let mut chart = IndividualMRChart::new();
1343 assert_eq!(
1344 chart.add_sample(&[1.0, 2.0]),
1345 Err(ControlChartError::SampleLengthMismatch {
1346 expected: 1,
1347 got: 2
1348 })
1349 );
1350 assert!(chart.points().is_empty());
1351 }
1352
1353 #[test]
1354 fn test_imr_detects_out_of_control() {
1355 let mut chart = IndividualMRChart::new();
1356 for i in 0..10 {
1357 chart.add_sample(&[50.0 + (i as f64 % 3.0) * 0.5]).unwrap();
1358 }
1359 chart.add_sample(&[100.0]).unwrap();
1361
1362 assert!(!chart.is_in_control());
1363 }
1364
1365 #[test]
1366 fn test_imr_default() {
1367 let chart = IndividualMRChart::default();
1368 assert!(chart.points().is_empty());
1369 }
1370
1371 #[test]
1374 fn test_subgroup_range() {
1375 assert!((subgroup_range(&[1.0, 5.0, 3.0]) - 4.0).abs() < f64::EPSILON);
1376 assert!((subgroup_range(&[10.0, 10.0, 10.0])).abs() < f64::EPSILON);
1377 }
1378
1379 #[test]
1382 fn test_xbar_r_chart_factors_n5() {
1383 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1386 chart.add_sample(&[45.0, 47.0, 50.0, 53.0, 55.0]).unwrap();
1387
1388 let limits = chart.control_limits().expect("limits");
1389 assert!((limits.cl - 50.0).abs() < f64::EPSILON);
1390
1391 let r_limits = chart.r_limits().expect("R limits");
1392 assert!((r_limits.cl - 10.0).abs() < f64::EPSILON);
1393
1394 assert!((limits.ucl - 55.77).abs() < 0.01);
1396 assert!((limits.lcl - 44.23).abs() < 0.01);
1398 }
1399
1400 #[test]
1403 fn test_imr_e2_factor() {
1404 let mut chart = IndividualMRChart::new();
1407 chart.add_sample(&[95.0]).unwrap();
1408 chart.add_sample(&[105.0]).unwrap();
1409
1410 let limits = chart.control_limits().expect("limits");
1411 assert!((limits.cl - 100.0).abs() < f64::EPSILON);
1413 assert!((limits.ucl - 126.6).abs() < 0.1);
1416 assert!((limits.lcl - 73.4).abs() < 0.1);
1418 }
1419
1420 #[test]
1432 fn xbar_r_sigma_hat_is_r_bar_over_d2() {
1433 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1434 let groups = [
1435 [9.9, 10.1, 10.0, 9.8, 10.2],
1436 [10.3, 9.7, 10.0, 10.1, 9.9],
1437 [9.8, 10.2, 10.1, 9.9, 10.0],
1438 [10.5, 9.5, 10.0, 10.2, 9.8],
1439 ];
1440 for g in &groups {
1441 chart.add_sample(g).unwrap();
1442 }
1443 let r_bar = chart.r_limits().expect("limits").cl;
1444 let expected = r_bar / D2[5 - 2];
1445 let sigma = chart.sigma_hat().expect("sigma-hat");
1446 assert!(
1447 (sigma - expected).abs() < 1e-12,
1448 "sigma-hat must be R-bar/d2: got {sigma}, expected {expected}"
1449 );
1450 assert!(sigma > 0.0);
1451 }
1452
1453 #[test]
1454 fn xbar_s_sigma_hat_is_s_bar_over_c4() {
1455 let mut chart = XBarSChart::new(5).expect("subgroup size is in range");
1456 for g in &[
1457 [9.9, 10.1, 10.0, 9.8, 10.2],
1458 [10.3, 9.7, 10.0, 10.1, 9.9],
1459 [9.8, 10.2, 10.1, 9.9, 10.0],
1460 ] {
1461 chart.add_sample(g).unwrap();
1462 }
1463 let s_bar = chart.s_limits().expect("limits").cl;
1464 let expected = s_bar / C4[5 - 2];
1465 let sigma = chart.sigma_hat().expect("sigma-hat");
1466 assert!((sigma - expected).abs() < 1e-12);
1467 }
1468
1469 #[test]
1470 fn sigma_hat_is_none_without_limits() {
1471 assert!(XBarRChart::new(5).unwrap().sigma_hat().is_none());
1472 assert!(XBarSChart::new(5).unwrap().sigma_hat().is_none());
1473 }
1474
1475 #[test]
1476 fn short_term_sigma_differs_from_overall_when_subgroups_drift() {
1477 use u_numflow::stats;
1478
1479 let groups = [
1482 [9.9, 10.1, 10.0, 9.8, 10.2],
1483 [10.3, 9.7, 10.0, 10.1, 9.9],
1484 [9.8, 10.2, 10.1, 9.9, 10.0],
1485 [10.5, 9.5, 10.0, 10.2, 9.8],
1486 [9.6, 10.4, 10.0, 9.9, 10.1],
1487 [10.1, 9.9, 10.0, 10.3, 9.7],
1488 ];
1489 let mut chart = XBarRChart::new(5).expect("subgroup size is in range");
1490 for g in &groups {
1491 chart.add_sample(g).unwrap();
1492 }
1493 let sigma_within = chart.sigma_hat().expect("sigma-hat");
1494 let flat: Vec<f64> = groups.iter().flatten().copied().collect();
1495 let sigma_overall = stats::std_dev(&flat).expect("overall sigma");
1496
1497 assert!(
1498 (sigma_within - sigma_overall).abs() > 1e-6,
1499 "short-term and long-term sigma must differ on drifting subgroups: within={sigma_within}, overall={sigma_overall}"
1500 );
1501 }
1502}