Semapp 1.0.0__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.

@@ -0,0 +1,408 @@
1
+ """
2
+ A class to manage and display frames in the UI, providing functionality
3
+ for plotting and saving combined screenshots of images and plots.
4
+ """
5
+ import os
6
+ import numpy as np
7
+ import glob
8
+ import re
9
+ import pandas as pd
10
+ from PIL import Image
11
+ from PyQt5.QtWidgets import QFrame, QGroupBox, QWidget, QVBoxLayout, QPushButton, \
12
+ QGridLayout, QLabel, QFileDialog, QProgressDialog, QMessageBox
13
+ from PyQt5.QtGui import QImage, QPixmap
14
+ from PyQt5.QtCore import Qt
15
+ from matplotlib.figure import Figure
16
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
17
+ import matplotlib.pyplot as plt
18
+ from semapp.Plot.utils import create_savebutton
19
+ from semapp.Plot.styles import OPEN_BUTTON_STYLE, MESSAGE_BOX_STYLE, FRAME_STYLE
20
+
21
+ # Constants
22
+ FRAME_SIZE = 600
23
+ CANVAS_SIZE = 600
24
+ radius = 10
25
+
26
+ class PlotFrame(QWidget):
27
+ """
28
+ A class to manage and display frames in the UI,
29
+ allowing plotting and image viewing.
30
+ Provides functionality to open and display TIFF
31
+ images and plot coordinate mappings.
32
+ """
33
+
34
+ def __init__(self, layout, button_frame):
35
+ """
36
+ Initializes the PlotFrame class by setting up the
37
+ UI components and initializing variables.
38
+
39
+ :param layout: The layout to which the frames will be added.
40
+ :param button_frame: The button frame containing
41
+ additional control elements.
42
+ """
43
+ super().__init__()
44
+ self.layout = layout
45
+ self.button_frame = button_frame
46
+
47
+ # Initialize state
48
+ self.coordinates = None
49
+ self.image_list = []
50
+ self.current_index = 0
51
+ self.canvas_connection_id = None
52
+ self.selected_wafer = None
53
+ self.radius = None
54
+
55
+ self._setup_frames()
56
+ self._setup_plot()
57
+ self._setup_controls()
58
+
59
+ def _setup_frames(self):
60
+ """Initialize left and right display frames."""
61
+ # Left frame for images
62
+ self.frame_left = self._create_frame()
63
+ self.frame_left_layout = QVBoxLayout()
64
+ self.frame_left.setLayout(self.frame_left_layout)
65
+
66
+ # Right frame for plots
67
+ self.frame_right = self._create_frame()
68
+ self.frame_right_layout = QGridLayout()
69
+ self.frame_right.setLayout(self.frame_right_layout)
70
+
71
+ # Add frames to main layout
72
+ self.layout.addWidget(self.frame_left, 2, 0, 1, 3)
73
+ self.layout.addWidget(self.frame_right, 2, 3, 1, 3)
74
+
75
+ def _create_frame(self):
76
+ """Create a styled frame with fixed size."""
77
+ frame = QFrame()
78
+ frame.setFrameShape(QFrame.StyledPanel)
79
+ frame.setStyleSheet(FRAME_STYLE)
80
+ frame.setFixedSize(FRAME_SIZE+100, FRAME_SIZE)
81
+ return frame
82
+
83
+ def _setup_plot(self):
84
+ """Initialize matplotlib figure and canvas."""
85
+ self.figure = Figure(figsize=(5, 5))
86
+ self.ax = self.figure.add_subplot(111)
87
+ self.canvas = FigureCanvas(self.figure)
88
+ self.frame_right_layout.addWidget(self.canvas)
89
+
90
+ # Initialize image display
91
+ self.image_label = QLabel(self)
92
+ self.image_label.setAlignment(Qt.AlignCenter)
93
+ self.frame_left_layout.addWidget(self.image_label)
94
+
95
+ def _setup_controls(self):
96
+ """Set up control buttons."""
97
+ create_savebutton(self.layout, self.frame_left, self.frame_right)
98
+
99
+ open_button = QPushButton('Open TIFF', self)
100
+ open_button.setStyleSheet(OPEN_BUTTON_STYLE)
101
+ open_button.clicked.connect(self.open_tiff)
102
+ self.layout.addWidget(open_button, 1, 5)
103
+
104
+ def extract_positions(self, filepath):
105
+
106
+ data = {
107
+ "SampleSize": None,
108
+ "DiePitch": {"X": None, "Y": None},
109
+ "DieOrigin": {"X": None, "Y": None},
110
+ "SampleCenterLocation": {"X": None, "Y": None},
111
+ "Defects": []
112
+ }
113
+
114
+ dans_defect_list = False
115
+
116
+ with open(filepath, "r", encoding="utf-8") as f:
117
+ for ligne in f:
118
+ ligne = ligne.strip()
119
+
120
+ if ligne.startswith("SampleSize"):
121
+ match = re.search(r"SampleSize\s+1\s+(\d+)", ligne)
122
+ if match:
123
+ data["SampleSize"] = int(match.group(1))
124
+
125
+ elif ligne.startswith("DiePitch"):
126
+ match = re.search(r"DiePitch\s+([0-9.]+)\s+([0-9.]+);", ligne)
127
+ if match:
128
+ data["DiePitch"]["X"] = float(match.group(1))
129
+ data["DiePitch"]["Y"] = float(match.group(2))
130
+
131
+ elif ligne.startswith("DieOrigin"):
132
+ match = re.search(r"DieOrigin\s+([0-9.]+)\s+([0-9.]+);", ligne)
133
+ if match:
134
+ data["DieOrigin"]["X"] = float(match.group(1))
135
+ data["DieOrigin"]["Y"] = float(match.group(2))
136
+
137
+ elif ligne.startswith("SampleCenterLocation"):
138
+ match = re.search(r"SampleCenterLocation\s+([0-9.]+)\s+([0-9.]+);", ligne)
139
+ if match:
140
+ data["SampleCenterLocation"]["X"] = float(match.group(1))
141
+ data["SampleCenterLocation"]["Y"] = float(match.group(2))
142
+
143
+ elif ligne.startswith("DefectList"):
144
+ dans_defect_list = True
145
+ continue
146
+
147
+ elif dans_defect_list:
148
+ if re.match(r"^\d+\s", ligne):
149
+ valeurs = ligne.split()
150
+ if len(valeurs) >= 18:
151
+ defect = {f"val{i+1}": float(val) for i, val in enumerate(valeurs[:18])}
152
+ data["Defects"].append(defect)
153
+
154
+ pitch_x = data["DiePitch"]["X"]
155
+ pitch_y = data["DiePitch"]["Y"]
156
+ Xcenter = data["SampleCenterLocation"]["X"]
157
+ Ycenter = data["SampleCenterLocation"]["Y"]
158
+
159
+ corrected_positions = []
160
+
161
+ for d in data["Defects"]:
162
+ val1 = d["val1"]
163
+ val2 = d["val2"]
164
+ val3 = d["val3"]
165
+ val4_scaled = d["val4"] * pitch_x - Xcenter
166
+ val5_scaled = d["val5"] * pitch_y - Ycenter
167
+
168
+ x_corr = round((val2 + val4_scaled) / 10000, 1)
169
+ y_corr = round((val3 + val5_scaled) / 10000, 1)
170
+
171
+ corrected_positions.append({
172
+ "defect_id": val1,
173
+ "X": x_corr,
174
+ "Y": y_corr
175
+ })
176
+
177
+
178
+ self.coordinates = pd.DataFrame(corrected_positions, columns=["X", "Y"])
179
+
180
+ return self.coordinates
181
+
182
+ def load_coordinates(self, csv_path):
183
+ """
184
+ Loads the X/Y coordinates from a CSV file for plotting.
185
+
186
+ :param csv_path: Path to the CSV file containing the coordinates.
187
+ """
188
+ if os.path.exists(csv_path):
189
+ self.coordinates = pd.read_csv(csv_path)
190
+ print(f"Coordinates loaded: {self.coordinates.head()}")
191
+ else:
192
+ print(f"CSV file not found: {csv_path}")
193
+
194
+ def open_tiff(self):
195
+ """Handle TIFF file opening and display."""
196
+ self.selected_wafer = self.button_frame.get_selected_option()
197
+
198
+ if not all([self.selected_wafer]):
199
+ print("Recipe and wafer selection required")
200
+ self._reset_display()
201
+ return
202
+
203
+
204
+ folder_path = os.path.join(self.button_frame.folder_var_changed(),
205
+ str(self.selected_wafer))
206
+
207
+ # Recherche du fichier qui se termine par .001 dans le dossier
208
+ matching_files = glob.glob(os.path.join(folder_path, '*.001'))
209
+
210
+ # Si au moins un fichier correspond, on prend le premier
211
+ if matching_files:
212
+ recipe_path = matching_files[0]
213
+ else:
214
+ recipe_path = None # Ou tu peux lever une exception ou afficher un message d’erreur
215
+ # Charger les coordonnées depuis le fichier CSV (recipe)
216
+ self.coordinates = self.extract_positions(recipe_path)
217
+
218
+ tiff_path = os.path.join(folder_path, "data.tif")
219
+
220
+ if not os.path.isfile(tiff_path):
221
+ print(f"TIFF file not found in {folder_path}")
222
+ self._reset_display()
223
+ return
224
+
225
+ self._load_tiff(tiff_path)
226
+ self._update_plot() # Maintenant les coordonnées seront disponibles pour le plot
227
+
228
+ # Pop-up stylisé
229
+ msg = QMessageBox()
230
+ msg.setIcon(QMessageBox.Information)
231
+ msg.setText(f"Wafer {self.selected_wafer} opened successfully")
232
+ msg.setWindowTitle("Wafer Opened")
233
+ msg.setStyleSheet(MESSAGE_BOX_STYLE)
234
+ msg.exec_()
235
+
236
+ def _reset_display(self):
237
+ """
238
+ Resets the display by clearing the figure and reinitializing the subplot.
239
+ Also clears the frame_left_layout to remove any existing widgets.
240
+ """
241
+ # Clear all widgets from the left frame layout
242
+ while self.frame_left_layout.count():
243
+ item = self.frame_left_layout.takeAt(0)
244
+ widget = item.widget()
245
+ if widget is not None:
246
+ widget.deleteLater() # Properly delete the widget
247
+
248
+ # Recreate the image label in the left frame
249
+ self.image_label = QLabel(self)
250
+ self.image_label.setAlignment(Qt.AlignCenter)
251
+ self.frame_left_layout.addWidget(self.image_label)
252
+
253
+ # Clear the figure associated with the canvas
254
+ self.figure.clear()
255
+ self.ax = self.figure.add_subplot(111) # Create a new subplot
256
+ self.plot_mapping_tpl(self.ax) # Plot the default template
257
+
258
+ # Disconnect any existing signal connection
259
+ if self.canvas_connection_id is not None:
260
+ self.canvas.mpl_disconnect(self.canvas_connection_id)
261
+ self.canvas_connection_id = None
262
+
263
+ self.canvas.draw() # Redraw the updated canvas
264
+
265
+ def _update_plot(self):
266
+ """
267
+ Updates the plot with the current wafer mapping.
268
+ Ensures the plot is clean before adding new data.
269
+ """
270
+ if hasattr(self, 'ax') and self.ax:
271
+ self.ax.clear() # Clear the existing plot
272
+ else:
273
+ self.ax = self.figure.add_subplot(111) # Create new axes
274
+
275
+ self.plot_mapping_tpl(self.ax) # Plot wafer mapping
276
+
277
+ # Ensure only one connection to the button press event
278
+ if self.canvas_connection_id is not None:
279
+ self.canvas.mpl_disconnect(self.canvas_connection_id)
280
+
281
+ self.canvas_connection_id = self.canvas.mpl_connect(
282
+ 'button_press_event', self.on_click)
283
+ self.canvas.draw()
284
+
285
+ def show_image(self):
286
+ """
287
+ Displays the current image from the image list in the QLabel.
288
+ """
289
+ if self.image_list:
290
+ pil_image = self.image_list[self.current_index]
291
+ pil_image = pil_image.convert("RGBA")
292
+ data = pil_image.tobytes("raw", "RGBA")
293
+ qimage = QImage(data, pil_image.width, pil_image.height,
294
+ QImage.Format_RGBA8888)
295
+ pixmap = QPixmap.fromImage(qimage)
296
+ self.image_label.setPixmap(pixmap)
297
+
298
+ def plot_mapping_tpl(self, ax):
299
+ """Plots the mapping of the wafer with coordinate points."""
300
+ ax.set_xlabel('X (cm)', fontsize=20)
301
+ ax.set_ylabel('Y (cm)', fontsize=20)
302
+
303
+ if self.coordinates is not None:
304
+ x_coords = self.coordinates.iloc[:, 0]
305
+ y_coords = self.coordinates.iloc[:, 1]
306
+
307
+ # Calcul de la valeur maximale absolue parmi toutes les coordonnées
308
+ max_val = max(abs(x_coords).max(), abs(y_coords).max())
309
+
310
+ if max_val <= 5:
311
+ radius = 5
312
+ elif max_val <= 7.5:
313
+ radius = 7.5
314
+ elif max_val <= 10:
315
+ radius = 10
316
+ elif max_val <= 15:
317
+ radius = 15
318
+ else:
319
+ radius = max_val # fallback pour les cas supérieurs à 30
320
+
321
+ self.radius = radius
322
+
323
+ ax.scatter(x_coords, y_coords, color='blue', marker='o',
324
+ s=100, label='Positions')
325
+
326
+ # Mise à l'échelle du graphique en fonction du radius
327
+ ax.set_xlim(-radius - 1, radius + 1)
328
+ ax.set_ylim(-radius - 1, radius + 1)
329
+
330
+ circle = plt.Circle((0, 0), radius, color='black',
331
+ fill=False, linewidth=0.5)
332
+ ax.add_patch(circle)
333
+ ax.set_aspect('equal')
334
+ else:
335
+ print("No coordinates available to plot")
336
+
337
+
338
+ ax.figure.subplots_adjust(left=0.15, right=0.95, top=0.90, bottom=0.1)
339
+ self.canvas.draw()
340
+
341
+ def on_click(self, event):
342
+ """
343
+ Handles mouse click events on the plot, identifying the closest point
344
+ and updating the plot with a red circle around the selected point.
345
+
346
+ :param event: The event generated by the mouse click.
347
+ """
348
+ result = self.button_frame.get_selected_image()
349
+ if result is not None:
350
+ self.image_type, self_number_type = result
351
+ else:
352
+ print("No image selected.")
353
+ return
354
+
355
+ if event.inaxes:
356
+ x_pos = event.xdata
357
+ y_pos = event.ydata
358
+
359
+
360
+ if self.coordinates is not None and not self.coordinates.empty:
361
+ distances = np.sqrt((self.coordinates['X'] - x_pos) ** 2 +
362
+ (self.coordinates['Y'] - y_pos) ** 2)
363
+ closest_idx = distances.idxmin()
364
+ closest_pt = self.coordinates.iloc[closest_idx]
365
+ print(f"The closest point is: X = {closest_pt['X']}, "
366
+ f"Y = {closest_pt['Y']}")
367
+
368
+ # Replot with a red circle around the selected point
369
+ self.ax.clear() # Clear the existing plot
370
+ self.plot_mapping_tpl(self.ax)
371
+ self.ax.scatter([closest_pt['X']], [closest_pt['Y']],
372
+ color='red', marker='o', s=100,
373
+ label='Selected point')
374
+ coord_text = f"{closest_pt['X']:.1f} / {closest_pt['Y']:.1f}"
375
+ self.ax.text(-self.radius -0.5, self.radius-0.5, coord_text, fontsize=16, color='black')
376
+ self.canvas.draw()
377
+
378
+ # Update the image based on the selected point
379
+ result = self.image_type + (closest_idx * self_number_type)
380
+ self.current_index = result
381
+ self.show_image()
382
+
383
+ def _load_tiff(self, tiff_path):
384
+ """Load and prepare TIFF images for display.
385
+
386
+ Args:
387
+ tiff_path: Path to the TIFF file to load
388
+ """
389
+ try:
390
+ img = Image.open(tiff_path)
391
+ self.image_list = []
392
+
393
+ # Load all TIFF pages and resize them
394
+ while True:
395
+ resized_img = img.copy().resize((CANVAS_SIZE, CANVAS_SIZE),
396
+ Image.Resampling.LANCZOS)
397
+ self.image_list.append(resized_img)
398
+ try:
399
+ img.seek(img.tell() + 1) # Move to next page
400
+ except EOFError:
401
+ break # No more pages
402
+
403
+ self.current_index = 0
404
+ self.show_image() # Display first image
405
+
406
+ except Exception as e:
407
+ print(f"Error loading TIFF file: {e}")
408
+ self._reset_display()
semapp/Plot/styles.py ADDED
@@ -0,0 +1,40 @@
1
+ """Styles definitions for the GUI components."""
2
+
3
+ # Button styles
4
+ OPEN_BUTTON_STYLE = """
5
+ QPushButton {
6
+ font-size: 16px;
7
+ background-color: #ffcc80;
8
+ border: 2px solid #8c8c8c;
9
+ border-radius: 10px;
10
+ padding: 5px;
11
+ height: 50px;
12
+ }
13
+ QPushButton:hover {
14
+ background-color: #ffb74d;
15
+ }
16
+ """
17
+
18
+ # Message box styles
19
+ MESSAGE_BOX_STYLE = """
20
+ QMessageBox {
21
+ background-color: white;
22
+ }
23
+ QMessageBox QLabel {
24
+ color: #333;
25
+ font-size: 14px;
26
+ }
27
+ QPushButton {
28
+ background-color: #ffcc80;
29
+ border: 2px solid #8c8c8c;
30
+ border-radius: 5px;
31
+ padding: 5px 15px;
32
+ font-size: 12px;
33
+ }
34
+ QPushButton:hover {
35
+ background-color: #ffb74d;
36
+ }
37
+ """
38
+
39
+ # Frame styles
40
+ FRAME_STYLE = "background-color: white;"
semapp/Plot/utils.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ Utility functions for frame management and screenshot functionality.
3
+
4
+ This module provides functions to create save buttons and manage frame layouts
5
+ in the application. It includes functionality for capturing and saving
6
+ combined screenshots of multiple frames.
7
+ """
8
+
9
+ from PyQt5.QtCore import Qt
10
+ from PyQt5.QtWidgets import QPushButton, QFileDialog
11
+ from PyQt5.QtGui import QPixmap, QPainter
12
+
13
+ # Style constants
14
+ SAVE_BUTTON_STYLE = """
15
+ QPushButton {
16
+ font-size: 16px;
17
+ background-color: #e1bee7;
18
+ border: 2px solid #8c8c8c;
19
+ border-radius: 10px;
20
+ padding: 5px;
21
+ height: 20px;
22
+ }
23
+ QPushButton:hover {
24
+ background-color: #ce93d8;
25
+ }
26
+ """
27
+
28
+ BUTTON_POSITION = {
29
+ 'row': 4,
30
+ 'column': 0,
31
+ 'row_span': 1,
32
+ 'column_span': 6
33
+ }
34
+
35
+
36
+ def create_savebutton(layout, frame_left, frame_right):
37
+ """
38
+ Create a save button to capture and save specific frames as a combined image.
39
+
40
+ Args:
41
+ layout: The layout where the save button will be added
42
+ frame_left: The left frame to capture
43
+ frame_right: The right frame to capture
44
+
45
+ Returns:
46
+ None
47
+ """
48
+ def save_image():
49
+ """
50
+ Capture and save specific frames as a combined image.
51
+
52
+ Creates a screenshot of both left and right frames, combines them
53
+ horizontally and saves the result as a PNG file.
54
+ """
55
+ screen_left = frame_left.grab()
56
+ screen_right = frame_right.grab()
57
+
58
+ file_name, _ = QFileDialog.getSaveFileName(
59
+ parent=None,
60
+ caption="Save screenshot",
61
+ directory="",
62
+ filter="PNG Files (*.png);;All Files (*)"
63
+ )
64
+
65
+ if file_name:
66
+ combined_width = screen_left.width() + screen_right.width()
67
+ combined_height = max(screen_left.height(), screen_right.height())
68
+ combined_pixmap = QPixmap(combined_width, combined_height)
69
+
70
+ # Fill the combined QPixmap with a white background
71
+ combined_pixmap.fill(Qt.white)
72
+
73
+ painter = QPainter(combined_pixmap)
74
+ painter.drawPixmap(0, 0, screen_left)
75
+ painter.drawPixmap(screen_left.width(), 0, screen_right)
76
+ painter.end()
77
+
78
+ # Save the combined image
79
+ combined_pixmap.save(file_name, "PNG")
80
+
81
+ # Create and configure the save button
82
+ save_button = QPushButton("Screenshot")
83
+ save_button.setStyleSheet(SAVE_BUTTON_STYLE)
84
+ save_button.clicked.connect(save_image)
85
+
86
+ # Add the button to the layout using position constants
87
+ layout.addWidget(
88
+ save_button,
89
+ BUTTON_POSITION['row'],
90
+ BUTTON_POSITION['column'],
91
+ BUTTON_POSITION['row_span'],
92
+ BUTTON_POSITION['column_span']
93
+ )
@@ -0,0 +1,4 @@
1
+ """Processing module initialization"""
2
+ from .processing import Process
3
+
4
+ __all__ = ['Process']