Semapp 1.0.2__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 +162 -50
- semapp/Plot/__init__.py +3 -3
- semapp/Plot/frame_attributes.py +208 -54
- semapp/Processing/processing.py +85 -87
- 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.2.dist-info/METADATA +0 -20
- semapp-1.0.2.dist-info/RECORD +0 -19
- {semapp-1.0.2.dist-info → semapp-1.0.3.dist-info}/WHEEL +0 -0
- {semapp-1.0.2.dist-info → semapp-1.0.3.dist-info}/entry_points.txt +0 -0
- {semapp-1.0.2.dist-info → semapp-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {semapp-1.0.2.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,6 +120,9 @@ 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
128
|
lines = f.readlines()
|
|
@@ -119,6 +130,42 @@ class PlotFrame(QWidget):
|
|
|
119
130
|
for i, line in enumerate(lines):
|
|
120
131
|
line = line.strip()
|
|
121
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
|
|
167
|
+
continue
|
|
168
|
+
|
|
122
169
|
if line.startswith("SampleSize"):
|
|
123
170
|
match = re.search(r"SampleSize\s+1\s+(\d+)", line)
|
|
124
171
|
if match:
|
|
@@ -147,16 +194,29 @@ class PlotFrame(QWidget):
|
|
|
147
194
|
continue
|
|
148
195
|
|
|
149
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
|
+
|
|
150
204
|
if re.match(r"^\d+\s", line):
|
|
151
205
|
value = line.split()
|
|
152
206
|
if len(value) >= 12:
|
|
153
|
-
#
|
|
207
|
+
# Check if next line has exactly 2 columns
|
|
154
208
|
if i + 1 < len(lines):
|
|
155
209
|
next_line = lines[i + 1].strip()
|
|
156
210
|
next_values = next_line.split()
|
|
157
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])
|
|
158
214
|
defect = {f"val{i+1}": float(val) for i, val in enumerate(value[:10])}
|
|
215
|
+
defect["defect_id"] = real_defect_id
|
|
159
216
|
data["Defects"].append(defect)
|
|
217
|
+
elif line.startswith("EndOfFile") or line.startswith("}"):
|
|
218
|
+
# End of DefectList
|
|
219
|
+
dans_defect_list = False
|
|
160
220
|
|
|
161
221
|
pitch_x = data["DiePitch"]["X"]
|
|
162
222
|
pitch_y = data["DiePitch"]["Y"]
|
|
@@ -164,9 +224,8 @@ class PlotFrame(QWidget):
|
|
|
164
224
|
Ycenter = data["SampleCenterLocation"]["Y"]
|
|
165
225
|
|
|
166
226
|
corrected_positions = []
|
|
167
|
-
|
|
168
227
|
for d in data["Defects"]:
|
|
169
|
-
|
|
228
|
+
real_defect_id = d["defect_id"] # Real DEFECTID (63, 64, 261, 262...)
|
|
170
229
|
val2 = d["val2"]
|
|
171
230
|
val3 = d["val3"]
|
|
172
231
|
val4_scaled = d["val4"] * pitch_x - Xcenter
|
|
@@ -177,22 +236,29 @@ class PlotFrame(QWidget):
|
|
|
177
236
|
y_corr = round((val3 + val5_scaled) / 10000, 1)
|
|
178
237
|
|
|
179
238
|
corrected_positions.append({
|
|
180
|
-
"defect_id":
|
|
239
|
+
"defect_id": real_defect_id,
|
|
181
240
|
"X": x_corr,
|
|
182
241
|
"Y": y_corr,
|
|
183
242
|
"defect_size": defect_size
|
|
184
|
-
|
|
185
243
|
})
|
|
186
244
|
|
|
187
|
-
|
|
188
|
-
self.coordinates = pd.DataFrame(corrected_positions, columns=["X", "Y", "defect_size"])
|
|
245
|
+
self.coordinates = pd.DataFrame(corrected_positions, columns=["defect_id", "X", "Y", "defect_size"])
|
|
189
246
|
|
|
190
|
-
#
|
|
247
|
+
# Save mapping to CSV
|
|
191
248
|
import os
|
|
192
249
|
file_dir = os.path.dirname(filepath)
|
|
193
|
-
|
|
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
|
+
|
|
194
261
|
self.coordinates.to_csv(csv_path, index=False)
|
|
195
|
-
print(f"Mapping saved to: {csv_path}")
|
|
196
262
|
|
|
197
263
|
return self.coordinates
|
|
198
264
|
|
|
@@ -204,43 +270,76 @@ class PlotFrame(QWidget):
|
|
|
204
270
|
"""
|
|
205
271
|
if os.path.exists(csv_path):
|
|
206
272
|
self.coordinates = pd.read_csv(csv_path)
|
|
207
|
-
print(f"Coordinates loaded: {self.coordinates.head()}")
|
|
208
273
|
else:
|
|
209
|
-
|
|
274
|
+
# CSV not found, will need to extract from KLARF file
|
|
275
|
+
pass
|
|
210
276
|
|
|
211
277
|
def open_tiff(self):
|
|
212
278
|
"""Handle TIFF file opening and display."""
|
|
213
279
|
self.selected_wafer = self.button_frame.get_selected_option()
|
|
214
280
|
|
|
215
281
|
if not all([self.selected_wafer]):
|
|
216
|
-
print("Recipe and wafer selection required")
|
|
217
282
|
self._reset_display()
|
|
218
283
|
return
|
|
219
284
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
223
289
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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)
|
|
230
316
|
else:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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'))
|
|
236
322
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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)
|
|
241
337
|
|
|
242
338
|
self._load_tiff(tiff_path)
|
|
243
|
-
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
|
|
244
343
|
|
|
245
344
|
msg = QMessageBox()
|
|
246
345
|
msg.setIcon(QMessageBox.Information)
|
|
@@ -249,6 +348,34 @@ class PlotFrame(QWidget):
|
|
|
249
348
|
msg.setStyleSheet(MESSAGE_BOX_STYLE)
|
|
250
349
|
msg.exec_()
|
|
251
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
|
+
|
|
252
379
|
def _reset_display(self):
|
|
253
380
|
"""
|
|
254
381
|
Resets the display by clearing the figure and reinitializing the subplot.
|
|
@@ -317,13 +444,32 @@ class PlotFrame(QWidget):
|
|
|
317
444
|
ax.set_ylabel('Y (cm)', fontsize=20)
|
|
318
445
|
|
|
319
446
|
if self.coordinates is not None:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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')
|
|
324
468
|
|
|
325
|
-
# Calculate the maximum value for scaling
|
|
326
|
-
|
|
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())
|
|
327
473
|
|
|
328
474
|
if max_val <= 5:
|
|
329
475
|
radius = 5
|
|
@@ -337,12 +483,6 @@ class PlotFrame(QWidget):
|
|
|
337
483
|
radius = max_val # fallback for > 15
|
|
338
484
|
|
|
339
485
|
self.radius = radius
|
|
340
|
-
|
|
341
|
-
# Create color array based on defect_size (values are in scientific notation)
|
|
342
|
-
colors = ['red' if size > 1.0e+01 else 'blue' for size in defect_size]
|
|
343
|
-
|
|
344
|
-
ax.scatter(x_coords, y_coords, color=colors, marker='o',
|
|
345
|
-
s=100, label='Positions')
|
|
346
486
|
|
|
347
487
|
# Set limits based on the radius
|
|
348
488
|
ax.set_xlim(-radius - 1, radius + 1)
|
|
@@ -352,8 +492,13 @@ class PlotFrame(QWidget):
|
|
|
352
492
|
fill=False, linewidth=0.5)
|
|
353
493
|
ax.add_patch(circle)
|
|
354
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)
|
|
355
499
|
else:
|
|
356
|
-
|
|
500
|
+
# No coordinates available
|
|
501
|
+
pass
|
|
357
502
|
|
|
358
503
|
|
|
359
504
|
ax.figure.subplots_adjust(left=0.15, right=0.95, top=0.90, bottom=0.1)
|
|
@@ -370,7 +515,6 @@ class PlotFrame(QWidget):
|
|
|
370
515
|
if result is not None:
|
|
371
516
|
self.image_type, self_number_type = result
|
|
372
517
|
else:
|
|
373
|
-
print("No image selected.")
|
|
374
518
|
return
|
|
375
519
|
|
|
376
520
|
if event.inaxes:
|
|
@@ -383,9 +527,7 @@ class PlotFrame(QWidget):
|
|
|
383
527
|
(self.coordinates['Y'] - y_pos) ** 2)
|
|
384
528
|
closest_idx = distances.idxmin()
|
|
385
529
|
closest_pt = self.coordinates.iloc[closest_idx]
|
|
386
|
-
|
|
387
|
-
f"Y = {closest_pt['Y']}")
|
|
388
|
-
|
|
530
|
+
|
|
389
531
|
# Replot with a red circle around the selected point
|
|
390
532
|
self.ax.clear() # Clear the existing plot
|
|
391
533
|
self.plot_mapping_tpl(self.ax)
|
|
@@ -397,9 +539,20 @@ class PlotFrame(QWidget):
|
|
|
397
539
|
self.canvas.draw()
|
|
398
540
|
|
|
399
541
|
# Update the image based on the selected point
|
|
400
|
-
|
|
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
|
+
|
|
401
551
|
self.current_index = result
|
|
402
|
-
|
|
552
|
+
|
|
553
|
+
# Check if index is valid
|
|
554
|
+
if 0 <= self.current_index < len(self.image_list):
|
|
555
|
+
self.show_image()
|
|
403
556
|
|
|
404
557
|
def _load_tiff(self, tiff_path):
|
|
405
558
|
"""Load and prepare TIFF images for display.
|
|
@@ -425,5 +578,6 @@ class PlotFrame(QWidget):
|
|
|
425
578
|
self.show_image() # Display first image
|
|
426
579
|
|
|
427
580
|
except Exception as e:
|
|
428
|
-
|
|
581
|
+
# Error loading TIFF file
|
|
582
|
+
pass
|
|
429
583
|
self._reset_display()
|