use u_numflow::stats;
pub struct ProcessCapability {
usl: Option<f64>,
lsl: Option<f64>,
target: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct CapabilityIndices {
pub cp: Option<f64>,
pub cpk: Option<f64>,
pub cpu: Option<f64>,
pub cpl: Option<f64>,
pub pp: Option<f64>,
pub ppk: Option<f64>,
pub ppu: Option<f64>,
pub ppl: Option<f64>,
pub cpm: Option<f64>,
pub mean: f64,
pub std_dev_within: f64,
pub std_dev_overall: f64,
}
impl ProcessCapability {
pub fn new(usl: Option<f64>, lsl: Option<f64>) -> Result<Self, &'static str> {
if usl.is_none() && lsl.is_none() {
return Err("at least one specification limit (USL or LSL) is required");
}
if let Some(u) = usl {
if !u.is_finite() {
return Err("USL must be finite");
}
}
if let Some(l) = lsl {
if !l.is_finite() {
return Err("LSL must be finite");
}
}
if let (Some(u), Some(l)) = (usl, lsl) {
if u <= l {
return Err("USL must be greater than LSL");
}
}
Ok(Self {
usl,
lsl,
target: None,
})
}
pub fn with_target(mut self, target: f64) -> Self {
self.target = Some(target);
self
}
pub fn compute(&self, data: &[f64], sigma_within: f64) -> Option<CapabilityIndices> {
if !sigma_within.is_finite() || sigma_within <= 0.0 {
return None;
}
let x_bar = stats::mean(data)?;
let sigma_overall = stats::std_dev(data)?;
Some(self.compute_indices(x_bar, sigma_within, sigma_overall))
}
pub fn compute_overall(&self, data: &[f64]) -> Option<CapabilityIndices> {
let x_bar = stats::mean(data)?;
let sigma_overall = stats::std_dev(data)?;
Some(self.compute_indices(x_bar, sigma_overall, sigma_overall))
}
fn compute_indices(
&self,
x_bar: f64,
sigma_within: f64,
sigma_overall: f64,
) -> CapabilityIndices {
let cpu = self.usl.map(|u| (u - x_bar) / (3.0 * sigma_within));
let cpl = self.lsl.map(|l| (x_bar - l) / (3.0 * sigma_within));
let cp = match (self.usl, self.lsl) {
(Some(u), Some(l)) => Some((u - l) / (6.0 * sigma_within)),
_ => None,
};
let cpk = match (cpu, cpl) {
(Some(u), Some(l)) => Some(u.min(l)),
(Some(u), None) => Some(u),
(None, Some(l)) => Some(l),
(None, None) => None,
};
let ppu = self.usl.map(|u| (u - x_bar) / (3.0 * sigma_overall));
let ppl = self.lsl.map(|l| (x_bar - l) / (3.0 * sigma_overall));
let pp = match (self.usl, self.lsl) {
(Some(u), Some(l)) => Some((u - l) / (6.0 * sigma_overall)),
_ => None,
};
let ppk = match (ppu, ppl) {
(Some(u), Some(l)) => Some(u.min(l)),
(Some(u), None) => Some(u),
(None, Some(l)) => Some(l),
(None, None) => None,
};
let cpm = cp.and_then(|cp_val| {
let target = self.target.or_else(|| match (self.usl, self.lsl) {
(Some(u), Some(l)) => Some((u + l) / 2.0),
_ => None,
})?;
let deviation_ratio = (x_bar - target) / sigma_within;
Some(cp_val / (1.0 + deviation_ratio * deviation_ratio).sqrt())
});
CapabilityIndices {
cp,
cpk,
cpu,
cpl,
pp,
ppk,
ppu,
ppl,
cpm,
mean: x_bar,
std_dev_within: sigma_within,
std_dev_overall: sigma_overall,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_requires_at_least_one_limit() {
assert!(ProcessCapability::new(None, None).is_err());
}
#[test]
fn new_rejects_usl_leq_lsl() {
assert!(ProcessCapability::new(Some(5.0), Some(10.0)).is_err());
assert!(ProcessCapability::new(Some(5.0), Some(5.0)).is_err());
}
#[test]
fn new_rejects_non_finite() {
assert!(ProcessCapability::new(Some(f64::NAN), Some(1.0)).is_err());
assert!(ProcessCapability::new(Some(10.0), Some(f64::INFINITY)).is_err());
}
#[test]
fn new_accepts_valid_two_sided() {
assert!(ProcessCapability::new(Some(10.0), Some(5.0)).is_ok());
}
#[test]
fn new_accepts_usl_only() {
assert!(ProcessCapability::new(Some(10.0), None).is_ok());
}
#[test]
fn new_accepts_lsl_only() {
assert!(ProcessCapability::new(None, Some(5.0)).is_ok());
}
#[test]
fn textbook_centered_process() {
let spec = ProcessCapability::new(Some(220.0), Some(200.0)).unwrap();
let data = [
208.0, 209.0, 210.0, 211.0, 212.0, 208.5, 209.5, 210.5, 211.5, 210.0, 209.0, 211.0,
210.0, 209.5, 210.5, 210.0, 210.0, 210.0, 209.0, 211.0,
];
let sigma_within = 2.0;
let indices = spec.compute(&data, sigma_within).unwrap();
let cp = indices.cp.unwrap();
assert!(
(cp - 1.6667).abs() < 0.001,
"expected Cp ~ 1.6667, got {cp}"
);
let cpk = indices.cpk.unwrap();
assert!(cpk > 0.0, "Cpk should be positive");
let cpm = indices.cpm.unwrap();
assert!(cpm > 0.0, "Cpm should be positive for centered process");
}
#[test]
fn off_center_process() {
let spec = ProcessCapability::new(Some(220.0), Some(200.0)).unwrap();
let data = [
213.0, 214.0, 215.0, 216.0, 217.0, 213.5, 214.5, 215.5, 216.5, 215.0, 214.0, 216.0,
215.0, 214.5, 215.5, 215.0, 215.0, 215.0, 214.0, 216.0,
];
let sigma_within = 2.0;
let indices = spec.compute(&data, sigma_within).unwrap();
let cp = indices.cp.unwrap();
assert!(
(cp - 1.6667).abs() < 0.001,
"expected Cp ~ 1.6667, got {cp}"
);
let cpu = indices.cpu.unwrap();
assert!(
(cpu - 0.8333).abs() < 0.05,
"expected Cpu ~ 0.8333, got {cpu}"
);
let cpl = indices.cpl.unwrap();
assert!((cpl - 2.5).abs() < 0.05, "expected Cpl ~ 2.5, got {cpl}");
let cpk = indices.cpk.unwrap();
assert!((cpk - cpu).abs() < 1e-15, "Cpk should equal min(Cpu, Cpl)");
}
#[test]
fn usl_only_computes_cpu_not_cpl() {
let spec = ProcessCapability::new(Some(10.0), None).unwrap();
let data = [7.0, 8.0, 9.0, 7.5, 8.5, 8.0, 7.0, 9.0, 8.0, 8.5];
let indices = spec.compute(&data, 0.5).unwrap();
assert!(indices.cpu.is_some());
assert!(indices.cpl.is_none());
assert!(indices.cp.is_none(), "Cp requires both limits");
assert!(indices.cpk.is_some(), "Cpk should be Cpu for USL-only");
assert!(
(indices.cpk.unwrap() - indices.cpu.unwrap()).abs() < 1e-15,
"Cpk == Cpu when only USL is set"
);
}
#[test]
fn lsl_only_computes_cpl_not_cpu() {
let spec = ProcessCapability::new(None, Some(5.0)).unwrap();
let data = [7.0, 8.0, 9.0, 7.5, 8.5, 8.0, 7.0, 9.0, 8.0, 8.5];
let indices = spec.compute(&data, 0.5).unwrap();
assert!(indices.cpl.is_some());
assert!(indices.cpu.is_none());
assert!(indices.cp.is_none());
assert!(indices.cpk.is_some(), "Cpk should be Cpl for LSL-only");
assert!(
(indices.cpk.unwrap() - indices.cpl.unwrap()).abs() < 1e-15,
"Cpk == Cpl when only LSL is set"
);
}
#[test]
fn compute_overall_matches_pp_equals_cp() {
let spec = ProcessCapability::new(Some(220.0), Some(200.0)).unwrap();
let data = [
208.0, 209.0, 210.0, 211.0, 212.0, 208.5, 209.5, 210.5, 211.5, 210.0,
];
let indices = spec.compute_overall(&data).unwrap();
let cp = indices.cp.unwrap();
let pp = indices.pp.unwrap();
assert!(
(cp - pp).abs() < 1e-15,
"Cp should equal Pp in compute_overall"
);
let cpk = indices.cpk.unwrap();
let ppk = indices.ppk.unwrap();
assert!(
(cpk - ppk).abs() < 1e-15,
"Cpk should equal Ppk in compute_overall"
);
}
#[test]
fn cpm_with_explicit_target() {
let spec = ProcessCapability::new(Some(220.0), Some(200.0))
.unwrap()
.with_target(212.0);
let data = [
208.0, 209.0, 210.0, 211.0, 212.0, 208.5, 209.5, 210.5, 211.5, 210.0,
];
let sigma_within = 2.0;
let indices = spec.compute(&data, sigma_within).unwrap();
let cpm = indices.cpm.unwrap();
let cp = indices.cp.unwrap();
assert!(
cpm < cp,
"Cpm ({cpm}) should be less than Cp ({cp}) when mean != target"
);
}
#[test]
fn cpm_equals_cp_when_on_target() {
let spec = ProcessCapability::new(Some(220.0), Some(200.0)).unwrap();
let data = [210.0; 20];
let sigma_within = 2.0;
let indices = spec.compute(&data, sigma_within).unwrap();
let cpm = indices.cpm.unwrap();
let cp = indices.cp.unwrap();
assert!(
(cpm - cp).abs() < 1e-10,
"Cpm ({cpm}) should equal Cp ({cp}) when mean == target"
);
}
#[test]
fn compute_returns_none_for_insufficient_data() {
let spec = ProcessCapability::new(Some(10.0), Some(0.0)).unwrap();
assert!(spec.compute(&[5.0], 1.0).is_none());
assert!(spec.compute(&[], 1.0).is_none());
}
#[test]
fn compute_returns_none_for_invalid_sigma() {
let spec = ProcessCapability::new(Some(10.0), Some(0.0)).unwrap();
let data = [4.0, 5.0, 6.0, 5.0, 5.5];
assert!(spec.compute(&data, 0.0).is_none());
assert!(spec.compute(&data, -1.0).is_none());
assert!(spec.compute(&data, f64::NAN).is_none());
assert!(spec.compute(&data, f64::INFINITY).is_none());
}
#[test]
fn compute_returns_none_for_nan_data() {
let spec = ProcessCapability::new(Some(10.0), Some(0.0)).unwrap();
let data = [4.0, f64::NAN, 6.0];
assert!(spec.compute(&data, 1.0).is_none());
}
#[test]
fn compute_overall_returns_none_for_insufficient_data() {
let spec = ProcessCapability::new(Some(10.0), Some(0.0)).unwrap();
assert!(spec.compute_overall(&[5.0]).is_none());
assert!(spec.compute_overall(&[]).is_none());
}
#[test]
fn cp_cpk_montgomery_2020_reference() {
let usl = 74.05_f64;
let lsl = 73.95_f64;
let mu = 74.001_f64;
let sigma_within = 0.0099_f64;
let spec = ProcessCapability::new(Some(usl), Some(lsl)).unwrap();
let data = vec![mu; 100];
let indices = spec.compute(&data, sigma_within).unwrap();
let cp = indices.cp.unwrap();
let expected_cp = (usl - lsl) / (6.0 * sigma_within); assert!(
(cp - expected_cp).abs() < 0.001,
"Cp: expected {expected_cp:.4}, got {cp:.4}"
);
let cpu = indices.cpu.unwrap();
let expected_cpu = (usl - mu) / (3.0 * sigma_within); assert!(
(cpu - expected_cpu).abs() < 0.001,
"Cpu: expected {expected_cpu:.4}, got {cpu:.4}"
);
let cpl = indices.cpl.unwrap();
let expected_cpl = (mu - lsl) / (3.0 * sigma_within); assert!(
(cpl - expected_cpl).abs() < 0.001,
"Cpl: expected {expected_cpl:.4}, got {cpl:.4}"
);
let cpk = indices.cpk.unwrap();
let expected_cpk = expected_cpu.min(expected_cpl); assert!(
(cpk - expected_cpk).abs() < 0.001,
"Cpk: expected {expected_cpk:.4}, got {cpk:.4}"
);
assert!(
cpu < cpl,
"mean above centre → Cpu ({cpu:.4}) should be < Cpl ({cpl:.4})"
);
}
#[test]
fn pp_differs_from_cp_when_sigmas_differ() {
let spec = ProcessCapability::new(Some(220.0), Some(200.0)).unwrap();
let data = [
205.0, 207.0, 210.0, 213.0, 215.0, 206.0, 208.0, 212.0, 214.0, 210.0, 204.0, 216.0,
209.0, 211.0, 210.0,
];
let sigma_within = 1.5;
let indices = spec.compute(&data, sigma_within).unwrap();
let cp = indices.cp.unwrap();
let pp = indices.pp.unwrap();
assert!(
pp < cp,
"Pp ({pp}) should be < Cp ({cp}) when sigma_overall > sigma_within"
);
}
#[test]
fn exact_numerical_verification() {
let spec = ProcessCapability::new(Some(10.0), Some(0.0)).unwrap();
let data = [4.0, 4.5, 5.0, 5.5, 6.0, 4.0, 5.0, 6.0, 5.0, 5.0];
let x_bar = stats::mean(&data).unwrap();
let sigma_within = 1.0;
let indices = spec.compute(&data, sigma_within).unwrap();
let expected_cp = 10.0 / 6.0;
let expected_cpu = (10.0 - x_bar) / 3.0;
let expected_cpl = (x_bar - 0.0) / 3.0;
assert!(
(indices.cp.unwrap() - expected_cp).abs() < 1e-10,
"Cp: expected {expected_cp}, got {}",
indices.cp.unwrap()
);
assert!(
(indices.cpu.unwrap() - expected_cpu).abs() < 1e-10,
"Cpu: expected {expected_cpu}, got {}",
indices.cpu.unwrap()
);
assert!(
(indices.cpl.unwrap() - expected_cpl).abs() < 1e-10,
"Cpl: expected {expected_cpl}, got {}",
indices.cpl.unwrap()
);
assert!(
(indices.cpk.unwrap() - expected_cpu.min(expected_cpl)).abs() < 1e-10,
"Cpk: expected {}, got {}",
expected_cpu.min(expected_cpl),
indices.cpk.unwrap()
);
}
}