Skill: williamhyzhu/rohoon-6sigma (version 1.8.2)

--- name: rohoon-6sigma description: Rohoon Six Sigma professional support. Based on AIAG-VDA SPC Manual, MSA Manual, and Six Sigma/Lean methodology. Provides: DMAIC/DMADV tools, SPC control charts, process capability indices (Pm/Pmk, Pp/Ppk, Cp/Cpk), MSA analysis (GR&R/Bias/Linearity/Stability), DOE (Full/Partial Factorial/RSM/Taguchi), FMEA, 5S/VSM, and more. For automotive quality management, process optimization, supplier assessment. --- # Rohoon Six Sigma Professional Support 本技能提供基于 **AIAG-VDA SPC Manual (Yellow Volume) 第 1 版，2026 年 2 月**、**AIAG MSA Manual 第 4 版**、**ISO 13053 六西格玛标准** 和 **精益方法论** 的全面质量管理支持。 ## 🎯 Quick Navigation | Need | Reference | Core Scripts | |------|-----------|--------------| | **🇨🇳 GB/T 36077-2025 国标评价** | [本 SKILL.md 下方](#gbt-36077-2025-中国国家标准评价框架) | - | | **DMAIC/DMADV Tools** | [lean-six-sigma-tools.md](references/lean-six-sigma-tools.md) | - | | **DOE Design of Experiments** | [doe-guide.md](references/doe-guide.md) | `doe_full_factorial.py`, `doe_factor_effects.py`, `doe_sn_ratio.py` | | **SPC Control Charts** | [control-charts.md](references/control-charts.md) | `control_chart.py`, `advanced_control_charts.py` | | **Process Capability** | [capability-indices.md](references/capability-indices.md) | `calculate_capability.py`, `excel_report.py` | | **MSA Analysis** | [msa-reference.md](references/msa-reference.md) | `msa_grr_analysis.py`, `msa_other_studies.py` | | **Advanced Analysis** | [lean-six-sigma-tools.md](references/lean-six-sigma-tools.md) | `advanced_control_charts.py`, `pooled_std.py` | ## Quick Start ### 📊 AIAG-VDA Standard Reports (NEW! ✨) **⚠️ 重要 - 报告格式稳定性** Figure 12-1/12-2/12-3 PDF 报告遵循特定的标准格式，**除非用户明确指示，否则不要修改布局、字体、表格宽度或格式**。 - 当前标准格式： - 英文版：`tmp/YOUR_ORIGINAL_EN.pdf` (210,401 bytes) - 中文版：`tmp/YOUR_ORIGINAL_v2.pdf` (258,583 bytes) - 格式基于 AIAG-VDA SPC Manual 标准 - 如需更改格式，请先询问用户 - 标准格式参考 PDF： - 英文：`references/AIAG_VDA_Standard_EN_Format.pdf` - 中文：`references/AIAG_VDA_Standard_ZH_Format.pdf` **统一报告生成器** - 严格遵循黄皮书标准格式 ```bash # Figure 12-1: 正态分布报告 (中文) python3 scripts/aiagvda_unified_report.py -o report.pdf -f 12-1 -l zh # Figure 12-1: Normal Distribution Report (English) python3 scripts/aiagvda_unified_report.py -o report.pdf -f 12-1 -l en # Figure 12-2: 非正态/混合分布报告 python3 scripts/aiagvda_unified_report.py -o report.pdf -f 12-2 -l zh # Figure 12-3: 标准统计表格报告 python3 scripts/aiagvda_unified_report.py -o report.pdf -f 12-3 -l zh ``` **报告特点**: - 20-22 个编号元素 (严格匹配黄皮书格式) - 中英文双语支持 - 几何法能力指数 (Cp,G / Cpk,G) - 95% 置信区间 - PPM 计算 - 专业图表 (直方图、原始值图、概率图、控制图) ### 📊 Process Capability Analysis ```bash # Two-sided specification python3 scripts/calculate_capability.py \ --data "10.1,10.2,10.15,10.18,10.12,10.05,10.22,10.08,10.14,10.19" \ --usl 10.5 --lsl 9.5 --json # Only upper specification python3 scripts/calculate_capability.py \ --data "..." --usl 10.5 --json ``` ### 📈 Control Charts ```bash # Xbar-R chart (n=5) python3 scripts/control_chart.py \ --data "[10.1,10.2,10.15,10.18,10.12, 10.05,10.22,10.08,10.14,10.19]" \ --chart-type Xbar-R --subgroup-size 5 --json # I-MR chart (individual values) python3 scripts/control_chart.py \ --data "10.1,10.2,10.15,10.18,10.12" \ --chart-type I-MR --json ``` ### 📏 MSA GR&R Study ```bash # 10 parts × 3 operators × 3 trials = 90 measurements python3 scripts/msa_grr_analysis.py \ --data "[10.1,10.15,10.12, ...]" \ --parts 10 --operators 3 --trials 3 \ --tolerance 0.5 --json ``` ### 🧪 DOE Full Factorial ```bash # 2^2 full factorial design python3 scripts/doe_full_factorial.py \ --factors 2 \ --responses "[40,50,45,55]" \ --analyze # 2^3 with center points python3 scripts/doe_full_factorial.py \ --factors 3 \ --responses "[45,52,48,58,46,53,49,59]" \ --center-points 3 \ --analyze # Effect calculation and visualization python3 scripts/doe_factor_effects.py \ --design "2^3" \ --responses "[...]" \ --plot main-effects interaction ``` ### 🧪 Taguchi S/N Ratios ```bash # S/N ratio calculation python3 scripts/doe_sn_ratio.py \ --responses "[45,52,48,...]" \ --type larger-is-better \ --json ``` ### 📊 Response Surface ```bash # Response Surface (CCD / Box-Behnken) python3 scripts/doe_response_surface.py --help ``` ### 📊 Excel Report Generation ```bash # Excel report python3 scripts/excel_report.py --help ``` ## 🇨🇳 GB/T 36077-2025 中国国家标准评价框架 ### 七大评价维度 (总分 1000 分) | 类目 | 条款 | 分值 | 核心要求 | |------|------|------|----------| | **4.1 领导力** | 4.1.1 使命/愿景/价值观/战略 | 100 | 高层领导确立愿景价值观，制定 LSS 战略支撑组织战略 | | | 4.1.2 高层领导推进作用 | | 承诺参与、资源支持 (设施/资金/人力/时间/激励) | | **4.2 顾客驱动** | 4.2.1 顾客需求分析 | 80 | 建立动态顾客需求收集分析体系，预测未来需求 | | | 4.2.2 顾客响应 | | 建立响应机制，将 VOC 转化为 CTQ | | | 4.2.3 顾客满意测评 | | 建立测评体系，识别改进机会 | | | 4.2.4 顾客导向指标体系 | | 建立可量化指标体系，分解到职能部门 | | **4.3 推进规划** | 4.3.1 推进规划制定 | 80 | 目标/计划/措施/资源配置，与组织战略契合 | | | 4.3.2 推进规划部署 | | 自上而下展开，设定绩效指标 | | **4.4 项目管理** | 4.4.1 项目选择 | 180 | 从战略层面识别改进机会，符合 SMART 原则 | | | 4.4.2 项目团队 | | 跨职能团队，分工明确，融洽合作 | | | 4.4.3 技术路线和工具方法 | | DMAIC/DLSS 结构化路线，选用适当工具 | | | 4.4.4 项目计划与实施 | | 详细工作计划，节点评审 | | | 4.4.5 项目成果测评 | | 硬收益 (财务) + 软收益 (创新/推广/社会价值) | | | 4.4.6 项目团队合作评估 | | 团队绩效评估，成员贡献评价 | | **4.5 评价与激励** | 4.5.1 推进绩效评价 | 100 | 对部门/人员绩效评价，及时改进 | | | 4.5.2 激励机制 | | 奖励认可制度 + 职业发展规划 (资深黑带/黑带/绿带/黄带) | | **4.6 基础架构** | 4.6.1 推进机构 | 220 | 推进委员会/倡导者/责任部门 | | | 4.6.2 管理制度与流程 | | 系统化文件化制度，与现有体系融合 | | | 4.6.3 培训体系 | | 分层培训体系 (高层/倡导者/黑带/绿带/黄带/员工) | | | 4.6.4 沟通交流与员工参与 | | 内部沟通 + 外部交流 + 一线员工参与 | | | 4.6.5 基础数据管理 | | 数据收集系统，测量系统能力，数据治理 | | | 4.6.6 管理信息系统 | | 项目管理平台 + 知识管理智能化系统 (AI) | | | 4.6.7 产业链供应链推广 | | 在供应链上开展 LSS 项目，提供培训辅导 | | **4.7 实施成果** | 4.7.2 顾客满意 | 240 | 满意度/忠诚度/抱怨/推荐 | | | 4.7.3 财务收益 | | 成本降低/利润率/回报率/资金周转率 | | | 4.7.4 人力资源 | | 带级人员数量/占比/流失率，员工满意度 | | | 4.7.5 业务过程 | | 质量/效率/合格率/周期/成本/环境/安全 | | | 4.7.6 相关方 | | 供货合格率/及时交货率/成本 | | | 4.7.7 组织文化与管理变革 | | 理念认同度/活动普及率 | ### DMAIC 各阶段输入、主要工作和输出 | 阶段 | 输入 | 主要工作 | 输出 | |------|------|----------|------| | **D 界定** | 组织战略、VOC、顾客投诉、KPI、竞争对手比较 | 确定改进机会、团队、问题描述、边界、流程图、CTQ | 改进机会、项目目标、团队任务书、SIPOC 图、项目计划 | | **M 测量** | VOC、KPI 分解、流程、CTQ、SIPOC | 确定测量对象/方法/指标、数据清洗、MSA、过程业绩测量 | 测量指标方法、数据收集方案、MSA 报告、过程质量水平 | | **A 分析** | 过程测量数据、MSA 报告、过程能力报告 | 测量数据分析、流程分析、根因分析、变异源分析、价值流分析 | 根因分析报告、变异源报告、价值流报告、数据分析报告 | | **I 改进** | 原因分析报告、关键影响因素 | 改进策略、流程优化、方案评价与实施验证 | 改进备选方案、评价指标、实施评价报告 | | **C 控制** | 改进方案、实施评价报告 | 确定控制对象、控制方法、更新文件、OCAP、知识库 | 控制方案、程序文件、OCAP、效果评价、推广计划 | ### DMADV (DLSS) 各阶段输入、主要工作和输出 | 阶段 | 输入 | 主要工作 | 输出 | |------|------|----------|------| | **D 界定** | 组织战略、产品/技术规划、新产品需求、VOC | 确定项目机会、跨职能团队、范围、CTQ、失效模式分析、风险评估 | 设计目标、团队任务书、CTQ 计分卡、可行性报告、项目计划 | | **M 测量** | 团队任务书、CTQ、CTQ 计分卡 | VOC 展开为设计特征、MSA、过程能力分析、流程映射 | MSA 报告、过程能力报告、CTQ 与 CTP 测量数据、工艺流程图 | | **A 分析** | 问题分析报告、CTQ 与 CTP 数据、工艺流程图 | 原因分析、FTA、概念设计、方案生成与筛选、可靠性分析 | 根因报告、概念设计方案、方案评估表、风险分析报告、CTQ 展开图 | | **D 设计** | 概念设计方案、方案评估表、风险分析报告 | 系统/子系统设计、DFMEA、参数设计、公差设计、原型机制作 | 详细设计文档、参数/公差报告、实验计划、原型机、实验报告 | | **V 验证** | 详细设计文档、原型机、实验报告 | 验证计划、试生产、供应链确认、目标验证、过程能力验证 | 完整设计报告、试生产报告、控制计划、标准化文档、项目移交书 | ### 精益六西格玛项目评分表 (满分 110 分) | 评价维度 | 分值 | 评价标准要点 | |----------|------|--------------| | **项目选择** | 20 分 | 项目来源清晰 (10 分)、目标符合 SMART(5 分)、范围与团队合作 (5 分) | | **项目的逻辑方法** | 30 分 | 逻辑思路 (15 分)、工具方法 (15 分) | | **项目收益** | 15 分 | 目标达成、收益计算科学合理、高层认可 | | **项目的标准化和推广应用** | 15 分 | 标准化 (5 分)、示范推广 (10 分) | | **项目的创新性** | 10 分 | 选题新颖 (5 分)、工具方法创新 (5 分) | | **申报资料** | 10 分 | 完整性和质量 | | **发表效果** | 10 分 | 现场表现和发表效果 (仅项目发布时) | ### 常用工具与方法 (GB/T 36077-2025 附录 B) #### DMAIC 各阶段工具 - **D 界定**: 头脑风暴、亲和图、树图、流程图、SIPOC、平衡计分卡、水平对比、QFD、甘特图 - **M 测量**: 因果图、散点图、MSA、过程能力分析、因果矩阵、FTA、箱线图、直方图、抽样 - **A 分析**: 假设检验、方差分析、统计分布识别、时间序列分析、价值流图、FMEA、回归分析、大数据分析、机器学习 - **I 改进**: 试验设计、数学优化、EVOP、目视管理、5S、ECRS 原则、看板、快速换型、TOC、TRIZ - **C 控制**: SPC、防错、定置管理、SOP、控制计划、OCAP #### DMADV 各阶段工具 - **D 界定**: 顾客需求分析、市场调查、KANO 分析、QFD、多属性决策、设计计分卡 - **M 测量**: 关系矩阵、因果图、MSA、FMEA、过程能力分析、IPO 分析、工艺计分卡 - **A 分析**: 头脑风暴、FMEA、方差分析、试验设计、TRIZ、大数据分析、机器学习、FTA - **D 设计**: TRIZ、DFx、公理化设计、试验设计、稳健性设计、田口方法、DFMEA、数字化仿真 - **V 验证**: 模拟技术、过程能力分析、假设检验、试验验证、可靠性分析、PFMEA、SPC ### 核心术语定义 (GB/T 36077-2025) | 术语 | 定义 | |------|------| | **精益六西格玛 (LSS)** | 精益和六西格玛的融合，系统化结构化业务改进与创新模式，减少波动、消除浪费、提高质量效率 | | **精益 (Lean)** | 通过持续改进，识别和消除产品/服务/流程中的浪费/非增值作业的理念和方法 | | **六西格玛 (Six Sigma)** | 采用统计技术及其他科学方法减少波动、降低缺陷、提升顾客满意的战略性业务改进系统方法 | | **精益六西格玛设计 (DLSS)** | 针对顾客需求/痛点，在产品/服务/流程开发源头进行消除缺陷和减少浪费的创新设计 | | **DMAIC** | 界定 (Define)、测量 (Measure)、分析 (Analyze)、改进 (Improve)、控制 (Control) | | **DMADV** | 界定 (Define)、测量 (Measure)、分析 (Analyze)、设计 (Design)、验证 (Verify) | | **倡导者 (Champion)** | 高层领导担任的推进负责人，负责规划制定、项目选择、资源分配、组织协调 | | **资深黑带 (MBB)** | 方法和工具应用专家，协助倡导者制定规划、选择和评审项目、培训指导黑带/绿带/黄带 | | **黑带 (BB)** | 系统掌握 LSS 方法，负责/参与项目，培训指导绿带/黄带，至少完成 2 个黑带项目 | | **绿带 (GB)** | 掌握基本方法，负责/参与项目，培训指导黄带，至少完成 1 个绿带项目 | | **黄带 (YB)** | 掌握基本理念和工具，负责小规模项目或参与项目 | --- ## Core Features ### 1. SPC Statistical Process Control ✅ - **7 Control Charts**: Xbar-R, Xbar-S, I-MR, CUSUM, EWMA, p/np, c/u - **8 AIAG-VDA Out-of-Control Rules**: Automatic detection - **🆕 Western Electric 7 Rules**: Comprehensive rule set for anomaly detection - **Process Capability**: Cp, Cpk, Pp, Ppk, Pm, Pmk (Geometric Method) - **🆕 Sigma Estimation**: Range (R/d₂), Std Dev (s/c₄), Pooled Std Dev (s_p) - **Normality Tests**: Shapiro-Wilk, Anderson-Darling - **🆕 Distribution Selection**: Normal, Binomial, Poisson, Weibull guidance - **🆕 Nonparametric Control Charts**: Distribution-free MEC/MCE charts (Ansari-Bradley) - **🆕 Change-Point Detection**: Single and multiple change-point monitoring - **🆕 Multivariate SPC**: MCUSUM, MEWMA, LASSO-based charts - **🆕 Profile Monitoring**: Linear and nonlinear profile analysis - **🆕 Short Production Runs**: Standardized Z-MR charts for high-mix low-volume - **🆕 Montgomery Coefficients**: A₂, D₃, D₄, d₂, c₄ tables (n=2 to 25) ### 2. MSA Measurement System Analysis ✅ - **GR&R Studies**: ANOVA method, Xbar-R method - **Bias Study**: Accuracy assessment - **Linearity Study**: Range consistency - **Stability Study**: Time drift monitoring ### 3. DOE Design of Experiments ✅ - **Full Factorial**: 2^k designs (2-8 factors) - **Fractional Factorial**: Resolution III, IV, V - **Response Surface**: CCD, Box-Behnken - **Taguchi Methods**: L4, L8, L9, L16, L18, L27 orthogonal arrays - **ANOVA**: One-way, Two-way with interaction - **🆕 Main & Interaction Effects**: Effect calculation and visualization - **🆕 S/N Ratios**: Taguchi signal-to-noise ratio (smaller/larger/nominal-is-better) - **🆕 Mixture Designs**: Scheffé polynomial models - **🆕 Robust Parameter Design**: Control vs noise factors ### 4. Reports & Export ✅ - **Excel Reports**: Professional format with 3 worksheets - **PDF Reports**: AIAG-VDA standard format - **JSON Output**: For system integration ### 5. Advanced Analysis ✅ - **EWMA Control Charts**: Detect small shifts - **CUSUM Control Charts**: Cumulative sum monitoring - **Multivariate Control**: Hotelling T² charts - **Outlier Detection**: Modified Z-score, IQR, ARIMA - **🆕 Nonparametric MEC/MCE**: Mixed EWMA-CUSUM charts (distribution-free) - **🆕 Change-Point Detection**: Mean and variance monitoring - **🆕 Performance Metrics**: RMI, AEQL, PCI, ARARL - **🆕 Short Run SPC**: Standardized charts for high-mix low-volume production - **🆕 Capability Confidence Intervals**: 95% CI for Cp/Cpk/Pp/Ppk - **🆕 Western Electric Rules**: 7 rules for out-of-control detection - **🆕 Sigma Estimation Methods**: Range, Std Dev, Pooled Std Dev - **🆕 Distribution Selection Guide**: Normal, Binomial, Poisson, Weibull ### 6. System Integration ✅ - **HTTP API**: Real-time monitoring - **ERP/MES Integration**: CSV, Excel, JSON import/export - **Database Support**: SQLite, PostgreSQL - **🆕 ISA-95 B2MML**: Enterprise-control system integration ## Usage Examples ### Define Phase ```markdown - Project Charter Template - SIPOC Diagram - CTQ Tree - VOC/VOB Collection ``` ### Measure Phase ```bash # MSA GR&R Study python3 scripts/msa_grr_analysis.py \ --data "[...]" --parts 10 --operators 3 --trials 3 \ --tolerance 0.5 --json # Baseline Capability python3 scripts/calculate_capability.py \ --data "..." --usl 10.5 --lsl 9.5 --json ``` ### Analyze Phase ```bash # Control Chart with out-of-control detection python3 scripts/control_chart.py \ --data "[...]" --chart-type Xbar-R --subgroup-size 5 # Advanced Charts (CUSUM, EWMA, I-MR, MAMR, Hotelling T2) python3 scripts/advanced_control_charts.py ``` ### Improve Phase ```bash # DOE Full Factorial python3 scripts/doe_full_factorial.py \ --factors 3 --responses "[45,52,48,58,46,53,49,59]" \ --analyze # Factor Effects python3 scripts/doe_factor_effects.py \ --responses "[45,52,48,58,46,53,49,59]" --json # Response Surface python3 scripts/doe_response_surface.py --help ``` ### Control Phase ```bash # Excel Report python3 scripts/excel_report.py --help # Generate test data + batch reports python3 scripts/generate_test_data.py python3 scripts/batch_generate_reports_v2.py ``` ## Evaluation Criteria ### Process Capability | Index | Rating | Action | |-------|--------|--------| | ≥ 2.0 | 🟢 Six Sigma | Maintain, can relax control | | 1.67-2.0 | 🟢 Excellent | Maintain current state | | 1.33-1.67 | 🟡 Good | Maintain, continuous improvement | | 1.0-1.33 | 🟡 Marginal | Need improvement plan | | < 1.0 | 🔴 Insufficient | Must improve, 100% inspection | ### MSA GR&R | %GR&R | Rating | Action | |-------|--------|--------| | < 10% | 🟢 Acceptable | Measurement system OK | | 10-30% | 🟡 Conditionally Acceptable | Evaluate risk | | ≥ 30% | 🔴 Unacceptable | Must improve system | ### ndc (Number of Distinct Categories) - ndc ≥ 5: ✅ Adequate resolution - ndc < 5: ⚠️ Insufficient resolution ## Standards & References ### SPC Standards - **AIAG-VDA SPC Manual (Yellow Volume)**, 1st edition, February 2026 - **ISO 22514**: Statistical methods in process management - **ISO 3534**: Statistics - Vocabulary and symbols - **ISO 7870**: Control charts - **GB/T 36077-2025**: 精益六西格玛管理评价准则 (Criteria for lean six sigma management assessment) ### MSA Standards - **AIAG MSA Manual**, 4th edition - **ISO 22514-7**: Measurement system analysis - **VDA 5**: Measurement process capability ### Six Sigma Standards - **ASQ Six Sigma Body of Knowledge** - **ISO 13053**: Six Sigma methodology (DMAIC/DMADV) - **GB/T 36077-2025**: 精益六西格玛管理评价准则 (代替 GB/T 36077-2018) ### Quality System Standards - **IATF 16949**: Automotive quality management - **ISO 9001**: Quality management systems - **GB/T 19001**: 质量管理体系 要求 - **GB/T 19580**: 卓越绩效评价准则 ## Script Tools ### SPC Core - `calculate_capability.py` - Process capability indices (Cp, Cpk, Pp, Ppk, Pm, Pmk) - `control_chart.py` - Control charts (Xbar-R, Xbar-S, I-MR) - `attribute_control_charts.py` - Attribute control charts (p/np/c/u/Z) - `advanced_control_charts.py` - CUSUM, EWMA, MAMR, Hotelling T² - `pooled_std.py` - Sigma estimation (Range, Std Dev, Pooled) - `western_electric_rules.py` - Western Electric out-of-control rules - `spc_calculator.py` - SPC calculations - `spc_report.py` - SPC report generation - `excel_report.py` - Excel report generation ### MSA - `msa_calculator.py` - MSA calculations - `msa_grr_analysis.py` - GR&R study (Xbar-R method) - `msa_other_studies.py` - Bias, Linearity, Stability - `msa_report.py` - MSA report generation ### DOE - `doe_full_factorial.py` - 2^k full factorial designs - `doe_response_surface.py` - CCD, Box-Behnken - `doe_factor_effects.py` - Main and interaction effects - `doe_sn_ratio.py` - Taguchi S/N ratio calculation - `generate_doe_report.py` - DOE report generation ### Utilities - `generate_test_data.py` - Generate test datasets - `batch_generate_reports_v2.py` - Batch PDF report generation - `data_import.py` - Data import utilities - `demo_report_generation.py` - Demo report generation ## Version **Current**: v1.8.0 (2026-04-13) **Changes in v1.8.0**: - ✅ **NEW: GB/T 36077-2025 中国国家标准整合** - 完整融入《精益六西格玛管理评价准则》 - ✅ 七大评价维度框架 (领导力/顾客驱动/推进规划/项目管理/评价与激励/基础架构/实施成果) - ✅ DMAIC/DMADV 各阶段输入 - 主要工作 - 输出完整对照表 - ✅ 精益六西格玛项目评分表 (110 分制) - ✅ 常用工具与方法 (按 DMAIC/DMADV 各阶段分类) - ✅ 核心术语定义 (LSS/Lean/Six Sigma/DLSS/DMAIC/DMADV/倡导者/黑带/绿带/黄带) - ✅ 评价准则条款赋值表 (总分 1000 分) - ✅ 基于 GB/T 36077-2025 (代替 GB/T 36077-2018),2025-12-31 发布，2026-07-01 实施 - ✅ 支持中国组织精益六西格玛管理自我评价、相关方评价、第三方评价 **Changes in v1.7.0**: - ✅ NEW: Real-time data stream processing for Quality 4.0 - ✅ Predictive quality analytics framework - ✅ AI/ML anomaly detection integration - ✅ Supply chain quality management support - ✅ Digital twin integration guidelines - ✅ FMEA 7-step method (AIAG-VDA FMEA Handbook) - ✅ Quality big data visualization - ✅ Industry best practices from automotive conferences - ✅ Based on: Quality 4.0 conferences, automotive industry best practices **Changes in v1.6.0**: - ✅ NEW: Western Electric 7 rules for out-of-control detection - ✅ Sigma estimation methods (Range R/d₂, Std Dev s/c₄, Pooled s_p) - ✅ Extended coefficient tables (d₂, c₄ for n=2 to 25) - ✅ Distribution selection guide (Normal, Binomial, Poisson, Weibull) - ✅ Nonparametric multivariate control charts - ✅ Goodness-of-fit based nonparametric EWMA - ✅ MIL-HDBK-1916 guidance - ✅ Based on: ASQ SPC guidelines, Western Electric rules **Changes in v1.1.1**: - ✅ Fixed DOE parameter parsing - ✅ Added JSON output for EWMA - ✅ Improved error messages - ✅ Updated help documentation ## Support For issues or questions: 1. Check reference documentation in `references/` 2. Run scripts with `--help` for usage 3. Review example reports in `aiagvda_standard_reports/` 4. Check test cases in `tests/` (`python3 -m pytest tests/ -v`) 5. Install dependencies: `pip install -r requirements.txt` --- *Rohoon Six Sigma - Professional Quality Management Support* *Based on AIAG-VDA, ISO, and Lean Six Sigma Standards* --- ## 📊 Figure 12-3 标准格式报告生成 ### 功能说明 生成符合 AIAG/VDA SPC 协调标准 Figure 12-3 格式的综合分析报告，支持中英文双语。 ### 使用方式 ```bash # 1. 生成测试数据 cd ~/.openclaw/workspace/skills/rohoon-6sigma python3 scripts/generate_test_data.py # 2. 批量生成报告（中英文） python3 scripts/batch_generate_reports_v2.py ``` ### 输出文件 - **英文版**：`tmp/SPC_Reports_EN/*.pdf` - **中文版**：`tmp/SPC_Reports_CN/*.pdf` ### 报告特性 - 6 列等宽表头布局 - 字段名加粗，字段值蓝色 - 单元格自动合并 - 能力指数进度条可视化 - 字体 6 号紧凑排版 ### 应用场景 1. **客户审核** - 提供符合 AIAG/VDA 标准的正式报告 2. **内部培训** - 展示标准报告格式要求 3. **数据分析** - 识别过程能力问题 4. **持续改进** - 跟踪改进效果

path: EXCEL_IMPORT_REPORT_FEATURES.md
content: # 📊 Excel 导入与报告功能 - 完整实现

## ✅ 已实现功能

### 1. Excel 报告生成

#### MSA Excel 报告
**脚本**: `excel_report.py`

**支持类型**:
- ✅ GR&R 研究报告
- ✅ 偏倚研究报告
- ✅ 线性研究报告

**报告内容**:
- 封面页 (研究信息)
- 结果摘要页 (统计结果 + 评估)
- 原始数据页 (完整测量数据)
- 分析图表页 (Excel 图表)
- 统计详情页 (详细统计量)

**测试**:
```bash
python3 -c "
from excel_report import generate_msa_excel
import numpy as np

study_info = {'id': 1, 'study_type': 'GRR', ...}
results = {'grr_error': {...}, 'evaluation': {...}}
chart_data = {'data_array': np.array(...)}

excel_bytes = generate_msa_excel('GRR', study_info, results, chart_data)
# 输出：~14 KB Excel 文件
"
```

#### SPC Excel 报告
**脚本**: `spc_report.py`

**支持类型**:
- ✅ 控制图分析报告
- ✅ 过程能力分析报告

**报告内容**:
- 研究信息
- 控制图/能力指数结果
- 原始数据
- 图表

---

### 2. Excel 数据导入

**脚本**: `data_import.py`

**支持格式**:
- ✅ CSV 文件导入
- ✅ Excel 文件导入 (.xlsx)

**功能**:
- ✅ 数据验证 (必需列检查)
- ✅ 空值检测
- ✅ 数值格式验证
- ✅ 规格限验证 (USL > LSL)
- ✅ 日期格式验证
- ✅ 导入错误报告

**导入模板生成**:
```python
from data_import import generate_import_template

# 生成测量数据模板
template = generate_import_template('measurement')
# 输出：~6 KB Excel 模板

# 生成 GR&R 数据模板
template = generate_import_template('grr')
```

**数据导入**:
```python
from data_import import import_excel_data

result, df = import_excel_data(
    file_content,
    sheet_name='Sheet1',
    required_columns=['product_id', 'measurement_value', 'timestamp']
)

if result.success:
    print(f"导入成功：{result.imported_rows} 行")
else:
    print(f"导入失败：{result.errors}")
```

---

### 3. PDF 报告生成

**脚本**:
- `msa_report.py` - MSA PDF 报告
- `spc_report.py` - SPC PDF 报告
- `generate_spd_report_aiagvda_fig12-*.py` - AIAG-VDA 标准报告

**支持类型**:
- ✅ MSA GR&R 研究报告
- ✅ MSA 偏倚研究报告
- ✅ MSA 线性研究报告
- ✅ SPC 控制图分析报告
- ✅ SPC 过程能力分析报告
- ✅ AIAG-VDA Figure 12-1 (正态分布)
- ✅ AIAG-VDA Figure 12-2 (混合分布)
- ✅ AIAG-VDA Figure 12-3 (标准格式)

**报告特点**:
- ✅ A4 纵向格式
- ✅ 中文字体支持
- ✅ 原始数据表格
- ✅ 可视化图表
- ✅ 统计结果
- ✅ 评估结论

---

## 📁 文件清单

### Excel 相关 (3 个)
- ✅ `excel_report.py` (20 KB) - MSA/SPC Excel 报告生成
- ✅ `data_import.py` (6.5 KB) - Excel/CSV数据导入
- ✅ `generate_word_doc.py` (37 KB) - Word 文档生成

### PDF 报告 (9 个)
- ✅ `msa_report.py` (25 KB)
- ✅ `spc_report.py` (17 KB)
- ✅ `generate_spc_report.py` (12 KB)
- ✅ `generate_spd_report_aiagvda.py` (22 KB)
- ✅ `generate_spd_report_aiagvda_fig12-1.py` (16 KB)
- ✅ `generate_spd_report_aiagvda_fig12-2.py` (17 KB)
- ✅ `generate_spd_report_aiagvda_fig12-3.py` (25 KB)

---

## 🧪 测试结果

### Excel 报告生成
```
✅ Excel 报告生成成功：13.6 KB
```

### 导入模板生成
```
✅ 导入模板生成成功：5.6 KB
✅ Excel 数据导入验证：True
```

### PDF 报告生成
```
✅ AIAG-VDA Figure 12-1 Report generated
✅ AIAG-VDA Figure 12-2 Report generated
✅ AIAG-VDA Figure 12-3 Report generated
```

---

## 🚀 使用示例

### 1. 生成 MSA Excel 报告
```bash
cd ~/.openclaw/workspace/skills/rohoon-6sigma/scripts

python3 -c "
from excel_report import generate_msa_excel
import numpy as np

study_info = {
    'id': 1,
    'study_type': 'GRR',
    'study_name': 'GR&R Study',
    'n_parts': 10,
    'n_operators': 3,
    'n_trials': 3
}

results = {
    'grr_error': {'ev': 0.05, 'av': 0.03, 'rr': 0.06, 'pv': 0.21, 'tv': 0.22},
    'evaluation': {
        'percent_grr': 27.31,
        'percent_ev': 23.45,
        'percent_av': 13.99,
        'percent_pv': 96.19,
        'ndc': 5,
        'acceptance': 'conditionally_acceptable'
    }
}

np.random.seed(42)
chart_data = {'data_array': np.random.normal(10.0, 0.1, (10, 3, 3))}

excel_bytes = generate_msa_excel('GRR', study_info, results, chart_data)

with open('/tmp/msa_report.xlsx', 'wb') as f:
    f.write(excel_bytes)

print(f'报告已生成：{len(excel_bytes)/1024:.1f} KB')
"
```

### 2. 生成导入模板
```bash
python3 -c "
from data_import import generate_import_template

# 测量数据模板
template = generate_import_template('measurement')
with open('/tmp/measurement_template.xlsx', 'wb') as f:
    f.write(template)

# GR&R 数据模板
template = generate_import_template('grr')
with open('/tmp/grr_template.xlsx', 'wb') as f:
    f.write(template)

print('模板已生成')
"
```

### 3. 导入 Excel 数据
```bash
python3 -c "
from data_import import import_excel_data

with open('/tmp/data.xlsx', 'rb') as f:
    file_content = f.read()

result, df = import_excel_data(
    file_content,
    required_columns=['product_id', 'measurement_value', 'timestamp']
)

if result.success:
    print(f'导入成功：{result.imported_rows} 行')
else:
    print(f'导入失败：{result.errors}')
"
```

### 4. 生成 AIAG-VDA PDF 报告
```bash
# Figure 12-1 (正态分布)
python3 generate_spd_report_aiagvda_fig12-1.py --output /tmp/report.pdf

# Figure 12-2 (混合分布)
python3 generate_spd_report_aiagvda_fig12-2.py --output /tmp/report.pdf

# Figure 12-3 (标准格式)
python3 generate_spd_report_aiagvda_fig12-3.py --output /tmp/report.pdf
```

---

## ✅ 功能状态

| 功能 | 状态 | 脚本 | 测试 |
|------|------|------|------|
| **MSA Excel 报告** | ✅ 完成 | excel_report.py | ✅ 通过 |
| **SPC Excel 报告** | ✅ 完成 | spc_report.py | ✅ 通过 |
| **Excel 数据导入** | ✅ 完成 | data_import.py | ✅ 通过 |
| **CSV 数据导入** | ✅ 完成 | data_import.py | ✅ 通过 |
| **导入模板生成** | ✅ 完成 | data_import.py | ✅ 通过 |
| **MSA PDF 报告** | ✅ 完成 | msa_report.py | ✅ 通过 |
| **SPC PDF 报告** | ✅ 完成 | spc_report.py | ✅ 通过 |
| **AIAG-VDA Figure 12-1** | ✅ 完成 | generate_..._fig12-1.py | ✅ 通过 |
| **AIAG-VDA Figure 12-2** | ✅ 完成 | generate_..._fig12-2.py | ✅ 通过 |
| **AIAG-VDA Figure 12-3** | ✅ 完成 | generate_..._fig12-3.py | ✅ 通过 |

---

**所有 Excel 导入和报告功能已 100% 实现并测试通过！** ✅

*最后更新：2026-03-27 09:06*


path: package.json
content: {
  "name": "rohoon-6sigma",
  "version": "1.8.0",
  "description": "Rohoon Six Sigma professional support - AIAG-VDA SPC/MSA/DOE complete toolkit with GB/T 36077-2025",
  "author": "William",
  "license": "MIT",
  "keywords": [
    "six-sigma",
    "SPC",
    "MSA",
    "DOE",
    "AIAG-VDA",
    "IATF16949",
    "quality-management",
    "lean",
    "statistical-process-control"
  ],
  "files": [
    "SKILL.md",
    "scripts/",
    "references/",
    "tests/",
    "reports/",
    "docs/"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/rohoon/sixsigma-skill"
  },
  "scripts": {
    "test": "python3 -m pytest tests/ -v",
    "lint": "pylint scripts/"
  }
}


path: README.md
content: # Rohoon Six Sigma Professional Support

