#[derive(Debug, Clone)]
pub struct AttributeChartPoint {
pub index: usize,
pub value: f64,
pub ucl: f64,
pub cl: f64,
pub lcl: f64,
pub out_of_control: bool,
}
pub struct PChart {
samples: Vec<(u64, u64)>,
chart_points: Vec<AttributeChartPoint>,
p_bar: Option<f64>,
}
impl PChart {
pub fn new() -> Self {
Self {
samples: Vec::new(),
chart_points: Vec::new(),
p_bar: None,
}
}
pub fn add_sample(&mut self, defectives: u64, sample_size: u64) {
if sample_size == 0 || defectives > sample_size {
return;
}
self.samples.push((defectives, sample_size));
self.recompute();
}
pub fn p_bar(&self) -> Option<f64> {
self.p_bar
}
pub fn points(&self) -> &[AttributeChartPoint] {
&self.chart_points
}
pub fn is_in_control(&self) -> bool {
self.chart_points.iter().all(|p| !p.out_of_control)
}
fn recompute(&mut self) {
if self.samples.is_empty() {
self.p_bar = None;
self.chart_points.clear();
return;
}
let total_defectives: u64 = self.samples.iter().map(|&(d, _)| d).sum();
let total_inspected: u64 = self.samples.iter().map(|&(_, n)| n).sum();
let p_bar = total_defectives as f64 / total_inspected as f64;
self.p_bar = Some(p_bar);
self.chart_points = self
.samples
.iter()
.enumerate()
.map(|(i, &(defectives, sample_size))| {
let p = defectives as f64 / sample_size as f64;
let n = sample_size as f64;
let sigma = (p_bar * (1.0 - p_bar) / n).sqrt();
let ucl = p_bar + 3.0 * sigma;
let lcl = (p_bar - 3.0 * sigma).max(0.0);
AttributeChartPoint {
index: i,
value: p,
ucl,
cl: p_bar,
lcl,
out_of_control: p > ucl || p < lcl,
}
})
.collect();
}
}
impl Default for PChart {
fn default() -> Self {
Self::new()
}
}
pub struct NPChart {
sample_size: u64,
defective_counts: Vec<u64>,
chart_points: Vec<AttributeChartPoint>,
limits: Option<(f64, f64, f64)>, }
impl NPChart {
pub fn new(sample_size: u64) -> Self {
assert!(sample_size > 0, "sample_size must be > 0");
Self {
sample_size,
defective_counts: Vec::new(),
chart_points: Vec::new(),
limits: None,
}
}
pub fn add_sample(&mut self, defectives: u64) {
if defectives > self.sample_size {
return;
}
self.defective_counts.push(defectives);
self.recompute();
}
pub fn control_limits(&self) -> Option<(f64, f64, f64)> {
self.limits
}
pub fn points(&self) -> &[AttributeChartPoint] {
&self.chart_points
}
pub fn is_in_control(&self) -> bool {
self.chart_points.iter().all(|p| !p.out_of_control)
}
fn recompute(&mut self) {
if self.defective_counts.is_empty() {
self.limits = None;
self.chart_points.clear();
return;
}
let total_defectives: u64 = self.defective_counts.iter().sum();
let total_inspected = self.sample_size * self.defective_counts.len() as u64;
let p_bar = total_defectives as f64 / total_inspected as f64;
let n = self.sample_size as f64;
let np_bar = n * p_bar;
let sigma = (n * p_bar * (1.0 - p_bar)).sqrt();
let ucl = np_bar + 3.0 * sigma;
let lcl = (np_bar - 3.0 * sigma).max(0.0);
self.limits = Some((ucl, np_bar, lcl));
self.chart_points = self
.defective_counts
.iter()
.enumerate()
.map(|(i, &count)| {
let value = count as f64;
AttributeChartPoint {
index: i,
value,
ucl,
cl: np_bar,
lcl,
out_of_control: value > ucl || value < lcl,
}
})
.collect();
}
}
pub struct CChart {
defect_counts: Vec<u64>,
chart_points: Vec<AttributeChartPoint>,
limits: Option<(f64, f64, f64)>, }
impl CChart {
pub fn new() -> Self {
Self {
defect_counts: Vec::new(),
chart_points: Vec::new(),
limits: None,
}
}
pub fn add_sample(&mut self, defects: u64) {
self.defect_counts.push(defects);
self.recompute();
}
pub fn control_limits(&self) -> Option<(f64, f64, f64)> {
self.limits
}
pub fn points(&self) -> &[AttributeChartPoint] {
&self.chart_points
}
pub fn is_in_control(&self) -> bool {
self.chart_points.iter().all(|p| !p.out_of_control)
}
fn recompute(&mut self) {
if self.defect_counts.is_empty() {
self.limits = None;
self.chart_points.clear();
return;
}
let total: u64 = self.defect_counts.iter().sum();
let c_bar = total as f64 / self.defect_counts.len() as f64;
let sigma = c_bar.sqrt();
let ucl = c_bar + 3.0 * sigma;
let lcl = (c_bar - 3.0 * sigma).max(0.0);
self.limits = Some((ucl, c_bar, lcl));
self.chart_points = self
.defect_counts
.iter()
.enumerate()
.map(|(i, &count)| {
let value = count as f64;
AttributeChartPoint {
index: i,
value,
ucl,
cl: c_bar,
lcl,
out_of_control: value > ucl || value < lcl,
}
})
.collect();
}
}
impl Default for CChart {
fn default() -> Self {
Self::new()
}
}
pub struct UChart {
samples: Vec<(u64, f64)>,
chart_points: Vec<AttributeChartPoint>,
u_bar: Option<f64>,
}
impl UChart {
pub fn new() -> Self {
Self {
samples: Vec::new(),
chart_points: Vec::new(),
u_bar: None,
}
}
pub fn add_sample(&mut self, defects: u64, units_inspected: f64) {
if !units_inspected.is_finite() || units_inspected <= 0.0 {
return;
}
self.samples.push((defects, units_inspected));
self.recompute();
}
pub fn u_bar(&self) -> Option<f64> {
self.u_bar
}
pub fn points(&self) -> &[AttributeChartPoint] {
&self.chart_points
}
pub fn is_in_control(&self) -> bool {
self.chart_points.iter().all(|p| !p.out_of_control)
}
fn recompute(&mut self) {
if self.samples.is_empty() {
self.u_bar = None;
self.chart_points.clear();
return;
}
let total_defects: u64 = self.samples.iter().map(|&(d, _)| d).sum();
let total_units: f64 = self.samples.iter().map(|&(_, n)| n).sum();
let u_bar = total_defects as f64 / total_units;
self.u_bar = Some(u_bar);
self.chart_points = self
.samples
.iter()
.enumerate()
.map(|(i, &(defects, units))| {
let u = defects as f64 / units;
let sigma = (u_bar / units).sqrt();
let ucl = u_bar + 3.0 * sigma;
let lcl = (u_bar - 3.0 * sigma).max(0.0);
AttributeChartPoint {
index: i,
value: u,
ucl,
cl: u_bar,
lcl,
out_of_control: u > ucl || u < lcl,
}
})
.collect();
}
}
impl Default for UChart {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct LaneyAttributePoint {
pub index: usize,
pub value: f64,
pub ucl: f64,
pub cl: f64,
pub lcl: f64,
pub out_of_control: bool,
}
#[derive(Debug, Clone)]
pub struct LaneyPChart {
pub p_bar: f64,
pub phi: f64,
pub points: Vec<LaneyAttributePoint>,
}
#[derive(Debug, Clone)]
pub struct LaneyUChart {
pub u_bar: f64,
pub phi: f64,
pub points: Vec<LaneyAttributePoint>,
}
pub fn laney_p_chart(samples: &[(u64, u64)]) -> Option<LaneyPChart> {
if samples.len() < 3 {
return None;
}
let total_defectives: u64 = samples.iter().map(|&(d, _)| d).sum();
let total_inspected: u64 = samples.iter().map(|&(_, n)| n).sum();
if total_inspected == 0 {
return None;
}
let p_bar = total_defectives as f64 / total_inspected as f64;
let base_var = p_bar * (1.0 - p_bar);
if base_var <= 0.0 {
let points = samples
.iter()
.enumerate()
.map(|(i, &(d, n))| {
let value = if n > 0 { d as f64 / n as f64 } else { p_bar };
LaneyAttributePoint {
index: i,
value,
ucl: p_bar,
cl: p_bar,
lcl: p_bar,
out_of_control: false,
}
})
.collect();
return Some(LaneyPChart {
p_bar,
phi: 0.0,
points,
});
}
let z_scores: Vec<f64> = samples
.iter()
.map(|&(d, n)| {
let p_i = d as f64 / n as f64;
let sigma_i = (base_var / n as f64).sqrt();
(p_i - p_bar) / sigma_i
})
.collect();
let mr_bar = {
let mrs: Vec<f64> = z_scores.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
mrs.iter().sum::<f64>() / mrs.len() as f64
};
const D2: f64 = 1.128;
let phi = mr_bar / D2;
let points = samples
.iter()
.enumerate()
.map(|(i, &(d, n))| {
let p_i = d as f64 / n as f64;
let sigma_i = (base_var / n as f64).sqrt();
let ucl = p_bar + 3.0 * phi * sigma_i;
let lcl = (p_bar - 3.0 * phi * sigma_i).max(0.0);
LaneyAttributePoint {
index: i,
value: p_i,
ucl,
cl: p_bar,
lcl,
out_of_control: p_i > ucl || p_i < lcl,
}
})
.collect();
Some(LaneyPChart { p_bar, phi, points })
}
pub fn laney_u_chart(samples: &[(u64, f64)]) -> Option<LaneyUChart> {
if samples.len() < 3 {
return None;
}
if samples.iter().any(|&(_, n)| !n.is_finite() || n <= 0.0) {
return None;
}
let total_defects: u64 = samples.iter().map(|&(d, _)| d).sum();
let total_units: f64 = samples.iter().map(|&(_, n)| n).sum();
if total_units <= 0.0 {
return None;
}
let u_bar = total_defects as f64 / total_units;
if u_bar <= 0.0 {
let points = samples
.iter()
.enumerate()
.map(|(i, &(d, n))| {
let value = d as f64 / n;
LaneyAttributePoint {
index: i,
value,
ucl: 0.0,
cl: 0.0,
lcl: 0.0,
out_of_control: false,
}
})
.collect();
return Some(LaneyUChart {
u_bar: 0.0,
phi: 0.0,
points,
});
}
let z_scores: Vec<f64> = samples
.iter()
.map(|&(d, n)| {
let u_i = d as f64 / n;
let sigma_i = (u_bar / n).sqrt();
(u_i - u_bar) / sigma_i
})
.collect();
let mr_bar = {
let mrs: Vec<f64> = z_scores.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
mrs.iter().sum::<f64>() / mrs.len() as f64
};
const D2: f64 = 1.128;
let phi = mr_bar / D2;
let points = samples
.iter()
.enumerate()
.map(|(i, &(d, n))| {
let u_i = d as f64 / n;
let sigma_i = (u_bar / n).sqrt();
let ucl = u_bar + 3.0 * phi * sigma_i;
let lcl = (u_bar - 3.0 * phi * sigma_i).max(0.0);
LaneyAttributePoint {
index: i,
value: u_i,
ucl,
cl: u_bar,
lcl,
out_of_control: u_i > ucl || u_i < lcl,
}
})
.collect();
Some(LaneyUChart { u_bar, phi, points })
}
#[derive(Debug, Clone)]
pub struct GChartPoint {
pub index: usize,
pub value: f64,
pub ucl: f64,
pub cl: f64,
pub lcl: f64,
pub out_of_control: bool,
}
#[derive(Debug, Clone)]
pub struct GChart {
pub g_bar: f64,
pub points: Vec<GChartPoint>,
}
#[derive(Debug, Clone)]
pub struct TChart {
pub t_bar: f64,
pub points: Vec<TChartPoint>,
}
#[derive(Debug, Clone)]
pub struct TChartPoint {
pub index: usize,
pub value: f64,
pub ucl: f64,
pub cl: f64,
pub lcl: f64,
pub out_of_control: bool,
}
pub fn g_chart(inter_event_counts: &[f64]) -> Option<GChart> {
if inter_event_counts.len() < 3 {
return None;
}
if inter_event_counts
.iter()
.any(|&v| !v.is_finite() || v < 0.0)
{
return None;
}
let g_bar = inter_event_counts.iter().sum::<f64>() / inter_event_counts.len() as f64;
let spread = (g_bar * (g_bar + 1.0)).sqrt();
let ucl = g_bar + 3.0 * spread;
let lcl = (g_bar - 3.0 * spread).max(0.0);
let points = inter_event_counts
.iter()
.enumerate()
.map(|(i, &v)| GChartPoint {
index: i,
value: v,
ucl,
cl: g_bar,
lcl,
out_of_control: v > ucl || v < lcl,
})
.collect();
Some(GChart { g_bar, points })
}
pub fn t_chart(inter_event_times: &[f64]) -> Option<TChart> {
if inter_event_times.len() < 3 {
return None;
}
if inter_event_times
.iter()
.any(|&v| !v.is_finite() || v <= 0.0)
{
return None;
}
let t_bar = inter_event_times.iter().sum::<f64>() / inter_event_times.len() as f64;
let ucl_factor = -(0.00135_f64.ln()); let lcl_factor = -(0.99865_f64.ln());
let ucl = t_bar * ucl_factor;
let lcl = (t_bar * lcl_factor).max(0.0);
let points = inter_event_times
.iter()
.enumerate()
.map(|(i, &v)| TChartPoint {
index: i,
value: v,
ucl,
cl: t_bar,
lcl,
out_of_control: v > ucl || v < lcl,
})
.collect();
Some(TChart { t_bar, points })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_p_chart_basic() {
let mut chart = PChart::new();
let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
for &d in &defectives {
chart.add_sample(d, 100);
}
let p_bar = chart.p_bar().expect("should have p_bar");
assert!(
(p_bar - 0.055).abs() < 1e-10,
"p_bar={p_bar}, expected 0.055"
);
assert_eq!(chart.points().len(), 10);
for pt in chart.points() {
assert!((pt.cl - 0.055).abs() < 1e-10);
}
}
#[test]
fn test_p_chart_limits() {
let mut chart = PChart::new();
chart.add_sample(10, 100);
let pt = &chart.points()[0];
assert!((pt.cl - 0.1).abs() < 1e-10);
assert!((pt.ucl - 0.19).abs() < 0.001);
assert!((pt.lcl - 0.01).abs() < 0.001);
}
#[test]
fn test_p_chart_variable_sample_sizes() {
let mut chart = PChart::new();
chart.add_sample(5, 100);
chart.add_sample(10, 200);
chart.add_sample(3, 50);
let p_bar = chart.p_bar().expect("p_bar");
assert!((p_bar - 18.0 / 350.0).abs() < 1e-10);
let pts = chart.points();
assert!(pts[1].ucl - pts[1].cl < pts[0].ucl - pts[0].cl);
}
#[test]
fn test_p_chart_rejects_invalid() {
let mut chart = PChart::new();
chart.add_sample(5, 0); assert!(chart.p_bar().is_none());
chart.add_sample(10, 5); assert!(chart.p_bar().is_none());
}
#[test]
fn test_p_chart_lcl_clamped_to_zero() {
let mut chart = PChart::new();
chart.add_sample(1, 10);
let pt = &chart.points()[0];
assert!(pt.lcl >= 0.0);
}
#[test]
fn test_p_chart_out_of_control() {
let mut chart = PChart::new();
for _ in 0..20 {
chart.add_sample(5, 100);
}
chart.add_sample(30, 100);
assert!(!chart.is_in_control());
let last = chart.points().last().expect("should have points");
assert!(last.out_of_control);
}
#[test]
fn test_p_chart_default() {
let chart = PChart::default();
assert!(chart.p_bar().is_none());
assert!(chart.points().is_empty());
}
#[test]
fn test_np_chart_basic() {
let mut chart = NPChart::new(100);
let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
for &d in &defectives {
chart.add_sample(d);
}
let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
assert!((cl - 5.5).abs() < 1e-10);
assert!(ucl > cl);
assert!(lcl < cl);
assert!(lcl >= 0.0);
}
#[test]
fn test_np_chart_rejects_invalid() {
let mut chart = NPChart::new(100);
chart.add_sample(101); assert!(chart.control_limits().is_none());
}
#[test]
#[should_panic(expected = "sample_size must be > 0")]
fn test_np_chart_zero_sample_size() {
let _ = NPChart::new(0);
}
#[test]
fn test_np_chart_out_of_control() {
let mut chart = NPChart::new(100);
for _ in 0..20 {
chart.add_sample(5);
}
chart.add_sample(30);
assert!(!chart.is_in_control());
}
#[test]
fn test_np_chart_limits_formula() {
let mut chart = NPChart::new(200);
for _ in 0..10 {
chart.add_sample(10);
}
let (ucl, cl, lcl) = chart.control_limits().expect("limits");
assert!((cl - 10.0).abs() < 1e-10);
let expected_sigma = (200.0_f64 * 0.05 * 0.95).sqrt();
assert!((ucl - (10.0 + 3.0 * expected_sigma)).abs() < 0.01);
assert!((lcl - (10.0 - 3.0 * expected_sigma)).abs() < 0.01);
}
#[test]
fn test_c_chart_basic() {
let mut chart = CChart::new();
let counts = [3, 5, 4, 6, 2, 7, 3, 4, 5, 6];
for &c in &counts {
chart.add_sample(c);
}
let (ucl, cl, lcl) = chart.control_limits().expect("should have limits");
assert!((cl - 4.5).abs() < 1e-10);
let expected_ucl = 4.5 + 3.0 * 4.5_f64.sqrt();
assert!((ucl - expected_ucl).abs() < 0.01);
assert!(lcl >= 0.0);
}
#[test]
fn test_c_chart_out_of_control() {
let mut chart = CChart::new();
for _ in 0..20 {
chart.add_sample(5);
}
chart.add_sample(50);
assert!(!chart.is_in_control());
}
#[test]
fn test_c_chart_single_sample() {
let mut chart = CChart::new();
chart.add_sample(10);
let (_, cl, _) = chart.control_limits().expect("limits");
assert!((cl - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_c_chart_lcl_clamped() {
let mut chart = CChart::new();
chart.add_sample(1);
let (_, _, lcl) = chart.control_limits().expect("limits");
assert!((lcl - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_c_chart_default() {
let chart = CChart::default();
assert!(chart.control_limits().is_none());
assert!(chart.points().is_empty());
}
#[test]
fn test_u_chart_basic() {
let mut chart = UChart::new();
chart.add_sample(3, 10.0);
chart.add_sample(5, 10.0);
chart.add_sample(4, 10.0);
chart.add_sample(6, 10.0);
chart.add_sample(2, 10.0);
let u_bar = chart.u_bar().expect("should have u_bar");
assert!((u_bar - 0.4).abs() < 1e-10);
assert_eq!(chart.points().len(), 5);
}
#[test]
fn test_u_chart_variable_units() {
let mut chart = UChart::new();
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");
assert!((u_bar - 2.0).abs() < 1e-10);
let pts = chart.points();
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");
}
#[test]
fn test_u_chart_rejects_invalid() {
let mut chart = UChart::new();
chart.add_sample(5, 0.0); assert!(chart.u_bar().is_none());
chart.add_sample(5, -1.0); assert!(chart.u_bar().is_none());
chart.add_sample(5, f64::NAN); assert!(chart.u_bar().is_none());
chart.add_sample(5, f64::INFINITY); assert!(chart.u_bar().is_none());
}
#[test]
fn test_u_chart_out_of_control() {
let mut chart = UChart::new();
for _ in 0..20 {
chart.add_sample(4, 10.0);
}
chart.add_sample(50, 10.0);
assert!(!chart.is_in_control());
}
#[test]
fn test_u_chart_lcl_clamped() {
let mut chart = UChart::new();
chart.add_sample(1, 1.0);
let pt = &chart.points()[0];
assert!(pt.lcl >= 0.0);
}
#[test]
fn test_u_chart_default() {
let chart = UChart::default();
assert!(chart.u_bar().is_none());
assert!(chart.points().is_empty());
}
#[test]
fn test_u_chart_limits_formula() {
let mut chart = UChart::new();
chart.add_sample(8, 4.0);
let pt = &chart.points()[0];
assert!((pt.cl - 2.0).abs() < 1e-10);
let expected_sigma = (2.0_f64 / 4.0).sqrt();
assert!((pt.ucl - (2.0 + 3.0 * expected_sigma)).abs() < 0.001);
}
#[test]
fn test_p_and_np_consistent() {
let mut p_chart = PChart::new();
let mut np_chart = NPChart::new(100);
let defectives = [5, 8, 3, 6, 4];
for &d in &defectives {
p_chart.add_sample(d, 100);
np_chart.add_sample(d);
}
let p_bar = p_chart.p_bar().expect("p_bar");
let (_, np_cl, _) = np_chart.control_limits().expect("np limits");
assert!(
(np_cl - 100.0 * p_bar).abs() < 1e-10,
"NP CL should equal n * p_bar"
);
}
#[test]
fn test_c_and_u_consistent_equal_units() {
let mut c_chart = CChart::new();
let mut u_chart = UChart::new();
let defects = [3, 5, 4, 6, 2];
for &d in &defects {
c_chart.add_sample(d);
u_chart.add_sample(d, 1.0);
}
let (c_ucl, c_cl, c_lcl) = c_chart.control_limits().expect("C limits");
let u_bar = u_chart.u_bar().expect("u_bar");
assert!(
(c_cl - u_bar).abs() < 1e-10,
"C chart CL should equal U chart u-bar when n=1"
);
let u_pt = &u_chart.points()[0];
assert!((u_pt.ucl - c_ucl).abs() < 1e-10);
assert!((u_pt.lcl - c_lcl).abs() < 1e-10);
}
#[test]
fn laney_p_basic() {
let samples: Vec<(u64, u64)> = (0..10).map(|i| (i % 5 + 2, 200)).collect();
let chart = laney_p_chart(&samples).expect("chart should be Some");
assert!(chart.phi > 0.0);
assert!(chart.p_bar > 0.0 && chart.p_bar < 1.0);
assert_eq!(chart.points.len(), 10);
}
#[test]
fn laney_p_constant_proportion_phi_near_zero() {
let samples: Vec<(u64, u64)> = vec![(10, 1000); 20];
let chart = laney_p_chart(&samples).expect("chart should be Some");
assert!((chart.p_bar - 0.01).abs() < 1e-10);
assert!(chart.phi >= 0.0);
}
#[test]
fn laney_p_ucl_above_lcl() {
let samples: Vec<(u64, u64)> = vec![(5, 100), (8, 100), (3, 100), (6, 100), (4, 100)];
let chart = laney_p_chart(&samples).expect("chart should be Some");
for p in &chart.points {
assert!(p.ucl >= p.lcl);
assert!((p.cl - chart.p_bar).abs() < 1e-10);
}
}
#[test]
fn laney_p_insufficient_data() {
let samples: Vec<(u64, u64)> = vec![(2, 100), (3, 100)];
assert!(laney_p_chart(&samples).is_none());
}
#[test]
fn laney_u_basic() {
let samples: Vec<(u64, f64)> = vec![(5, 10.0); 10];
let chart = laney_u_chart(&samples).expect("chart should be Some");
assert!((chart.u_bar - 0.5).abs() < 1e-10);
assert!(chart.phi >= 0.0);
}
#[test]
fn laney_u_ucl_above_cl() {
let samples: Vec<(u64, f64)> = (0..8).map(|i| ((i % 4 + 2) as u64, 10.0)).collect();
let chart = laney_u_chart(&samples).expect("chart should be Some");
for p in &chart.points {
assert!(p.ucl > p.cl || (p.ucl - p.cl).abs() < 1e-10);
}
}
#[test]
fn p_chart_montgomery_reference_formula() {
let mut chart = PChart::new();
for _ in 0..19 {
chart.add_sample(10, 100);
}
chart.add_sample(8, 100);
let p_bar = chart.p_bar().expect("p_bar");
assert!(
(p_bar - 0.099).abs() < 1e-10,
"p̄ expected 0.099, got {p_bar}"
);
let sigma = (0.099_f64 * 0.901 / 100.0).sqrt();
let expected_ucl = 0.099 + 3.0 * sigma; let expected_lcl = (0.099 - 3.0 * sigma).max(0.0);
for pt in chart.points() {
assert!(
(pt.ucl - expected_ucl).abs() < 1e-6,
"UCL mismatch at index {}: expected {expected_ucl:.6}, got {:.6}",
pt.index,
pt.ucl
);
assert!(
(pt.lcl - expected_lcl).abs() < 1e-6,
"LCL mismatch at index {}: expected {expected_lcl:.6}, got {:.6}",
pt.index,
pt.lcl
);
}
}
#[test]
fn np_chart_montgomery_reference() {
let mut chart = NPChart::new(100);
for _ in 0..19 {
chart.add_sample(10);
}
chart.add_sample(8);
let (ucl, cl, lcl) = chart.control_limits().expect("limits");
assert!((cl - 9.9).abs() < 1e-10, "NP CL expected 9.9, got {cl}");
let expected_sigma = (9.9_f64 * 0.901).sqrt();
let expected_ucl = 9.9 + 3.0 * expected_sigma; let expected_lcl = (9.9 - 3.0 * expected_sigma).max(0.0); assert!(
(ucl - expected_ucl).abs() < 1e-6,
"NP UCL expected {expected_ucl:.4}, got {ucl:.4}"
);
assert!(
(lcl - expected_lcl).abs() < 1e-6,
"NP LCL expected {expected_lcl:.4}, got {lcl:.4}"
);
}
#[test]
fn c_chart_montgomery_reference() {
let mut chart = CChart::new();
for _ in 0..20 {
chart.add_sample(10);
}
let (ucl, cl, lcl) = chart.control_limits().expect("limits");
assert!(
(cl - 10.0).abs() < 1e-10,
"C chart CL expected 10.0, got {cl}"
);
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!(
(ucl - expected_ucl).abs() < 1e-6,
"C chart UCL expected {expected_ucl:.4}, got {ucl:.4}"
);
assert!(
(lcl - expected_lcl).abs() < 1e-6,
"C chart LCL expected {expected_lcl:.4}, got {lcl:.4}"
);
}
#[test]
fn u_chart_montgomery_reference() {
let mut chart = UChart::new();
for _ in 0..20 {
chart.add_sample(20, 10.0); }
let u_bar = chart.u_bar().expect("u_bar");
assert!(
(u_bar - 2.0).abs() < 1e-10,
"U chart ū expected 2.0, got {u_bar}"
);
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() {
assert!(
(pt.ucl - expected_ucl).abs() < 1e-6,
"U chart UCL expected {expected_ucl:.4}, got {:.4}",
pt.ucl
);
assert!(
(pt.lcl - expected_lcl).abs() < 1e-6,
"U chart LCL expected {expected_lcl:.4}, got {:.4}",
pt.lcl
);
}
}
#[test]
fn g_chart_ucl_above_cl() {
let gaps = vec![100.0, 120.0, 95.0, 110.0, 105.0];
let chart = g_chart(&gaps).expect("chart should be Some");
assert!(chart.points[0].ucl > chart.points[0].cl);
assert!(chart.points[0].lcl >= 0.0);
}
#[test]
fn g_chart_insufficient() {
assert!(g_chart(&[100.0, 120.0]).is_none());
}
#[test]
fn g_chart_all_same() {
let chart = g_chart(&[50.0; 8]).expect("chart should be Some");
assert!((chart.g_bar - 50.0).abs() < 1e-10);
assert!(chart.points[0].ucl > chart.points[0].cl);
}
#[test]
fn t_chart_ucl_factor() {
let times = vec![100.0; 10];
let chart = t_chart(×).expect("chart should be Some");
let ratio = chart.points[0].ucl / chart.t_bar;
assert!((ratio - 6.6077).abs() < 0.01, "ratio={ratio}");
}
#[test]
fn t_chart_non_positive() {
assert!(t_chart(&[10.0, -5.0, 20.0, 15.0]).is_none());
}
#[test]
fn t_chart_insufficient() {
assert!(t_chart(&[10.0, 20.0]).is_none());
}
#[test]
fn laney_p_ucl_formula_invariant() {
let samples: Vec<(u64, u64)> = vec![
(3, 100),
(7, 100),
(2, 100),
(8, 100),
(4, 100),
(5, 150),
(9, 150),
(3, 150),
(6, 150),
(4, 150),
];
let chart = laney_p_chart(&samples).expect("chart should be Some");
for (i, (&(d, n), pt)) in samples.iter().zip(&chart.points).enumerate() {
let p_i = d as f64 / n as f64;
let sigma_i = (chart.p_bar * (1.0 - chart.p_bar) / n as f64).sqrt();
let expected_ucl = chart.p_bar + 3.0 * chart.phi * sigma_i;
let expected_lcl = (chart.p_bar - 3.0 * chart.phi * sigma_i).max(0.0);
assert!(
(pt.value - p_i).abs() < 1e-10,
"point {i}: value expected {p_i:.6}, got {:.6}",
pt.value
);
assert!(
(pt.ucl - expected_ucl).abs() < 1e-10,
"point {i}: UCL expected {expected_ucl:.6}, got {:.6}",
pt.ucl
);
assert!(
(pt.lcl - expected_lcl).abs() < 1e-10,
"point {i}: LCL expected {expected_lcl:.6}, got {:.6}",
pt.lcl
);
}
}
#[test]
fn laney_p_phi_near_one_limits_close_to_standard() {
let n: u64 = 10_000;
let d_high: u64 = 5028; let d_low: u64 = 4972; let samples: Vec<(u64, u64)> = vec![
(d_high, n),
(d_low, n),
(d_high, n),
(d_low, n),
(d_high, n),
(d_low, n),
];
let laney = laney_p_chart(&samples).expect("chart should be Some");
assert!(
(laney.phi - 1.0).abs() < 0.01,
"φ expected ≈1.0, got {}",
laney.phi
);
let p_bar = laney.p_bar;
let sigma_std = (p_bar * (1.0 - p_bar) / n as f64).sqrt();
let std_ucl = p_bar + 3.0 * sigma_std;
let std_lcl = (p_bar - 3.0 * sigma_std).max(0.0);
let max_deviation = 3.0 * sigma_std * (laney.phi - 1.0).abs();
assert!(
(laney.points[0].ucl - std_ucl).abs() <= max_deviation + 1e-12,
"Laney UCL={:.6} vs std UCL={std_ucl:.6}, deviation bound={max_deviation:.2e}",
laney.points[0].ucl
);
assert!(
(laney.points[0].lcl - std_lcl).abs() <= max_deviation + 1e-12,
"Laney LCL={:.6} vs std LCL={std_lcl:.6}, deviation bound={max_deviation:.2e}",
laney.points[0].lcl
);
}
#[test]
fn laney_p_phi_gt_one_wider_than_standard() {
let samples: Vec<(u64, u64)> = vec![
(1, 100),
(20, 100),
(2, 100),
(18, 100),
(1, 100),
(22, 100),
(3, 100),
(19, 100),
(2, 100),
(20, 100),
];
let laney = laney_p_chart(&samples).expect("chart should be Some");
assert!(
laney.phi > 1.0,
"φ expected > 1 for overdispersed data, got {}",
laney.phi
);
let total_d: u64 = samples.iter().map(|&(d, _)| d).sum();
let total_n: u64 = samples.iter().map(|&(_, n)| n).sum();
let p_bar = total_d as f64 / total_n as f64;
let std_ucl_first = p_bar + 3.0 * (p_bar * (1.0 - p_bar) / 100.0_f64).sqrt();
assert!(
laney.points[0].ucl > std_ucl_first,
"Laney UCL ({:.4}) must exceed standard P chart UCL ({std_ucl_first:.4}) when φ>1",
laney.points[0].ucl
);
}
#[test]
fn laney_p_numerical_reference() {
let samples: Vec<(u64, u64)> = vec![(3, 50), (7, 50), (2, 50), (8, 50), (4, 50)];
let chart = laney_p_chart(&samples).expect("chart should be Some");
let p_bar = 24.0_f64 / 250.0;
assert!(
(chart.p_bar - p_bar).abs() < 1e-10,
"p̄ expected {p_bar:.6}, got {:.6}",
chart.p_bar
);
let sigma_i = (p_bar * (1.0 - p_bar) / 50.0_f64).sqrt();
let p_vals = [0.06_f64, 0.14, 0.04, 0.16, 0.08];
let z: Vec<f64> = p_vals.iter().map(|&p| (p - p_bar) / sigma_i).collect();
let mr_bar = z.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>() / 4.0;
let phi_expected = mr_bar / 1.128;
assert!(
(chart.phi - phi_expected).abs() < 1e-10,
"φ expected {phi_expected:.6}, got {:.6}",
chart.phi
);
let ucl_0 = p_bar + 3.0 * phi_expected * sigma_i;
let lcl_0 = (p_bar - 3.0 * phi_expected * sigma_i).max(0.0);
assert!(
(chart.points[0].ucl - ucl_0).abs() < 1e-10,
"UCL[0] expected {ucl_0:.6}, got {:.6}",
chart.points[0].ucl
);
assert!(
(chart.points[0].lcl - lcl_0).abs() < 1e-10,
"LCL[0] expected {lcl_0:.6}, got {:.6}",
chart.points[0].lcl
);
}
#[test]
fn laney_u_phi_gt_one_wider_than_standard() {
let samples: Vec<(u64, f64)> = vec![
(1, 10.0),
(15, 10.0),
(2, 10.0),
(14, 10.0),
(1, 10.0),
(16, 10.0),
(2, 10.0),
(13, 10.0),
];
let laney = laney_u_chart(&samples).expect("chart should be Some");
assert!(
laney.phi > 1.0,
"φ expected > 1 for overdispersed data, got {}",
laney.phi
);
let total_d: u64 = samples.iter().map(|&(d, _)| d).sum();
let total_n: f64 = samples.iter().map(|&(_, n)| n).sum();
let u_bar = total_d as f64 / total_n;
let std_ucl = u_bar + 3.0 * (u_bar / 10.0_f64).sqrt();
assert!(
laney.points[0].ucl > std_ucl,
"Laney U' UCL ({:.4}) must exceed standard U chart UCL ({std_ucl:.4}) when φ>1",
laney.points[0].ucl
);
}
#[test]
fn g_chart_formula_verification() {
let gaps = vec![50.0_f64; 8];
let chart = g_chart(&gaps).expect("chart should be Some");
let g_bar = 50.0_f64;
assert!(
(chart.g_bar - g_bar).abs() < 1e-10,
"ḡ expected 50.0, got {}",
chart.g_bar
);
let spread = (g_bar * (g_bar + 1.0)).sqrt(); let expected_ucl = g_bar + 3.0 * spread;
let expected_lcl = (g_bar - 3.0 * spread).max(0.0);
assert!(
(chart.points[0].ucl - expected_ucl).abs() < 1e-10,
"UCL expected {expected_ucl:.6}, got {:.6}",
chart.points[0].ucl
);
assert!(
(chart.points[0].lcl - expected_lcl).abs() < 1e-10,
"LCL expected {expected_lcl:.6}, got {:.6}",
chart.points[0].lcl
);
assert!(
chart.points[0].lcl >= 0.0,
"LCL must be clamped to 0, got {}",
chart.points[0].lcl
);
}
#[test]
fn g_chart_lcl_always_zero() {
for &g in &[1.0_f64, 5.0, 20.0, 100.0, 500.0] {
let gaps = vec![g; 5];
let chart = g_chart(&gaps).expect("chart should be Some");
assert!(
(chart.points[0].lcl - 0.0).abs() < 1e-10,
"LCL must be 0 for ḡ={g}, got {}",
chart.points[0].lcl
);
}
}
#[test]
fn t_chart_lcl_factor_verification() {
let times = vec![100.0_f64; 10];
let chart = t_chart(×).expect("chart should be Some");
let ucl_factor = chart.points[0].ucl / chart.t_bar;
let lcl_factor = chart.points[0].lcl / chart.t_bar;
let expected_ucl_factor = -(0.00135_f64.ln()); let expected_lcl_factor = -(0.99865_f64.ln());
assert!(
(ucl_factor - expected_ucl_factor).abs() < 1e-10,
"UCL factor expected {expected_ucl_factor:.6}, got {ucl_factor:.6}"
);
assert!(
(lcl_factor - expected_lcl_factor).abs() < 1e-10,
"LCL factor expected {expected_lcl_factor:.6}, got {lcl_factor:.6}"
);
}
#[test]
fn t_chart_ucl_lcl_ratio_scale_invariant() {
let k_u = -(0.00135_f64.ln());
let k_l = -(0.99865_f64.ln());
let expected_ratio = k_u / k_l;
for &t_bar in &[10.0_f64, 100.0, 1000.0] {
let times = vec![t_bar; 10];
let chart = t_chart(×).expect("chart should be Some");
let ratio = chart.points[0].ucl / chart.points[0].lcl;
assert!(
(ratio - expected_ratio).abs() < 0.01,
"UCL/LCL ratio expected {expected_ratio:.2}, got {ratio:.2} at t̄={t_bar}"
);
}
}
}