From 4071af92aee53a846e20de297dfe1f6ff337d910 Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Thu, 13 Mar 2025 10:59:14 -0400 Subject: [PATCH 01/22] Adding a generic motor interface as well as hardware spec for Thorlabs motors. Beginning to work on Grating Scan logic for controlling the entire experiment. --- src/qudi/hardware/camera/andor_camera.py | 2 +- src/qudi/hardware/servo/thorlabs_servo.py | 108 ++++++++++++++++++ src/qudi/interface/motor_interface.py | 92 +++++++++++++++ src/qudi/logic/common/motor_logic.py | 120 +++++++++++++++++++ src/qudi/logic/grating_scan_logic.py | 133 ++++++++++++++++++++++ 5 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 src/qudi/hardware/servo/thorlabs_servo.py create mode 100644 src/qudi/interface/motor_interface.py create mode 100644 src/qudi/logic/common/motor_logic.py diff --git a/src/qudi/hardware/camera/andor_camera.py b/src/qudi/hardware/camera/andor_camera.py index 39d19f3..077f5c6 100644 --- a/src/qudi/hardware/camera/andor_camera.py +++ b/src/qudi/hardware/camera/andor_camera.py @@ -19,7 +19,7 @@ class AndorCamera(CameraInterface): module.Class: 'camera.andor_camera.AndorCamera' options: - dll_location: r'C:\Users\hoodl\Downloads\pyAndorSDK2_NEW\pyAndorSDK2' # path to library file + dll_location: 'C:\Users\hoodl\Downloads\pyAndorSDK2_NEW\pyAndorSDK2' # path to library file andor_location: '/usr/local/etc/andor' default_temperature: -80 temp_tolerance: 1 diff --git a/src/qudi/hardware/servo/thorlabs_servo.py b/src/qudi/hardware/servo/thorlabs_servo.py new file mode 100644 index 0000000..0b03d9e --- /dev/null +++ b/src/qudi/hardware/servo/thorlabs_servo.py @@ -0,0 +1,108 @@ +import thorlabs_apt as apt +from typing import List + +from qudi.core.configoption import ConfigOption +from qudi.interface.motor_interface import MotorInterface + +class ThorlabsServo(MotorInterface): + """ Hardware class for Thorlabs Servo Motors. + + Example config for copy-paste: + + thorlabs_servo: + module.Class: 'servo.thorlabs_servo.ThorlabsServo' + + options: + serial_number: 83833940 + """ + + _serial_number = ConfigOption( + name='serial_number', + missing='error' + ) + + + def on_activate(self): + devices = apt.list_available_devices() + found = False + + for (t, sn) in enumerate(devices): + if sn == self._serial_number: + found = True + break + + if not found: + self.log.exception(f'No motor with serial number {self._serial_number} found.\n' \ + f'Detected devices are: {devices}') + + self._motor = apt.Motor(self._serial_number) + self._moving = self._motor.is_in_motion() + + (min_pos, max_pos, units, pitch) = self._motor.get_stage_axis_info() + self._min_pos = min_pos + self._max_pos = max_pos + + self._pos = self._motor.position + + + def on_deactivate(self): + pass + + + def get_motion_limits(self) -> List[float]: + """ Return motor motion limits. + + @return float[2]: motion limits (min, max) + """ + return [self._min_pos, self._max_pos] + + def get_position(self) -> float: + """ Return position + + @return float: motor position + """ + self._pos = self._motor.position + return self._pos + + def move(self, position: float, blocking: bool = False) -> None: + """ Initiates a move to a position (absolute) + + @param float position: position to move to + @param bool blocking: wait for move to complete? + """ + self._moving = True + self._motor.move_to(position, blocking) + + + def move_relative(self, position: float, blocking: bool = False) -> None: + """ Initiates a move to a position (relative) + + @param float position: position to move to + @param bool blocking: wait for move to complete? + """ + self._moving = True + self._motor.move_by(position, blocking) + + def in_motion(self) -> bool: + """ Returns if the motor thinks it is moving + + @return bool: if the motor is moving + """ + self._moving = self._motor.is_in_motion() + return self._moving + + + def center(self, blocking: bool = False): + """ Centers the motor, stopping other motion + + @param bool blocking: wait for move to complete? + """ + self._moving = True + self._motor.move_home(blocking) + + def stop(self): + """ Stops the motor motion + """ + self._motor.stop_profiled() + self._moving = False + \ No newline at end of file diff --git a/src/qudi/interface/motor_interface.py b/src/qudi/interface/motor_interface.py new file mode 100644 index 0000000..6f028e6 --- /dev/null +++ b/src/qudi/interface/motor_interface.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +""" +Interface file for servo motors. This is an (intentionally) fairly vague +interface providing goto float without thought as to whether or not that makes +sense for a particular piece of hardware. + +Copyright (c) 2025, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" + +from typing import List +from abc import abstractmethod +from qudi.core.module import Base + +class MotorInterface(Base): + """ This interface can be used to control a simple laser. It handles power control, control modes and shutter states + + This interface is useful for a standard, fixed wavelength laser that you can find in a lab. + It handles power control via constant power or constant current mode, a shutter state if the hardware has a shutter + and a temperature regulation control. + + """ + + @abstractmethod + def get_motion_limits(self) -> List[float]: + """ Return motor motion limits. + + @return float[2]: motion limits (min, max) + """ + pass + + @abstractmethod + def get_position(self) -> float: + """ Return position + + @return float: motor position + """ + pass + + @abstractmethod + def move(self, position: float, blocking: bool = False) -> None: + """ Initiates a move to a position (absolute) + + @param float position: position to move to + @param bool blocking: wait for move to complete? + """ + pass + + @abstractmethod + def move_relative(self, position: float, blocking: bool = False) -> None: + """ Initiates a relative move to a position + + @param float position: position to move to + @param bool blocking: wait for move to complete? + """ + pass + + @abstractmethod + def in_motion(self) -> bool: + """ Returns if the motor thinks it is moving + + @return bool: if the motor is moving + """ + pass + + + @abstractmethod + def center(self, blocking: bool = False): + """ Centers the motor, stopping other motion + + @param bool blocking: wait for move to complete? + """ + pass + + @abstractmethod + def stop(self): + """ Stops the motor motion + """ + pass \ No newline at end of file diff --git a/src/qudi/logic/common/motor_logic.py b/src/qudi/logic/common/motor_logic.py new file mode 100644 index 0000000..4311d8e --- /dev/null +++ b/src/qudi/logic/common/motor_logic.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +""" +A module for controlling a motor. + +Copyright (c) 2025, QuPIDC qudi developers. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" + + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib as mpl +from PySide2 import QtCore +from qudi.core.connector import Connector +from qudi.core.configoption import ConfigOption +from qudi.core.statusvariable import StatusVar +from qudi.util.mutex import RecursiveMutex +from qudi.core.module import LogicBase + +class MotorLogic(LogicBase): + """ Logic class for controlling a motor + + Example config for copy-paste: + + motor_logic: + module.Class: 'common.motor_logic.MotorLogic' + connect: + motor: motor_dummy + + """ + + # declare connectors + _motor = Connector(name='motor', interface='MotorInterface') + + # signals + sigMoveStarted = QtCore.Signal() + sigMoveFinished = QtCore.Signal() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() + + def on_activate(self): + """ Initialisation performed during activation of the module. + """ + motor = self._motor() + self._position = motor.get_position() + self._moving = motor.in_motion() + + self.__timer = QtCore.QTimer() + self.__timer.timeout.connect(self.__status_update) + self.__timer.setSingleShot(False) + self.__timer.start(25) # check if motor is moving every 25 ms + + + def on_deactivate(self): + """ Perform required deactivation. """ + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + @property + def position(self) -> float: + return self._position + + @property + def moving(self) -> bool: + return self._moving + + @QtCore.Slot(float) + def move(self, position: float): + """ Absolute move + """ + with self._thread_lock: + motor = self._motor() + motor.move(position) + self._moving = True + self.sigMoveStarted.emit() + + @QtCore.Slot(float) + def move_relative(self, position: float): + """ Relative move + """ + with self._thread_lock: + motor = self._motor() + motor.move_relative(position) + self._moving = True + self.sigMoveStarted.emit() + + @QtCore.Slot() + def stop(self): + with self._thread_lock: + motor = self._motor() + motor.stop() + self._moving = False + self.sigMoveFinished.emit() + + def __status_update(self): + with self._thread_lock: + motor = self._motor() + new_motion = motor.in_motion() + + if new_motion and not self._moving: + self.sigMoveStarted.emit() + elif not new_motion and self._moving: + self.sigMoveFinished.emit() + + self._moving = new_motion diff --git a/src/qudi/logic/grating_scan_logic.py b/src/qudi/logic/grating_scan_logic.py index e69de29..4bcdcd4 100644 --- a/src/qudi/logic/grating_scan_logic.py +++ b/src/qudi/logic/grating_scan_logic.py @@ -0,0 +1,133 @@ + +import numpy as np +import time +import datetime +import matplotlib.pyplot as plt +from PySide2 import QtCore + +from typing import List + +from qudi.core.module import LogicBase +from qudi.util.mutex import RecursiveMutex +from qudi.util.units import ScaledFloat +from qudi.core.connector import Connector +from qudi.core.configoption import ConfigOption +from qudi.core.statusvariable import StatusVar +from qudi.util.datastorage import TextDataStorage + + +class GratingScanData: + def __init__(): + pass + +class GratingScanLogic(LogicBase): + """ + This is the Logic class for Diffraction Grating measurements + + example config for copy-paste: + + grating_scan_logic: + module.Class: 'grating_scan_logic.GratingScanLogic' + connect: + camera: camera_logic + grating_motor: grating_logic + waveplate_motor: waveplate_logic + laser: scanning_laser_logic + options: + calibrate: False # Set to True to calibrate before each measurement + calibration_data: + pt1: # ignored + angle: 76.3919 # in degrees + wavelength = 0.715 # in um + pt2: # ignored + angle: 76.1283 # in degrees + wavelength = 0.720 # in um + pt3: # ignored + angle: 75.9913 # in degrees + wavelength = 0.725 # in um + pt4: # ignored + angle: 75.5985 # in degrees + wavelength = 0.730 # in um + + """ + + # declare connectors + _laser = Connector(name='laser', interface='ScanningLaserLogic') + _camera = Connector(name='camera', interface='CameraLogic') + _grating_motor = Connector(name='grating_motor', interface='MotorLogic') + _waveplate_motor = Connector(name='waveplate_motor', interface='MotorLogic') + + # declare config options + _calibrate = ConfigOption(name='calibrate', default=False) + _calibration_data = ConfigOption(name='calibration_data', + default={ + 'pt1': { + 'angle': 76.3919, + 'wavelength': 0.715 + } + },) + + # status variables: + _start_wavelength = StatusVar('start_wavelength', default=0.75) + _end_wavelength = StatusVar('end_wavelength', default=0.8) + _current_wavelength = StatusVar('current_wavelength', default=0.75) + + _laser_locked = StatusVar('laser_locked', default=False) + _current_data = StatusVar('current_data', default=[]) # list of GratingScanData + + + # Update signals, e.g. for GUI module + sigWavelengthUpdated = QtCore.Signal(float) + sigCountsUpdated = QtCore.Signal(List[TerascanData]) + + sigConfigureCounter = QtCore.Signal(float, float) + sigConfigureLaser = QtCore.Signal(float, float) + + sigStartScan = QtCore.Signal() + sigStopScan = QtCore.Signal() + + sigStopCounting = QtCore.Signal() + sigStartCounting = QtCore.Signal() + + sigScanFinished = QtCore.Signal() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._thread_lock = RecursiveMutex() + + def on_activate(self): + laser = self._laser() + counter = self._counter() + wavemeter = self._wavemeter() + daq = self._daq() + + # Outputs: + self.sigConfigureCounter.connect(counter.configure, QtCore.Qt.QueuedConnection) + self.sigConfigureLaser.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) + + self.sigStartScan.connect(counter.start_counter, QtCore.Qt.QueuedConnection) + self.sigStartScan.connect(laser.start_scan, QtCore.Qt.QueuedConnection) + self.sigStartScan.connect(wavemeter.start_reading, QtCore.Qt.QueuedConnection) + + self.sigStopScan.connect(counter.stop_counter, QtCore.Qt.QueuedConnection) + self.sigStopScan.connect(laser.stop_scan, QtCore.Qt.QueuedConnection) + self.sigStopScan.connect(wavemeter.stop_reading, QtCore.Qt.QueuedConnection) + + self.sigStopCounting.connect(counter.stop_counter, QtCore.Qt.QueuedConnection) + self.sigStartCounting.connect(counter.start_counter, QtCore.Qt.QueuedConnection) + + # Inputs: + laser.sigScanStarted.connect(self._laser_scan_started) + laser.sigScanFinished.connect(self._laser_scan_finished) + wavemeter.sigNewRawData.connect(self._new_wavemeter_data) + counter.sigScanFinished.connect(self._process_counter_data) + daq.sigNewData.connect(self._new_daq_data) + + # Configure Counter: + self._record_length_s = self._record_length_ms * 1e-3 + self._bin_width_s = self._record_length_s + self.sigConfigureCounter.emit(self._bin_width_s, self._record_length_s) + + def on_deactivate(self): + pass From 732821265eb558db3ca89b9897462201c6255202 Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Thu, 13 Mar 2025 10:59:14 -0400 Subject: [PATCH 02/22] Adding a generic motor interface as well as hardware spec for Thorlabs motors. Beginning to work on Grating Scan logic for controlling the entire experiment. --- src/qudi/hardware/camera/andor_camera.py | 2 +- src/qudi/hardware/servo/thorlabs_servo.py | 108 ++++++++++++++++++ src/qudi/interface/motor_interface.py | 92 +++++++++++++++ src/qudi/logic/common/motor_logic.py | 120 +++++++++++++++++++ src/qudi/logic/grating_scan_logic.py | 133 ++++++++++++++++++++++ 5 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 src/qudi/hardware/servo/thorlabs_servo.py create mode 100644 src/qudi/interface/motor_interface.py create mode 100644 src/qudi/logic/common/motor_logic.py diff --git a/src/qudi/hardware/camera/andor_camera.py b/src/qudi/hardware/camera/andor_camera.py index 58864a9..167ef74 100644 --- a/src/qudi/hardware/camera/andor_camera.py +++ b/src/qudi/hardware/camera/andor_camera.py @@ -20,7 +20,7 @@ class AndorCamera(CameraInterface): module.Class: 'camera.andor_camera.AndorCamera' options: - dll_location: r'C:\Users\hoodl\Downloads\pyAndorSDK2_NEW\pyAndorSDK2' # path to library file + dll_location: 'C:\Users\hoodl\Downloads\pyAndorSDK2_NEW\pyAndorSDK2' # path to library file andor_location: '/usr/local/etc/andor' default_temperature: -80 temp_tolerance: 1 diff --git a/src/qudi/hardware/servo/thorlabs_servo.py b/src/qudi/hardware/servo/thorlabs_servo.py new file mode 100644 index 0000000..0b03d9e --- /dev/null +++ b/src/qudi/hardware/servo/thorlabs_servo.py @@ -0,0 +1,108 @@ +import thorlabs_apt as apt +from typing import List + +from qudi.core.configoption import ConfigOption +from qudi.interface.motor_interface import MotorInterface + +class ThorlabsServo(MotorInterface): + """ Hardware class for Thorlabs Servo Motors. + + Example config for copy-paste: + + thorlabs_servo: + module.Class: 'servo.thorlabs_servo.ThorlabsServo' + + options: + serial_number: 83833940 + """ + + _serial_number = ConfigOption( + name='serial_number', + missing='error' + ) + + + def on_activate(self): + devices = apt.list_available_devices() + found = False + + for (t, sn) in enumerate(devices): + if sn == self._serial_number: + found = True + break + + if not found: + self.log.exception(f'No motor with serial number {self._serial_number} found.\n' \ + f'Detected devices are: {devices}') + + self._motor = apt.Motor(self._serial_number) + self._moving = self._motor.is_in_motion() + + (min_pos, max_pos, units, pitch) = self._motor.get_stage_axis_info() + self._min_pos = min_pos + self._max_pos = max_pos + + self._pos = self._motor.position + + + def on_deactivate(self): + pass + + + def get_motion_limits(self) -> List[float]: + """ Return motor motion limits. + + @return float[2]: motion limits (min, max) + """ + return [self._min_pos, self._max_pos] + + def get_position(self) -> float: + """ Return position + + @return float: motor position + """ + self._pos = self._motor.position + return self._pos + + def move(self, position: float, blocking: bool = False) -> None: + """ Initiates a move to a position (absolute) + + @param float position: position to move to + @param bool blocking: wait for move to complete? + """ + self._moving = True + self._motor.move_to(position, blocking) + + + def move_relative(self, position: float, blocking: bool = False) -> None: + """ Initiates a move to a position (relative) + + @param float position: position to move to + @param bool blocking: wait for move to complete? + """ + self._moving = True + self._motor.move_by(position, blocking) + + def in_motion(self) -> bool: + """ Returns if the motor thinks it is moving + + @return bool: if the motor is moving + """ + self._moving = self._motor.is_in_motion() + return self._moving + + + def center(self, blocking: bool = False): + """ Centers the motor, stopping other motion + + @param bool blocking: wait for move to complete? + """ + self._moving = True + self._motor.move_home(blocking) + + def stop(self): + """ Stops the motor motion + """ + self._motor.stop_profiled() + self._moving = False + \ No newline at end of file diff --git a/src/qudi/interface/motor_interface.py b/src/qudi/interface/motor_interface.py new file mode 100644 index 0000000..6f028e6 --- /dev/null +++ b/src/qudi/interface/motor_interface.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +""" +Interface file for servo motors. This is an (intentionally) fairly vague +interface providing goto float without thought as to whether or not that makes +sense for a particular piece of hardware. + +Copyright (c) 2025, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" + +from typing import List +from abc import abstractmethod +from qudi.core.module import Base + +class MotorInterface(Base): + """ This interface can be used to control a simple laser. It handles power control, control modes and shutter states + + This interface is useful for a standard, fixed wavelength laser that you can find in a lab. + It handles power control via constant power or constant current mode, a shutter state if the hardware has a shutter + and a temperature regulation control. + + """ + + @abstractmethod + def get_motion_limits(self) -> List[float]: + """ Return motor motion limits. + + @return float[2]: motion limits (min, max) + """ + pass + + @abstractmethod + def get_position(self) -> float: + """ Return position + + @return float: motor position + """ + pass + + @abstractmethod + def move(self, position: float, blocking: bool = False) -> None: + """ Initiates a move to a position (absolute) + + @param float position: position to move to + @param bool blocking: wait for move to complete? + """ + pass + + @abstractmethod + def move_relative(self, position: float, blocking: bool = False) -> None: + """ Initiates a relative move to a position + + @param float position: position to move to + @param bool blocking: wait for move to complete? + """ + pass + + @abstractmethod + def in_motion(self) -> bool: + """ Returns if the motor thinks it is moving + + @return bool: if the motor is moving + """ + pass + + + @abstractmethod + def center(self, blocking: bool = False): + """ Centers the motor, stopping other motion + + @param bool blocking: wait for move to complete? + """ + pass + + @abstractmethod + def stop(self): + """ Stops the motor motion + """ + pass \ No newline at end of file diff --git a/src/qudi/logic/common/motor_logic.py b/src/qudi/logic/common/motor_logic.py new file mode 100644 index 0000000..4311d8e --- /dev/null +++ b/src/qudi/logic/common/motor_logic.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +""" +A module for controlling a motor. + +Copyright (c) 2025, QuPIDC qudi developers. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" + + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib as mpl +from PySide2 import QtCore +from qudi.core.connector import Connector +from qudi.core.configoption import ConfigOption +from qudi.core.statusvariable import StatusVar +from qudi.util.mutex import RecursiveMutex +from qudi.core.module import LogicBase + +class MotorLogic(LogicBase): + """ Logic class for controlling a motor + + Example config for copy-paste: + + motor_logic: + module.Class: 'common.motor_logic.MotorLogic' + connect: + motor: motor_dummy + + """ + + # declare connectors + _motor = Connector(name='motor', interface='MotorInterface') + + # signals + sigMoveStarted = QtCore.Signal() + sigMoveFinished = QtCore.Signal() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() + + def on_activate(self): + """ Initialisation performed during activation of the module. + """ + motor = self._motor() + self._position = motor.get_position() + self._moving = motor.in_motion() + + self.__timer = QtCore.QTimer() + self.__timer.timeout.connect(self.__status_update) + self.__timer.setSingleShot(False) + self.__timer.start(25) # check if motor is moving every 25 ms + + + def on_deactivate(self): + """ Perform required deactivation. """ + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + @property + def position(self) -> float: + return self._position + + @property + def moving(self) -> bool: + return self._moving + + @QtCore.Slot(float) + def move(self, position: float): + """ Absolute move + """ + with self._thread_lock: + motor = self._motor() + motor.move(position) + self._moving = True + self.sigMoveStarted.emit() + + @QtCore.Slot(float) + def move_relative(self, position: float): + """ Relative move + """ + with self._thread_lock: + motor = self._motor() + motor.move_relative(position) + self._moving = True + self.sigMoveStarted.emit() + + @QtCore.Slot() + def stop(self): + with self._thread_lock: + motor = self._motor() + motor.stop() + self._moving = False + self.sigMoveFinished.emit() + + def __status_update(self): + with self._thread_lock: + motor = self._motor() + new_motion = motor.in_motion() + + if new_motion and not self._moving: + self.sigMoveStarted.emit() + elif not new_motion and self._moving: + self.sigMoveFinished.emit() + + self._moving = new_motion diff --git a/src/qudi/logic/grating_scan_logic.py b/src/qudi/logic/grating_scan_logic.py index e69de29..4bcdcd4 100644 --- a/src/qudi/logic/grating_scan_logic.py +++ b/src/qudi/logic/grating_scan_logic.py @@ -0,0 +1,133 @@ + +import numpy as np +import time +import datetime +import matplotlib.pyplot as plt +from PySide2 import QtCore + +from typing import List + +from qudi.core.module import LogicBase +from qudi.util.mutex import RecursiveMutex +from qudi.util.units import ScaledFloat +from qudi.core.connector import Connector +from qudi.core.configoption import ConfigOption +from qudi.core.statusvariable import StatusVar +from qudi.util.datastorage import TextDataStorage + + +class GratingScanData: + def __init__(): + pass + +class GratingScanLogic(LogicBase): + """ + This is the Logic class for Diffraction Grating measurements + + example config for copy-paste: + + grating_scan_logic: + module.Class: 'grating_scan_logic.GratingScanLogic' + connect: + camera: camera_logic + grating_motor: grating_logic + waveplate_motor: waveplate_logic + laser: scanning_laser_logic + options: + calibrate: False # Set to True to calibrate before each measurement + calibration_data: + pt1: # ignored + angle: 76.3919 # in degrees + wavelength = 0.715 # in um + pt2: # ignored + angle: 76.1283 # in degrees + wavelength = 0.720 # in um + pt3: # ignored + angle: 75.9913 # in degrees + wavelength = 0.725 # in um + pt4: # ignored + angle: 75.5985 # in degrees + wavelength = 0.730 # in um + + """ + + # declare connectors + _laser = Connector(name='laser', interface='ScanningLaserLogic') + _camera = Connector(name='camera', interface='CameraLogic') + _grating_motor = Connector(name='grating_motor', interface='MotorLogic') + _waveplate_motor = Connector(name='waveplate_motor', interface='MotorLogic') + + # declare config options + _calibrate = ConfigOption(name='calibrate', default=False) + _calibration_data = ConfigOption(name='calibration_data', + default={ + 'pt1': { + 'angle': 76.3919, + 'wavelength': 0.715 + } + },) + + # status variables: + _start_wavelength = StatusVar('start_wavelength', default=0.75) + _end_wavelength = StatusVar('end_wavelength', default=0.8) + _current_wavelength = StatusVar('current_wavelength', default=0.75) + + _laser_locked = StatusVar('laser_locked', default=False) + _current_data = StatusVar('current_data', default=[]) # list of GratingScanData + + + # Update signals, e.g. for GUI module + sigWavelengthUpdated = QtCore.Signal(float) + sigCountsUpdated = QtCore.Signal(List[TerascanData]) + + sigConfigureCounter = QtCore.Signal(float, float) + sigConfigureLaser = QtCore.Signal(float, float) + + sigStartScan = QtCore.Signal() + sigStopScan = QtCore.Signal() + + sigStopCounting = QtCore.Signal() + sigStartCounting = QtCore.Signal() + + sigScanFinished = QtCore.Signal() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._thread_lock = RecursiveMutex() + + def on_activate(self): + laser = self._laser() + counter = self._counter() + wavemeter = self._wavemeter() + daq = self._daq() + + # Outputs: + self.sigConfigureCounter.connect(counter.configure, QtCore.Qt.QueuedConnection) + self.sigConfigureLaser.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) + + self.sigStartScan.connect(counter.start_counter, QtCore.Qt.QueuedConnection) + self.sigStartScan.connect(laser.start_scan, QtCore.Qt.QueuedConnection) + self.sigStartScan.connect(wavemeter.start_reading, QtCore.Qt.QueuedConnection) + + self.sigStopScan.connect(counter.stop_counter, QtCore.Qt.QueuedConnection) + self.sigStopScan.connect(laser.stop_scan, QtCore.Qt.QueuedConnection) + self.sigStopScan.connect(wavemeter.stop_reading, QtCore.Qt.QueuedConnection) + + self.sigStopCounting.connect(counter.stop_counter, QtCore.Qt.QueuedConnection) + self.sigStartCounting.connect(counter.start_counter, QtCore.Qt.QueuedConnection) + + # Inputs: + laser.sigScanStarted.connect(self._laser_scan_started) + laser.sigScanFinished.connect(self._laser_scan_finished) + wavemeter.sigNewRawData.connect(self._new_wavemeter_data) + counter.sigScanFinished.connect(self._process_counter_data) + daq.sigNewData.connect(self._new_daq_data) + + # Configure Counter: + self._record_length_s = self._record_length_ms * 1e-3 + self._bin_width_s = self._record_length_s + self.sigConfigureCounter.emit(self._bin_width_s, self._record_length_s) + + def on_deactivate(self): + pass From b4264a0a26737bc306ebd4ddcca16c8359e8ab83 Mon Sep 17 00:00:00 2001 From: lange50 Date: Thu, 13 Mar 2025 16:44:12 -0400 Subject: [PATCH 03/22] Added set wavelength command for scanning laser, also beginning to interpret the experiment, but probably need to talk it through at some point as it seems to be doing a _lot_ of stuff. --- src/qudi/hardware/laser/solstis_laser.py | 5 +- .../interface/scanning_laser_interface.py | 5 ++ src/qudi/logic/common/camera_logic.py | 10 ++++ src/qudi/logic/common/scanning_laser_logic.py | 8 +++ src/qudi/logic/grating_scan_logic.py | 52 ++++++++----------- 5 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index ed0560d..9ae6f50 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -230,4 +230,7 @@ def get_wavelength(self) -> float: except solstis.SolstisError as e: self.log.exception(f'Scan resume failure: {e.message}') - return -1 \ No newline at end of file + return -1 + + def set_wavelength(self, wavelength: float): + solstis.set_wave_m(self.socket, wavelength*1e3) \ No newline at end of file diff --git a/src/qudi/interface/scanning_laser_interface.py b/src/qudi/interface/scanning_laser_interface.py index 445b788..1bd39bd 100644 --- a/src/qudi/interface/scanning_laser_interface.py +++ b/src/qudi/interface/scanning_laser_interface.py @@ -153,4 +153,9 @@ def resume_scan(self): @abstractmethod def get_wavelength(self): """Gets the current wavelength (in um)""" + pass + + @abstractmethod + def set_wavelength(self, wavelength: float): + """Sets the current wavelength (in um)""" pass \ No newline at end of file diff --git a/src/qudi/logic/common/camera_logic.py b/src/qudi/logic/common/camera_logic.py index 02c78a0..f9d9290 100644 --- a/src/qudi/logic/common/camera_logic.py +++ b/src/qudi/logic/common/camera_logic.py @@ -127,6 +127,16 @@ def capture_frame(self): else: self.log.error('Unable to capture single frame. Acquisition still in progress.') + @QtCore.Slot(float, float) + def configure(self, exposure: float, gain: float) -> None: + """ Configure camera settings. + """ + try: + self.set_exposure(exposure) + self.set_gain(gain) + except Exception as e: + self.log.error(f'Failed to configure camera: {e}') + def toggle_video(self, start): if start: self._start_video() diff --git a/src/qudi/logic/common/scanning_laser_logic.py b/src/qudi/logic/common/scanning_laser_logic.py index 8028d44..6b20f3c 100644 --- a/src/qudi/logic/common/scanning_laser_logic.py +++ b/src/qudi/logic/common/scanning_laser_logic.py @@ -102,6 +102,14 @@ def tolerance(self) -> float: def set_wavelengths(self, start: float, stop: float) -> None: self._start_wavelength = max(start, self._min_wavelength) self._end_wavelength = min(stop, self._max_wavelength) + + + @QtCore.Slot(float) + def goto_wavelength(self, wavelength: float) -> None: + with self._thread_lock: + laser = self._laser() + laser.set_wavelength(wavelength) + self._wavelength = wavelength @QtCore.Slot() def start_scan(self): diff --git a/src/qudi/logic/grating_scan_logic.py b/src/qudi/logic/grating_scan_logic.py index 4bcdcd4..7dff366 100644 --- a/src/qudi/logic/grating_scan_logic.py +++ b/src/qudi/logic/grating_scan_logic.py @@ -14,7 +14,7 @@ from qudi.core.configoption import ConfigOption from qudi.core.statusvariable import StatusVar from qudi.util.datastorage import TextDataStorage - +from qudi.util.fit_models.exp_decay import ExponentialDecay class GratingScanData: def __init__(): @@ -35,19 +35,8 @@ class GratingScanLogic(LogicBase): laser: scanning_laser_logic options: calibrate: False # Set to True to calibrate before each measurement - calibration_data: - pt1: # ignored - angle: 76.3919 # in degrees - wavelength = 0.715 # in um - pt2: # ignored - angle: 76.1283 # in degrees - wavelength = 0.720 # in um - pt3: # ignored - angle: 75.9913 # in degrees - wavelength = 0.725 # in um - pt4: # ignored - angle: 75.5985 # in degrees - wavelength = 0.730 # in um + calibration_filepath: 'D:\Experiment data\diffraction_grating\Calibrations\calibration.csv' + calibration_data: [(angle, wavelength), ...] # tuples of grating angle and corresponding wavelength """ @@ -60,12 +49,9 @@ class GratingScanLogic(LogicBase): # declare config options _calibrate = ConfigOption(name='calibrate', default=False) _calibration_data = ConfigOption(name='calibration_data', - default={ - 'pt1': { - 'angle': 76.3919, - 'wavelength': 0.715 - } - },) + default=[(76.3919, 0.715), (76.1283, 0.720), (75.9913, 0.725), (75.5985, 0.730)]) + + _calibration_path = ConfigOption(name='calibration_filepath', default='path/to/calibration/file.csv') # status variables: _start_wavelength = StatusVar('start_wavelength', default=0.75) @@ -78,17 +64,14 @@ class GratingScanLogic(LogicBase): # Update signals, e.g. for GUI module sigWavelengthUpdated = QtCore.Signal(float) - sigCountsUpdated = QtCore.Signal(List[TerascanData]) + sigCountsUpdated = QtCore.Signal(List[GratingScanData]) - sigConfigureCounter = QtCore.Signal(float, float) - sigConfigureLaser = QtCore.Signal(float, float) + sigConfigureCamera = QtCore.Signal(float, float) + sigSetWavelength = QtCore.Signal(float) sigStartScan = QtCore.Signal() sigStopScan = QtCore.Signal() - sigStopCounting = QtCore.Signal() - sigStartCounting = QtCore.Signal() - sigScanFinished = QtCore.Signal() def __init__(self, *args, **kwargs): @@ -98,13 +81,13 @@ def __init__(self, *args, **kwargs): def on_activate(self): laser = self._laser() - counter = self._counter() - wavemeter = self._wavemeter() - daq = self._daq() + grating_m = self._grating_motor() + waveplate_m = self._waveplate_motor() + camera = self._camera() # Outputs: - self.sigConfigureCounter.connect(counter.configure, QtCore.Qt.QueuedConnection) - self.sigConfigureLaser.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) + self.sigSetWavelength.connect(laser.goto_wavelength, QtCore.Qt.QueuedConnection) + self.sigConfigureCamera.connect(camera.configure, QtCore.Qt.QueuedConnection) self.sigStartScan.connect(counter.start_counter, QtCore.Qt.QueuedConnection) self.sigStartScan.connect(laser.start_scan, QtCore.Qt.QueuedConnection) @@ -131,3 +114,10 @@ def on_activate(self): def on_deactivate(self): pass + + """ + Overview of the experiment: + 1. Calibrate the grating (if necessary) + a. Move the grating to a known angle with a known wavelength + b. Take a picture with the camera and a power reading with power meter + """ From 9320b6beb54d17ea36c0779ed9726eae7b2c7b0c Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Mon, 17 Mar 2025 15:11:17 -0400 Subject: [PATCH 04/22] Continuing work on the grating scan logic. Lots of additions including signals and internal processing. No actual data handling yet (beyond acquiring raw images) as I still don't understand what the current experiment _does_. --- src/qudi/logic/common/camera_logic.py | 5 +- src/qudi/logic/grating_scan_logic.py | 533 ++++++++++++++++++++++++-- 2 files changed, 498 insertions(+), 40 deletions(-) diff --git a/src/qudi/logic/common/camera_logic.py b/src/qudi/logic/common/camera_logic.py index f9d9290..9b3a8f7 100644 --- a/src/qudi/logic/common/camera_logic.py +++ b/src/qudi/logic/common/camera_logic.py @@ -52,7 +52,7 @@ class CameraLogic(LogicBase): missing='warn') # signals - sigFrameChanged = QtCore.Signal(object) + sigFrameChanged = QtCore.Signal(np.ndarray) sigAcquisitionFinished = QtCore.Signal() def __init__(self, *args, **kwargs): @@ -112,8 +112,9 @@ def get_gain(self): self._gain = self._camera().get_gain() return self._gain + @QtCore.Slot() def capture_frame(self): - """ + """ Capture a single frame """ with self._thread_lock: if self.module_state() == 'idle': diff --git a/src/qudi/logic/grating_scan_logic.py b/src/qudi/logic/grating_scan_logic.py index 7dff366..00956f7 100644 --- a/src/qudi/logic/grating_scan_logic.py +++ b/src/qudi/logic/grating_scan_logic.py @@ -5,6 +5,7 @@ import matplotlib.pyplot as plt from PySide2 import QtCore +from enum import Enum from typing import List from qudi.core.module import LogicBase @@ -16,9 +17,54 @@ from qudi.util.datastorage import TextDataStorage from qudi.util.fit_models.exp_decay import ExponentialDecay +from qudi.interface.daq_reader_interface import InputType, ReaderVal + class GratingScanData: - def __init__(): - pass + laser_wave: float + grating_angle: float + waveplate_angle: float + power_val: float + raw_data: np.ndarray + processed_data: np.ndarray + + def __init__(self, + laser_wave: float, + grating_angle: float, + waveplate_angle: float, + power_val: float, + raw_data: np.ndarray, + processed_data: np.ndarray = np.array([])): + + self.laser_wave = laser_wave + self.grating_angle = grating_angle + self.waveplate_angle = waveplate_angle + self.power_val = power_val + self.raw_data = raw_data + self.processed_data = processed_data + +class GratingScanConfig: + laser_wave_start: float + laser_wave_stop: float + laser_wave_step: float + grating_angle_start: float + grating_angle_stop: float + grating_angle_step: float + waveplate_angle_start: float + waveplate_angle_stop: float + waveplate_angle_step: float + num_repeats: int + calibrate: bool + calibrate_each_iter: bool + save_calibration: bool # will save each iteration if asked to + exposure_time_s: float + gain: int + + +class GratingScanState(Enum): + IDLE = 1, + CALIBRATING = 2, + ACQUIRING = 3 + class GratingScanLogic(LogicBase): """ @@ -33,10 +79,11 @@ class GratingScanLogic(LogicBase): grating_motor: grating_logic waveplate_motor: waveplate_logic laser: scanning_laser_logic + daq: daq_reader_logic # Note that this logic assumes there is exactly one (digital) input to the DAQ. options: - calibrate: False # Set to True to calibrate before each measurement calibration_filepath: 'D:\Experiment data\diffraction_grating\Calibrations\calibration.csv' - calibration_data: [(angle, wavelength), ...] # tuples of grating angle and corresponding wavelength + calibration_locations: [(angle, wavelength), ...] # tuples of grating angle and corresponding wavelength + normalization_factor_tol: 0.001 # If normalized intensity is below this limit, data will be excluded from processed spectrum """ @@ -45,79 +92,489 @@ class GratingScanLogic(LogicBase): _camera = Connector(name='camera', interface='CameraLogic') _grating_motor = Connector(name='grating_motor', interface='MotorLogic') _waveplate_motor = Connector(name='waveplate_motor', interface='MotorLogic') + _daq = Connector(name='daq', interface='DAQReaderLogic') # declare config options - _calibrate = ConfigOption(name='calibrate', default=False) - _calibration_data = ConfigOption(name='calibration_data', + _calibration_locations = ConfigOption(name='calibration_locations', default=[(76.3919, 0.715), (76.1283, 0.720), (75.9913, 0.725), (75.5985, 0.730)]) _calibration_path = ConfigOption(name='calibration_filepath', default='path/to/calibration/file.csv') + _norm_factor_tol = ConfigOption(name='normalization_factor_tol', default=0.001) + # status variables: - _start_wavelength = StatusVar('start_wavelength', default=0.75) - _end_wavelength = StatusVar('end_wavelength', default=0.8) - _current_wavelength = StatusVar('current_wavelength', default=0.75) + _wavelength_start = StatusVar('start_wavelength', default=0.75) + _wavelength_end = StatusVar('end_wavelength', default=0.75) + _wavelength_step = StatusVar('step_wavelength', default=0.05) + _current_wavelength: float + _wavelength_list: np.ndarray + + _grating_angle_start = StatusVar('start_grating_angle', default=75.0) + _grating_angle_stop = StatusVar('stop_grating_angle', default=75.0) + _grating_angle_step = StatusVar('grating_angle_step', default=0.05) + _current_grating_angle: float + _grating_angle_list: np.ndarray + _grating_moving: bool + + _waveplate_angle_start = StatusVar('start_grating_angle', default=0.0) + _waveplate_angle_stop = StatusVar('stop_grating_angle', default=0.0) + _waveplate_angle_step = StatusVar('waveplate_angle_step', default=0.05) + _current_waveplate_angle: float + _waveplate_angle_list: np.ndarray + _waveplate_moving: bool + + _num_repeats = StatusVar('num_repeats', default=int(1)) + _repeat_counter: int + _measurements_per_repeat: int # used internally, total number of images we will take + _measurement_counter: int + + _calibrate = StatusVar('calibrate', default=True) + _calibrate_each_iter = StatusVar('calibrate_each_iter', default=False) + _save_calibration = StatusVar('save_calibration', default=True) + + _exposure_time_s = StatusVar('exposure_time_s', default=0.25) + _gain = StatusVar('gain', default=int(50)) + _laser_locked = StatusVar('laser_locked', default=False) - _current_data = StatusVar('current_data', default=[]) # list of GratingScanData + _current_data: List[GratingScanData] + _calibration_data: List[GratingScanData] + + _calibration_data: List[GratingScanData] + _calibration_index: int + _current_data: List[GratingScanData] + + _power_val: float # Update signals, e.g. for GUI module sigWavelengthUpdated = QtCore.Signal(float) - sigCountsUpdated = QtCore.Signal(List[GratingScanData]) + sigDataUpdated = QtCore.Signal(object) # is a List[GratingScanData] + sigScanFinished = QtCore.Signal() + sigLaserLocked = QtCore.Signal(bool) + # Signals to control other logic modules sigConfigureCamera = QtCore.Signal(float, float) sigSetWavelength = QtCore.Signal(float) + sigSetWaveplateAngle = QtCore.Signal(float) + sigSetGratingAngle = QtCore.Signal(float) - sigStartScan = QtCore.Signal() - sigStopScan = QtCore.Signal() + sigStartCamera = QtCore.Signal() - sigScanFinished = QtCore.Signal() + # Internal signals: + _sigStartAcq = QtCore.Signal() + _sigNextAcq = QtCore.Signal() + _sigStopAcq = QtCore.Signal() + _sigCalibrate = QtCore.Signal() + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._thread_lock = RecursiveMutex() + self.__timer = None + + self._scan_state = GratingScanState.IDLE + + self._grating_moving = False + self._waveplate_moving = False + self._power_val = 0 def on_activate(self): laser = self._laser() grating_m = self._grating_motor() waveplate_m = self._waveplate_motor() camera = self._camera() + daq = self._daq() # Outputs: self.sigSetWavelength.connect(laser.goto_wavelength, QtCore.Qt.QueuedConnection) self.sigConfigureCamera.connect(camera.configure, QtCore.Qt.QueuedConnection) + self.sigSetGratingAngle.connect(grating_m.move, QtCore.Qt.QueuedConnection) + self.sigSetWaveplateAngle.connect(waveplate_m.move, QtCore.Qt.QueuedConnection) - self.sigStartScan.connect(counter.start_counter, QtCore.Qt.QueuedConnection) - self.sigStartScan.connect(laser.start_scan, QtCore.Qt.QueuedConnection) - self.sigStartScan.connect(wavemeter.start_reading, QtCore.Qt.QueuedConnection) - - self.sigStopScan.connect(counter.stop_counter, QtCore.Qt.QueuedConnection) - self.sigStopScan.connect(laser.stop_scan, QtCore.Qt.QueuedConnection) - self.sigStopScan.connect(wavemeter.stop_reading, QtCore.Qt.QueuedConnection) - - self.sigStopCounting.connect(counter.stop_counter, QtCore.Qt.QueuedConnection) - self.sigStartCounting.connect(counter.start_counter, QtCore.Qt.QueuedConnection) + self.sigStartCamera.connect(camera.capture_frame, QtCore.Qt.QueuedConnection) # Inputs: - laser.sigScanStarted.connect(self._laser_scan_started) - laser.sigScanFinished.connect(self._laser_scan_finished) - wavemeter.sigNewRawData.connect(self._new_wavemeter_data) - counter.sigScanFinished.connect(self._process_counter_data) + camera.sigFrameChanged.connect(self._new_camera_frame, QtCore.Qt.QueuedConnection) + camera.sigAcquisitionFinished.connect(self._camera_acquisiton_finished, QtCore.Qt.QueuedConnection) daq.sigNewData.connect(self._new_daq_data) - # Configure Counter: - self._record_length_s = self._record_length_ms * 1e-3 - self._bin_width_s = self._record_length_s - self.sigConfigureCounter.emit(self._bin_width_s, self._record_length_s) + grating_m.sigMoveFinished.connect(self._grating_move_finished, QtCore.Qt.QueuedConnection) + waveplate_m.sigMoveFinished.connect(self._waveplate_move_finished, QtCore.Qt.QueuedConnection) + + # Internal signals: (Queued) + self._sigStartAcq.connect(self._start_acq, QtCore.Qt.QueuedConnection) + self._sigStopAcq.connect(self._stop_acq, QtCore.Qt.QueuedConnection) + self._sigNextAcq.connect(self._next_acq, QtCore.Qt.QueuedConnection) + self._sigCalibrate.connect(self._next_calibration, QtCore.Qt.QueuedConnection) + + # Timer for checking if we're ready: + self.__timer = QtCore.QTimer() + self.__timer.timeout.connect(self.__wait_for_ready) + self.__timer.setSingleShot(True) def on_deactivate(self): + if (self.__timer is not None): + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + @property + def locked(self) -> bool: + return self._laser_locked + + @property + def moving(self) -> bool: + return not self._grating_moving and not self._waveplate_moving + + @property + def power(self) -> float: + return self._power_val + + @property + def status(self) -> GratingScanState: + return self._scan_state + + @QtCore.Slot() + def start_scan(self): + with self._thread_lock: + if self.module_state() == 'idle': + self.module_state.lock() + self._current_data = [] + self._calibration_data = [] + self._calibration_index = 0 + self.sigDataUpdated.emit(self._current_data) + + # Configure Camera: + self.sigConfigureCamera.emit(self._exposure_time_s, self._gain) + + # Move motors if needed: + self._move_motors(self._grating_angle_start, self._waveplate_angle_start) + + num_waves = np.abs( + int((self._wavelength_end - self._wavelength_start) / + self._wavelength_step)) + 1 + + num_grating_angles = np.abs( + int((self._grating_angle_stop - self._grating_angle_start) / + self._grating_angle_step)) + 1 + + num_waveplate_angles = np.abs( + int((self._waveplate_angle_stop - self._waveplate_angle_start) / + self._waveplate_angle_step)) + 1 + + self._measurements_per_repeat = num_waves * num_grating_angles \ + * num_waveplate_angles + + self._measurement_counter = 0 + self._repeat_counter = 0 + + + self._wavelength_list = np.linspace(self._wavelength_start, + self._wavelength_end, + num_waves) + self._waveplate_angle_list = np.arange(self._waveplate_angle_start, + self._waveplate_angle_stop, + num_grating_angles) + self._waveplate_angle_list = np.arange(self._waveplate_angle_start, + self._waveplate_angle_stop, + num_waveplate_angles) + + self._sigStartAcq.emit() + + @QtCore.Slot() + def stop_scan(self): + with self._thread_lock: + if self.module_state() == 'locked': + self._sigStopAcq.emit() + + + @QtCore.Slot(object) + def configure_scan(self, config: GratingScanConfig): + with self._thread_lock: + if self.module_state() == 'idle': + self._wavelength_start = config.laser_wave_start + self._wavelength_end = config.laser_wave_stop + self._wavelength_step = config.laser_wave_step + + self._grating_angle_start = config.grating_angle_start + self._grating_angle_stop = config.grating_angle_stop + self._grating_angle_step = config.grating_angle_step + + self._waveplate_angle_start = config.waveplate_angle_start + self._waveplate_angle_stop = config.waveplate_angle_stop + self._waveplate_angle_step = config.waveplate_angle_step + + + self._calibrate = config.calibrate + self._calibrate_each_iter = config.calibrate_each_iter + self._save_calibration = config.save_calibration + + self._exposure_time_s = config.exposure_time_s + self._gain = config.gain + + else: + self.__logger.warning( + 'Tried to configure while a scan was running.'\ + 'Please wait until it is finished or stop it.') + + @QtCore.Slot() + def _start_acq(self): + """ Internal method for starting a scan + """ + with self._thread_lock: + if self.module_state() == 'locked': + if not self._needs_cal(): + self._load_calibration() + + self._sigNextAcq.emit() + + @QtCore.Slot() + def _stop_acq(self): + """ Internal method for stopping a scan + """ + with self._thread_lock: + if self.module_state() == 'locked': + # Stop motors: + grating_m = self._grating_motor() + waveplate_m = self._waveplate_motor() + + if (grating_m.moving): + grating_m.stop() + if (waveplate_m.moving): + waveplate_m.stop() + + self._measurement_counter = self._measurements_per_repeat + self._repeat_counter = self._num_repeats + self.module_state.unlock() + + @QtCore.Slot() + def _next_acq(self): + with self._thread_lock: + if self.module_state() == 'locked': + if self._needs_cal(): + self._scan_state = GratingScanState.CALIBRATING + self._sigCalibrate.emit() + + else: + self._scan_state = GratingScanState.ACQUIRING + wavelength_id, grating_id, waveplate_id = self._counter_to_indices() + + wavelength = self._wavelength_list[wavelength_id] + grating = self._grating_angle_list[grating_id] + waveplate = self._waveplate_angle_list[waveplate_id] + + self._set_wavelength(wavelength=wavelength) + self._move_motors(grating=grating, waveplate=waveplate) + + self.__timer.start(1) + + + + + + @QtCore.Slot() + def _next_calibration(self): + """ Performs next calibration or signals for the start of the + acquisition if calibration is completed (for now) + """ + with self._thread_lock: + if self.module_state() == 'locked': + self._scan_state = GratingScanState.CALIBRATING + + if not self._needs_cal(): + if (self._save_calibration): + self._store_calibration() + + self._calibration_index = 0 + self._sigNextAcq.emit() + + else: + # TODO: Waveplate Cal???? + grating, wavelength = self._calibration_locations[self._calibration_index] + self._set_wavelength(wavelength=wavelength) + self._move_motors(grating=grating) + self.__timer.start(1) + + + + def _store_calibration(self): + ds = TextDataStorage( + root_dir=self.module_default_data_dir, + column_formats='.15e' + ) + + ds.save_data(self._calibration_data) + self.log.info(f'Calibration data saved to {self._calibration_path}') + + def _load_calibration(self): + ds = TextDataStorage( + root_dir=self.module_default_data_dir, + column_formats='.15e' + ) + + self._calibration_data, _, _ = ds.load_data(self._calibration_path) + if (len(self._calibration_data) == 0): + self.log.warning(f'Calibration data not found at path {self._calibration_path}') + return + + + + def _needs_cal(self) -> bool: + if not self._calibrate_each_iter and \ + self._calibration_index == len(self._calibration_data): + return False + + if (self._calibrate + and self._repeat_counter == 0 + and self._measurement_counter == 0): + return True + + if (self._calibrate_each_iter + and self._measurement_counter == 0): + return True + + return False + + def _set_wavelength(self, wavelength: float): + if (self._current_wavelength != wavelength): + self.sigSetWavelength.emit(wavelength) + + def _move_motors(self, grating: float = None, waveplate: float = None): + """ Assumes thread locked externally + """ + # Move motors if needed: + grating_m = self._grating_motor() + waveplate_m = self._waveplate_motor() + + if (grating is not None \ + and grating_m.position != grating \ + and grating != self._current_grating_angle): + self._grating_moving = True + self.sigSetGratingAngle.emit(grating) + + if (waveplate is not None \ + and waveplate_m.position != waveplate \ + and waveplate != self._current_waveplate_angle): + self._waveplate_moving = True + self.sigSetWaveplateAngle.emit(waveplate) + + + def _counter_to_indices(self) -> tuple[int, int, int]: + """ Utility function to convert self._measurement_counter to indices + appropriate for passing to wavelength_list, grating_angle_list, and + waveplate_angle_list. + + @return (wave_ind, grating_ind, waveplate_ind) + """ + + wave_ind = 0 + grating_ind = 0 + waveplate_ind = 0 + + # num_waves = len(self._wavelength_list) + num_grating = len(self._grating_angle_list) + num_waveplate = len(self._waveplate_angle_list) + + for _ in self._wavelength_list: + for _ in self._grating_angle_list: + for _ in self._waveplate_angle_list: + waveplate_ind += 1 + if (waveplate_ind - 1) \ + + num_waveplate * grating_ind \ + + num_grating * wave_ind \ + == self._measurement_counter: + return (wave_ind, grating_ind, waveplate_ind) + waveplate_ind = 0 + grating_ind += 1 + grating_ind = 0 + wave_ind += 1 + + raise ValueError('Measurement counter out of range') + + + def __wait_for_ready(self): + """ + Wait until motors are stopped and laser is locked. Assumes thread is + locked externally + """ + if (self.locked and not self.moving): + self.sigStartCamera.emit() + else: + self.__timer.start(50) # wait 50 ms, then check again + + + + @QtCore.Slot(np.ndarray) + def _new_camera_frame(self, data: np.ndarray): + scan_data = GratingScanData( + laser_wave=self._current_wavelength, + grating_angle=self._current_grating_angle, + waveplate_angle=self._current_waveplate_angle, + power_val=self.power, + raw_data=data + ) + + if self._scan_state == GratingScanState.CALIBRATING: + self._calibration_data.append(scan_data) + self._process_calibration() + self._calibration_index += 1 + self._sigCalibrate.emit() + else: + self._current_data.append(scan_data) + self._process_data() + + self._measurement_counter += 1 + if (self._measurement_counter >= self._measurements_per_repeat): + self._measurement_counter = 0 + self._repeat_counter += 1 + + self._sigNextAcq.emit() + + @QtCore.Slot() + def _camera_acquisition_finished(self): + # This is connected, but we are assuming single frames so it does nothing pass - """ - Overview of the experiment: - 1. Calibrate the grating (if necessary) - a. Move the grating to a known angle with a known wavelength - b. Take a picture with the camera and a power reading with power meter - """ + def _process_data(self): + """ Modifies last element of self._current_data """ + if (len(self._calibration_data) == 0): + self.log.warning('No calibration data, not processing raw data') + return + + pass + + def _process_calibration(self): + """ Modifies last element of self._calibration_data + """ + + pass + + @QtCore.Slot(object) + def _new_daq_data(self, data: List[ReaderVal]): + with self._thread_lock: + if self.module_state() == 'locked': + for i in data: + if i.type is InputType.DIGITAL: + # We assume there is only one digital input for this measurement + if i.val and not self._laser_locked: + self._laser_locked = True + + if not i.val and self._laser_locked: + self._laser_locked = False + + self.sigLaserLocked.emit(self._laser_locked) + + + @QtCore.Slot() + def _grating_move_finished(self): + grating_m = self._grating_motor() + self._current_grating_angle = grating_m.position() + self._grating_moving = False + + @QtCore.Slot() + def _waveplate_move_finished(self): + waveplate_m = self._waveplate_motor() + self._current_waveplate_angle = waveplate_m.position() + self._waveplate_moving = False \ No newline at end of file From 98e2e19e68a1e91af7767e717a1129a26e40d131 Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Mon, 17 Mar 2025 15:11:48 -0400 Subject: [PATCH 05/22] Adding a powermeter interface, hardware, dummy, and logic. --- src/qudi/hardware/dummy/powermeter_dummy.py | 29 +++++++ .../powermeter/thorlabs_power_meter.py | 42 ++++++++++ .../interface/simple_powermeter_interface.py | 17 ++++ src/qudi/logic/common/powermeter_logic.py | 80 +++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 src/qudi/hardware/dummy/powermeter_dummy.py create mode 100644 src/qudi/hardware/powermeter/thorlabs_power_meter.py create mode 100644 src/qudi/interface/simple_powermeter_interface.py create mode 100644 src/qudi/logic/common/powermeter_logic.py diff --git a/src/qudi/hardware/dummy/powermeter_dummy.py b/src/qudi/hardware/dummy/powermeter_dummy.py new file mode 100644 index 0000000..6e56420 --- /dev/null +++ b/src/qudi/hardware/dummy/powermeter_dummy.py @@ -0,0 +1,29 @@ +import numpy as np +from time import sleep +from qudi.core.configoption import ConfigOption +from qudi.interface.simple_powermeter_interface import SimplePowermeterInterface + +class DummyPowerMeter(SimplePowermeterInterface): + """ Hardware class for Thorlabs power meter. Assumes only one meter is attached + Example config for copy-paste: + + powermeter_dummy: + # This module assumes only one thorlabs meter is attached + module.Class: 'powermeter.thorlabs_power_meter.ThorlabsPowerMeter' + """ + + _averages = ConfigOption( + name='average_count', + default=5 + ) + + def on_activate(self): + pass + + + def on_deactivate(self): + pass + + def get_power(self): + sleep(0.01) # simulate 10 ms acq. time + return 10 * np.random.rand() \ No newline at end of file diff --git a/src/qudi/hardware/powermeter/thorlabs_power_meter.py b/src/qudi/hardware/powermeter/thorlabs_power_meter.py new file mode 100644 index 0000000..18edaac --- /dev/null +++ b/src/qudi/hardware/powermeter/thorlabs_power_meter.py @@ -0,0 +1,42 @@ + +from ThorlabsPM100 import ThorlabsPM100, USBTMC +# ref: https://github.com/clade/ThorlabsPM100 + +from qudi.core.configoption import ConfigOption +from qudi.interface.simple_powermeter_interface import SimplePowermeterInterface + +class ThorlabsPowerMeter(SimplePowermeterInterface): + """ Hardware class for Thorlabs power meter. Assumes only one meter is attached + Example config for copy-paste: + + powermeter_thorlabs: + # This module assumes only one thorlabs meter is attached + module.Class: 'powermeter.thorlabs_power_meter.ThorlabsPowerMeter' + + options: + average_count: 5 # Internal samples to average in the power meter + """ + + _averages = ConfigOption( + name='average_count', + default=5 + ) + + def on_activate(self): + inst = USBTMC() + self._meter = ThorlabsPM100(inst=inst) + self._meter.sense.power.dc.range.auto = "ON" # auto-range + self._meter.input.pdiode.filter.lpass.state = 0 # high bandwidth, 1 for low + self.set_average_count(self._averages) + + + def on_deactivate(self): + pass + + def get_power(self): + return self._meter.read + + + def set_average_count(self, count: int): + self._averages = count + self._meter.sense.average.count = self._averages \ No newline at end of file diff --git a/src/qudi/interface/simple_powermeter_interface.py b/src/qudi/interface/simple_powermeter_interface.py new file mode 100644 index 0000000..eb36651 --- /dev/null +++ b/src/qudi/interface/simple_powermeter_interface.py @@ -0,0 +1,17 @@ +from abc import abstractmethod +from qudi.core.module import Base + + +class SimplePowermeterInterface(Base): + """ This interface is for simple use of a power meter. + It just gets the power from the device. + + """ + + @abstractmethod + def get_power(self) -> float: + """ Retrieve the current wavelength(s) from the wavemeter + + @return float: power on the device + """ + pass \ No newline at end of file diff --git a/src/qudi/logic/common/powermeter_logic.py b/src/qudi/logic/common/powermeter_logic.py new file mode 100644 index 0000000..a878798 --- /dev/null +++ b/src/qudi/logic/common/powermeter_logic.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +""" +A module for controlling a simple power meter. + +Copyright (c) 2025, QuPIDC qudi developers. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" + + +from PySide2 import QtCore +from qudi.core.connector import Connector +from qudi.core.configoption import ConfigOption +from qudi.util.mutex import RecursiveMutex +from qudi.core.module import LogicBase + +class PowerMeterLogic(LogicBase): + """ Logic class for controlling a camera. + + Example config for copy-paste: + + powermeter_logic: + module.Class: 'common.powermeter_logic.PowerMeterLogic' + connect: + meter: powermeter_dummy + options: + update_interval: 0 # Period in ms to check for data updates. Integers only. 0 is as fast as possible + + """ + + # declare connectors + _meter = Connector(name='meter', interface='SimplePowerMeterInterface') + + # declare config options + _update_interval = ConfigOption(name='update_period', + default=0, + missing='info') + + + + # signals + sigNewData = QtCore.Signal(float) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() + + def on_activate(self): + """ Initialisation performed during activation of the module. + """ + self.__timer = QtCore.QTimer() + self.__timer.timeout.connect(self.__data_update) + self.__timer.setSingleShot(False) + self.__timer.start(int(self._update_interval)) + + + def on_deactivate(self): + """ Perform required deactivation. """ + if (self.__timer is not None): + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + + def __data_update(self): + with self._thread_lock: + meter = self._meter() + data = meter.get_power() + self.sigNewData.emit(data) \ No newline at end of file From b5ca3ae4ba975d58eca250855b5f8e8090c08ca6 Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Tue, 18 Mar 2025 08:47:37 -0400 Subject: [PATCH 06/22] Adjusted how calibration is handled. Likely still needs work. --- src/qudi/logic/grating_scan_logic.py | 53 ++++++++++++++-------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/src/qudi/logic/grating_scan_logic.py b/src/qudi/logic/grating_scan_logic.py index 00956f7..b334ecd 100644 --- a/src/qudi/logic/grating_scan_logic.py +++ b/src/qudi/logic/grating_scan_logic.py @@ -80,9 +80,9 @@ class GratingScanLogic(LogicBase): waveplate_motor: waveplate_logic laser: scanning_laser_logic daq: daq_reader_logic # Note that this logic assumes there is exactly one (digital) input to the DAQ. + powermeter: powermeter_logic # assumes there is only one Thorlabs power meter attached to system options: - calibration_filepath: 'D:\Experiment data\diffraction_grating\Calibrations\calibration.csv' - calibration_locations: [(angle, wavelength), ...] # tuples of grating angle and corresponding wavelength + calibration_directory: 'D:\Experiment data\diffraction_grating\Calibrations' normalization_factor_tol: 0.001 # If normalized intensity is below this limit, data will be excluded from processed spectrum """ @@ -93,12 +93,10 @@ class GratingScanLogic(LogicBase): _grating_motor = Connector(name='grating_motor', interface='MotorLogic') _waveplate_motor = Connector(name='waveplate_motor', interface='MotorLogic') _daq = Connector(name='daq', interface='DAQReaderLogic') + _powermeter = Connector(name='powermeter', interface='PowerMeterLogic') # declare config options - _calibration_locations = ConfigOption(name='calibration_locations', - default=[(76.3919, 0.715), (76.1283, 0.720), (75.9913, 0.725), (75.5985, 0.730)]) - - _calibration_path = ConfigOption(name='calibration_filepath', default='path/to/calibration/file.csv') + _calibration_directory = ConfigOption(name='calibration_directory', default='path/to/calibration/file.csv') _norm_factor_tol = ConfigOption(name='normalization_factor_tol', default=0.001) @@ -186,6 +184,7 @@ def on_activate(self): waveplate_m = self._waveplate_motor() camera = self._camera() daq = self._daq() + powermeter = self._powermeter() # Outputs: self.sigSetWavelength.connect(laser.goto_wavelength, QtCore.Qt.QueuedConnection) @@ -198,7 +197,8 @@ def on_activate(self): # Inputs: camera.sigFrameChanged.connect(self._new_camera_frame, QtCore.Qt.QueuedConnection) camera.sigAcquisitionFinished.connect(self._camera_acquisiton_finished, QtCore.Qt.QueuedConnection) - daq.sigNewData.connect(self._new_daq_data) + daq.sigNewData.connect(self._new_daq_data, QtCore.Qt.QueuedConnection) + powermeter.sigNewData.connect(self._new_powermeter_data, QtCore.Qt.QueuedConnection) grating_m.sigMoveFinished.connect(self._grating_move_finished, QtCore.Qt.QueuedConnection) waveplate_m.sigMoveFinished.connect(self._waveplate_move_finished, QtCore.Qt.QueuedConnection) @@ -353,22 +353,20 @@ def _stop_acq(self): def _next_acq(self): with self._thread_lock: if self.module_state() == 'locked': + wavelength_id, grating_id, waveplate_id = self._counter_to_indices() + + wavelength = self._wavelength_list[wavelength_id] + grating = self._grating_angle_list[grating_id] + waveplate = self._waveplate_angle_list[waveplate_id] + + self._move_motors(grating=grating, waveplate=waveplate) + if self._needs_cal(): self._scan_state = GratingScanState.CALIBRATING self._sigCalibrate.emit() - else: - self._scan_state = GratingScanState.ACQUIRING - wavelength_id, grating_id, waveplate_id = self._counter_to_indices() - - wavelength = self._wavelength_list[wavelength_id] - grating = self._grating_angle_list[grating_id] - waveplate = self._waveplate_angle_list[waveplate_id] - - self._set_wavelength(wavelength=wavelength) - self._move_motors(grating=grating, waveplate=waveplate) - - self.__timer.start(1) + self._set_wavelength(wavelength=wavelength) + self.__timer.start(1) @@ -388,14 +386,13 @@ def _next_calibration(self): self._store_calibration() self._calibration_index = 0 + self._scan_state = GratingScanState.ACQUIRING self._sigNextAcq.emit() else: - # TODO: Waveplate Cal???? - grating, wavelength = self._calibration_locations[self._calibration_index] + # TODO: Fix to generate correct calibration wavelengths for this grating angle + # grating, wavelength = self._calibration_locations[self._calibration_index] self._set_wavelength(wavelength=wavelength) - self._move_motors(grating=grating) - self.__timer.start(1) @@ -406,7 +403,7 @@ def _store_calibration(self): ) ds.save_data(self._calibration_data) - self.log.info(f'Calibration data saved to {self._calibration_path}') + self.log.info(f'Calibration data saved to {self._calibration_directory}') def _load_calibration(self): ds = TextDataStorage( @@ -414,9 +411,9 @@ def _load_calibration(self): column_formats='.15e' ) - self._calibration_data, _, _ = ds.load_data(self._calibration_path) + self._calibration_data, _, _ = ds.load_data(self._calibration_directory) if (len(self._calibration_data) == 0): - self.log.warning(f'Calibration data not found at path {self._calibration_path}') + self.log.warning(f'Calibration data not found at path {self._calibration_directory}') return @@ -566,6 +563,10 @@ def _new_daq_data(self, data: List[ReaderVal]): self.sigLaserLocked.emit(self._laser_locked) + @QtCore.Slot(float) + def _new_powermeter_data(self, power: float): + self._power_val = power + @QtCore.Slot() def _grating_move_finished(self): From a97696e32a1f486194090cbdc551311fdd44f8ae Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Thu, 20 Mar 2025 11:16:55 -0400 Subject: [PATCH 07/22] Adding in grating scan GUI and main window code. --- src/qudi/gui/grating_scan/grating_scan_gui.py | 285 ++++++++++++++++++ .../grating_scan/grating_scan_main_window.py | 127 ++++++++ src/qudi/gui/terascan/terascan_gui.py | 9 +- src/qudi/logic/common/camera_logic.py | 8 + src/qudi/logic/common/scanning_laser_logic.py | 3 +- src/qudi/logic/grating_scan_logic.py | 129 ++++++-- 6 files changed, 541 insertions(+), 20 deletions(-) create mode 100644 src/qudi/gui/grating_scan/grating_scan_gui.py create mode 100644 src/qudi/gui/grating_scan/grating_scan_main_window.py diff --git a/src/qudi/gui/grating_scan/grating_scan_gui.py b/src/qudi/gui/grating_scan/grating_scan_gui.py new file mode 100644 index 0000000..2d3d757 --- /dev/null +++ b/src/qudi/gui/grating_scan/grating_scan_gui.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- + +__all__ = ['TerascanGui'] + +import numpy as np +import os +from PySide2 import QtCore, QtGui +from typing import List + +from qudi.util.datastorage import TextDataStorage +from qudi.core.module import GuiBase +from qudi.core.connector import Connector +from qudi.core.statusvariable import StatusVar +from qudi.gui.grating_scan.grating_scan_gui import GratingScanMainWindow +from qudi.util.paths import get_artwork_dir + +from qudi.logic.grating_scan_logic import GratingScanData, GratingScanConfig + +""" +Note that this Gui is currently designed for single-measurements only. Similarly, +the logic only really expects to take a single measurement at a time. + +Things are nominally in place to support sweeps, but everything would need to be +put together to support that and the calibration stuff would need to be adapted +to work with sweeps. + + exaple config for copy-paste: + grating_scan_gui: + module.Class: 'grating_scan.grating_scan_gui.GratingScanGui' + connect: + grating_scan_logic: grating_scan_logic +""" + +# qudi GUI measurement modules must inherit qudi.core.module.GuiBase or other GUI modules. +class GratingScanGui(GuiBase): + """ Terascan Measurement GUI """ + # Signal declaration for outgoing control signals to logic + sigStartMeasurement = QtCore.Signal() + sigStopMeasurement = QtCore.Signal() + sigConfigMeasurement = QtCore.Signal(object) # GratingScanConfig + sigSaveData = QtCore.Signal() + + # One logic module + _grating_scan_logic = Connector(name='grating_scan_logic', interface='GratingScanLogic') + + # Declare static parameters that can/must be declared in the qudi configuration + # _my_config_option = ConfigOption(name='my_config_option', default=1, missing='warn') + + # Declare status variables that are saved in the AppStatus upon deactivation of the module and + # are initialized to the saved value again upon activation. + # _start_wavelength = StatusVar(name='start_wavelength', default=0.785) + # _stop_wavelength = StatusVar(name='stop_wavelength', default=0.785) + _current_wavelength: float + + # _grating_angle_start = StatusVar(name='grating_angle_start', default=75.0) + # _grating_angle_stop = StatusVar(name='grating_angle_stop', default=75.0) + _grating_angle_current: float + + # _waveplate_angle_start = StatusVar(name='waveplate_angle_start', default=0.0) + # _waveplate_angle_stop = StatusVar(name='waveplate_angle_stop', default=0.0) + _waveplate_angle_current: float + + # _gain = StatusVar(name='gain', default=int(50)) + # _exposure_time_s = StatusVar(name='exposure_time_s', default=0.25) + + _config: GratingScanConfig = StatusVar(name='config', default=GratingScanConfig()) + + def on_activate(self) -> None: + # initialize the main window + self._mw = GratingScanMainWindow() + self._mw.start_wavelength.setValue(self._config.laser_wave_start) + self._mw.grating_angle.setValue(self._config.grating_angle_start) + self._mw.waveplate_angle.setValue(self._config.waveplate_angle_start) + self._mw.gain.setValue(self._config.gain) + self._mw.exposure_s.setValue(self._config.exposure_time_s) + self._mw.calibrate.setValue(self._config.calibrate) + + # connect all GUI internal signals + self._mw.start_wavelength.valueChanged.connect(self._start_changed) + self._mw.grating_angle.valueChanged.connect(self._grating_angle_changed) + self._mw.waveplate_angle.valueChanged.connect(self._waveplate_angle_changed) + self._mw.gain.valueChanged.connect(self._gain_changed) + self._mw.exposure_s.valueChanged.connect(self._exposure_time_changed) + self._mw.calibrate.checkStateChanged.connect(self._calibrate_changed) + + + self._mw.start_stop_button.clicked.connect(self._start_stop_pressed) + self._mw.action_save_data.triggered.connect(self._save_data) + + # Connect all signals to and from the logic. Make sure the connections are QueuedConnection. + # Inputs: + self._grating_scan_logic().sigWavelengthUpdated.connect( + self._wavelength_changed, QtCore.Qt.QueuedConnection + ) + + self._grating_scan_logic().sigCountsUpdated.connect( + self._receive_data, QtCore.Qt.QueuedConnection + ) + + self._grating_scan_logic().sigScanFinished.connect( + self._scan_finished, QtCore.Qt.QueuedConnection + ) + + self._grating_scan_logic().sigLaserLocked.connect( + self._laser_lock_ui, QtCore.Qt.QueuedConnection + ) + + + # Outputs: + self.sigStartMeasurement.connect( + self._grating_scan_logic().start_scan, QtCore.Qt.QueuedConnection + ) + self.sigStopMeasurement.connect( + self._grating_scan_logic().stop_scan, QtCore.Qt.QueuedConnection + ) + + self.sigConfigMeasurement.connect( + self._grating_scan_logic().configure_scan, QtCore.Qt.QueuedConnection + ) + + + + self._data = [] + + # Turn on update timer: + self.__timer = QtCore.QTimer() + self.__timer.setSingleShot(False) + self.__timer.timeout.connect(self.__update) + + # Show the main window and raise it above all others + self.show() + + def on_deactivate(self) -> None: + # Disconnect all connections done in "on_activate" that are inputs + self._grating_scan_logic().sigWavelengthUpdated.disconnect(self._wavelength_changed) + self._grating_scan_logic().sigCountsUpdated.disconnect(self._receive_data) + self._grating_scan_logic().sigScanFinished.disconnect(self._scan_finished) + self._grating_scan_logic().sigLaserLocked.disconnect(self._laser_lock_ui) + + # Use "plain" disconnects (without argument) only on signals owned by this module + self._mw.start_wavelength.valueChanged.disconnect() + self._mw.grating_angle.valueChanged.disconnect() + self._mw.grating_angle.valueChanged.disconnect() + self._mw.waveplate_angle.valueChanged.disconnect() + self._mw.gain.valueChanged.disconnect() + self._mw.exposure_s.valueChanged.disconnect() + self._mw.calibrate.checkStateChanged.disconnect() + + self._mw.start_stop_button.clicked.disconnect() + + self.sigStartMeasurement.disconnect() + self.sigStopMeasurement.disconnect() + self.sigConfigMeasurement.disconnect() + + # disable update timer: + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + # Close main window + self._mw.close() + + + def show(self) -> None: + """ Mandatory method to show the main window """ + self._mw.show() + self._mw.raise_() + + + ### Handlers for Signals from UI ### + @QtCore.Slot(float) + def _start_changed(self, wave: float) -> None: + """ Qt slot to be called upon wavelength change """ + self._config.laser_wave_start = wave + self._config.laser_wave_stop = wave + # NOTE: THIS IS CODED FOR A SINGLE WAVELENGTH AT PRESENT + self.sigConfigMeasurement.emit(self._config) + + + @QtCore.Slot(float) + def _stop_changed(self, wave: float) -> None: + """ Qt slot to be called upon wavelength change """ + # self._stop_wavelength = wave + # self.sigSetWavelengths.emit(self._start_wavelength, self._stop_wavelength) + pass + + + @QtCore.Slot() + def _start_stop_pressed(self) -> None: + """ Qt slot to be called upon wavelength change """ + if self._mw.start_stop_button.text() == 'Start Measurement': + self._update_ui(True) + self.__timer.start(250) + self.sigStartMeasurement.emit() + else: + self._update_ui(False) + self.__timer.stop() + self.sigStopMeasurement.emit() + + + @QtCore.Slot(float) + def _grating_angle_changed(self, angle: float): + self._config.grating_angle_start = angle + self._config.grating_angle_stop = angle + # NOTE: THIS IS CODED FOR A SINGLE ANGLE + self.sigConfigMeasurement.emit(self._config) + + @QtCore.Slot(float) + def _waveplate_angle_changed(self, angle: float): + self._config.waveplate_angle_start = angle + self._config.waveplate_angle_stop = angle + # NOTE: THIS IS CODED FOR A SINGLE ANGLE + self.sigConfigMeasurement.emit(self._config) + + + @QtCore.Slot(int) + def _gain_changed(self, gain: int): + self._config.gain = gain + self.sigConfigMeasurement.emit(self._config) + + @QtCore.Slot(float) + def _exposure_time_changed(self, exposure_s: float): + self._config.exposure_time_s = exposure_s + self.sigConfigMeasurement.emit(self._config) + + @QtCore.Slot(bool) + def _calibrate_changed(self, calibrate: bool): + self._config.calibrate = calibrate + self.sigConfigMeasurement.emit(self._config) + + ### END HANDLERS FROM UI ### + + def _update_ui(self, running: bool) -> None: + if running: + self._mw.start_stop_button.setText('Stop Measurement') + self._mw.plot_widget.setXRange(self._start_wavelength, self._stop_wavelength) + self._mw._statusbar.clearMessage() + self._mw._progress_bar.setValue(0) + + else: + self._mw.start_stop_button.setText('Start Measurement') + self._mw._statusbar.showMessage('Ready') + + ### BEGIN HANDLERS FROM LOGIC ### + @QtCore.Slot() + def _scan_finished(self) -> None: + self._mw.start_stop_button.setText('Start Measurement') + + @QtCore.Slot(object) + def _receive_data(self, data: List[GratingScanData]) -> None: + self._data = data + + + @QtCore.Slot(float) + def _wavelength_changed(self, wave: float) -> None: + self._current_wavelength = wave + percent = 100 * (wave*1e-3 - self._start_wavelength) / (self._stop_wavelength - self._start_wavelength); + self._mw._progress_bar.setValue(int(round(percent))) + + @QtCore.Slot() + def _save_data(self) -> None: + ds = TextDataStorage( + root_dir=self.module_default_data_dir, + column_formats='.15e' + ) + + ds.save_data(self._data) + + @QtCore.Slot(bool) + def _laser_lock_ui(self, locked: bool) -> None: + icon = 'network-connect' if locked else 'network-disconnect' + pix = QtGui.QPixmap(os.path.join(get_artwork_dir(), 'icons', icon)) + self._mw._locked_indicator.setPixmap(pix.scaled(16, 16)) + + ### END HANDLERS FROM LOGIC ### + + def __update(self) -> None: + self._mw.status_label.setText(self._grating_scan_logic().status.value) + if (len(self._data) == 0): + return + + self._mw.data_item.setData( + x = self._data[-1].processed_data[:,0], + y = self._data[-1].processed_data[:,1]) + diff --git a/src/qudi/gui/grating_scan/grating_scan_main_window.py b/src/qudi/gui/grating_scan/grating_scan_main_window.py new file mode 100644 index 0000000..54843d2 --- /dev/null +++ b/src/qudi/gui/grating_scan/grating_scan_main_window.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- + +__all__ = ['GratingScanMainWindow'] + +import os +from PySide2 import QtGui, QtCore, QtWidgets +import pyqtgraph as pg + +from qudi.util.paths import get_artwork_dir +from qudi.util.colordefs import QudiPalettePale as palette + +class GratingScanMainWindow(QtWidgets.QMainWindow): + """ Main window for Grating Scan measurement + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Construct a simple GUI window with some widgets to tinker with + self.setWindowTitle('Grating Scan Measurement') + + + # Create menu bar + menu_bar = QtWidgets.QMenuBar() + menu = menu_bar.addMenu('File') + self.action_save_data = QtWidgets.QAction('Save Data') + path = os.path.join(get_artwork_dir(), 'icons', 'document-save') + self.action_save_data.setIcon(QtGui.QIcon(path)) + menu.addAction(self.action_save_data) + menu.addSeparator() + + self.action_close = QtWidgets.QAction('Close') + path = os.path.join(get_artwork_dir(), 'icons', 'application-exit') + self.action_close.setIcon(QtGui.QIcon(path)) + self.action_close.triggered.connect(self.close) + menu.addAction(self.action_close) + self.setMenuBar(menu_bar) + + # Create statusbar and indicators + self._statusbar = self.statusBar() + self._statusbar.showMessage('Ready') + + self._status_label = QtWidgets.QLabel() + + self._locked_indicator = QtWidgets.QLabel() + self._locked_indicator.setPixmap(QtGui.QPixmap(os.path.join(get_artwork_dir(), 'icons', 'network-disconnect')).scaled(16, 16)) + + self._statusbar.addWidget(self._locked_indicator) + self._statusbar.addWidget(self._status_label) + + # Initialize widgets + self.start_stop_button = QtWidgets.QPushButton('Start Measurement') + + self.start_wavelength_label = QtWidgets.QLabel('Laser Wavelength (um)') + self.start_wavelength = QtWidgets.QDoubleSpinBox() + self.start_wavelength.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.start_wavelength.setAlignment(QtCore.Qt.AlignHCenter) + self.start_wavelength.setRange(0.3, 2) + self.start_wavelength.setDecimals(4) + + self.grating_angle_label = QtWidgets.QLabel('Grating Angle (deg)') + self.grating_angle = QtWidgets.QDoubleSpinBox() + self.grating_angle.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.grating_angle.setAlignment(QtCore.Qt.AlignHCenter) + self.grating_angle.setRange(0.0, 90) + self.grating_angle.setDecimals(4) + + self.waveplate_angle_label = QtWidgets.QLabel('Waveplate Angle (deg)') + self.waveplate_angle = QtWidgets.QDoubleSpinBox() + self.waveplate_angle.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.waveplate_angle.setAlignment(QtCore.Qt.AlignHCenter) + self.waveplate_angle.setRange(0, 360) + self.waveplate_angle.setDecimals(4) + + self.gain_label = QtWidgets.QLabel('Gain') + self.gain = QtWidgets.QSpinBox() + self.gain.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.gain.setAlignment(QtCore.Qt.AlignHCenter) + self.gain.setRange(0, 100) + + self.exposure_s_label = QtWidgets.QLabel('Exposure Time (s)') + self.exposure_s = QtWidgets.QDoubleSpinBox() + self.exposure_s.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.exposure_s.setAlignment(QtCore.Qt.AlignHCenter) + self.exposure_s.setRange(0.0, 60.0) + self.exposure_s.setDecimals(3) + + self.calibrate = QtWidgets.QCheckBox('Perform Calibration?') + + + self.plot_widget = pg.PlotWidget() + self.plot_widget.getPlotItem().setContentsMargins(1, 1, 1, 1) + self.plot_widget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + self.plot_widget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.plot_widget.setLabel('bottom', text='Wavelength', units='um') + self.plot_widget.setLabel('left', text='Intensity (a.u.)') + + self.data_item = pg.PlotDataItem(pen=pg.mkPen(palette.c1, style=QtCore.Qt.DotLine), + symbol='o', + symbolPen=palette.c1, + symbolBrush=palette.c1, + symbolSize=7) + self.plot_widget.addItem(self.data_item) + + # arrange widgets in layout + layout = QtWidgets.QGridLayout() + layout.addWidget(self.plot_widget, 0, 0, 4, 4) + layout.addWidget(self.calibrate, 0, 5) + layout.addWidget(self.gain_label, 0, 5) + layout.addWidget(self.gain, 0, 6) + layout.addWidget(self.exposure_s_label, 0, 5) + layout.addWidget(self.exposure_s, 0, 6) + layout.addWidget(self.start_wavelength_label, 1, 5) + layout.addWidget(self.start_wavelength, 1, 6) + layout.addWidget(self.grating_angle_label, 2, 5) + layout.addWidget(self.grating_angle, 2, 6) + layout.addWidget(self.waveplate_angle_label, 3, 5) + layout.addWidget(self.waveplate_angle, 3, 6) + layout.addWidget(self.start_stop_button, 4, 6) + + + layout.setColumnStretch(1, 1) + + + # Create dummy widget as main widget and set layout + central_widget = QtWidgets.QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index ca152bb..2892fcd 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -19,7 +19,14 @@ # qudi GUI measurement modules must inherit qudi.core.module.GuiBase or other GUI modules. class TerascanGui(GuiBase): - """ Terascan Measurement GUI """ + """ Terascan Measurement GUI + + exaple config for copy-paste: + terascan_gui: + module.Class: 'terascan.terascan_gui.TerascanGui' + connect: + terascan_logic: terascan_logic + """ # Signal declaration for outgoing control signals to logic sigStartMeasurement = QtCore.Signal() sigStopMeasurement = QtCore.Signal() diff --git a/src/qudi/logic/common/camera_logic.py b/src/qudi/logic/common/camera_logic.py index 9b3a8f7..8cc2174 100644 --- a/src/qudi/logic/common/camera_logic.py +++ b/src/qudi/logic/common/camera_logic.py @@ -81,6 +81,14 @@ def on_deactivate(self): @property def last_frame(self): return self._last_frame + + @property + def size(self) -> tuple[int, int]: + """ Retrieve size of the image in pixel + + @return tuple: Size (width, height) + """ + return self._camera().get_size() def set_exposure(self, time): """ Set exposure time of camera """ diff --git a/src/qudi/logic/common/scanning_laser_logic.py b/src/qudi/logic/common/scanning_laser_logic.py index 6b20f3c..9823f1a 100644 --- a/src/qudi/logic/common/scanning_laser_logic.py +++ b/src/qudi/logic/common/scanning_laser_logic.py @@ -92,7 +92,8 @@ def on_deactivate(self): @property def wavelength(self) -> float: - return self._wavelength + """In um""" + return self._laser().get_wavelength() @property def tolerance(self) -> float: diff --git a/src/qudi/logic/grating_scan_logic.py b/src/qudi/logic/grating_scan_logic.py index b334ecd..da54fb4 100644 --- a/src/qudi/logic/grating_scan_logic.py +++ b/src/qudi/logic/grating_scan_logic.py @@ -42,6 +42,10 @@ def __init__(self, self.raw_data = raw_data self.processed_data = processed_data +class GratingCalData(GratingScanData): + pixel: int + cal_signal: float + class GratingScanConfig: laser_wave_start: float laser_wave_stop: float @@ -59,11 +63,44 @@ class GratingScanConfig: exposure_time_s: float gain: int + def __init__(self, + laser_wave_start: float = 0.785, + laser_wave_stop: float = 0.785, + laser_wave_step: float = 0.01, + grating_angle_start: float = 75.0, + grating_angle_stop: float = 75.0, + grating_angle_step: float = 0.1, + waveplate_angle_start: float = 0.0, + waveplate_angle_stop: float = 0.0, + waveplate_angle_step: float = 0.1, + num_repeats: int = 1, + calibrate: bool = True, + calibrate_each_iter: bool = False, + save_calibration: bool = True, # will save each iteration if asked to + exposure_time_s: float = 0.25, + gain: int = 50, + ): + self.laser_wave_start = laser_wave_start + self.laser_wave_stop = laser_wave_stop + self.laser_wave_step = laser_wave_step + self.grating_angle_start = grating_angle_start + self.grating_angle_stop = grating_angle_stop + self.grating_angle_step = grating_angle_step + self.waveplate_angle_start = waveplate_angle_start + self.waveplate_angle_stop = waveplate_angle_stop + self.waveplate_angle_step = waveplate_angle_step + self.num_repeats = num_repeats + self.calibrate = calibrate + self.calibrate_each_iter = calibrate_each_iter + self.save_calibration = save_calibration # will save each iteration if asked to + self.exposure_time_s = exposure_time_s + self.gain = gain + class GratingScanState(Enum): - IDLE = 1, - CALIBRATING = 2, - ACQUIRING = 3 + IDLE = 'Idle', + CALIBRATING = 'Calibrating', + ACQUIRING = 'Acquiring' class GratingScanLogic(LogicBase): @@ -84,7 +121,7 @@ class GratingScanLogic(LogicBase): options: calibration_directory: 'D:\Experiment data\diffraction_grating\Calibrations' normalization_factor_tol: 0.001 # If normalized intensity is below this limit, data will be excluded from processed spectrum - + pixels_per_nm: 2 # Number of pixels that correspond to a single nm in a spectrum """ # declare connectors @@ -100,6 +137,8 @@ class GratingScanLogic(LogicBase): _norm_factor_tol = ConfigOption(name='normalization_factor_tol', default=0.001) + _pixels_per_nm = ConfigOption(name='pixels_per_nm', missing='error') + # status variables: _wavelength_start = StatusVar('start_wavelength', default=0.75) _wavelength_end = StatusVar('end_wavelength', default=0.75) @@ -136,10 +175,9 @@ class GratingScanLogic(LogicBase): _laser_locked = StatusVar('laser_locked', default=False) _current_data: List[GratingScanData] - _calibration_data: List[GratingScanData] - - _calibration_data: List[GratingScanData] + _calibration_data: List[GratingCalData] _calibration_index: int + _calibration_list: np.ndarray _current_data: List[GratingScanData] _power_val: float @@ -244,6 +282,7 @@ def start_scan(self): self._current_data = [] self._calibration_data = [] self._calibration_index = 0 + self._calibration_list = [] self.sigDataUpdated.emit(self._current_data) # Configure Camera: @@ -352,7 +391,9 @@ def _stop_acq(self): @QtCore.Slot() def _next_acq(self): with self._thread_lock: - if self.module_state() == 'locked': + if (self.module_state() == 'locked' + and self._needs_acq): + wavelength_id, grating_id, waveplate_id = self._counter_to_indices() wavelength = self._wavelength_list[wavelength_id] @@ -363,10 +404,21 @@ def _next_acq(self): if self._needs_cal(): self._scan_state = GratingScanState.CALIBRATING + width, _ = self._camera().size + cal_points = int(self._pixels_per_nm * width) + self._calibration_list = np.linspace( + wavelength - ((width - 1)/2), + wavelength + ((width - 1)/2), + cal_points + ) self._sigCalibrate.emit() - - self._set_wavelength(wavelength=wavelength) - self.__timer.start(1) + else: + self._set_wavelength(wavelength=wavelength) + self.__timer.start(1) + else: + self.__timer.stop() + self._scan_state = GratingScanState.IDLE + self.sigScanFinished.emit() @@ -390,9 +442,9 @@ def _next_calibration(self): self._sigNextAcq.emit() else: - # TODO: Fix to generate correct calibration wavelengths for this grating angle - # grating, wavelength = self._calibration_locations[self._calibration_index] + wavelength = self._calibration_list[self._calibration_index] self._set_wavelength(wavelength=wavelength) + self.__timer.start(1) @@ -416,6 +468,12 @@ def _load_calibration(self): self.log.warning(f'Calibration data not found at path {self._calibration_directory}') return + def _needs_acq(self) -> bool: + if (self._repeat_counter >= self._num_repeats + and self._measurement_counter >= self._measurements_per_repeat): + return False + + return True def _needs_cal(self) -> bool: @@ -505,8 +563,9 @@ def __wait_for_ready(self): @QtCore.Slot(np.ndarray) def _new_camera_frame(self, data: np.ndarray): + wave = self._laser().wavelength scan_data = GratingScanData( - laser_wave=self._current_wavelength, + laser_wave=wave, grating_angle=self._current_grating_angle, waveplate_angle=self._current_waveplate_angle, power_val=self.power, @@ -535,18 +594,52 @@ def _camera_acquisition_finished(self): pass def _process_data(self): - """ Modifies last element of self._current_data """ + """ Modifies last element of self._current_data to set processed_data + to a spectrum corresponding to the image, based on the loaded + calibration + """ if (len(self._calibration_data) == 0): self.log.warning('No calibration data, not processing raw data') return - pass + raw_data = self._current_data[-1].raw_data + num_pts = raw_data.shape[1] + processed_data = np.zeros((num_pts, 2)) + idx = 0 + for cal in self._calibration_data: + wvl = cal.laser_wave + pix = cal.pixel + if (idx == pix): + processed_data[idx, 0] = wvl + elif (idx < pix): + processed_data[idx, 0] = wvl \ + - (0.001 / self._pixels_per_nm) * (pix - idx) + elif (idx > pix): + processed_data[idx, 0] = wvl \ + + (0.001 / self._pixels_per_nm) * (idx - pix) + + idx += 1 + + for i in range(num_pts): + max_row = np.max(raw_data[i, :]) + processed_data[i, 1] = np.sum(raw_data[i, max_row - 4: max_row + 5]) + + + self._current_data[-1].processed_data = processed_data + self.sigDataUpdated.emit(self._current_data) + + def _process_calibration(self): """ Modifies last element of self._calibration_data """ - - pass + data = self._calibration_data[-1] + sig_max = np.max(data.raw_data) + y, x = np.argwhere(data.raw_data == sig_max)[0] + self._calibration_data[-1].pixel = x + cal_signal = data.raw_data[y-5:y+5, x] + cal_signal = np.sum(cal_signal) + self._calibration_data[-1].cal_signal = cal_signal @QtCore.Slot(object) def _new_daq_data(self, data: List[ReaderVal]): From 7ae19633f6f7755b64e86e6c6328c497a9f66ec2 Mon Sep 17 00:00:00 2001 From: lange50 Date: Thu, 20 Mar 2025 13:14:25 -0400 Subject: [PATCH 08/22] Added set wavelength to dummy --- src/qudi/hardware/dummy/scanning_laser_dummy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/qudi/hardware/dummy/scanning_laser_dummy.py b/src/qudi/hardware/dummy/scanning_laser_dummy.py index 1634cf4..7d3a3e3 100644 --- a/src/qudi/hardware/dummy/scanning_laser_dummy.py +++ b/src/qudi/hardware/dummy/scanning_laser_dummy.py @@ -158,4 +158,7 @@ def resume_scan(self): def get_wavelength(self) -> float: """Gets the current wavelength (in um)""" return 0.785 * random.gauss(1, 0.1) + + def set_wavelength(self, wavelength): + pass From a63d0b7e6bb5a677f49027df5c6188a9ea89f818 Mon Sep 17 00:00:00 2001 From: lange50 Date: Thu, 20 Mar 2025 14:10:21 -0400 Subject: [PATCH 09/22] Adding Christians requests to the GUI. Still need to be connected to the logic. --- cfg/grating_scan.cfg | 127 ++++++++++++++++++ src/qudi/gui/terascan/terascan_main_window.py | 28 +++- 2 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 cfg/grating_scan.cfg diff --git a/cfg/grating_scan.cfg b/cfg/grating_scan.cfg new file mode 100644 index 0000000..7ea95ce --- /dev/null +++ b/cfg/grating_scan.cfg @@ -0,0 +1,127 @@ +global: + # list of modules to load when starting + startup_modules: [] + + # Module server configuration for accessing qudi GUI/logic/hardware modules from remote clients + remote_modules_server: + address: 'localhost' + port: 12345 + + # Server port for serving the active qudi module namespace locally (localhost). + # Used by e.g. the Qudi jupyter kernel. + namespace_server_port: 18861 + + # If this flag is set (True), all arguments passed to qudi module APIs from remote + # (jupyter notebook, qudi console, remote modules) will be wrapped and passed "per value" + # (serialized and de-serialized). This is avoiding a lot of inconveniences with using numpy in + # remote clients. + # If you do not want to use this workaround and know what you are doing, you can disable this + # feature by setting this flag to False. + force_remote_calls_by_value: True + + # Qss stylesheet for controlling the appearance of the GUIs. + # Absolute path or relative to qudi.artwork.styles + stylesheet: 'qdark.qss' + + # Default root directory for measurement data storage. All eventual data sub-directories should + # be contained within this directory. This is not enforced, just convention. + # The fallback directory is /qudi/Data/ + # default_data_dir: C:\Users\neverhorst\qudi\Data + + # Save data to daily data sub-directories by default + daily_data_dirs: True + + +gui: + grating_scan_gui: + module.Class: 'terascan.terascan_gui.TerascanGui' + connect: + terascan_logic: terascan_logic + +logic: + terascan_logic: + module.Class: 'terascan_logic.TerascanLogic' + connect: + laser: scanning_laser_logic + wavemeter: wavemeter_logic + counter: fast_counter_logic + daq: daq_reader_logic # Note that this logic assumes there is exactly one (digital) input to the DAQ. + options: + record_length_ms: 1 # Length of data (in ms) to take at each wavelength. Only integers. + + + daq_reader_logic: + module.Class: 'common.daq_reader_logic.DAQReaderLogic' + connect: + daq: nidaq # daq_reader_dummy + options: + update_interval: 0 # Period in ms to check for data updates. Integers only. 0 is as fast as possible + + + wavemeter_logic: + module.Class: 'common.wavemeter_logic.WavemeterLogic' + connect: + wavemeter: wavemeter # wavemeter_dummy + + + fast_counter_logic: + module.Class: 'common.fast_counter_logic.FastCounterLogic' + connect: + fast_counter: swabian_timetagger # fast_counter_dummy + + + scanning_laser_logic: + module.Class: 'common.scanning_laser_logic.ScanningLaserLogic' + connect: + laser: solstis_laser # scanning_laser_dummy + options: + min_wavelength: 0.700 # in um + max_wavelength: 0.800 # in um + + +hardware: + + # Real Hardware: + nidaq: + module.Class: 'daq.nidaq.NIDAQ' + options: + device_str: 'Dev2' + channels: + signal: + description: 'Laser Lock TTL' + type: 0 # 0 for Digital, 1 for Analog + name: 'line0' # The name as idintified by the card + port: 1 # port number identified by the card + + wavemeter: + module.Class: 'wavemeter.high_finesse_wavemeter.HighFinesseWavemeter' + + swabian_timetagger: + module.Class: 'timetagger.swabian_tagger.SwabianTimeTagger' + options: + channels: + counts: 1 # label and channel number on the time tagger (1,2,3,4) + + solstis_laser: + module.Class: 'laser.solstis_laser.SolstisLaser' + options: + host_ip_addr: '192.168.1.225' # IP address of control computer + laser_ip_addr: '192.168.1.222' # IP address of laser + laser_port: 39933 # Port number to connect on + scan_rate: 13 # See "solstis_constants.py" TeraScanRate Enum for values + scan_type: 2 # see "solstis_constants.py" TeraScanType Enum for values + + # Dummy Hardware: + daq_reader_dummy: + module.Class: 'dummy.daq_reader_dummy.DAQReaderDummy' + + fast_counter_dummy: + module.Class: 'dummy.fast_counter_dummy.FastCounterDummy' + + wavemeter_dummy: + module.Class: 'dummy.wavemeter_dummy.WavemeterDummy' + + scanning_laser_dummy: + module.Class: 'dummy.scanning_laser_dummy.ScanningLaserDummy' + + \ No newline at end of file diff --git a/src/qudi/gui/terascan/terascan_main_window.py b/src/qudi/gui/terascan/terascan_main_window.py index 8dbe594..74e5da1 100644 --- a/src/qudi/gui/terascan/terascan_main_window.py +++ b/src/qudi/gui/terascan/terascan_main_window.py @@ -10,6 +10,8 @@ from qudi.util.paths import get_artwork_dir from qudi.util.colordefs import QudiPalettePale as palette +from qudi.hardware.laser.solstis_constants import * + class TerascanMainWindow(QtWidgets.QMainWindow): """ Main window for Terascan measurement """ @@ -66,6 +68,14 @@ def __init__(self, *args, **kwargs): self.stop_wavelength.setAlignment(QtCore.Qt.AlignHCenter) self.stop_wavelength.setRange(0.3, 2) self.stop_wavelength.setDecimals(4) + + self.scan_rate_label = QtWidgets.QLabel('Scan Rate') + self.scan_rate = QtWidgets.QComboBox() + self.scan_rate.addItem(TeraScanRate.SCAN_RATE_FINE_LINE_20_GHZ.name, TeraScanRate.SCAN_RATE_FINE_LINE_20_GHZ.value) + + self.scan_type_label = QtWidgets.QLabel('Scan Type') + self.scan_type = QtWidgets.QComboBox() + self.scan_type.addItem(TeraScanType.SCAN_TYPE_LINE.name, TeraScanType.SCAN_TYPE_LINE.value) self.plot_widget = pg.PlotWidget() self.plot_widget.getPlotItem().setContentsMargins(1, 1, 1, 1) @@ -85,13 +95,19 @@ def __init__(self, *args, **kwargs): # arrange widgets in layout layout = QtWidgets.QGridLayout() layout.addWidget(self.plot_widget, 0, 0, 4, 4) - layout.addWidget(self.start_wavelength_label, 2, 5) - layout.addWidget(self.start_wavelength, 2, 6) - layout.addWidget(self.stop_wavelength_label, 3, 5) - layout.addWidget(self.stop_wavelength, 3, 6) - layout.addWidget(self.start_stop_button, 4, 6) - + control_layout = QtWidgets.QVBoxLayout() + control_layout.addWidget(self.scan_rate_label, 0, QtCore.Qt.AlignBottom) + control_layout.addWidget(self.scan_rate, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.scan_type_label, 0, QtCore.Qt.AlignBottom) + control_layout.addWidget(self.scan_type, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.start_wavelength_label, 0, QtCore.Qt.AlignBottom) + control_layout.addWidget(self.start_wavelength, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.stop_wavelength_label, 0, QtCore.Qt.AlignBottom) + control_layout.addWidget(self.stop_wavelength, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.start_stop_button) + + layout.addLayout(control_layout, 0, 5, 5, 1) layout.setColumnStretch(1, 1) From a6f971aa9284b095ad6232c8d73a95ccc76a9898 Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Fri, 21 Mar 2025 10:54:14 -0400 Subject: [PATCH 10/22] Updating to move scan_rate and scan_type to front panel and to only show values that are valid. --- cfg/terascan.cfg | 2 - src/qudi/gui/terascan/terascan_gui.py | 67 +++++++++++++++---- src/qudi/gui/terascan/terascan_main_window.py | 8 +-- .../interface/scanning_laser_interface.py | 32 +++++++++ src/qudi/logic/common/scanning_laser_logic.py | 26 +++++++ src/qudi/logic/terascan_logic.py | 50 ++++++++++++-- 6 files changed, 162 insertions(+), 23 deletions(-) diff --git a/cfg/terascan.cfg b/cfg/terascan.cfg index a398735..8e00d45 100644 --- a/cfg/terascan.cfg +++ b/cfg/terascan.cfg @@ -108,8 +108,6 @@ hardware: host_ip_addr: '192.168.1.225' # IP address of control computer laser_ip_addr: '192.168.1.222' # IP address of laser laser_port: 39933 # Port number to connect on - scan_rate: 13 # See "solstis_constants.py" TeraScanRate Enum for values - scan_type: 2 # see "solstis_constants.py" TeraScanType Enum for values # Dummy Hardware: daq_reader_dummy: diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index 2892fcd..e6a7cb9 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -31,6 +31,8 @@ class TerascanGui(GuiBase): sigStartMeasurement = QtCore.Signal() sigStopMeasurement = QtCore.Signal() sigSetWavelengths = QtCore.Signal(float, float) + sigSetScanType = QtCore.Signal(int) + sigSetScanRate = QtCore.Signal(int) sigSaveData = QtCore.Signal() # One logic module @@ -51,11 +53,18 @@ def on_activate(self) -> None: self._mw.start_wavelength.setValue(self._start_wavelength) self._mw.stop_wavelength.setValue(self._stop_wavelength) + for txt, scan_type in self._terascan_logic().scan_types.items(): + self._mw.scan_type.addItem(txt, scan_type) + for txt, scan_rate in self._terascan_logic().scan_rates.items(): + self._mw.scan_rate.addItem(txt, scan_rate) + # connect all GUI internal signals self._mw.start_wavelength.valueChanged.connect(self._start_changed) self._mw.stop_wavelength.valueChanged.connect(self._stop_changed) self._mw.start_stop_button.clicked.connect(self._start_stop_pressed) self._mw.action_save_data.triggered.connect(self._save_data) + self._mw.scan_type.currentIndexChanged.connect(self._scan_type_changed) + self._mw.scan_rate.currentIndexChanged.connect(self._scan_rate_changed) # Connect all signals to and from the logic. Make sure the connections are QueuedConnection. # Inputs: @@ -88,6 +97,14 @@ def on_activate(self) -> None: self._terascan_logic().configure_scan, QtCore.Qt.QueuedConnection ) + self.sigSetScanType.connect( + self._terascan_logic().set_scan_type, QtCore.Qt.QueuedConnection + ) + + self.sigSetScanRate.connect( + self._terascan_logic().set_scan_rate, QtCore.Qt.QueuedConnection + ) + self._data = [] # Turn on update timer: @@ -127,6 +144,8 @@ def show(self) -> None: self._mw.show() self._mw.raise_() + + ### Handlers from the UI ### @QtCore.Slot(float) def _start_changed(self, wave: float) -> None: """ Qt slot to be called upon wavelength change """ @@ -154,18 +173,27 @@ def _start_stop_pressed(self) -> None: self.sigStopMeasurement.emit() + @QtCore.Slot(int) + def _scan_type_changed(self, _: int): + """ Qt slot to be called upon scan type change """ + self.sigSetScanType.emit(self._mw.scan_type.currentData()) - def _update_ui(self, running: bool) -> None: - if running: - self._mw.start_stop_button.setText('Stop Measurement') - self._mw.plot_widget.setXRange(self._start_wavelength, self._stop_wavelength) - self._mw._statusbar.clearMessage() - self._mw._progress_bar.setValue(0) - - else: - self._mw.start_stop_button.setText('Start Measurement') - self._mw._statusbar.showMessage('Ready') + # When type changes, update UI for allowed rates + self._mw_scan_rate.clear() # Clear previous rates + for txt, scan_rate in self._terascan_logic().scan_rates.items(): + self._mw.scan_rate.addItem(txt, scan_rate) + + + @QtCore.Slot(int) + def _scan_rate_changed(self, _: int): + """ Qt slot to be called upon scan rate change """ + self.sigSetScanRate.emit(self._mw.scan_rate.currentData()) + ### End Handlers from UI ### + + + ### Begin Handlers from Logic ### + @QtCore.Slot() def _scan_finished(self) -> None: self._mw.start_stop_button.setText('Start Measurement') @@ -188,7 +216,8 @@ def _save_data(self) -> None: column_formats='.15e' ) - ds.save_data(self._data) + array = np.array([(d.wavelength, d.counts) for d in self._data]) + ds.save_data(array) @QtCore.Slot(bool) def _laser_lock_ui(self, locked: bool) -> None: @@ -196,8 +225,22 @@ def _laser_lock_ui(self, locked: bool) -> None: pix = QtGui.QPixmap(os.path.join(get_artwork_dir(), 'icons', icon)) self._mw._locked_indicator.setPixmap(pix.scaled(16, 16)) - + ### End Handlers from Logic ### + + ### Begin Private internal Functions ### + + def _update_ui(self, running: bool) -> None: + if running: + self._mw.start_stop_button.setText('Stop Measurement') + self._mw.plot_widget.setXRange(self._start_wavelength, self._stop_wavelength) + self._mw._statusbar.clearMessage() + self._mw._progress_bar.setValue(0) + + else: + self._mw.start_stop_button.setText('Start Measurement') + self._mw._statusbar.showMessage('Ready') + def _update_plot(self) -> None: if (len(self._data) == 0): return diff --git a/src/qudi/gui/terascan/terascan_main_window.py b/src/qudi/gui/terascan/terascan_main_window.py index 74e5da1..92e6769 100644 --- a/src/qudi/gui/terascan/terascan_main_window.py +++ b/src/qudi/gui/terascan/terascan_main_window.py @@ -71,11 +71,9 @@ def __init__(self, *args, **kwargs): self.scan_rate_label = QtWidgets.QLabel('Scan Rate') self.scan_rate = QtWidgets.QComboBox() - self.scan_rate.addItem(TeraScanRate.SCAN_RATE_FINE_LINE_20_GHZ.name, TeraScanRate.SCAN_RATE_FINE_LINE_20_GHZ.value) self.scan_type_label = QtWidgets.QLabel('Scan Type') self.scan_type = QtWidgets.QComboBox() - self.scan_type.addItem(TeraScanType.SCAN_TYPE_LINE.name, TeraScanType.SCAN_TYPE_LINE.value) self.plot_widget = pg.PlotWidget() self.plot_widget.getPlotItem().setContentsMargins(1, 1, 1, 1) @@ -96,11 +94,11 @@ def __init__(self, *args, **kwargs): layout = QtWidgets.QGridLayout() layout.addWidget(self.plot_widget, 0, 0, 4, 4) - control_layout = QtWidgets.QVBoxLayout() - control_layout.addWidget(self.scan_rate_label, 0, QtCore.Qt.AlignBottom) - control_layout.addWidget(self.scan_rate, 0, QtCore.Qt.AlignTop) + control_layout = QtWidgets.QVBoxLayout() control_layout.addWidget(self.scan_type_label, 0, QtCore.Qt.AlignBottom) control_layout.addWidget(self.scan_type, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.scan_rate_label, 0, QtCore.Qt.AlignBottom) + control_layout.addWidget(self.scan_rate, 0, QtCore.Qt.AlignTop) control_layout.addWidget(self.start_wavelength_label, 0, QtCore.Qt.AlignBottom) control_layout.addWidget(self.start_wavelength, 0, QtCore.Qt.AlignTop) control_layout.addWidget(self.stop_wavelength_label, 0, QtCore.Qt.AlignBottom) diff --git a/src/qudi/interface/scanning_laser_interface.py b/src/qudi/interface/scanning_laser_interface.py index 1bd39bd..27f018c 100644 --- a/src/qudi/interface/scanning_laser_interface.py +++ b/src/qudi/interface/scanning_laser_interface.py @@ -158,4 +158,36 @@ def get_wavelength(self): @abstractmethod def set_wavelength(self, wavelength: float): """Sets the current wavelength (in um)""" + pass + + @abstractmethod + def get_scan_types(self) -> dict: + """ Returns a dict that maps str labels of scan types to enum value + """ + pass + + @abstractmethod + def set_scan_type(self, scan_type) -> None: + """Sets the current scan type""" + pass + + @abstractmethod + def set_scan_rate(self, scan_rate) -> None: + """Sets the current scan rate""" + pass + + @abstractmethod + def get_scan_rates(self, scan_type) -> dict: + """ Returns a dict for a particular scan type that maps descriptive + labels to enum values for scan rates + """ + + @abstractmethod + def get_default_scan_type(self): + """Returns the default scan type""" + pass + + @abstractmethod + def get_default_scan_rate(self): + """Returns the default scan rate""" pass \ No newline at end of file diff --git a/src/qudi/logic/common/scanning_laser_logic.py b/src/qudi/logic/common/scanning_laser_logic.py index 9823f1a..3646153 100644 --- a/src/qudi/logic/common/scanning_laser_logic.py +++ b/src/qudi/logic/common/scanning_laser_logic.py @@ -65,6 +65,9 @@ class ScanningLaserLogic(LogicBase): _start_wavelength = StatusVar('start_wavelength', default=0.78) _end_wavelength = StatusVar('end_wavelength', default=0.785) + _scan_type = StatusVar('scan_type', default=-1) + _scan_rate = StatusVar('scan_rate', default=-1) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__timer = None @@ -83,6 +86,10 @@ def on_activate(self): self.__timer.start(100) # Check every 100 ms # self.__timer.start(0) # 0-timer to call as often as possible + if (self._scan_type == -1): + self._scan_type = laser.get_default_scan_type() + self._scan_rate = laser.get_default_scan_rate() + def on_deactivate(self): """ Perform required deactivation. """ @@ -99,6 +106,25 @@ def wavelength(self) -> float: def tolerance(self) -> float: return -1 + @property + def scan_types(self) -> dict: + return self._laser().get_scan_types() + + @property + def scan_rates(self) -> dict: + return self._laser().get_scan_rates(self.scan_type) + + @QtCore.Slot(int) + def set_scan_type(self, scan_type) -> None: + self._scan_type = scan_type + self._laser().set_scan_type(scan_type) + + @QtCore.Slot(int) + def set_scan_rate(self, scan_rate) -> None: + self._scan_rate = scan_rate + self._laser().set_scan_rate(scan_rate) + + @QtCore.Slot(float, float) def set_wavelengths(self, start: float, stop: float) -> None: self._start_wavelength = max(start, self._min_wavelength) diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index fb5ed0d..7467abd 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -59,6 +59,9 @@ class TerascanLogic(LogicBase): _end_wavelength = StatusVar('end_wavelength', default=0.8) _current_wavelength = StatusVar('current_wavelength', default=0.75) + _scan_rate = StatusVar('scan_rate', default=12) # SCAN_RATE_FINE_LINE_20_GHZ + _scan_type = StatusVar('scan_type', default=2) # SCAN_TYPE_FINE + _laser_locked = StatusVar('laser_locked', default=False) _current_data = StatusVar('current_data', default=[]) # list of TerascanData @@ -70,7 +73,9 @@ class TerascanLogic(LogicBase): # Update signals for other logics sigConfigureCounter = QtCore.Signal(float, float) - sigConfigureLaser = QtCore.Signal(float, float) + sigSetLaserWavelengths = QtCore.Signal(float, float) + sigSetLaserScanRate = QtCore.Signal(int) + sigSetLaserScanType = QtCore.Signal(int) sigStartScan = QtCore.Signal() sigStopScan = QtCore.Signal() @@ -93,7 +98,9 @@ def on_activate(self): # Outputs: self.sigConfigureCounter.connect(counter.configure, QtCore.Qt.QueuedConnection) - self.sigConfigureLaser.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) + self.sigSetLaserWavelengths.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) + self.sigSetLaserScanRate.connect(laser.set_scan_rate, QtCore.Qt.QueuedConnection) + self.sigSetLaserScanType.connect(laser.set_scan_type, QtCore.Qt.QueuedConnection) self.sigStartScan.connect(counter.start_counter, QtCore.Qt.QueuedConnection) self.sigStartScan.connect(laser.start_scan, QtCore.Qt.QueuedConnection) @@ -124,7 +131,15 @@ def on_deactivate(self): @property def locked(self) -> bool: return self._laser_locked - + + @property + def scan_types(self) -> dict: + return self._laser().get_scan_types() + + @property + def scan_rates(self) -> dict: + return self._laser().get_scan_rates() + @QtCore.Slot() def start_scan(self): with self._thread_lock: @@ -148,7 +163,34 @@ def configure_scan(self, if self.module_state() == 'idle': self._start_wavelength = start self._end_wavelength = stop - self.sigConfigureLaser.emit(start, stop) + self.sigSetLaserWavelengths.emit(start, stop) + else: + self.__logger.warning( + 'Tried to configure while a scan was running.'\ + 'Please wait until it is finished or stop it.') + + + @QtCore.Slot(int) + def set_scan_rate(self, scan_rate: int): + with self._thread_lock: + if self.module_state() == 'idle': + self._scan_rate = scan_rate + self.sigSetLaserScanRate.emit(scan_rate) + else: + self.__logger.warning( + 'Tried to configure while a scan was running.'\ + 'Please wait until it is finished or stop it.') + + @QtCore.Slot(int) + def set_scan_type(self, scan_type: int): + with self._thread_lock: + if self.module_state() == 'idle': + self._scan_type = scan_type + self.sigSetLaserScanType.emit(scan_type) + + self._scan_rate = list(self._laser().get_scan_rates(scan_type))[0] + self.sigSetLaserScanRate.emit(self._scan_rate) + else: self.__logger.warning( 'Tried to configure while a scan was running.'\ From 31878d50c0d5217675d5ad93a9f426abddd5b563 Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Fri, 21 Mar 2025 10:54:43 -0400 Subject: [PATCH 11/22] Adding functions to match with the new interface to get allowed scan rates and scan types. --- src/qudi/hardware/laser/solstis_laser.py | 72 +++++++++++++++++++++--- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index 9ae6f50..880197c 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -18,6 +18,7 @@ from typing import List from qudi.core.configoption import ConfigOption +from qudi.core.statusvariable import StatusVar from qudi.interface.scanning_laser_interface import ScanningLaserInterface from qudi.interface.scanning_laser_interface import ShutterState @@ -36,23 +37,19 @@ class SolstisLaser(ScanningLaserInterface): host_ip_addr: '192.168.1.225' # IP address of control computer laser_ip_addr: '192.168.1.222' # IP address of laser laser_port: 39933 # Port number to connect on - scan_rate: 13 # See "solstis_constants.py" TeraScanRate Enum for values - scan_type: 2 # see "solstis_constants.py" TeraScanType Enum for values """ _host_ip = ConfigOption(name='host_ip_addr', default='192.168.1.225', missing='warn') _laser_ip = ConfigOption(name='laser_ip_addr', default='192.168.1.222', missing='warn') _laser_port = ConfigOption(name='laser_port', default=39933, missing='warn') - _scan_rate = ConfigOption(name='scan_rate', default=13, missing='warn') - _scan_type= ConfigOption(name='scan_type', default=2, missing='warn') + _scan_rate = StatusVar(name='scan_rate', default=TeraScanRate.SCAN_RATE_FINE_LINE_10_GHZ) + _scan_type= StatusVar(name='scan_type', default=TeraScanType.SCAN_TYPE_FINE) def on_activate(self): """ Activate module. """ self.connect_laser() - self._scan_rate = TeraScanRate(self._scan_rate) - self._scan_type = TeraScanType(self._scan_type) def on_deactivate(self): """ Deactivate module. @@ -233,4 +230,65 @@ def get_wavelength(self) -> float: return -1 def set_wavelength(self, wavelength: float): - solstis.set_wave_m(self.socket, wavelength*1e3) \ No newline at end of file + "Sets wavelength (wavelength in um)" + solstis.set_wave_m(self.socket, wavelength*1e3) + + def get_scan_types(self) -> dict: + return { + 'Medium': TeraScanType.SCAN_TYPE_MEDIUM, + 'Fine': TeraScanType.SCAN_TYPE_FINE, + 'Line': TeraScanType.SCAN_TYPE_LINE + } + + def get_scan_rates(self, scan_type) -> dict: + if scan_type == TeraScanType.SCAN_TYPE_MEDIUM: + return { + '100 GHz': TeraScanRate.SCAN_RATE_MEDIUM_100_GHZ, + '50 GHz': TeraScanRate.SCAN_RATE_MEDIUM_50_GHZ, + '20 GHz': TeraScanRate.SCAN_RATE_MEDIUM_20_GHZ, + '15 Ghz': TeraScanRate.SCAN_RATE_MEDIUM_15_GHZ, + '10 GHz': TeraScanRate.SCAN_RATE_MEDIUM_100_GHZ, + '5 GHz': TeraScanRate.SCAN_RATE_MEDIUM_5_GHZ, + '2 GHz': TeraScanRate.SCAN_RATE_MEDIUM_2_GHZ, + '1 GHz': TeraScanRate.SCAN_RATE_MEDIUM_1_GHZ + } + elif scan_type == TeraScanType.SCAN_TYPE_FINE: + return { + '20 GHz': TeraScanRate.SCAN_RATE_FINE_LINE_20_GHZ, + '10 GHz': TeraScanRate.SCAN_RATE_FINE_LINE_10_GHZ, + '5 GHz': TeraScanRate.SCAN_RATE_FINE_LINE_5_GHZ, + '2 GHz': TeraScanRate.SCAN_RATE_FINE_LINE_2_GHZ, + '1 GHz': TeraScanRate.SCAN_RATE_FINE_LINE_1_GHZ, + '500 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_500_MHZ, + '200 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_200_MHZ, + '100 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_100_MHZ, + '50 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_50_MHZ, + '20 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_20_MHZ, + '10 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_10_MHZ, + '5 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_5_MHZ, + '2 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_2_MHZ, + '1 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_1_MHZ + } + + elif scan_type == TeraScanType.SCAN_TYPE_LINE: + return { + '500 KHz': TeraScanRate.SCAN_RATE_LINE_500_KHZ, + '200 KHz': TeraScanRate.SCAN_RATE_LINE_200_KHZ, + '100 KHz': TeraScanRate.SCAN_RATE_LINE_100_KHZ, + '50 KHz': TeraScanRate.SCAN_RATE_LINE_50_KHZ + } + + self.log.warning('Unknown scan type passed to get_scan_rates') + + + def get_default_scan_type(self) -> dict: + return {'Fine': TeraScanType.SCAN_TYPE_FINE} + + def get_default_scan_rate(self) -> dict: + return {'1 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_1_MHZ} + + def set_scan_type(self, scan_type): + self._scan_type = scan_type + + def set_scan_rate(self, scan_rate): + self._scan_rate = scan_rate \ No newline at end of file From 145df003b0e8e16bdef0693f2bd5c87eb4ee4c41 Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Fri, 21 Mar 2025 10:55:42 -0400 Subject: [PATCH 12/22] Updating Grating Scan to improve the UI as well as poll the laser for its wavelength to check if we're at the right wavelength or not instead of using the DAQ (which does not work when setting the wavelength directly) --- .../grating_scan/grating_scan_main_window.py | 29 +++++----- src/qudi/logic/grating_scan_logic.py | 53 ++++++++++--------- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/src/qudi/gui/grating_scan/grating_scan_main_window.py b/src/qudi/gui/grating_scan/grating_scan_main_window.py index 54843d2..343ce93 100644 --- a/src/qudi/gui/grating_scan/grating_scan_main_window.py +++ b/src/qudi/gui/grating_scan/grating_scan_main_window.py @@ -104,20 +104,23 @@ def __init__(self, *args, **kwargs): # arrange widgets in layout layout = QtWidgets.QGridLayout() layout.addWidget(self.plot_widget, 0, 0, 4, 4) - layout.addWidget(self.calibrate, 0, 5) - layout.addWidget(self.gain_label, 0, 5) - layout.addWidget(self.gain, 0, 6) - layout.addWidget(self.exposure_s_label, 0, 5) - layout.addWidget(self.exposure_s, 0, 6) - layout.addWidget(self.start_wavelength_label, 1, 5) - layout.addWidget(self.start_wavelength, 1, 6) - layout.addWidget(self.grating_angle_label, 2, 5) - layout.addWidget(self.grating_angle, 2, 6) - layout.addWidget(self.waveplate_angle_label, 3, 5) - layout.addWidget(self.waveplate_angle, 3, 6) - layout.addWidget(self.start_stop_button, 4, 6) - + + control_layout = QtWidgets.QVBoxLayout() + control_layout.addWidget(self.calibrate, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.gain_label, 0, QtCore.Qt.AlignBottom) + control_layout.addWidget(self.gain, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.exposure_s_label, 0, QtCore.Qt.AlignBottom) + control_layout.addWidget(self.exposure_s, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.start_wavelength_label, 0, QtCore.Qt.AlignBottom) + control_layout.addWidget(self.start_wavelength, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.grating_angle_label, 0, QtCore.Qt.AlignBottom) + control_layout.addWidget(self.grating_angle, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.waveplate_angle_label, 0, QtCore.Qt.AlignBottom) + control_layout.addWidget(self.waveplate_angle, 0, QtCore.Qt.AlignTop) + control_layout.addWidget(self.start_stop_button) + + layout.addLayout(control_layout, 0, 5, 5, 1) layout.setColumnStretch(1, 1) diff --git a/src/qudi/logic/grating_scan_logic.py b/src/qudi/logic/grating_scan_logic.py index da54fb4..521080b 100644 --- a/src/qudi/logic/grating_scan_logic.py +++ b/src/qudi/logic/grating_scan_logic.py @@ -10,14 +10,10 @@ from qudi.core.module import LogicBase from qudi.util.mutex import RecursiveMutex -from qudi.util.units import ScaledFloat from qudi.core.connector import Connector from qudi.core.configoption import ConfigOption from qudi.core.statusvariable import StatusVar from qudi.util.datastorage import TextDataStorage -from qudi.util.fit_models.exp_decay import ExponentialDecay - -from qudi.interface.daq_reader_interface import InputType, ReaderVal class GratingScanData: laser_wave: float @@ -116,12 +112,12 @@ class GratingScanLogic(LogicBase): grating_motor: grating_logic waveplate_motor: waveplate_logic laser: scanning_laser_logic - daq: daq_reader_logic # Note that this logic assumes there is exactly one (digital) input to the DAQ. powermeter: powermeter_logic # assumes there is only one Thorlabs power meter attached to system options: calibration_directory: 'D:\Experiment data\diffraction_grating\Calibrations' normalization_factor_tol: 0.001 # If normalized intensity is below this limit, data will be excluded from processed spectrum pixels_per_nm: 2 # Number of pixels that correspond to a single nm in a spectrum + wavelength_tol: 1e-6 # If we are within this tolerance of the target wavelength, we consider it to be at the target (in um) """ # declare connectors @@ -129,7 +125,6 @@ class GratingScanLogic(LogicBase): _camera = Connector(name='camera', interface='CameraLogic') _grating_motor = Connector(name='grating_motor', interface='MotorLogic') _waveplate_motor = Connector(name='waveplate_motor', interface='MotorLogic') - _daq = Connector(name='daq', interface='DAQReaderLogic') _powermeter = Connector(name='powermeter', interface='PowerMeterLogic') # declare config options @@ -139,6 +134,9 @@ class GratingScanLogic(LogicBase): _pixels_per_nm = ConfigOption(name='pixels_per_nm', missing='error') + _wavelength_tol = ConfigOption(name='wavelength_tolerance', missing='info', + default=1e-6) # in um + # status variables: _wavelength_start = StatusVar('start_wavelength', default=0.75) _wavelength_end = StatusVar('end_wavelength', default=0.75) @@ -173,7 +171,6 @@ class GratingScanLogic(LogicBase): _gain = StatusVar('gain', default=int(50)) - _laser_locked = StatusVar('laser_locked', default=False) _current_data: List[GratingScanData] _calibration_data: List[GratingCalData] _calibration_index: int @@ -221,7 +218,6 @@ def on_activate(self): grating_m = self._grating_motor() waveplate_m = self._waveplate_motor() camera = self._camera() - daq = self._daq() powermeter = self._powermeter() # Outputs: @@ -235,7 +231,6 @@ def on_activate(self): # Inputs: camera.sigFrameChanged.connect(self._new_camera_frame, QtCore.Qt.QueuedConnection) camera.sigAcquisitionFinished.connect(self._camera_acquisiton_finished, QtCore.Qt.QueuedConnection) - daq.sigNewData.connect(self._new_daq_data, QtCore.Qt.QueuedConnection) powermeter.sigNewData.connect(self._new_powermeter_data, QtCore.Qt.QueuedConnection) grating_m.sigMoveFinished.connect(self._grating_move_finished, QtCore.Qt.QueuedConnection) @@ -260,7 +255,7 @@ def on_deactivate(self): @property def locked(self) -> bool: - return self._laser_locked + return self._laser_at_target @property def moving(self) -> bool: @@ -493,7 +488,11 @@ def _needs_cal(self) -> bool: return False def _set_wavelength(self, wavelength: float): + """ Assumes thread locked externally + """ + # Set wavelength if needed: if (self._current_wavelength != wavelength): + self._current_wavelength = wavelength self.sigSetWavelength.emit(wavelength) def _move_motors(self, grating: float = None, waveplate: float = None): @@ -554,6 +553,9 @@ def __wait_for_ready(self): Wait until motors are stopped and laser is locked. Assumes thread is locked externally """ + self.locked = self._check_wavelength_status() + self.sigLaserLocked.emit(self.locked) + if (self.locked and not self.moving): self.sigStartCamera.emit() else: @@ -641,20 +643,7 @@ def _process_calibration(self): cal_signal = np.sum(cal_signal) self._calibration_data[-1].cal_signal = cal_signal - @QtCore.Slot(object) - def _new_daq_data(self, data: List[ReaderVal]): - with self._thread_lock: - if self.module_state() == 'locked': - for i in data: - if i.type is InputType.DIGITAL: - # We assume there is only one digital input for this measurement - if i.val and not self._laser_locked: - self._laser_locked = True - - if not i.val and self._laser_locked: - self._laser_locked = False - - self.sigLaserLocked.emit(self._laser_locked) + @QtCore.Slot(float) def _new_powermeter_data(self, power: float): @@ -671,4 +660,18 @@ def _grating_move_finished(self): def _waveplate_move_finished(self): waveplate_m = self._waveplate_motor() self._current_waveplate_angle = waveplate_m.position() - self._waveplate_moving = False \ No newline at end of file + self._waveplate_moving = False + + + def _check_wavelength_status(self) -> bool: + with self._thread_lock: + if self.module_state() == 'locked': + target = self._current_wavelength + current = self._laser().wavelength() + if abs(current - target) <= self._wavelength_tol: + return True + + return False + + self.log.error('Measurement is not running currently') + return False \ No newline at end of file From 48d1259bfef14899b30ba21c60906d85530ac48c Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Fri, 21 Mar 2025 10:57:19 -0400 Subject: [PATCH 13/22] Updates to the save routine to (hopefully) get the data we care about out... --- src/qudi/gui/grating_scan/grating_scan_gui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/qudi/gui/grating_scan/grating_scan_gui.py b/src/qudi/gui/grating_scan/grating_scan_gui.py index 2d3d757..1242ea9 100644 --- a/src/qudi/gui/grating_scan/grating_scan_gui.py +++ b/src/qudi/gui/grating_scan/grating_scan_gui.py @@ -263,8 +263,9 @@ def _save_data(self) -> None: root_dir=self.module_default_data_dir, column_formats='.15e' ) - - ds.save_data(self._data) + + array = np.array([d.processed_data for d in self._data]) + ds.save_data(array) @QtCore.Slot(bool) def _laser_lock_ui(self, locked: bool) -> None: From 668afd0a10bb435eac0edb0428b1852c1fd156de Mon Sep 17 00:00:00 2001 From: Ben Cerjan Date: Fri, 21 Mar 2025 15:25:09 -0400 Subject: [PATCH 14/22] Fixed wavelength calibration for processing data. --- src/qudi/logic/grating_scan_logic.py | 53 ++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/qudi/logic/grating_scan_logic.py b/src/qudi/logic/grating_scan_logic.py index 521080b..473d8a8 100644 --- a/src/qudi/logic/grating_scan_logic.py +++ b/src/qudi/logic/grating_scan_logic.py @@ -607,30 +607,53 @@ def _process_data(self): raw_data = self._current_data[-1].raw_data num_pts = raw_data.shape[1] processed_data = np.zeros((num_pts, 2)) - idx = 0 - for cal in self._calibration_data: - wvl = cal.laser_wave - pix = cal.pixel - if (idx == pix): - processed_data[idx, 0] = wvl - elif (idx < pix): - processed_data[idx, 0] = wvl \ - - (0.001 / self._pixels_per_nm) * (pix - idx) - elif (idx > pix): - processed_data[idx, 0] = wvl \ - + (0.001 / self._pixels_per_nm) * (idx - pix) - - idx += 1 + + wvls = self._pixel_to_wvl() for i in range(num_pts): max_row = np.max(raw_data[i, :]) + processed_data[i, 0] = wvls[i] processed_data[i, 1] = np.sum(raw_data[i, max_row - 4: max_row + 5]) self._current_data[-1].processed_data = processed_data self.sigDataUpdated.emit(self._current_data) - + def _pixel_to_wvl(self) -> np.ndarray: + """ Returns a 1D array where index = pixel number and value = wavelength + (in um) + """ + num_pts = self._current_data[-1].raw_data.shape[1] + wvls = np.zeros((num_pts, 1)) + + match_ind = 0 + + for idx in range(1, num_pts): + diff = 1000 # just a huge number + counter = 0 + for cal in self._calibration_data[match_ind:]: + wvl = cal.laser_wave + pix = cal.pixel + if (idx == pix): + wvls[idx] = wvl + match_ind = idx + break + elif (idx < pix and np.abs(idx - pix) < diff): + wvls[idx] = wvl \ + - (0.001 / self._pixels_per_nm) * (pix - idx) + diff = np.abs(idx - pix) + elif (idx > pix and np.abs(idx - pix) < diff): + wvls[idx] = wvl \ + + (0.001 / self._pixels_per_nm) * (idx - pix) + diff = np.abs(idx - pix) + else: + counter += 1 + # Don't search the whole list if we don't need to + if counter > 5 and diff < 3: + break + + return wvls + def _process_calibration(self): """ Modifies last element of self._calibration_data From 35112c6487d493e3adc3d5b1fe9c211ed1a5471a Mon Sep 17 00:00:00 2001 From: lange50 Date: Wed, 26 Mar 2025 10:50:43 -0400 Subject: [PATCH 15/22] Updated port number --- cfg/terascan.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfg/terascan.cfg b/cfg/terascan.cfg index 8e00d45..ec8e9ec 100644 --- a/cfg/terascan.cfg +++ b/cfg/terascan.cfg @@ -107,7 +107,7 @@ hardware: options: host_ip_addr: '192.168.1.225' # IP address of control computer laser_ip_addr: '192.168.1.222' # IP address of laser - laser_port: 39933 # Port number to connect on + laser_port:39900 # Port number to connect on # Dummy Hardware: daq_reader_dummy: From fb90d928faece9998a3d9e70f814730bccecb14d Mon Sep 17 00:00:00 2001 From: lange50 Date: Wed, 26 Mar 2025 10:53:58 -0400 Subject: [PATCH 16/22] Updates to how laser scan rate and scan type were passed down to the hardware. Also minor fix to get progress bar and laser lock icon to show. --- src/qudi/gui/terascan/terascan_gui.py | 6 +++--- src/qudi/gui/terascan/terascan_main_window.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index e6a7cb9..19aed8c 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -176,10 +176,10 @@ def _start_stop_pressed(self) -> None: @QtCore.Slot(int) def _scan_type_changed(self, _: int): """ Qt slot to be called upon scan type change """ - self.sigSetScanType.emit(self._mw.scan_type.currentData()) + self.sigSetScanType.emit(self._mw.scan_type.currentData().value) # When type changes, update UI for allowed rates - self._mw_scan_rate.clear() # Clear previous rates + self._mw.scan_rate.clear() # Clear previous rates for txt, scan_rate in self._terascan_logic().scan_rates.items(): self._mw.scan_rate.addItem(txt, scan_rate) @@ -187,7 +187,7 @@ def _scan_type_changed(self, _: int): @QtCore.Slot(int) def _scan_rate_changed(self, _: int): """ Qt slot to be called upon scan rate change """ - self.sigSetScanRate.emit(self._mw.scan_rate.currentData()) + self.sigSetScanRate.emit(self._mw.scan_rate.currentData().value) ### End Handlers from UI ### diff --git a/src/qudi/gui/terascan/terascan_main_window.py b/src/qudi/gui/terascan/terascan_main_window.py index 92e6769..bb0f946 100644 --- a/src/qudi/gui/terascan/terascan_main_window.py +++ b/src/qudi/gui/terascan/terascan_main_window.py @@ -39,7 +39,7 @@ def __init__(self, *args, **kwargs): # Create statusbar and indicators self._statusbar = self.statusBar() - self._statusbar.showMessage('Ready') + # self._statusbar.showMessage('Ready') self._progress_bar = QtWidgets.QProgressBar() self._progress_bar.setRange(0, 100) From 0002bd58a54a424b66dc0fb14baadbbe9a87b559 Mon Sep 17 00:00:00 2001 From: lange50 Date: Wed, 26 Mar 2025 10:55:04 -0400 Subject: [PATCH 17/22] Updates to how scan rate and scan type are handled and set. Should correctly use the inputs from the front panel now. --- src/qudi/hardware/laser/solstis_laser.py | 27 ++++++++++++------- src/qudi/logic/common/scanning_laser_logic.py | 8 +++--- src/qudi/logic/terascan_logic.py | 7 ++--- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index 880197c..29a37ec 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -241,7 +241,10 @@ def get_scan_types(self) -> dict: } def get_scan_rates(self, scan_type) -> dict: - if scan_type == TeraScanType.SCAN_TYPE_MEDIUM: + if scan_type in [ + TeraScanType.SCAN_TYPE_MEDIUM, + TeraScanType.SCAN_TYPE_MEDIUM.value, + ]: return { '100 GHz': TeraScanRate.SCAN_RATE_MEDIUM_100_GHZ, '50 GHz': TeraScanRate.SCAN_RATE_MEDIUM_50_GHZ, @@ -252,7 +255,10 @@ def get_scan_rates(self, scan_type) -> dict: '2 GHz': TeraScanRate.SCAN_RATE_MEDIUM_2_GHZ, '1 GHz': TeraScanRate.SCAN_RATE_MEDIUM_1_GHZ } - elif scan_type == TeraScanType.SCAN_TYPE_FINE: + elif scan_type in [ + TeraScanType.SCAN_TYPE_FINE, + TeraScanType.SCAN_TYPE_FINE.value + ]: return { '20 GHz': TeraScanRate.SCAN_RATE_FINE_LINE_20_GHZ, '10 GHz': TeraScanRate.SCAN_RATE_FINE_LINE_10_GHZ, @@ -269,15 +275,18 @@ def get_scan_rates(self, scan_type) -> dict: '2 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_2_MHZ, '1 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_1_MHZ } - - elif scan_type == TeraScanType.SCAN_TYPE_LINE: + + elif scan_type in [ + TeraScanType.SCAN_TYPE_LINE, + TeraScanType.SCAN_TYPE_LINE.value + ]: return { '500 KHz': TeraScanRate.SCAN_RATE_LINE_500_KHZ, '200 KHz': TeraScanRate.SCAN_RATE_LINE_200_KHZ, '100 KHz': TeraScanRate.SCAN_RATE_LINE_100_KHZ, '50 KHz': TeraScanRate.SCAN_RATE_LINE_50_KHZ } - + self.log.warning('Unknown scan type passed to get_scan_rates') @@ -287,8 +296,8 @@ def get_default_scan_type(self) -> dict: def get_default_scan_rate(self) -> dict: return {'1 MHz': TeraScanRate.SCAN_RATE_FINE_LINE_1_MHZ} - def set_scan_type(self, scan_type): - self._scan_type = scan_type + def set_scan_type(self, scan_type: int): + self._scan_type = TeraScanType(scan_type) - def set_scan_rate(self, scan_rate): - self._scan_rate = scan_rate \ No newline at end of file + def set_scan_rate(self, scan_rate: int): + self._scan_rate = TeraScanRate(scan_rate) \ No newline at end of file diff --git a/src/qudi/logic/common/scanning_laser_logic.py b/src/qudi/logic/common/scanning_laser_logic.py index 3646153..c17ce92 100644 --- a/src/qudi/logic/common/scanning_laser_logic.py +++ b/src/qudi/logic/common/scanning_laser_logic.py @@ -65,7 +65,7 @@ class ScanningLaserLogic(LogicBase): _start_wavelength = StatusVar('start_wavelength', default=0.78) _end_wavelength = StatusVar('end_wavelength', default=0.785) - _scan_type = StatusVar('scan_type', default=-1) + _scan_type = StatusVar('scan_type', default=2) _scan_rate = StatusVar('scan_rate', default=-1) def __init__(self, *args, **kwargs): @@ -107,12 +107,12 @@ def tolerance(self) -> float: return -1 @property - def scan_types(self) -> dict: + def get_scan_types(self) -> dict: return self._laser().get_scan_types() @property - def scan_rates(self) -> dict: - return self._laser().get_scan_rates(self.scan_type) + def get_scan_rates(self) -> dict: + return self._laser().get_scan_rates(self._scan_type) @QtCore.Slot(int) def set_scan_type(self, scan_type) -> None: diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index 7467abd..24dc7ce 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -134,11 +134,11 @@ def locked(self) -> bool: @property def scan_types(self) -> dict: - return self._laser().get_scan_types() + return self._laser().get_scan_types @property def scan_rates(self) -> dict: - return self._laser().get_scan_rates() + return self._laser().get_scan_rates @QtCore.Slot() def start_scan(self): @@ -204,6 +204,7 @@ def _laser_scan_started(self): def _laser_scan_finished(self): with self._thread_lock: self.sigScanFinished.emit() + self.sigStopCounting.emit() self.module_state.unlock() @QtCore.Slot(np.ndarray) @@ -221,7 +222,7 @@ def _process_counter_data(self, data: np.ndarray): self._current_data.append( TerascanData( wavelength=self._current_wavelength, - counts=data[0][0], + counts=data, ) ) self.sigCountsUpdated.emit(self._current_data) From 80bd9a96de031140a646b6c6ac1883212250870a Mon Sep 17 00:00:00 2001 From: lange50 Date: Wed, 26 Mar 2025 14:12:38 -0400 Subject: [PATCH 18/22] Updated to have correct laser port, to not use redundant logic modules, and to auto-start the terascan GUI on load. --- cfg/terascan.cfg | 70 ++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/cfg/terascan.cfg b/cfg/terascan.cfg index ec8e9ec..275a27b 100644 --- a/cfg/terascan.cfg +++ b/cfg/terascan.cfg @@ -1,6 +1,6 @@ global: # list of modules to load when starting - startup_modules: [] + startup_modules: [terascan_gui] # Module server configuration for accessing qudi GUI/logic/hardware modules from remote clients remote_modules_server: @@ -42,41 +42,41 @@ logic: terascan_logic: module.Class: 'terascan_logic.TerascanLogic' connect: - laser: scanning_laser_logic - wavemeter: wavemeter_logic - counter: fast_counter_logic - daq: daq_reader_logic # Note that this logic assumes there is exactly one (digital) input to the DAQ. + laser: solstis_laser + wavemeter: wavemeter + counter: swabian_timetagger + daq: nidaq # Note that this logic assumes there is exactly one (digital) input to the DAQ. options: record_length_ms: 1 # Length of data (in ms) to take at each wavelength. Only integers. - daq_reader_logic: - module.Class: 'common.daq_reader_logic.DAQReaderLogic' - connect: - daq: nidaq # daq_reader_dummy - options: - update_interval: 0 # Period in ms to check for data updates. Integers only. 0 is as fast as possible + # daq_reader_logic: + # module.Class: 'common.daq_reader_logic.DAQReaderLogic' + # connect: + # daq: nidaq # daq_reader_dummy + # options: + # update_interval: 0 # Period in ms to check for data updates. Integers only. 0 is as fast as possible - wavemeter_logic: - module.Class: 'common.wavemeter_logic.WavemeterLogic' - connect: - wavemeter: wavemeter # wavemeter_dummy + # wavemeter_logic: + # module.Class: 'common.wavemeter_logic.WavemeterLogic' + # connect: + # wavemeter: wavemeter # wavemeter_dummy - fast_counter_logic: - module.Class: 'common.fast_counter_logic.FastCounterLogic' - connect: - fast_counter: swabian_timetagger # fast_counter_dummy + # fast_counter_logic: + # module.Class: 'common.fast_counter_logic.FastCounterLogic' + # connect: + # fast_counter: swabian_timetagger # fast_counter_dummy - scanning_laser_logic: - module.Class: 'common.scanning_laser_logic.ScanningLaserLogic' - connect: - laser: solstis_laser # scanning_laser_dummy - options: - min_wavelength: 0.700 # in um - max_wavelength: 0.800 # in um + # scanning_laser_logic: + # module.Class: 'common.scanning_laser_logic.ScanningLaserLogic' + # connect: + # laser: solstis_laser # scanning_laser_dummy + # options: + # min_wavelength: 0.700 # in um + # max_wavelength: 0.800 # in um hardware: @@ -107,19 +107,19 @@ hardware: options: host_ip_addr: '192.168.1.225' # IP address of control computer laser_ip_addr: '192.168.1.222' # IP address of laser - laser_port:39900 # Port number to connect on + laser_port: 39900 # Port number to connect on # Dummy Hardware: - daq_reader_dummy: - module.Class: 'dummy.daq_reader_dummy.DAQReaderDummy' + # daq_reader_dummy: + # module.Class: 'dummy.daq_reader_dummy.DAQReaderDummy' - fast_counter_dummy: - module.Class: 'dummy.fast_counter_dummy.FastCounterDummy' + # fast_counter_dummy: + # module.Class: 'dummy.fast_counter_dummy.FastCounterDummy' - wavemeter_dummy: - module.Class: 'dummy.wavemeter_dummy.WavemeterDummy' + # wavemeter_dummy: + # module.Class: 'dummy.wavemeter_dummy.WavemeterDummy' - scanning_laser_dummy: - module.Class: 'dummy.scanning_laser_dummy.ScanningLaserDummy' + # scanning_laser_dummy: + # module.Class: 'dummy.scanning_laser_dummy.ScanningLaserDummy' \ No newline at end of file From 3c1093e7ed265abba6a44b16dcab35a45ee4ca5c Mon Sep 17 00:00:00 2001 From: lange50 Date: Wed, 26 Mar 2025 14:13:56 -0400 Subject: [PATCH 19/22] Removed duplicate common/ logic modules and moved their functionality to the hardware directly. Have not deleted them yet in case something comes up and they are necessary for reference. --- src/qudi/gui/terascan/terascan_gui.py | 4 +- src/qudi/gui/terascan/terascan_main_window.py | 4 +- src/qudi/hardware/camera/andor_camera.py | 89 +++++++++++++++- src/qudi/hardware/daq/nidaq.py | 34 +++++- src/qudi/hardware/laser/solstis_laser.py | 100 ++++++++++++++---- .../powermeter/thorlabs_power_meter.py | 38 ++++++- src/qudi/hardware/servo/thorlabs_servo.py | 79 +++++++++++--- .../hardware/timetagger/swabian_tagger.py | 79 ++++++++++---- .../wavemeter/high_finesse_wavemeter.py | 20 +++- .../interface/scanning_laser_interface.py | 13 ++- src/qudi/logic/terascan_logic.py | 26 ++--- 11 files changed, 402 insertions(+), 84 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index 19aed8c..5c4be30 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -176,6 +176,7 @@ def _start_stop_pressed(self) -> None: @QtCore.Slot(int) def _scan_type_changed(self, _: int): """ Qt slot to be called upon scan type change """ + self.sigSetScanType.emit(self._mw.scan_type.currentData().value) # When type changes, update UI for allowed rates @@ -187,7 +188,8 @@ def _scan_type_changed(self, _: int): @QtCore.Slot(int) def _scan_rate_changed(self, _: int): """ Qt slot to be called upon scan rate change """ - self.sigSetScanRate.emit(self._mw.scan_rate.currentData().value) + if self._mw.scan_rate.currentData() is not None: + self.sigSetScanRate.emit(self._mw.scan_rate.currentData().value) ### End Handlers from UI ### diff --git a/src/qudi/gui/terascan/terascan_main_window.py b/src/qudi/gui/terascan/terascan_main_window.py index bb0f946..43360f6 100644 --- a/src/qudi/gui/terascan/terascan_main_window.py +++ b/src/qudi/gui/terascan/terascan_main_window.py @@ -60,14 +60,14 @@ def __init__(self, *args, **kwargs): self.start_wavelength.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) self.start_wavelength.setAlignment(QtCore.Qt.AlignHCenter) self.start_wavelength.setRange(0.3, 2) - self.start_wavelength.setDecimals(4) + self.start_wavelength.setDecimals(6) self.stop_wavelength_label = QtWidgets.QLabel('Stop Wavelength (um)') self.stop_wavelength = QtWidgets.QDoubleSpinBox() self.stop_wavelength.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) self.stop_wavelength.setAlignment(QtCore.Qt.AlignHCenter) self.stop_wavelength.setRange(0.3, 2) - self.stop_wavelength.setDecimals(4) + self.stop_wavelength.setDecimals(6) self.scan_rate_label = QtWidgets.QLabel('Scan Rate') self.scan_rate = QtWidgets.QComboBox() diff --git a/src/qudi/hardware/camera/andor_camera.py b/src/qudi/hardware/camera/andor_camera.py index 167ef74..c878065 100644 --- a/src/qudi/hardware/camera/andor_camera.py +++ b/src/qudi/hardware/camera/andor_camera.py @@ -4,9 +4,12 @@ import time from collections.abc import Callable +from PySide2 import QtCore + import pyAndorSDK2 as sdk from pyAndorSDK2 import atmcd_errors +from qudi.util.mutex import RecursiveMutex from qudi.core.configoption import ConfigOption from qudi.interface.camera_interface import CameraInterface @@ -66,7 +69,20 @@ class AndorCamera(CameraInterface): name='default_exposure', default=1.0 ) + + # signals + sigFrameChanged = QtCore.Signal(np.ndarray) + sigAcquisitionFinished = QtCore.Signal() + + # Specify that camera should run in its own thread: + _threaded = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() + self._wavelength: float = -1 def on_activate(self): self._andor = sdk.atmcd(self._dll_location) @@ -129,6 +145,13 @@ def on_activate(self): if np.abs(current_temp - self.set_temp) > self._temp_tolerance: self.log.warning(f'Andor temperature not within set tolerance. Stabilized at: {current_temp} C.\nPROCEED WITH CAUTION!') + self.exposure_time = self.get_exposure() + self.gain = self.get_gain() + + self.__timer = QtCore.QTimer() + self.__timer.setSingleShot(True) + self.__timer.timeout.connect(self.__acquire_video_frame) + def on_deactivate(self): self._andor.SetTemperature(self._shutoff_temp) @@ -151,6 +174,22 @@ def on_deactivate(self): self._andor.CoolerOFF() + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + + @property + def last_frame(self): + return self.get_acquired_data() + + @property + def size(self) -> tuple[int, int]: + """ Retrieve size of the image in pixel + + @return tuple: Size (width, height) + """ + return self.get_size() def get_name(self): """ Retrieve an identifier of the camera that the GUI can print @@ -264,10 +303,16 @@ def set_exposure(self, exposure: float) -> float: @param float exposure: desired new exposure time - @return float: setted new exposure time + @return float: set new exposure time """ - self.exposure_time = exposure - return self.exposure_time + with self._thread_lock: + if self.module_state() == 'idle': + self.set_exposure(exposure) + self.exposure_time = exposure + return exposure + else: + self.log.error('Unable to set exposure time. Acquisition still in progress.') + def get_exposure(self) -> float: """ Get the exposure time in seconds @@ -302,6 +347,31 @@ def get_ready_state(self) -> bool: return self._error_handler(ret) + @QtCore.Slot() + def capture_frame(self): + """ Capture a single frame + """ + with self._thread_lock: + if self.module_state() == 'idle': + self.module_state.lock() + self.start_single_acquisition() + self.sigFrameChanged.emit(self.get_acquired_data()) + self.module_state.unlock() + self.sigAcquisitionFinished.emit() + else: + self.log.error('Unable to capture single frame. Acquisition still in progress.') + + + @QtCore.Slot(float, float) + def configure(self, exposure: float, gain: float) -> None: + """ Configure camera settings. + """ + try: + self.set_exposure(exposure) + self.set_gain(gain) + except Exception as e: + self.log.error(f'Failed to configure camera: {e}') + def _error_handler(self, ret: int, msg: str = None) -> bool: """ Error handling function, returns True on success, false on error """ @@ -348,4 +418,15 @@ def _check_if_running(self) -> bool: return True self._error_handler(ret) - return False \ No newline at end of file + return False + + + def __acquire_video_frame(self): + """ Execute step in the data recording loop: save one of each control and process values + """ + with self._thread_lock: + self.sigFrameChanged.emit(self.get_acquired_data()) + if self.module_state() == 'locked': + self.__timer.start(1000 * self.exposure_time) + if not self.support_live_acquisition(): + self.start_single_acquisition() # the hardware has to check it's not busy diff --git a/src/qudi/hardware/daq/nidaq.py b/src/qudi/hardware/daq/nidaq.py index 031b973..a95db4d 100644 --- a/src/qudi/hardware/daq/nidaq.py +++ b/src/qudi/hardware/daq/nidaq.py @@ -1,7 +1,8 @@ import nidaqmx from typing import List, Any, Dict - +from PySide2 import QtCore +from qudi.util.mutex import RecursiveMutex from qudi.core.configoption import ConfigOption from qudi.interface.daq_reader_interface import DAQReaderInterface, InputType, \ ReaderVal @@ -16,6 +17,7 @@ class NIDAQ(DAQReaderInterface): nidaq: module.Class: 'daq.nidaq.NIDAQ' options: + update_interval: 0 # Period in ms to check for data updates. Integers only. 0 is as fast as possible device_str: 'Dev2' channels: signal: @@ -26,6 +28,9 @@ class NIDAQ(DAQReaderInterface): """ # config options + _update_interval: int = ConfigOption(name='update_interval', + default=0) + _daq_name: str = ConfigOption(name='device_str', default='Dev1', missing='warn') @@ -41,6 +46,15 @@ class NIDAQ(DAQReaderInterface): }, missing='warn' ) + + + # signals + sigNewData = QtCore.Signal(object) # is a List[ReaderVal] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() def on_activate(self): """ Activate module. @@ -63,18 +77,25 @@ def on_activate(self): self._analog_channels.append(i) elif (i.type == InputType.DIGITAL): self._digital_channels.append(i) + + self.__timer = QtCore.QTimer() + self.__timer.timeout.connect(self.__data_update) + self.__timer.setSingleShot(False) + self.__timer.start(int(self._update_interval)) def on_deactivate(self): """ Deactivate module. """ - pass + if (self.__timer is not None): + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None def active_channels(self) -> List[str]: """ Read-only property returning the currently configured active channel names """ out = self._analog_channels.copy() out.extend(self._digital_channels) - temp = (i.description for i in out) - return temp + return (i.description for i in out) def get_reading(self) -> List[ReaderVal]: @@ -124,4 +145,9 @@ def _update_vals(self, vals: List[float], chans: List[ReaderVal]) -> None: for v, c in zip(vals, chans): c.val = v + + def __data_update(self): + with self._thread_lock: + data = self.get_reading() + self.sigNewData.emit(data) diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index 29a37ec..a7853ba 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -17,6 +17,8 @@ """ from typing import List +from PySide2 import QtCore + from qudi.core.configoption import ConfigOption from qudi.core.statusvariable import StatusVar from qudi.interface.scanning_laser_interface import ScanningLaserInterface @@ -46,15 +48,53 @@ class SolstisLaser(ScanningLaserInterface): _scan_rate = StatusVar(name='scan_rate', default=TeraScanRate.SCAN_RATE_FINE_LINE_10_GHZ) _scan_type= StatusVar(name='scan_type', default=TeraScanType.SCAN_TYPE_FINE) + # signals + sigScanStarted = QtCore.Signal() + sigScanFinished = QtCore.Signal() + + + # status variables: + _start_wavelength = StatusVar('start_wavelength', default=0.78) + _end_wavelength = StatusVar('end_wavelength', default=0.785) + + _scan_type = StatusVar('scan_type', default=2) + _scan_rate = StatusVar('scan_rate', default=13) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._wavelength = -1 + def on_activate(self): """ Activate module. """ self.connect_laser() + + self._wavelength = self.get_wavelength() + + self.__timer = QtCore.QTimer() + self.__timer.timeout.connect(self.__status_update) + self.__timer.setSingleShot(False) + self.__timer.start(100) # Check every 100 ms + # self.__timer.start(0) # 0-timer to call as often as possible + + if (self._scan_type == -1): + self._scan_type = self.get_default_scan_type() + self._scan_rate = self.get_default_scan_rate() def on_deactivate(self): """ Deactivate module. """ self.disconnect_laser() + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + + @property + def wavelength(self) -> float: + """In um""" + return self.get_wavelength() def connect_laser(self) -> bool: """ Connect to Instrument. @@ -172,34 +212,48 @@ def get_extra_info(self): """ pass - def start_scan(self, start, stop) -> bool: + @QtCore.Slot(float, float) + def set_wavelengths(self, start: float, stop: float): + self._start_wavelength = start + self._end_wavelength = stop + + @QtCore.Slot() + def start_scan(self) -> bool: """Start a wavelength scan from start wavelength to stop wavelength specified in um. @return bool: True on success, False on failure """ try: - solstis.scan_stitch_initialize(self.socket, self._scan_type, - start*1e3, stop*1e3, self._scan_rate) - - solstis.terascan_output(self.socket, - transmission_id=1, - operation=False, - delay=1, - update_step=0, - pause=True) - solstis.scan_stitch_op(self.socket, self._scan_type, "start") - return True + if self.module_state() == 'idle': + solstis.scan_stitch_initialize(self.socket, self._scan_type, + self._start_wavelength*1e3, + self._end_wavelength*1e3, + self._scan_rate) + + solstis.terascan_output(self.socket, + transmission_id=1, + operation=False, + delay=1, + update_step=0, + pause=True) + solstis.scan_stitch_op(self.socket, self._scan_type, "start") + self.sigScanStarted.emit() + self.module_state.lock() + return True except solstis.SolstisError as e: self.log.exception(f'Scan start failure: {e.message}') return False - + @QtCore.Slot() def stop_scan(self) -> bool: """Stop a running scan""" try: - solstis.scan_stitch_op(self.socket, self._scan_type, "stop") + if self.module_state() == 'locked': + solstis.scan_stitch_op(self.socket, self._scan_type, "stop") + self.sigScanFinished.emit() + self.module_state.unlock() return True except solstis.SolstisError as e: @@ -229,18 +283,20 @@ def get_wavelength(self) -> float: self.log.exception(f'Scan resume failure: {e.message}') return -1 + @QtCore.Slot(float) def set_wavelength(self, wavelength: float): "Sets wavelength (wavelength in um)" solstis.set_wave_m(self.socket, wavelength*1e3) - + @property def get_scan_types(self) -> dict: return { 'Medium': TeraScanType.SCAN_TYPE_MEDIUM, 'Fine': TeraScanType.SCAN_TYPE_FINE, 'Line': TeraScanType.SCAN_TYPE_LINE } - - def get_scan_rates(self, scan_type) -> dict: + @property + def get_scan_rates(self) -> dict: + scan_type = self._scan_type if scan_type in [ TeraScanType.SCAN_TYPE_MEDIUM, TeraScanType.SCAN_TYPE_MEDIUM.value, @@ -300,4 +356,12 @@ def set_scan_type(self, scan_type: int): self._scan_type = TeraScanType(scan_type) def set_scan_rate(self, scan_rate: int): - self._scan_rate = TeraScanRate(scan_rate) \ No newline at end of file + self._scan_rate = TeraScanRate(scan_rate) + + + def __status_update(self): + if self.module_state() == 'locked': + status = self.get_laser_state() + if status['in_progress'] == False: + self.sigScanFinished.emit() + self.module_state.unlock() \ No newline at end of file diff --git a/src/qudi/hardware/powermeter/thorlabs_power_meter.py b/src/qudi/hardware/powermeter/thorlabs_power_meter.py index 18edaac..7db15b3 100644 --- a/src/qudi/hardware/powermeter/thorlabs_power_meter.py +++ b/src/qudi/hardware/powermeter/thorlabs_power_meter.py @@ -1,8 +1,10 @@ +from PySide2 import QtCore from ThorlabsPM100 import ThorlabsPM100, USBTMC # ref: https://github.com/clade/ThorlabsPM100 from qudi.core.configoption import ConfigOption +from qudi.util.mutex import RecursiveMutex from qudi.interface.simple_powermeter_interface import SimplePowermeterInterface class ThorlabsPowerMeter(SimplePowermeterInterface): @@ -15,12 +17,28 @@ class ThorlabsPowerMeter(SimplePowermeterInterface): options: average_count: 5 # Internal samples to average in the power meter + update_interval: 0 # Period in ms to check for data updates. Integers only. 0 is as fast as possible """ _averages = ConfigOption( name='average_count', default=5 ) + + _update_interval = ConfigOption(name='update_period', + default=0, + missing='info') + + # signals + sigNewData = QtCore.Signal(float) + + # Run this in its own thread: + _threaded = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() def on_activate(self): inst = USBTMC() @@ -29,14 +47,28 @@ def on_activate(self): self._meter.input.pdiode.filter.lpass.state = 0 # high bandwidth, 1 for low self.set_average_count(self._averages) + self.__timer = QtCore.QTimer() + self.__timer.timeout.connect(self.__data_update) + self.__timer.setSingleShot(False) + self.__timer.start(int(self._update_interval)) + def on_deactivate(self): - pass - + if (self.__timer is not None): + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + def get_power(self): return self._meter.read def set_average_count(self, count: int): self._averages = count - self._meter.sense.average.count = self._averages \ No newline at end of file + self._meter.sense.average.count = self._averages + + + def __data_update(self): + with self._thread_lock: + data = self.get_power() + self.sigNewData.emit(data) \ No newline at end of file diff --git a/src/qudi/hardware/servo/thorlabs_servo.py b/src/qudi/hardware/servo/thorlabs_servo.py index 0b03d9e..0711ddc 100644 --- a/src/qudi/hardware/servo/thorlabs_servo.py +++ b/src/qudi/hardware/servo/thorlabs_servo.py @@ -1,7 +1,10 @@ import thorlabs_apt as apt from typing import List +from PySide2 import QtCore + from qudi.core.configoption import ConfigOption +from qudi.util.mutex import RecursiveMutex from qudi.interface.motor_interface import MotorInterface class ThorlabsServo(MotorInterface): @@ -20,6 +23,18 @@ class ThorlabsServo(MotorInterface): name='serial_number', missing='error' ) + + # signals + sigMoveStarted = QtCore.Signal() + sigMoveFinished = QtCore.Signal() + + # Run in separate thread: + _threaded = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() def on_activate(self): @@ -43,10 +58,29 @@ def on_activate(self): self._max_pos = max_pos self._pos = self._motor.position + + self._position = self.get_position() + self._moving = self.in_motion() + + self.__timer = QtCore.QTimer() + self.__timer.timeout.connect(self.__status_update) + self.__timer.setSingleShot(False) + self.__timer.start(25) # check if motor is moving every 25 ms def on_deactivate(self): - pass + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + + @property + def position(self) -> float: + return self._position + + @property + def moving(self) -> bool: + return self._moving def get_motion_limits(self) -> List[float]: @@ -64,24 +98,27 @@ def get_position(self) -> float: self._pos = self._motor.position return self._pos - def move(self, position: float, blocking: bool = False) -> None: + @QtCore.Slot(float) + def move(self, position: float) -> None: """ Initiates a move to a position (absolute) @param float position: position to move to @param bool blocking: wait for move to complete? """ - self._moving = True - self._motor.move_to(position, blocking) + with self._thread_lock: + self._moving = True + self._motor.move_to(position, False) - - def move_relative(self, position: float, blocking: bool = False) -> None: + @QtCore.Slot(float) + def move_relative(self, position: float) -> None: """ Initiates a move to a position (relative) @param float position: position to move to @param bool blocking: wait for move to complete? """ - self._moving = True - self._motor.move_by(position, blocking) + with self._thread_lock: + self._moving = True + self._motor.move_by(position, False) def in_motion(self) -> bool: """ Returns if the motor thinks it is moving @@ -97,12 +134,26 @@ def center(self, blocking: bool = False): @param bool blocking: wait for move to complete? """ - self._moving = True - self._motor.move_home(blocking) - + with self._thread_lock: + self._moving = True + self._motor.move_home(blocking) + + @QtCore.Slot() def stop(self): """ Stops the motor motion """ - self._motor.stop_profiled() - self._moving = False - \ No newline at end of file + with self._thread_lock: + self._motor.stop_profiled() + self._moving = False + + + def __status_update(self): + with self._thread_lock: + new_motion = self.in_motion() + + if new_motion and not self._moving: + self.sigMoveStarted.emit() + elif not new_motion and self._moving: + self.sigMoveFinished.emit() + + self._moving = new_motion \ No newline at end of file diff --git a/src/qudi/hardware/timetagger/swabian_tagger.py b/src/qudi/hardware/timetagger/swabian_tagger.py index b4b9548..5a7c966 100644 --- a/src/qudi/hardware/timetagger/swabian_tagger.py +++ b/src/qudi/hardware/timetagger/swabian_tagger.py @@ -24,6 +24,9 @@ import TimeTagger as tt from typing import Dict +from PySide2 import QtCore + +from qudi.util.mutex import RecursiveMutex from qudi.interface.fast_counter_interface import FastCounterInterface from qudi.core.configoption import ConfigOption @@ -48,11 +51,19 @@ class SwabianTimeTagger(FastCounterInterface): missing='warn' ) - # _channel_apd_0 = ConfigOption('timetagger_channel_apd_0', missing='error') - # _channel_apd_1 = ConfigOption('timetagger_channel_apd_1', missing='error') - # _channel_detect = ConfigOption('timetagger_channel_detect', missing='error') - # _channel_sequence = ConfigOption('timetagger_channel_sequence', missing='error') - # _sum_channels = ConfigOption('timetagger_sum_channels', True, missing='warn') + + # Signals: + sigScanStarted = QtCore.Signal() + sigScanFinished = QtCore.Signal(np.ndarray) + + # set to threaded: + _threaded = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() + self._last_data = np.zeros(1) def on_activate(self): """ Connect and configure the access to the FPGA. @@ -60,12 +71,17 @@ def on_activate(self): self._bin_width: int = 1 self._record_length: int = 4000 + self._record_length_s: int = self._bin_width * self._record_length self._tagger = tt.createTimeTagger() self._tagger.reset() self.counter = None self.statusvar = 0 + + self.__timer = QtCore.QTimer() + self.__timer.setSingleShot(True) + self.__timer.timeout.connect(self.__update_data) def get_constraints(self): """ Retrieve the hardware constrains from the Fast counting device. @@ -122,43 +138,62 @@ def on_deactivate(self): self.counter.clear() self.counter = None tt.freeTimeTagger(self._tagger) + + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + @QtCore.Slot(float, float) def configure(self, bin_width_s, record_length_s): """ Configuration of the fast counter. @param float bin_width_s: Length of a single time bin in the time trace - histogram in seconds. + histogram in seconds. @param float record_length_s: Total length of the timetrace/each single - gate in seconds. + gate in seconds. @return tuple(binwidth_s, gate_length_s): binwidth_s: float the actual set binwidth in seconds gate_length_s: the actual set gate length in seconds """ - self._bin_width = bin_width_s * 1e9 - self._record_length = 1 + int(record_length_s / bin_width_s) - self.statusvar = 1 - self.counter = tt.Counter( - tagger=self._tagger, - channels=[self._channel_config[i] for i in self._channel_config], - binwidth=int(np.round(self._bin_width * 1000)), # in ps - n_values=1, - ) + with self._thread_lock: + self._bin_width = bin_width_s * 1e9 + self._record_length_s = record_length_s + self._record_length = 1 + int(record_length_s / bin_width_s) + + if (bin_width_s >= record_length_s): + self.log.warning('Bin width is greater than or equal to record length') + + self.statusvar = 1 + + self.counter = tt.Counter( + tagger=self._tagger, + channels=[self._channel_config[i] for i in self._channel_config], + binwidth=int(np.round(self._bin_width * 1000)), # in ps + n_values=1, + ) - self.counter.stop() - - return bin_width_s, record_length_s + self.counter.stop() + return bin_width_s, record_length_s + + + @QtCore.Slot() def start_measure(self): """ Start the fast counter. """ self.module_state.lock() self.counter.clear() self.counter.start() + + self.__timer.start(int(self._record_length_s*1000)) + self.sigScanStarted.emit() + self.statusvar = 2 return 0 + @QtCore.Slot() def stop_measure(self): """ Stop the fast counter. """ if self.module_state() == 'locked': @@ -226,3 +261,9 @@ def get_binwidth(self): """ Returns the width of a single timebin in the timetrace in seconds. """ width_in_seconds = self._bin_width * 1e-9 return width_in_seconds + + + def __update_data(self): + with self._thread_lock: + self.__timer.start(int(self._record_length_s*1000)) + self.sigScanFinished.emit(np.squeeze(self.get_data_trace())) diff --git a/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py b/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py index 0af0f7f..47c0a5a 100644 --- a/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py +++ b/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py @@ -39,6 +39,8 @@ class HighFinesseWavemeter(SimpleWavemeterInterface): sigWavelengthUpdated = QtCore.Signal(np.ndarray) # 1-d array of wavelengths in nm + _threaded = True + def on_activate(self): """ Activate module. """ @@ -81,6 +83,17 @@ def get_wavelengths(self) -> np.ndarray: """ return self._data + + @QtCore.Slot() + def start_reading(self): + if self.module_state() == 'idle': + self.module_state.lock() + + @QtCore.Slot() + def stop_reading(self): + if self.module_state() == 'locked': + self.module_state.unlock() + def _update(self, mode, _intval, dblval): i = -1 if mode == wlmConst.cmiWavelength1: @@ -91,7 +104,8 @@ def _update(self, mode, _intval, dblval): i = 2 elif mode == wlmConst.cmiWavelength4: i = 3 - - self._data[i] = dblval - self.sigWavelengthUpdated.emit(self._data) + if self.module_state() == 'locked': + self._data[i] = dblval + self.sigWavelengthUpdated.emit(self._data) + diff --git a/src/qudi/interface/scanning_laser_interface.py b/src/qudi/interface/scanning_laser_interface.py index 27f018c..32f07ad 100644 --- a/src/qudi/interface/scanning_laser_interface.py +++ b/src/qudi/interface/scanning_laser_interface.py @@ -124,13 +124,20 @@ def get_temperatures(self): @abstractmethod def get_extra_info(self): """ Show dianostic information about lasers. - @return str: diagnostic info as a string + @return str: diagnostic info as a string """ pass @abstractmethod - def start_scan(self, start, stop): - """Begin a wavelength scan (units in um) + def start_scan(self): + """Begin a wavelength scan + + """ + pass + + @abstractmethod + def set_wavelengths(self, start: float, stop: float): + """Set the start and stop wavelengths for a scan (units in um) """ pass diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index 24dc7ce..c13c91a 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -34,20 +34,20 @@ class TerascanLogic(LogicBase): terascan_logic: module.Class: 'terascan_logic.TerascanLogic' connect: - laser: scanning_laser_logic - wavemeter: wavemeter_logic - counter: fast_counter_logic - daq: daq_reader_logic # Note that this logic assumes there is exactly one (digital) input to the DAQ. + laser: solstis_laser + wavemeter: wavemeter + counter: swabian_timetagger + daq: nidaq # Note that this logic assumes there is exactly one (digital) input to the DAQ. options: record_length_ms: 1 # Length of data (in ms) to take at each wavelength. Only integers. """ # declare connectors - _laser = Connector(name='laser', interface='ScanningLaserLogic') - _wavemeter = Connector(name='wavemeter', interface='WavemeterLogic') - _counter = Connector(name='counter', interface='FastCounterLogic') - _daq = Connector(name='daq', interface='DAQReaderLogic') + _laser = Connector(name='laser', interface='ScanningLaserInterface') + _wavemeter = Connector(name='wavemeter', interface='SimpleWavemeterInterface') + _counter = Connector(name='counter', interface='FastCounterInterface') + _daq = Connector(name='daq', interface='DAQReaderInterface') # declare config options _record_length_ms = ConfigOption(name='record_length_ms', @@ -102,21 +102,21 @@ def on_activate(self): self.sigSetLaserScanRate.connect(laser.set_scan_rate, QtCore.Qt.QueuedConnection) self.sigSetLaserScanType.connect(laser.set_scan_type, QtCore.Qt.QueuedConnection) - self.sigStartScan.connect(counter.start_counter, QtCore.Qt.QueuedConnection) + self.sigStartScan.connect(counter.start_measure, QtCore.Qt.QueuedConnection) self.sigStartScan.connect(laser.start_scan, QtCore.Qt.QueuedConnection) self.sigStartScan.connect(wavemeter.start_reading, QtCore.Qt.QueuedConnection) - self.sigStopScan.connect(counter.stop_counter, QtCore.Qt.QueuedConnection) + self.sigStopScan.connect(counter.stop_measure, QtCore.Qt.QueuedConnection) self.sigStopScan.connect(laser.stop_scan, QtCore.Qt.QueuedConnection) self.sigStopScan.connect(wavemeter.stop_reading, QtCore.Qt.QueuedConnection) - self.sigStartCounting.connect(counter.start_counter, QtCore.Qt.QueuedConnection) - self.sigStopCounting.connect(counter.stop_counter, QtCore.Qt.QueuedConnection) + self.sigStartCounting.connect(counter.start_measure, QtCore.Qt.QueuedConnection) + self.sigStopCounting.connect(counter.stop_measure, QtCore.Qt.QueuedConnection) # Inputs: laser.sigScanStarted.connect(self._laser_scan_started) laser.sigScanFinished.connect(self._laser_scan_finished) - wavemeter.sigNewData.connect(self._new_wavemeter_data) + wavemeter.sigWavelengthUpdated.connect(self._new_wavemeter_data) counter.sigScanFinished.connect(self._process_counter_data) daq.sigNewData.connect(self._new_daq_data) From 20eb17ebcbd35575ee940b11eb9be3264f531ac8 Mon Sep 17 00:00:00 2001 From: lange50 Date: Wed, 26 Mar 2025 14:19:46 -0400 Subject: [PATCH 20/22] Moved signals into interface, from hardware spec. I assume this is allowed (?) and it better represents how the interface functions. --- src/qudi/hardware/camera/andor_camera.py | 4 ---- src/qudi/hardware/daq/nidaq.py | 3 --- src/qudi/hardware/laser/solstis_laser.py | 4 +--- src/qudi/hardware/powermeter/thorlabs_power_meter.py | 4 +--- src/qudi/hardware/servo/thorlabs_servo.py | 4 ---- src/qudi/hardware/timetagger/swabian_tagger.py | 5 ----- src/qudi/hardware/wavemeter/high_finesse_wavemeter.py | 2 -- src/qudi/interface/camera_interface.py | 6 ++++++ src/qudi/interface/daq_reader_interface.py | 4 ++++ src/qudi/interface/fast_counter_interface.py | 6 ++++++ src/qudi/interface/motor_interface.py | 5 +++++ src/qudi/interface/scanning_laser_interface.py | 6 ++++++ src/qudi/interface/simple_powermeter_interface.py | 4 ++++ src/qudi/interface/simple_wavemeter_interface.py | 3 +++ 14 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/qudi/hardware/camera/andor_camera.py b/src/qudi/hardware/camera/andor_camera.py index c878065..8c64d9b 100644 --- a/src/qudi/hardware/camera/andor_camera.py +++ b/src/qudi/hardware/camera/andor_camera.py @@ -70,10 +70,6 @@ class AndorCamera(CameraInterface): default=1.0 ) - # signals - sigFrameChanged = QtCore.Signal(np.ndarray) - sigAcquisitionFinished = QtCore.Signal() - # Specify that camera should run in its own thread: _threaded = True diff --git a/src/qudi/hardware/daq/nidaq.py b/src/qudi/hardware/daq/nidaq.py index a95db4d..cc78938 100644 --- a/src/qudi/hardware/daq/nidaq.py +++ b/src/qudi/hardware/daq/nidaq.py @@ -47,9 +47,6 @@ class NIDAQ(DAQReaderInterface): missing='warn' ) - - # signals - sigNewData = QtCore.Signal(object) # is a List[ReaderVal] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index a7853ba..46087c7 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -48,9 +48,7 @@ class SolstisLaser(ScanningLaserInterface): _scan_rate = StatusVar(name='scan_rate', default=TeraScanRate.SCAN_RATE_FINE_LINE_10_GHZ) _scan_type= StatusVar(name='scan_type', default=TeraScanType.SCAN_TYPE_FINE) - # signals - sigScanStarted = QtCore.Signal() - sigScanFinished = QtCore.Signal() + # status variables: diff --git a/src/qudi/hardware/powermeter/thorlabs_power_meter.py b/src/qudi/hardware/powermeter/thorlabs_power_meter.py index 7db15b3..54b2e9c 100644 --- a/src/qudi/hardware/powermeter/thorlabs_power_meter.py +++ b/src/qudi/hardware/powermeter/thorlabs_power_meter.py @@ -28,9 +28,7 @@ class ThorlabsPowerMeter(SimplePowermeterInterface): _update_interval = ConfigOption(name='update_period', default=0, missing='info') - - # signals - sigNewData = QtCore.Signal(float) + # Run this in its own thread: _threaded = True diff --git a/src/qudi/hardware/servo/thorlabs_servo.py b/src/qudi/hardware/servo/thorlabs_servo.py index 0711ddc..7a48164 100644 --- a/src/qudi/hardware/servo/thorlabs_servo.py +++ b/src/qudi/hardware/servo/thorlabs_servo.py @@ -24,10 +24,6 @@ class ThorlabsServo(MotorInterface): missing='error' ) - # signals - sigMoveStarted = QtCore.Signal() - sigMoveFinished = QtCore.Signal() - # Run in separate thread: _threaded = True diff --git a/src/qudi/hardware/timetagger/swabian_tagger.py b/src/qudi/hardware/timetagger/swabian_tagger.py index 5a7c966..bdef915 100644 --- a/src/qudi/hardware/timetagger/swabian_tagger.py +++ b/src/qudi/hardware/timetagger/swabian_tagger.py @@ -50,11 +50,6 @@ class SwabianTimeTagger(FastCounterInterface): default={'counts': 0}, missing='warn' ) - - - # Signals: - sigScanStarted = QtCore.Signal() - sigScanFinished = QtCore.Signal(np.ndarray) # set to threaded: _threaded = True diff --git a/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py b/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py index 47c0a5a..87267f9 100644 --- a/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py +++ b/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py @@ -37,8 +37,6 @@ class HighFinesseWavemeter(SimpleWavemeterInterface): module.Class: 'wavemeter.high_finesse_wavemeter.HighFinesseWavemeter' """ - sigWavelengthUpdated = QtCore.Signal(np.ndarray) # 1-d array of wavelengths in nm - _threaded = True def on_activate(self): diff --git a/src/qudi/interface/camera_interface.py b/src/qudi/interface/camera_interface.py index 481c43f..93b2113 100644 --- a/src/qudi/interface/camera_interface.py +++ b/src/qudi/interface/camera_interface.py @@ -21,12 +21,18 @@ """ from abc import abstractmethod +from PySide2 import QtCore +import numpy as np from qudi.core.module import Base class CameraInterface(Base): """ This interface is used to manage and visualize a simple camera """ + + # signals + sigFrameChanged = QtCore.Signal(np.ndarray) + sigAcquisitionFinished = QtCore.Signal() @abstractmethod def get_name(self): diff --git a/src/qudi/interface/daq_reader_interface.py b/src/qudi/interface/daq_reader_interface.py index ad7c753..1c3568f 100644 --- a/src/qudi/interface/daq_reader_interface.py +++ b/src/qudi/interface/daq_reader_interface.py @@ -20,6 +20,7 @@ from enum import Enum from abc import abstractmethod +from PySide2 import QtCore from typing import List, Sequence from qudi.core.module import Base @@ -51,6 +52,9 @@ def __init__(self, class DAQReaderInterface(Base): + + # signals + sigNewData = QtCore.Signal(object) # is a List[ReaderVal] @property @abstractmethod def active_channels(self) -> List[str]: diff --git a/src/qudi/interface/fast_counter_interface.py b/src/qudi/interface/fast_counter_interface.py index 3b5460b..38cb0bc 100644 --- a/src/qudi/interface/fast_counter_interface.py +++ b/src/qudi/interface/fast_counter_interface.py @@ -21,6 +21,8 @@ """ from abc import abstractmethod +from PySide2 import QtCore +import numpy as np from qudi.core.module import Base @@ -39,6 +41,10 @@ class FastCounterInterface(Base): generally enough for a lot of experiment, where a memory consuming 2d array is not necessary. """ + + # Signals: + sigScanStarted = QtCore.Signal() + sigScanFinished = QtCore.Signal(np.ndarray) @abstractmethod def get_constraints(self): diff --git a/src/qudi/interface/motor_interface.py b/src/qudi/interface/motor_interface.py index 6f028e6..322f2b6 100644 --- a/src/qudi/interface/motor_interface.py +++ b/src/qudi/interface/motor_interface.py @@ -23,6 +23,7 @@ from typing import List from abc import abstractmethod +from PySide2 import QtCore from qudi.core.module import Base class MotorInterface(Base): @@ -33,6 +34,10 @@ class MotorInterface(Base): and a temperature regulation control. """ + + # signals + sigMoveStarted = QtCore.Signal() + sigMoveFinished = QtCore.Signal() @abstractmethod def get_motion_limits(self) -> List[float]: diff --git a/src/qudi/interface/scanning_laser_interface.py b/src/qudi/interface/scanning_laser_interface.py index 32f07ad..46fed6a 100644 --- a/src/qudi/interface/scanning_laser_interface.py +++ b/src/qudi/interface/scanning_laser_interface.py @@ -22,6 +22,8 @@ from enum import IntEnum from abc import abstractmethod from typing import List +from PySide2 import QtCore + from qudi.core.module import Base class ShutterState(IntEnum): @@ -47,6 +49,10 @@ class ScanningLaserInterface(Base): and a temperature regulation control. """ + + # signals + sigScanStarted = QtCore.Signal() + sigScanFinished = QtCore.Signal() @abstractmethod def get_power_range(self) -> List[float]: diff --git a/src/qudi/interface/simple_powermeter_interface.py b/src/qudi/interface/simple_powermeter_interface.py index eb36651..79eca5b 100644 --- a/src/qudi/interface/simple_powermeter_interface.py +++ b/src/qudi/interface/simple_powermeter_interface.py @@ -1,4 +1,5 @@ from abc import abstractmethod +from PySide2 import QtCore from qudi.core.module import Base @@ -8,6 +9,9 @@ class SimplePowermeterInterface(Base): """ + # signals + sigNewData = QtCore.Signal(float) + @abstractmethod def get_power(self) -> float: """ Retrieve the current wavelength(s) from the wavemeter diff --git a/src/qudi/interface/simple_wavemeter_interface.py b/src/qudi/interface/simple_wavemeter_interface.py index 843a15b..f42d2ee 100644 --- a/src/qudi/interface/simple_wavemeter_interface.py +++ b/src/qudi/interface/simple_wavemeter_interface.py @@ -1,4 +1,5 @@ import numpy as np +from PySide2 import QtCore from abc import abstractmethod from qudi.core.module import Base @@ -8,6 +9,8 @@ class SimpleWavemeterInterface(Base): """ + sigWavelengthUpdated = QtCore.Signal(np.ndarray) # 1-d array of wavelengths in nm + @abstractmethod def get_wavelengths(self) -> np.ndarray: """ Retrieve the current wavelength(s) from the wavemeter From 76f479aa26efb87f18fd24de05717ef780354683 Mon Sep 17 00:00:00 2001 From: lange50 Date: Thu, 27 Mar 2025 16:04:12 -0400 Subject: [PATCH 21/22] Fixes to get the laser rate/type to match properly. Updated scan type selection to be a DirectConnection so we don't need to pause weirdly to make sure the update has ocurred before we check for the new allowed rates. --- src/qudi/gui/terascan/terascan_gui.py | 6 ++++-- src/qudi/hardware/laser/solstis_laser.py | 4 ++-- src/qudi/logic/terascan_logic.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index 5c4be30..d23ed71 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -6,6 +6,7 @@ import os from PySide2 import QtCore, QtGui from typing import List +from time import sleep from qudi.util.datastorage import TextDataStorage from qudi.core.module import GuiBase @@ -54,9 +55,11 @@ def on_activate(self) -> None: self._mw.stop_wavelength.setValue(self._stop_wavelength) for txt, scan_type in self._terascan_logic().scan_types.items(): + print(txt, scan_type) self._mw.scan_type.addItem(txt, scan_type) for txt, scan_rate in self._terascan_logic().scan_rates.items(): self._mw.scan_rate.addItem(txt, scan_rate) + print(txt, scan_rate) # connect all GUI internal signals self._mw.start_wavelength.valueChanged.connect(self._start_changed) @@ -98,7 +101,7 @@ def on_activate(self) -> None: ) self.sigSetScanType.connect( - self._terascan_logic().set_scan_type, QtCore.Qt.QueuedConnection + self._terascan_logic().set_scan_type, QtCore.Qt.DirectConnection # this is direct on purpose ) self.sigSetScanRate.connect( @@ -178,7 +181,6 @@ def _scan_type_changed(self, _: int): """ Qt slot to be called upon scan type change """ self.sigSetScanType.emit(self._mw.scan_type.currentData().value) - # When type changes, update UI for allowed rates self._mw.scan_rate.clear() # Clear previous rates for txt, scan_rate in self._terascan_logic().scan_rates.items(): diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index 46087c7..5bbc200 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -55,8 +55,8 @@ class SolstisLaser(ScanningLaserInterface): _start_wavelength = StatusVar('start_wavelength', default=0.78) _end_wavelength = StatusVar('end_wavelength', default=0.785) - _scan_type = StatusVar('scan_type', default=2) - _scan_rate = StatusVar('scan_rate', default=13) + _scan_type = StatusVar('scan_type', default=TeraScanType.SCAN_TYPE_FINE) + _scan_rate = StatusVar('scan_rate', default=TeraScanRate.SCAN_RATE_FINE_LINE_20_GHZ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index c13c91a..1fefc9b 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -100,7 +100,7 @@ def on_activate(self): self.sigConfigureCounter.connect(counter.configure, QtCore.Qt.QueuedConnection) self.sigSetLaserWavelengths.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) self.sigSetLaserScanRate.connect(laser.set_scan_rate, QtCore.Qt.QueuedConnection) - self.sigSetLaserScanType.connect(laser.set_scan_type, QtCore.Qt.QueuedConnection) + self.sigSetLaserScanType.connect(laser.set_scan_type, QtCore.Qt.DirectConnection) # Is direct on purpose self.sigStartScan.connect(counter.start_measure, QtCore.Qt.QueuedConnection) self.sigStartScan.connect(laser.start_scan, QtCore.Qt.QueuedConnection) @@ -188,7 +188,7 @@ def set_scan_type(self, scan_type: int): self._scan_type = scan_type self.sigSetLaserScanType.emit(scan_type) - self._scan_rate = list(self._laser().get_scan_rates(scan_type))[0] + self._scan_rate = list(self._laser().get_scan_rates.values())[0].value self.sigSetLaserScanRate.emit(self._scan_rate) else: From efb5395217f82568462a37020efdf98a5f814ab9 Mon Sep 17 00:00:00 2001 From: lange50 Date: Thu, 27 Mar 2025 16:04:37 -0400 Subject: [PATCH 22/22] Removed debug prints. --- src/qudi/gui/terascan/terascan_gui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index d23ed71..c99ba1e 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -55,11 +55,9 @@ def on_activate(self) -> None: self._mw.stop_wavelength.setValue(self._stop_wavelength) for txt, scan_type in self._terascan_logic().scan_types.items(): - print(txt, scan_type) self._mw.scan_type.addItem(txt, scan_type) for txt, scan_rate in self._terascan_logic().scan_rates.items(): self._mw.scan_rate.addItem(txt, scan_rate) - print(txt, scan_rate) # connect all GUI internal signals self._mw.start_wavelength.valueChanged.connect(self._start_changed)