[![ClawHub](https://img.shields.io/badge/ClawHub-rohoon--6sigma-blue)](https://clawhub.ai/williamhyzhu/rohoon-6sigma)
![Version](https://img.shields.io/badge/version-1.8.1-green)
![License](https://img.shields.io/badge/license-MIT-blue)

Comprehensive Six Sigma and Lean quality management toolkit for AI agents. Based on **AIAG-VDA SPC Manual (Yellow Volume)**, **AIAG MSA Manual 4th Ed.**, **ISO 13053**, and **GB/T 36077-2025**.

## Features

- **SPC Control Charts**: Xbar-R, Xbar-S, I-MR, CUSUM, EWMA, p/np, c/u, MAMR, Hotelling T²
- **Out-of-Control Detection**: 8 AIAG-VDA rules + 7 Western Electric rules
- **Process Capability**: Cp, Cpk, Pp, Ppk, Pm, Pmk (Geometric Method) with confidence intervals
- **MSA Analysis**: GR&R (ANOVA method), Bias, Linearity, Stability studies
- **DOE**: Full Factorial (2^k), Response Surface (CCD/Box-Behnken), Taguchi S/N Ratios, Factor Effects
- **AIAG-VDA Standard Reports**: PDF reports strictly following Figure 12-1/12-2/12-3 formats (bilingual)
- **Sigma Estimation**: Range (R̄/d₂), Std Dev (s̄/c₄), Pooled Std Dev (s_p)
- **Excel Reports**: Professional multi-worksheet reports
- **GB/T 36077-2025**: Complete China national standard evaluation framework (7 dimensions, 1000 points)

## Installation

```bash
# Install via ClawHub
clawhub install rohoon-6sigma

# Install Python dependencies
cd skills/rohoon-6sigma
pip install -r requirements.txt
```

## Quick Start

### Process Capability Analysis

```bash
python scripts/calculate_capability.py \
  --data "10.1,10.2,10.15,10.18,10.12,10.05,10.22,10.08,10.14,10.19" \
  --usl 10.5 --lsl 9.5 --json
```

### Control Charts

```bash
# Xbar-R chart (subgroup n=5)
python scripts/control_chart.py \
  --data "[10.1,10.2,10.15,10.18,10.12, 10.05,10.22,10.08,10.14,10.19]" \
  --chart-type Xbar-R --subgroup-size 5 --json

# I-MR chart (individual values)
python scripts/control_chart.py \
  --data "10.1,10.2,10.15,10.18,10.12" \
  --chart-type I-MR --json
```

### MSA GR&R Study

```bash
python scripts/msa_grr_analysis.py \
  --data "[10.1,10.15,10.12, ...]" \
  --parts 10 --operators 3 --trials 3 \
  --tolerance 0.5 --json
```

### DOE Full Factorial

```bash
python scripts/doe_full_factorial.py \
  --factors 2 --responses "[40,50,45,55]" --analyze --json
```

### AIAG-VDA Standard PDF Reports

```bash
# Figure 12-1: Normal distribution report (Chinese)
python scripts/aiagvda_unified_report.py -o report.pdf -f 12-1 -l zh

# Figure 12-1: Normal distribution report (English)
python scripts/aiagvda_unified_report.py -o report.pdf -f 12-1 -l en

# Figure 12-2: Non-normal / mixed distribution
python scripts/aiagvda_unified_report.py -o report.pdf -f 12-2 -l zh

# Figure 12-3: Standard statistical summary table
python scripts/aiagvda_unified_report.py -o report.pdf -f 12-3 -l zh
```

### Run Tests

```bash
python -m pytest tests/ -v
```

## Standards

| Standard | Description |
|----------|-------------|
| **AIAG-VDA SPC Manual** | Yellow Volume, 1st Ed., February 2026 |
| **AIAG MSA Manual** | 4th Edition |
| **ISO 13053** | Six Sigma Methodology (DMAIC/DMADV) |
| **ISO 22514** | Statistical Methods in Process Management |
| **ISO 7870** | Control Charts |
| **GB/T 36077-2025** | 精益六西格玛管理评价准则 (replaces 2018) |
| **IATF 16949** | Automotive Quality Management |
| **ISO 9001** | Quality Management Systems |

## Use Cases

- **Automotive Quality Management**: IATF 16949 compliance, PPAP documentation
- **Process Capability Studies**: Cp/Cpk analysis for customer audits
- **Measurement System Analysis**: GR&R studies before production launches
- **Design of Experiments**: Process optimization, parameter screening
- **Supplier Assessment**: Evaluate incoming material quality
- **Continuous Improvement**: DMAIC project support
- **China National Standard**: GB/T 36077-2025 self-evaluation

## Scripts Overview

| Script | Purpose |
|--------|---------|
| `calculate_capability.py` | Process capability indices |
| `control_chart.py` | Xbar-R, Xbar-S, I-MR charts |
| `attribute_control_charts.py` | p/np/c/u/Z attribute charts |
| `advanced_control_charts.py` | CUSUM, EWMA, MAMR, Hotelling T² |
| `pooled_std.py` | Sigma estimation methods |
| `western_electric_rules.py` | Out-of-control rule detection |
| `msa_grr_analysis.py` | GR&R study (ANOVA) |
| `msa_other_studies.py` | Bias, Linearity, Stability |
| `doe_full_factorial.py` | 2^k factorial designs |
| `doe_response_surface.py` | CCD, Box-Behnken |
| `doe_factor_effects.py` | Main & interaction effects |
| `doe_sn_ratio.py` | Taguchi S/N ratios |
| `aiagvda_unified_report.py` | Standard PDF report generator |
| `excel_report.py` | Excel report generation |
| `batch_generate_reports_v2.py` | Batch PDF report generation |
| `generate_test_data.py` | Generate test datasets |

## Evaluation Criteria

### Process Capability
| Index | Rating | Action |
|-------|--------|--------|
| ≥ 2.0 | 🟢 Six Sigma | Maintain |
| 1.67–2.0 | 🟢 Excellent | Maintain |
| 1.33–1.67 | 🟡 Good | Improve |
| 1.0–1.33 | 🟡 Marginal | Plan improvement |
| < 1.0 | 🔴 Insufficient | Must improve |

### MSA GR&R
| %GR&R | Rating | Action |
|-------|--------|--------|
| < 10% | 🟢 Acceptable | OK |
| 10–30% | 🟡 Conditional | Evaluate risk |
| ≥ 30% | 🔴 Unacceptable | Improve system |

## Dependencies

Requires Python 3.10+ with: `numpy`, `scipy`, `reportlab`, `openpyxl`, `matplotlib`, `pandas`

## License

MIT

## Author

William (WilliamHYZhu on ClawHub)


path: references/capability-indices.md
content: # Process Capability Indices

## Short-Term Capability
| Index | Formula | Use |
|-------|---------|-----|
| Cp | (USL-LSL)/6σ | Potential capability |
| Cpk | min[(USL-μ)/3σ, (μ-LSL)/3σ] | Actual capability |

## Long-Term Performance
| Index | Formula | Use |
|-------|---------|-----|
| Pp | (USL-LSL)/6σ_total | Potential performance |
| Ppk | min[(USL-μ)/3σ_total, (μ-LSL)/3σ_total] | Actual performance |

## Evaluation Criteria
| Cpk/Ppk | Rating | Action |
|---------|--------|--------|
| ≥2.0 | Six Sigma | Maintain |
| 1.67-2.0 | Excellent | Maintain |
| 1.33-1.67 | Good | Continuous improvement |
| 1.0-1.33 | Marginal | Improvement plan needed |
| <1.0 | Insufficient | 100% inspection required |


path: references/control-charts.md
content: # Control Charts Reference

## Variable Control Charts
| Chart | Subgroup Size | Use Case |
|-------|--------------|----------|
| Xbar-R | 2-9 | Most common |
| Xbar-S | ≥10 | Large subgroups |
| I-MR | 1 | Individual values |

## Attribute Control Charts
| Chart | Sample Size | Use Case |
|-------|------------|----------|
| p | Variable | Proportion defective |
| np | Fixed | Number defective |
| c | Fixed | Count of defects |
| u | Variable | Defects per unit |

## 8 Out-of-Control Rules (AIAG-VDA)
1. 1 point beyond 3σ
2. 9 points on same side of CL
3. 6 points trending up/down
4. 14 points alternating
5. 2 of 3 points beyond 2σ
6. 4 of 5 points beyond 1σ
7. 15 points within 1σ
8. 8 points beyond 1σ


path: references/doe-guide.md
content: # Design of Experiments (DOE) Guide

## Full Factorial Designs
- 2^k designs (k = 2 to 8 factors)
- Resolution: Full
- Runs: 2^k
- Use: When all interactions are important

## Fractional Factorial Designs
- 2^(k-p) designs
- Resolution III, IV, V
- Runs: 2^(k-p)
- Use: Screening experiments

## Response Surface Methods
- Central Composite Design (CCD)
- Box-Behnken Design
- Use: Optimization

## Taguchi Methods
- Orthogonal Arrays: L4, L8, L9, L16, L18, L27
- Signal-to-Noise Ratios
- Use: Robust design

## ANOVA
- One-way ANOVA
- Two-way ANOVA with interaction
- Use: Statistical significance testing


path: references/lean-six-sigma-tools.md
content: # Lean Six Sigma Tools Reference

## DMAIC Methodology

### Define Phase
- Project Charter
- SIPOC Diagram
- CTQ Tree
- VOC/VOB Collection

### Measure Phase
- Data Collection Plan
- MSA (GR&R, Bias, Linearity, Stability)
- Baseline Capability (Ppk/Cpk)
- Process Flow Map

### Analyze Phase
- Control Charts (Xbar-R, Xbar-S, I-MR)
- 8 Out-of-Control Rules
- Hypothesis Testing
- Root Cause Analysis (5 Why, Fishbone)

### Improve Phase
- DOE (Full Factorial, Fractional, RSM, Taguchi)
- Poka-Yoke
- Solution Selection Matrix
- Pilot Testing

### Control Phase
- Control Plans
- OCAP (Out-of-Control Action Plan)
- SPC Implementation
- Lessons Learned

## Lean Tools
- 5S (Sort, Set in order, Shine, Standardize, Sustain)
- VSM (Value Stream Mapping)
- Kanban
- SMED (Single-Minute Exchange of Dies)
- TPM (Total Productive Maintenance)
- FMEA (Failure Mode and Effects Analysis)


path: references/msa-reference.md
content: # MSA Reference Guide

## GR&R Studies
- Method: Xbar-R or ANOVA
- Standard: 10 parts × 3 operators × 3 trials
- Acceptance:
  - %GR&R < 10%: Acceptable
  - 10-30%: Conditionally acceptable
  - >30%: Unacceptable
- ndc ≥ 5: Adequate resolution

## Bias Study
- 10-25 measurements of reference standard
- Acceptance: %Bias < 10%

## Linearity Study
- 5 parts covering full range
- 10-15 measurements per part
- Acceptance: %Linearity < 10%, R² > 0.75

## Stability Study
- Control chart monitoring over time
- Acceptance: In control, no trends


path: requirements.txt
content: numpy>=1.23.0
scipy>=1.10.0
reportlab>=4.0.0
openpyxl>=3.1.0
matplotlib>=3.7.0
pandas>=2.0.0


path: scripts/advanced_control_charts.py
content: #!/usr/bin/env python3
"""
高级控制图模块
包含: CUSUM, EWMA, I-MR, Moving Average, Z-Chart
"""

import numpy as np
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
import io


@dataclass
class ControlChartResult:
    """控制图结果"""
    chart_type: str
    data: np.ndarray
    cl: float  # Center Line
    ucl: float  # Upper Control Limit
    lcl: float  # Lower Control Limit
    violations: List[int]  # 违规点索引
    statistics: Dict[str, float]


class CUSUMChart:
    """
    CUSUM (累积和控制图)
    用于检测过程均值的小偏移
    """
    
    def __init__(self, target: float, sigma: float, k: float = 0.5):
        """
        初始化 CUSUM 图
        
        参数:
            target: 目标值/均值
            sigma: 标准差
            k: 参考值 (通常为 0.5 倍标准差)
        """
        self.target = target
        self.sigma = sigma
        self.k = k * sigma  # 允许偏移量
        
    def calculate(self, data: np.ndarray) -> ControlChartResult:
        """计算 CUSUM 统计量"""
        n = len(data)
        
        # 累积和 (双向)
        cusum_plus = np.zeros(n)  # 检测向上偏移
        cusum_minus = np.zeros(n)  # 检测向下偏移
        
        for i in range(n):
            if i == 0:
                cusum_plus[i] = max(0, data[i] - self.target - self.k)
                cusum_minus[i] = max(0, self.target - self.k - data[i])
            else:
                cusum_plus[i] = max(0, cusum_plus[i-1] + data[i] - self.target - self.k)
                cusum_minus[i] = max(0, cusum_minus[i-1] + self.target - self.k - data[i])
        
        # 组合 CUSUM (可选)
        cusum_combined = cusum_plus + cusum_minus
        
        # 控制限 (h 通常设为 4-5 倍 sigma)
        h = 5 * self.sigma
        ucl = h
        lcl = 0
        
        # 检测违规点
        violations = []
        for i in range(n):
            if cusum_combined[i] > ucl:
                violations.append(i)
        
        return ControlChartResult(
            chart_type='CUSUM',
            data=cusum_combined,
            cl=0,
            ucl=ucl,
            lcl=lcl,
            violations=violations,
            statistics={
                'target': self.target,
                'sigma': self.sigma,
                'k': self.k,
                'h': h,
                'max_cusum': np.max(cusum_combined)
            }
        )


class EWMAChart:
    """
    EWMA (指数加权移动平均控制图)
    用于检测过程均值的中小偏移
    """
    
    def __init__(self, target: float, sigma: float, lambda_: float = 0.2):
        """
        初始化 EWMA 图
        
        参数:
            target: 目标值/均值
            sigma: 标准差
            lambda_: 平滑参数 (0 < lambda_ <= 1)
        """
        self.target = target
        self.sigma = sigma
        self.lambda_ = lambda_
        
    def calculate(self, data: np.ndarray) -> ControlChartResult:
        """计算 EWMA 统计量"""
        n = len(data)
        
        # EWMA 统计量
        ewma = np.zeros(n)
        
        for i in range(n):
            if i == 0:
                ewma[i] = self.target
            else:
                ewma[i] = self.lambda_ * data[i] + (1 - self.lambda_) * ewma[i-1]
        
        # 控制限 (随时间收缩)
        sigma_ewma = self.sigma * np.sqrt(self.lambda_ / (2 - self.lambda_))
        
        # 初始控制限较宽，后续收缩
        ucl = np.array([self.target + 3 * sigma_ewma * np.sqrt(self.lambda_ * (1 - (1 - self.lambda_)**(2*(i+1))) / (2 - self.lambda_)) for i in range(n)])
        lcl = np.array([self.target - 3 * sigma_ewma * np.sqrt(self.lambda_ * (1 - (1 - self.lambda_)**(2*(i+1))) / (2 - self.lambda_)) for i in range(n)])
        
        # 检测违规点
        violations = []
        for i in range(n):
            if i >= 1 and (ewma[i] > ucl[i] or ewma[i] < lcl[i]):
                violations.append(i)
        
        return ControlChartResult(
            chart_type='EWMA',
            data=ewma,
            cl=self.target,
            ucl=ucl[-1],  # 稳态控制限
            lcl=lcl[-1],
            violations=violations,
            statistics={
                'target': self.target,
                'sigma': self.sigma,
                'lambda': self.lambda_,
                'sigma_ewma': sigma_ewma
            }
        )


class IMRChart:
    """
    I-MR (单值-移动极差控制图)
    用于个体测量值的过程监控
    """
    
    def __init__(self):
        pass
        
    def calculate(self, data: np.ndarray) -> ControlChartResult:
        """计算 I-MR 控制图"""
        n = len(data)
        
        # I 图 (单值图)
        i_values = data
        i_mean = np.mean(i_values)
        
        # 移动极差
        mr = np.abs(np.diff(data))
        mr_bar = np.mean(mr)
        
        # 控制限 (d2 = 1.128 for n=2)
        d2 = 1.128
        sigma = mr_bar / d2
        
        # I 图控制限
        ucl_i = i_mean + 3 * sigma
        lcl_i = i_mean - 3 * sigma
        
        # MR 图控制限
        D3, D4 = 0, 3.267  # for n=2
        ucl_r = D4 * mr_bar
        lcl_r = D3 * mr_bar
        
        # 检测违规点
        violations_i = [i for i in range(n) if i_values[i] > ucl_i or i_values[i] < lcl_i]
        violations_mr = [i for i in range(n-1) if mr[i] > ucl_r or mr[i] < lcl_r]
        violations = violations_i + violations_mr
        
        return ControlChartResult(
            chart_type='I-MR',
            data=i_values,
            cl=i_mean,
            ucl=ucl_i,
            lcl=lcl_i,
            violations=violations,
            statistics={
                'i_mean': i_mean,
                'mr_bar': mr_bar,
                'sigma': sigma,
                'ucl_mr': ucl_r,
                'lcl_mr': lcl_r
            }
        )


class MAMRChart:
    """
    MAMR (移动平均-移动极差控制图)
    """
    
    def __init__(self, window_size: int = 5):
        """
        初始化 MAMR 图
        
        参数:
            window_size: 移动窗口大小
        """
        self.window_size = window_size
        
    def calculate(self, data: np.ndarray) -> ControlChartResult:
        """计算 MAMR 控制图"""
        n = len(data)
        
        # 移动平均
        ma = np.zeros(n)
        for i in range(n):
            start = max(0, i - self.window_size + 1)
            ma[i] = np.mean(data[start:i+1])
        
        # 移动极差
        mr = np.zeros(n)
        for i in range(n):
            start = max(0, i - self.window_size + 1)
            mr[i] = np.ptp(data[start:i+1])
        
        mr_bar = np.mean(mr[1:])  # 跳过第一个点
        
        # 控制限
        d2 = 1.128
        sigma = mr_bar / d2
        
        ucl = np.mean(ma) + 3 * sigma / np.sqrt(self.window_size)
        lcl = np.mean(ma) - 3 * sigma / np.sqrt(self.window_size)
        
        # 检测违规点
        violations = [i for i in range(n) if ma[i] > ucl or ma[i] < lcl]
        
        return ControlChartResult(
            chart_type='MAMR',
            data=ma,
            cl=np.mean(ma),
            ucl=ucl,
            lcl=lcl,
            violations=violations,
            statistics={
                'window_size': self.window_size,
                'mr_bar': mr_bar,
                'sigma': sigma
            }
        )


class HotellingT2Chart:
    """
    Hotelling T2 控制图
    多变量统计过程控制
    """
    
    def __init__(self):
        pass
        
    def calculate(self, data: np.ndarray, alpha: float = 0.05) -> ControlChartResult:
        """
        计算 Hotelling T2 统计量
        
        参数:
            data: 二维数组 (n_samples × n_variables)
            alpha: 显著性水平
        """
        import scipy.stats as scipy_stats
        n, p = data.shape
        
        # 均值向量
        mean_vec = np.mean(data, axis=0)
        
        # 协方差矩阵
        cov_matrix = np.cov(data, rowvar=False)
        
        # 逆协方差矩阵
        try:
            cov_inv = np.linalg.inv(cov_matrix)
        except np.linalg.LinAlgError:
            # 如果协方差矩阵奇异，使用伪逆
            cov_inv = np.linalg.pinv(cov_matrix)
        
        # 计算 T² 统计量
        t2_values = np.zeros(n)
        for i in range(n):
            diff = data[i] - mean_vec
            t2_values[i] = diff @ cov_inv @ diff
        
        # 控制限
        # Phase I (with estimated parameters)
        f_stat = (p * (n - 1) * (n + 1)) / (n * (n - p))
        ucl_phase1 = f_stat * scipy_stats.f.ppf(1 - alpha, p, n - p)
        
        # Phase II (known parameters)
        ucl_phase2 = (p * (n + 1) * (n - 1)) / (n * (n - p)) * scipy_stats.f.ppf(1 - alpha, p, n - p)
        
        # 使用 Phase I 控制限
        ucl = ucl_phase1
        lcl = 0
        
        # 检测违规点
        violations = [i for i in range(n) if t2_values[i] > ucl]
        
        return ControlChartResult(
            chart_type='Hotelling T2',
            data=t2_values,
            cl=np.mean(t2_values),
            ucl=ucl,
            lcl=lcl,
            violations=violations,
            statistics={
                'n_samples': n,
                'n_variables': p,
                'mean_vector': mean_vec,
                'ucl_phase1': ucl_phase1,
                'ucl_phase2': ucl_phase2
            }
        )


# 需要 scipy.stats
import scipy.stats


def run_cusum(data: np.ndarray, target: float, sigma: float, k: float = 0.5) -> Dict:
    """运行 CUSUM 分析"""
    chart = CUSUMChart(target, sigma, k)
    result = chart.calculate(data)
    return {
        'chart_type': result.chart_type,
        'data': result.data.tolist(),
        'cl': result.cl,
        'ucl': result.ucl,
        'lcl': result.lcl,
        'violations': result.violations,
        'statistics': result.statistics
    }


def run_ewma(data: np.ndarray, target: float, sigma: float, lambda_: float = 0.2) -> Dict:
    """运行 EWMA 分析"""
    chart = EWMAChart(target, sigma, lambda_)
    result = chart.calculate(data)
    return {
        'chart_type': result.chart_type,
        'data': result.data.tolist(),
        'cl': result.cl,
        'ucl': result.ucl,
        'lcl': result.lcl,
        'violations': result.violations,
        'statistics': result.statistics
    }


def run_imr(data: np.ndarray) -> Dict:
    """运行 I-MR 分析"""
    chart = IMRChart()
    result = chart.calculate(data)
    return {
        'chart_type': result.chart_type,
        'data': result.data.tolist(),
        'cl': result.cl,
        'ucl': result.ucl,
        'lcl': result.lcl,
        'violations': result.violations,
        'statistics': result.statistics
    }


def run_mamr(data: np.ndarray, window_size: int = 5) -> Dict:
    """运行 MAMR 分析"""
    chart = MAMRChart(window_size)
    result = chart.calculate(data)
    return {
        'chart_type': result.chart_type,
        'data': result.data.tolist(),
        'cl': result.cl,
        'ucl': result.ucl,
        'lcl': result.lcl,
        'violations': result.violations,
        'statistics': result.statistics
    }


def run_hotelling_t2(data: np.ndarray, alpha: float = 0.05) -> Dict:
    """运行 Hotelling T2 分析"""
    import scipy.stats as scipy_stats
    chart = HotellingT2Chart()
    result = chart.calculate(data, alpha)
    return {
        'chart_type': result.chart_type,
        'data': result.data.tolist(),
        'cl': result.cl,
        'ucl': result.ucl,
        'lcl': result.lcl,
        'violations': result.violations,
        'statistics': result.statistics
    }


if __name__ == '__main__':
    import json
    
    # 测试数据
    np.random.seed(42)
    data = np.random.normal(10, 0.1, 50)
    
    # 测试各控制图
    print("Testing CUSUM...")
    print(json.dumps(run_cusum(data, 10.0, 0.1), indent=2))
    
    print("\nTesting EWMA...")
    print(json.dumps(run_ewma(data, 10.0, 0.1), indent=2))
    
    print("\nTesting I-MR...")
    print(json.dumps(run_imr(data), indent=2))
    
    print("\nTesting MAMR...")
    print(json.dumps(run_mamr(data, 5), indent=2))
    
    # 测试 Hotelling T2 (二维数据)
    print("\nTesting Hotelling T2...")
    data_2d = np.random.normal(10, 0.1, (50, 3))
    print(json.dumps(run_hotelling_t2(data_2d), indent=2))

path: scripts/aiagvda_unified_report.py
content: #!/usr/bin/env python3
"""
AIAG-VDA SPC Yellow Volume Unified Report Generator
AIAG-VDA SPC 黄皮书统一报告生成器
"""

import io
import os
import numpy as np
from datetime import datetime
from typing import Dict, Optional
from dataclasses import dataclass

from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

import matplotlib
matplotlib.use('Agg')
matplotlib.rcParams['axes.unicode_minus'] = False
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from matplotlib.patches import Circle
from scipy import stats

# 注册中文字体
FONT_PATHS = [
    'C:/Windows/Fonts/msyh.ttc',  # 微软雅黑
    'C:/Windows/Fonts/simhei.ttf',  # 黑体
    'C:/Windows/Fonts/simsun.ttc',  # 宋体
    '/System/Library/Fonts/STHeiti Light.ttc',
    '/System/Library/Fonts/PingFang.ttc',
]
_chinese_font_path = None
for fp in FONT_PATHS:
    if os.path.exists(fp):
        _chinese_font_path = fp
        try:
            pdfmetrics.registerFont(TTFont('Chinese', fp, validate=False))
        except:
            pass
        break

_font_prop = fm.FontProperties(fname=_chinese_font_path) if _chinese_font_path else None

# 颜色
COLORS = {
    'primary': '#1F4E79',
    'secondary': '#2E75B6',
    'accent': '#00B050',
    'warning': '#FF0000',
    'light_gray': '#F2F2F2',
}

# 文本
TEXTS = {
    'zh': {
        'title': 'SPC 过程能力研究报告',
        'subtitle': 'AIAG / VDA SPC 统一标准',
        'process_name': '过程名称',
        'machine_name': '设备名称',
        'study_location': '研究地点',
        'study_date': '研究日期',
        'part_name_id': '零件名称及编号',
        'characteristic_name_id': '特性名称及编号',
        'study_remarks': '研究备注',
        'sample_size': '样本量',
        'subgroup_size': '子组大小',
        'histogram': '直方图',
        'raw_value_chart': '原始值图',
        'probability_plot': '概率图',
        'xbar_s_chart': 'Xbar-S 控制图',
        'x50_percent': 'X50%',
        'spread': '展宽',
        'distribution_model': '分布模型',
        'capability_requirement': '能力要求',
        'conclusion': '结论',
        'measurement_value': '测量值',
        'frequency': '频数',
        'subgroup': '子组',
        'expected_normal': '期望正态值',
        'ordered_data': '有序数据',
        'ppm_upper': 'PPM > USL',
        'ppm_lower': 'PPM < LSL',
        'ci_95': '95% 置信区间',
        'geometric_method': '几何法',
        'normal': '正态',
        'non_normal': '非正态',
    },
    'en': {
        'title': 'SPC Process Capability Study Report',
        'subtitle': 'AIAG / VDA SPC Harmonized Standard',
        'process_name': 'Process Name',
        'machine_name': 'Machine Name',
        'study_location': 'Study Location',
        'study_date': 'Study Date',
        'part_name_id': 'Part Name & ID',
        'characteristic_name_id': 'Characteristic Name & ID',
        'study_remarks': 'Study Remarks',
        'sample_size': 'Sample Size',
        'subgroup_size': 'Subgroup Size',
        'histogram': 'Histogram',
        'raw_value_chart': 'Raw Value Chart',
        'probability_plot': 'Probability Plot',
        'xbar_s_chart': 'Xbar-S Chart',
        'x50_percent': 'X50%',
        'spread': 'Spread',
        'distribution_model': 'Distribution Model',
        'capability_requirement': 'Capability Requirement',
        'conclusion': 'Conclusion',
        'measurement_value': 'Measurement Value',
        'frequency': 'Frequency',
        'subgroup': 'Subgroup',
        'expected_normal': 'Expected Normal Value',
        'ordered_data': 'Ordered Data',
        'ppm_upper': 'PPM > USL',
        'ppm_lower': 'PPM < LSL',
        'ci_95': '95% CI',
        'geometric_method': 'Geometric Method',
        'normal': 'Normal',
        'non_normal': 'Non-Normal',
    }
}


@dataclass
class StudyInfo:
    process_name: str = ""
    machine_name: str = ""
    study_location: str = ""
    study_date: str = ""
    part_name: str = ""
    part_id: str = ""
    characteristic_name: str = ""
    study_remarks: str = "Capability Study"
    subgroup_size: int = 5


@dataclass
class Specification:
    usl: float
    lsl: float
    target: Optional[float] = None


def _get_font(lang: str):
    return _font_prop if lang == 'zh' else None


def create_histogram(data, spec, lang):
    fig, ax = plt.subplots(figsize=(5.5, 3.5), dpi=150)
    ax.hist(data, bins=30, color=COLORS['accent'], edgecolor='white', alpha=0.7)
    ax.axvline(x=spec.usl, color=COLORS['warning'], linewidth=1.5, label=f"USL={spec.usl:.4f}")
    ax.axvline(x=spec.lsl, color=COLORS['warning'], linewidth=1.5, label=f"LSL={spec.lsl:.4f}")
    if spec.target:
        ax.axvline(x=spec.target, color=COLORS['secondary'], linestyle='--', linewidth=1)
    ax.axvline(x=np.median(data), color=COLORS['primary'], linestyle=':', linewidth=1.5)
    t = TEXTS[lang]
    ax.set_xlabel(t['measurement_value'], fontsize=8, fontproperties=_get_font(lang))
    ax.set_ylabel(t['frequency'], fontsize=8, fontproperties=_get_font(lang))
    ax.set_title(t['histogram'], fontsize=9, fontweight='bold', fontproperties=_get_font(lang))
    ax.legend(fontsize=6)
    ax.grid(True, linestyle='--', alpha=0.3)
    plt.tight_layout()
    buf = io.BytesIO()
    plt.savefig(buf, format='png', bbox_inches='tight')
    buf.seek(0)
    plt.close(fig)
    return buf


def create_raw_chart(data, spec, lang):
    fig, ax = plt.subplots(figsize=(7, 3), dpi=150)
    ax.scatter(range(len(data)), data, s=4, c=COLORS['accent'], alpha=0.6, marker='.')
    ax.axhline(y=spec.usl, color=COLORS['warning'], linewidth=1, label=f"USL={spec.usl:.4f}")
    ax.axhline(y=spec.lsl, color=COLORS['warning'], linewidth=1, label=f"LSL={spec.lsl:.4f}")
    ax.axhline(y=np.mean(data), color=COLORS['secondary'], linestyle='--', linewidth=1)
    t = TEXTS[lang]
    ax.set_xlabel('Value No.', fontsize=8, fontproperties=_get_font(lang))
    ax.set_ylabel(t['measurement_value'], fontsize=8, fontproperties=_get_font(lang))
    ax.set_title(t['raw_value_chart'], fontsize=9, fontweight='bold', fontproperties=_get_font(lang))
    ax.legend(fontsize=6)
    ax.grid(True, linestyle='--', alpha=0.3)
    plt.tight_layout()
    buf = io.BytesIO()
    plt.savefig(buf, format='png', bbox_inches='tight')
    buf.seek(0)
    plt.close(fig)
    return buf


def create_probability_plot(data, lang):
    fig, ax = plt.subplots(figsize=(4, 3.5), dpi=150)
    (osm, osr), (slope, intercept, r) = stats.probplot(data, dist="norm", fit=True)
    ax.scatter(osm, osr, s=6, c=COLORS['accent'], marker='.')
    ax.plot(osm, slope * osm + intercept, color=COLORS['warning'], linewidth=1.5)
    t = TEXTS[lang]
    ax.set_xlabel(t['expected_normal'], fontsize=8, fontproperties=_get_font(lang))
    ax.set_ylabel(t['ordered_data'], fontsize=8, fontproperties=_get_font(lang))
    ax.set_title(t['probability_plot'], fontsize=9, fontweight='bold', fontproperties=_get_font(lang))
    ax.text(0.05, 0.95, f"R² = {r**2:.4f}", transform=ax.transAxes, fontsize=7)
    ax.grid(True, linestyle='--', alpha=0.3)
    plt.tight_layout()
    buf = io.BytesIO()
    plt.savefig(buf, format='png', bbox_inches='tight')
    buf.seek(0)
    plt.close(fig)
    return buf


def create_xbar_s_chart(data, subgroup_size, lang):
    n_subgroups = len(data) // subgroup_size
    if n_subgroups < 2:
        fig, ax = plt.subplots(figsize=(7, 3))
        ax.text(0.5, 0.5, "Insufficient data", ha='center', va='center')
        buf = io.BytesIO()
        plt.savefig(buf, format='png')
        buf.seek(0)
        plt.close(fig)
        return buf
    
    subgroups = data[:n_subgroups * subgroup_size].reshape(n_subgroups, subgroup_size)
    xbar = np.mean(subgroups, axis=1)
    s = np.std(subgroups, axis=1, ddof=1)
    
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(7, 4.5), sharex=True, dpi=150)
    
    xbar_bar, s_bar = np.mean(xbar), np.mean(s)
    A3, B3, B4 = 1.427, 0, 2.089
    
    ax1.plot(range(len(xbar)), xbar, 'g-', linewidth=0.8, marker='.', markersize=4)
    ax1.axhline(y=xbar_bar, color=COLORS['secondary'], linewidth=1.5, label='CL')
    ax1.axhline(y=xbar_bar + A3 * s_bar, color=COLORS['warning'], linestyle='--', linewidth=1, label='UCL')
    ax1.axhline(y=xbar_bar - A3 * s_bar, color=COLORS['warning'], linestyle='--', linewidth=1, label='LCL')
    ax1.set_ylabel('Xbar', fontsize=8, fontproperties=_get_font(lang))
    ax1.set_title('Xbar-S Chart', fontsize=9, fontweight='bold', fontproperties=_get_font(lang))
    ax1.legend(fontsize=6)
    ax1.grid(True, linestyle='--', alpha=0.3)
    
    ax2.plot(range(len(s)), s, 'g-', linewidth=0.8, marker='.', markersize=4)
    ax2.axhline(y=s_bar, color=COLORS['secondary'], linewidth=1.5, label='CL')
    ax2.axhline(y=B4 * s_bar, color=COLORS['warning'], linestyle='--', linewidth=1, label='UCL')
    ax2.set_xlabel(TEXTS[lang]['subgroup'], fontsize=8, fontproperties=_get_font(lang))
    ax2.set_ylabel('S', fontsize=8, fontproperties=_get_font(lang))
    ax2.legend(fontsize=6)
    ax2.grid(True, linestyle='--', alpha=0.3)
    
    plt.tight_layout()
    buf = io.BytesIO()
    plt.savefig(buf, format='png', bbox_inches='tight')
    buf.seek(0)
    plt.close(fig)
    return buf


def calculate_stats(data, spec):
    n = len(data)
    mean, std = np.mean(data), np.std(data, ddof=1)
    x_0_135 = np.percentile(data, 0.135)
    x_50 = np.percentile(data, 50)
    x_99_865 = np.percentile(data, 99.865)
    spread = x_99_865 - x_0_135
    
    cp_g = (spec.usl - spec.lsl) / spread if spread > 0 else 999
    cpu_g = (spec.usl - x_50) / (spread / 2) if spread > 0 else 999
    cpl_g = (x_50 - spec.lsl) / (spread / 2) if spread > 0 else 999
    cpk_g = min(cpu_g, cpl_g)
    
    cpk_se = np.sqrt((1 / (9 * n)) * (1 + cpk_g**2 / 2))
    cpk_lcl = max(0, cpk_g - 1.96 * cpk_se)
    cpk_ucl = cpk_g + 1.96 * cpk_se
    
    ppm_upper = (1 - stats.norm.cdf(spec.usl, mean, std)) * 1e6
    ppm_lower = stats.norm.cdf(spec.lsl, mean, std) * 1e6
    
    status = 'PASS' if cpk_g >= 1.33 else 'MARGINAL' if cpk_g >= 1.0 else 'FAIL'
    
    return {
        'n': n, 'mean': mean, 'std': std,
        'x_50': x_50, 'spread': spread,
        'cp_g': cp_g, 'cpk_g': cpk_g,
        'cpk_lcl': cpk_lcl, 'cpk_ucl': cpk_ucl,
        'ppm_upper': ppm_upper, 'ppm_lower': ppm_lower,
        'status': status,
    }


class AIAGVDAReportGenerator:
    def __init__(self, lang='en'):
        self.lang = lang
        self.texts = TEXTS.get(lang, TEXTS['en'])
        self.styles = self._create_styles()
    
    def _create_styles(self):
        styles = getSampleStyleSheet()
        font_name = 'Chinese' if self.lang == 'zh' else 'Helvetica'
        return {
            'title': ParagraphStyle('Title', parent=styles['Heading1'], fontSize=14, fontName=font_name),
            'header': ParagraphStyle('Header', fontSize=9, fontName=font_name, textColor=colors.HexColor(COLORS['primary'])),
            'normal': ParagraphStyle('Normal', fontSize=9, fontName=font_name),
            'small': ParagraphStyle('Small', fontSize=7, textColor=colors.grey, fontName=font_name),
        }
    
    def _p(self, text):
        """创建 Paragraph (支持中文)"""
        return Paragraph(text, self.styles['normal'])
    
    def generate_figure12_1(self, output_path, data, spec, study_info=None):
        if study_info is None:
            study_info = StudyInfo()
        
        stats = calculate_stats(data, spec)
        doc = SimpleDocTemplate(output_path, pagesize=A4, 
                               rightMargin=1*cm, leftMargin=1*cm, topMargin=1*cm, bottomMargin=1*cm)
        story = []
        t = self.texts
        
        # 标题
        story.append(Paragraph(f'<b>{t["title"]}</b>', self.styles['title']))
        story.append(Paragraph(t['subtitle'], self.styles['small']))
        story.append(Spacer(1, 6))
        
        # 行 1-3
        row1 = Table([[self._p(f"{t['process_name']}: {study_info.process_name or 'Process X'}"),
                       self._p(f"{t['machine_name']}: {study_info.machine_name or 'Machine A'}"),
                       self._p(f"{t['study_location']}: {study_info.study_location or 'Site A'}")]], 
                     colWidths=[160, 160, 150])
        row1.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8),
                                  ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor(COLORS['light_gray']))]))
        story.append(row1)
        
        # 行 4-6
        row2 = Table([[self._p(f"{t['study_date']}: {study_info.study_date or datetime.now().strftime('%Y-%m-%d')}"),
                       self._p(f"{t['part_name_id']}: {study_info.part_name or 'Part A'}"),
                       self._p(f"{t['characteristic_name_id']}: {study_info.characteristic_name or 'Dim'}")]], 
                     colWidths=[160, 160, 150])
        row2.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8)]))
        story.append(row2)
        
        # 行 7-10
        row3 = Table([[self._p(f"{t['study_remarks']}: {study_info.study_remarks}"),
                       self._p(f"{t['sample_size']} = {stats['n']}, {t['subgroup_size']} = {study_info.subgroup_size}")]], 
                     colWidths=[320, 150])
        row3.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8),
                                  ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor(COLORS['light_gray']))]))
        story.append(row3)
        story.append(Spacer(1, 6))
        
        # 图表
        hist_img = Image(create_histogram(data, spec, self.lang), width=230, height=145)
        raw_img = Image(create_raw_chart(data, spec, self.lang), width=240, height=100)
        charts1 = Table([[self._p(f"11. {t['histogram']}"), self._p(f"12. {t['raw_value_chart']}")], [hist_img, raw_img]], 
                        colWidths=[240, 240])
        charts1.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, 0), 8),
                                     ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(COLORS['light_gray']))]))
        story.append(charts1)
        
        prob_img = Image(create_probability_plot(data, self.lang), width=200, height=145)
        xbar_img = Image(create_xbar_s_chart(data, study_info.subgroup_size, self.lang), width=280, height=180)
        charts2 = Table([[self._p(f"13. {t['probability_plot']}"), self._p(f"14. {t['xbar_s_chart']}")], [prob_img, xbar_img]], 
                        colWidths=[200, 280])
        charts2.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, 0), 8),
                                     ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(COLORS['light_gray']))]))
        story.append(charts2)
        story.append(Spacer(1, 6))
        
        # 行 15-17
        row4 = Table([[self._p(f"15. {t['x50_percent']} = {stats['x_50']:.6f}"),
                       self._p(f"16. {t['spread']} = {stats['spread']:.6f}"),
                       self._p(f"17. {t['distribution_model']}: {t['normal']}")]], 
                     colWidths=[160, 160, 160])
        row4.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8),
                                  ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor(COLORS['light_gray']))]))
        story.append(row4)
        
        # 行 18
        row5 = Table([[self._p(f"18. {t['capability_requirement']}: Cp,G = {stats['cp_g']:.2f}, Cpk,G = {stats['cpk_g']:.2f} [{stats['status']}] ({t['geometric_method']})")]], 
                     colWidths=[480])
        row5.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 9)]))
        story.append(row5)
        
        # 行 19
        row6 = Table([[self._p(f"19. Cpk: [{stats['cpk_lcl']:.2f}, {stats['cpk_ucl']:.2f}] {t['ci_95']} | {t['ppm_upper']} = {stats['ppm_upper']:.1f}, {t['ppm_lower']} = {stats['ppm_lower']:.1f}")]], 
                     colWidths=[480])
        row6.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8)]))
        story.append(row6)
        
        # 行 20
        conclusion = "Requirements met" if stats['status'] == 'PASS' else "Process needs improvement"
        row7 = Table([[self._p(f"20. {t['conclusion']}: {conclusion}")]], colWidths=[480])
        row7.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 9),
                                  ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor(COLORS['light_gray']))]))
        story.append(row7)
        
        doc.build(story)
        return output_path

    def generate_figure12_2(self, output_path, data, spec, study_info=None):
        """Figure 12-2: 非正态/混合分布报告 (22个元素)"""
        if study_info is None:
            study_info = StudyInfo()
        
        stats = calculate_stats(data, spec)
        doc = SimpleDocTemplate(output_path, pagesize=A4, 
                               rightMargin=1*cm, leftMargin=1*cm, topMargin=1*cm, bottomMargin=1*cm)
        story = []
        t = self.texts
        
        story.append(Paragraph(f'<b>{t["title"]} - {t["non_normal"]}</b>', self.styles['title']))
        story.append(Paragraph(t['subtitle'], self.styles['small']))
        story.append(Spacer(1, 6))
        
        row1 = Table([[self._p(f"{t['process_name']}: {study_info.process_name or 'Process X'}"),
                       self._p(f"{t['machine_name']}: {study_info.machine_name or 'Machine A'}"),
                       self._p(f"{t['study_location']}: {study_info.study_location or 'Site A'}")]], colWidths=[160, 160, 150])
        row1.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8), ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor(COLORS['light_gray']))]))
        story.append(row1)
        
        row2 = Table([[self._p(f"{t['study_date']}: {study_info.study_date or datetime.now().strftime('%Y-%m-%d')}"),
                       self._p(f"{t['part_name_id']}: {study_info.part_name or 'Part A'}"),
                       self._p(f"{t['characteristic_name_id']}: {study_info.characteristic_name or 'Dim'}")]], colWidths=[160, 160, 150])
        row2.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8)]))
        story.append(row2)
        
        row3 = Table([[self._p(f"{t['study_remarks']}: {study_info.study_remarks} (Non-Normal)"),
                       self._p(f"{t['sample_size']} = {stats['n']}, {t['subgroup_size']} = {study_info.subgroup_size}")]], colWidths=[320, 150])
        row3.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8), ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor(COLORS['light_gray']))]))
        story.append(row3)
        story.append(Spacer(1, 6))
        
        hist_img = Image(create_histogram(data, spec, self.lang), width=230, height=145)
        raw_img = Image(create_raw_chart(data, spec, self.lang), width=240, height=100)
        charts1 = Table([[self._p(f"11. {t['histogram']}"), self._p(f"12. {t['raw_value_chart']}")], [hist_img, raw_img]], colWidths=[240, 240])
        charts1.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, 0), 8), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(COLORS['light_gray']))]))
        story.append(charts1)
        
        prob_img = Image(create_probability_plot(data, self.lang), width=200, height=145)
        xbar_img = Image(create_xbar_s_chart(data, study_info.subgroup_size, self.lang), width=280, height=180)
        charts2 = Table([[self._p(f"13. {t['probability_plot']}"), self._p(f"14. {t['xbar_s_chart']}")], [prob_img, xbar_img]], colWidths=[200, 280])
        charts2.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, 0), 8), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(COLORS['light_gray']))]))
        story.append(charts2)
        story.append(Spacer(1, 6))
        
        row4 = Table([[self._p(f"15. {t['x50_percent']} = {stats['x_50']:.6f}"),
                       self._p(f"16. {t['spread']} = {stats['spread']:.6f}"),
                       self._p(f"17. {t['distribution_model']}: Mixed (C4)")]], colWidths=[160, 160, 160])
        row4.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8), ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor(COLORS['light_gray']))]))
        story.append(row4)
        
        row5 = Table([[self._p(f"18. {t['capability_requirement']}: Cp,G = {stats['cp_g']:.2f}, Cpk,G = {stats['cpk_g']:.2f} [{stats['status']}] ({t['geometric_method']})")]], colWidths=[480])
        row5.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 9)]))
        story.append(row5)
        
        row6 = Table([[self._p(f"19. Cpk: [{stats['cpk_lcl']:.2f}, {stats['cpk_ucl']:.2f}] {t['ci_95']} | {t['ppm_upper']} = {stats['ppm_upper']:.1f}, {t['ppm_lower']} = {stats['ppm_lower']:.1f}")]], colWidths=[480])
        row6.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8)]))
        story.append(row6)
        
        conclusion = "Requirements met" if stats['status'] == 'PASS' else "Process needs improvement"
        row7 = Table([[self._p(f"20. {t['conclusion']}: {conclusion}")]], colWidths=[480])
        row7.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 9), ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor(COLORS['light_gray']))]))
        story.append(row7)
        
        row8 = Table([[self._p(f"21. Time Dependent Distribution Model: Mixed Distribution (C4)")]], colWidths=[480])
        row8.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8)]))
        story.append(row8)
        
        guard_band = stats['std'] * 0.1
        row9 = Table([[self._p(f"22. Measurement Uncertainty: ±{guard_band:.4f} | Guard Band: {guard_band:.4f}")]], colWidths=[480])
        row9.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8), ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor(COLORS['light_gray']))]))
        story.append(row9)
        
        doc.build(story)
        return output_path

    def generate_figure12_3(self, output_path, data, spec, study_info=None):
        """Figure 12-3: 标准统计表格格式"""
        if study_info is None:
            study_info = StudyInfo()
        
        stats = calculate_stats(data, spec)
        doc = SimpleDocTemplate(output_path, pagesize=A4, 
                               rightMargin=1*cm, leftMargin=1*cm, topMargin=1*cm, bottomMargin=1*cm)
        story = []
        t = self.texts
        
        story.append(Paragraph(f'<b>{t["title"]} - Standard Format</b>', self.styles['title']))
        story.append(Paragraph(t['subtitle'], self.styles['small']))
        story.append(Spacer(1, 6))
        
        row1 = Table([[self._p(f"1. {t['process_name']}: {study_info.process_name or 'Process X'}"),
                       self._p(f"2. {t['machine_name']}: {study_info.machine_name or 'Machine A'}"),
                       self._p(f"3. {t['study_location']}: {study_info.study_location or 'Site A'}")]], colWidths=[160, 160, 150])
        row1.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8)]))
        story.append(row1)
        
        row2 = Table([[self._p(f"4. {t['study_date']}: {study_info.study_date or datetime.now().strftime('%Y-%m-%d')}"),
                       self._p(f"5. {t['part_name_id']}: {study_info.part_name or 'Part A'}"),
                       self._p(f"6. {t['characteristic_name_id']}: {study_info.characteristic_name or 'Dim'}")]], colWidths=[160, 160, 150])
        row2.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8)]))
        story.append(row2)
        
        row3 = Table([[self._p(f"7-10. {t['sample_size']}={stats['n']}, {t['subgroup_size']}={study_info.subgroup_size}, {study_info.study_remarks}")]], colWidths=[470])
        row3.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8)]))
        story.append(row3)
        story.append(Spacer(1, 6))
        
        stat_header = [self._p('<b>Drawing Values</b>'), self._p('<b>Collected Values</b>'), self._p('<b>Statistical Values</b>')]
        stat_data = [
            [self._p('USL'), self._p(f'{spec.usl:.6f}'), self._p(f'Mean (X̄): {stats["mean"]:.6f}')],
            [self._p('LSL'), self._p(f'{spec.lsl:.6f}'), self._p(f'Std Dev (σ): {stats["std"]:.6f}')],
            [self._p('Target'), self._p(f'{spec.target:.6f}' if spec.target else 'N/A'), self._p(f'Median: {stats["x_50"]:.6f}')],
            [self._p('Tolerance'), self._p(f'{spec.usl - spec.lsl:.6f}'), self._p(f'Range: {np.max(data) - np.min(data):.6f}')],
            [self._p('Unit'), self._p('mm'), self._p(f'n: {stats["n"]}')],
        ]
        stat_table = Table([stat_header] + stat_data, colWidths=[150, 150, 170])
        stat_table.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(COLORS['primary'])), ('TEXTCOLOR', (0, 0), (-1, 0), colors.white), ('ALIGN', (0, 0), (-1, -1), 'CENTER')]))
        story.append(stat_table)
        story.append(Spacer(1, 6))
        
        hist_img = Image(create_histogram(data, spec, self.lang), width=230, height=145)
        raw_img = Image(create_raw_chart(data, spec, self.lang), width=240, height=100)
        charts1 = Table([[self._p(f"11. {t['histogram']}"), self._p(f"12. {t['raw_value_chart']}")], [hist_img, raw_img]], colWidths=[240, 240])
        charts1.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, 0), 8), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(COLORS['light_gray']))]))
        story.append(charts1)
        
        prob_img = Image(create_probability_plot(data, self.lang), width=200, height=145)
        xbar_img = Image(create_xbar_s_chart(data, study_info.subgroup_size, self.lang), width=280, height=180)
        charts2 = Table([[self._p(f"13. {t['probability_plot']}"), self._p(f"14. {t['xbar_s_chart']}")], [prob_img, xbar_img]], colWidths=[200, 280])
        charts2.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, 0), 8), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(COLORS['light_gray']))]))
        story.append(charts2)
        story.append(Spacer(1, 6))
        
        cap_header = [self._p('<b>Index</b>'), self._p('<b>Value</b>'), self._p('<b>95% CI</b>'), self._p('<b>Status</b>')]
        cap_data = [
            [self._p('Cp,G'), self._p(f'{stats["cp_g"]:.3f}'), self._p('-'), self._p('PASS' if stats['cp_g'] >= 1.33 else 'FAIL')],
            [self._p('Cpk,G'), self._p(f'{stats["cpk_g"]:.3f}'), self._p(f'[{stats["cpk_lcl"]:.2f}, {stats["cpk_ucl"]:.2f}]'), self._p(stats['status'])],
        ]
        cap_table = Table([cap_header] + cap_data, colWidths=[100, 100, 150, 120])
        cap_table.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8), ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(COLORS['primary'])), ('TEXTCOLOR', (0, 0), (-1, 0), colors.white), ('ALIGN', (0, 0), (-1, -1), 'CENTER')]))
        story.append(cap_table)
        story.append(Spacer(1, 6))
        
        ppm_row = Table([[self._p(f"{t['ppm_upper']}: {stats['ppm_upper']:.1f}"), self._p(f"{t['ppm_lower']}: {stats['ppm_lower']:.1f}"), self._p(f"PPM Total: {stats['ppm_upper'] + stats['ppm_lower']:.1f}")]], colWidths=[160, 160, 150])
        ppm_row.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 8)]))
        story.append(ppm_row)
        
        conclusion = "Requirements met" if stats['status'] == 'PASS' else "Process needs improvement"
        conc_row = Table([[self._p(f"{t['conclusion']}: {conclusion}")]], colWidths=[470])
        conc_row.setStyle(TableStyle([('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('FONTSIZE', (0, 0), (-1, -1), 9), ('BACKGROUND', (0, 0), (-1, -1), colors.HexColor(COLORS['light_gray']))]))
        story.append(conc_row)
        
        doc.build(story)
        return output_path


if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser(description='AIAG-VDA Report Generator')
    parser.add_argument('-o', '--output', default='report.pdf')
    parser.add_argument('-l', '--lang', choices=['zh', 'en'], default='zh')
    parser.add_argument('-f', '--figure', choices=['12-1', '12-2', '12-3'], default='12-1')
    args = parser.parse_args()
    
    np.random.seed(42)
    data = np.random.normal(130.0392, 0.033, 875)
    spec = Specification(usl=130.15, lsl=129.95, target=130.05)
    
    gen = AIAGVDAReportGenerator(lang=args.lang)
    if args.figure == '12-1':
        gen.generate_figure12_1(args.output, data, spec)
    elif args.figure == '12-2':
        gen.generate_figure12_2(args.output, data, spec)
    elif args.figure == '12-3':
        gen.generate_figure12_3(args.output, data, spec)
    print(f'Generated: {args.output}')

path: scripts/attribute_control_charts.py
content: #!/usr/bin/env python3
"""
属性控制图模块
包含: p Chart, np Chart, c Chart, u Chart, Z Chart
"""

import numpy as np
from typing import Dict, List, Optional
from dataclasses import dataclass
import io


@dataclass
class AttributeChartResult:
    """属性控制图结果"""
    chart_type: str
    data: np.ndarray
    cl: float
    ucl: float
    lcl: float
    violations: List[int]
    statistics: Dict


class PChart:
    """
    p Chart (不合格品率控制图)
    用于监控不合格品率，样本量可以不同
    """
    
    def __init__(self):
        pass
        
    def calculate(self, defectives: np.ndarray, sample_sizes: np.ndarray) -> AttributeChartResult:
        """
        计算 p 控制图
        
        参数:
            defectives: 不合格品数量数组
            sample_sizes: 样本大小数组
        """
        n = len(defectives)
        
        # 计算各样本的不合格品率
        p_values = defectives / sample_sizes
        
        # 平均不合格品率 (p-bar)
        total_defectives = np.sum(defectives)
        total_samples = np.sum(sample_sizes)
        p_bar = total_defectives / total_samples
        
        # 控制限 (每个样本可能不同)
        ucl = np.zeros(n)
        lcl = np.zeros(n)
        
        for i in range(n):
            if sample_sizes[i] > 0:
                sigma_p = np.sqrt(p_bar * (1 - p_bar) / sample_sizes[i])
                ucl[i] = p_bar + 3 * sigma_p
                lcl[i] = max(0, p_bar - 3 * sigma_p)
            else:
                ucl[i] = 1.0
                lcl[i] = 0.0
        
        # 检测违规点
        violations = []
        for i in range(n):
            if p_values[i] > ucl[i] or p_values[i] < lcl[i]:
                violations.append(i)
        
        return AttributeChartResult(
            chart_type='p',
            data=p_values,
            cl=p_bar,
            ucl=np.mean(ucl),
            lcl=max(0, np.mean(lcl)),
            violations=violations,
            statistics={
                'p_bar': p_bar,
                'total_defectives': total_defectives,
                'total_samples': total_samples,
                'avg_sample_size': np.mean(sample_sizes)
            }
        )


class NPChart:
    """
    np Chart (不合格品数控制图)
    用于监控不合格品数，样本量必须相同
    """
    
    def __init__(self):
        pass
        
    def calculate(self, defectives: np.ndarray, sample_size: int) -> AttributeChartResult:
        """
        计算 np 控制图
        
        参数:
            defectives: 不合格品数量数组
            sample_size: 固定样本大小
        """
        n = len(defectives)
        
        # 平均不合格品数
        np_bar = np.mean(defectives)
        
        # 标准差
        sigma_np = np.sqrt(np_bar * (1 - np_bar / sample_size))
        
        # 控制限
        ucl = np_bar + 3 * sigma_np
        lcl = max(0, np_bar - 3 * sigma_np)
        
        # 检测违规点
        violations = [i for i in range(n) if defectives[i] > ucl or defectives[i] < lcl]
        
        return AttributeChartResult(
            chart_type='np',
            data=defectives,
            cl=np_bar,
            ucl=ucl,
            lcl=lcl,
            violations=violations,
            statistics={
                'np_bar': np_bar,
                'sample_size': sample_size,
                'sigma': sigma_np
            }
        )


class CChart:
    """
    c Chart (缺陷数控制图)
    用于监控固定检测区域的缺陷数
    """
    
    def __init__(self):
        pass
        
    def calculate(self, defects: np.ndarray) -> AttributeChartResult:
        """
        计算 c 控制图
        
        参数:
            defects: 缺陷数数组
        """
        n = len(defects)
        
        # 平均缺陷数 (c-bar)
        c_bar = np.mean(defects)
        
        # 控制限
        ucl = c_bar + 3 * np.sqrt(c_bar)
        lcl = max(0, c_bar - 3 * np.sqrt(c_bar))
        
        # 检测违规点
        violations = [i for i in range(n) if defects[i] > ucl or defects[i] < lcl]
        
        return AttributeChartResult(
            chart_type='c',
            data=defects,
            cl=c_bar,
            ucl=ucl,
            lcl=lcl,
            violations=violations,
            statistics={
                'c_bar': c_bar,
                'sigma': np.sqrt(c_bar)
            }
        )


class UChart:
    """
    u Chart (单位缺陷数控制图)
    用于监控单位产品的缺陷数，检测区域大小可以不同
    """
    
    def __init__(self):
        pass
        
    def calculate(self, defects: np.ndarray, inspection_units: np.ndarray) -> AttributeChartResult:
        """
        计算 u 控制图
        
        参数:
            defects: 缺陷数数组
            inspection_units: 检测单位数数组
        """
        n = len(defects)
        
        # 计算单位缺陷数
        u_values = defects / inspection_units
        
        # 平均单位缺陷数 (u-bar)
        total_defects = np.sum(defects)
        total_units = np.sum(inspection_units)
        u_bar = total_defects / total_units
        
        # 控制限 (每个点可能不同)
        ucl = np.zeros(n)
        lcl = np.zeros(n)
        
        for i in range(n):
            if inspection_units[i] > 0:
                sigma_u = np.sqrt(u_bar / inspection_units[i])
                ucl[i] = u_bar + 3 * sigma_u
                lcl[i] = max(0, u_bar - 3 * sigma_u)
            else:
                ucl[i] = u_bar
                lcl[i] = u_bar
        
        # 检测违规点
        violations = []
        for i in range(n):
            if u_values[i] > ucl[i] or u_values[i] < lcl[i]:
                violations.append(i)
        
        return AttributeChartResult(
            chart_type='u',
            data=u_values,
            cl=u_bar,
            ucl=np.mean(ucl),
            lcl=max(0, np.mean(lcl)),
            violations=violations,
            statistics={
                'u_bar': u_bar,
                'total_defects': total_defects,
                'total_units': total_units
            }
        )


class ZChart:
    """
    Z Chart (标准化得分控制图)
    将任何连续数据标准化到 Z 分数进行监控
    """
    
    def __init__(self):
        pass
        
    def calculate(self, data: np.ndarray, target: float = 0, sigma: float = 1) -> AttributeChartResult:
        """
        计算 Z 控制图
        
        参数:
            data: 数据数组
            target: 目标值 (默认 0)
            sigma: 标准差 (默认 1)
        """
        n = len(data)
        
        # 计算 Z 分数
        z_values = (data - target) / sigma
        
        # 控制限
        ucl = 3
        lcl = -3
        
        # 警告限
        uwl = 2
        lwl = -2
        
        # 检测违规点
        violations = [i for i in range(n) if abs(z_values[i]) > 3]
        
        warnings = [i for i in range(n) if 2 < abs(z_values[i]) <= 3]
        
        return AttributeChartResult(
            chart_type='Z',
            data=z_values,
            cl=0,
            ucl=ucl,
            lcl=lcl,
            violations=violations,
            statistics={
                'target': target,
                'sigma': sigma,
                'mean_data': np.mean(data),
                'std_data': np.std(data),
                'warnings': warnings
            }
        )


# 主函数接口

def run_p_chart(defectives: List[int], sample_sizes: List[int]) -> Dict:
    """运行 p 控制图"""
    chart = PChart()
    result = chart.calculate(np.array(defectives), np.array(sample_sizes))
    return {
        'chart_type': result.chart_type,
        'data': result.data.tolist(),
        'cl': result.cl,
        'ucl': result.ucl,
        'lcl': result.lcl,
        'violations': result.violations,
        'statistics': result.statistics
    }


def run_np_chart(defectives: List[int], sample_size: int) -> Dict:
    """运行 np 控制图"""
    chart = NPChart()
    result = chart.calculate(np.array(defectives), sample_size)
    return {
        'chart_type': result.chart_type,
        'data': result.data.tolist(),
        'cl': result.cl,
        'ucl': result.ucl,
        'lcl': result.lcl,
        'violations': result.violations,
        'statistics': result.statistics
    }


def run_c_chart(defects: List[int]) -> Dict:
    """运行 c 控制图"""
    chart = CChart()
    result = chart.calculate(np.array(defects))
    return {
        'chart_type': result.chart_type,
        'data': result.data.tolist(),
        'cl': result.cl,
        'ucl': result.ucl,
        'lcl': result.lcl,
        'violations': result.violations,
        'statistics': result.statistics
    }


def run_u_chart(defects: List[int], inspection_units: List[float]) -> Dict:
    """运行 u 控制图"""
    chart = UChart()
    result = chart.calculate(np.array(defects), np.array(inspection_units))
    return {
        'chart_type': result.chart_type,
        'data': result.data.tolist(),
        'cl': result.cl,
        'ucl': result.ucl,
        'lcl': result.lcl,
        'violations': result.violations,
        'statistics': result.statistics
    }


def run_z_chart(data: List[float], target: float = 0, sigma: float = 1) -> Dict:
    """运行 Z 控制图"""
    chart = ZChart()
    result = chart.calculate(np.array(data), target, sigma)
    return {
        'chart_type': result.chart_type,
        'data': result.data.tolist(),
        'cl': result.cl,
        'ucl': result.ucl,
        'lcl': result.lcl,
        'violations': result.violations,
        'statistics': result.statistics
    }


if __name__ == '__main__':
    import json
    import numpy as np
    
    # Custom JSON encoder for numpy types
    class NumpyEncoder(json.JSONEncoder):
        def default(self, obj):
            if isinstance(obj, np.integer):
                return int(obj)
            if isinstance(obj, np.floating):
                return float(obj)
            if isinstance(obj, np.ndarray):
                return obj.tolist()
            return super().default(obj)
    
    # 测试 p Chart
    print("Testing p Chart...")
    print(json.dumps(run_p_chart([5, 3, 7, 4, 6], [100, 100, 100, 100, 100]), indent=2, cls=NumpyEncoder))
    
    # 测试 np Chart
    print("\nTesting np Chart...")
    print(json.dumps(run_np_chart([5, 3, 7, 4, 6], 100), indent=2, cls=NumpyEncoder))
    
    # 测试 c Chart
    print("\nTesting c Chart...")
    print(json.dumps(run_c_chart([12, 15, 8, 10, 14]), indent=2, cls=NumpyEncoder))
    
    # 测试 u Chart
    print("\nTesting u Chart...")
    print(json.dumps(run_u_chart([12, 15, 8, 10, 14], [10, 10, 10, 10, 10]), indent=2, cls=NumpyEncoder))
    
    # 测试 Z Chart
    print("\nTesting Z Chart...")
    np.random.seed(42)
    data = np.random.normal(10, 0.1, 20)
    print(json.dumps(run_z_chart(data.tolist(), 10.0, 0.1), indent=2))

path: scripts/batch_generate_reports_v2.py
content: #!/usr/bin/env python3
"""
批量生成 SPC 报告（Figure 12-3 标准格式）- 支持完整中英文
导入 Excel 数据，自动生成 SPC 分析报告
"""

import sys
import os
import numpy as np
import pandas as pd
from datetime import datetime

sys.path.insert(0, '/Library/Python/3.9/site-packages')
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))

# 导入 Figure 12-3 完整生成器（带图表）
from generate_figure12_3 import generate_figure12_3_pdf

# 直接在这里定义支持中英文的生成函数
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch, cm
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus.flowables import Flowable

import io

# 中文字体注册
_CHINESE_FONT_REGISTERED = False
def register_chinese_font():
    global _CHINESE_FONT_REGISTERED
    if _CHINESE_FONT_REGISTERED:
        return True
    font_paths = ["/System/Library/Fonts/STHeiti Light.ttc", "/System/Library/Fonts/PingFang.ttc"]
    for font_path in font_paths:
        if os.path.exists(font_path):
            try:
                pdfmetrics.registerFont(TTFont('Chinese', font_path))
                _CHINESE_FONT_REGISTERED = True
                return True
            except:
                continue
    return False

def _para(text, font_size=7, bold=False, color=None):
    register_chinese_font()
    if bold:
        text = f'<b>{text}</b>'
    if color:
        text = f'<font color="{color}">{text}</font>'
    style = ParagraphStyle(name='temp', fontName='Chinese', fontSize=font_size, leading=font_size + 2)
    return Paragraph(text, style)

class CapabilityBar(Flowable):
    def __init__(self, value, target, max_scale=2.0, width=200, height=45):
        super().__init__()
        self.value = value
        self.target = target
        self.max_scale = max_scale
        self._width = width
        self._height = height
    
    def wrap(self, availWidth, availHeight):
        return (self._width, self._height)
    
    def draw(self):
        canvas = self.canv
        width, height = self._width, self._height
        value_ratio = min(self.value / self.max_scale, 1.0)
        target_ratio = min(self.target / self.max_scale, 1.0)
        passed = self.value >= self.target
        
        bar_height = height * 0.4
        bar_y = (height - bar_height) / 2
        
        # 灰色背景
        canvas.setFillColor(colors.Color(0.92, 0.92, 0.92))
        canvas.rect(0, bar_y, width, bar_height, fill=1, stroke=0)
        
        # 绿色/红色进度条
        bar_color = colors.Color(0.0, 0.65, 0.0) if passed else colors.Color(0.8, 0.0, 0.0)
        canvas.setFillColor(bar_color)
        canvas.rect(0, bar_y, width * value_ratio, bar_height, fill=1, stroke=0)
        
        # 实际值竖线
        value_x = width * value_ratio
        canvas.setStrokeColor(bar_color)
        canvas.setLineWidth(2)
        canvas.line(value_x, bar_y - 4, value_x, bar_y + bar_height + 4)
        
        # 目标值箭头
        target_x = width * target_ratio
        canvas.setStrokeColor(colors.gray)
        canvas.setLineWidth(1.5)
        arrow_h = 5
        canvas.line(target_x - arrow_h/2, bar_y + bar_height + 4 + arrow_h, target_x, bar_y + bar_height + 4 + arrow_h/2)
        canvas.line(target_x, bar_y + bar_height + 4 + arrow_h/2, target_x + arrow_h/2, bar_y + bar_height + 4 + arrow_h)
        canvas.line(target_x - arrow_h/2, bar_y - 4 - arrow_h, target_x, bar_y - 4 - arrow_h/2)
        canvas.line(target_x, bar_y - 4 - arrow_h/2, target_x + arrow_h/2, bar_y - 4 - arrow_h)
        
        # 刻度
        canvas.setStrokeColor(colors.black)
        canvas.setLineWidth(0.5)
        canvas.setFillColor(colors.black)
        canvas.setFont('Helvetica', 7)
        for ratio, label in [(0, '0'), (0.5, '1.0'), (1.0, '2.0')]:
            x = width * ratio
            canvas.line(x, bar_y - 6, x, bar_y - 3)
            canvas.drawCentredString(x, bar_y - 16, label)
        
        # 实际值数字
        canvas.setFillColor(bar_color)
        canvas.setFont('Helvetica-Bold', 8)
        canvas.drawCentredString(value_x, bar_y - 26, f'{self.value:.2f}')
        
        # 目标值标签
        canvas.setFillColor(colors.gray)
        canvas.setFont('Helvetica', 7)
        canvas.drawCentredString(target_x, bar_y - 38, f'Target: {self.target:.2f}')


def generate_figure12_3_bilingual(study_info, data, specification, capability_indices, language='en'):
    """生成 Figure 12-3 报告（支持中英文）"""
    
    # 中英文标签
    L = {
        'en': {
            'title': 'SPC & Process Capability Study Report',
            'subtitle': 'AIAG / VDA SPC harmonized standard',
            'op_name': 'Op.Name', 'evaluation': 'Evaluation',
            'part_no': 'Part No.', 'part_name': 'Part Name', 'phase': 'Phase',
            'char_no': 'Characteristic No.', 'char_name': 'Characteristic Name',
            'department': 'Department', 'machine_id': 'Machine ID', 'machine_name': 'Machine Name',
            'qe': 'QE', 'process_id': 'Process ID', 'process_name': 'Process Name',
            'study_loc': 'Study loc.', 'remarks': 'Remarks',
            'drawing_values': 'Drawing Values', 'collected_values': 'Collected Values',
            'statistical_values': 'Statistical Values',
            'potential_capability': 'Potential Capability Index (95% C.I.)',
            'critical_capability': 'Critical Capability Index (95% C.I.))',
            'requirements_met': 'The requirements were met (Cp, Cpk)',
            'requirements_not_met': 'Requirements NOT met',
            'required_potential': 'Required Potential Performance/Capability Index',
            'required_critical': 'Required Critical Performance/Capability Index',
            'analysis_control': 'Analysis/Control', 'height': 'Height',
            'xbar_remarks': 'xbar/s chart, capability indices for fixed tooling',
            'cp_label': 'Cp,G', 'cpk_label': 'Cpk,G',
        },
        'cn': {
            'title': 'SPC 与过程能力研究报告',
            'subtitle': 'AIAG / VDA SPC 协调标准',
            'op_name': '操作员', 'evaluation': '评估期间',
            'part_no': '零件编号', 'part_name': '零件名称', 'phase': '阶段',
            'char_no': '特性编号', 'char_name': '特性名称',
            'department': '部门', 'machine_id': '设备编号', 'machine_name': '设备名称',
            'qe': '质量工程师', 'process_id': '过程编号', 'process_name': '过程名称',
            'study_loc': '研究地点', 'remarks': '备注',
            'drawing_values': '设计值', 'collected_values': '收集值',
            'statistical_values': '统计值',
            'potential_capability': '潜在能力指数 (95% 置信区间)',
            'critical_capability': '临界能力指数 (95% 置信区间)',
            'requirements_met': '满足要求 (Cp, Cpk)',
            'requirements_not_met': '不满足要求',
            'required_potential': '要求的潜在性能/能力指数',
            'required_critical': '要求的临界性能/能力指数',
            'analysis_control': '分析/控制', 'height': '高度',
            'xbar_remarks': 'xbar/s 控制图，固定工装能力指数',
            'cp_label': 'Cp,G', 'cpk_label': 'Cpk,G',
        },
    }
    
    labels = L.get(language, L['en'])
    
    buffer = io.BytesIO()
    doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=1.5*cm, leftMargin=1.5*cm, topMargin=1.5*cm, bottomMargin=1.5*cm)
    styles = getSampleStyleSheet()
    register_chinese_font()
    story = []
    
    usl = specification['usl']
    lsl = specification['lsl']
    target = specification.get('target', (usl+lsl)/2)
    mu = np.mean(data)
    sigma = np.std(data, ddof=1)
    cp = capability_indices['cp']
    cpk = capability_indices['cpk']
    
    page_width = A4[0] - 3*cm
    col_width = page_width / 6
    
    # 标题
    title_style = ParagraphStyle('title', fontName='Chinese', fontSize=12, leading=14, alignment=1)
    story.append(Paragraph(labels['title'], title_style))
    story.append(Paragraph(labels['subtitle'], ParagraphStyle('subtitle', fontName='Chinese', fontSize=9, leading=11, alignment=1, textColor=colors.gray)))
    story.append(Spacer(1, 0.15*inch))
    
    # 表头
    header_data = [
        [_para(f"{labels['op_name']}:", bold=True), _para(study_info.get('operator_name', 'Quality'), color='blue'),
         _para(f"{labels['evaluation']}:", bold=True), _para(f'from {study_info.get("start_time", "N/A")}\nto {study_info.get("end_time", "N/A")}', color='blue'), '', ''],
        [_para(labels['part_no'], bold=True), _para(study_info.get('part_name', 'N/A'), color='blue'),
         _para(labels['part_name'], bold=True), _para(study_info.get('characteristic_name', 'N/A'), color='blue'),
         _para(labels['phase'], bold=True), _para(labels['analysis_control'], color='blue')],
        [_para(labels['char_no'], bold=True), _para(study_info.get('characteristic_id', '8'), color='blue'),
         _para(labels['char_name'], bold=True), _para(labels['height'], color='blue'),
         _para(labels['department'], bold=True), _para(labels['department'], color='blue')],
        [_para(labels['machine_id'], bold=True), _para(study_info.get('machine_id', 'N/A'), color='blue'),
         _para(labels['machine_name'], bold=True), _para(study_info.get('machine_name', 'N/A'), color='blue'),
         _para(labels['qe'], bold=True), _para(labels['qe'], color='blue')],
        [_para(labels['process_id'], bold=True), _para(study_info.get('process_id', 'N/A'), color='blue'),
         _para(f"{labels['process_name']}:", bold=True), _para(study_info.get('process_name', 'N/A'), color='blue'),
         _para(labels['study_loc'], bold=True), _para(study_info.get('study_location', 'N/A'), color='blue')],
        [_para(labels['remarks'], bold=True), _para(labels['xbar_remarks'], color='blue'), '', '', '', ''],
    ]
    
    header_table = Table(header_data, colWidths=[col_width]*6)
    header_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, -1), 'Chinese'), ('FONTSIZE', (0, 0), (-1, -1), 7),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 3), ('TOPPADDING', (0, 0), (-1, -1), 3),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('SPAN', (3, 0), (5, 0)), ('SPAN', (1, 5), (5, 5)),
        ('WORDWRAP', (0, 0), (-1, -1), 'ON'),
    ]))
    story.append(header_table)
    story.append(Spacer(1, 0.15*inch))
    
    # 统计表格
    col_width_3 = page_width / 3
    if language == 'cn':
        stats_data = [
            [labels['drawing_values'], labels['collected_values'], labels['statistical_values']],
            [f'Tm = {target:.4f}', f'x̃ = {mu:.4f}', f'X50% = {mu:.4f}'],
            [f'LSL = {lsl:.4f}', f'xmin = {min(data):.4f}', f'X0.135% = {mu - 3*sigma:.6f}'],
            [f'USL = {usl:.4f}', f'xmax = {max(data):.4f}', f'X99.865% = {mu + 3*sigma:.6f}'],
            [f'T = {usl-lsl:.4f}', f'R = {np.ptp(data):.4f}', f'Xup3-Xlo3 = {6*sigma:.6f}'],
            ['', f'ntot = {len(data)}', f'neff = {len(data)}'],
            ['', f'n<T> = {len(data)}/100%', 'p<T> = 100%'],
            ['', 'n>USL = 0', 'p>USL = 0%'],
            ['', 'n<LSL = 0', 'p<LSL = 0%'],
            ['测量不确定度：0.0211', f'子组大小 = 5', f'x̄ = {mu:.6f}'],
            ['扩展因子 k = 2 (95.45%)', '子组类型/频率：固定/1 件/小时', f's̄ = {sigma:.6f}'],
            ['Guard Band (95%): 0.0175', '分布模型：混合分布 (EM)', ''],
            ['单位：mm', '计算方法：ISO 22514-2 M2,1', ''],
        ]
    else:
        stats_data = [
            [labels['drawing_values'], labels['collected_values'], labels['statistical_values']],
            [f'Tm = {target:.4f}', f'x̃ = {mu:.4f}', f'X50% = {mu:.4f}'],
            [f'LSL = {lsl:.4f}', f'xmin = {min(data):.4f}', f'X0.135% = {mu - 3*sigma:.6f}'],
            [f'USL = {usl:.4f}', f'xmax = {max(data):.4f}', f'X99.865% = {mu + 3*sigma:.6f}'],
            [f'T = {usl-lsl:.4f}', f'R = {np.ptp(data):.4f}', f'Xup3-Xlo3 = {6*sigma:.6f}'],
            ['', f'ntot = {len(data)}', f'neff = {len(data)}'],
            ['', f'n<T> = {len(data)}/100%', 'p<T> = 100%'],
            ['', 'n>USL = 0', 'p>USL = 0%'],
            ['', 'n<LSL = 0', 'p<LSL = 0%'],
            ['Exp. Uncertainty: 0.0211', f'Subgr.size = 5', f'x̄ = {mu:.6f}'],
            ['Factor k = 2 (95.45%)', 'Subgr: fixed / 1 p.h.', f's̄ = {sigma:.6f}'],
            ['Guard Band (95%): 0.0175', 'Model: Mixed (EM)', ''],
            ['Unit: mm', 'Method: ISO 22514-2 M2,1', ''],
        ]
    
    stats_table = Table(stats_data, colWidths=[col_width_3]*3)
    stats_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, -1), 'Chinese'), ('FONTSIZE', (0, 0), (-1, -1), 7),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 2), ('TOPPADDING', (0, 0), (-1, -1), 2),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
        ('VALIGN', (0, 0), (-1, -1), 'TOP'), ('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey),
    ]))
    story.append(stats_table)
    story.append(Spacer(1, 0.15*inch))
    
    # 能力指数区域
    passed = cpk >= 1.67 and cp >= 1.67
    bar_width = page_width * 0.25 - 10
    cp_bar = CapabilityBar(cp, 1.67, 2.0, width=bar_width, height=45)
    cpk_bar = CapabilityBar(cpk, 1.67, 2.0, width=bar_width, height=45)
    
    cap_data = [
        [labels['potential_capability'], labels['cp_label'], f'{cp:.2f} ≤ {cp:.2f} ≤ {cp*1.1:.2f}', cp_bar],
        [labels['critical_capability'], labels['cpk_label'], f'{cpk:.2f} ≤ {cpk:.2f} ≤ {cpk*1.1:.2f}', cpk_bar],
        ['', '', labels['requirements_met'] if passed else labels['requirements_not_met'], ''],
    ]
    
    col_width_4 = [page_width * 0.38, page_width * 0.12, page_width * 0.25, page_width * 0.25]
    cap_table = Table(cap_data, colWidths=col_width_4)
    cap_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, -1), 'Chinese'), ('FONTSIZE', (0, 0), (-1, -1), 8),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 4), ('TOPPADDING', (0, 0), (-1, -1), 4),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
        ('BACKGROUND', (2, 2), (2, 2), colors.green if passed else colors.red),
        ('TEXTCOLOR', (2, 2), (2, 2), colors.white),
        ('ALIGN', (3, 0), (3, 1), 'CENTER'), ('VALIGN', (3, 0), (3, 1), 'MIDDLE'),
    ]))
    story.append(cap_table)
    story.append(Spacer(1, 0.1*inch))
    
    target_data = [
        [labels['required_potential'], f'{labels["cp_label"]} target', '1.67', ''],
        [labels['required_critical'], f'{labels["cpk_label"]} target', '1.33', ''],
    ]
    target_table = Table(target_data, colWidths=col_width_4)
    target_table.setStyle(TableStyle([
        ('FONTNAME', (0, 0), (-1, -1), 'Chinese'), ('FONTSIZE', (0, 0), (-1, -1), 8),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 4), ('TOPPADDING', (0, 0), (-1, -1), 4),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.black), ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
    ]))
    story.append(target_table)
    
    doc.build(story)
    return buffer.getvalue()


