1use super::chart::{ChartPoint, ControlChart, ControlLimits, Violation, ViolationType};
21use super::rules::{NelsonRules, RunRule};
22
23const A2: [f64; 9] = [
32 1.880, 1.023, 0.729, 0.577, 0.483, 0.419, 0.373, 0.337, 0.308,
33];
34
35const D3: [f64; 9] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.076, 0.136, 0.184, 0.223];
39
40const D4: [f64; 9] = [
44 3.267, 2.575, 2.282, 2.114, 2.004, 1.924, 1.864, 1.816, 1.777,
45];
46
47#[allow(dead_code)]
51const D2: [f64; 9] = [
52 1.128, 1.693, 2.059, 2.326, 2.534, 2.704, 2.847, 2.970, 3.078,
53];
54
55const A3: [f64; 9] = [
59 2.659, 1.954, 1.628, 1.427, 1.287, 1.182, 1.099, 1.032, 0.975,
60];
61
62const B3: [f64; 9] = [0.0, 0.0, 0.0, 0.0, 0.030, 0.118, 0.185, 0.239, 0.284];
66
67const B4: [f64; 9] = [
71 3.267, 2.568, 2.266, 2.089, 1.970, 1.882, 1.815, 1.761, 1.716,
72];
73
74#[allow(dead_code)]
78const C4: [f64; 9] = [
79 0.7979, 0.8862, 0.9213, 0.9400, 0.9515, 0.9594, 0.9650, 0.9693, 0.9727,
80];
81
82const E2: f64 = 2.660;
87
88const D4_MR: f64 = 3.267;
90
91pub struct XBarRChart {
127 subgroup_size: usize,
129 subgroups: Vec<Vec<f64>>,
131 xbar_points: Vec<ChartPoint>,
133 r_points: Vec<ChartPoint>,
135 xbar_limits: Option<ControlLimits>,
137 r_limits: Option<ControlLimits>,
139}
140
141impl XBarRChart {
142 pub fn new(subgroup_size: usize) -> Self {
148 assert!(
149 (2..=10).contains(&subgroup_size),
150 "subgroup_size must be 2..=10, got {subgroup_size}"
151 );
152 Self {
153 subgroup_size,
154 subgroups: Vec::new(),
155 xbar_points: Vec::new(),
156 r_points: Vec::new(),
157 xbar_limits: None,
158 r_limits: None,
159 }
160 }
161
162 pub fn r_limits(&self) -> Option<ControlLimits> {
164 self.r_limits.clone()
165 }
166
167 pub fn r_points(&self) -> &[ChartPoint] {
169 &self.r_points
170 }
171
172 fn recompute(&mut self) {
174 if self.subgroups.is_empty() {
175 self.xbar_limits = None;
176 self.r_limits = None;
177 self.xbar_points.clear();
178 self.r_points.clear();
179 return;
180 }
181
182 let idx = self.subgroup_size - 2; let mut xbar_values = Vec::with_capacity(self.subgroups.len());
186 let mut r_values = Vec::with_capacity(self.subgroups.len());
187
188 for subgroup in &self.subgroups {
189 let mean_val = u_numflow::stats::mean(subgroup)
190 .expect("subgroup should be non-empty with finite values");
191 let range = subgroup_range(subgroup);
192 xbar_values.push(mean_val);
193 r_values.push(range);
194 }
195
196 let grand_mean = u_numflow::stats::mean(&xbar_values)
198 .expect("xbar_values should be non-empty with finite values");
199 let r_bar = u_numflow::stats::mean(&r_values)
200 .expect("r_values should be non-empty with finite values");
201
202 let a2 = A2[idx];
204 self.xbar_limits = Some(ControlLimits {
205 ucl: grand_mean + a2 * r_bar,
206 cl: grand_mean,
207 lcl: grand_mean - a2 * r_bar,
208 });
209
210 let d3 = D3[idx];
212 let d4 = D4[idx];
213 self.r_limits = Some(ControlLimits {
214 ucl: d4 * r_bar,
215 cl: r_bar,
216 lcl: d3 * r_bar,
217 });
218
219 self.xbar_points = xbar_values
221 .iter()
222 .enumerate()
223 .map(|(i, &v)| ChartPoint {
224 value: v,
225 index: i,
226 violations: Vec::new(),
227 })
228 .collect();
229
230 self.r_points = r_values
231 .iter()
232 .enumerate()
233 .map(|(i, &v)| ChartPoint {
234 value: v,
235 index: i,
236 violations: Vec::new(),
237 })
238 .collect();
239
240 if let Some(ref limits) = self.xbar_limits {
242 let nelson = NelsonRules;
243 let violations = nelson.check(&self.xbar_points, limits);
244 apply_violations(&mut self.xbar_points, &violations);
245 }
246
247 if let Some(ref limits) = self.r_limits {
249 let nelson = NelsonRules;
250 let violations = nelson.check(&self.r_points, limits);
251 apply_violations(&mut self.r_points, &violations);
252 }
253 }
254}
255
256impl ControlChart for XBarRChart {
257 fn add_sample(&mut self, sample: &[f64]) {
259 if sample.len() != self.subgroup_size {
260 return;
261 }
262 if !sample.iter().all(|x| x.is_finite()) {
263 return;
264 }
265 self.subgroups.push(sample.to_vec());
266 self.recompute();
267 }
268
269 fn control_limits(&self) -> Option<ControlLimits> {
270 self.xbar_limits.clone()
271 }
272
273 fn is_in_control(&self) -> bool {
274 self.xbar_points.iter().all(|p| p.violations.is_empty())
275 && self.r_points.iter().all(|p| p.violations.is_empty())
276 }
277
278 fn violations(&self) -> Vec<Violation> {
279 collect_violations(&self.xbar_points)
280 .into_iter()
281 .chain(collect_violations(&self.r_points))
282 .collect()
283 }
284
285 fn points(&self) -> &[ChartPoint] {
286 &self.xbar_points
287 }
288}
289
290pub struct XBarSChart {
312 subgroup_size: usize,
314 subgroups: Vec<Vec<f64>>,
316 xbar_points: Vec<ChartPoint>,
318 s_points: Vec<ChartPoint>,
320 xbar_limits: Option<ControlLimits>,
322 s_limits: Option<ControlLimits>,
324}
325
326impl XBarSChart {
327 pub fn new(subgroup_size: usize) -> Self {
333 assert!(
334 (2..=10).contains(&subgroup_size),
335 "subgroup_size must be 2..=10, got {subgroup_size}"
336 );
337 Self {
338 subgroup_size,
339 subgroups: Vec::new(),
340 xbar_points: Vec::new(),
341 s_points: Vec::new(),
342 xbar_limits: None,
343 s_limits: None,
344 }
345 }
346
347 pub fn s_limits(&self) -> Option<ControlLimits> {
349 self.s_limits.clone()
350 }
351
352 pub fn s_points(&self) -> &[ChartPoint] {
354 &self.s_points
355 }
356
357 fn recompute(&mut self) {
359 if self.subgroups.is_empty() {
360 self.xbar_limits = None;
361 self.s_limits = None;
362 self.xbar_points.clear();
363 self.s_points.clear();
364 return;
365 }
366
367 let idx = self.subgroup_size - 2;
368
369 let mut xbar_values = Vec::with_capacity(self.subgroups.len());
371 let mut s_values = Vec::with_capacity(self.subgroups.len());
372
373 for subgroup in &self.subgroups {
374 let mean_val = u_numflow::stats::mean(subgroup)
375 .expect("subgroup should be non-empty with finite values");
376 let sd = u_numflow::stats::std_dev(subgroup)
377 .expect("subgroup should have >= 2 elements for std_dev");
378 xbar_values.push(mean_val);
379 s_values.push(sd);
380 }
381
382 let grand_mean = u_numflow::stats::mean(&xbar_values)
384 .expect("xbar_values should be non-empty with finite values");
385 let s_bar = u_numflow::stats::mean(&s_values)
386 .expect("s_values should be non-empty with finite values");
387
388 let a3 = A3[idx];
390 self.xbar_limits = Some(ControlLimits {
391 ucl: grand_mean + a3 * s_bar,
392 cl: grand_mean,
393 lcl: grand_mean - a3 * s_bar,
394 });
395
396 let b3 = B3[idx];
398 let b4 = B4[idx];
399 self.s_limits = Some(ControlLimits {
400 ucl: b4 * s_bar,
401 cl: s_bar,
402 lcl: b3 * s_bar,
403 });
404
405 self.xbar_points = xbar_values
407 .iter()
408 .enumerate()
409 .map(|(i, &v)| ChartPoint {
410 value: v,
411 index: i,
412 violations: Vec::new(),
413 })
414 .collect();
415
416 self.s_points = s_values
417 .iter()
418 .enumerate()
419 .map(|(i, &v)| ChartPoint {
420 value: v,
421 index: i,
422 violations: Vec::new(),
423 })
424 .collect();
425
426 if let Some(ref limits) = self.xbar_limits {
428 let nelson = NelsonRules;
429 let violations = nelson.check(&self.xbar_points, limits);
430 apply_violations(&mut self.xbar_points, &violations);
431 }
432
433 if let Some(ref limits) = self.s_limits {
435 let nelson = NelsonRules;
436 let violations = nelson.check(&self.s_points, limits);
437 apply_violations(&mut self.s_points, &violations);
438 }
439 }
440}
441
442impl ControlChart for XBarSChart {
443 fn add_sample(&mut self, sample: &[f64]) {
445 if sample.len() != self.subgroup_size {
446 return;
447 }
448 if !sample.iter().all(|x| x.is_finite()) {
449 return;
450 }
451 self.subgroups.push(sample.to_vec());
452 self.recompute();
453 }
454
455 fn control_limits(&self) -> Option<ControlLimits> {
456 self.xbar_limits.clone()
457 }
458
459 fn is_in_control(&self) -> bool {
460 self.xbar_points.iter().all(|p| p.violations.is_empty())
461 && self.s_points.iter().all(|p| p.violations.is_empty())
462 }
463
464 fn violations(&self) -> Vec<Violation> {
465 collect_violations(&self.xbar_points)
466 .into_iter()
467 .chain(collect_violations(&self.s_points))
468 .collect()
469 }
470
471 fn points(&self) -> &[ChartPoint] {
472 &self.xbar_points
473 }
474}
475
476pub struct IndividualMRChart {
513 observations: Vec<f64>,
515 i_points: Vec<ChartPoint>,
517 mr_points: Vec<ChartPoint>,
519 i_limits: Option<ControlLimits>,
521 mr_limits: Option<ControlLimits>,
523}
524
525impl IndividualMRChart {
526 pub fn new() -> Self {
528 Self {
529 observations: Vec::new(),
530 i_points: Vec::new(),
531 mr_points: Vec::new(),
532 i_limits: None,
533 mr_limits: None,
534 }
535 }
536
537 pub fn mr_limits(&self) -> Option<ControlLimits> {
539 self.mr_limits.clone()
540 }
541
542 pub fn mr_points(&self) -> &[ChartPoint] {
544 &self.mr_points
545 }
546
547 fn recompute(&mut self) {
549 if self.observations.len() < 2 {
550 self.i_limits = None;
551 self.mr_limits = None;
552 self.i_points.clear();
553 self.mr_points.clear();
554 return;
555 }
556
557 let mr_values: Vec<f64> = self
559 .observations
560 .windows(2)
561 .map(|w| (w[1] - w[0]).abs())
562 .collect();
563
564 let x_bar = u_numflow::stats::mean(&self.observations)
566 .expect("observations should be non-empty with finite values");
567 let mr_bar = u_numflow::stats::mean(&mr_values)
568 .expect("mr_values should be non-empty with finite values");
569
570 self.i_limits = Some(ControlLimits {
572 ucl: x_bar + E2 * mr_bar,
573 cl: x_bar,
574 lcl: x_bar - E2 * mr_bar,
575 });
576
577 self.mr_limits = Some(ControlLimits {
579 ucl: D4_MR * mr_bar,
580 cl: mr_bar,
581 lcl: 0.0,
582 });
583
584 self.i_points = self
586 .observations
587 .iter()
588 .enumerate()
589 .map(|(i, &v)| ChartPoint {
590 value: v,
591 index: i,
592 violations: Vec::new(),
593 })
594 .collect();
595
596 self.mr_points = mr_values
598 .iter()
599 .enumerate()
600 .map(|(i, &v)| ChartPoint {
601 value: v,
602 index: i + 1,
603 violations: Vec::new(),
604 })
605 .collect();
606
607 if let Some(ref limits) = self.i_limits {
609 let nelson = NelsonRules;
610 let violations = nelson.check(&self.i_points, limits);
611 apply_violations(&mut self.i_points, &violations);
612 }
613
614 if let Some(ref limits) = self.mr_limits {
616 let nelson = NelsonRules;
617 let violations = nelson.check(&self.mr_points, limits);
618 apply_violations(&mut self.mr_points, &violations);
619 }
620 }
621}
622
623impl Default for IndividualMRChart {
624 fn default() -> Self {
625 Self::new()
626 }
627}
628
629impl ControlChart for IndividualMRChart {
630 fn add_sample(&mut self, sample: &[f64]) {
632 if sample.len() != 1 {
633 return;
634 }
635 if !sample[0].is_finite() {
636 return;
637 }
638 self.observations.push(sample[0]);
639 self.recompute();
640 }
641
642 fn control_limits(&self) -> Option<ControlLimits> {
643 self.i_limits.clone()
644 }
645
646 fn is_in_control(&self) -> bool {
647 self.i_points.iter().all(|p| p.violations.is_empty())
648 && self.mr_points.iter().all(|p| p.violations.is_empty())
649 }
650
651 fn violations(&self) -> Vec<Violation> {
652 collect_violations(&self.i_points)
653 .into_iter()
654 .chain(collect_violations(&self.mr_points))
655 .collect()
656 }
657
658 fn points(&self) -> &[ChartPoint] {
659 &self.i_points
660 }
661}
662
663fn subgroup_range(subgroup: &[f64]) -> f64 {
673 let max_val =
674 u_numflow::stats::max(subgroup).expect("subgroup should be non-empty without NaN");
675 let min_val =
676 u_numflow::stats::min(subgroup).expect("subgroup should be non-empty without NaN");
677 max_val - min_val
678}
679
680fn apply_violations(points: &mut [ChartPoint], violations: &[(usize, ViolationType)]) {
682 for &(idx, vtype) in violations {
683 if let Some(point) = points.iter_mut().find(|p| p.index == idx) {
684 point.violations.push(vtype);
685 }
686 }
687}
688
689fn collect_violations(points: &[ChartPoint]) -> Vec<Violation> {
691 let mut result = Vec::new();
692 for point in points {
693 for &vtype in &point.violations {
694 result.push(Violation {
695 point_index: point.index,
696 violation_type: vtype,
697 });
698 }
699 }
700 result
701}
702
703#[cfg(test)]
708mod tests {
709 use super::*;
710
711 #[test]
714 fn test_xbar_r_basic_limits() {
715 let mut chart = XBarRChart::new(4);
716 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]);
717 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]);
718 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]);
719 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]);
720 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]);
721
722 let limits = chart.control_limits().expect("should have limits");
723 let expected_grand_mean = (71.0 + 54.5 + 52.5 + 63.0 + 57.25) / 5.0;
725 assert!(
726 (limits.cl - expected_grand_mean).abs() < 0.1,
727 "CL={}, expected ~{expected_grand_mean}",
728 limits.cl
729 );
730
731 assert!(limits.ucl > limits.cl);
733 assert!(limits.cl > limits.lcl);
734 }
735
736 #[test]
737 fn test_xbar_r_rejects_wrong_size() {
738 let mut chart = XBarRChart::new(5);
739 chart.add_sample(&[1.0, 2.0, 3.0]); assert!(chart.control_limits().is_none());
741 }
742
743 #[test]
744 fn test_xbar_r_rejects_nan() {
745 let mut chart = XBarRChart::new(3);
746 chart.add_sample(&[1.0, f64::NAN, 3.0]);
747 assert!(chart.control_limits().is_none());
748 }
749
750 #[test]
751 fn test_xbar_r_r_chart_limits() {
752 let mut chart = XBarRChart::new(5);
753 chart.add_sample(&[10.0, 12.0, 11.0, 13.0, 14.0]);
754 chart.add_sample(&[11.0, 13.0, 12.0, 10.0, 15.0]);
755 chart.add_sample(&[12.0, 11.0, 14.0, 13.0, 10.0]);
756
757 let r_limits = chart.r_limits().expect("should have R limits");
758 assert!(r_limits.ucl > r_limits.cl);
759 assert!(r_limits.lcl >= 0.0);
760 }
761
762 #[test]
763 fn test_xbar_r_constant_subgroups() {
764 let mut chart = XBarRChart::new(3);
766 chart.add_sample(&[10.0, 10.0, 10.0]);
767 chart.add_sample(&[10.0, 10.0, 10.0]);
768
769 let limits = chart.control_limits().expect("should have limits");
770 assert!((limits.cl - 10.0).abs() < f64::EPSILON);
771 assert!((limits.ucl - 10.0).abs() < f64::EPSILON);
772 assert!((limits.lcl - 10.0).abs() < f64::EPSILON);
773 }
774
775 #[test]
776 fn test_xbar_r_detects_out_of_control() {
777 let mut chart = XBarRChart::new(3);
778 for _ in 0..5 {
779 chart.add_sample(&[10.0, 10.5, 9.5]);
780 }
781 chart.add_sample(&[50.0, 51.0, 49.0]);
783
784 assert!(!chart.is_in_control());
785 }
786
787 #[test]
788 #[should_panic(expected = "subgroup_size must be 2..=10")]
789 fn test_xbar_r_invalid_size_1() {
790 let _ = XBarRChart::new(1);
791 }
792
793 #[test]
794 #[should_panic(expected = "subgroup_size must be 2..=10")]
795 fn test_xbar_r_invalid_size_11() {
796 let _ = XBarRChart::new(11);
797 }
798
799 #[test]
802 fn test_xbar_s_basic_limits() {
803 let mut chart = XBarSChart::new(4);
804 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]);
805 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]);
806 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]);
807 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]);
808 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]);
809
810 let limits = chart.control_limits().expect("should have limits");
811 assert!(limits.ucl > limits.cl);
812 assert!(limits.cl > limits.lcl);
813
814 let s_limits = chart.s_limits().expect("should have S limits");
815 assert!(s_limits.ucl > s_limits.cl);
816 assert!(s_limits.lcl >= 0.0);
817 }
818
819 #[test]
820 fn test_xbar_s_rejects_wrong_size() {
821 let mut chart = XBarSChart::new(5);
822 chart.add_sample(&[1.0, 2.0]);
823 assert!(chart.control_limits().is_none());
824 }
825
826 #[test]
827 fn test_xbar_s_in_control() {
828 let mut chart = XBarSChart::new(4);
829 for _ in 0..10 {
830 chart.add_sample(&[10.0, 10.2, 9.8, 10.1]);
831 }
832 assert!(chart.is_in_control());
833 }
834
835 #[test]
838 fn test_imr_basic_limits() {
839 let mut chart = IndividualMRChart::new();
840 let data = [10.0, 12.0, 11.0, 13.0, 10.0, 14.0, 11.0, 12.0, 13.0, 10.0];
841 for &x in &data {
842 chart.add_sample(&[x]);
843 }
844
845 let limits = chart.control_limits().expect("should have limits");
846 assert!(limits.ucl > limits.cl);
847 assert!(limits.cl > limits.lcl);
848
849 let mr_limits = chart.mr_limits().expect("should have MR limits");
850 assert!(mr_limits.ucl > mr_limits.cl);
851 assert!((mr_limits.lcl).abs() < f64::EPSILON);
852 }
853
854 #[test]
855 fn test_imr_needs_two_points() {
856 let mut chart = IndividualMRChart::new();
857 chart.add_sample(&[10.0]);
858 assert!(chart.control_limits().is_none());
859 }
860
861 #[test]
862 fn test_imr_center_line_is_mean() {
863 let mut chart = IndividualMRChart::new();
864 let data = [5.0, 10.0, 15.0, 20.0, 25.0];
865 for &x in &data {
866 chart.add_sample(&[x]);
867 }
868 let limits = chart.control_limits().expect("should have limits");
869 assert!((limits.cl - 15.0).abs() < f64::EPSILON);
870 }
871
872 #[test]
873 fn test_imr_mr_values() {
874 let mut chart = IndividualMRChart::new();
875 let data = [10.0, 12.0, 9.0];
876 for &x in &data {
877 chart.add_sample(&[x]);
878 }
879 let mr_pts = chart.mr_points();
881 assert_eq!(mr_pts.len(), 2);
882 assert!((mr_pts[0].value - 2.0).abs() < f64::EPSILON);
883 assert!((mr_pts[1].value - 3.0).abs() < f64::EPSILON);
884 }
885
886 #[test]
887 fn test_imr_rejects_multi_element_sample() {
888 let mut chart = IndividualMRChart::new();
889 chart.add_sample(&[1.0, 2.0]);
890 assert!(chart.points().is_empty());
891 }
892
893 #[test]
894 fn test_imr_detects_out_of_control() {
895 let mut chart = IndividualMRChart::new();
896 for i in 0..10 {
897 chart.add_sample(&[50.0 + (i as f64 % 3.0) * 0.5]);
898 }
899 chart.add_sample(&[100.0]);
901
902 assert!(!chart.is_in_control());
903 }
904
905 #[test]
906 fn test_imr_default() {
907 let chart = IndividualMRChart::default();
908 assert!(chart.points().is_empty());
909 }
910
911 #[test]
914 fn test_subgroup_range() {
915 assert!((subgroup_range(&[1.0, 5.0, 3.0]) - 4.0).abs() < f64::EPSILON);
916 assert!((subgroup_range(&[10.0, 10.0, 10.0])).abs() < f64::EPSILON);
917 }
918
919 #[test]
922 fn test_xbar_r_chart_factors_n5() {
923 let mut chart = XBarRChart::new(5);
926 chart.add_sample(&[45.0, 47.0, 50.0, 53.0, 55.0]);
927
928 let limits = chart.control_limits().expect("limits");
929 assert!((limits.cl - 50.0).abs() < f64::EPSILON);
930
931 let r_limits = chart.r_limits().expect("R limits");
932 assert!((r_limits.cl - 10.0).abs() < f64::EPSILON);
933
934 assert!((limits.ucl - 55.77).abs() < 0.01);
936 assert!((limits.lcl - 44.23).abs() < 0.01);
938 }
939
940 #[test]
943 fn test_imr_e2_factor() {
944 let mut chart = IndividualMRChart::new();
947 chart.add_sample(&[95.0]);
948 chart.add_sample(&[105.0]);
949
950 let limits = chart.control_limits().expect("limits");
951 assert!((limits.cl - 100.0).abs() < f64::EPSILON);
953 assert!((limits.ucl - 126.6).abs() < 0.1);
956 assert!((limits.lcl - 73.4).abs() < 0.1);
958 }
959}