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.

@@ -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
- # Vérifier si la ligne suivante a exactement 2 colonnes
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
- val1 = d["val1"]
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": val1,
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
- # Enregistrer le mapping en CSV dans le même dossier que filepath
247
+ # Save mapping to CSV
191
248
  import os
192
249
  file_dir = os.path.dirname(filepath)
193
- csv_path = os.path.join(file_dir, "mapping.csv")
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
- print(f"CSV file not found: {csv_path}")
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
- folder_path = os.path.join(self.button_frame.folder_var_changed(),
222
- 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
223
289
 
224
- # Find the first .001 file in the selected folder
225
- matching_files = glob.glob(os.path.join(folder_path, '*.001'))
226
-
227
- # Sort the files to ensure consistent ordering
228
- if matching_files:
229
- 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)
230
316
  else:
231
- recipe_path = None
232
-
233
- self.coordinates = self.extract_positions(recipe_path)
234
-
235
- 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'))
236
322
 
237
- if not os.path.isfile(tiff_path):
238
- print(f"TIFF file not found in {folder_path}")
239
- self._reset_display()
240
- 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)
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
- x_coords = self.coordinates.iloc[:, 0]
321
- y_coords = self.coordinates.iloc[:, 1]
322
- defect_size = self.coordinates.iloc[:, 2]
323
- print(f"defect_size: {defect_size}")
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
- 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())
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
- print("No coordinates available to plot")
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
- print(f"The closest point is: X = {closest_pt['X']}, "
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
- 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
+
401
551
  self.current_index = result
402
- self.show_image()
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
- print(f"Error loading TIFF file: {e}")
581
+ # Error loading TIFF file
582
+ pass
429
583
  self._reset_display()