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::InvalidStandard { parameter })
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::InvalidStandard { parameter })
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::InvalidStandard { parameter: "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 assert_eq!(
1592 PChart::with_center(p).err(),
1593 Some(ControlChartError::InvalidStandard { parameter: "p_bar" }),
1594 "p̄ = {p}"
1595 );
1596 }
1597 assert!(UChart::with_center(0.0).is_err());
1598 let bad_phi = LaneyStandard {
1599 center: 0.1,
1600 phi: -1.0,
1601 };
1602 assert_eq!(
1603 laney_p_chart(&[(1, 10)], Some(bad_phi)).unwrap_err(),
1604 ChartInputError::Standard(ControlChartError::InvalidStandard { parameter: "phi" })
1605 );
1606 let good = LaneyStandard {
1607 center: 0.1,
1608 phi: 1.0,
1609 };
1610 assert_eq!(
1611 laney_u_chart(&[], Some(good)).unwrap_err(),
1612 ChartInputError::TooFewSamples { got: 0, min: 1 }
1613 );
1614 }
1615
1616 #[test]
1617 fn z_is_the_point_on_the_scale_where_every_limit_is_three() {
1618 let mut p = PChart::new();
1621 for (d, n) in [(3, 100), (12, 180), (1, 60), (9, 150), (4, 90)] {
1622 p.add_sample(d, n).unwrap();
1623 }
1624 for pt in p.points() {
1625 let sigma = (pt.ucl - pt.cl) / 3.0;
1626 let z = pt.z.expect("p̄ strictly inside (0, 1)");
1627 assert!((pt.cl + z * sigma - pt.value).abs() < 1e-12);
1628 if pt.lcl > 0.0 {
1629 assert_eq!(pt.out_of_control, z.abs() > 3.0);
1630 }
1631 }
1632 let laney =
1633 laney_p_chart(&[(3, 100), (12, 180), (1, 60), (9, 150), (4, 90)], None).expect("valid");
1634 for pt in &laney.points {
1635 let z = pt.z.expect("φ > 0");
1636 assert!((pt.cl + z * (pt.ucl - pt.cl) / 3.0 - pt.value).abs() < 1e-12);
1637 }
1638 let flat = laney_p_chart(&[(0, 10), (0, 12), (0, 9)], None).expect("valid");
1640 assert!(flat.points.iter().all(|p| p.z.is_none()));
1641 }
1642
1643 #[test]
1644 fn laney_u_rejects_units_by_position() {
1645 assert_eq!(
1646 laney_u_chart(&[(3, 10.0), (4, 10.0), (2, 10.0), (5, 0.0)], None).unwrap_err(),
1647 ChartInputError::Sample {
1648 index: 3,
1649 error: ControlChartError::NonPositiveUnits
1650 }
1651 );
1652 assert_eq!(
1653 laney_u_chart(&[(3, 10.0)], None).unwrap_err(),
1654 ChartInputError::TooFewSamples { got: 1, min: 3 }
1655 );
1656 }
1657 #[test]
1658 fn laney_u_basic() {
1659 let samples: Vec<(u64, f64)> = vec![(5, 10.0); 10];
1660 let chart = laney_u_chart(&samples, None).expect("valid samples");
1661 assert!((chart.u_bar - 0.5).abs() < 1e-10);
1662 assert!(chart.phi >= 0.0);
1663 }
1664
1665 #[test]
1666 fn laney_u_ucl_above_cl() {
1667 let samples: Vec<(u64, f64)> = (0..8).map(|i| ((i % 4 + 2) as u64, 10.0)).collect();
1668 let chart = laney_u_chart(&samples, None).expect("valid samples");
1669 for p in &chart.points {
1670 assert!(p.ucl > p.cl || (p.ucl - p.cl).abs() < 1e-10);
1671 }
1672 }
1673
1674 #[test]
1685 fn p_chart_montgomery_reference_formula() {
1686 let mut chart = PChart::new();
1689 for _ in 0..19 {
1690 chart.add_sample(10, 100).unwrap();
1691 }
1692 chart.add_sample(8, 100).unwrap();
1693
1694 let p_bar = chart.p_bar().expect("p_bar");
1695 assert!(
1696 (p_bar - 0.099).abs() < 1e-10,
1697 "p̄ expected 0.099, got {p_bar}"
1698 );
1699
1700 let sigma = (0.099_f64 * 0.901 / 100.0).sqrt();
1701 let expected_ucl = 0.099 + 3.0 * sigma; let expected_lcl = (0.099 - 3.0 * sigma).max(0.0); for pt in chart.points() {
1705 assert!(
1706 (pt.ucl - expected_ucl).abs() < 1e-6,
1707 "UCL mismatch at index {}: expected {expected_ucl:.6}, got {:.6}",
1708 pt.index,
1709 pt.ucl
1710 );
1711 assert!(
1712 (pt.lcl - expected_lcl).abs() < 1e-6,
1713 "LCL mismatch at index {}: expected {expected_lcl:.6}, got {:.6}",
1714 pt.index,
1715 pt.lcl
1716 );
1717 }
1718 }
1719
1720 #[test]
1729 fn np_chart_montgomery_reference() {
1730 let mut chart = NPChart::new(100).expect("100 is a valid sample size");
1732 for _ in 0..19 {
1733 chart.add_sample(10).unwrap();
1734 }
1735 chart.add_sample(8).unwrap();
1736
1737 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1738 assert!((cl - 9.9).abs() < 1e-10, "NP CL expected 9.9, got {cl}");
1740 let expected_sigma = (9.9_f64 * 0.901).sqrt();
1742 let expected_ucl = 9.9 + 3.0 * expected_sigma; let expected_lcl = (9.9 - 3.0 * expected_sigma).max(0.0); assert!(
1745 (ucl - expected_ucl).abs() < 1e-6,
1746 "NP UCL expected {expected_ucl:.4}, got {ucl:.4}"
1747 );
1748 assert!(
1749 (lcl - expected_lcl).abs() < 1e-6,
1750 "NP LCL expected {expected_lcl:.4}, got {lcl:.4}"
1751 );
1752 }
1753
1754 #[test]
1760 fn c_chart_montgomery_reference() {
1761 let mut chart = CChart::new();
1763 for _ in 0..20 {
1764 chart.add_sample(10);
1765 }
1766
1767 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
1768 assert!(
1769 (cl - 10.0).abs() < 1e-10,
1770 "C chart CL expected 10.0, got {cl}"
1771 );
1772 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!(
1775 (ucl - expected_ucl).abs() < 1e-6,
1776 "C chart UCL expected {expected_ucl:.4}, got {ucl:.4}"
1777 );
1778 assert!(
1779 (lcl - expected_lcl).abs() < 1e-6,
1780 "C chart LCL expected {expected_lcl:.4}, got {lcl:.4}"
1781 );
1782 }
1783
1784 #[test]
1790 fn u_chart_montgomery_reference() {
1791 let mut chart = UChart::new();
1793 for _ in 0..20 {
1794 chart.add_sample(20, 10.0).unwrap(); }
1796
1797 let u_bar = chart.u_bar().expect("u_bar");
1798 assert!(
1799 (u_bar - 2.0).abs() < 1e-10,
1800 "U chart ū expected 2.0, got {u_bar}"
1801 );
1802
1803 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() {
1808 assert!(
1809 (pt.ucl - expected_ucl).abs() < 1e-6,
1810 "U chart UCL expected {expected_ucl:.4}, got {:.4}",
1811 pt.ucl
1812 );
1813 assert!(
1814 (pt.lcl - expected_lcl).abs() < 1e-6,
1815 "U chart LCL expected {expected_lcl:.4}, got {:.4}",
1816 pt.lcl
1817 );
1818 }
1819 }
1820
1821 #[test]
1824 fn g_chart_ucl_above_cl() {
1825 let gaps = vec![100.0, 120.0, 95.0, 110.0, 105.0];
1826 let chart = g_chart(&gaps).expect("valid samples");
1827 assert!(chart.points[0].ucl > chart.points[0].cl);
1828 assert!(chart.points[0].lcl >= 0.0);
1829 }
1830
1831 #[test]
1832 fn g_chart_insufficient() {
1833 assert!(g_chart(&[100.0, 120.0]).is_none());
1834 }
1835
1836 #[test]
1837 fn g_chart_all_same() {
1838 let chart = g_chart(&[50.0; 8]).expect("valid samples");
1839 assert!((chart.g_bar - 50.0).abs() < 1e-10);
1840 assert!(chart.points[0].ucl > chart.points[0].cl);
1841 }
1842
1843 #[test]
1846 fn t_chart_ucl_factor() {
1847 let times = vec![100.0; 10];
1849 let chart = t_chart(×).expect("valid samples");
1850 let ratio = chart.points[0].ucl / chart.t_bar;
1851 assert!((ratio - 6.6077).abs() < 0.01, "ratio={ratio}");
1852 }
1853
1854 #[test]
1855 fn t_chart_non_positive() {
1856 assert!(t_chart(&[10.0, -5.0, 20.0, 15.0]).is_none());
1857 }
1858
1859 #[test]
1860 fn t_chart_insufficient() {
1861 assert!(t_chart(&[10.0, 20.0]).is_none());
1862 }
1863
1864 #[test]
1871 fn laney_p_ucl_formula_invariant() {
1872 let samples: Vec<(u64, u64)> = vec![
1873 (3, 100),
1874 (7, 100),
1875 (2, 100),
1876 (8, 100),
1877 (4, 100),
1878 (5, 150),
1879 (9, 150),
1880 (3, 150),
1881 (6, 150),
1882 (4, 150),
1883 ];
1884 let chart = laney_p_chart(&samples, None).expect("valid samples");
1885
1886 for (i, (&(d, n), pt)) in samples.iter().zip(&chart.points).enumerate() {
1887 let p_i = d as f64 / n as f64;
1888 let sigma_i = (chart.p_bar * (1.0 - chart.p_bar) / n as f64).sqrt();
1889 let expected_ucl = chart.p_bar + 3.0 * chart.phi * sigma_i;
1890 let expected_lcl = (chart.p_bar - 3.0 * chart.phi * sigma_i).max(0.0);
1891
1892 assert!(
1893 (pt.value - p_i).abs() < 1e-10,
1894 "point {i}: value expected {p_i:.6}, got {:.6}",
1895 pt.value
1896 );
1897 assert!(
1898 (pt.ucl - expected_ucl).abs() < 1e-10,
1899 "point {i}: UCL expected {expected_ucl:.6}, got {:.6}",
1900 pt.ucl
1901 );
1902 assert!(
1903 (pt.lcl - expected_lcl).abs() < 1e-10,
1904 "point {i}: LCL expected {expected_lcl:.6}, got {:.6}",
1905 pt.lcl
1906 );
1907 }
1908 }
1909
1910 #[test]
1928 fn laney_p_phi_near_one_limits_close_to_standard() {
1929 let n: u64 = 10_000;
1933 let d_high: u64 = 5028; let d_low: u64 = 4972; let samples: Vec<(u64, u64)> = vec![
1937 (d_high, n),
1938 (d_low, n),
1939 (d_high, n),
1940 (d_low, n),
1941 (d_high, n),
1942 (d_low, n),
1943 ];
1944
1945 let laney = laney_p_chart(&samples, None).expect("valid samples");
1946
1947 assert!(
1949 (laney.phi - 1.0).abs() < 0.01,
1950 "φ expected ≈1.0, got {}",
1951 laney.phi
1952 );
1953
1954 let p_bar = laney.p_bar;
1956 let sigma_std = (p_bar * (1.0 - p_bar) / n as f64).sqrt();
1957 let std_ucl = p_bar + 3.0 * sigma_std;
1958 let std_lcl = (p_bar - 3.0 * sigma_std).max(0.0);
1959
1960 let max_deviation = 3.0 * sigma_std * (laney.phi - 1.0).abs();
1963 assert!(
1964 (laney.points[0].ucl - std_ucl).abs() <= max_deviation + 1e-12,
1965 "Laney UCL={:.6} vs std UCL={std_ucl:.6}, deviation bound={max_deviation:.2e}",
1966 laney.points[0].ucl
1967 );
1968 assert!(
1969 (laney.points[0].lcl - std_lcl).abs() <= max_deviation + 1e-12,
1970 "Laney LCL={:.6} vs std LCL={std_lcl:.6}, deviation bound={max_deviation:.2e}",
1971 laney.points[0].lcl
1972 );
1973 }
1974
1975 #[test]
1982 fn laney_p_phi_gt_one_wider_than_standard() {
1983 let samples: Vec<(u64, u64)> = vec![
1985 (1, 100),
1986 (20, 100),
1987 (2, 100),
1988 (18, 100),
1989 (1, 100),
1990 (22, 100),
1991 (3, 100),
1992 (19, 100),
1993 (2, 100),
1994 (20, 100),
1995 ];
1996
1997 let laney = laney_p_chart(&samples, None).expect("valid samples");
1998 assert!(
1999 laney.phi > 1.0,
2000 "φ expected > 1 for overdispersed data, got {}",
2001 laney.phi
2002 );
2003
2004 let total_d: u64 = samples.iter().map(|&(d, _)| d).sum();
2006 let total_n: u64 = samples.iter().map(|&(_, n)| n).sum();
2007 let p_bar = total_d as f64 / total_n as f64;
2008 let std_ucl_first = p_bar + 3.0 * (p_bar * (1.0 - p_bar) / 100.0_f64).sqrt();
2009
2010 assert!(
2011 laney.points[0].ucl > std_ucl_first,
2012 "Laney UCL ({:.4}) must exceed standard P chart UCL ({std_ucl_first:.4}) when φ>1",
2013 laney.points[0].ucl
2014 );
2015 }
2016
2017 #[test]
2035 fn laney_p_numerical_reference() {
2036 let samples: Vec<(u64, u64)> = vec![(3, 50), (7, 50), (2, 50), (8, 50), (4, 50)];
2037 let chart = laney_p_chart(&samples, None).expect("valid samples");
2038
2039 let p_bar = 24.0_f64 / 250.0;
2040 assert!(
2041 (chart.p_bar - p_bar).abs() < 1e-10,
2042 "p̄ expected {p_bar:.6}, got {:.6}",
2043 chart.p_bar
2044 );
2045
2046 let sigma_i = (p_bar * (1.0 - p_bar) / 50.0_f64).sqrt();
2048 let p_vals = [0.06_f64, 0.14, 0.04, 0.16, 0.08];
2049 let z: Vec<f64> = p_vals.iter().map(|&p| (p - p_bar) / sigma_i).collect();
2050 let mr_bar = z.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>() / 4.0;
2051 let phi_expected = mr_bar / 1.128;
2052
2053 assert!(
2054 (chart.phi - phi_expected).abs() < 1e-10,
2055 "φ expected {phi_expected:.6}, got {:.6}",
2056 chart.phi
2057 );
2058
2059 let ucl_0 = p_bar + 3.0 * phi_expected * sigma_i;
2061 let lcl_0 = (p_bar - 3.0 * phi_expected * sigma_i).max(0.0);
2062 assert!(
2063 (chart.points[0].ucl - ucl_0).abs() < 1e-10,
2064 "UCL[0] expected {ucl_0:.6}, got {:.6}",
2065 chart.points[0].ucl
2066 );
2067 assert!(
2068 (chart.points[0].lcl - lcl_0).abs() < 1e-10,
2069 "LCL[0] expected {lcl_0:.6}, got {:.6}",
2070 chart.points[0].lcl
2071 );
2072 }
2073
2074 #[test]
2078 fn laney_u_phi_gt_one_wider_than_standard() {
2079 let samples: Vec<(u64, f64)> = vec![
2081 (1, 10.0),
2082 (15, 10.0),
2083 (2, 10.0),
2084 (14, 10.0),
2085 (1, 10.0),
2086 (16, 10.0),
2087 (2, 10.0),
2088 (13, 10.0),
2089 ];
2090
2091 let laney = laney_u_chart(&samples, None).expect("valid samples");
2092 assert!(
2093 laney.phi > 1.0,
2094 "φ expected > 1 for overdispersed data, got {}",
2095 laney.phi
2096 );
2097
2098 let total_d: u64 = samples.iter().map(|&(d, _)| d).sum();
2099 let total_n: f64 = samples.iter().map(|&(_, n)| n).sum();
2100 let u_bar = total_d as f64 / total_n;
2101 let std_ucl = u_bar + 3.0 * (u_bar / 10.0_f64).sqrt();
2102
2103 assert!(
2104 laney.points[0].ucl > std_ucl,
2105 "Laney U' UCL ({:.4}) must exceed standard U chart UCL ({std_ucl:.4}) when φ>1",
2106 laney.points[0].ucl
2107 );
2108 }
2109
2110 #[test]
2123 fn g_chart_formula_verification() {
2124 let gaps = vec![50.0_f64; 8];
2126 let chart = g_chart(&gaps).expect("valid samples");
2127
2128 let g_bar = 50.0_f64;
2129 assert!(
2130 (chart.g_bar - g_bar).abs() < 1e-10,
2131 "ḡ expected 50.0, got {}",
2132 chart.g_bar
2133 );
2134
2135 let spread = (g_bar * (g_bar + 1.0)).sqrt(); let expected_ucl = g_bar + 3.0 * spread;
2137 let expected_lcl = (g_bar - 3.0 * spread).max(0.0);
2138
2139 assert!(
2140 (chart.points[0].ucl - expected_ucl).abs() < 1e-10,
2141 "UCL expected {expected_ucl:.6}, got {:.6}",
2142 chart.points[0].ucl
2143 );
2144 assert!(
2145 (chart.points[0].lcl - expected_lcl).abs() < 1e-10,
2146 "LCL expected {expected_lcl:.6}, got {:.6}",
2147 chart.points[0].lcl
2148 );
2149 assert!(
2151 chart.points[0].lcl >= 0.0,
2152 "LCL must be clamped to 0, got {}",
2153 chart.points[0].lcl
2154 );
2155 }
2156
2157 #[test]
2162 fn g_chart_lcl_always_zero() {
2163 for &g in &[1.0_f64, 5.0, 20.0, 100.0, 500.0] {
2164 let gaps = vec![g; 5];
2165 let chart = g_chart(&gaps).expect("valid samples");
2166 assert!(
2167 (chart.points[0].lcl - 0.0).abs() < 1e-10,
2168 "LCL must be 0 for ḡ={g}, got {}",
2169 chart.points[0].lcl
2170 );
2171 }
2172 }
2173
2174 #[test]
2185 fn t_chart_lcl_factor_verification() {
2186 let times = vec![100.0_f64; 10];
2187 let chart = t_chart(×).expect("valid samples");
2188
2189 let ucl_factor = chart.points[0].ucl / chart.t_bar;
2190 let lcl_factor = chart.points[0].lcl / chart.t_bar;
2191
2192 let expected_ucl_factor = -(0.00135_f64.ln()); let expected_lcl_factor = -(0.99865_f64.ln()); assert!(
2196 (ucl_factor - expected_ucl_factor).abs() < 1e-10,
2197 "UCL factor expected {expected_ucl_factor:.6}, got {ucl_factor:.6}"
2198 );
2199 assert!(
2200 (lcl_factor - expected_lcl_factor).abs() < 1e-10,
2201 "LCL factor expected {expected_lcl_factor:.6}, got {lcl_factor:.6}"
2202 );
2203 }
2204
2205 #[test]
2211 fn t_chart_ucl_lcl_ratio_scale_invariant() {
2212 let k_u = -(0.00135_f64.ln());
2213 let k_l = -(0.99865_f64.ln());
2214 let expected_ratio = k_u / k_l;
2215
2216 for &t_bar in &[10.0_f64, 100.0, 1000.0] {
2217 let times = vec![t_bar; 10];
2218 let chart = t_chart(×).expect("valid samples");
2219 let ratio = chart.points[0].ucl / chart.points[0].lcl;
2220 assert!(
2221 (ratio - expected_ratio).abs() < 0.01,
2222 "UCL/LCL ratio expected {expected_ratio:.2}, got {ratio:.2} at t̄={t_bar}"
2223 );
2224 }
2225 }
2226}