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)]
58const D2: [f64; 9] = [
59 1.128, 1.693, 2.059, 2.326, 2.534, 2.704, 2.847, 2.970, 3.078,
60];
61
62const A3: [f64; 9] = [
66 2.659, 1.954, 1.628, 1.427, 1.287, 1.182, 1.099, 1.032, 0.975,
67];
68
69const B3: [f64; 9] = [0.0, 0.0, 0.0, 0.0, 0.030, 0.118, 0.185, 0.239, 0.284];
73
74const B4: [f64; 9] = [
78 3.267, 2.568, 2.266, 2.089, 1.970, 1.882, 1.815, 1.761, 1.716,
79];
80
81#[allow(dead_code)]
92const C4: [f64; 9] = [
93 0.7979, 0.8862, 0.9213, 0.9400, 0.9515, 0.9594, 0.9650, 0.9693, 0.9727,
94];
95
96const E2: f64 = 2.660;
101
102const D4_MR: f64 = 3.267;
104
105pub struct XBarRChart {
141 subgroup_size: usize,
143 subgroups: Vec<Vec<f64>>,
145 xbar_points: Vec<ChartPoint>,
147 r_points: Vec<ChartPoint>,
149 xbar_limits: Option<ControlLimits>,
151 r_limits: Option<ControlLimits>,
153}
154
155impl XBarRChart {
156 pub fn new(subgroup_size: usize) -> Self {
162 assert!(
163 (2..=10).contains(&subgroup_size),
164 "subgroup_size must be 2..=10, got {subgroup_size}"
165 );
166 Self {
167 subgroup_size,
168 subgroups: Vec::new(),
169 xbar_points: Vec::new(),
170 r_points: Vec::new(),
171 xbar_limits: None,
172 r_limits: None,
173 }
174 }
175
176 pub fn r_limits(&self) -> Option<ControlLimits> {
178 self.r_limits.clone()
179 }
180
181 pub fn r_points(&self) -> &[ChartPoint] {
183 &self.r_points
184 }
185
186 fn recompute(&mut self) {
188 if self.subgroups.is_empty() {
189 self.xbar_limits = None;
190 self.r_limits = None;
191 self.xbar_points.clear();
192 self.r_points.clear();
193 return;
194 }
195
196 let idx = self.subgroup_size - 2; let mut xbar_values = Vec::with_capacity(self.subgroups.len());
200 let mut r_values = Vec::with_capacity(self.subgroups.len());
201
202 for subgroup in &self.subgroups {
203 let mean_val = u_numflow::stats::mean(subgroup)
204 .expect("subgroup should be non-empty with finite values");
205 let range = subgroup_range(subgroup);
206 xbar_values.push(mean_val);
207 r_values.push(range);
208 }
209
210 let grand_mean = u_numflow::stats::mean(&xbar_values)
212 .expect("xbar_values should be non-empty with finite values");
213 let r_bar = u_numflow::stats::mean(&r_values)
214 .expect("r_values should be non-empty with finite values");
215
216 let a2 = A2[idx];
218 self.xbar_limits = Some(ControlLimits {
219 ucl: grand_mean + a2 * r_bar,
220 cl: grand_mean,
221 lcl: grand_mean - a2 * r_bar,
222 });
223
224 let d3 = D3[idx];
226 let d4 = D4[idx];
227 self.r_limits = Some(ControlLimits {
228 ucl: d4 * r_bar,
229 cl: r_bar,
230 lcl: d3 * r_bar,
231 });
232
233 self.xbar_points = xbar_values
235 .iter()
236 .enumerate()
237 .map(|(i, &v)| ChartPoint {
238 value: v,
239 index: i,
240 violations: Vec::new(),
241 })
242 .collect();
243
244 self.r_points = r_values
245 .iter()
246 .enumerate()
247 .map(|(i, &v)| ChartPoint {
248 value: v,
249 index: i,
250 violations: Vec::new(),
251 })
252 .collect();
253
254 if let Some(ref limits) = self.xbar_limits {
256 let nelson = NelsonRules;
257 let violations = nelson.check(&self.xbar_points, limits);
258 apply_violations(&mut self.xbar_points, &violations);
259 }
260
261 if let Some(ref limits) = self.r_limits {
263 let nelson = NelsonRules;
264 let violations = nelson.check(&self.r_points, limits);
265 apply_violations(&mut self.r_points, &violations);
266 }
267 }
268}
269
270impl ControlChart for XBarRChart {
271 fn add_sample(&mut self, sample: &[f64]) {
273 if sample.len() != self.subgroup_size {
274 return;
275 }
276 if !sample.iter().all(|x| x.is_finite()) {
277 return;
278 }
279 self.subgroups.push(sample.to_vec());
280 self.recompute();
281 }
282
283 fn control_limits(&self) -> Option<ControlLimits> {
284 self.xbar_limits.clone()
285 }
286
287 fn is_in_control(&self) -> bool {
288 self.xbar_points.iter().all(|p| p.violations.is_empty())
289 && self.r_points.iter().all(|p| p.violations.is_empty())
290 }
291
292 fn violations(&self) -> Vec<Violation> {
293 collect_violations(&self.xbar_points)
294 .into_iter()
295 .chain(collect_violations(&self.r_points))
296 .collect()
297 }
298
299 fn points(&self) -> &[ChartPoint] {
300 &self.xbar_points
301 }
302}
303
304pub struct XBarSChart {
326 subgroup_size: usize,
328 subgroups: Vec<Vec<f64>>,
330 xbar_points: Vec<ChartPoint>,
332 s_points: Vec<ChartPoint>,
334 xbar_limits: Option<ControlLimits>,
336 s_limits: Option<ControlLimits>,
338}
339
340impl XBarSChart {
341 pub fn new(subgroup_size: usize) -> Self {
347 assert!(
348 (2..=10).contains(&subgroup_size),
349 "subgroup_size must be 2..=10, got {subgroup_size}"
350 );
351 Self {
352 subgroup_size,
353 subgroups: Vec::new(),
354 xbar_points: Vec::new(),
355 s_points: Vec::new(),
356 xbar_limits: None,
357 s_limits: None,
358 }
359 }
360
361 pub fn s_limits(&self) -> Option<ControlLimits> {
363 self.s_limits.clone()
364 }
365
366 pub fn s_points(&self) -> &[ChartPoint] {
368 &self.s_points
369 }
370
371 fn recompute(&mut self) {
373 if self.subgroups.is_empty() {
374 self.xbar_limits = None;
375 self.s_limits = None;
376 self.xbar_points.clear();
377 self.s_points.clear();
378 return;
379 }
380
381 let idx = self.subgroup_size - 2;
382
383 let mut xbar_values = Vec::with_capacity(self.subgroups.len());
385 let mut s_values = Vec::with_capacity(self.subgroups.len());
386
387 for subgroup in &self.subgroups {
388 let mean_val = u_numflow::stats::mean(subgroup)
389 .expect("subgroup should be non-empty with finite values");
390 let sd = u_numflow::stats::std_dev(subgroup)
391 .expect("subgroup should have >= 2 elements for std_dev");
392 xbar_values.push(mean_val);
393 s_values.push(sd);
394 }
395
396 let grand_mean = u_numflow::stats::mean(&xbar_values)
398 .expect("xbar_values should be non-empty with finite values");
399 let s_bar = u_numflow::stats::mean(&s_values)
400 .expect("s_values should be non-empty with finite values");
401
402 let a3 = A3[idx];
404 self.xbar_limits = Some(ControlLimits {
405 ucl: grand_mean + a3 * s_bar,
406 cl: grand_mean,
407 lcl: grand_mean - a3 * s_bar,
408 });
409
410 let b3 = B3[idx];
412 let b4 = B4[idx];
413 self.s_limits = Some(ControlLimits {
414 ucl: b4 * s_bar,
415 cl: s_bar,
416 lcl: b3 * s_bar,
417 });
418
419 self.xbar_points = xbar_values
421 .iter()
422 .enumerate()
423 .map(|(i, &v)| ChartPoint {
424 value: v,
425 index: i,
426 violations: Vec::new(),
427 })
428 .collect();
429
430 self.s_points = s_values
431 .iter()
432 .enumerate()
433 .map(|(i, &v)| ChartPoint {
434 value: v,
435 index: i,
436 violations: Vec::new(),
437 })
438 .collect();
439
440 if let Some(ref limits) = self.xbar_limits {
442 let nelson = NelsonRules;
443 let violations = nelson.check(&self.xbar_points, limits);
444 apply_violations(&mut self.xbar_points, &violations);
445 }
446
447 if let Some(ref limits) = self.s_limits {
449 let nelson = NelsonRules;
450 let violations = nelson.check(&self.s_points, limits);
451 apply_violations(&mut self.s_points, &violations);
452 }
453 }
454}
455
456impl ControlChart for XBarSChart {
457 fn add_sample(&mut self, sample: &[f64]) {
459 if sample.len() != self.subgroup_size {
460 return;
461 }
462 if !sample.iter().all(|x| x.is_finite()) {
463 return;
464 }
465 self.subgroups.push(sample.to_vec());
466 self.recompute();
467 }
468
469 fn control_limits(&self) -> Option<ControlLimits> {
470 self.xbar_limits.clone()
471 }
472
473 fn is_in_control(&self) -> bool {
474 self.xbar_points.iter().all(|p| p.violations.is_empty())
475 && self.s_points.iter().all(|p| p.violations.is_empty())
476 }
477
478 fn violations(&self) -> Vec<Violation> {
479 collect_violations(&self.xbar_points)
480 .into_iter()
481 .chain(collect_violations(&self.s_points))
482 .collect()
483 }
484
485 fn points(&self) -> &[ChartPoint] {
486 &self.xbar_points
487 }
488}
489
490pub struct IndividualMRChart {
527 observations: Vec<f64>,
529 i_points: Vec<ChartPoint>,
531 mr_points: Vec<ChartPoint>,
533 i_limits: Option<ControlLimits>,
535 mr_limits: Option<ControlLimits>,
537}
538
539impl IndividualMRChart {
540 pub fn new() -> Self {
542 Self {
543 observations: Vec::new(),
544 i_points: Vec::new(),
545 mr_points: Vec::new(),
546 i_limits: None,
547 mr_limits: None,
548 }
549 }
550
551 pub fn mr_limits(&self) -> Option<ControlLimits> {
553 self.mr_limits.clone()
554 }
555
556 pub fn mr_points(&self) -> &[ChartPoint] {
558 &self.mr_points
559 }
560
561 fn recompute(&mut self) {
563 if self.observations.len() < 2 {
564 self.i_limits = None;
565 self.mr_limits = None;
566 self.i_points.clear();
567 self.mr_points.clear();
568 return;
569 }
570
571 let mr_values: Vec<f64> = self
573 .observations
574 .windows(2)
575 .map(|w| (w[1] - w[0]).abs())
576 .collect();
577
578 let x_bar = u_numflow::stats::mean(&self.observations)
580 .expect("observations should be non-empty with finite values");
581 let mr_bar = u_numflow::stats::mean(&mr_values)
582 .expect("mr_values should be non-empty with finite values");
583
584 self.i_limits = Some(ControlLimits {
586 ucl: x_bar + E2 * mr_bar,
587 cl: x_bar,
588 lcl: x_bar - E2 * mr_bar,
589 });
590
591 self.mr_limits = Some(ControlLimits {
593 ucl: D4_MR * mr_bar,
594 cl: mr_bar,
595 lcl: 0.0,
596 });
597
598 self.i_points = self
600 .observations
601 .iter()
602 .enumerate()
603 .map(|(i, &v)| ChartPoint {
604 value: v,
605 index: i,
606 violations: Vec::new(),
607 })
608 .collect();
609
610 self.mr_points = mr_values
612 .iter()
613 .enumerate()
614 .map(|(i, &v)| ChartPoint {
615 value: v,
616 index: i + 1,
617 violations: Vec::new(),
618 })
619 .collect();
620
621 if let Some(ref limits) = self.i_limits {
623 let nelson = NelsonRules;
624 let violations = nelson.check(&self.i_points, limits);
625 apply_violations(&mut self.i_points, &violations);
626 }
627
628 if let Some(ref limits) = self.mr_limits {
630 let nelson = NelsonRules;
631 let violations = nelson.check(&self.mr_points, limits);
632 apply_violations(&mut self.mr_points, &violations);
633 }
634 }
635}
636
637impl Default for IndividualMRChart {
638 fn default() -> Self {
639 Self::new()
640 }
641}
642
643impl ControlChart for IndividualMRChart {
644 fn add_sample(&mut self, sample: &[f64]) {
646 if sample.len() != 1 {
647 return;
648 }
649 if !sample[0].is_finite() {
650 return;
651 }
652 self.observations.push(sample[0]);
653 self.recompute();
654 }
655
656 fn control_limits(&self) -> Option<ControlLimits> {
657 self.i_limits.clone()
658 }
659
660 fn is_in_control(&self) -> bool {
661 self.i_points.iter().all(|p| p.violations.is_empty())
662 && self.mr_points.iter().all(|p| p.violations.is_empty())
663 }
664
665 fn violations(&self) -> Vec<Violation> {
666 collect_violations(&self.i_points)
667 .into_iter()
668 .chain(collect_violations(&self.mr_points))
669 .collect()
670 }
671
672 fn points(&self) -> &[ChartPoint] {
673 &self.i_points
674 }
675}
676
677fn subgroup_range(subgroup: &[f64]) -> f64 {
687 let max_val =
688 u_numflow::stats::max(subgroup).expect("subgroup should be non-empty without NaN");
689 let min_val =
690 u_numflow::stats::min(subgroup).expect("subgroup should be non-empty without NaN");
691 max_val - min_val
692}
693
694fn apply_violations(points: &mut [ChartPoint], violations: &[(usize, ViolationType)]) {
696 for &(idx, vtype) in violations {
697 if let Some(point) = points.iter_mut().find(|p| p.index == idx) {
698 point.violations.push(vtype);
699 }
700 }
701}
702
703fn collect_violations(points: &[ChartPoint]) -> Vec<Violation> {
705 let mut result = Vec::new();
706 for point in points {
707 for &vtype in &point.violations {
708 result.push(Violation {
709 point_index: point.index,
710 violation_type: vtype,
711 });
712 }
713 }
714 result
715}
716
717#[cfg(test)]
722mod tests {
723 use super::*;
724
725 #[test]
728 fn test_xbar_r_basic_limits() {
729 let mut chart = XBarRChart::new(4);
730 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]);
731 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]);
732 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]);
733 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]);
734 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]);
735
736 let limits = chart.control_limits().expect("should have limits");
737 let expected_grand_mean = (71.0 + 54.5 + 52.5 + 63.0 + 57.25) / 5.0;
739 assert!(
740 (limits.cl - expected_grand_mean).abs() < 0.1,
741 "CL={}, expected ~{expected_grand_mean}",
742 limits.cl
743 );
744
745 assert!(limits.ucl > limits.cl);
747 assert!(limits.cl > limits.lcl);
748 }
749
750 #[test]
751 fn test_xbar_r_rejects_wrong_size() {
752 let mut chart = XBarRChart::new(5);
753 chart.add_sample(&[1.0, 2.0, 3.0]); assert!(chart.control_limits().is_none());
755 }
756
757 #[test]
758 fn test_xbar_r_rejects_nan() {
759 let mut chart = XBarRChart::new(3);
760 chart.add_sample(&[1.0, f64::NAN, 3.0]);
761 assert!(chart.control_limits().is_none());
762 }
763
764 #[test]
765 fn test_xbar_r_r_chart_limits() {
766 let mut chart = XBarRChart::new(5);
767 chart.add_sample(&[10.0, 12.0, 11.0, 13.0, 14.0]);
768 chart.add_sample(&[11.0, 13.0, 12.0, 10.0, 15.0]);
769 chart.add_sample(&[12.0, 11.0, 14.0, 13.0, 10.0]);
770
771 let r_limits = chart.r_limits().expect("should have R limits");
772 assert!(r_limits.ucl > r_limits.cl);
773 assert!(r_limits.lcl >= 0.0);
774 }
775
776 #[test]
777 fn test_xbar_r_constant_subgroups() {
778 let mut chart = XBarRChart::new(3);
780 chart.add_sample(&[10.0, 10.0, 10.0]);
781 chart.add_sample(&[10.0, 10.0, 10.0]);
782
783 let limits = chart.control_limits().expect("should have limits");
784 assert!((limits.cl - 10.0).abs() < f64::EPSILON);
785 assert!((limits.ucl - 10.0).abs() < f64::EPSILON);
786 assert!((limits.lcl - 10.0).abs() < f64::EPSILON);
787 }
788
789 #[test]
790 fn test_xbar_r_detects_out_of_control() {
791 let mut chart = XBarRChart::new(3);
792 for _ in 0..5 {
793 chart.add_sample(&[10.0, 10.5, 9.5]);
794 }
795 chart.add_sample(&[50.0, 51.0, 49.0]);
797
798 assert!(!chart.is_in_control());
799 }
800
801 #[test]
802 #[should_panic(expected = "subgroup_size must be 2..=10")]
803 fn test_xbar_r_invalid_size_1() {
804 let _ = XBarRChart::new(1);
805 }
806
807 #[test]
808 #[should_panic(expected = "subgroup_size must be 2..=10")]
809 fn test_xbar_r_invalid_size_11() {
810 let _ = XBarRChart::new(11);
811 }
812
813 #[test]
816 fn test_xbar_s_basic_limits() {
817 let mut chart = XBarSChart::new(4);
818 chart.add_sample(&[72.0, 84.0, 79.0, 49.0]);
819 chart.add_sample(&[56.0, 87.0, 33.0, 42.0]);
820 chart.add_sample(&[55.0, 73.0, 22.0, 60.0]);
821 chart.add_sample(&[44.0, 80.0, 54.0, 74.0]);
822 chart.add_sample(&[97.0, 26.0, 48.0, 58.0]);
823
824 let limits = chart.control_limits().expect("should have limits");
825 assert!(limits.ucl > limits.cl);
826 assert!(limits.cl > limits.lcl);
827
828 let s_limits = chart.s_limits().expect("should have S limits");
829 assert!(s_limits.ucl > s_limits.cl);
830 assert!(s_limits.lcl >= 0.0);
831 }
832
833 #[test]
834 fn test_xbar_s_rejects_wrong_size() {
835 let mut chart = XBarSChart::new(5);
836 chart.add_sample(&[1.0, 2.0]);
837 assert!(chart.control_limits().is_none());
838 }
839
840 #[test]
841 fn test_xbar_s_in_control() {
842 let mut chart = XBarSChart::new(4);
843 for _ in 0..10 {
844 chart.add_sample(&[10.0, 10.2, 9.8, 10.1]);
845 }
846 assert!(chart.is_in_control());
847 }
848
849 #[test]
852 fn test_imr_basic_limits() {
853 let mut chart = IndividualMRChart::new();
854 let data = [10.0, 12.0, 11.0, 13.0, 10.0, 14.0, 11.0, 12.0, 13.0, 10.0];
855 for &x in &data {
856 chart.add_sample(&[x]);
857 }
858
859 let limits = chart.control_limits().expect("should have limits");
860 assert!(limits.ucl > limits.cl);
861 assert!(limits.cl > limits.lcl);
862
863 let mr_limits = chart.mr_limits().expect("should have MR limits");
864 assert!(mr_limits.ucl > mr_limits.cl);
865 assert!((mr_limits.lcl).abs() < f64::EPSILON);
866 }
867
868 #[test]
869 fn test_imr_needs_two_points() {
870 let mut chart = IndividualMRChart::new();
871 chart.add_sample(&[10.0]);
872 assert!(chart.control_limits().is_none());
873 }
874
875 #[test]
876 fn test_imr_center_line_is_mean() {
877 let mut chart = IndividualMRChart::new();
878 let data = [5.0, 10.0, 15.0, 20.0, 25.0];
879 for &x in &data {
880 chart.add_sample(&[x]);
881 }
882 let limits = chart.control_limits().expect("should have limits");
883 assert!((limits.cl - 15.0).abs() < f64::EPSILON);
884 }
885
886 #[test]
887 fn test_imr_mr_values() {
888 let mut chart = IndividualMRChart::new();
889 let data = [10.0, 12.0, 9.0];
890 for &x in &data {
891 chart.add_sample(&[x]);
892 }
893 let mr_pts = chart.mr_points();
895 assert_eq!(mr_pts.len(), 2);
896 assert!((mr_pts[0].value - 2.0).abs() < f64::EPSILON);
897 assert!((mr_pts[1].value - 3.0).abs() < f64::EPSILON);
898 }
899
900 #[test]
901 fn test_imr_rejects_multi_element_sample() {
902 let mut chart = IndividualMRChart::new();
903 chart.add_sample(&[1.0, 2.0]);
904 assert!(chart.points().is_empty());
905 }
906
907 #[test]
908 fn test_imr_detects_out_of_control() {
909 let mut chart = IndividualMRChart::new();
910 for i in 0..10 {
911 chart.add_sample(&[50.0 + (i as f64 % 3.0) * 0.5]);
912 }
913 chart.add_sample(&[100.0]);
915
916 assert!(!chart.is_in_control());
917 }
918
919 #[test]
920 fn test_imr_default() {
921 let chart = IndividualMRChart::default();
922 assert!(chart.points().is_empty());
923 }
924
925 #[test]
928 fn test_subgroup_range() {
929 assert!((subgroup_range(&[1.0, 5.0, 3.0]) - 4.0).abs() < f64::EPSILON);
930 assert!((subgroup_range(&[10.0, 10.0, 10.0])).abs() < f64::EPSILON);
931 }
932
933 #[test]
936 fn test_xbar_r_chart_factors_n5() {
937 let mut chart = XBarRChart::new(5);
940 chart.add_sample(&[45.0, 47.0, 50.0, 53.0, 55.0]);
941
942 let limits = chart.control_limits().expect("limits");
943 assert!((limits.cl - 50.0).abs() < f64::EPSILON);
944
945 let r_limits = chart.r_limits().expect("R limits");
946 assert!((r_limits.cl - 10.0).abs() < f64::EPSILON);
947
948 assert!((limits.ucl - 55.77).abs() < 0.01);
950 assert!((limits.lcl - 44.23).abs() < 0.01);
952 }
953
954 #[test]
957 fn test_imr_e2_factor() {
958 let mut chart = IndividualMRChart::new();
961 chart.add_sample(&[95.0]);
962 chart.add_sample(&[105.0]);
963
964 let limits = chart.control_limits().expect("limits");
965 assert!((limits.cl - 100.0).abs() < f64::EPSILON);
967 assert!((limits.ucl - 126.6).abs() < 0.1);
970 assert!((limits.lcl - 73.4).abs() < 0.1);
972 }
973
974 #[test]
983 fn test_d2_constants_montgomery_table_vi() {
984 let expected: [(usize, f64); 9] = [
986 (2, 1.128),
987 (3, 1.693),
988 (4, 2.059),
989 (5, 2.326),
990 (6, 2.534),
991 (7, 2.704),
992 (8, 2.847),
993 (9, 2.970),
994 (10, 3.078),
995 ];
996 for (n, d2_ref) in expected {
997 let d2 = D2[n - 2];
998 assert!(
999 (d2 - d2_ref).abs() < 0.001,
1000 "d2(n={n}): expected {d2_ref}, got {d2}"
1001 );
1002 }
1003 }
1004
1005 #[test]
1014 fn test_c4_constants_montgomery_table_vi() {
1015 let expected: [(usize, f64); 6] = [
1017 (2, 0.7979),
1018 (3, 0.8862),
1019 (4, 0.9213),
1020 (5, 0.9400),
1021 (6, 0.9515),
1022 (7, 0.9594),
1023 ];
1024 for (n, c4_ref) in expected {
1025 let c4 = C4[n - 2];
1026 assert!(
1027 (c4 - c4_ref).abs() < 0.001,
1028 "c4(n={n}): expected {c4_ref}, got {c4}"
1029 );
1030 }
1031 }
1032}