use std::fmt;
use u_numflow::transforms::{box_cox, estimate_lambda, TransformError};
use crate::capability::{CapabilityIndices, ProcessCapability};
#[derive(Debug, Clone, PartialEq)]
pub enum NonNormalCapabilityError {
NonPositiveData,
InsufficientData,
SpecTransformError,
CapabilityError,
NoSpecLimits,
}
impl fmt::Display for NonNormalCapabilityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NonNormalCapabilityError::NonPositiveData => {
write!(
f,
"Box-Cox requires all data values to be strictly positive"
)
}
NonNormalCapabilityError::InsufficientData => {
write!(
f,
"at least 4 data points are required for capability analysis"
)
}
NonNormalCapabilityError::SpecTransformError => {
write!(
f,
"failed to transform specification limit — limit must be positive"
)
}
NonNormalCapabilityError::CapabilityError => {
write!(
f,
"capability computation failed — check that data has non-zero variance"
)
}
NonNormalCapabilityError::NoSpecLimits => {
write!(
f,
"at least one specification limit (USL or LSL) must be provided"
)
}
}
}
}
impl std::error::Error for NonNormalCapabilityError {}
impl From<TransformError> for NonNormalCapabilityError {
fn from(e: TransformError) -> Self {
match e {
TransformError::NonPositiveData => NonNormalCapabilityError::NonPositiveData,
TransformError::InsufficientData => NonNormalCapabilityError::InsufficientData,
TransformError::InvalidInverse => NonNormalCapabilityError::SpecTransformError,
}
}
}
#[derive(Debug, Clone)]
pub struct NonNormalCapabilityResult {
pub lambda: f64,
pub indices: CapabilityIndices,
}
pub fn boxcox_capability(
data: &[f64],
usl: Option<f64>,
lsl: Option<f64>,
) -> Result<NonNormalCapabilityResult, NonNormalCapabilityError> {
if usl.is_none() && lsl.is_none() {
return Err(NonNormalCapabilityError::NoSpecLimits);
}
if data.len() < 4 {
return Err(NonNormalCapabilityError::InsufficientData);
}
if data.iter().any(|&v| v <= 0.0) {
return Err(NonNormalCapabilityError::NonPositiveData);
}
let lambda = estimate_lambda(data, -2.0, 2.0)?;
let y_t = box_cox(data, lambda)?;
let usl_t = usl
.map(|u| {
if u <= 0.0 {
return Err(NonNormalCapabilityError::SpecTransformError);
}
let pair = [u, data[0]];
box_cox(&pair, lambda)
.map(|v| v[0])
.map_err(|_| NonNormalCapabilityError::SpecTransformError)
})
.transpose()?;
let lsl_t = lsl
.map(|l| {
if l <= 0.0 {
return Err(NonNormalCapabilityError::SpecTransformError);
}
let pair = [l, data[0]];
box_cox(&pair, lambda)
.map(|v| v[0])
.map_err(|_| NonNormalCapabilityError::SpecTransformError)
})
.transpose()?;
let n = y_t.len();
let mean_t = y_t.iter().sum::<f64>() / n as f64;
let overall_std_t =
(y_t.iter().map(|&v| (v - mean_t).powi(2)).sum::<f64>() / (n - 1) as f64).sqrt();
let spec = ProcessCapability::new(usl_t, lsl_t)
.map_err(|_| NonNormalCapabilityError::CapabilityError)?;
let indices = spec
.compute(&y_t, overall_std_t)
.ok_or(NonNormalCapabilityError::CapabilityError)?;
Ok(NonNormalCapabilityResult { lambda, indices })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn boxcox_capability_skewed_data() {
let data: Vec<f64> = (1..=25).map(|i| (i as f64 * 0.2).exp()).collect();
let result = boxcox_capability(&data, Some(150.0), Some(1.0)).unwrap();
assert!(result.lambda.abs() < 0.6, "lambda={}", result.lambda);
assert!(result.indices.pp.is_some() || result.indices.ppk.is_some());
}
#[test]
fn boxcox_capability_non_positive_error() {
let data = vec![1.0, -1.0, 2.0, 3.0, 4.0, 5.0];
assert!(boxcox_capability(&data, Some(10.0), None).is_err());
}
#[test]
fn boxcox_capability_insufficient_data() {
let data = vec![1.0, 2.0, 3.0]; assert!(boxcox_capability(&data, Some(10.0), None).is_err());
}
#[test]
fn boxcox_capability_lambda_in_range() {
let data: Vec<f64> = (1..=20).map(|i| i as f64).collect();
let result = boxcox_capability(&data, Some(25.0), Some(0.5)).unwrap();
assert!(result.lambda >= -2.0 && result.lambda <= 2.0);
}
#[test]
fn boxcox_capability_no_spec_error() {
let data: Vec<f64> = (1..=10).map(|i| i as f64).collect();
assert!(boxcox_capability(&data, None, None).is_err());
}
#[test]
fn boxcox_capability_usl_only() {
let data: Vec<f64> = (1..=20).map(|i| i as f64 * 0.5).collect();
let result = boxcox_capability(&data, Some(20.0), None).unwrap();
assert!(result.indices.ppk.is_some());
assert!(result.indices.pp.is_none());
}
#[test]
fn boxcox_capability_lsl_only() {
let data: Vec<f64> = (1..=20).map(|i| i as f64).collect();
let result = boxcox_capability(&data, None, Some(0.5)).unwrap();
assert!(result.indices.ppk.is_some());
assert!(result.indices.pp.is_none());
}
#[test]
fn boxcox_capability_two_sided() {
let data: Vec<f64> = (1..=30).map(|i| (i as f64 * 0.1).exp()).collect();
let result = boxcox_capability(&data, Some(20.0), Some(1.0)).unwrap();
assert!(result.indices.pp.is_some());
assert!(result.indices.ppk.is_some());
assert!(result.indices.cp.is_some());
assert!(result.indices.cpk.is_some());
}
#[test]
fn boxcox_capability_non_positive_spec_error() {
let data: Vec<f64> = (1..=10).map(|i| i as f64).collect();
assert!(boxcox_capability(&data, Some(20.0), Some(-1.0)).is_err());
}
#[test]
fn boxcox_capability_result_has_valid_lambda() {
let data: Vec<f64> = (1..=15).map(|i| (i as f64).powi(2)).collect();
let result = boxcox_capability(&data, Some(250.0), Some(0.5)).unwrap();
assert!(result.lambda.is_finite());
assert!(result.lambda >= -2.0 && result.lambda <= 2.0);
}
}