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(&mut self, defectives: u64, sample_size: u64) {
99 if sample_size == 0 || defectives > sample_size {
100 return;
101 }
102 self.samples.push((defectives, sample_size));
103 self.recompute();
104 }
105
106 pub fn p_bar(&self) -> Option<f64> {
108 self.p_bar
109 }
110
111 pub fn points(&self) -> &[AttributeChartPoint] {
113 &self.chart_points
114 }
115
116 pub fn is_in_control(&self) -> bool {
118 self.chart_points.iter().all(|p| !p.out_of_control)
119 }
120
121 fn recompute(&mut self) {
123 if self.samples.is_empty() {
124 self.p_bar = None;
125 self.chart_points.clear();
126 return;
127 }
128
129 let total_defectives: u64 = self.samples.iter().map(|&(d, _)| d).sum();
130 let total_inspected: u64 = self.samples.iter().map(|&(_, n)| n).sum();
131 let p_bar = total_defectives as f64 / total_inspected as f64;
132 self.p_bar = Some(p_bar);
133
134 self.chart_points = self
135 .samples
136 .iter()
137 .enumerate()
138 .map(|(i, &(defectives, sample_size))| {
139 let p = defectives as f64 / sample_size as f64;
140 let n = sample_size as f64;
141 let sigma = (p_bar * (1.0 - p_bar) / n).sqrt();
142 let ucl = p_bar + 3.0 * sigma;
143 let lcl = (p_bar - 3.0 * sigma).max(0.0);
144
145 AttributeChartPoint {
146 index: i,
147 value: p,
148 ucl,
149 cl: p_bar,
150 lcl,
151 out_of_control: p > ucl || p < lcl,
152 }
153 })
154 .collect();
155 }
156}
157
158impl Default for PChart {
159 fn default() -> Self {
160 Self::new()
161 }
162}
163
164pub struct NPChart {
184 sample_size: u64,
186 defective_counts: Vec<u64>,
188 chart_points: Vec<AttributeChartPoint>,
190 limits: Option<(f64, f64, f64)>, }
193
194impl NPChart {
195 pub fn new(sample_size: u64) -> Self {
201 assert!(sample_size > 0, "sample_size must be > 0");
202 Self {
203 sample_size,
204 defective_counts: Vec::new(),
205 chart_points: Vec::new(),
206 limits: None,
207 }
208 }
209
210 pub fn add_sample(&mut self, defectives: u64) {
214 if defectives > self.sample_size {
215 return;
216 }
217 self.defective_counts.push(defectives);
218 self.recompute();
219 }
220
221 pub fn control_limits(&self) -> Option<(f64, f64, f64)> {
223 self.limits
224 }
225
226 pub fn points(&self) -> &[AttributeChartPoint] {
228 &self.chart_points
229 }
230
231 pub fn is_in_control(&self) -> bool {
233 self.chart_points.iter().all(|p| !p.out_of_control)
234 }
235
236 fn recompute(&mut self) {
238 if self.defective_counts.is_empty() {
239 self.limits = None;
240 self.chart_points.clear();
241 return;
242 }
243
244 let total_defectives: u64 = self.defective_counts.iter().sum();
245 let total_inspected = self.sample_size * self.defective_counts.len() as u64;
246 let p_bar = total_defectives as f64 / total_inspected as f64;
247 let n = self.sample_size as f64;
248
249 let np_bar = n * p_bar;
250 let sigma = (n * p_bar * (1.0 - p_bar)).sqrt();
251 let ucl = np_bar + 3.0 * sigma;
252 let lcl = (np_bar - 3.0 * sigma).max(0.0);
253
254 self.limits = Some((ucl, np_bar, lcl));
255
256 self.chart_points = self
257 .defective_counts
258 .iter()
259 .enumerate()
260 .map(|(i, &count)| {
261 let value = count as f64;
262 AttributeChartPoint {
263 index: i,
264 value,
265 ucl,
266 cl: np_bar,
267 lcl,
268 out_of_control: value > ucl || value < lcl,
269 }
270 })
271 .collect();
272 }
273}
274
275pub struct CChart {
295 defect_counts: Vec<u64>,
297 chart_points: Vec<AttributeChartPoint>,
299 limits: Option<(f64, f64, f64)>, }
302
303impl CChart {
304 pub fn new() -> Self {
306 Self {
307 defect_counts: Vec::new(),
308 chart_points: Vec::new(),
309 limits: None,
310 }
311 }
312
313 pub fn add_sample(&mut self, defects: u64) {
315 self.defect_counts.push(defects);
316 self.recompute();
317 }
318
319 pub fn control_limits(&self) -> Option<(f64, f64, f64)> {
321 self.limits
322 }
323
324 pub fn points(&self) -> &[AttributeChartPoint] {
326 &self.chart_points
327 }
328
329 pub fn is_in_control(&self) -> bool {
331 self.chart_points.iter().all(|p| !p.out_of_control)
332 }
333
334 fn recompute(&mut self) {
336 if self.defect_counts.is_empty() {
337 self.limits = None;
338 self.chart_points.clear();
339 return;
340 }
341
342 let total: u64 = self.defect_counts.iter().sum();
343 let c_bar = total as f64 / self.defect_counts.len() as f64;
344 let sigma = c_bar.sqrt();
345 let ucl = c_bar + 3.0 * sigma;
346 let lcl = (c_bar - 3.0 * sigma).max(0.0);
347
348 self.limits = Some((ucl, c_bar, lcl));
349
350 self.chart_points = self
351 .defect_counts
352 .iter()
353 .enumerate()
354 .map(|(i, &count)| {
355 let value = count as f64;
356 AttributeChartPoint {
357 index: i,
358 value,
359 ucl,
360 cl: c_bar,
361 lcl,
362 out_of_control: value > ucl || value < lcl,
363 }
364 })
365 .collect();
366 }
367}
368
369impl Default for CChart {
370 fn default() -> Self {
371 Self::new()
372 }
373}
374
375pub struct UChart {
396 samples: Vec<(u64, f64)>,
398 chart_points: Vec<AttributeChartPoint>,
400 u_bar: Option<f64>,
402}
403
404impl UChart {
405 pub fn new() -> Self {
407 Self {
408 samples: Vec::new(),
409 chart_points: Vec::new(),
410 u_bar: None,
411 }
412 }
413
414 pub fn add_sample(&mut self, defects: u64, units_inspected: f64) {
419 if !units_inspected.is_finite() || units_inspected <= 0.0 {
420 return;
421 }
422 self.samples.push((defects, units_inspected));
423 self.recompute();
424 }
425
426 pub fn u_bar(&self) -> Option<f64> {
428 self.u_bar
429 }
430
431 pub fn points(&self) -> &[AttributeChartPoint] {
433 &self.chart_points
434 }
435
436 pub fn is_in_control(&self) -> bool {
438 self.chart_points.iter().all(|p| !p.out_of_control)
439 }
440
441 fn recompute(&mut self) {
443 if self.samples.is_empty() {
444 self.u_bar = None;
445 self.chart_points.clear();
446 return;
447 }
448
449 let total_defects: u64 = self.samples.iter().map(|&(d, _)| d).sum();
450 let total_units: f64 = self.samples.iter().map(|&(_, n)| n).sum();
451 let u_bar = total_defects as f64 / total_units;
452 self.u_bar = Some(u_bar);
453
454 self.chart_points = self
455 .samples
456 .iter()
457 .enumerate()
458 .map(|(i, &(defects, units))| {
459 let u = defects as f64 / units;
460 let sigma = (u_bar / units).sqrt();
461 let ucl = u_bar + 3.0 * sigma;
462 let lcl = (u_bar - 3.0 * sigma).max(0.0);
463
464 AttributeChartPoint {
465 index: i,
466 value: u,
467 ucl,
468 cl: u_bar,
469 lcl,
470 out_of_control: u > ucl || u < lcl,
471 }
472 })
473 .collect();
474 }
475}
476
477impl Default for UChart {
478 fn default() -> Self {
479 Self::new()
480 }
481}
482
483#[derive(Debug, Clone)]
498pub struct LaneyAttributePoint {
499 pub index: usize,
501 pub value: f64,
503 pub ucl: f64,
505 pub cl: f64,
507 pub lcl: f64,
509 pub out_of_control: bool,
511}
512
513#[derive(Debug, Clone)]
518pub struct LaneyPChart {
519 pub p_bar: f64,
521 pub phi: f64,
524 pub points: Vec<LaneyAttributePoint>,
526}
527
528#[derive(Debug, Clone)]
533pub struct LaneyUChart {
534 pub u_bar: f64,
536 pub phi: f64,
539 pub points: Vec<LaneyAttributePoint>,
541}
542
543pub fn laney_p_chart(samples: &[(u64, u64)]) -> Option<LaneyPChart> {
568 if samples.len() < 3 {
569 return None;
570 }
571
572 let total_defectives: u64 = samples.iter().map(|&(d, _)| d).sum();
573 let total_inspected: u64 = samples.iter().map(|&(_, n)| n).sum();
574 if total_inspected == 0 {
575 return None;
576 }
577
578 let p_bar = total_defectives as f64 / total_inspected as f64;
579 let base_var = p_bar * (1.0 - p_bar);
581 if base_var <= 0.0 {
582 let points = samples
584 .iter()
585 .enumerate()
586 .map(|(i, &(d, n))| {
587 let value = if n > 0 { d as f64 / n as f64 } else { p_bar };
588 LaneyAttributePoint {
589 index: i,
590 value,
591 ucl: p_bar,
592 cl: p_bar,
593 lcl: p_bar,
594 out_of_control: false,
595 }
596 })
597 .collect();
598 return Some(LaneyPChart {
599 p_bar,
600 phi: 0.0,
601 points,
602 });
603 }
604
605 let z_scores: Vec<f64> = samples
607 .iter()
608 .map(|&(d, n)| {
609 let p_i = d as f64 / n as f64;
610 let sigma_i = (base_var / n as f64).sqrt();
611 (p_i - p_bar) / sigma_i
612 })
613 .collect();
614
615 let mr_bar = {
617 let mrs: Vec<f64> = z_scores.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
618 mrs.iter().sum::<f64>() / mrs.len() as f64
619 };
620
621 const D2: f64 = 1.128;
623 let phi = mr_bar / D2;
624
625 let points = samples
627 .iter()
628 .enumerate()
629 .map(|(i, &(d, n))| {
630 let p_i = d as f64 / n as f64;
631 let sigma_i = (base_var / n as f64).sqrt();
632 let ucl = p_bar + 3.0 * phi * sigma_i;
633 let lcl = (p_bar - 3.0 * phi * sigma_i).max(0.0);
634 LaneyAttributePoint {
635 index: i,
636 value: p_i,
637 ucl,
638 cl: p_bar,
639 lcl,
640 out_of_control: p_i > ucl || p_i < lcl,
641 }
642 })
643 .collect();
644
645 Some(LaneyPChart { p_bar, phi, points })
646}
647
648pub fn laney_u_chart(samples: &[(u64, f64)]) -> Option<LaneyUChart> {
673 if samples.len() < 3 {
674 return None;
675 }
676
677 if samples.iter().any(|&(_, n)| !n.is_finite() || n <= 0.0) {
679 return None;
680 }
681
682 let total_defects: u64 = samples.iter().map(|&(d, _)| d).sum();
683 let total_units: f64 = samples.iter().map(|&(_, n)| n).sum();
684 if total_units <= 0.0 {
685 return None;
686 }
687
688 let u_bar = total_defects as f64 / total_units;
689
690 if u_bar <= 0.0 {
692 let points = samples
693 .iter()
694 .enumerate()
695 .map(|(i, &(d, n))| {
696 let value = d as f64 / n;
697 LaneyAttributePoint {
698 index: i,
699 value,
700 ucl: 0.0,
701 cl: 0.0,
702 lcl: 0.0,
703 out_of_control: false,
704 }
705 })
706 .collect();
707 return Some(LaneyUChart {
708 u_bar: 0.0,
709 phi: 0.0,
710 points,
711 });
712 }
713
714 let z_scores: Vec<f64> = samples
716 .iter()
717 .map(|&(d, n)| {
718 let u_i = d as f64 / n;
719 let sigma_i = (u_bar / n).sqrt();
720 (u_i - u_bar) / sigma_i
721 })
722 .collect();
723
724 let mr_bar = {
726 let mrs: Vec<f64> = z_scores.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
727 mrs.iter().sum::<f64>() / mrs.len() as f64
728 };
729
730 const D2: f64 = 1.128;
732 let phi = mr_bar / D2;
733
734 let points = samples
736 .iter()
737 .enumerate()
738 .map(|(i, &(d, n))| {
739 let u_i = d as f64 / n;
740 let sigma_i = (u_bar / n).sqrt();
741 let ucl = u_bar + 3.0 * phi * sigma_i;
742 let lcl = (u_bar - 3.0 * phi * sigma_i).max(0.0);
743 LaneyAttributePoint {
744 index: i,
745 value: u_i,
746 ucl,
747 cl: u_bar,
748 lcl,
749 out_of_control: u_i > ucl || u_i < lcl,
750 }
751 })
752 .collect();
753
754 Some(LaneyUChart { u_bar, phi, points })
755}
756
757#[derive(Debug, Clone)]
763pub struct GChartPoint {
764 pub index: usize,
766 pub value: f64,
768 pub ucl: f64,
770 pub cl: f64,
772 pub lcl: f64,
774 pub out_of_control: bool,
776}
777
778#[derive(Debug, Clone)]
795pub struct GChart {
796 pub g_bar: f64,
798 pub points: Vec<GChartPoint>,
800}
801
802#[derive(Debug, Clone)]
814pub struct TChart {
815 pub t_bar: f64,
817 pub points: Vec<TChartPoint>,
819}
820
821#[derive(Debug, Clone)]
823pub struct TChartPoint {
824 pub index: usize,
826 pub value: f64,
828 pub ucl: f64,
830 pub cl: f64,
832 pub lcl: f64,
834 pub out_of_control: bool,
836}
837
838pub fn g_chart(inter_event_counts: &[f64]) -> Option<GChart> {
864 if inter_event_counts.len() < 3 {
865 return None;
866 }
867 if inter_event_counts
868 .iter()
869 .any(|&v| !v.is_finite() || v < 0.0)
870 {
871 return None;
872 }
873
874 let g_bar = inter_event_counts.iter().sum::<f64>() / inter_event_counts.len() as f64;
875 let spread = (g_bar * (g_bar + 1.0)).sqrt();
876 let ucl = g_bar + 3.0 * spread;
877 let lcl = (g_bar - 3.0 * spread).max(0.0);
878
879 let points = inter_event_counts
880 .iter()
881 .enumerate()
882 .map(|(i, &v)| GChartPoint {
883 index: i,
884 value: v,
885 ucl,
886 cl: g_bar,
887 lcl,
888 out_of_control: v > ucl || v < lcl,
889 })
890 .collect();
891
892 Some(GChart { g_bar, points })
893}
894
895pub fn t_chart(inter_event_times: &[f64]) -> Option<TChart> {
917 if inter_event_times.len() < 3 {
918 return None;
919 }
920 if inter_event_times
921 .iter()
922 .any(|&v| !v.is_finite() || v <= 0.0)
923 {
924 return None;
925 }
926
927 let t_bar = inter_event_times.iter().sum::<f64>() / inter_event_times.len() as f64;
928
929 let ucl_factor = -(0.00135_f64.ln()); let lcl_factor = -(0.99865_f64.ln()); let ucl = t_bar * ucl_factor;
936 let lcl = (t_bar * lcl_factor).max(0.0);
937
938 let points = inter_event_times
939 .iter()
940 .enumerate()
941 .map(|(i, &v)| TChartPoint {
942 index: i,
943 value: v,
944 ucl,
945 cl: t_bar,
946 lcl,
947 out_of_control: v > ucl || v < lcl,
948 })
949 .collect();
950
951 Some(TChart { t_bar, points })
952}
953
954#[cfg(test)]
959mod tests {
960 use super::*;
961
962 #[test]
965 fn test_p_chart_basic() {
966 let mut chart = PChart::new();
968 let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
969 for &d in &defectives {
970 chart.add_sample(d, 100);
971 }
972
973 let p_bar = chart.p_bar().expect("should have p_bar");
974 assert!(
976 (p_bar - 0.055).abs() < 1e-10,
977 "p_bar={p_bar}, expected 0.055"
978 );
979
980 assert_eq!(chart.points().len(), 10);
982
983 for pt in chart.points() {
985 assert!((pt.cl - 0.055).abs() < 1e-10);
986 }
987 }
988
989 #[test]
990 fn test_p_chart_limits() {
991 let mut chart = PChart::new();
992 chart.add_sample(10, 100);
997
998 let pt = &chart.points()[0];
999 assert!((pt.cl - 0.1).abs() < 1e-10);
1000 assert!((pt.ucl - 0.19).abs() < 0.001);
1001 assert!((pt.lcl - 0.01).abs() < 0.001);
1002 }
1003
1004 #[test]
1005 fn test_p_chart_variable_sample_sizes() {
1006 let mut chart = PChart::new();
1007 chart.add_sample(5, 100);
1008 chart.add_sample(10, 200);
1009 chart.add_sample(3, 50);
1010
1011 let p_bar = chart.p_bar().expect("p_bar");
1013 assert!((p_bar - 18.0 / 350.0).abs() < 1e-10);
1014
1015 let pts = chart.points();
1017 assert!(pts[1].ucl - pts[1].cl < pts[0].ucl - pts[0].cl);
1019 }
1020
1021 #[test]
1022 fn test_p_chart_rejects_invalid() {
1023 let mut chart = PChart::new();
1024 chart.add_sample(5, 0); assert!(chart.p_bar().is_none());
1026
1027 chart.add_sample(10, 5); assert!(chart.p_bar().is_none());
1029 }
1030
1031 #[test]
1032 fn test_p_chart_lcl_clamped_to_zero() {
1033 let mut chart = PChart::new();
1034 chart.add_sample(1, 10);
1036 let pt = &chart.points()[0];
1037 assert!(pt.lcl >= 0.0);
1038 }
1039
1040 #[test]
1041 fn test_p_chart_out_of_control() {
1042 let mut chart = PChart::new();
1043 for _ in 0..20 {
1045 chart.add_sample(5, 100);
1046 }
1047 chart.add_sample(30, 100);
1049
1050 assert!(!chart.is_in_control());
1051 let last = chart.points().last().expect("should have points");
1052 assert!(last.out_of_control);
1053 }
1054
1055 #[test]
1056 fn test_p_chart_default() {
1057 let chart = PChart::default();
1058 assert!(chart.p_bar().is_none());
1059 assert!(chart.points().is_empty());
1060 }
1061
1062 #[test]
1065 fn test_np_chart_basic() {
1066 let mut chart = NPChart::new(100);
1067 let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
1068 for &d in &defectives {
1069 chart.add_sample(d);
1070 }
1071
1072 let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
1073 assert!((cl - 5.5).abs() < 1e-10);
1075 assert!(ucl > cl);
1076 assert!(lcl < cl);
1077 assert!(lcl >= 0.0);
1078 }
1079
1080 #[test]
1081 fn test_np_chart_rejects_invalid() {
1082 let mut chart = NPChart::new(100);
1083 chart.add_sample(101); assert!(chart.control_limits().is_none());
1085 }
1086
1087 #[test]
1088 #[should_panic(expected = "sample_size must be > 0")]
1089 fn test_np_chart_zero_sample_size() {
1090 let _ = NPChart::new(0);
1091 }
1092
1093 #[test]
1094 fn test_np_chart_out_of_control() {
1095 let mut chart = NPChart::new(100);
1096 for _ in 0..20 {
1097 chart.add_sample(5);
1098 }
1099 chart.add_sample(30);
1100
1101 assert!(!chart.is_in_control());
1102 }
1103
1104 #[test]
1105 fn test_np_chart_limits_formula() {
1106 let mut chart = NPChart::new(200);
1111 for _ in 0..10 {
1112 chart.add_sample(10);
1113 }
1114
1115 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1116 assert!((cl - 10.0).abs() < 1e-10);
1117 let expected_sigma = (200.0_f64 * 0.05 * 0.95).sqrt();
1118 assert!((ucl - (10.0 + 3.0 * expected_sigma)).abs() < 0.01);
1119 assert!((lcl - (10.0 - 3.0 * expected_sigma)).abs() < 0.01);
1120 }
1121
1122 #[test]
1125 fn test_c_chart_basic() {
1126 let mut chart = CChart::new();
1127 let counts = [3, 5, 4, 6, 2, 7, 3, 4, 5, 6];
1128 for &c in &counts {
1129 chart.add_sample(c);
1130 }
1131
1132 let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
1133 assert!((cl - 4.5).abs() < 1e-10);
1135 let expected_ucl = 4.5 + 3.0 * 4.5_f64.sqrt();
1137 assert!((ucl - expected_ucl).abs() < 0.01);
1138 assert!(lcl >= 0.0);
1139 }
1140
1141 #[test]
1142 fn test_c_chart_out_of_control() {
1143 let mut chart = CChart::new();
1144 for _ in 0..20 {
1145 chart.add_sample(5);
1146 }
1147 chart.add_sample(50); assert!(!chart.is_in_control());
1150 }
1151
1152 #[test]
1153 fn test_c_chart_single_sample() {
1154 let mut chart = CChart::new();
1155 chart.add_sample(10);
1156
1157 let (_, cl, _) = chart.control_limits().expect("limits");
1158 assert!((cl - 10.0).abs() < f64::EPSILON);
1159 }
1160
1161 #[test]
1162 fn test_c_chart_lcl_clamped() {
1163 let mut chart = CChart::new();
1165 chart.add_sample(1);
1166
1167 let (_, _, lcl) = chart.control_limits().expect("limits");
1168 assert!((lcl - 0.0).abs() < f64::EPSILON);
1169 }
1170
1171 #[test]
1172 fn test_c_chart_default() {
1173 let chart = CChart::default();
1174 assert!(chart.control_limits().is_none());
1175 assert!(chart.points().is_empty());
1176 }
1177
1178 #[test]
1181 fn test_u_chart_basic() {
1182 let mut chart = UChart::new();
1183 chart.add_sample(3, 10.0);
1185 chart.add_sample(5, 10.0);
1186 chart.add_sample(4, 10.0);
1187 chart.add_sample(6, 10.0);
1188 chart.add_sample(2, 10.0);
1189
1190 let u_bar = chart.u_bar().expect("should have u_bar");
1191 assert!((u_bar - 0.4).abs() < 1e-10);
1193
1194 assert_eq!(chart.points().len(), 5);
1195 }
1196
1197 #[test]
1198 fn test_u_chart_variable_units() {
1199 let mut chart = UChart::new();
1200 chart.add_sample(10, 5.0); chart.add_sample(20, 10.0); chart.add_sample(5, 2.5); let u_bar = chart.u_bar().expect("u_bar");
1205 assert!((u_bar - 2.0).abs() < 1e-10);
1207
1208 let pts = chart.points();
1210 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");
1213 }
1214
1215 #[test]
1216 fn test_u_chart_rejects_invalid() {
1217 let mut chart = UChart::new();
1218 chart.add_sample(5, 0.0); assert!(chart.u_bar().is_none());
1220
1221 chart.add_sample(5, -1.0); assert!(chart.u_bar().is_none());
1223
1224 chart.add_sample(5, f64::NAN); assert!(chart.u_bar().is_none());
1226
1227 chart.add_sample(5, f64::INFINITY); assert!(chart.u_bar().is_none());
1229 }
1230
1231 #[test]
1232 fn test_u_chart_out_of_control() {
1233 let mut chart = UChart::new();
1234 for _ in 0..20 {
1235 chart.add_sample(4, 10.0);
1236 }
1237 chart.add_sample(50, 10.0); assert!(!chart.is_in_control());
1240 }
1241
1242 #[test]
1243 fn test_u_chart_lcl_clamped() {
1244 let mut chart = UChart::new();
1245 chart.add_sample(1, 1.0);
1247 let pt = &chart.points()[0];
1248 assert!(pt.lcl >= 0.0);
1249 }
1250
1251 #[test]
1252 fn test_u_chart_default() {
1253 let chart = UChart::default();
1254 assert!(chart.u_bar().is_none());
1255 assert!(chart.points().is_empty());
1256 }
1257
1258 #[test]
1259 fn test_u_chart_limits_formula() {
1260 let mut chart = UChart::new();
1265 chart.add_sample(8, 4.0);
1266
1267 let pt = &chart.points()[0];
1268 assert!((pt.cl - 2.0).abs() < 1e-10);
1269 let expected_sigma = (2.0_f64 / 4.0).sqrt();
1270 assert!((pt.ucl - (2.0 + 3.0 * expected_sigma)).abs() < 0.001);
1271 }
1272
1273 #[test]
1276 fn test_p_and_np_consistent() {
1277 let mut p_chart = PChart::new();
1279 let mut np_chart = NPChart::new(100);
1280
1281 let defectives = [5, 8, 3, 6, 4];
1282 for &d in &defectives {
1283 p_chart.add_sample(d, 100);
1284 np_chart.add_sample(d);
1285 }
1286
1287 let p_bar = p_chart.p_bar().expect("p_bar");
1288 let (_, np_cl, _) = np_chart.control_limits().expect("np limits");
1289
1290 assert!(
1292 (np_cl - 100.0 * p_bar).abs() < 1e-10,
1293 "NP CL should equal n * p_bar"
1294 );
1295 }
1296
1297 #[test]
1298 fn test_c_and_u_consistent_equal_units() {
1299 let mut c_chart = CChart::new();
1301 let mut u_chart = UChart::new();
1302
1303 let defects = [3, 5, 4, 6, 2];
1304 for &d in &defects {
1305 c_chart.add_sample(d);
1306 u_chart.add_sample(d, 1.0);
1307 }
1308
1309 let (c_ucl, c_cl, c_lcl) = c_chart.control_limits().expect("C limits");
1310 let u_bar = u_chart.u_bar().expect("u_bar");
1311
1312 assert!(
1313 (c_cl - u_bar).abs() < 1e-10,
1314 "C chart CL should equal U chart u-bar when n=1"
1315 );
1316
1317 let u_pt = &u_chart.points()[0];
1318 assert!((u_pt.ucl - c_ucl).abs() < 1e-10);
1319 assert!((u_pt.lcl - c_lcl).abs() < 1e-10);
1320 }
1321
1322 #[test]
1325 fn laney_p_basic() {
1326 let samples: Vec<(u64, u64)> = (0..10).map(|i| (i % 5 + 2, 200)).collect();
1327 let chart = laney_p_chart(&samples).expect("chart should be Some");
1328 assert!(chart.phi > 0.0);
1329 assert!(chart.p_bar > 0.0 && chart.p_bar < 1.0);
1330 assert_eq!(chart.points.len(), 10);
1331 }
1332
1333 #[test]
1334 fn laney_p_constant_proportion_phi_near_zero() {
1335 let samples: Vec<(u64, u64)> = vec![(10, 1000); 20];
1337 let chart = laney_p_chart(&samples).expect("chart should be Some");
1338 assert!((chart.p_bar - 0.01).abs() < 1e-10);
1339 assert!(chart.phi >= 0.0);
1340 }
1341
1342 #[test]
1343 fn laney_p_ucl_above_lcl() {
1344 let samples: Vec<(u64, u64)> = vec![(5, 100), (8, 100), (3, 100), (6, 100), (4, 100)];
1345 let chart = laney_p_chart(&samples).expect("chart should be Some");
1346 for p in &chart.points {
1347 assert!(p.ucl >= p.lcl);
1348 assert!((p.cl - chart.p_bar).abs() < 1e-10);
1349 }
1350 }
1351
1352 #[test]
1353 fn laney_p_insufficient_data() {
1354 let samples: Vec<(u64, u64)> = vec![(2, 100), (3, 100)];
1355 assert!(laney_p_chart(&samples).is_none());
1356 }
1357
1358 #[test]
1359 fn laney_u_basic() {
1360 let samples: Vec<(u64, f64)> = vec![(5, 10.0); 10];
1361 let chart = laney_u_chart(&samples).expect("chart should be Some");
1362 assert!((chart.u_bar - 0.5).abs() < 1e-10);
1363 assert!(chart.phi >= 0.0);
1364 }
1365
1366 #[test]
1367 fn laney_u_ucl_above_cl() {
1368 let samples: Vec<(u64, f64)> = (0..8).map(|i| ((i % 4 + 2) as u64, 10.0)).collect();
1369 let chart = laney_u_chart(&samples).expect("chart should be Some");
1370 for p in &chart.points {
1371 assert!(p.ucl > p.cl || (p.ucl - p.cl).abs() < 1e-10);
1372 }
1373 }
1374
1375 #[test]
1386 fn p_chart_montgomery_reference_formula() {
1387 let mut chart = PChart::new();
1390 for _ in 0..19 {
1391 chart.add_sample(10, 100);
1392 }
1393 chart.add_sample(8, 100);
1394
1395 let p_bar = chart.p_bar().expect("p_bar");
1396 assert!(
1397 (p_bar - 0.099).abs() < 1e-10,
1398 "p̄ expected 0.099, got {p_bar}"
1399 );
1400
1401 let sigma = (0.099_f64 * 0.901 / 100.0).sqrt();
1402 let expected_ucl = 0.099 + 3.0 * sigma; let expected_lcl = (0.099 - 3.0 * sigma).max(0.0); for pt in chart.points() {
1406 assert!(
1407 (pt.ucl - expected_ucl).abs() < 1e-6,
1408 "UCL mismatch at index {}: expected {expected_ucl:.6}, got {:.6}",
1409 pt.index,
1410 pt.ucl
1411 );
1412 assert!(
1413 (pt.lcl - expected_lcl).abs() < 1e-6,
1414 "LCL mismatch at index {}: expected {expected_lcl:.6}, got {:.6}",
1415 pt.index,
1416 pt.lcl
1417 );
1418 }
1419 }
1420
1421 #[test]
1430 fn np_chart_montgomery_reference() {
1431 let mut chart = NPChart::new(100);
1433 for _ in 0..19 {
1434 chart.add_sample(10);
1435 }
1436 chart.add_sample(8);
1437
1438 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1439 assert!((cl - 9.9).abs() < 1e-10, "NP CL expected 9.9, got {cl}");
1441 let expected_sigma = (9.9_f64 * 0.901).sqrt();
1443 let expected_ucl = 9.9 + 3.0 * expected_sigma; let expected_lcl = (9.9 - 3.0 * expected_sigma).max(0.0); assert!(
1446 (ucl - expected_ucl).abs() < 1e-6,
1447 "NP UCL expected {expected_ucl:.4}, got {ucl:.4}"
1448 );
1449 assert!(
1450 (lcl - expected_lcl).abs() < 1e-6,
1451 "NP LCL expected {expected_lcl:.4}, got {lcl:.4}"
1452 );
1453 }
1454
1455 #[test]
1461 fn c_chart_montgomery_reference() {
1462 let mut chart = CChart::new();
1464 for _ in 0..20 {
1465 chart.add_sample(10);
1466 }
1467
1468 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1469 assert!(
1470 (cl - 10.0).abs() < 1e-10,
1471 "C chart CL expected 10.0, got {cl}"
1472 );
1473 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!(
1476 (ucl - expected_ucl).abs() < 1e-6,
1477 "C chart UCL expected {expected_ucl:.4}, got {ucl:.4}"
1478 );
1479 assert!(
1480 (lcl - expected_lcl).abs() < 1e-6,
1481 "C chart LCL expected {expected_lcl:.4}, got {lcl:.4}"
1482 );
1483 }
1484
1485 #[test]
1491 fn u_chart_montgomery_reference() {
1492 let mut chart = UChart::new();
1494 for _ in 0..20 {
1495 chart.add_sample(20, 10.0); }
1497
1498 let u_bar = chart.u_bar().expect("u_bar");
1499 assert!(
1500 (u_bar - 2.0).abs() < 1e-10,
1501 "U chart ū expected 2.0, got {u_bar}"
1502 );
1503
1504 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() {
1509 assert!(
1510 (pt.ucl - expected_ucl).abs() < 1e-6,
1511 "U chart UCL expected {expected_ucl:.4}, got {:.4}",
1512 pt.ucl
1513 );
1514 assert!(
1515 (pt.lcl - expected_lcl).abs() < 1e-6,
1516 "U chart LCL expected {expected_lcl:.4}, got {:.4}",
1517 pt.lcl
1518 );
1519 }
1520 }
1521
1522 #[test]
1525 fn g_chart_ucl_above_cl() {
1526 let gaps = vec![100.0, 120.0, 95.0, 110.0, 105.0];
1527 let chart = g_chart(&gaps).expect("chart should be Some");
1528 assert!(chart.points[0].ucl > chart.points[0].cl);
1529 assert!(chart.points[0].lcl >= 0.0);
1530 }
1531
1532 #[test]
1533 fn g_chart_insufficient() {
1534 assert!(g_chart(&[100.0, 120.0]).is_none());
1535 }
1536
1537 #[test]
1538 fn g_chart_all_same() {
1539 let chart = g_chart(&[50.0; 8]).expect("chart should be Some");
1540 assert!((chart.g_bar - 50.0).abs() < 1e-10);
1541 assert!(chart.points[0].ucl > chart.points[0].cl);
1542 }
1543
1544 #[test]
1547 fn t_chart_ucl_factor() {
1548 let times = vec![100.0; 10];
1550 let chart = t_chart(×).expect("chart should be Some");
1551 let ratio = chart.points[0].ucl / chart.t_bar;
1552 assert!((ratio - 6.6077).abs() < 0.01, "ratio={ratio}");
1553 }
1554
1555 #[test]
1556 fn t_chart_non_positive() {
1557 assert!(t_chart(&[10.0, -5.0, 20.0, 15.0]).is_none());
1558 }
1559
1560 #[test]
1561 fn t_chart_insufficient() {
1562 assert!(t_chart(&[10.0, 20.0]).is_none());
1563 }
1564
1565 #[test]
1572 fn laney_p_ucl_formula_invariant() {
1573 let samples: Vec<(u64, u64)> = vec![
1574 (3, 100),
1575 (7, 100),
1576 (2, 100),
1577 (8, 100),
1578 (4, 100),
1579 (5, 150),
1580 (9, 150),
1581 (3, 150),
1582 (6, 150),
1583 (4, 150),
1584 ];
1585 let chart = laney_p_chart(&samples).expect("chart should be Some");
1586
1587 for (i, (&(d, n), pt)) in samples.iter().zip(&chart.points).enumerate() {
1588 let p_i = d as f64 / n as f64;
1589 let sigma_i = (chart.p_bar * (1.0 - chart.p_bar) / n as f64).sqrt();
1590 let expected_ucl = chart.p_bar + 3.0 * chart.phi * sigma_i;
1591 let expected_lcl = (chart.p_bar - 3.0 * chart.phi * sigma_i).max(0.0);
1592
1593 assert!(
1594 (pt.value - p_i).abs() < 1e-10,
1595 "point {i}: value expected {p_i:.6}, got {:.6}",
1596 pt.value
1597 );
1598 assert!(
1599 (pt.ucl - expected_ucl).abs() < 1e-10,
1600 "point {i}: UCL expected {expected_ucl:.6}, got {:.6}",
1601 pt.ucl
1602 );
1603 assert!(
1604 (pt.lcl - expected_lcl).abs() < 1e-10,
1605 "point {i}: LCL expected {expected_lcl:.6}, got {:.6}",
1606 pt.lcl
1607 );
1608 }
1609 }
1610
1611 #[test]
1629 fn laney_p_phi_near_one_limits_close_to_standard() {
1630 let n: u64 = 10_000;
1634 let d_high: u64 = 5028; let d_low: u64 = 4972; let samples: Vec<(u64, u64)> = vec![
1638 (d_high, n),
1639 (d_low, n),
1640 (d_high, n),
1641 (d_low, n),
1642 (d_high, n),
1643 (d_low, n),
1644 ];
1645
1646 let laney = laney_p_chart(&samples).expect("chart should be Some");
1647
1648 assert!(
1650 (laney.phi - 1.0).abs() < 0.01,
1651 "φ expected ≈1.0, got {}",
1652 laney.phi
1653 );
1654
1655 let p_bar = laney.p_bar;
1657 let sigma_std = (p_bar * (1.0 - p_bar) / n as f64).sqrt();
1658 let std_ucl = p_bar + 3.0 * sigma_std;
1659 let std_lcl = (p_bar - 3.0 * sigma_std).max(0.0);
1660
1661 let max_deviation = 3.0 * sigma_std * (laney.phi - 1.0).abs();
1664 assert!(
1665 (laney.points[0].ucl - std_ucl).abs() <= max_deviation + 1e-12,
1666 "Laney UCL={:.6} vs std UCL={std_ucl:.6}, deviation bound={max_deviation:.2e}",
1667 laney.points[0].ucl
1668 );
1669 assert!(
1670 (laney.points[0].lcl - std_lcl).abs() <= max_deviation + 1e-12,
1671 "Laney LCL={:.6} vs std LCL={std_lcl:.6}, deviation bound={max_deviation:.2e}",
1672 laney.points[0].lcl
1673 );
1674 }
1675
1676 #[test]
1683 fn laney_p_phi_gt_one_wider_than_standard() {
1684 let samples: Vec<(u64, u64)> = vec![
1686 (1, 100),
1687 (20, 100),
1688 (2, 100),
1689 (18, 100),
1690 (1, 100),
1691 (22, 100),
1692 (3, 100),
1693 (19, 100),
1694 (2, 100),
1695 (20, 100),
1696 ];
1697
1698 let laney = laney_p_chart(&samples).expect("chart should be Some");
1699 assert!(
1700 laney.phi > 1.0,
1701 "φ expected > 1 for overdispersed data, got {}",
1702 laney.phi
1703 );
1704
1705 let total_d: u64 = samples.iter().map(|&(d, _)| d).sum();
1707 let total_n: u64 = samples.iter().map(|&(_, n)| n).sum();
1708 let p_bar = total_d as f64 / total_n as f64;
1709 let std_ucl_first = p_bar + 3.0 * (p_bar * (1.0 - p_bar) / 100.0_f64).sqrt();
1710
1711 assert!(
1712 laney.points[0].ucl > std_ucl_first,
1713 "Laney UCL ({:.4}) must exceed standard P chart UCL ({std_ucl_first:.4}) when φ>1",
1714 laney.points[0].ucl
1715 );
1716 }
1717
1718 #[test]
1736 fn laney_p_numerical_reference() {
1737 let samples: Vec<(u64, u64)> = vec![(3, 50), (7, 50), (2, 50), (8, 50), (4, 50)];
1738 let chart = laney_p_chart(&samples).expect("chart should be Some");
1739
1740 let p_bar = 24.0_f64 / 250.0;
1741 assert!(
1742 (chart.p_bar - p_bar).abs() < 1e-10,
1743 "p̄ expected {p_bar:.6}, got {:.6}",
1744 chart.p_bar
1745 );
1746
1747 let sigma_i = (p_bar * (1.0 - p_bar) / 50.0_f64).sqrt();
1749 let p_vals = [0.06_f64, 0.14, 0.04, 0.16, 0.08];
1750 let z: Vec<f64> = p_vals.iter().map(|&p| (p - p_bar) / sigma_i).collect();
1751 let mr_bar = z.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>() / 4.0;
1752 let phi_expected = mr_bar / 1.128;
1753
1754 assert!(
1755 (chart.phi - phi_expected).abs() < 1e-10,
1756 "φ expected {phi_expected:.6}, got {:.6}",
1757 chart.phi
1758 );
1759
1760 let ucl_0 = p_bar + 3.0 * phi_expected * sigma_i;
1762 let lcl_0 = (p_bar - 3.0 * phi_expected * sigma_i).max(0.0);
1763 assert!(
1764 (chart.points[0].ucl - ucl_0).abs() < 1e-10,
1765 "UCL[0] expected {ucl_0:.6}, got {:.6}",
1766 chart.points[0].ucl
1767 );
1768 assert!(
1769 (chart.points[0].lcl - lcl_0).abs() < 1e-10,
1770 "LCL[0] expected {lcl_0:.6}, got {:.6}",
1771 chart.points[0].lcl
1772 );
1773 }
1774
1775 #[test]
1779 fn laney_u_phi_gt_one_wider_than_standard() {
1780 let samples: Vec<(u64, f64)> = vec![
1782 (1, 10.0),
1783 (15, 10.0),
1784 (2, 10.0),
1785 (14, 10.0),
1786 (1, 10.0),
1787 (16, 10.0),
1788 (2, 10.0),
1789 (13, 10.0),
1790 ];
1791
1792 let laney = laney_u_chart(&samples).expect("chart should be Some");
1793 assert!(
1794 laney.phi > 1.0,
1795 "φ expected > 1 for overdispersed data, got {}",
1796 laney.phi
1797 );
1798
1799 let total_d: u64 = samples.iter().map(|&(d, _)| d).sum();
1800 let total_n: f64 = samples.iter().map(|&(_, n)| n).sum();
1801 let u_bar = total_d as f64 / total_n;
1802 let std_ucl = u_bar + 3.0 * (u_bar / 10.0_f64).sqrt();
1803
1804 assert!(
1805 laney.points[0].ucl > std_ucl,
1806 "Laney U' UCL ({:.4}) must exceed standard U chart UCL ({std_ucl:.4}) when φ>1",
1807 laney.points[0].ucl
1808 );
1809 }
1810
1811 #[test]
1824 fn g_chart_formula_verification() {
1825 let gaps = vec![50.0_f64; 8];
1827 let chart = g_chart(&gaps).expect("chart should be Some");
1828
1829 let g_bar = 50.0_f64;
1830 assert!(
1831 (chart.g_bar - g_bar).abs() < 1e-10,
1832 "ḡ expected 50.0, got {}",
1833 chart.g_bar
1834 );
1835
1836 let spread = (g_bar * (g_bar + 1.0)).sqrt(); let expected_ucl = g_bar + 3.0 * spread;
1838 let expected_lcl = (g_bar - 3.0 * spread).max(0.0);
1839
1840 assert!(
1841 (chart.points[0].ucl - expected_ucl).abs() < 1e-10,
1842 "UCL expected {expected_ucl:.6}, got {:.6}",
1843 chart.points[0].ucl
1844 );
1845 assert!(
1846 (chart.points[0].lcl - expected_lcl).abs() < 1e-10,
1847 "LCL expected {expected_lcl:.6}, got {:.6}",
1848 chart.points[0].lcl
1849 );
1850 assert!(
1852 chart.points[0].lcl >= 0.0,
1853 "LCL must be clamped to 0, got {}",
1854 chart.points[0].lcl
1855 );
1856 }
1857
1858 #[test]
1863 fn g_chart_lcl_always_zero() {
1864 for &g in &[1.0_f64, 5.0, 20.0, 100.0, 500.0] {
1865 let gaps = vec![g; 5];
1866 let chart = g_chart(&gaps).expect("chart should be Some");
1867 assert!(
1868 (chart.points[0].lcl - 0.0).abs() < 1e-10,
1869 "LCL must be 0 for ḡ={g}, got {}",
1870 chart.points[0].lcl
1871 );
1872 }
1873 }
1874
1875 #[test]
1886 fn t_chart_lcl_factor_verification() {
1887 let times = vec![100.0_f64; 10];
1888 let chart = t_chart(×).expect("chart should be Some");
1889
1890 let ucl_factor = chart.points[0].ucl / chart.t_bar;
1891 let lcl_factor = chart.points[0].lcl / chart.t_bar;
1892
1893 let expected_ucl_factor = -(0.00135_f64.ln()); let expected_lcl_factor = -(0.99865_f64.ln()); assert!(
1897 (ucl_factor - expected_ucl_factor).abs() < 1e-10,
1898 "UCL factor expected {expected_ucl_factor:.6}, got {ucl_factor:.6}"
1899 );
1900 assert!(
1901 (lcl_factor - expected_lcl_factor).abs() < 1e-10,
1902 "LCL factor expected {expected_lcl_factor:.6}, got {lcl_factor:.6}"
1903 );
1904 }
1905
1906 #[test]
1912 fn t_chart_ucl_lcl_ratio_scale_invariant() {
1913 let k_u = -(0.00135_f64.ln());
1914 let k_l = -(0.99865_f64.ln());
1915 let expected_ratio = k_u / k_l;
1916
1917 for &t_bar in &[10.0_f64, 100.0, 1000.0] {
1918 let times = vec![t_bar; 10];
1919 let chart = t_chart(×).expect("chart should be Some");
1920 let ratio = chart.points[0].ucl / chart.points[0].lcl;
1921 assert!(
1922 (ratio - expected_ratio).abs() < 0.01,
1923 "UCL/LCL ratio expected {expected_ratio:.2}, got {ratio:.2} at t̄={t_bar}"
1924 );
1925 }
1926 }
1927}