semiconductor / middleware /dashboard.py
Scribbler310
Production deployment with LFS models
a985b94
"""
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
# ============================================================
@callback(Output('tab-content', 'children'), Input('tabs', 'value'))
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}),
]),
])
@callback(
Output('prediction-result', 'children'),
Input('btn-predict', 'n_clicks'),
State('slider-scans', 'value'),
State('slider-fail-rate', 'value'),
prevent_initial_call=True,
)
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)