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#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
494 fn test_p_chart_basic() {
495 let mut chart = PChart::new();
497 let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
498 for &d in &defectives {
499 chart.add_sample(d, 100);
500 }
501
502 let p_bar = chart.p_bar().expect("should have p_bar");
503 assert!(
505 (p_bar - 0.055).abs() < 1e-10,
506 "p_bar={p_bar}, expected 0.055"
507 );
508
509 assert_eq!(chart.points().len(), 10);
511
512 for pt in chart.points() {
514 assert!((pt.cl - 0.055).abs() < 1e-10);
515 }
516 }
517
518 #[test]
519 fn test_p_chart_limits() {
520 let mut chart = PChart::new();
521 chart.add_sample(10, 100);
526
527 let pt = &chart.points()[0];
528 assert!((pt.cl - 0.1).abs() < 1e-10);
529 assert!((pt.ucl - 0.19).abs() < 0.001);
530 assert!((pt.lcl - 0.01).abs() < 0.001);
531 }
532
533 #[test]
534 fn test_p_chart_variable_sample_sizes() {
535 let mut chart = PChart::new();
536 chart.add_sample(5, 100);
537 chart.add_sample(10, 200);
538 chart.add_sample(3, 50);
539
540 let p_bar = chart.p_bar().expect("p_bar");
542 assert!((p_bar - 18.0 / 350.0).abs() < 1e-10);
543
544 let pts = chart.points();
546 assert!(pts[1].ucl - pts[1].cl < pts[0].ucl - pts[0].cl);
548 }
549
550 #[test]
551 fn test_p_chart_rejects_invalid() {
552 let mut chart = PChart::new();
553 chart.add_sample(5, 0); assert!(chart.p_bar().is_none());
555
556 chart.add_sample(10, 5); assert!(chart.p_bar().is_none());
558 }
559
560 #[test]
561 fn test_p_chart_lcl_clamped_to_zero() {
562 let mut chart = PChart::new();
563 chart.add_sample(1, 10);
565 let pt = &chart.points()[0];
566 assert!(pt.lcl >= 0.0);
567 }
568
569 #[test]
570 fn test_p_chart_out_of_control() {
571 let mut chart = PChart::new();
572 for _ in 0..20 {
574 chart.add_sample(5, 100);
575 }
576 chart.add_sample(30, 100);
578
579 assert!(!chart.is_in_control());
580 let last = chart.points().last().expect("should have points");
581 assert!(last.out_of_control);
582 }
583
584 #[test]
585 fn test_p_chart_default() {
586 let chart = PChart::default();
587 assert!(chart.p_bar().is_none());
588 assert!(chart.points().is_empty());
589 }
590
591 #[test]
594 fn test_np_chart_basic() {
595 let mut chart = NPChart::new(100);
596 let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
597 for &d in &defectives {
598 chart.add_sample(d);
599 }
600
601 let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
602 assert!((cl - 5.5).abs() < 1e-10);
604 assert!(ucl > cl);
605 assert!(lcl < cl);
606 assert!(lcl >= 0.0);
607 }
608
609 #[test]
610 fn test_np_chart_rejects_invalid() {
611 let mut chart = NPChart::new(100);
612 chart.add_sample(101); assert!(chart.control_limits().is_none());
614 }
615
616 #[test]
617 #[should_panic(expected = "sample_size must be > 0")]
618 fn test_np_chart_zero_sample_size() {
619 let _ = NPChart::new(0);
620 }
621
622 #[test]
623 fn test_np_chart_out_of_control() {
624 let mut chart = NPChart::new(100);
625 for _ in 0..20 {
626 chart.add_sample(5);
627 }
628 chart.add_sample(30);
629
630 assert!(!chart.is_in_control());
631 }
632
633 #[test]
634 fn test_np_chart_limits_formula() {
635 let mut chart = NPChart::new(200);
640 for _ in 0..10 {
641 chart.add_sample(10);
642 }
643
644 let (ucl, cl, lcl) = chart.control_limits().expect("limits");
645 assert!((cl - 10.0).abs() < 1e-10);
646 let expected_sigma = (200.0_f64 * 0.05 * 0.95).sqrt();
647 assert!((ucl - (10.0 + 3.0 * expected_sigma)).abs() < 0.01);
648 assert!((lcl - (10.0 - 3.0 * expected_sigma)).abs() < 0.01);
649 }
650
651 #[test]
654 fn test_c_chart_basic() {
655 let mut chart = CChart::new();
656 let counts = [3, 5, 4, 6, 2, 7, 3, 4, 5, 6];
657 for &c in &counts {
658 chart.add_sample(c);
659 }
660
661 let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
662 assert!((cl - 4.5).abs() < 1e-10);
664 let expected_ucl = 4.5 + 3.0 * 4.5_f64.sqrt();
666 assert!((ucl - expected_ucl).abs() < 0.01);
667 assert!(lcl >= 0.0);
668 }
669
670 #[test]
671 fn test_c_chart_out_of_control() {
672 let mut chart = CChart::new();
673 for _ in 0..20 {
674 chart.add_sample(5);
675 }
676 chart.add_sample(50); assert!(!chart.is_in_control());
679 }
680
681 #[test]
682 fn test_c_chart_single_sample() {
683 let mut chart = CChart::new();
684 chart.add_sample(10);
685
686 let (_, cl, _) = chart.control_limits().expect("limits");
687 assert!((cl - 10.0).abs() < f64::EPSILON);
688 }
689
690 #[test]
691 fn test_c_chart_lcl_clamped() {
692 let mut chart = CChart::new();
694 chart.add_sample(1);
695
696 let (_, _, lcl) = chart.control_limits().expect("limits");
697 assert!((lcl - 0.0).abs() < f64::EPSILON);
698 }
699
700 #[test]
701 fn test_c_chart_default() {
702 let chart = CChart::default();
703 assert!(chart.control_limits().is_none());
704 assert!(chart.points().is_empty());
705 }
706
707 #[test]
710 fn test_u_chart_basic() {
711 let mut chart = UChart::new();
712 chart.add_sample(3, 10.0);
714 chart.add_sample(5, 10.0);
715 chart.add_sample(4, 10.0);
716 chart.add_sample(6, 10.0);
717 chart.add_sample(2, 10.0);
718
719 let u_bar = chart.u_bar().expect("should have u_bar");
720 assert!((u_bar - 0.4).abs() < 1e-10);
722
723 assert_eq!(chart.points().len(), 5);
724 }
725
726 #[test]
727 fn test_u_chart_variable_units() {
728 let mut chart = UChart::new();
729 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");
734 assert!((u_bar - 2.0).abs() < 1e-10);
736
737 let pts = chart.points();
739 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");
742 }
743
744 #[test]
745 fn test_u_chart_rejects_invalid() {
746 let mut chart = UChart::new();
747 chart.add_sample(5, 0.0); assert!(chart.u_bar().is_none());
749
750 chart.add_sample(5, -1.0); assert!(chart.u_bar().is_none());
752
753 chart.add_sample(5, f64::NAN); assert!(chart.u_bar().is_none());
755
756 chart.add_sample(5, f64::INFINITY); assert!(chart.u_bar().is_none());
758 }
759
760 #[test]
761 fn test_u_chart_out_of_control() {
762 let mut chart = UChart::new();
763 for _ in 0..20 {
764 chart.add_sample(4, 10.0);
765 }
766 chart.add_sample(50, 10.0); assert!(!chart.is_in_control());
769 }
770
771 #[test]
772 fn test_u_chart_lcl_clamped() {
773 let mut chart = UChart::new();
774 chart.add_sample(1, 1.0);
776 let pt = &chart.points()[0];
777 assert!(pt.lcl >= 0.0);
778 }
779
780 #[test]
781 fn test_u_chart_default() {
782 let chart = UChart::default();
783 assert!(chart.u_bar().is_none());
784 assert!(chart.points().is_empty());
785 }
786
787 #[test]
788 fn test_u_chart_limits_formula() {
789 let mut chart = UChart::new();
794 chart.add_sample(8, 4.0);
795
796 let pt = &chart.points()[0];
797 assert!((pt.cl - 2.0).abs() < 1e-10);
798 let expected_sigma = (2.0_f64 / 4.0).sqrt();
799 assert!((pt.ucl - (2.0 + 3.0 * expected_sigma)).abs() < 0.001);
800 }
801
802 #[test]
805 fn test_p_and_np_consistent() {
806 let mut p_chart = PChart::new();
808 let mut np_chart = NPChart::new(100);
809
810 let defectives = [5, 8, 3, 6, 4];
811 for &d in &defectives {
812 p_chart.add_sample(d, 100);
813 np_chart.add_sample(d);
814 }
815
816 let p_bar = p_chart.p_bar().expect("p_bar");
817 let (_, np_cl, _) = np_chart.control_limits().expect("np limits");
818
819 assert!(
821 (np_cl - 100.0 * p_bar).abs() < 1e-10,
822 "NP CL should equal n * p_bar"
823 );
824 }
825
826 #[test]
827 fn test_c_and_u_consistent_equal_units() {
828 let mut c_chart = CChart::new();
830 let mut u_chart = UChart::new();
831
832 let defects = [3, 5, 4, 6, 2];
833 for &d in &defects {
834 c_chart.add_sample(d);
835 u_chart.add_sample(d, 1.0);
836 }
837
838 let (c_ucl, c_cl, c_lcl) = c_chart.control_limits().expect("C limits");
839 let u_bar = u_chart.u_bar().expect("u_bar");
840
841 assert!(
842 (c_cl - u_bar).abs() < 1e-10,
843 "C chart CL should equal U chart u-bar when n=1"
844 );
845
846 let u_pt = &u_chart.points()[0];
847 assert!((u_pt.ucl - c_ucl).abs() < 1e-10);
848 assert!((u_pt.lcl - c_lcl).abs() < 1e-10);
849 }
850}