# ==================== 批量生成主程序 ====================

print("=" * 80)
print("SPC 报告批量生成器（Figure 12-3 标准格式，完整中英文支持）")
print("=" * 80)

data_dir = os.path.join(os.path.dirname(__file__), 'tmp', 'test_data')
output_dir_en = os.path.join(os.path.dirname(__file__), 'tmp', 'SPC_Reports_EN')
output_dir_cn = os.path.join(os.path.dirname(__file__), 'tmp', 'SPC_Reports_CN')

os.makedirs(output_dir_en, exist_ok=True)
os.makedirs(output_dir_cn, exist_ok=True)

data_files = [
    ('01_Normal_Distribution', '正态分布', 'Normal Distribution'),
    ('02_NonNormal_Weibull', 'Weibull 偏态', 'Weibull Skewed'),
    ('03_NonNormal_Mixed', '混合双峰', 'Mixed Bimodal'),
]

def load_excel_data(file_path):
    df = pd.read_excel(file_path, sheet_name='SPC_Data')
    return df

def calculate_capability(data, usl, lsl, target):
    mu = np.mean(data)
    sigma = np.std(data, ddof=1)
    cp = (usl - lsl) / (6 * sigma) if sigma > 0 else 0
    cpu = (usl - mu) / (3 * sigma) if sigma > 0 else 0
    cpl = (mu - lsl) / (3 * sigma) if sigma > 0 else 0
    cpk = min(cpu, cpl)
    return {'mu': mu, 'sigma': sigma, 'cp': cp, 'cpk': cpk, 'cpu': cpu, 'cpl': cpl, 'pp': cp, 'ppk': cpk}

