Initial commit: CMM Report Analyzer

FastAPI app that parses CMM inspection reports (PDF/Excel/CSV),
computes SPC metrics (Cp/Cpk/Pp/Ppk, control limits, Shapiro-Wilk),
generates interactive Plotly charts, and provides AI-powered quality
summaries via Azure OpenAI with graceful fallback.

Includes 21 passing tests covering parsers, SPC calculations, and
API endpoints.
This commit is contained in:
chrisryn
2026-02-19 10:38:51 -06:00
commit 9abf9b4b58
28 changed files with 1727 additions and 0 deletions

84
tests/test_spc.py Normal file
View File

@@ -0,0 +1,84 @@
from app.analysis.spc import SPCResult, calculate_spc
from app.parsers.models import MeasurementRecord
def _make_records(name: str, nominal: float, tol: float, actuals: list[float]):
return [
MeasurementRecord(
feature_name=name,
nominal=nominal,
tolerance_plus=tol,
tolerance_minus=-tol,
actual=a,
deviation=a - nominal,
)
for a in actuals
]
def test_single_measurement_returns_none_indices():
records = _make_records("D1", 10.0, 0.05, [10.01])
results = calculate_spc(records)
assert len(results) == 1
r = results[0]
assert r.cp is None
assert r.cpk is None
assert r.pp is None
assert r.ppk is None
def test_basic_spc_calculation():
actuals = [10.01, 10.02, 9.99, 10.00, 10.03, 9.98, 10.01, 10.02, 9.99, 10.00]
records = _make_records("D1", 10.0, 0.05, actuals)
results = calculate_spc(records)
assert len(results) == 1
r = results[0]
assert r.n == 10
assert r.pp is not None
assert r.ppk is not None
assert r.cp is not None
assert r.cpk is not None
assert r.pp > 0
assert r.cpk > 0
assert r.out_of_spec_count == 0
def test_out_of_spec_count():
actuals = [10.0, 10.06, 9.94, 10.0, 10.0] # 10.06 and 9.94 outside ±0.05
records = _make_records("D1", 10.0, 0.05, actuals)
results = calculate_spc(records)
assert results[0].out_of_spec_count == 2
def test_multiple_features():
records = (
_make_records("D1", 10.0, 0.05, [10.01, 10.02, 9.99])
+ _make_records("D2", 20.0, 0.10, [20.05, 19.95, 20.01])
)
results = calculate_spc(records)
assert len(results) == 2
names = {r.feature_name for r in results}
assert names == {"D1", "D2"}
def test_shapiro_not_computed_for_small_n():
records = _make_records("D1", 10.0, 0.05, [10.01, 10.02])
results = calculate_spc(records)
assert results[0].shapiro_p is None
def test_shapiro_computed_for_n_ge_3():
records = _make_records("D1", 10.0, 0.05, [10.01, 10.02, 9.99])
results = calculate_spc(records)
assert results[0].shapiro_p is not None
def test_to_dict():
records = _make_records("D1", 10.0, 0.05, [10.01, 10.02, 9.99, 10.00, 10.03])
results = calculate_spc(records)
d = results[0].to_dict()
assert "feature_name" in d
assert "cpk" in d
assert "values" in d
assert isinstance(d["values"], list)