test_app_utils.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import math
  2. import unittest
  3. import pandas as pd
  4. from app_utils import (
  5. apply_defect_filters,
  6. build_html_report,
  7. build_ml_factor_insights,
  8. build_diagnostic_dashboard,
  9. classify_panel_zone,
  10. calculate_kpis,
  11. calculate_spc_metrics,
  12. generate_industry_diagnosis,
  13. generate_report_charts,
  14. normalize_defect_schema,
  15. )
  16. class AppUtilsTest(unittest.TestCase):
  17. def setUp(self):
  18. self.df = pd.DataFrame(
  19. {
  20. "defect_id": ["D1", "D2", "D3", "D4"],
  21. "panel_id": ["P1", "P2", "P2", "P3"],
  22. "batch_id": ["B1", "B1", "B2", "B2"],
  23. "equipment_id": ["E1", "E1", "E2", "E2"],
  24. "seat_id": ["S1", "S2", "S1", "S2"],
  25. "timestamp": pd.to_datetime(
  26. [
  27. "2026-04-01 00:00:00",
  28. "2026-04-01 23:59:59",
  29. "2026-04-02 12:00:00",
  30. "2026-04-03 00:00:01",
  31. ]
  32. ),
  33. "defect_type": ["划痕", "亮点", "划痕", "暗点"],
  34. "severity": ["严重", "轻微", "中等", "严重"],
  35. "shift": ["白班", "夜班", "白班", "白班"],
  36. "day": ["2026-04-01", "2026-04-01", "2026-04-02", "2026-04-03"],
  37. }
  38. )
  39. def test_date_filter_includes_full_end_date(self):
  40. filtered = apply_defect_filters(
  41. self.df,
  42. start_date=pd.Timestamp("2026-04-01"),
  43. end_date=pd.Timestamp("2026-04-01"),
  44. selected_types=["划痕", "亮点", "暗点"],
  45. selected_batches=["B1", "B2"],
  46. selected_equipment=["E1", "E2"],
  47. selected_seats=["S1", "S2"],
  48. selected_shift="全部",
  49. selected_severity="全部",
  50. )
  51. self.assertEqual(["D1", "D2"], filtered["defect_id"].tolist())
  52. def test_kpis_use_same_filter_scope_for_total_panels(self):
  53. filtered = apply_defect_filters(
  54. self.df,
  55. start_date=pd.Timestamp("2026-04-01"),
  56. end_date=pd.Timestamp("2026-04-02"),
  57. selected_types=["划痕"],
  58. selected_batches=["B1", "B2"],
  59. selected_equipment=["E1", "E2"],
  60. selected_seats=["S1"],
  61. selected_shift="全部",
  62. selected_severity="全部",
  63. )
  64. kpis = calculate_kpis(self.df, filtered)
  65. self.assertEqual(2, kpis["total_panels_inspected"])
  66. self.assertEqual(2, kpis["defective_panels"])
  67. self.assertEqual(0.0, kpis["yield_rate"])
  68. def test_spc_metrics_clamp_estimated_rate_to_valid_probability(self):
  69. metrics = calculate_spc_metrics(self.df)
  70. self.assertTrue(math.isfinite(metrics["p_bar"]))
  71. self.assertTrue(math.isfinite(metrics["ucl"]))
  72. self.assertTrue(math.isfinite(metrics["lcl"]))
  73. self.assertLessEqual(metrics["daily"]["defect_rate"].max(), 1.0)
  74. def test_diagnostic_dashboard_ranks_root_cause_candidates(self):
  75. dashboard = build_diagnostic_dashboard(self.df)
  76. self.assertEqual("严重", dashboard["severity_level"])
  77. self.assertEqual("E1 / S1", dashboard["root_causes"].iloc[0]["根因候选"])
  78. self.assertEqual("划痕", dashboard["top_defect_type"])
  79. self.assertIn("优先排查", dashboard["primary_recommendation"])
  80. def test_diagnostic_dashboard_reports_baseline_lift(self):
  81. rows = []
  82. for i in range(10):
  83. rows.append(
  84. {
  85. "defect_id": f"D{i}",
  86. "panel_id": f"P{i}",
  87. "batch_id": "B1",
  88. "equipment_id": "E1",
  89. "seat_id": "S-hot" if i < 8 else "S-cold",
  90. "timestamp": pd.Timestamp("2026-04-01"),
  91. "defect_type": "气泡",
  92. "severity": "严重" if i < 2 else "轻微",
  93. "shift": "白班",
  94. "day": "2026-04-01",
  95. }
  96. )
  97. df = pd.DataFrame(rows)
  98. dashboard = build_diagnostic_dashboard(df)
  99. top = dashboard["root_causes"].iloc[0]
  100. self.assertEqual("E1 / S-hot", top["根因候选"])
  101. self.assertGreater(top["异常倍数"], 1.0)
  102. def test_classify_panel_zone_uses_3c_panel_regions(self):
  103. zones = classify_panel_zone(
  104. pd.DataFrame(
  105. {
  106. "x_mm": [2.0, 77.5, 150.0, 80.0],
  107. "y_mm": [335.0, 255.0, 170.0, 20.0],
  108. "panel_width_mm": [155.0] * 4,
  109. "panel_height_mm": [340.0] * 4,
  110. }
  111. )
  112. )
  113. self.assertIn("角落区", zones.iloc[0])
  114. self.assertIn("FPC/绑定区", zones.iloc[1])
  115. self.assertIn("右边缘区", zones.iloc[2])
  116. self.assertIn("下边缘区", zones.iloc[3])
  117. def test_industry_diagnosis_generates_panel_sop_recommendation(self):
  118. rows = []
  119. for i in range(12):
  120. rows.append(
  121. {
  122. "defect_id": f"D{i}",
  123. "panel_id": f"P{i}",
  124. "batch_id": "B1",
  125. "equipment_id": "LAM-A01",
  126. "seat_id": "R2C3",
  127. "timestamp": pd.Timestamp("2026-04-01"),
  128. "defect_type": "气泡",
  129. "severity": "严重" if i < 4 else "中等",
  130. "x_mm": 5.0 + i * 0.3,
  131. "y_mm": 250.0,
  132. "panel_width_mm": 155.0,
  133. "panel_height_mm": 340.0,
  134. "shift": "白班",
  135. "day": "2026-04-01",
  136. }
  137. )
  138. df = pd.DataFrame(rows)
  139. dashboard = build_diagnostic_dashboard(df)
  140. diagnosis = generate_industry_diagnosis(df, dashboard)
  141. self.assertIn("边缘", diagnosis["headline"])
  142. self.assertIn("气泡", diagnosis["headline"])
  143. self.assertTrue(any("贴合" in item for item in diagnosis["recommendations"]))
  144. self.assertTrue(any("跨面板重复" in pattern for pattern in diagnosis["patterns"]))
  145. def test_normalize_defect_schema_backfills_industry_fields(self):
  146. normalized = normalize_defect_schema(self.df)
  147. self.assertIn("defect_geometry_type", normalized.columns)
  148. self.assertIn("lam_equipment_id", normalized.columns)
  149. self.assertIn("clean_equipment_id", normalized.columns)
  150. self.assertEqual(["point"] * len(normalized), normalized["defect_geometry_type"].tolist())
  151. self.assertEqual(normalized["equipment_id"].tolist(), normalized["lam_equipment_id"].tolist())
  152. self.assertEqual(normalized["seat_id"].tolist(), normalized["lam_seat_id"].tolist())
  153. self.assertTrue((normalized["area_mm2"] >= 0).all())
  154. def test_diagnostic_dashboard_includes_extended_root_causes(self):
  155. rows = []
  156. for i in range(12):
  157. rows.append(
  158. {
  159. "defect_id": f"D{i}",
  160. "panel_id": f"P{i}",
  161. "batch_id": "B1",
  162. "equipment_id": "LAM-A01",
  163. "seat_id": f"R{i % 4 + 1}C1",
  164. "inspection_station": "AOI-1",
  165. "timestamp": pd.Timestamp("2026-04-01"),
  166. "defect_type": "划痕",
  167. "severity": "严重" if i < 4 else "轻微",
  168. "x_mm": 10 + i,
  169. "y_mm": 20 + i,
  170. "panel_width_mm": 155.0,
  171. "panel_height_mm": 340.0,
  172. "hour": 8,
  173. "shift": "白班",
  174. "day": "2026-04-01",
  175. "lam_fixture_id": "FIX-HOT" if i < 9 else "FIX-OK",
  176. "lam_nozzle_id": "NZ-01" if i < 9 else "NZ-02",
  177. "material_lot_oca": "OCA-HOT" if i < 9 else "OCA-OK",
  178. }
  179. )
  180. df = normalize_defect_schema(pd.DataFrame(rows))
  181. dashboard = build_diagnostic_dashboard(df)
  182. extended = dashboard["extended_root_causes"]
  183. self.assertFalse(extended.empty)
  184. self.assertEqual("lam_fixture_id", extended.iloc[0]["维度"])
  185. self.assertEqual("FIX-HOT", extended.iloc[0]["候选值"])
  186. self.assertGreater(extended.iloc[0]["异常倍数"], 1.0)
  187. def test_ml_factor_insights_include_model_audit_outputs(self):
  188. rows = []
  189. for i in range(40):
  190. hot = i < 24
  191. rows.append(
  192. {
  193. "defect_id": f"D{i}",
  194. "panel_id": f"P{i}",
  195. "batch_id": "B1",
  196. "equipment_id": "LAM-A01" if hot else "LAM-B01",
  197. "seat_id": "R1C1" if hot else "R2C2",
  198. "inspection_station": "AOI-1",
  199. "timestamp": pd.Timestamp("2026-04-01 08:00:00"),
  200. "defect_type": "气泡" if hot else "划痕",
  201. "severity": "严重" if i % 5 == 0 else "轻微",
  202. "x_mm": 10.0 + i,
  203. "y_mm": 20.0,
  204. "panel_width_mm": 155.0,
  205. "panel_height_mm": 340.0,
  206. "hour": 8,
  207. "shift": "白班",
  208. "day": "2026-04-01",
  209. "lam_fixture_id": "FIX-HOT" if hot else "FIX-OK",
  210. "material_lot_oca": "OCA-HOT" if hot else "OCA-OK",
  211. }
  212. )
  213. df = normalize_defect_schema(pd.DataFrame(rows))
  214. insights = build_ml_factor_insights(df, target_defect_type="气泡", model_name="random_forest", top_n=5)
  215. self.assertIsNone(insights["error"])
  216. self.assertEqual("气泡", insights["target_defect_type"])
  217. self.assertFalse(insights["key_factors"].empty)
  218. self.assertIn("validation_auc", insights["validation_metrics"])
  219. self.assertGreater(len(insights["feature_importance"]), 0)
  220. def test_build_html_report_creates_self_contained_safe_webpage(self):
  221. html = build_html_report(
  222. generated_at="2026-05-18 10:00:00",
  223. date_range_text="2026-04-01 ~ 2026-04-30",
  224. view_mode="工程师",
  225. defect_count=12,
  226. panel_count=8,
  227. kpis={
  228. "total_panels_inspected": 8,
  229. "defective_panels": 3,
  230. "yield_rate": 62.5,
  231. "critical_defects": 2,
  232. },
  233. type_counts=pd.Series({"气泡<script>": 7, "划痕": 5}),
  234. equipment_counts=pd.Series({"LAM-A01": 9}),
  235. seat_top=pd.Series({"R1C1": 4}),
  236. trend_summary="缺陷数趋势: 上升",
  237. anomaly_rows=[{"equipment": "LAM-A01", "seat": "R1C1", "count": 4}],
  238. recommendations=["重点关注气泡"],
  239. chart_summaries=["TOP1 缺陷类型:气泡,占比 58.3%"],
  240. )
  241. self.assertIn("<!doctype html>", html.lower())
  242. self.assertIn("缺陷集中性分析综合报告", html)
  243. self.assertIn("气泡&lt;script&gt;", html)
  244. self.assertNotIn("气泡<script>", html)
  245. self.assertIn("图表摘要", html)
  246. self.assertIn("TOP1 缺陷类型:气泡,占比 58.3%", html)
  247. def test_generate_report_charts_creates_four_offline_images(self):
  248. df = pd.DataFrame(
  249. {
  250. "defect_id": ["D1", "D2", "D3", "D4"],
  251. "panel_id": ["P1", "P2", "P3", "P4"],
  252. "batch_id": ["B1"] * 4,
  253. "equipment_id": ["LAM-A01", "LAM-A01", "LAM-B01", "LAM-B01"],
  254. "seat_id": ["R1C1", "R1C2", "R2C1", "R2C2"],
  255. "timestamp": pd.to_datetime(["2026-04-01", "2026-04-02", "2026-04-02", "2026-04-03"]),
  256. "defect_type": ["气泡", "气泡", "划痕", "异物"],
  257. "severity": ["轻微", "中等", "严重", "中等"],
  258. "shift": ["白班"] * 4,
  259. "day": ["2026-04-01", "2026-04-02", "2026-04-02", "2026-04-03"],
  260. }
  261. )
  262. daily = df.groupby("day").size().rename("缺陷数").reset_index()
  263. charts = generate_report_charts(df, daily_trend_df=daily)
  264. self.assertEqual(
  265. {"type_distribution", "daily_trend", "equipment_distribution", "severity_pie"},
  266. set(charts),
  267. )
  268. self.assertTrue(all(value.startswith("data:image/png;base64,") for value in charts.values()))
  269. if __name__ == "__main__":
  270. unittest.main()