use super::chart::{ChartInputError, ControlChartError};
const LANEY_MIN_SAMPLES: usize = 3;
fn check_proportion(defectives: u64, sample_size: u64) -> Result<(), ControlChartError> {
if sample_size == 0 {
return Err(ControlChartError::ZeroSampleSize);
}
if defectives > sample_size {
return Err(ControlChartError::DefectivesExceedSampleSize {
defectives,
sample_size,
});
}
Ok(())
}
fn check_units(units: f64) -> Result<(), ControlChartError> {
if units.is_finite() && units > 0.0 {
Ok(())
} else {
Err(ControlChartError::NonPositiveUnits)
}
}
#[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 z: Option<f64>,
}
fn attribute_point(index: usize, value: f64, cl: f64, sigma: f64) -> AttributeChartPoint {
let ucl = cl + 3.0 * sigma;
let lcl = (cl - 3.0 * sigma).max(0.0);
AttributeChartPoint {
index,
value,
ucl,
cl,
lcl,
out_of_control: value > ucl || value < lcl,
z: standardized(value, cl, sigma),
}
}
fn standardized(value: f64, cl: f64, sigma: f64) -> Option<f64> {
(sigma > 0.0 && sigma.is_finite()).then(|| (value - cl) / sigma)
}
fn check_proportion_standard(p: f64, parameter: &'static str) -> Result<f64, ControlChartError> {
if p.is_finite() && p > 0.0 && p < 1.0 {
Ok(p)
} else {
Err(ControlChartError::proportion_standard(parameter, p))
}
}
fn check_rate_standard(u: f64, parameter: &'static str) -> Result<f64, ControlChartError> {
if u.is_finite() && u > 0.0 {
Ok(u)
} else {
Err(ControlChartError::rate_standard(parameter, u))
}
}
pub struct PChart {
samples: Vec<(u64, u64)>,
chart_points: Vec<AttributeChartPoint>,
p_bar: Option<f64>,
center: Option<f64>,
}
impl PChart {
pub fn new() -> Self {
Self {
samples: Vec::new(),
chart_points: Vec::new(),
p_bar: None,
center: None,
}
}
pub fn with_center(p_bar: f64) -> Result<Self, ControlChartError> {
Ok(Self {
center: Some(check_proportion_standard(p_bar, "p_bar")?),
..Self::new()
})
}
pub fn add_sample(
&mut self,
defectives: u64,
sample_size: u64,
) -> Result<(), ControlChartError> {
check_proportion(defectives, sample_size)?;
self.samples.push((defectives, sample_size));
self.recompute();
Ok(())
}
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 p_bar = self.center.unwrap_or_else(|| {
let total_defectives: u64 = self.samples.iter().map(|&(d, _)| d).sum();
let total_inspected: u64 = self.samples.iter().map(|&(_, n)| n).sum();
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 sigma = (p_bar * (1.0 - p_bar) / sample_size as f64).sqrt();
attribute_point(i, p, p_bar, sigma)
})
.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) -> Result<Self, ControlChartError> {
if sample_size == 0 {
return Err(ControlChartError::ZeroSampleSize);
}
Ok(Self {
sample_size,
defective_counts: Vec::new(),
chart_points: Vec::new(),
limits: None,
})
}
pub fn add_sample(&mut self, defectives: u64) -> Result<(), ControlChartError> {
if defectives > self.sample_size {
return Err(ControlChartError::DefectivesExceedSampleSize {
defectives,
sample_size: self.sample_size,
});
}
self.defective_counts.push(defectives);
self.recompute();
Ok(())
}
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();
self.chart_points = self
.defective_counts
.iter()
.enumerate()
.map(|(i, &count)| attribute_point(i, count as f64, np_bar, sigma))
.collect();
let first = &self.chart_points[0];
self.limits = Some((first.ucl, np_bar, first.lcl));
}
}
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();
self.chart_points = self
.defect_counts
.iter()
.enumerate()
.map(|(i, &count)| attribute_point(i, count as f64, c_bar, sigma))
.collect();
let first = &self.chart_points[0];
self.limits = Some((first.ucl, c_bar, first.lcl));
}
}
impl Default for CChart {
fn default() -> Self {
Self::new()
}
}
pub struct UChart {
samples: Vec<(u64, f64)>,
chart_points: Vec<AttributeChartPoint>,
u_bar: Option<f64>,
center: Option<f64>,
}
impl UChart {
pub fn new() -> Self {
Self {
samples: Vec::new(),
chart_points: Vec::new(),
u_bar: None,
center: None,
}
}
pub fn with_center(u_bar: f64) -> Result<Self, ControlChartError> {
Ok(Self {
center: Some(check_rate_standard(u_bar, "u_bar")?),
..Self::new()
})
}
pub fn add_sample(
&mut self,
defects: u64,
units_inspected: f64,
) -> Result<(), ControlChartError> {
check_units(units_inspected)?;
self.samples.push((defects, units_inspected));
self.recompute();
Ok(())
}
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 u_bar = self.center.unwrap_or_else(|| {
let total_defects: u64 = self.samples.iter().map(|&(d, _)| d).sum();
let total_units: f64 = self.samples.iter().map(|&(_, n)| n).sum();
total_defects as f64 / total_units
});
self.u_bar = Some(u_bar);
self.chart_points = self
.samples
.iter()
.enumerate()
.map(|(i, &(defects, units))| {
attribute_point(i, defects as f64 / units, u_bar, (u_bar / units).sqrt())
})
.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,
pub z: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LaneyStandard {
pub center: f64,
pub phi: f64,
}
#[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>,
}
const LANEY_D2: f64 = 1.128;
fn laney_phi(rows: &[(f64, f64)], center: f64) -> f64 {
if rows.iter().any(|&(_, sigma)| sigma <= 0.0) {
return 0.0;
}
let z: Vec<f64> = rows.iter().map(|&(v, s)| (v - center) / s).collect();
let mr_bar = z.windows(2).map(|w| (w[1] - w[0]).abs()).sum::<f64>() / (z.len() - 1) as f64;
mr_bar / LANEY_D2
}
fn laney_points(rows: &[(f64, f64)], center: f64, phi: f64) -> Vec<LaneyAttributePoint> {
rows.iter()
.enumerate()
.map(|(index, &(value, sigma_i))| {
let sigma = phi * sigma_i;
let ucl = center + 3.0 * sigma;
let lcl = (center - 3.0 * sigma).max(0.0);
LaneyAttributePoint {
index,
value,
ucl,
cl: center,
lcl,
out_of_control: value > ucl || value < lcl,
z: standardized(value, center, sigma),
}
})
.collect()
}
fn laney_preconditions(
n: usize,
standard: Option<LaneyStandard>,
check_center: fn(f64, &'static str) -> Result<f64, ControlChartError>,
center_name: &'static str,
) -> Result<(), ChartInputError> {
if let Some(s) = standard {
check_center(s.center, center_name).map_err(ChartInputError::Standard)?;
if !(s.phi.is_finite() && s.phi >= 0.0) {
return Err(ChartInputError::Standard(
ControlChartError::dispersion_standard("phi", s.phi),
));
}
}
let min = if standard.is_some() {
1
} else {
LANEY_MIN_SAMPLES
};
if n < min {
return Err(ChartInputError::TooFewSamples { got: n, min });
}
Ok(())
}
pub fn laney_p_chart(
samples: &[(u64, u64)],
standard: Option<LaneyStandard>,
) -> Result<LaneyPChart, ChartInputError> {
laney_preconditions(samples.len(), standard, check_proportion_standard, "p_bar")?;
for (index, &(defectives, sample_size)) in samples.iter().enumerate() {
if let Err(error) = check_proportion(defectives, sample_size) {
return Err(ChartInputError::Sample { index, error });
}
}
let p_bar = standard.map_or_else(
|| {
let total_defectives: u64 = samples.iter().map(|&(d, _)| d).sum();
let total_inspected: u64 = samples.iter().map(|&(_, n)| n).sum();
total_defectives as f64 / total_inspected as f64
},
|s| s.center,
);
let base_var = p_bar * (1.0 - p_bar);
let rows: Vec<(f64, f64)> = samples
.iter()
.map(|&(d, n)| (d as f64 / n as f64, (base_var / n as f64).sqrt()))
.collect();
let phi = standard.map_or_else(|| laney_phi(&rows, p_bar), |s| s.phi);
Ok(LaneyPChart {
p_bar,
phi,
points: laney_points(&rows, p_bar, phi),
})
}
pub fn laney_u_chart(
samples: &[(u64, f64)],
standard: Option<LaneyStandard>,
) -> Result<LaneyUChart, ChartInputError> {
laney_preconditions(samples.len(), standard, check_rate_standard, "u_bar")?;
for (index, &(_, units)) in samples.iter().enumerate() {
if let Err(error) = check_units(units) {
return Err(ChartInputError::Sample { index, error });
}
}
let u_bar = standard.map_or_else(
|| {
let total_defects: u64 = samples.iter().map(|&(d, _)| d).sum();
let total_units: f64 = samples.iter().map(|&(_, n)| n).sum();
total_defects as f64 / total_units
},
|s| s.center,
);
let rows: Vec<(f64, f64)> = samples
.iter()
.map(|&(d, n)| (d as f64 / n, (u_bar / n).sqrt()))
.collect();
let phi = standard.map_or_else(|| laney_phi(&rows, u_bar), |s| s.phi);
Ok(LaneyUChart {
u_bar,
phi,
points: laney_points(&rows, u_bar, phi),
})
}
#[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).unwrap();
}
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).unwrap();
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).unwrap();
chart.add_sample(10, 200).unwrap();
chart.add_sample(3, 50).unwrap();
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() {
use crate::spc::ControlChartError;
let mut chart = PChart::new();
assert_eq!(
chart.add_sample(5, 0),
Err(ControlChartError::ZeroSampleSize)
);
assert_eq!(
chart.add_sample(10, 5),
Err(ControlChartError::DefectivesExceedSampleSize {
defectives: 10,
sample_size: 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).unwrap();
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).unwrap();
}
chart.add_sample(30, 100).unwrap();
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).expect("100 is a valid sample size");
let defectives = [5, 8, 3, 6, 4, 7, 2, 9, 5, 6];
for &d in &defectives {
chart.add_sample(d).unwrap();
}
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).expect("100 is a valid sample size");
assert_eq!(
chart.add_sample(101),
Err(crate::spc::ControlChartError::DefectivesExceedSampleSize {
defectives: 101,
sample_size: 100
})
);
assert!(chart.control_limits().is_none());
}
#[test]
fn test_np_chart_zero_sample_size() {
assert!(matches!(
NPChart::new(0),
Err(crate::spc::ControlChartError::ZeroSampleSize)
));
}
#[test]
fn test_np_chart_out_of_control() {
let mut chart = NPChart::new(100).expect("100 is a valid sample size");
for _ in 0..20 {
chart.add_sample(5).unwrap();
}
chart.add_sample(30).unwrap();
assert!(!chart.is_in_control());
}
#[test]
fn test_np_chart_limits_formula() {
let mut chart = NPChart::new(200).expect("200 is a valid sample size");
for _ in 0..10 {
chart.add_sample(10).unwrap();
}
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).unwrap();
chart.add_sample(5, 10.0).unwrap();
chart.add_sample(4, 10.0).unwrap();
chart.add_sample(6, 10.0).unwrap();
chart.add_sample(2, 10.0).unwrap();
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).unwrap(); chart.add_sample(20, 10.0).unwrap(); chart.add_sample(5, 2.5).unwrap();
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();
for units in [0.0, -1.0, f64::NAN, f64::INFINITY] {
assert_eq!(
chart.add_sample(5, units),
Err(crate::spc::ControlChartError::NonPositiveUnits),
"units = {units}"
);
}
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).unwrap();
}
chart.add_sample(50, 10.0).unwrap();
assert!(!chart.is_in_control());
}
#[test]
fn test_u_chart_lcl_clamped() {
let mut chart = UChart::new();
chart.add_sample(1, 1.0).unwrap();
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).unwrap();
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).expect("100 is a valid sample size");
let defectives = [5, 8, 3, 6, 4];
for &d in &defectives {
p_chart.add_sample(d, 100).unwrap();
np_chart.add_sample(d).unwrap();
}
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).unwrap();
}
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, None).expect("valid samples");
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, None).expect("valid samples");
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, None).expect("valid samples");
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_eq!(
laney_p_chart(&samples, None).unwrap_err(),
ChartInputError::TooFewSamples { got: 2, min: 3 }
);
}
#[test]
fn laney_p_rejects_a_sample_with_no_proportion() {
assert_eq!(
laney_p_chart(&[(3, 100), (0, 0), (4, 100), (2, 100)], None).unwrap_err(),
ChartInputError::Sample {
index: 1,
error: ControlChartError::ZeroSampleSize
}
);
assert_eq!(
laney_p_chart(&[(3, 100), (4, 100), (12, 10), (2, 100)], None).unwrap_err(),
ChartInputError::Sample {
index: 2,
error: ControlChartError::DefectivesExceedSampleSize {
defectives: 12,
sample_size: 10
}
}
);
}
#[test]
fn laney_p_phase_two_judges_a_point_against_phase_one() {
let standard = LaneyStandard {
center: 0.0514,
phi: 0.236,
};
let chart = laney_p_chart(&[(11, 170)], Some(standard)).expect("one sample suffices");
assert_eq!(
(chart.p_bar, chart.phi),
(0.0514, 0.236),
"the standard, not an estimate"
);
let p = &chart.points[0];
let expected_ucl = 0.0514 + 3.0 * 0.236 * (0.0514 * (1.0 - 0.0514) / 170.0_f64).sqrt();
assert!((p.ucl - expected_ucl).abs() < 1e-15);
assert!((p.ucl - 0.0634).abs() < 5e-5, "ucl = {}", p.ucl);
assert!(p.out_of_control);
}
#[test]
fn re_estimating_across_phases_lets_a_shift_widen_its_own_limits() {
let phase_one: Vec<(u64, u64)> = [(8, 160), (9, 170), (7, 150), (10, 180), (8, 165)].into();
let pooled_input: Vec<(u64, u64)> = phase_one
.iter()
.copied()
.chain([(30, 170), (32, 175)])
.collect();
let p1 = laney_p_chart(&phase_one, None).expect("phase I");
let pooled = laney_p_chart(&pooled_input, None).expect("pooled");
let phase_two = laney_p_chart(
&[(30, 170), (32, 175)],
Some(LaneyStandard {
center: p1.p_bar,
phi: p1.phi,
}),
)
.expect("phase II");
assert!(
pooled.p_bar > p1.p_bar,
"the shift pulls the pooled centre up"
);
assert!(phase_two.points.iter().all(|p| p.cl == p1.p_bar));
assert!(pooled.points[5].ucl > phase_two.points[0].ucl);
}
#[test]
fn p_and_u_charts_take_a_known_center() {
let mut p = PChart::with_center(0.05).expect("valid p̄");
p.add_sample(2, 100).unwrap();
p.add_sample(20, 100).unwrap();
assert_eq!(p.p_bar(), Some(0.05), "not re-estimated from the samples");
let sigma = (0.05_f64 * 0.95 / 100.0).sqrt();
assert!((p.points()[1].ucl - (0.05 + 3.0 * sigma)).abs() < 1e-15);
assert!(p.points()[1].out_of_control);
let mut u = UChart::with_center(2.0).expect("valid ū");
u.add_sample(9, 2.0).unwrap();
assert_eq!(u.u_bar(), Some(2.0));
assert!((u.points()[0].ucl - (2.0 + 3.0 * (2.0_f64 / 2.0).sqrt())).abs() < 1e-15);
}
#[test]
fn a_standard_outside_its_domain_is_refused() {
for p in [0.0, 1.0, -0.1, f64::NAN] {
match PChart::with_center(p).err() {
Some(ControlChartError::InvalidStandard {
parameter: "p_bar",
got,
exclusive_min: Some(0.0),
exclusive_max: Some(1.0),
min: None,
max: None,
..
}) => assert!(got == p || (got.is_nan() && p.is_nan()), "p̄ = {p}"),
other => panic!("p̄ = {p}: {other:?}"),
}
}
assert!(UChart::with_center(0.0).is_err());
let bad_phi = LaneyStandard {
center: 0.1,
phi: -1.0,
};
assert_eq!(
laney_p_chart(&[(1, 10)], Some(bad_phi)).unwrap_err(),
ChartInputError::Standard(ControlChartError::dispersion_standard("phi", -1.0))
);
let good = LaneyStandard {
center: 0.1,
phi: 1.0,
};
assert_eq!(
laney_u_chart(&[], Some(good)).unwrap_err(),
ChartInputError::TooFewSamples { got: 0, min: 1 }
);
}
#[test]
fn z_is_the_point_on_the_scale_where_every_limit_is_three() {
let mut p = PChart::new();
for (d, n) in [(3, 100), (12, 180), (1, 60), (9, 150), (4, 90)] {
p.add_sample(d, n).unwrap();
}
for pt in p.points() {
let sigma = (pt.ucl - pt.cl) / 3.0;
let z = pt.z.expect("p̄ strictly inside (0, 1)");
assert!((pt.cl + z * sigma - pt.value).abs() < 1e-12);
if pt.lcl > 0.0 {
assert_eq!(pt.out_of_control, z.abs() > 3.0);
}
}
let laney =
laney_p_chart(&[(3, 100), (12, 180), (1, 60), (9, 150), (4, 90)], None).expect("valid");
for pt in &laney.points {
let z = pt.z.expect("φ > 0");
assert!((pt.cl + z * (pt.ucl - pt.cl) / 3.0 - pt.value).abs() < 1e-12);
}
let flat = laney_p_chart(&[(0, 10), (0, 12), (0, 9)], None).expect("valid");
assert!(flat.points.iter().all(|p| p.z.is_none()));
}
#[test]
fn laney_u_rejects_units_by_position() {
assert_eq!(
laney_u_chart(&[(3, 10.0), (4, 10.0), (2, 10.0), (5, 0.0)], None).unwrap_err(),
ChartInputError::Sample {
index: 3,
error: ControlChartError::NonPositiveUnits
}
);
assert_eq!(
laney_u_chart(&[(3, 10.0)], None).unwrap_err(),
ChartInputError::TooFewSamples { got: 1, min: 3 }
);
}
#[test]
fn laney_u_basic() {
let samples: Vec<(u64, f64)> = vec![(5, 10.0); 10];
let chart = laney_u_chart(&samples, None).expect("valid samples");
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, None).expect("valid samples");
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).unwrap();
}
chart.add_sample(8, 100).unwrap();
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).expect("100 is a valid sample size");
for _ in 0..19 {
chart.add_sample(10).unwrap();
}
chart.add_sample(8).unwrap();
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).unwrap(); }
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("valid samples");
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("valid samples");
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("valid samples");
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, None).expect("valid samples");
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, None).expect("valid samples");
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, None).expect("valid samples");
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, None).expect("valid samples");
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, None).expect("valid samples");
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("valid samples");
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("valid samples");
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("valid samples");
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("valid samples");
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}"
);
}
}
}