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.

@@ -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
- for line in f:
118
- line = line.strip()
119
-
120
- if line.startswith("SampleSize"):
121
- match = re.search(r"SampleSize\s+1\s+(\d+)", line)
122
- if match:
123
- data["SampleSize"] = int(match.group(1))
124
-
125
- elif line.startswith("DiePitch"):
126
- match = re.search(r"DiePitch\s+([0-9.]+)\s+([0-9.]+);", line)
127
- if match:
128
- data["DiePitch"]["X"] = float(match.group(1))
129
- data["DiePitch"]["Y"] = float(match.group(2))
130
-
131
- elif line.startswith("DieOrigin"):
132
- match = re.search(r"DieOrigin\s+([0-9.]+)\s+([0-9.]+);", line)
133
- if match:
134
- data["DieOrigin"]["X"] = float(match.group(1))
135
- data["DieOrigin"]["Y"] = float(match.group(2))
136
-
137
- elif line.startswith("SampleCenterLocation"):
138
- match = re.search(r"SampleCenterLocation\s+([0-9.]+)\s+([0-9.]+);", line)
139
- if match:
140
- data["SampleCenterLocation"]["X"] = float(match.group(1))
141
- data["SampleCenterLocation"]["Y"] = float(match.group(2))
142
-
143
- elif line.startswith("DefectList"):
144
- dans_defect_list = True
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
- elif dans_defect_list:
148
- if re.match(r"^\d+\s", line):
149
- value = line.split()
150
- if len(value) >= 18:
151
- defect = {f"val{i+1}": float(val) for i, val in enumerate(value[:18])}
152
- data["Defects"].append(defect)
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
- val1 = d["val1"]
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": val1,
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
- self.coordinates = pd.DataFrame(corrected_positions, columns=["X", "Y"])
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
- print(f"CSV file not found: {csv_path}")
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
- folder_path = os.path.join(self.button_frame.folder_var_changed(),
205
- str(self.selected_wafer))
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
- # Find the first .001 file in the selected folder
208
- matching_files = glob.glob(os.path.join(folder_path, '*.001'))
209
-
210
- # Sort the files to ensure consistent ordering
211
- if matching_files:
212
- recipe_path = matching_files[0]
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
- recipe_path = None
215
-
216
- self.coordinates = self.extract_positions(recipe_path)
217
-
218
- tiff_path = os.path.join(folder_path, "data.tif")
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
- if not os.path.isfile(tiff_path):
221
- print(f"TIFF file not found in {folder_path}")
222
- self._reset_display()
223
- return
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
- x_coords = self.coordinates.iloc[:, 0]
304
- y_coords = self.coordinates.iloc[:, 1]
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
- max_val = max(abs(x_coords).max(), abs(y_coords).max())
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
- print("No coordinates available to plot")
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
- print(f"The closest point is: X = {closest_pt['X']}, "
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
- result = self.image_type + (closest_idx * self_number_type)
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
- self.show_image()
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
- print(f"Error loading TIFF file: {e}")
581
+ # Error loading TIFF file
582
+ pass
407
583
  self._reset_display()