From f8664713b2e9f4f239deb86611ac5f6a6c1fe781 Mon Sep 17 00:00:00 2001 From: xofoxxy Date: Wed, 18 Feb 2026 11:20:27 -0700 Subject: [PATCH 1/2] Added Rastering --- .idea/TempLabLaser.iml | 2 +- .idea/misc.xml | 2 +- src/gui_tabs/rasteringTab.py | 706 +++++++++++++++++++++++++++++++++++ src/instrumentManager.py | 4 +- src/microscopeGUI.py | 6 +- 5 files changed, 715 insertions(+), 5 deletions(-) create mode 100644 src/gui_tabs/rasteringTab.py diff --git a/.idea/TempLabLaser.iml b/.idea/TempLabLaser.iml index 4a04924..34b48df 100644 --- a/.idea/TempLabLaser.iml +++ b/.idea/TempLabLaser.iml @@ -5,7 +5,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 879f8fd..633621d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/src/gui_tabs/rasteringTab.py b/src/gui_tabs/rasteringTab.py new file mode 100644 index 0000000..90af3bb --- /dev/null +++ b/src/gui_tabs/rasteringTab.py @@ -0,0 +1,706 @@ +import tkinter as tk +import tkinter.filedialog +from tkinter import ttk +import numpy as np +import threading +import time +import os +import pandas as pd +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +from matplotlib.figure import Figure + + +class RasteringTab: + """ + Tab for rastering the sample to detect fiducial markers. + + The fiducial is made up of 3 squares in an L-shape (right angle). + - Areas where the fiducial is etched WILL reflect (high LIA magnitude) + - Areas where the fiducial space is (unetched) will NOT reflect (low/zero LIA magnitude) + + This tab performs a snake-pattern raster scan and records LIA magnitude + to build a 2D heatmap for fiducial detection. + """ + + # Default safe ranges for hexapod X/Y movement (mm) + X_MIN = -25.0 + X_MAX = 25.0 + Y_MIN = -25.0 + Y_MAX = 25.0 + + def __init__(self, parent, instruments, main_gui): + self.parent = parent + self.instruments = instruments + self.main_gui = main_gui + self.hexapod = None + + # Raster scan data + self.scan_data = None # Will be a 2D numpy array of LIA amplitudes (for heatmap) + self.phase_data = None # Will be a 2D numpy array of LIA phases + self.x_positions = None + self.y_positions = None + self.scan_running = False + self.scan_thread = None + + # Fiducial detection results + self.fiducial_centers = [] # List of (x, y) centers for the 3 squares + self.sample_angle = None + self.sample_position = None + + self.setup_ui() + + def setup_ui(self): + rastering_tab = self.parent + + # Create a main frame to hold the canvas for scrolling + main_frame = tk.Frame(rastering_tab) + main_frame.pack(fill=tk.BOTH, expand=1) + + # Create a canvas + canvas = tk.Canvas(main_frame) + canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=1) + + # Add a scrollbar to the canvas + scrollbar = tk.Scrollbar(main_frame, orient=tk.VERTICAL, command=canvas.yview) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Configure the canvas + canvas.configure(yscrollcommand=scrollbar.set) + + # Create a frame inside the canvas to hold the widgets + inner_frame = tk.Frame(canvas) + canvas.create_window((0, 0), window=inner_frame, anchor='nw') + + # Create main frames + params_frame = ttk.LabelFrame(inner_frame, text="Scan Parameters") + params_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky='nsew') + + control_frame = ttk.LabelFrame(inner_frame, text="Scan Control") + control_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky='nsew') + + output_frame = ttk.LabelFrame(inner_frame, text="Scan Output") + output_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky='nsew') + + results_frame = ttk.LabelFrame(inner_frame, text="Fiducial Detection Results") + results_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky='nsew') + + # === Scan Parameters Section === + # X range + self.xStartLabel = tk.Label(params_frame, text="X Start (mm):") + self.xStartLabel.grid(row=0, column=0, padx=10, pady=5, sticky=tk.E) + self.xStartInput = tk.Entry(params_frame, width=10) + self.xStartInput.insert(0, "-5.0") + self.xStartInput.grid(row=0, column=1, padx=5, pady=5) + + self.xEndLabel = tk.Label(params_frame, text="X End (mm):") + self.xEndLabel.grid(row=0, column=2, padx=10, pady=5, sticky=tk.E) + self.xEndInput = tk.Entry(params_frame, width=10) + self.xEndInput.insert(0, "5.0") + self.xEndInput.grid(row=0, column=3, padx=5, pady=5) + + # Y range + self.yStartLabel = tk.Label(params_frame, text="Y Start (mm):") + self.yStartLabel.grid(row=1, column=0, padx=10, pady=5, sticky=tk.E) + self.yStartInput = tk.Entry(params_frame, width=10) + self.yStartInput.insert(0, "-5.0") + self.yStartInput.grid(row=1, column=1, padx=5, pady=5) + + self.yEndLabel = tk.Label(params_frame, text="Y End (mm):") + self.yEndLabel.grid(row=1, column=2, padx=10, pady=5, sticky=tk.E) + self.yEndInput = tk.Entry(params_frame, width=10) + self.yEndInput.insert(0, "5.0") + self.yEndInput.grid(row=1, column=3, padx=5, pady=5) + + # Step size + self.stepSizeLabel = tk.Label(params_frame, text="Step Size (mm):") + self.stepSizeLabel.grid(row=2, column=0, padx=10, pady=5, sticky=tk.E) + self.stepSizeInput = tk.Entry(params_frame, width=10) + self.stepSizeInput.insert(0, "0.1") + self.stepSizeInput.grid(row=2, column=1, padx=5, pady=5) + + # Dwell time + self.dwellTimeLabel = tk.Label(params_frame, text="Dwell Time (s):") + self.dwellTimeLabel.grid(row=2, column=2, padx=10, pady=5, sticky=tk.E) + self.dwellTimeInput = tk.Entry(params_frame, width=10) + self.dwellTimeInput.insert(0, "0.1") + self.dwellTimeInput.grid(row=2, column=3, padx=5, pady=5) + + # Threshold for fiducial detection + self.thresholdLabel = tk.Label(params_frame, text="Detection Threshold:") + self.thresholdLabel.grid(row=3, column=0, padx=10, pady=5, sticky=tk.E) + self.thresholdInput = tk.Entry(params_frame, width=10) + self.thresholdInput.insert(0, "0.5") + self.thresholdInput.grid(row=3, column=1, padx=5, pady=5) + + self.thresholdInfoLabel = tk.Label(params_frame, text="(fraction of max signal)", font=('Arial', 8)) + self.thresholdInfoLabel.grid(row=3, column=2, columnspan=2, padx=5, pady=5, sticky=tk.W) + + # Return to origin checkbox + self.returnToOrigin = tk.BooleanVar(value=True) + self.returnToOriginCheck = tk.Checkbutton(params_frame, text="Return to origin after scan", + variable=self.returnToOrigin) + self.returnToOriginCheck.grid(row=4, column=0, columnspan=2, padx=10, pady=5, sticky=tk.W) + + # === Control Section === + self.startScanButton = tk.Button(control_frame, text="Start Raster Scan", + command=self.start_raster_scan) + self.startScanButton.grid(row=0, column=0, padx=10, pady=10) + + self.stopScanButton = tk.Button(control_frame, text="Stop Scan", + command=self.stop_raster_scan, state="disabled") + self.stopScanButton.grid(row=0, column=1, padx=10, pady=10) + + self.detectFiducialButton = tk.Button(control_frame, text="Detect Fiducial", + command=self.detect_fiducial, state="disabled") + self.detectFiducialButton.grid(row=0, column=2, padx=10, pady=10) + + self.saveDataButton = tk.Button(control_frame, text="Save Scan Data", + command=self.save_scan_data, state="disabled") + self.saveDataButton.grid(row=0, column=3, padx=10, pady=10) + + self.returnOriginButton = tk.Button(control_frame, text="Return to Origin", + command=self.return_to_origin) + self.returnOriginButton.grid(row=0, column=4, padx=10, pady=10) + + # Progress bar + self.progressLabel = tk.Label(control_frame, text="Progress:") + self.progressLabel.grid(row=1, column=0, padx=10, pady=5, sticky=tk.E) + self.progressBar = ttk.Progressbar(control_frame, length=300, mode='determinate') + self.progressBar.grid(row=1, column=1, columnspan=2, padx=10, pady=5, sticky='ew') + + self.progressText = tk.StringVar(value="Ready") + self.progressTextLabel = tk.Label(control_frame, textvariable=self.progressText) + self.progressTextLabel.grid(row=1, column=3, columnspan=2, padx=10, pady=5) + + # === Output Section (Heatmap) === + self.fig = Figure(figsize=(6, 5), dpi=100) + self.ax = self.fig.add_subplot(111) + self.ax.set_xlabel('X Position (mm)') + self.ax.set_ylabel('Y Position (mm)') + self.ax.set_title('LIA Magnitude Heatmap') + + self.canvas_plot = FigureCanvasTkAgg(self.fig, master=output_frame) + self.canvas_plot.draw() + self.canvas_plot.get_tk_widget().grid(row=0, column=0, padx=10, pady=10) + + # === Results Section === + self.resultsText = tk.Text(results_frame, height=8, width=60, font=('Arial', 10)) + self.resultsText.grid(row=0, column=0, padx=10, pady=10) + self.resultsText.insert('1.0', "No fiducial detected yet.\nRun a scan and click 'Detect Fiducial'.\n") + + # Configure grid weights + inner_frame.grid_columnconfigure(0, weight=1) + inner_frame.grid_columnconfigure(1, weight=1) + + # Update scroll region when the size of the frame changes + def _on_frame_configure(event): + canvas.configure(scrollregion=canvas.bbox("all")) + + inner_frame.bind('', _on_frame_configure) + + # Bind mouse wheel to scroll + def _on_mousewheel(event): + canvas.yview_scroll(int(-1*(event.delta/120)), "units") + + canvas.bind_all("", _on_mousewheel) + + def return_to_origin(self): + """Move hexapod back to origin (0, 0, 0).""" + hexapod = self.get_hexapod() + if hexapod is None: + self.progressText.set("Error: Hexapod not connected") + return + + if not hexapod.ready_for_commands: + self.progressText.set("Error: Hexapod busy") + return + + self.progressText.set("Returning to origin...") + + def do_return(): + try: + # Get current position from hexapod status + hexapod.getState() + if hexapod.status_dict: + current_x = hexapod.status_dict.get("s_mtp_tx", 0) + current_y = hexapod.status_dict.get("s_mtp_ty", 0) + + # Move to origin + hexapod.translate(np.array([-current_x, -current_y, 0.0])) + while not hexapod.ready_for_commands: + time.sleep(0.05) + + self.parent.after(0, lambda: self.progressText.set("Returned to origin")) + else: + self.parent.after(0, lambda: self.progressText.set("Could not get hexapod position")) + except Exception as e: + self.parent.after(0, lambda: self.progressText.set(f"Error: {e}")) + + threading.Thread(target=do_return).start() + + def validate_parameters(self): + """Validate scan parameters and return them if valid.""" + try: + x_start = float(self.xStartInput.get()) + x_end = float(self.xEndInput.get()) + y_start = float(self.yStartInput.get()) + y_end = float(self.yEndInput.get()) + step_size = float(self.stepSizeInput.get()) + dwell_time = float(self.dwellTimeInput.get()) + threshold = float(self.thresholdInput.get()) + + # Check ranges + if x_start < self.X_MIN or x_end > self.X_MAX: + raise ValueError(f"X range must be between {self.X_MIN} and {self.X_MAX} mm") + if y_start < self.Y_MIN or y_end > self.Y_MAX: + raise ValueError(f"Y range must be between {self.Y_MIN} and {self.Y_MAX} mm") + if x_start >= x_end: + raise ValueError("X Start must be less than X End") + if y_start >= y_end: + raise ValueError("Y Start must be less than Y End") + if step_size <= 0: + raise ValueError("Step size must be positive") + if dwell_time <= 0: + raise ValueError("Dwell time must be positive") + if threshold < 0 or threshold > 1: + raise ValueError("Threshold must be between 0 and 1") + + return { + 'x_start': x_start, + 'x_end': x_end, + 'y_start': y_start, + 'y_end': y_end, + 'step_size': step_size, + 'dwell_time': dwell_time, + 'threshold': threshold + } + except ValueError as e: + self.progressText.set(f"Error: {e}") + return None + + def get_hexapod(self): + """Get hexapod reference from main GUI.""" + if self.hexapod is None: + # Try to get hexapod from the hexapod tab + if hasattr(self.main_gui, 'hexapodTabObject') and self.main_gui.hexapodTabObject.hexapod: + self.hexapod = self.main_gui.hexapodTabObject.hexapod + return self.hexapod + + def start_raster_scan(self): + """Start the raster scan in a background thread.""" + params = self.validate_parameters() + if params is None: + return + + hexapod = self.get_hexapod() + if hexapod is None: + self.progressText.set("Error: Hexapod not connected") + return + + if not hexapod.ready_for_commands: + self.progressText.set("Error: Hexapod busy") + return + + # Update UI state + self.scan_running = True + self.startScanButton['state'] = 'disabled' + self.stopScanButton['state'] = 'normal' + self.detectFiducialButton['state'] = 'disabled' + self.saveDataButton['state'] = 'disabled' + + # Start scan in background thread + self.scan_thread = threading.Thread(target=self._run_raster_scan, args=(params,)) + self.scan_thread.start() + + def _run_raster_scan(self, params): + """Execute the snake-pattern raster scan.""" + x_start = params['x_start'] + x_end = params['x_end'] + y_start = params['y_start'] + y_end = params['y_end'] + step_size = params['step_size'] + dwell_time = params['dwell_time'] + + # Generate position arrays + self.x_positions = np.arange(x_start, x_end + step_size, step_size) + self.y_positions = np.arange(y_start, y_end + step_size, step_size) + + n_x = len(self.x_positions) + n_y = len(self.y_positions) + total_points = n_x * n_y + + # Initialize data arrays + self.scan_data = np.zeros((n_y, n_x)) # Amplitude data for heatmap + self.phase_data = np.zeros((n_y, n_x)) # Phase data + + hexapod = self.get_hexapod() + current_point = 0 + + # Track current position for relative moves + current_x = 0.0 + current_y = 0.0 + + try: + # First, move to the starting position + delta_x = x_start - current_x + delta_y = y_start - current_y + hexapod.translate(np.array([delta_x, delta_y, 0.0])) + while not hexapod.ready_for_commands: + time.sleep(0.05) + current_x = x_start + current_y = y_start + + for j, y in enumerate(self.y_positions): + if not self.scan_running: + break + + # Move to the Y position for this row (if not first row) + if j > 0: + delta_y = y - current_y + hexapod.translate(np.array([0.0, delta_y, 0.0])) + while not hexapod.ready_for_commands: + time.sleep(0.05) + current_y = y + + # Snake pattern: alternate X direction on each row + if j % 2 == 0: + x_range = self.x_positions + x_indices = range(n_x) + else: + x_range = self.x_positions[::-1] + x_indices = range(n_x - 1, -1, -1) + + for i, x in zip(x_indices, x_range): + if not self.scan_running: + break + + # Move hexapod to position using relative move + delta_x = x - current_x + if abs(delta_x) > 0.0001: # Only move if there's actual displacement + hexapod.translate(np.array([delta_x, 0.0, 0.0])) + time_waiting = 0 + while not hexapod.ready_for_commands: + if time_waiting > 0: + print(f"Waiting for hexapod to be ready... {time_waiting:.1f}s") + time.sleep(0.05) + time_waiting += 0.05 + current_x = x + + # Dwell and take measurement + time.sleep(dwell_time) + measurement = self.instruments.take_measurement() + + if measurement: + amplitude, phase = measurement + self.scan_data[j, i] = amplitude # Amplitude for heatmap display + self.phase_data[j, i] = phase # Phase stored separately + else: + self.scan_data[j, i] = 0.0 + self.phase_data[j, i] = 0.0 + + current_point += 1 + progress = (current_point / total_points) * 100 + + # Update UI from main thread + self.parent.after(0, lambda p=progress, cp=current_point, tp=total_points: + self._update_progress(p, f"{cp}/{tp} points")) + + # Periodically update heatmap + if current_point % 10 == 0: + self.parent.after(0, self._update_heatmap) + + # Return to origin if requested + if self.returnToOrigin.get() and self.scan_running: + hexapod.translate(np.array([-current_x, -current_y, 0.0])) + while not hexapod.ready_for_commands: + time.sleep(0.05) + + # Final update + self.parent.after(0, self._update_heatmap) + self.parent.after(0, lambda: self._scan_complete()) + + except Exception as e: + self.parent.after(0, lambda: self.progressText.set(f"Error: {e}")) + self.parent.after(0, lambda: self._scan_complete()) + + def _update_progress(self, progress, text): + """Update progress bar and text.""" + self.progressBar['value'] = progress + self.progressText.set(text) + + def _update_heatmap(self): + """Update the heatmap display.""" + if self.scan_data is None: + return + + self.ax.clear() + + # Create heatmap + if self.x_positions is not None and self.y_positions is not None: + extent = [self.x_positions[0], self.x_positions[-1], + self.y_positions[0], self.y_positions[-1]] + im = self.ax.imshow(self.scan_data[:,:][0], extent=extent, origin='lower', + aspect='auto', cmap='hot') + self.ax.set_xlabel('X Position (mm)') + self.ax.set_ylabel('Y Position (mm)') + self.ax.set_title('LIA Magnitude Heatmap') + + self.canvas_plot.draw() + + def _scan_complete(self): + """Called when scan is complete.""" + self.scan_running = False + self.startScanButton['state'] = 'normal' + self.stopScanButton['state'] = 'disabled' + self.detectFiducialButton['state'] = 'normal' + self.saveDataButton['state'] = 'normal' + self.progressText.set("Scan complete") + + def stop_raster_scan(self): + """Stop the raster scan.""" + self.scan_running = False + self.progressText.set("Stopping scan...") + + # Stop the hexapod + hexapod = self.get_hexapod() + if hexapod: + hexapod.stop() + + def detect_fiducial(self): + """ + Detect the fiducial marker from the scan data. + + The fiducial consists of 3 squares arranged in an L-shape (right angle). + High LIA magnitude = etched area (reflective) + Low LIA magnitude = unetched area (non-reflective) + + Algorithm: + 1. Threshold the image to create binary mask + 2. Find connected components (the 3 squares) + 3. Calculate centroids of each square + 4. Determine the corner square and calculate sample angle + """ + if self.scan_data is None: + self.resultsText.delete('1.0', tk.END) + self.resultsText.insert('1.0', "No scan data available. Run a scan first.\n") + return + + try: + threshold = float(self.thresholdInput.get()) + + # Normalize data + data_max = np.max(self.scan_data) + if data_max == 0: + self.resultsText.delete('1.0', tk.END) + self.resultsText.insert('1.0', "Error: No signal detected in scan data.\n") + return + + normalized_data = self.scan_data / data_max + + # Create binary mask (high signal = fiducial squares) + binary_mask = normalized_data > threshold + + # Find connected components using simple flood fill + labeled, num_features = self._label_connected_components(binary_mask) + + if num_features < 3: + self.resultsText.delete('1.0', tk.END) + self.resultsText.insert('1.0', + f"Warning: Found {num_features} regions, expected 3.\n" + f"Try adjusting the threshold value.\n") + if num_features == 0: + return + + # Calculate centroids for each component + centroids = [] + for label in range(1, num_features + 1): + component_mask = labeled == label + y_indices, x_indices = np.where(component_mask) + + if len(x_indices) > 0: + # Convert pixel indices to mm coordinates + x_center = np.mean(x_indices) + y_center = np.mean(y_indices) + + x_mm = self.x_positions[0] + x_center * (self.x_positions[-1] - self.x_positions[0]) / (len(self.x_positions) - 1) + y_mm = self.y_positions[0] + y_center * (self.y_positions[-1] - self.y_positions[0]) / (len(self.y_positions) - 1) + + centroids.append((x_mm, y_mm, len(x_indices))) # x, y, area in pixels + + # Sort by area to identify the squares + centroids.sort(key=lambda c: c[2], reverse=True) + + # Take top 3 centroids (if available) + self.fiducial_centers = [(c[0], c[1]) for c in centroids[:min(3, len(centroids))]] + + # Calculate sample angle and position from the L-shape + if len(self.fiducial_centers) >= 3: + self._calculate_sample_orientation() + + # Update results display + self._display_results() + + # Update heatmap with detected fiducial markers + self._update_heatmap_with_fiducials() + + except Exception as e: + self.resultsText.delete('1.0', tk.END) + self.resultsText.insert('1.0', f"Error during fiducial detection: {e}\n") + + def _label_connected_components(self, binary_mask): + """Simple connected component labeling using flood fill.""" + labeled = np.zeros_like(binary_mask, dtype=int) + current_label = 0 + + def flood_fill(start_y, start_x, label): + stack = [(start_y, start_x)] + while stack: + y, x = stack.pop() + if (0 <= y < binary_mask.shape[0] and + 0 <= x < binary_mask.shape[1] and + binary_mask[y, x] and + labeled[y, x] == 0): + labeled[y, x] = label + stack.extend([(y+1, x), (y-1, x), (y, x+1), (y, x-1)]) + + for y in range(binary_mask.shape[0]): + for x in range(binary_mask.shape[1]): + if binary_mask[y, x] and labeled[y, x] == 0: + current_label += 1 + flood_fill(y, x, current_label) + + return labeled, current_label + + def _calculate_sample_orientation(self): + """ + Calculate sample position and angle from the 3 fiducial squares. + + The L-shape has one corner square and two end squares. + The corner square is the one closest to the other two. + """ + if len(self.fiducial_centers) < 3: + return + + # Find distances between all pairs + p1, p2, p3 = self.fiducial_centers[:3] + + d12 = np.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2) + d13 = np.sqrt((p1[0] - p3[0])**2 + (p1[1] - p3[1])**2) + d23 = np.sqrt((p2[0] - p3[0])**2 + (p2[1] - p3[1])**2) + + # The corner square is the one with smallest sum of distances to others + sum1 = d12 + d13 + sum2 = d12 + d23 + sum3 = d13 + d23 + + if sum1 <= sum2 and sum1 <= sum3: + corner = p1 + end1, end2 = p2, p3 + elif sum2 <= sum1 and sum2 <= sum3: + corner = p2 + end1, end2 = p1, p3 + else: + corner = p3 + end1, end2 = p1, p2 + + # Calculate angle from the corner to the two ends + angle1 = np.arctan2(end1[1] - corner[1], end1[0] - corner[0]) + angle2 = np.arctan2(end2[1] - corner[1], end2[0] - corner[0]) + + # Sample orientation is the average angle (one arm of the L) + # We'll use the arm that's more horizontal as reference + if abs(np.cos(angle1)) > abs(np.cos(angle2)): + self.sample_angle = np.degrees(angle1) + else: + self.sample_angle = np.degrees(angle2) + + # Sample position is the corner of the L + self.sample_position = corner + + def _display_results(self): + """Display fiducial detection results.""" + self.resultsText.delete('1.0', tk.END) + + result_str = "=== Fiducial Detection Results ===\n\n" + + result_str += f"Number of squares detected: {len(self.fiducial_centers)}\n\n" + + for i, (x, y) in enumerate(self.fiducial_centers): + result_str += f"Square {i+1} center: X={x:.3f} mm, Y={y:.3f} mm\n" + + result_str += "\n" + + if self.sample_position: + result_str += f"Sample corner position: X={self.sample_position[0]:.3f} mm, Y={self.sample_position[1]:.3f} mm\n" + + if self.sample_angle is not None: + result_str += f"Sample angle: {self.sample_angle:.2f} degrees\n" + + self.resultsText.insert('1.0', result_str) + + def _update_heatmap_with_fiducials(self): + """Update heatmap to show detected fiducial centers.""" + self._update_heatmap() + + # Plot fiducial centers + for i, (x, y) in enumerate(self.fiducial_centers): + self.ax.plot(x, y, 'b+', markersize=15, markeredgewidth=2) + self.ax.annotate(f'{i+1}', (x, y), textcoords="offset points", + xytext=(5, 5), fontsize=10, color='blue') + + # Draw lines connecting the L-shape if we have 3 points + if len(self.fiducial_centers) >= 3 and self.sample_position: + corner = self.sample_position + for (x, y) in self.fiducial_centers: + if (x, y) != corner: + self.ax.plot([corner[0], x], [corner[1], y], 'g--', linewidth=2) + + self.canvas_plot.draw() + + def save_scan_data(self): + """Save scan data to a CSV file including both amplitude and phase.""" + if self.scan_data is None: + self.progressText.set("No data to save") + return + + file_path = tk.filedialog.asksaveasfilename( + defaultextension=".csv", + filetypes=[("CSV files", "*.csv"), ("All files", "*.*")], + title="Save Scan Data" + ) + + if file_path: + try: + # Create a comprehensive DataFrame with all measurement data + # Format: X_Position, Y_Position, Amplitude, Phase + rows = [] + for j, y in enumerate(self.y_positions): + for i, x in enumerate(self.x_positions): + rows.append({ + 'X_Position': x, + 'Y_Position': y, + 'Amplitude': self.scan_data[j, i], + 'Phase': self.phase_data[j, i] if self.phase_data is not None else 0.0 + }) + + df = pd.DataFrame(rows) + df.to_csv(file_path, index=False) + + # Also save fiducial results if available + if self.fiducial_centers: + results_path = file_path.replace('.csv', '_fiducial_results.txt') + with open(results_path, 'w') as f: + f.write("Fiducial Detection Results\n") + f.write("=" * 30 + "\n\n") + for i, (x, y) in enumerate(self.fiducial_centers): + f.write(f"Square {i+1}: X={x:.3f} mm, Y={y:.3f} mm\n") + if self.sample_position: + f.write(f"\nCorner Position: X={self.sample_position[0]:.3f} mm, Y={self.sample_position[1]:.3f} mm\n") + if self.sample_angle is not None: + f.write(f"Sample Angle: {self.sample_angle:.2f} degrees\n") + + self.progressText.set(f"Data saved to {os.path.basename(file_path)}") + + except Exception as e: + self.progressText.set(f"Error saving: {e}") + diff --git a/src/instrumentManager.py b/src/instrumentManager.py index 40213b0..8d9a826 100644 --- a/src/instrumentManager.py +++ b/src/instrumentManager.py @@ -372,9 +372,9 @@ def automatic_measuring(self, settings, filepath, convergence_check, degree = No # Save data to CSV if not data.empty and filepath: if degree is None: - name = f"measurement_data_{datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".csv"}" + name = f"measurement_data_{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv" else: - name = f"{round(degree,1)} degrees" + ".csv" + name = f"{round(degree,1)} degrees.csv" full_path = os.path.join(filepath, name) os.makedirs(os.path.dirname(full_path), exist_ok=True) data.to_csv(full_path, index=False) diff --git a/src/microscopeGUI.py b/src/microscopeGUI.py index 24d1dcf..da8883e 100644 --- a/src/microscopeGUI.py +++ b/src/microscopeGUI.py @@ -1,7 +1,7 @@ import tkinter as tk from tkinter import ttk from instrumentManager import InstrumentInitialize -from src.gui_tabs import amplifierTab, hexapodTab, automationLaserTab, instrumentsTab, automationHexapodTab, automationManagementTab#, sampleMappingTab +from src.gui_tabs import amplifierTab, hexapodTab, automationLaserTab, instrumentsTab, automationHexapodTab, automationManagementTab, rasteringTab#, sampleMappingTab import os # import pymeasure.instruments.srs.sr830 as lia @@ -48,12 +48,14 @@ def on_closing(): hexapodAutomationFrame = ttk.Frame(notebook) sampleMappingFrame = ttk.Frame(notebook) generalAutomationFrame = ttk.Frame(notebook) + rasteringFrame = ttk.Frame(notebook) # Add frames to notebook notebook.add(laserAutomationFrame, text='Laser Automation') notebook.add(hexapodAutomationFrame, text='Hexapod Automation') notebook.add(sampleMappingFrame, text='Sample Mapping') notebook.add(generalAutomationFrame, text='Automation Finalization') + notebook.add(rasteringFrame, text='Rastering') # Create tab objects self.laserTabObject = automationLaserTab.AutomationTab(laserAutomationFrame, self.instruments, self) @@ -66,6 +68,8 @@ def on_closing(): self.automationTabObject = automationManagementTab.AutomationManagerTab(generalAutomationFrame, self.instruments, self) + self.rasteringTabObject = rasteringTab.RasteringTab(rasteringFrame, self.instruments, self) + window.after(100, self.laserTabObject.schedule_automation_update) window.protocol("WM_DELETE_WINDOW", on_closing) From dd9648c88750aac3f5192404f8e8af42b06a9720 Mon Sep 17 00:00:00 2001 From: xofoxxy Date: Wed, 18 Feb 2026 11:42:40 -0700 Subject: [PATCH 2/2] Graphing bug fix --- src/gui_tabs/rasteringTab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui_tabs/rasteringTab.py b/src/gui_tabs/rasteringTab.py index 90af3bb..ad18070 100644 --- a/src/gui_tabs/rasteringTab.py +++ b/src/gui_tabs/rasteringTab.py @@ -439,11 +439,11 @@ def _update_heatmap(self): if self.x_positions is not None and self.y_positions is not None: extent = [self.x_positions[0], self.x_positions[-1], self.y_positions[0], self.y_positions[-1]] - im = self.ax.imshow(self.scan_data[:,:][0], extent=extent, origin='lower', + im = self.ax.imshow(self.scan_data, extent=extent, origin='lower', aspect='auto', cmap='hot') self.ax.set_xlabel('X Position (mm)') self.ax.set_ylabel('Y Position (mm)') - self.ax.set_title('LIA Magnitude Heatmap') + self.ax.set_title('LIA Amplitude Heatmap') self.canvas_plot.draw()