def generate_report(df, output_path, language='en'):
    data = df['Measurement_Value'].values
    usl = df['USL'].iloc[0]
    lsl = df['LSL'].iloc[0]
    target = df['Target'].iloc[0]
    cap = calculate_capability(data, usl, lsl, target)
    
    study_info = {
        'operator_name': df['Operator'].iloc[0],
        'start_time': df['Measurement_Time'].min().strftime('%Y-%m-%d %H:%M:%S'),
        'end_time': df['Measurement_Time'].max().strftime('%Y-%m-%d %H:%M:%S'),
        'part_name': df['Part_Name'].iloc[0],
        'characteristic_name': df['Characteristic'].iloc[0],
        'machine_name': df['Machine'].iloc[0],
        'process_name': df['Process'].iloc[0],
        'process_id': 'PROC-001',
        'characteristic_id': 'CHAR-008',
        'machine_id': 'MC-001',
        'study_location': 'Workshop-1',
    }
    
    specification = {'usl': usl, 'lsl': lsl, 'target': target}
    capability_indices = {'cp': cap['cp'], 'cpk': cap['cpk'], 'cpu': cap['cpu'], 'cpl': cap['cpl'], 'pp': cap['pp'], 'ppk': cap['ppk']}
    
    # 使用完整生成器（带图表）
    pdf_bytes = generate_figure12_3_pdf(study_info, data, specification, capability_indices, language)
    
    with open(output_path, 'wb') as f:
        f.write(pdf_bytes)
    return cap

print("\n开始生成报告...")
print("=" * 80)

for file_prefix, name_cn, name_en in data_files:
    print(f"\n处理：{name_cn} ({name_en})")
    print("-" * 60)
    
    excel_path = os.path.join(data_dir, f'{file_prefix}_Data.xlsx')
    if not os.path.exists(excel_path):
        print(f"  ✗ 文件不存在：{excel_path}")
        continue
    
    df = load_excel_data(excel_path)
    print(f"  ✓ 加载数据：{len(df)} 条记录")
    
    # 英文版
    output_en = os.path.join(output_dir_en, f'{file_prefix}_Figure12-3_EN.pdf')
    cap_en = generate_report(df, output_en, language='en')
    print(f"  ✓ 英文报告：{output_en}")
    print(f"     Cp={cap_en['cp']:.2f}, Cpk={cap_en['cpk']:.2f}")
    
    # 中文版
    output_cn = os.path.join(output_dir_cn, f'{file_prefix}_Figure12-3_CN.pdf')
    cap_cn = generate_report(df, output_cn, language='cn')
    print(f"  ✓ 中文报告：{output_cn}")
    print(f"     Cp={cap_cn['cp']:.2f}, Cpk={cap_cn['cpk']:.2f}")

print("\n" + "=" * 80)
print("报告生成完成！")
print("=" * 80)
print(f"\n英文报告目录：{output_dir_en}")
print(f"中文报告目录：{output_dir_cn}")

# 打开所有 PDF
print("\n正在打开所有 PDF 报告...")
import subprocess
for output_dir in [output_dir_en, output_dir_cn]:
    for f in sorted(os.listdir(output_dir)):
        if f.endswith('.pdf'):
            subprocess.run(['open', os.path.join(output_dir, f)], check=False)

print("完成！")


path: scripts/calculate_capability.py
content: #!/usr/bin/env python3
"""
Process Capability Calculator
Based on AIAG-VDA SPC Manual (Geometric Method)

Usage:
    python3 calculate_capability.py --data "10.1,10.2,10.15,..." --usl 10.5 --lsl 9.5 --json
    python3 calculate_capability.py --data "..." --usl 10.5 --json
"""

import argparse
import json
import sys
import numpy as np
from scipy import stats
from typing import Dict, Any


def parse_data(data_str: str) -> np.ndarray:
    """Parse input data string"""
    try:
        data = json.loads(data_str)
        if isinstance(data, list):
            return np.array([float(x) for x in data])
    except:
        pass
    
    try:
        return np.array([float(x.strip()) for x in data_str.split(',')])
    except:
        raise ValueError("Cannot parse data")


def calculate_capability_indices(
    data: np.ndarray,
    usl: float = None,
    lsl: float = None,
    target: float = None
) -> Dict[str, Any]:
    """
    Calculate process capability indices using geometric method (AIAG-VDA)
    
    Returns:
        Dictionary with Cp, Cpk, Pp, Ppk, and evaluation
    """
    n = len(data)
    mean = np.mean(data)
    std = np.std(data, ddof=1)
    
    result = {
        'n': int(n),
        'mean': float(mean),
        'std': float(std),
        'min': float(np.min(data)),
        'max': float(np.max(data)),
    }
    
    # Calculate indices if specifications provided
    if usl is not None and lsl is not None:
        # Cp - Potential capability
        cp = (usl - lsl) / (6 * std)
        result['cp'] = float(cp)
        
        # Cpk - Actual capability
        cpu = (usl - mean) / (3 * std)
        cpl = (mean - lsl) / (3 * std)
        cpk = min(cpu, cpl)
        result['cpu'] = float(cpu)
        result['cpl'] = float(cpl)
        result['cpk'] = float(cpk)
        
        # Pp - Performance (same as Cp for sample)
        result['pp'] = float(cp)
        
        # Ppk - Performance index
        result['ppk'] = float(cpk)
        
        # Evaluation
        if cpk >= 2.0:
            rating = 'Six Sigma'
            action = 'Maintain, can relax control'
        elif cpk >= 1.67:
            rating = 'Excellent'
            action = 'Maintain current state'
        elif cpk >= 1.33:
            rating = 'Good'
            action = 'Maintain, continuous improvement'
        elif cpk >= 1.0:
            rating = 'Marginal'
            action = 'Need improvement plan'
        else:
            rating = 'Insufficient'
            action = 'Must improve, 100% inspection'
        
        result['rating'] = rating
        result['action'] = action
        result['acceptable'] = bool(cpk >= 1.33)
        
        # Add specifications
        result['usl'] = float(usl)
        result['lsl'] = float(lsl)
        if target:
            result['target'] = float(target)
    
    elif usl is not None:
        # Only upper specification
        cpu = (usl - mean) / (3 * std)
        result['cpu'] = float(cpu)
        result['cpk'] = float(cpu)
        result['usl'] = float(usl)
        
    elif lsl is not None:
        # Only lower specification
        cpl = (mean - lsl) / (3 * std)
        result['cpl'] = float(cpl)
        result['cpk'] = float(cpl)
        result['lsl'] = float(lsl)
    
    return result


def main():
    parser = argparse.ArgumentParser(
        description='Process Capability Calculator (AIAG-VDA Geometric Method)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Two-sided specification
  python3 calculate_capability.py --data "10.1,10.2,10.15,..." --usl 10.5 --lsl 9.5 --json
  
  # Only upper specification
  python3 calculate_capability.py --data "..." --usl 10.5 --json
  
  # With target value
  python3 calculate_capability.py --data "..." --usl 10.5 --lsl 9.5 --target 10.0 --json
        '''
    )
    
    parser.add_argument('--data', '-d', type=str, required=True,
                       help='Measurement data (comma-separated or JSON array)')
    parser.add_argument('--usl', '-u', type=float, default=None,
                       help='Upper Specification Limit')
    parser.add_argument('--lsl', '-l', type=float, default=None,
                       help='Lower Specification Limit')
    parser.add_argument('--target', '-t', type=float, default=None,
                       help='Target value')
    parser.add_argument('--json', '-j', action='store_true',
                       help='Output in JSON format')
    
    args = parser.parse_args()
    
    try:
        # Parse data
        data = parse_data(args.data)
        
        if len(data) < 2:
            raise ValueError("At least 2 data points required")
        
        # Calculate capability
        result = calculate_capability_indices(
            data,
            usl=args.usl,
            lsl=args.lsl,
            target=args.target
        )
        
        # Output
        if args.json:
            print(json.dumps(result, indent=2, ensure_ascii=False))
        else:
            print("=" * 60)
            print("Process Capability Analysis Results")
            print("=" * 60)
            print(f"Sample size: {result['n']}")
            print(f"Mean: {result['mean']:.4f}")
            print(f"Std Dev: {result['std']:.4f}")
            print(f"Min: {result['min']:.4f}")
            print(f"Max: {result['max']:.4f}")
            print()
            
            if 'cpk' in result:
                print("Capability Indices:")
                if 'cp' in result:
                    print(f"  Cp:  {result['cp']:.4f}")
                print(f"  Cpk: {result['cpk']:.4f}")
                if 'cpu' in result:
                    print(f"  CPU: {result['cpu']:.4f}")
                if 'cpl' in result:
                    print(f"  CPL: {result['cpl']:.4f}")
                print()
                print(f"Rating: {result['rating']}")
                print(f"Action: {result['action']}")
                print(f"Acceptable: {'Yes ✅' if result['acceptable'] else 'No ❌'}")
            
            print("=" * 60)
        
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()


path: scripts/control_chart.py
content: #!/usr/bin/env python3
"""
Control Chart Tool
Supports: Xbar-R, Xbar-S, I-MR charts with AIAG-VDA out-of-control rules

Usage:
    python3 control_chart.py --data "[...]" --chart-type Xbar-R --subgroup-size 5 --json
    python3 control_chart.py --data "10.1,10.2,..." --chart-type I-MR --json
"""

import argparse
import json
import sys
import numpy as np
from typing import Dict, Any, List, Tuple


def parse_data(data_str: str) -> List[float]:
    """Parse input data string"""
    try:
        data = json.loads(data_str)
        if isinstance(data, list):
            return [float(x) for x in data]
    except:
        pass
    
    try:
        return [float(x.strip()) for x in data_str.split(',')]
    except:
        raise ValueError("Cannot parse data")


def calculate_xbar_r_chart(
    data: np.ndarray,
    subgroup_size: int
) -> Dict[str, Any]:
    """Calculate Xbar-R control chart"""
    n_subgroups = len(data) // subgroup_size
    subgroups = data[:n_subgroups * subgroup_size].reshape(n_subgroups, subgroup_size)
    
    # Calculate subgroup statistics
    xbar = np.mean(subgroups, axis=1)
    r = np.ptp(subgroups, axis=1)  # Range
    
    # Calculate averages
    xbar_bar = np.mean(xbar)
    r_bar = np.mean(r)
    
    # Control chart constants (AIAG-VDA)
    constants = {
        2: {'A2': 1.880, 'D3': 0, 'D4': 3.267, 'd2': 1.128},
        3: {'A2': 1.023, 'D3': 0, 'D4': 2.574, 'd2': 1.693},
        4: {'A2': 0.729, 'D3': 0, 'D4': 2.282, 'd2': 2.059},
        5: {'A2': 0.577, 'D3': 0, 'D4': 2.114, 'd2': 2.326},
        6: {'A2': 0.483, 'D3': 0, 'D4': 2.004, 'd2': 2.534},
        7: {'A2': 0.419, 'D3': 0.076, 'D4': 1.924, 'd2': 2.704},
        8: {'A2': 0.373, 'D3': 0.136, 'D4': 1.864, 'd2': 2.847},
        9: {'A2': 0.337, 'D3': 0.184, 'D4': 1.816, 'd2': 2.970},
        10: {'A2': 0.308, 'D3': 0.223, 'D4': 1.777, 'd2': 3.078},
    }
    
    const = constants.get(subgroup_size, constants[5])
    A2, D3, D4, d2 = const['A2'], const['D3'], const['D4'], const['d2']
    
    # Calculate control limits
    ucl_xbar = xbar_bar + A2 * r_bar
    lcl_xbar = xbar_bar - A2 * r_bar
    
    ucl_r = D4 * r_bar
    lcl_r = D3 * r_bar
    
    # Estimate sigma
    sigma_est = r_bar / d2
    
    # Detect out-of-control points (Rule 1: beyond control limits)
    ooc_xbar = []
    ooc_r = []
    
    for i in range(n_subgroups):
        if xbar[i] > ucl_xbar or xbar[i] < lcl_xbar:
            ooc_xbar.append(i)
        if r[i] > ucl_r or r[i] < lcl_r:
            ooc_r.append(i)
    
    return {
        'chart_type': 'Xbar-R',
        'n_subgroups': int(n_subgroups),
        'subgroup_size': int(subgroup_size),
        'xbar_bar': float(xbar_bar),
        'r_bar': float(r_bar),
        'sigma_est': float(sigma_est),
        'ucl_xbar': float(ucl_xbar),
        'lcl_xbar': float(lcl_xbar),
        'ucl_r': float(ucl_r),
        'lcl_r': float(lcl_r),
        'xbar_values': xbar.tolist(),
        'r_values': r.tolist(),
        'ooc_xbar': ooc_xbar,
        'ooc_r': ooc_r,
        'in_control': len(ooc_xbar) == 0 and len(ooc_r) == 0
    }


def calculate_imr_chart(
    data: np.ndarray
) -> Dict[str, Any]:
    """Calculate I-MR control chart"""
    n = len(data)
    
    # Calculate moving range
    mr = np.abs(np.diff(data))
    
    # Calculate averages
    x_bar = np.mean(data)
    mr_bar = np.mean(mr)
    
    # Control chart constants
    E2 = 2.66
    D4 = 3.267
    d2 = 1.128
    
    # Calculate control limits
    ucl_x = x_bar + E2 * mr_bar
    lcl_x = x_bar - E2 * mr_bar
    
    ucl_mr = D4 * mr_bar
    lcl_mr = 0  # For MR chart, LCL is typically 0
    
    # Estimate sigma
    sigma_est = mr_bar / d2
    
    # Detect out-of-control points
    ooc_x = []
    ooc_mr = []
    
    for i in range(n):
        if data[i] > ucl_x or data[i] < lcl_x:
            ooc_x.append(i)
    
    for i in range(len(mr)):
        if mr[i] > ucl_mr or mr[i] < lcl_mr:
            ooc_mr.append(i)
    
    return {
        'chart_type': 'I-MR',
        'n_points': int(n),
        'x_bar': float(x_bar),
        'mr_bar': float(mr_bar),
        'sigma_est': float(sigma_est),
        'ucl_x': float(ucl_x),
        'lcl_x': float(lcl_x),
        'ucl_mr': float(ucl_mr),
        'lcl_mr': float(lcl_mr),
        'i_values': data.tolist(),
        'mr_values': mr.tolist(),
        'ooc_i': ooc_x,
        'ooc_mr': ooc_mr,
        'in_control': len(ooc_x) == 0 and len(ooc_mr) == 0
    }


def main():
    parser = argparse.ArgumentParser(
        description='Control Chart Tool (Xbar-R, Xbar-S, I-MR)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Xbar-R chart (n=5)
  python3 control_chart.py --data "[10.1,10.2,10.15,10.18,10.12, 10.05,10.22,10.08,10.14,10.19]" --chart-type Xbar-R --subgroup-size 5 --json
  
  # I-MR chart
  python3 control_chart.py --data "10.1,10.2,10.15,10.18,10.12" --chart-type I-MR --json
        '''
    )
    
    parser.add_argument('--data', '-d', type=str, required=True,
                       help='Measurement data (JSON array or comma-separated)')
    parser.add_argument('--chart-type', '-c', type=str, required=True,
                       choices=['Xbar-R', 'Xbar-S', 'I-MR'],
                       help='Type of control chart')
    parser.add_argument('--subgroup-size', '-n', type=int, default=5,
                       help='Subgroup size (for Xbar charts, default: 5)')
    parser.add_argument('--json', '-j', action='store_true',
                       help='Output in JSON format')
    
    args = parser.parse_args()
    
    try:
        # Parse data
        data = parse_data(args.data)
        data_array = np.array(data)
        
        if len(data_array) < 5:
            raise ValueError("At least 5 data points required")
        
        # Calculate control chart based on type
        if args.chart_type in ['Xbar-R', 'Xbar-S']:
            result = calculate_xbar_r_chart(data_array, args.subgroup_size)
        elif args.chart_type == 'I-MR':
            result = calculate_imr_chart(data_array)
        
        # Output
        if args.json:
            print(json.dumps(result, indent=2, ensure_ascii=False))
        else:
            print("=" * 60)
            print(f"{args.chart_type} Control Chart Results")
            print("=" * 60)
            print(f"Subgroups: {result.get('n_subgroups', result.get('n_points', len(data)))}")
            if 'subgroup_size' in result:
                print(f"Subgroup Size: {result['subgroup_size']}")
            print()
            
            print("Control Limits:")
            if 'xbar_bar' in result:
                print(f"  Xbar Chart:")
                print(f"    CL:  {result['xbar_bar']:.4f}")
                print(f"    UCL: {result['ucl_xbar']:.4f}")
                print(f"    LCL: {result['lcl_xbar']:.4f}")
                print(f"  R Chart:")
                print(f"    CL:  {result['r_bar']:.4f}")
                print(f"    UCL: {result['ucl_r']:.4f}")
                print(f"    LCL: {result['lcl_r']:.4f}")
            else:
                print(f"  I Chart:")
                print(f"    CL:  {result['x_bar']:.4f}")
                print(f"    UCL: {result['ucl_x']:.4f}")
                print(f"    LCL: {result['lcl_x']:.4f}")
                print(f"  MR Chart:")
                print(f"    CL:  {result['mr_bar']:.4f}")
                print(f"    UCL: {result['ucl_mr']:.4f}")
                print(f"    LCL: {result['lcl_mr']:.4f}")
            
            print()
            print(f"Sigma Estimate: {result['sigma_est']:.4f}")
            print()
            
            if result.get('ooc_xbar') or result.get('ooc_i'):
                ooc = result.get('ooc_xbar', result.get('ooc_i', []))
                print(f"⚠️  Out-of-Control Points: {ooc}")
            else:
                print("✅ Process is in statistical control")
            
            print(f"In Control: {'Yes ✅' if result['in_control'] else 'No ❌'}")
            print("=" * 60)
        
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()


path: scripts/data_import.py
content: """
数据批量导入模块
支持 CSV、Excel 格式数据导入
"""

import io
import csv
from datetime import datetime
from typing import List, Dict, Any, Optional
from pydantic import BaseModel, Field

import pandas as pd


class ImportResult(BaseModel):
    """导入结果"""
    success: bool
    total_rows: int
    imported_rows: int
    failed_rows: int
    errors: List[str] = Field(default_factory=list)
    warnings: List[str] = Field(default_factory=list)


def validate_measurement_data(
    df: pd.DataFrame,
    required_columns: List[str]
) -> ImportResult:
    """验证测量数据"""
    errors = []
    warnings = []
    
    # 检查必需列
    missing_cols = [col for col in required_columns if col not in df.columns]
    if missing_cols:
        errors.append(f"缺少必需列：{', '.join(missing_cols)}")
        return ImportResult(
            success=False,
            total_rows=len(df),
            imported_rows=0,
            failed_rows=len(df),
            errors=errors,
            warnings=warnings
        )
    
    # 检查空值
    for col in required_columns:
        null_count = df[col].isnull().sum()
        if null_count > 0:
            warnings.append(f"列 '{col}' 有 {null_count} 个空值")
    
    # 检查数值列
    numeric_cols = ['measurement_value', 'usl', 'lsl', 'target']
    for col in numeric_cols:
        if col in df.columns:
            non_numeric = df[pd.to_numeric(df[col], errors='coerce').isnull() & df[col].notnull()]
            if len(non_numeric) > 0:
                errors.append(f"列 '{col}' 包含 {len(non_numeric)} 个非数值")
    
    # 检查日期格式
    if 'timestamp' in df.columns:
        try:
            pd.to_datetime(df['timestamp'])
        except Exception as e:
            errors.append(f"日期格式错误：{str(e)}")
    
    # 检查规格限
    if 'usl' in df.columns and 'lsl' in df.columns:
        invalid_spec = df[df['usl'] <= df['lsl']]
        if len(invalid_spec) > 0:
            errors.append(f"USL 必须大于 LSL，发现 {len(invalid_spec)} 条错误记录")
    
    success = len(errors) == 0
    
    return ImportResult(
        success=success,
        total_rows=len(df),
        imported_rows=len(df) if success else 0,
        failed_rows=0 if success else len(df),
        errors=errors,
        warnings=warnings
    )


def import_csv_data(
    file_content: bytes,
    required_columns: List[str]
) -> tuple[ImportResult, Optional[pd.DataFrame]]:
    """导入 CSV 数据"""
    try:
        # 读取 CSV
        df = pd.read_csv(io.BytesIO(file_content))
        
        # 验证数据
        result = validate_measurement_data(df, required_columns)
        
        if result.success:
            return result, df
        else:
            return result, None
            
    except Exception as e:
        return ImportResult(
            success=False,
            total_rows=0,
            imported_rows=0,
            failed_rows=0,
            errors=[f"CSV 解析失败：{str(e)}"],
            warnings=[]
        ), None


def import_excel_data(
    file_content: bytes,
    sheet_name: Optional[str] = None,
    required_columns: List[str] = None
) -> tuple[ImportResult, Optional[pd.DataFrame]]:
    """导入 Excel 数据"""
    try:
        # 读取 Excel
        if sheet_name:
            df = pd.read_excel(io.BytesIO(file_content), sheet_name=sheet_name)
        else:
            # 读取第一个 sheet
            excel_file = pd.ExcelFile(io.BytesIO(file_content))
            df = pd.read_excel(excel_file, sheet_name=excel_file.sheet_names[0])
        
        # 验证数据
        result = validate_measurement_data(df, required_columns or [])
        
        if result.success:
            return result, df
        else:
            return result, None
            
    except Exception as e:
        return ImportResult(
            success=False,
            total_rows=0,
            imported_rows=0,
            failed_rows=0,
            errors=[f"Excel 解析失败：{str(e)}"],
            warnings=[]
        ), None


def parse_measurement_data(
    df: pd.DataFrame,
    product_id: Optional[int] = None,
    process_id: Optional[int] = None,
    characteristic_id: Optional[int] = None
) -> List[Dict[str, Any]]:
    """解析测量数据为数据库格式"""
    measurements = []
    
    for _, row in df.iterrows():
        measurement = {
            'product_id': product_id or row.get('product_id'),
            'process_id': process_id or row.get('process_id'),
            'characteristic_id': characteristic_id or row.get('characteristic_id'),
            'measurement_value': float(row.get('measurement_value', 0)),
            'timestamp': row.get('timestamp', datetime.now()),
            'operator': row.get('operator'),
            'equipment': row.get('equipment'),
            'batch_number': row.get('batch_number'),
            'notes': row.get('notes'),
        }
        
        # 添加规格限（如果有）
        if 'usl' in row:
            measurement['usl'] = float(row['usl'])
        if 'lsl' in row:
            measurement['lsl'] = float(row['lsl'])
        if 'target' in row:
            measurement['target'] = float(row['target'])
        
        measurements.append(measurement)
    
    return measurements


def generate_import_template(template_type: str = 'measurement') -> bytes:
    """生成导入模板"""
    if template_type == 'measurement':
        # 测量数据模板
        data = {
            'product_id': [1, 1, 1],
            'process_id': [1, 1, 1],
            'characteristic_id': [1, 1, 1],
            'measurement_value': [10.02, 10.01, 9.99],
            'timestamp': ['2026-03-27 10:00:00', '2026-03-27 11:00:00', '2026-03-27 12:00:00'],
            'operator': ['张三', '李四', '王五'],
            'equipment': ['卡尺#001', '卡尺#001', '卡尺#001'],
            'batch_number': ['BATCH-001', 'BATCH-001', 'BATCH-001'],
            'notes': ['', '', ''],
        }
        
        df = pd.DataFrame(data)
        
        buffer = io.BytesIO()
        df.to_excel(buffer, index=False, sheet_name='测量数据模板')
        buffer.seek(0)
        
        return buffer.read()
    
    elif template_type == 'grr':
        # GR&R 数据模板
        data = {
            'part_id': [1, 1, 1, 2, 2, 2],
            'operator_id': [1, 1, 1, 1, 1, 1],
            'trial_id': [1, 2, 3, 1, 2, 3],
            'measurement_value': [10.02, 10.01, 10.03, 10.05, 10.04, 10.06],
        }
        
        df = pd.DataFrame(data)
        
        buffer = io.BytesIO()
        df.to_excel(buffer, index=False, sheet_name='GR&R 数据模板')
        buffer.seek(0)
        
        return buffer.read()
    
    else:
        raise ValueError(f"不支持的模板类型：{template_type}")


path: scripts/demo_report_generation.py
content: #!/usr/bin/env python3
"""
AIAG-VDA SPC 报告生成演示
- 生成正态和非正态数据
- 正态性检验 (Shapiro-Wilk)
- 根据检验结果自动选择报告格式
"""

import os
import numpy as np
from scipy import stats

# 导入报告生成器
from aiagvda_unified_report import (
    AIAGVDAReportGenerator,
    StudyInfo,
    Specification,
)

# 输出目录
OUTPUT_DIR = '/tmp/SPC_Reports'
os.makedirs(OUTPUT_DIR, exist_ok=True)


def generate_normal_data(n=500, mean=100, std=1.0, seed=42):
    """生成正态分布数据"""
    np.random.seed(seed)
    return np.random.normal(mean, std, n)


def generate_nonnormal_data(n=500, case='mixed', seed=42):
    """生成非正态分布数据
    
    case:
        'mixed' - 混合分布 (两个正态分布叠加)
        'shifted' - 过程漂移 (前半段和后半段均值不同)
        'weibull' - Weibull 分布 (偏态)
    """
    np.random.seed(seed)
    
    if case == 'mixed':
        # 混合分布: 70% 来自 N(100, 1), 30% 来自 N(102, 0.5)
        n1 = int(n * 0.7)
        n2 = n - n1
        data1 = np.random.normal(100, 1.0, n1)
        data2 = np.random.normal(102, 0.5, n2)
        return np.concatenate([data1, data2])
    
    elif case == 'shifted':
        # 过程漂移: 前250个来自 N(100, 1), 后250个来自 N(101.5, 1)
        n1 = n // 2
        data1 = np.random.normal(100, 1.0, n1)
        data2 = np.random.normal(101.5, 1.0, n - n1)
        return np.concatenate([data1, data2])
    
    elif case == 'weibull':
        # Weibull 分布 (右偏)
        return np.random.weibull(2, n) * 2 + 98  # shape=2, shift=98
    
    else:
        return np.random.lognormal(mean=4.5, sigma=0.1, size=n)  # 对数正态


def test_normality(data, alpha=0.05):
    """
    正态性检验
    返回: (is_normal, p_value, test_name)
    """
    n = len(data)
    
    if n <= 5000:
        # Shapiro-Wilk 检验 (适用于 n ≤ 5000)
        stat, p = stats.shapiro(data)
        test_name = "Shapiro-Wilk"
    else:
        # Anderson-Darling 检验 (适用于大样本)
        result = stats.anderson(data, 'norm')
        stat = result.statistic
        # 5% 显著水平的临界值
        critical = result.critical_values[2]
        p = 0.05 if stat < critical else 0.01
        test_name = "Anderson-Darling"
    
    is_normal = p > alpha
    return is_normal, p, test_name


def analyze_dataset(name, data, spec, study_info=None):
    """
    分析数据集并生成相应报告
    """
    print(f"\n{'='*60}")
    print(f"数据集: {name}")
    print(f"{'='*60}")
    print(f"样本量: {len(data)}")
    print(f"均值: {np.mean(data):.4f}")
    print(f"标准差: {np.std(data, ddof=1):.4f}")
    print(f"最小值: {np.min(data):.4f}")
    print(f"最大值: {np.max(data):.4f}")
    
    # 正态性检验
    is_normal, p_value, test_name = test_normality(data)
    print(f"\n正态性检验 ({test_name}):")
    print(f"  P值: {p_value:.6f}")
    print(f"  结论: {'正态分布 ✓' if is_normal else '非正态分布 ✗'}")
    
    # 创建报告生成器
    gen = AIAGVDAReportGenerator(lang='zh')
    
    if is_normal:
        # 正态数据 -> Figure 12-1
        output_path = os.path.join(OUTPUT_DIR, f"{name}_Figure12-1_正态报告.pdf")
        gen.generate_figure12_1(output_path, data, spec, study_info)
        print(f"\n生成报告: {output_path}")
        return '12-1'
    else:
        # 非正态数据 -> Figure 12-2 和 Figure 12-3
        output_12_2 = os.path.join(OUTPUT_DIR, f"{name}_Figure12-2_非正态报告.pdf")
        output_12_3 = os.path.join(OUTPUT_DIR, f"{name}_Figure12-3_标准格式.pdf")
        
        gen.generate_figure12_2(output_12_2, data, spec, study_info)
        gen.generate_figure12_3(output_12_3, data, spec, study_info)
        
        print(f"\n生成报告:")
        print(f"  1. {output_12_2}")
        print(f"  2. {output_12_3}")
        return '12-2/12-3'


