Semapp 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Semapp might be problematic. Click here for more details.
- semapp/Layout/create_button.py +163 -51
- semapp/Plot/__init__.py +3 -3
- semapp/Plot/frame_attributes.py +255 -79
- semapp/Processing/processing.py +132 -77
- semapp/__init__.py +10 -0
- semapp/main.py +10 -1
- semapp-1.0.3.dist-info/METADATA +226 -0
- semapp-1.0.3.dist-info/RECORD +19 -0
- {semapp-1.0.1.dist-info → semapp-1.0.3.dist-info}/WHEEL +1 -1
- semapp-1.0.1.dist-info/METADATA +0 -20
- semapp-1.0.1.dist-info/RECORD +0 -19
- {semapp-1.0.1.dist-info → semapp-1.0.3.dist-info}/entry_points.txt +0 -0
- {semapp-1.0.1.dist-info → semapp-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {semapp-1.0.1.dist-info → semapp-1.0.3.dist-info}/top_level.txt +0 -0
semapp/Plot/frame_attributes.py
CHANGED
|
@@ -51,6 +51,7 @@ class PlotFrame(QWidget):
|
|
|
51
51
|
self.canvas_connection_id = None
|
|
52
52
|
self.selected_wafer = None
|
|
53
53
|
self.radius = None
|
|
54
|
+
self.is_complus4t_mode = False # COMPLUS4T mode detected
|
|
54
55
|
|
|
55
56
|
self._setup_frames()
|
|
56
57
|
self._setup_plot()
|
|
@@ -101,8 +102,15 @@ class PlotFrame(QWidget):
|
|
|
101
102
|
open_button.clicked.connect(self.open_tiff)
|
|
102
103
|
self.layout.addWidget(open_button, 1, 5)
|
|
103
104
|
|
|
104
|
-
def extract_positions(self, filepath):
|
|
105
|
-
|
|
105
|
+
def extract_positions(self, filepath, wafer_id=None):
|
|
106
|
+
"""
|
|
107
|
+
Extract defect positions from KLARF file.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
filepath: Path to the KLARF (.001) file
|
|
111
|
+
wafer_id: Specific wafer ID to extract (for COMPLUS4T files with multiple wafers)
|
|
112
|
+
If None, extracts all defects (normal mode)
|
|
113
|
+
"""
|
|
106
114
|
data = {
|
|
107
115
|
"SampleSize": None,
|
|
108
116
|
"DiePitch": {"X": None, "Y": None},
|
|
@@ -112,44 +120,103 @@ class PlotFrame(QWidget):
|
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
dans_defect_list = False
|
|
123
|
+
current_wafer_id = None
|
|
124
|
+
target_wafer_found = False
|
|
125
|
+
reading_target_wafer = False
|
|
115
126
|
|
|
116
127
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
128
|
+
lines = f.readlines()
|
|
129
|
+
|
|
130
|
+
for i, line in enumerate(lines):
|
|
131
|
+
line = line.strip()
|
|
132
|
+
|
|
133
|
+
# Detect WaferID
|
|
134
|
+
if line.startswith("WaferID"):
|
|
135
|
+
match = re.search(r'WaferID\s+"@(\d+)"', line)
|
|
136
|
+
if match:
|
|
137
|
+
current_wafer_id = int(match.group(1))
|
|
138
|
+
|
|
139
|
+
# If looking for a specific wafer
|
|
140
|
+
if wafer_id is not None:
|
|
141
|
+
if current_wafer_id == wafer_id:
|
|
142
|
+
target_wafer_found = True
|
|
143
|
+
reading_target_wafer = True
|
|
144
|
+
# Do NOT reset global data already read (DiePitch, etc.)
|
|
145
|
+
# Only reset Defects
|
|
146
|
+
data["Defects"] = []
|
|
147
|
+
elif target_wafer_found:
|
|
148
|
+
# Already found our wafer and reached another one
|
|
149
|
+
# Stop reading
|
|
150
|
+
break
|
|
151
|
+
else:
|
|
152
|
+
reading_target_wafer = False
|
|
153
|
+
else:
|
|
154
|
+
# Normal mode (no wafer_id specified)
|
|
155
|
+
reading_target_wafer = True
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
# If looking for specific wafer, skip lines until finding the right wafer
|
|
159
|
+
# EXCEPT for DefectList where we filter in the elif block
|
|
160
|
+
if wafer_id is not None and not reading_target_wafer and not line.startswith("DefectList"):
|
|
161
|
+
# Don't skip parameters before first WaferID
|
|
162
|
+
if current_wafer_id is None:
|
|
163
|
+
# Haven't seen WaferID yet, read global parameters
|
|
164
|
+
pass
|
|
165
|
+
else:
|
|
166
|
+
# Seen a WaferID but it's not the right one, skip
|
|
145
167
|
continue
|
|
146
168
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
169
|
+
if line.startswith("SampleSize"):
|
|
170
|
+
match = re.search(r"SampleSize\s+1\s+(\d+)", line)
|
|
171
|
+
if match:
|
|
172
|
+
data["SampleSize"] = int(match.group(1))
|
|
173
|
+
|
|
174
|
+
elif line.startswith("DiePitch"):
|
|
175
|
+
match = re.search(r"DiePitch\s+([0-9.e+-]+)\s+([0-9.e+-]+);", line)
|
|
176
|
+
if match:
|
|
177
|
+
data["DiePitch"]["X"] = float(match.group(1))
|
|
178
|
+
data["DiePitch"]["Y"] = float(match.group(2))
|
|
179
|
+
|
|
180
|
+
elif line.startswith("DieOrigin"):
|
|
181
|
+
match = re.search(r"DieOrigin\s+([0-9.e+-]+)\s+([0-9.e+-]+);", line)
|
|
182
|
+
if match:
|
|
183
|
+
data["DieOrigin"]["X"] = float(match.group(1))
|
|
184
|
+
data["DieOrigin"]["Y"] = float(match.group(2))
|
|
185
|
+
|
|
186
|
+
elif line.startswith("SampleCenterLocation"):
|
|
187
|
+
match = re.search(r"SampleCenterLocation\s+([0-9.e+-]+)\s+([0-9.e+-]+);", line)
|
|
188
|
+
if match:
|
|
189
|
+
data["SampleCenterLocation"]["X"] = float(match.group(1))
|
|
190
|
+
data["SampleCenterLocation"]["Y"] = float(match.group(2))
|
|
191
|
+
|
|
192
|
+
elif line.startswith("DefectList"):
|
|
193
|
+
dans_defect_list = True
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
elif dans_defect_list:
|
|
197
|
+
# If in DefectList, filter by wafer if necessary
|
|
198
|
+
if wafer_id is not None and not reading_target_wafer:
|
|
199
|
+
# In DefectList but not the right wafer, skip
|
|
200
|
+
if line.startswith("EndOfFile") or line.startswith("}"):
|
|
201
|
+
dans_defect_list = False
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
if re.match(r"^\d+\s", line):
|
|
205
|
+
value = line.split()
|
|
206
|
+
if len(value) >= 12:
|
|
207
|
+
# Check if next line has exactly 2 columns
|
|
208
|
+
if i + 1 < len(lines):
|
|
209
|
+
next_line = lines[i + 1].strip()
|
|
210
|
+
next_values = next_line.split()
|
|
211
|
+
if len(next_values) == 2:
|
|
212
|
+
# The real DEFECTID is the first element of next_values
|
|
213
|
+
real_defect_id = int(next_values[0])
|
|
214
|
+
defect = {f"val{i+1}": float(val) for i, val in enumerate(value[:10])}
|
|
215
|
+
defect["defect_id"] = real_defect_id
|
|
216
|
+
data["Defects"].append(defect)
|
|
217
|
+
elif line.startswith("EndOfFile") or line.startswith("}"):
|
|
218
|
+
# End of DefectList
|
|
219
|
+
dans_defect_list = False
|
|
153
220
|
|
|
154
221
|
pitch_x = data["DiePitch"]["X"]
|
|
155
222
|
pitch_y = data["DiePitch"]["Y"]
|
|
@@ -157,25 +224,41 @@ class PlotFrame(QWidget):
|
|
|
157
224
|
Ycenter = data["SampleCenterLocation"]["Y"]
|
|
158
225
|
|
|
159
226
|
corrected_positions = []
|
|
160
|
-
|
|
161
227
|
for d in data["Defects"]:
|
|
162
|
-
|
|
228
|
+
real_defect_id = d["defect_id"] # Real DEFECTID (63, 64, 261, 262...)
|
|
163
229
|
val2 = d["val2"]
|
|
164
230
|
val3 = d["val3"]
|
|
165
231
|
val4_scaled = d["val4"] * pitch_x - Xcenter
|
|
166
232
|
val5_scaled = d["val5"] * pitch_y - Ycenter
|
|
233
|
+
defect_size = d["val9"]
|
|
167
234
|
|
|
168
235
|
x_corr = round((val2 + val4_scaled) / 10000, 1)
|
|
169
236
|
y_corr = round((val3 + val5_scaled) / 10000, 1)
|
|
170
237
|
|
|
171
238
|
corrected_positions.append({
|
|
172
|
-
"defect_id":
|
|
239
|
+
"defect_id": real_defect_id,
|
|
173
240
|
"X": x_corr,
|
|
174
|
-
"Y": y_corr
|
|
241
|
+
"Y": y_corr,
|
|
242
|
+
"defect_size": defect_size
|
|
175
243
|
})
|
|
176
244
|
|
|
177
|
-
|
|
178
|
-
|
|
245
|
+
self.coordinates = pd.DataFrame(corrected_positions, columns=["defect_id", "X", "Y", "defect_size"])
|
|
246
|
+
|
|
247
|
+
# Save mapping to CSV
|
|
248
|
+
import os
|
|
249
|
+
file_dir = os.path.dirname(filepath)
|
|
250
|
+
|
|
251
|
+
# If wafer_id is specified (COMPLUS4T mode), save to wafer subfolder
|
|
252
|
+
if wafer_id is not None:
|
|
253
|
+
# COMPLUS4T mode: save to dirname/wafer_id/mapping.csv
|
|
254
|
+
csv_folder = os.path.join(file_dir, str(wafer_id))
|
|
255
|
+
os.makedirs(csv_folder, exist_ok=True)
|
|
256
|
+
csv_path = os.path.join(csv_folder, "mapping.csv")
|
|
257
|
+
else:
|
|
258
|
+
# Normal mode: save in same folder as .001 file
|
|
259
|
+
csv_path = os.path.join(file_dir, "mapping.csv")
|
|
260
|
+
|
|
261
|
+
self.coordinates.to_csv(csv_path, index=False)
|
|
179
262
|
|
|
180
263
|
return self.coordinates
|
|
181
264
|
|
|
@@ -187,43 +270,76 @@ class PlotFrame(QWidget):
|
|
|
187
270
|
"""
|
|
188
271
|
if os.path.exists(csv_path):
|
|
189
272
|
self.coordinates = pd.read_csv(csv_path)
|
|
190
|
-
print(f"Coordinates loaded: {self.coordinates.head()}")
|
|
191
273
|
else:
|
|
192
|
-
|
|
274
|
+
# CSV not found, will need to extract from KLARF file
|
|
275
|
+
pass
|
|
193
276
|
|
|
194
277
|
def open_tiff(self):
|
|
195
278
|
"""Handle TIFF file opening and display."""
|
|
196
279
|
self.selected_wafer = self.button_frame.get_selected_option()
|
|
197
280
|
|
|
198
281
|
if not all([self.selected_wafer]):
|
|
199
|
-
print("Recipe and wafer selection required")
|
|
200
282
|
self._reset_display()
|
|
201
283
|
return
|
|
202
284
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
285
|
+
# Check if COMPLUS4T mode is active
|
|
286
|
+
dirname = self.button_frame.folder_var_changed()
|
|
287
|
+
is_complus4t = self._check_complus4t_mode(dirname)
|
|
288
|
+
self.is_complus4t_mode = is_complus4t # Store mode for later use
|
|
206
289
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
290
|
+
if is_complus4t:
|
|
291
|
+
# COMPLUS4T mode: .001 and .tiff files in parent directory
|
|
292
|
+
folder_path = dirname
|
|
293
|
+
|
|
294
|
+
# Find the .001 file with the selected wafer ID in parent directory
|
|
295
|
+
matching_files = glob.glob(os.path.join(dirname, '*.001'))
|
|
296
|
+
recipe_path = None
|
|
297
|
+
|
|
298
|
+
for file_path in matching_files:
|
|
299
|
+
if self._is_wafer_in_klarf(file_path, self.selected_wafer):
|
|
300
|
+
recipe_path = file_path
|
|
301
|
+
break
|
|
302
|
+
|
|
303
|
+
# Find the only .tiff file in the parent directory
|
|
304
|
+
tiff_files = glob.glob(os.path.join(dirname, '*.tiff'))
|
|
305
|
+
if not tiff_files:
|
|
306
|
+
tiff_files = glob.glob(os.path.join(dirname, '*.tif'))
|
|
307
|
+
|
|
308
|
+
if tiff_files:
|
|
309
|
+
tiff_path = tiff_files[0]
|
|
310
|
+
else:
|
|
311
|
+
self._reset_display()
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
# Extract positions for the specific wafer
|
|
315
|
+
self.coordinates = self.extract_positions(recipe_path, wafer_id=self.selected_wafer)
|
|
213
316
|
else:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
317
|
+
# Normal mode: subfolders
|
|
318
|
+
folder_path = os.path.join(dirname, str(self.selected_wafer))
|
|
319
|
+
|
|
320
|
+
# Find the first .001 file in the selected folder
|
|
321
|
+
matching_files = glob.glob(os.path.join(folder_path, '*.001'))
|
|
219
322
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
323
|
+
# Sort the files to ensure consistent ordering
|
|
324
|
+
if matching_files:
|
|
325
|
+
recipe_path = matching_files[0]
|
|
326
|
+
else:
|
|
327
|
+
recipe_path = None
|
|
328
|
+
|
|
329
|
+
tiff_path = os.path.join(folder_path, "data.tif")
|
|
330
|
+
|
|
331
|
+
if not os.path.isfile(tiff_path):
|
|
332
|
+
self._reset_display()
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
# Extract all positions (normal mode)
|
|
336
|
+
self.coordinates = self.extract_positions(recipe_path)
|
|
224
337
|
|
|
225
338
|
self._load_tiff(tiff_path)
|
|
226
|
-
self._update_plot()
|
|
339
|
+
self._update_plot()
|
|
340
|
+
|
|
341
|
+
# Set reference to plot_frame in button_frame for slider updates
|
|
342
|
+
self.button_frame.plot_frame = self
|
|
227
343
|
|
|
228
344
|
msg = QMessageBox()
|
|
229
345
|
msg.setIcon(QMessageBox.Information)
|
|
@@ -232,6 +348,34 @@ class PlotFrame(QWidget):
|
|
|
232
348
|
msg.setStyleSheet(MESSAGE_BOX_STYLE)
|
|
233
349
|
msg.exec_()
|
|
234
350
|
|
|
351
|
+
def _check_complus4t_mode(self, dirname):
|
|
352
|
+
"""Check if we are in COMPLUS4T mode (.001 files with COMPLUS4T in parent directory)."""
|
|
353
|
+
if not dirname or not os.path.exists(dirname):
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
# Check for .001 files with COMPLUS4T in the parent directory
|
|
357
|
+
matching_files = glob.glob(os.path.join(dirname, '*.001'))
|
|
358
|
+
for file_path in matching_files:
|
|
359
|
+
try:
|
|
360
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
361
|
+
content = f.read()
|
|
362
|
+
if 'COMPLUS4T' in content:
|
|
363
|
+
return True
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
def _is_wafer_in_klarf(self, file_path, wafer_id):
|
|
370
|
+
"""Check if a specific wafer ID is in the KLARF file."""
|
|
371
|
+
try:
|
|
372
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
373
|
+
content = f.read()
|
|
374
|
+
pattern = r'WaferID\s+"@' + str(wafer_id) + r'"'
|
|
375
|
+
return re.search(pattern, content) is not None
|
|
376
|
+
except Exception:
|
|
377
|
+
return False
|
|
378
|
+
|
|
235
379
|
def _reset_display(self):
|
|
236
380
|
"""
|
|
237
381
|
Resets the display by clearing the figure and reinitializing the subplot.
|
|
@@ -300,11 +444,32 @@ class PlotFrame(QWidget):
|
|
|
300
444
|
ax.set_ylabel('Y (cm)', fontsize=20)
|
|
301
445
|
|
|
302
446
|
if self.coordinates is not None:
|
|
303
|
-
|
|
304
|
-
|
|
447
|
+
# Get all coordinates
|
|
448
|
+
x_coords = self.coordinates['X']
|
|
449
|
+
y_coords = self.coordinates['Y']
|
|
450
|
+
defect_size = self.coordinates['defect_size']
|
|
451
|
+
|
|
452
|
+
# Determine color based on mode
|
|
453
|
+
if self.is_complus4t_mode:
|
|
454
|
+
# Mode COMPLUS4T: color based on slider threshold
|
|
455
|
+
threshold = 0.0 # Default threshold
|
|
456
|
+
result = self.button_frame.get_selected_image()
|
|
457
|
+
if result is not None:
|
|
458
|
+
threshold = result[0] # Slider value in nm
|
|
459
|
+
|
|
460
|
+
# Red if size >= threshold, blue otherwise
|
|
461
|
+
colors = ['red' if size >= threshold else 'blue' for size in defect_size]
|
|
462
|
+
else:
|
|
463
|
+
# Normal mode: color based on fixed threshold (10 nm)
|
|
464
|
+
colors = ['red' if size > 1.0e+01 else 'blue' for size in defect_size]
|
|
465
|
+
|
|
466
|
+
ax.scatter(x_coords, y_coords, color=colors, marker='o',
|
|
467
|
+
s=100, label='Positions')
|
|
305
468
|
|
|
306
|
-
# Calculate the maximum value for scaling
|
|
307
|
-
|
|
469
|
+
# Calculate the maximum value for scaling using ALL coordinates
|
|
470
|
+
x_coords_all = self.coordinates['X']
|
|
471
|
+
y_coords_all = self.coordinates['Y']
|
|
472
|
+
max_val = max(abs(x_coords_all).max(), abs(y_coords_all).max())
|
|
308
473
|
|
|
309
474
|
if max_val <= 5:
|
|
310
475
|
radius = 5
|
|
@@ -318,9 +483,6 @@ class PlotFrame(QWidget):
|
|
|
318
483
|
radius = max_val # fallback for > 15
|
|
319
484
|
|
|
320
485
|
self.radius = radius
|
|
321
|
-
|
|
322
|
-
ax.scatter(x_coords, y_coords, color='blue', marker='o',
|
|
323
|
-
s=100, label='Positions')
|
|
324
486
|
|
|
325
487
|
# Set limits based on the radius
|
|
326
488
|
ax.set_xlim(-radius - 1, radius + 1)
|
|
@@ -330,8 +492,13 @@ class PlotFrame(QWidget):
|
|
|
330
492
|
fill=False, linewidth=0.5)
|
|
331
493
|
ax.add_patch(circle)
|
|
332
494
|
ax.set_aspect('equal')
|
|
495
|
+
|
|
496
|
+
# Add grid
|
|
497
|
+
ax.grid(True, alpha=0.3, linestyle='-', linewidth=0.5)
|
|
498
|
+
ax.set_axisbelow(True)
|
|
333
499
|
else:
|
|
334
|
-
|
|
500
|
+
# No coordinates available
|
|
501
|
+
pass
|
|
335
502
|
|
|
336
503
|
|
|
337
504
|
ax.figure.subplots_adjust(left=0.15, right=0.95, top=0.90, bottom=0.1)
|
|
@@ -348,7 +515,6 @@ class PlotFrame(QWidget):
|
|
|
348
515
|
if result is not None:
|
|
349
516
|
self.image_type, self_number_type = result
|
|
350
517
|
else:
|
|
351
|
-
print("No image selected.")
|
|
352
518
|
return
|
|
353
519
|
|
|
354
520
|
if event.inaxes:
|
|
@@ -361,9 +527,7 @@ class PlotFrame(QWidget):
|
|
|
361
527
|
(self.coordinates['Y'] - y_pos) ** 2)
|
|
362
528
|
closest_idx = distances.idxmin()
|
|
363
529
|
closest_pt = self.coordinates.iloc[closest_idx]
|
|
364
|
-
|
|
365
|
-
f"Y = {closest_pt['Y']}")
|
|
366
|
-
|
|
530
|
+
|
|
367
531
|
# Replot with a red circle around the selected point
|
|
368
532
|
self.ax.clear() # Clear the existing plot
|
|
369
533
|
self.plot_mapping_tpl(self.ax)
|
|
@@ -375,9 +539,20 @@ class PlotFrame(QWidget):
|
|
|
375
539
|
self.canvas.draw()
|
|
376
540
|
|
|
377
541
|
# Update the image based on the selected point
|
|
378
|
-
|
|
542
|
+
if self.is_complus4t_mode:
|
|
543
|
+
# COMPLUS4T mode: use DEFECTID from KLARF file
|
|
544
|
+
defect_id = int(closest_pt['defect_id'])
|
|
545
|
+
# DEFECTID starts at 1, but Python indices start at 0
|
|
546
|
+
result = defect_id - 1
|
|
547
|
+
else:
|
|
548
|
+
# Normal mode: use DataFrame index (original behavior)
|
|
549
|
+
result = self.image_type + (closest_idx * self_number_type)
|
|
550
|
+
|
|
379
551
|
self.current_index = result
|
|
380
|
-
|
|
552
|
+
|
|
553
|
+
# Check if index is valid
|
|
554
|
+
if 0 <= self.current_index < len(self.image_list):
|
|
555
|
+
self.show_image()
|
|
381
556
|
|
|
382
557
|
def _load_tiff(self, tiff_path):
|
|
383
558
|
"""Load and prepare TIFF images for display.
|
|
@@ -403,5 +578,6 @@ class PlotFrame(QWidget):
|
|
|
403
578
|
self.show_image() # Display first image
|
|
404
579
|
|
|
405
580
|
except Exception as e:
|
|
406
|
-
|
|
581
|
+
# Error loading TIFF file
|
|
582
|
+
pass
|
|
407
583
|
self._reset_display()
|