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/cfg/terascan.cfg b/cfg/terascan.cfg index 519d32a..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: @@ -108,20 +108,18 @@ 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: 39900 # 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' + # 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 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..1242ea9 --- /dev/null +++ b/src/qudi/gui/grating_scan/grating_scan_gui.py @@ -0,0 +1,286 @@ +# -*- 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' + ) + + 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: + 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..343ce93 --- /dev/null +++ b/src/qudi/gui/grating_scan/grating_scan_main_window.py @@ -0,0 +1,130 @@ +# -*- 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) + + + 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) + + + # 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..c99ba1e 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 @@ -19,11 +20,20 @@ # 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() sigSetWavelengths = QtCore.Signal(float, float) + sigSetScanType = QtCore.Signal(int) + sigSetScanRate = QtCore.Signal(int) sigSaveData = QtCore.Signal() # One logic module @@ -44,11 +54,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: @@ -81,6 +98,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.DirectConnection # this is direct on purpose + ) + + self.sigSetScanRate.connect( + self._terascan_logic().set_scan_rate, QtCore.Qt.QueuedConnection + ) + self._data = [] # Turn on update timer: @@ -120,6 +145,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 """ @@ -147,18 +174,28 @@ 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().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(): + 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 """ + if self._mw.scan_rate.currentData() is not None: + self.sigSetScanRate.emit(self._mw.scan_rate.currentData().value) + + ### 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') @@ -181,7 +218,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: @@ -189,8 +227,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 8dbe594..43360f6 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 """ @@ -37,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) @@ -58,14 +60,20 @@ 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() + + self.scan_type_label = QtWidgets.QLabel('Scan Type') + self.scan_type = QtWidgets.QComboBox() self.plot_widget = pg.PlotWidget() self.plot_widget.getPlotItem().setContentsMargins(1, 1, 1, 1) @@ -85,13 +93,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_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) + 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) diff --git a/src/qudi/hardware/camera/andor_camera.py b/src/qudi/hardware/camera/andor_camera.py index 58864a9..8c64d9b 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 @@ -20,7 +23,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 @@ -66,8 +69,17 @@ class AndorCamera(CameraInterface): name='default_exposure', default=1.0 ) + + # 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) self._handle = self._andor.GetCameraHandle(0) @@ -129,6 +141,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 +170,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 +299,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 +343,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 +414,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..cc78938 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,12 @@ class NIDAQ(DAQReaderInterface): }, missing='warn' ) + + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() def on_activate(self): """ Activate module. @@ -63,18 +74,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 +142,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/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/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 diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index ed0560d..5bbc200 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -17,7 +17,10 @@ """ 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 from qudi.interface.scanning_laser_interface import ShutterState @@ -36,28 +39,60 @@ 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) + + + + + # status variables: + _start_wavelength = StatusVar('start_wavelength', default=0.78) + _end_wavelength = StatusVar('end_wavelength', default=0.785) + + _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) + self.__timer = None + self._wavelength = -1 def on_activate(self): """ Activate module. """ self.connect_laser() - self._scan_rate = TeraScanRate(self._scan_rate) - self._scan_type = TeraScanType(self._scan_type) + + 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. @@ -175,34 +210,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: @@ -230,4 +279,87 @@ 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 + + @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 + } + @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, + ]: + 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 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, + '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 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') + + + 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: int): + self._scan_type = TeraScanType(scan_type) + + def set_scan_rate(self, scan_rate: int): + 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 new file mode 100644 index 0000000..54b2e9c --- /dev/null +++ b/src/qudi/hardware/powermeter/thorlabs_power_meter.py @@ -0,0 +1,72 @@ +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): + """ 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 + 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') + + + # 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() + 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) + + 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): + 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 + + + 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 new file mode 100644 index 0000000..7a48164 --- /dev/null +++ b/src/qudi/hardware/servo/thorlabs_servo.py @@ -0,0 +1,155 @@ +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): + """ 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' + ) + + # 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): + 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 + + 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): + 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]: + """ 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 + + @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? + """ + with self._thread_lock: + self._moving = True + self._motor.move_to(position, False) + + @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? + """ + 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 + + @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? + """ + with self._thread_lock: + self._moving = True + self._motor.move_home(blocking) + + @QtCore.Slot() + def stop(self): + """ Stops the motor motion + """ + 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..bdef915 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 @@ -47,12 +50,15 @@ class SwabianTimeTagger(FastCounterInterface): default={'counts': 0}, 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') + + # 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 +66,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 +133,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 +256,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..87267f9 100644 --- a/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py +++ b/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py @@ -37,7 +37,7 @@ 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): """ Activate module. @@ -81,6 +81,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 +102,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/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 new file mode 100644 index 0000000..322f2b6 --- /dev/null +++ b/src/qudi/interface/motor_interface.py @@ -0,0 +1,97 @@ +# -*- 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 PySide2 import QtCore +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. + + """ + + # signals + sigMoveStarted = QtCore.Signal() + sigMoveFinished = QtCore.Signal() + + @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/interface/scanning_laser_interface.py b/src/qudi/interface/scanning_laser_interface.py index 445b788..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]: @@ -124,13 +130,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 @@ -153,4 +166,41 @@ 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 + + @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/interface/simple_powermeter_interface.py b/src/qudi/interface/simple_powermeter_interface.py new file mode 100644 index 0000000..79eca5b --- /dev/null +++ b/src/qudi/interface/simple_powermeter_interface.py @@ -0,0 +1,21 @@ +from abc import abstractmethod +from PySide2 import QtCore +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. + + """ + + # signals + sigNewData = QtCore.Signal(float) + + @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/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 diff --git a/src/qudi/logic/common/camera_logic.py b/src/qudi/logic/common/camera_logic.py index 02c78a0..8cc2174 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): @@ -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 """ @@ -112,8 +120,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': @@ -127,6 +136,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/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/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 diff --git a/src/qudi/logic/common/scanning_laser_logic.py b/src/qudi/logic/common/scanning_laser_logic.py index 8028d44..c17ce92 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=2) + _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. """ @@ -92,16 +99,44 @@ def on_deactivate(self): @property def wavelength(self) -> float: - return self._wavelength + """In um""" + return self._laser().get_wavelength() @property def tolerance(self) -> float: return -1 + @property + def get_scan_types(self) -> dict: + return self._laser().get_scan_types() + + @property + 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: + 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) 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 e69de29..473d8a8 100644 --- a/src/qudi/logic/grating_scan_logic.py +++ b/src/qudi/logic/grating_scan_logic.py @@ -0,0 +1,700 @@ + +import numpy as np +import time +import datetime +import matplotlib.pyplot as plt +from PySide2 import QtCore + +from enum import Enum +from typing import List + +from qudi.core.module import LogicBase +from qudi.util.mutex import RecursiveMutex +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: + 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 GratingCalData(GratingScanData): + pixel: int + cal_signal: float + +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 + + 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 = 'Idle', + CALIBRATING = 'Calibrating', + ACQUIRING = 'Acquiring' + + +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 + 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 + _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') + _powermeter = Connector(name='powermeter', interface='PowerMeterLogic') + + # declare config options + _calibration_directory = ConfigOption(name='calibration_directory', default='path/to/calibration/file.csv') + + _norm_factor_tol = ConfigOption(name='normalization_factor_tol', default=0.001) + + _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) + _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)) + + + _current_data: List[GratingScanData] + _calibration_data: List[GratingCalData] + _calibration_index: int + _calibration_list: np.ndarray + _current_data: List[GratingScanData] + + _power_val: float + + + # Update signals, e.g. for GUI module + sigWavelengthUpdated = QtCore.Signal(float) + 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) + + sigStartCamera = 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() + powermeter = self._powermeter() + + # 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.sigStartCamera.connect(camera.capture_frame, QtCore.Qt.QueuedConnection) + + # Inputs: + camera.sigFrameChanged.connect(self._new_camera_frame, QtCore.Qt.QueuedConnection) + camera.sigAcquisitionFinished.connect(self._camera_acquisiton_finished, 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) + + # 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_at_target + + @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._calibration_list = [] + 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' + and self._needs_acq): + + 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 + 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() + else: + self._set_wavelength(wavelength=wavelength) + self.__timer.start(1) + else: + self.__timer.stop() + self._scan_state = GratingScanState.IDLE + self.sigScanFinished.emit() + + + + + + @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._scan_state = GratingScanState.ACQUIRING + self._sigNextAcq.emit() + + else: + wavelength = self._calibration_list[self._calibration_index] + self._set_wavelength(wavelength=wavelength) + 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_directory}') + + def _load_calibration(self): + ds = TextDataStorage( + root_dir=self.module_default_data_dir, + column_formats='.15e' + ) + + 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_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: + 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): + """ 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): + """ 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 + """ + self.locked = self._check_wavelength_status() + self.sigLaserLocked.emit(self.locked) + + 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): + wave = self._laser().wavelength + scan_data = GratingScanData( + laser_wave=wave, + 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 + + def _process_data(self): + """ 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 + + raw_data = self._current_data[-1].raw_data + num_pts = raw_data.shape[1] + processed_data = np.zeros((num_pts, 2)) + + 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 + """ + 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(float) + def _new_powermeter_data(self, power: float): + self._power_val = power + + + @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 + + + 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 diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index fb5ed0d..1fefc9b 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', @@ -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,23 +98,25 @@ 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.DirectConnection) # Is direct on purpose - 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) @@ -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.values())[0].value + self.sigSetLaserScanRate.emit(self._scan_rate) + else: self.__logger.warning( 'Tried to configure while a scan was running.'\ @@ -162,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) @@ -179,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)