test_app_utils.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import math
  2. import unittest
  3. import pandas as pd
  4. from app_utils import (
  5. apply_defect_filters,
  6. build_diagnostic_dashboard,
  7. classify_panel_zone,
  8. calculate_kpis,
  9. calculate_spc_metrics,
  10. generate_industry_diagnosis,
  11. normalize_defect_schema,
  12. )
  13. class AppUtilsTest(unittest.TestCase):
  14. def setUp(self):
  15. self.df = pd.DataFrame(
  16. {
  17. "defect_id": ["D1", "D2", "D3", "D4"],
  18. "panel_id": ["P1", "P2", "P2", "P3"],
  19. "batch_id": ["B1", "B1", "B2", "B2"],
  20. "equipment_id": ["E1", "E1", "E2", "E2"],
  21. "seat_id": ["S1", "S2", "S1", "S2"],
  22. "timestamp": pd.to_datetime(
  23. [
  24. "2026-04-01 00:00:00",
  25. "2026-04-01 23:59:59",
  26. "2026-04-02 12:00:00",
  27. "2026-04-03 00:00:01",
  28. ]
  29. ),
  30. "defect_type": ["划痕", "亮点", "划痕", "暗点"],
  31. "severity": ["严重", "轻微", "中等", "严重"],
  32. "shift": ["白班", "夜班", "白班", "白班"],
  33. "day": ["2026-04-01", "2026-04-01", "2026-04-02", "2026-04-03"],
  34. }
  35. )
  36. def test_date_filter_includes_full_end_date(self):
  37. filtered = apply_defect_filters(
  38. self.df,
  39. start_date=pd.Timestamp("2026-04-01"),
  40. end_date=pd.Timestamp("2026-04-01"),
  41. selected_types=["划痕", "亮点", "暗点"],
  42. selected_batches=["B1", "B2"],
  43. selected_equipment=["E1", "E2"],
  44. selected_seats=["S1", "S2"],
  45. selected_shift="全部",
  46. selected_severity="全部",
  47. )
  48. self.assertEqual(["D1", "D2"], filtered["defect_id"].tolist())
  49. def test_kpis_use_same_filter_scope_for_total_panels(self):
  50. filtered = apply_defect_filters(
  51. self.df,
  52. start_date=pd.Timestamp("2026-04-01"),
  53. end_date=pd.Timestamp("2026-04-02"),
  54. selected_types=["划痕"],
  55. selected_batches=["B1", "B2"],
  56. selected_equipment=["E1", "E2"],
  57. selected_seats=["S1"],
  58. selected_shift="全部",
  59. selected_severity="全部",
  60. )
  61. kpis = calculate_kpis(self.df, filtered)
  62. self.assertEqual(2, kpis["total_panels_inspected"])
  63. self.assertEqual(2, kpis["defective_panels"])
  64. self.assertEqual(0.0, kpis["yield_rate"])
  65. def test_spc_metrics_clamp_estimated_rate_to_valid_probability(self):
  66. metrics = calculate_spc_metrics(self.df)
  67. self.assertTrue(math.isfinite(metrics["p_bar"]))
  68. self.assertTrue(math.isfinite(metrics["ucl"]))
  69. self.assertTrue(math.isfinite(metrics["lcl"]))
  70. self.assertLessEqual(metrics["daily"]["defect_rate"].max(), 1.0)
  71. def test_diagnostic_dashboard_ranks_root_cause_candidates(self):
  72. dashboard = build_diagnostic_dashboard(self.df)
  73. self.assertEqual("严重", dashboard["severity_level"])
  74. self.assertEqual("E1 / S1", dashboard["root_causes"].iloc[0]["根因候选"])
  75. self.assertEqual("划痕", dashboard["top_defect_type"])
  76. self.assertIn("优先排查", dashboard["primary_recommendation"])
  77. def test_diagnostic_dashboard_reports_baseline_lift(self):
  78. rows = []
  79. for i in range(10):
  80. rows.append(
  81. {
  82. "defect_id": f"D{i}",
  83. "panel_id": f"P{i}",
  84. "batch_id": "B1",
  85. "equipment_id": "E1",
  86. "seat_id": "S-hot" if i < 8 else "S-cold",
  87. "timestamp": pd.Timestamp("2026-04-01"),
  88. "defect_type": "气泡",
  89. "severity": "严重" if i < 2 else "轻微",
  90. "shift": "白班",
  91. "day": "2026-04-01",
  92. }
  93. )
  94. df = pd.DataFrame(rows)
  95. dashboard = build_diagnostic_dashboard(df)
  96. top = dashboard["root_causes"].iloc[0]
  97. self.assertEqual("E1 / S-hot", top["根因候选"])
  98. self.assertGreater(top["异常倍数"], 1.0)
  99. def test_classify_panel_zone_uses_3c_panel_regions(self):
  100. zones = classify_panel_zone(
  101. pd.DataFrame(
  102. {
  103. "x_mm": [2.0, 77.5, 150.0, 80.0],
  104. "y_mm": [335.0, 255.0, 170.0, 20.0],
  105. "panel_width_mm": [155.0] * 4,
  106. "panel_height_mm": [340.0] * 4,
  107. }
  108. )
  109. )
  110. self.assertIn("角落区", zones.iloc[0])
  111. self.assertIn("FPC/绑定区", zones.iloc[1])
  112. self.assertIn("右边缘区", zones.iloc[2])
  113. self.assertIn("下边缘区", zones.iloc[3])
  114. def test_industry_diagnosis_generates_panel_sop_recommendation(self):
  115. rows = []
  116. for i in range(12):
  117. rows.append(
  118. {
  119. "defect_id": f"D{i}",
  120. "panel_id": f"P{i}",
  121. "batch_id": "B1",
  122. "equipment_id": "LAM-A01",
  123. "seat_id": "R2C3",
  124. "timestamp": pd.Timestamp("2026-04-01"),
  125. "defect_type": "气泡",
  126. "severity": "严重" if i < 4 else "中等",
  127. "x_mm": 5.0 + i * 0.3,
  128. "y_mm": 250.0,
  129. "panel_width_mm": 155.0,
  130. "panel_height_mm": 340.0,
  131. "shift": "白班",
  132. "day": "2026-04-01",
  133. }
  134. )
  135. df = pd.DataFrame(rows)
  136. dashboard = build_diagnostic_dashboard(df)
  137. diagnosis = generate_industry_diagnosis(df, dashboard)
  138. self.assertIn("边缘", diagnosis["headline"])
  139. self.assertIn("气泡", diagnosis["headline"])
  140. self.assertTrue(any("贴合" in item for item in diagnosis["recommendations"]))
  141. self.assertTrue(any("跨面板重复" in pattern for pattern in diagnosis["patterns"]))
  142. def test_normalize_defect_schema_backfills_industry_fields(self):
  143. normalized = normalize_defect_schema(self.df)
  144. self.assertIn("defect_geometry_type", normalized.columns)
  145. self.assertIn("lam_equipment_id", normalized.columns)
  146. self.assertIn("clean_equipment_id", normalized.columns)
  147. self.assertEqual(["point"] * len(normalized), normalized["defect_geometry_type"].tolist())
  148. self.assertEqual(normalized["equipment_id"].tolist(), normalized["lam_equipment_id"].tolist())
  149. self.assertEqual(normalized["seat_id"].tolist(), normalized["lam_seat_id"].tolist())
  150. self.assertTrue((normalized["area_mm2"] >= 0).all())
  151. def test_diagnostic_dashboard_includes_extended_root_causes(self):
  152. rows = []
  153. for i in range(12):
  154. rows.append(
  155. {
  156. "defect_id": f"D{i}",
  157. "panel_id": f"P{i}",
  158. "batch_id": "B1",
  159. "equipment_id": "LAM-A01",
  160. "seat_id": f"R{i % 4 + 1}C1",
  161. "inspection_station": "AOI-1",
  162. "timestamp": pd.Timestamp("2026-04-01"),
  163. "defect_type": "划痕",
  164. "severity": "严重" if i < 4 else "轻微",
  165. "x_mm": 10 + i,
  166. "y_mm": 20 + i,
  167. "panel_width_mm": 155.0,
  168. "panel_height_mm": 340.0,
  169. "hour": 8,
  170. "shift": "白班",
  171. "day": "2026-04-01",
  172. "lam_fixture_id": "FIX-HOT" if i < 9 else "FIX-OK",
  173. "lam_nozzle_id": "NZ-01" if i < 9 else "NZ-02",
  174. "material_lot_oca": "OCA-HOT" if i < 9 else "OCA-OK",
  175. }
  176. )
  177. df = normalize_defect_schema(pd.DataFrame(rows))
  178. dashboard = build_diagnostic_dashboard(df)
  179. extended = dashboard["extended_root_causes"]
  180. self.assertFalse(extended.empty)
  181. self.assertEqual("lam_fixture_id", extended.iloc[0]["维度"])
  182. self.assertEqual("FIX-HOT", extended.iloc[0]["候选值"])
  183. self.assertGreater(extended.iloc[0]["异常倍数"], 1.0)
  184. if __name__ == "__main__":
  185. unittest.main()