| |
| """ |
| μ¨μ΄νΌ κ²°ν¨ λ°μ΄ν° μ²λ¦¬ κ³΅μ© μ νΈλ¦¬ν°. |
| |
| μ΄ λͺ¨λμ LLS(Laser Light Scattering) κ²°ν¨ λΆμ νμ΄νλΌμΈμμ |
| ν¨ν΄ λΆλ₯ μ΄μ Β·μ΄ν λ¨κ³μ 곡ν΅μΌλ‘ μ¬μ©λλ ν¨μλ€μ μ 곡νλ€. |
| |
| ν¬κ² 6κ°μ§ λ²μ£Όλ₯Ό ν¬ν¨: |
| 1. νκ²½ μ€μ : νκΈ ν°νΈ, JSON config λ‘λ |
| 2. κ²°ν¨ λΌλ²¨ λ§€ν : roughbin_no β νκΈ κ²°ν¨ λΆλ₯λͺ
|
| 3. Zone λΌλ²¨λ§ : μκ³λ°©ν₯ 12ꡬμ Γ Inner/Outer λΆλ₯ |
| 4. Fine-grid μ²λ¦¬ : κ²°ν¨ μ’νλ₯Ό 격μ cellμ ν λΉ |
| 5. νν°λ§ : cell λ¨μ wafer μ κΈ°μ€ λ
Έμ΄μ¦ μ κ±° |
| 6. μκ°ν : μ¨μ΄νΌ λ§΅ (μ°μ λ + zone + centroid λ§νΉ) |
| |
| ν΄λμ€ `WaferUtils`λ‘ λͺ¨λ μ νΈλ¦¬ν°λ₯Ό λ¬Άμ΄ IDE μλμμ±/νμ
ννΈ μΌκ΄μ±μ λμ΄κ³ , |
| νμ νΈνμ μν΄ λμΌ μ΄λ¦μ λͺ¨λ λ 벨 ν¨μλ ν¨κ» λ
ΈμΆνλ€. |
| """ |
| from __future__ import annotations |
|
|
| import os |
| import json |
| from typing import Optional, Tuple |
|
|
| import numpy as np |
| import pandas as pd |
| import matplotlib.pyplot as plt |
| import matplotlib.font_manager as fm |
| from matplotlib.patches import Circle, Wedge |
|
|
|
|
| |
| |
| |
|
|
| |
| |
| ROUGHBIN_MAPPING = { |
| 0: 'LPD', 100: 'LPD-N', 110: 'Micro-Scratch', 111: 'Void', 115: 'PID', |
| 120: 'LPD-E', 130: 'LPD-S', 140: 'LLPD', 141: 'Air Pocket', 150: 'DIC-Unique', |
| 160: 'Stain', 170: 'COP', 200: 'Cluster Area', 205: 'Extended Defects', |
| 210: 'Scratch', 220: 'Slipline', 230: 'Line', 231: 'Area', 233: 'Radial', |
| 234: 'Ring', 512: 'Residue', 520: 'Boat Mark', 902: 'Streak', 999: 'Nuisance', |
| 990: 'LPD Nuisance', 991: 'PPD Nuisance', 501: 'Haze Slipline', 502: 'Hazeline', |
| 600: 'Grid', 700: 'ROI', 800: 'X Section', |
| } |
|
|
| |
| CLOCK_LABELS = ["12", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11"] |
|
|
| |
| PATTERN_COLORS = { |
| "νν": "darkorange", |
| "μ ν": "forestgreen", |
| "κ΅°μ§": "mediumpurple", |
| "μ μ/λ―Έλ¬": "gray", |
| "Others": "gray", |
| } |
|
|
|
|
| |
| |
| |
| class WaferUtils: |
| """ |
| μ¨μ΄νΌ κ²°ν¨ λ°μ΄ν° μ²λ¦¬ μ νΈλ¦¬ν° ν΄λμ€ (facade νν). |
| |
| μνκ° νμ μλ μμ ν¨μλ€μ΄λ―λ‘ λλΆλΆ `@staticmethod`λ‘ κ΅¬μ±λλ©°, |
| μΈμ€ν΄μ€ν μμ΄ `WaferUtils.method(...)` ννλ‘ μ¬μ©ν μ μλ€. |
| |
| Examples |
| -------- |
| >>> df = WaferUtils.assign_fine_grid(df, cell_size_mm=3.0) |
| >>> df = WaferUtils.add_zone_labels(df, inner_radius=105.0) |
| >>> WaferUtils.plot_wafer_map(result_df, key="...", pattern_list=["νν"], ...) |
| """ |
|
|
| |
| |
| |
| @staticmethod |
| def setup_korean_font() -> Optional[str]: |
| """ |
| μμ€ν
νκΈ ν°νΈλ₯Ό matplotlib κΈ°λ³Έ ν°νΈλ‘ λ±λ‘. |
| |
| μ νΈ μμ: Malgun Gothic > Nanum Gothic > NanumBarunGothic > Batang > Gulim > AppleGothic. |
| μ νΈ νλ³΄κ° μμΌλ©΄ μμ€ν
μμ 'gothic/mincho/dotum/gulim/malgun/sans/korean' |
| ν€μλκ° ν¬ν¨λ 첫 λ²μ§Έ νκΈ ν보λ₯Ό μ¬μ©. |
| |
| Returns |
| ------- |
| Optional[str] |
| μ μ©λ ν°νΈλͺ
. μμ€ν
μ νκΈ ν°νΈκ° μ ν μμΌλ©΄ None. |
| """ |
| korean_fonts = [ |
| f.name for f in fm.fontManager.ttflist |
| if any(k in f.name.lower() |
| for k in ["gothic", "mincho", "dotum", "gulim", "malgun", "sans", "korean"]) |
| ] |
| preferred = ["Malgun Gothic", "Nanum Gothic", "NanumBarunGothic", |
| "Batang", "Gulim", "AppleGothic"] |
| selected = next((f for f in preferred if f in korean_fonts), None) |
| if selected is None and korean_fonts: |
| selected = korean_fonts[0] |
|
|
| if selected: |
| plt.rcParams["font.family"] = selected |
| plt.rcParams["font.size"] = 10 |
| |
| plt.rcParams["axes.unicode_minus"] = False |
| print(f"β
νκΈ ν°νΈ μ€μ μλ£: {selected}") |
| else: |
| print("β οΈ κ²½κ³ : μμ€ν
μ νκΈ ν°νΈκ° μμ΅λλ€. κΈ°λ³Έ ν°νΈ μ¬μ© (κΈμ κΉ¨μ§ λ°μ)") |
| return selected |
|
|
| @staticmethod |
| def load_config(config_path: str = "./lls_config.json") -> dict: |
| """ |
| JSON νμμ λΆμ μ€μ νμΌ λ‘λ. |
| |
| Parameters |
| ---------- |
| config_path : str |
| μ€μ νμΌ κ²½λ‘ (UTF-8 μΈμ½λ© κ°μ ). |
| |
| Returns |
| ------- |
| dict |
| μ€μ νΈλ¦¬ (preprocessing / clustering / ring / linear / lof / cluster / |
| misc / contact_mapping λ±μ ν€ ν¬ν¨). |
| |
| Raises |
| ------ |
| FileNotFoundError |
| μ§μ ν κ²½λ‘μ νμΌμ΄ μλ κ²½μ°. |
| """ |
| if not os.path.exists(config_path): |
| raise FileNotFoundError(f"μ€μ νμΌ μμ: {config_path}") |
| with open(config_path, "r", encoding="utf-8") as f: |
| return json.load(f) |
|
|
| |
| |
| |
| @staticmethod |
| def map_roughbin_no(roughbin) -> Optional[str]: |
| """ |
| roughbin_no(κ²μ¬κΈ° raw μ½λ)λ₯Ό μ΄μ κ²°ν¨ λΆλ₯λͺ
μΌλ‘ λ§€ν. |
| |
| κ³ μ λ§€ν ν
μ΄λΈ(`ROUGHBIN_MAPPING`)μ μ°μ μ‘°ννκ³ , |
| λ²μν μ½λ(541~548 λ±)λ λ³λ if-쑰건μΌλ‘ μ²λ¦¬νλ€. |
| |
| Parameters |
| ---------- |
| roughbin : Any |
| μ μ λλ μ μ λ³ν κ°λ₯ν κ°. NaN/None/λ¬Έμμ΄ λ±μ None λ°ν. |
| |
| Returns |
| ------- |
| Optional[str] |
| λΆλ₯λͺ
("LPD", "Haze Slipline" λ±). λ§€ν μ€ν¨ μ "Unknown", |
| μ
λ ₯μ΄ NaN/λ³ν λΆκ° μ None. |
| """ |
| if pd.isna(roughbin): |
| return None |
| try: |
| roughbin = int(roughbin) |
| except (TypeError, ValueError): |
| return None |
|
|
| if roughbin in ROUGHBIN_MAPPING: |
| return ROUGHBIN_MAPPING[roughbin] |
| |
| if 541 <= roughbin <= 548: return "Haze Slipline" |
| if 531 <= roughbin <= 538: return "Hazeline" |
| if 601 <= roughbin <= 609: return "Grid" |
| if 701 <= roughbin <= 709: return "ROI" |
| if 801 <= roughbin <= 809: return "X Section" |
| return "Unknown" |
|
|
| |
| |
| |
| @staticmethod |
| def add_zone_labels(df: pd.DataFrame, inner_radius: float = 105.0) -> pd.DataFrame: |
| """ |
| κ²°ν¨ μ’νμ zone λΌλ²¨μ λΆμ¬. |
| |
| Zone λΌλ²¨ νμ: `{Inner|Outer}_{μκ³μμΉ 2μ리}` |
| μ) "Inner_03" = λ°μ§λ¦ β€ inner_radius, 3μ λ°©ν₯ |
| "Outer_12" = λ°μ§λ¦ > inner_radius, 12μ λ°©ν₯ |
| |
| κ°λ λ³ν: |
| - μν κ°λ(atan2) β 12μ κΈ°μ€ μκ³λ°©ν₯ κ°λ(`theta_from_12 = (90Β° - math) mod 360`) |
| - sector index = floor(theta_from_12 / 30Β°) % 12 |
| |
| Parameters |
| ---------- |
| df : pd.DataFrame |
| 'coor_x', 'coor_y' 컬λΌμ ν¬ν¨ν κ²°ν¨ μ’ν DF. |
| inner_radius : float |
| Inner / Outer κ²½κ³κ° λλ λ°μ§λ¦ (mm). |
| |
| Returns |
| ------- |
| pd.DataFrame |
| 'zone_label', 'r' (μμ 거리), 'theta_deg' (μν κ°λ) μ»¬λΌ μΆκ°λ μ¬λ³Έ. |
| """ |
| df = df.copy() |
| r = np.hypot(df["coor_x"], df["coor_y"]) |
| theta_deg = np.degrees(np.arctan2(df["coor_y"], df["coor_x"])) |
| |
| theta_from_12 = (90.0 - theta_deg) % 360.0 |
| sector_index = (theta_from_12 // 30).astype(int) % 12 |
|
|
| clock_str = pd.Series([CLOCK_LABELS[i] for i in sector_index], index=df.index) |
| zone_type = np.where(r <= inner_radius, "Inner", "Outer") |
|
|
| df["zone_label"] = [f"{zt}_{c}" for zt, c in zip(zone_type, clock_str)] |
| df["r"] = r |
| df["theta_deg"] = theta_deg |
| return df |
|
|
| |
| |
| |
| @staticmethod |
| def assign_fine_grid(df: pd.DataFrame, cell_size_mm: float = 3.0) -> pd.DataFrame: |
| """ |
| κ²°ν¨ μ’νλ₯Ό fine-grid cell μ ν λΉ. |
| |
| μ¨μ΄νΌ μ’ν λ²μ [-150, 150] Γ [-150, 150] (mm)λ₯Ό `cell_size_mm` ν¬κΈ°μ |
| μ μ¬κ° 격μλ‘ λΆν νκ³ , κ° κ²°ν¨μ΄ μνλ cellμ μ€μ¬ μ’νμ IDλ₯Ό λΆμ¬. |
| |
| Parameters |
| ---------- |
| df : pd.DataFrame |
| 'coor_x', 'coor_y' μ»¬λΌ ν¬ν¨ DF. |
| cell_size_mm : float |
| μ
ν λ³μ ν¬κΈ° (mm). κΈ°λ³Έ 3.0. |
| |
| Returns |
| ------- |
| pd.DataFrame |
| 'cell_x', 'cell_y' (μ
μ€μ¬ μ’ν mm), |
| 'cell_id' ("{int_x}_{int_y}" νμμ unique ID) μΆκ°λ μ¬λ³Έ. |
| |
| Notes |
| ----- |
| cell_idλ cell_x/cell_yλ₯Ό λ°μ¬λ¦Όνμ¬ μ μνν λ¬Έμμ΄μ΄λΌ |
| cell_size_mmκ° μ μ κ²½κ³μ μ΄κΈλλ©΄ μΆ©λ κ°λ₯. ν΅μ 3.0/5.0 λ± μ μ κΆμ₯. |
| """ |
| df = df.copy() |
| |
| bin_x = np.floor((df["coor_x"] + 150) / cell_size_mm).astype(int) |
| bin_y = np.floor((df["coor_y"] + 150) / cell_size_mm).astype(int) |
|
|
| |
| df["cell_x"] = bin_x * cell_size_mm - 150 + cell_size_mm / 2 |
| df["cell_y"] = bin_y * cell_size_mm - 150 + cell_size_mm / 2 |
|
|
| cell_x_int = np.round(df["cell_x"]).astype(int) |
| cell_y_int = np.round(df["cell_y"]).astype(int) |
| df["cell_id"] = cell_x_int.astype(str) + "_" + cell_y_int.astype(str) |
| return df |
|
|
| @staticmethod |
| def get_cell_wafer_counts(df: pd.DataFrame) -> pd.DataFrame: |
| """ |
| κ° cellμμ κ²°ν¨μ΄ λ°μν unique wafer μμ κ²°ν¨ μλ₯Ό μ§κ³. |
| |
| Parameters |
| ---------- |
| df : pd.DataFrame |
| 'cell_id', 'WAF_ID' μ»¬λΌ ν¬ν¨ DF. |
| |
| Returns |
| ------- |
| pd.DataFrame |
| index = cell_id |
| columns = ['wafer_count', 'defect_count', 'wafer_ratio'] |
| - wafer_count : ν΄λΉ cellμμ κ²°ν¨μ λ³΄μΈ unique wafer μ |
| - defect_count : ν΄λΉ cellμ μ 체 κ²°ν¨ μ |
| - wafer_ratio : wafer_count / μ 체 unique wafer μ |
| """ |
| total_wafers = df["WAF_ID"].nunique() |
| cell_stats = df.groupby("cell_id").agg( |
| wafer_count=("WAF_ID", "nunique"), |
| defect_count=("WAF_ID", "size"), |
| ) |
| cell_stats["wafer_ratio"] = cell_stats["wafer_count"] / total_wafers if total_wafers else 0.0 |
| return cell_stats |
|
|
| @staticmethod |
| def filter_by_cell_wafer_count( |
| df: pd.DataFrame, |
| n1_min_wafer: int, |
| cell_size_mm: float = 3.0, |
| ) -> pd.DataFrame: |
| """ |
| Fine-grid κΈ°λ° n1 νν°: μΆ©λΆν waferμμ κ³΅ν΅ λ°μν cellμ κ²°ν¨λ§ μ μ§. |
| |
| 'κ³΅ν΅ μμΉμ λ°λ³΅ λ°μνλ κ²°ν¨λ§ μ μλ―Ένλ€'λ κ°μ μ ꡬν. |
| unique wafer μκ° `n1_min_wafer` λ―Έλ§μΈ cellμ λ
Έμ΄μ¦λ‘ κ°μ£Ό μ κ±°. |
| |
| Parameters |
| ---------- |
| df : pd.DataFrame |
| 'coor_x', 'coor_y', 'WAF_ID' ν¬ν¨ DF (cell ν λΉμ λ΄λΆμμ μν). |
| n1_min_wafer : int |
| cellμ΄ μ ν¨νκΈ° μν΄ νμν μ΅μ unique wafer μ. |
| cell_size_mm : float |
| fine-grid cell ν¬κΈ° (mm). |
| |
| Returns |
| ------- |
| pd.DataFrame |
| n1 쑰건μ ν΅κ³Όν cellμ κ²°ν¨λ§ ν¬ν¨. 'cell_wafer_count' μ»¬λΌ μΆκ°. |
| """ |
| df = WaferUtils.assign_fine_grid(df, cell_size_mm=cell_size_mm) |
| cell_stats = WaferUtils.get_cell_wafer_counts(df) |
|
|
| valid_cells = cell_stats[cell_stats["wafer_count"] >= n1_min_wafer].index |
| df_filtered = df[df["cell_id"].isin(valid_cells)].copy() |
| df_filtered = df_filtered.merge( |
| cell_stats[["wafer_count", "wafer_ratio"]], |
| left_on="cell_id", right_index=True, how="left", |
| ) |
| df_filtered.rename(columns={"wafer_count": "cell_wafer_count"}, inplace=True) |
| return df_filtered |
|
|
| @staticmethod |
| def summarize_filtering_result( |
| df_original: pd.DataFrame, |
| df_filtered: pd.DataFrame, |
| ) -> dict: |
| """ |
| νν°λ§ μ ν κ²°ν¨/Cell μ μμ½ ν΅κ³. |
| |
| Returns |
| ------- |
| dict |
| original_defects, filtered_defects, removed_defects, removal_rate(%), |
| original_cells, valid_cells. |
| """ |
| orig = len(df_original) |
| filt = len(df_filtered) |
| removed = orig - filt |
| rate = (removed / orig * 100) if orig else 0.0 |
| return { |
| "original_defects": orig, |
| "filtered_defects": filt, |
| "removed_defects": removed, |
| "removal_rate": round(rate, 2), |
| "original_cells": df_original["cell_id"].nunique() if "cell_id" in df_original.columns else 0, |
| "valid_cells": df_filtered["cell_id"].nunique() if "cell_id" in df_filtered.columns else 0, |
| } |
|
|
| |
| |
| |
| @staticmethod |
| def plot_wafer_map( |
| result_df: pd.DataFrame, |
| key: str, |
| pattern_list, |
| dominant_zone: str, |
| meta: Optional[dict] = None, |
| figsize: Tuple[int, int] = (8, 8), |
| save_path: Optional[str] = None, |
| show_mode: bool = False, |
| ) -> None: |
| """ |
| μ¨μ΄νΌ κ²°ν¨ λ§΅ μκ°ν. |
| |
| κ΅¬μ± μμ |
| ---------- |
| 1. λ°°κ²½ μμ |
| - νν ν¨ν΄: μ 체 μ μμμ λ² μ΄μ§μμΌλ‘ νμ |
| - κ·Έ μΈ: dominant_zoneμ ν΄λΉνλ wedgeλ§ λ² μ΄μ§μ νμ |
| 2. κ²°ν¨ μ°μ λ |
| - inlier 컬λΌμ΄ μμΌλ©΄ inlier/outlier μμ λΆλ¦¬ |
| - inlier μμμ ν¨ν΄μ λ°λΌ PATTERN_COLORS λ§€ν |
| 3. Centroid λ§μ»€ (νν μ μΈ) |
| - λΉ¨κ° μ(10mm) + X λ§μ»€ |
| 4. λμ¬μ: 30/45/60/90/120/150mm |
| 5. μκ³λ°©ν₯ 그리λ + 12μΒ·1μΒ·...Β·11μ λΌλ²¨ |
| 6. μΊ‘μ
: ν¨ν΄/ꡬμ/κ²°ν¨μ/μ₯λΉ/μ¨μ΄νΌ |
| |
| Parameters |
| ---------- |
| result_df : pd.DataFrame |
| 'coor_x', 'coor_y', (μ ν) 'inlier', 'zone_label' 컬λΌ. |
| key : str |
| μ μ₯ νμΌλͺ
Β·μΊ‘μ
μ μ¬μ©ν ν€. |
| pattern_list : list[str] | str |
| ν¨ν΄λͺ
. ['νν','κ΅°μ§'] κ°μ 리μ€νΈλ νμ©. |
| dominant_zone : str |
| μ£Όμ zone λΌλ²¨ (μ: 'Inner_03'). 'N/A' μ΄λ©΄ λ―Ένμ. |
| meta : dict, optional |
| 'main_centroid_x', 'main_centroid_y', 'wafer_count', 'EQP_NM_8030' λ±. |
| figsize : (int, int) |
| Figure ν¬κΈ°. |
| save_path : str, optional |
| μ μ₯ κ²½λ‘. Noneμ΄λ©΄ './result/result_figures/{key}.jpg'. |
| show_mode : bool |
| True λ©΄ plt.show() νΈμΆ. |
| """ |
| solid_radii = [45, 90, 150] |
| dashed_radii = [30, 60, 120] |
|
|
| |
| if isinstance(pattern_list, list): |
| pattern_str = ", ".join(pattern_list) |
| first_pattern = pattern_list[0] |
| else: |
| pattern_str = str(pattern_list) |
| first_pattern = pattern_str.split(",")[0].strip() |
|
|
| color = PATTERN_COLORS.get(first_pattern, "steelblue") |
| fig, ax = plt.subplots(figsize=figsize) |
|
|
| |
| if "νν" in pattern_str: |
| ax.add_patch(Circle((0, 0), 150, facecolor="#F5F5DC", |
| edgecolor="none", alpha=0.8, zorder=1)) |
| elif dominant_zone and dominant_zone != "N/A": |
| try: |
| for zone in [z.strip() for z in dominant_zone.split(",")]: |
| ztype, zclock = zone.split("_") |
| r_min = 0 if ztype == "Inner" else 105 |
| r_max = 105 if ztype == "Inner" else 150 |
| if zclock in CLOCK_LABELS: |
| idx = CLOCK_LABELS.index(zclock) |
| |
| math_start = 90 - (idx + 1) * 30 |
| math_end = 90 - idx * 30 |
| ax.add_patch(Wedge((0, 0), r_max, math_start, math_end, |
| width=(r_max - r_min), |
| facecolor="#F5F5DC", |
| edgecolor="none", alpha=0.8, zorder=1)) |
| except Exception: |
| |
| pass |
|
|
| |
| if "inlier" in result_df.columns: |
| inliers = result_df[result_df["inlier"] == True] |
| outliers = result_df[result_df["inlier"] == False] |
| ax.scatter(outliers["coor_x"], outliers["coor_y"], |
| c="lightgray", s=15, alpha=0.3, zorder=4) |
| ax.scatter(inliers["coor_x"], inliers["coor_y"], |
| c=color, s=35, alpha=0.5, |
| label=f"Inlier ({pattern_str})", zorder=5) |
| else: |
| ax.scatter(result_df["coor_x"], result_df["coor_y"], |
| c=color, s=30, alpha=0.5, zorder=5) |
|
|
| |
| if meta and "νν" not in pattern_str: |
| cx = meta.get("main_centroid_x") |
| cy = meta.get("main_centroid_y") |
| if cx is not None and cy is not None: |
| ax.add_patch(Circle((cx, cy), radius=10, facecolor="none", |
| edgecolor="red", linewidth=2.5, |
| linestyle="-", alpha=0.9, zorder=7)) |
| ax.scatter(cx, cy, c="red", s=80, marker="x", |
| linewidths=2.5, zorder=8, label="Centroid") |
|
|
| |
| for r in solid_radii: |
| ax.add_patch(plt.Circle((0, 0), r, color="black", fill=False, |
| linestyle="-", linewidth=1.2, alpha=0.7, zorder=2)) |
| for r in dashed_radii: |
| ax.add_patch(plt.Circle((0, 0), r, color="gray", fill=False, |
| linestyle="--", linewidth=0.8, alpha=0.5, zorder=2)) |
|
|
| |
| clock_angles = {0: "12μ", 30: "1μ", 60: "2μ", 90: "3μ", |
| 120: "4μ", 150: "5μ", 180: "6μ", 210: "7μ", |
| 240: "8μ", 270: "9μ", 300: "10μ", 330: "11μ"} |
| grid_end = max(solid_radii) + 12 |
| label_r = grid_end * 0.93 |
| for angle_deg, label_text in clock_angles.items(): |
| |
| math_rad = np.deg2rad(90 - angle_deg) |
| ax.plot([0, grid_end * np.cos(math_rad)], |
| [0, grid_end * np.sin(math_rad)], |
| color="gray", linestyle=":", linewidth=0.8, zorder=2) |
| ax.text(label_r * np.cos(math_rad), label_r * np.sin(math_rad), |
| label_text, color="darkblue", fontsize=8, |
| ha="center", va="center", weight="bold", alpha=0.75, zorder=3) |
|
|
| ax.axhline(0, color="k", linewidth=0.4, zorder=3) |
| ax.axvline(0, color="k", linewidth=0.4, zorder=3) |
| max_range = max(solid_radii) + 20 |
| ax.set_xlim(-max_range, max_range) |
| ax.set_ylim(-max_range, max_range) |
| ax.set_aspect("equal", "box") |
| ax.set_xlabel("X (mm)") |
| ax.set_ylabel("Y (mm)") |
| ax.legend(loc="upper right", fontsize=8) |
| ax.grid(True, alpha=0.15) |
|
|
| |
| total = len(result_df) |
| dom_cnt = 0 |
| if "zone_label" in result_df.columns and dominant_zone != "N/A": |
| dom_zones = [z.strip() for z in dominant_zone.split(",")] |
| dom_cnt = result_df[result_df["zone_label"].isin(dom_zones)].shape[0] |
| ratio = (dom_cnt / total * 100) if total else 0.0 |
|
|
| lines = [ |
| f"Key: {key}", |
| f"ν¨ν΄: {pattern_str} | λ°μꡬμ: {dominant_zone}", |
| f"μ 체 κ²°ν¨: {total}건 | μ£Όμμμ κ²°ν¨: {dom_cnt}건 | λΉμ¨: {ratio:.1f}%", |
| ] |
| if meta: |
| lines.append(f"μ₯λΉ: {meta.get('EQP_NM_8030', '-')} | μ¨μ΄νΌ: {meta.get('wafer_count', '-')}λ§€") |
| ax.set_title("\n".join(lines), fontsize=9, loc="left", pad=8) |
| plt.tight_layout() |
|
|
| if save_path is None: |
| save_dir = "./result/result_figures" |
| os.makedirs(save_dir, exist_ok=True) |
| save_path = os.path.join(save_dir, f"{key}.jpg") |
| plt.savefig(save_path, dpi=150, bbox_inches="tight") |
| if show_mode: |
| plt.show() |
| plt.close() |
|
|
|
|
| |
| |
| |
| |
| setup_korean_font = WaferUtils.setup_korean_font |
| load_config = WaferUtils.load_config |
| map_roughbin_no = WaferUtils.map_roughbin_no |
| add_zone_labels = WaferUtils.add_zone_labels |
| assign_fine_grid = WaferUtils.assign_fine_grid |
| get_cell_wafer_counts = WaferUtils.get_cell_wafer_counts |
| filter_by_cell_wafer_count = WaferUtils.filter_by_cell_wafer_count |
| summarize_filtering_result = WaferUtils.summarize_filtering_result |
| plot_wafer_map = WaferUtils.plot_wafer_map |
|
|