1use super::chart::{ChartInputError, ControlChartError};
23
24const LANEY_MIN_SAMPLES: usize = 3;
27
28fn check_proportion(defectives: u64, sample_size: u64) -> Result<(), ControlChartError> {
30 if sample_size == 0 {
31 return Err(ControlChartError::ZeroSampleSize);
32 }
33 if defectives > sample_size {
34 return Err(ControlChartError::DefectivesExceedSampleSize {
35 defectives,
36 sample_size,
37 });
38 }
39 Ok(())
40}
41
42fn check_units(units: f64) -> Result<(), ControlChartError> {
44 if units.is_finite() && units > 0.0 {
45 Ok(())
46 } else {
47 Err(ControlChartError::NonPositiveUnits)
48 }
49}
50
51#[derive(Debug, Clone)]
56pub struct AttributeChartPoint {
57 pub index: usize,
59 pub value: f64,
61 pub ucl: f64,
63 pub cl: f64,
65 pub lcl: f64,
67 pub out_of_control: bool,
69 pub z: Option<f64>,
79}
80
81fn attribute_point(index: usize, value: f64, cl: f64, sigma: f64) -> AttributeChartPoint {
83 let ucl = cl + 3.0 * sigma;
84 let lcl = (cl - 3.0 * sigma).max(0.0);
85 AttributeChartPoint {
86 index,
87 value,
88 ucl,
89 cl,
90 lcl,
91 out_of_control: value > ucl || value < lcl,
92 z: standardized(value, cl, sigma),
93 }
94}
95
96fn standardized(value: f64, cl: f64, sigma: f64) -> Option<f64> {
97 (sigma > 0.0 && sigma.is_finite()).then(|| (value - cl) / sigma)
98}
99
100fn check_proportion_standard(p: f64, parameter: &'static str) -> Result<f64, ControlChartError> {
102 if p.is_finite() && p > 0.0 && p < 1.0 {
103 Ok(p)
104 } else {
105 Err(ControlChartError::proportion_standard(parameter, p))
106 }
107}
108
109fn check_rate_standard(u: f64, parameter: &'static str) -> Result<f64, ControlChartError> {
111 if u.is_finite() && u > 0.0 {
112 Ok(u)
113 } else {
114 Err(ControlChartError::rate_standard(parameter, u))
115 }
116}
117
118pub struct PChart {
162 samples: Vec<(u64, u64)>,
164 chart_points: Vec<AttributeChartPoint>,
166 p_bar: Option<f64>,
168 center: Option<f64>,
170}
171
172impl PChart {
173 pub fn new() -> Self {
175 Self {
176 samples: Vec::new(),
177 chart_points: Vec::new(),
178 p_bar: None,
179 center: None,
180 }
181 }
182
183 pub fn with_center(p_bar: f64) -> Result<Self, ControlChartError> {
192 Ok(Self {
193 center: Some(check_proportion_standard(p_bar, "p_bar")?),
194 ..Self::new()
195 })
196 }
197
198 pub fn add_sample(
208 &mut self,
209 defectives: u64,
210 sample_size: u64,
211 ) -> Result<(), ControlChartError> {
212 check_proportion(defectives, sample_size)?;
213 self.samples.push((defectives, sample_size));
214 self.recompute();
215 Ok(())
216 }
217
218 pub fn p_bar(&self) -> Option<f64> {
222 self.p_bar
223 }
224
225 pub fn points(&self) -> &[AttributeChartPoint] {
227 &self.chart_points
228 }
229
230 pub fn is_in_control(&self) -> bool {
232 self.chart_points.iter().all(|p| !p.out_of_control)
233 }
234
235 fn recompute(&mut self) {
237 if self.samples.is_empty() {
238 self.p_bar = None;
239 self.chart_points.clear();
240 return;
241 }
242
243 let p_bar = self.center.unwrap_or_else(|| {
244 let total_defectives: u64 = self.samples.iter().map(|&(d, _)| d).sum();
245 let total_inspected: u64 = self.samples.iter().map(|&(_, n)| n).sum();
246 total_defectives as f64 / total_inspected as f64
247 });
248 self.p_bar = Some(p_bar);
249
250 self.chart_points = self
251 .samples
252 .iter()
253 .enumerate()
254 .map(|(i, &(defectives, sample_size))| {
255 let p = defectives as f64 / sample_size as f64;
256 let sigma = (p_bar * (1.0 - p_bar) / sample_size as f64).sqrt();
257 attribute_point(i, p, p_bar, sigma)
258 })
259 .collect();
260 }
261}
262
263impl Default for PChart {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269pub struct NPChart {
289 sample_size: u64,
291 defective_counts: Vec<u64>,
293 chart_points: Vec<AttributeChartPoint>,
295 limits: Option<(f64, f64, f64)>, }
298
299impl NPChart {
300 pub fn new(sample_size: u64) -> Result<Self, ControlChartError> {
309 if sample_size == 0 {
310 return Err(ControlChartError::ZeroSampleSize);
311 }
312 Ok(Self {
313 sample_size,
314 defective_counts: Vec::new(),
315 chart_points: Vec::new(),
316 limits: None,
317 })
318 }
319
320 pub fn add_sample(&mut self, defectives: u64) -> Result<(), ControlChartError> {
328 if defectives > self.sample_size {
329 return Err(ControlChartError::DefectivesExceedSampleSize {
330 defectives,
331 sample_size: self.sample_size,
332 });
333 }
334 self.defective_counts.push(defectives);
335 self.recompute();
336 Ok(())
337 }
338
339 pub fn control_limits(&self) -> Option<(f64, f64, f64)> {
341 self.limits
342 }
343
344 pub fn points(&self) -> &[AttributeChartPoint] {
346 &self.chart_points
347 }
348
349 pub fn is_in_control(&self) -> bool {
351 self.chart_points.iter().all(|p| !p.out_of_control)
352 }
353
354 fn recompute(&mut self) {
356 if self.defective_counts.is_empty() {
357 self.limits = None;
358 self.chart_points.clear();
359 return;
360 }
361
362 let total_defectives: u64 = self.defective_counts.iter().sum();
363 let total_inspected = self.sample_size * self.defective_counts.len() as u64;
364 let p_bar = total_defectives as f64 / total_inspected as f64;
365 let n = self.sample_size as f64;
366
367 let np_bar = n * p_bar;
368 let sigma = (n * p_bar * (1.0 - p_bar)).sqrt();
369
370 self.chart_points = self
371 .defective_counts
372 .iter()
373 .enumerate()
374 .map(|(i, &count)| attribute_point(i, count as f64, np_bar, sigma))
375 .collect();
376 let first = &self.chart_points[0];
377 self.limits = Some((first.ucl, np_bar, first.lcl));
378 }
379}
380
381pub struct CChart {
401 defect_counts: Vec<u64>,
403 chart_points: Vec<AttributeChartPoint>,
405 limits: Option<(f64, f64, f64)>, }
408
409impl CChart {
410 pub fn new() -> Self {
412 Self {
413 defect_counts: Vec::new(),
414 chart_points: Vec::new(),
415 limits: None,
416 }
417 }
418
419 pub fn add_sample(&mut self, defects: u64) {
421 self.defect_counts.push(defects);
422 self.recompute();
423 }
424
425 pub fn control_limits(&self) -> Option<(f64, f64, f64)> {
427 self.limits
428 }
429
430 pub fn points(&self) -> &[AttributeChartPoint] {
432 &self.chart_points
433 }
434
435 pub fn is_in_control(&self) -> bool {
437 self.chart_points.iter().all(|p| !p.out_of_control)
438 }
439
440 fn recompute(&mut self) {
442 if self.defect_counts.is_empty() {
443 self.limits = None;
444 self.chart_points.clear();
445 return;
446 }
447
448 let total: u64 = self.defect_counts.iter().sum();
449 let c_bar = total as f64 / self.defect_counts.len() as f64;
450 let sigma = c_bar.sqrt();
451
452 self.chart_points = self
453 .defect_counts
454 .iter()
455 .enumerate()
456 .map(|(i, &count)| attribute_point(i, count as f64, c_bar, sigma))
457 .collect();
458 let first = &self.chart_points[0];
459 self.limits = Some((first.ucl, c_bar, first.lcl));
460 }
461}
462
463impl Default for CChart {
464 fn default() -> Self {
465 Self::new()
466 }
467}
468
469pub struct UChart {
493 samples: Vec<(u64, f64)>,
495 chart_points: Vec<AttributeChartPoint>,
497 u_bar: Option<f64>,
499 center: Option<f64>,
501}
502
503impl UChart {
504 pub fn new() -> Self {
506 Self {
507 samples: Vec::new(),
508 chart_points: Vec::new(),
509 u_bar: None,
510 center: None,
511 }
512 }
513
514 pub fn with_center(u_bar: f64) -> Result<Self, ControlChartError> {
521 Ok(Self {
522 center: Some(check_rate_standard(u_bar, "u_bar")?),
523 ..Self::new()
524 })
525 }
526
527 pub fn add_sample(
537 &mut self,
538 defects: u64,
539 units_inspected: f64,
540 ) -> Result<(), ControlChartError> {
541 check_units(units_inspected)?;
542 self.samples.push((defects, units_inspected));
543 self.recompute();
544 Ok(())
545 }
546
547 pub fn u_bar(&self) -> Option<f64> {
551 self.u_bar
552 }
553
554 pub fn points(&self) -> &[AttributeChartPoint] {
556 &self.chart_points
557 }
558
559 pub fn is_in_control(&self) -> bool {
561 self.chart_points.iter().all(|p| !p.out_of_control)
562 }
563
564 fn recompute(&mut self) {
566 if self.samples.is_empty() {
567 self.u_bar = None;
568 self.chart_points.clear();
569 return;
570 }
571
572 let u_bar = self.center.unwrap_or_else(|| {
573 let total_defects: u64 = self.samples.iter().map(|&(d, _)| d).sum();
574 let total_units: f64 = self.samples.iter().map(|&(_, n)| n).sum();
575 total_defects as f64 / total_units
576 });
577 self.u_bar = Some(u_bar);
578
579 self.chart_points = self
580 .samples
581 .iter()
582 .enumerate()
583 .map(|(i, &(defects, units))| {
584 attribute_point(i, defects as f64 / units, u_bar, (u_bar / units).sqrt())
585 })
586 .collect();
587 }
588}
589
590impl Default for UChart {
591 fn default() -> Self {
592 Self::new()
593 }
594}
595
596#[derive(Debug, Clone)]
611pub struct LaneyAttributePoint {
612 pub index: usize,
614 pub value: f64,
616 pub ucl: f64,
618 pub cl: f64,
620 pub lcl: f64,
622 pub out_of_control: bool,
624 pub z: Option<f64>,
628}
629
630#[derive(Debug, Clone, Copy, PartialEq)]
637pub struct LaneyStandard {
638 pub center: f64,
641 pub phi: f64,
643}
644
645#[derive(Debug, Clone)]
650pub struct LaneyPChart {
651 pub p_bar: f64,
654 pub phi: f64,
658 pub points: Vec<LaneyAttributePoint>,
660}
661
662#[derive(Debug, Clone)]
667pub struct LaneyUChart {
668 pub u_bar: f64,
671 pub phi: f64,
675 pub points: Vec<LaneyAttributePoint>,
677}
678
679const LANEY_D2: f64 = 1.128;
681
682fn laney_phi(rows: &[(f64, f64)], center: f64) -> f64 {
685 if rows.iter().any(|&(_, sigma)| sigma <= 0.0) {
686 return 0.0;
687 }
688 let z: Vec<f64> = rows.iter().map(|&(v, s)| (v - center) / s).collect();
689 let mr_bar = z.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>() / (z.len() - 1) as f64;
690 mr_bar / LANEY_D2
691}
692
693fn laney_points(rows: &[(f64, f64)], center: f64, phi: f64) -> Vec<LaneyAttributePoint> {
695 rows.iter()
696 .enumerate()
697 .map(|(index, &(value, sigma_i))| {
698 let sigma = phi * sigma_i;
699 let ucl = center + 3.0 * sigma;
700 let lcl = (center - 3.0 * sigma).max(0.0);
701 LaneyAttributePoint {
702 index,
703 value,
704 ucl,
705 cl: center,
706 lcl,
707 out_of_control: value > ucl || value < lcl,
708 z: standardized(value, center, sigma),
709 }
710 })
711 .collect()
712}
713
714fn laney_preconditions(
717 n: usize,
718 standard: Option<LaneyStandard>,
719 check_center: fn(f64, &'static str) -> Result<f64, ControlChartError>,
720 center_name: &'static str,
721) -> Result<(), ChartInputError> {
722 if let Some(s) = standard {
723 check_center(s.center, center_name).map_err(ChartInputError::Standard)?;
724 if !(s.phi.is_finite() && s.phi >= 0.0) {
725 return Err(ChartInputError::Standard(
726 ControlChartError::dispersion_standard("phi", s.phi),
727 ));
728 }
729 }
730 let min = if standard.is_some() {
731 1
732 } else {
733 LANEY_MIN_SAMPLES
734 };
735 if n < min {
736 return Err(ChartInputError::TooFewSamples { got: n, min });
737 }
738 Ok(())
739}
740
741pub fn laney_p_chart(
779 samples: &[(u64, u64)],
780 standard: Option<LaneyStandard>,
781) -> Result<LaneyPChart, ChartInputError> {
782 laney_preconditions(samples.len(), standard, check_proportion_standard, "p_bar")?;
783 for (index, &(defectives, sample_size)) in samples.iter().enumerate() {
787 if let Err(error) = check_proportion(defectives, sample_size) {
788 return Err(ChartInputError::Sample { index, error });
789 }
790 }
791
792 let p_bar = standard.map_or_else(
793 || {
794 let total_defectives: u64 = samples.iter().map(|&(d, _)| d).sum();
795 let total_inspected: u64 = samples.iter().map(|&(_, n)| n).sum();
796 total_defectives as f64 / total_inspected as f64
797 },
798 |s| s.center,
799 );
800 let base_var = p_bar * (1.0 - p_bar);
801 let rows: Vec<(f64, f64)> = samples
802 .iter()
803 .map(|&(d, n)| (d as f64 / n as f64, (base_var / n as f64).sqrt()))
804 .collect();
805 let phi = standard.map_or_else(|| laney_phi(&rows, p_bar), |s| s.phi);
806
807 Ok(LaneyPChart {
808 p_bar,
809 phi,
810 points: laney_points(&rows, p_bar, phi),
811 })
812}
813
814pub fn laney_u_chart(
847 samples: &[(u64, f64)],
848 standard: Option<LaneyStandard>,
849) -> Result<LaneyUChart, ChartInputError> {
850 laney_preconditions(samples.len(), standard, check_rate_standard, "u_bar")?;
851 for (index, &(_, units)) in samples.iter().enumerate() {
852 if let Err(error) = check_units(units) {
853 return Err(ChartInputError::Sample { index, error });
854 }
855 }
856
857 let u_bar = standard.map_or_else(
858 || {
859 let total_defects: u64 = samples.iter().map(|&(d, _)| d).sum();
860 let total_units: f64 = samples.iter().map(|&(_, n)| n).sum();
862 total_defects as f64 / total_units
863 },
864 |s| s.center,
865 );
866 let rows: Vec<(f64, f64)> = samples
867 .iter()
868 .map(|&(d, n)| (d as f64 / n, (u_bar / n).sqrt()))
869 .collect();
870 let phi = standard.map_or_else(|| laney_phi(&rows, u_bar), |s| s.phi);
871
872 Ok(LaneyUChart {
873 u_bar,
874 phi,
875 points: laney_points(&rows, u_bar, phi),
876 })
877}
878#[derive(Debug, Clone)]
884pub struct GChartPoint {
885 pub index: usize,
887 pub value: f64,
889 pub ucl: f64,
891 pub cl: f64,
893 pub lcl: f64,
895 pub out_of_control: bool,
897}
898
899#[derive(Debug, Clone)]
916pub struct GChart {
917 pub g_bar: f64,
919 pub points: Vec<GChartPoint>,
921}
922
923#[derive(Debug, Clone)]
935pub struct TChart {
936 pub t_bar: f64,
938 pub points: Vec<TChartPoint>,
940}
941
942#[derive(Debug, Clone)]
944pub struct TChartPoint {
945 pub index: usize,
947 pub value: f64,
949 pub ucl: f64,
951 pub cl: f64,
953 pub lcl: f64,
955 pub out_of_control: bool,
957}
958
959pub fn g_chart(inter_event_counts: &[f64]) -> Option<GChart> {
985 if inter_event_counts.len() < 3 {
986 return None;
987 }
988 if inter_event_counts
989 .iter()
990 .any(|&v| !v.is_finite() || v < 0.0)
991 {
992 return None;
993 }
994
995 let g_bar = inter_event_counts.iter().sum::<f64>() / inter_event_counts.len() as f64;
996 let spread = (g_bar * (g_bar + 1.0)).sqrt();
997 let ucl = g_bar + 3.0 * spread;
998 let lcl = (g_bar - 3.0 * spread).max(0.0);
999
1000 let points = inter_event_counts
1001 .iter()
1002 .enumerate()
1003 .map(|(i, &v)| GChartPoint {
1004 index: i,
1005 value: v,
1006 ucl,
1007 cl: g_bar,
1008 lcl,
1009 out_of_control: v > ucl || v < lcl,
1010 })
1011 .collect();
1012
1013 Some(GChart { g_bar, points })
1014}
1015
1016pub fn t_chart(inter_event_times: &[f64]) -> Option<TChart> {
1038 if inter_event_times.len() < 3 {
1039 return None;
1040 }
1041 if inter_event_times
1042 .iter()
1043 .any(|&v| !v.is_finite() || v <= 0.0)
1044 {
1045 return None;
1046 }
1047
1048 let t_bar = inter_event_times.iter().sum::<f64>() / inter_event_times.len() as f64;
1049
1050 let ucl_factor = -(0.00135_f64.ln()); let lcl_factor = -(0.99865_f64.ln()); let ucl = t_bar * ucl_factor;
1057 let lcl = (t_bar * lcl_factor).max(0.0);
1058
1059 let points = inter_event_times
1060 .iter()
1061 .enumerate()
1062 .map(|(i, &v)| TChartPoint {
1063 index: i,
1064 value: v,
1065 ucl,
1066 cl: t_bar,
1067 lcl,
1068 out_of_control: v > ucl || v < lcl,
1069 })
1070 .collect();
1071
1072 Some(TChart { t_bar, points })
1073}
1074
1075#[cfg(test)]
1080mod tests {
1081 use super::*;
1082
1083 #[test]
1086 fn test_p_chart_basic() {
1087 let mut chart = PChart::new();
1089 let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
1090 for &d in &defectives {
1091 chart.add_sample(d, 100).unwrap();
1092 }
1093
1094 let p_bar = chart.p_bar().expect("should have p_bar");
1095 assert!(
1097 (p_bar - 0.055).abs() < 1e-10,
1098 "p_bar={p_bar}, expected 0.055"
1099 );
1100
1101 assert_eq!(chart.points().len(), 10);
1103
1104 for pt in chart.points() {
1106 assert!((pt.cl - 0.055).abs() < 1e-10);
1107 }
1108 }
1109
1110 #[test]
1111 fn test_p_chart_limits() {
1112 let mut chart = PChart::new();
1113 chart.add_sample(10, 100).unwrap();
1118
1119 let pt = &chart.points()[0];
1120 assert!((pt.cl - 0.1).abs() < 1e-10);
1121 assert!((pt.ucl - 0.19).abs() < 0.001);
1122 assert!((pt.lcl - 0.01).abs() < 0.001);
1123 }
1124
1125 #[test]
1126 fn test_p_chart_variable_sample_sizes() {
1127 let mut chart = PChart::new();
1128 chart.add_sample(5, 100).unwrap();
1129 chart.add_sample(10, 200).unwrap();
1130 chart.add_sample(3, 50).unwrap();
1131
1132 let p_bar = chart.p_bar().expect("p_bar");
1134 assert!((p_bar - 18.0 / 350.0).abs() < 1e-10);
1135
1136 let pts = chart.points();
1138 assert!(pts[1].ucl - pts[1].cl < pts[0].ucl - pts[0].cl);
1140 }
1141
1142 #[test]
1143 fn test_p_chart_rejects_invalid() {
1144 use crate::spc::ControlChartError;
1145 let mut chart = PChart::new();
1146 assert_eq!(
1147 chart.add_sample(5, 0),
1148 Err(ControlChartError::ZeroSampleSize)
1149 );
1150 assert_eq!(
1151 chart.add_sample(10, 5),
1152 Err(ControlChartError::DefectivesExceedSampleSize {
1153 defectives: 10,
1154 sample_size: 5
1155 })
1156 );
1157 assert!(chart.p_bar().is_none());
1158 }
1159
1160 #[test]
1161 fn test_p_chart_lcl_clamped_to_zero() {
1162 let mut chart = PChart::new();
1163 chart.add_sample(1, 10).unwrap();
1165 let pt = &chart.points()[0];
1166 assert!(pt.lcl >= 0.0);
1167 }
1168
1169 #[test]
1170 fn test_p_chart_out_of_control() {
1171 let mut chart = PChart::new();
1172 for _ in 0..20 {
1174 chart.add_sample(5, 100).unwrap();
1175 }
1176 chart.add_sample(30, 100).unwrap();
1178
1179 assert!(!chart.is_in_control());
1180 let last = chart.points().last().expect("should have points");
1181 assert!(last.out_of_control);
1182 }
1183
1184 #[test]
1185 fn test_p_chart_default() {
1186 let chart = PChart::default();
1187 assert!(chart.p_bar().is_none());
1188 assert!(chart.points().is_empty());
1189 }
1190
1191 #[test]
1194 fn test_np_chart_basic() {
1195 let mut chart = NPChart::new(100).expect("100 is a valid sample size");
1196 let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
1197 for &d in &defectives {
1198 chart.add_sample(d).unwrap();
1199 }
1200
1201 let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
1202 assert!((cl - 5.5).abs() < 1e-10);
1204 assert!(ucl > cl);
1205 assert!(lcl < cl);
1206 assert!(lcl >= 0.0);
1207 }
1208
1209 #[test]
1210 fn test_np_chart_rejects_invalid() {
1211 let mut chart = NPChart::new(100).expect("100 is a valid sample size");
1212 assert_eq!(
1213 chart.add_sample(101),
1214 Err(crate::spc::ControlChartError::DefectivesExceedSampleSize {
1215 defectives: 101,
1216 sample_size: 100
1217 })
1218 );
1219 assert!(chart.control_limits().is_none());
1220 }
1221
1222 #[test]
1223 fn test_np_chart_zero_sample_size() {
1224 assert!(matches!(
1225 NPChart::new(0),
1226 Err(crate::spc::ControlChartError::ZeroSampleSize)
1227 ));
1228 }
1229
1230 #[test]
1231 fn test_np_chart_out_of_control() {
1232 let mut chart = NPChart::new(100).expect("100 is a valid sample size");
1233 for _ in 0..20 {
1234 chart.add_sample(5).unwrap();
1235 }
1236 chart.add_sample(30).unwrap();
1237
1238 assert!(!chart.is_in_control());
1239 }
1240
1241 #[test]
1242 fn test_np_chart_limits_formula() {
1243 let mut chart = NPChart::new(200).expect("200 is a valid sample size");
1248 for _ in 0..10 {
1249 chart.add_sample(10).unwrap();
1250 }
1251
1252 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1253 assert!((cl - 10.0).abs() < 1e-10);
1254 let expected_sigma = (200.0_f64 * 0.05 * 0.95).sqrt();
1255 assert!((ucl - (10.0 + 3.0 * expected_sigma)).abs() < 0.01);
1256 assert!((lcl - (10.0 - 3.0 * expected_sigma)).abs() < 0.01);
1257 }
1258
1259 #[test]
1262 fn test_c_chart_basic() {
1263 let mut chart = CChart::new();
1264 let counts = [3, 5, 4, 6, 2, 7, 3, 4, 5, 6];
1265 for &c in &counts {
1266 chart.add_sample(c);
1267 }
1268
1269 let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
1270 assert!((cl - 4.5).abs() < 1e-10);
1272 let expected_ucl = 4.5 + 3.0 * 4.5_f64.sqrt();
1274 assert!((ucl - expected_ucl).abs() < 0.01);
1275 assert!(lcl >= 0.0);
1276 }
1277
1278 #[test]
1279 fn test_c_chart_out_of_control() {
1280 let mut chart = CChart::new();
1281 for _ in 0..20 {
1282 chart.add_sample(5);
1283 }
1284 chart.add_sample(50); assert!(!chart.is_in_control());
1287 }
1288
1289 #[test]
1290 fn test_c_chart_single_sample() {
1291 let mut chart = CChart::new();
1292 chart.add_sample(10);
1293
1294 let (_, cl, _) = chart.control_limits().expect("limits");
1295 assert!((cl - 10.0).abs() < f64::EPSILON);
1296 }
1297
1298 #[test]
1299 fn test_c_chart_lcl_clamped() {
1300 let mut chart = CChart::new();
1302 chart.add_sample(1);
1303
1304 let (_, _, lcl) = chart.control_limits().expect("limits");
1305 assert!((lcl - 0.0).abs() < f64::EPSILON);
1306 }
1307
1308 #[test]
1309 fn test_c_chart_default() {
1310 let chart = CChart::default();
1311 assert!(chart.control_limits().is_none());
1312 assert!(chart.points().is_empty());
1313 }
1314
1315 #[test]
1318 fn test_u_chart_basic() {
1319 let mut chart = UChart::new();
1320 chart.add_sample(3, 10.0).unwrap();
1322 chart.add_sample(5, 10.0).unwrap();
1323 chart.add_sample(4, 10.0).unwrap();
1324 chart.add_sample(6, 10.0).unwrap();
1325 chart.add_sample(2, 10.0).unwrap();
1326
1327 let u_bar = chart.u_bar().expect("should have u_bar");
1328 assert!((u_bar - 0.4).abs() < 1e-10);
1330
1331 assert_eq!(chart.points().len(), 5);
1332 }
1333
1334 #[test]
1335 fn test_u_chart_variable_units() {
1336 let mut chart = UChart::new();
1337 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");
1342 assert!((u_bar - 2.0).abs() < 1e-10);
1344
1345 let pts = chart.points();
1347 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");
1350 }
1351
1352 #[test]
1353 fn test_u_chart_rejects_invalid() {
1354 let mut chart = UChart::new();
1355 for units in [0.0, -1.0, f64::NAN, f64::INFINITY] {
1356 assert_eq!(
1357 chart.add_sample(5, units),
1358 Err(crate::spc::ControlChartError::NonPositiveUnits),
1359 "units = {units}"
1360 );
1361 }
1362 assert!(chart.u_bar().is_none());
1363 }
1364
1365 #[test]
1366 fn test_u_chart_out_of_control() {
1367 let mut chart = UChart::new();
1368 for _ in 0..20 {
1369 chart.add_sample(4, 10.0).unwrap();
1370 }
1371 chart.add_sample(50, 10.0).unwrap(); assert!(!chart.is_in_control());
1374 }
1375
1376 #[test]
1377 fn test_u_chart_lcl_clamped() {
1378 let mut chart = UChart::new();
1379 chart.add_sample(1, 1.0).unwrap();
1381 let pt = &chart.points()[0];
1382 assert!(pt.lcl >= 0.0);
1383 }
1384
1385 #[test]
1386 fn test_u_chart_default() {
1387 let chart = UChart::default();
1388 assert!(chart.u_bar().is_none());
1389 assert!(chart.points().is_empty());
1390 }
1391
1392 #[test]
1393 fn test_u_chart_limits_formula() {
1394 let mut chart = UChart::new();
1399 chart.add_sample(8, 4.0).unwrap();
1400
1401 let pt = &chart.points()[0];
1402 assert!((pt.cl - 2.0).abs() < 1e-10);
1403 let expected_sigma = (2.0_f64 / 4.0).sqrt();
1404 assert!((pt.ucl - (2.0 + 3.0 * expected_sigma)).abs() < 0.001);
1405 }
1406
1407 #[test]
1410 fn test_p_and_np_consistent() {
1411 let mut p_chart = PChart::new();
1413 let mut np_chart = NPChart::new(100).expect("100 is a valid sample size");
1414
1415 let defectives = [5, 8, 3, 6, 4];
1416 for &d in &defectives {
1417 p_chart.add_sample(d, 100).unwrap();
1418 np_chart.add_sample(d).unwrap();
1419 }
1420
1421 let p_bar = p_chart.p_bar().expect("p_bar");
1422 let (_, np_cl, _) = np_chart.control_limits().expect("np limits");
1423
1424 assert!(
1426 (np_cl - 100.0 * p_bar).abs() < 1e-10,
1427 "NP CL should equal n * p_bar"
1428 );
1429 }
1430
1431 #[test]
1432 fn test_c_and_u_consistent_equal_units() {
1433 let mut c_chart = CChart::new();
1435 let mut u_chart = UChart::new();
1436
1437 let defects = [3, 5, 4, 6, 2];
1438 for &d in &defects {
1439 c_chart.add_sample(d);
1440 u_chart.add_sample(d, 1.0).unwrap();
1441 }
1442
1443 let (c_ucl, c_cl, c_lcl) = c_chart.control_limits().expect("C limits");
1444 let u_bar = u_chart.u_bar().expect("u_bar");
1445
1446 assert!(
1447 (c_cl - u_bar).abs() < 1e-10,
1448 "C chart CL should equal U chart u-bar when n=1"
1449 );
1450
1451 let u_pt = &u_chart.points()[0];
1452 assert!((u_pt.ucl - c_ucl).abs() < 1e-10);
1453 assert!((u_pt.lcl - c_lcl).abs() < 1e-10);
1454 }
1455
1456 #[test]
1459 fn laney_p_basic() {
1460 let samples: Vec<(u64, u64)> = (0..10).map(|i| (i % 5 + 2, 200)).collect();
1461 let chart = laney_p_chart(&samples, None).expect("valid samples");
1462 assert!(chart.phi > 0.0);
1463 assert!(chart.p_bar > 0.0 && chart.p_bar < 1.0);
1464 assert_eq!(chart.points.len(), 10);
1465 }
1466
1467 #[test]
1468 fn laney_p_constant_proportion_phi_near_zero() {
1469 let samples: Vec<(u64, u64)> = vec![(10, 1000); 20];
1471 let chart = laney_p_chart(&samples, None).expect("valid samples");
1472 assert!((chart.p_bar - 0.01).abs() < 1e-10);
1473 assert!(chart.phi >= 0.0);
1474 }
1475
1476 #[test]
1477 fn laney_p_ucl_above_lcl() {
1478 let samples: Vec<(u64, u64)> = vec![(5, 100), (8, 100), (3, 100), (6, 100), (4, 100)];
1479 let chart = laney_p_chart(&samples, None).expect("valid samples");
1480 for p in &chart.points {
1481 assert!(p.ucl >= p.lcl);
1482 assert!((p.cl - chart.p_bar).abs() < 1e-10);
1483 }
1484 }
1485
1486 #[test]
1487 fn laney_p_insufficient_data() {
1488 let samples: Vec<(u64, u64)> = vec![(2, 100), (3, 100)];
1489 assert_eq!(
1490 laney_p_chart(&samples, None).unwrap_err(),
1491 ChartInputError::TooFewSamples { got: 2, min: 3 }
1492 );
1493 }
1494
1495 #[test]
1496 fn laney_p_rejects_a_sample_with_no_proportion() {
1497 assert_eq!(
1501 laney_p_chart(&[(3, 100), (0, 0), (4, 100), (2, 100)], None).unwrap_err(),
1502 ChartInputError::Sample {
1503 index: 1,
1504 error: ControlChartError::ZeroSampleSize
1505 }
1506 );
1507 assert_eq!(
1510 laney_p_chart(&[(3, 100), (4, 100), (12, 10), (2, 100)], None).unwrap_err(),
1511 ChartInputError::Sample {
1512 index: 2,
1513 error: ControlChartError::DefectivesExceedSampleSize {
1514 defectives: 12,
1515 sample_size: 10
1516 }
1517 }
1518 );
1519 }
1520
1521 #[test]
1524 fn laney_p_phase_two_judges_a_point_against_phase_one() {
1525 let standard = LaneyStandard {
1528 center: 0.0514,
1529 phi: 0.236,
1530 };
1531 let chart = laney_p_chart(&[(11, 170)], Some(standard)).expect("one sample suffices");
1532 assert_eq!(
1533 (chart.p_bar, chart.phi),
1534 (0.0514, 0.236),
1535 "the standard, not an estimate"
1536 );
1537 let p = &chart.points[0];
1538 let expected_ucl = 0.0514 + 3.0 * 0.236 * (0.0514 * (1.0 - 0.0514) / 170.0_f64).sqrt();
1539 assert!((p.ucl - expected_ucl).abs() < 1e-15);
1540 assert!((p.ucl - 0.0634).abs() < 5e-5, "ucl = {}", p.ucl);
1541 assert!(p.out_of_control);
1542 }
1543
1544 #[test]
1545 fn re_estimating_across_phases_lets_a_shift_widen_its_own_limits() {
1546 let phase_one: Vec<(u64, u64)> = [(8, 160), (9, 170), (7, 150), (10, 180), (8, 165)].into();
1549 let pooled_input: Vec<(u64, u64)> = phase_one
1550 .iter()
1551 .copied()
1552 .chain([(30, 170), (32, 175)])
1553 .collect();
1554 let p1 = laney_p_chart(&phase_one, None).expect("phase I");
1555 let pooled = laney_p_chart(&pooled_input, None).expect("pooled");
1556 let phase_two = laney_p_chart(
1557 &[(30, 170), (32, 175)],
1558 Some(LaneyStandard {
1559 center: p1.p_bar,
1560 phi: p1.phi,
1561 }),
1562 )
1563 .expect("phase II");
1564 assert!(
1565 pooled.p_bar > p1.p_bar,
1566 "the shift pulls the pooled centre up"
1567 );
1568 assert!(phase_two.points.iter().all(|p| p.cl == p1.p_bar));
1569 assert!(pooled.points[5].ucl > phase_two.points[0].ucl);
1570 }
1571
1572 #[test]
1573 fn p_and_u_charts_take_a_known_center() {
1574 let mut p = PChart::with_center(0.05).expect("valid p̄");
1575 p.add_sample(2, 100).unwrap();
1576 p.add_sample(20, 100).unwrap();
1577 assert_eq!(p.p_bar(), Some(0.05), "not re-estimated from the samples");
1578 let sigma = (0.05_f64 * 0.95 / 100.0).sqrt();
1579 assert!((p.points()[1].ucl - (0.05 + 3.0 * sigma)).abs() < 1e-15);
1580 assert!(p.points()[1].out_of_control);
1581
1582 let mut u = UChart::with_center(2.0).expect("valid ū");
1583 u.add_sample(9, 2.0).unwrap();
1584 assert_eq!(u.u_bar(), Some(2.0));
1585 assert!((u.points()[0].ucl - (2.0 + 3.0 * (2.0_f64 / 2.0).sqrt())).abs() < 1e-15);
1586 }
1587
1588 #[test]
1589 fn a_standard_outside_its_domain_is_refused() {
1590 for p in [0.0, 1.0, -0.1, f64::NAN] {
1591 match PChart::with_center(p).err() {
1592 Some(ControlChartError::InvalidStandard {
1593 parameter: "p_bar",
1594 got,
1595 exclusive_min: Some(0.0),
1596 exclusive_max: Some(1.0),
1597 min: None,
1598 max: None,
1599 ..
1600 }) => assert!(got == p || (got.is_nan() && p.is_nan()), "p̄ = {p}"),
1601 other => panic!("p̄ = {p}: {other:?}"),
1602 }
1603 }
1604 assert!(UChart::with_center(0.0).is_err());
1605 let bad_phi = LaneyStandard {
1606 center: 0.1,
1607 phi: -1.0,
1608 };
1609 assert_eq!(
1610 laney_p_chart(&[(1, 10)], Some(bad_phi)).unwrap_err(),
1611 ChartInputError::Standard(ControlChartError::dispersion_standard("phi", -1.0))
1612 );
1613 let good = LaneyStandard {
1614 center: 0.1,
1615 phi: 1.0,
1616 };
1617 assert_eq!(
1618 laney_u_chart(&[], Some(good)).unwrap_err(),
1619 ChartInputError::TooFewSamples { got: 0, min: 1 }
1620 );
1621 }
1622
1623 #[test]
1624 fn z_is_the_point_on_the_scale_where_every_limit_is_three() {
1625 let mut p = PChart::new();
1628 for (d, n) in [(3, 100), (12, 180), (1, 60), (9, 150), (4, 90)] {
1629 p.add_sample(d, n).unwrap();
1630 }
1631 for pt in p.points() {
1632 let sigma = (pt.ucl - pt.cl) / 3.0;
1633 let z = pt.z.expect("p̄ strictly inside (0, 1)");
1634 assert!((pt.cl + z * sigma - pt.value).abs() < 1e-12);
1635 if pt.lcl > 0.0 {
1636 assert_eq!(pt.out_of_control, z.abs() > 3.0);
1637 }
1638 }
1639 let laney =
1640 laney_p_chart(&[(3, 100), (12, 180), (1, 60), (9, 150), (4, 90)], None).expect("valid");
1641 for pt in &laney.points {
1642 let z = pt.z.expect("φ > 0");
1643 assert!((pt.cl + z * (pt.ucl - pt.cl) / 3.0 - pt.value).abs() < 1e-12);
1644 }
1645 let flat = laney_p_chart(&[(0, 10), (0, 12), (0, 9)], None).expect("valid");
1647 assert!(flat.points.iter().all(|p| p.z.is_none()));
1648 }
1649
1650 #[test]
1651 fn laney_u_rejects_units_by_position() {
1652 assert_eq!(
1653 laney_u_chart(&[(3, 10.0), (4, 10.0), (2, 10.0), (5, 0.0)], None).unwrap_err(),
1654 ChartInputError::Sample {
1655 index: 3,
1656 error: ControlChartError::NonPositiveUnits
1657 }
1658 );
1659 assert_eq!(
1660 laney_u_chart(&[(3, 10.0)], None).unwrap_err(),
1661 ChartInputError::TooFewSamples { got: 1, min: 3 }
1662 );
1663 }
1664 #[test]
1665 fn laney_u_basic() {
1666 let samples: Vec<(u64, f64)> = vec![(5, 10.0); 10];
1667 let chart = laney_u_chart(&samples, None).expect("valid samples");
1668 assert!((chart.u_bar - 0.5).abs() < 1e-10);
1669 assert!(chart.phi >= 0.0);
1670 }
1671
1672 #[test]
1673 fn laney_u_ucl_above_cl() {
1674 let samples: Vec<(u64, f64)> = (0..8).map(|i| ((i % 4 + 2) as u64, 10.0)).collect();
1675 let chart = laney_u_chart(&samples, None).expect("valid samples");
1676 for p in &chart.points {
1677 assert!(p.ucl > p.cl || (p.ucl - p.cl).abs() < 1e-10);
1678 }
1679 }
1680
1681 #[test]
1692 fn p_chart_montgomery_reference_formula() {
1693 let mut chart = PChart::new();
1696 for _ in 0..19 {
1697 chart.add_sample(10, 100).unwrap();
1698 }
1699 chart.add_sample(8, 100).unwrap();
1700
1701 let p_bar = chart.p_bar().expect("p_bar");
1702 assert!(
1703 (p_bar - 0.099).abs() < 1e-10,
1704 "p̄ expected 0.099, got {p_bar}"
1705 );
1706
1707 let sigma = (0.099_f64 * 0.901 / 100.0).sqrt();
1708 let expected_ucl = 0.099 + 3.0 * sigma; let expected_lcl = (0.099 - 3.0 * sigma).max(0.0); for pt in chart.points() {
1712 assert!(
1713 (pt.ucl - expected_ucl).abs() < 1e-6,
1714 "UCL mismatch at index {}: expected {expected_ucl:.6}, got {:.6}",
1715 pt.index,
1716 pt.ucl
1717 );
1718 assert!(
1719 (pt.lcl - expected_lcl).abs() < 1e-6,
1720 "LCL mismatch at index {}: expected {expected_lcl:.6}, got {:.6}",
1721 pt.index,
1722 pt.lcl
1723 );
1724 }
1725 }
1726
1727 #[test]
1736 fn np_chart_montgomery_reference() {
1737 let mut chart = NPChart::new(100).expect("100 is a valid sample size");
1739 for _ in 0..19 {
1740 chart.add_sample(10).unwrap();
1741 }
1742 chart.add_sample(8).unwrap();
1743
1744 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1745 assert!((cl - 9.9).abs() < 1e-10, "NP CL expected 9.9, got {cl}");
1747 let expected_sigma = (9.9_f64 * 0.901).sqrt();
1749 let expected_ucl = 9.9 + 3.0 * expected_sigma; let expected_lcl = (9.9 - 3.0 * expected_sigma).max(0.0); assert!(
1752 (ucl - expected_ucl).abs() < 1e-6,
1753 "NP UCL expected {expected_ucl:.4}, got {ucl:.4}"
1754 );
1755 assert!(
1756 (lcl - expected_lcl).abs() < 1e-6,
1757 "NP LCL expected {expected_lcl:.4}, got {lcl:.4}"
1758 );
1759 }
1760
1761 #[test]
1767 fn c_chart_montgomery_reference() {
1768 let mut chart = CChart::new();
1770 for _ in 0..20 {
1771 chart.add_sample(10);
1772 }
1773
1774 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1775 assert!(
1776 (cl - 10.0).abs() < 1e-10,
1777 "C chart CL expected 10.0, got {cl}"
1778 );
1779 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!(
1782 (ucl - expected_ucl).abs() < 1e-6,
1783 "C chart UCL expected {expected_ucl:.4}, got {ucl:.4}"
1784 );
1785 assert!(
1786 (lcl - expected_lcl).abs() < 1e-6,
1787 "C chart LCL expected {expected_lcl:.4}, got {lcl:.4}"
1788 );
1789 }
1790
1791 #[test]
1797 fn u_chart_montgomery_reference() {
1798 let mut chart = UChart::new();
1800 for _ in 0..20 {
1801 chart.add_sample(20, 10.0).unwrap(); }
1803
1804 let u_bar = chart.u_bar().expect("u_bar");
1805 assert!(
1806 (u_bar - 2.0).abs() < 1e-10,
1807 "U chart ū expected 2.0, got {u_bar}"
1808 );
1809
1810 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() {
1815 assert!(
1816 (pt.ucl - expected_ucl).abs() < 1e-6,
1817 "U chart UCL expected {expected_ucl:.4}, got {:.4}",
1818 pt.ucl
1819 );
1820 assert!(
1821 (pt.lcl - expected_lcl).abs() < 1e-6,
1822 "U chart LCL expected {expected_lcl:.4}, got {:.4}",
1823 pt.lcl
1824 );
1825 }
1826 }
1827
1828 #[test]
1831 fn g_chart_ucl_above_cl() {
1832 let gaps = vec![100.0, 120.0, 95.0, 110.0, 105.0];
1833 let chart = g_chart(&gaps).expect("valid samples");
1834 assert!(chart.points[0].ucl > chart.points[0].cl);
1835 assert!(chart.points[0].lcl >= 0.0);
1836 }
1837
1838 #[test]
1839 fn g_chart_insufficient() {
1840 assert!(g_chart(&[100.0, 120.0]).is_none());
1841 }
1842
1843 #[test]
1844 fn g_chart_all_same() {
1845 let chart = g_chart(&[50.0; 8]).expect("valid samples");
1846 assert!((chart.g_bar - 50.0).abs() < 1e-10);
1847 assert!(chart.points[0].ucl > chart.points[0].cl);
1848 }
1849
1850 #[test]
1853 fn t_chart_ucl_factor() {
1854 let times = vec![100.0; 10];
1856 let chart = t_chart(×).expect("valid samples");
1857 let ratio = chart.points[0].ucl / chart.t_bar;
1858 assert!((ratio - 6.6077).abs() < 0.01, "ratio={ratio}");
1859 }
1860
1861 #[test]
1862 fn t_chart_non_positive() {
1863 assert!(t_chart(&[10.0, -5.0, 20.0, 15.0]).is_none());
1864 }
1865
1866 #[test]
1867 fn t_chart_insufficient() {
1868 assert!(t_chart(&[10.0, 20.0]).is_none());
1869 }
1870
1871 #[test]
1878 fn laney_p_ucl_formula_invariant() {
1879 let samples: Vec<(u64, u64)> = vec![
1880 (3, 100),
1881 (7, 100),
1882 (2, 100),
1883 (8, 100),
1884 (4, 100),
1885 (5, 150),
1886 (9, 150),
1887 (3, 150),
1888 (6, 150),
1889 (4, 150),
1890 ];
1891 let chart = laney_p_chart(&samples, None).expect("valid samples");
1892
1893 for (i, (&(d, n), pt)) in samples.iter().zip(&chart.points).enumerate() {
1894 let p_i = d as f64 / n as f64;
1895 let sigma_i = (chart.p_bar * (1.0 - chart.p_bar) / n as f64).sqrt();
1896 let expected_ucl = chart.p_bar + 3.0 * chart.phi * sigma_i;
1897 let expected_lcl = (chart.p_bar - 3.0 * chart.phi * sigma_i).max(0.0);
1898
1899 assert!(
1900 (pt.value - p_i).abs() < 1e-10,
1901 "point {i}: value expected {p_i:.6}, got {:.6}",
1902 pt.value
1903 );
1904 assert!(
1905 (pt.ucl - expected_ucl).abs() < 1e-10,
1906 "point {i}: UCL expected {expected_ucl:.6}, got {:.6}",
1907 pt.ucl
1908 );
1909 assert!(
1910 (pt.lcl - expected_lcl).abs() < 1e-10,
1911 "point {i}: LCL expected {expected_lcl:.6}, got {:.6}",
1912 pt.lcl
1913 );
1914 }
1915 }
1916
1917 #[test]
1935 fn laney_p_phi_near_one_limits_close_to_standard() {
1936 let n: u64 = 10_000;
1940 let d_high: u64 = 5028; let d_low: u64 = 4972; let samples: Vec<(u64, u64)> = vec![
1944 (d_high, n),
1945 (d_low, n),
1946 (d_high, n),
1947 (d_low, n),
1948 (d_high, n),
1949 (d_low, n),
1950 ];
1951
1952 let laney = laney_p_chart(&samples, None).expect("valid samples");
1953
1954 assert!(
1956 (laney.phi - 1.0).abs() < 0.01,
1957 "φ expected ≈1.0, got {}",
1958 laney.phi
1959 );
1960
1961 let p_bar = laney.p_bar;
1963 let sigma_std = (p_bar * (1.0 - p_bar) / n as f64).sqrt();
1964 let std_ucl = p_bar + 3.0 * sigma_std;
1965 let std_lcl = (p_bar - 3.0 * sigma_std).max(0.0);
1966
1967 let max_deviation = 3.0 * sigma_std * (laney.phi - 1.0).abs();
1970 assert!(
1971 (laney.points[0].ucl - std_ucl).abs() <= max_deviation + 1e-12,
1972 "Laney UCL={:.6} vs std UCL={std_ucl:.6}, deviation bound={max_deviation:.2e}",
1973 laney.points[0].ucl
1974 );
1975 assert!(
1976 (laney.points[0].lcl - std_lcl).abs() <= max_deviation + 1e-12,
1977 "Laney LCL={:.6} vs std LCL={std_lcl:.6}, deviation bound={max_deviation:.2e}",
1978 laney.points[0].lcl
1979 );
1980 }
1981
1982 #[test]
1989 fn laney_p_phi_gt_one_wider_than_standard() {
1990 let samples: Vec<(u64, u64)> = vec![
1992 (1, 100),
1993 (20, 100),
1994 (2, 100),
1995 (18, 100),
1996 (1, 100),
1997 (22, 100),
1998 (3, 100),
1999 (19, 100),
2000 (2, 100),
2001 (20, 100),
2002 ];
2003
2004 let laney = laney_p_chart(&samples, None).expect("valid samples");
2005 assert!(
2006 laney.phi > 1.0,
2007 "φ expected > 1 for overdispersed data, got {}",
2008 laney.phi
2009 );
2010
2011 let total_d: u64 = samples.iter().map(|&(d, _)| d).sum();
2013 let total_n: u64 = samples.iter().map(|&(_, n)| n).sum();
2014 let p_bar = total_d as f64 / total_n as f64;
2015 let std_ucl_first = p_bar + 3.0 * (p_bar * (1.0 - p_bar) / 100.0_f64).sqrt();
2016
2017 assert!(
2018 laney.points[0].ucl > std_ucl_first,
2019 "Laney UCL ({:.4}) must exceed standard P chart UCL ({std_ucl_first:.4}) when φ>1",
2020 laney.points[0].ucl
2021 );
2022 }
2023
2024 #[test]
2042 fn laney_p_numerical_reference() {
2043 let samples: Vec<(u64, u64)> = vec![(3, 50), (7, 50), (2, 50), (8, 50), (4, 50)];
2044 let chart = laney_p_chart(&samples, None).expect("valid samples");
2045
2046 let p_bar = 24.0_f64 / 250.0;
2047 assert!(
2048 (chart.p_bar - p_bar).abs() < 1e-10,
2049 "p̄ expected {p_bar:.6}, got {:.6}",
2050 chart.p_bar
2051 );
2052
2053 let sigma_i = (p_bar * (1.0 - p_bar) / 50.0_f64).sqrt();
2055 let p_vals = [0.06_f64, 0.14, 0.04, 0.16, 0.08];
2056 let z: Vec<f64> = p_vals.iter().map(|&p| (p - p_bar) / sigma_i).collect();
2057 let mr_bar = z.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>() / 4.0;
2058 let phi_expected = mr_bar / 1.128;
2059
2060 assert!(
2061 (chart.phi - phi_expected).abs() < 1e-10,
2062 "φ expected {phi_expected:.6}, got {:.6}",
2063 chart.phi
2064 );
2065
2066 let ucl_0 = p_bar + 3.0 * phi_expected * sigma_i;
2068 let lcl_0 = (p_bar - 3.0 * phi_expected * sigma_i).max(0.0);
2069 assert!(
2070 (chart.points[0].ucl - ucl_0).abs() < 1e-10,
2071 "UCL[0] expected {ucl_0:.6}, got {:.6}",
2072 chart.points[0].ucl
2073 );
2074 assert!(
2075 (chart.points[0].lcl - lcl_0).abs() < 1e-10,
2076 "LCL[0] expected {lcl_0:.6}, got {:.6}",
2077 chart.points[0].lcl
2078 );
2079 }
2080
2081 #[test]
2085 fn laney_u_phi_gt_one_wider_than_standard() {
2086 let samples: Vec<(u64, f64)> = vec![
2088 (1, 10.0),
2089 (15, 10.0),
2090 (2, 10.0),
2091 (14, 10.0),
2092 (1, 10.0),
2093 (16, 10.0),
2094 (2, 10.0),
2095 (13, 10.0),
2096 ];
2097
2098 let laney = laney_u_chart(&samples, None).expect("valid samples");
2099 assert!(
2100 laney.phi > 1.0,
2101 "φ expected > 1 for overdispersed data, got {}",
2102 laney.phi
2103 );
2104
2105 let total_d: u64 = samples.iter().map(|&(d, _)| d).sum();
2106 let total_n: f64 = samples.iter().map(|&(_, n)| n).sum();
2107 let u_bar = total_d as f64 / total_n;
2108 let std_ucl = u_bar + 3.0 * (u_bar / 10.0_f64).sqrt();
2109
2110 assert!(
2111 laney.points[0].ucl > std_ucl,
2112 "Laney U' UCL ({:.4}) must exceed standard U chart UCL ({std_ucl:.4}) when φ>1",
2113 laney.points[0].ucl
2114 );
2115 }
2116
2117 #[test]
2130 fn g_chart_formula_verification() {
2131 let gaps = vec![50.0_f64; 8];
2133 let chart = g_chart(&gaps).expect("valid samples");
2134
2135 let g_bar = 50.0_f64;
2136 assert!(
2137 (chart.g_bar - g_bar).abs() < 1e-10,
2138 "ḡ expected 50.0, got {}",
2139 chart.g_bar
2140 );
2141
2142 let spread = (g_bar * (g_bar + 1.0)).sqrt(); let expected_ucl = g_bar + 3.0 * spread;
2144 let expected_lcl = (g_bar - 3.0 * spread).max(0.0);
2145
2146 assert!(
2147 (chart.points[0].ucl - expected_ucl).abs() < 1e-10,
2148 "UCL expected {expected_ucl:.6}, got {:.6}",
2149 chart.points[0].ucl
2150 );
2151 assert!(
2152 (chart.points[0].lcl - expected_lcl).abs() < 1e-10,
2153 "LCL expected {expected_lcl:.6}, got {:.6}",
2154 chart.points[0].lcl
2155 );
2156 assert!(
2158 chart.points[0].lcl >= 0.0,
2159 "LCL must be clamped to 0, got {}",
2160 chart.points[0].lcl
2161 );
2162 }
2163
2164 #[test]
2169 fn g_chart_lcl_always_zero() {
2170 for &g in &[1.0_f64, 5.0, 20.0, 100.0, 500.0] {
2171 let gaps = vec![g; 5];
2172 let chart = g_chart(&gaps).expect("valid samples");
2173 assert!(
2174 (chart.points[0].lcl - 0.0).abs() < 1e-10,
2175 "LCL must be 0 for ḡ={g}, got {}",
2176 chart.points[0].lcl
2177 );
2178 }
2179 }
2180
2181 #[test]
2192 fn t_chart_lcl_factor_verification() {
2193 let times = vec![100.0_f64; 10];
2194 let chart = t_chart(×).expect("valid samples");
2195
2196 let ucl_factor = chart.points[0].ucl / chart.t_bar;
2197 let lcl_factor = chart.points[0].lcl / chart.t_bar;
2198
2199 let expected_ucl_factor = -(0.00135_f64.ln()); let expected_lcl_factor = -(0.99865_f64.ln()); assert!(
2203 (ucl_factor - expected_ucl_factor).abs() < 1e-10,
2204 "UCL factor expected {expected_ucl_factor:.6}, got {ucl_factor:.6}"
2205 );
2206 assert!(
2207 (lcl_factor - expected_lcl_factor).abs() < 1e-10,
2208 "LCL factor expected {expected_lcl_factor:.6}, got {lcl_factor:.6}"
2209 );
2210 }
2211
2212 #[test]
2218 fn t_chart_ucl_lcl_ratio_scale_invariant() {
2219 let k_u = -(0.00135_f64.ln());
2220 let k_l = -(0.99865_f64.ln());
2221 let expected_ratio = k_u / k_l;
2222
2223 for &t_bar in &[10.0_f64, 100.0, 1000.0] {
2224 let times = vec![t_bar; 10];
2225 let chart = t_chart(×).expect("valid samples");
2226 let ratio = chart.points[0].ucl / chart.points[0].lcl;
2227 assert!(
2228 (ratio - expected_ratio).abs() < 0.01,
2229 "UCL/LCL ratio expected {expected_ratio:.2}, got {ratio:.2} at t̄={t_bar}"
2230 );
2231 }
2232 }
2233}