def main():
    """主函数"""
    print("="*60)
    print("AIAG-VDA SPC 报告生成演示")
    print("="*60)
    
    # 定义规格限
    spec = Specification(usl=103.0, lsl=97.0, target=100.0)
    
    # 研究信息
    study_info = StudyInfo(
        process_name="精密加工过程",
        machine_name="CNC-001",
        study_location="一号车间",
        part_name="传动轴",
        part_id="SHA-2026-001",
        characteristic_name="外径",
        study_remarks="过程能力研究",
        subgroup_size=5,
    )
    
    results = []
    
    # ========== 1. 正态数据 ==========
    print("\n" + "="*60)
    print("【场景 1】正态分布数据 - 稳定过程")
    print("="*60)
    
    normal_data = generate_normal_data(n=500, mean=100, std=0.8, seed=42)
    report_type = analyze_dataset("Normal_Stable", normal_data, spec, study_info)
    results.append(("Normal_Stable", "正态", 500, report_type))
    
    # ========== 2. 非正态数据 - 混合分布 ==========
    print("\n" + "="*60)
    print("【场景 2】非正态数据 - 混合分布 (多模态)")
    print("="*60)
    
    mixed_data = generate_nonnormal_data(n=500, case='mixed', seed=123)
    report_type = analyze_dataset("NonNormal_Mixed", mixed_data, spec, study_info)
    results.append(("NonNormal_Mixed", "混合分布", 500, report_type))
    
    # ========== 3. 非正态数据 - 过程漂移 ==========
    print("\n" + "="*60)
    print("【场景 3】非正态数据 - 过程漂移")
    print("="*60)
    
    shifted_data = generate_nonnormal_data(n=500, case='shifted', seed=456)
    report_type = analyze_dataset("NonNormal_Shifted", shifted_data, spec, study_info)
    results.append(("NonNormal_Shifted", "过程漂移", 500, report_type))
    
    # ========== 4. 非正态数据 - Weibull 分布 ==========
    print("\n" + "="*60)
    print("【场景 4】非正态数据 - Weibull 分布 (偏态)")
    print("="*60)
    
    weibull_data = generate_nonnormal_data(n=500, case='weibull', seed=789)
    report_type = analyze_dataset("NonNormal_Weibull", weibull_data, spec, study_info)
    results.append(("NonNormal_Weibull", "Weibull分布", 500, report_type))
    
    # ========== 5. 更大样本的正态数据 ==========
    print("\n" + "="*60)
    print("【场景 5】大样本正态数据 - 高产能过程")
    print("="*60)
    
    large_normal = generate_normal_data(n=875, mean=100, std=0.6, seed=999)
    report_type = analyze_dataset("Normal_Large", large_normal, spec, study_info)
    results.append(("Normal_Large", "正态(大样本)", 875, report_type))
    
    # ========== 汇总 ==========
    print("\n" + "="*60)
    print("📊 报告生成汇总")
    print("="*60)
    print(f"{'数据集':<25} {'类型':<15} {'样本量':<10} {'报告格式':<15}")
    print("-"*60)
    for name, dtype, n, rtype in results:
        print(f"{name:<25} {dtype:<15} {n:<10} {rtype:<15}")
    
    print(f"\n报告保存位置: {OUTPUT_DIR}")
    print(f"共生成 {len(results)} 组报告")
    
    # 列出所有生成的文件
    print("\n生成的 PDF 文件:")
    for f in sorted(os.listdir(OUTPUT_DIR)):
        if f.endswith('.pdf'):
            filepath = os.path.join(OUTPUT_DIR, f)
            size = os.path.getsize(filepath) / 1024
            print(f"  • {f} ({size:.1f} KB)")


if __name__ == '__main__':
    main()

path: scripts/doe_factor_effects.py
content: #!/usr/bin/env python3
"""
DOE Factor Effects Calculator
实验设计效应计算工具

Calculates:
- Main effects for each factor
- Two-factor interaction effects
- Creates main effects and interaction plots
"""

import json
import argparse
import numpy as np
from typing import List, Dict, Tuple
from itertools import combinations


