Wafer Process Statistical Control and R2R Management
Semiconductor manufacturing requires precision control at the nanometer scale. In this chapter, we will learn about statistical control of wafer processes and AI technologies, including Run-to-Run (R2R) control, Virtual Metrology (VM), and process drift detection.
Run-to-Run control is an adaptive control method that feeds back measurement results from the previous wafer (lot) to adjust manufacturing conditions for the next wafer.
\( u_k \): Control input at run k (process parameter)
\( y_k \): Measured value at run k
\( T \): Target value
\( K \): Control gain (0 < K < 1)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
class R2RController:
"""Run-to-Run EWMA Control System"""
def __init__(self, target, initial_input, control_gain=0.5):
"""
Args:
target: Target value (e.g., film thickness 100nm)
initial_input: Initial control input (e.g., deposition time 60s)
control_gain: Control gain K (0 < K < 1)
"""
self.target = target
self.u = initial_input # Current control input
self.K = control_gain
self.history = {
'run': [],
'input': [],
'output': [],
'error': []
}
def process_model(self, u, drift=0, noise_std=1.0):
"""
Process model (simplified)
Args:
u: Control input (deposition time, etc.)
drift: Process drift
noise_std: Process noise standard deviation
Returns:
Measured value (film thickness, etc.)
"""
# Linear model: y = 1.5 * u + drift + noise
gain = 1.5 # Process gain
y = gain * u + drift + np.random.normal(0, noise_std)
return y
def update_control(self, measurement):
"""
Update control input (EWMA control)
Args:
measurement: Measured value
Returns:
Next control input
"""
error = self.target - measurement
# EWMA control law
u_next = self.u + self.K * error
# Control input constraints (physical constraints)
u_next = np.clip(u_next, 30, 90) # Range of 30-90 seconds
self.u = u_next
return u_next
def run_simulation(self, n_runs=100, drift_start=50, drift_rate=0.1):
"""
R2R control simulation
Args:
n_runs: Number of runs (wafers)
drift_start: Drift start run
drift_rate: Drift rate (/run)
"""
for run in range(n_runs):
# Calculate process drift
if run >= drift_start:
drift = (run - drift_start) * drift_rate
else:
drift = 0
# Process execution and measurement
y = self.process_model(self.u, drift=drift, noise_std=2.0)
# Record history
error = y - self.target
self.history['run'].append(run + 1)
self.history['input'].append(self.u)
self.history['output'].append(y)
self.history['error'].append(error)
# Update next control input
self.update_control(y)
return pd.DataFrame(self.history)
def plot_results(self, df):
"""Visualize control results"""
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
# Measurement value trend
axes[0].plot(df['run'], df['output'], marker='o', color='#11998e',
linewidth=1.5, markersize=4, label='Measured Value')
axes[0].axhline(y=self.target, color='red', linestyle='--',
linewidth=2, label=f'Target Value ({self.target}nm)')
axes[0].fill_between(df['run'], self.target - 3, self.target + 3,
alpha=0.2, color='green', label='Tolerance Range (±3nm)')
axes[0].set_xlabel('Run Number (Wafer)')
axes[0].set_ylabel('Film Thickness (nm)')
axes[0].set_title('Film Thickness Control by R2R', fontsize=12, fontweight='bold')
axes[0].legend()
axes[0].grid(alpha=0.3)
# Control input trend
axes[1].plot(df['run'], df['input'], marker='s', color='#38ef7d',
linewidth=1.5, markersize=4, label='Deposition Time')
axes[1].set_xlabel('Run Number (Wafer)')
axes[1].set_ylabel('Deposition Time (s)')
axes[1].set_title('R2R Control Input', fontsize=12, fontweight='bold')
axes[1].legend()
axes[1].grid(alpha=0.3)
# Control error
axes[2].plot(df['run'], df['error'], marker='^', color='#f38181',
linewidth=1.5, markersize=4, label='Control Error')
axes[2].axhline(y=0, color='black', linestyle='-', linewidth=1)
axes[2].fill_between(df['run'], -3, 3, alpha=0.2, color='green',
label='Tolerance Error (±3nm)')
axes[2].set_xlabel('Run Number (Wafer)')
axes[2].set_ylabel('Error (nm)')
axes[2].set_title('Control Error', fontsize=12, fontweight='bold')
axes[2].legend()
axes[2].grid(alpha=0.3)
plt.tight_layout()
plt.savefig('r2r_control_results.png', dpi=300, bbox_inches='tight')
plt.show()
# Execution example
print("=" * 60)
print("Run-to-Run EWMA Control System (CVD Deposition Process)")
print("=" * 60)
# Initialize R2R control system
r2r = R2RController(
target=100.0, # Target film thickness 100nm
initial_input=60.0, # Initial deposition time 60s
control_gain=0.5 # Control gain K=0.5
)
# Run simulation
df_results = r2r.run_simulation(
n_runs=100,
drift_start=50, # Drift starts at run 50
drift_rate=0.1 # Drift of 0.1nm/run
)
# Performance evaluation
print(f"\nControl Performance Evaluation:")
print(f"Average film thickness: {df_results['output'].mean():.2f} nm")
print(f"Film thickness standard deviation: {df_results['output'].std():.2f} nm")
print(f"Mean absolute error: {df_results['error'].abs().mean():.2f} nm")
print(f"Maximum error: {df_results['error'].abs().max():.2f} nm")
# Calculate in-spec rate
in_spec = df_results[(df_results['output'] >= 97) & (df_results['output'] <= 103)]
print(f"In-spec rate (100±3nm): {len(in_spec)/len(df_results)*100:.1f}%")
# Comparison before and after drift
before_drift = df_results[df_results['run'] < 50]
after_drift = df_results[df_results['run'] >= 50]
print(f"\nAverage before drift (runs 1-49): {before_drift['output'].mean():.2f} nm")
print(f"Average after drift (runs 50-100): {after_drift['output'].mean():.2f} nm")
# Visualization
r2r.plot_results(df_results)
Implementation Points:
Virtual Metrology (VM) is a technology that predicts wafer quality characteristics (film thickness, CD, electrical properties, etc.) from process equipment sensor data (temperature, pressure, flow rate, etc.) without using measurement instruments.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
import warnings
warnings.filterwarnings('ignore')
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
class VirtualMetrologySystem:
"""Virtual Metrology System"""
def __init__(self):
self.model = None
self.feature_names = None
def generate_process_data(self, n_samples=500):
"""
Generate process sensor data (assuming etching process)
Returns:
(Sensor data, CD measurement values)
"""
np.random.seed(42)
# Process conditions (equipment sensor data)
rf_power = np.random.normal(1000, 50, n_samples) # RF Power (W)
pressure = np.random.normal(50, 5, n_samples) # Pressure (mTorr)
gas_flow_ar = np.random.normal(200, 10, n_samples) # Ar gas flow (sccm)
gas_flow_cf4 = np.random.normal(50, 5, n_samples) # CF4 gas flow (sccm)
temp_chamber = np.random.normal(60, 3, n_samples) # Chamber temperature (°C)
etch_time = np.random.normal(120, 5, n_samples) # Etching time (s)
# CD (Critical Dimension) physical model
# CD = f(RF power, pressure, gas flows, temp, time) + noise
cd_base = 90 # Base CD (nm)
cd = (cd_base
- 0.002 * (rf_power - 1000) # RF power↑ → CD↓
+ 0.05 * (pressure - 50) # Pressure↑ → CD↑
- 0.01 * (gas_flow_cf4 - 50) # CF4↑ → CD↓ (etching promotion)
+ 0.005 * (temp_chamber - 60) # Temperature↑ → CD↑
- 0.03 * (etch_time - 120) # Time↑ → CD↓
+ np.random.normal(0, 1, n_samples)) # Noise
# Create DataFrame
df = pd.DataFrame({
'rf_power': rf_power,
'pressure': pressure,
'gas_flow_ar': gas_flow_ar,
'gas_flow_cf4': gas_flow_cf4,
'temp_chamber': temp_chamber,
'etch_time': etch_time,
'cd_measured': cd
})
return df
def train_vm_model(self, df, feature_columns, target_column='cd_measured'):
"""
Train VM model
Args:
df: Process data
feature_columns: Feature columns
target_column: Target column for prediction
"""
X = df[feature_columns].values
y = df[target_column].values
# Split training/test data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42
)
# Random Forest model
self.model = RandomForestRegressor(
n_estimators=100,
max_depth=15,
min_samples_split=5,
random_state=42
)
self.model.fit(X_train, y_train)
self.feature_names = feature_columns
# Prediction and evaluation
y_pred_train = self.model.predict(X_train)
y_pred_test = self.model.predict(X_test)
# Performance metrics
r2_train = r2_score(y_train, y_pred_train)
r2_test = r2_score(y_test, y_pred_test)
rmse_test = np.sqrt(mean_squared_error(y_test, y_pred_test))
mae_test = mean_absolute_error(y_test, y_pred_test)
results = {
'X_train': X_train, 'y_train': y_train,
'X_test': X_test, 'y_test': y_test,
'y_pred_train': y_pred_train, 'y_pred_test': y_pred_test,
'r2_train': r2_train, 'r2_test': r2_test,
'rmse_test': rmse_test, 'mae_test': mae_test
}
return results
def predict_cd(self, process_conditions):
"""
Predict CD from process conditions
Args:
process_conditions: Array or DataFrame of process conditions
Returns:
Predicted CD value
"""
if self.model is None:
raise ValueError("Model has not been trained")
return self.model.predict(process_conditions)
def plot_vm_results(self, results):
"""Visualize VM results"""
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Prediction accuracy plot (training data)
axes[0, 0].scatter(results['y_train'], results['y_pred_train'],
alpha=0.5, s=20, color='#11998e', label='Training Data')
axes[0, 0].plot([85, 95], [85, 95], 'r--', linewidth=2, label='Ideal Line')
axes[0, 0].set_xlabel('Measured CD (nm)')
axes[0, 0].set_ylabel('Predicted CD (nm)')
axes[0, 0].set_title(f'Training Data Prediction Accuracy (R²={results["r2_train"]:.4f})',
fontsize=12, fontweight='bold')
axes[0, 0].legend()
axes[0, 0].grid(alpha=0.3)
# Prediction accuracy plot (test data)
axes[0, 1].scatter(results['y_test'], results['y_pred_test'],
alpha=0.5, s=20, color='#38ef7d', label='Test Data')
axes[0, 1].plot([85, 95], [85, 95], 'r--', linewidth=2, label='Ideal Line')
axes[0, 1].set_xlabel('Measured CD (nm)')
axes[0, 1].set_ylabel('Predicted CD (nm)')
axes[0, 1].set_title(f'Test Data Prediction Accuracy (R²={results["r2_test"]:.4f})',
fontsize=12, fontweight='bold')
axes[0, 1].legend()
axes[0, 1].grid(alpha=0.3)
# Prediction error distribution
errors = results['y_pred_test'] - results['y_test']
axes[1, 0].hist(errors, bins=30, color='#4ecdc4', alpha=0.7, edgecolor='black')
axes[1, 0].axvline(x=0, color='red', linestyle='--', linewidth=2, label='Zero Error')
axes[1, 0].set_xlabel('Prediction Error (nm)')
axes[1, 0].set_ylabel('Frequency')
axes[1, 0].set_title(f'Prediction Error Distribution (MAE={results["mae_test"]:.2f}nm)',
fontsize=12, fontweight='bold')
axes[1, 0].legend()
axes[1, 0].grid(alpha=0.3)
# Feature importance
feature_importance = pd.DataFrame({
'feature': self.feature_names,
'importance': self.model.feature_importances_
}).sort_values('importance', ascending=True)
axes[1, 1].barh(feature_importance['feature'], feature_importance['importance'],
color='#f38181', alpha=0.8)
axes[1, 1].set_xlabel('Importance')
axes[1, 1].set_title('Feature Importance', fontsize=12, fontweight='bold')
axes[1, 1].grid(axis='x', alpha=0.3)
plt.tight_layout()
plt.savefig('virtual_metrology_results.png', dpi=300, bbox_inches='tight')
plt.show()
# Execution example
print("=" * 60)
print("Virtual Metrology System (Etching Process)")
print("=" * 60)
# Initialize VM system
vm = VirtualMetrologySystem()
# Generate process data
df_process = vm.generate_process_data(n_samples=500)
print(f"\nGenerated data count: {len(df_process)}")
print(f"CD range: {df_process['cd_measured'].min():.2f} - {df_process['cd_measured'].max():.2f} nm")
# Define features
feature_cols = ['rf_power', 'pressure', 'gas_flow_ar', 'gas_flow_cf4',
'temp_chamber', 'etch_time']
# Train VM model
results = vm.train_vm_model(df_process, feature_cols)
print(f"\nVM Model Performance:")
print(f"Training data R² = {results['r2_train']:.4f}")
print(f"Test data R² = {results['r2_test']:.4f}")
print(f"RMSE = {results['rmse_test']:.2f} nm")
print(f"MAE = {results['mae_test']:.2f} nm")
# New wafer prediction example
print(f"\nNew Wafer CD Prediction:")
new_wafer = np.array([[1020, 52, 205, 48, 61, 118]]) # New process conditions
predicted_cd = vm.predict_cd(new_wafer)
print(f"Predicted CD: {predicted_cd[0]:.2f} nm")
# Visualization
vm.plot_vm_results(results)
Implementation Points:
In this chapter, we learned about statistical control of wafer processes.