use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)]
pub(crate) struct XbarRChartDto {
pub(crate) xbar_cl: f64,
pub(crate) xbar_ucl: f64,
pub(crate) xbar_lcl: f64,
pub(crate) r_cl: f64,
pub(crate) r_ucl: f64,
pub(crate) r_lcl: f64,
pub(crate) sigma_hat: Option<f64>,
pub(crate) xbar_points: Vec<ChartPointDto>,
pub(crate) r_points: Vec<ChartPointDto>,
pub(crate) in_control: bool,
}
#[derive(Serialize, Debug)]
pub(crate) struct ChartPointDto {
pub(crate) index: usize,
pub(crate) value: f64,
pub(crate) violations: Vec<String>,
}
#[derive(Serialize, Debug)]
pub(crate) struct PChartDto {
pub(crate) p_bar: f64,
pub(crate) points: Vec<AttributeChartPointDto>,
pub(crate) in_control: bool,
}
#[derive(Serialize, Debug)]
pub(crate) struct AttributeChartPointDto {
pub(crate) index: usize,
pub(crate) value: f64,
pub(crate) ucl: f64,
pub(crate) cl: f64,
pub(crate) lcl: f64,
pub(crate) out_of_control: bool,
pub(crate) z: Option<f64>,
}
#[derive(Deserialize, Default, Debug)]
#[serde(deny_unknown_fields)]
pub(crate) struct AttributeStandardDto {
#[serde(default)]
pub(crate) p_bar: Option<f64>,
#[serde(default)]
pub(crate) u_bar: Option<f64>,
#[serde(default)]
pub(crate) phi: Option<f64>,
}
impl AttributeStandardDto {
pub(crate) fn refuse(&self, chart: &str, forbidden: &[(&str, bool)]) -> Result<(), WireError> {
match forbidden.iter().find(|(_, present)| *present) {
Some((name, _)) => Err(WireError::new(
code::MALFORMED_INPUT,
None,
format!("options: {chart} does not take `{name}`"),
)),
None => Ok(()),
}
}
pub(crate) fn laney(
&self,
center: Option<f64>,
center_name: &str,
) -> Result<Option<crate::spc::LaneyStandard>, WireError> {
match (center, self.phi) {
(Some(center), Some(phi)) => Ok(Some(crate::spc::LaneyStandard { center, phi })),
(None, None) => Ok(None),
_ => Err(WireError::invalid_input(format!(
"options: `{center_name}` and `phi` are given together -- a Phase I standard \
fixes both, since phi scales the error about that centre"
))),
}
}
}
#[derive(Serialize, Debug)]
pub(crate) struct LaneyPChartDto {
pub(crate) p_bar: f64,
pub(crate) phi: f64,
pub(crate) points: Vec<AttributeChartPointDto>,
}
pub(crate) fn violation_name(v: crate::spc::ViolationType) -> &'static str {
use crate::spc::ViolationType;
match v {
ViolationType::BeyondLimits => "BeyondLimits",
ViolationType::NineOneSide => "NineOneSide",
ViolationType::SixTrend => "SixTrend",
ViolationType::FourteenAlternating => "FourteenAlternating",
ViolationType::TwoOfThreeBeyond2Sigma => "TwoOfThreeBeyond2Sigma",
ViolationType::FourOfFiveBeyond1Sigma => "FourOfFiveBeyond1Sigma",
ViolationType::FifteenWithin1Sigma => "FifteenWithin1Sigma",
ViolationType::EightBeyond1Sigma => "EightBeyond1Sigma",
}
}
pub(crate) fn rules_from_json(
options: Option<serde_json::Value>,
) -> Result<crate::spc::RuleSet, String> {
use crate::spc::{RuleSet, ViolationType};
let Some(value) = options else {
return Ok(RuleSet::default());
};
if value.is_null() {
return Ok(RuleSet::default());
}
let Some(names) = value.get("rules") else {
return Ok(RuleSet::default());
};
if names.is_null() {
return Ok(RuleSet::default());
}
let names = names
.as_array()
.ok_or_else(|| "rules: expected an array of rule names".to_string())?;
let mut set = RuleSet::none();
for name in names {
let name = name
.as_str()
.ok_or_else(|| "rules: expected an array of rule names".to_string())?;
let rule = match name {
"BeyondLimits" => ViolationType::BeyondLimits,
"NineOneSide" => ViolationType::NineOneSide,
"SixTrend" => ViolationType::SixTrend,
"FourteenAlternating" => ViolationType::FourteenAlternating,
"TwoOfThreeBeyond2Sigma" => ViolationType::TwoOfThreeBeyond2Sigma,
"FourOfFiveBeyond1Sigma" => ViolationType::FourOfFiveBeyond1Sigma,
"FifteenWithin1Sigma" => ViolationType::FifteenWithin1Sigma,
"EightBeyond1Sigma" => ViolationType::EightBeyond1Sigma,
other => {
return Err(format!(
"rules: unknown rule {other:?} -- the names are the values `violations` reports"
))
}
};
set = set.with(rule);
}
Ok(set)
}
pub(crate) fn point_dtos(points: &[crate::spc::ChartPoint]) -> Vec<ChartPointDto> {
points
.iter()
.map(|p| ChartPointDto {
index: p.index,
value: p.value,
violations: p
.violations
.iter()
.map(|&v| violation_name(v).to_owned())
.collect(),
})
.collect()
}
pub(crate) fn subgroup_size(subgroups: &[Vec<f64>]) -> Result<usize, WireError> {
subgroups
.first()
.map(Vec::len)
.ok_or_else(|| WireError::too_few_samples("subgroups: at least one subgroup required"))
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub(crate) struct WireError {
pub(crate) code: &'static str,
pub(crate) index: Option<usize>,
pub(crate) message: String,
}
pub(crate) mod code {
pub(crate) const INVALID_INPUT: &str = "invalid_input";
pub(crate) const MALFORMED_INPUT: &str = "malformed_input";
pub(crate) const COUNT_NOT_WHOLE: &str = "count_not_whole";
pub(crate) const SAMPLE_SIZE_NOT_POSITIVE: &str = "sample_size_not_positive";
pub(crate) const DEFECTIVES_EXCEED_SAMPLE: &str = "defectives_exceed_sample";
pub(crate) const UNITS_NOT_POSITIVE: &str = "units_not_positive";
pub(crate) const SUBGROUP_LENGTH_MISMATCH: &str = "subgroup_length_mismatch";
pub(crate) const SUBGROUP_SIZE_OUT_OF_RANGE: &str = "subgroup_size_out_of_range";
pub(crate) const VALUE_NOT_FINITE: &str = "value_not_finite";
pub(crate) const TOO_FEW_SAMPLES: &str = "too_few_samples";
pub(crate) const STANDARD_OUT_OF_RANGE: &str = "standard_out_of_range";
}
impl WireError {
pub(crate) fn new(
code: &'static str,
index: Option<usize>,
message: impl Into<String>,
) -> Self {
WireError {
code,
index,
message: message.into(),
}
}
pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
Self::new(code::INVALID_INPUT, None, message)
}
pub(crate) fn too_few_samples(message: impl Into<String>) -> Self {
Self::new(code::TOO_FEW_SAMPLES, None, message)
}
pub(crate) fn chart(
label: &str,
index: Option<usize>,
error: &crate::spc::ControlChartError,
) -> Self {
let message = match index {
Some(i) => format!("{label}[{i}]: {error}"),
None => error.to_string(),
};
Self::new(chart_error_code(error), index, message)
}
pub(crate) fn chart_input(label: &str, error: &crate::spc::ChartInputError) -> Self {
use crate::spc::ChartInputError;
match error {
ChartInputError::Sample { index, error } => Self::chart(label, Some(*index), error),
ChartInputError::TooFewSamples { .. } => {
Self::too_few_samples(format!("{label}: {error}"))
}
ChartInputError::Standard(error) => Self::chart(label, None, error),
}
}
}
impl std::fmt::Display for WireError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl From<String> for WireError {
fn from(message: String) -> Self {
Self::invalid_input(message)
}
}
impl From<&String> for WireError {
fn from(message: &String) -> Self {
Self::invalid_input(message.as_str())
}
}
impl From<&WireError> for WireError {
fn from(error: &WireError) -> Self {
error.clone()
}
}
impl From<&str> for WireError {
fn from(message: &str) -> Self {
Self::invalid_input(message)
}
}
pub(crate) fn chart_error_code(error: &crate::spc::ControlChartError) -> &'static str {
use crate::spc::ControlChartError as E;
match error {
E::SampleLengthMismatch { .. } => code::SUBGROUP_LENGTH_MISMATCH,
E::NonFiniteValue => code::VALUE_NOT_FINITE,
E::DefectivesExceedSampleSize { .. } => code::DEFECTIVES_EXCEED_SAMPLE,
E::NonPositiveUnits => code::UNITS_NOT_POSITIVE,
E::SubgroupSizeOutOfRange { .. } => code::SUBGROUP_SIZE_OUT_OF_RANGE,
E::ZeroSampleSize => code::SAMPLE_SIZE_NOT_POSITIVE,
E::InvalidStandard { .. } => code::STANDARD_OUT_OF_RANGE,
}
}
pub(crate) fn add_rows<T>(
rows: impl IntoIterator<Item = T>,
label: &str,
mut add: impl FnMut(T) -> Result<(), crate::spc::ControlChartError>,
) -> Result<(), WireError> {
for (i, row) in rows.into_iter().enumerate() {
add(row).map_err(|e| WireError::chart(label, Some(i), &e))?;
}
Ok(())
}
const MAX_EXACT_COUNT: f64 = 9_007_199_254_740_992.0;
fn whole_count(
value: &serde_json::Value,
at: &str,
index: Option<usize>,
) -> Result<u64, WireError> {
if let Some(n) = value.as_u64() {
return Ok(n);
}
let refuse = |shown: String| {
WireError::new(
code::COUNT_NOT_WHOLE,
index,
format!("{at}: a count must be a whole number of at least 0, got {shown}"),
)
};
match value.as_f64() {
Some(x) if x >= 0.0 && x.fract() == 0.0 && x <= MAX_EXACT_COUNT => Ok(x as u64),
Some(x) => Err(refuse(x.to_string())),
None => Err(refuse(value.to_string())),
}
}
fn as_array<'a>(
value: &'a serde_json::Value,
at: &str,
expected: &str,
) -> Result<&'a Vec<serde_json::Value>, WireError> {
value.as_array().ok_or_else(|| {
WireError::new(
code::MALFORMED_INPUT,
None,
format!("{at}: expected {expected}"),
)
})
}
fn pair<'a>(
row: &'a serde_json::Value,
label: &str,
i: usize,
expected: &str,
) -> Result<(&'a serde_json::Value, &'a serde_json::Value), WireError> {
match row.as_array().map(Vec::as_slice) {
Some([a, b]) => Ok((a, b)),
_ => Err(WireError::new(
code::MALFORMED_INPUT,
Some(i),
format!("{label}[{i}]: expected a pair {expected}, got {row}"),
)),
}
}
pub(crate) fn count_rows(value: &serde_json::Value, label: &str) -> Result<Vec<u64>, WireError> {
as_array(value, label, "an array of counts")?
.iter()
.enumerate()
.map(|(i, v)| whole_count(v, &format!("{label}[{i}]"), Some(i)))
.collect()
}
pub(crate) fn count_value(value: &serde_json::Value, label: &str) -> Result<u64, WireError> {
whole_count(value, label, None)
}
pub(crate) fn count_pairs(
value: &serde_json::Value,
label: &str,
) -> Result<Vec<(u64, u64)>, WireError> {
const SHAPE: &str = "[defectives, sample_size]";
as_array(value, label, &format!("an array of {SHAPE} pairs"))?
.iter()
.enumerate()
.map(|(i, row)| {
let (d, n) = pair(row, label, i, SHAPE)?;
Ok((
whole_count(d, &format!("{label}[{i}][0] (defectives)"), Some(i))?,
whole_count(n, &format!("{label}[{i}][1] (sample size)"), Some(i))?,
))
})
.collect()
}
pub(crate) fn rate_pairs(
value: &serde_json::Value,
label: &str,
) -> Result<Vec<(u64, f64)>, WireError> {
const SHAPE: &str = "[defects, units]";
as_array(value, label, &format!("an array of {SHAPE} pairs"))?
.iter()
.enumerate()
.map(|(i, row)| {
let (d, u) = pair(row, label, i, SHAPE)?;
let defects = whole_count(d, &format!("{label}[{i}][0] (defects)"), Some(i))?;
let units = u.as_f64().ok_or_else(|| {
WireError::new(
code::UNITS_NOT_POSITIVE,
Some(i),
format!("{label}[{i}][1] (units): expected a positive number, got {u}"),
)
})?;
Ok((defects, units))
})
.collect()
}
pub(crate) fn attribute_point_dtos(
points: &[crate::spc::AttributeChartPoint],
) -> Vec<AttributeChartPointDto> {
points
.iter()
.map(|p| AttributeChartPointDto {
index: p.index,
value: p.value,
ucl: p.ucl,
cl: p.cl,
lcl: p.lcl,
out_of_control: p.out_of_control,
z: p.z,
})
.collect()
}
pub(crate) fn laney_point_dtos(
points: &[crate::spc::LaneyAttributePoint],
) -> Vec<AttributeChartPointDto> {
points
.iter()
.map(|p| AttributeChartPointDto {
index: p.index,
value: p.value,
ucl: p.ucl,
cl: p.cl,
lcl: p.lcl,
out_of_control: p.out_of_control,
z: p.z,
})
.collect()
}
pub(crate) fn xbar_r_dto(
subgroups: Vec<Vec<f64>>,
rules: crate::spc::RuleSet,
) -> Result<XbarRChartDto, WireError> {
use crate::spc::{ControlChart, XBarRChart};
let n = subgroup_size(&subgroups)?;
let mut chart = XBarRChart::new(n)
.map_err(|e| WireError::chart("subgroups", None, &e))?
.with_rules(rules);
add_rows(&subgroups, "subgroups", |g| chart.add_sample(g))?;
let x = chart
.control_limits()
.ok_or("insufficient data for control limits")?;
let r = chart
.r_limits()
.ok_or("insufficient data for R chart limits")?;
Ok(XbarRChartDto {
xbar_cl: x.cl,
xbar_ucl: x.ucl,
xbar_lcl: x.lcl,
r_cl: r.cl,
r_ucl: r.ucl,
r_lcl: r.lcl,
sigma_hat: chart.sigma_hat(),
xbar_points: point_dtos(chart.points()),
r_points: point_dtos(chart.r_points()),
in_control: chart.is_in_control(),
})
}
pub(crate) fn p_chart_dto(
samples: &[(u64, u64)],
standard: &AttributeStandardDto,
) -> Result<PChartDto, WireError> {
use crate::spc::PChart;
standard.refuse(
"p_chart",
&[
("u_bar", standard.u_bar.is_some()),
("phi", standard.phi.is_some()),
],
)?;
let mut chart = match standard.p_bar {
Some(p) => PChart::with_center(p).map_err(|e| WireError::chart("options", None, &e))?,
None => PChart::new(),
};
add_rows(samples, "samples", |&(d, n)| chart.add_sample(d, n))?;
let p_bar = chart
.p_bar()
.ok_or_else(|| WireError::too_few_samples("samples: at least 1 sample is needed"))?;
Ok(PChartDto {
p_bar,
points: attribute_point_dtos(chart.points()),
in_control: chart.is_in_control(),
})
}
pub(crate) fn laney_p_dto(
samples: &[(u64, u64)],
standard: &AttributeStandardDto,
) -> Result<LaneyPChartDto, WireError> {
standard.refuse("laney_p_chart", &[("u_bar", standard.u_bar.is_some())])?;
let laney = standard.laney(standard.p_bar, "p_bar")?;
let chart = crate::spc::laney_p_chart(samples, laney)
.map_err(|e| WireError::chart_input("samples", &e))?;
Ok(LaneyPChartDto {
p_bar: chart.p_bar,
phi: chart.phi,
points: laney_point_dtos(&chart.points),
})
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct CapabilityInputDto {
pub(crate) data: Vec<f64>,
#[serde(default)]
pub(crate) usl: Option<f64>,
#[serde(default)]
pub(crate) lsl: Option<f64>,
#[serde(default)]
pub(crate) sigma_within: Option<f64>,
#[serde(default)]
pub(crate) target: Option<f64>,
}
#[derive(Serialize, Debug)]
pub(crate) struct CapabilityDto {
pub(crate) mean: f64,
pub(crate) sigma_source: &'static str,
pub(crate) std_dev_within: Option<f64>,
pub(crate) std_dev_overall: f64,
pub(crate) cp: Option<f64>,
pub(crate) cpk: Option<f64>,
pub(crate) cpu: Option<f64>,
pub(crate) cpl: Option<f64>,
pub(crate) pp: Option<f64>,
pub(crate) ppk: Option<f64>,
pub(crate) ppu: Option<f64>,
pub(crate) ppl: Option<f64>,
pub(crate) cpm: Option<f64>,
}
pub(crate) fn capability_dto(input: CapabilityInputDto) -> Result<CapabilityDto, String> {
use crate::capability::ProcessCapability;
let mut spec = ProcessCapability::new(input.usl, input.lsl)
.map_err(|e| format!("invalid specification limits: {e}"))?;
if let Some(target) = input.target {
if !target.is_finite() {
return Err("target must be finite".to_string());
}
spec = spec.with_target(target);
}
let indices = match input.sigma_within {
Some(sigma_within) => {
if !sigma_within.is_finite() || sigma_within <= 0.0 {
return Err("sigma_within must be a positive, finite number \
(R-bar/d2 or S-bar/c4 from the control chart)"
.to_string());
}
spec.compute(&input.data, sigma_within)
}
None => spec.compute_overall(&input.data),
}
.ok_or("insufficient or invalid data (need >= 2 finite values)")?;
let dto = CapabilityDto {
mean: indices.mean,
sigma_source: if indices.std_dev_within.is_some() {
"within"
} else {
"overall"
},
std_dev_within: indices.std_dev_within,
std_dev_overall: indices.std_dev_overall,
cp: indices.cp,
cpk: indices.cpk,
cpu: indices.cpu,
cpl: indices.cpl,
pp: indices.pp,
ppk: indices.ppk,
ppu: indices.ppu,
ppl: indices.ppl,
cpm: indices.cpm,
};
Ok(dto)
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct PercentileCapabilityInputDto {
pub(crate) data: Vec<f64>,
pub(crate) lsl: Option<f64>,
pub(crate) usl: Option<f64>,
}
#[derive(Serialize)]
pub(crate) struct PercentileCapabilityDto {
pub(crate) cp_star: Option<f64>,
pub(crate) cpk_star: Option<f64>,
pub(crate) cpu_star: Option<f64>,
pub(crate) cpl_star: Option<f64>,
pub(crate) median: f64,
pub(crate) percentile_lower: f64,
pub(crate) percentile_upper: f64,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct GageRRInputDto {
pub(crate) measurements: Vec<Vec<Vec<f64>>>,
pub(crate) tolerance: Option<f64>,
}
#[derive(Serialize)]
pub(crate) struct GageRRResultDto {
pub(crate) ev: f64,
pub(crate) av: f64,
pub(crate) grr: f64,
pub(crate) pv: f64,
pub(crate) tv: f64,
pub(crate) percent_ev: f64,
pub(crate) percent_av: f64,
pub(crate) percent_grr: f64,
pub(crate) percent_pv: f64,
pub(crate) percent_tolerance: Option<f64>,
pub(crate) ndc: u32,
pub(crate) status: String,
}
#[derive(Serialize)]
pub(crate) struct GageRRAnovaResultDto {
pub(crate) anova_table: Vec<AnovaRowDto>,
pub(crate) variance_components: VarianceComponentsDto,
pub(crate) ev: f64,
pub(crate) av: f64,
pub(crate) grr: f64,
pub(crate) pv: f64,
pub(crate) tv: f64,
pub(crate) percent_grr: f64,
pub(crate) percent_tolerance: Option<f64>,
pub(crate) ndc: u32,
pub(crate) status: String,
pub(crate) interaction_significant: bool,
pub(crate) interaction_pooled: bool,
}
#[derive(Serialize)]
pub(crate) struct AnovaRowDto {
pub(crate) source: String,
pub(crate) df: f64,
pub(crate) ss: f64,
pub(crate) ms: f64,
pub(crate) f_value: Option<f64>,
pub(crate) p_value: Option<f64>,
}
#[derive(Serialize)]
pub(crate) struct VarianceComponentsDto {
pub(crate) part: f64,
pub(crate) operator: f64,
pub(crate) interaction: f64,
pub(crate) repeatability: f64,
pub(crate) reproducibility: f64,
pub(crate) total: f64,
}
pub(crate) fn grr_status_str(status: crate::msa::GrrStatus) -> &'static str {
match status {
crate::msa::GrrStatus::Acceptable => "Acceptable",
crate::msa::GrrStatus::Marginal => "Marginal",
crate::msa::GrrStatus::Unacceptable => "Unacceptable",
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct PeltInputDto {
pub(crate) data: Vec<f64>,
#[serde(default = "default_cost")]
pub(crate) cost: String,
#[serde(default = "default_penalty")]
pub(crate) penalty: PeltPenaltyDto,
#[serde(default = "default_min_seg")]
pub(crate) min_segment_len: usize,
}
#[derive(Deserialize)]
#[serde(untagged)]
pub(crate) enum PeltPenaltyDto {
Named(String),
Value(f64),
}
#[derive(Serialize)]
pub(crate) struct PeltResultDto {
pub(crate) changepoints: Vec<usize>,
pub(crate) n_segments: usize,
}
pub(crate) fn default_cost() -> String {
"l2".to_owned()
}
pub(crate) fn default_penalty() -> PeltPenaltyDto {
PeltPenaltyDto::Named("bic".to_owned())
}
pub(crate) fn default_min_seg() -> usize {
2
}
pub(crate) fn percentile_capability_dto(
input: PercentileCapabilityInputDto,
) -> Result<PercentileCapabilityDto, String> {
let result = crate::capability::percentile_capability(&input.data, input.lsl, input.usl)
.map_err(|e| e.to_string())?;
Ok(PercentileCapabilityDto {
cp_star: result.cp_star,
cpk_star: result.cpk_star,
cpu_star: result.cpu_star,
cpl_star: result.cpl_star,
median: result.median,
percentile_lower: result.percentile_lower,
percentile_upper: result.percentile_upper,
})
}
pub(crate) fn gage_rr_xbar_r_dto(dto: GageRRInputDto) -> Result<GageRRResultDto, String> {
let input = crate::msa::GageRRInput {
measurements: dto.measurements,
tolerance: dto.tolerance,
};
let result = crate::msa::gage_rr_xbar_r(&input).map_err(|e| e.to_string())?;
Ok(GageRRResultDto {
ev: result.ev,
av: result.av,
grr: result.grr,
pv: result.pv,
tv: result.tv,
percent_ev: result.percent_ev,
percent_av: result.percent_av,
percent_grr: result.percent_grr,
percent_pv: result.percent_pv,
percent_tolerance: result.percent_tolerance,
ndc: result.ndc,
status: grr_status_str(result.status).to_owned(),
})
}
pub(crate) fn gage_rr_anova_dto(dto: GageRRInputDto) -> Result<GageRRAnovaResultDto, String> {
let input = crate::msa::GageRRInput {
measurements: dto.measurements,
tolerance: dto.tolerance,
};
let result = crate::msa::gage_rr_anova(&input).map_err(|e| e.to_string())?;
let anova_table = result
.anova_table
.rows
.iter()
.map(|r| AnovaRowDto {
source: r.source.clone(),
df: r.df,
ss: r.ss,
ms: r.ms,
f_value: r.f_value,
p_value: r.p_value,
})
.collect();
let vc = &result.variance_components;
Ok(GageRRAnovaResultDto {
anova_table,
variance_components: VarianceComponentsDto {
part: vc.part,
operator: vc.operator,
interaction: vc.interaction,
repeatability: vc.repeatability,
reproducibility: vc.reproducibility,
total: vc.total,
},
ev: result.ev,
av: result.av,
grr: result.grr,
pv: result.pv,
tv: result.tv,
percent_grr: result.percent_grr,
percent_tolerance: result.percent_tolerance,
ndc: result.ndc,
status: grr_status_str(result.status).to_owned(),
interaction_significant: result.interaction_significant,
interaction_pooled: result.interaction_pooled,
})
}
pub(crate) fn pelt_dto(input: PeltInputDto) -> Result<PeltResultDto, String> {
if input.data.is_empty() {
return Err("data must not be empty".to_owned());
}
let cost = match input.cost.as_str() {
"l2" => crate::detection::CostFunction::L2,
"normal" => crate::detection::CostFunction::Normal,
other => return Err(format!("unknown cost function: {other}")),
};
let penalty = match input.penalty {
PeltPenaltyDto::Named(ref s) if s == "bic" => crate::detection::Penalty::Bic,
PeltPenaltyDto::Named(ref s) => return Err(format!("unknown penalty: {s}")),
PeltPenaltyDto::Value(v) => crate::detection::Penalty::Custom(v),
};
let pelt = crate::detection::Pelt::with_min_segment_len(cost, penalty, input.min_segment_len)
.ok_or("invalid parameters (penalty must be positive, min_segment_len >= 2)")?;
let result = pelt.detect(&input.data);
Ok(PeltResultDto {
n_segments: result.changepoints.len() + 1,
changepoints: result.changepoints,
})
}
#[derive(Serialize, Debug)]
pub(crate) struct XbarSChartDto {
pub(crate) xbar_cl: f64,
pub(crate) xbar_ucl: f64,
pub(crate) xbar_lcl: f64,
pub(crate) s_cl: f64,
pub(crate) s_ucl: f64,
pub(crate) s_lcl: f64,
pub(crate) sigma_hat: Option<f64>,
pub(crate) xbar_points: Vec<ChartPointDto>,
pub(crate) s_points: Vec<ChartPointDto>,
pub(crate) in_control: bool,
}
#[derive(Serialize, Debug)]
pub(crate) struct ImrChartDto {
pub(crate) i_cl: f64,
pub(crate) i_ucl: f64,
pub(crate) i_lcl: f64,
pub(crate) mr_cl: f64,
pub(crate) mr_ucl: f64,
pub(crate) mr_lcl: f64,
pub(crate) sigma_hat: Option<f64>,
pub(crate) i_points: Vec<ChartPointDto>,
pub(crate) mr_points: Vec<ChartPointDto>,
pub(crate) in_control: bool,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct LimitsInputDto {
pub(crate) ucl: f64,
pub(crate) cl: f64,
pub(crate) lcl: f64,
}
pub(crate) fn xbar_s_dto(
subgroups: Vec<Vec<f64>>,
rules: crate::spc::RuleSet,
) -> Result<XbarSChartDto, WireError> {
use crate::spc::{ControlChart, XBarSChart};
let n = subgroup_size(&subgroups)?;
let mut chart = XBarSChart::new(n)
.map_err(|e| WireError::chart("subgroups", None, &e))?
.with_rules(rules);
add_rows(&subgroups, "subgroups", |g| chart.add_sample(g))?;
let x = chart
.control_limits()
.ok_or("insufficient data for control limits")?;
let s = chart
.s_limits()
.ok_or("insufficient data for S chart limits")?;
Ok(XbarSChartDto {
xbar_cl: x.cl,
xbar_ucl: x.ucl,
xbar_lcl: x.lcl,
s_cl: s.cl,
s_ucl: s.ucl,
s_lcl: s.lcl,
sigma_hat: chart.sigma_hat(),
xbar_points: point_dtos(chart.points()),
s_points: point_dtos(chart.s_points()),
in_control: chart.is_in_control(),
})
}
pub(crate) fn imr_dto(
values: Vec<f64>,
rules: crate::spc::RuleSet,
) -> Result<ImrChartDto, WireError> {
use crate::spc::{ControlChart, IndividualMRChart};
let mut chart = IndividualMRChart::new().with_rules(rules);
add_rows(&values, "values", |x| {
chart.add_sample(std::slice::from_ref(x))
})?;
let i = chart
.control_limits()
.ok_or("at least two values are needed for control limits")?;
let mr = chart
.mr_limits()
.ok_or("at least two values are needed for control limits")?;
Ok(ImrChartDto {
i_cl: i.cl,
i_ucl: i.ucl,
i_lcl: i.lcl,
mr_cl: mr.cl,
mr_ucl: mr.ucl,
mr_lcl: mr.lcl,
sigma_hat: chart.sigma_hat(),
i_points: point_dtos(chart.points()),
mr_points: point_dtos(chart.mr_points()),
in_control: chart.is_in_control(),
})
}
pub(crate) fn run_rules_dto(
values: Vec<f64>,
limits: LimitsInputDto,
rules: crate::spc::RuleSet,
) -> Result<Vec<ChartPointDto>, String> {
use crate::spc::{ChartPoint, ControlLimits, RunRule};
let LimitsInputDto { ucl, cl, lcl } = limits;
if !(lcl <= cl && cl <= ucl) {
return Err(format!(
"limits: need lcl <= cl <= ucl, got lcl={lcl} cl={cl} ucl={ucl}"
));
}
if let Some(i) = values.iter().position(|x| !x.is_finite()) {
return Err(format!("values[{i}] is not a finite number"));
}
let mut points: Vec<ChartPoint> = values
.iter()
.enumerate()
.map(|(index, &value)| ChartPoint {
value,
index,
violations: Vec::new(),
})
.collect();
for (index, rule) in rules.check(&points, &ControlLimits { ucl, cl, lcl }) {
if let Some(point) = points.get_mut(index) {
point.violations.push(rule);
}
}
Ok(point_dtos(&points))
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct SeasonalityInputDto {
pub(crate) data: Vec<f64>,
}
#[derive(Serialize, Debug)]
pub(crate) struct PeriodCandidateDto {
pub(crate) period: usize,
pub(crate) acf: f64,
pub(crate) bin: usize,
pub(crate) power: f64,
pub(crate) power_share: f64,
}
#[derive(Serialize, Debug)]
pub(crate) struct SeasonalityDto {
pub(crate) period: Option<usize>,
pub(crate) candidates: Vec<PeriodCandidateDto>,
pub(crate) n: usize,
pub(crate) acf_threshold: f64,
pub(crate) power_threshold: f64,
}
pub(crate) fn seasonality_dto(input: SeasonalityInputDto) -> Result<SeasonalityDto, String> {
if let Some(i) = input.data.iter().position(|x| !x.is_finite()) {
return Err(format!("data[{i}] is not a finite number"));
}
let r = crate::seasonality::estimate_period(&input.data).ok_or_else(|| {
format!(
"data must have at least {} observations, got {}",
crate::seasonality::MIN_OBSERVATIONS,
input.data.len()
)
})?;
Ok(SeasonalityDto {
period: r.period,
candidates: r
.candidates
.into_iter()
.map(|c| PeriodCandidateDto {
period: c.period,
acf: c.acf,
bin: c.bin,
power: c.power,
power_share: c.power_share,
})
.collect(),
n: r.n,
acf_threshold: r.acf_threshold,
power_threshold: r.power_threshold,
})
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct SpectralResidualInputDto {
pub(crate) data: Vec<f64>,
#[serde(default)]
pub(crate) averaging_window: Option<usize>,
#[serde(default)]
pub(crate) judgement_window: Option<usize>,
#[serde(default)]
pub(crate) threshold: Option<f64>,
#[serde(default)]
pub(crate) min_zscore: Option<f64>,
#[serde(default)]
pub(crate) sensitivity: Option<f64>,
#[serde(default)]
pub(crate) batch_size: Option<usize>,
}
#[derive(Serialize, Debug)]
pub(crate) struct SrPointDto {
pub(crate) index: usize,
pub(crate) value: f64,
pub(crate) saliency: f64,
pub(crate) score: f64,
pub(crate) expected: f64,
pub(crate) lower: f64,
pub(crate) upper: f64,
pub(crate) is_anomaly: bool,
}
#[derive(Serialize, Debug)]
pub(crate) struct SpectralResidualDto {
pub(crate) points: Vec<SrPointDto>,
pub(crate) anomalies: Vec<usize>,
}
pub(crate) fn spectral_residual_dto(
input: SpectralResidualInputDto,
) -> Result<SpectralResidualDto, String> {
if let Some(i) = input.data.iter().position(|x| !x.is_finite()) {
return Err(format!("data[{i}] is not a finite number"));
}
let mut sr = crate::detection::SpectralResidual::new();
if let Some(q) = input.averaging_window {
sr = sr.with_averaging_window(q);
}
if let Some(z) = input.judgement_window {
sr = sr.with_judgement_window(z);
}
if let Some(t) = input.threshold {
sr = sr.with_threshold(t);
}
if let Some(z) = input.min_zscore {
sr = sr.with_min_zscore(z);
}
if let Some(s) = input.sensitivity {
sr = sr.with_sensitivity(s);
}
if input.batch_size.is_some() {
sr = sr.with_batch_size(input.batch_size);
}
let points = sr.analyze(&input.data).ok_or_else(|| {
format!(
"invalid configuration or data (need at least {} finite values; averaging_window >= 1, \
judgement_window >= 1, threshold > 0, min_zscore >= 0, 0 < sensitivity < 100, \
batch_size >= {})",
crate::detection::SR_MIN_OBSERVATIONS,
crate::detection::SR_MIN_OBSERVATIONS
)
})?;
let anomalies = points
.iter()
.filter(|p| p.is_anomaly)
.map(|p| p.index)
.collect();
Ok(SpectralResidualDto {
points: points
.into_iter()
.map(|p| SrPointDto {
index: p.index,
value: p.value,
saliency: p.saliency,
score: p.score,
expected: p.expected,
lower: p.lower,
upper: p.upper,
is_anomaly: p.is_anomaly,
})
.collect(),
anomalies,
})
}
#[cfg(test)]
mod input_error_tests {
use super::code;
use super::*;
use serde_json::json;
fn err<T: std::fmt::Debug>(r: Result<T, WireError>) -> (&'static str, Option<usize>, String) {
let e = r.expect_err("must be refused");
(e.code, e.index, e.message)
}
#[test]
fn a_count_that_is_not_a_whole_number_is_refused_with_its_row() {
for bad in [json!(1.5), json!(-1), json!("3"), json!(null), json!(1e300)] {
let (c, i, m) = err(count_rows(&json!([2, 0, bad]), "defects"));
assert_eq!((c, i), (code::COUNT_NOT_WHOLE, Some(2)), "{m}");
assert!(m.starts_with("defects[2]"), "{m}");
}
let (c, i, _) = err(count_pairs(&json!([[1, 10], [2.5, 10]]), "samples"));
assert_eq!((c, i), (code::COUNT_NOT_WHOLE, Some(1)));
let (c, i, m) = err(count_pairs(&json!([[1, 10], [2, 10], [3, -4]]), "samples"));
assert_eq!((c, i), (code::COUNT_NOT_WHOLE, Some(2)));
assert!(m.contains("sample size"), "{m}");
let (c, i, _) = err(rate_pairs(&json!([[1, 1.0], [0.5, 2.0]]), "samples"));
assert_eq!((c, i), (code::COUNT_NOT_WHOLE, Some(1)));
let (c, i, _) = err(count_value(&json!(10.5), "sample_size"));
assert_eq!((c, i), (code::COUNT_NOT_WHOLE, None));
}
#[test]
fn whole_counts_written_as_floats_are_accepted() {
assert_eq!(
count_rows(&json!([3.0, 0, 7]), "c").expect("whole"),
vec![3, 0, 7]
);
assert_eq!(count_value(&json!(100.0), "n").expect("whole"), 100);
}
#[test]
fn a_row_that_is_not_a_pair_is_malformed_at_its_row() {
let (c, i, _) = err(count_pairs(&json!([[1, 10], [1, 2, 3]]), "samples"));
assert_eq!((c, i), (code::MALFORMED_INPUT, Some(1)));
let (c, i, _) = err(rate_pairs(&json!([[1, 1.0], 4]), "samples"));
assert_eq!((c, i), (code::MALFORMED_INPUT, Some(1)));
let (c, i, _) = err(count_pairs(&json!({ "samples": [] }), "samples"));
assert_eq!((c, i), (code::MALFORMED_INPUT, None));
let (c, i, _) = err(rate_pairs(&json!([[1, 1.0], [2, "x"]]), "samples"));
assert_eq!((c, i), (code::UNITS_NOT_POSITIVE, Some(1)));
}
#[test]
fn every_chart_error_has_its_own_code() {
use crate::spc::ControlChartError as E;
let all = [
E::SampleLengthMismatch {
expected: 2,
got: 3,
},
E::NonFiniteValue,
E::DefectivesExceedSampleSize {
defectives: 2,
sample_size: 1,
},
E::NonPositiveUnits,
E::SubgroupSizeOutOfRange {
got: 1,
min: 2,
max: 25,
},
E::ZeroSampleSize,
E::InvalidStandard { parameter: "phi" },
];
let codes: std::collections::HashSet<_> = all.iter().map(chart_error_code).collect();
assert_eq!(codes.len(), all.len(), "codes must tell the reasons apart");
assert!(!codes.contains(code::INVALID_INPUT));
}
#[test]
fn the_error_serializes_as_code_index_message() {
let e = WireError::new(code::COUNT_NOT_WHOLE, Some(4), "defects[4]: nope");
assert_eq!(
serde_json::to_value(&e).expect("serializable"),
json!({ "code": "count_not_whole", "index": 4, "message": "defects[4]: nope" })
);
}
fn standard(v: serde_json::Value) -> AttributeStandardDto {
serde_json::from_value(v).expect("valid options")
}
#[test]
fn phase_two_options_reach_the_chart_and_are_checked() {
let samples = [(11_u64, 170_u64)];
let d = laney_p_dto(
&samples,
&standard(json!({ "p_bar": 0.0514, "phi": 0.236 })),
)
.expect("one sample with a standard");
assert_eq!((d.p_bar, d.phi), (0.0514, 0.236));
assert!(d.points[0].out_of_control);
assert!(d.points[0].z.expect("scaled") > 3.0);
let d = p_chart_dto(&[(2, 100), (20, 100)], &standard(json!({ "p_bar": 0.05 })))
.expect("valid");
assert_eq!(d.p_bar, 0.05);
let (c, _, m) = err(laney_p_dto(&samples, &standard(json!({ "p_bar": 0.05 }))));
assert_eq!(c, code::INVALID_INPUT, "{m}");
let (c, _, m) = err(p_chart_dto(&samples, &standard(json!({ "phi": 1.0 }))));
assert_eq!(c, code::MALFORMED_INPUT, "{m}");
assert!(m.contains("phi"), "{m}");
let (c, i, _) = err(p_chart_dto(&samples, &standard(json!({ "p_bar": 1.5 }))));
assert_eq!((c, i), (code::STANDARD_OUT_OF_RANGE, None));
let (c, _, _) = err(laney_p_dto(
&samples,
&standard(json!({ "p_bar": 0.05, "phi": -2.0 })),
));
assert_eq!(c, code::STANDARD_OUT_OF_RANGE);
assert!(serde_json::from_value::<AttributeStandardDto>(json!({ "pbar": 0.1 })).is_err());
}
}