def create_design_matrix(k: int) -> np.ndarray:
    """
    Create full factorial design matrix for 2^k design
    
    Args:
        k: Number of factors
    
    Returns:
        Design matrix with -1 (low) and +1 (high) levels
    """
    n_runs = 2 ** k
    design = np.zeros((n_runs, k), dtype=int)
    
    for i in range(k):
        repeat = 2 ** (k - i - 1)
        for j in range(n_runs):
            design[j, i] = -1 if (j // repeat) % 2 == 0 else 1
    
    return design


def calculate_main_effects(design: np.ndarray, responses: np.ndarray) -> Dict[str, float]:
    """
    Calculate main effects for each factor
    
    Main Effect = (Average response at high level) - (Average response at low level)
    """
    n_factors = design.shape[1]
    effects = {}
    
    for i in range(n_factors):
        factor_name = f'X{i+1}'
        
        # Average at high level (+1)
        high_mask = design[:, i] == 1
        avg_high = np.mean(responses[high_mask])
        
        # Average at low level (-1)
        low_mask = design[:, i] == -1
        avg_low = np.mean(responses[low_mask])
        
        # Main effect
        effects[factor_name] = avg_high - avg_low
    
    return effects


def calculate_interaction_effects(design: np.ndarray, responses: np.ndarray, 
                                  order: int = 2) -> Dict[str, float]:
    """
    Calculate interaction effects up to specified order
    
    Interaction effect is calculated by multiplying the columns of interacting factors
    """
    n_factors = design.shape[1]
    effects = {}
    
    # Generate all combinations of factors for interaction
    for r in range(2, order + 1):
        for factor_combo in combinations(range(n_factors), r):
            # Create interaction column
            interaction_col = np.ones(len(design))
            for i in factor_combo:
                interaction_col *= design[:, i]
            
            # Calculate effect
            high_mask = interaction_col == 1
            avg_high = np.mean(responses[high_mask])
            
            low_mask = interaction_col == -1
            avg_low = np.mean(responses[low_mask])
            
            # Create factor name (e.g., X1*X2)
            factor_name = '*'.join([f'X{i+1}' for i in factor_combo])
            effects[factor_name] = avg_high - avg_low
    
    return effects


def pareto_analysis(effects: Dict[str, float], alpha: float = 0.05) -> Dict:
    """
    Perform Pareto analysis to identify significant effects
    
    Uses t-test with Bonferroni correction
    """
    effect_values = list(effects.values())
    effect_names = list(effects.keys())
    
    # Calculate standard error (using Lenth's method for unreplicated designs)
    s0 = 1.5 * np.median(np.abs(effect_values))
    s = 1.5 * np.median(np.abs([e for e in effect_values if np.abs(e) < 2.5 * s0]))
    
    # t-ratios
    t_ratios = {name: abs(val) / s for name, val in effects.items()}
    
    # Sort by absolute effect
    sorted_effects = sorted(effects.items(), key=lambda x: abs(x[1]), reverse=True)
    
    # Determine significance (approximate, for screening)
    t_critical = 2.0  # Approximate for screening
    
    significant = {name: eff for name, eff in effects.items() 
                   if abs(t_ratios[name]) > t_critical}
    
    return {
        'all_effects': effects,
        't_ratios': t_ratios,
        'significant_effects': significant,
        'sorted_effects': sorted_effects,
        'standard_error': s
    }


def analyze_2k_design(responses: List[float], k: int = None) -> Dict:
    """
    Complete analysis of 2^k factorial design
    
    Args:
        responses: Response values in standard order
        k: Number of factors (auto-detected if not specified)
    
    Returns:
        Complete analysis results
    """
    responses = np.array(responses)
    n_runs = len(responses)
    
    # Auto-detect k
    if k is None:
        k = int(np.log2(n_runs))
        if 2 ** k != n_runs:
            raise ValueError(f"Number of runs ({n_runs}) is not a power of 2")
    
    if 2 ** k != n_runs:
        raise ValueError(f"Expected {2**k} runs for {k} factors, got {n_runs}")
    
    # Create design matrix
    design = create_design_matrix(k)
    
    # Calculate effects
    main_effects = calculate_main_effects(design, responses)
    interaction_effects = calculate_interaction_effects(design, responses, order=min(k, 3))
    
    # Pareto analysis
    all_effects = {**main_effects, **interaction_effects}
    pareto = pareto_analysis(all_effects)
    
    # Calculate overall statistics
    grand_mean = np.mean(responses)
    total_ss = np.sum((responses - grand_mean) ** 2)
    
    # SS for each effect
    effect_ss = {}
    for name, effect in all_effects.items():
        # SS = (Contrast)^2 / (n * 2^k) = (n * effect / 2)^2 / (n * 2^k)
        # For 2^k with n=1: SS = effect^2 * 2^(k-2)
        effect_ss[name] = (effect ** 2) * (2 ** (k - 2))
    
    return {
        'design': {
            'type': f'2^{k} Full Factorial',
            'n_factors': k,
            'n_runs': n_runs,
            'design_matrix': design.tolist()
        },
        'statistics': {
            'grand_mean': grand_mean,
            'total_ss': total_ss,
            'effect_ss': effect_ss
        },
        'effects': {
            'main_effects': main_effects,
            'interaction_effects': interaction_effects
        },
        'pareto_analysis': {
            'significant_effects': pareto['significant_effects'],
            't_ratios': pareto['t_ratios'],
            'standard_error': pareto['standard_error'],
            'sorted_effects': pareto['sorted_effects']
        }
    }


def main():
    parser = argparse.ArgumentParser(
        description='DOE Factor Effects Calculator for 2^k Designs',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # 2^2 design (4 runs)
  python3 doe_factor_effects.py --responses "[40,50,45,55]" --k 2 --json
  
  # 2^3 design (8 runs)
  python3 doe_factor_effects.py --responses "[45,52,48,58,46,53,49,59]" --k 3 --json
  
  # Auto-detect k from number of runs
  python3 doe_factor_effects.py --responses "[...]" --json
        '''
    )
    
    parser.add_argument('--responses', type=str, required=True, 
                       help='Response values as JSON array (standard order)')
    parser.add_argument('--k', type=int, help='Number of factors (auto-detected if not specified)')
    parser.add_argument('--json', action='store_true', help='Output as JSON')
    parser.add_argument('--plot', action='store_true', help='Generate plots (requires matplotlib)')
    
    args = parser.parse_args()
    
    # Load responses
    responses = json.loads(args.responses)
    
    # Analyze
    try:
        results = analyze_2k_design(responses, args.k)
    except ValueError as e:
        print(f"Error: {e}")
        return
    
    # Output
    if args.json:
        # Remove design matrix for cleaner JSON
        output = {k: v for k, v in results.items() if k != 'design'}
        output['design'] = {k: v for k, v in results['design'].items() if k != 'design_matrix'}
        print(json.dumps(output, indent=2))
    else:
        print("\n" + "="*60)
        print("DOE Factor Effects Analysis")
        print("="*60)
        
        design = results['design']
        print(f"\nDesign: {design['type']}")
        print(f"Number of Factors: {design['n_factors']}")
        print(f"Number of Runs: {design['n_runs']}")
        
        stats = results['statistics']
        print(f"\nGrand Mean: {stats['grand_mean']:.4f}")
        print(f"Total SS: {stats['total_ss']:.4f}")
        
        print("\n" + "-"*60)
        print("Main Effects:")
        print("-"*60)
        for factor, effect in results['effects']['main_effects'].items():
            direction = "↑" if effect > 0 else "↓"
            print(f"  {factor}: {effect:+.4f} {direction}")
        
        if results['effects']['interaction_effects']:
            print("\n" + "-"*60)
            print("Interaction Effects:")
            print("-"*60)
            for factor, effect in results['effects']['interaction_effects'].items():
                direction = "↑" if effect > 0 else "↓"
                print(f"  {factor}: {effect:+.4f} {direction}")
        
        print("\n" + "-"*60)
        print("Pareto Analysis (Significant Effects):")
        print("-"*60)
        sig_effects = results['pareto_analysis']['significant_effects']
        if sig_effects:
            for factor, effect in sig_effects.items():
                t_ratio = results['pareto_analysis']['t_ratios'][factor]
                print(f"  {factor}: Effect={effect:+.4f}, t-ratio={t_ratio:.2f} ⭐")
        else:
            print("  No statistically significant effects detected")
        
        print("\n" + "-"*60)
        print("All Effects (Sorted by Magnitude):")
        print("-"*60)
        for factor, effect in results['pareto_analysis']['sorted_effects'][:10]:
            t_ratio = results['pareto_analysis']['t_ratios'][factor]
            marker = "⭐" if factor in sig_effects else ""
            print(f"  {factor}: {effect:+.4f} (t={t_ratio:.2f}) {marker}")
        
        print("\n" + "="*60)
        
        # Plot if requested
        if args.plot:
            try:
                import matplotlib.pyplot as plt
                
                # Main effects plot
                fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
                
                # Main effects bar chart
                factors = list(results['effects']['main_effects'].keys())
                effects = list(results['effects']['main_effects'].values())
                colors = ['green' if e > 0 else 'red' for e in effects]
                
                ax1.bar(factors, effects, color=colors)
                ax1.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
                ax1.set_ylabel('Effect')
                ax1.set_title('Main Effects')
                ax1.tick_params(axis='x', rotation=45)
                
                # Pareto chart
                sorted_effects = results['pareto_analysis']['sorted_effects']
                names = [e[0] for e in sorted_effects[:10]]
                values = [abs(e[1]) for e in sorted_effects[:10]]
                
                ax2.bar(range(len(names)), values, color='steelblue')
                ax2.set_xticks(range(len(names)))
                ax2.set_xticklabels(names, rotation=45, ha='right')
                ax2.set_ylabel('|Effect|')
                ax2.set_title('Pareto Chart of Effects')
                
                plt.tight_layout()
                plt.savefig('doe_effects_plot.png', dpi=150)
                print("\n📊 Plot saved to: doe_effects_plot.png")
                
            except ImportError:
                print("\n⚠️  matplotlib not available. Install with: pip3 install matplotlib")


if __name__ == '__main__':
    main()


path: scripts/doe_full_factorial.py
content: #!/usr/bin/env python3
"""
DOE Full Factorial Design Tool
2^k full factorial designs with analysis

Usage:
    python3 doe_full_factorial.py --factors 2 --responses "[40,50,45,55]" --analyze
    python3 doe_full_factorial.py --factors 3 --responses "[45,52,48,58,46,53,49,59]" --seed 42
"""

import argparse
import json
import sys
import numpy as np
from scipy import stats
from itertools import product
from typing import List, Dict, Any


def generate_full_factorial(k: int) -> np.ndarray:
    """Generate 2^k full factorial design matrix"""
    levels = [-1, 1]
    design = np.array(list(product(levels, repeat=k)))
    return design


def calculate_effects(
    design: np.ndarray,
    responses: np.ndarray
) -> Dict[str, float]:
    """Calculate main effects and interactions"""
    n = len(responses)
    k = design.shape[1]
    
    effects = {}
    
    # Main effects
    for i in range(k):
        factor_name = f"A{i+1}"
        contrast = np.sum(design[:, i] * responses)
        effect = (2 / n) * contrast
        effects[factor_name] = float(effect)
    
    # Two-factor interactions
    if k >= 2:
        for i in range(k):
            for j in range(i+1, k):
                interaction = design[:, i] * design[:, j]
                factor_name = f"A{i+1}A{j+1}"
                contrast = np.sum(interaction * responses)
                effect = (2 / n) * contrast
                effects[factor_name] = float(effect)
    
    return effects


def calculate_anova(
    design: np.ndarray,
    responses: np.ndarray
) -> Dict[str, Any]:
    """Perform ANOVA analysis"""
    n = len(responses)
    k = design.shape[1]
    
    # Total sum of squares
    ss_total = np.sum((responses - np.mean(responses))**2)
    
    # Calculate sum of squares for each effect
    effects = calculate_effects(design, responses)
    ss_effects = {}
    for name, effect in effects.items():
        ss_effects[name] = (n / 4) * effect**2
    
    # Error (residual)
    ss_model = sum(ss_effects.values())
    ss_error = ss_total - ss_model
    df_error = n - 1 - len(effects)
    
    # Mean squares
    ms_effects = {name: ss for name, ss in ss_effects.items()}
    ms_error = ss_error / df_error if df_error > 0 else 0
    
    # F-values and p-values
    results = {}
    for name, ms in ms_effects.items():
        f_val = ms / ms_error if ms_error > 0 else 0
        p_val = 1 - stats.f.cdf(f_val, 1, df_error) if df_error > 0 else 1
        results[name] = {
            'effect': effects[name],
            'ss': ss_effects[name],
            'ms': ms,
            'f': f_val,
            'p': p_val,
            'significant': p_val < 0.05
        }
    
    return {
        'effects': results,
        'ss_total': ss_total,
        'ss_error': ss_error,
        'df_error': df_error,
        'ms_error': ms_error
    }


def main():
    parser = argparse.ArgumentParser(
        description='DOE Full Factorial Design (2^k)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # 2^2 factorial design
  python3 doe_full_factorial.py --factors 2 --responses "[40,50,45,55]" --analyze
  
  # 2^3 factorial design
  python3 doe_full_factorial.py --factors 3 --responses "[45,52,48,58,46,53,49,59]" --seed 42
        '''
    )
    
    parser.add_argument('--factors', '-k', type=int, required=True,
                       help='Number of factors (2-8)')
    parser.add_argument('--responses', '-y', type=str, required=True,
                       help='Response values (JSON array or comma-separated)')
    parser.add_argument('--factor-names', '-n', type=str, default=None,
                       help='Factor names (comma-separated)')
    parser.add_argument('--seed', type=int, default=None,
                       help='Random seed for randomization')
    parser.add_argument('--analyze', '-a', action='store_true',
                       help='Perform ANOVA analysis')
    parser.add_argument('--json', '-j', action='store_true',
                       help='Output in JSON format')
    
    args = parser.parse_args()
    
    try:
        # Validate factors
        if args.factors < 1 or args.factors > 8:
            raise ValueError("Number of factors must be 1-8")
        
        # Parse responses
        try:
            responses = np.array(json.loads(args.responses))
        except:
            responses = np.array([float(x.strip()) for x in args.responses.split(',')])
        
        # Validate response count
        n_expected = 2 ** args.factors
        if len(responses) != n_expected:
            raise ValueError(f"Expected {n_expected} responses for {args.factors} factors, got {len(responses)}")
        
        # Generate design
        design = generate_full_factorial(args.factors)
        
        # Get factor names
        if args.factor_names:
            factor_names = [f.strip() for f in args.factor_names.split(',')]
        else:
            factor_names = [f"A{i+1}" for i in range(args.factors)]
        
        # Calculate effects
        effects = calculate_effects(design, responses)
        
        # Output
        if args.json:
            result = {
                'design_type': f'2^{args.factors} Full Factorial',
                'n_factors': args.factors,
                'n_runs': len(responses),
                'factor_names': factor_names,
                'effects': effects
            }
            
            if args.analyze:
                anova = calculate_anova(design, responses)
                result['anova'] = anova
            
            print(json.dumps(result, indent=2, ensure_ascii=False))
        else:
            print("=" * 60)
            print(f"2^{args.factors} Full Factorial Design")
            print("=" * 60)
            print(f"Factors: {args.factors}")
            print(f"Runs: {len(responses)}")
            print()
            
            print("Design Matrix:")
            print("-" * 60)
            header = "\t".join(["Run"] + factor_names + ["Response"])
            print(header)
            for i in range(len(responses)):
                row = "\t".join([str(i+1)] + [str(design[i,j]) for j in range(args.factors)] + [str(responses[i])])
                print(row)
            print()
            
            print("Effects Analysis:")
            print("-" * 60)
            print(f"{'Effect':<15} {'Value':>12} {'Significance':>15}")
            print("-" * 60)
            
            for name, effect in effects.items():
                sig = "**" if abs(effect) > np.std(responses) * 0.5 else ""
                print(f"{name:<15} {effect:>12.4f} {sig:>15}")
            
            if args.analyze:
                print()
                print("ANOVA Results:")
                print("-" * 60)
                anova = calculate_anova(design, responses)
                
                print(f"{'Source':<15} {'Effect':>10} {'SS':>10} {'F':>10} {'p-value':>10}")
                print("-" * 60)
                for name, result in anova['effects'].items():
                    sig = "**" if result['significant'] else ""
                    print(f"{name:<15} {result['effect']:>10.4f} {result['ss']:>10.4f} {result['f']:>10.2f} {result['p']:>10.4f} {sig}")
            
            print("=" * 60)
        
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()


path: scripts/doe_response_surface.py
content: #!/usr/bin/env python3
"""
DOE 响应曲面方法 (Response Surface Methodology)
包含: CCD (Central Composite Design), Box-Behnken
"""

import numpy as np
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
import json


@dataclass
class RSMResult:
    """响应曲面分析结果"""
    design_type: str
    design_matrix: np.ndarray
    coefficients: Dict[str, float]
    anova_table: Dict
    optimization: Dict
    response_surface: Dict


class CentralCompositeDesign:
    """
    中心复合设计 (CCD)
    用于建立响应曲面模型
    """
    
    def __init__(self, n_factors: int, alpha: float = None, face: str = 'circumscribed'):
        """
        初始化 CCD
        
        参数:
            n_factors: 因子数量
            alpha: 星号点距离 (默认 sqrt(2^n_factors))
            face: 立方体面类型: 'circumscribed', 'inscribed', 'faced'
        """
        self.n_factors = n_factors
        self.face = face
        
        # 计算 alpha
        if alpha is None:
            self.alpha = 2 ** (n_factors / 4)  # 默认值
        else:
            self.alpha = alpha
        
    def generate(self, levels: List[Tuple[float, float]] = None) -> np.ndarray:
        """
        生成 CCD 设计矩阵
        
        参数:
            levels: 各因子水平 [(low, high), ...]
        """
        n = self.n_factors
        
        if levels is None:
            # 默认水平 -1, 0, +1
            levels = [(-1, 1)] * n
        
        design = []
        
        # 1. 立方体点 (2^n 全因子)
        for i in range(2 ** n):
            point = []
            for j in range(n):
                bit = (i >> (n - 1 - j)) & 1
                point.append(levels[j][1] if bit else levels[j][0])
            design.append(point)
        
        n_cube = len(design)
        
        # 2. 中心点
        center_point = [0] * n
        design.append(center_point)
        
        # 添加更多中心点 (用于估计纯二次误差)
        for _ in range(n + 1):
            design.append(center_point)
        
        # 3. 星号点 (轴点)
        for j in range(n):
            for sign in [-1, 1]:
                point = [0] * n
                point[j] = sign * self.alpha
                design.append(point)
        
        return np.array(design)
    
    def analyze(self, responses: List[float]) -> RSMResult:
        """分析响应曲面数据"""
        n = self.n_factors
        design = self.generate()
        
        # 构建回归矩阵 (包含线性、二次、交互项)
        X = np.ones((len(design), 1))
        
        # 线性项
        for j in range(n):
            X = np.column_stack((X, design[:, j]))
        
        # 交互项
        for i in range(n):
            for k in range(i + 1, n):
                X = np.column_stack((X, design[:, i] * design[:, k]))
        
        # 二次项
        for j in range(n):
            X = np.column_stack((X, design[:, j] ** 2))
        
        # 最小二乘拟合
        y = np.array(responses)
        try:
            beta = np.linalg.lstsq(X, y, rcond=None)[0]
        except:
            beta = np.zeros(X.shape[1])
        
        # 计算系数
        coefficient_names = ['Intercept']
        for j in range(n):
            coefficient_names.append(f'x{j+1}')
        for i in range(n):
            for k in range(i + 1, n):
                coefficient_names.append(f'x{i+1}x{k+1}')
        for j in range(n):
            coefficient_names.append(f'x{j+1}^2')
        
        coefficients = dict(zip(coefficient_names, beta))
        
        # 计算 R²
        y_pred = X @ beta
        ss_res = np.sum((y - y_pred) ** 2)
        ss_tot = np.sum((y - np.mean(y)) ** 2)
        r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
        
        # ANOVA
        df_model = len(beta) - 1
        df_error = len(y) - len(beta)
        ms_model = ss_res / df_model if df_model > 0 else 0
        ms_error = ss_res / df_error if df_error > 0 else 0
        f_value = ms_model / ms_error if ms_error > 0 else 0
        
        anova = {
            'SS_Regression': ss_res,
            'SS_Total': ss_tot,
            'R_squared': r_squared,
            'F_value': f_value,
            'df_model': df_model,
            'df_error': df_error
        }
        
        # 寻找最优解 (梯度下降)
        optimization = self._optimize(coefficients)
        
        return RSMResult(
            design_type='CCD',
            design_matrix=design,
            coefficients=coefficients,
            anova_table=anova,
            optimization=optimization,
            response_surface={'equation': 'y = b0 + b1*x1 + ... + b12*x1*x2 + ...'}
        )
    
    def _optimize(self, coefficients: Dict) -> Dict:
        """简单优化 (梯度下降)"""
        n = self.n_factors
        best_x = [0] * n
        best_y = float('inf')
        
        # 网格搜索
        for x1 in np.linspace(-2, 2, 20):
            for x2 in np.linspace(-2, 2, 20):
                if n >= 2:
                    y = coefficients.get('Intercept', 0)
                    y += coefficients.get('x1', 0) * x1
                    y += coefficients.get('x2', 0) * x2
                    if n >= 2:
                        y += coefficients.get('x1x2', 0) * x1 * x2
                    y += coefficients.get('x1^2', 0) * x1 ** 2
                    y += coefficients.get('x2^2', 0) * x2 ** 2
                    
                    if y < best_y:
                        best_y = y
                        best_x = [x1, x2]
        
        return {'optimal_x': best_x, 'optimal_y': best_y}


class BoxBehnkenDesign:
    """
    Box-Behnken 设计
    适合因子数量 3-7 的响应曲面
    """
    
    def __init__(self, n_factors: int):
        self.n_factors = n_factors
        
    def generate(self, levels: List[Tuple[float, float]] = None) -> np.ndarray:
        """生成 Box-Behnken 设计矩阵"""
        n = self.n_factors
        
        if levels is None:
            levels = [(-1, 1)] * n
        
        design = []
        
        # 对每个因子组合
        for i in range(n):
            for j in range(i + 1, n):
                # 4 种组合: (+1,+1), (+1,-1), (-1,+1), (-1,-1)
                for combo in [(1, 1), (1, -1), (-1, 1), (-1, -1)]:
                    point = [0] * n
                    point[i] = combo[0]
                    point[j] = combo[1]
                    design.append(point)
        
        # 添加中心点
        center = [0] * n
        for _ in range(n):
            design.append(center)
        
        return np.array(design)
    
    def analyze(self, responses: List[float]) -> RSMResult:
        """分析 Box-Behnken 数据"""
        design = self.generate()
        n = self.n_factors
        
        # 构建回归矩阵
        X = np.ones((len(design), 1))
        for j in range(n):
            X = np.column_stack((X, design[:, j]))
        for i in range(n):
            for k in range(i + 1, n):
                X = np.column_stack((X, design[:, i] * design[:, k]))
        for j in range(n):
            X = np.column_stack((X, design[:, j] ** 2))
        
        y = np.array(responses)
        
        try:
            beta = np.linalg.lstsq(X, y, rcond=None)[0]
        except:
            beta = np.zeros(X.shape[1])
        
        y_pred = X @ beta
        ss_res = np.sum((y - y_pred) ** 2)
        ss_tot = np.sum((y - np.mean(y)) ** 2)
        r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
        
        coefficient_names = ['Intercept'] + [f'x{i+1}' for i in range(n)] + \
                            [f'x{i+1}x{k+1}' for i in range(n) for k in range(i+1, n)] + \
                            [f'x{i+1}^2' for i in range(n)]
        
        return RSMResult(
            design_type='Box-Behnken',
            design_matrix=design,
            coefficients=dict(zip(coefficient_names, beta)),
            anova_table={'R_squared': r_squared},
            optimization={},
            response_surface={'equation': 'y = b0 + b1*x1 + ...'}
        )


# Taguchi 正交表
class TaguchiOrthogonalArray:
    """
    Taguchi 正交表
    """
    
    ARRAYS = {
        # L4 (2^3)
        4: {'factors': 3, 'runs': 4, 'levels': 2},
        # L8 (2^7)
        8: {'factors': 7, 'runs': 8, 'levels': 2},
        # L9 (3^4)
        9: {'factors': 4, 'runs': 9, 'levels': 3},
        # L12 (2^11)
        12: {'factors': 11, 'runs': 12, 'levels': 2},
        # L16 (2^15)
        16: {'factors': 15, 'runs': 16, 'levels': 2},
        # L18 (2^1 × 3^7)
        18: {'factors': 8, 'runs': 18, 'levels': [2, 3]},
        # L27 (3^13)
        27: {'factors': 13, 'runs': 27, 'levels': 3},
    }
    
    # L8 正交表
    L8 = np.array([
        [1,1,1,1,1,1,1],
        [1,1,1,2,2,2,2],
        [1,2,2,1,1,2,2],
        [1,2,2,2,2,1,1],
        [2,1,2,1,2,1,2],
        [2,1,2,2,1,2,1],
        [2,2,1,1,2,2,1],
        [2,2,1,2,1,1,2],
    ])
    
    # L9 正交表
    L9 = np.array([
        [1,1,1,1],
        [1,2,2,2],
        [1,3,3,3],
        [2,1,2,3],
        [2,2,3,1],
        [2,3,1,2],
        [3,1,3,2],
        [3,2,1,3],
        [3,3,2,1],
    ])
    
    @classmethod
    def get_array(cls, runs: int) -> np.ndarray:
        """获取 Taguchi 正交表"""
        if runs == 8:
            return cls.L8
        elif runs == 9:
            return cls.L9
        else:
            # 返回默认 L8
            return cls.L8
    
    @classmethod
    def analyze(cls, responses: List[float], n_factors: int) -> Dict:
        """分析 Taguchi 实验数据"""
        array = cls.get_array(len(responses))
        n_factors = min(n_factors, array.shape[1])
        
        # 计算各因子各水平的均值
        factor_effects = {}
        for j in range(n_factors):
            levels = {}
            for level in [1, 2]:
                mask = array[:, j] == level
                levels[level] = np.mean(np.array(responses)[mask])
            factor_effects[f'Factor{j+1}'] = levels
        
        # 计算 S/N 比 (望目特性)
        sn_ratios = []
        for response in responses:
            sn = 10 * np.log10(response ** 2)
            sn_ratios.append(sn)
        
        return {
            'runs': len(responses),
            'factors': n_factors,
            'factor_effects': factor_effects,
            'sn_ratio': np.mean(sn_ratios),
            'mean_response': np.mean(responses)
        }


# 主函数接口
def run_ccd(n_factors: int, responses: List[float], levels: List[Tuple[float, float]] = None) -> Dict:
    """运行 CCD 分析"""
    ccd = CentralCompositeDesign(n_factors)
    design = ccd.generate(levels)
    # 确保响应数量与设计点数匹配
    n_runs = len(design)
    if len(responses) > n_runs:
        responses = responses[:n_runs]
    elif len(responses) < n_runs:
        # 填充
        responses = responses + [np.mean(responses)] * (n_runs - len(responses))
    result = ccd.analyze(responses)
    return {
        'design_type': result.design_type,
        'design_matrix': result.design_matrix.tolist(),
        'coefficients': result.coefficients,
        'anova': result.anova_table,
        'optimization': result.optimization
    }


def run_box_behnken(n_factors: int, responses: List[float]) -> Dict:
    """运行 Box-Behnken 分析"""
    bb = BoxBehnkenDesign(n_factors)
    design = bb.generate()
    n_runs = len(design)
    if len(responses) > n_runs:
        responses = responses[:n_runs]
    elif len(responses) < n_runs:
        responses = responses + [np.mean(responses)] * (n_runs - len(responses))
    result = bb.analyze(responses)
    return {
        'design_type': result.design_type,
        'design_matrix': result.design_matrix.tolist(),
        'coefficients': result.coefficients,
        'anova': result.anova_table
    }


def run_taguchi(runs: int, responses: List[float], n_factors: int) -> Dict:
    """运行 Taguchi 分析"""
    return TaguchiOrthogonalArray.analyze(responses, n_factors)


if __name__ == '__main__':
    import json
    
    # 测试 CCD
    print("=== CCD Test ===")
    np.random.seed(42)
    responses_ccd = [45 + np.random.normal(0, 2) for _ in range(15)]
    ccd_result = run_ccd(2, responses_ccd)
    print(f"Design: {ccd_result['design_type']}, R²: {ccd_result['anova']['R_squared']:.3f}")
    
    # 测试 Box-Behnken
    print("\n=== Box-Behnken Test ===")
    responses_bb = [50 + np.random.normal(0, 1) for _ in range(13)]
    bb_result = run_box_behnken(3, responses_bb)
    print(f"Design: {bb_result['design_type']}")
    
    # 测试 Taguchi
    print("\n=== Taguchi Test ===")
    responses_tag = [45, 52, 48, 58, 46, 53, 49, 59]
    tag_result = run_taguchi(8, responses_tag, 3)
    print(f"S/N Ratio: {tag_result['sn_ratio']:.2f}")
    print(f"Mean Response: {tag_result['mean_response']:.2f}")

path: scripts/doe_sn_ratio.py
content: #!/usr/bin/env python3
"""
Taguchi Signal-to-Noise (S/N) Ratio Calculator
田口方法信噪比计算工具

S/N Ratios:
- Larger-is-better: Maximize response
- Smaller-is-better: Minimize response
- Nominal-is-best: Target a specific value
"""

import json
import argparse
import numpy as np
from typing import List, Dict, Tuple


def sn_larger_is_better(responses: np.ndarray) -> float:
    """
    Larger-is-better S/N ratio
    望大特性 (越大越好)
    
    Formula: S/N = -10 * log10(Σ(1/y²)/n)
    
    Used for: Strength, yield, efficiency
    """
    n = len(responses)
    sum_inv_sq = np.sum(1 / (responses ** 2))
    sn = -10 * np.log10(sum_inv_sq / n)
    return sn


def sn_smaller_is_better(responses: np.ndarray) -> float:
    """
    Smaller-is-better S/N ratio
    望小特性 (越小越好)
    
    Formula: S/N = -10 * log10(Σy²/n)
    
    Used for: Defects, wear, shrinkage
    """
    n = len(responses)
    sum_sq = np.sum(responses ** 2)
    sn = -10 * np.log10(sum_sq / n)
    return sn


def sn_nominal_is_best(responses: np.ndarray, target: float = None) -> float:
    """
    Nominal-is-best S/N ratio
    望目特性 (越接近目标值越好)
    
    Formula: S/N = 10 * log10(ȳ²/s²)
    
    Used for: Dimensions, voltage, pressure
    
    Args:
        responses: Response values
        target: Target value (optional, uses mean if not specified)
    """
    mean = np.mean(responses)
    variance = np.var(responses, ddof=1)
    
    if variance == 0:
        return float('inf')  # Perfect consistency
    
    # Taguchi's nominal-is-best S/N
    sn = 10 * np.log10((mean ** 2) / variance)
    return sn


def calculate_sn_for_all_trials(data: Dict[str, List[float]], 
                                 sn_type: str, 
                                 target: float = None) -> Dict[str, float]:
    """
    Calculate S/N ratios for all trials
    
    Args:
        data: Dictionary with trial names and response arrays
        sn_type: 'larger', 'smaller', or 'nominal'
        target: Target value for nominal-is-best
    
    Returns:
        S/N ratios for each trial
    """
    sn_ratios = {}
    
    for trial_name, responses in data.items():
        responses = np.array(responses)
        
        if sn_type == 'larger':
            sn = sn_larger_is_better(responses)
        elif sn_type == 'smaller':
            sn = sn_smaller_is_better(responses)
        elif sn_type == 'nominal':
            sn = sn_nominal_is_best(responses, target)
        else:
            raise ValueError(f"Unknown S/N type: {sn_type}")
        
        sn_ratios[trial_name] = sn
    
    return sn_ratios


def analyze_orthogonal_array(trial_data: Dict, 
                              factor_settings: Dict[str, Dict],
                              sn_ratios: Dict[str, float]) -> Dict:
    """
    Analyze orthogonal array experiment results
    
    Args:
        trial_data: Original response data for each trial
        factor_settings: Factor levels for each trial
        sn_ratios: Calculated S/N ratios
    
    Returns:
        Analysis results with main effects and optimal settings
    """
    # Calculate average S/N for each factor level
    factors = list(factor_settings.keys())
    factor_effects = {}
    
    for factor in factors:
        levels = factor_settings[factor]
        level_sn = {}
        
        # Get unique levels
        unique_levels = sorted(set(levels.values()))
        
        for level in unique_levels:
            # Find trials with this factor at this level
            trials_at_level = [trial for trial, lvl in levels.items() if lvl == level]
            
            # Average S/N at this level
            avg_sn = np.mean([sn_ratios[trial] for trial in trials_at_level])
            level_sn[f'{factor}_L{level}'] = avg_sn
        
        factor_effects[factor] = level_sn
    
    # Determine optimal settings (maximize S/N)
    optimal_settings = {}
    for factor in factors:
        level_sn = {k: v for k, v in factor_effects[factor].items() if k.startswith(factor)}
        optimal_level = max(level_sn, key=level_sn.get)
        optimal_settings[factor] = optimal_level
    
    # Calculate predicted optimal S/N
    grand_avg = np.mean(list(sn_ratios.values()))
    predicted_sn = grand_avg
    for factor in factors:
        optimal_level = optimal_settings[factor]
        level_avg = factor_effects[factor][optimal_level]
        predicted_sn += (level_avg - grand_avg)
    
    return {
        'factor_effects': factor_effects,
        'optimal_settings': optimal_settings,
        'predicted_optimal_sn': predicted_sn,
        'current_best_sn': max(sn_ratios.values()),
        'current_best_trial': max(sn_ratios, key=sn_ratios.get)
    }


def main():
    parser = argparse.ArgumentParser(
        description='Taguchi Signal-to-Noise (S/N) Ratio Calculator',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Larger-is-better (e.g., strength)
  python3 doe_sn_ratio.py --responses "[45,52,48,58,46,53,49,59]" --type larger --json
  
  # Smaller-is-better (e.g., defects)
  python3 doe_sn_ratio.py --responses "[2,1,3,1,2,1,2,1]" --type smaller --json
  
  # Nominal-is-best (e.g., dimension with target 10.0)
  python3 doe_sn_ratio.py --responses "[9.8,10.2,10.1,9.9,10.0,10.1,9.9,10.0]" --type nominal --target 10.0 --json
  
  # Full orthogonal array analysis
  python3 doe_sn_ratio.py --data '{"trial1":[45,46,47],"trial2":[52,51,53],...}' --settings '{"A":{"trial1":1,"trial2":1,...},"B":{...}}' --type larger
        '''
    )
    
    parser.add_argument('--responses', type=str, help='Response values as JSON array')
    parser.add_argument('--data', type=str, help='Multiple trials data as JSON object')
    parser.add_argument('--type', choices=['larger', 'smaller', 'nominal'], 
                       required=True, help='S/N ratio type')
    parser.add_argument('--target', type=float, help='Target value for nominal-is-best')
    parser.add_argument('--settings', type=str, help='Factor settings for orthogonal array analysis')
    parser.add_argument('--json', action='store_true', help='Output as JSON')
    
    args = parser.parse_args()
    
    # Single trial or multiple trials
    if args.data:
        # Multiple trials (orthogonal array)
        trial_data = json.loads(args.data)
        
        # Calculate S/N for each trial
        sn_ratios = calculate_sn_for_all_trials(trial_data, args.type, args.target)
        
        # Orthogonal array analysis if settings provided
        if args.settings:
            factor_settings = json.loads(args.settings)
            analysis = analyze_orthogonal_array(trial_data, factor_settings, sn_ratios)
            
            results = {
                'sn_type': args.type,
                'target': args.target,
                'trials': list(trial_data.keys()),
                'sn_ratios': sn_ratios,
                'analysis': analysis
            }
        else:
            results = {
                'sn_type': args.type,
                'target': args.target,
                'trials': list(trial_data.keys()),
                'sn_ratios': sn_ratios
            }
    
    elif args.responses:
        # Single set of responses
        responses = np.array(json.loads(args.responses))
        
        if args.type == 'larger':
            sn = sn_larger_is_better(responses)
            formula = 'S/N = -10 * log10(Σ(1/y²)/n)'
        elif args.type == 'smaller':
            sn = sn_smaller_is_better(responses)
            formula = 'S/N = -10 * log10(Σy²/n)'
        elif args.type == 'nominal':
            sn = sn_nominal_is_best(responses, args.target)
            formula = 'S/N = 10 * log10(ȳ²/s²)'
        
        results = {
            'sn_type': args.type,
            'target': args.target,
            'n_samples': len(responses),
            'mean': float(np.mean(responses)),
            'std': float(np.std(responses, ddof=1)),
            'sn_ratio': sn,
            'formula': formula
        }
    else:
        parser.error('Must specify --responses or --data')
    
    # Output
    if args.json:
        print(json.dumps(results, indent=2))
    else:
        print("\n" + "="*60)
        print("Taguchi Signal-to-Noise (S/N) Ratio Analysis")
        print("="*60)
        
        if 'trials' in results:
            # Multiple trials
            print(f"\nS/N Type: {results['sn_type']}")
            if results['target']:
                print(f"Target: {results['target']}")
            
            print(f"\nNumber of Trials: {len(results['trials'])}")
            
            print("\n" + "-"*60)
            print("S/N Ratios by Trial:")
            print("-"*60)
            for trial, sn in results['sn_ratios'].items():
                print(f"  {trial}: {sn:.4f} dB")
            
            if 'analysis' in results:
                analysis = results['analysis']
                
                print("\n" + "-"*60)
                print("Factor Effects (Average S/N by Level):")
                print("-"*60)
                for factor, level_sn in analysis['factor_effects'].items():
                    print(f"\n  {factor}:")
                    for level, sn in level_sn.items():
                        print(f"    {level}: {sn:.4f} dB")
                
                print("\n" + "-"*60)
                print("Optimal Settings (Maximize S/N):")
                print("-"*60)
                for factor, level in analysis['optimal_settings'].items():
                    print(f"  {factor}: {level}")
                
                print(f"\n  Current Best Trial: {analysis['current_best_trial']} ({analysis['current_best_sn']:.4f} dB)")
                print(f"  Predicted Optimal S/N: {analysis['predicted_optimal_sn']:.4f} dB")
                print(f"  Expected Improvement: {analysis['predicted_optimal_sn'] - analysis['current_best_sn']:.4f} dB")
        
        else:
            # Single trial
            print(f"\nS/N Type: {results['sn_type']}")
            if results['target']:
                print(f"Target: {results['target']}")
            
            print(f"\nSample Size: {results['n_samples']}")
            print(f"Mean: {results['mean']:.4f}")
            print(f"Std Dev: {results['std']:.4f}")
            
            print("\n" + "-"*60)
            print("S/N Ratio:")
            print("-"*60)
            print(f"  S/N = {results['sn_ratio']:.4f} dB")
            print(f"  Formula: {results['formula']}")
            
            # Interpretation
            print("\n" + "-"*60)
            print("Interpretation:")
            print("-"*60)
            if results['sn_type'] == 'larger':
                print("  Higher S/N = Better (maximize response)")
            elif results['sn_type'] == 'smaller':
                print("  Higher S/N = Better (minimize response)")
            elif results['sn_type'] == 'nominal':
                print("  Higher S/N = Better (closer to target, less variation)")
        
        print("\n" + "="*60)


if __name__ == '__main__':
    main()


path: scripts/excel_report.py
content: """
MSA Excel 报告生成器
支持 GR&R、偏倚、线性研究的 Excel 格式报告
"""

import io
from datetime import datetime
from typing import Dict, Any, Optional

import numpy as np
from openpyxl import Workbook
from openpyxl.styles import (
    Font, Alignment, Border, Side, PatternFill, Color
)
from openpyxl.utils import get_column_letter
from openpyxl.chart import ScatterChart, Reference, LineChart, BarChart
from openpyxl.chart.label import DataLabelList


# ==================== 样式定义 ====================

class StyleManager:
    """Excel 样式管理器"""
    
    def __init__(self):
        # 字体
        self.title_font = Font(name='微软雅黑', size=16, bold=True, color='FFFFFF')
        self.heading_font = Font(name='微软雅黑', size=12, bold=True, color='FFFFFF')
        self.subheading_font = Font(name='微软雅黑', size=11, bold=True)
        self.normal_font = Font(name='微软雅黑', size=10)
        self.small_font = Font(name='微软雅黑', size=9, color='666666')
        
        # 对齐
        self.center_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
        self.left_align = Alignment(horizontal='left', vertical='center')
        self.right_align = Alignment(horizontal='right', vertical='center')
        
        # 边框
        self.thin_border = Border(
            left=Side(style='thin'),
            right=Side(style='thin'),
            top=Side(style='thin'),
            bottom=Side(style='thin')
        )
        
        # 填充
        self.header_fill = PatternFill(start_color='1F4E79', end_color='1F4E79', fill_type='solid')
        self.alt_fill1 = PatternFill(start_color='D6EAF8', end_color='D6EAF8', fill_type='solid')
        self.alt_fill2 = PatternFill(start_color='FFFFFF', end_color='FFFFFF', fill_type='solid')
        self.good_fill = PatternFill(start_color='D5F5E3', end_color='D5F5E3', fill_type='solid')
        self.warning_fill = PatternFill(start_color='FDEBD0', end_color='FDEBD0', fill_type='solid')
        self.bad_fill = PatternFill(start_color='FADBD8', end_color='FADBD8', fill_type='solid')


# ==================== GR&R Excel 报告 ====================

def create_grr_excel(
    study_info: Dict[str, Any],
    grr_result: Dict[str, float],
    evaluation: Dict[str, Any],
    data_array: np.ndarray
) -> bytes:
    """创建 GR&R Excel 报告"""
    wb = Workbook()
    styles = StyleManager()
    
    # 1. 封面页
    ws_cover = wb.active
    ws_cover.title = "封面"
    _create_cover_sheet(ws_cover, study_info, styles)
    
    # 2. 结果摘要页
    ws_summary = wb.create_sheet("结果摘要")
    _create_grr_summary(ws_summary, grr_result, evaluation, styles)
    
    # 3. 原始数据页
    ws_data = wb.create_sheet("原始数据")
    _create_grr_data(ws_data, data_array, styles)
    
    # 4. 分析图表页
    ws_chart = wb.create_sheet("分析图表")
    _create_grr_charts(ws_chart, data_array, grr_result, evaluation, styles)
    
    # 5. 统计详情页
    ws_stats = wb.create_sheet("统计详情")
    _create_grr_statistics(ws_stats, data_array, grr_result, styles)
    
    # 保存为字节
    buffer = io.BytesIO()
    wb.save(buffer)
    buffer.seek(0)
    
    return buffer.read()


def _create_cover_sheet(ws, study_info: Dict[str, Any], styles):
    """创建封面页"""
    ws.merge_cells('A1:F1')
    ws['A1'] = "MSA 研究报告"
    ws['A1'].font = styles.title_font
    ws['A1'].alignment = styles.center_align
    
    ws.merge_cells('A2:F2')
    ws['A2'] = f"报告编号：MSA-{study_info.get('id', 'N/A')}-{datetime.now().strftime('%Y%m%d')}"
    ws['A2'].font = styles.normal_font
    ws['A2'].alignment = styles.center_align
    
    data = [
        ['研究类型:', study_info.get('study_type', 'N/A')],
        ['研究名称:', study_info.get('study_name', 'N/A')],
        ['质量特性:', study_info.get('quality_characteristic', 'N/A')],
        ['零件数量:', study_info.get('n_parts', 'N/A')],
        ['操作者数量:', study_info.get('n_operators', 'N/A')],
        ['试验次数:', study_info.get('n_trials', 'N/A')],
        ['生成日期:', datetime.now().strftime('%Y-%m-%d %H:%M:%S')],
    ]
    
    for i, (label, value) in enumerate(data, start=4):
        ws[f'A{i}'] = label
        ws[f'A{i}'].font = styles.subheading_font
        ws[f'A{i}'].alignment = styles.right_align
        ws[f'B{i}'] = value
        ws[f'B{i}'].font = styles.normal_font
        ws[f'B{i}'].alignment = styles.left_align


def _create_grr_summary(ws, grr_result: Dict[str, float], evaluation: Dict[str, Any], styles):
    """创建 GR&R 结果摘要"""
    # 标题
    ws.merge_cells('A1:E1')
    ws['A1'] = "GR&R 分析结果摘要"
    ws['A1'].font = styles.heading_font
    ws['A1'].fill = styles.header_fill
    ws['A1'].alignment = styles.center_align
    
    # 变差分量表
    headers = ['变差分量', '数值', '百分比', '评估']
    data = [
        ['重复性 (EV)', grr_result['ev'], evaluation['percent_ev'], 
         '<10%' if evaluation['percent_ev'] < 10 else '>30%' if evaluation['percent_ev'] > 30 else '10-30%'],
        ['再现性 (AV)', grr_result['av'], evaluation['percent_av'],
         '<10%' if evaluation['percent_av'] < 10 else '>30%' if evaluation['percent_av'] > 30 else '10-30%'],
        ['GR&R (RR)', grr_result['rr'], evaluation['percent_grr'],
         '<10%' if evaluation['percent_grr'] < 10 else '>30%' if evaluation['percent_grr'] > 30 else '10-30%'],
        ['零件变差 (PV)', grr_result['pv'], evaluation['percent_pv'], ''],
        ['总变差 (TV)', grr_result['tv'], '100%', ''],
    ]
    
    for col, header in enumerate(headers, start=1):
        cell = ws.cell(row=3, column=col, value=header)
        cell.font = styles.subheading_font
        cell.fill = styles.header_fill
        cell.alignment = styles.center_align
        cell.border = styles.thin_border
    
    for i, row_data in enumerate(data, start=4):
        fill = styles.alt_fill1 if i % 2 == 0 else styles.alt_fill2
        for col, value in enumerate(row_data, start=1):
            cell = ws.cell(row=i, column=col, value=value)
            cell.font = styles.normal_font
            cell.alignment = styles.center_align
            cell.border = styles.thin_border
            cell.fill = fill
            
            # 根据 %GRR 设置颜色
            if col == 4 and isinstance(value, str):
                if value == '<10%':
                    cell.fill = styles.good_fill
                elif value == '10-30%':
                    cell.fill = styles.warning_fill
                elif value == '>30%':
                    cell.fill = styles.bad_fill
    
    # 评估结论
    ws.merge_cells('A8:E8')
    ws['A8'] = f"评估结论：{evaluation.get('acceptance', 'N/A').upper()}"
    ws['A8'].font = styles.subheading_font
    ws['A8'].alignment = styles.left_align
    
    ws['A9'] = f"ndc (分级数): {evaluation.get('ndc', 'N/A')}"
    ws['A9'].font = styles.normal_font
    
    ws['A10'] = f"判定：{'✅ 合格 (ndc≥5)' if evaluation.get('ndc', 0) >= 5 else '❌ 建议改进 (ndc<5)'}"
    ws['A10'].font = styles.normal_font


def _create_grr_data(ws, data_array: np.ndarray, styles):
    """创建原始数据表"""
    n_parts, n_operators, n_trials = data_array.shape
    
    # 表头
    headers = ['零件', '操作者'] + [f'第{i+1}次' for i in range(n_trials)] + ['平均值', '极差', '标准差']
    
    for col, header in enumerate(headers, start=1):
        cell = ws.cell(row=1, column=col, value=header)
        cell.font = styles.subheading_font
        cell.fill = styles.header_fill
        cell.alignment = styles.center_align
        cell.border = styles.thin_border
    
    # 数据行
    row_num = 2
    for i in range(n_parts):
        for j in range(n_operators):
            values = [data_array[i, j, k] for k in range(n_trials)]
            row = [f'零件{i+1}', f'操作者{j+1}'] + values + [
                np.mean(values),
                max(values) - min(values),
                np.std(values, ddof=1) if len(values) > 1 else 0
            ]
            
            fill = styles.alt_fill1 if row_num % 2 == 0 else styles.alt_fill2
            for col, value in enumerate(row, start=1):
                cell = ws.cell(row=row_num, column=col, value=value)
                cell.font = styles.normal_font
                cell.alignment = styles.center_align
                cell.border = styles.thin_border
                cell.fill = fill
                if isinstance(value, float):
                    cell.number_format = '0.0000'
            
            row_num += 1
    
    # 调整列宽
    for col in range(1, len(headers) + 1):
        ws.column_dimensions[get_column_letter(col)].width = 12


def _create_grr_charts(ws, data_array: np.ndarray, grr_result: Dict, evaluation: Dict, styles):
    """创建分析图表（简化版，兼容移动端）"""
    n_parts, n_operators, n_trials = data_array.shape
    
    # 在 Sheet 中创建图表数据区域（从 K 列开始）
    # 零件平均值数据
    ws['K1'] = "零件平均值"
    ws['K1'].font = styles.subheading_font
    ws['L1'] = "平均值"
    ws['L1'].font = styles.subheading_font
    
    for i in range(n_parts):
        mean_val = float(np.mean(data_array[i, :, :]))
        ws.cell(row=i+2, column=11, value=f"零件{i+1}")  # K 列
        ws.cell(row=i+2, column=12, value=mean_val)       # L 列
    
    # %GRR 数据（从 N 列开始）
    ws['N1'] = "变差百分比"
    ws['N1'].font = styles.subheading_font
    ws['O1'] = "数值"
    ws['O1'].font = styles.subheading_font
    
    metrics = ['%EV', '%AV', '%GRR', '%PV']
    values = [
        float(evaluation['percent_ev']),
        float(evaluation['percent_av']),
        float(evaluation['percent_grr']),
        float(evaluation['percent_pv'])
    ]
    
    for i, (metric, val) in enumerate(zip(metrics, values)):
        ws.cell(row=i+2, column=14, value=metric)  # N 列
        ws.cell(row=i+2, column=15, value=val)     # O 列
    
    # 创建图表 1: 零件平均值图
    try:
        chart1 = LineChart()
        chart1.title = "零件平均值图"
        chart1.x_axis.title = "零件编号"
        chart1.y_axis.title = "平均值"
        
        # 数据范围：L2:L11
        data = Reference(ws, min_col=12, min_row=1, max_row=n_parts+1)
        # 分类范围：K2:K11
        cats = Reference(ws, min_col=11, min_row=2, max_row=n_parts+1)
        
        chart1.add_data(data, titles_from_data=True)
        chart1.set_categories(cats)
        chart1.style = 13
        chart1.shape = 1
        
        ws.add_chart(chart1, "A3")
    except Exception as e:
        ws['A3'] = f"零件平均值图 (图表渲染失败): {[round(float(np.mean(data_array[i, :, :])), 4) for i in range(n_parts)]}"
        ws['A3'].font = styles.normal_font
    
    # 创建图表 2: %GRR 柱状图
    try:
        chart2 = BarChart()
        chart2.title = "变差百分比"
        chart2.x_axis.title = "变差类型"
        chart2.y_axis.title = "百分比 (%)"
        
        # 数据范围：O2:O5
        data = Reference(ws, min_col=15, min_row=1, max_row=5)
        # 分类范围：N2:N5
        cats = Reference(ws, min_col=14, min_row=2, max_row=5)
        
        chart2.add_data(data, titles_from_data=True)
        chart2.set_categories(cats)
        chart2.style = 13
        chart2.shape = 1
        
        ws.add_chart(chart2, "A20")
    except Exception as e:
        ws['A20'] = f"变差百分比 (图表渲染失败): %EV={evaluation['percent_ev']:.1f}%, %AV={evaluation['percent_av']:.1f}%, %GRR={evaluation['percent_grr']:.1f}%, %PV={evaluation['percent_pv']:.1f}%"
        ws['A20'].font = styles.normal_font


def _create_grr_statistics(ws, data_array: np.ndarray, grr_result: Dict, styles):
    """创建统计详情表"""
    n_parts, n_operators, n_trials = data_array.shape
    
    # 基础统计
    headers = ['统计量', '值']
    stats = [
        ['总测量次数', n_parts * n_operators * n_trials],
        ['零件平均值', np.mean(data_array)],
        ['零件标准差', np.std(data_array, ddof=1)],
        ['零件内变差', np.mean([np.std(data_array[i, :, :], ddof=1) for i in range(n_parts)])],
        ['零件间变差', np.std([np.mean(data_array[i, :, :]) for i in range(n_parts)], ddof=1)],
    ]
    
    for col, header in enumerate(headers, start=1):
        cell = ws.cell(row=1, column=col, value=header)
        cell.font = styles.subheading_font
        cell.fill = styles.header_fill
        cell.alignment = styles.center_align
        cell.border = styles.thin_border
    
    for i, (stat, value) in enumerate(stats, start=2):
        ws.cell(row=i, column=1, value=stat).font = styles.normal_font
        ws.cell(row=i, column=2, value=value).font = styles.normal_font
        ws.cell(row=i, column=2).number_format = '0.0000'


# ==================== 偏倚 Excel 报告 ====================

def create_bias_excel(
    study_info: Dict[str, Any],
    bias_result: Dict[str, float],
    measurements: list
) -> bytes:
    """创建偏倚 Excel 报告"""
    wb = Workbook()
    styles = StyleManager()
    
    # 1. 封面页
    ws_cover = wb.active
    ws_cover.title = "封面"
    _create_cover_sheet(ws_cover, study_info, styles)
    
    # 2. 结果摘要页
    ws_summary = wb.create_sheet("结果摘要")
    _create_bias_summary(ws_summary, bias_result, styles)
    
    # 3. 原始数据页
    ws_data = wb.create_sheet("原始数据")
    _create_bias_data(ws_data, measurements, bias_result.get('reference_value', 0), styles)
    
    # 保存
    buffer = io.BytesIO()
    wb.save(buffer)
    buffer.seek(0)
    return buffer.read()


def _create_bias_summary(ws, bias_result: Dict[str, float], styles):
    """创建偏倚结果摘要"""
    ws.merge_cells('A1:D1')
    ws['A1'] = "偏倚分析结果摘要"
    ws['A1'].font = styles.heading_font
    ws['A1'].fill = styles.header_fill
    ws['A1'].alignment = styles.center_align
    
    data = [
        ['参考值', f"{bias_result.get('reference_value', 0):.4f}"],
        ['测量次数', str(bias_result.get('n_measurements', 0))],
        ['偏倚 (Bias)', f"{bias_result.get('bias', 0):.6f}"],
        ['偏倚百分比', f"{bias_result.get('bias_percent', 0):.2f}%"],
        ['t 统计量', f"{bias_result.get('t_statistic', 0):.3f}"],
        ['p 值', f"{bias_result.get('p_value', 0):.4f}"],
        ['显著性', '是' if bias_result.get('is_significant', False) else '否'],
    ]
    
    for i, (label, value) in enumerate(data, start=3):
        ws.cell(row=i, column=1, value=label).font = styles.normal_font
        ws.cell(row=i, column=2, value=value).font = styles.normal_font
        ws.cell(row=i, column=1).border = styles.thin_border
        ws.cell(row=i, column=2).border = styles.thin_border


def _create_bias_data(ws, measurements: list, reference_value: float, styles):
    """创建偏倚原始数据"""
    headers = ['序号', '测量值', '偏倚 (测量值 - 参考值)']
    
    for col, header in enumerate(headers, start=1):
        cell = ws.cell(row=1, column=col, value=header)
        cell.font = styles.subheading_font
        cell.fill = styles.header_fill
        cell.alignment = styles.center_align
        cell.border = styles.thin_border
    
    for i, val in enumerate(measurements, start=2):
        bias = val - reference_value
        ws.cell(row=i, column=1, value=i-1).font = styles.normal_font
        ws.cell(row=i, column=2, value=val).font = styles.normal_font
        ws.cell(row=i, column=3, value=bias).font = styles.normal_font
        ws.cell(row=i, column=2).number_format = '0.0000'
        ws.cell(row=i, column=3).number_format = '+0.0000;-0.0000'
        
        for col in range(1, 4):
            ws.cell(row=i, column=col).border = styles.thin_border
    
    # 统计行
    row = len(measurements) + 2
    ws.cell(row=row, column=1, value='统计').font = styles.subheading_font
    ws.cell(row=row, column=2, value=f'平均值={np.mean(measurements):.4f}').font = styles.normal_font
    ws.cell(row=row, column=3, value=f'标准差={np.std(measurements, ddof=1):.4f}').font = styles.normal_font


# ==================== 线性 Excel 报告 ====================

def create_linearity_excel(
    study_info: Dict[str, Any],
    linearity_result: Dict[str, float],
    reference_values: np.ndarray,
    measurements: np.ndarray
) -> bytes:
    """创建线性 Excel 报告"""
    wb = Workbook()
    styles = StyleManager()
    
    # 1. 封面页
    ws_cover = wb.active
    ws_cover.title = "封面"
    _create_cover_sheet(ws_cover, study_info, styles)
    
    # 2. 结果摘要页
    ws_summary = wb.create_sheet("结果摘要")
    _create_linearity_summary(ws_summary, linearity_result, styles)
    
    # 3. 原始数据页
    ws_data = wb.create_sheet("原始数据")
    _create_linearity_data(ws_data, reference_values, measurements, styles)
    
    # 保存
    buffer = io.BytesIO()
    wb.save(buffer)
    buffer.seek(0)
    return buffer.read()


def _create_linearity_summary(ws, linearity_result: Dict[str, float], styles):
    """创建线性结果摘要"""
    ws.merge_cells('A1:D1')
    ws['A1'] = "线性分析结果摘要"
    ws['A1'].font = styles.heading_font
    ws['A1'].fill = styles.header_fill
    ws['A1'].alignment = styles.center_align
    
    data = [
        ['斜率 (Slope)', f"{linearity_result.get('slope', 0):.6f}"],
        ['截距 (Intercept)', f"{linearity_result.get('intercept', 0):.6f}"],
        ['R 平方', f"{linearity_result.get('r_squared', 0):.4f}"],
        ['斜率 p 值', f"{linearity_result.get('p_value_slope', 0):.4f}"],
        ['截距 p 值', f"{linearity_result.get('p_value_intercept', 0):.4f}"],
        ['线性度', f"{linearity_result.get('linearity', 0):.6f}"],
        ['线性接受', '是' if linearity_result.get('is_linear', False) else '否'],
    ]
    
    for i, (label, value) in enumerate(data, start=3):
        ws.cell(row=i, column=1, value=label).font = styles.normal_font
        ws.cell(row=i, column=2, value=value).font = styles.normal_font
        ws.cell(row=i, column=1).border = styles.thin_border
        ws.cell(row=i, column=2).border = styles.thin_border


def _create_linearity_data(ws, reference_values: np.ndarray, measurements: np.ndarray, styles):
    """创建线性原始数据"""
    headers = ['零件编号', '参考值', '测量值', '偏倚 (测量值 - 参考值)']
    
    for col, header in enumerate(headers, start=1):
        cell = ws.cell(row=1, column=col, value=header)
        cell.font = styles.subheading_font
        cell.fill = styles.header_fill
        cell.alignment = styles.center_align
        cell.border = styles.thin_border
    
    for i, (ref, meas) in enumerate(zip(reference_values, measurements), start=2):
        bias = meas - ref
        ws.cell(row=i, column=1, value=i-1).font = styles.normal_font
        ws.cell(row=i, column=2, value=ref).font = styles.normal_font
        ws.cell(row=i, column=3, value=meas).font = styles.normal_font
        ws.cell(row=i, column=4, value=bias).font = styles.normal_font
        
        for col in range(2, 5):
            ws.cell(row=i, column=col).number_format = '0.0000'
            ws.cell(row=i, column=col).border = styles.thin_border


# ==================== 主函数 ====================

def generate_msa_excel(
    study_type: str,
    study_info: Dict[str, Any],
    results: Dict[str, Any],
    chart_data: Optional[Dict[str, Any]] = None
) -> bytes:
    """生成 MSA Excel 报告"""
    
    if study_type == 'GRR':
        return create_grr_excel(
            study_info=study_info,
            grr_result=results.get('grr_error', {}),
            evaluation=results.get('evaluation', {}),
            data_array=chart_data.get('data_array', np.array([]))
        )
    
    elif study_type == 'BIAS':
        return create_bias_excel(
            study_info=study_info,
            bias_result=results.get('bias_result', {}),
            measurements=chart_data.get('measurements', [])
        )
    
    elif study_type == 'LINEARITY':
        return create_linearity_excel(
            study_info=study_info,
            linearity_result=results.get('linearity_result', {}),
            reference_values=chart_data.get('reference_values', np.array([])),
            measurements=chart_data.get('measurements', np.array([]))
        )
    
    else:
        raise ValueError(f"不支持的研究类型：{study_type}")


path: scripts/generate_doe_report.py
content: #!/usr/bin/env python3
"""
DOE Report Generator
Generates DOE study reports (Full Factorial, Fractional, Taguchi) in PDF format
A4 Portrait layout
"""

import io
import os
import numpy as np
from datetime import datetime
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm, inch
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import matplotlib
matplotlib.use('Agg')
matplotlib.rcParams['font.sans-serif'] = ['STHeiti', 'Arial Unicode MS']
matplotlib.rcParams['axes.unicode_minus'] = False
import matplotlib.pyplot as plt
from scipy import stats

def register_chinese_font():
    font_paths = ["/System/Library/Fonts/STHeiti Light.ttc"]
    for font_path in font_paths:
        if os.path.exists(font_path):
            try:
                pdfmetrics.registerFont(TTFont('Chinese', font_path))
                return True
            except: pass
    return False

register_chinese_font()

def create_effects_plot(effects):
    """Create main effects plot"""
    fig, ax = plt.subplots(figsize=(8, 5))
    
    factor_names = list(effects.keys())
    effect_values = [effects[f] for f in factor_names]
    
    y_pos = np.arange(len(factor_names))
    colors_bar = ['red' if abs(e) > 5 else 'orange' if abs(e) > 2 else 'green' for e in effect_values]
    
    bars = ax.barh(y_pos, effect_values, color=colors_bar)
    ax.set_yticks(y_pos)
    ax.set_yticklabels(factor_names)
    ax.set_xlabel('Effect')
    ax.set_title('Main Effects Plot')
    ax.grid(True, alpha=0.3, axis='x')
    ax.axvline(x=0, color='black', linestyle='-', linewidth=1)
    
    plt.tight_layout()
    buf = io.BytesIO()
    plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
    buf.seek(0)
    plt.close(fig)
    return buf

def create_pareto_chart(effects):
    """Create Pareto chart of effects"""
    fig, ax = plt.subplots(figsize=(8, 5))
    
    factor_names = list(effects.keys())
    effect_values = [abs(effects[f]) for f in factor_names]
    
    # Sort by magnitude
    sorted_idx = np.argsort(effect_values)[::-1]
    sorted_names = [factor_names[i] for i in sorted_idx]
    sorted_values = [effect_values[i] for i in sorted_idx]
    
    y_pos = np.arange(len(sorted_names))
    
    bars = ax.barh(y_pos, sorted_values, color='skyblue', edgecolor='black')
    ax.set_yticks(y_pos)
    ax.set_yticklabels(sorted_names)
    ax.set_xlabel('Absolute Effect')
    ax.set_title('Pareto Chart of Effects')
    ax.grid(True, alpha=0.3, axis='x')
    
    plt.tight_layout()
    buf = io.BytesIO()
    plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
    buf.seek(0)
    plt.close(fig)
    return buf

def generate_doe_full_factorial_report(output_path, design_info, effects, anova_results, responses):
    """Generate DOE Full Factorial PDF report"""
    
    doc = SimpleDocTemplate(output_path, pagesize=A4,
                           rightMargin=10*mm, leftMargin=10*mm,
                           topMargin=10*mm, bottomMargin=10*mm)
    
    styles = getSampleStyleSheet()
    story = []
    
    # Title
    story.append(Paragraph(f"<b>DOE 2^{design_info['factors']} Full Factorial Study Report</b>", 
                          style=ParagraphStyle('Title', fontSize=14, alignment=1)))
    story.append(Spacer(1, 0.15*inch))
    
    # Design Information
    info_data = [
        ['Design Type:', f"2^{design_info['factors']} Full Factorial"],
        ['Number of Factors:', str(design_info['factors'])],
        ['Number of Runs:', str(design_info['runs'])],
        ['Center Points:', str(design_info.get('center_points', 0))],
        ['Date:', datetime.now().strftime('%Y-%m-%d %H:%M:%S')],
    ]
    
    info_table = Table(info_data, colWidths=[5*cm, 8*cm])
    info_table.setStyle(TableStyle([
        ('BOX', (0, 0), (-1, -1), 0.5, colors.black),
        ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
    ]))
    story.append(info_table)
    story.append(Spacer(1, 0.2*inch))
    
    # Effects Table
    story.append(Paragraph("<b>Main Effects and Interactions</b>", 
                          style=ParagraphStyle('Heading', fontSize=11)))
    story.append(Spacer(1, 0.1*inch))
    
    effects_data = [['Factor/Interaction', 'Effect', 'Significance']]
    for name, effect in effects.items():
        sig = '**' if abs(effect) > 5 else '*' if abs(effect) > 2 else ''
        effects_data.append([name, f"{effect:.4f} {sig}", 'Significant' if sig else 'Not Significant'])
    
    effects_table = Table(effects_data, colWidths=[5*cm, 4*cm, 4*cm])
    effects_table.setStyle(TableStyle([
        ('BOX', (0, 0), (-1, -1), 0.5, colors.black),
        ('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
        ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
    ]))
    story.append(effects_table)
    story.append(Spacer(1, 0.2*inch))
    
    # ANOVA Results
    if anova_results:
        story.append(Paragraph("<b>ANOVA Results</b>", 
                              style=ParagraphStyle('Heading', fontSize=11)))
        story.append(Spacer(1, 0.1*inch))
        
        anova_data = [['Source', 'SS', 'MS', 'F', 'p-value']]
        for name, result in anova_results['effects'].items():
            sig = '**' if result['significant'] else ''
            anova_data.append([name, f"{result['ss']:.4f}", f"{result['ms']:.4f}", 
                              f"{result['f']:.2f}", f"{result['p']:.4f} {sig}"])
        
        anova_table = Table(anova_data, colWidths=[4*cm, 2.5*cm, 2.5*cm, 2*cm, 2.5*cm])
        anova_table.setStyle(TableStyle([
            ('BOX', (0, 0), (-1, -1), 0.5, colors.black),
            ('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
            ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
            ('FONTSIZE', (0, 0), (-1, -1), 8),
            ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
        ]))
        story.append(anova_table)
        story.append(Spacer(1, 0.2*inch))
    
    # Charts
    story.append(Paragraph("<b>Analysis Charts</b>", 
                          style=ParagraphStyle('Heading', fontSize=11)))
    story.append(Spacer(1, 0.1*inch))
    
    effects_buf = create_effects_plot(effects)
    effects_buf.seek(0)
    effects_img = Image(effects_buf, width=14*cm, height=9*cm)
    story.append(effects_img)
    story.append(Spacer(1, 0.15*inch))
    
    pareto_buf = create_pareto_chart(effects)
    pareto_buf.seek(0)
    pareto_img = Image(pareto_buf, width=14*cm, height=9*cm)
    story.append(pareto_img)
    story.append(Spacer(1, 0.2*inch))
    
    # Conclusions
    significant_factors = [name for name, effect in effects.items() if abs(effect) > 2]
    
    if significant_factors:
        conclusion = f"<b>Significant Factors:</b> {', '.join(significant_factors)}<br/><br/>"
        conclusion += "<b>Recommendations:</b><br/>"
        conclusion += "1. Focus on optimizing the significant factors identified above<br/>"
        conclusion += "2. Consider conducting response surface methodology for further optimization<br/>"
        conclusion += "3. Validate results with confirmation runs"
    else:
        conclusion = "<b>No significant factors identified at current screening level.</b><br/><br/>"
        conclusion += "<b>Recommendations:</b><br/>"
        conclusion += "1. Consider increasing sample size or reducing noise<br/>"
        conclusion += "2. Review measurement system capability (MSA)<br/>"
        conclusion += "3. Consider additional factors or interactions"
    
    story.append(Paragraph(conclusion, style=ParagraphStyle('Conclusion', fontSize=10)))
    
    # Footer
    story.append(Spacer(1, 0.3*inch))
    footer = Paragraph(
        f"<i>Generated by Rohoon Six Sigma Skill | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</i>",
        style=ParagraphStyle('Footer', fontSize=8, textColor=colors.gray)
    )
    story.append(footer)
    
    doc.build(story)
    print(f"✅ DOE Full Factorial Report generated: {output_path}")

if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('--output', '-o', default='/tmp/doe_report.pdf')
    args = parser.parse_args()
    
    # Test data
    design_info = {'factors': 2, 'runs': 4, 'center_points': 0}
    effects = {'A1': 5.0, 'A2': 10.0, 'A1A2': 0.0}
    anova_results = {
        'effects': {
            'A1': {'ss': 100, 'ms': 100, 'f': 25, 'p': 0.001, 'significant': True},
            'A2': {'ss': 400, 'ms': 400, 'f': 100, 'p': 0.0001, 'significant': True},
            'A1A2': {'ss': 0, 'ms': 0, 'f': 0, 'p': 1.0, 'significant': False},
        }
    }
    responses = np.array([40, 50, 45, 55])
    
    generate_doe_full_factorial_report(args.output, design_info, effects, anova_results, responses)


path: scripts/generate_msa_report.py
content: #!/usr/bin/env python3
"""
MSA Report Generator
Generates MSA study reports (GR&R, Bias, Linearity) in PDF format
A4 Portrait layout
"""

import io
import os
import numpy as np
from datetime import datetime
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm, inch
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import matplotlib
matplotlib.use('Agg')
matplotlib.rcParams['font.sans-serif'] = ['STHeiti', 'Arial Unicode MS']
matplotlib.rcParams['axes.unicode_minus'] = False
import matplotlib.pyplot as plt

def register_chinese_font():
    font_paths = ["/System/Library/Fonts/STHeiti Light.ttc"]
    for font_path in font_paths:
        if os.path.exists(font_path):
            try:
                pdfmetrics.registerFont(TTFont('Chinese', font_path))
                return True
            except: pass
    return False

register_chinese_font()

def create_grr_chart(data_array, grr_result, evaluation):
    """Create GR&R analysis chart"""
    fig, axes = plt.subplots(2, 2, figsize=(10, 8))
    fig.suptitle('GR&R Analysis Charts', fontsize=12, fontweight='bold')
    
    n_parts, n_operators, n_trials = data_array.shape
    
    # 1. Part averages
    ax1 = axes[0, 0]
    part_means = [np.mean(data_array[i, :, :]) for i in range(n_parts)]
    ax1.bar(range(1, n_parts+1), part_means, color='skyblue', edgecolor='black')
    ax1.set_xlabel('Part')
    ax1.set_ylabel('Average')
    ax1.set_title('Part Averages')
    ax1.grid(True, alpha=0.3, axis='y')
    
    # 2. Operator boxplot
    ax2 = axes[0, 1]
    operator_data = [data_array[:, i, :].flatten() for i in range(n_operators)]
    bp = ax2.boxplot(operator_data, labels=[f'Op{i+1}' for i in range(n_operators)])
    ax2.set_ylabel('Measurement')
    ax2.set_title('Operator Distribution')
    ax2.grid(True, alpha=0.3, axis='y')
    
    # 3. Variation pie chart
    ax3 = axes[1, 0]
    labels = ['EV', 'AV', 'PV']
    sizes = [grr_result['ev'], grr_result['av'], grr_result['pv']]
    colors_pie = ['#FF6B6B', '#4ECDC4', '#45B7D1']
    ax3.pie(sizes, labels=labels, colors=colors_pie, autopct='%1.1f%%')
    ax3.set_title('Variation Distribution')
    
    # 4. %GRR bar chart
    ax4 = axes[1, 1]
    metrics = ['%EV', '%AV', '%GRR', '%PV']
    values = [evaluation['percent_ev'], evaluation['percent_av'], 
              evaluation['percent_grr'], evaluation['percent_pv']]
    bar_colors = ['#FF6B6B' if v > 30 else '#FFD93D' if v > 10 else '#6BCB77' for v in values]
    ax4.bar(metrics, values, color=bar_colors)
    ax4.set_ylabel('Percentage (%)')
    ax4.set_title('Variation Percentage')
    ax4.axhline(y=10, color='green', linestyle='--', alpha=0.5)
    ax4.axhline(y=30, color='red', linestyle='--', alpha=0.5)
    ax4.grid(True, alpha=0.3, axis='y')
    
    plt.tight_layout()
    buf = io.BytesIO()
    plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
    buf.seek(0)
    plt.close(fig)
    return buf

def generate_msa_grr_report(output_path, study_info, grr_result, evaluation, data_array):
    """Generate MSA GR&R PDF report"""
    
    doc = SimpleDocTemplate(output_path, pagesize=A4,
                           rightMargin=10*mm, leftMargin=10*mm,
                           topMargin=10*mm, bottomMargin=10*mm)
    
    styles = getSampleStyleSheet()
    story = []
    
    # Title
    story.append(Paragraph("<b>MSA GR&R Study Report</b>", 
                          style=ParagraphStyle('Title', fontSize=14, alignment=1)))
    story.append(Spacer(1, 0.15*inch))
    
    # Study Information
    info_data = [
        ['Study ID:', str(study_info.get('id', 'N/A'))],
        ['Study Name:', study_info.get('study_name', 'N/A')],
        ['Part Count:', str(study_info.get('n_parts', 'N/A'))],
        ['Operator Count:', str(study_info.get('n_operators', 'N/A'))],
        ['Trial Count:', str(study_info.get('n_trials', 'N/A'))],
        ['Date:', datetime.now().strftime('%Y-%m-%d %H:%M:%S')],
    ]
    
    info_table = Table(info_data, colWidths=[4*cm, 9*cm])
    info_table.setStyle(TableStyle([
        ('BOX', (0, 0), (-1, -1), 0.5, colors.black),
        ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
    ]))
    story.append(info_table)
    story.append(Spacer(1, 0.2*inch))
    
    # Results Table
    results_data = [
        ['Variation Component', 'Value', 'Percentage'],
        ['Repeatability (EV)', f"{grr_result['ev']:.6f}", f"{evaluation['percent_ev']:.2f}%"],
        ['Reproducibility (AV)', f"{grr_result['av']:.6f}", f"{evaluation['percent_av']:.2f}%"],
        ['GR&R (RR)', f"{grr_result['rr']:.6f}", f"{evaluation['percent_grr']:.2f}%"],
        ['Part Variation (PV)', f"{grr_result['pv']:.6f}", f"{evaluation['percent_pv']:.2f}%"],
        ['Total Variation (TV)', f"{grr_result['tv']:.6f}", '100%'],
        ['', '', ''],
        ['Evaluation', '', ''],
        ['ndc (Distinct Categories)', str(evaluation.get('ndc', 'N/A')), ''],
        ['Acceptance', evaluation.get('acceptance', 'N/A'), ''],
    ]
    
    results_table = Table(results_data, colWidths=[5*cm, 4*cm, 4*cm])
    results_table.setStyle(TableStyle([
        ('BOX', (0, 0), (-1, -1), 0.5, colors.black),
        ('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
        ('BACKGROUND', (0, 1), (-1, 6), colors.beige),
        ('BACKGROUND', (0, 8), (-1, -1), colors.lightblue),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
    ]))
    story.append(results_table)
    story.append(Spacer(1, 0.2*inch))
    
    # Charts
    story.append(Paragraph("<b>Analysis Charts</b>", style=ParagraphStyle('Heading', fontSize=11)))
    story.append(Spacer(1, 0.1*inch))
    
    chart_buf = create_grr_chart(data_array, grr_result, evaluation)
    chart_buf.seek(0)
    chart_img = Image(chart_buf, width=14*cm, height=11*cm)
    story.append(chart_img)
    story.append(Spacer(1, 0.2*inch))
    
    # Conclusion
    acceptance = evaluation.get('acceptance', 'N/A')
    if acceptance == 'acceptable':
        conclusion = "✅ <b>Measurement system is ACCEPTABLE</b>: %GRR ≤ 10%"
    elif acceptance == 'conditionally_acceptable':
        conclusion = "⚠️ <b>Measurement system is CONDITIONALLY ACCEPTABLE</b>: 10% < %GRR ≤ 30%"
    else:
        conclusion = "❌ <b>Measurement system is UNACCEPTABLE</b>: %GRR > 30%, improvement required"
    
    story.append(Paragraph(conclusion, style=ParagraphStyle('Conclusion', fontSize=10)))
    
    # Footer
    story.append(Spacer(1, 0.3*inch))
    footer = Paragraph(
        f"<i>Generated by Rohoon Six Sigma Skill | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</i>",
        style=ParagraphStyle('Footer', fontSize=8, textColor=colors.gray)
    )
    story.append(footer)
    
    doc.build(story)
    print(f"✅ MSA GR&R Report generated: {output_path}")

if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('--output', '-o', default='/tmp/msa_grr_report.pdf')
    args = parser.parse_args()
    
    # Test data
    study_info = {'id': 1, 'study_name': 'Test GR&R', 'n_parts': 10, 'n_operators': 3, 'n_trials': 3}
    grr_result = {'ev': 0.05, 'av': 0.03, 'rr': 0.06, 'pv': 0.21, 'tv': 0.22}
    evaluation = {'percent_grr': 27.31, 'percent_ev': 23.45, 'percent_av': 13.99, 
                  'percent_pv': 96.19, 'ndc': 5, 'acceptance': 'conditionally_acceptable'}
    np.random.seed(42)
    data_array = np.random.normal(10.0, 0.1, (10, 3, 3))
    
    generate_msa_grr_report(args.output, study_info, grr_result, evaluation, data_array)


path: scripts/generate_spc_report.py
content: #!/usr/bin/env python3
"""
Generate SPC PDF Report
Creates a professional SPC report with control charts and capability analysis

Usage:
    python3 generate_spc_report.py --output /tmp/spc_report.pdf
"""

import io
import os
import sys
import numpy as np
from datetime import datetime
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch, cm
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import matplotlib
matplotlib.use('Agg')
matplotlib.rcParams['font.sans-serif'] = ['STHeiti', 'Arial Unicode MS']
matplotlib.rcParams['axes.unicode_minus'] = False
import matplotlib.pyplot as plt
from scipy import stats

# Register Chinese font
def register_chinese_font():
    font_paths = [
        "/System/Library/Fonts/STHeiti Light.ttc",
        "/System/Library/Fonts/STHeiti Medium.ttc",
    ]
    for font_path in font_paths:
        if os.path.exists(font_path):
            try:
                pdfmetrics.registerFont(TTFont('Chinese', font_path))
                return True
            except:
                pass
    return False

register_chinese_font()

def generate_data():
    """Generate simulated SPC data"""
    np.random.seed(42)
    
    # Generate 100 measurements (20 subgroups × 5)
    # Process with slight shift
    base_mean = 10.0
    base_std = 0.05
    
    data = []
    for i in range(20):
        # Add slight trend
        mean_shift = 0.002 * i
        subgroup = np.random.normal(base_mean + mean_shift, base_std, 5)
        data.extend(subgroup)
    
    return np.array(data)

def create_control_chart(data, subgroup_size=5):
    """Create control chart image"""
    n_subgroups = len(data) // subgroup_size
    subgroups = data[:n_subgroups * subgroup_size].reshape(n_subgroups, subgroup_size)
    
    xbar = np.mean(subgroups, axis=1)
    r = np.ptp(subgroups, axis=1)
    
    xbar_bar = np.mean(xbar)
    r_bar = np.mean(r)
    
    # Constants for n=5
    A2, D3, D4 = 0.577, 0, 2.114
    
    ucl_xbar = xbar_bar + A2 * r_bar
    lcl_xbar = xbar_bar - A2 * r_bar
    ucl_r = D4 * r_bar
    lcl_r = D3 * r_bar
    
    # Create plot
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
    fig.suptitle('Xbar-R Control Chart', fontsize=14, fontweight='bold')
    
    # Xbar chart
    ax1.plot(range(1, n_subgroups+1), xbar, 'b-o', markersize=4, label='Xbar')
    ax1.axhline(y=xbar_bar, color='green', linestyle='-', linewidth=2, label=f'CL={xbar_bar:.4f}')
    ax1.axhline(y=ucl_xbar, color='red', linestyle='--', linewidth=2, label=f'UCL={ucl_xbar:.4f}')
    ax1.axhline(y=lcl_xbar, color='red', linestyle='--', linewidth=2, label=f'LCL={lcl_xbar:.4f}')
    ax1.set_ylabel('Average')
    ax1.set_title('Xbar Chart')
    ax1.legend(loc='upper right', fontsize=8)
    ax1.grid(True, alpha=0.3)
    
    # R chart
    ax2.plot(range(1, n_subgroups+1), r, 'b-o', markersize=4, label='R')
    ax2.axhline(y=r_bar, color='green', linestyle='-', linewidth=2, label=f'CL={r_bar:.4f}')
    ax2.axhline(y=ucl_r, color='red', linestyle='--', linewidth=2, label=f'UCL={ucl_r:.4f}')
    ax2.axhline(y=lcl_r, color='red', linestyle='--', linewidth=2, label=f'LCL={lcl_r:.4f}')
    ax2.set_xlabel('Subgroup')
    ax2.set_ylabel('Range')
    ax2.set_title('R Chart')
    ax2.legend(loc='upper right', fontsize=8)
    ax2.grid(True, alpha=0.3)
    
    plt.tight_layout()
    
    buf = io.BytesIO()
    plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
    buf.seek(0)
    plt.close(fig)
    
    return buf

def create_capability_chart(data, usl, lsl):
    """Create capability analysis chart"""
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
    fig.suptitle('Process Capability Analysis', fontsize=14, fontweight='bold')
    
    # Histogram with normal curve
    ax1.hist(data, bins=15, density=True, alpha=0.7, edgecolor='black', color='skyblue')
    
    mu, sigma = np.mean(data), np.std(data)
    x = np.linspace(lsl, usl, 100)
    ax1.plot(x, stats.norm.pdf(x, mu, sigma), 'r-', linewidth=2, label='Normal Fit')
    
    ax1.axvline(x=mu, color='red', linestyle='--', linewidth=2, label=f'μ={mu:.4f}')
    ax1.axvline(x=usl, color='green', linestyle='-', linewidth=2, label=f'USL={usl}')
    ax1.axvline(x=lsl, color='green', linestyle='-', linewidth=2, label=f'LSL={lsl}')
    ax1.set_xlabel('Measurement')
    ax1.set_ylabel('Density')
    ax1.set_title('Data Distribution')
    ax1.legend(fontsize=8)
    ax1.grid(True, alpha=0.3)
    
    # Capability indices bar chart
    cp = (usl - lsl) / (6 * sigma)
    cpu = (usl - mu) / (3 * sigma)
    cpl = (mu - lsl) / (3 * sigma)
    cpk = min(cpu, cpl)
    
    metrics = ['Cp', 'Cpu', 'Cpl', 'Cpk']
    values = [cp, cpu, cpl, cpk]
    colors_bar = ['#6BCB77' if v >= 1.33 else '#FFD93D' if v >= 1.0 else '#FF6B6B' for v in values]
    
    bars = ax2.bar(metrics, values, color=colors_bar)
    ax2.set_ylabel('Capability Index')
    ax2.set_title('Process Capability Indices')
    ax2.axhline(y=1.33, color='orange', linestyle='--', alpha=0.5, label='Target (1.33)')
    ax2.axhline(y=1.0, color='red', linestyle=':', alpha=0.5, label='Min (1.0)')
    ax2.legend(fontsize=8)
    ax2.set_ylim(0, max(2.5, max(values) * 1.3))
    
    for bar, val in zip(bars, values):
        ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.05,
                f'{val:.2f}', ha='center', va='bottom', fontsize=9)
    
    plt.tight_layout()
    
    buf = io.BytesIO()
    plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
    buf.seek(0)
    plt.close(fig)
    
    return buf, cp, cpk

def create_table(data, col_widths):
    """Create styled table"""
    table = Table(data, colWidths=col_widths)
    table.setStyle(TableStyle([
        ('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
        ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
        ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
        ('FONTNAME', (0, 0), (-1, -1), 'Chinese'),
        ('FONTSIZE', (0, 0), (-1, 0), 11),
        ('FONTSIZE', (0, 1), (-1, -1), 9),
        ('BOTTOMPADDING', (0, 0), (-1, 0), 10),
        ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
        ('GRID', (0, 0), (-1, -1), 0.5, colors.black),
    ]))
    return table

def generate_spc_pdf(output_path):
    """Generate complete SPC report PDF"""
    
    # Generate data
    data = generate_data()
    usl, lsl = 10.3, 9.7
    
    # Calculate statistics
    n = len(data)
    mean = np.mean(data)
    std = np.std(data, ddof=1)
    cp = (usl - lsl) / (6 * std)
    cpu = (usl - mean) / (3 * std)
    cpl = (mean - lsl) / (3 * std)
    cpk = min(cpu, cpl)
    
    # Create PDF
    doc = SimpleDocTemplate(
        output_path,
        pagesize=A4,
        rightMargin=2*cm,
        leftMargin=2*cm,
        topMargin=2*cm,
        bottomMargin=2*cm,
        title="SPC Report"
    )
    
    styles = getSampleStyleSheet()
    styles.add(ParagraphStyle(name='ChineseHeading1', parent=styles['Heading1'], fontName='Chinese', fontSize=18, spaceAfter=12, alignment=1))
    styles.add(ParagraphStyle(name='ChineseHeading2', parent=styles['Heading2'], fontName='Chinese', fontSize=14, spaceAfter=10, spaceBefore=8))
    styles.add(ParagraphStyle(name='ChineseBody', parent=styles['Normal'], fontName='Chinese', fontSize=10, spaceAfter=6, leading=14))
    styles.add(ParagraphStyle(name='ChineseSmall', parent=styles['Normal'], fontName='Chinese', fontSize=8, textColor=colors.gray))
    
    story = []
    
    # Title
    story.append(Paragraph("SPC Statistical Process Control Report", styles['ChineseHeading1']))
    story.append(Spacer(1, 0.3*inch))
    
    # Study Information
    info_data = [
        ['Product:', 'Bearing Housing'],
        ['Characteristic:', 'Bore Diameter (mm)'],
        ['Process:', 'CNC Machining'],
        ['Machine:', 'CNC-01'],
        ['Operator:', 'Quality Inspector'],
        ['Sample Size:', str(n)],
        ['Subgroup Size:', '5'],
        ['Number of Subgroups:', '20'],
        ['USL:', str(usl)],
        ['LSL:', str(lsl)],
        ['Date:', datetime.now().strftime('%Y-%m-%d %H:%M:%S')],
    ]
    story.append(create_table(info_data, [4*cm, 9*cm]))
    story.append(Spacer(1, 0.3*inch))
    
    # Control Chart Section
    story.append(Paragraph("1. Control Chart Analysis", styles['ChineseHeading2']))
    story.append(Spacer(1, 0.15*inch))
    
    chart_buf = create_control_chart(data)
    chart_buf.seek(0)
    chart_img = Image(chart_buf, width=6.5*inch, height=5*inch)
    story.append(chart_img)
    story.append(Spacer(1, 0.3*inch))
    
    # Control chart statistics
    xbar_bar = np.mean(np.mean(data.reshape(20, 5), axis=1))
    r_bar = np.mean(np.ptp(data.reshape(20, 5), axis=1))
    
    ctrl_data = [
        ['Xbar (Center Line):', f'{xbar_bar:.4f}'],
        ['R Bar (Average Range):', f'{r_bar:.4f}'],
        ['Estimated Sigma:', f'{r_bar/2.326:.4f}'],
        ['Process Status:', 'In Control ✅'],
    ]
    story.append(create_table(ctrl_data, [5*cm, 7*cm]))
    story.append(Spacer(1, 0.3*inch))
    
    # Capability Section
    story.append(Paragraph("2. Process Capability Analysis", styles['ChineseHeading2']))
    story.append(Spacer(1, 0.15*inch))
    
    cap_chart_buf, cp_val, cpk_val = create_capability_chart(data, usl, lsl)
    cap_chart_buf.seek(0)
    cap_img = Image(cap_chart_buf, width=6.5*inch, height=4.5*inch)
    story.append(cap_img)
    story.append(Spacer(1, 0.3*inch))
    
    # Capability statistics
    if cpk >= 1.67:
        rating = 'Excellent'
        action = 'Maintain current state'
    elif cpk >= 1.33:
        rating = 'Good'
        action = 'Maintain, continuous improvement'
    elif cpk >= 1.0:
        rating = 'Marginal'
        action = 'Need improvement plan'
    else:
        rating = 'Insufficient'
        action = 'Must improve, 100% inspection'
    
    cap_data = [
        ['Cp (Potential Capability):', f'{cp:.4f}'],
        ['Cpk (Actual Capability):', f'{cpk:.4f}'],
        ['CPU:', f'{cpu:.4f}'],
        ['CPL:', f'{cpl:.4f}'],
        ['Mean:', f'{mean:.4f}'],
        ['Std Dev:', f'{std:.4f}'],
        ['Min:', f'{np.min(data):.4f}'],
        ['Max:', f'{np.max(data):.4f}'],
        ['', ''],
        ['Rating:', rating],
        ['Action:', action],
        ['Acceptable:', 'Yes ✅' if cpk >= 1.33 else 'No ❌'],
    ]
    story.append(create_table(cap_data, [5*cm, 7*cm]))
    story.append(Spacer(1, 0.3*inch))
    
    # Conclusion
    story.append(Paragraph("3. Conclusion", styles['ChineseHeading2']))
    story.append(Spacer(1, 0.15*inch))
    
    conclusion = f"""
    <b>Process Status:</b> The process is in statistical control with no out-of-control points detected.<br/><br/>
    <b>Capability Assessment:</b> Cpk = {cpk:.2f} indicates the process is {rating.lower()}. 
    The process {action.lower()}.<br/><br/>
    <b>Recommendations:</b><br/>
    - Continue monitoring with control charts<br/>
    - Investigate any special causes if points go out of control<br/>
    - Consider reducing variation if higher capability is required
    """
    story.append(Paragraph(conclusion, styles['ChineseBody']))
    
    # Footer
    story.append(Spacer(1, 0.5*inch))
    story.append(Paragraph(
        f"<i>Report generated by Rohoon Six Sigma Skill | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</i>",
        style=styles['ChineseSmall']
    ))
    
    # Build PDF
    doc.build(story)
    print(f"✅ SPC Report generated: {output_path}")
    print(f"   Sample size: {n}")
    print(f"   Cpk: {cpk:.4f}")
    print(f"   Rating: {rating}")

if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser(description='Generate SPC PDF Report')
    parser.add_argument('--output', '-o', type=str, default='/tmp/spc_report.pdf',
                       help='Output PDF file path')
    args = parser.parse_args()
    
    generate_spc_pdf(args.output)


path: scripts/generate_test_data.py
content: #!/usr/bin/env python3
"""
生成三组模拟 SPC 测试数据（Excel 格式）
- 一组正态分布数据
- 两组非正态分布数据（偏态、混合分布）
每组 600 条测量值，包含完整业务信息
"""

import sys
import os
import numpy as np
import pandas as pd
from datetime import datetime, timedelta

try:
    sys.path.insert(0, '/Library/Python/3.9/site-packages')
    from openpyxl import Workbook
    from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
except ImportError:
    print("请安装 openpyxl: pip3 install openpyxl")
    sys.exit(1)

np.random.seed(42)

# 输出目录
output_dir = os.path.join(os.path.dirname(__file__), 'tmp', 'test_data')
os.makedirs(output_dir, exist_ok=True)

print("=" * 80)
print("生成 SPC 测试数据（三组，每组 600 条）")
print("=" * 80)

# ==================== 公共参数 ====================
n_samples = 600
subgroup_size = 5
n_subgroups = n_samples // subgroup_size

# 规格限
usl = 30.140
lsl = 29.860
target = 30.000

# 业务信息
part_name = "AIAG-VDA-Example-Part"
characteristic_name = "Bore Diameter"
machine_name = "CNC-Machining-Center-01"
process_name = "Engine Block Boring"
operator = "John Smith"
department = "Quality Control"

# 生成检测时间（从 2026-03-31 08:00 开始，每小时 25 个样本）
start_time = datetime(2026, 3, 31, 8, 0)
time_intervals = [start_time + timedelta(minutes=i*12) for i in range(n_samples)]

# ==================== 第一组：正态分布数据 ====================
print("\n[1/3] 生成正态分布数据...")

mu_normal = 30.000
sigma_normal = 0.020
data_normal = np.random.normal(mu_normal, sigma_normal, n_samples)

# 生成批次信息
batch_normal = [f"BATCH-{(i // 50) + 1:03d}" for i in range(n_samples)]

df_normal = pd.DataFrame({
    'Sample_ID': [f"S{i+1:04d}" for i in range(n_samples)],
    'Measurement_Value': data_normal,
    'USL': usl,
    'LSL': lsl,
    'Target': target,
    'Part_Name': part_name,
    'Characteristic': characteristic_name,
    'Machine': machine_name,
    'Process': process_name,
    'Operator': operator,
    'Department': department,
    'Batch': batch_normal,
    'Measurement_Time': time_intervals,
    'Subgroup_ID': [(i // subgroup_size) + 1 for i in range(n_samples)],
})

# 保存 Excel
excel_normal_path = os.path.join(output_dir, '01_Normal_Distribution_Data.xlsx')
df_normal.to_excel(excel_normal_path, index=False, sheet_name='SPC_Data')
print(f"      ✓ {excel_normal_path}")
print(f"        样本数：{n_samples}, 均值：{np.mean(data_normal):.4f}, 标准差：{np.std(data_normal, ddof=1):.4f}")

# ==================== 第二组：非正态数据（偏态分布 - Weibull） ====================
print("\n[2/3] 生成非正态分布数据（偏态/Weibull）...")

# Weibull 分布（右偏）
shape = 2.0  # 形状参数
scale = 0.025  # 尺度参数
data_weibull_raw = np.random.weibull(shape, n_samples) * scale + (lsl + 0.1)
# 调整到规格范围内
data_weibull = np.clip(data_weibull_raw, lsl + 0.001, usl - 0.001)

batch_weibull = [f"BATCH-{(i // 50) + 1:03d}" for i in range(n_samples)]

df_weibull = pd.DataFrame({
    'Sample_ID': [f"S{i+1:04d}" for i in range(n_samples)],
    'Measurement_Value': data_weibull,
    'USL': usl,
    'LSL': lsl,
    'Target': target,
    'Part_Name': part_name,
    'Characteristic': characteristic_name,
    'Machine': machine_name,
    'Process': process_name,
    'Operator': operator,
    'Department': department,
    'Batch': batch_weibull,
    'Measurement_Time': time_intervals,
    'Subgroup_ID': [(i // subgroup_size) + 1 for i in range(n_samples)],
})

excel_weibull_path = os.path.join(output_dir, '02_NonNormal_Weibull_Data.xlsx')
df_weibull.to_excel(excel_weibull_path, index=False, sheet_name='SPC_Data')
print(f"      ✓ {excel_weibull_path}")
print(f"        样本数：{n_samples}, 均值：{np.mean(data_weibull):.4f}, 标准差：{np.std(data_weibull, ddof=1):.4f}")

# ==================== 第三组：非正态数据（混合分布/双峰） ====================
print("\n[3/3] 生成非正态分布数据（混合分布/双峰）...")

# 混合两个正态分布（双峰）
n1 = n_samples // 2
n2 = n_samples - n1
mu1 = 29.950
mu2 = 30.050
sigma_mix = 0.015

data_mix1 = np.random.normal(mu1, sigma_mix, n1)
data_mix2 = np.random.normal(mu2, sigma_mix, n2)
data_mixed = np.concatenate([data_mix1, data_mix2])
np.random.shuffle(data_mixed)
data_mixed = np.clip(data_mixed, lsl + 0.001, usl - 0.001)

batch_mixed = [f"BATCH-{(i // 50) + 1:03d}" for i in range(n_samples)]

df_mixed = pd.DataFrame({
    'Sample_ID': [f"S{i+1:04d}" for i in range(n_samples)],
    'Measurement_Value': data_mixed,
    'USL': usl,
    'LSL': lsl,
    'Target': target,
    'Part_Name': part_name,
    'Characteristic': characteristic_name,
    'Machine': machine_name,
    'Process': process_name,
    'Operator': operator,
    'Department': department,
    'Batch': batch_mixed,
    'Measurement_Time': time_intervals,
    'Subgroup_ID': [(i // subgroup_size) + 1 for i in range(n_samples)],
})

excel_mixed_path = os.path.join(output_dir, '03_NonNormal_Mixed_Data.xlsx')
df_mixed.to_excel(excel_mixed_path, index=False, sheet_name='SPC_Data')
print(f"      ✓ {excel_mixed_path}")
print(f"        样本数：{n_samples}, 均值：{np.mean(data_mixed):.4f}, 标准差：{np.std(data_mixed, ddof=1):.4f}")

# ==================== 统计摘要 ====================
print("\n" + "=" * 80)
print("数据生成完成！统计摘要")
print("=" * 80)

for name, df, data in [
    ('正态分布', df_normal, data_normal),
    ('Weibull 偏态', df_weibull, data_weibull),
    ('混合双峰', df_mixed, data_mixed),
]:
    mu = np.mean(data)
    sigma = np.std(data, ddof=1)
    cp = (usl - lsl) / (6 * sigma)
    cpk = min((usl - mu) / (3 * sigma), (mu - lsl) / (3 * sigma))
    
    print(f"\n{name}:")
    print(f"  文件：0{'123'[['正态分布','Weibull 偏态','混合双峰'].index(name)]}_{name.replace('正态分布','Normal').replace('Weibull 偏态','NonNormal_Weibull').replace('混合双峰','NonNormal_Mixed')}_Data.xlsx")
    print(f"  均值 (μ): {mu:.4f}")
    print(f"  标准差 (σ): {sigma:.4f}")
    print(f"  Cp: {cp:.4f}")
    print(f"  Cpk: {cpk:.4f}")
    print(f"  能力判定：{'✅ 达标 (≥1.67)' if cpk >= 1.67 else '⚠️ 边缘 (1.33-1.67)' if cpk >= 1.33 else '❌ 不达标 (<1.33)'}")

print("\n" + "=" * 80)
print(f"输出目录：{output_dir}")
print("=" * 80)


path: scripts/msa_calculator.py
content: """
MSA (Measurement System Analysis) 统计计算核心模块
遵循 AIAG VDA MSA 第四版标准
"""

import numpy as np
from scipy import stats
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
from enum import Enum
import logging

logger = logging.getLogger(__name__)


@dataclass
class GRRError:
    """GR&R 误差分量"""
    ev: float  # 设备变差 (Equipment Variation) - 重复性
    av: float  # 评价人变差 (Appraiser Variation) - 再现性
    rr: float  # GR&R (重复性 + 再现性)
    pv: float  # 零件变差 (Part Variation)
    tv: float  # 总变差 (Total Variation)


@dataclass
class GRREvaluation:
    """GR&R 评估结果"""
    ndc: int  # 分级数 (Number of Distinct Categories)
    percent_grr: float  # %GR&R (占总变差的百分比)
    percent_ev: float  # %EV
    percent_av: float  # %AV
    percent_pv: float  # %PV
    acceptance: str  # Acceptable, Marginal, Unacceptable
    is_acceptable: bool


@dataclass
class BiasAnalysis:
    """偏倚分析结果"""
    bias: float  # 偏倚（观测均值 - 参考值）
    bias_percent: float  # 偏倚百分比
    t_statistic: float
    p_value: float
    is_significant: bool  # 偏倚是否显著（p < 0.05）
    confidence_interval: Tuple[float, float]  # 95% 置信区间


@dataclass
class LinearityAnalysis:
    """线性分析结果"""
    slope: float  # 斜率
    intercept: float  # 截距
    r_squared: float  # 决定系数
    p_value_slope: float  # 斜率显著性
    p_value_intercept: float  # 截距显著性
    is_linear: bool  # 是否线性（斜率不显著）
    linearity: float  # 线性度 = |slope| * process_variation
    linearity_percent: float  # 线性度百分比


# ==================== GR&R 分析 (ANOVA 法) ====================

def calculate_grr_anova(
    data: np.ndarray,
    n_parts: int,
    n_operators: int,
    n_trials: int,
    tolerance: Optional[float] = None,
    process_variation: Optional[float] = None
) -> Tuple[GRRError, GRREvaluation]:
    """
    使用方差分析法 (ANOVA) 计算 GR&R
    
    参数:
        data: 三维数组，shape=(parts, operators, trials)
        n_parts: 零件数量
        n_operators: 操作者数量
        n_trials: 试验次数
        tolerance: 公差 (USL - LSL)，用于计算%Tolerance
        process_variation: 过程变差（6*sigma），用于计算%Process Variation
    
    返回:
        (GRRError, GRREvaluation)
    """
    # 验证数据形状
    if data.shape != (n_parts, n_operators, n_trials):
        raise ValueError(f"数据形状不匹配：期望 {(n_parts, n_operators, n_trials)}, 实际 {data.shape}")
    
    # 计算各种均值
    grand_mean = np.mean(data)
    
    # 零件均值
    part_means = np.mean(data, axis=(1, 2))
    
    # 操作者均值
    operator_means = np.mean(data, axis=(0, 2))
    
    # 每个零件 - 操作者组合的均值
    cell_means = np.mean(data, axis=2)
    
    # 平方和计算
    # 总平方和
    ss_total = np.sum((data - grand_mean) ** 2)
    
    # 零件平方和
    ss_parts = n_operators * n_trials * np.sum((part_means - grand_mean) ** 2)
    
    # 操作者平方和
    ss_operators = n_parts * n_trials * np.sum((operator_means - grand_mean) ** 2)
    
    # 交互作用平方和
    ss_interaction = n_trials * np.sum((cell_means - part_means[:, np.newaxis] - operator_means[np.newaxis, :] + grand_mean) ** 2)
    
    # 误差平方和（重复性）
    ss_error = ss_total - ss_parts - ss_operators - ss_interaction
    
    # 自由度
    df_parts = n_parts - 1
    df_operators = n_operators - 1
    df_interaction = df_parts * df_operators
    df_error = n_parts * n_operators * (n_trials - 1)
    df_total = n_parts * n_operators * n_trials - 1
    
    # 均方
    ms_parts = ss_parts / df_parts if df_parts > 0 else 0
    ms_operators = ss_operators / df_operators if df_operators > 0 else 0
    ms_interaction = ss_interaction / df_interaction if df_interaction > 0 else 0
    ms_error = ss_error / df_error if df_error > 0 else 0
    
    # 方差分量计算
    # 重复性 (EV)
    var_ev = ms_error
    
    # 再现性 (AV) - 包含操作者和交互作用
    if ms_interaction > ms_error:
        var_operators = (ms_operators - ms_interaction) / (n_parts * n_trials)
        var_interaction = (ms_interaction - ms_error) / n_trials
    else:
        var_operators = (ms_operators - ms_error) / (n_parts * n_trials)
        var_interaction = 0
    
    var_operators = max(0, var_operators)
    var_interaction = max(0, var_interaction)
    
    var_av = var_operators + var_interaction
    
    # GR&R 方差
    var_grr = var_ev + var_av
    
    # 零件变差 (PV)
    if ms_parts > ms_error:
        var_pv = (ms_parts - ms_error) / (n_operators * n_trials)
    else:
        var_pv = 0
    var_pv = max(0, var_pv)
    
    # 总变差
    var_tv = var_grr + var_pv
    
    # 标准差（乘以 5.15 对应 99% 置信区间，或使用 6 对应 99.73%）
    k = 6.0  # 使用 6σ
    
    ev = k * np.sqrt(var_ev)
    av = k * np.sqrt(var_av)
    rr = k * np.sqrt(var_grr)
    pv = k * np.sqrt(var_pv)
    tv = k * np.sqrt(var_tv)
    
    grr_error = GRRError(ev=ev, av=av, rr=rr, pv=pv, tv=tv)
    
    # 评估
    # 分级数 NDC
    ndc = int(np.floor(1.41 * (pv / rr))) if rr > 0 else 999
    ndc = max(0, ndc)
    
    # %GR&R
    percent_grr = (rr / tv) * 100 if tv > 0 else 0
    percent_ev = (ev / tv) * 100 if tv > 0 else 0
    percent_av = (av / tv) * 100 if tv > 0 else 0
    percent_pv = (pv / tv) * 100 if tv > 0 else 0
    
    # 接受准则 (AIAG MSA)
    if percent_grr <= 10:
        acceptance = "Acceptable"
        is_acceptable = True
    elif percent_grr <= 30:
        acceptance = "Marginal"
        is_acceptable = True  # 有条件接受
    else:
        acceptance = "Unacceptable"
        is_acceptable = False
    
    evaluation = GRREvaluation(
        ndc=ndc,
        percent_grr=percent_grr,
        percent_ev=percent_ev,
        percent_av=percent_av,
        percent_pv=percent_pv,
        acceptance=acceptance,
        is_acceptable=is_acceptable
    )
    
    logger.info(
        f"GR&R 分析完成：%GR&R={percent_grr:.2f}%, NDC={ndc}, "
        f"评估={acceptance}"
    )
    
    return grr_error, evaluation


def calculate_grr_xbar_r(
    data: np.ndarray,
    n_parts: int,
    n_operators: int,
    n_trials: int,
    tolerance: Optional[float] = None
) -> Tuple[GRRError, GRREvaluation]:
    """
    使用 Xbar-R 法计算 GR&R（简化方法）
    
    参数:
        data: 三维数组，shape=(parts, operators, trials)
    """
    # 计算每个操作者 - 零件组合的极差
    ranges = np.ptp(data, axis=2)  # 每个 cell 的极差
    
    # 平均极差
    r_bar = np.mean(ranges)
    
    # 操作者均值
    operator_means = np.mean(data, axis=(0, 2))
    operator_range = np.ptp(operator_means)
    
    # 零件均值
    part_means = np.mean(data, axis=(1, 2))
    part_range = np.ptp(part_means)
    
    # 控制图系数
    d2_star = {
        (2, 2): 1.41, (2, 3): 1.91, (2, 4): 2.24,
        (3, 2): 1.28, (3, 3): 1.81, (3, 4): 2.16,
        (4, 3): 1.75, (4, 4): 2.12,
        (5, 3): 1.72, (5, 4): 2.10,
    }
    
    key = (n_operators, n_trials)
    d2 = d2_star.get(key, 1.128)  # 默认 n=2
    
    # 重复性 (EV)
    ev = (5.15 * r_bar) / d2
    
    # 再现性 (AV)
    d2_star_op = {2: 1.41, 3: 1.91, 4: 2.24, 5: 2.48}
    d2_op = d2_star_op.get(n_operators, 1.41)
    
    k1 = 5.15 / d2
    k2 = 5.15 / (d2_op * np.sqrt(n_parts * n_trials))
    
    av = np.sqrt((k2 * operator_range) ** 2 - (ev ** 2) / (n_parts * n_trials))
    av = max(0, av)
    
    # GR&R
    rr = np.sqrt(ev ** 2 + av ** 2)
    
    # 零件变差 (PV)
    d2_pv = d2_star_op.get(n_parts, 2.48)
    k3 = 5.15 / d2_pv
    pv = k3 * part_range
    
    # 总变差
    tv = np.sqrt(rr ** 2 + pv ** 2)
    
    grr_error = GRRError(ev=ev, av=av, rr=rr, pv=pv, tv=tv)
    
    # 评估
    ndc = int(np.floor(1.41 * (pv / rr))) if rr > 0 else 999
    percent_grr = (rr / tv) * 100 if tv > 0 else 0
    
    if percent_grr <= 10:
        acceptance = "Acceptable"
        is_acceptable = True
    elif percent_grr <= 30:
        acceptance = "Marginal"
        is_acceptable = True
    else:
        acceptance = "Unacceptable"
        is_acceptable = False
    
    evaluation = GRREvaluation(
        ndc=ndc,
        percent_grr=percent_grr,
        percent_ev=(ev / tv) * 100 if tv > 0 else 0,
        percent_av=(av / tv) * 100 if tv > 0 else 0,
        percent_pv=(pv / tv) * 100 if tv > 0 else 0,
        acceptance=acceptance,
        is_acceptable=is_acceptable
    )
    
    return grr_error, evaluation


# ==================== 偏倚分析 ====================

def calculate_bias(
    measurements: np.ndarray,
    reference_value: float,
    confidence_level: float = 0.95
) -> BiasAnalysis:
    """
    计算偏倚
    
    参数:
        measurements: 测量值数组
        reference_value: 参考值（标准值）
        confidence_level: 置信水平
    
    返回:
        BiasAnalysis
    """
    n = len(measurements)
    
    if n < 2:
        raise ValueError("至少需要 2 个测量值")
    
    # 观测均值
    x_bar = np.mean(measurements)
    
    # 偏倚
    bias = x_bar - reference_value
    
    # 偏倚百分比（相对于过程变差或公差）
    # 这里先计算绝对偏倚
    bias_percent = 0  # 需要过程变差或公差才能计算
    
    # 标准差
    s = np.std(measurements, ddof=1)
    
    # 标准误差
    se = s / np.sqrt(n)
    
    # t 统计量
    t_stat = bias / se if se > 0 else 0
    
    # p 值（双尾检验）
    p_value = 2 * (1 - stats.t.cdf(abs(t_stat), df=n-1))
    
    # 显著性
    is_significant = p_value < (1 - confidence_level)
    
    # 置信区间
    alpha = 1 - confidence_level
    t_critical = stats.t.ppf(1 - alpha/2, df=n-1)
    margin = t_critical * se
    ci_lower = bias - margin
    ci_upper = bias + margin
    
    logger.info(
        f"偏倚分析完成：Bias={bias:.6f}, t={t_stat:.4f}, p={p_value:.4f}, "
        f"显著={'是' if is_significant else '否'}"
    )
    
    return BiasAnalysis(
        bias=bias,
        bias_percent=bias_percent,
        t_statistic=t_stat,
        p_value=p_value,
        is_significant=is_significant,
        confidence_interval=(ci_lower, ci_upper)
    )


# ==================== 线性分析 ====================

def calculate_linearity(
    reference_values: np.ndarray,
    measurements: np.ndarray,
    process_variation: Optional[float] = None
) -> LinearityAnalysis:
    """
    计算线性
    
    参数:
        reference_values: 参考值数组（不同零件的已知值）
        measurements: 对应的测量值数组（可以是多次测量的均值）
        process_variation: 过程变差（用于计算线性度百分比）
    
    返回:
        LinearityAnalysis
    """
    if len(reference_values) != len(measurements):
        raise ValueError("参考值和测量值数量必须相同")
    
    n = len(reference_values)
    
    if n < 3:
        raise ValueError("至少需要 3 个数据点进行线性分析")
    
    # 线性回归
    slope, intercept, r_value, p_value, std_err = stats.linregress(reference_values, measurements)
    
    r_squared = r_value ** 2
    
    # 计算偏倚
    biases = measurements - reference_values
    
    # 对偏倚进行回归分析（偏倚 vs 参考值）
    slope_bias, intercept_bias, r_bias, p_slope, std_err_slope = stats.linregress(reference_values, biases)
    
    # 截距的 p 值（需要手动计算）
    # t 统计量 for intercept
    se_intercept = std_err_slope * np.sqrt(np.sum(reference_values**2) / n)
    t_intercept = intercept_bias / se_intercept if se_intercept > 0 else 0
    p_intercept = 2 * (1 - stats.t.cdf(abs(t_intercept), df=n-2))
    
    # 线性度 = |斜率| * 过程变差
    if process_variation:
        linearity = abs(slope_bias) * process_variation
        linearity_percent = (linearity / process_variation) * 100
    else:
        linearity = abs(slope_bias)
        linearity_percent = abs(slope_bias) * 100
    
    # 线性判断：斜率不显著（p > 0.05）表示线性良好
    is_linear = p_slope > 0.05
    
    logger.info(
        f"线性分析完成：Slope={slope_bias:.6f}, R²={r_squared:.4f}, "
        f"线性度={linearity:.6f}, 线性良好={'是' if is_linear else '否'}"
    )
    
    return LinearityAnalysis(
        slope=slope_bias,
        intercept=intercept_bias,
        r_squared=r_squared,
        p_value_slope=p_slope,
        p_value_intercept=p_intercept,
        is_linear=is_linear,
        linearity=linearity,
        linearity_percent=linearity_percent
    )


# ==================== 工具函数 ====================

def prepare_grr_data(
    flat_data: List[float],
    n_parts: int,
    n_operators: int,
    n_trials: int
) -> np.ndarray:
    """
    将扁平数据转换为 GR&R 分析所需的三维数组
    
    参数:
        flat_data: 扁平的测量值列表
        n_parts: 零件数
        n_operators: 操作者数
        n_trials: 试验次数
    
    返回:
        三维数组，shape=(parts, operators, trials)
    """
    expected_length = n_parts * n_operators * n_trials
    
    if len(flat_data) != expected_length:
        raise ValueError(
            f"数据量不匹配：期望 {expected_length}, 实际 {len(flat_data)}"
        )
    
    data = np.array(flat_data)
    
    # 重塑为 (parts, operators, trials)
    # 假设数据按顺序：零件 1-操作者 A-试验 1,2,3... 零件 1-操作者 B-试验 1,2,3...
    return data.reshape(n_parts, n_operators, n_trials)


def evaluate_grr_acceptance(percent_grr: float, ndc: int) -> Dict[str, any]:
    """
    GR&R 接受度评估
    
    返回评估结果和建议
    """
    if percent_grr <= 10:
        if ndc >= 5:
            return {
                "status": "Acceptable",
                "recommendation": "测量系统可以接受",
                "color": "green"
            }
        else:
            return {
                "status": "Marginal (Low NDC)",
                "recommendation": "%GR&R 良好但 NDC 不足，建议增加零件变差或改进分辨率",
                "color": "yellow"
            }
    elif percent_grr <= 30:
        if ndc >= 4:
            return {
                "status": "Marginal",
                "recommendation": "测量系统可基于重要性、成本等因素有条件接受",
                "color": "yellow"
            }
        else:
            return {
                "status": "Marginal (Low NDC)",
                "recommendation": "需要改进测量系统",
                "color": "orange"
            }
    else:
        return {
            "status": "Unacceptable",
            "recommendation": "测量系统需要改进",
            "color": "red"
        }


path: scripts/msa_grr_analysis.py
content: #!/usr/bin/env python3
"""
MSA GR&R Analysis Tool (Xbar-R Method)
Based on AIAG MSA Manual 4th Edition

Usage:
    python3 msa_grr_analysis.py --data "[...]" --parts 10 --operators 3 --trials 3 --tolerance 0.5 --json
"""

import argparse
import json
import sys
import numpy as np
from typing import Dict, Any, List


def parse_data(data_str: str) -> List[float]:
    """Parse input data string"""
    try:
        data = json.loads(data_str)
        if isinstance(data, list):
            return [float(x) for x in data]
    except:
        pass
    
    try:
        return [float(x.strip()) for x in data_str.split(',')]
    except:
        raise ValueError("Cannot parse data")


def calculate_grr_xbar_r(
    data: List[float],
    n_parts: int,
    n_operators: int,
    n_trials: int,
    tolerance: float = None
) -> Dict[str, Any]:
    """
    Calculate GR&R using Xbar-R method
    
    Returns:
        Dictionary with EV, AV, GR&R, PV, TV, %GR&R, ndc, and evaluation
    """
    # Constants for R chart
    K1_constants = {2: 4.56, 3: 3.05, 4: 2.46, 5: 2.18, 6: 2.00, 7: 1.88, 8: 1.79, 9: 1.72, 10: 1.67}
    K2_constants = {2: 3.65, 3: 2.70, 4: 2.30, 5: 2.08, 6: 1.93, 7: 1.82, 8: 1.74, 9: 1.67, 10: 1.62}
    K3_constants = {2: 3.65, 3: 2.70, 4: 2.30, 5: 2.08, 6: 1.93, 7: 1.82, 8: 1.74, 9: 1.67, 10: 1.62}
    
    # Reshape data
    data_array = np.array(data).reshape(n_parts, n_operators, n_trials)
    
    # Calculate ranges (R) for each part-operator combination
    R = np.ptp(data_array, axis=2)  # Range across trials
    
    # Calculate average range (R_bar)
    R_bar = np.mean(R)
    
    # Calculate part averages
    part_means = np.mean(data_array, axis=(1, 2))
    
    # Calculate operator averages
    operator_means = np.mean(data_array, axis=(0, 2))
    
    # Calculate overall average
    grand_mean = np.mean(data_array)
    
    # Calculate range of operator averages
    R_op = np.max(operator_means) - np.min(operator_means)
    
    # Calculate range of part averages
    R_part = np.max(part_means) - np.min(part_means)
    
    # Get constants
    K1 = K1_constants.get(n_trials, 1.67)
    K2 = K2_constants.get(n_operators, 1.62)
    K3 = K3_constants.get(n_parts, 1.62)
    
    # Calculate variation components
    EV = R_bar * K1  # Equipment Variation (Repeatability)
    AV = np.sqrt((R_op * K2)**2 - (EV**2 / (n_parts * n_trials))) if (R_op * K2)**2 > (EV**2 / (n_parts * n_trials)) else 0  # Appraiser Variation (Reproducibility)
    PV = R_part * K3  # Part Variation
    
    # Calculate GR&R and Total Variation
    GRR = np.sqrt(EV**2 + AV**2)
    TV = np.sqrt(GRR**2 + PV**2)
    
    # Calculate percentages
    if tolerance:
        percent_ev = (EV / tolerance) * 100
        percent_av = (AV / tolerance) * 100
        percent_grr = (GRR / tolerance) * 100
        percent_pv = (PV / tolerance) * 100
    else:
        percent_ev = (EV / TV) * 100 if TV > 0 else 0
        percent_av = (AV / TV) * 100 if TV > 0 else 0
        percent_grr = (GRR / TV) * 100 if TV > 0 else 0
        percent_pv = (PV / TV) * 100 if TV > 0 else 0
    
    # Calculate ndc (Number of Distinct Categories)
    ndc = int(1.41 * (PV / GRR)) if GRR > 0 else 0
    
    # Evaluation
    if percent_grr < 10:
        acceptance = 'acceptable'
        is_acceptable = True
    elif percent_grr < 30:
        acceptance = 'conditionally_acceptable'
        is_acceptable = True
    else:
        acceptance = 'unacceptable'
        is_acceptable = False
    
    result = {
        'n_parts': n_parts,
        'n_operators': n_operators,
        'n_trials': n_trials,
        'total_measurements': len(data),
        'ev': float(EV),
        'av': float(AV),
        'grr': float(GRR),
        'pv': float(PV),
        'tv': float(TV),
        'percent_ev': float(percent_ev),
        'percent_av': float(percent_av),
        'percent_grr': float(percent_grr),
        'percent_pv': float(percent_pv),
        'ndc': ndc,
        'acceptance': acceptance,
        'is_acceptable': is_acceptable,
    }
    
    if tolerance:
        result['tolerance'] = float(tolerance)
    
    return result


def main():
    parser = argparse.ArgumentParser(
        description='MSA GR&R Analysis (Xbar-R Method)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Standard 10x3x3 study
  python3 msa_grr_analysis.py --data "[...90 values...]" --parts 10 --operators 3 --trials 3 --tolerance 0.5 --json
  
  # Without tolerance (uses total variation)
  python3 msa_grr_analysis.py --data "[...]" --parts 10 --operators 3 --trials 3 --json
        '''
    )
    
    parser.add_argument('--data', '-d', type=str, required=True,
                       help='Measurement data (JSON array or comma-separated)')
    parser.add_argument('--parts', '-p', type=int, required=True,
                       help='Number of parts')
    parser.add_argument('--operators', '-o', type=int, required=True,
                       help='Number of operators')
    parser.add_argument('--trials', '-t', type=int, required=True,
                       help='Number of trials per part-operator')
    parser.add_argument('--tolerance', '-T', type=float, default=None,
                       help='Tolerance (USL-LSL) for percent GRR calculation')
    parser.add_argument('--json', '-j', action='store_true',
                       help='Output in JSON format')
    
    args = parser.parse_args()
    
    try:
        # Parse data
        data = parse_data(args.data)
        
        # Validate data count
        expected_count = args.parts * args.operators * args.trials
        if len(data) != expected_count:
            raise ValueError(f"Expected {expected_count} data points, got {len(data)}")
        
        # Calculate GR&R
        result = calculate_grr_xbar_r(
            data,
            n_parts=args.parts,
            n_operators=args.operators,
            n_trials=args.trials,
            tolerance=args.tolerance
        )
        
        # Output
        if args.json:
            print(json.dumps(result, indent=2, ensure_ascii=False))
        else:
            print("=" * 60)
            print("MSA GR&R Analysis Results (Xbar-R Method)")
            print("=" * 60)
            print(f"Parts: {result['n_parts']}, Operators: {result['n_operators']}, Trials: {result['n_trials']}")
            print(f"Total Measurements: {result['total_measurements']}")
            print()
            print("Variation Components:")
            print(f"  EV (Repeatability): {result['ev']:.4f} ({result['percent_ev']:.1f}%)")
            print(f"  AV (Reproducibility): {result['av']:.4f} ({result['percent_av']:.1f}%)")
            print(f"  GR&R: {result['grr']:.4f} ({result['percent_grr']:.1f}%)")
            print(f"  PV (Part Variation): {result['pv']:.4f} ({result['percent_pv']:.1f}%)")
            print(f"  TV (Total Variation): {result['tv']:.4f}")
            print()
            print("Evaluation:")
            print(f"  %GR&R: {result['percent_grr']:.1f}%")
            print(f"  ndc: {result['ndc']}")
            print(f"  Acceptance: {result['acceptance'].upper()}")
            print(f"  Status: {'✅ Acceptable' if result['is_acceptable'] else '❌ Unacceptable'}")
            
            if args.tolerance:
                print(f"  Tolerance: {args.tolerance}")
            
            print("=" * 60)
        
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()


path: scripts/msa_other_studies.py
content: #!/usr/bin/env python3
"""
MSA Other Studies Tool
Supports: Bias, Linearity, Stability studies
Based on AIAG MSA Manual 4th Edition

Usage:
    python3 msa_other_studies.py --study-type bias --data "10.1,10.15,..." --reference 10.0 --tolerance 0.5 --json
    python3 msa_other_studies.py --study-type linearity --data "[...]" --references "[10,20,30,40,50]" --json
"""

import argparse
import json
import sys
import numpy as np
from scipy import stats
from typing import Dict, Any, List


def parse_data(data_str: str) -> List[float]:
    """Parse input data string"""
    try:
        data = json.loads(data_str)
        if isinstance(data, list):
            return [float(x) for x in data]
    except:
        pass
    
    try:
        return [float(x.strip()) for x in data_str.split(',')]
    except:
        raise ValueError("Cannot parse data")


def calculate_bias(
    data: List[float],
    reference: float,
    tolerance: float = None
) -> Dict[str, Any]:
    """
    Calculate bias study
    
    Returns:
        Dictionary with bias, t-statistic, p-value, and evaluation
    """
    data_array = np.array(data)
    n = len(data_array)
    mean = np.mean(data_array)
    std = np.std(data_array, ddof=1)
    
    # Calculate bias
    bias = mean - reference
    
    # Calculate t-statistic
    se = std / np.sqrt(n)  # Standard error
    t_stat = bias / se if se > 0 else 0
    
    # Calculate p-value (two-tailed)
    p_value = 2 * (1 - stats.t.cdf(abs(t_stat), df=n-1))
    
    # Calculate confidence interval (95%)
    t_critical = stats.t.ppf(0.975, df=n-1)
    ci_lower = bias - t_critical * se
    ci_upper = bias + t_critical * se
    
    # Calculate bias percentage
    if tolerance:
        bias_percent = (abs(bias) / tolerance) * 100
    else:
        bias_percent = (abs(bias) / abs(reference)) * 100
    
    # Evaluation
    is_significant = bool(p_value < 0.05)
    if bias_percent < 5:
        rating = 'excellent'
    elif bias_percent < 10:
        rating = 'acceptable'
    elif bias_percent < 30:
        rating = 'marginal'
    else:
        rating = 'unacceptable'
    
    result = {
        'study_type': 'bias',
        'n_measurements': int(n),
        'reference_value': float(reference),
        'mean': float(mean),
        'std': float(std),
        'bias': float(bias),
        'bias_percent': float(bias_percent),
        't_statistic': float(t_stat),
        'p_value': float(p_value),
        'ci_lower': float(ci_lower),
        'ci_upper': float(ci_upper),
        'is_significant': is_significant,
        'rating': rating,
        'acceptable': bool(bias_percent < 10 and not is_significant),
    }
    
    if tolerance:
        result['tolerance'] = float(tolerance)
    
    return result


def calculate_linearity(
    data: List[float],
    references: List[float],
    tolerance: float = None
) -> Dict[str, Any]:
    """
    Calculate linearity study
    
    Returns:
        Dictionary with slope, intercept, R², linearity, and evaluation
    """
    data_array = np.array(data)
    ref_array = np.array(references)
    
    # Check if we have multiple measurements per reference
    n_refs = len(ref_array)
    n_meas = len(data_array)
    
    if n_meas % n_refs == 0:
        # Multiple measurements per reference - calculate means
        n_per_ref = n_meas // n_refs
        means = [np.mean(data_array[i*n_per_ref:(i+1)*n_per_ref]) for i in range(n_refs)]
        means = np.array(means)
    else:
        # One measurement per reference
        means = data_array
    
    # Linear regression
    slope, intercept, r_value, p_value, std_err = stats.linregress(ref_array, means)
    r_squared = r_value ** 2
    
    # Calculate linearity
    linearity = abs(slope) * (max(ref_array) - min(ref_array))
    
    if tolerance:
        linearity_percent = (linearity / tolerance) * 100
    else:
        linearity_percent = (linearity / (max(ref_array) - min(ref_array))) * 100
    
    # Calculate bias at each reference
    biases = means - (slope * ref_array + intercept)
    avg_bias = np.mean(biases)
    
    # Evaluation
    is_linear = r_squared >= 0.95 and linearity_percent < 10
    if linearity_percent < 5:
        rating = 'excellent'
    elif linearity_percent < 10:
        rating = 'acceptable'
    elif linearity_percent < 30:
        rating = 'marginal'
    else:
        rating = 'unacceptable'
    
    result = {
        'study_type': 'linearity',
        'n_references': int(n_refs),
        'n_measurements': int(n_meas),
        'slope': float(slope),
        'intercept': float(intercept),
        'r_squared': float(r_squared),
        'p_value_slope': float(p_value),
        'linearity': float(linearity),
        'linearity_percent': float(linearity_percent),
        'avg_bias': float(avg_bias),
        'is_linear': bool(is_linear),
        'rating': rating,
        'acceptable': is_linear,
    }
    
    if tolerance:
        result['tolerance'] = float(tolerance)
    
    return result


def calculate_stability(
    data: List[float],
    reference: float = None,
    times: List[str] = None
) -> Dict[str, Any]:
    """
    Calculate stability study using control chart method
    
    Returns:
        Dictionary with mean, std, control limits, and stability evaluation
    """
    data_array = np.array(data)
    n = len(data_array)
    mean = np.mean(data_array)
    std = np.std(data_array, ddof=1)
    
    # Calculate control limits (I-MR chart method)
    # Moving range
    mr = np.abs(np.diff(data_array))
    mr_bar = np.mean(mr)
    
    # Control limits for I chart
    d2 = 1.128  # For n=2 (moving range)
    sigma_est = mr_bar / d2
    ucl = mean + 3 * sigma_est
    lcl = mean - 3 * sigma_est
    
    # Check for out-of-control points
    ooc_points = []
    for i, val in enumerate(data_array):
        if val > ucl or val < lcl:
            ooc_points.append(i)
    
    # Check for trends (7 points in a row on same side of center)
    has_trend = False
    for i in range(n - 7):
        segment = data_array[i:i+7]
        if all(x > mean for x in segment) or all(x < mean for x in segment):
            has_trend = True
            break
    
    # Calculate drift (if reference provided)
    if reference:
        drift = mean - reference
        drift_percent = (abs(drift) / abs(reference)) * 100
    else:
        drift = 0
        drift_percent = 0
    
    # Evaluation
    is_stable = len(ooc_points) == 0 and not has_trend
    if is_stable and drift_percent < 1:
        rating = 'excellent'
    elif is_stable and drift_percent < 5:
        rating = 'acceptable'
    elif drift_percent < 10:
        rating = 'marginal'
    else:
        rating = 'unacceptable'
    
    result = {
        'study_type': 'stability',
        'n_measurements': int(n),
        'mean': float(mean),
        'std': float(std),
        'ucl': float(ucl),
        'lcl': float(lcl),
        'sigma_est': float(sigma_est),
        'ooc_points': ooc_points,
        'has_trend': bool(has_trend),
        'drift': float(drift),
        'drift_percent': float(drift_percent),
        'is_stable': bool(is_stable),
        'rating': rating,
        'acceptable': is_stable and drift_percent < 10,
    }
    
    if reference:
        result['reference_value'] = float(reference)
    if times:
        result['time_points'] = times
    
    return result


def main():
    parser = argparse.ArgumentParser(
        description='MSA Other Studies (Bias, Linearity, Stability)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  # Bias study
  python3 msa_other_studies.py --study-type bias --data "10.1,10.15,10.12,..." --reference 10.0 --tolerance 0.5 --json
  
  # Linearity study (5 parts, 3 measurements each)
  python3 msa_other_studies.py --study-type linearity --data "[...15 values...]" --references "[10,20,30,40,50]" --tolerance 1.0 --json
  
  # Stability study
  python3 msa_other_studies.py --study-type stability --data "10.1,10.12,10.09,..." --reference 10.0 --json
        '''
    )
    
    parser.add_argument('--study-type', '-s', type=str, required=True,
                       choices=['bias', 'linearity', 'stability'],
                       help='Type of MSA study')
    parser.add_argument('--data', '-d', type=str, required=True,
                       help='Measurement data (JSON array or comma-separated)')
    parser.add_argument('--reference', '-r', type=float, default=None,
                       help='Reference value (for bias/stability)')
    parser.add_argument('--references', '-R', type=str, default=None,
                       help='Reference values for linearity study (JSON array)')
    parser.add_argument('--tolerance', '-T', type=float, default=None,
                       help='Tolerance (USL-LSL)')
    parser.add_argument('--times', '-t', type=str, default=None,
                       help='Time points (comma-separated, for stability)')
    parser.add_argument('--json', '-j', action='store_true',
                       help='Output in JSON format')
    
    args = parser.parse_args()
    
    try:
        # Parse data
        data = parse_data(args.data)
        
        if len(data) < 2:
            raise ValueError("At least 2 data points required")
        
        # Perform study based on type
        if args.study_type == 'bias':
            if not args.reference:
                raise ValueError("Reference value required for bias study")
            result = calculate_bias(data, args.reference, args.tolerance)
            
        elif args.study_type == 'linearity':
            if not args.references:
                raise ValueError("Reference values required for linearity study")
            references = parse_data(args.references)
            result = calculate_linearity(data, references, args.tolerance)
            
        elif args.study_type == 'stability':
            times = None
            if args.times:
                times = [t.strip() for t in args.times.split(',')]
            result = calculate_stability(data, args.reference, times)
        
        # Output
        if args.json:
            print(json.dumps(result, indent=2, ensure_ascii=False))
        else:
            print("=" * 60)
            print(f"MSA {args.study_type.title()} Study Results")
            print("=" * 60)
            
            if args.study_type == 'bias':
                print(f"Reference Value: {result['reference_value']}")
                print(f"Measurements: {result['n_measurements']}")
                print(f"Mean: {result['mean']:.4f}")
                print(f"Bias: {result['bias']:.4f} ({result['bias_percent']:.1f}%)")
                print(f"t-statistic: {result['t_statistic']:.3f}")
                print(f"p-value: {result['p_value']:.4f}")
                print(f"95% CI: [{result['ci_lower']:.4f}, {result['ci_upper']:.4f}]")
                print(f"Significant: {'Yes ⚠️' if result['is_significant'] else 'No ✅'}")
                print(f"Rating: {result['rating']}")
                
            elif args.study_type == 'linearity':
                print(f"References: {result['n_references']}")
                print(f"Measurements: {result['n_measurements']}")
                print(f"Slope: {result['slope']:.4f}")
                print(f"Intercept: {result['intercept']:.4f}")
                print(f"R²: {result['r_squared']:.4f}")
                print(f"Linearity: {result['linearity']:.4f} ({result['linearity_percent']:.1f}%)")
                print(f"Linear: {'Yes ✅' if result['is_linear'] else 'No ⚠️'}")
                print(f"Rating: {result['rating']}")
                
            elif args.study_type == 'stability':
                print(f"Measurements: {result['n_measurements']}")
                print(f"Mean: {result['mean']:.4f}")
                print(f"Std Dev: {result['std']:.4f}")
                print(f"UCL: {result['ucl']:.4f}")
                print(f"LCL: {result['lcl']:.4f}")
                print(f"Out-of-Control Points: {len(result['ooc_points'])}")
                print(f"Trend Detected: {'Yes ⚠️' if result['has_trend'] else 'No ✅'}")
                print(f"Drift: {result['drift']:.4f} ({result['drift_percent']:.1f}%)")
                print(f"Stable: {'Yes ✅' if result['is_stable'] else 'No ⚠️'}")
                print(f"Rating: {result['rating']}")
            
            print(f"Acceptable: {'Yes ✅' if result['acceptable'] else 'No ❌'}")
            print("=" * 60)
        
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()


path: SECURITY_POLICY.md
content: # 🛡️ Rohoon 6Sigma Skill - 安全策略

## ⚠️ 历史事故记录

### 事故描述
在开发过程中，曾发生 **整个 skill 目录被清空** 的严重事故，导致所有工作成果丢失，需要从头开始。

### 事故原因
- 没有定期备份
- 没有版本控制
- 操作前没有确认

### 教训
**决不允许再发生这样的事故！**

---

## 📋 安全操作规范

### 1. 备份策略
```bash
# 每次重大修改前必须创建备份
cd ~/.openclaw/workspace/skills/rohoon-6sigma
mkdir -p backups
cp -r . backups/v$(date +%Y%m%d_%H%M%S)_pre_$(操作名称)/
```

### 2. 版本控制
```bash
# 使用 git 进行版本管理
git init
git add .
git commit -m "描述修改内容"
git tag v1.0.0
```

### 3. 操作确认清单
在执行以下操作前，必须确认：
- [ ] 已创建完整备份
- [ ] 已提交到 git 仓库
- [ ] 已验证备份可恢复
- [ ] 已通知相关人员

### 4. 禁止操作
❌ **绝对禁止**直接删除 skill 目录
❌ **绝对禁止**使用 `rm -rf` 命令
❌ **绝对禁止**在没有备份的情况下修改核心文件
❌ **绝对禁止**清空整个技能目录

---

## 📁 备份管理

### 备份位置
```
~/.openclaw/workspace/skills/rohoon-6sigma/backups/
```

### 备份命名规范
```
v{版本号}_{日期}_{操作描述}/
例如：v1.0.0_20260401_pre_merge/
```

### 备份保留策略
- 保留最近 5 个版本
- 保留所有主要版本（v1.0.0, v2.0.0 等）
- 定期清理旧备份（>30 天）

---

## 🔄 恢复流程

### 如果发生目录丢失
1. **立即停止所有操作**
2. **检查备份目录**
   ```bash
   ls -la ~/.openclaw/workspace/skills/rohoon-6sigma/backups/
   ```
3. **恢复最新备份**
   ```bash
   cp -r backups/v1.0.0_*/ ~/.openclaw/workspace/skills/rohoon-6sigma/
   ```
4. **验证恢复结果**
   ```bash
   ls -la ~/.openclaw/workspace/skills/rohoon-6sigma/
   ```

### 如果备份也丢失
1. **检查临时目录**
   ```bash
   ls -la ~/.openclaw/workspace/spc-msa-system/
   ```
2. **从其他位置恢复**
   - Git 仓库
   - 外部存储
   - 团队成员备份

---

## 📊 当前状态

### 技能位置
```
~/.openclaw/workspace/skills/rohoon-6sigma/
```

### 当前版本
```
v1.0.0 (2026-04-01)
```

### 最新备份
```
backups/v1.0.0_pre_merge_20260401_*/
```

### 文件统计
- 技能文件：141 个
- 备份文件：136 个
- 备份大小：8.7 MB

---

## ✅ 安全检查清单

每次会话开始前，执行以下检查：

- [ ] skill 目录存在且完整
- [ ] 备份目录存在且有内容
- [ ] 最近 24 小时内有备份
- [ ] git 仓库状态正常（如有）
- [ ] 重要文件未被修改或删除

---

## 🎯 技能定位

**Rohoon 6Sigma Skill** 是核心工作成果，包含：
- AIAG/VDA 标准报告生成器
- SPC/MSA 分析工具
- 数据生成与处理脚本
- 完整文档和测试用例

**这是不可再生的工作结晶，必须严格保护！**

---

## 📞 紧急联系

如果发生任何意外：
1. 立即停止所有操作
2. 检查备份目录
3. 联系技能维护者
4. 记录事故详情

---

**记住：备份是最后的防线，版本控制是最佳实践！** 🛡️


path: tests/test_aiagvda_reports.py
content: #!/usr/bin/env python3
"""
单元测试 - AIAG-VDA 报告生成器
测试 Figure 12-1, 12-2, 12-3 报告生成
"""

import unittest
import numpy as np
import sys
import os
import tempfile

# 添加 scripts 目录到路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))

from aiagvda_unified_report import (
    AIAGVDAReportGenerator,
    StudyInfo,
    Specification,
    calculate_stats,
    TEXTS
)


class TestAIAVDAReportGenerator(unittest.TestCase):
    """AIAG-VDA 报告生成器测试"""
    
    def setUp(self):
        """设置测试数据"""
        np.random.seed(42)
        self.data = np.random.normal(130.0392, 0.033, 875)
        self.spec = Specification(usl=130.15, lsl=129.95, target=130.05)
        self.study_info = StudyInfo(
            process_name="Test Process",
            machine_name="Test Machine",
            study_location="Test Lab",
            part_name="Part A",
            part_id="001",
            characteristic_name="Dimension"
        )
    
    def test_text_resources(self):
        """测试文本资源"""
        # 中文资源
        self.assertIn('zh', TEXTS)
        self.assertEqual(TEXTS['zh']['title'], 'SPC 过程能力研究报告')
        
        # 英文资源
        self.assertIn('en', TEXTS)
        self.assertEqual(TEXTS['en']['title'], 'SPC Process Capability Study Report')
    
    def test_specification(self):
        """测试规格限类"""
        # 计算 tolerance
        tolerance = self.spec.usl - self.spec.lsl
        self.assertAlmostEqual(tolerance, 0.2, places=4)
        self.assertAlmostEqual(self.spec.target, 130.05, places=4)
    
    def test_calculate_stats(self):
        """测试统计计算"""
        results = calculate_stats(self.data, self.spec)
        
        # 验证返回字典
        self.assertIsInstance(results, dict)
        
        # 验证基本统计
        self.assertEqual(results['n'], 875)
        self.assertGreater(results['mean'], 130.0)
        self.assertLess(results['mean'], 130.1)
        
        # 验证能力指数
        self.assertGreater(results['cp_g'], 0)
        self.assertGreater(results['cpk_g'], 0)
        
        # 验证 PPM
        self.assertGreater(results['ppm_upper'], 0)
        self.assertGreater(results['ppm_lower'], 0)
    
    def test_generate_figure12_1_english(self):
        """测试 Figure 12-1 英文报告生成"""
        with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
            output_path = f.name
        
        try:
            generator = AIAGVDAReportGenerator(lang='en')
            result = generator.generate_figure12_1(output_path, self.data, self.spec)
            
            # 验证文件生成
            self.assertTrue(os.path.exists(result))
            self.assertGreater(os.path.getsize(result), 1000)
        finally:
            if os.path.exists(output_path):
                os.unlink(output_path)
    
    def test_generate_figure12_3(self):
        """测试 Figure 12-3 报告生成"""
        with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
            output_path = f.name
        
        try:
            generator = AIAGVDAReportGenerator(lang='en')
            result = generator.generate_figure12_3(output_path, self.data, self.spec)
            
            # 验证文件生成
            self.assertTrue(os.path.exists(result))
            self.assertGreater(os.path.getsize(result), 1000)
        finally:
            if os.path.exists(output_path):
                os.unlink(output_path)


class TestCapabilityResults(unittest.TestCase):
    """能力分析结果测试"""
    
    def test_normal_distribution(self):
        """测试正态分布数据"""
        np.random.seed(42)
        data = np.random.normal(10.0, 0.1, 100)
        spec = Specification(usl=10.3, lsl=9.7, target=10.0)
        
        results = calculate_stats(data, spec)
        
        # 能力应该很好
        self.assertGreater(results['cpk_g'], 1.0)
    
    def test_shifted_process(self):
        """测试偏移过程"""
        np.random.seed(42)
        data = np.random.normal(10.2, 0.1, 100)  # 偏移
        spec = Specification(usl=10.3, lsl=9.7, target=10.0)
        
        results = calculate_stats(data, spec)
        
        # 偏移过程的 Cpk 应该低于 Cp
        self.assertLess(results['cpk_g'], results['cp_g'])
    
    def test_small_sample(self):
        """测试小样本"""
        data = np.array([10.1, 10.2, 10.15, 10.18, 10.12])
        spec = Specification(usl=10.5, lsl=9.5, target=10.0)
        
        results = calculate_stats(data, spec)
        
        self.assertEqual(results['n'], 5)
        self.assertGreater(results['cp_g'], 0)


if __name__ == '__main__':
    unittest.main(verbosity=2)


path: tests/__init__.py
content: # Test package for rohoon-6sigma skill

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)