Spaces:
Running
Running
| """ | |
| Wafer Defect Analytics Dashboard — Phase 5 | |
| Plotly Dash web application for semiconductor material waste analysis | |
| and predictive material requirement estimation. | |
| Now using the Mixed-type Wafer Defect Dataset (38,015 wafers). | |
| """ | |
| import os | |
| import pickle | |
| import sqlite3 | |
| import numpy as np | |
| import pandas as pd | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from dash import Dash, html, dcc, callback, Input, Output, State | |
| # --- CONFIGURATION --- | |
| DB_PATH = os.path.join(os.path.dirname(__file__), 'wafer_control.db') | |
| MODEL_PATH = os.path.join(os.path.dirname(__file__), 'material_model.pkl') | |
| # Semiconductor-themed color palette | |
| COLORS = { | |
| 'bg': '#0a0e17', | |
| 'card': '#131a2e', | |
| 'card_border': '#1e2d4a', | |
| 'accent': '#00d4ff', | |
| 'accent2': '#7c3aed', | |
| 'accent3': '#10b981', | |
| 'danger': '#ef4444', | |
| 'warning': '#f59e0b', | |
| 'text': '#e2e8f0', | |
| 'text_muted': '#94a3b8', | |
| } | |
| DEFECT_COLORS = { | |
| 'Center': '#ef4444', 'Donut': '#f59e0b', 'Edge-Loc': '#10b981', | |
| 'Edge-Ring': '#3b82f6', 'Loc': '#8b5cf6', 'Random': '#ec4899', | |
| 'Scratch': '#06b6d4', 'Near-full': '#f97316', 'None': '#6b7280', | |
| 'Undetected': '#374151', | |
| } | |
| def load_data(): | |
| conn = sqlite3.connect(DB_PATH) | |
| df = pd.read_sql_query("SELECT * FROM wafer_logs", conn) | |
| conn.close() | |
| df['scan_time'] = pd.to_datetime(df['scan_time']) | |
| df['scan_date'] = df['scan_time'].dt.date | |
| return df | |
| def load_model(): | |
| if os.path.exists(MODEL_PATH): | |
| with open(MODEL_PATH, 'rb') as f: | |
| return pickle.load(f) | |
| return None | |
| # --- LOAD DATA --- | |
| df = load_data() | |
| model_pkg = load_model() | |
| # --- PRE-COMPUTE STATS --- | |
| total_scans = len(df) | |
| fail_count = len(df[df['status'] == 'FAIL']) | |
| pass_count = len(df[df['status'] == 'PASS']) | |
| pass_rate = round((pass_count / total_scans) * 100, 1) | |
| scrap_count = len(df[df['action'] == 'ROUTE_TO_SCRAP']) | |
| total_waste = round(df['material_wasted_pct'].sum(), 1) | |
| avg_waste = round(df[df['status'] == 'FAIL']['material_wasted_pct'].mean(), 2) | |
| avg_confidence = round(df[df['status'] == 'FAIL']['confidence'].mean(), 2) | |
| # Daily aggregations | |
| daily = df.groupby('scan_date').agg( | |
| scans=('id', 'count'), | |
| fails=('status', lambda x: (x == 'FAIL').sum()), | |
| waste=('material_wasted_pct', lambda x: x.sum() / 100.0), | |
| avg_waste=('material_wasted_pct', 'mean'), | |
| ).reset_index() | |
| daily['fail_rate'] = round((daily['fails'] / daily['scans']) * 100, 1) | |
| daily['scan_date'] = pd.to_datetime(daily['scan_date']) | |
| # ============================================================ | |
| # DASH APP | |
| # ============================================================ | |
| app = Dash(__name__, suppress_callback_exceptions=True) | |
| app.title = "Wafer Defect Analytics — Semiconductor QC Dashboard" | |
| card_style = { | |
| 'backgroundColor': COLORS['card'], 'border': f"1px solid {COLORS['card_border']}", | |
| 'borderRadius': '12px', 'padding': '24px', 'marginBottom': '16px', | |
| } | |
| kpi_style = { | |
| 'backgroundColor': COLORS['card'], 'border': f"1px solid {COLORS['card_border']}", | |
| 'borderRadius': '12px', 'padding': '20px', 'textAlign': 'center', 'flex': '1', 'minWidth': '160px', | |
| } | |
| def make_kpi(title, value, subtitle="", color=COLORS['accent']): | |
| return html.Div(style=kpi_style, children=[ | |
| html.P(title, style={'color': COLORS['text_muted'], 'fontSize': '12px', 'marginBottom': '4px', 'fontWeight': '500', 'textTransform': 'uppercase', 'letterSpacing': '1px'}), | |
| html.H2(str(value), style={'color': color, 'fontSize': '28px', 'fontWeight': '700', 'margin': '4px 0'}), | |
| html.P(subtitle, style={'color': COLORS['text_muted'], 'fontSize': '11px', 'marginTop': '4px'}), | |
| ]) | |
| def chart_layout(title): | |
| return dict( | |
| template='plotly_dark', paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', | |
| title=dict(text=title, font=dict(size=16, color=COLORS['text'])), | |
| font=dict(color=COLORS['text_muted'], size=12), | |
| margin=dict(l=40, r=20, t=50, b=40), | |
| legend=dict(bgcolor='rgba(0,0,0,0)'), | |
| ) | |
| # ============================================================ | |
| # FIGURES | |
| # ============================================================ | |
| # 1. Defect type distribution (pie) — YOLO predictions | |
| defect_counts = df[df['status'] == 'FAIL']['defect_type'].value_counts().reset_index() | |
| defect_counts.columns = ['defect_type', 'count'] | |
| fig_pie = px.pie(defect_counts, names='defect_type', values='count', color='defect_type', | |
| color_discrete_map=DEFECT_COLORS, hole=0.45) | |
| fig_pie.update_layout(**chart_layout('YOLOv8 Predicted Defect Distribution')) | |
| fig_pie.update_traces(textinfo='label+percent', textfont_size=11) | |
| # 2. Ground truth distribution (pie) — actual labels | |
| gt_counts = df[df['status'] == 'FAIL']['ground_truth'].value_counts().reset_index() | |
| gt_counts.columns = ['ground_truth', 'count'] | |
| fig_gt_pie = px.pie(gt_counts.head(15), names='ground_truth', values='count', hole=0.45) | |
| fig_gt_pie.update_layout(**chart_layout('Ground Truth Label Distribution (Top 15)')) | |
| fig_gt_pie.update_traces(textinfo='label+percent', textfont_size=10) | |
| # 3. Material waste by defect type (bar) | |
| waste_by_type = df[df['status'] == 'FAIL'].groupby('defect_type').agg( | |
| total_waste=('material_wasted_pct', lambda x: x.sum() / 100.0), count=('id', 'count'), | |
| ).reset_index().sort_values('total_waste', ascending=True) | |
| fig_waste_bar = go.Figure() | |
| fig_waste_bar.add_trace(go.Bar( | |
| y=waste_by_type['defect_type'], x=waste_by_type['total_waste'], orientation='h', | |
| marker_color=[DEFECT_COLORS.get(d, '#6b7280') for d in waste_by_type['defect_type']], | |
| text=[f"{v:.1f}" for v in waste_by_type['total_waste']], textposition='outside', | |
| )) | |
| fig_waste_bar.update_layout(**chart_layout('Total Material Waste by Predicted Defect')) | |
| fig_waste_bar.update_xaxes(title_text='Equivalent Lost Wafers') | |
| # 4. Daily fail rate trend | |
| fig_trend = go.Figure() | |
| fig_trend.add_trace(go.Scatter( | |
| x=daily['scan_date'], y=daily['fail_rate'], mode='lines+markers', | |
| line=dict(color=COLORS['danger'], width=2), marker=dict(size=5), | |
| name='Fail Rate %', fill='tozeroy', fillcolor='rgba(239, 68, 68, 0.1)', | |
| )) | |
| fig_trend.update_layout(**chart_layout('Daily Defect Rate Over Time')) | |
| fig_trend.update_yaxes(title_text='Fail Rate %', range=[0, 105]) | |
| # 5. Daily waste trend | |
| fig_waste_trend = go.Figure() | |
| fig_waste_trend.add_trace(go.Scatter( | |
| x=daily['scan_date'], y=daily['waste'], mode='lines+markers', | |
| line=dict(color=COLORS['warning'], width=2), marker=dict(size=5), | |
| name='Lost Wafers', fill='tozeroy', fillcolor='rgba(245, 158, 11, 0.1)', | |
| )) | |
| fig_waste_trend.update_layout(**chart_layout('Daily Material Waste Over Time')) | |
| fig_waste_trend.update_yaxes(title_text='Total Lost Wafers') | |
| # 6. Action breakdown | |
| action_counts = df['action'].value_counts().reset_index() | |
| action_counts.columns = ['action', 'count'] | |
| action_colors = {'ROUTE_TO_SCRAP': COLORS['danger'], 'MOVE_TO_MICRO_STAGE': COLORS['warning'], 'ROUTE_TO_ASSEMBLY': COLORS['accent3']} | |
| fig_action = px.bar(action_counts, x='action', y='count', color='action', | |
| color_discrete_map=action_colors, text='count') | |
| fig_action.update_layout(**chart_layout('Wafer Routing Actions')) | |
| fig_action.update_traces(textposition='outside') | |
| # 7. Feature importance | |
| fig_importance = go.Figure() | |
| if model_pkg: | |
| imp = model_pkg['metrics']['importances'] | |
| imp_df = pd.DataFrame({'feature': list(imp.keys()), 'importance': list(imp.values())}) | |
| imp_df = imp_df.sort_values('importance', ascending=True).tail(10) | |
| fig_importance.add_trace(go.Bar( | |
| y=imp_df['feature'], x=imp_df['importance'], orientation='h', | |
| marker_color=COLORS['accent2'], | |
| text=[f"{v:.3f}" for v in imp_df['importance']], textposition='outside', | |
| )) | |
| fig_importance.update_layout(**chart_layout('Top 10 Prediction Features')) | |
| # ============================================================ | |
| # LAYOUT | |
| # ============================================================ | |
| app.layout = html.Div(style={ | |
| 'backgroundColor': COLORS['bg'], 'minHeight': '100vh', | |
| 'fontFamily': "'Inter', -apple-system, BlinkMacSystemFont, sans-serif", | |
| 'color': COLORS['text'], 'padding': '24px 32px', | |
| }, children=[ | |
| # HEADER | |
| html.Div(style={'marginBottom': '32px', 'borderBottom': f"1px solid {COLORS['card_border']}", 'paddingBottom': '20px'}, children=[ | |
| html.Div(style={'display': 'flex', 'alignItems': 'center', 'gap': '12px'}, children=[ | |
| html.Div(style={'width': '12px', 'height': '12px', 'borderRadius': '50%', | |
| 'backgroundColor': COLORS['accent3'], 'boxShadow': f"0 0 8px {COLORS['accent3']}"}), | |
| html.H1("Wafer Defect Analytics", style={ | |
| 'margin': '0', 'fontSize': '28px', 'fontWeight': '700', | |
| 'background': f"linear-gradient(135deg, {COLORS['accent']}, {COLORS['accent2']})", | |
| 'WebkitBackgroundClip': 'text', 'WebkitTextFillColor': 'transparent', | |
| }), | |
| ]), | |
| html.P("Mixed-type Wafer Defect Dataset — Material Waste Dashboard", | |
| style={'color': COLORS['text_muted'], 'marginTop': '4px', 'fontSize': '14px'}), | |
| ]), | |
| # TABS | |
| dcc.Tabs(id='tabs', value='tab-waste', style={'marginBottom': '24px'}, children=[ | |
| dcc.Tab(label='📊 Historical Waste Analysis', value='tab-waste', style={ | |
| 'backgroundColor': COLORS['card'], 'color': COLORS['text_muted'], | |
| 'border': f"1px solid {COLORS['card_border']}", 'borderRadius': '8px 8px 0 0', | |
| 'padding': '12px 24px', 'fontWeight': '600', | |
| }, selected_style={ | |
| 'backgroundColor': COLORS['accent2'], 'color': '#fff', | |
| 'border': f"1px solid {COLORS['accent2']}", 'borderRadius': '8px 8px 0 0', | |
| 'padding': '12px 24px', 'fontWeight': '600', | |
| }), | |
| dcc.Tab(label='🔮 Material Prediction', value='tab-predict', style={ | |
| 'backgroundColor': COLORS['card'], 'color': COLORS['text_muted'], | |
| 'border': f"1px solid {COLORS['card_border']}", 'borderRadius': '8px 8px 0 0', | |
| 'padding': '12px 24px', 'fontWeight': '600', | |
| }, selected_style={ | |
| 'backgroundColor': COLORS['accent2'], 'color': '#fff', | |
| 'border': f"1px solid {COLORS['accent2']}", 'borderRadius': '8px 8px 0 0', | |
| 'padding': '12px 24px', 'fontWeight': '600', | |
| }), | |
| ]), | |
| html.Div(id='tab-content'), | |
| ]) | |
| # ============================================================ | |
| # CALLBACKS | |
| # ============================================================ | |
| def render_tab(tab): | |
| if tab == 'tab-waste': | |
| return html.Div([ | |
| # KPI Row | |
| html.Div(style={'display': 'flex', 'gap': '12px', 'flexWrap': 'wrap', 'marginBottom': '24px'}, children=[ | |
| make_kpi("Total Scans", f"{total_scans:,}", "wafers inspected", COLORS['accent']), | |
| make_kpi("Pass Rate", f"{pass_rate}%", f"{pass_count:,} passed", COLORS['accent3']), | |
| make_kpi("Fail Rate", f"{100-pass_rate}%", f"{fail_count:,} defective", COLORS['danger']), | |
| make_kpi("Scrapped", f"{scrap_count:,}", "routed to scrap", COLORS['warning']), | |
| make_kpi("Avg Waste/Wafer", f"{avg_waste}%", "per defective wafer", COLORS['danger']), | |
| make_kpi("Avg Confidence", f"{avg_confidence}", "model certainty", COLORS['accent3']), | |
| ]), | |
| # Charts Row 1: YOLO predictions vs Ground Truth | |
| html.Div(style={'display': 'grid', 'gridTemplateColumns': '1fr 1fr', 'gap': '16px', 'marginBottom': '16px'}, children=[ | |
| html.Div(style=card_style, children=[dcc.Graph(figure=fig_pie, config={'displayModeBar': False})]), | |
| html.Div(style=card_style, children=[dcc.Graph(figure=fig_gt_pie, config={'displayModeBar': False})]), | |
| ]), | |
| # Charts Row 2 | |
| html.Div(style={'display': 'grid', 'gridTemplateColumns': '1fr 1fr', 'gap': '16px', 'marginBottom': '16px'}, children=[ | |
| html.Div(style=card_style, children=[dcc.Graph(figure=fig_waste_bar, config={'displayModeBar': False})]), | |
| html.Div(style=card_style, children=[dcc.Graph(figure=fig_action, config={'displayModeBar': False})]), | |
| ]), | |
| # Trend charts | |
| html.Div(style={'display': 'grid', 'gridTemplateColumns': '1fr 1fr', 'gap': '16px'}, children=[ | |
| html.Div(style=card_style, children=[dcc.Graph(figure=fig_trend, config={'displayModeBar': False})]), | |
| html.Div(style=card_style, children=[dcc.Graph(figure=fig_waste_trend, config={'displayModeBar': False})]), | |
| ]), | |
| ]) | |
| elif tab == 'tab-predict': | |
| model_status = "✅ Model loaded" if model_pkg else "❌ No model found" | |
| model_metrics = "" | |
| if model_pkg: | |
| m = model_pkg['metrics'] | |
| model_metrics = f"R² = {m['r2']:.4f} | MAE = {m['mae']:.2f}%" | |
| return html.Div([ | |
| html.Div(style={**card_style, 'display': 'flex', 'justifyContent': 'space-between', 'alignItems': 'center'}, children=[ | |
| html.Div([ | |
| html.H3("Prediction Model", style={'margin': '0 0 4px 0', 'fontSize': '18px'}), | |
| html.P(model_status, style={'margin': '0', 'color': COLORS['accent3'] if model_pkg else COLORS['danger']}), | |
| ]), | |
| html.P(model_metrics, style={'color': COLORS['text_muted'], 'fontFamily': 'monospace'}), | |
| ]), | |
| html.Div(style=card_style, children=[ | |
| html.H3("Forecast Parameters", style={'marginTop': '0', 'fontSize': '18px', 'marginBottom': '20px'}), | |
| html.Div(style={'display': 'grid', 'gridTemplateColumns': '1fr 1fr', 'gap': '24px'}, children=[ | |
| html.Div([ | |
| html.Label("Expected Daily Production (wafers)", style={'color': COLORS['text_muted'], 'fontSize': '13px'}), | |
| dcc.Slider(id='slider-scans', min=100, max=2000, step=50, value=1300, | |
| marks={100: '100', 500: '500', 1000: '1000', 1500: '1500', 2000: '2000'}, | |
| tooltip={"placement": "bottom", "always_visible": True}), | |
| ]), | |
| html.Div([ | |
| html.Label("Expected Defect Rate (%)", style={'color': COLORS['text_muted'], 'fontSize': '13px'}), | |
| dcc.Slider(id='slider-fail-rate', min=0, max=100, step=5, value=97, | |
| marks={0: '0%', 25: '25%', 50: '50%', 75: '75%', 100: '100%'}, | |
| tooltip={"placement": "bottom", "always_visible": True}), | |
| ]), | |
| ]), | |
| html.Br(), | |
| html.Button("🔮 Predict Material Needs", id='btn-predict', n_clicks=0, style={ | |
| 'backgroundColor': COLORS['accent2'], 'color': '#fff', 'border': 'none', | |
| 'borderRadius': '8px', 'padding': '12px 32px', 'fontSize': '15px', | |
| 'fontWeight': '600', 'cursor': 'pointer', 'width': '100%', | |
| }), | |
| ]), | |
| html.Div(id='prediction-result'), | |
| html.Div(style=card_style, children=[ | |
| dcc.Graph(figure=fig_importance, config={'displayModeBar': False}), | |
| ]), | |
| ]) | |
| def predict(n_clicks, n_scans, fail_pct): | |
| if not model_pkg: | |
| return html.Div(style={**card_style, 'borderColor': COLORS['danger']}, children=[ | |
| html.P("❌ No model loaded.", style={'color': COLORS['danger']}), | |
| ]) | |
| from material_predictor import predict_material_needs | |
| model = model_pkg['model'] | |
| feat_cols = model_pkg['feature_cols'] | |
| fail_df = df[df['status'] == 'FAIL'] | |
| dist = fail_df['defect_type'].value_counts(normalize=True).to_dict() | |
| pred = predict_material_needs(model, feat_cols, n_scans, fail_pct / 100.0, dist) | |
| return html.Div(style={ | |
| **card_style, 'borderColor': COLORS['accent'], | |
| 'background': f"linear-gradient(135deg, {COLORS['card']}, #1a1040)", | |
| }, children=[ | |
| html.Div(style={'display': 'flex', 'justifyContent': 'space-around', 'flexWrap': 'wrap', 'gap': '16px'}, children=[ | |
| make_kpi("Daily Production", f"{n_scans}", "wafers", COLORS['accent']), | |
| make_kpi("Expected Defect Rate", f"{fail_pct}%", f"~{int(n_scans * fail_pct / 100)} defective", COLORS['danger']), | |
| make_kpi("Avg Waste/Wafer", f"{pred['avg_waste_per_wafer']:.1f}%", "per defective wafer loss", COLORS['warning']), | |
| make_kpi("Total Daily Waste", f"{pred['total_daily_waste']:.1f} wafers", "total estimated loss", COLORS['danger']), | |
| ]), | |
| ]) | |
| if __name__ == '__main__': | |
| print("\n" + "=" * 60) | |
| print(" WAFER DEFECT ANALYTICS DASHBOARD") | |
| print(f" Data: {total_scans:,} scans | {fail_count:,} defects | {pass_count:,} pass") | |
| print(f" Model: {'Loaded ✅' if model_pkg else 'Not found ❌'}") | |
| print("=" * 60) | |
| print(f"\n 🌐 Open: http://127.0.0.1:8050\n") | |
| app.run(debug=True, port=8050) | |