221 lines
7.9 KiB
Python
221 lines
7.9 KiB
Python
import numpy as np
|
|
import os
|
|
|
|
def get_unique_chars(file_path):
|
|
"""
|
|
Reads a wafer map file and returns all unique characters found,
|
|
excluding whitespace.
|
|
"""
|
|
unique_chars = set()
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as file:
|
|
lines = file.readlines()
|
|
# Filter out empty lines before calculating max_length
|
|
non_empty_lines = [line.strip() for line in lines if line.strip()]
|
|
if not non_empty_lines:
|
|
return []
|
|
|
|
max_length = max(len(line) for line in non_empty_lines)
|
|
|
|
for line in non_empty_lines:
|
|
# Process only lines that have the max length
|
|
if len(line) == max_length:
|
|
for char in line:
|
|
if char != ' ':
|
|
unique_chars.add(char)
|
|
return sorted(list(unique_chars))
|
|
except Exception as e:
|
|
print(f"Error reading unique chars: {e}")
|
|
return []
|
|
|
|
def read_wafer_map(file_path, char_to_bin_mapping):
|
|
"""
|
|
Reads a wafer map file and converts it to a numerical numpy array based on
|
|
the provided character-to-bin mapping.
|
|
"""
|
|
wafer_map = []
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as file:
|
|
lines = file.readlines()
|
|
non_empty_lines = [line.strip() for line in lines if line.strip()]
|
|
if not non_empty_lines:
|
|
return np.array([])
|
|
|
|
max_length = max(len(line) for line in non_empty_lines)
|
|
|
|
for line in non_empty_lines:
|
|
# Pad the line to ensure uniform length for all processed rows
|
|
padded_line = line.ljust(max_length, ' ')
|
|
row = [char_to_bin_mapping.get(char, -1) for char in padded_line]
|
|
wafer_map.append(row)
|
|
|
|
return np.array(wafer_map)
|
|
except Exception as e:
|
|
print(f"Error reading wafer map: {e}")
|
|
return np.array([])
|
|
|
|
def save_wafer_map(wafer_map, bin_to_char_mapping, file_path):
|
|
"""
|
|
Saves the numerical wafer map back to a character-based file.
|
|
"""
|
|
with open(file_path, 'w', encoding='utf-8') as file:
|
|
for row in wafer_map:
|
|
file.write(''.join([bin_to_char_mapping.get(int(bin_code), ' ') for bin_code in row]) + '\n')
|
|
|
|
def get_bin_counts(wafer_map):
|
|
"""
|
|
Calculates the count of each bin type in the wafer map.
|
|
"""
|
|
unique, counts = np.unique(wafer_map, return_counts=True)
|
|
return dict(zip(unique, counts))
|
|
|
|
def inset_wafer_map(wafer_map, layers, from_bin_value, to_bin_value):
|
|
"""
|
|
Finds the outer N layers of the main die area, defined by proximity to
|
|
'ignore' bins, and changes them from 'from_bin_value' to 'to_bin_value'.
|
|
"""
|
|
if layers <= 0:
|
|
return wafer_map
|
|
|
|
rows, cols = wafer_map.shape
|
|
|
|
# Mask for all dies that are candidates for changing.
|
|
target_mask = (wafer_map == from_bin_value)
|
|
|
|
# Mask for dies that are NOT part of the wafer (e.g., ignore bins)
|
|
background_mask = (wafer_map == -1)
|
|
|
|
# This will hold all dies that have been identified for changing.
|
|
final_change_mask = np.zeros_like(wafer_map, dtype=bool)
|
|
|
|
# This mask represents the "peeling front". Initially, it's the background.
|
|
peel_front_mask = np.copy(background_mask)
|
|
|
|
for _ in range(layers):
|
|
# Find dies in our target_mask that are adjacent to the current peel_front_mask
|
|
newly_found_edge_mask = np.zeros_like(wafer_map, dtype=bool)
|
|
|
|
# Iterate only over the candidates to be more efficient
|
|
for r, c in np.argwhere(target_mask):
|
|
# Check neighbors
|
|
for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
|
nr, nc = r + dr, c + dc
|
|
|
|
# If a neighbor is part of the current "peel front"
|
|
if (0 <= nr < rows and 0 <= nc < cols and peel_front_mask[nr, nc]):
|
|
newly_found_edge_mask[r, c] = True
|
|
break # Found it's an edge, no need to check other neighbors
|
|
|
|
# If we didn't find any new edge dies, we're done.
|
|
if not np.any(newly_found_edge_mask):
|
|
break
|
|
|
|
# Add the newly found layer to our final mask of dies to change.
|
|
final_change_mask |= newly_found_edge_mask
|
|
|
|
# The layer we just found becomes the new "peel front" for the next iteration.
|
|
peel_front_mask |= newly_found_edge_mask
|
|
|
|
# Remove the dies we just processed from the pool of candidates.
|
|
target_mask &= ~newly_found_edge_mask
|
|
|
|
# Apply the change to a copy of the original map
|
|
wafer_map_copy = np.copy(wafer_map)
|
|
wafer_map_copy[final_change_mask] = to_bin_value
|
|
|
|
return wafer_map_copy
|
|
|
|
def compare_wafer_maps(maps, references):
|
|
"""
|
|
Compares multiple wafer maps by aligning them based on reference points.
|
|
"""
|
|
if not maps or len(maps) != len(references):
|
|
return np.array([]), {}
|
|
|
|
# Determine the bounds of the combined map
|
|
max_up = 0
|
|
max_down = 0
|
|
max_left = 0
|
|
max_right = 0
|
|
|
|
for i, (map_arr, ref) in enumerate(zip(maps, references)):
|
|
ref_r, ref_c = ref
|
|
rows, cols = map_arr.shape
|
|
|
|
max_up = max(max_up, ref_r)
|
|
max_down = max(max_down, rows - ref_r - 1)
|
|
max_left = max(max_left, ref_c)
|
|
max_right = max(max_right, cols - ref_c - 1)
|
|
|
|
# Total dimensions of the comparison grid
|
|
total_rows = max_up + max_down + 1
|
|
total_cols = max_left + max_right + 1
|
|
|
|
# A grid where each cell holds a list of bin codes from all maps at that position
|
|
# Initialize with a special value for "no map data"
|
|
NO_DATA = -10
|
|
comparison_grid = [[[NO_DATA] * len(maps) for _ in range(total_cols)] for _ in range(total_rows)]
|
|
|
|
# Place each map onto the comparison grid
|
|
for i, (map_arr, ref) in enumerate(zip(maps, references)):
|
|
ref_r, ref_c = ref
|
|
rows, cols = map_arr.shape
|
|
|
|
offset_r = max_up - ref_r
|
|
offset_c = max_left - ref_c
|
|
|
|
for r in range(rows):
|
|
for c in range(cols):
|
|
# Only consider non-ignore bins
|
|
if map_arr[r, c] != -1:
|
|
comparison_grid[offset_r + r][offset_c + c][i] = map_arr[r, c]
|
|
|
|
# --- Process the grid to create a final result map ---
|
|
# Result codes:
|
|
# -1: No data from any map (Ignore)
|
|
# 0: Match (Good)
|
|
# 1: Match (NG)
|
|
# 2: Match (Dummy)
|
|
# 3: Mismatch (mix of Good, NG, Dummy)
|
|
# 4: Partial data (some maps have data, others don't)
|
|
|
|
result_map = np.full((total_rows, total_cols), -1, dtype=int)
|
|
|
|
for r in range(total_rows):
|
|
for c in range(total_cols):
|
|
bins = [b for b in comparison_grid[r][c] if b != NO_DATA]
|
|
|
|
if not bins: # No map has data here
|
|
result_map[r, c] = -1
|
|
continue
|
|
|
|
if len(bins) < len(maps): # Some maps have data, others don't
|
|
result_map[r, c] = 4
|
|
continue
|
|
|
|
# All maps have data, check for match or mismatch
|
|
first_bin = bins[0]
|
|
is_match = all(b == first_bin for b in bins)
|
|
|
|
if is_match:
|
|
if first_bin == 2: # Good
|
|
result_map[r, c] = 0
|
|
elif first_bin == 1: # NG
|
|
result_map[r, c] = 1
|
|
elif first_bin == 0: # Dummy
|
|
result_map[r, c] = 2
|
|
else: # Mismatch
|
|
result_map[r, c] = 3
|
|
|
|
legend = {
|
|
-1: {"label": "No Data", "color": "#3e3e42"},
|
|
0: {"label": "Match (Good)", "color": "#28a745"},
|
|
1: {"label": "Match (NG)", "color": "#4a90e2"}, # Different color for match NG
|
|
2: {"label": "Match (Dummy)", "color": "#7f8c8d"},
|
|
3: {"label": "Mismatch", "color": "#dc3545"},
|
|
4: {"label": "Partial Data", "color": "#f39c12"}
|
|
}
|
|
|
|
# Also return the detailed grid for tooltip purposes
|
|
return result_map, legend, comparison_grid
|