1#[derive(Debug, Clone)]
27pub struct AttributeChartPoint {
28 pub index: usize,
30 pub value: f64,
32 pub ucl: f64,
34 pub cl: f64,
36 pub lcl: f64,
38 pub out_of_control: bool,
40}
41
42pub struct PChart {
77 samples: Vec<(u64, u64)>,
79 chart_points: Vec<AttributeChartPoint>,
81 p_bar: Option<f64>,
83}
84
85impl PChart {
86 pub fn new() -> Self {
88 Self {
89 samples: Vec::new(),
90 chart_points: Vec::new(),
91 p_bar: None,
92 }
93 }
94
95 pub fn add_sample(
105 &mut self,
106 defectives: u64,
107 sample_size: u64,
108 ) -> Result<(), super::chart::ControlChartError> {
109 use super::chart::ControlChartError;
110 if sample_size == 0 {
111 return Err(ControlChartError::ZeroSampleSize);
112 }
113 if defectives > sample_size {
114 return Err(ControlChartError::DefectivesExceedSampleSize {
115 defectives,
116 sample_size,
117 });
118 }
119 self.samples.push((defectives, sample_size));
120 self.recompute();
121 Ok(())
122 }
123
124 pub fn p_bar(&self) -> Option<f64> {
126 self.p_bar
127 }
128
129 pub fn points(&self) -> &[AttributeChartPoint] {
131 &self.chart_points
132 }
133
134 pub fn is_in_control(&self) -> bool {
136 self.chart_points.iter().all(|p| !p.out_of_control)
137 }
138
139 fn recompute(&mut self) {
141 if self.samples.is_empty() {
142 self.p_bar = None;
143 self.chart_points.clear();
144 return;
145 }
146
147 let total_defectives: u64 = self.samples.iter().map(|&(d, _)| d).sum();
148 let total_inspected: u64 = self.samples.iter().map(|&(_, n)| n).sum();
149 let p_bar = total_defectives as f64 / total_inspected as f64;
150 self.p_bar = Some(p_bar);
151
152 self.chart_points = self
153 .samples
154 .iter()
155 .enumerate()
156 .map(|(i, &(defectives, sample_size))| {
157 let p = defectives as f64 / sample_size as f64;
158 let n = sample_size as f64;
159 let sigma = (p_bar * (1.0 - p_bar) / n).sqrt();
160 let ucl = p_bar + 3.0 * sigma;
161 let lcl = (p_bar - 3.0 * sigma).max(0.0);
162
163 AttributeChartPoint {
164 index: i,
165 value: p,
166 ucl,
167 cl: p_bar,
168 lcl,
169 out_of_control: p > ucl || p < lcl,
170 }
171 })
172 .collect();
173 }
174}
175
176impl Default for PChart {
177 fn default() -> Self {
178 Self::new()
179 }
180}
181
182pub struct NPChart {
202 sample_size: u64,
204 defective_counts: Vec<u64>,
206 chart_points: Vec<AttributeChartPoint>,
208 limits: Option<(f64, f64, f64)>, }
211
212impl NPChart {
213 pub fn new(sample_size: u64) -> Result<Self, super::chart::ControlChartError> {
222 if sample_size == 0 {
223 return Err(super::chart::ControlChartError::ZeroSampleSize);
224 }
225 Ok(Self {
226 sample_size,
227 defective_counts: Vec::new(),
228 chart_points: Vec::new(),
229 limits: None,
230 })
231 }
232
233 pub fn add_sample(&mut self, defectives: u64) -> Result<(), super::chart::ControlChartError> {
241 if defectives > self.sample_size {
242 return Err(
243 super::chart::ControlChartError::DefectivesExceedSampleSize {
244 defectives,
245 sample_size: self.sample_size,
246 },
247 );
248 }
249 self.defective_counts.push(defectives);
250 self.recompute();
251 Ok(())
252 }
253
254 pub fn control_limits(&self) -> Option<(f64, f64, f64)> {
256 self.limits
257 }
258
259 pub fn points(&self) -> &[AttributeChartPoint] {
261 &self.chart_points
262 }
263
264 pub fn is_in_control(&self) -> bool {
266 self.chart_points.iter().all(|p| !p.out_of_control)
267 }
268
269 fn recompute(&mut self) {
271 if self.defective_counts.is_empty() {
272 self.limits = None;
273 self.chart_points.clear();
274 return;
275 }
276
277 let total_defectives: u64 = self.defective_counts.iter().sum();
278 let total_inspected = self.sample_size * self.defective_counts.len() as u64;
279 let p_bar = total_defectives as f64 / total_inspected as f64;
280 let n = self.sample_size as f64;
281
282 let np_bar = n * p_bar;
283 let sigma = (n * p_bar * (1.0 - p_bar)).sqrt();
284 let ucl = np_bar + 3.0 * sigma;
285 let lcl = (np_bar - 3.0 * sigma).max(0.0);
286
287 self.limits = Some((ucl, np_bar, lcl));
288
289 self.chart_points = self
290 .defective_counts
291 .iter()
292 .enumerate()
293 .map(|(i, &count)| {
294 let value = count as f64;
295 AttributeChartPoint {
296 index: i,
297 value,
298 ucl,
299 cl: np_bar,
300 lcl,
301 out_of_control: value > ucl || value < lcl,
302 }
303 })
304 .collect();
305 }
306}
307
308pub struct CChart {
328 defect_counts: Vec<u64>,
330 chart_points: Vec<AttributeChartPoint>,
332 limits: Option<(f64, f64, f64)>, }
335
336impl CChart {
337 pub fn new() -> Self {
339 Self {
340 defect_counts: Vec::new(),
341 chart_points: Vec::new(),
342 limits: None,
343 }
344 }
345
346 pub fn add_sample(&mut self, defects: u64) {
348 self.defect_counts.push(defects);
349 self.recompute();
350 }
351
352 pub fn control_limits(&self) -> Option<(f64, f64, f64)> {
354 self.limits
355 }
356
357 pub fn points(&self) -> &[AttributeChartPoint] {
359 &self.chart_points
360 }
361
362 pub fn is_in_control(&self) -> bool {
364 self.chart_points.iter().all(|p| !p.out_of_control)
365 }
366
367 fn recompute(&mut self) {
369 if self.defect_counts.is_empty() {
370 self.limits = None;
371 self.chart_points.clear();
372 return;
373 }
374
375 let total: u64 = self.defect_counts.iter().sum();
376 let c_bar = total as f64 / self.defect_counts.len() as f64;
377 let sigma = c_bar.sqrt();
378 let ucl = c_bar + 3.0 * sigma;
379 let lcl = (c_bar - 3.0 * sigma).max(0.0);
380
381 self.limits = Some((ucl, c_bar, lcl));
382
383 self.chart_points = self
384 .defect_counts
385 .iter()
386 .enumerate()
387 .map(|(i, &count)| {
388 let value = count as f64;
389 AttributeChartPoint {
390 index: i,
391 value,
392 ucl,
393 cl: c_bar,
394 lcl,
395 out_of_control: value > ucl || value < lcl,
396 }
397 })
398 .collect();
399 }
400}
401
402impl Default for CChart {
403 fn default() -> Self {
404 Self::new()
405 }
406}
407
408pub struct UChart {
429 samples: Vec<(u64, f64)>,
431 chart_points: Vec<AttributeChartPoint>,
433 u_bar: Option<f64>,
435}
436
437impl UChart {
438 pub fn new() -> Self {
440 Self {
441 samples: Vec::new(),
442 chart_points: Vec::new(),
443 u_bar: None,
444 }
445 }
446
447 pub fn add_sample(
457 &mut self,
458 defects: u64,
459 units_inspected: f64,
460 ) -> Result<(), super::chart::ControlChartError> {
461 if !units_inspected.is_finite() || units_inspected <= 0.0 {
462 return Err(super::chart::ControlChartError::NonPositiveUnits);
463 }
464 self.samples.push((defects, units_inspected));
465 self.recompute();
466 Ok(())
467 }
468
469 pub fn u_bar(&self) -> Option<f64> {
471 self.u_bar
472 }
473
474 pub fn points(&self) -> &[AttributeChartPoint] {
476 &self.chart_points
477 }
478
479 pub fn is_in_control(&self) -> bool {
481 self.chart_points.iter().all(|p| !p.out_of_control)
482 }
483
484 fn recompute(&mut self) {
486 if self.samples.is_empty() {
487 self.u_bar = None;
488 self.chart_points.clear();
489 return;
490 }
491
492 let total_defects: u64 = self.samples.iter().map(|&(d, _)| d).sum();
493 let total_units: f64 = self.samples.iter().map(|&(_, n)| n).sum();
494 let u_bar = total_defects as f64 / total_units;
495 self.u_bar = Some(u_bar);
496
497 self.chart_points = self
498 .samples
499 .iter()
500 .enumerate()
501 .map(|(i, &(defects, units))| {
502 let u = defects as f64 / units;
503 let sigma = (u_bar / units).sqrt();
504 let ucl = u_bar + 3.0 * sigma;
505 let lcl = (u_bar - 3.0 * sigma).max(0.0);
506
507 AttributeChartPoint {
508 index: i,
509 value: u,
510 ucl,
511 cl: u_bar,
512 lcl,
513 out_of_control: u > ucl || u < lcl,
514 }
515 })
516 .collect();
517 }
518}
519
520impl Default for UChart {
521 fn default() -> Self {
522 Self::new()
523 }
524}
525
526#[derive(Debug, Clone)]
541pub struct LaneyAttributePoint {
542 pub index: usize,
544 pub value: f64,
546 pub ucl: f64,
548 pub cl: f64,
550 pub lcl: f64,
552 pub out_of_control: bool,
554}
555
556#[derive(Debug, Clone)]
561pub struct LaneyPChart {
562 pub p_bar: f64,
564 pub phi: f64,
567 pub points: Vec<LaneyAttributePoint>,
569}
570
571#[derive(Debug, Clone)]
576pub struct LaneyUChart {
577 pub u_bar: f64,
579 pub phi: f64,
582 pub points: Vec<LaneyAttributePoint>,
584}
585
586pub fn laney_p_chart(samples: &[(u64, u64)]) -> Option<LaneyPChart> {
613 if samples.len() < 3 {
614 return None;
615 }
616 if samples.iter().any(|&(d, n)| n == 0 || d > n) {
620 return None;
621 }
622
623 let total_defectives: u64 = samples.iter().map(|&(d, _)| d).sum();
624 let total_inspected: u64 = samples.iter().map(|&(_, n)| n).sum();
625
626 let p_bar = total_defectives as f64 / total_inspected as f64;
627 let base_var = p_bar * (1.0 - p_bar);
629 if base_var <= 0.0 {
630 let points = samples
632 .iter()
633 .enumerate()
634 .map(|(i, &(d, n))| {
635 let value = d as f64 / n as f64;
636 LaneyAttributePoint {
637 index: i,
638 value,
639 ucl: p_bar,
640 cl: p_bar,
641 lcl: p_bar,
642 out_of_control: false,
643 }
644 })
645 .collect();
646 return Some(LaneyPChart {
647 p_bar,
648 phi: 0.0,
649 points,
650 });
651 }
652
653 let z_scores: Vec<f64> = samples
655 .iter()
656 .map(|&(d, n)| {
657 let p_i = d as f64 / n as f64;
658 let sigma_i = (base_var / n as f64).sqrt();
659 (p_i - p_bar) / sigma_i
660 })
661 .collect();
662
663 let mr_bar = {
665 let mrs: Vec<f64> = z_scores.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
666 mrs.iter().sum::<f64>() / mrs.len() as f64
667 };
668
669 const D2: f64 = 1.128;
671 let phi = mr_bar / D2;
672
673 let points = samples
675 .iter()
676 .enumerate()
677 .map(|(i, &(d, n))| {
678 let p_i = d as f64 / n as f64;
679 let sigma_i = (base_var / n as f64).sqrt();
680 let ucl = p_bar + 3.0 * phi * sigma_i;
681 let lcl = (p_bar - 3.0 * phi * sigma_i).max(0.0);
682 LaneyAttributePoint {
683 index: i,
684 value: p_i,
685 ucl,
686 cl: p_bar,
687 lcl,
688 out_of_control: p_i > ucl || p_i < lcl,
689 }
690 })
691 .collect();
692
693 Some(LaneyPChart { p_bar, phi, points })
694}
695
696pub fn laney_u_chart(samples: &[(u64, f64)]) -> Option<LaneyUChart> {
721 if samples.len() < 3 {
722 return None;
723 }
724
725 if samples.iter().any(|&(_, n)| !n.is_finite() || n <= 0.0) {
727 return None;
728 }
729
730 let total_defects: u64 = samples.iter().map(|&(d, _)| d).sum();
731 let total_units: f64 = samples.iter().map(|&(_, n)| n).sum();
732 if total_units <= 0.0 {
733 return None;
734 }
735
736 let u_bar = total_defects as f64 / total_units;
737
738 if u_bar <= 0.0 {
740 let points = samples
741 .iter()
742 .enumerate()
743 .map(|(i, &(d, n))| {
744 let value = d as f64 / n;
745 LaneyAttributePoint {
746 index: i,
747 value,
748 ucl: 0.0,
749 cl: 0.0,
750 lcl: 0.0,
751 out_of_control: false,
752 }
753 })
754 .collect();
755 return Some(LaneyUChart {
756 u_bar: 0.0,
757 phi: 0.0,
758 points,
759 });
760 }
761
762 let z_scores: Vec<f64> = samples
764 .iter()
765 .map(|&(d, n)| {
766 let u_i = d as f64 / n;
767 let sigma_i = (u_bar / n).sqrt();
768 (u_i - u_bar) / sigma_i
769 })
770 .collect();
771
772 let mr_bar = {
774 let mrs: Vec<f64> = z_scores.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
775 mrs.iter().sum::<f64>() / mrs.len() as f64
776 };
777
778 const D2: f64 = 1.128;
780 let phi = mr_bar / D2;
781
782 let points = samples
784 .iter()
785 .enumerate()
786 .map(|(i, &(d, n))| {
787 let u_i = d as f64 / n;
788 let sigma_i = (u_bar / n).sqrt();
789 let ucl = u_bar + 3.0 * phi * sigma_i;
790 let lcl = (u_bar - 3.0 * phi * sigma_i).max(0.0);
791 LaneyAttributePoint {
792 index: i,
793 value: u_i,
794 ucl,
795 cl: u_bar,
796 lcl,
797 out_of_control: u_i > ucl || u_i < lcl,
798 }
799 })
800 .collect();
801
802 Some(LaneyUChart { u_bar, phi, points })
803}
804
805#[derive(Debug, Clone)]
811pub struct GChartPoint {
812 pub index: usize,
814 pub value: f64,
816 pub ucl: f64,
818 pub cl: f64,
820 pub lcl: f64,
822 pub out_of_control: bool,
824}
825
826#[derive(Debug, Clone)]
843pub struct GChart {
844 pub g_bar: f64,
846 pub points: Vec<GChartPoint>,
848}
849
850#[derive(Debug, Clone)]
862pub struct TChart {
863 pub t_bar: f64,
865 pub points: Vec<TChartPoint>,
867}
868
869#[derive(Debug, Clone)]
871pub struct TChartPoint {
872 pub index: usize,
874 pub value: f64,
876 pub ucl: f64,
878 pub cl: f64,
880 pub lcl: f64,
882 pub out_of_control: bool,
884}
885
886pub fn g_chart(inter_event_counts: &[f64]) -> Option<GChart> {
912 if inter_event_counts.len() < 3 {
913 return None;
914 }
915 if inter_event_counts
916 .iter()
917 .any(|&v| !v.is_finite() || v < 0.0)
918 {
919 return None;
920 }
921
922 let g_bar = inter_event_counts.iter().sum::<f64>() / inter_event_counts.len() as f64;
923 let spread = (g_bar * (g_bar + 1.0)).sqrt();
924 let ucl = g_bar + 3.0 * spread;
925 let lcl = (g_bar - 3.0 * spread).max(0.0);
926
927 let points = inter_event_counts
928 .iter()
929 .enumerate()
930 .map(|(i, &v)| GChartPoint {
931 index: i,
932 value: v,
933 ucl,
934 cl: g_bar,
935 lcl,
936 out_of_control: v > ucl || v < lcl,
937 })
938 .collect();
939
940 Some(GChart { g_bar, points })
941}
942
943pub fn t_chart(inter_event_times: &[f64]) -> Option<TChart> {
965 if inter_event_times.len() < 3 {
966 return None;
967 }
968 if inter_event_times
969 .iter()
970 .any(|&v| !v.is_finite() || v <= 0.0)
971 {
972 return None;
973 }
974
975 let t_bar = inter_event_times.iter().sum::<f64>() / inter_event_times.len() as f64;
976
977 let ucl_factor = -(0.00135_f64.ln()); let lcl_factor = -(0.99865_f64.ln()); let ucl = t_bar * ucl_factor;
984 let lcl = (t_bar * lcl_factor).max(0.0);
985
986 let points = inter_event_times
987 .iter()
988 .enumerate()
989 .map(|(i, &v)| TChartPoint {
990 index: i,
991 value: v,
992 ucl,
993 cl: t_bar,
994 lcl,
995 out_of_control: v > ucl || v < lcl,
996 })
997 .collect();
998
999 Some(TChart { t_bar, points })
1000}
1001
1002#[cfg(test)]
1007mod tests {
1008 use super::*;
1009
1010 #[test]
1013 fn test_p_chart_basic() {
1014 let mut chart = PChart::new();
1016 let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
1017 for &d in &defectives {
1018 chart.add_sample(d, 100).unwrap();
1019 }
1020
1021 let p_bar = chart.p_bar().expect("should have p_bar");
1022 assert!(
1024 (p_bar - 0.055).abs() < 1e-10,
1025 "p_bar={p_bar}, expected 0.055"
1026 );
1027
1028 assert_eq!(chart.points().len(), 10);
1030
1031 for pt in chart.points() {
1033 assert!((pt.cl - 0.055).abs() < 1e-10);
1034 }
1035 }
1036
1037 #[test]
1038 fn test_p_chart_limits() {
1039 let mut chart = PChart::new();
1040 chart.add_sample(10, 100).unwrap();
1045
1046 let pt = &chart.points()[0];
1047 assert!((pt.cl - 0.1).abs() < 1e-10);
1048 assert!((pt.ucl - 0.19).abs() < 0.001);
1049 assert!((pt.lcl - 0.01).abs() < 0.001);
1050 }
1051
1052 #[test]
1053 fn test_p_chart_variable_sample_sizes() {
1054 let mut chart = PChart::new();
1055 chart.add_sample(5, 100).unwrap();
1056 chart.add_sample(10, 200).unwrap();
1057 chart.add_sample(3, 50).unwrap();
1058
1059 let p_bar = chart.p_bar().expect("p_bar");
1061 assert!((p_bar - 18.0 / 350.0).abs() < 1e-10);
1062
1063 let pts = chart.points();
1065 assert!(pts[1].ucl - pts[1].cl < pts[0].ucl - pts[0].cl);
1067 }
1068
1069 #[test]
1070 fn test_p_chart_rejects_invalid() {
1071 use crate::spc::ControlChartError;
1072 let mut chart = PChart::new();
1073 assert_eq!(
1074 chart.add_sample(5, 0),
1075 Err(ControlChartError::ZeroSampleSize)
1076 );
1077 assert_eq!(
1078 chart.add_sample(10, 5),
1079 Err(ControlChartError::DefectivesExceedSampleSize {
1080 defectives: 10,
1081 sample_size: 5
1082 })
1083 );
1084 assert!(chart.p_bar().is_none());
1085 }
1086
1087 #[test]
1088 fn test_p_chart_lcl_clamped_to_zero() {
1089 let mut chart = PChart::new();
1090 chart.add_sample(1, 10).unwrap();
1092 let pt = &chart.points()[0];
1093 assert!(pt.lcl >= 0.0);
1094 }
1095
1096 #[test]
1097 fn test_p_chart_out_of_control() {
1098 let mut chart = PChart::new();
1099 for _ in 0..20 {
1101 chart.add_sample(5, 100).unwrap();
1102 }
1103 chart.add_sample(30, 100).unwrap();
1105
1106 assert!(!chart.is_in_control());
1107 let last = chart.points().last().expect("should have points");
1108 assert!(last.out_of_control);
1109 }
1110
1111 #[test]
1112 fn test_p_chart_default() {
1113 let chart = PChart::default();
1114 assert!(chart.p_bar().is_none());
1115 assert!(chart.points().is_empty());
1116 }
1117
1118 #[test]
1121 fn test_np_chart_basic() {
1122 let mut chart = NPChart::new(100).expect("100 is a valid sample size");
1123 let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
1124 for &d in &defectives {
1125 chart.add_sample(d).unwrap();
1126 }
1127
1128 let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
1129 assert!((cl - 5.5).abs() < 1e-10);
1131 assert!(ucl > cl);
1132 assert!(lcl < cl);
1133 assert!(lcl >= 0.0);
1134 }
1135
1136 #[test]
1137 fn test_np_chart_rejects_invalid() {
1138 let mut chart = NPChart::new(100).expect("100 is a valid sample size");
1139 assert_eq!(
1140 chart.add_sample(101),
1141 Err(crate::spc::ControlChartError::DefectivesExceedSampleSize {
1142 defectives: 101,
1143 sample_size: 100
1144 })
1145 );
1146 assert!(chart.control_limits().is_none());
1147 }
1148
1149 #[test]
1150 fn test_np_chart_zero_sample_size() {
1151 assert!(matches!(
1152 NPChart::new(0),
1153 Err(crate::spc::ControlChartError::ZeroSampleSize)
1154 ));
1155 }
1156
1157 #[test]
1158 fn test_np_chart_out_of_control() {
1159 let mut chart = NPChart::new(100).expect("100 is a valid sample size");
1160 for _ in 0..20 {
1161 chart.add_sample(5).unwrap();
1162 }
1163 chart.add_sample(30).unwrap();
1164
1165 assert!(!chart.is_in_control());
1166 }
1167
1168 #[test]
1169 fn test_np_chart_limits_formula() {
1170 let mut chart = NPChart::new(200).expect("200 is a valid sample size");
1175 for _ in 0..10 {
1176 chart.add_sample(10).unwrap();
1177 }
1178
1179 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1180 assert!((cl - 10.0).abs() < 1e-10);
1181 let expected_sigma = (200.0_f64 * 0.05 * 0.95).sqrt();
1182 assert!((ucl - (10.0 + 3.0 * expected_sigma)).abs() < 0.01);
1183 assert!((lcl - (10.0 - 3.0 * expected_sigma)).abs() < 0.01);
1184 }
1185
1186 #[test]
1189 fn test_c_chart_basic() {
1190 let mut chart = CChart::new();
1191 let counts = [3, 5, 4, 6, 2, 7, 3, 4, 5, 6];
1192 for &c in &counts {
1193 chart.add_sample(c);
1194 }
1195
1196 let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
1197 assert!((cl - 4.5).abs() < 1e-10);
1199 let expected_ucl = 4.5 + 3.0 * 4.5_f64.sqrt();
1201 assert!((ucl - expected_ucl).abs() < 0.01);
1202 assert!(lcl >= 0.0);
1203 }
1204
1205 #[test]
1206 fn test_c_chart_out_of_control() {
1207 let mut chart = CChart::new();
1208 for _ in 0..20 {
1209 chart.add_sample(5);
1210 }
1211 chart.add_sample(50); assert!(!chart.is_in_control());
1214 }
1215
1216 #[test]
1217 fn test_c_chart_single_sample() {
1218 let mut chart = CChart::new();
1219 chart.add_sample(10);
1220
1221 let (_, cl, _) = chart.control_limits().expect("limits");
1222 assert!((cl - 10.0).abs() < f64::EPSILON);
1223 }
1224
1225 #[test]
1226 fn test_c_chart_lcl_clamped() {
1227 let mut chart = CChart::new();
1229 chart.add_sample(1);
1230
1231 let (_, _, lcl) = chart.control_limits().expect("limits");
1232 assert!((lcl - 0.0).abs() < f64::EPSILON);
1233 }
1234
1235 #[test]
1236 fn test_c_chart_default() {
1237 let chart = CChart::default();
1238 assert!(chart.control_limits().is_none());
1239 assert!(chart.points().is_empty());
1240 }
1241
1242 #[test]
1245 fn test_u_chart_basic() {
1246 let mut chart = UChart::new();
1247 chart.add_sample(3, 10.0).unwrap();
1249 chart.add_sample(5, 10.0).unwrap();
1250 chart.add_sample(4, 10.0).unwrap();
1251 chart.add_sample(6, 10.0).unwrap();
1252 chart.add_sample(2, 10.0).unwrap();
1253
1254 let u_bar = chart.u_bar().expect("should have u_bar");
1255 assert!((u_bar - 0.4).abs() < 1e-10);
1257
1258 assert_eq!(chart.points().len(), 5);
1259 }
1260
1261 #[test]
1262 fn test_u_chart_variable_units() {
1263 let mut chart = UChart::new();
1264 chart.add_sample(10, 5.0).unwrap(); chart.add_sample(20, 10.0).unwrap(); chart.add_sample(5, 2.5).unwrap(); let u_bar = chart.u_bar().expect("u_bar");
1269 assert!((u_bar - 2.0).abs() < 1e-10);
1271
1272 let pts = chart.points();
1274 let width_0 = pts[0].ucl - pts[0].cl; let width_1 = pts[1].ucl - pts[1].cl; assert!(width_1 < width_0, "larger n should have tighter limits");
1277 }
1278
1279 #[test]
1280 fn test_u_chart_rejects_invalid() {
1281 let mut chart = UChart::new();
1282 for units in [0.0, -1.0, f64::NAN, f64::INFINITY] {
1283 assert_eq!(
1284 chart.add_sample(5, units),
1285 Err(crate::spc::ControlChartError::NonPositiveUnits),
1286 "units = {units}"
1287 );
1288 }
1289 assert!(chart.u_bar().is_none());
1290 }
1291
1292 #[test]
1293 fn test_u_chart_out_of_control() {
1294 let mut chart = UChart::new();
1295 for _ in 0..20 {
1296 chart.add_sample(4, 10.0).unwrap();
1297 }
1298 chart.add_sample(50, 10.0).unwrap(); assert!(!chart.is_in_control());
1301 }
1302
1303 #[test]
1304 fn test_u_chart_lcl_clamped() {
1305 let mut chart = UChart::new();
1306 chart.add_sample(1, 1.0).unwrap();
1308 let pt = &chart.points()[0];
1309 assert!(pt.lcl >= 0.0);
1310 }
1311
1312 #[test]
1313 fn test_u_chart_default() {
1314 let chart = UChart::default();
1315 assert!(chart.u_bar().is_none());
1316 assert!(chart.points().is_empty());
1317 }
1318
1319 #[test]
1320 fn test_u_chart_limits_formula() {
1321 let mut chart = UChart::new();
1326 chart.add_sample(8, 4.0).unwrap();
1327
1328 let pt = &chart.points()[0];
1329 assert!((pt.cl - 2.0).abs() < 1e-10);
1330 let expected_sigma = (2.0_f64 / 4.0).sqrt();
1331 assert!((pt.ucl - (2.0 + 3.0 * expected_sigma)).abs() < 0.001);
1332 }
1333
1334 #[test]
1337 fn test_p_and_np_consistent() {
1338 let mut p_chart = PChart::new();
1340 let mut np_chart = NPChart::new(100).expect("100 is a valid sample size");
1341
1342 let defectives = [5, 8, 3, 6, 4];
1343 for &d in &defectives {
1344 p_chart.add_sample(d, 100).unwrap();
1345 np_chart.add_sample(d).unwrap();
1346 }
1347
1348 let p_bar = p_chart.p_bar().expect("p_bar");
1349 let (_, np_cl, _) = np_chart.control_limits().expect("np limits");
1350
1351 assert!(
1353 (np_cl - 100.0 * p_bar).abs() < 1e-10,
1354 "NP CL should equal n * p_bar"
1355 );
1356 }
1357
1358 #[test]
1359 fn test_c_and_u_consistent_equal_units() {
1360 let mut c_chart = CChart::new();
1362 let mut u_chart = UChart::new();
1363
1364 let defects = [3, 5, 4, 6, 2];
1365 for &d in &defects {
1366 c_chart.add_sample(d);
1367 u_chart.add_sample(d, 1.0).unwrap();
1368 }
1369
1370 let (c_ucl, c_cl, c_lcl) = c_chart.control_limits().expect("C limits");
1371 let u_bar = u_chart.u_bar().expect("u_bar");
1372
1373 assert!(
1374 (c_cl - u_bar).abs() < 1e-10,
1375 "C chart CL should equal U chart u-bar when n=1"
1376 );
1377
1378 let u_pt = &u_chart.points()[0];
1379 assert!((u_pt.ucl - c_ucl).abs() < 1e-10);
1380 assert!((u_pt.lcl - c_lcl).abs() < 1e-10);
1381 }
1382
1383 #[test]
1386 fn laney_p_basic() {
1387 let samples: Vec<(u64, u64)> = (0..10).map(|i| (i % 5 + 2, 200)).collect();
1388 let chart = laney_p_chart(&samples).expect("chart should be Some");
1389 assert!(chart.phi > 0.0);
1390 assert!(chart.p_bar > 0.0 && chart.p_bar < 1.0);
1391 assert_eq!(chart.points.len(), 10);
1392 }
1393
1394 #[test]
1395 fn laney_p_constant_proportion_phi_near_zero() {
1396 let samples: Vec<(u64, u64)> = vec![(10, 1000); 20];
1398 let chart = laney_p_chart(&samples).expect("chart should be Some");
1399 assert!((chart.p_bar - 0.01).abs() < 1e-10);
1400 assert!(chart.phi >= 0.0);
1401 }
1402
1403 #[test]
1404 fn laney_p_ucl_above_lcl() {
1405 let samples: Vec<(u64, u64)> = vec![(5, 100), (8, 100), (3, 100), (6, 100), (4, 100)];
1406 let chart = laney_p_chart(&samples).expect("chart should be Some");
1407 for p in &chart.points {
1408 assert!(p.ucl >= p.lcl);
1409 assert!((p.cl - chart.p_bar).abs() < 1e-10);
1410 }
1411 }
1412
1413 #[test]
1414 fn laney_p_insufficient_data() {
1415 let samples: Vec<(u64, u64)> = vec![(2, 100), (3, 100)];
1416 assert!(laney_p_chart(&samples).is_none());
1417 }
1418
1419 #[test]
1420 fn laney_p_rejects_a_sample_with_no_proportion() {
1421 assert!(laney_p_chart(&[(3, 100), (0, 0), (4, 100), (2, 100)]).is_none());
1425 assert!(laney_p_chart(&[(3, 100), (12, 10), (4, 100), (2, 100)]).is_none());
1427 }
1428 #[test]
1429 fn laney_u_basic() {
1430 let samples: Vec<(u64, f64)> = vec![(5, 10.0); 10];
1431 let chart = laney_u_chart(&samples).expect("chart should be Some");
1432 assert!((chart.u_bar - 0.5).abs() < 1e-10);
1433 assert!(chart.phi >= 0.0);
1434 }
1435
1436 #[test]
1437 fn laney_u_ucl_above_cl() {
1438 let samples: Vec<(u64, f64)> = (0..8).map(|i| ((i % 4 + 2) as u64, 10.0)).collect();
1439 let chart = laney_u_chart(&samples).expect("chart should be Some");
1440 for p in &chart.points {
1441 assert!(p.ucl > p.cl || (p.ucl - p.cl).abs() < 1e-10);
1442 }
1443 }
1444
1445 #[test]
1456 fn p_chart_montgomery_reference_formula() {
1457 let mut chart = PChart::new();
1460 for _ in 0..19 {
1461 chart.add_sample(10, 100).unwrap();
1462 }
1463 chart.add_sample(8, 100).unwrap();
1464
1465 let p_bar = chart.p_bar().expect("p_bar");
1466 assert!(
1467 (p_bar - 0.099).abs() < 1e-10,
1468 "p̄ expected 0.099, got {p_bar}"
1469 );
1470
1471 let sigma = (0.099_f64 * 0.901 / 100.0).sqrt();
1472 let expected_ucl = 0.099 + 3.0 * sigma; let expected_lcl = (0.099 - 3.0 * sigma).max(0.0); for pt in chart.points() {
1476 assert!(
1477 (pt.ucl - expected_ucl).abs() < 1e-6,
1478 "UCL mismatch at index {}: expected {expected_ucl:.6}, got {:.6}",
1479 pt.index,
1480 pt.ucl
1481 );
1482 assert!(
1483 (pt.lcl - expected_lcl).abs() < 1e-6,
1484 "LCL mismatch at index {}: expected {expected_lcl:.6}, got {:.6}",
1485 pt.index,
1486 pt.lcl
1487 );
1488 }
1489 }
1490
1491 #[test]
1500 fn np_chart_montgomery_reference() {
1501 let mut chart = NPChart::new(100).expect("100 is a valid sample size");
1503 for _ in 0..19 {
1504 chart.add_sample(10).unwrap();
1505 }
1506 chart.add_sample(8).unwrap();
1507
1508 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1509 assert!((cl - 9.9).abs() < 1e-10, "NP CL expected 9.9, got {cl}");
1511 let expected_sigma = (9.9_f64 * 0.901).sqrt();
1513 let expected_ucl = 9.9 + 3.0 * expected_sigma; let expected_lcl = (9.9 - 3.0 * expected_sigma).max(0.0); assert!(
1516 (ucl - expected_ucl).abs() < 1e-6,
1517 "NP UCL expected {expected_ucl:.4}, got {ucl:.4}"
1518 );
1519 assert!(
1520 (lcl - expected_lcl).abs() < 1e-6,
1521 "NP LCL expected {expected_lcl:.4}, got {lcl:.4}"
1522 );
1523 }
1524
1525 #[test]
1531 fn c_chart_montgomery_reference() {
1532 let mut chart = CChart::new();
1534 for _ in 0..20 {
1535 chart.add_sample(10);
1536 }
1537
1538 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1539 assert!(
1540 (cl - 10.0).abs() < 1e-10,
1541 "C chart CL expected 10.0, got {cl}"
1542 );
1543 let expected_ucl = 10.0 + 3.0 * 10.0_f64.sqrt(); let expected_lcl = (10.0 - 3.0 * 10.0_f64.sqrt()).max(0.0); assert!(
1546 (ucl - expected_ucl).abs() < 1e-6,
1547 "C chart UCL expected {expected_ucl:.4}, got {ucl:.4}"
1548 );
1549 assert!(
1550 (lcl - expected_lcl).abs() < 1e-6,
1551 "C chart LCL expected {expected_lcl:.4}, got {lcl:.4}"
1552 );
1553 }
1554
1555 #[test]
1561 fn u_chart_montgomery_reference() {
1562 let mut chart = UChart::new();
1564 for _ in 0..20 {
1565 chart.add_sample(20, 10.0).unwrap(); }
1567
1568 let u_bar = chart.u_bar().expect("u_bar");
1569 assert!(
1570 (u_bar - 2.0).abs() < 1e-10,
1571 "U chart ū expected 2.0, got {u_bar}"
1572 );
1573
1574 let sigma = (2.0_f64 / 10.0).sqrt(); let expected_ucl = 2.0 + 3.0 * sigma; let expected_lcl = (2.0 - 3.0 * sigma).max(0.0); for pt in chart.points() {
1579 assert!(
1580 (pt.ucl - expected_ucl).abs() < 1e-6,
1581 "U chart UCL expected {expected_ucl:.4}, got {:.4}",
1582 pt.ucl
1583 );
1584 assert!(
1585 (pt.lcl - expected_lcl).abs() < 1e-6,
1586 "U chart LCL expected {expected_lcl:.4}, got {:.4}",
1587 pt.lcl
1588 );
1589 }
1590 }
1591
1592 #[test]
1595 fn g_chart_ucl_above_cl() {
1596 let gaps = vec![100.0, 120.0, 95.0, 110.0, 105.0];
1597 let chart = g_chart(&gaps).expect("chart should be Some");
1598 assert!(chart.points[0].ucl > chart.points[0].cl);
1599 assert!(chart.points[0].lcl >= 0.0);
1600 }
1601
1602 #[test]
1603 fn g_chart_insufficient() {
1604 assert!(g_chart(&[100.0, 120.0]).is_none());
1605 }
1606
1607 #[test]
1608 fn g_chart_all_same() {
1609 let chart = g_chart(&[50.0; 8]).expect("chart should be Some");
1610 assert!((chart.g_bar - 50.0).abs() < 1e-10);
1611 assert!(chart.points[0].ucl > chart.points[0].cl);
1612 }
1613
1614 #[test]
1617 fn t_chart_ucl_factor() {
1618 let times = vec![100.0; 10];
1620 let chart = t_chart(×).expect("chart should be Some");
1621 let ratio = chart.points[0].ucl / chart.t_bar;
1622 assert!((ratio - 6.6077).abs() < 0.01, "ratio={ratio}");
1623 }
1624
1625 #[test]
1626 fn t_chart_non_positive() {
1627 assert!(t_chart(&[10.0, -5.0, 20.0, 15.0]).is_none());
1628 }
1629
1630 #[test]
1631 fn t_chart_insufficient() {
1632 assert!(t_chart(&[10.0, 20.0]).is_none());
1633 }
1634
1635 #[test]
1642 fn laney_p_ucl_formula_invariant() {
1643 let samples: Vec<(u64, u64)> = vec![
1644 (3, 100),
1645 (7, 100),
1646 (2, 100),
1647 (8, 100),
1648 (4, 100),
1649 (5, 150),
1650 (9, 150),
1651 (3, 150),
1652 (6, 150),
1653 (4, 150),
1654 ];
1655 let chart = laney_p_chart(&samples).expect("chart should be Some");
1656
1657 for (i, (&(d, n), pt)) in samples.iter().zip(&chart.points).enumerate() {
1658 let p_i = d as f64 / n as f64;
1659 let sigma_i = (chart.p_bar * (1.0 - chart.p_bar) / n as f64).sqrt();
1660 let expected_ucl = chart.p_bar + 3.0 * chart.phi * sigma_i;
1661 let expected_lcl = (chart.p_bar - 3.0 * chart.phi * sigma_i).max(0.0);
1662
1663 assert!(
1664 (pt.value - p_i).abs() < 1e-10,
1665 "point {i}: value expected {p_i:.6}, got {:.6}",
1666 pt.value
1667 );
1668 assert!(
1669 (pt.ucl - expected_ucl).abs() < 1e-10,
1670 "point {i}: UCL expected {expected_ucl:.6}, got {:.6}",
1671 pt.ucl
1672 );
1673 assert!(
1674 (pt.lcl - expected_lcl).abs() < 1e-10,
1675 "point {i}: LCL expected {expected_lcl:.6}, got {:.6}",
1676 pt.lcl
1677 );
1678 }
1679 }
1680
1681 #[test]
1699 fn laney_p_phi_near_one_limits_close_to_standard() {
1700 let n: u64 = 10_000;
1704 let d_high: u64 = 5028; let d_low: u64 = 4972; let samples: Vec<(u64, u64)> = vec![
1708 (d_high, n),
1709 (d_low, n),
1710 (d_high, n),
1711 (d_low, n),
1712 (d_high, n),
1713 (d_low, n),
1714 ];
1715
1716 let laney = laney_p_chart(&samples).expect("chart should be Some");
1717
1718 assert!(
1720 (laney.phi - 1.0).abs() < 0.01,
1721 "φ expected ≈1.0, got {}",
1722 laney.phi
1723 );
1724
1725 let p_bar = laney.p_bar;
1727 let sigma_std = (p_bar * (1.0 - p_bar) / n as f64).sqrt();
1728 let std_ucl = p_bar + 3.0 * sigma_std;
1729 let std_lcl = (p_bar - 3.0 * sigma_std).max(0.0);
1730
1731 let max_deviation = 3.0 * sigma_std * (laney.phi - 1.0).abs();
1734 assert!(
1735 (laney.points[0].ucl - std_ucl).abs() <= max_deviation + 1e-12,
1736 "Laney UCL={:.6} vs std UCL={std_ucl:.6}, deviation bound={max_deviation:.2e}",
1737 laney.points[0].ucl
1738 );
1739 assert!(
1740 (laney.points[0].lcl - std_lcl).abs() <= max_deviation + 1e-12,
1741 "Laney LCL={:.6} vs std LCL={std_lcl:.6}, deviation bound={max_deviation:.2e}",
1742 laney.points[0].lcl
1743 );
1744 }
1745
1746 #[test]
1753 fn laney_p_phi_gt_one_wider_than_standard() {
1754 let samples: Vec<(u64, u64)> = vec![
1756 (1, 100),
1757 (20, 100),
1758 (2, 100),
1759 (18, 100),
1760 (1, 100),
1761 (22, 100),
1762 (3, 100),
1763 (19, 100),
1764 (2, 100),
1765 (20, 100),
1766 ];
1767
1768 let laney = laney_p_chart(&samples).expect("chart should be Some");
1769 assert!(
1770 laney.phi > 1.0,
1771 "φ expected > 1 for overdispersed data, got {}",
1772 laney.phi
1773 );
1774
1775 let total_d: u64 = samples.iter().map(|&(d, _)| d).sum();
1777 let total_n: u64 = samples.iter().map(|&(_, n)| n).sum();
1778 let p_bar = total_d as f64 / total_n as f64;
1779 let std_ucl_first = p_bar + 3.0 * (p_bar * (1.0 - p_bar) / 100.0_f64).sqrt();
1780
1781 assert!(
1782 laney.points[0].ucl > std_ucl_first,
1783 "Laney UCL ({:.4}) must exceed standard P chart UCL ({std_ucl_first:.4}) when φ>1",
1784 laney.points[0].ucl
1785 );
1786 }
1787
1788 #[test]
1806 fn laney_p_numerical_reference() {
1807 let samples: Vec<(u64, u64)> = vec![(3, 50), (7, 50), (2, 50), (8, 50), (4, 50)];
1808 let chart = laney_p_chart(&samples).expect("chart should be Some");
1809
1810 let p_bar = 24.0_f64 / 250.0;
1811 assert!(
1812 (chart.p_bar - p_bar).abs() < 1e-10,
1813 "p̄ expected {p_bar:.6}, got {:.6}",
1814 chart.p_bar
1815 );
1816
1817 let sigma_i = (p_bar * (1.0 - p_bar) / 50.0_f64).sqrt();
1819 let p_vals = [0.06_f64, 0.14, 0.04, 0.16, 0.08];
1820 let z: Vec<f64> = p_vals.iter().map(|&p| (p - p_bar) / sigma_i).collect();
1821 let mr_bar = z.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>() / 4.0;
1822 let phi_expected = mr_bar / 1.128;
1823
1824 assert!(
1825 (chart.phi - phi_expected).abs() < 1e-10,
1826 "φ expected {phi_expected:.6}, got {:.6}",
1827 chart.phi
1828 );
1829
1830 let ucl_0 = p_bar + 3.0 * phi_expected * sigma_i;
1832 let lcl_0 = (p_bar - 3.0 * phi_expected * sigma_i).max(0.0);
1833 assert!(
1834 (chart.points[0].ucl - ucl_0).abs() < 1e-10,
1835 "UCL[0] expected {ucl_0:.6}, got {:.6}",
1836 chart.points[0].ucl
1837 );
1838 assert!(
1839 (chart.points[0].lcl - lcl_0).abs() < 1e-10,
1840 "LCL[0] expected {lcl_0:.6}, got {:.6}",
1841 chart.points[0].lcl
1842 );
1843 }
1844
1845 #[test]
1849 fn laney_u_phi_gt_one_wider_than_standard() {
1850 let samples: Vec<(u64, f64)> = vec![
1852 (1, 10.0),
1853 (15, 10.0),
1854 (2, 10.0),
1855 (14, 10.0),
1856 (1, 10.0),
1857 (16, 10.0),
1858 (2, 10.0),
1859 (13, 10.0),
1860 ];
1861
1862 let laney = laney_u_chart(&samples).expect("chart should be Some");
1863 assert!(
1864 laney.phi > 1.0,
1865 "φ expected > 1 for overdispersed data, got {}",
1866 laney.phi
1867 );
1868
1869 let total_d: u64 = samples.iter().map(|&(d, _)| d).sum();
1870 let total_n: f64 = samples.iter().map(|&(_, n)| n).sum();
1871 let u_bar = total_d as f64 / total_n;
1872 let std_ucl = u_bar + 3.0 * (u_bar / 10.0_f64).sqrt();
1873
1874 assert!(
1875 laney.points[0].ucl > std_ucl,
1876 "Laney U' UCL ({:.4}) must exceed standard U chart UCL ({std_ucl:.4}) when φ>1",
1877 laney.points[0].ucl
1878 );
1879 }
1880
1881 #[test]
1894 fn g_chart_formula_verification() {
1895 let gaps = vec![50.0_f64; 8];
1897 let chart = g_chart(&gaps).expect("chart should be Some");
1898
1899 let g_bar = 50.0_f64;
1900 assert!(
1901 (chart.g_bar - g_bar).abs() < 1e-10,
1902 "ḡ expected 50.0, got {}",
1903 chart.g_bar
1904 );
1905
1906 let spread = (g_bar * (g_bar + 1.0)).sqrt(); let expected_ucl = g_bar + 3.0 * spread;
1908 let expected_lcl = (g_bar - 3.0 * spread).max(0.0);
1909
1910 assert!(
1911 (chart.points[0].ucl - expected_ucl).abs() < 1e-10,
1912 "UCL expected {expected_ucl:.6}, got {:.6}",
1913 chart.points[0].ucl
1914 );
1915 assert!(
1916 (chart.points[0].lcl - expected_lcl).abs() < 1e-10,
1917 "LCL expected {expected_lcl:.6}, got {:.6}",
1918 chart.points[0].lcl
1919 );
1920 assert!(
1922 chart.points[0].lcl >= 0.0,
1923 "LCL must be clamped to 0, got {}",
1924 chart.points[0].lcl
1925 );
1926 }
1927
1928 #[test]
1933 fn g_chart_lcl_always_zero() {
1934 for &g in &[1.0_f64, 5.0, 20.0, 100.0, 500.0] {
1935 let gaps = vec![g; 5];
1936 let chart = g_chart(&gaps).expect("chart should be Some");
1937 assert!(
1938 (chart.points[0].lcl - 0.0).abs() < 1e-10,
1939 "LCL must be 0 for ḡ={g}, got {}",
1940 chart.points[0].lcl
1941 );
1942 }
1943 }
1944
1945 #[test]
1956 fn t_chart_lcl_factor_verification() {
1957 let times = vec![100.0_f64; 10];
1958 let chart = t_chart(×).expect("chart should be Some");
1959
1960 let ucl_factor = chart.points[0].ucl / chart.t_bar;
1961 let lcl_factor = chart.points[0].lcl / chart.t_bar;
1962
1963 let expected_ucl_factor = -(0.00135_f64.ln()); let expected_lcl_factor = -(0.99865_f64.ln()); assert!(
1967 (ucl_factor - expected_ucl_factor).abs() < 1e-10,
1968 "UCL factor expected {expected_ucl_factor:.6}, got {ucl_factor:.6}"
1969 );
1970 assert!(
1971 (lcl_factor - expected_lcl_factor).abs() < 1e-10,
1972 "LCL factor expected {expected_lcl_factor:.6}, got {lcl_factor:.6}"
1973 );
1974 }
1975
1976 #[test]
1982 fn t_chart_ucl_lcl_ratio_scale_invariant() {
1983 let k_u = -(0.00135_f64.ln());
1984 let k_l = -(0.99865_f64.ln());
1985 let expected_ratio = k_u / k_l;
1986
1987 for &t_bar in &[10.0_f64, 100.0, 1000.0] {
1988 let times = vec![t_bar; 10];
1989 let chart = t_chart(×).expect("chart should be Some");
1990 let ratio = chart.points[0].ucl / chart.points[0].lcl;
1991 assert!(
1992 (ratio - expected_ratio).abs() < 0.01,
1993 "UCL/LCL ratio expected {expected_ratio:.2}, got {ratio:.2} at t̄={t_bar}"
1994 );
1995 }
1996 }
1997}