From b6f413abe48f579a2370352626e9e313aee9eb70 Mon Sep 17 00:00:00 2001 From: lange50 Date: Sat, 26 Apr 2025 16:30:49 -0400 Subject: [PATCH 01/16] Basically rewrote everything The main change is that the timetagger doesn't ever stop the measurement. It just gets the most recent data. So there is no down time between collections. --- src/qudi/gui/terascan/terascan_gui.py | 38 +- src/qudi/hardware/daq/nidaq.py | 136 +- src/qudi/hardware/daq/nidaq_ben.py | 150 ++ src/qudi/hardware/laser/solstis_laser.py | 4 +- src/qudi/hardware/laser/solstis_laser_ben.py | 383 +++++ src/qudi/hardware/timetagger/Untitled-1.ipynb | 1501 +++++++++++++++++ .../hardware/timetagger/swabian_tagger.py | 266 +-- .../hardware/timetagger/swabian_tagger_ben.py | 270 +++ src/qudi/hardware/wavemeter/wavemeter.py | 87 + .../high_finesse_wavemeter.py | 0 .../{wavemeter => wavemeter_ben}/wlmConst.py | 0 .../{wavemeter => wavemeter_ben}/wlmData.py | 0 src/qudi/interface/fast_counter_interface.py | 212 +-- .../interface/simple_wavemeter_interface.py | 17 +- src/qudi/logic/terascan_logic.py | 146 +- src/qudi/logic/terascan_logic_ben.py | 312 ++++ 16 files changed, 3004 insertions(+), 518 deletions(-) create mode 100644 src/qudi/hardware/daq/nidaq_ben.py create mode 100644 src/qudi/hardware/laser/solstis_laser_ben.py create mode 100644 src/qudi/hardware/timetagger/Untitled-1.ipynb create mode 100644 src/qudi/hardware/timetagger/swabian_tagger_ben.py create mode 100644 src/qudi/hardware/wavemeter/wavemeter.py rename src/qudi/hardware/{wavemeter => wavemeter_ben}/high_finesse_wavemeter.py (100%) rename src/qudi/hardware/{wavemeter => wavemeter_ben}/wlmConst.py (100%) rename src/qudi/hardware/{wavemeter => wavemeter_ben}/wlmData.py (100%) create mode 100644 src/qudi/logic/terascan_logic_ben.py diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index a6882f8..5faf756 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -17,6 +17,8 @@ from qudi.logic.terascan_logic import TerascanData + +# TODO: No status variables. Just grab the ones from the logic module. class TerascanGui(GuiBase): """ Terascan Measurement GUI @@ -56,7 +58,7 @@ def on_activate(self) -> None: for txt, scan_rate in self._terascan_logic().scan_rates.items(): self._mw.scan_rate.addItem(txt, scan_rate) - # Connect GUI internal signals + ################# CONNECT SIGNALS FROM GUI TO GUI ############################ 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) @@ -64,7 +66,7 @@ def on_activate(self) -> None: self._mw.scan_type.currentIndexChanged.connect(self._scan_type_changed) self._mw.scan_rate.currentIndexChanged.connect(self._scan_rate_changed) - # Connect signals from the logic module + #################### CONNECT SIGNALS FROM LOGIC TO GUI #################### self._terascan_logic().sigWavelengthUpdated.connect( self._wavelength_changed, QtCore.Qt.QueuedConnection ) @@ -78,7 +80,7 @@ def on_activate(self) -> None: self._laser_lock_ui, QtCore.Qt.QueuedConnection ) - # Connect output signals to logic + ################### CONNECT SIGNALS FROM GUI TO LOGIC #################### self.sigStartMeasurement.connect( self._terascan_logic().start_scan, QtCore.Qt.QueuedConnection ) @@ -179,11 +181,13 @@ def _scan_finished(self) -> None: def _receive_data(self, data: List[TerascanData]) -> None: self._data = data + #TODO: bring the loading bar back @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))) + pass + # 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: @@ -213,13 +217,22 @@ def _update_ui(self, running: bool) -> None: def _update_plot(self): # Make a local snapshot of the data - local_data = self._data[:] - if not local_data: + + x_array = np.array(self._terascan_logic().wavelength_data) + y_array = np.array(self._terascan_logic().counts_data[:len(x_array)]) + mask = self._terascan_logic().ttl_data + + x_array = x_array[mask] + y_array = y_array[mask] + + if x_array.shape[0] != y_array.shape[0]: + return # skip this update + if x_array.shape[0] == 0 or y_array.shape[0] == 0: return - # Convert data to arrays - x_array = np.array([d.wavelength*1e-3 for d in local_data]) - y_array = np.array([d.counts for d in local_data]) + # downsample + x_array = x_array[::10] + y_array = y_array[::10] # If running average is enabled, apply a rolling average if self._mw.checkbox_running_avg.isChecked(): @@ -229,8 +242,7 @@ def _update_plot(self): y_array = np.convolve(y_array, kernel, mode='same') # Final sanity-check - if x_array.shape[0] != y_array.shape[0]: - return # skip this update + self._mw.data_item.setData(x=x_array, y=y_array) diff --git a/src/qudi/hardware/daq/nidaq.py b/src/qudi/hardware/daq/nidaq.py index cc78938..c2b6e04 100644 --- a/src/qudi/hardware/daq/nidaq.py +++ b/src/qudi/hardware/daq/nidaq.py @@ -1,14 +1,29 @@ import nidaqmx from typing import List, Any, Dict +import time from PySide2 import QtCore +from qudi.core.module import Base from qudi.util.mutex import RecursiveMutex from qudi.core.configoption import ConfigOption from qudi.interface.daq_reader_interface import DAQReaderInterface, InputType, \ ReaderVal -class NIDAQ(DAQReaderInterface): + +ICEBLOC = "Dev2/port1/line0" +FLIPPER = 'Dev2/port1/line2' +SHUTTER = 'Dev2/port2/line0' +GO_line = 'Dev2/port1/line1' +M2_CAVITY = "Dev2/ai0, Dev2/ai4" # 0 is positive, 4 is negative + + +# TODO: extend daq reader interface +# TODO: get status variable +# TODO: Define the tasks in the _init function and start them in the on_activate function. +# This is much more time efficient +# You will still have to have a way to stop that if you want to use a digital output +class NIDAQ(Base): """ Generic interface for reading from NIDAQ hardware. @@ -26,59 +41,25 @@ class NIDAQ(DAQReaderInterface): name: 'line0' # The name as identified by the card port: 1 # port number identified by the card """ - - # config options - _update_interval: int = ConfigOption(name='update_interval', - default=0) - - _daq_name: str = ConfigOption(name='device_str', default='Dev1', - missing='warn') - - _daq_ch_config: Dict[str, Dict[str, Any]] = ConfigOption( - name='channels', - default={ - 'default_channel': { - 'description': 'Input Signal', - 'type': 0, - 'name': 'line0', - 'port': 0 - } - }, - missing='warn' - ) - + # define the daq signal + sigNewData = QtCore.Signal(float, object) # is a List[ReaderVal] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__timer = None self._thread_lock = RecursiveMutex() + self._data = 0 def on_activate(self): """ Activate module. """ - - temp: List[ReaderVal] = ( - ReaderVal( - type=InputType(v['type']), - name=v['name'], - port=v['port'], - description=v['description'], - ) for v in self._daq_ch_config.values() - ) - - self._analog_channels: List[ReaderVal] = [] - self._digital_channels: List[ReaderVal] = [] - - for i in temp: - if (i.type == InputType.ANALOG): - self._analog_channels.append(i) - elif (i.type == InputType.DIGITAL): - self._digital_channels.append(i) - + self._ttl_task = nidaqmx.Task() + self._ttl_task.di_channels.add_di_chan(ICEBLOC) + self.__timer = QtCore.QTimer() self.__timer.timeout.connect(self.__data_update) self.__timer.setSingleShot(False) - self.__timer.start(int(self._update_interval)) + self.__timer.start(10) def on_deactivate(self): """ Deactivate module. @@ -87,64 +68,29 @@ def on_deactivate(self): 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) - return (i.description for i in out) + if self._ttl_task: + self._ttl_task.close() + self._ttl_task = None - def get_reading(self) -> List[ReaderVal]: - """ Gets a reading from the device """ - - if (len(self._analog_channels) > 0): - with nidaqmx.Task() as task: - for i in self._analog_channels: - chan = self._get_channel(i) - task.ai_channels.add_ai_voltage_chan(chan) - - data = task.read() - if not isinstance(data, list): - data = [data] - - self._update_vals(data, self._analog_channels) - - if (len(self._digital_channels) > 0): - with nidaqmx.Task() as task: - for i in self._digital_channels: - chan = self._get_channel(i) - task.di_channels.add_di_chan(chan) - - data = task.read() - # if len(data) == 1: - if not isinstance(data, list): - data = [data] - - self._update_vals(data, self._digital_channels) + def start_reading(self): + self.module_state.lock() + self.statusvar = 2 + + def stop_reading(self): + if self.module_state() == 'locked': + self.module_state.unlock() + self.statusvar = 1 - - out = self._analog_channels.copy() - out.extend(self._digital_channels) - return out - - - - def _get_channel(self, chan: ReaderVal) -> str: - return f"{self._daq_name}/port{chan.port}/{chan.name}" - def _update_vals(self, vals: List[float], chans: List[ReaderVal]) -> None: - """ Updates channels in-place with new value. Assumes one sample per channel - """ - - if (len(vals) != len(chans)): - self.log.warning('Mismatch between number of configured channels and number of data points read.') - - for v, c in zip(vals, chans): - c.val = v + def get_solstis_ttl(self) -> int: + return self._ttl_task.read() + def __data_update(self): with self._thread_lock: - data = self.get_reading() - self.sigNewData.emit(data) + # It takes < 2 ms to read + timestamp = time.perf_counter() + self._data = self._ttl_task.read() + self.sigNewData.emit(timestamp, self._data) diff --git a/src/qudi/hardware/daq/nidaq_ben.py b/src/qudi/hardware/daq/nidaq_ben.py new file mode 100644 index 0000000..cc78938 --- /dev/null +++ b/src/qudi/hardware/daq/nidaq_ben.py @@ -0,0 +1,150 @@ +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 + + +class NIDAQ(DAQReaderInterface): + """ + Generic interface for reading from NIDAQ hardware. + + Example config for copy-paste: + + 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: + description: 'Input Signal' + type: 0 # 0 for Digital, 1 for Analog + name: 'line0' # The name as identified by the card + port: 1 # port number identified by the card + """ + + # config options + _update_interval: int = ConfigOption(name='update_interval', + default=0) + + _daq_name: str = ConfigOption(name='device_str', default='Dev1', + missing='warn') + + _daq_ch_config: Dict[str, Dict[str, Any]] = ConfigOption( + name='channels', + default={ + 'default_channel': { + 'description': 'Input Signal', + 'type': 0, + 'name': 'line0', + 'port': 0 + } + }, + missing='warn' + ) + + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() + + def on_activate(self): + """ Activate module. + """ + + temp: List[ReaderVal] = ( + ReaderVal( + type=InputType(v['type']), + name=v['name'], + port=v['port'], + description=v['description'], + ) for v in self._daq_ch_config.values() + ) + + self._analog_channels: List[ReaderVal] = [] + self._digital_channels: List[ReaderVal] = [] + + for i in temp: + if (i.type == InputType.ANALOG): + 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. + """ + 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) + return (i.description for i in out) + + + def get_reading(self) -> List[ReaderVal]: + """ Gets a reading from the device """ + + if (len(self._analog_channels) > 0): + with nidaqmx.Task() as task: + for i in self._analog_channels: + chan = self._get_channel(i) + task.ai_channels.add_ai_voltage_chan(chan) + + data = task.read() + if not isinstance(data, list): + data = [data] + + self._update_vals(data, self._analog_channels) + + if (len(self._digital_channels) > 0): + with nidaqmx.Task() as task: + for i in self._digital_channels: + chan = self._get_channel(i) + task.di_channels.add_di_chan(chan) + + data = task.read() + # if len(data) == 1: + if not isinstance(data, list): + data = [data] + + self._update_vals(data, self._digital_channels) + + + out = self._analog_channels.copy() + out.extend(self._digital_channels) + return out + + + + def _get_channel(self, chan: ReaderVal) -> str: + return f"{self._daq_name}/port{chan.port}/{chan.name}" + + def _update_vals(self, vals: List[float], chans: List[ReaderVal]) -> None: + """ Updates channels in-place with new value. Assumes one sample per channel + """ + + if (len(vals) != len(chans)): + self.log.warning('Mismatch between number of configured channels and number of data points read.') + + for v, c in zip(vals, chans): + c.val = v + + def __data_update(self): + with self._thread_lock: + data = self.get_reading() + self.sigNewData.emit(data) + diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index 795fefa..f4e95e6 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -44,7 +44,7 @@ class SolstisLaser(ScanningLaserInterface): _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') + _laser_port = ConfigOption(name='laser_port', default=39900, 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) @@ -54,7 +54,7 @@ class SolstisLaser(ScanningLaserInterface): # status variables: _start_wavelength = StatusVar('start_wavelength', default=0.78) - _end_wavelength = StatusVar('end_wavelength', default=0.785) + _end_wavelength = StatusVar('end_wavelength', default=0.7801) _scan_type = StatusVar('scan_type', default=TeraScanType.SCAN_TYPE_FINE) _scan_rate = StatusVar('scan_rate', default=TeraScanRate.SCAN_RATE_FINE_LINE_20_GHZ) diff --git a/src/qudi/hardware/laser/solstis_laser_ben.py b/src/qudi/hardware/laser/solstis_laser_ben.py new file mode 100644 index 0000000..0f3819c --- /dev/null +++ b/src/qudi/hardware/laser/solstis_laser_ben.py @@ -0,0 +1,383 @@ +# -*- coding: utf-8 -*- +""" +This module controls Solstis Lasers. + +Copyright (c) 2025, the 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 typing import List + +from PySide2 import QtCore +from time import time + +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 + +import qudi.hardware.laser.solstis_funcs as solstis +from qudi.hardware.laser.solstis_constants import * + + +class SolstisLaser(ScanningLaserInterface): + """ + Hardware file for solstis laser. + Example config for copy-paste: + + 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 + """ + + _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 = 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.7801) + + _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._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() + + self._scan_started = time() + + 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. + + @return bool: connection success + """ + try: + self.socket = solstis.init_socket(address=self._laser_ip, + port=self._laser_port) + solstis.start_link(sock=self.socket, ip_address=self._host_ip) + except solstis.SolstisError as e: + self.log.exception(f'Communication Failure: {e.message}') + return False + else: + return True + + def disconnect_laser(self) -> None: + """ Close the connection to the instrument. + """ + try: + self.socket.close() + except Exception as e: + print(e) + + def get_power(self) -> float: + """ Get laser power. + + @return float: laser power in watts + """ + try: + answer = solstis.get_status(self.socket) + return(answer['output_monitor']) + except solstis.SolstisError as e: + self.log.exception(f'Failure getting power: {e.message}') + return -1 + + def get_power_setpoint(self) -> float: + """ Get the laser power setpoint. (unimplemented) + + @return float: laser power setpoint in watts + """ + return -1 + + def get_power_range(self) -> List[float]: + """ Get laser power range. (unimplemented) + + @return float[2]: laser power range + """ + return [0, -1] + + def set_power(self, power: float): + """ Set laser power (unimplemented) + + @param float power: desired laser power in watts + """ + pass + + def get_shutter_state(self): + """ Get laser shutter state. + + @return ShutterState: laser shutter state + """ + return ShutterState.NO_SHUTTER + + def set_shutter_state(self, state): + """ Set the desired laser shutter state. + + @param ShutterState state: desired laser shutter state + @return ShutterState: actual laser shutter state + """ + pass + + + def get_temperatures(self) -> dict: + """ Get all available temperatures. + + @return dict: dict of temperature names and value + """ + + try: + answer = solstis.get_status(self.socket) + return {'laser': float(answer['temperature'])} + except solstis.SolstisError as e: + self.log.exception(f'Failure getting temperature: {e.message}') + return -1 + + + + def get_laser_state(self): + """ Get laser operation state + + @return LaserState: laser state + """ + + try: + return solstis.scan_stitch_status(self.socket, self._scan_type) + except solstis.SolstisError as e: + self.log.exception(f'Failure getting status: {e.message}') + return -1 + + + def set_laser_state(self, status): + """ Set desited laser state. (unimplemented) + + @param LaserState status: desired laser state + """ + pass + + + def get_extra_info(self): + """ Extra information from laser. (unimplemented) + For LaserQuantum devices, this is the firmware version, dump and timers information + + @return str: multiple lines of text with information about laser + """ + pass + + @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: + 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._scan_started = time() + 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: + 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: + self.log.exception(f'Scan stop failure: {e.message}') + return False + + @QtCore.Slot(float) + def restart_scan(self, wavelength: float) -> bool: + """Restart a scan from a specified wavelength""" + + if self.module_state() == 'locked': + self.module_state.unlock() + self._start_wavelength = wavelength + self._scan_started = time() + return self.start_scan() + else: + self.log.warning('Restart scan called when not running') + return False + + def pause_scan(self): + """Pause a running scan (unimplemented)""" + pass + + def resume_scan(self) -> bool: + try: + solstis.terascan_continue(self.socket) + return True + + except solstis.SolstisError as e: + self.log.exception(f'Scan resume failure: {e.message}') + return False + + def get_wavelength(self) -> float: + "Returns wavelength in um" + try: + resp = solstis.poll_wave_m(self.socket) + return resp[0] * 1e-3 + + except solstis.SolstisError as e: + self.log.exception(f'Scan resume failure: {e.message}') + return -1 + + @QtCore.Slot(float) + def set_wavelength(self, wavelength: float): + "Sets wavelength (wavelength in um)" + solstis.set_wave_m(self.socket, wavelength*1e3) + @property + def get_scan_types(self) -> dict: + return { + 'Medium': TeraScanType.SCAN_TYPE_MEDIUM, + 'Fine': TeraScanType.SCAN_TYPE_FINE, + 'Line': TeraScanType.SCAN_TYPE_LINE + } + @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' \ + and self._scan_started + 3 < time(): # if we check for status too soon, we might get a false positive + 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/timetagger/Untitled-1.ipynb b/src/qudi/hardware/timetagger/Untitled-1.ipynb new file mode 100644 index 0000000..ea4312d --- /dev/null +++ b/src/qudi/hardware/timetagger/Untitled-1.ipynb @@ -0,0 +1,1501 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "38eaf3ee", + "metadata": {}, + "source": [ + "# ASDFSDA" + ] + }, + { + "cell_type": "code", + "execution_count": 147, + "id": "337f7288", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "import socket\n", + "import json\n", + "\n", + "from qudi.hardware.laser.solstis_constants import *\n", + "\n", + "\"\"\" https://github.com/Rywais/solstis_tcpip\"\"\"\n", + "\n", + "#Global variables for use within module\n", + "next_data = '' #Extra TCP socket data to carry forward for next read statement\n", + "\n", + "\n", + "\n", + "\n", + "#Exception class for Solstis specific errors\n", + "class SolstisError(Exception):\n", + " \"\"\"Exception raised when the Solstis response indicates an error\n", + "\n", + " Attributes:\n", + " message ~ explanation of the error\n", + " \"\"\"\n", + " def __init__(self,message):\n", + " self.message = message\n", + "\n", + "def init_socket(address='192.168.1.222',port=39933) -> socket.socket:\n", + " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n", + " sock.connect((address,port))\n", + " sock.settimeout(30)\n", + " return sock\n", + "\n", + "def send_msg(s,transmission_id=1,op='start_link',params=None,debug=False):\n", + " \"\"\"\n", + " Function to carry out the most basic communication send function\n", + " s ~ Socket\n", + " transmission_id ~ Arbitrary(?) integer\n", + " op ~ String containing operating command\n", + " params ~ dict containing Solstis Key/Value pairs as necessary\n", + " \"\"\"\n", + " if params is not None:\n", + " message = {\"transmission_id\": [transmission_id],\n", + " \"op\": op,\n", + " \"parameters\": params}\n", + " else:\n", + " message = {\"transmission_id\": [transmission_id],\n", + " \"op\": op}\n", + " command = {\"message\": message}\n", + " send_msg = json.dumps(command).encode('utf8')\n", + " if debug==True:\n", + " print(send_msg)\n", + " s.sendall(send_msg)\n", + "\n", + "def recv_msg(s,timeout=30.):\n", + " global next_data\n", + " i = 0 #Index\n", + " open_brc_count = 1 #Open Brace Count\n", + " close_brc_count = 0 #Closing brace count\n", + " #Initialize data\n", + " data = next_data\n", + "\n", + " #Check For existing data and if so, parse it\n", + " if len(data) > 0:\n", + " if data[0] != \"{\":\n", + " raise SolstisError(\"Stored data from previous TCP/IP is invalid.\")\n", + " \n", + " #Check if existing data contains complete message\n", + " for i in range(1,len(data)):\n", + " if data[i] == \"{\":\n", + " open_brc_count += 1\n", + " elif data[i] == \"}\":\n", + " close_brc_count += 1\n", + " if close_brc_count == open_brc_count:\n", + " next_data = data[i+1:len(data)]\n", + " data = data[0:i+1]\n", + " return json.loads(data)\n", + " \n", + " #There is NOT a complete message cached so we must continue to read TCP/IP\n", + "\n", + " #Start timing in case of timeout\n", + " init_time = time.perf_counter()\n", + " #Loop reading TCP/IP until there is some data\n", + " while len(data) == 0:\n", + " data += s.recv(1024).decode('utf8')\n", + " if time.perf_counter() - init_time > timeout:\n", + " raise TimeoutError()\n", + "\n", + " #Check (if not already done so) that the message starts with a '{'\n", + " if i == 0:\n", + " if data[0] != \"{\":\n", + " raise SolstisError(\"Received data from TCP/IP is invalid.\")\n", + "\n", + " #Loop checking for complete message and receiving new data\n", + " while True:\n", + " if len(data) > i+1:\n", + " for i in range(i+1,len(data)):\n", + " if data[i] == \"{\":\n", + " open_brc_count += 1\n", + " elif data[i] == \"}\":\n", + " close_brc_count += 1\n", + " if close_brc_count == open_brc_count:\n", + " next_data = data[i+1:len(data)]\n", + " data = data[0:i+1]\n", + " return json.loads(data)\n", + " data += s.recv(1024).decode('utf8')\n", + " if time.perf_counter() - init_time > timeout:\n", + " raise TimeoutError()\n", + "\n", + "def verify_msg(msg,op=None,transmission_id=None):\n", + " msgID = msg[\"message\"][\"transmission_id\"][0]\n", + " msgOP = msg[\"message\"][\"op\"]\n", + " if transmission_id is not None:\n", + " if msgID != transmission_id:\n", + " err_msg = \"Message with ID\"+str(msgID)+\" did not match expected ID of: \"+\\\n", + " str(transmission_id)\n", + " raise SolstisError(err_msg)\n", + " if msgOP == \"parse_fail\":\n", + " err_msg = \"Mesage with ID \"+str(msgID)+\" failed to parse.\"\n", + " err_msg += '\\n\\n'+str(msg)\n", + " raise SolstisError(err_msg)\n", + " if op is not None:\n", + " if msgOP != op:\n", + " msg = \"Message with ID\"+str(msgID)+\"with operation command of '\"+msgOP+\\\n", + " \"' did not match expected operation command of: \"+op\n", + " raise SolstisError(msg)\n", + "\n", + "def start_link(sock,transmission_id=1,ip_address='192.168.1.222'):\n", + " send_msg(sock,transmission_id,'start_link',{'ip_address': ip_address})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op='start_link_reply')\n", + " if val[\"message\"][\"parameters\"][\"status\"] == \"ok\":\n", + " return\n", + " elif val[\"message\"][\"parameters\"][\"status\"] == \"failed\":\n", + " raise SolstisError(\"Link could not be formed\")\n", + " else:\n", + " raise SolstisError(\"Unknown error: Could not determine link status\")\n", + "\n", + "def set_wave_m(sock, wavelength, transmission_id = 1):\n", + " \"\"\"Sets wavelength given that a wavelength meter is configured\n", + "\n", + " Parameters:\n", + " sock ~ Socket object to use\n", + " wavelength ~ (float) wavelength to tune to in nanometers\n", + " transmission_id ~ (int) Arbitrary integer\n", + " Returns:\n", + " The wavelength of the most recent measurement made by the wavelength meter\n", + " \"\"\"\n", + " send_msg(sock,transmission_id,\"set_wave_m\",{\"wavelength\": [wavelength]})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op=\"set_wave_m_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"]\n", + " if status == 1:\n", + " raise SolstisError(\"No (wavelength) meter found.\")\n", + " elif status == 2:\n", + " raise SolstisError(\"Wavelength Out of Range.\")\n", + " return val[\"message\"][\"parameters\"][\"wavelength\"][0]\n", + "\n", + "#Same as above but requests a final report as well\n", + "def set_wave_m_f_r(sock, wavelength, transmission_id = 1):\n", + " \"\"\"Sets wavelength given that a wavelength meter is configured\n", + "\n", + " Parameters:\n", + " sock ~ Socket object to use\n", + " wavelength ~ (float) wavelength to tune to in nanometers\n", + " transmission_id ~ (int) Arbitrary integer\n", + " Returns:\n", + " The wavelength of the most recent measurement made by the wavelength meter\n", + " \"\"\"\n", + " send_msg(sock,transmission_id,\"set_wave_m\",{\"wavelength\": [wavelength],\n", + " \"report\": \"finished\"})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op=\"set_wave_m_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"]\n", + " if status == 1:\n", + " raise SolstisError(\"No (wavelength) meter found.\")\n", + " elif status == 2:\n", + " raise SolstisError(\"Wavelength Out of Range.\")\n", + " #Final Report\n", + " val = recv_msg(sock)\n", + " verify_msg(val,op=\"set_wave_m_f_r\")\n", + " #TODO: Check other variables\n", + " return val[\"message\"][\"parameters\"][\"wavelength\"][0]\n", + "\n", + "def poll_wave_m(sock,transmission_id=1):\n", + " \"\"\"Gets the latest Wavemeter reading and current wavelength tuning status\n", + "\n", + " Parameters:\n", + " sock ~ socket object to use\n", + " transmission_id ~ (int) Arbitrary integer to use for communications\n", + " Returns:\n", + " Tuple containing (in increasing index order):\n", + " -floating point value for current wavelength\n", + " -Boolean stating whether tuning is done/inactive (True = Not tuning)\n", + " \"\"\"\n", + "\n", + " send_msg(sock,transmission_id,\"poll_wave_m\")\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op=\"poll_wave_m_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 1:\n", + " raise SolstisError(\"No (wavelength) meter found.\")\n", + " elif status == 0 or status == 3:\n", + " status = True #Not tuning\n", + " else:\n", + " status = False #Still Tuning\n", + " return val[\"message\"][\"parameters\"][\"current_wavelength\"][0], status\n", + "\n", + "def move_wave_t(sock, wavelength, transmission_id=1):\n", + " \"\"\"Sets the wavelength based on wavelength table\n", + "\n", + " Parameters:\n", + " sock ~ socket object to use\n", + " wavelength ~ (float) wavelength set point\n", + " transmission_id ~ (int) Arbitrary integer for communications\n", + " Returns:\n", + " Nothing\n", + " \"\"\"\n", + "\n", + " send_msg(sock,transmission_id,\"move_wave_t\", {\"wavelength\": [wavelength]})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op=\"move_wave_t_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"move_wave_t: Failed, is your wavemeter configured?\")\n", + " else:\n", + " raise SolstisError(\"Wavelength out of range.\")\n", + "\n", + "def poll_move_wave_t(sock,transmission_id=1):\n", + " \"\"\"Gets the currently set wavelength according to wavelength table\n", + "\n", + " Parameters:\n", + " sock ~ socket object to use\n", + " transmission_id ~ (int) Arbitrary integer for communications\n", + " Returns:\n", + " Tuple containing the following (in increasing index order):\n", + " -Current wavelength\n", + " -Boolean with value True if Tuning is not taking place, False o/w\n", + " \"\"\"\n", + " send_msg(sock,transmission_id,\"poll_move_wave_t\")\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op=\"poll_move_wave_t_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 2:\n", + " raise SolstisError(\"poll_move_wave_t: Failed,is your wavemeter configured?\")\n", + " else:\n", + " status = True\n", + " return val[\"message\"][\"parameters\"][\"wavelength\"][0], status\n", + "\n", + "\n", + "\n", + "#TODO: Ensure that the Units parameters is filled in\n", + "def scan_stitch_initialize(sock,\n", + " scan_type,\n", + " start,\n", + " stop,\n", + " scan_rate,\n", + " transmission_id=1):\n", + " \"\"\"Initializes TeraScan operations\n", + "\n", + " Parameters:\n", + " sock ~ Socket to use for communications\n", + " transmission_id ~ (int) Arbitrary integer for communications \n", + " scan_type ~ (TeraScan Enum) Type of scan to perform\n", + " start ~ (float) Starting wavelength for scan\n", + " stop ~ (float) Ending wavelength for scan\n", + " scan_rate ~ (TeraScan Enum) Scan rate for scan1\n", + " Returns:\n", + " Nothing on success\n", + " Raises:\n", + " SolstisError on failure to initialize\n", + " ValueError on illegal argument input\n", + " \"\"\"\n", + "\n", + " #Create the message based on Input:\n", + " #Scan Type:\n", + " if scan_type == TeraScanType.SCAN_TYPE_MEDIUM:\n", + " scan_type = \"medium\"\n", + " elif scan_type == TeraScanType.SCAN_TYPE_FINE:\n", + " scan_type = \"fine\"\n", + " elif scan_type == TeraScanType.SCAN_TYPE_LINE:\n", + " scan_type = \"line\"\n", + " else:\n", + " raise ValueError('scan_type is not a valid TeraScan Enum')\n", + "\n", + " #Scan Rate and units:\n", + " if scan_rate == TeraScanRate.SCAN_RATE_MEDIUM_100_GHZ:\n", + " scan_rate = [100]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_MEDIUM_50_GHZ:\n", + " scan_rate = [50]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_MEDIUM_20_GHZ:\n", + " scan_rate = [20]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_MEDIUM_15_GHZ:\n", + " scan_rate = [15]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_MEDIUM_10_GHZ:\n", + " scan_rate = [10]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_MEDIUM_5_GHZ:\n", + " scan_rate = [5]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_MEDIUM_2_GHZ:\n", + " scan_rate = [2]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_MEDIUM_1_GHZ:\n", + " scan_rate = [1]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_20_GHZ:\n", + " scan_rate = [20]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_10_GHZ:\n", + " scan_rate = [10]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_5_GHZ:\n", + " scan_rate = [5]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_2_GHZ:\n", + " scan_rate = [2]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_1_GHZ:\n", + " scan_rate = [1]; units = \"GHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_500_MHZ:\n", + " scan_rate = [500]; units = \"MHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_200_MHZ:\n", + " scan_rate = [200]; units = \"MHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_100_MHZ:\n", + " scan_rate = [100]; units = \"MHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_50_MHZ:\n", + " scan_rate = [50]; units = \"MHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_20_MHZ:\n", + " scan_rate = [20]; units = \"MHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_10_MHZ:\n", + " scan_rate = [10]; units = \"MHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_5_MHZ:\n", + " scan_rate = [5]; units = \"MHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_2_MHZ:\n", + " scan_rate = [2]; units = \"MHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_FINE_LINE_1_MHZ:\n", + " scan_rate = [1]; units = \"MHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_LINE_500_KHZ:\n", + " scan_rate = [500]; units = \"kHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_LINE_200_KHZ:\n", + " scan_rate = [200]; units = \"kHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_LINE_100_KHZ:\n", + " scan_rate = [100]; units = \"kHz/s\"\n", + " elif scan_rate == TeraScanRate.SCAN_RATE_LINE_50_KHZ:\n", + " scan_rate = [50]; units = \"kHz/s\"\n", + " else:\n", + " raise ValueError(\"Input Scan rate is not valid TeraScanRate Enum.\")\n", + "\n", + " send_msg(sock,transmission_id,\"scan_stitch_initialise\",\n", + " {\"scan\": scan_type,\n", + " \"start\": [start],\n", + " \"stop\": [stop],\n", + " \"rate\": scan_rate,\n", + " \"units\": units})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,\n", + " op=\"scan_stitch_initialise_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"TeraScan start wavelength out of range.\")\n", + " elif status == 2:\n", + " raise SolstisError(\"TeraScan stop wavelength out of range.\")\n", + " elif status == 3:\n", + " raise SolstisError(\"TeraScan requested scan range is out of range.\")\n", + " else:\n", + " raise SolstisError(\"TeraScan is not available.\")\n", + "\n", + "def scan_stitch_op(sock, scan_type, operation, transmission_id=1):\n", + " \"\"\"Controls the TeraScan Operation\n", + "\n", + " Parameters:\n", + " sock ~ Socket to use for communications\n", + " transmission_id ~ (int) Arbitrary integer for use in communications\n", + " scan_type ~ (TeraScan Enum) Type of scan to carry out \n", + " operation ~ (str) Either \"start\" or \"stop\"\n", + " Returns:\n", + " Nothing\n", + " Raises:\n", + " SolstisError on failure to execute command\n", + " ValueError if scan type is invalid\n", + " \"\"\"\n", + "\n", + " #Translate Scan type:\n", + " if scan_type == TeraScanType.SCAN_TYPE_MEDIUM:\n", + " scan_type = \"medium\"\n", + " elif scan_type == TeraScanType.SCAN_TYPE_FINE:\n", + " scan_type = \"fine\"\n", + " elif scan_type == TeraScanType.SCAN_TYPE_LINE:\n", + " scan_type = \"line\"\n", + " else:\n", + " raise ValueError(\"scan_type is not a valid TeraScan Enum\")\n", + "\n", + " send_msg(sock,transmission_id,\"scan_stitch_op\",{\n", + " \"scan\": scan_type,\n", + " \"operation\": operation})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op=\"scan_stitch_op_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"TeraScan Failed; Unknown Reason.\")\n", + " else:\n", + " raise SolstisError(\"TeraScan not Available.\")\n", + "\n", + "def scan_stitch_status(sock,scan_type,transmission_id=1):\n", + " \"\"\"Checks the status of the TeraScan operations on Solstis\n", + "\n", + " Parameters:\n", + " sock ~ Socket to use for communications\n", + " transmission_id ~ (int) Arbitrary integer for communications\n", + " scan_type ~ (TeraScan Enum) Type of TeraScan\n", + " Returns:\n", + " Dictionary containing the following key/value pairs:\n", + " \"in_progress\" ~ (Boolean) True if a scan is in progress [Note: Other\n", + " values will be omitted if this is False.]\n", + " \"wavelength\" ~ (float) Current wavelength in scan\n", + " \"start\" ~ (float) Starting wavelength from scan\n", + " \"stop\" ~ (float) Ending wavelength in scan\n", + " \"tuning\" ~ (Boolean) True if TeraScan is currently tuning and False if\n", + " it's currently scanning\n", + " Raises:\n", + " SolstisError if TeraScan is not available\n", + " ValueError if scan_type is not a valid TeraScan Enum\n", + "\n", + " \"\"\"\n", + " #Scan Type:\n", + " if scan_type == TeraScanType.SCAN_TYPE_MEDIUM:\n", + " scan_type = \"medium\"\n", + " elif scan_type == TeraScanType.SCAN_TYPE_FINE:\n", + " scan_type = \"fine\"\n", + " elif scan_type == TeraScanType.SCAN_TYPE_LINE:\n", + " scan_type = \"line\"\n", + " else:\n", + " raise ValueError('scan_type is not a valid TeraScan Enum')\n", + " send_msg(sock,transmission_id,\"scan_stitch_status\",{\"scan\":scan_type})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,\n", + " op=\"scan_stitch_status_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " in_progress = False\n", + " return {\"in_progress\":in_progress}\n", + " elif status == 1:\n", + " in_progress = True\n", + " else:\n", + " raise SolstisError(\"TeraScan is not available\")\n", + "\n", + " #At this point we know in_progress=True so we fill out other entries\n", + " wavelength = val[\"message\"][\"parameters\"][\"current\"][0]\n", + " start = val[\"message\"][\"parameters\"][\"start\"][0]\n", + " stop = val[\"message\"][\"parameters\"][\"stop\"][0]\n", + " current_op = val[\"message\"][\"parameters\"][\"operation\"][0]\n", + " if current_op == 0:\n", + " tuning = True\n", + " else:\n", + " tuning = False\n", + "\n", + " return_dict = {\"in_progress\": in_progress, \"wavelength\": wavelength,\n", + " \"start\": start, \"stop\": stop, \"tuning\": tuning}\n", + " return return_dict\n", + "\n", + "def terascan_output(sock,\n", + " transmission_id=1,\n", + " operation=True,\n", + " delay=1,\n", + " update_step=1,\n", + " pause=False):\n", + " \"\"\"Configures Terascan automatic TCP/IP transmission during transmission\n", + "\n", + " Parameters:\n", + " sock ~ Socket object to use\n", + " transmission_id ~ (int) Arbitrary int to use for communications\n", + " operation ~ (Boolean) True turns the feature on and False disables it\n", + " delay ~ (int 1-1000) Scan delay after start transmission in 1/100s\n", + " update_step ~ (int 0-50) Causes automatic output messges to be generated\n", + " the specified number of internal tuning DAC steps\n", + " have been made. i.e. higher number = less output\n", + " Note: setting to zero will disable mid scan\n", + " segment output.\n", + " pause ~ (Boolean) True to enable the feature where the TeraScan will stop\n", + " after every message transmission of status \"start\" or\n", + " \"repeat\" and will continue upon transmission of a\n", + " terascan_continue command\n", + " Returns:\n", + " Nothing on successful call\n", + " Raises:\n", + " SolstisError if the command cannot be carried out\n", + " \"\"\"\n", + " \n", + " #Create message:\n", + " if operation == True:\n", + " operation = \"start\"\n", + " else: \n", + " operation = \"stop\"\n", + "\n", + " if pause == True:\n", + " pause = \"on\"\n", + " else:\n", + " pause = \"off\"\n", + "\n", + "\n", + " send_msg(sock,transmission_id,\"terascan_output\",{\n", + " \"operation\": operation,\n", + " \"delay\": [delay],\n", + " \"update\": [update_step],\n", + " \"pause\": pause})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op=\"terascan_output_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"Automatic Output Configuration failed\")\n", + " elif status == 2:\n", + " raise SolstisError(\"Automatic Output failed; Delay period out of range\")\n", + " elif status == 3:\n", + " raise SolstisError(\"Automatic Output failed; Update step out of range\")\n", + " else:\n", + " raise SolstisError(\"TeraScan not available.\")\n", + "\n", + "def recv_auto_output(sock):\n", + " \"\"\"Receives an automatic message from the Solstis during a TeraScan\n", + " \n", + " Parameters:\n", + " sock ~ Socket to use for communications\n", + " Returns:\n", + " A dictionary object containing the following key/value pairs:\n", + " \"wavelength\" ~ The current wavelength reading in nm (between 650-1100)\n", + " \"status\" ~ String being one of \"start\", \"repeat\", \"recover\", \"scan\", or\n", + " \"end\". See Solstis_3_TCP_JSON_protocol_V21.pdf for details\n", + " Note: If pausing is configured, then a contiue message must be\n", + " sent after reveiving any \"start\" or \"repeat\" values\n", + " Raises:\n", + " SolstisError on bad transmission\n", + " TimeoutError when the socket times out\n", + " \"\"\"\n", + "\n", + " try:\n", + " val = recv_msg(sock)\n", + " except socket.timeout:\n", + " raise TimeoutError\n", + " verify_msg(val,op=\"automatic_output\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"]\n", + " wavelength = val[\"message\"][\"parameters\"][\"wavelength\"][0]\n", + "\n", + " return {\"wavelength\": wavelength, \"status\": status}\n", + "\n", + "def terascan_continue(sock,transmission_id=1):\n", + " \"\"\"Instructs a paused terascan using automatic output to continue\n", + "\n", + " Parameters: \n", + " sock ~ Socket object to use for communications\n", + " transmision_id ~ (int) arbitrary integer used for communications\n", + " Returns:\n", + " Nothing on valid execution\n", + " Raises:\n", + " SolstisError on operation failure\n", + " \"\"\"\n", + " send_msg(sock,transmission_id,\"terascan_continue\")\n", + " val = recv_msg(sock)\n", + " verify_msg(val,op=\"terascan_continue_reply\",transmission_id=transmission_id)\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"terascan_continue failed; TeraScan was not paused.\")\n", + " else:\n", + " raise SolstisError(\"TeraScan is not available.\")\n", + "\n", + "def get_status(sock, transmission_id=1):\n", + " \"\"\"Retrieves the system status information available to the user\n", + "\n", + " Parameters:\n", + " sock ~ Socket object to use\n", + " transmission_id ~ (int) arbitrary integer to use for communications\n", + " Returns:\n", + " A dictionary containing the following key/value pairs:\n", + " \"status\" ~ 0 on a succesful call, and 1 otherwise \n", + " \"wavelength\" ~ The current wavelength in nm\n", + " \"temperature\" ~ Current temperature in degrees Celcius\n", + " \"temperature_status\" ~ \"on\" or \"off\"\n", + " \"etalon_lock\" ~ \"on\",\"off\",\"debug\",\"error\",\"search\" or \"low\". See Manual.\n", + " \"etalon_voltage\" ~ Reading in Volts\n", + " \"cavity_lock\" ~ \"on\",\"off\",\"debug\",\"error\",\"search\" or \"low\"\n", + " \"resonator_voltage\" ~ Reading in Volts\n", + " \"ecd_lock\" ~ \"not_fitted\",\"on\",\"off\",\"debug\",\"error\",\"search\" or \"low\"\n", + " \"ecd_voltage\" ~ Reading in Volts\n", + " \"output_monitor\" ~ Reading in Volts\n", + " \"etalon_pd_dc\" ~ Reading in Volts\n", + " \"dither\" ~ \"on\" or \"off\"\n", + " Raises:\n", + " SolstisError on operation failure\n", + " \"\"\"\n", + "\n", + " send_msg(sock,transmission_id,\"get_status\")\n", + " val = recv_msg(sock)\n", + " verify_msg(val,op=\"get_status_reply\",transmission_id=transmission_id)\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 1:\n", + " raise SolstisError(\"get_status failed: reason unknown\")\n", + " params = val[\"message\"][\"parameters\"]\n", + " return_val = {\"status\": 0}\n", + " return_val[\"wavelength\"] = params[\"wavelength\"][0]\n", + " return_val[\"temperature\"] = params[\"temperature\"][0]\n", + " return_val[\"temperature_status\"] = params[\"temperature_status\"]\n", + " return_val[\"etalon_lock\"] = params[\"etalon_lock\"]\n", + " return_val[\"etalon_voltage\"] = params[\"etalon_voltage\"][0]\n", + " return_val[\"cavity_lock\"] = params[\"cavity_lock\"]\n", + " return_val[\"resonator_voltage\"] = params[\"resonator_voltage\"][0]\n", + " return_val[\"ecd_lock\"] = params[\"ecd_lock\"]\n", + " if params[\"ecd_voltage\"] == \"not_fitted\":\n", + " return_val[\"ecd_voltage\"] = -float('inf')\n", + " else:\n", + " return_val[\"ecd_voltage\"] = params[\"ecd_voltage\"][0]\n", + " return_val[\"output_monitor\"] = params[\"output_monitor\"][0]\n", + " return_val[\"etalon_pd_dc\"] = params[\"etalon_pd_dc\"][0]\n", + " return_val[\"dither\"] = params[\"dither\"]\n", + "\n", + " return return_val\n", + "\n", + "def tune_etalon(sock, setting, transmission_id=1):\n", + " \"\"\"Tunes the etalon to user-defined value\n", + "\n", + " Parameters:\n", + " sock ~ Socket object to use for communications\n", + " setting ~ (float) Percentage (0-100) of etalon range to go to\n", + " transmission_id ~ (int) Arbitrary integer for communications\n", + " Returns:\n", + " Nothing on success\n", + " Raises:\n", + " SolstisError on failure to execute\n", + " \"\"\"\n", + "\n", + " send_msg(sock,transmission_id,\"tune_etalon\",{\"setting\": [setting]})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,op=\"tune_etalon_reply\",transmission_id=transmission_id)\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"Etalon Tuning value is out of range.\")\n", + " else:\n", + " raise SolstisError(\"tune_etalon Failed; Reason Unknown\")\n", + "\n", + "def tune_resonator(sock, setting, transmission_id=1):\n", + " \"\"\"Tunes the resonator to user-defined value\n", + "\n", + " Parameters:\n", + " sock ~ Socket object to use for communications\n", + " setting ~ (float) Percentage (0-100) of resonator range to go to\n", + " transmission_id ~ (int) Arbitrary integer for communications\n", + " Returns:\n", + " Nothing on success\n", + " Raises:\n", + " SolstisError on failure to execute\n", + " \"\"\"\n", + "\n", + " send_msg(sock,transmission_id,\"tune_resonator\",{\"setting\": [setting]})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,op=\"tune_resonator_reply\",transmission_id=transmission_id)\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"Resonator Tuning value is out of range.\")\n", + " else:\n", + " raise SolstisError(\"tune_resonator Failed; Reason Unknown\")\n", + "\n", + "def fine_tune_resonator(sock, setting, transmission_id=1):\n", + " \"\"\"Fine-Tunes the resonator to user-defined value\n", + "\n", + " Parameters:\n", + " sock ~ Socket object to use for communications\n", + " setting ~ (float) Percentage (0-100) of resonator fine-tuning range to go to\n", + " transmission_id ~ (int) Arbitrary integer for communications\n", + " Returns:\n", + " Nothing on success\n", + " Raises:\n", + " SolstisError on failure to execute\n", + " \"\"\"\n", + "\n", + " send_msg(sock,transmission_id,\"fine_tune_resonator\",{\"setting\": [setting]})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,op=\"fine_tune_resonator_reply\",transmission_id=transmission_id)\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"Resonator Fine-Tuning value is out of range.\")\n", + " else:\n", + " raise SolstisError(\"fine_tune_resonator Failed; Reason Unknown\")\n", + "\n", + "def etalon_lock(sock,lock,transmission_id=1):\n", + " \"\"\"Either locks or unlocks the etalon\n", + "\n", + " Parameters:\n", + " sock ~ Socket object to use for communications\n", + " lock ~ (Boolean) True to lock the etalon, False to unlock it \n", + " transmission_id ~ (int) arbitrary integer for use in communications\n", + " Returns:\n", + " Nothing on success\n", + " Raises:\n", + " SolstisError on failure\n", + " \"\"\"\n", + "\n", + " if lock == True:\n", + " lock = \"on\"\n", + " else:\n", + " lock = \"off\"\n", + "\n", + " send_msg(sock,transmission_id,\"etalon_lock\",{\"operation\": lock})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,op=\"etalon_lock_reply\",transmission_id=transmission_id)\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " else:\n", + " raise SolstisError(\"etalon_lock Failed; Reason Unknown\")\n", + "\n", + "def fast_scan_start(sock,\n", + " scan_type=\"etalon_continuous\",\n", + " width=0.01,\n", + " time=0.01,\n", + " transmission_id=1):\n", + " \"\"\"Starts a Fast scan centered at the current set wavelength\n", + "\n", + " Parameters:\n", + " sock ~ Socket object to use for communications\n", + " scan_type ~ One of: \"etalon_continuous\", \"etalon_single\",\n", + " \"cavity_continuous\", \"cavity_single\",\n", + " \"resonator_continuous\", \"resonator_single\",\n", + " \"ecd_continuous\", \"fringe_test\", \"resonator_ramp\",\n", + " \"ecd_ramp\", \"cavity_triangular\", \"resonator_triangular\"\n", + " See Manual for details\n", + " width ~ (float) Width of scan about center frequency in GHz\n", + " time ~ (float) Duration of scan in seconds. Will ramp at max speed if time\n", + " segment is too small.\n", + " transmission_id ~ (int) Arbitrary integer for use in communications\n", + " Returns:\n", + " Nothing on a succesful execution\n", + " Raises:\n", + " SolstisError on failed execution\n", + " \"\"\"\n", + "\n", + " send_msg(sock,transmission_id,\"fast_scan_start\",\n", + " {\"scan\": scan_type,\n", + " \"width\": width,\n", + " \"time\": time} )\n", + " val = recv_msg(sock)\n", + " verify_msg(val,op=\"fast_scan_start_reply\",transmission_id=transmission_id)\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"Fast Scan Failed: Scan width too large for position\")\n", + " elif status == 2:\n", + " raise SolstisError(\"Fast Scan Failed: No reference cavity fitted\")\n", + " elif status == 3:\n", + " raise SolstisError(\"Fast Scan Failed: no ERC fitted\")\n", + " elif status == 4:\n", + " raise SolstisError(\"Fast Scan Failed: Invalid Scan Type requested\")\n", + " else:\n", + " raise SolstisError(\"Fast Scan Failed: Time > 10000 seconds\")\n", + "\n", + "def fast_scan_poll(sock, scan_type=\"etalon_continuous\", transmission_id=1):\n", + " \"\"\"Polls a currently running fast scan.\n", + "\n", + " Parameters:\n", + " sock ~ Sock object to use for communications\n", + " scan_type ~ One of: \"etalon_continuous\", \"etalon_single\",\n", + " \"cavity_continuous\", \"cavity_single\",\n", + " \"resonator_continuous\", \"resonator_single\",\n", + " \"ecd_continuous\", \"fringe_test\", \"resonator_ramp\",\n", + " \"ecd_ramp\", \"cavity_triangular\", \"resonator_triangular\"\n", + " See Manual for details\n", + " transmission_id ~ (int) Arbitrary integer to use for communications\n", + " Returns:\n", + " Tuple containing (in increasing index order):\n", + " -floating point value representing the current tuner value\n", + " -Boolean stating whether tuning is done/inactive (True = Not scanning)\n", + " Raises:\n", + " SolstisError on execution failure\n", + " \"\"\"\n", + "\n", + " send_msg(sock,transmission_id,\"fast_scan_poll\",{\"scan\": scan_type})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op=\"fast_scan_poll_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " tuner_value = val[\"message\"][\"parameters\"][\"tuner_value\"][0]\n", + " if status == 1:\n", + " status = False\n", + " else:\n", + " status = True\n", + " return (tuner_value,status)\n", + "\n", + "def fast_scan_stop(sock,scan_type=\"etalon_continuous\",transmission_id=1):\n", + " \"\"\"Stops a fast-scan in progress\n", + "\n", + " Parameters:\n", + " sock ~ Sock object to use for communications\n", + " scan_type ~ One of: \"etalon_continuous\", \"etalon_single\",\n", + " \"cavity_continuous\", \"cavity_single\",\n", + " \"resonator_continuous\", \"resonator_single\",\n", + " \"ecd_continuous\", \"fringe_test\", \"resonator_ramp\",\n", + " \"ecd_ramp\", \"cavity_triangular\", \"resonator_triangular\"\n", + " See Manual for details\n", + " transmission_id ~ (int) Arbitrary integer to use for communications\n", + " Returns:\n", + " Nothing on successful execution\n", + " Raises:\n", + " SolstisError on failed execution\n", + " \"\"\"\n", + "\n", + " send_msg(sock,transmission_id,\"fast_scan_stop\",{\"scan\": scan_type})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op=\"fast_scan_stop_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"fast_scan_stop Failed; Cause unknown\")\n", + " elif status == 2:\n", + " raise SolstisError(\"fast_scan_stop Failed; Reference Cavity not fitted.\")\n", + " elif status == 3:\n", + " raise SolstisError(\"fast_scan_stop Failed; ECD not fitted.\")\n", + " else:\n", + " raise SolstisError(\"fast_scan_stop Failed; Invalid Scan Type.\")\n", + "\n", + "def fast_scan_stop_nr(sock,scan_type=\"etalon_continuous\",transmission_id=1):\n", + " \"\"\"Stops a fast-scan in progress without returning to the original position\n", + "\n", + " Parameters:\n", + " sock ~ Sock object to use for communications\n", + " scan_type ~ One of: \"etalon_continuous\", \"etalon_single\",\n", + " \"cavity_continuous\", \"cavity_single\",\n", + " \"resonator_continuous\", \"resonator_single\",\n", + " \"ecd_continuous\", \"fringe_test\", \"resonator_ramp\",\n", + " \"ecd_ramp\", \"cavity_triangular\", \"resonator_triangular\"\n", + " See Manual for details\n", + " transmission_id ~ (int) Arbitrary integer to use for communications\n", + " Returns:\n", + " Nothing on successful execution\n", + " Raises:\n", + " SolstisError on failed execution\n", + " \"\"\"\n", + "\n", + " send_msg(sock,transmission_id,\"fast_scan_stop_nr\",{\"scan\": scan_type})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,transmission_id=transmission_id,op=\"fast_scan_stop_nr_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"fast_scan_stop_nr Failed; Cause unknown\")\n", + " elif status == 2:\n", + " raise SolstisError(\"fast_scan_stop_nr Failed; Reference Cavity not fitted.\")\n", + " elif status == 3:\n", + " raise SolstisError(\"fast_scan_stop_nr Failed; ECD not fitted.\")\n", + " else:\n", + " raise SolstisError(\"fast_scan_stop_nr Failed; Invalid Scan Type.\")\n", + "\n", + "def set_wave_tolerance_m(sock,tolerance=1.0,transmission_id=1):\n", + " \"\"\"Sets the tolerance for the sending of the set_wave_m final report\n", + "\n", + " Parameters:\n", + " sock ~ Socket object to use for communications\n", + " tolerance ~ (float) New tolerance value\n", + " transmission_id ~ (int) Arbitrary integer for use in communications\n", + " Returns:\n", + " Nothing on successful execution\n", + " Raises:\n", + " SolstisError on failed execution\n", + " \"\"\"\n", + " send_msg(sock,transmission_id,\"set_wave_tolerance_m\",{\"tolerance\": tolerance})\n", + " val = recv_msg(sock)\n", + " verify_msg(val,\n", + " transmission_id=transmission_id,\n", + " op=\"set_wave_tolerance_m_reply\")\n", + " status = val[\"message\"][\"parameters\"][\"status\"][0]\n", + " if status == 0:\n", + " return\n", + " elif status == 1:\n", + " raise SolstisError(\"Could not set tolerance; No wavemeter connected\")\n", + " else:\n", + " raise SolstisError(\"Could not set tolerance; Tolerance Value Out of Range\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 148, + "id": "1fcca717", + "metadata": {}, + "outputs": [], + "source": [ + "from enum import Enum\n", + "\n", + "class TeraScanType(Enum):\n", + " SCAN_TYPE_MEDIUM = 1\n", + " SCAN_TYPE_FINE = 2\n", + " SCAN_TYPE_LINE = 3\n", + "\n", + "class TeraScanRate(Enum):\n", + " SCAN_RATE_MEDIUM_100_GHZ = 4\n", + " SCAN_RATE_MEDIUM_50_GHZ = 5\n", + " SCAN_RATE_MEDIUM_20_GHZ = 6\n", + " SCAN_RATE_MEDIUM_15_GHZ = 7\n", + " SCAN_RATE_MEDIUM_10_GHZ = 8\n", + " SCAN_RATE_MEDIUM_5_GHZ = 9\n", + " SCAN_RATE_MEDIUM_2_GHZ = 10\n", + " SCAN_RATE_MEDIUM_1_GHZ = 11\n", + " SCAN_RATE_FINE_LINE_20_GHZ = 12\n", + " SCAN_RATE_FINE_LINE_10_GHZ = 13\n", + " SCAN_RATE_FINE_LINE_5_GHZ = 14\n", + " SCAN_RATE_FINE_LINE_2_GHZ = 15\n", + " SCAN_RATE_FINE_LINE_1_GHZ = 16\n", + " SCAN_RATE_FINE_LINE_500_MHZ = 17\n", + " SCAN_RATE_FINE_LINE_200_MHZ = 18\n", + " SCAN_RATE_FINE_LINE_100_MHZ = 19\n", + " SCAN_RATE_FINE_LINE_50_MHZ = 20\n", + " SCAN_RATE_FINE_LINE_20_MHZ = 21\n", + " SCAN_RATE_FINE_LINE_10_MHZ = 22\n", + " SCAN_RATE_FINE_LINE_5_MHZ = 23\n", + " SCAN_RATE_FINE_LINE_2_MHZ = 24\n", + " SCAN_RATE_FINE_LINE_1_MHZ = 25\n", + " SCAN_RATE_LINE_500_KHZ = 26\n", + " SCAN_RATE_LINE_200_KHZ = 27\n", + " SCAN_RATE_LINE_100_KHZ = 28\n", + " SCAN_RATE_LINE_50_KHZ = 29" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "062cdbf5", + "metadata": {}, + "outputs": [], + "source": [ + " _host_ip = ConfigOption(name='host_ip_addr', default='192.168.1.225', missing='warn')\n", + " _laser_ip = ConfigOption(name='laser_ip_addr', default='192.168.1.222', missing='warn')\n", + " _laser_port = ConfigOption(name='laser_port', default=39933, missing='warn')\n", + "\n", + " _scan_rate = StatusVar(name='scan_rate', default=TeraScanRate.SCAN_RATE_FINE_LINE_10_GHZ)\n", + " _scan_type= StatusVar(name='scan_type', default=TeraScanType.SCAN_TYPE_FINE)" + ] + }, + { + "cell_type": "code", + "execution_count": 149, + "id": "bdd00ec1", + "metadata": {}, + "outputs": [], + "source": [ + "_laser_ip = '192.168.1.222'\n", + "_host_ip = '192.168.1.225'\n", + "_laser_port = 39900" + ] + }, + { + "cell_type": "code", + "execution_count": 150, + "id": "2e08a945", + "metadata": {}, + "outputs": [], + "source": [ + "socket = init_socket(_laser_ip, port=39900)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 156, + "id": "64f59ab4", + "metadata": {}, + "outputs": [], + "source": [ + "_start_wavelength = 0.780\n", + "_end_wavelength = 0.78001\n", + "_scan_rate = TeraScanRate.SCAN_RATE_FINE_LINE_10_GHZ\n", + "_scan_type = TeraScanType.SCAN_TYPE_FINE\n", + "\n", + "scan_stitch_initialize(socket, _scan_type,\n", + " _start_wavelength*1e3,\n", + " _end_wavelength*1e3,\n", + " _scan_rate)\n", + "\n", + "terascan_output(socket,\n", + " transmission_id=1,\n", + " operation=False,\n", + " delay=1,\n", + " update_step=0,\n", + " pause=True)\n", + "scan_stitch_op(socket, _scan_type, \"start\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f327ccf6", + "metadata": {}, + "outputs": [], + "source": [ + "start_link(sock=socket, ip_address='192.168.1.225')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 157, + "id": "0daa068d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'in_progress': True, 'wavelength': 779.5361, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6459, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6459, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6459, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6459, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0914, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0914, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.079, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.079, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.5114, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.5114, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.5114, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.5114, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.5114, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.5114, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.5114, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0651, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0651, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0651, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6211, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6211, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6211, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6211, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6211, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6211, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6211, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.6211, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0651, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0651, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0512, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0512, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0373, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0218, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0218, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0048, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0048, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9893, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9893, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9939, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9939, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9971, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9971, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9971, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9971, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9986, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0001, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0008, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0008, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0008, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0007, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0006, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9997, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9998, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9998, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9998, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9998, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 779.9998, 'start': 780, 'stop': 780.01001, 'tuning': False}\n", + "{'in_progress': True, 'wavelength': 779.9998, 'start': 780, 'stop': 780.01001, 'tuning': False}\n", + "{'in_progress': True, 'wavelength': 780.0006, 'start': 780, 'stop': 780.01001, 'tuning': False}\n", + "{'in_progress': True, 'wavelength': 780.0017, 'start': 780, 'stop': 780.01001, 'tuning': False}\n", + "{'in_progress': True, 'wavelength': 780.0031, 'start': 780, 'stop': 780.01001, 'tuning': False}\n", + "{'in_progress': True, 'wavelength': 780.0046, 'start': 780, 'stop': 780.01001, 'tuning': False}\n", + "{'in_progress': True, 'wavelength': 780.0061, 'start': 780, 'stop': 780.01001, 'tuning': False}\n", + "{'in_progress': True, 'wavelength': 780.0077, 'start': 780, 'stop': 780.01001, 'tuning': False}\n", + "{'in_progress': True, 'wavelength': 780.009, 'start': 780, 'stop': 780.01001, 'tuning': False}\n", + "{'in_progress': True, 'wavelength': 780.0107, 'start': 780, 'stop': 780.01001, 'tuning': False}\n", + "{'in_progress': True, 'wavelength': 780.0107, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0107, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0107, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': True, 'wavelength': 780.0107, 'start': 780, 'stop': 780.01001, 'tuning': True}\n", + "{'in_progress': False}\n" + ] + } + ], + "source": [ + "while True:\n", + " status = scan_stitch_status(socket, _scan_type)\n", + " print(status)\n", + " if status[\"in_progress\"] == False:\n", + " break" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5addb57b", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 158, + "id": "e2d986b2", + "metadata": {}, + "outputs": [], + "source": [ + "socket.close()" + ] + }, + { + "cell_type": "code", + "execution_count": 159, + "id": "c5f64823", + "metadata": {}, + "outputs": [], + "source": [ + "import nidaqmx" + ] + }, + { + "cell_type": "code", + "execution_count": 160, + "id": "16a9967d", + "metadata": {}, + "outputs": [], + "source": [ + "ICEBLOC = \"Dev2/port1/line0\"\n", + "FLIPPER = 'Dev2/port1/line2'\n", + "SHUTTER = 'Dev2/port2/line0'\n", + "GO_line = 'Dev2/port1/line1'\n", + "M2_CAVITY = \"Dev2/ai0, Dev2/ai4\" # 0 is positive, 4 is negative" + ] + }, + { + "cell_type": "code", + "execution_count": 202, + "id": "82ff3eee", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "DIChannel(name=Dev2/port1/line0)" + ] + }, + "execution_count": 202, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ttl_task = nidaqmx.Task()\n", + "ttl_task.di_channels.add_di_chan(ICEBLOC)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 228, + "id": "da8fe7fb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TTL read time: 0.000997781753540039\n" + ] + } + ], + "source": [ + "\n", + "st = time.time()\n", + "ttl_task.read()\n", + "et = time.time()\n", + "\n", + "print(\"TTL read time: \", et-st)" + ] + }, + { + "cell_type": "code", + "execution_count": 200, + "id": "141155aa", + "metadata": {}, + "outputs": [], + "source": [ + "ttl_task.close()" + ] + }, + { + "cell_type": "code", + "execution_count": 231, + "id": "25f82685", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time to read ICEBLOC: 0.0019948482513427734\n" + ] + } + ], + "source": [ + "st = time.time()\n", + "with nidaqmx.Task() as task:\n", + " task.di_channels.add_di_chan(ICEBLOC)\n", + " data = task.read()\n", + "et = time.time()\n", + "print(\"Time to read ICEBLOC: \", et-st)" + ] + }, + { + "cell_type": "code", + "execution_count": 232, + "id": "5d40540c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 232, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data" + ] + }, + { + "cell_type": "code", + "execution_count": 230, + "id": "8307ce03", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " [Emitter] Emitting: ts=1745686159.0873752, data=[532.14]\n", + "[Receiver] Got ts=1745686159.0873752, wavelength=[532.14]\n", + "✅ Signal/slot test passed!\n" + ] + } + ], + "source": [ + "# %% [markdown]\n", + "# ## Signal/Slot Test for wavemeter data\n", + "# This cell creates a dummy signal emitter with signature (float, object),\n", + "# a receiver slot that unpacks those two args, and verifies the connection.\n", + "\n", + "# %% [code]\n", + "import sys\n", + "import numpy as np\n", + "from PySide2 import QtCore, QtWidgets\n", + "\n", + "# Dummy emitter with the same signal as your wavemeter\n", + "class DummyWavemeter(QtCore.QObject):\n", + " sigWavelengthUpdated = QtCore.Signal(float, object)\n", + "\n", + " def send(self, timestamp, data):\n", + " print(f\" [Emitter] Emitting: ts={timestamp}, data={data}\")\n", + " self.sigWavelengthUpdated.emit(timestamp, data)\n", + "\n", + "# Receiver that matches the slot signature\n", + "class Receiver(QtCore.QObject):\n", + " def __init__(self):\n", + " super().__init__()\n", + " self.received = None\n", + "\n", + " @QtCore.Slot(float, object)\n", + " def on_new_data(self, timestamp, data):\n", + " print(f\"[Receiver] Got ts={timestamp}, wavelength={data}\")\n", + " self.received = (timestamp, data)\n", + "\n", + "# Set up Qt application (needed for the event loop)\n", + "app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)\n", + "\n", + "# Instantiate\n", + "wavemeter = DummyWavemeter()\n", + "receiver = Receiver()\n", + "\n", + "# Connect signal to slot\n", + "wavemeter.sigWavelengthUpdated.connect(receiver.on_new_data)\n", + "\n", + "# Fire a test emission\n", + "ts = time.time()\n", + "wave = np.array([532.14]) # example wavelength in nm\n", + "wavemeter.send(ts, wave)\n", + "\n", + "# Optionally process pending events to ensure the slot runs\n", + "app.processEvents()\n", + "\n", + "# Check the result\n", + "assert receiver.received == (ts, wave), \"Slot did not receive the correct data!\"\n", + "print(\"✅ Signal/slot test passed!\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c59700a", + "metadata": {}, + "outputs": [], + "source": [ + "from TimeTagger import createTimeTagger, Counter\n", + "import time\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# 1. Connect\n", + "tagger = createTimeTagger()\n", + "\n", + "\n", + "total_number_of_events = 0\n", + "# 2. Make a Counter: binwidth = 100 ms → 1e8 ps; n_values = 1000 bins\n", + "counter = Counter(tagger, channels=[1], binwidth=1_000_000_000, n_values=10_000)\n", + "counter.clear()\n", + "counter.start()\n", + "do_list = []\n", + "do = counter.getDataObject(remove=True)\n", + "st = time.perf_counter()\n", + "time.sleep(3)\n", + "et = time.perf_counter()\n", + "do = counter.getDataObject(remove=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 343, + "id": "bc358b69", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3.006327700044494" + ] + }, + "execution_count": 343, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "et - st" + ] + }, + { + "cell_type": "code", + "execution_count": 342, + "id": "1e0fbecf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2972" + ] + }, + "execution_count": 342, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(do.getData()[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 335, + "id": "82403c58", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time to get data object: 0.00017190002836287022\n" + ] + } + ], + "source": [ + "st = time.perf_counter()\n", + "x = counter.getDataObject(remove=True)\n", + "et = time.perf_counter()\n", + "print(\"Time to get data object: \", et-st)" + ] + }, + { + "cell_type": "code", + "execution_count": 338, + "id": "d4ad5a39", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3.0038418999756686" + ] + }, + "execution_count": 338, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "et-st" + ] + }, + { + "cell_type": "code", + "execution_count": 339, + "id": "b4f7938b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2973" + ] + }, + "execution_count": 339, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "total_number_of_events" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ba0a2e5", + "metadata": {}, + "outputs": [], + "source": [ + "TimeTagger.freeTimeTagger(tagger)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qudi-env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/qudi/hardware/timetagger/swabian_tagger.py b/src/qudi/hardware/timetagger/swabian_tagger.py index 7d842db..792e60f 100644 --- a/src/qudi/hardware/timetagger/swabian_tagger.py +++ b/src/qudi/hardware/timetagger/swabian_tagger.py @@ -1,29 +1,9 @@ # -*- coding: utf-8 -*- -""" -A hardware module for communicating with the fast counter FPGA. - -Copyright (c) 2021, 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 . -""" - import numpy as np import time -import TimeTagger as tt -from typing import Dict +import TimeTagger +from typing import Dict, List from PySide2 import QtCore @@ -32,6 +12,11 @@ from qudi.core.configoption import ConfigOption +# TODO: Have a way to collect and save the entire buffer. This will return in ps so you will have to convert it to ms. +# You could also start an acquisition while sending the data to the GUI. +# TODO: Things should be set by config +# TODO: Add things to the interface +# TODO: Change start_measure to start_reading or something class SwabianTimeTagger(FastCounterInterface): """ Hardware class to controls a Time Tagger from Swabian Instruments. @@ -42,204 +27,53 @@ class SwabianTimeTagger(FastCounterInterface): options: channels: photon_counts: 0 - dark_counts: 1 - """ - _channel_config: Dict[str, int] = ConfigOption( - name='channels', - default={'counts': 0}, - missing='warn' - ) - - # set to threaded: + _channel_apd_0 = 1 + _bin_width_ps = 1_000_000_000 + _record_length_ms = 100 _threaded = True + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.__timer = None + self.most_recent_data: List[float] = [] self._thread_lock = RecursiveMutex() - self._last_data = np.zeros(1) + def on_activate(self): """ Connect and configure the access to the FPGA. """ - - 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._tagger = TimeTagger.createTimeTagger() + self._counter = TimeTagger.Counter( + tagger=self._tagger, + channels=[1], + binwidth=1_000_000_000, + n_values=1_000 + ) - self.__timer = QtCore.QTimer() - self.__timer.setSingleShot(False) - self.__timer.timeout.connect(self.__update_data) - self.__timer.start(0) # call as often as possible - - def get_constraints(self): - """ Retrieve the hardware constrains from the Fast counting device. - - @return dict: dict with keys being the constraint names as string and - items are the definition for the constaints. - - The keys of the returned dictionary are the str name for the constraints - (which are set in this method). - - NO OTHER KEYS SHOULD BE INVENTED! - - If you are not sure about the meaning, look in other hardware files to - get an impression. If still additional constraints are needed, then they - have to be added to all files containing this interface. - The items of the keys are again dictionaries which have the generic - dictionary form: - {'min': , - 'max': , - 'step': , - 'unit': ''} - - Only the key 'hardware_binwidth_list' differs, since they - contain the list of possible binwidths. - - If the constraints cannot be set in the fast counting hardware then - write just zero to each key of the generic dicts. - Note that there is a difference between float input (0.0) and - integer input (0), because some logic modules might rely on that - distinction. - - ALL THE PRESENT KEYS OF THE CONSTRAINTS DICT MUST BE ASSIGNED! - """ - - constraints = dict() - - # the unit of those entries are seconds per bin. In order to get the - # current binwidth in seonds use the get_binwidth method. - constraints['hardware_binwidth_list'] = [1 / 1000e6] - - # TODO: think maybe about a software_binwidth_list, which will - # postprocess the obtained counts. These bins must be integer - # multiples of the current hardware_binwidth - - return constraints - - def on_deactivate(self): - """ Deactivate the FPGA. - """ - if (self.counter is not None): - if self.module_state() == 'locked': - self.counter.stop() - self.counter.clear() - self.counter = None - tt.freeTimeTagger(self._tagger) + self.statusvar = 0 # 0 = unconfigured, 1 = idle, 2 = running, 3 = paused, -1 = error state - 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. - @param float record_length_s: Total length of the timetrace/each single - 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 - """ - - 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.__timer = QtCore.QTimer() + self.__timer.setSingleShot(False) # Make this a repeating timer + self.__timer.timeout.connect(self.__update_data) # connect to the update function + self.__timer.start(10) # call in 10 ms intervals - @QtCore.Slot() - def start_measure(self): - """ Start the fast counter. """ + + # TODO: Should be a slot i think + def start_reading(self): self.module_state.lock() - self.counter.clear() - self.counter.startFor(self._bin_width * 1000) # in ps, should be stored as such #TODO - - self.sigScanStarted.emit() - + self._counter.start() self.statusvar = 2 - return 0 - - @QtCore.Slot() - def stop_measure(self): - """ Stop the fast counter. """ + + + # TODO: Should be a slot + def stop_reading(self): if self.module_state() == 'locked': - self.counter.stop() self.module_state.unlock() - self.statusvar = 1 - return 0 - - def pause_measure(self): - """ Pauses the current measurement. - - Fast counter must be initially in the run state to make it pause. - """ - if self.module_state() == 'locked': - self.counter.stop() - self.statusvar = 3 - return 0 - - def continue_measure(self): - """ Continues the current measurement. - - If fast counter is in pause state, then fast counter will be continued. - """ - if self.module_state() == 'locked': - self.counter.startFor(self._bin_width * 1000) # in ps, should be stored as such #TODO - self.statusvar = 2 - return 0 - - def is_gated(self): - """ Check the gated counting possibility. - - Boolean return value indicates if the fast counter is a gated counter - (TRUE) or not (FALSE). - """ - return True - - def get_data_trace(self, rolling: bool = False) -> np.ndarray: - """ Polls the current timetrace data from the fast counter. - - @return numpy.array: 2 dimensional array of dtype = int64. This counter - is gated the the return array has the following - shape: - returnarray[gate_index, timebin_index] - - The binning, specified by calling configure() in forehand, must be taken - care of in this hardware class. A possible overflow of the histogram - bins must be caught here and taken care of. - """ - - return np.array(self.counter.getData(rolling=rolling), dtype='int64') + def get_status(self): """ Receives the current status of the Fast Counter and outputs it as @@ -253,18 +87,30 @@ def get_status(self): """ return self.statusvar - 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 + + + # TODO: Should I really clear the counter here? Or just stop it? + def on_deactivate(self): + """ Deactivate the FPGA. + """ + if (self._counter is not None): + if self.module_state() == 'locked': + self._counter.stop() + self._counter.clear() + self._counter = None + TimeTagger.freeTimeTagger(self._tagger) + + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + self.statusvar = 0 def __update_data(self): with self._thread_lock: if self.module_state() == 'locked': - self.counter.waitUntilFinished(self._bin_width * 1e-6 * 100) # in ms, should never reach this limit, but just in case - self.sigScanFinished.emit( - np.array(np.squeeze(self.get_data_trace())) - ) - - self.counter.startFor(self._bin_width * 1000) # in ps, should be stored as such #TODO + data_obj = self._counter.getDataObject(remove=True) + timestamp = time.perf_counter() + self.most_recent_data = np.squeeze(data_obj.getData()) + self.sigScanFinished.emit(timestamp, self.most_recent_data) diff --git a/src/qudi/hardware/timetagger/swabian_tagger_ben.py b/src/qudi/hardware/timetagger/swabian_tagger_ben.py new file mode 100644 index 0000000..7d842db --- /dev/null +++ b/src/qudi/hardware/timetagger/swabian_tagger_ben.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- + +""" +A hardware module for communicating with the fast counter FPGA. + +Copyright (c) 2021, 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 . +""" + +import numpy as np +import time +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 + + +class SwabianTimeTagger(FastCounterInterface): + """ Hardware class to controls a Time Tagger from Swabian Instruments. + + Example config for copy-paste: + + swabian_timetagger: + module.Class: 'timetagger.swabian_tagger.SwabianTimeTagger' + options: + channels: + photon_counts: 0 + dark_counts: 1 + + """ + + _channel_config: Dict[str, int] = ConfigOption( + name='channels', + default={'counts': 0}, + 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. + """ + + 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(False) + self.__timer.timeout.connect(self.__update_data) + self.__timer.start(0) # call as often as possible + + def get_constraints(self): + """ Retrieve the hardware constrains from the Fast counting device. + + @return dict: dict with keys being the constraint names as string and + items are the definition for the constaints. + + The keys of the returned dictionary are the str name for the constraints + (which are set in this method). + + NO OTHER KEYS SHOULD BE INVENTED! + + If you are not sure about the meaning, look in other hardware files to + get an impression. If still additional constraints are needed, then they + have to be added to all files containing this interface. + + The items of the keys are again dictionaries which have the generic + dictionary form: + {'min': , + 'max': , + 'step': , + 'unit': ''} + + Only the key 'hardware_binwidth_list' differs, since they + contain the list of possible binwidths. + + If the constraints cannot be set in the fast counting hardware then + write just zero to each key of the generic dicts. + Note that there is a difference between float input (0.0) and + integer input (0), because some logic modules might rely on that + distinction. + + ALL THE PRESENT KEYS OF THE CONSTRAINTS DICT MUST BE ASSIGNED! + """ + + constraints = dict() + + # the unit of those entries are seconds per bin. In order to get the + # current binwidth in seonds use the get_binwidth method. + constraints['hardware_binwidth_list'] = [1 / 1000e6] + + # TODO: think maybe about a software_binwidth_list, which will + # postprocess the obtained counts. These bins must be integer + # multiples of the current hardware_binwidth + + return constraints + + def on_deactivate(self): + """ Deactivate the FPGA. + """ + if (self.counter is not None): + if self.module_state() == 'locked': + self.counter.stop() + 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. + @param float record_length_s: Total length of the timetrace/each single + 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 + """ + + 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 + + + @QtCore.Slot() + def start_measure(self): + """ Start the fast counter. """ + self.module_state.lock() + self.counter.clear() + self.counter.startFor(self._bin_width * 1000) # in ps, should be stored as such #TODO + + self.sigScanStarted.emit() + + self.statusvar = 2 + return 0 + + @QtCore.Slot() + def stop_measure(self): + """ Stop the fast counter. """ + if self.module_state() == 'locked': + self.counter.stop() + self.module_state.unlock() + self.statusvar = 1 + return 0 + + def pause_measure(self): + """ Pauses the current measurement. + + Fast counter must be initially in the run state to make it pause. + """ + if self.module_state() == 'locked': + self.counter.stop() + self.statusvar = 3 + return 0 + + def continue_measure(self): + """ Continues the current measurement. + + If fast counter is in pause state, then fast counter will be continued. + """ + if self.module_state() == 'locked': + self.counter.startFor(self._bin_width * 1000) # in ps, should be stored as such #TODO + self.statusvar = 2 + return 0 + + def is_gated(self): + """ Check the gated counting possibility. + + Boolean return value indicates if the fast counter is a gated counter + (TRUE) or not (FALSE). + """ + return True + + def get_data_trace(self, rolling: bool = False) -> np.ndarray: + """ Polls the current timetrace data from the fast counter. + + @return numpy.array: 2 dimensional array of dtype = int64. This counter + is gated the the return array has the following + shape: + returnarray[gate_index, timebin_index] + + The binning, specified by calling configure() in forehand, must be taken + care of in this hardware class. A possible overflow of the histogram + bins must be caught here and taken care of. + """ + + return np.array(self.counter.getData(rolling=rolling), dtype='int64') + + def get_status(self): + """ Receives the current status of the Fast Counter and outputs it as + return value. + + 0 = unconfigured + 1 = idle + 2 = running + 3 = paused + -1 = error state + """ + return self.statusvar + + 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: + if self.module_state() == 'locked': + self.counter.waitUntilFinished(self._bin_width * 1e-6 * 100) # in ms, should never reach this limit, but just in case + self.sigScanFinished.emit( + np.array(np.squeeze(self.get_data_trace())) + ) + + self.counter.startFor(self._bin_width * 1000) # in ps, should be stored as such #TODO diff --git a/src/qudi/hardware/wavemeter/wavemeter.py b/src/qudi/hardware/wavemeter/wavemeter.py new file mode 100644 index 0000000..ea2b505 --- /dev/null +++ b/src/qudi/hardware/wavemeter/wavemeter.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +import os, time +from pylablib.devices import HighFinesse +import numpy as np +from PySide2 import QtCore + +from qudi.util.mutex import RecursiveMutex +from qudi.interface.simple_wavemeter_interface import SimpleWavemeterInterface + + +# TODO: add docs +# TODO: Base this off of the iqo instantiation of the wavemeter. The way they do it with the dll is much better than the way we do it here. +"""https://pylablib.readthedocs.io/en/latest/devices/HighFinesse.html""" +class Wavemeter(SimpleWavemeterInterface): + """_summary_ + + Args: + SimpleWavemeterInterface (_type_): _description_ + + Returns: + _type_: _description_ + """ + + _threaded = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._data = 0 + self._thread_lock = RecursiveMutex() + + def on_activate(self): + """ Activate module. + """ + try: + self.serial_number = 3167 + self.channel = 1 + #app_folder = r"C:\Program Files (x86)\HighFinesse\Wavelength Meter WS6 822" + app_folder = r'C:\Program Files (x86)\HighFinesse\Wavelength Meter WS6 3167' + # app_folder = r'C:\Program Files (x86)\HighFinesse\Wavelength Meter WS7 1856' + dll_path = os.path.join(app_folder,"Projects","64") + app_path = os.path.join(app_folder, "wlm_ws6.exe") + # app_path = os.path.join(app_folder, "wlm_ws7.exe") + self.wavemeter = HighFinesse.WLM(self.serial_number,dll_path=dll_path,app_path=app_path) + except OSError as err: + self.log.exception(f'{err}\nPlease check if the wlmData DLL is installed correctly!') + + + self._data = 0 + + self.__timer = QtCore.QTimer() + self.__timer.setSingleShot(False) + self.__timer.timeout.connect(self.__update_data) + self.__timer.start(10) # 10 ms timer + + + def on_deactivate(self): + """ Deactivate module. + """ + self.wavemeter.close() + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + + @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 get_wavelength(self) -> np.float64: + """Returns the wavelength in um""" + wavelength = self.wavemeter.get_wavelength(channel=self.channel, error_on_invalid=False, wait=True, timeout=1) + return np.float64(wavelength * 1e6) # convert to um + + + def __update_data(self): + with self._thread_lock: + if self.module_state() == 'locked': + self._data = self.get_wavelength() + timestamp = time.perf_counter() + self.sigWavelengthUpdated.emit(timestamp, self._data) \ No newline at end of file diff --git a/src/qudi/hardware/wavemeter/high_finesse_wavemeter.py b/src/qudi/hardware/wavemeter_ben/high_finesse_wavemeter.py similarity index 100% rename from src/qudi/hardware/wavemeter/high_finesse_wavemeter.py rename to src/qudi/hardware/wavemeter_ben/high_finesse_wavemeter.py diff --git a/src/qudi/hardware/wavemeter/wlmConst.py b/src/qudi/hardware/wavemeter_ben/wlmConst.py similarity index 100% rename from src/qudi/hardware/wavemeter/wlmConst.py rename to src/qudi/hardware/wavemeter_ben/wlmConst.py diff --git a/src/qudi/hardware/wavemeter/wlmData.py b/src/qudi/hardware/wavemeter_ben/wlmData.py similarity index 100% rename from src/qudi/hardware/wavemeter/wlmData.py rename to src/qudi/hardware/wavemeter_ben/wlmData.py diff --git a/src/qudi/interface/fast_counter_interface.py b/src/qudi/interface/fast_counter_interface.py index 38cb0bc..4a6526f 100644 --- a/src/qudi/interface/fast_counter_interface.py +++ b/src/qudi/interface/fast_counter_interface.py @@ -44,65 +44,65 @@ class FastCounterInterface(Base): # Signals: sigScanStarted = QtCore.Signal() - sigScanFinished = QtCore.Signal(np.ndarray) + sigScanFinished = QtCore.Signal(float, object) # (elapsed_time, data) - @abstractmethod - def get_constraints(self): - """ Retrieve the hardware constrains from the Fast counting device. + # @abstractmethod + # def get_constraints(self): + # """ Retrieve the hardware constrains from the Fast counting device. - @return dict: dict with keys being the constraint names as string and - items are the definition for the constaints. + # @return dict: dict with keys being the constraint names as string and + # items are the definition for the constaints. - The keys of the returned dictionary are the str name for the constraints - (which are set in this method). + # The keys of the returned dictionary are the str name for the constraints + # (which are set in this method). - NO OTHER KEYS SHOULD BE INVENTED! + # NO OTHER KEYS SHOULD BE INVENTED! - If you are not sure about the meaning, look in other hardware files to - get an impression. If still additional constraints are needed, then they - have to be added to all files containing this interface. + # If you are not sure about the meaning, look in other hardware files to + # get an impression. If still additional constraints are needed, then they + # have to be added to all files containing this interface. - The items of the keys are again dictionaries which have the generic - dictionary form: - {'min': , - 'max': , - 'step': , - 'unit': ''} + # The items of the keys are again dictionaries which have the generic + # dictionary form: + # {'min': , + # 'max': , + # 'step': , + # 'unit': ''} - Only the key 'hardware_binwidth_list' differs, since they - contain the list of possible binwidths. + # Only the key 'hardware_binwidth_list' differs, since they + # contain the list of possible binwidths. - If the constraints cannot be set in the fast counting hardware then - write just zero to each key of the generic dicts. - Note that there is a difference between float input (0.0) and - integer input (0), because some logic modules might rely on that - distinction. + # If the constraints cannot be set in the fast counting hardware then + # write just zero to each key of the generic dicts. + # Note that there is a difference between float input (0.0) and + # integer input (0), because some logic modules might rely on that + # distinction. - ALL THE PRESENT KEYS OF THE CONSTRAINTS DICT MUST BE ASSIGNED! + # ALL THE PRESENT KEYS OF THE CONSTRAINTS DICT MUST BE ASSIGNED! - # Example for configuration with default values: + # # Example for configuration with default values: - constraints = dict() + # constraints = dict() - # the unit of those entries are seconds per bin. In order to get the - # current binwidth in seonds use the get_binwidth method. - constraints['hardware_binwidth_list'] = [] + # # the unit of those entries are seconds per bin. In order to get the + # # current binwidth in seonds use the get_binwidth method. + # constraints['hardware_binwidth_list'] = [] - """ - pass + # """ + # pass - @abstractmethod - def configure(self, bin_width_s, record_length_s): - """ Configuration of the fast counter. + # @abstractmethod + # 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 race histogram in seconds. - @param float record_length_s: Total length of the timetrace/each single gate in seconds. + # @param float bin_width_s: Length of a single time bin in the time race histogram in seconds. + # @param float record_length_s: Total length of the timetrace/each single gate in seconds. - @return tuple(binwidth_s, record_length_s, number_of_gates): - binwidth_s: float the actual set binwidth in seconds - gate_length_s: the actual record length in seconds - """ - pass + # @return tuple(binwidth_s, record_length_s, number_of_gates): + # binwidth_s: float the actual set binwidth in seconds + # gate_length_s: the actual record length in seconds + # """ + # pass @abstractmethod def get_status(self): @@ -116,66 +116,66 @@ def get_status(self): """ pass - @abstractmethod - def start_measure(self): - """ Start the fast counter. """ - pass - - @abstractmethod - def stop_measure(self): - """ Stop the fast counter. """ - pass - - @abstractmethod - def pause_measure(self): - """ Pauses the current measurement. - - Fast counter must be initially in the run state to make it pause. - """ - pass - - @abstractmethod - def continue_measure(self): - """ Continues the current measurement. - - If fast counter is in pause state, then fast counter will be continued. - """ - pass - - @abstractmethod - def is_gated(self): - """ Check the gated counting possibility. - - @return bool: Boolean value indicates if the fast counter is a gated - counter (TRUE) or not (FALSE). - """ - pass - - @abstractmethod - def get_binwidth(self): - """ Returns the width of a single timebin in the timetrace in seconds. - - @return float: current length of a single bin in seconds (seconds/bin) - """ - pass - - @abstractmethod - def get_data_trace(self, rolling: bool = False): - """ Polls the current timetrace data from the fast counter. - - Return value is a numpy array (dtype = int64). - The binning, specified by calling configure() in forehand, must be - taken care of in this hardware class. A possible overflow of the - histogram bins must be caught here and taken care of. - If the counter is NOT GATED it will return a tuple (1D-numpy-array, info_dict) with - returnarray[timebin_index] - If the counter is GATED it will return a tuple (2D-numpy-array, info_dict) with - returnarray[gate_index, timebin_index] - - info_dict is a dictionary with keys : - - 'elapsed_sweeps' : the elapsed number of sweeps - - 'elapsed_time' : the elapsed time in seconds - - If the hardware does not support these features, the values should be None - """ - pass + # @abstractmethod + # def start_measure(self): + # """ Start the fast counter. """ + # pass + + # @abstractmethod + # def stop_measure(self): + # """ Stop the fast counter. """ + # pass + + # @abstractmethod + # def pause_measure(self): + # """ Pauses the current measurement. + + # Fast counter must be initially in the run state to make it pause. + # """ + # pass + + # @abstractmethod + # def continue_measure(self): + # """ Continues the current measurement. + + # If fast counter is in pause state, then fast counter will be continued. + # """ + # pass + + # @abstractmethod + # def is_gated(self): + # """ Check the gated counting possibility. + + # @return bool: Boolean value indicates if the fast counter is a gated + # counter (TRUE) or not (FALSE). + # """ + # pass + + # @abstractmethod + # def get_binwidth(self): + # """ Returns the width of a single timebin in the timetrace in seconds. + + # @return float: current length of a single bin in seconds (seconds/bin) + # """ + # pass + + # @abstractmethod + # def get_data_trace(self, rolling: bool = False): + # """ Polls the current timetrace data from the fast counter. + + # Return value is a numpy array (dtype = int64). + # The binning, specified by calling configure() in forehand, must be + # taken care of in this hardware class. A possible overflow of the + # histogram bins must be caught here and taken care of. + # If the counter is NOT GATED it will return a tuple (1D-numpy-array, info_dict) with + # returnarray[timebin_index] + # If the counter is GATED it will return a tuple (2D-numpy-array, info_dict) with + # returnarray[gate_index, timebin_index] + + # info_dict is a dictionary with keys : + # - 'elapsed_sweeps' : the elapsed number of sweeps + # - 'elapsed_time' : the elapsed time in seconds + + # If the hardware does not support these features, the values should be None + # """ + # pass diff --git a/src/qudi/interface/simple_wavemeter_interface.py b/src/qudi/interface/simple_wavemeter_interface.py index f42d2ee..13d2272 100644 --- a/src/qudi/interface/simple_wavemeter_interface.py +++ b/src/qudi/interface/simple_wavemeter_interface.py @@ -8,13 +8,22 @@ class SimpleWavemeterInterface(Base): """ This interface is for simple use of a wavemeter. It just gets wavelength for any available channels """ - - sigWavelengthUpdated = QtCore.Signal(np.ndarray) # 1-d array of wavelengths in nm + + sigWavelengthUpdated = QtCore.Signal(float, object) # time and wavelength in meters @abstractmethod - def get_wavelengths(self) -> np.ndarray: + def get_wavelength(self) -> np.float64: """ Retrieve the current wavelength(s) from the wavemeter @return np.ndarray: wavelength in nm per-channel """ - pass \ No newline at end of file + pass + # 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 + + # @return np.ndarray: wavelength in nm per-channel + # """ + # pass \ No newline at end of file diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index f496f0e..12c6dcd 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -17,6 +17,8 @@ from qudi.interface.daq_reader_interface import InputType, ReaderVal +from rpyc.utils.classic import obtain + class TerascanData(): wavelength: float counts: int @@ -45,13 +47,13 @@ class TerascanLogic(LogicBase): mode_hop_overlap_fine: 0.00025 # "" """ - # declare connectors + #################### CONNECTORS #################### _laser = Connector(name='laser', interface='ScanningLaserInterface') _wavemeter = Connector(name='wavemeter', interface='SimpleWavemeterInterface') _counter = Connector(name='counter', interface='FastCounterInterface') - _daq = Connector(name='daq', interface='DAQReaderInterface') + _daq = Connector(name='daq', interface='Base') - # declare config options + #################### CONFIGURATION OPTIONS #################### _record_length_ms = ConfigOption(name='record_length_ms', default=1, missing='info') @@ -61,7 +63,7 @@ class TerascanLogic(LogicBase): _mode_hop_overlap_med = ConfigOption(name='mode_hop_overlap_med', default=0.001) _mode_hop_overlap_fine = ConfigOption(name='mode_hop_overlap_fine', default=0.00025) - # status variables: + #################### STATUS VARIABLES #################### _start_wavelength = StatusVar('start_wavelength', default=0.785) _end_wavelength = StatusVar('end_wavelength', default=0.7851) _current_wavelength = StatusVar('current_wavelength', default=0.785) @@ -74,12 +76,12 @@ class TerascanLogic(LogicBase): _last_locked: float = 0 - # Update signals, e.g. for GUI module - sigWavelengthUpdated = QtCore.Signal(float) + #################### SIGNALS FOR GUI MODULE #################### + sigWavelengthUpdated = QtCore.Signal(object) sigCountsUpdated = QtCore.Signal(object) # is a List[TerascanData] sigLaserLocked = QtCore.Signal(bool) - # Update signals for other logics + #################### SIGNALS FOR OTHER LOGIC MODULES #################### sigConfigureCounter = QtCore.Signal(float, float) sigSetLaserWavelengths = QtCore.Signal(float, float) sigSetLaserScanRate = QtCore.Signal(int) @@ -98,48 +100,53 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__timer = None self._thread_lock = RecursiveMutex() + + + self.wavelength_data = [] + self.ttl_data = [] + self.counts_data = [] def on_activate(self): laser = self._laser() counter = self._counter() wavemeter = self._wavemeter() daq = self._daq() + + ##################### INITIALIZE DATA INPUT #################### + counter.start_reading() + wavemeter.start_reading() + daq.start_reading() - # Outputs: - self.sigConfigureCounter.connect(counter.configure, QtCore.Qt.QueuedConnection) + #################### OUTPUTS #################### + # self.sigConfigureCounter.connect(counter.configure, QtCore.Qt.QueuedConnection) self.sigSetLaserWavelengths.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) self.sigSetLaserScanRate.connect(laser.set_scan_rate, QtCore.Qt.QueuedConnection) self.sigSetLaserScanType.connect(laser.set_scan_type, QtCore.Qt.DirectConnection) # Is direct on purpose self.sigRestartLaser.connect(laser.restart_scan, QtCore.Qt.QueuedConnection) - self.sigStartScan.connect(counter.start_measure, 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.sigStartScan.connect(wavemeter.start_reading, QtCore.Qt.QueuedConnection) - self.sigStopScan.connect(counter.stop_measure, 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.sigStopScan.connect(wavemeter.stop_reading, QtCore.Qt.QueuedConnection) - self.sigStartCounting.connect(counter.start_measure, QtCore.Qt.QueuedConnection) - self.sigStopCounting.connect(counter.stop_measure, QtCore.Qt.QueuedConnection) + # self.sigStartCounting.connect(counter.start_measure, QtCore.Qt.QueuedConnection) + # self.sigStopCounting.connect(counter.stop_measure, QtCore.Qt.QueuedConnection) - # Inputs: + #################### INPUTS #################### laser.sigScanStarted.connect(self._laser_scan_started) laser.sigScanFinished.connect(self._laser_scan_finished) wavemeter.sigWavelengthUpdated.connect(self._new_wavemeter_data) counter.sigScanFinished.connect(self._process_counter_data) daq.sigNewData.connect(self._new_daq_data) - - # Configure Counter: - self._record_length_s = self._record_length_ms * 1e-3 - self._bin_width_s = self._record_length_s - self.sigConfigureCounter.emit(self._bin_width_s, self._record_length_s) - # Add watchdog timer + #################### WATCHDOG TIMER #################### self.__timer = QtCore.QTimer() self.__timer.setSingleShot(False) self.__timer.timeout.connect(self.__watchdog) - self.__timer.start(500) + self.__timer.start(100) # 100 ms timer def on_deactivate(self): self.__timer.stop() @@ -162,6 +169,7 @@ def scan_rates(self) -> dict: def start_scan(self): with self._thread_lock: if self.module_state() == 'idle': + self.experiment_start_time = time.perf_counter() self.module_state.lock() self._current_data = [] self._last_locked = time.time() @@ -227,76 +235,38 @@ def _laser_scan_finished(self): self.sigScanFinished.emit() self.sigStopCounting.emit() self.module_state.unlock() - - @QtCore.Slot(np.ndarray) - def _new_wavemeter_data(self, data: np.ndarray): - with self._thread_lock: - if self.module_state() == 'locked' and self._laser_locked: - wave = data[0][0] - self._current_wavelength = wave # ? - self.sigWavelengthUpdated.emit(wave) - - @QtCore.Slot(np.ndarray) - def _process_counter_data(self, data: np.ndarray): - with self._thread_lock: - if self.module_state() == 'locked' and self._laser_locked: - self._current_data.append( - TerascanData( - wavelength=self._current_wavelength, - counts=data, - ) - ) - self.sigCountsUpdated.emit(self._current_data) - - @QtCore.Slot(object) - def _new_daq_data(self, data: List[ReaderVal]): + + + @QtCore.Slot(float, np.ndarray) + def _process_counter_data(self, timestamp, data: np.ndarray): with self._thread_lock: if self.module_state() == 'locked': - for i in data: - if i.type is InputType.DIGITAL: - # We assume there is only one digital input for this measurement - if i.val and not self._laser_locked: - self._laser_locked = True - # self.sigStartCounting.emit() - - if not i.val and self._laser_locked: - # We just mode hopped and are now unlocked - self._laser_locked = False - self._remove_mode_hop() - - - self.sigLaserLocked.emit(self._laser_locked) - + self.counts_data += data.tolist() + - ### Internal Functions ### - def _remove_mode_hop(self): - """ Function to remove previous data when a mode hop occurs. - Assumes we are locked externally. - """ - target = -1 - scan_up = self._start_wavelength < self._end_wavelength - - if self._scan_type == 1: # MEDIUM Scan - target = self._current_wavelength - self._mode_hop_overlap_med \ - if scan_up else self._current_wavelength + self._mode_hop_overlap_med - elif self._scan_type == 2: # FINE Scan - target = self._current_wavelength - self._mode_hop_overlap_fine \ - if scan_up else self._current_wavelength + self._mode_hop_overlap_fine - else: - self.log.warning('Unknown scan type. Not removing data.') - return - - while len(self._current_data) > 0: - if (scan_up and \ - self._current_data[-1].wavelength > target) or \ - (not scan_up and \ - self._current_data[-1].wavelength < target): - # We need to check if we are scanning up or down - self._current_data.pop() - else: - break + @QtCore.Slot(float, object) + def _new_wavemeter_data(self, timestamp, data: np.float64): + """Called on every new wavemeter reading when locked.""" + with self._thread_lock: + # only run when we’re in the ‘locked’ state + if self.module_state() != 'locked': + return + + most_recent_wavelength = self.wavelength_data[-1] if len(self.wavelength_data) > 0 else data + n_new_points = len(self.counts_data) - len(self.wavelength_data) + self.wavelength_data += np.linspace(most_recent_wavelength, data, n_new_points).tolist() + + @QtCore.Slot(float, object) + def _new_daq_data(self, timestamp, data: bool): + with self._thread_lock: + if self.module_state() != 'locked': + return + most_recent_ttl = self.ttl_data[-1] if len(self.ttl_data) > 0 else data + n_new_points = len(self.counts_data) - len(self.ttl_data) + self.ttl_data += [data] * n_new_points + #### Watchdog Timer #### def __watchdog(self): diff --git a/src/qudi/logic/terascan_logic_ben.py b/src/qudi/logic/terascan_logic_ben.py new file mode 100644 index 0000000..f496f0e --- /dev/null +++ b/src/qudi/logic/terascan_logic_ben.py @@ -0,0 +1,312 @@ + +import numpy as np +import time +import datetime +import matplotlib.pyplot as plt +from PySide2 import QtCore + +from typing import List + +from qudi.core.module import LogicBase +from qudi.util.mutex import RecursiveMutex +from qudi.util.units import ScaledFloat +from qudi.core.connector import Connector +from qudi.core.configoption import ConfigOption +from qudi.core.statusvariable import StatusVar +from qudi.util.datastorage import TextDataStorage + +from qudi.interface.daq_reader_interface import InputType, ReaderVal + +class TerascanData(): + wavelength: float + counts: int + + def __init__(self, wavelength: float, counts: int): + self.wavelength = wavelength + self.counts = counts + +class TerascanLogic(LogicBase): + """ + This is the Logic class for Terascan measurements + + example config for copy-paste: + + terascan_logic: + module.Class: 'terascan_logic.TerascanLogic' + connect: + 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. + laser_timeout_s: 10 # Time in seconds to wait for the laser to lock before automatically restarting the scan + mode_hop_overlap_med: 0.001 # in nm, from the SolsTiS control panel. This is how far back we go to discard data every time a mode hop occurs + mode_hop_overlap_fine: 0.00025 # "" + """ + + # declare connectors + _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', + default=1, + missing='info') + + _laser_timeout_s = ConfigOption(name='laser_timeout_s', default=10) + + _mode_hop_overlap_med = ConfigOption(name='mode_hop_overlap_med', default=0.001) + _mode_hop_overlap_fine = ConfigOption(name='mode_hop_overlap_fine', default=0.00025) + + # status variables: + _start_wavelength = StatusVar('start_wavelength', default=0.785) + _end_wavelength = StatusVar('end_wavelength', default=0.7851) + _current_wavelength = StatusVar('current_wavelength', default=0.785) + + _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 = [] # list of TerascanData + + _last_locked: float = 0 + + # Update signals, e.g. for GUI module + sigWavelengthUpdated = QtCore.Signal(float) + sigCountsUpdated = QtCore.Signal(object) # is a List[TerascanData] + sigLaserLocked = QtCore.Signal(bool) + + # Update signals for other logics + sigConfigureCounter = QtCore.Signal(float, float) + sigSetLaserWavelengths = QtCore.Signal(float, float) + sigSetLaserScanRate = QtCore.Signal(int) + sigSetLaserScanType = QtCore.Signal(int) + sigRestartLaser = QtCore.Signal(float) # used when we need to restart the laser, as everything else is already running + + sigStartScan = QtCore.Signal() + sigStopScan = QtCore.Signal() + + sigStopCounting = QtCore.Signal() + sigStartCounting = QtCore.Signal() + + sigScanFinished = QtCore.Signal() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() + + def on_activate(self): + laser = self._laser() + counter = self._counter() + wavemeter = self._wavemeter() + daq = self._daq() + + # Outputs: + self.sigConfigureCounter.connect(counter.configure, QtCore.Qt.QueuedConnection) + self.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.sigRestartLaser.connect(laser.restart_scan, 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_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_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.sigWavelengthUpdated.connect(self._new_wavemeter_data) + counter.sigScanFinished.connect(self._process_counter_data) + daq.sigNewData.connect(self._new_daq_data) + + # Configure Counter: + self._record_length_s = self._record_length_ms * 1e-3 + self._bin_width_s = self._record_length_s + self.sigConfigureCounter.emit(self._bin_width_s, self._record_length_s) + + # Add watchdog timer + self.__timer = QtCore.QTimer() + self.__timer.setSingleShot(False) + self.__timer.timeout.connect(self.__watchdog) + self.__timer.start(500) + + def on_deactivate(self): + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + @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: + if self.module_state() == 'idle': + self.module_state.lock() + self._current_data = [] + self._last_locked = time.time() + self.sigSetLaserWavelengths.emit(self._start_wavelength, self._end_wavelength) + self.sigStopCounting.emit() # In case we are counting from somewhere else + self.sigStartScan.emit() + + @QtCore.Slot() + def stop_scan(self): + with self._thread_lock: + if self.module_state() == 'locked': + self.sigStopScan.emit() + self.module_state.unlock() + + @QtCore.Slot(float, float) + def configure_scan(self, + start: float, stop: float + ): + with self._thread_lock: + if self.module_state() == 'idle': + self._start_wavelength = start + self._end_wavelength = stop + self.sigSetLaserWavelengths.emit(start, stop) + else: + self.log.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.log.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.log.warning( + 'Tried to configure while a scan was running.'\ + 'Please wait until it is finished or stop it.') + + @QtCore.Slot() + def _laser_scan_started(self): + pass + + @QtCore.Slot() + def _laser_scan_finished(self): + with self._thread_lock: + self.sigScanFinished.emit() + self.sigStopCounting.emit() + self.module_state.unlock() + + @QtCore.Slot(np.ndarray) + def _new_wavemeter_data(self, data: np.ndarray): + with self._thread_lock: + if self.module_state() == 'locked' and self._laser_locked: + wave = data[0][0] + self._current_wavelength = wave # ? + self.sigWavelengthUpdated.emit(wave) + + @QtCore.Slot(np.ndarray) + def _process_counter_data(self, data: np.ndarray): + with self._thread_lock: + if self.module_state() == 'locked' and self._laser_locked: + self._current_data.append( + TerascanData( + wavelength=self._current_wavelength, + counts=data, + ) + ) + self.sigCountsUpdated.emit(self._current_data) + + @QtCore.Slot(object) + def _new_daq_data(self, data: List[ReaderVal]): + with self._thread_lock: + if self.module_state() == 'locked': + for i in data: + if i.type is InputType.DIGITAL: + # We assume there is only one digital input for this measurement + if i.val and not self._laser_locked: + self._laser_locked = True + # self.sigStartCounting.emit() + + if not i.val and self._laser_locked: + # We just mode hopped and are now unlocked + self._laser_locked = False + self._remove_mode_hop() + + + self.sigLaserLocked.emit(self._laser_locked) + + + ### Internal Functions ### + def _remove_mode_hop(self): + """ Function to remove previous data when a mode hop occurs. + Assumes we are locked externally. + """ + target = -1 + scan_up = self._start_wavelength < self._end_wavelength + + if self._scan_type == 1: # MEDIUM Scan + target = self._current_wavelength - self._mode_hop_overlap_med \ + if scan_up else self._current_wavelength + self._mode_hop_overlap_med + elif self._scan_type == 2: # FINE Scan + target = self._current_wavelength - self._mode_hop_overlap_fine \ + if scan_up else self._current_wavelength + self._mode_hop_overlap_fine + else: + self.log.warning('Unknown scan type. Not removing data.') + return + + while len(self._current_data) > 0: + if (scan_up and \ + self._current_data[-1].wavelength > target) or \ + (not scan_up and \ + self._current_data[-1].wavelength < target): + # We need to check if we are scanning up or down + self._current_data.pop() + else: + break + + + + #### Watchdog Timer #### + def __watchdog(self): + with self._thread_lock: + if self.module_state() == 'locked': + if self._laser_locked: + self._last_locked = time.time() + elif time.time() - self._last_locked > self._laser_timeout_s: + self.log.info('Laser did not lock in time. Restarting scan.') + new_start = self._current_wavelength*1e-3 + self._last_locked = time.time() + self.sigRestartLaser.emit(new_start) + From 3dd9816e695a70ae7c65fee0ce1a3d5956a19089 Mon Sep 17 00:00:00 2001 From: lange50 Date: Sat, 26 Apr 2025 16:50:18 -0400 Subject: [PATCH 02/16] Update gui The scan is working. But you cannot take more than one scan in a row. I am working on getting confident that there is no lost data. --- src/qudi/gui/terascan/terascan_gui.py | 28 +++++++++++++++++++----- src/qudi/hardware/laser/solstis_laser.py | 19 ++++++++-------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index 5faf756..44971ba 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -189,13 +189,28 @@ def _wavelength_changed(self, wave: float) -> None: # percent = 100 * (wave*1e-3 - self._start_wavelength) / (self._stop_wavelength - self._start_wavelength) # self._mw._progress_bar.setValue(int(round(percent))) + + # TODO: redundant code in _update_plot and _save_data @QtCore.Slot() def _save_data(self) -> None: ds = TextDataStorage( root_dir=self.save_dir, # Use the configurable save directory column_formats='.15e' ) - array = np.array([(d.wavelength, d.counts) for d in self._data]) + + x_array = np.array(self._terascan_logic().wavelength_data) + y_array = np.array(self._terascan_logic().counts_data[:len(x_array)]) + mask = self._terascan_logic().ttl_data + + min_length = min(len(x_array), len(y_array), len(mask)) + x_array = x_array[:min_length] + y_array = y_array[:min_length] + mask = mask[:min_length] + + x_array = x_array[mask] + y_array = y_array[mask] + + array = np.column_stack((x_array, y_array)) ds.save_data(array) @QtCore.Slot(bool) @@ -222,6 +237,11 @@ def _update_plot(self): y_array = np.array(self._terascan_logic().counts_data[:len(x_array)]) mask = self._terascan_logic().ttl_data + min_length = min(len(x_array), len(y_array), len(mask)) + x_array = x_array[:min_length] + y_array = y_array[:min_length] + mask = mask[:min_length] + x_array = x_array[mask] y_array = y_array[mask] @@ -231,8 +251,8 @@ def _update_plot(self): return # downsample - x_array = x_array[::10] - y_array = y_array[::10] + # x_array = x_array[::10] + # y_array = y_array[::10] # If running average is enabled, apply a rolling average if self._mw.checkbox_running_avg.isChecked(): @@ -241,8 +261,6 @@ def _update_plot(self): kernel = np.ones(window_size) / float(window_size) y_array = np.convolve(y_array, kernel, mode='same') - # Final sanity-check - self._mw.data_item.setData(x=x_array, y=y_array) diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index f4e95e6..8d67331 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -262,18 +262,19 @@ def stop_scan(self) -> bool: self.log.exception(f'Scan stop failure: {e.message}') return False + # TODO: Implement this function @QtCore.Slot(float) def restart_scan(self, wavelength: float) -> bool: """Restart a scan from a specified wavelength""" - - if self.module_state() == 'locked': - self.module_state.unlock() - self._start_wavelength = wavelength - self._scan_started = time() - return self.start_scan() - else: - self.log.warning('Restart scan called when not running') - return False + pass + # if self.module_state() == 'locked': + # self.module_state.unlock() + # self._start_wavelength = wavelength + # self._scan_started = time() + # return self.start_scan() + # else: + # self.log.warning('Restart scan called when not running') + # return False def pause_scan(self): """Pause a running scan (unimplemented)""" From 7cbc4827b76ed11fed0e843aaf614b33f73194c2 Mon Sep 17 00:00:00 2001 From: "Lange, Christian M" Date: Sun, 27 Apr 2025 22:49:22 -0400 Subject: [PATCH 03/16] Refactor terascan_logic and terascan_gui --- src/qudi/gui/terascan/terascan_gui.py | 235 ++++++++++----- src/qudi/gui/terascan/terascan_main_window.py | 128 -------- src/qudi/hardware/daq/dummy_nidaq.py | 96 ++++++ src/qudi/hardware/daq/nidaq_ben.py | 150 ---------- .../{solstis_laser.py => dummy_solstis.py} | 46 ++- .../{solstis_laser_ben.py => solstis.py} | 83 ++++-- .../hardware/timetagger/swabian_tagger.py | 6 +- src/qudi/hardware/wavemeter/wavemeter.py | 6 +- src/qudi/logic/terascan_logic.py | 276 +++++++++++------- src/qupidc_qudi_modules.egg-info/SOURCES.txt | 27 +- 10 files changed, 521 insertions(+), 532 deletions(-) delete mode 100644 src/qudi/gui/terascan/terascan_main_window.py create mode 100644 src/qudi/hardware/daq/dummy_nidaq.py delete mode 100644 src/qudi/hardware/daq/nidaq_ben.py rename src/qudi/hardware/laser/{solstis_laser.py => dummy_solstis.py} (90%) rename src/qudi/hardware/laser/{solstis_laser_ben.py => solstis.py} (87%) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index 44971ba..9adbdb4 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -3,7 +3,8 @@ import numpy as np import os -from PySide2 import QtCore, QtGui +from PySide2 import QtCore, QtGui, QtWidgets +import pyqtgraph as pg from typing import List from time import sleep @@ -14,9 +15,12 @@ from qudi.core.statusvariable import StatusVar from qudi.gui.terascan.terascan_main_window import TerascanMainWindow from qudi.util.paths import get_artwork_dir +from qudi.util.colordefs import QudiPalettePale as palette from qudi.logic.terascan_logic import TerascanData +pg.setConfigOption('useOpenGL', True) # Add this at the top of your file + # TODO: No status variables. Just grab the ones from the logic module. class TerascanGui(GuiBase): @@ -31,27 +35,22 @@ class TerascanGui(GuiBase): # Signals for outgoing control signals to logic sigStartMeasurement = QtCore.Signal() sigStopMeasurement = QtCore.Signal() - sigSetWavelengths = QtCore.Signal(float, float) + sigSetStartWavelength = QtCore.Signal(float) + sigSetStopWavelength = QtCore.Signal(float) sigSetScanType = QtCore.Signal(int) sigSetScanRate = QtCore.Signal(int) sigSaveData = QtCore.Signal() - # Connector to the logic module + #################### CONNECTOR TO LOGIC #################### _terascan_logic = Connector(name='terascan_logic', interface='TerascanLogic') - save_dir = ConfigOption('save_dir', default='C:\\Users\\hoodl\\qudi\\Data') - # Status variables saved in the AppStatus: - _start_wavelength = StatusVar(name='start_wavelength', default=0.775) - _stop_wavelength = StatusVar(name='stop_wavelength', default=0.790) - _current_wavelength = StatusVar('current_wavelength', default=0.785) - # New status variable for the running average window size: + #################### STATUS VARIABLES #################### _running_avg_points = StatusVar(name='running_avg_points', default=5) + # TODO: operate in terascan_logic instead of terascan_logic() def on_activate(self) -> None: # Initialize the main window and set wavelength controls: self._mw = TerascanMainWindow() - 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) @@ -87,8 +86,11 @@ def on_activate(self) -> None: self.sigStopMeasurement.connect( self._terascan_logic().stop_scan, QtCore.Qt.QueuedConnection ) - self.sigSetWavelengths.connect( - self._terascan_logic().configure_scan, QtCore.Qt.QueuedConnection + self.sigSetStartWavelength.connect( + self._terascan_logic().set_start_wavelength, QtCore.Qt.QueuedConnection + ) + self.sigSetStopWavelength.connect( + self._terascan_logic().set_stop_wavelength, QtCore.Qt.QueuedConnection ) self.sigSetScanType.connect( self._terascan_logic().set_scan_type, QtCore.Qt.DirectConnection @@ -103,9 +105,8 @@ def on_activate(self) -> None: self.__timer = QtCore.QTimer() self.__timer.setSingleShot(False) self.__timer.timeout.connect(self._update_plot) - - # Restore saved wavelengths: - self.sigSetWavelengths.emit(self._start_wavelength, self._stop_wavelength) + self.__timer.start(250) # Update every 250 ms + # TODO: Make this more configurable and maybe more efficient? # Restore running average points from the StatusVar: self._mw.spin_avg_points.setValue(self._running_avg_points) @@ -141,24 +142,18 @@ def show(self) -> None: # Handlers from the UI: @QtCore.Slot(float) def _start_changed(self, wave: float) -> None: - self._start_wavelength = wave - self.sigSetWavelengths.emit(self._start_wavelength, self._stop_wavelength) + self.sigSetStartWavelength.emit(wave) @QtCore.Slot(float) def _stop_changed(self, wave: float) -> None: - self._stop_wavelength = wave - self.sigSetWavelengths.emit(self._start_wavelength, self._stop_wavelength) + self.sigSetStopWavelength.emit(wave) @QtCore.Slot() def _start_stop_pressed(self) -> None: if self._mw.start_stop_button.text() == 'Start Measurement': - self._update_ui(True) - self.__timer.start(250) - self.sigStartMeasurement.emit() + self.sigStartMeasurement.emit() # Tell the logic to start the scan else: - self._update_ui(False) - self.__timer.stop() - self.sigStopMeasurement.emit() + self.sigStopMeasurement.emit() # Tell the logic to stop the scan @QtCore.Slot(int) def _scan_type_changed(self, _: int): @@ -172,65 +167,26 @@ def _scan_rate_changed(self, _: int): if self._mw.scan_rate.currentData() is not None: self.sigSetScanRate.emit(self._mw.scan_rate.currentData().value) - # Handlers from the 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[TerascanData]) -> None: - self._data = data - - #TODO: bring the loading bar back - @QtCore.Slot(float) - def _wavelength_changed(self, wave: float) -> None: - pass - # 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))) - - - # TODO: redundant code in _update_plot and _save_data - @QtCore.Slot() - def _save_data(self) -> None: - ds = TextDataStorage( - root_dir=self.save_dir, # Use the configurable save directory - column_formats='.15e' - ) - - x_array = np.array(self._terascan_logic().wavelength_data) - y_array = np.array(self._terascan_logic().counts_data[:len(x_array)]) - mask = self._terascan_logic().ttl_data - - min_length = min(len(x_array), len(y_array), len(mask)) - x_array = x_array[:min_length] - y_array = y_array[:min_length] - mask = mask[:min_length] - - x_array = x_array[mask] - y_array = y_array[mask] - - array = np.column_stack((x_array, y_array)) - 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)) + # @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)) # Private internal functions: - def _update_ui(self, running: bool) -> None: - if running: + def _update_ui(self) -> None: + if self._terascan_logic().is_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') + # TODO: Find a good place for this + # self._mw.plot_widget.setXRange(self._start_wavelength, self._stop_wavelength) + def _update_plot(self): + # self._mw.start_stop_button.setText('Start Measurement') # Make a local snapshot of the data x_array = np.array(self._terascan_logic().wavelength_data) @@ -268,3 +224,130 @@ def _update_plot(self): @QtCore.Slot(int) def _update_running_avg_points(self, points: int) -> None: self._running_avg_points = points + + + def __update(self) -> None: + """ Update the GUI with the latest data from the logic module """ + # Update the setText button + self._update_ui() + self._update_plot() + + + + + + +class TerascanMainWindow(QtWidgets.QMainWindow): + """ Main window for Terascan measurement """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setWindowTitle('Terascan Measurement') + self.resize(1250, 500) + + # 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._progress_bar = QtWidgets.QProgressBar() + self._progress_bar.setRange(0, 100) + self._progress_bar.setValue(0) + + 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._progress_bar) + + # Initialize widgets for wavelengths, scan, etc. + self.start_wavelength_label = QtWidgets.QLabel('Start 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(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(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.setAntialiasing(False) + 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='Counts') + + self.data_item = pg.PlotDataItem( + pen=pg.mkPen(palette.c1, style=QtCore.Qt.SolidLine), + # downsampling if you want + # downsample=10, # Render 1 out of every 10 points + # downsampleMethod='mean' # Average points for smoother results + ) + self.plot_widget.addItem(self.data_item) + + # Running Average controls + self.checkbox_running_avg = QtWidgets.QCheckBox("Enable Running Average") + self.checkbox_running_avg.setChecked(False) + self.label_avg_points = QtWidgets.QLabel("Points in Rolling Average:") + self.spin_avg_points = QtWidgets.QSpinBox() + self.spin_avg_points.setRange(1, 9999) + self.spin_avg_points.setValue(5) # default value; this will be set from a StatusVar in the GUI + + # The Start Measurement button (we want this at the very bottom) + self.start_stop_button = QtWidgets.QPushButton('Start Measurement') + + # Arrange widgets in layout + layout = QtWidgets.QGridLayout() + layout.addWidget(self.plot_widget, 0, 0, 4, 4) + + 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) + + # Place Running Average controls ABOVE the Start Measurement button: + control_layout.addWidget(self.checkbox_running_avg) + control_layout.addWidget(self.label_avg_points) + control_layout.addWidget(self.spin_avg_points) + + # Add stretch to push the start button to the bottom: + control_layout.addStretch() + control_layout.addWidget(self.start_stop_button) + + layout.addLayout(control_layout, 0, 5, 5, 1) + layout.setColumnStretch(1, 1) + + central_widget = QtWidgets.QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) diff --git a/src/qudi/gui/terascan/terascan_main_window.py b/src/qudi/gui/terascan/terascan_main_window.py deleted file mode 100644 index dab0b3d..0000000 --- a/src/qudi/gui/terascan/terascan_main_window.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -__all__ = ['TerascanMainWindow'] - -import os -from PySide2 import QtGui, QtCore, QtWidgets -import pyqtgraph as pg -pg.setConfigOption('useOpenGL', True) # Add this at the top of your file - -from qudi.util.widgets.plotting.image_widget import ImageWidget -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 """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setWindowTitle('Terascan Measurement') - self.resize(1250, 500) - - # 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._progress_bar = QtWidgets.QProgressBar() - self._progress_bar.setRange(0, 100) - self._progress_bar.setValue(0) - - 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._progress_bar) - - # Initialize widgets for wavelengths, scan, etc. - self.start_wavelength_label = QtWidgets.QLabel('Start 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(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(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.setAntialiasing(False) - 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='Counts') - - self.data_item = pg.PlotDataItem( - pen=pg.mkPen(palette.c1, style=QtCore.Qt.SolidLine), - # downsampling if you want - # downsample=10, # Render 1 out of every 10 points - # downsampleMethod='mean' # Average points for smoother results - ) - self.plot_widget.addItem(self.data_item) - - # Running Average controls - self.checkbox_running_avg = QtWidgets.QCheckBox("Enable Running Average") - self.checkbox_running_avg.setChecked(False) - self.label_avg_points = QtWidgets.QLabel("Points in Rolling Average:") - self.spin_avg_points = QtWidgets.QSpinBox() - self.spin_avg_points.setRange(1, 9999) - self.spin_avg_points.setValue(5) # default value; this will be set from a StatusVar in the GUI - - # The Start Measurement button (we want this at the very bottom) - self.start_stop_button = QtWidgets.QPushButton('Start Measurement') - - # Arrange widgets in layout - layout = QtWidgets.QGridLayout() - layout.addWidget(self.plot_widget, 0, 0, 4, 4) - - 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) - - # Place Running Average controls ABOVE the Start Measurement button: - control_layout.addWidget(self.checkbox_running_avg) - control_layout.addWidget(self.label_avg_points) - control_layout.addWidget(self.spin_avg_points) - - # Add stretch to push the start button to the bottom: - control_layout.addStretch() - control_layout.addWidget(self.start_stop_button) - - layout.addLayout(control_layout, 0, 5, 5, 1) - layout.setColumnStretch(1, 1) - - central_widget = QtWidgets.QWidget() - central_widget.setLayout(layout) - self.setCentralWidget(central_widget) diff --git a/src/qudi/hardware/daq/dummy_nidaq.py b/src/qudi/hardware/daq/dummy_nidaq.py new file mode 100644 index 0000000..abe36db --- /dev/null +++ b/src/qudi/hardware/daq/dummy_nidaq.py @@ -0,0 +1,96 @@ +# import nidaqmx +from typing import List, Any, Dict +import time + +from PySide2 import QtCore +from qudi.core.module import Base +from qudi.util.mutex import RecursiveMutex +from qudi.core.configoption import ConfigOption +from qudi.interface.daq_reader_interface import DAQReaderInterface, InputType, \ + ReaderVal +from qudi.core.module import Base + + + +ICEBLOC = "Dev2/port1/line0" +FLIPPER = 'Dev2/port1/line2' +SHUTTER = 'Dev2/port2/line0' +GO_line = 'Dev2/port1/line1' +M2_CAVITY = "Dev2/ai0, Dev2/ai4" # 0 is positive, 4 is negative + + +# TODO: extend daq reader interface +# TODO: get status variable +# TODO: Define the tasks in the _init function and start them in the on_activate function. +# This is much more time efficient +# You will still have to have a way to stop that if you want to use a digital output +class NIDAQ(Base): + """ + Generic interface for reading from NIDAQ hardware. + + Example config for copy-paste: + + 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: + description: 'Input Signal' + type: 0 # 0 for Digital, 1 for Analog + name: 'line0' # The name as identified by the card + port: 1 # port number identified by the card + """ + # define the daq signal + sigNewData = QtCore.Signal(float, object) # is a List[ReaderVal] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() + self._data = 0 + + def on_activate(self): + """ Activate module. + """ + + self.__timer = QtCore.QTimer() + self.__timer.timeout.connect(self.__data_update) + self.__timer.setSingleShot(False) + self.__timer.start(10) + + def on_deactivate(self): + """ Deactivate module. + """ + if (self.__timer is not None): + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + if self._ttl_task: + self._ttl_task.close() + self._ttl_task = None + + + def start_reading(self): + self.module_state.lock() + self.statusvar = 2 + + def stop_reading(self): + if self.module_state() == 'locked': + self.module_state.unlock() + self.statusvar = 1 + + + def get_solstis_ttl(self) -> int: + # return a 1 or 0 randomly + return np.random.randint(0, 2) + + + def __data_update(self): + with self._thread_lock: + # It takes < 2 ms to read + timestamp = time.perf_counter() + self._data = self._ttl_task.read() + self.sigNewData.emit(timestamp, self._data) + diff --git a/src/qudi/hardware/daq/nidaq_ben.py b/src/qudi/hardware/daq/nidaq_ben.py deleted file mode 100644 index cc78938..0000000 --- a/src/qudi/hardware/daq/nidaq_ben.py +++ /dev/null @@ -1,150 +0,0 @@ -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 - - -class NIDAQ(DAQReaderInterface): - """ - Generic interface for reading from NIDAQ hardware. - - Example config for copy-paste: - - 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: - description: 'Input Signal' - type: 0 # 0 for Digital, 1 for Analog - name: 'line0' # The name as identified by the card - port: 1 # port number identified by the card - """ - - # config options - _update_interval: int = ConfigOption(name='update_interval', - default=0) - - _daq_name: str = ConfigOption(name='device_str', default='Dev1', - missing='warn') - - _daq_ch_config: Dict[str, Dict[str, Any]] = ConfigOption( - name='channels', - default={ - 'default_channel': { - 'description': 'Input Signal', - 'type': 0, - 'name': 'line0', - 'port': 0 - } - }, - missing='warn' - ) - - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__timer = None - self._thread_lock = RecursiveMutex() - - def on_activate(self): - """ Activate module. - """ - - temp: List[ReaderVal] = ( - ReaderVal( - type=InputType(v['type']), - name=v['name'], - port=v['port'], - description=v['description'], - ) for v in self._daq_ch_config.values() - ) - - self._analog_channels: List[ReaderVal] = [] - self._digital_channels: List[ReaderVal] = [] - - for i in temp: - if (i.type == InputType.ANALOG): - 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. - """ - 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) - return (i.description for i in out) - - - def get_reading(self) -> List[ReaderVal]: - """ Gets a reading from the device """ - - if (len(self._analog_channels) > 0): - with nidaqmx.Task() as task: - for i in self._analog_channels: - chan = self._get_channel(i) - task.ai_channels.add_ai_voltage_chan(chan) - - data = task.read() - if not isinstance(data, list): - data = [data] - - self._update_vals(data, self._analog_channels) - - if (len(self._digital_channels) > 0): - with nidaqmx.Task() as task: - for i in self._digital_channels: - chan = self._get_channel(i) - task.di_channels.add_di_chan(chan) - - data = task.read() - # if len(data) == 1: - if not isinstance(data, list): - data = [data] - - self._update_vals(data, self._digital_channels) - - - out = self._analog_channels.copy() - out.extend(self._digital_channels) - return out - - - - def _get_channel(self, chan: ReaderVal) -> str: - return f"{self._daq_name}/port{chan.port}/{chan.name}" - - def _update_vals(self, vals: List[float], chans: List[ReaderVal]) -> None: - """ Updates channels in-place with new value. Assumes one sample per channel - """ - - if (len(vals) != len(chans)): - self.log.warning('Mismatch between number of configured channels and number of data points read.') - - for v, c in zip(vals, chans): - c.val = v - - def __data_update(self): - with self._thread_lock: - data = self.get_reading() - self.sigNewData.emit(data) - diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/dummy_solstis.py similarity index 90% rename from src/qudi/hardware/laser/solstis_laser.py rename to src/qudi/hardware/laser/dummy_solstis.py index 8d67331..332d95e 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/dummy_solstis.py @@ -25,10 +25,21 @@ from qudi.interface.scanning_laser_interface import ScanningLaserInterface from qudi.interface.scanning_laser_interface import ShutterState -import qudi.hardware.laser.solstis_funcs as solstis from qudi.hardware.laser.solstis_constants import * +""" +TODO: Implement this class. It is a placeholder for now. +Possible states +state_0 = {"in_progress": False} +state_1 = lambda wl, tuning: { + "in_progress": True, + "wavelength": wl, + "start": self._start_wavelength, + "stop": self._end_wavelength, + "tuning": tuning +} +""" class SolstisLaser(ScanningLaserInterface): """ Hardware file for solstis laser. @@ -63,6 +74,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__timer = None self._wavelength = -1 + self._test_status = {"in_progress": False} def on_activate(self): """ Activate module. @@ -75,7 +87,6 @@ def on_activate(self): 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() @@ -102,15 +113,7 @@ def connect_laser(self) -> bool: @return bool: connection success """ - try: - self.socket = solstis.init_socket(address=self._laser_ip, - port=self._laser_port) - solstis.start_link(sock=self.socket, ip_address=self._host_ip) - except solstis.SolstisError as e: - self.log.exception(f'Communication Failure: {e.message}') - return False - else: - return True + return True def disconnect_laser(self) -> None: """ Close the connection to the instrument. @@ -125,12 +128,7 @@ def get_power(self) -> float: @return float: laser power in watts """ - try: - answer = solstis.get_status(self.socket) - return(answer['output_monitor']) - except solstis.SolstisError as e: - self.log.exception(f'Failure getting power: {e.message}') - return -1 + return -1 def get_power_setpoint(self) -> float: """ Get the laser power setpoint. (unimplemented) @@ -175,12 +173,7 @@ def get_temperatures(self) -> dict: @return dict: dict of temperature names and value """ - try: - answer = solstis.get_status(self.socket) - return {'laser': float(answer['temperature'])} - except solstis.SolstisError as e: - self.log.exception(f'Failure getting temperature: {e.message}') - return -1 + return -1 @@ -189,12 +182,7 @@ def get_laser_state(self): @return LaserState: laser state """ - - try: - return solstis.scan_stitch_status(self.socket, self._scan_type) - except solstis.SolstisError as e: - self.log.exception(f'Failure getting status: {e.message}') - return -1 + return self._test_status def set_laser_state(self, status): diff --git a/src/qudi/hardware/laser/solstis_laser_ben.py b/src/qudi/hardware/laser/solstis.py similarity index 87% rename from src/qudi/hardware/laser/solstis_laser_ben.py rename to src/qudi/hardware/laser/solstis.py index 0f3819c..17b8045 100644 --- a/src/qudi/hardware/laser/solstis_laser_ben.py +++ b/src/qudi/hardware/laser/solstis.py @@ -24,12 +24,12 @@ from qudi.core.statusvariable import StatusVar from qudi.interface.scanning_laser_interface import ScanningLaserInterface from qudi.interface.scanning_laser_interface import ShutterState - +from qudi.core.module import Base import qudi.hardware.laser.solstis_funcs as solstis from qudi.hardware.laser.solstis_constants import * -class SolstisLaser(ScanningLaserInterface): +class SolstisLaser(Base): """ Hardware file for solstis laser. Example config for copy-paste: @@ -42,9 +42,12 @@ class SolstisLaser(ScanningLaserInterface): laser_port: 39933 # Port number to connect on """ + ###################### SIGNAL #################### + sigNewData = QtCore.Signal(float, object) # timestamp, data + _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') + _laser_port = ConfigOption(name='laser_port', default=39900, 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) @@ -62,7 +65,17 @@ class SolstisLaser(ScanningLaserInterface): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__timer = None - self._wavelength = -1 + + self._time_of_most_recent_wavelength_update = time.time() + self._current_wavelength = -1 + + self.timeout = 3.0 # seconds + self.statusvar = 0 + """ statusvar + 0 = idle + 1 = running + -1 = error state + """ def on_activate(self): """ Activate module. @@ -75,7 +88,6 @@ def on_activate(self): 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() @@ -125,12 +137,7 @@ def get_power(self) -> float: @return float: laser power in watts """ - try: - answer = solstis.get_status(self.socket) - return(answer['output_monitor']) - except solstis.SolstisError as e: - self.log.exception(f'Failure getting power: {e.message}') - return -1 + return -1 def get_power_setpoint(self) -> float: """ Get the laser power setpoint. (unimplemented) @@ -239,7 +246,6 @@ def start_scan(self) -> bool: update_step=0, pause=True) solstis.scan_stitch_op(self.socket, self._scan_type, "start") - self.sigScanStarted.emit() self._scan_started = time() self.module_state.lock() return True @@ -254,26 +260,12 @@ def stop_scan(self) -> bool: try: 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: self.log.exception(f'Scan stop failure: {e.message}') return False - - @QtCore.Slot(float) - def restart_scan(self, wavelength: float) -> bool: - """Restart a scan from a specified wavelength""" - - if self.module_state() == 'locked': - self.module_state.unlock() - self._start_wavelength = wavelength - self._scan_started = time() - return self.start_scan() - else: - self.log.warning('Restart scan called when not running') - return False def pause_scan(self): """Pause a running scan (unimplemented)""" @@ -302,6 +294,7 @@ def get_wavelength(self) -> 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 { @@ -309,6 +302,7 @@ def get_scan_types(self) -> dict: 'Fine': TeraScanType.SCAN_TYPE_FINE, 'Line': TeraScanType.SCAN_TYPE_LINE } + @property def get_scan_rates(self) -> dict: scan_type = self._scan_type @@ -374,10 +368,37 @@ def set_scan_rate(self, scan_rate: int): self._scan_rate = TeraScanRate(scan_rate) + """ + Here are the possible states of the laser: + state_0 = {"in_progress": False} + state_1 = lambda wl, tuning: { + "in_progress": True, + "wavelength": wl, + "start": self._start_wavelength, + "stop": self._end_wavelength, + "tuning": tuning + } + + Case 1: The laser is not scanning, and it is not trying to scan. + If it should be scanning, but it is not, then the laser is in an error state. Otherwise, it is idle. + + Case 2: The laser is not scanning, but it is trying to scan. + Signature: {"in_progress": False}, but the wavelength will be moving around + + Case 3: The laser is scanning, and the scan is in progress. + Signature: {"in_progress": True}, and the wavelength will be moving around + """ def __status_update(self): - if self.module_state() == 'locked' \ - and self._scan_started + 3 < time(): # if we check for status too soon, we might get a false positive - status = self.get_laser_state() + try: + status = solstis.scan_stitch_status(self.socket, self._scan_type) if status['in_progress'] == False: - self.sigScanFinished.emit() - self.module_state.unlock() \ No newline at end of file + self.statusvar = 0 + elif status['in_progress'] == True: + self.statusvar = 1 + else: + self.statusvar = -1 + except solstis.SolstisError as e: + self.log.exception(f'Failure getting status: {e.message}') + self.statusvar = -1 + timestamp = time.perf_counter() + self.sigNewData.emit(timestamp, status) \ 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 792e60f..9cbd750 100644 --- a/src/qudi/hardware/timetagger/swabian_tagger.py +++ b/src/qudi/hardware/timetagger/swabian_tagger.py @@ -10,7 +10,7 @@ from qudi.util.mutex import RecursiveMutex from qudi.interface.fast_counter_interface import FastCounterInterface from qudi.core.configoption import ConfigOption - +from qudi.core.module import Base # TODO: Have a way to collect and save the entire buffer. This will return in ps so you will have to convert it to ms. # You could also start an acquisition while sending the data to the GUI. @@ -28,6 +28,8 @@ class SwabianTimeTagger(FastCounterInterface): channels: photon_counts: 0 """ + ####################### SIGNALS #################### + sigNewData = QtCore.Signal(float, object) # timestamp, data _channel_apd_0 = 1 _bin_width_ps = 1_000_000_000 @@ -113,4 +115,4 @@ def __update_data(self): data_obj = self._counter.getDataObject(remove=True) timestamp = time.perf_counter() self.most_recent_data = np.squeeze(data_obj.getData()) - self.sigScanFinished.emit(timestamp, self.most_recent_data) + self.sigNewData.emit(timestamp, self.most_recent_data) diff --git a/src/qudi/hardware/wavemeter/wavemeter.py b/src/qudi/hardware/wavemeter/wavemeter.py index ea2b505..3853b64 100644 --- a/src/qudi/hardware/wavemeter/wavemeter.py +++ b/src/qudi/hardware/wavemeter/wavemeter.py @@ -7,6 +7,7 @@ from qudi.util.mutex import RecursiveMutex from qudi.interface.simple_wavemeter_interface import SimpleWavemeterInterface +from qudi.core.module import Base # TODO: add docs @@ -21,6 +22,9 @@ class Wavemeter(SimpleWavemeterInterface): Returns: _type_: _description_ """ + + ####################### SIGNALS #################### + sigNewData = QtCore.Signal(float, object) # timestamp, data _threaded = True @@ -84,4 +88,4 @@ def __update_data(self): if self.module_state() == 'locked': self._data = self.get_wavelength() timestamp = time.perf_counter() - self.sigWavelengthUpdated.emit(timestamp, self._data) \ No newline at end of file + self.sigNewData.emit(timestamp, self._data) \ No newline at end of file diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index 12c6dcd..b40a66b 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -17,15 +17,6 @@ from qudi.interface.daq_reader_interface import InputType, ReaderVal -from rpyc.utils.classic import obtain - -class TerascanData(): - wavelength: float - counts: int - - def __init__(self, wavelength: float, counts: int): - self.wavelength = wavelength - self.counts = counts class TerascanLogic(LogicBase): """ @@ -54,57 +45,39 @@ class TerascanLogic(LogicBase): _daq = Connector(name='daq', interface='Base') #################### CONFIGURATION OPTIONS #################### - _record_length_ms = ConfigOption(name='record_length_ms', - default=1, - missing='info') - + save_dir = ConfigOption('save_dir', default='C:\\Users\\hoodl\\qudi\\Data', missing='error') _laser_timeout_s = ConfigOption(name='laser_timeout_s', default=10) - - _mode_hop_overlap_med = ConfigOption(name='mode_hop_overlap_med', default=0.001) - _mode_hop_overlap_fine = ConfigOption(name='mode_hop_overlap_fine', default=0.00025) #################### STATUS VARIABLES #################### - _start_wavelength = StatusVar('start_wavelength', default=0.785) - _end_wavelength = StatusVar('end_wavelength', default=0.7851) - _current_wavelength = StatusVar('current_wavelength', default=0.785) - - _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 = [] # list of TerascanData - - _last_locked: float = 0 - - #################### SIGNALS FOR GUI MODULE #################### - sigWavelengthUpdated = QtCore.Signal(object) - sigCountsUpdated = QtCore.Signal(object) # is a List[TerascanData] - sigLaserLocked = QtCore.Signal(bool) + start_wavelength = StatusVar('start_wavelength', default=0.785) + stop_wavelength = StatusVar('stop_wavelength', default=0.7851) + scan_rate = StatusVar('scan_rate', default=12) # SCAN_RATE_FINE_LINE_20_GHZ + scan_type = StatusVar('scan_type', default=2) # SCAN_TYPE_FINE #################### SIGNALS FOR OTHER LOGIC MODULES #################### - sigConfigureCounter = QtCore.Signal(float, float) - sigSetLaserWavelengths = QtCore.Signal(float, float) - sigSetLaserScanRate = QtCore.Signal(int) - sigSetLaserScanType = QtCore.Signal(int) - sigRestartLaser = QtCore.Signal(float) # used when we need to restart the laser, as everything else is already running - - sigStartScan = QtCore.Signal() - sigStopScan = QtCore.Signal() - - sigStopCounting = QtCore.Signal() - sigStartCounting = QtCore.Signal() - - sigScanFinished = QtCore.Signal() + # TODO: Move appropriate function calls here def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__timer = None self._thread_lock = RecursiveMutex() + ##################### EXPERIMENT PARAMETERS #################### + + ############## DATA #################### + """ + All of these lists should eventually be the same length, except for laser_status_data. + That is just for bookkeeping purposes. + """ + self._time_since_last_wl_change = time.time() + self._curr_wavelength = 0.0 + self._forward_direction = True + self.wavelength_data = [] self.ttl_data = [] self.counts_data = [] + self.laser_status_data = [] def on_activate(self): laser = self._laser() @@ -118,34 +91,22 @@ def on_activate(self): daq.start_reading() #################### OUTPUTS #################### - # self.sigConfigureCounter.connect(counter.configure, QtCore.Qt.QueuedConnection) - self.sigSetLaserWavelengths.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) - self.sigSetLaserScanRate.connect(laser.set_scan_rate, QtCore.Qt.QueuedConnection) - self.sigSetLaserScanType.connect(laser.set_scan_type, QtCore.Qt.DirectConnection) # Is direct on purpose - self.sigRestartLaser.connect(laser.restart_scan, 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_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_measure, QtCore.Qt.QueuedConnection) - # self.sigStopCounting.connect(counter.stop_measure, QtCore.Qt.QueuedConnection) + # self.sigStartScan.connect(laser.start_scan, QtCore.Qt.QueuedConnection) + # self.sigStopScan.connect(laser.stop_scan, QtCore.Qt.QueuedConnection) + # self.sigSetWavelengths.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) + # self.sigSetScanRate.connect(laser.set_scan_rate, QtCore.Qt.QueuedConnection) + # self.sigSetScanType.connect(laser.set_scan_type, QtCore.Qt.QueuedConnection) #################### INPUTS #################### - laser.sigScanStarted.connect(self._laser_scan_started) - laser.sigScanFinished.connect(self._laser_scan_finished) - wavemeter.sigWavelengthUpdated.connect(self._new_wavemeter_data) - counter.sigScanFinished.connect(self._process_counter_data) + laser.sigNewData.connect(self._new_laser_data) + wavemeter.sigNewData.connect(self._new_wavemeter_data) + counter.sigNewData.connect(self._new_counter_data) daq.sigNewData.connect(self._new_daq_data) #################### WATCHDOG TIMER #################### self.__timer = QtCore.QTimer() self.__timer.setSingleShot(False) - self.__timer.timeout.connect(self.__watchdog) + self.__timer.timeout.connect(self.__update) self.__timer.start(100) # 100 ms timer def on_deactivate(self): @@ -165,46 +126,81 @@ def scan_types(self) -> dict: def scan_rates(self) -> dict: return self._laser().get_scan_rates - @QtCore.Slot() + def get_start_wavelength(self) -> float: + with self._thread_lock: + return self.start_wavelength + + def get_stop_wavelength(self) -> float: + with self._thread_lock: + return self.stop_wavelength + + def get_scan_rate(self) -> int: + with self._thread_lock: + return self.scan_rate + + def get_scan_type(self) -> int: + with self._thread_lock: + return self.scan_type + + def is_running(self) -> bool: + with self._thread_lock: + if self.module_state() == 'locked': + return True + else: + return False + def start_scan(self): with self._thread_lock: if self.module_state() == 'idle': - self.experiment_start_time = time.perf_counter() self.module_state.lock() - self._current_data = [] - self._last_locked = time.time() - self.sigSetLaserWavelengths.emit(self._start_wavelength, self._end_wavelength) - self.sigStopCounting.emit() # In case we are counting from somewhere else - self.sigStartScan.emit() + self._laser.set_wavelengths(self.start_wavelength, self.stop_wavelength) + self._laser.set_scan_type(self.scan_type) + self._laser.set_scan_rate(self.scan_rate) + self._laser.start_scan() - @QtCore.Slot() def stop_scan(self): with self._thread_lock: if self.module_state() == 'locked': - self.sigStopScan.emit() self.module_state.unlock() + self._laser.stop_scan() + - @QtCore.Slot(float, float) - def configure_scan(self, - start: float, stop: float - ): + def set_start_wavelength(self, start: float): with self._thread_lock: if self.module_state() == 'idle': - self._start_wavelength = start - self._end_wavelength = stop - self.sigSetLaserWavelengths.emit(start, stop) + self.start_wavelength = start else: self.log.warning( 'Tried to configure while a scan was running.'\ 'Please wait until it is finished or stop it.') + def set_stop_wavelength(self, stop: float): + with self._thread_lock: + if self.module_state() == 'idle': + self.stop_wavelength = stop + else: + self.log.warning( + 'Tried to configure while a scan was running.'\ + 'Please wait until it is finished or stop it.') + + # TODO: Deprecated + @QtCore.Slot(float, float) + def set_wavelengths(self, start: float, stop: float): + with self._thread_lock: + if self.module_state() == 'idle': + self.start_wavelength = start + self.stop_wavelength = stop + self._forward_direction = start < stop + else: + self.log.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) + self.scan_rate = scan_rate else: self.log.warning( 'Tried to configure while a scan was running.'\ @@ -214,31 +210,15 @@ def set_scan_rate(self, scan_rate: 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) - + self.scan_type = scan_type else: self.log.warning( 'Tried to configure while a scan was running.'\ 'Please wait until it is finished or stop it.') - - @QtCore.Slot() - def _laser_scan_started(self): - pass - - @QtCore.Slot() - def _laser_scan_finished(self): - with self._thread_lock: - self.sigScanFinished.emit() - self.sigStopCounting.emit() - self.module_state.unlock() @QtCore.Slot(float, np.ndarray) - def _process_counter_data(self, timestamp, data: np.ndarray): + def _new_counter_data(self, timestamp, data: np.ndarray): with self._thread_lock: if self.module_state() == 'locked': self.counts_data += data.tolist() @@ -255,8 +235,7 @@ def _new_wavemeter_data(self, timestamp, data: np.float64): most_recent_wavelength = self.wavelength_data[-1] if len(self.wavelength_data) > 0 else data n_new_points = len(self.counts_data) - len(self.wavelength_data) self.wavelength_data += np.linspace(most_recent_wavelength, data, n_new_points).tolist() - - + @QtCore.Slot(float, object) def _new_daq_data(self, timestamp, data: bool): @@ -266,17 +245,90 @@ def _new_daq_data(self, timestamp, data: bool): most_recent_ttl = self.ttl_data[-1] if len(self.ttl_data) > 0 else data n_new_points = len(self.counts_data) - len(self.ttl_data) self.ttl_data += [data] * n_new_points + + + @QtCore.Slot(float, object) + def _new_laser_data(self, timestamp, data: ReaderVal): + with self._thread_lock: + if self.module_state() != 'locked': + return + self.laser_status_data += [data] + + + def clear_data(self): + with self._thread_lock: + self.wavelength_data = [] + self.ttl_data = [] + self.counts_data = [] + self.laser_status_data = [] + + + def _save_data(self) -> None: + ds = TextDataStorage( + root_dir=self.save_dir, # Use the configurable save directory + column_formats='.15e' + ) + + x_array = np.array(self.wavelength_data) + y_array = np.array(self.counts_data[:len(x_array)]) + mask = self.ttl_data + + min_length = min(len(x_array), len(y_array), len(mask)) + x_array = x_array[:min_length] + y_array = y_array[:min_length] + mask = mask[:min_length] + + x_array = x_array[mask] + y_array = y_array[mask] + + array = np.column_stack((x_array, y_array)) + ds.save_data(array) #### Watchdog Timer #### - def __watchdog(self): + def __update(self): with self._thread_lock: + # If the module is locked, then we should be doing a scan if self.module_state() == 'locked': - if self._laser_locked: - self._last_locked = time.time() - elif time.time() - self._last_locked > self._laser_timeout_s: - self.log.info('Laser did not lock in time. Restarting scan.') - new_start = self._current_wavelength*1e-3 - self._last_locked = time.time() - self.sigRestartLaser.emit(new_start) + ### Best case scenarios ### + + if len(self.laser_status_data) > 0: + # 1. If a scan is already in progress, we should leave it alone + if self.laser_status_data[-1] == 1: # Laser is scanning + return + # 2. If the scan has terminated normally, we should stop the scan + # TODO: This limits the scan size to 0.0001 nm. Fix this. and no hard coading wl numbers + if self.laser_status_data[-1] == 0: # Laser is not scanning + if self._forward_direction: + if self._curr_wavelength + 0.0001 >= self.stop_wavelength: + self.stop_scan() + return + else: + if self._curr_wavelength - 0.0001 <= self.stop_wavelength: + self.stop_scan() + return + + # 1e-5 nm is 5 MHz, which is the resolution of the wavemeter + # If a scan is not in progress, we should check if we need to start one + # The easiest way to check is with a timeout + if len(self.wavelength_data) > 0: + if np.abs(self._curr_wavelength - self.wavelength_data[-1]) > 2e-5: + self._curr_wavelength = self.wavelength_data[-1] + self._time_since_last_wl_change = time.time() + # print(f"Current wavelength: {self._curr_wavelength}") + if time.time() - self._time_since_last_wl_change > self._laser_timeout_s: + # The laser has timed out, so we should and restart the scan from the current wavelength + curr_data_length = min(len(self.wavelength_data), len(self.ttl_data), len(self.counts_data)) + mask = self.ttl_data[:curr_data_length] + valid_wavelengths = self.wavelength_data[:curr_data_length] + valid_wavelengths = valid_wavelengths[mask] + last_valid_wavelength = valid_wavelengths[-1] if len(valid_wavelengths) > 0 else self.start_wavelength + + # Restart sequence + self._laser.stop_scan() + time.sleep(1) + self._laser.set_wavelengths(last_valid_wavelength, self.stop_wavelength) + # self._laser.set_scan_type(self.scan_type) + # self._laser.set_scan_rate(self.scan_rate) + self._laser.start_scan() diff --git a/src/qupidc_qudi_modules.egg-info/SOURCES.txt b/src/qupidc_qudi_modules.egg-info/SOURCES.txt index 44df7a9..7f21dee 100644 --- a/src/qupidc_qudi_modules.egg-info/SOURCES.txt +++ b/src/qupidc_qudi_modules.egg-info/SOURCES.txt @@ -3,37 +3,58 @@ LICENSE.LESSER README.md pyproject.toml setup.py +src/qudi/gui/data_analysis/data_analysis_gui.py +src/qudi/gui/grating_scan/grating_scan_gui.py +src/qudi/gui/grating_scan/grating_scan_main_window.py +src/qudi/gui/powermeter/powermeter_gui.py +src/qudi/gui/powermeter/powermeter_main_window.py +src/qudi/gui/swabian/photon_counts_time_average_gui.py +src/qudi/gui/swabian/photon_counts_time_average_main_window.py src/qudi/gui/template/template_gui.py src/qudi/gui/template/template_main_window.py src/qudi/gui/terascan/terascan_gui.py src/qudi/gui/terascan/terascan_main_window.py src/qudi/hardware/camera/andor_camera.py src/qudi/hardware/daq/nidaq.py +src/qudi/hardware/daq/nidaq_ben.py src/qudi/hardware/dummy/camera_dummy.py src/qudi/hardware/dummy/daq_reader_dummy.py src/qudi/hardware/dummy/fast_counter_dummy.py +src/qudi/hardware/dummy/powermeter_dummy.py src/qudi/hardware/dummy/scanning_laser_dummy.py src/qudi/hardware/dummy/wavemeter_dummy.py src/qudi/hardware/laser/solstis_constants.py src/qudi/hardware/laser/solstis_funcs.py src/qudi/hardware/laser/solstis_laser.py +src/qudi/hardware/laser/solstis_laser_ben.py +src/qudi/hardware/powermeter/thorlabs_power_meter.py +src/qudi/hardware/servo/thorlabs_servo.py src/qudi/hardware/timetagger/swabian_tagger.py -src/qudi/hardware/wavemeter/high_finesse_wavemeter.py -src/qudi/hardware/wavemeter/wlmConst.py -src/qudi/hardware/wavemeter/wlmData.py +src/qudi/hardware/timetagger/swabian_tagger_ben.py +src/qudi/hardware/wavemeter/wavemeter.py +src/qudi/hardware/wavemeter_ben/high_finesse_wavemeter.py +src/qudi/hardware/wavemeter_ben/wlmConst.py +src/qudi/hardware/wavemeter_ben/wlmData.py src/qudi/interface/camera_interface.py src/qudi/interface/daq_reader_interface.py src/qudi/interface/fast_counter_interface.py +src/qudi/interface/motor_interface.py src/qudi/interface/scanning_laser_interface.py +src/qudi/interface/simple_powermeter_interface.py src/qudi/interface/simple_wavemeter_interface.py src/qudi/logic/grating_scan_logic.py +src/qudi/logic/powermeter_logic.py src/qudi/logic/template_logic.py src/qudi/logic/terascan_logic.py +src/qudi/logic/terascan_logic_ben.py src/qudi/logic/common/camera_logic.py src/qudi/logic/common/daq_reader_logic.py src/qudi/logic/common/fast_counter_logic.py +src/qudi/logic/common/motor_logic.py +src/qudi/logic/common/powermeter_logic.py src/qudi/logic/common/scanning_laser_logic.py src/qudi/logic/common/wavemeter_logic.py +src/qudi/logic/data_analysis_logic/data_analysis_logic.py src/qupidc_qudi_modules.egg-info/PKG-INFO src/qupidc_qudi_modules.egg-info/SOURCES.txt src/qupidc_qudi_modules.egg-info/dependency_links.txt From b9513849033a9ec0eb1ed0869bb6b46ea7e49eba Mon Sep 17 00:00:00 2001 From: lange50 Date: Mon, 28 Apr 2025 10:44:02 -0400 Subject: [PATCH 04/16] fix import bug --- .../laser/{solstis.py => solstis_laser.py} | 16 ++---- src/qudi/logic/terascan_logic.py | 49 ++++++++----------- 2 files changed, 26 insertions(+), 39 deletions(-) rename src/qudi/hardware/laser/{solstis.py => solstis_laser.py} (97%) diff --git a/src/qudi/hardware/laser/solstis.py b/src/qudi/hardware/laser/solstis_laser.py similarity index 97% rename from src/qudi/hardware/laser/solstis.py rename to src/qudi/hardware/laser/solstis_laser.py index 17b8045..1bc1032 100644 --- a/src/qudi/hardware/laser/solstis.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -18,7 +18,7 @@ from typing import List from PySide2 import QtCore -from time import time +import time from qudi.core.configoption import ConfigOption from qudi.core.statusvariable import StatusVar @@ -56,8 +56,8 @@ class SolstisLaser(Base): # status variables: - _start_wavelength = StatusVar('start_wavelength', default=0.78) - _end_wavelength = StatusVar('end_wavelength', default=0.7801) + _start_wavelength = StatusVar('start_wavelength', default=780) + _end_wavelength = StatusVar('end_wavelength', default=780.1) _scan_type = StatusVar('scan_type', default=TeraScanType.SCAN_TYPE_FINE) _scan_rate = StatusVar('scan_rate', default=TeraScanRate.SCAN_RATE_FINE_LINE_20_GHZ) @@ -65,11 +65,8 @@ class SolstisLaser(Base): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__timer = None - - self._time_of_most_recent_wavelength_update = time.time() self._current_wavelength = -1 - self.timeout = 3.0 # seconds self.statusvar = 0 """ statusvar 0 = idle @@ -92,8 +89,6 @@ def on_activate(self): if (self._scan_type == -1): self._scan_type = self.get_default_scan_type() self._scan_rate = self.get_default_scan_rate() - - self._scan_started = time() def on_deactivate(self): """ Deactivate module. @@ -235,8 +230,8 @@ def start_scan(self) -> bool: try: if self.module_state() == 'idle': solstis.scan_stitch_initialize(self.socket, self._scan_type, - self._start_wavelength*1e3, - self._end_wavelength*1e3, + self._start_wavelength, + self._end_wavelength, self._scan_rate) solstis.terascan_output(self.socket, @@ -246,7 +241,6 @@ def start_scan(self) -> bool: update_step=0, pause=True) solstis.scan_stitch_op(self.socket, self._scan_type, "start") - self._scan_started = time() self.module_state.lock() return True diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index b40a66b..3a67096 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -1,21 +1,18 @@ import numpy as np import time -import datetime -import matplotlib.pyplot as plt from PySide2 import QtCore -from typing import List +from typing import List, Dict from qudi.core.module import LogicBase from qudi.util.mutex import RecursiveMutex -from qudi.util.units import ScaledFloat from qudi.core.connector import Connector from qudi.core.configoption import ConfigOption from qudi.core.statusvariable import StatusVar from qudi.util.datastorage import TextDataStorage -from qudi.interface.daq_reader_interface import InputType, ReaderVal +from qudi.interface.daq_reader_interface import ReaderVal class TerascanLogic(LogicBase): @@ -39,13 +36,13 @@ class TerascanLogic(LogicBase): """ #################### CONNECTORS #################### - _laser = Connector(name='laser', interface='ScanningLaserInterface') - _wavemeter = Connector(name='wavemeter', interface='SimpleWavemeterInterface') - _counter = Connector(name='counter', interface='FastCounterInterface') + _laser = Connector(name='laser', interface='Base') + _wavemeter = Connector(name='wavemeter', interface='Base') + _counter = Connector(name='counter', interface='Base') _daq = Connector(name='daq', interface='Base') #################### CONFIGURATION OPTIONS #################### - save_dir = ConfigOption('save_dir', default='C:\\Users\\hoodl\\qudi\\Data', missing='error') + save_dir = ConfigOption('save_dir', default='C:\\Users\\hoodl\\qudi\\Data', missing='warn') _laser_timeout_s = ConfigOption(name='laser_timeout_s', default=10) #################### STATUS VARIABLES #################### @@ -69,7 +66,7 @@ def __init__(self, *args, **kwargs): All of these lists should eventually be the same length, except for laser_status_data. That is just for bookkeeping purposes. """ - self._time_since_last_wl_change = time.time() + self._time_since_last_wl_change = time.perf_counter() self._curr_wavelength = 0.0 self._forward_direction = True @@ -113,10 +110,6 @@ def on_deactivate(self): self.__timer.stop() self.__timer.timeout.disconnect() self.__timer = None - - @property - def locked(self) -> bool: - return self._laser_locked @property def scan_types(self) -> dict: @@ -153,16 +146,16 @@ def start_scan(self): with self._thread_lock: if self.module_state() == 'idle': self.module_state.lock() - self._laser.set_wavelengths(self.start_wavelength, self.stop_wavelength) - self._laser.set_scan_type(self.scan_type) - self._laser.set_scan_rate(self.scan_rate) - self._laser.start_scan() + self._laser().set_wavelengths(self.start_wavelength, self.stop_wavelength) + self._laser().set_scan_type(self.scan_type) + self._laser().set_scan_rate(self.scan_rate) + self._laser().start_scan() def stop_scan(self): with self._thread_lock: if self.module_state() == 'locked': self.module_state.unlock() - self._laser.stop_scan() + self._laser().stop_scan() def set_start_wavelength(self, start: float): @@ -183,7 +176,7 @@ def set_stop_wavelength(self, stop: float): 'Tried to configure while a scan was running.'\ 'Please wait until it is finished or stop it.') - # TODO: Deprecated + # TODO: Deprecated. Use individual set_start_wavelength and set_stop_wavelength instead. @QtCore.Slot(float, float) def set_wavelengths(self, start: float, stop: float): with self._thread_lock: @@ -235,7 +228,7 @@ def _new_wavemeter_data(self, timestamp, data: np.float64): most_recent_wavelength = self.wavelength_data[-1] if len(self.wavelength_data) > 0 else data n_new_points = len(self.counts_data) - len(self.wavelength_data) self.wavelength_data += np.linspace(most_recent_wavelength, data, n_new_points).tolist() - + @QtCore.Slot(float, object) def _new_daq_data(self, timestamp, data: bool): @@ -315,9 +308,9 @@ def __update(self): if len(self.wavelength_data) > 0: if np.abs(self._curr_wavelength - self.wavelength_data[-1]) > 2e-5: self._curr_wavelength = self.wavelength_data[-1] - self._time_since_last_wl_change = time.time() + self._time_since_last_wl_change = time.perf_counter() # print(f"Current wavelength: {self._curr_wavelength}") - if time.time() - self._time_since_last_wl_change > self._laser_timeout_s: + if time.perf_counter() - self._time_since_last_wl_change > self._laser_timeout_s: # The laser has timed out, so we should and restart the scan from the current wavelength curr_data_length = min(len(self.wavelength_data), len(self.ttl_data), len(self.counts_data)) mask = self.ttl_data[:curr_data_length] @@ -326,9 +319,9 @@ def __update(self): last_valid_wavelength = valid_wavelengths[-1] if len(valid_wavelengths) > 0 else self.start_wavelength # Restart sequence - self._laser.stop_scan() + self._laser().stop_scan() time.sleep(1) - self._laser.set_wavelengths(last_valid_wavelength, self.stop_wavelength) - # self._laser.set_scan_type(self.scan_type) - # self._laser.set_scan_rate(self.scan_rate) - self._laser.start_scan() + self._laser().set_wavelengths(last_valid_wavelength, self.stop_wavelength) + # self._laser().set_scan_type(self.scan_type) + # self._laser().set_scan_rate(self.scan_rate) + self._laser().start_scan() From 95577b7fb4c29912e7f4a35c8ef55a2ee781dea5 Mon Sep 17 00:00:00 2001 From: lange50 Date: Mon, 28 Apr 2025 12:36:56 -0400 Subject: [PATCH 05/16] Add working terascan rough draft --- src/qudi/gui/terascan/terascan_gui.py | 2 +- src/qudi/hardware/laser/solstis_laser.py | 22 ++-- src/qudi/hardware/wavemeter/wavemeter.py | 2 +- src/qudi/logic/terascan_logic.py | 148 ++++++++++++++++------- 4 files changed, 118 insertions(+), 56 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index 9adbdb4..b134795 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -13,7 +13,7 @@ from qudi.core.connector import Connector from qudi.core.configoption import ConfigOption from qudi.core.statusvariable import StatusVar -from qudi.gui.terascan.terascan_main_window import TerascanMainWindow +# from qudi.gui.terascan.terascan_main_window import TerascanMainWindow from qudi.util.paths import get_artwork_dir from qudi.util.colordefs import QudiPalettePale as palette diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index 1bc1032..e130309 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -253,12 +253,13 @@ def stop_scan(self) -> bool: """Stop a running scan""" try: if self.module_state() == 'locked': - solstis.scan_stitch_op(self.socket, self._scan_type, "stop") self.module_state.unlock() + solstis.scan_stitch_op(self.socket, self._scan_type, "stop") return True except solstis.SolstisError as e: - self.log.exception(f'Scan stop failure: {e.message}') + # self.log.exception(f'Scan stop failure: {e.message}') + self.log.warning(f'Scan stop failure: {e.message}') return False def pause_scan(self): @@ -384,15 +385,18 @@ def set_scan_rate(self, scan_rate: int): """ def __status_update(self): try: - status = solstis.scan_stitch_status(self.socket, self._scan_type) - if status['in_progress'] == False: - self.statusvar = 0 - elif status['in_progress'] == True: - self.statusvar = 1 + if self.module_state() == 'locked': + status = solstis.scan_stitch_status(self.socket, self._scan_type) + if status['in_progress'] == False: + self.statusvar = 0 + elif status['in_progress'] == True: + self.statusvar = 1 + else: + self.statusvar = -1 else: - self.statusvar = -1 + self.statusvar = 0 except solstis.SolstisError as e: self.log.exception(f'Failure getting status: {e.message}') self.statusvar = -1 timestamp = time.perf_counter() - self.sigNewData.emit(timestamp, status) \ No newline at end of file + self.sigNewData.emit(timestamp, self.statusvar) \ No newline at end of file diff --git a/src/qudi/hardware/wavemeter/wavemeter.py b/src/qudi/hardware/wavemeter/wavemeter.py index 3853b64..b09231b 100644 --- a/src/qudi/hardware/wavemeter/wavemeter.py +++ b/src/qudi/hardware/wavemeter/wavemeter.py @@ -80,7 +80,7 @@ def stop_reading(self): def get_wavelength(self) -> np.float64: """Returns the wavelength in um""" wavelength = self.wavemeter.get_wavelength(channel=self.channel, error_on_invalid=False, wait=True, timeout=1) - return np.float64(wavelength * 1e6) # convert to um + return np.float64(wavelength * 1e9) # convert to nm def __update_data(self): diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index 3a67096..eed8bbe 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -278,50 +278,108 @@ def _save_data(self) -> None: ds.save_data(array) - #### Watchdog Timer #### def __update(self): with self._thread_lock: - # If the module is locked, then we should be doing a scan - if self.module_state() == 'locked': - ### Best case scenarios ### - - if len(self.laser_status_data) > 0: - # 1. If a scan is already in progress, we should leave it alone - if self.laser_status_data[-1] == 1: # Laser is scanning - return - - # 2. If the scan has terminated normally, we should stop the scan - # TODO: This limits the scan size to 0.0001 nm. Fix this. and no hard coading wl numbers - if self.laser_status_data[-1] == 0: # Laser is not scanning - if self._forward_direction: - if self._curr_wavelength + 0.0001 >= self.stop_wavelength: - self.stop_scan() - return - else: - if self._curr_wavelength - 0.0001 <= self.stop_wavelength: - self.stop_scan() - return - - # 1e-5 nm is 5 MHz, which is the resolution of the wavemeter - # If a scan is not in progress, we should check if we need to start one - # The easiest way to check is with a timeout - if len(self.wavelength_data) > 0: - if np.abs(self._curr_wavelength - self.wavelength_data[-1]) > 2e-5: - self._curr_wavelength = self.wavelength_data[-1] - self._time_since_last_wl_change = time.perf_counter() - # print(f"Current wavelength: {self._curr_wavelength}") - if time.perf_counter() - self._time_since_last_wl_change > self._laser_timeout_s: - # The laser has timed out, so we should and restart the scan from the current wavelength - curr_data_length = min(len(self.wavelength_data), len(self.ttl_data), len(self.counts_data)) - mask = self.ttl_data[:curr_data_length] - valid_wavelengths = self.wavelength_data[:curr_data_length] - valid_wavelengths = valid_wavelengths[mask] - last_valid_wavelength = valid_wavelengths[-1] if len(valid_wavelengths) > 0 else self.start_wavelength - - # Restart sequence - self._laser().stop_scan() - time.sleep(1) - self._laser().set_wavelengths(last_valid_wavelength, self.stop_wavelength) - # self._laser().set_scan_type(self.scan_type) - # self._laser().set_scan_rate(self.scan_rate) - self._laser().start_scan() + if self.module_state() != 'locked': + self.log.debug("Watchdog: module not locked, skipping") + return + + if not self.laser_status_data: + self.log.debug("Watchdog: no laser status yet") + return + + last_status = self.laser_status_data[-1] + self.log.debug(f"Watchdog: last laser status = {last_status}") + + # 1) If laser is actively scanning, nothing to do + if last_status == 1: + self.log.debug("Watchdog: laser is scanning → ok") + return + + # 2) If laser idle, check for normal scan end + if last_status == 0: + if self._check_for_end_of_scan(): + self.log.info("Watchdog: reached end wavelength, stopping scan") + self.stop_scan() + return + else: + self.log.debug("Watchdog: laser idle but not at end yet") + + # 3) Detect stalled motion by wavemeter + if self._update_wavelength_motion(): + self.log.debug(f"Watchdog: motion detected, updated curr_wavelength to {self._curr_wavelength}") + return # reset timer, leave scan running + + # 4) Timeout: no motion for > timeout_s + if time.perf_counter() - self._time_since_last_wl_change > self._laser_timeout_s: + self.log.warning("Watchdog: motion timeout exceeded, restarting scan") + self._restart_from_last_valid() + + # ——— Helpers —————————————————————————————————————————————— + def _check_for_end_of_scan(self) -> bool: + """ + In order for the scan to be considered finished, + 1. There must be a valid wavelength (wl with ttl high) that is close to the start wavelength + 2. The last valid wavelength must be close to the stop wavelength + """ + + # First check + tol = 1e-4 + if not self.wavelength_data: + self.log.debug("Motion check: no wavelength data yet") + return False + n = min(len(self.wavelength_data), len(self.ttl_data), len(self.counts_data)) + mask = self.ttl_data[:n] + valid_wl = np.asarray(self.wavelength_data[:n])[mask] + + if np.any(np.isclose(valid_wl, self.start_wavelength, atol=tol)) and \ + np.any(np.isclose(valid_wl, self.stop_wavelength, atol=tol)): + self.log.debug("Check end-of-scan: found valid wavelength close to start and stop") + return True + else: + return False + + + def _update_wavelength_motion(self) -> bool: + """ + Check if the wavemeter has recorded a new wavelength since last seen. + If so, update curr_wavelength & reset timeout timer. + Returns True if motion detected. + """ + if not self.wavelength_data: + self.log.debug("Motion check: no wavelength data yet") + return False + + latest = self.wavelength_data[-1] + delta = abs(self._curr_wavelength - latest) + threshold = 2e-5 + + if delta > threshold: + self.log.info(f"Motion detected: Δλ={delta:.2e} nm > {threshold:.2e}") + self._curr_wavelength = latest + self._time_since_last_wl_change = time.perf_counter() + return True + + self.log.debug(f"No motion: Δλ={delta:.2e} nm ≤ threshold {threshold:.2e}") + return False + + def _restart_from_last_valid(self): + """ + Find the last TTL‐valid wavelength, stop the scan, and restart + from there to the end_wavelength. + """ + n = min(len(self.wavelength_data), len(self.ttl_data), len(self.counts_data)) + mask = self.ttl_data[:n] + valid = [wl for wl, ok in zip(self.wavelength_data[:n], mask) if ok] + + if valid: + last_wl = valid[-1] + else: + last_wl = self.start_wavelength + self.log.warning("No valid TTL points found → restarting from start_wavelength") + + self.log.info(f"Restarting scan from λ={last_wl} to {self.stop_wavelength}") + self._laser().stop_scan() + time.sleep(1) + self._laser().set_wavelengths(last_wl, self.stop_wavelength) + self._laser().start_scan() From e7676acdc3a78e268452f73e5bb523ff9356f7df Mon Sep 17 00:00:00 2001 From: lange50 Date: Mon, 28 Apr 2025 13:10:29 -0400 Subject: [PATCH 06/16] Add first draft of gui. Scans run but not all buttons work. --- src/qudi/gui/terascan/terascan_gui.py | 48 +++++++++++++++------------ src/qudi/logic/terascan_logic.py | 2 +- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index b134795..35ebf77 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -17,8 +17,6 @@ from qudi.util.paths import get_artwork_dir from qudi.util.colordefs import QudiPalettePale as palette -from qudi.logic.terascan_logic import TerascanData - pg.setConfigOption('useOpenGL', True) # Add this at the top of your file @@ -66,18 +64,18 @@ def on_activate(self) -> None: self._mw.scan_rate.currentIndexChanged.connect(self._scan_rate_changed) #################### CONNECT SIGNALS FROM LOGIC TO GUI #################### - self._terascan_logic().sigWavelengthUpdated.connect( - self._wavelength_changed, QtCore.Qt.QueuedConnection - ) - self._terascan_logic().sigCountsUpdated.connect( - self._receive_data, QtCore.Qt.QueuedConnection - ) - self._terascan_logic().sigScanFinished.connect( - self._scan_finished, QtCore.Qt.QueuedConnection - ) - self._terascan_logic().sigLaserLocked.connect( - self._laser_lock_ui, QtCore.Qt.QueuedConnection - ) + # self._terascan_logic().sigWavelengthUpdated.connect( + # self._wavelength_changed, QtCore.Qt.QueuedConnection + # ) + # self._terascan_logic().sigCountsUpdated.connect( + # self._receive_data, QtCore.Qt.QueuedConnection + # ) + # self._terascan_logic().sigScanFinished.connect( + # self._scan_finished, QtCore.Qt.QueuedConnection + # ) + # self._terascan_logic().sigLaserLocked.connect( + # self._laser_lock_ui, QtCore.Qt.QueuedConnection + # ) ################### CONNECT SIGNALS FROM GUI TO LOGIC #################### self.sigStartMeasurement.connect( @@ -116,10 +114,10 @@ def on_activate(self) -> None: self.show() def on_deactivate(self) -> None: - self._terascan_logic().sigWavelengthUpdated.disconnect(self._wavelength_changed) - self._terascan_logic().sigCountsUpdated.disconnect(self._receive_data) - self._terascan_logic().sigScanFinished.disconnect(self._scan_finished) - self._terascan_logic().sigLaserLocked.disconnect(self._laser_lock_ui) + # self._terascan_logic().sigWavelengthUpdated.disconnect(self._wavelength_changed) + # self._terascan_logic().sigCountsUpdated.disconnect(self._receive_data) + # self._terascan_logic().sigScanFinished.disconnect(self._scan_finished) + # self._terascan_logic().sigLaserLocked.disconnect(self._laser_lock_ui) self._mw.start_wavelength.valueChanged.disconnect() self._mw.stop_wavelength.valueChanged.disconnect() @@ -138,6 +136,11 @@ def on_deactivate(self) -> None: def show(self) -> None: self._mw.show() self._mw.raise_() + + + def _save_data(self) -> None: + """ Save the data to a file """ + self._terascan_logic().save_data() # Handlers from the UI: @QtCore.Slot(float) @@ -152,6 +155,12 @@ def _stop_changed(self, wave: float) -> None: def _start_stop_pressed(self) -> None: if self._mw.start_stop_button.text() == 'Start Measurement': self.sigStartMeasurement.emit() # Tell the logic to start the scan + + # TODO: This should be a signal + self._terascan_logic().clear_data() + start_wl = self._terascan_logic().start_wavelength + stop_wl = self._terascan_logic().stop_wavelength + self._mw.plot_widget.setXRange(start_wl, stop_wl) else: self.sigStopMeasurement.emit() # Tell the logic to stop the scan @@ -234,9 +243,6 @@ def __update(self) -> None: - - - class TerascanMainWindow(QtWidgets.QMainWindow): """ Main window for Terascan measurement """ def __init__(self, *args, **kwargs): diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index eed8bbe..861af4f 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -256,7 +256,7 @@ def clear_data(self): self.laser_status_data = [] - def _save_data(self) -> None: + def save_data(self) -> None: ds = TextDataStorage( root_dir=self.save_dir, # Use the configurable save directory column_formats='.15e' From 4e0a184f2509cba2c0f4e0bba9e0576a6e81c683 Mon Sep 17 00:00:00 2001 From: "Lange, Christian M" Date: Mon, 28 Apr 2025 15:54:59 -0400 Subject: [PATCH 07/16] Added TerascanLogicData to make adding data more efficient Data is now packaged in TerascanLogicData I also added some more signals to connect logic and gui. This code is untested. --- src/qudi/gui/terascan/terascan_gui.py | 70 ++--- .../hardware/timetagger/swabian_tagger_ben.py | 270 ------------------ src/qudi/logic/terascan_logic.py | 150 +++++++--- 3 files changed, 145 insertions(+), 345 deletions(-) delete mode 100644 src/qudi/hardware/timetagger/swabian_tagger_ben.py diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index 35ebf77..2356a03 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -17,6 +17,8 @@ from qudi.util.paths import get_artwork_dir from qudi.util.colordefs import QudiPalettePale as palette +from qudi.hardware.terascan.terascan_logic import TerascanLogicData + pg.setConfigOption('useOpenGL', True) # Add this at the top of your file @@ -30,7 +32,8 @@ class TerascanGui(GuiBase): connect: terascan_logic: terascan_logic """ - # Signals for outgoing control signals to logic + + ####################### SIGNALS FROM GUI TO LOGIC #################### sigStartMeasurement = QtCore.Signal() sigStopMeasurement = QtCore.Signal() sigSetStartWavelength = QtCore.Signal(float) @@ -64,18 +67,10 @@ def on_activate(self) -> None: self._mw.scan_rate.currentIndexChanged.connect(self._scan_rate_changed) #################### CONNECT SIGNALS FROM LOGIC TO GUI #################### - # self._terascan_logic().sigWavelengthUpdated.connect( - # self._wavelength_changed, QtCore.Qt.QueuedConnection - # ) - # self._terascan_logic().sigCountsUpdated.connect( - # self._receive_data, QtCore.Qt.QueuedConnection - # ) - # self._terascan_logic().sigScanFinished.connect( - # self._scan_finished, QtCore.Qt.QueuedConnection - # ) - # self._terascan_logic().sigLaserLocked.connect( - # self._laser_lock_ui, QtCore.Qt.QueuedConnection - # ) + self._terascan_logic().sigNewData.connect(self._update_data) # Update the GUI with new data + self._terascan_logic().sigStartScan.connect(self._scan_started) # Update the GUI when the scan starts + self._terascan_logic().sigStopScan.connect(self._scan_stopped) # Update the GUI when the scan stops + ################### CONNECT SIGNALS FROM GUI TO LOGIC #################### self.sigStartMeasurement.connect( @@ -96,13 +91,17 @@ def on_activate(self) -> None: self.sigSetScanRate.connect( self._terascan_logic().set_scan_rate, QtCore.Qt.QueuedConnection ) + self.sigSaveData.connect( + self._terascan_logic().save_data, QtCore.Qt.QueuedConnection + ) - self._data = [] + ####################### REFERENCE TO TERASCAN DATA #################### + self.data: TerascanLogicData = TerascanLogicData() # Set up update timer for plot updates self.__timer = QtCore.QTimer() self.__timer.setSingleShot(False) - self.__timer.timeout.connect(self._update_plot) + self.__timer.timeout.connect(self.__update_gui) self.__timer.start(250) # Update every 250 ms # TODO: Make this more configurable and maybe more efficient? @@ -114,10 +113,7 @@ def on_activate(self) -> None: self.show() def on_deactivate(self) -> None: - # self._terascan_logic().sigWavelengthUpdated.disconnect(self._wavelength_changed) - # self._terascan_logic().sigCountsUpdated.disconnect(self._receive_data) - # self._terascan_logic().sigScanFinished.disconnect(self._scan_finished) - # self._terascan_logic().sigLaserLocked.disconnect(self._laser_lock_ui) + self._terascan_logic().sigNewData.disconnect(self._update_data) self._mw.start_wavelength.valueChanged.disconnect() self._mw.stop_wavelength.valueChanged.disconnect() @@ -137,10 +133,9 @@ def show(self) -> None: self._mw.show() self._mw.raise_() - def _save_data(self) -> None: - """ Save the data to a file """ - self._terascan_logic().save_data() + """ Tell the logic to save the data """ + self.sigSaveData.emit() # Handlers from the UI: @QtCore.Slot(float) @@ -183,16 +178,17 @@ def _scan_rate_changed(self, _: int): # self._mw._locked_indicator.setPixmap(pix.scaled(16, 16)) # Private internal functions: - def _update_ui(self) -> None: - if self._terascan_logic().is_running(): - self._mw.start_stop_button.setText('Stop Measurement') - self._mw._statusbar.clearMessage() - else: - self._mw.start_stop_button.setText('Start Measurement') - self._mw._statusbar.showMessage('Ready') - - # TODO: Find a good place for this - # self._mw.plot_widget.setXRange(self._start_wavelength, self._stop_wavelength) + def _scan_started(self) -> None: + """ Update the GUI when the scan starts """ + self._mw.start_stop_button.setText('Stop Measurement') + self._mw._statusbar.clearMessage() + self._mw._progress_bar.setValue(0) + + def _scan_stopped(self) -> None: + """ Update the GUI when the scan stops """ + self._mw.start_stop_button.setText('Start Measurement') + self._mw._statusbar.showMessage('Ready') + self._mw._progress_bar.setValue(100) def _update_plot(self): # self._mw.start_stop_button.setText('Start Measurement') @@ -234,8 +230,14 @@ def _update_plot(self): def _update_running_avg_points(self, points: int) -> None: self._running_avg_points = points - - def __update(self) -> None: + @QtCore.Slot() + def _update_data(self, data: TerascanLogicdata) -> None: + """ Update the data from the logic module """ + self.data = data + + + @QtCore.Slot() + def __update_gui(self) -> None: """ Update the GUI with the latest data from the logic module """ # Update the setText button self._update_ui() diff --git a/src/qudi/hardware/timetagger/swabian_tagger_ben.py b/src/qudi/hardware/timetagger/swabian_tagger_ben.py deleted file mode 100644 index 7d842db..0000000 --- a/src/qudi/hardware/timetagger/swabian_tagger_ben.py +++ /dev/null @@ -1,270 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -A hardware module for communicating with the fast counter FPGA. - -Copyright (c) 2021, 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 . -""" - -import numpy as np -import time -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 - - -class SwabianTimeTagger(FastCounterInterface): - """ Hardware class to controls a Time Tagger from Swabian Instruments. - - Example config for copy-paste: - - swabian_timetagger: - module.Class: 'timetagger.swabian_tagger.SwabianTimeTagger' - options: - channels: - photon_counts: 0 - dark_counts: 1 - - """ - - _channel_config: Dict[str, int] = ConfigOption( - name='channels', - default={'counts': 0}, - 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. - """ - - 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(False) - self.__timer.timeout.connect(self.__update_data) - self.__timer.start(0) # call as often as possible - - def get_constraints(self): - """ Retrieve the hardware constrains from the Fast counting device. - - @return dict: dict with keys being the constraint names as string and - items are the definition for the constaints. - - The keys of the returned dictionary are the str name for the constraints - (which are set in this method). - - NO OTHER KEYS SHOULD BE INVENTED! - - If you are not sure about the meaning, look in other hardware files to - get an impression. If still additional constraints are needed, then they - have to be added to all files containing this interface. - - The items of the keys are again dictionaries which have the generic - dictionary form: - {'min': , - 'max': , - 'step': , - 'unit': ''} - - Only the key 'hardware_binwidth_list' differs, since they - contain the list of possible binwidths. - - If the constraints cannot be set in the fast counting hardware then - write just zero to each key of the generic dicts. - Note that there is a difference between float input (0.0) and - integer input (0), because some logic modules might rely on that - distinction. - - ALL THE PRESENT KEYS OF THE CONSTRAINTS DICT MUST BE ASSIGNED! - """ - - constraints = dict() - - # the unit of those entries are seconds per bin. In order to get the - # current binwidth in seonds use the get_binwidth method. - constraints['hardware_binwidth_list'] = [1 / 1000e6] - - # TODO: think maybe about a software_binwidth_list, which will - # postprocess the obtained counts. These bins must be integer - # multiples of the current hardware_binwidth - - return constraints - - def on_deactivate(self): - """ Deactivate the FPGA. - """ - if (self.counter is not None): - if self.module_state() == 'locked': - self.counter.stop() - 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. - @param float record_length_s: Total length of the timetrace/each single - 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 - """ - - 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 - - - @QtCore.Slot() - def start_measure(self): - """ Start the fast counter. """ - self.module_state.lock() - self.counter.clear() - self.counter.startFor(self._bin_width * 1000) # in ps, should be stored as such #TODO - - self.sigScanStarted.emit() - - self.statusvar = 2 - return 0 - - @QtCore.Slot() - def stop_measure(self): - """ Stop the fast counter. """ - if self.module_state() == 'locked': - self.counter.stop() - self.module_state.unlock() - self.statusvar = 1 - return 0 - - def pause_measure(self): - """ Pauses the current measurement. - - Fast counter must be initially in the run state to make it pause. - """ - if self.module_state() == 'locked': - self.counter.stop() - self.statusvar = 3 - return 0 - - def continue_measure(self): - """ Continues the current measurement. - - If fast counter is in pause state, then fast counter will be continued. - """ - if self.module_state() == 'locked': - self.counter.startFor(self._bin_width * 1000) # in ps, should be stored as such #TODO - self.statusvar = 2 - return 0 - - def is_gated(self): - """ Check the gated counting possibility. - - Boolean return value indicates if the fast counter is a gated counter - (TRUE) or not (FALSE). - """ - return True - - def get_data_trace(self, rolling: bool = False) -> np.ndarray: - """ Polls the current timetrace data from the fast counter. - - @return numpy.array: 2 dimensional array of dtype = int64. This counter - is gated the the return array has the following - shape: - returnarray[gate_index, timebin_index] - - The binning, specified by calling configure() in forehand, must be taken - care of in this hardware class. A possible overflow of the histogram - bins must be caught here and taken care of. - """ - - return np.array(self.counter.getData(rolling=rolling), dtype='int64') - - def get_status(self): - """ Receives the current status of the Fast Counter and outputs it as - return value. - - 0 = unconfigured - 1 = idle - 2 = running - 3 = paused - -1 = error state - """ - return self.statusvar - - 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: - if self.module_state() == 'locked': - self.counter.waitUntilFinished(self._bin_width * 1e-6 * 100) # in ms, should never reach this limit, but just in case - self.sigScanFinished.emit( - np.array(np.squeeze(self.get_data_trace())) - ) - - self.counter.startFor(self._bin_width * 1000) # in ps, should be stored as such #TODO diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index 861af4f..cd3180e 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -14,7 +14,56 @@ from qudi.interface.daq_reader_interface import ReaderVal +class TerascanLogicData: + def __init__(self): + """Initialize the TerascanLogicData class with empty data lists. + The valid data lists are equal in length and calculated using ttl_data. + """ + self.counts_data = [] + self.wavelength_data = [] + self.ttl_data = [] + + self.valid_counts_data = [] + self.valid_wavelength_data = [] + + def add_counts_data(self, counts_data: List[float]): + """Add counts data to the TerascanLogicData object. + + Args: + counts_data (list): A list of counts data to be added. + """ + self.counts_data.extend(counts_data) + + def add_wavelength_data(self, wavelength_data: List[float]): + """Add wavelength data to the TerascanLogicData object. + + Args: + wavelength_data (list): A list of wavelength data to be added. + """ + self.wavelength_data.extend(wavelength_data) + + def add_ttl_data(self, ttl_data: List[bool]): + """Add TTL data to the TerascanLogicData object. + + Args: + ttl_data (list): A list of TTL data to be added. + """ + self.ttl_data.extend(ttl_data) + new_common_length = min(len(self.counts_data), len(self.wavelength_data), len(self.ttl_data)) + + # update valid counts + counts_mask = self.ttl_data[len(self.valid_counts_data):new_common_length] + new_counts = np.array(self.counts_data[len(self.valid_counts_data):new_common_length])[counts_mask].tolist() + self.valid_counts_data.extend(new_counts) + + # update valid wavelength + wavelength_mask = self.ttl_data[len(self.valid_wavelength_data):new_common_length] + new_wavelength = np.array(self.wavelength_data[len(self.valid_wavelength_data):new_common_length])[wavelength_mask].tolist() + self.valid_wavelength_data.extend(new_wavelength) + + +# Find a way to append valid data to the lists instead of creating new ones. class TerascanLogic(LogicBase): """ This is the Logic class for Terascan measurements @@ -52,7 +101,9 @@ class TerascanLogic(LogicBase): scan_type = StatusVar('scan_type', default=2) # SCAN_TYPE_FINE #################### SIGNALS FOR OTHER LOGIC MODULES #################### - # TODO: Move appropriate function calls here + sigNewData = QtCore.Signal(float, object) # timestamp, data + sigStartScan = QtCore.Signal() + sigStopScan = QtCore.Signal() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -62,21 +113,26 @@ def __init__(self, *args, **kwargs): ##################### EXPERIMENT PARAMETERS #################### ############## DATA #################### - """ - All of these lists should eventually be the same length, except for laser_status_data. - That is just for bookkeeping purposes. - """ self._time_since_last_wl_change = time.perf_counter() self._curr_wavelength = 0.0 self._forward_direction = True - - self.wavelength_data = [] - self.ttl_data = [] - self.counts_data = [] - self.laser_status_data = [] + """ + All of these lists should eventually be the same length, except for laser_status_data. + That is just for bookkeeping purposes. + """ + self.laser_status_data = [] # List of laser status data (0 = idle, 1 = scanning, 2 = error) + self.data = TerascanLogicData() + # self.laser_status_data = [] + # self.counts_data = [] + # self.wavelength_data = [] + # self.ttl_data = [] + + # self.valid_wavelength_data = [] + # self.valid_counts_data = [] def on_activate(self): + # TODO: Is it a good idea to do this here? Or should I always do self._instrument()? laser = self._laser() counter = self._counter() wavemeter = self._wavemeter() @@ -103,7 +159,8 @@ def on_activate(self): #################### WATCHDOG TIMER #################### self.__timer = QtCore.QTimer() self.__timer.setSingleShot(False) - self.__timer.timeout.connect(self.__update) + self.__timer.timeout.connect(self.__update_scan) + self.__timer.timeout.connect(self.__update_data) self.__timer.start(100) # 100 ms timer def on_deactivate(self): @@ -135,17 +192,12 @@ def get_scan_type(self) -> int: with self._thread_lock: return self.scan_type - def is_running(self) -> bool: - with self._thread_lock: - if self.module_state() == 'locked': - return True - else: - return False - def start_scan(self): with self._thread_lock: if self.module_state() == 'idle': self.module_state.lock() + self.sigScanStarted.emit() + self._laser().set_wavelengths(self.start_wavelength, self.stop_wavelength) self._laser().set_scan_type(self.scan_type) self._laser().set_scan_rate(self.scan_rate) @@ -155,6 +207,7 @@ def stop_scan(self): with self._thread_lock: if self.module_state() == 'locked': self.module_state.unlock() + self.sigScanStopped.emit() self._laser().stop_scan() @@ -214,8 +267,9 @@ def set_scan_type(self, scan_type: int): def _new_counter_data(self, timestamp, data: np.ndarray): with self._thread_lock: if self.module_state() == 'locked': - self.counts_data += data.tolist() - + new_counts_data = data.tolist() + self.data.add_counts_data(new_counts_data) + @QtCore.Slot(float, object) def _new_wavemeter_data(self, timestamp, data: np.float64): @@ -227,7 +281,8 @@ def _new_wavemeter_data(self, timestamp, data: np.float64): most_recent_wavelength = self.wavelength_data[-1] if len(self.wavelength_data) > 0 else data n_new_points = len(self.counts_data) - len(self.wavelength_data) - self.wavelength_data += np.linspace(most_recent_wavelength, data, n_new_points).tolist() + new_wavelength_data = np.linspace(most_recent_wavelength, data, n_new_points).tolist() + self.data.add_wavelength_data(new_wavelength_data) @QtCore.Slot(float, object) @@ -237,7 +292,8 @@ def _new_daq_data(self, timestamp, data: bool): return most_recent_ttl = self.ttl_data[-1] if len(self.ttl_data) > 0 else data n_new_points = len(self.counts_data) - len(self.ttl_data) - self.ttl_data += [data] * n_new_points + new_ttl_data = [data] * n_new_points + self.data.add_ttl_data(new_ttl_data) @QtCore.Slot(float, object) @@ -250,35 +306,47 @@ def _new_laser_data(self, timestamp, data: ReaderVal): def clear_data(self): with self._thread_lock: - self.wavelength_data = [] - self.ttl_data = [] - self.counts_data = [] self.laser_status_data = [] + self.data = TerascanLogicData() + # TODO: Clean this up. I don't like the logic. + @QtCore.Slot() def save_data(self) -> None: ds = TextDataStorage( root_dir=self.save_dir, # Use the configurable save directory column_formats='.15e' - ) - - x_array = np.array(self.wavelength_data) - y_array = np.array(self.counts_data[:len(x_array)]) - mask = self.ttl_data - - min_length = min(len(x_array), len(y_array), len(mask)) - x_array = x_array[:min_length] - y_array = y_array[:min_length] - mask = mask[:min_length] - - x_array = x_array[mask] - y_array = y_array[mask] - - array = np.column_stack((x_array, y_array)) + ) + array = np.column_stack(( + self.data.valid_wavelength_data, + self.data.valid_counts_data, + self.data.ttl_data, + self.data.wavelength_data, + self.data.counts_data, + )) ds.save_data(array) + + + def __update_scan_status(self): + with self._thread_lock: + if self.module_state() == 'locked': + status = 1 + else: + status = 0 + + self.sigScanStatus.emit(status) + + + def __update_data(self): + with self._thread_lock: + if self.module_state() != 'locked': + return + + # Emit the new data signal + self.sigNewData.emit(time.perf_counter(), self.data) - def __update(self): + def __update_scan(self): with self._thread_lock: if self.module_state() != 'locked': self.log.debug("Watchdog: module not locked, skipping") From e0777441ef6df701a4b6d0662ee467c8469bcb5f Mon Sep 17 00:00:00 2001 From: "Lange, Christian M" Date: Mon, 28 Apr 2025 15:58:21 -0400 Subject: [PATCH 08/16] Update terascan_gui.py --- src/qudi/gui/terascan/terascan_gui.py | 41 ++++++--------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index 2356a03..0fd9e09 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -190,30 +190,10 @@ def _scan_stopped(self) -> None: self._mw._statusbar.showMessage('Ready') self._mw._progress_bar.setValue(100) - def _update_plot(self): - # self._mw.start_stop_button.setText('Start Measurement') - # Make a local snapshot of the data - - x_array = np.array(self._terascan_logic().wavelength_data) - y_array = np.array(self._terascan_logic().counts_data[:len(x_array)]) - mask = self._terascan_logic().ttl_data - - min_length = min(len(x_array), len(y_array), len(mask)) - x_array = x_array[:min_length] - y_array = y_array[:min_length] - mask = mask[:min_length] - - x_array = x_array[mask] - y_array = y_array[mask] - - if x_array.shape[0] != y_array.shape[0]: - return # skip this update - if x_array.shape[0] == 0 or y_array.shape[0] == 0: - return - - # downsample - # x_array = x_array[::10] - # y_array = y_array[::10] + @QtCore.Slot() + def __update_gui(self): + x_array = self.data.valid_wavelength_data + y_array = self.data.valid_counts_data # If running average is enabled, apply a rolling average if self._mw.checkbox_running_avg.isChecked(): @@ -223,7 +203,10 @@ def _update_plot(self): y_array = np.convolve(y_array, kernel, mode='same') - self._mw.data_item.setData(x=x_array, y=y_array) + self._mw.data_item.setData( + x=x_array, + y=y_array, + ) @QtCore.Slot(int) @@ -235,14 +218,6 @@ def _update_data(self, data: TerascanLogicdata) -> None: """ Update the data from the logic module """ self.data = data - - @QtCore.Slot() - def __update_gui(self) -> None: - """ Update the GUI with the latest data from the logic module """ - # Update the setText button - self._update_ui() - self._update_plot() - class TerascanMainWindow(QtWidgets.QMainWindow): From d84945e75519cab6b437ccd8ccb8b812f1870de2 Mon Sep 17 00:00:00 2001 From: lange50 Date: Mon, 28 Apr 2025 17:26:44 -0400 Subject: [PATCH 09/16] add working terascan_logic --- src/qudi/gui/terascan/terascan_gui.py | 36 +- src/qudi/logic/terascan_logic copy.py | 499 ++++++++++++++++++++++++++ src/qudi/logic/terascan_logic.py | 206 ++++++----- src/qudi/logic/terascan_logic_ben.py | 312 ---------------- 4 files changed, 641 insertions(+), 412 deletions(-) create mode 100644 src/qudi/logic/terascan_logic copy.py delete mode 100644 src/qudi/logic/terascan_logic_ben.py diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index 0fd9e09..b63e5dc 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -17,8 +17,6 @@ from qudi.util.paths import get_artwork_dir from qudi.util.colordefs import QudiPalettePale as palette -from qudi.hardware.terascan.terascan_logic import TerascanLogicData - pg.setConfigOption('useOpenGL', True) # Add this at the top of your file @@ -68,8 +66,10 @@ def on_activate(self) -> None: #################### CONNECT SIGNALS FROM LOGIC TO GUI #################### self._terascan_logic().sigNewData.connect(self._update_data) # Update the GUI with new data - self._terascan_logic().sigStartScan.connect(self._scan_started) # Update the GUI when the scan starts - self._terascan_logic().sigStopScan.connect(self._scan_stopped) # Update the GUI when the scan stops + self._terascan_logic().sigSetScanType.connect(self._set_scan_type) # Update the GUI with new scan type + self._terascan_logic().sigSetScanRate.connect(self._set_scan_rate) # Update the GUI with new scan rate + self._terascan_logic().sigScanStarted.connect(self._scan_started) # Update the GUI when the scan starts + self._terascan_logic().sigScanStopped.connect(self._scan_stopped) # Update the GUI when the scan stops ################### CONNECT SIGNALS FROM GUI TO LOGIC #################### @@ -96,7 +96,8 @@ def on_activate(self) -> None: ) ####################### REFERENCE TO TERASCAN DATA #################### - self.data: TerascanLogicData = TerascanLogicData() + self.wavelength_data: List[float] = [] # Wavelength data from the logic module + self.counts_data: List[float] = [] # Counts data from the logic module # Set up update timer for plot updates self.__timer = QtCore.QTimer() @@ -157,19 +158,10 @@ def _start_stop_pressed(self) -> None: stop_wl = self._terascan_logic().stop_wavelength self._mw.plot_widget.setXRange(start_wl, stop_wl) else: - self.sigStopMeasurement.emit() # Tell the logic to stop the scan + self.sigStopMeasurement.emit() # Tell the logic to stop the scan + - @QtCore.Slot(int) - def _scan_type_changed(self, _: int): - self.sigSetScanType.emit(self._mw.scan_type.currentData().value) - self._mw.scan_rate.clear() - 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): - if self._mw.scan_rate.currentData() is not None: - self.sigSetScanRate.emit(self._mw.scan_rate.currentData().value) # @QtCore.Slot(bool) # def _laser_lock_ui(self, locked: bool) -> None: @@ -192,8 +184,11 @@ def _scan_stopped(self) -> None: @QtCore.Slot() def __update_gui(self): - x_array = self.data.valid_wavelength_data - y_array = self.data.valid_counts_data + x_array = self.wavelength_data + y_array = self.counts_data + + if len(x_array) == 0 or len(y_array) == 0: + return # If running average is enabled, apply a rolling average if self._mw.checkbox_running_avg.isChecked(): @@ -214,9 +209,10 @@ def _update_running_avg_points(self, points: int) -> None: self._running_avg_points = points @QtCore.Slot() - def _update_data(self, data: TerascanLogicdata) -> None: + def _update_data(self, wavelength_data, counts_data) -> None: """ Update the data from the logic module """ - self.data = data + self.wavelength_data = wavelength_data + self.counts_data = counts_data diff --git a/src/qudi/logic/terascan_logic copy.py b/src/qudi/logic/terascan_logic copy.py new file mode 100644 index 0000000..d663572 --- /dev/null +++ b/src/qudi/logic/terascan_logic copy.py @@ -0,0 +1,499 @@ + +import numpy as np +import time +from PySide2 import QtCore + +from typing import List, Dict + +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 + +from qudi.interface.daq_reader_interface import ReaderVal + + +# Find a way to append valid data to the lists instead of creating new ones. +class TerascanLogic(LogicBase): + """ + This is the Logic class for Terascan measurements + + example config for copy-paste: + + terascan_logic: + module.Class: 'terascan_logic.TerascanLogic' + connect: + 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. + laser_timeout_s: 10 # Time in seconds to wait for the laser to lock before automatically restarting the scan + mode_hop_overlap_med: 0.001 # in nm, from the SolsTiS control panel. This is how far back we go to discard data every time a mode hop occurs + mode_hop_overlap_fine: 0.00025 # "" + """ + + #################### CONNECTORS #################### + _laser = Connector(name='laser', interface='Base') + _wavemeter = Connector(name='wavemeter', interface='Base') + _counter = Connector(name='counter', interface='Base') + _daq = Connector(name='daq', interface='Base') + + #################### CONFIGURATION OPTIONS #################### + save_dir = ConfigOption('save_dir', default='C:\\Users\\hoodl\\qudi\\Data', missing='warn') + _laser_timeout_s = ConfigOption(name='laser_timeout_s', default=10) + + #################### STATUS VARIABLES #################### + start_wavelength = StatusVar('start_wavelength', default=0.785) + stop_wavelength = StatusVar('stop_wavelength', default=0.7851) + scan_rate = StatusVar('scan_rate', default=12) # SCAN_RATE_FINE_LINE_20_GHZ + scan_type = StatusVar('scan_type', default=2) # SCAN_TYPE_FINE + + #################### SIGNALS FOR OTHER LOGIC MODULES #################### + sigNewData = QtCore.Signal(float, object, object) # timestamp, wl, counts + sigScanStarted = QtCore.Signal() + sigScanStopped = QtCore.Signal() + sigSetScanType = QtCore.Signal(int) + sigSetScanRate = QtCore.Signal(int) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__timer = None + self._thread_lock = RecursiveMutex() + + ##################### EXPERIMENT PARAMETERS #################### + + ############## DATA #################### + self._time_since_last_wl_change = time.perf_counter() + self._curr_wavelength = 0.0 + self._forward_direction = True + + """ + All of these lists should eventually be the same length, except for laser_status_data. + That is just for bookkeeping purposes. + """ + self.laser_status_data = [] # List of laser status data (0 = idle, 1 = scanning, 2 = error) + self.counts_data = [] + self.wavelength_data = [] + self.ttl_data = [] + # These all have a common length and are calculated using ttl_data + self.valid_counts_data = [] + self.valid_wavelength_data = [] + + self.set_scan_type(self.scan_type) + self.set_scan_rate(self.scan_rate) + + def on_activate(self): + # TODO: Is it a good idea to do this here? Or should I always do self._instrument()? + laser = self._laser() + counter = self._counter() + wavemeter = self._wavemeter() + daq = self._daq() + + ##################### INITIALIZE DATA INPUT #################### + counter.start_reading() + wavemeter.start_reading() + daq.start_reading() + + #################### OUTPUTS #################### + # self.sigStartScan.connect(laser.start_scan, QtCore.Qt.QueuedConnection) + # self.sigStopScan.connect(laser.stop_scan, QtCore.Qt.QueuedConnection) + # self.sigSetWavelengths.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) + # self.sigSetScanRate.connect(laser.set_scan_rate, QtCore.Qt.QueuedConnection) + # self.sigSetScanType.connect(laser.set_scan_type, QtCore.Qt.QueuedConnection) + + #################### INPUTS #################### + laser.sigNewData.connect(self._new_laser_data) + wavemeter.sigNewData.connect(self._new_wavemeter_data) + counter.sigNewData.connect(self._new_counter_data) + daq.sigNewData.connect(self._new_daq_data) + + #################### WATCHDOG TIMER #################### + self.__timer = QtCore.QTimer() + self.__timer.setSingleShot(False) + self.__timer.timeout.connect(self.__update_scan) + self.__timer.timeout.connect(self.__update_data) + self.__timer.start(100) # 100 ms timer + + def on_deactivate(self): + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + @property + def scan_types(self) -> dict: + return self._laser().get_scan_types + + @property + def scan_rates(self) -> dict: + return self._laser().get_scan_rates + + def get_start_wavelength(self) -> float: + with self._thread_lock: + return self.start_wavelength + + def get_stop_wavelength(self) -> float: + with self._thread_lock: + return self.stop_wavelength + + def get_scan_rate(self) -> int: + with self._thread_lock: + return self.scan_rate + + def get_scan_type(self) -> int: + with self._thread_lock: + return self.scan_type + + def start_scan(self): + with self._thread_lock: + if self.module_state() == 'idle': + self.module_state.lock() + self.sigScanStarted.emit() + + self._laser().set_wavelengths(self.start_wavelength, self.stop_wavelength) + self._laser().set_scan_type(self.scan_type) + self._laser().set_scan_rate(self.scan_rate) + self._laser().start_scan() + + def stop_scan(self): + with self._thread_lock: + if self.module_state() == 'locked': + self.module_state.unlock() + self.sigScanStopped.emit() + self._laser().stop_scan() + + + def set_start_wavelength(self, start: float): + with self._thread_lock: + if self.module_state() == 'idle': + self.start_wavelength = start + else: + self.log.warning( + 'Tried to configure while a scan was running.'\ + 'Please wait until it is finished or stop it.') + + def set_stop_wavelength(self, stop: float): + with self._thread_lock: + if self.module_state() == 'idle': + self.stop_wavelength = stop + else: + self.log.warning( + 'Tried to configure while a scan was running.'\ + 'Please wait until it is finished or stop it.') + + # TODO: Deprecated. Use individual set_start_wavelength and set_stop_wavelength instead. + @QtCore.Slot(float, float) + def set_wavelengths(self, start: float, stop: float): + with self._thread_lock: + if self.module_state() == 'idle': + self.start_wavelength = start + self.stop_wavelength = stop + self._forward_direction = start < stop + else: + self.log.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.sigScanRateChanged.emit(scan_rate) + else: + self.log.warning( + 'Tried to configure while a scan was running.'\ + 'Please wait until it is finished or stop it.') + + @QtCore.Slot(int) + # TODO: Should this also set the solstis scan type? Or is that done in the start_scan method? + def set_scan_type(self, scan_type: int): + with self._thread_lock: + if self.module_state() == 'idle': + self.scan_type = scan_type + self.sigScanTypeChanged.emit(scan_type) + else: + self.log.warning( + 'Tried to configure while a scan was running.'\ + 'Please wait until it is finished or stop it.') + + + @QtCore.Slot(float, np.ndarray) + def _new_counter_data(self, timestamp, data: np.ndarray): + with self._thread_lock: + if self.module_state() == 'locked': + new_counts_data = data.tolist() + self.counts_data.extend(new_counts_data) + + + @QtCore.Slot(float, object) + def _new_wavemeter_data(self, timestamp, data: np.float64): + """Called on every new wavemeter reading when locked.""" + with self._thread_lock: + # only run when we’re in the ‘locked’ state + if self.module_state() != 'locked': + return + + most_recent_wavelength = self.wavelength_data[-1] if len(self.wavelength_data) > 0 else data + n_new_points = len(self.counts_data) - len(self.wavelength_data) + new_wavelength_data = np.linspace(most_recent_wavelength, data, n_new_points).tolist() + self.wavelength_data.extend(new_wavelength_data) + + + @QtCore.Slot(float, object) + def _new_daq_data(self, timestamp, data: bool): + with self._thread_lock: + if self.module_state() != 'locked': + return + + n_new_points = len(self.counts_data) - len(self.ttl_data) + new_ttl_data = [data] * n_new_points + self.ttl_data.extend(new_ttl_data) + + + @QtCore.Slot(float, object) + def _new_laser_data(self, timestamp, data: ReaderVal): + with self._thread_lock: + if self.module_state() != 'locked': + return + self.laser_status_data += [data] + + + def clear_data(self): + with self._thread_lock: + self.laser_status_data = [] + self.wavelength_data = [] + self.counts_data = [] + self.ttl_data = [] + self.valid_counts_data = [] + self.valid_wavelength_data = [] + self._curr_wavelength = 0.0 + self._time_since_last_wl_change = time.perf_counter() + + + # TODO: Clean this up. I don't like the logic. + @QtCore.Slot() + def save_data(self) -> None: + ds = TextDataStorage( + root_dir=self.save_dir, # Use the configurable save directory + column_formats='.15e' + ) + array = np.column_stack(( + self.valid_wavelength_data, + self.valid_counts_data, + self.ttl_data, + self.wavelength_data, + self.counts_data, + )) + ds.save_data(array) + + + def __update_data(self): + with self._thread_lock: + if self.module_state() != 'locked': + return + + # TODO: Make this more efficient. It's not a good idea to recreate the valid data lists every time. + ######################### UPDATE VALID DATA ######################### + common_length = min(len(self.counts_data), len(self.wavelength_data), len(self.ttl_data)) + mask = self.ttl_data[:common_length] + self.valid_counts_data = np.array(self.counts_data[:common_length])[mask].tolist() + self.valid_wavelength_data = np.array(self.wavelength_data[:common_length])[mask].tolist() + + # update valid counts + # counts_mask = self.ttl_data[len(self.valid_counts_data):new_common_length] + # new_counts = np.array(self.counts_data[len(self.valid_counts_data):new_common_length])[counts_mask].tolist() + # self.valid_counts_data.extend(new_counts) + + # # update valid wavelength + # wavelength_mask = self.ttl_data[len(self.valid_wavelength_data):new_common_length] + # new_wavelength = np.array(self.wavelength_data[len(self.valid_wavelength_data):new_common_length])[wavelength_mask].tolist() + # self.valid_wavelength_data.extend(new_wavelength) + + # Emit the new data signal + self.sigNewData.emit(time.perf_counter(), self.valid_wavelength_data, self.valid_counts_data) + + + def __update_scan(self): + with self._thread_lock: + if self.module_state() != 'locked': + self.log.debug("Watchdog: module not locked, skipping") + return + + if not self.laser_status_data: + self.log.debug("Watchdog: no laser status yet") + return + + last_status = self.laser_status_data[-1] + self.log.debug(f"Watchdog: last laser status = {last_status}") + + # 1) If laser is actively scanning, nothing to do + if last_status == 1: + self.log.debug("Watchdog: laser is scanning → ok") + return + + # 2) If laser idle, check for normal scan end + if last_status == 0: + if self._check_for_end_of_scan(): + self.log.info("Watchdog: reached end wavelength, stopping scan") + self.stop_scan() + return + else: + self.log.debug("Watchdog: laser idle but not at end yet") + + # 3) Detect stalled motion by wavemeter + if self._update_wavelength_motion(): + self.log.debug(f"Watchdog: motion detected, updated curr_wavelength to {self._curr_wavelength}") + return # reset timer, leave scan running + + # 4) Timeout: no motion for > timeout_s + if time.perf_counter() - self._time_since_last_wl_change > self._laser_timeout_s: + self.log.warning("Watchdog: motion timeout exceeded, restarting scan") + self._restart_from_last_valid() + + # ——— Helpers —————————————————————————————————————————————— + def _check_for_end_of_scan(self) -> bool: + """ + In order for the scan to be considered finished, + 1. There must be a valid wavelength (wl with ttl high) that is close to the start wavelength + 2. The last valid wavelength must be close to the stop wavelength + """ + + # First check + tol = 1e-4 + if not self.wavelength_data: + self.log.debug("Motion check: no wavelength data yet") + return False + n = min(len(self.wavelength_data), len(self.ttl_data), len(self.counts_data)) + mask = self.ttl_data[:n] + valid_wl = np.asarray(self.wavelength_data[:n])[mask] + + if np.any(np.isclose(valid_wl, self.start_wavelength, atol=tol)) and \ + np.any(np.isclose(valid_wl, self.stop_wavelength, atol=tol)): + self.log.debug("Check end-of-scan: found valid wavelength close to start and stop") + return True + else: + return False + + + def _update_wavelength_motion(self) -> bool: + """ + Check if the wavemeter has recorded a new wavelength since last seen. + If so, update curr_wavelength & reset timeout timer. + Returns True if motion detected. + """ + if not self.wavelength_data: + self.log.debug("Motion check: no wavelength data yet") + return False + + latest = self.wavelength_data[-1] + delta = abs(self._curr_wavelength - latest) + threshold = 2e-5 + + if delta > threshold: + self.log.info(f"Motion detected: Δλ={delta:.2e} nm > {threshold:.2e}") + self._curr_wavelength = latest + self._time_since_last_wl_change = time.perf_counter() + return True + + self.log.debug(f"No motion: Δλ={delta:.2e} nm ≤ threshold {threshold:.2e}") + return False + + def _restart_from_last_valid(self): + """ + Find the last TTL‐valid wavelength, stop the scan, and restart + from there to the end_wavelength. + """ + n = min(len(self.wavelength_data), len(self.ttl_data), len(self.counts_data)) + mask = self.ttl_data[:n] + valid = [wl for wl, ok in zip(self.wavelength_data[:n], mask) if ok] + + if valid: + last_wl = valid[-1] + else: + last_wl = self.start_wavelength + self.log.warning("No valid TTL points found → restarting from start_wavelength") + + self.log.info(f"Restarting scan from λ={last_wl} to {self.stop_wavelength}") + self._laser().stop_scan() + time.sleep(1) + self._laser().set_wavelengths(last_wl, self.stop_wavelength) + self._laser().start_scan() + + + + +class TerascanLogicData: + def __init__(self): + """Initialize the TerascanLogicData class with empty data lists. + + The valid data lists are equal in length and calculated using ttl_data. + """ + self.counts_data = [] + self.wavelength_data = [] + self.ttl_data = [] + + self.valid_counts_data = [] + self.valid_wavelength_data = [] + + def get_counts_data(self) -> List[float]: + """Get the counts data. + + Returns: + list: The counts data. + """ + return self.counts_data + + def get_wavelength_data(self) -> List[float]: + """Get the wavelength data. + + Returns: + list: The wavelength data. + """ + return self.wavelength_data + + def get_ttl_data(self) -> List[bool]: + """Get the TTL data. + + Returns: + list: The TTL data. + """ + return self.ttl_data + + def add_counts_data(self, counts_data: List[float]): + """Add counts data to the TerascanLogicData object. + + Args: + counts_data (list): A list of counts data to be added. + """ + self.counts_data.extend(counts_data) + + def add_wavelength_data(self, wavelength_data: List[float]): + """Add wavelength data to the TerascanLogicData object. + + Args: + wavelength_data (list): A list of wavelength data to be added. + """ + self.wavelength_data.extend(wavelength_data) + + def add_ttl_data(self, ttl_data: List[bool]): + """Add TTL data to the TerascanLogicData object. + + Args: + ttl_data (list): A list of TTL data to be added. + """ + self.ttl_data.extend(ttl_data) + new_common_length = min(len(self.counts_data), len(self.wavelength_data), len(self.ttl_data)) + + # update valid counts + counts_mask = self.ttl_data[len(self.valid_counts_data):new_common_length] + new_counts = np.array(self.counts_data[len(self.valid_counts_data):new_common_length])[counts_mask].tolist() + self.valid_counts_data.extend(new_counts) + + # update valid wavelength + wavelength_mask = self.ttl_data[len(self.valid_wavelength_data):new_common_length] + new_wavelength = np.array(self.wavelength_data[len(self.valid_wavelength_data):new_common_length])[wavelength_mask].tolist() + self.valid_wavelength_data.extend(new_wavelength) \ No newline at end of file diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index cd3180e..d663572 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -14,54 +14,6 @@ from qudi.interface.daq_reader_interface import ReaderVal -class TerascanLogicData: - def __init__(self): - """Initialize the TerascanLogicData class with empty data lists. - - The valid data lists are equal in length and calculated using ttl_data. - """ - self.counts_data = [] - self.wavelength_data = [] - self.ttl_data = [] - - self.valid_counts_data = [] - self.valid_wavelength_data = [] - - def add_counts_data(self, counts_data: List[float]): - """Add counts data to the TerascanLogicData object. - - Args: - counts_data (list): A list of counts data to be added. - """ - self.counts_data.extend(counts_data) - - def add_wavelength_data(self, wavelength_data: List[float]): - """Add wavelength data to the TerascanLogicData object. - - Args: - wavelength_data (list): A list of wavelength data to be added. - """ - self.wavelength_data.extend(wavelength_data) - - def add_ttl_data(self, ttl_data: List[bool]): - """Add TTL data to the TerascanLogicData object. - - Args: - ttl_data (list): A list of TTL data to be added. - """ - self.ttl_data.extend(ttl_data) - new_common_length = min(len(self.counts_data), len(self.wavelength_data), len(self.ttl_data)) - - # update valid counts - counts_mask = self.ttl_data[len(self.valid_counts_data):new_common_length] - new_counts = np.array(self.counts_data[len(self.valid_counts_data):new_common_length])[counts_mask].tolist() - self.valid_counts_data.extend(new_counts) - - # update valid wavelength - wavelength_mask = self.ttl_data[len(self.valid_wavelength_data):new_common_length] - new_wavelength = np.array(self.wavelength_data[len(self.valid_wavelength_data):new_common_length])[wavelength_mask].tolist() - self.valid_wavelength_data.extend(new_wavelength) - # Find a way to append valid data to the lists instead of creating new ones. class TerascanLogic(LogicBase): @@ -101,9 +53,11 @@ class TerascanLogic(LogicBase): scan_type = StatusVar('scan_type', default=2) # SCAN_TYPE_FINE #################### SIGNALS FOR OTHER LOGIC MODULES #################### - sigNewData = QtCore.Signal(float, object) # timestamp, data - sigStartScan = QtCore.Signal() - sigStopScan = QtCore.Signal() + sigNewData = QtCore.Signal(float, object, object) # timestamp, wl, counts + sigScanStarted = QtCore.Signal() + sigScanStopped = QtCore.Signal() + sigSetScanType = QtCore.Signal(int) + sigSetScanRate = QtCore.Signal(int) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -122,14 +76,15 @@ def __init__(self, *args, **kwargs): That is just for bookkeeping purposes. """ self.laser_status_data = [] # List of laser status data (0 = idle, 1 = scanning, 2 = error) - self.data = TerascanLogicData() - # self.laser_status_data = [] - # self.counts_data = [] - # self.wavelength_data = [] - # self.ttl_data = [] - - # self.valid_wavelength_data = [] - # self.valid_counts_data = [] + self.counts_data = [] + self.wavelength_data = [] + self.ttl_data = [] + # These all have a common length and are calculated using ttl_data + self.valid_counts_data = [] + self.valid_wavelength_data = [] + + self.set_scan_type(self.scan_type) + self.set_scan_rate(self.scan_rate) def on_activate(self): # TODO: Is it a good idea to do this here? Or should I always do self._instrument()? @@ -247,16 +202,19 @@ def set_scan_rate(self, scan_rate: int): with self._thread_lock: if self.module_state() == 'idle': self.scan_rate = scan_rate + self.sigScanRateChanged.emit(scan_rate) else: self.log.warning( 'Tried to configure while a scan was running.'\ 'Please wait until it is finished or stop it.') @QtCore.Slot(int) + # TODO: Should this also set the solstis scan type? Or is that done in the start_scan method? def set_scan_type(self, scan_type: int): with self._thread_lock: if self.module_state() == 'idle': self.scan_type = scan_type + self.sigScanTypeChanged.emit(scan_type) else: self.log.warning( 'Tried to configure while a scan was running.'\ @@ -268,7 +226,7 @@ def _new_counter_data(self, timestamp, data: np.ndarray): with self._thread_lock: if self.module_state() == 'locked': new_counts_data = data.tolist() - self.data.add_counts_data(new_counts_data) + self.counts_data.extend(new_counts_data) @QtCore.Slot(float, object) @@ -282,7 +240,7 @@ def _new_wavemeter_data(self, timestamp, data: np.float64): most_recent_wavelength = self.wavelength_data[-1] if len(self.wavelength_data) > 0 else data n_new_points = len(self.counts_data) - len(self.wavelength_data) new_wavelength_data = np.linspace(most_recent_wavelength, data, n_new_points).tolist() - self.data.add_wavelength_data(new_wavelength_data) + self.wavelength_data.extend(new_wavelength_data) @QtCore.Slot(float, object) @@ -290,10 +248,10 @@ def _new_daq_data(self, timestamp, data: bool): with self._thread_lock: if self.module_state() != 'locked': return - most_recent_ttl = self.ttl_data[-1] if len(self.ttl_data) > 0 else data + n_new_points = len(self.counts_data) - len(self.ttl_data) new_ttl_data = [data] * n_new_points - self.data.add_ttl_data(new_ttl_data) + self.ttl_data.extend(new_ttl_data) @QtCore.Slot(float, object) @@ -307,7 +265,13 @@ def _new_laser_data(self, timestamp, data: ReaderVal): def clear_data(self): with self._thread_lock: self.laser_status_data = [] - self.data = TerascanLogicData() + self.wavelength_data = [] + self.counts_data = [] + self.ttl_data = [] + self.valid_counts_data = [] + self.valid_wavelength_data = [] + self._curr_wavelength = 0.0 + self._time_since_last_wl_change = time.perf_counter() # TODO: Clean this up. I don't like the logic. @@ -318,32 +282,39 @@ def save_data(self) -> None: column_formats='.15e' ) array = np.column_stack(( - self.data.valid_wavelength_data, - self.data.valid_counts_data, - self.data.ttl_data, - self.data.wavelength_data, - self.data.counts_data, + self.valid_wavelength_data, + self.valid_counts_data, + self.ttl_data, + self.wavelength_data, + self.counts_data, )) ds.save_data(array) - def __update_scan_status(self): - with self._thread_lock: - if self.module_state() == 'locked': - status = 1 - else: - status = 0 - - self.sigScanStatus.emit(status) - - def __update_data(self): with self._thread_lock: if self.module_state() != 'locked': return + + # TODO: Make this more efficient. It's not a good idea to recreate the valid data lists every time. + ######################### UPDATE VALID DATA ######################### + common_length = min(len(self.counts_data), len(self.wavelength_data), len(self.ttl_data)) + mask = self.ttl_data[:common_length] + self.valid_counts_data = np.array(self.counts_data[:common_length])[mask].tolist() + self.valid_wavelength_data = np.array(self.wavelength_data[:common_length])[mask].tolist() + + # update valid counts + # counts_mask = self.ttl_data[len(self.valid_counts_data):new_common_length] + # new_counts = np.array(self.counts_data[len(self.valid_counts_data):new_common_length])[counts_mask].tolist() + # self.valid_counts_data.extend(new_counts) + + # # update valid wavelength + # wavelength_mask = self.ttl_data[len(self.valid_wavelength_data):new_common_length] + # new_wavelength = np.array(self.wavelength_data[len(self.valid_wavelength_data):new_common_length])[wavelength_mask].tolist() + # self.valid_wavelength_data.extend(new_wavelength) # Emit the new data signal - self.sigNewData.emit(time.perf_counter(), self.data) + self.sigNewData.emit(time.perf_counter(), self.valid_wavelength_data, self.valid_counts_data) def __update_scan(self): @@ -451,3 +422,78 @@ def _restart_from_last_valid(self): time.sleep(1) self._laser().set_wavelengths(last_wl, self.stop_wavelength) self._laser().start_scan() + + + + +class TerascanLogicData: + def __init__(self): + """Initialize the TerascanLogicData class with empty data lists. + + The valid data lists are equal in length and calculated using ttl_data. + """ + self.counts_data = [] + self.wavelength_data = [] + self.ttl_data = [] + + self.valid_counts_data = [] + self.valid_wavelength_data = [] + + def get_counts_data(self) -> List[float]: + """Get the counts data. + + Returns: + list: The counts data. + """ + return self.counts_data + + def get_wavelength_data(self) -> List[float]: + """Get the wavelength data. + + Returns: + list: The wavelength data. + """ + return self.wavelength_data + + def get_ttl_data(self) -> List[bool]: + """Get the TTL data. + + Returns: + list: The TTL data. + """ + return self.ttl_data + + def add_counts_data(self, counts_data: List[float]): + """Add counts data to the TerascanLogicData object. + + Args: + counts_data (list): A list of counts data to be added. + """ + self.counts_data.extend(counts_data) + + def add_wavelength_data(self, wavelength_data: List[float]): + """Add wavelength data to the TerascanLogicData object. + + Args: + wavelength_data (list): A list of wavelength data to be added. + """ + self.wavelength_data.extend(wavelength_data) + + def add_ttl_data(self, ttl_data: List[bool]): + """Add TTL data to the TerascanLogicData object. + + Args: + ttl_data (list): A list of TTL data to be added. + """ + self.ttl_data.extend(ttl_data) + new_common_length = min(len(self.counts_data), len(self.wavelength_data), len(self.ttl_data)) + + # update valid counts + counts_mask = self.ttl_data[len(self.valid_counts_data):new_common_length] + new_counts = np.array(self.counts_data[len(self.valid_counts_data):new_common_length])[counts_mask].tolist() + self.valid_counts_data.extend(new_counts) + + # update valid wavelength + wavelength_mask = self.ttl_data[len(self.valid_wavelength_data):new_common_length] + new_wavelength = np.array(self.wavelength_data[len(self.valid_wavelength_data):new_common_length])[wavelength_mask].tolist() + self.valid_wavelength_data.extend(new_wavelength) \ No newline at end of file diff --git a/src/qudi/logic/terascan_logic_ben.py b/src/qudi/logic/terascan_logic_ben.py deleted file mode 100644 index f496f0e..0000000 --- a/src/qudi/logic/terascan_logic_ben.py +++ /dev/null @@ -1,312 +0,0 @@ - -import numpy as np -import time -import datetime -import matplotlib.pyplot as plt -from PySide2 import QtCore - -from typing import List - -from qudi.core.module import LogicBase -from qudi.util.mutex import RecursiveMutex -from qudi.util.units import ScaledFloat -from qudi.core.connector import Connector -from qudi.core.configoption import ConfigOption -from qudi.core.statusvariable import StatusVar -from qudi.util.datastorage import TextDataStorage - -from qudi.interface.daq_reader_interface import InputType, ReaderVal - -class TerascanData(): - wavelength: float - counts: int - - def __init__(self, wavelength: float, counts: int): - self.wavelength = wavelength - self.counts = counts - -class TerascanLogic(LogicBase): - """ - This is the Logic class for Terascan measurements - - example config for copy-paste: - - terascan_logic: - module.Class: 'terascan_logic.TerascanLogic' - connect: - 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. - laser_timeout_s: 10 # Time in seconds to wait for the laser to lock before automatically restarting the scan - mode_hop_overlap_med: 0.001 # in nm, from the SolsTiS control panel. This is how far back we go to discard data every time a mode hop occurs - mode_hop_overlap_fine: 0.00025 # "" - """ - - # declare connectors - _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', - default=1, - missing='info') - - _laser_timeout_s = ConfigOption(name='laser_timeout_s', default=10) - - _mode_hop_overlap_med = ConfigOption(name='mode_hop_overlap_med', default=0.001) - _mode_hop_overlap_fine = ConfigOption(name='mode_hop_overlap_fine', default=0.00025) - - # status variables: - _start_wavelength = StatusVar('start_wavelength', default=0.785) - _end_wavelength = StatusVar('end_wavelength', default=0.7851) - _current_wavelength = StatusVar('current_wavelength', default=0.785) - - _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 = [] # list of TerascanData - - _last_locked: float = 0 - - # Update signals, e.g. for GUI module - sigWavelengthUpdated = QtCore.Signal(float) - sigCountsUpdated = QtCore.Signal(object) # is a List[TerascanData] - sigLaserLocked = QtCore.Signal(bool) - - # Update signals for other logics - sigConfigureCounter = QtCore.Signal(float, float) - sigSetLaserWavelengths = QtCore.Signal(float, float) - sigSetLaserScanRate = QtCore.Signal(int) - sigSetLaserScanType = QtCore.Signal(int) - sigRestartLaser = QtCore.Signal(float) # used when we need to restart the laser, as everything else is already running - - sigStartScan = QtCore.Signal() - sigStopScan = QtCore.Signal() - - sigStopCounting = QtCore.Signal() - sigStartCounting = QtCore.Signal() - - sigScanFinished = QtCore.Signal() - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__timer = None - self._thread_lock = RecursiveMutex() - - def on_activate(self): - laser = self._laser() - counter = self._counter() - wavemeter = self._wavemeter() - daq = self._daq() - - # Outputs: - self.sigConfigureCounter.connect(counter.configure, QtCore.Qt.QueuedConnection) - self.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.sigRestartLaser.connect(laser.restart_scan, 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_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_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.sigWavelengthUpdated.connect(self._new_wavemeter_data) - counter.sigScanFinished.connect(self._process_counter_data) - daq.sigNewData.connect(self._new_daq_data) - - # Configure Counter: - self._record_length_s = self._record_length_ms * 1e-3 - self._bin_width_s = self._record_length_s - self.sigConfigureCounter.emit(self._bin_width_s, self._record_length_s) - - # Add watchdog timer - self.__timer = QtCore.QTimer() - self.__timer.setSingleShot(False) - self.__timer.timeout.connect(self.__watchdog) - self.__timer.start(500) - - def on_deactivate(self): - self.__timer.stop() - self.__timer.timeout.disconnect() - self.__timer = None - - @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: - if self.module_state() == 'idle': - self.module_state.lock() - self._current_data = [] - self._last_locked = time.time() - self.sigSetLaserWavelengths.emit(self._start_wavelength, self._end_wavelength) - self.sigStopCounting.emit() # In case we are counting from somewhere else - self.sigStartScan.emit() - - @QtCore.Slot() - def stop_scan(self): - with self._thread_lock: - if self.module_state() == 'locked': - self.sigStopScan.emit() - self.module_state.unlock() - - @QtCore.Slot(float, float) - def configure_scan(self, - start: float, stop: float - ): - with self._thread_lock: - if self.module_state() == 'idle': - self._start_wavelength = start - self._end_wavelength = stop - self.sigSetLaserWavelengths.emit(start, stop) - else: - self.log.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.log.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.log.warning( - 'Tried to configure while a scan was running.'\ - 'Please wait until it is finished or stop it.') - - @QtCore.Slot() - def _laser_scan_started(self): - pass - - @QtCore.Slot() - def _laser_scan_finished(self): - with self._thread_lock: - self.sigScanFinished.emit() - self.sigStopCounting.emit() - self.module_state.unlock() - - @QtCore.Slot(np.ndarray) - def _new_wavemeter_data(self, data: np.ndarray): - with self._thread_lock: - if self.module_state() == 'locked' and self._laser_locked: - wave = data[0][0] - self._current_wavelength = wave # ? - self.sigWavelengthUpdated.emit(wave) - - @QtCore.Slot(np.ndarray) - def _process_counter_data(self, data: np.ndarray): - with self._thread_lock: - if self.module_state() == 'locked' and self._laser_locked: - self._current_data.append( - TerascanData( - wavelength=self._current_wavelength, - counts=data, - ) - ) - self.sigCountsUpdated.emit(self._current_data) - - @QtCore.Slot(object) - def _new_daq_data(self, data: List[ReaderVal]): - with self._thread_lock: - if self.module_state() == 'locked': - for i in data: - if i.type is InputType.DIGITAL: - # We assume there is only one digital input for this measurement - if i.val and not self._laser_locked: - self._laser_locked = True - # self.sigStartCounting.emit() - - if not i.val and self._laser_locked: - # We just mode hopped and are now unlocked - self._laser_locked = False - self._remove_mode_hop() - - - self.sigLaserLocked.emit(self._laser_locked) - - - ### Internal Functions ### - def _remove_mode_hop(self): - """ Function to remove previous data when a mode hop occurs. - Assumes we are locked externally. - """ - target = -1 - scan_up = self._start_wavelength < self._end_wavelength - - if self._scan_type == 1: # MEDIUM Scan - target = self._current_wavelength - self._mode_hop_overlap_med \ - if scan_up else self._current_wavelength + self._mode_hop_overlap_med - elif self._scan_type == 2: # FINE Scan - target = self._current_wavelength - self._mode_hop_overlap_fine \ - if scan_up else self._current_wavelength + self._mode_hop_overlap_fine - else: - self.log.warning('Unknown scan type. Not removing data.') - return - - while len(self._current_data) > 0: - if (scan_up and \ - self._current_data[-1].wavelength > target) or \ - (not scan_up and \ - self._current_data[-1].wavelength < target): - # We need to check if we are scanning up or down - self._current_data.pop() - else: - break - - - - #### Watchdog Timer #### - def __watchdog(self): - with self._thread_lock: - if self.module_state() == 'locked': - if self._laser_locked: - self._last_locked = time.time() - elif time.time() - self._last_locked > self._laser_timeout_s: - self.log.info('Laser did not lock in time. Restarting scan.') - new_start = self._current_wavelength*1e-3 - self._last_locked = time.time() - self.sigRestartLaser.emit(new_start) - From 33eb53186b4a6036810e849229f81ce73472de3e Mon Sep 17 00:00:00 2001 From: lange50 Date: Mon, 28 Apr 2025 18:00:50 -0400 Subject: [PATCH 10/16] Add new terscan_gui This is not carefully tested. --- src/qudi/gui/terascan/terascan_gui copy.py | 367 +++++++++++++++++++ src/qudi/gui/terascan/terascan_gui.py | 407 +++++++++++---------- src/qudi/hardware/laser/solstis_laser.py | 37 +- src/qudi/logic/terascan_logic.py | 55 ++- 4 files changed, 666 insertions(+), 200 deletions(-) create mode 100644 src/qudi/gui/terascan/terascan_gui copy.py diff --git a/src/qudi/gui/terascan/terascan_gui copy.py b/src/qudi/gui/terascan/terascan_gui copy.py new file mode 100644 index 0000000..e912a8c --- /dev/null +++ b/src/qudi/gui/terascan/terascan_gui copy.py @@ -0,0 +1,367 @@ +# -*- coding: utf-8 -*- +__all__ = ['TerascanGui'] + +import numpy as np +import os +from PySide2 import QtCore, QtGui, QtWidgets +import pyqtgraph as pg +from typing import List +from time import sleep + +from qudi.util.datastorage import TextDataStorage +from qudi.core.module import GuiBase +from qudi.core.connector import Connector +from qudi.core.configoption import ConfigOption +from qudi.core.statusvariable import StatusVar +# from qudi.gui.terascan.terascan_main_window import TerascanMainWindow +from qudi.util.paths import get_artwork_dir +from qudi.util.colordefs import QudiPalettePale as palette +from enum import Enum + +# TODO: This is a copy of the one in the logic module. We should probably move this to a common place. +class TeraScanType(Enum): + SCAN_TYPE_MEDIUM = 1 + SCAN_TYPE_FINE = 2 + SCAN_TYPE_LINE = 3 + +class TeraScanRate(Enum): + SCAN_RATE_MEDIUM_100_GHZ = 4 + SCAN_RATE_MEDIUM_50_GHZ = 5 + SCAN_RATE_MEDIUM_20_GHZ = 6 + SCAN_RATE_MEDIUM_15_GHZ = 7 + SCAN_RATE_MEDIUM_10_GHZ = 8 + SCAN_RATE_MEDIUM_5_GHZ = 9 + SCAN_RATE_MEDIUM_2_GHZ = 10 + SCAN_RATE_MEDIUM_1_GHZ = 11 + SCAN_RATE_FINE_LINE_20_GHZ = 12 + SCAN_RATE_FINE_LINE_10_GHZ = 13 + SCAN_RATE_FINE_LINE_5_GHZ = 14 + SCAN_RATE_FINE_LINE_2_GHZ = 15 + SCAN_RATE_FINE_LINE_1_GHZ = 16 + SCAN_RATE_FINE_LINE_500_MHZ = 17 + SCAN_RATE_FINE_LINE_200_MHZ = 18 + SCAN_RATE_FINE_LINE_100_MHZ = 19 + SCAN_RATE_FINE_LINE_50_MHZ = 20 + SCAN_RATE_FINE_LINE_20_MHZ = 21 + SCAN_RATE_FINE_LINE_10_MHZ = 22 + SCAN_RATE_FINE_LINE_5_MHZ = 23 + SCAN_RATE_FINE_LINE_2_MHZ = 24 + SCAN_RATE_FINE_LINE_1_MHZ = 25 + SCAN_RATE_LINE_500_KHZ = 26 + SCAN_RATE_LINE_200_KHZ = 27 + SCAN_RATE_LINE_100_KHZ = 28 + SCAN_RATE_LINE_50_KHZ = 29 + +pg.setConfigOption('useOpenGL', True) # Add this at the top of your file + + +# TODO: No status variables. Just grab the ones from the logic module. +class TerascanGui(GuiBase): + """ Terascan Measurement GUI + + example config for copy-paste: + terascan_gui: + module.Class: 'terascan.terascan_gui.TerascanGui' + connect: + terascan_logic: terascan_logic + """ + + ####################### SIGNALS FROM GUI TO LOGIC #################### + sigStartMeasurement = QtCore.Signal() + sigStopMeasurement = QtCore.Signal() + sigSetStartWavelength = QtCore.Signal(float) + sigSetStopWavelength = QtCore.Signal(float) + sigSetScanType = QtCore.Signal(int) + sigSetScanRate = QtCore.Signal(int) + sigSaveData = QtCore.Signal() + + #################### CONNECTOR TO LOGIC #################### + _terascan_logic = Connector(name='terascan_logic', interface='TerascanLogic') + + #################### STATUS VARIABLES #################### + _running_avg_points = StatusVar(name='running_avg_points', default=5) + + # TODO: operate in terascan_logic instead of terascan_logic() + def on_activate(self) -> None: + # Initialize the main window and set wavelength controls: + self._mw = TerascanMainWindow() + + 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 SIGNALS FROM GUI TO GUI ############################ + 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 SIGNALS FROM LOGIC TO GUI #################### + self._terascan_logic().sigNewData.connect(self._update_data) # Update the GUI with new data + self._terascan_logic().sigSetScanType.connect(self._set_scan_type) # Update the GUI with new scan type + self._terascan_logic().sigSetScanRate.connect(self._set_scan_rate) # Update the GUI with new scan rate + self._terascan_logic().sigScanStarted.connect(self._scan_started) # Update the GUI when the scan starts + self._terascan_logic().sigScanStopped.connect(self._scan_stopped) # Update the GUI when the scan stops + + + ################### CONNECT SIGNALS FROM GUI TO LOGIC #################### + self.sigStartMeasurement.connect( + self._terascan_logic().start_scan, QtCore.Qt.QueuedConnection + ) + self.sigStopMeasurement.connect( + self._terascan_logic().stop_scan, QtCore.Qt.QueuedConnection + ) + self.sigSetStartWavelength.connect( + self._terascan_logic().set_start_wavelength, QtCore.Qt.QueuedConnection + ) + self.sigSetStopWavelength.connect( + self._terascan_logic().set_stop_wavelength, QtCore.Qt.QueuedConnection + ) + self.sigSetScanType.connect( + self._terascan_logic().set_scan_type, QtCore.Qt.DirectConnection + ) + self.sigSetScanRate.connect( + self._terascan_logic().set_scan_rate, QtCore.Qt.QueuedConnection + ) + self.sigSaveData.connect( + self._terascan_logic().save_data, QtCore.Qt.QueuedConnection + ) + + ####################### REFERENCE TO TERASCAN DATA #################### + self.wavelength_data: List[float] = [] # Wavelength data from the logic module + self.counts_data: List[float] = [] # Counts data from the logic module + + # Set up update timer for plot updates + self.__timer = QtCore.QTimer() + self.__timer.setSingleShot(False) + self.__timer.timeout.connect(self.__update_gui) + self.__timer.start(250) # Update every 250 ms + # TODO: Make this more configurable and maybe more efficient? + + # Restore running average points from the StatusVar: + self._mw.spin_avg_points.setValue(self._running_avg_points) + # Connect changes of the spin box to update our status variable + self._mw.spin_avg_points.valueChanged.connect(self._update_running_avg_points) + + self.show() + + def on_deactivate(self) -> None: + self._terascan_logic().sigNewData.disconnect(self._update_data) + + self._mw.start_wavelength.valueChanged.disconnect() + self._mw.stop_wavelength.valueChanged.disconnect() + self._mw.start_stop_button.clicked.disconnect() + self.sigStartMeasurement.disconnect() + self.sigStopMeasurement.disconnect() + self.sigSetWavelengths.disconnect() + + self.__timer.stop() + self.__timer.timeout.disconnect() + self.__timer = None + + self._mw.spin_avg_points.valueChanged.disconnect(self._update_running_avg_points) + self._mw.close() + + def show(self) -> None: + self._mw.show() + self._mw.raise_() + + def _save_data(self) -> None: + """ Tell the logic to save the data """ + self.sigSaveData.emit() + + # Handlers from the UI: + @QtCore.Slot(float) + def _start_changed(self, wave: float) -> None: + self.sigSetStartWavelength.emit(wave) + + @QtCore.Slot(float) + def _stop_changed(self, wave: float) -> None: + self.sigSetStopWavelength.emit(wave) + + @QtCore.Slot() + def _start_stop_pressed(self) -> None: + if self._mw.start_stop_button.text() == 'Start Measurement': + self.sigStartMeasurement.emit() # Tell the logic to start the scan + + # TODO: This should be a signal + self._terascan_logic().clear_data() + start_wl = self._terascan_logic().start_wavelength + stop_wl = self._terascan_logic().stop_wavelength + self._mw.plot_widget.setXRange(start_wl, stop_wl) + else: + self.sigStopMeasurement.emit() # Tell the logic to stop the scan + + + + + # @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)) + + # Private internal functions: + def _scan_started(self) -> None: + """ Update the GUI when the scan starts """ + self._mw.start_stop_button.setText('Stop Measurement') + self._mw._statusbar.clearMessage() + self._mw._progress_bar.setValue(0) + + def _scan_stopped(self) -> None: + """ Update the GUI when the scan stops """ + self._mw.start_stop_button.setText('Start Measurement') + self._mw._statusbar.showMessage('Ready') + self._mw._progress_bar.setValue(100) + + @QtCore.Slot() + def __update_gui(self): + x_array = self.wavelength_data + y_array = self.counts_data + + if len(x_array) == 0 or len(y_array) == 0: + return + + # If running average is enabled, apply a rolling average + if self._mw.checkbox_running_avg.isChecked(): + window_size = self._mw.spin_avg_points.value() + if window_size > 1 and window_size <= len(y_array): + kernel = np.ones(window_size) / float(window_size) + y_array = np.convolve(y_array, kernel, mode='same') + + + self._mw.data_item.setData( + x=x_array, + y=y_array, + ) + + + @QtCore.Slot(int) + def _update_running_avg_points(self, points: int) -> None: + self._running_avg_points = points + + @QtCore.Slot() + def _update_data(self, wavelength_data, counts_data) -> None: + """ Update the data from the logic module """ + self.wavelength_data = wavelength_data + self.counts_data = counts_data + + + +class TerascanMainWindow(QtWidgets.QMainWindow): + """ Main window for Terascan measurement """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setWindowTitle('Terascan Measurement') + self.resize(1250, 500) + + # 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._progress_bar = QtWidgets.QProgressBar() + self._progress_bar.setRange(0, 100) + self._progress_bar.setValue(0) + + 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._progress_bar) + + # Initialize widgets for wavelengths, scan, etc. + self.start_wavelength_label = QtWidgets.QLabel('Start 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(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(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.setAntialiasing(False) + 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='Counts') + + self.data_item = pg.PlotDataItem( + pen=pg.mkPen(palette.c1, style=QtCore.Qt.SolidLine), + # downsampling if you want + # downsample=10, # Render 1 out of every 10 points + # downsampleMethod='mean' # Average points for smoother results + ) + self.plot_widget.addItem(self.data_item) + + # Running Average controls + self.checkbox_running_avg = QtWidgets.QCheckBox("Enable Running Average") + self.checkbox_running_avg.setChecked(False) + self.label_avg_points = QtWidgets.QLabel("Points in Rolling Average:") + self.spin_avg_points = QtWidgets.QSpinBox() + self.spin_avg_points.setRange(1, 9999) + self.spin_avg_points.setValue(5) # default value; this will be set from a StatusVar in the GUI + + # The Start Measurement button (we want this at the very bottom) + self.start_stop_button = QtWidgets.QPushButton('Start Measurement') + + # Arrange widgets in layout + layout = QtWidgets.QGridLayout() + layout.addWidget(self.plot_widget, 0, 0, 4, 4) + + 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) + + # Place Running Average controls ABOVE the Start Measurement button: + control_layout.addWidget(self.checkbox_running_avg) + control_layout.addWidget(self.label_avg_points) + control_layout.addWidget(self.spin_avg_points) + + # Add stretch to push the start button to the bottom: + control_layout.addStretch() + control_layout.addWidget(self.start_stop_button) + + layout.addLayout(control_layout, 0, 5, 5, 1) + layout.setColumnStretch(1, 1) + + 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 b63e5dc..caa70ca 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -6,32 +6,65 @@ from PySide2 import QtCore, QtGui, QtWidgets import pyqtgraph as pg from typing import List -from time import sleep -from qudi.util.datastorage import TextDataStorage from qudi.core.module import GuiBase from qudi.core.connector import Connector -from qudi.core.configoption import ConfigOption from qudi.core.statusvariable import StatusVar -# from qudi.gui.terascan.terascan_main_window import TerascanMainWindow from qudi.util.paths import get_artwork_dir from qudi.util.colordefs import QudiPalettePale as palette -pg.setConfigOption('useOpenGL', True) # Add this at the top of your file - - -# TODO: No status variables. Just grab the ones from the logic module. +from enum import Enum + + +# ---------------------------------------------------------------------- +# enums (kept local – same numeric values as in the logic) +# ---------------------------------------------------------------------- +class TeraScanType(Enum): + SCAN_TYPE_MEDIUM = 1 + SCAN_TYPE_FINE = 2 + SCAN_TYPE_LINE = 3 + + +class TeraScanRate(Enum): + SCAN_RATE_MEDIUM_100_GHZ = 4 + SCAN_RATE_MEDIUM_50_GHZ = 5 + SCAN_RATE_MEDIUM_20_GHZ = 6 + SCAN_RATE_MEDIUM_15_GHZ = 7 + SCAN_RATE_MEDIUM_10_GHZ = 8 + SCAN_RATE_MEDIUM_5_GHZ = 9 + SCAN_RATE_MEDIUM_2_GHZ = 10 + SCAN_RATE_MEDIUM_1_GHZ = 11 + SCAN_RATE_FINE_LINE_20_GHZ = 12 + SCAN_RATE_FINE_LINE_10_GHZ = 13 + SCAN_RATE_FINE_LINE_5_GHZ = 14 + SCAN_RATE_FINE_LINE_2_GHZ = 15 + SCAN_RATE_FINE_LINE_1_GHZ = 16 + SCAN_RATE_FINE_LINE_500_MHZ = 17 + SCAN_RATE_FINE_LINE_200_MHZ = 18 + SCAN_RATE_FINE_LINE_100_MHZ = 19 + SCAN_RATE_FINE_LINE_50_MHZ = 20 + SCAN_RATE_FINE_LINE_20_MHZ = 21 + SCAN_RATE_FINE_LINE_10_MHZ = 22 + SCAN_RATE_FINE_LINE_5_MHZ = 23 + SCAN_RATE_FINE_LINE_2_MHZ = 24 + SCAN_RATE_FINE_LINE_1_MHZ = 25 + SCAN_RATE_LINE_500_KHZ = 26 + SCAN_RATE_LINE_200_KHZ = 27 + SCAN_RATE_LINE_100_KHZ = 28 + SCAN_RATE_LINE_50_KHZ = 29 + + +pg.setConfigOption('useOpenGL', True) + + +# ────────────────────────────────────────────────────────────────────── class TerascanGui(GuiBase): - """ Terascan Measurement GUI - - example config for copy-paste: - terascan_gui: - module.Class: 'terascan.terascan_gui.TerascanGui' - connect: - terascan_logic: terascan_logic + """ + GUI module for the Terascan logic. unchanged layout – just patched + so the module loads and stays in sync with the logic. """ - ####################### SIGNALS FROM GUI TO LOGIC #################### + # ▸▸ GUI → logic signals sigStartMeasurement = QtCore.Signal() sigStopMeasurement = QtCore.Signal() sigSetStartWavelength = QtCore.Signal(float) @@ -40,23 +73,28 @@ class TerascanGui(GuiBase): sigSetScanRate = QtCore.Signal(int) sigSaveData = QtCore.Signal() - #################### CONNECTOR TO LOGIC #################### + # connector _terascan_logic = Connector(name='terascan_logic', interface='TerascanLogic') - #################### STATUS VARIABLES #################### + # one GUI preference _running_avg_points = StatusVar(name='running_avg_points', default=5) - # TODO: operate in terascan_logic instead of terascan_logic() - def on_activate(self) -> None: - # Initialize the main window and set wavelength controls: + # ─────────────── Qudi life-cycle ─────────────────────────────── + def on_activate(self): + # create the main window self._mw = TerascanMainWindow() + # ← populate start/stop λ in nm from the logic’s µm values + logic = self._terascan_logic() + self._mw.start_wavelength.setValue(logic.get_start_wavelength()) + self._mw.stop_wavelength.setValue(logic.get_stop_wavelength()) + # populate combo-boxes with entries from the logic 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 SIGNALS FROM GUI TO GUI ############################ + # ▸ GUI widgets → local slots 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) @@ -64,158 +102,147 @@ def on_activate(self) -> None: self._mw.scan_type.currentIndexChanged.connect(self._scan_type_changed) self._mw.scan_rate.currentIndexChanged.connect(self._scan_rate_changed) - #################### CONNECT SIGNALS FROM LOGIC TO GUI #################### - self._terascan_logic().sigNewData.connect(self._update_data) # Update the GUI with new data - self._terascan_logic().sigSetScanType.connect(self._set_scan_type) # Update the GUI with new scan type - self._terascan_logic().sigSetScanRate.connect(self._set_scan_rate) # Update the GUI with new scan rate - self._terascan_logic().sigScanStarted.connect(self._scan_started) # Update the GUI when the scan starts - self._terascan_logic().sigScanStopped.connect(self._scan_stopped) # Update the GUI when the scan stops - - - ################### CONNECT SIGNALS FROM GUI TO LOGIC #################### - self.sigStartMeasurement.connect( - self._terascan_logic().start_scan, QtCore.Qt.QueuedConnection - ) - self.sigStopMeasurement.connect( - self._terascan_logic().stop_scan, QtCore.Qt.QueuedConnection - ) - self.sigSetStartWavelength.connect( - self._terascan_logic().set_start_wavelength, QtCore.Qt.QueuedConnection - ) - self.sigSetStopWavelength.connect( - self._terascan_logic().set_stop_wavelength, QtCore.Qt.QueuedConnection - ) - self.sigSetScanType.connect( - self._terascan_logic().set_scan_type, QtCore.Qt.DirectConnection - ) - self.sigSetScanRate.connect( - self._terascan_logic().set_scan_rate, QtCore.Qt.QueuedConnection - ) - self.sigSaveData.connect( - self._terascan_logic().save_data, QtCore.Qt.QueuedConnection - ) - - ####################### REFERENCE TO TERASCAN DATA #################### - self.wavelength_data: List[float] = [] # Wavelength data from the logic module - self.counts_data: List[float] = [] # Counts data from the logic module - - # Set up update timer for plot updates - self.__timer = QtCore.QTimer() - self.__timer.setSingleShot(False) + # ▸ logic → GUI + logic = self._terascan_logic() + logic.sigNewData.connect(self._update_data) # fixed signature + logic.sigScanTypeChanged.connect(self._set_scan_type) # corrected names + logic.sigScanRateChanged.connect(self._set_scan_rate) + logic.sigScanStarted.connect(self._scan_started) + logic.sigScanStopped.connect(self._scan_stopped) + + # ▸ GUI → logic + self.sigStartMeasurement.connect(logic.start_scan, QtCore.Qt.QueuedConnection) + self.sigStopMeasurement.connect(logic.stop_scan, QtCore.Qt.QueuedConnection) + self.sigSetStartWavelength.connect(logic.set_start_wavelength, QtCore.Qt.QueuedConnection) + self.sigSetStopWavelength.connect(logic.set_stop_wavelength, QtCore.Qt.QueuedConnection) + self.sigSetScanType.connect(logic.set_scan_type, QtCore.Qt.QueuedConnection) + self.sigSetScanRate.connect(logic.set_scan_rate, QtCore.Qt.QueuedConnection) + self.sigSaveData.connect(logic.save_data, QtCore.Qt.QueuedConnection) + + # keep copies of live data + self.wavelength_data: List[float] = [] + self.counts_data: List[float] = [] + + # 250 ms plot refresh + self.__timer = QtCore.QTimer(self) self.__timer.timeout.connect(self.__update_gui) - self.__timer.start(250) # Update every 250 ms - # TODO: Make this more configurable and maybe more efficient? + self.__timer.start(250) - # Restore running average points from the StatusVar: + # restore running-avg preference self._mw.spin_avg_points.setValue(self._running_avg_points) - # Connect changes of the spin box to update our status variable self._mw.spin_avg_points.valueChanged.connect(self._update_running_avg_points) self.show() - def on_deactivate(self) -> None: - self._terascan_logic().sigNewData.disconnect(self._update_data) - - self._mw.start_wavelength.valueChanged.disconnect() - self._mw.stop_wavelength.valueChanged.disconnect() - self._mw.start_stop_button.clicked.disconnect() - self.sigStartMeasurement.disconnect() - self.sigStopMeasurement.disconnect() - self.sigSetWavelengths.disconnect() + def on_deactivate(self): + logic = self._terascan_logic() + logic.sigNewData.disconnect(self._update_data) + logic.sigScanTypeChanged.disconnect(self._set_scan_type) + logic.sigScanRateChanged.disconnect(self._set_scan_rate) self.__timer.stop() self.__timer.timeout.disconnect() - self.__timer = None - self._mw.spin_avg_points.valueChanged.disconnect(self._update_running_avg_points) self._mw.close() - def show(self) -> None: - self._mw.show() - self._mw.raise_() - - def _save_data(self) -> None: - """ Tell the logic to save the data """ - self.sigSaveData.emit() - - # Handlers from the UI: + # ─────────── GUI → logic handler slots ─────────────────────────── @QtCore.Slot(float) - def _start_changed(self, wave: float) -> None: - self.sigSetStartWavelength.emit(wave) + def _start_changed(self, wl): + self.sigSetStartWavelength.emit(wl) @QtCore.Slot(float) - def _stop_changed(self, wave: float) -> None: - self.sigSetStopWavelength.emit(wave) + def _stop_changed(self, wl): + self.sigSetStopWavelength.emit(wl) + + @QtCore.Slot(int) + def _scan_type_changed(self, index): + val = self._mw.scan_type.itemData(index) + if val is not None: + self.sigSetScanType.emit(val.value) + + @QtCore.Slot(int) + def _scan_rate_changed(self, index): + val = self._mw.scan_rate.itemData(index) + if val is not None: + self.sigSetScanRate.emit(val.value) @QtCore.Slot() - def _start_stop_pressed(self) -> None: + def _start_stop_pressed(self): if self._mw.start_stop_button.text() == 'Start Measurement': - self.sigStartMeasurement.emit() # Tell the logic to start the scan - - # TODO: This should be a signal + self.sigStartMeasurement.emit() self._terascan_logic().clear_data() - start_wl = self._terascan_logic().start_wavelength - stop_wl = self._terascan_logic().stop_wavelength - self._mw.plot_widget.setXRange(start_wl, stop_wl) else: - self.sigStopMeasurement.emit() # Tell the logic to stop the scan - - - - - # @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)) + self.sigStopMeasurement.emit() - # Private internal functions: - def _scan_started(self) -> None: - """ Update the GUI when the scan starts """ + # ─────────── logic → GUI slots ─────────────────────────────────── + def _scan_started(self): self._mw.start_stop_button.setText('Stop Measurement') self._mw._statusbar.clearMessage() self._mw._progress_bar.setValue(0) - def _scan_stopped(self) -> None: - """ Update the GUI when the scan stops """ + def _scan_stopped(self): self._mw.start_stop_button.setText('Start Measurement') self._mw._statusbar.showMessage('Ready') self._mw._progress_bar.setValue(100) + @QtCore.Slot(int) + def _set_scan_type(self, scan_type): + idx = self._mw.scan_type.findData(scan_type) + if idx >= 0: + self._mw.scan_type.blockSignals(True) + self._mw.scan_type.setCurrentIndex(idx) + self._mw.scan_type.blockSignals(False) + + @QtCore.Slot(int) + def _set_scan_rate(self, scan_rate): + idx = self._mw.scan_rate.findData(scan_rate) + if idx >= 0: + self._mw.scan_rate.blockSignals(True) + self._mw.scan_rate.setCurrentIndex(idx) + self._mw.scan_rate.blockSignals(False) + + @QtCore.Slot(float, object, object) + def _update_data(self, _timestamp, wavelength_data, counts_data): + """Slot connected to logic.sigNewData""" + self.wavelength_data = list(wavelength_data) + self.counts_data = list(counts_data) + + # ─────────── GUI housekeeping ──────────────────────────────────── @QtCore.Slot() def __update_gui(self): + if not self.wavelength_data: + return + x_array = self.wavelength_data y_array = self.counts_data - - if len(x_array) == 0 or len(y_array) == 0: - return - # If running average is enabled, apply a rolling average if self._mw.checkbox_running_avg.isChecked(): - window_size = self._mw.spin_avg_points.value() - if window_size > 1 and window_size <= len(y_array): - kernel = np.ones(window_size) / float(window_size) - y_array = np.convolve(y_array, kernel, mode='same') - - - self._mw.data_item.setData( - x=x_array, - y=y_array, - ) + window = self._mw.spin_avg_points.value() + if 1 < window <= len(y_array): + y_array = np.convolve(y_array, + np.ones(window) / window, + mode='same') + self._mw.data_item.setData(x=x_array, y=y_array) @QtCore.Slot(int) - def _update_running_avg_points(self, points: int) -> None: - self._running_avg_points = points - - @QtCore.Slot() - def _update_data(self, wavelength_data, counts_data) -> None: - """ Update the data from the logic module """ - self.wavelength_data = wavelength_data - self.counts_data = counts_data + def _update_running_avg_points(self, pts): + self._running_avg_points = pts + # expose the window to Qudi’s tray show-action + def show(self): + self._mw.show() + self._mw.raise_() + def _save_data(self) -> None: + """Forward the File ▸ Save Data action to the logic module.""" + self.sigSaveData.emit() + +# ────────────────────────────────────────────────────────────────────── +# unchanged TerascanMainWindow – kept verbatim +# ( only inside file so import works in-place ) +# ────────────────────────────────────────────────────────────────────── class TerascanMainWindow(QtWidgets.QMainWindow): """ Main window for Terascan measurement """ def __init__(self, *args, **kwargs): @@ -223,7 +250,7 @@ def __init__(self, *args, **kwargs): self.setWindowTitle('Terascan Measurement') self.resize(1250, 500) - # Create menu bar + # menu bar menu_bar = QtWidgets.QMenuBar() menu = menu_bar.addMenu('File') self.action_save_data = QtWidgets.QAction('Save Data') @@ -231,102 +258,88 @@ def __init__(self, *args, **kwargs): 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) + action_close = QtWidgets.QAction('Close') + action_close.setIcon(QtGui.QIcon(os.path.join(get_artwork_dir(), + 'icons', + 'application-exit'))) + action_close.triggered.connect(self.close) + menu.addAction(action_close) self.setMenuBar(menu_bar) - # Create statusbar and indicators + # status-bar self._statusbar = self.statusBar() self._progress_bar = QtWidgets.QProgressBar() self._progress_bar.setRange(0, 100) self._progress_bar.setValue(0) - self._locked_indicator = QtWidgets.QLabel() self._locked_indicator.setPixmap( - QtGui.QPixmap(os.path.join(get_artwork_dir(), 'icons', 'network-disconnect')).scaled(16, 16) + QtGui.QPixmap(os.path.join(get_artwork_dir(), + 'icons', + 'network-disconnect')).scaled(16, 16) ) self._statusbar.addWidget(self._locked_indicator) self._statusbar.addWidget(self._progress_bar) - # Initialize widgets for wavelengths, scan, etc. + # widgets self.start_wavelength_label = QtWidgets.QLabel('Start 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(6) + self.start_wavelength = _spinbox() 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(6) - + self.stop_wavelength = _spinbox() + 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.setAntialiasing(False) - 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='Counts') - self.data_item = pg.PlotDataItem( - pen=pg.mkPen(palette.c1, style=QtCore.Qt.SolidLine), - # downsampling if you want - # downsample=10, # Render 1 out of every 10 points - # downsampleMethod='mean' # Average points for smoother results - ) + pen=pg.mkPen(palette.c1, style=QtCore.Qt.SolidLine)) self.plot_widget.addItem(self.data_item) - # Running Average controls + # running average controls self.checkbox_running_avg = QtWidgets.QCheckBox("Enable Running Average") - self.checkbox_running_avg.setChecked(False) self.label_avg_points = QtWidgets.QLabel("Points in Rolling Average:") self.spin_avg_points = QtWidgets.QSpinBox() self.spin_avg_points.setRange(1, 9999) - self.spin_avg_points.setValue(5) # default value; this will be set from a StatusVar in the GUI + self.spin_avg_points.setValue(5) - # The Start Measurement button (we want this at the very bottom) self.start_stop_button = QtWidgets.QPushButton('Start Measurement') - - # Arrange widgets in layout + + # layout layout = QtWidgets.QGridLayout() layout.addWidget(self.plot_widget, 0, 0, 4, 4) - - 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) - - # Place Running Average controls ABOVE the Start Measurement button: - control_layout.addWidget(self.checkbox_running_avg) - control_layout.addWidget(self.label_avg_points) - control_layout.addWidget(self.spin_avg_points) - - # Add stretch to push the start button to the bottom: - control_layout.addStretch() - control_layout.addWidget(self.start_stop_button) - - layout.addLayout(control_layout, 0, 5, 5, 1) + + controls = QtWidgets.QVBoxLayout() + for lab, wid in ( + (self.scan_type_label, self.scan_type), + (self.scan_rate_label, self.scan_rate), + (self.start_wavelength_label, self.start_wavelength), + (self.stop_wavelength_label, self.stop_wavelength), + ): + controls.addWidget(lab, 0, QtCore.Qt.AlignBottom) + controls.addWidget(wid, 0, QtCore.Qt.AlignTop) + + controls.addWidget(self.checkbox_running_avg) + controls.addWidget(self.label_avg_points) + controls.addWidget(self.spin_avg_points) + controls.addStretch() + controls.addWidget(self.start_stop_button) + + layout.addLayout(controls, 0, 5, 5, 1) layout.setColumnStretch(1, 1) - - central_widget = QtWidgets.QWidget() - central_widget.setLayout(layout) - self.setCentralWidget(central_widget) + central = QtWidgets.QWidget() + central.setLayout(layout) + self.setCentralWidget(central) + + +def _spinbox(): + sb = QtWidgets.QDoubleSpinBox() + sb.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + sb.setAlignment(QtCore.Qt.AlignHCenter) + sb.setRange(700, 1000) + sb.setDecimals(6) + return sb diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index e130309..1777805 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -19,6 +19,7 @@ from PySide2 import QtCore import time +from enum import Enum from qudi.core.configoption import ConfigOption from qudi.core.statusvariable import StatusVar @@ -399,4 +400,38 @@ def __status_update(self): self.log.exception(f'Failure getting status: {e.message}') self.statusvar = -1 timestamp = time.perf_counter() - self.sigNewData.emit(timestamp, self.statusvar) \ No newline at end of file + self.sigNewData.emit(timestamp, self.statusvar) + + + class TeraScanType(Enum): + SCAN_TYPE_MEDIUM = 1 + SCAN_TYPE_FINE = 2 + SCAN_TYPE_LINE = 3 + + class TeraScanRate(Enum): + SCAN_RATE_MEDIUM_100_GHZ = 4 + SCAN_RATE_MEDIUM_50_GHZ = 5 + SCAN_RATE_MEDIUM_20_GHZ = 6 + SCAN_RATE_MEDIUM_15_GHZ = 7 + SCAN_RATE_MEDIUM_10_GHZ = 8 + SCAN_RATE_MEDIUM_5_GHZ = 9 + SCAN_RATE_MEDIUM_2_GHZ = 10 + SCAN_RATE_MEDIUM_1_GHZ = 11 + SCAN_RATE_FINE_LINE_20_GHZ = 12 + SCAN_RATE_FINE_LINE_10_GHZ = 13 + SCAN_RATE_FINE_LINE_5_GHZ = 14 + SCAN_RATE_FINE_LINE_2_GHZ = 15 + SCAN_RATE_FINE_LINE_1_GHZ = 16 + SCAN_RATE_FINE_LINE_500_MHZ = 17 + SCAN_RATE_FINE_LINE_200_MHZ = 18 + SCAN_RATE_FINE_LINE_100_MHZ = 19 + SCAN_RATE_FINE_LINE_50_MHZ = 20 + SCAN_RATE_FINE_LINE_20_MHZ = 21 + SCAN_RATE_FINE_LINE_10_MHZ = 22 + SCAN_RATE_FINE_LINE_5_MHZ = 23 + SCAN_RATE_FINE_LINE_2_MHZ = 24 + SCAN_RATE_FINE_LINE_1_MHZ = 25 + SCAN_RATE_LINE_500_KHZ = 26 + SCAN_RATE_LINE_200_KHZ = 27 + SCAN_RATE_LINE_100_KHZ = 28 + SCAN_RATE_LINE_50_KHZ = 29 \ No newline at end of file diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index d663572..b2cb5e8 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -14,6 +14,42 @@ from qudi.interface.daq_reader_interface import ReaderVal +from enum import Enum + +# TODO: This is a copy of the one in the logic module. We should probably move this to a common place. +class TeraScanType(Enum): + SCAN_TYPE_MEDIUM = 1 + SCAN_TYPE_FINE = 2 + SCAN_TYPE_LINE = 3 + +class TeraScanRate(Enum): + SCAN_RATE_MEDIUM_100_GHZ = 4 + SCAN_RATE_MEDIUM_50_GHZ = 5 + SCAN_RATE_MEDIUM_20_GHZ = 6 + SCAN_RATE_MEDIUM_15_GHZ = 7 + SCAN_RATE_MEDIUM_10_GHZ = 8 + SCAN_RATE_MEDIUM_5_GHZ = 9 + SCAN_RATE_MEDIUM_2_GHZ = 10 + SCAN_RATE_MEDIUM_1_GHZ = 11 + SCAN_RATE_FINE_LINE_20_GHZ = 12 + SCAN_RATE_FINE_LINE_10_GHZ = 13 + SCAN_RATE_FINE_LINE_5_GHZ = 14 + SCAN_RATE_FINE_LINE_2_GHZ = 15 + SCAN_RATE_FINE_LINE_1_GHZ = 16 + SCAN_RATE_FINE_LINE_500_MHZ = 17 + SCAN_RATE_FINE_LINE_200_MHZ = 18 + SCAN_RATE_FINE_LINE_100_MHZ = 19 + SCAN_RATE_FINE_LINE_50_MHZ = 20 + SCAN_RATE_FINE_LINE_20_MHZ = 21 + SCAN_RATE_FINE_LINE_10_MHZ = 22 + SCAN_RATE_FINE_LINE_5_MHZ = 23 + SCAN_RATE_FINE_LINE_2_MHZ = 24 + SCAN_RATE_FINE_LINE_1_MHZ = 25 + SCAN_RATE_LINE_500_KHZ = 26 + SCAN_RATE_LINE_200_KHZ = 27 + SCAN_RATE_LINE_100_KHZ = 28 + SCAN_RATE_LINE_50_KHZ = 29 + # Find a way to append valid data to the lists instead of creating new ones. class TerascanLogic(LogicBase): @@ -56,8 +92,11 @@ class TerascanLogic(LogicBase): sigNewData = QtCore.Signal(float, object, object) # timestamp, wl, counts sigScanStarted = QtCore.Signal() sigScanStopped = QtCore.Signal() - sigSetScanType = QtCore.Signal(int) - sigSetScanRate = QtCore.Signal(int) + sigScanRateChanged = QtCore.Signal(int) + sigScanTypeChanged = QtCore.Signal(int) + sigStartWavelengthChanged = QtCore.Signal(float) + sigStopWavelengthChanged = QtCore.Signal(float) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -170,6 +209,7 @@ def set_start_wavelength(self, start: float): with self._thread_lock: if self.module_state() == 'idle': self.start_wavelength = start + self.sigStartWavelengthChanged.emit(start) else: self.log.warning( 'Tried to configure while a scan was running.'\ @@ -179,6 +219,7 @@ def set_stop_wavelength(self, stop: float): with self._thread_lock: if self.module_state() == 'idle': self.stop_wavelength = stop + self.sigStopWavelengthChanged.emit(stop) else: self.log.warning( 'Tried to configure while a scan was running.'\ @@ -201,8 +242,18 @@ def set_wavelengths(self, start: float, stop: float): def set_scan_rate(self, scan_rate: int): with self._thread_lock: if self.module_state() == 'idle': + # auto-choose type from the enum name + name = TeraScanRate(scan_rate).name + if name.startswith('SCAN_RATE_MEDIUM'): + self.scan_type = TeraScanType.SCAN_TYPE_MEDIUM.value + elif name.startswith('SCAN_RATE_LINE'): + self.scan_type = TeraScanType.SCAN_TYPE_LINE.value + else: + self.scan_type = TeraScanType.SCAN_TYPE_FINE.value self.scan_rate = scan_rate + self.sigScanTypeChanged.emit(self.scan_type) self.sigScanRateChanged.emit(scan_rate) + else: self.log.warning( 'Tried to configure while a scan was running.'\ From bc216ceffb69f0d21ecf0cd6eae0a41a745e226f Mon Sep 17 00:00:00 2001 From: lange50 Date: Mon, 28 Apr 2025 18:03:52 -0400 Subject: [PATCH 11/16] Fix the units of the gui wavelength boxes --- src/qudi/gui/terascan/terascan_gui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index caa70ca..61bf4f7 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -84,6 +84,7 @@ def on_activate(self): # create the main window self._mw = TerascanMainWindow() # ← populate start/stop λ in nm from the logic’s µm values + # TODO: Should I do this with a signal from the logic instead? logic = self._terascan_logic() self._mw.start_wavelength.setValue(logic.get_start_wavelength()) self._mw.stop_wavelength.setValue(logic.get_stop_wavelength()) @@ -281,10 +282,10 @@ def __init__(self, *args, **kwargs): self._statusbar.addWidget(self._progress_bar) # widgets - self.start_wavelength_label = QtWidgets.QLabel('Start Wavelength (um)') + self.start_wavelength_label = QtWidgets.QLabel('Start Wavelength (nm)') self.start_wavelength = _spinbox() - self.stop_wavelength_label = QtWidgets.QLabel('Stop Wavelength (um)') + self.stop_wavelength_label = QtWidgets.QLabel('Stop Wavelength (nm)') self.stop_wavelength = _spinbox() self.scan_rate_label = QtWidgets.QLabel('Scan Rate') From 80d5c6867ec3683f23bd61eec26169dfeb1f39a7 Mon Sep 17 00:00:00 2001 From: lange50 Date: Mon, 28 Apr 2025 18:09:19 -0400 Subject: [PATCH 12/16] Fix gui default start and stop wavelengths default scan tyep and rate still not working Save data is not working --- src/qudi/gui/terascan/terascan_gui.py | 9 +++++++++ src/qudi/hardware/laser/solstis_laser.py | 10 +++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index 61bf4f7..db77418 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -85,9 +85,13 @@ def on_activate(self): self._mw = TerascanMainWindow() # ← populate start/stop λ in nm from the logic’s µm values # TODO: Should I do this with a signal from the logic instead? + # TODO: Why isn't this working? logic = self._terascan_logic() self._mw.start_wavelength.setValue(logic.get_start_wavelength()) self._mw.stop_wavelength.setValue(logic.get_stop_wavelength()) + # same for rate and teyp + self._mw.scan_rate.setCurrentIndex(logic.get_scan_rate()) + self._mw.scan_type.setCurrentIndex(logic.get_scan_type()) # populate combo-boxes with entries from the logic for txt, scan_type in self._terascan_logic().scan_types.items(): @@ -172,6 +176,11 @@ def _start_stop_pressed(self): if self._mw.start_stop_button.text() == 'Start Measurement': self.sigStartMeasurement.emit() self._terascan_logic().clear_data() + # TODO: This should be a signal + self._terascan_logic().clear_data() + start_wl = self._terascan_logic().start_wavelength + stop_wl = self._terascan_logic().stop_wavelength + self._mw.plot_widget.setXRange(start_wl, stop_wl) else: self.sigStopMeasurement.emit() diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index 1777805..4a2e18c 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -406,7 +406,7 @@ def __status_update(self): class TeraScanType(Enum): SCAN_TYPE_MEDIUM = 1 SCAN_TYPE_FINE = 2 - SCAN_TYPE_LINE = 3 + # SCAN_TYPE_LINE = 3 # We don't have this capability yet class TeraScanRate(Enum): SCAN_RATE_MEDIUM_100_GHZ = 4 @@ -431,7 +431,7 @@ class TeraScanRate(Enum): SCAN_RATE_FINE_LINE_5_MHZ = 23 SCAN_RATE_FINE_LINE_2_MHZ = 24 SCAN_RATE_FINE_LINE_1_MHZ = 25 - SCAN_RATE_LINE_500_KHZ = 26 - SCAN_RATE_LINE_200_KHZ = 27 - SCAN_RATE_LINE_100_KHZ = 28 - SCAN_RATE_LINE_50_KHZ = 29 \ No newline at end of file + # SCAN_RATE_LINE_500_KHZ = 26 + # SCAN_RATE_LINE_200_KHZ = 27 + # SCAN_RATE_LINE_100_KHZ = 28 + # SCAN_RATE_LINE_50_KHZ = 29 \ No newline at end of file From 747b62411d1254ebf6f90c109e6d7fe82d26cf3a Mon Sep 17 00:00:00 2001 From: "Lange, Christian M" Date: Mon, 28 Apr 2025 21:01:41 -0400 Subject: [PATCH 13/16] Update TerascanLogic.save_dage() I added a function written by ChatGPT. I have not tested it yet. --- src/qudi/logic/terascan_logic.py | 49 +++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index b2cb5e8..9a32fa3 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -1,4 +1,6 @@ +import os +import re import numpy as np import time from PySide2 import QtCore @@ -324,22 +326,43 @@ def clear_data(self): self._curr_wavelength = 0.0 self._time_since_last_wl_change = time.perf_counter() - - # TODO: Clean this up. I don't like the logic. + @QtCore.Slot() def save_data(self) -> None: - ds = TextDataStorage( - root_dir=self.save_dir, # Use the configurable save directory - column_formats='.15e' - ) - array = np.column_stack(( + """ + Save self.valid_wavelength_data and self.valid_counts_data to a new + 8-digit, zero-padded .dat file in self.save_dir, e.g. '00000001.dat'. + """ + # 1) Make sure directory exists + os.makedirs(self.save_dir, exist_ok=True) + + # 2) Find existing files of form '########.dat' + existing = [] + for fn in os.listdir(self.save_dir): + if re.fullmatch(r'\d{8}\.dat', fn): + existing.append(int(fn[:8])) + # 3) Determine next index + next_idx = max(existing) + 1 if existing else 1 + filename = f"{next_idx:08d}.dat" + + # 4) Stack wavelength and counts into an N×2 array + data = np.vstack([ self.valid_wavelength_data, - self.valid_counts_data, - self.ttl_data, - self.wavelength_data, - self.counts_data, - )) - ds.save_data(array) + self.valid_counts_data + ]).T # shape (N, 2) + + # 5) Instantiate storage and save + storage = TextDataStorage( + root_dir=self.save_dir, + comments='# ', + delimiter='\t', + file_extension='.dat', + column_formats=('.8f', '.15e'), + include_global_metadata=False + ) + # save_data returns (file_path, timestamp, (rows, columns)) + storage.save_data(data, filename=filename) + def __update_data(self): From c2dd374bb1f5ddab9a0645dfd1fd31b2399579b6 Mon Sep 17 00:00:00 2001 From: lange50 Date: Wed, 30 Apr 2025 09:59:24 -0400 Subject: [PATCH 14/16] Fix photon counts window I changed the photon counts window to work with the new swabian internal logic. I also added a number of counts that updates with the plot. --- .../swabian/photon_counts_time_average_gui.py | 299 ++++++++--- .../photon_counts_time_average_main_window.py | 69 --- src/qudi/gui/terascan/terascan_gui copy.py | 367 ------------- .../hardware/timetagger/swabian_tagger.py | 10 +- src/qudi/logic/terascan_logic copy.py | 499 ------------------ src/qudi/logic/terascan_logic.py | 8 +- 6 files changed, 234 insertions(+), 1018 deletions(-) delete mode 100644 src/qudi/gui/swabian/photon_counts_time_average_main_window.py delete mode 100644 src/qudi/gui/terascan/terascan_gui copy.py delete mode 100644 src/qudi/logic/terascan_logic copy.py diff --git a/src/qudi/gui/swabian/photon_counts_time_average_gui.py b/src/qudi/gui/swabian/photon_counts_time_average_gui.py index 22a68a1..0432500 100644 --- a/src/qudi/gui/swabian/photon_counts_time_average_gui.py +++ b/src/qudi/gui/swabian/photon_counts_time_average_gui.py @@ -1,93 +1,242 @@ # -*- coding: utf-8 -*- +"""Photon Counts Time‑Average GUI for Qudi -__all__ = ['PhotonCountsTimeAverageGui'] +This GUI displays live photon‑count data coming from a *FastCounterInterface* logic +module (e.g. a Swabian Time Tagger). It shows a continuously‑updated plot of the +most recent samples, a large read‑out of the mean count‑rate over a configurable +window, and provides optional on‑the‑fly **running averaging** (smoothing) and +**down‑sampling** to reduce plot load. + +Key features +------------ +* **Start button removed** – acquisition starts automatically on activation. +* **Large numerical display** – configurable window (default 250 ms). +* **Running average** – box‑car smoothing over *N* points (user‑settable). +* **Down‑sample** – decimate by averaging groups of *N* points (user‑settable). +* **All user parameters** exposed either as *ConfigOption* (persisted in cfg) + or *StatusVar* (runtime‑modifiable). + +The file contains two classes: +* :class:`PhotonCountsTimeAverageGui` – the Qudi *GuiBase* module. +* :class:`PhotonCountsTimeAverageMainWindow` – the Qt main‑window widget. +""" -import numpy as np -import os -from PySide2 import QtCore, QtGui -from typing import List -from time import sleep from collections import deque +from typing import Deque, List + +import numpy as np +from PySide2 import QtCore, QtGui, QtWidgets +import pyqtgraph as pg -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.core.configoption import ConfigOption -from qudi.gui.swabian.photon_counts_time_average_main_window import PhotonCountsTimeAverageMainWindow -from qudi.util.paths import get_artwork_dir +from qudi.core.module import GuiBase +from qudi.core.statusvariable import StatusVar +from qudi.util.colordefs import QudiPalettePale as palette + +__all__ = ["PhotonCountsTimeAverageGui"] -from qudi.logic.terascan_logic import TerascanData -# TODO: put the maxlen in the config file class PhotonCountsTimeAverageGui(GuiBase): - """ Photon Counting Time Average GUI - - example config for copy-paste: - photon_counts_time_average_gui: - module.Class: 'swabian.photon_counts_time_average_gui.PhotonCountsTimeAverageGui' - connect: - swabian_timetagger: 'swabian_timetagger' - options: - ring_buffer_length_s: 10 - """ - # One logic module - _ring_buffer_length_s = ConfigOption(name='ring_buffer_length_s', default=10, missing='warn') - _photon_counts_logic = Connector(name='swabian_timetagger', interface='FastCounterInterface') - + """Qudi GUI module that visualises live photon‑count data.""" + + # ----------------------------- configuration --------------------------------- + _ring_buffer_length_s: int = ConfigOption( + name="ring_buffer_length_s", default=10, missing="warn", + ) + _average_display_window_ms: int = ConfigOption( + name="average_display_window_ms", default=1000, missing="warn", + ) + _update_interval_ms: int = ConfigOption( + name="update_interval_ms", default=100, missing="warn", + ) + + # ------------------------------ connectors ----------------------------------- + _counter_logic = Connector(name="timetagger", interface="FastCounterInterface") + + # ----------------------------- status‑vars ----------------------------------- + smoothing_window = StatusVar(default=1) + downsample_factor = StatusVar(default=1) + + # ----------------------------------------------------------------------------- + # Qudi lifecycle + # ----------------------------------------------------------------------------- def on_activate(self) -> None: - self._data = deque(maxlen=1000 * self._ring_buffer_length_s) - - # initialize the main window - self._mw = PhotonCountsTimeAverageMainWindow() - - # Signals from GUI: - self._mw.start_button.clicked.connect(self._photon_counts_logic().start_measure) - - # Connect signals from logic modules - # The first function is a QtCore.Slot - self._photon_counts_logic().sigScanFinished.connect( + """Initialise GUI and begin acquisition.""" + # Circular buffer for the most recent raw samples (1 kHz rate) + self._data: Deque[int] = deque(maxlen=1000 * self._ring_buffer_length_s) + + # Build the main window + self._mw = PhotonCountsTimeAverageMainWindow( + avg_window_ms=self._average_display_window_ms + ) + + # Connect GUI → status vars + self._mw.running_avg_checkbox.toggled.connect(self._toggle_running_avg) + self._mw.running_avg_spin.valueChanged.connect(self._change_smoothing) + self._mw.downsample_checkbox.toggled.connect(self._toggle_downsample) + self._mw.downsample_spin.valueChanged.connect(self._change_downsample) + + # Connect counter logic → GUI + self._counter_logic().sigNewData.connect( self._counts_changed, QtCore.Qt.QueuedConnection ) - - # Turn on update timer: - self.__timer = QtCore.QTimer() - self.__timer.setSingleShot(False) # False means that the timer will repeat - self.__timer.timeout.connect(self.__update_plot) - self.__timer.start(250) # 250 ms - + + # Acquisition starts immediately (no separate *Start* button) + try: + self._counter_logic().start_reading() + except AttributeError: + self.log.warning("Connected logic module has no 'start_reading()' method.") + + # Periodic UI refresh + self._timer = QtCore.QTimer(self) + self._timer.setInterval(self._update_interval_ms) + self._timer.timeout.connect(self._refresh_ui) + self._timer.start() + self.show() - - + + # ------------------------------------------------------------------------- def on_deactivate(self) -> None: - # When you call a connector, you should do it as a function, as shown here. Noone knows why. - # For some reason, when connecting to an external connector, you also need to specify which function you are disconnecting - self._photon_counts_logic().sigScanFinished.disconnect(self._counts_changed) - - self._mw.start_button.clicked.disconnect() - - # disable update timer: - self.__timer.stop() - self.__timer.timeout.disconnect() - self.__timer = None - + """Clean‑up connections and stop timers.""" + self._counter_logic().sigNewData.disconnect(self._counts_changed) + if self._timer is not None: + self._timer.stop() + self._timer.timeout.disconnect() + self._timer = None self._mw.close() - - def show(self) -> None: - """ Mandatory method to show the main window """ + + # ------------------------------------------------------------------------- + def show(self) -> None: # noqa: D401 (Qudi naming convention) + """Show (raise) the Qt main‑window.""" self._mw.show() self._mw.raise_() - - @QtCore.Slot(np.ndarray) - def _counts_changed(self, counts: np.ndarray) -> None: - self._data.append(np.sum(counts)) - - def __update_plot(self) -> None: - if (len(self._data) == 0): - return - - x = range(len(self._data)) - y = list(self._data) - - - self._mw.data_item.setData(x = x, y = y) \ No newline at end of file + + # ------------------------------------------------------------------------- + # Slots + # ------------------------------------------------------------------------- + @QtCore.Slot(float, np.ndarray) + def _counts_changed(self, timestamp: float, counts: np.ndarray) -> None: + """Receive new data burst (1 ms bins) from the logic module.""" + self._data.extend(counts.astype(int)) + + # ------------------------------------------------------------------------- + def _refresh_ui(self) -> None: + """Update numeric display and plot.""" + if not self._data: + return # nothing to show yet + + # ------------------------- numeric display --------------------------- + window_pts = min(len(self._data), self._average_display_window_ms) + recent = list(self._data)[-window_pts:] + cps = np.mean(recent) * 1000.0 # convert to counts/s (Hz) + self._mw.avg_label.setText(f"{cps:,.0f} cps") + + # ----------------------------- plotting ------------------------------ + y = np.fromiter(self._data, dtype=np.int64) # fast + avoids copy + + # Running average (box‑car smoothing) + if self.smoothing_window > 1 and self._mw.running_avg_checkbox.isChecked(): + kernel = np.ones(self.smoothing_window) / self.smoothing_window + y = np.convolve(y, kernel, mode="valid") + x_offset = self.smoothing_window - 1 + else: + x_offset = 0 + + # Down‑sample (decimate by averaging groups) + if self.downsample_factor > 1 and self._mw.downsample_checkbox.isChecked(): + excess = y.size % self.downsample_factor + if excess: + y = y[:-excess] # truncate so that len is divisible by factor + y = y.reshape(-1, self.downsample_factor).mean(axis=1) + x_vals = np.arange(y.size) * self.downsample_factor + x_offset + else: + x_vals = np.arange(y.size) + x_offset + + self._mw.data_item.setData(x=x_vals, y=y) + + # ------------------------------------------------------------------------- + # GUI → Status‑Var handlers + # ------------------------------------------------------------------------- + def _toggle_running_avg(self, checked: bool) -> None: + self._mw.running_avg_spin.setEnabled(checked) + + def _change_smoothing(self, value: int) -> None: + self.smoothing_window = max(1, value) + + def _toggle_downsample(self, checked: bool) -> None: + self._mw.downsample_spin.setEnabled(checked) + + def _change_downsample(self, value: int) -> None: + self.downsample_factor = max(1, value) + + +# ============================================================================== +# Main Window +# ============================================================================== +class PhotonCountsTimeAverageMainWindow(QtWidgets.QMainWindow): + """Qt window that hosts the plot, large count‑rate display and controls.""" + + def __init__(self, *, avg_window_ms: int, parent: QtWidgets.QWidget | None = None): + super().__init__(parent) + self.setWindowTitle("Time‑Averaged Photon Counts") + + # ----------------------------- widgets -------------------------------- + # Large numeric display (readable across the lab) + self.avg_label = QtWidgets.QLabel("0 cps") + big_font = QtGui.QFont() + big_font.setPointSize(48) + big_font.setBold(True) + self.avg_label.setFont(big_font) + self.avg_label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) + + # Live plot + self.plot_widget = pg.PlotWidget() + self.plot_widget.getPlotItem().setContentsMargins(1, 1, 1, 1) + self.plot_widget.setLabel("bottom", "Sample # (ms)") + self.plot_widget.setLabel("left", "Counts / ms") + + self.data_item = pg.PlotDataItem( + pen=pg.mkPen(palette.c1), symbol="o", symbolPen=palette.c1, + symbolBrush=palette.c1, symbolSize=5 + ) + self.plot_widget.addItem(self.data_item) + + # ------------------------ averaging controls ------------------------- + # Running average + self.running_avg_checkbox = QtWidgets.QCheckBox("Running average") + self.running_avg_spin = QtWidgets.QSpinBox() + self.running_avg_spin.setRange(1, 5000) + self.running_avg_spin.setValue(1) + self.running_avg_spin.setEnabled(False) + self.running_avg_checkbox.setToolTip("Smooth data by averaging over N points") + + # Down‑sample + self.downsample_checkbox = QtWidgets.QCheckBox("Down‑sample") + self.downsample_spin = QtWidgets.QSpinBox() + self.downsample_spin.setRange(1, 5000) + self.downsample_spin.setValue(1) + self.downsample_spin.setEnabled(False) + self.downsample_checkbox.setToolTip("Average N points and plot only that average") + + # ----------------------------- layout --------------------------------- + controls_layout = QtWidgets.QGridLayout() + controls_layout.addWidget(self.running_avg_checkbox, 0, 0) + controls_layout.addWidget(self.running_avg_spin, 0, 1) + controls_layout.addWidget(self.downsample_checkbox, 1, 0) + controls_layout.addWidget(self.downsample_spin, 1, 1) + controls_group = QtWidgets.QGroupBox("Data reduction") + controls_group.setLayout(controls_layout) + + central_layout = QtWidgets.QVBoxLayout() + central_layout.addWidget(self.avg_label) + central_layout.addWidget(self.plot_widget, 1) + central_layout.addWidget(controls_group) + + central_widget = QtWidgets.QWidget() + central_widget.setLayout(central_layout) + self.setCentralWidget(central_widget) + + # ---------------------------- misc tweaks ---------------------------- + self.resize(900, 600) + self.show() diff --git a/src/qudi/gui/swabian/photon_counts_time_average_main_window.py b/src/qudi/gui/swabian/photon_counts_time_average_main_window.py deleted file mode 100644 index e5a5b89..0000000 --- a/src/qudi/gui/swabian/photon_counts_time_average_main_window.py +++ /dev/null @@ -1,69 +0,0 @@ -# -*- coding: utf-8 -*- - -__all__ = ['PhotonCountsTimeAverageMainWindow'] - -import os # imported for potential future use (e.g., loading icons or other assets) -from PySide2 import QtGui, QtCore, QtWidgets -import pyqtgraph as pg - -# Although these imports are not used in the current code, they might be needed for extended functionality. -from qudi.util.widgets.plotting.image_widget import ImageWidget -from qudi.util.paths import get_artwork_dir -from qudi.util.colordefs import QudiPalettePale as palette -from qudi.hardware.laser.solstis_constants import * # wildcard import used per Qudi convention - -class PhotonCountsTimeAverageMainWindow(QtWidgets.QMainWindow): - """ - Main window for displaying time-averaged photon counts. - - This window contains a live-updating plot using pyqtgraph that displays photon counts on the y-axis - versus time (in seconds) on the x-axis. The current implementation sets up the plot layout and visual style. - Additional widgets, such as an LCD display for the live number, are provided as commented code for future use. - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Set the window title to reflect its function - self.setWindowTitle('Time Averaged Photon Counts') - - # Create the plot widget using pyqtgraph - self.plot_widget = pg.PlotWidget() - # Set minimal margins for the plot area - self.plot_widget.getPlotItem().setContentsMargins(1, 1, 1, 1) - # Ensure the widget expands with the window - self.plot_widget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) - # Prevent the plot widget from receiving focus - self.plot_widget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - # Set axis labels - self.plot_widget.setLabel('bottom', text='Time', units='s') - self.plot_widget.setLabel('left', text='Counts') - - # Create a PlotDataItem for displaying data points on the plot - 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) - - self.start_button = QtWidgets.QPushButton('Start') - - # Optional: An LCD widget to display the current photon count as a number. - # Uncomment and adjust if a numerical display is required. - # self.lcd = QtWidgets.QLCDNumber() - # self.lcd.setDigitCount(5) - - # Arrange widgets in a grid layout - layout = QtWidgets.QGridLayout() - # Place the plot widget spanning 4 rows and 4 columns - layout.addWidget(self.plot_widget, 0, 0, 4, 4) - layout.addWidget(self.start_button, 4, 5, 1, 1) - # Set column stretch to ensure proper scaling - layout.setColumnStretch(1, 1) - - # Create a central widget, set the layout, and then assign it as the main window's central widget - central_widget = QtWidgets.QWidget() - central_widget.setLayout(layout) - self.setCentralWidget(central_widget) diff --git a/src/qudi/gui/terascan/terascan_gui copy.py b/src/qudi/gui/terascan/terascan_gui copy.py deleted file mode 100644 index e912a8c..0000000 --- a/src/qudi/gui/terascan/terascan_gui copy.py +++ /dev/null @@ -1,367 +0,0 @@ -# -*- coding: utf-8 -*- -__all__ = ['TerascanGui'] - -import numpy as np -import os -from PySide2 import QtCore, QtGui, QtWidgets -import pyqtgraph as pg -from typing import List -from time import sleep - -from qudi.util.datastorage import TextDataStorage -from qudi.core.module import GuiBase -from qudi.core.connector import Connector -from qudi.core.configoption import ConfigOption -from qudi.core.statusvariable import StatusVar -# from qudi.gui.terascan.terascan_main_window import TerascanMainWindow -from qudi.util.paths import get_artwork_dir -from qudi.util.colordefs import QudiPalettePale as palette -from enum import Enum - -# TODO: This is a copy of the one in the logic module. We should probably move this to a common place. -class TeraScanType(Enum): - SCAN_TYPE_MEDIUM = 1 - SCAN_TYPE_FINE = 2 - SCAN_TYPE_LINE = 3 - -class TeraScanRate(Enum): - SCAN_RATE_MEDIUM_100_GHZ = 4 - SCAN_RATE_MEDIUM_50_GHZ = 5 - SCAN_RATE_MEDIUM_20_GHZ = 6 - SCAN_RATE_MEDIUM_15_GHZ = 7 - SCAN_RATE_MEDIUM_10_GHZ = 8 - SCAN_RATE_MEDIUM_5_GHZ = 9 - SCAN_RATE_MEDIUM_2_GHZ = 10 - SCAN_RATE_MEDIUM_1_GHZ = 11 - SCAN_RATE_FINE_LINE_20_GHZ = 12 - SCAN_RATE_FINE_LINE_10_GHZ = 13 - SCAN_RATE_FINE_LINE_5_GHZ = 14 - SCAN_RATE_FINE_LINE_2_GHZ = 15 - SCAN_RATE_FINE_LINE_1_GHZ = 16 - SCAN_RATE_FINE_LINE_500_MHZ = 17 - SCAN_RATE_FINE_LINE_200_MHZ = 18 - SCAN_RATE_FINE_LINE_100_MHZ = 19 - SCAN_RATE_FINE_LINE_50_MHZ = 20 - SCAN_RATE_FINE_LINE_20_MHZ = 21 - SCAN_RATE_FINE_LINE_10_MHZ = 22 - SCAN_RATE_FINE_LINE_5_MHZ = 23 - SCAN_RATE_FINE_LINE_2_MHZ = 24 - SCAN_RATE_FINE_LINE_1_MHZ = 25 - SCAN_RATE_LINE_500_KHZ = 26 - SCAN_RATE_LINE_200_KHZ = 27 - SCAN_RATE_LINE_100_KHZ = 28 - SCAN_RATE_LINE_50_KHZ = 29 - -pg.setConfigOption('useOpenGL', True) # Add this at the top of your file - - -# TODO: No status variables. Just grab the ones from the logic module. -class TerascanGui(GuiBase): - """ Terascan Measurement GUI - - example config for copy-paste: - terascan_gui: - module.Class: 'terascan.terascan_gui.TerascanGui' - connect: - terascan_logic: terascan_logic - """ - - ####################### SIGNALS FROM GUI TO LOGIC #################### - sigStartMeasurement = QtCore.Signal() - sigStopMeasurement = QtCore.Signal() - sigSetStartWavelength = QtCore.Signal(float) - sigSetStopWavelength = QtCore.Signal(float) - sigSetScanType = QtCore.Signal(int) - sigSetScanRate = QtCore.Signal(int) - sigSaveData = QtCore.Signal() - - #################### CONNECTOR TO LOGIC #################### - _terascan_logic = Connector(name='terascan_logic', interface='TerascanLogic') - - #################### STATUS VARIABLES #################### - _running_avg_points = StatusVar(name='running_avg_points', default=5) - - # TODO: operate in terascan_logic instead of terascan_logic() - def on_activate(self) -> None: - # Initialize the main window and set wavelength controls: - self._mw = TerascanMainWindow() - - 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 SIGNALS FROM GUI TO GUI ############################ - 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 SIGNALS FROM LOGIC TO GUI #################### - self._terascan_logic().sigNewData.connect(self._update_data) # Update the GUI with new data - self._terascan_logic().sigSetScanType.connect(self._set_scan_type) # Update the GUI with new scan type - self._terascan_logic().sigSetScanRate.connect(self._set_scan_rate) # Update the GUI with new scan rate - self._terascan_logic().sigScanStarted.connect(self._scan_started) # Update the GUI when the scan starts - self._terascan_logic().sigScanStopped.connect(self._scan_stopped) # Update the GUI when the scan stops - - - ################### CONNECT SIGNALS FROM GUI TO LOGIC #################### - self.sigStartMeasurement.connect( - self._terascan_logic().start_scan, QtCore.Qt.QueuedConnection - ) - self.sigStopMeasurement.connect( - self._terascan_logic().stop_scan, QtCore.Qt.QueuedConnection - ) - self.sigSetStartWavelength.connect( - self._terascan_logic().set_start_wavelength, QtCore.Qt.QueuedConnection - ) - self.sigSetStopWavelength.connect( - self._terascan_logic().set_stop_wavelength, QtCore.Qt.QueuedConnection - ) - self.sigSetScanType.connect( - self._terascan_logic().set_scan_type, QtCore.Qt.DirectConnection - ) - self.sigSetScanRate.connect( - self._terascan_logic().set_scan_rate, QtCore.Qt.QueuedConnection - ) - self.sigSaveData.connect( - self._terascan_logic().save_data, QtCore.Qt.QueuedConnection - ) - - ####################### REFERENCE TO TERASCAN DATA #################### - self.wavelength_data: List[float] = [] # Wavelength data from the logic module - self.counts_data: List[float] = [] # Counts data from the logic module - - # Set up update timer for plot updates - self.__timer = QtCore.QTimer() - self.__timer.setSingleShot(False) - self.__timer.timeout.connect(self.__update_gui) - self.__timer.start(250) # Update every 250 ms - # TODO: Make this more configurable and maybe more efficient? - - # Restore running average points from the StatusVar: - self._mw.spin_avg_points.setValue(self._running_avg_points) - # Connect changes of the spin box to update our status variable - self._mw.spin_avg_points.valueChanged.connect(self._update_running_avg_points) - - self.show() - - def on_deactivate(self) -> None: - self._terascan_logic().sigNewData.disconnect(self._update_data) - - self._mw.start_wavelength.valueChanged.disconnect() - self._mw.stop_wavelength.valueChanged.disconnect() - self._mw.start_stop_button.clicked.disconnect() - self.sigStartMeasurement.disconnect() - self.sigStopMeasurement.disconnect() - self.sigSetWavelengths.disconnect() - - self.__timer.stop() - self.__timer.timeout.disconnect() - self.__timer = None - - self._mw.spin_avg_points.valueChanged.disconnect(self._update_running_avg_points) - self._mw.close() - - def show(self) -> None: - self._mw.show() - self._mw.raise_() - - def _save_data(self) -> None: - """ Tell the logic to save the data """ - self.sigSaveData.emit() - - # Handlers from the UI: - @QtCore.Slot(float) - def _start_changed(self, wave: float) -> None: - self.sigSetStartWavelength.emit(wave) - - @QtCore.Slot(float) - def _stop_changed(self, wave: float) -> None: - self.sigSetStopWavelength.emit(wave) - - @QtCore.Slot() - def _start_stop_pressed(self) -> None: - if self._mw.start_stop_button.text() == 'Start Measurement': - self.sigStartMeasurement.emit() # Tell the logic to start the scan - - # TODO: This should be a signal - self._terascan_logic().clear_data() - start_wl = self._terascan_logic().start_wavelength - stop_wl = self._terascan_logic().stop_wavelength - self._mw.plot_widget.setXRange(start_wl, stop_wl) - else: - self.sigStopMeasurement.emit() # Tell the logic to stop the scan - - - - - # @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)) - - # Private internal functions: - def _scan_started(self) -> None: - """ Update the GUI when the scan starts """ - self._mw.start_stop_button.setText('Stop Measurement') - self._mw._statusbar.clearMessage() - self._mw._progress_bar.setValue(0) - - def _scan_stopped(self) -> None: - """ Update the GUI when the scan stops """ - self._mw.start_stop_button.setText('Start Measurement') - self._mw._statusbar.showMessage('Ready') - self._mw._progress_bar.setValue(100) - - @QtCore.Slot() - def __update_gui(self): - x_array = self.wavelength_data - y_array = self.counts_data - - if len(x_array) == 0 or len(y_array) == 0: - return - - # If running average is enabled, apply a rolling average - if self._mw.checkbox_running_avg.isChecked(): - window_size = self._mw.spin_avg_points.value() - if window_size > 1 and window_size <= len(y_array): - kernel = np.ones(window_size) / float(window_size) - y_array = np.convolve(y_array, kernel, mode='same') - - - self._mw.data_item.setData( - x=x_array, - y=y_array, - ) - - - @QtCore.Slot(int) - def _update_running_avg_points(self, points: int) -> None: - self._running_avg_points = points - - @QtCore.Slot() - def _update_data(self, wavelength_data, counts_data) -> None: - """ Update the data from the logic module """ - self.wavelength_data = wavelength_data - self.counts_data = counts_data - - - -class TerascanMainWindow(QtWidgets.QMainWindow): - """ Main window for Terascan measurement """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setWindowTitle('Terascan Measurement') - self.resize(1250, 500) - - # 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._progress_bar = QtWidgets.QProgressBar() - self._progress_bar.setRange(0, 100) - self._progress_bar.setValue(0) - - 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._progress_bar) - - # Initialize widgets for wavelengths, scan, etc. - self.start_wavelength_label = QtWidgets.QLabel('Start 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(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(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.setAntialiasing(False) - 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='Counts') - - self.data_item = pg.PlotDataItem( - pen=pg.mkPen(palette.c1, style=QtCore.Qt.SolidLine), - # downsampling if you want - # downsample=10, # Render 1 out of every 10 points - # downsampleMethod='mean' # Average points for smoother results - ) - self.plot_widget.addItem(self.data_item) - - # Running Average controls - self.checkbox_running_avg = QtWidgets.QCheckBox("Enable Running Average") - self.checkbox_running_avg.setChecked(False) - self.label_avg_points = QtWidgets.QLabel("Points in Rolling Average:") - self.spin_avg_points = QtWidgets.QSpinBox() - self.spin_avg_points.setRange(1, 9999) - self.spin_avg_points.setValue(5) # default value; this will be set from a StatusVar in the GUI - - # The Start Measurement button (we want this at the very bottom) - self.start_stop_button = QtWidgets.QPushButton('Start Measurement') - - # Arrange widgets in layout - layout = QtWidgets.QGridLayout() - layout.addWidget(self.plot_widget, 0, 0, 4, 4) - - 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) - - # Place Running Average controls ABOVE the Start Measurement button: - control_layout.addWidget(self.checkbox_running_avg) - control_layout.addWidget(self.label_avg_points) - control_layout.addWidget(self.spin_avg_points) - - # Add stretch to push the start button to the bottom: - control_layout.addStretch() - control_layout.addWidget(self.start_stop_button) - - layout.addLayout(control_layout, 0, 5, 5, 1) - layout.setColumnStretch(1, 1) - - central_widget = QtWidgets.QWidget() - central_widget.setLayout(layout) - self.setCentralWidget(central_widget) diff --git a/src/qudi/hardware/timetagger/swabian_tagger.py b/src/qudi/hardware/timetagger/swabian_tagger.py index 9cbd750..2648eb4 100644 --- a/src/qudi/hardware/timetagger/swabian_tagger.py +++ b/src/qudi/hardware/timetagger/swabian_tagger.py @@ -66,9 +66,10 @@ def on_activate(self): # TODO: Should be a slot i think def start_reading(self): - self.module_state.lock() - self._counter.start() - self.statusvar = 2 + if self.module_state() == 'idle': + self.module_state.lock() + self._counter.start() + self.statusvar = 2 # TODO: Should be a slot @@ -114,5 +115,6 @@ def __update_data(self): if self.module_state() == 'locked': data_obj = self._counter.getDataObject(remove=True) timestamp = time.perf_counter() - self.most_recent_data = np.squeeze(data_obj.getData()) + # self.most_recent_data = np.squeeze(data_obj.getData()) + self.most_recent_data = np.squeeze(data_obj.getDataNormalized()) / 1000 # convert to ms^-1 self.sigNewData.emit(timestamp, self.most_recent_data) diff --git a/src/qudi/logic/terascan_logic copy.py b/src/qudi/logic/terascan_logic copy.py deleted file mode 100644 index d663572..0000000 --- a/src/qudi/logic/terascan_logic copy.py +++ /dev/null @@ -1,499 +0,0 @@ - -import numpy as np -import time -from PySide2 import QtCore - -from typing import List, Dict - -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 - -from qudi.interface.daq_reader_interface import ReaderVal - - -# Find a way to append valid data to the lists instead of creating new ones. -class TerascanLogic(LogicBase): - """ - This is the Logic class for Terascan measurements - - example config for copy-paste: - - terascan_logic: - module.Class: 'terascan_logic.TerascanLogic' - connect: - 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. - laser_timeout_s: 10 # Time in seconds to wait for the laser to lock before automatically restarting the scan - mode_hop_overlap_med: 0.001 # in nm, from the SolsTiS control panel. This is how far back we go to discard data every time a mode hop occurs - mode_hop_overlap_fine: 0.00025 # "" - """ - - #################### CONNECTORS #################### - _laser = Connector(name='laser', interface='Base') - _wavemeter = Connector(name='wavemeter', interface='Base') - _counter = Connector(name='counter', interface='Base') - _daq = Connector(name='daq', interface='Base') - - #################### CONFIGURATION OPTIONS #################### - save_dir = ConfigOption('save_dir', default='C:\\Users\\hoodl\\qudi\\Data', missing='warn') - _laser_timeout_s = ConfigOption(name='laser_timeout_s', default=10) - - #################### STATUS VARIABLES #################### - start_wavelength = StatusVar('start_wavelength', default=0.785) - stop_wavelength = StatusVar('stop_wavelength', default=0.7851) - scan_rate = StatusVar('scan_rate', default=12) # SCAN_RATE_FINE_LINE_20_GHZ - scan_type = StatusVar('scan_type', default=2) # SCAN_TYPE_FINE - - #################### SIGNALS FOR OTHER LOGIC MODULES #################### - sigNewData = QtCore.Signal(float, object, object) # timestamp, wl, counts - sigScanStarted = QtCore.Signal() - sigScanStopped = QtCore.Signal() - sigSetScanType = QtCore.Signal(int) - sigSetScanRate = QtCore.Signal(int) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__timer = None - self._thread_lock = RecursiveMutex() - - ##################### EXPERIMENT PARAMETERS #################### - - ############## DATA #################### - self._time_since_last_wl_change = time.perf_counter() - self._curr_wavelength = 0.0 - self._forward_direction = True - - """ - All of these lists should eventually be the same length, except for laser_status_data. - That is just for bookkeeping purposes. - """ - self.laser_status_data = [] # List of laser status data (0 = idle, 1 = scanning, 2 = error) - self.counts_data = [] - self.wavelength_data = [] - self.ttl_data = [] - # These all have a common length and are calculated using ttl_data - self.valid_counts_data = [] - self.valid_wavelength_data = [] - - self.set_scan_type(self.scan_type) - self.set_scan_rate(self.scan_rate) - - def on_activate(self): - # TODO: Is it a good idea to do this here? Or should I always do self._instrument()? - laser = self._laser() - counter = self._counter() - wavemeter = self._wavemeter() - daq = self._daq() - - ##################### INITIALIZE DATA INPUT #################### - counter.start_reading() - wavemeter.start_reading() - daq.start_reading() - - #################### OUTPUTS #################### - # self.sigStartScan.connect(laser.start_scan, QtCore.Qt.QueuedConnection) - # self.sigStopScan.connect(laser.stop_scan, QtCore.Qt.QueuedConnection) - # self.sigSetWavelengths.connect(laser.set_wavelengths, QtCore.Qt.QueuedConnection) - # self.sigSetScanRate.connect(laser.set_scan_rate, QtCore.Qt.QueuedConnection) - # self.sigSetScanType.connect(laser.set_scan_type, QtCore.Qt.QueuedConnection) - - #################### INPUTS #################### - laser.sigNewData.connect(self._new_laser_data) - wavemeter.sigNewData.connect(self._new_wavemeter_data) - counter.sigNewData.connect(self._new_counter_data) - daq.sigNewData.connect(self._new_daq_data) - - #################### WATCHDOG TIMER #################### - self.__timer = QtCore.QTimer() - self.__timer.setSingleShot(False) - self.__timer.timeout.connect(self.__update_scan) - self.__timer.timeout.connect(self.__update_data) - self.__timer.start(100) # 100 ms timer - - def on_deactivate(self): - self.__timer.stop() - self.__timer.timeout.disconnect() - self.__timer = None - - @property - def scan_types(self) -> dict: - return self._laser().get_scan_types - - @property - def scan_rates(self) -> dict: - return self._laser().get_scan_rates - - def get_start_wavelength(self) -> float: - with self._thread_lock: - return self.start_wavelength - - def get_stop_wavelength(self) -> float: - with self._thread_lock: - return self.stop_wavelength - - def get_scan_rate(self) -> int: - with self._thread_lock: - return self.scan_rate - - def get_scan_type(self) -> int: - with self._thread_lock: - return self.scan_type - - def start_scan(self): - with self._thread_lock: - if self.module_state() == 'idle': - self.module_state.lock() - self.sigScanStarted.emit() - - self._laser().set_wavelengths(self.start_wavelength, self.stop_wavelength) - self._laser().set_scan_type(self.scan_type) - self._laser().set_scan_rate(self.scan_rate) - self._laser().start_scan() - - def stop_scan(self): - with self._thread_lock: - if self.module_state() == 'locked': - self.module_state.unlock() - self.sigScanStopped.emit() - self._laser().stop_scan() - - - def set_start_wavelength(self, start: float): - with self._thread_lock: - if self.module_state() == 'idle': - self.start_wavelength = start - else: - self.log.warning( - 'Tried to configure while a scan was running.'\ - 'Please wait until it is finished or stop it.') - - def set_stop_wavelength(self, stop: float): - with self._thread_lock: - if self.module_state() == 'idle': - self.stop_wavelength = stop - else: - self.log.warning( - 'Tried to configure while a scan was running.'\ - 'Please wait until it is finished or stop it.') - - # TODO: Deprecated. Use individual set_start_wavelength and set_stop_wavelength instead. - @QtCore.Slot(float, float) - def set_wavelengths(self, start: float, stop: float): - with self._thread_lock: - if self.module_state() == 'idle': - self.start_wavelength = start - self.stop_wavelength = stop - self._forward_direction = start < stop - else: - self.log.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.sigScanRateChanged.emit(scan_rate) - else: - self.log.warning( - 'Tried to configure while a scan was running.'\ - 'Please wait until it is finished or stop it.') - - @QtCore.Slot(int) - # TODO: Should this also set the solstis scan type? Or is that done in the start_scan method? - def set_scan_type(self, scan_type: int): - with self._thread_lock: - if self.module_state() == 'idle': - self.scan_type = scan_type - self.sigScanTypeChanged.emit(scan_type) - else: - self.log.warning( - 'Tried to configure while a scan was running.'\ - 'Please wait until it is finished or stop it.') - - - @QtCore.Slot(float, np.ndarray) - def _new_counter_data(self, timestamp, data: np.ndarray): - with self._thread_lock: - if self.module_state() == 'locked': - new_counts_data = data.tolist() - self.counts_data.extend(new_counts_data) - - - @QtCore.Slot(float, object) - def _new_wavemeter_data(self, timestamp, data: np.float64): - """Called on every new wavemeter reading when locked.""" - with self._thread_lock: - # only run when we’re in the ‘locked’ state - if self.module_state() != 'locked': - return - - most_recent_wavelength = self.wavelength_data[-1] if len(self.wavelength_data) > 0 else data - n_new_points = len(self.counts_data) - len(self.wavelength_data) - new_wavelength_data = np.linspace(most_recent_wavelength, data, n_new_points).tolist() - self.wavelength_data.extend(new_wavelength_data) - - - @QtCore.Slot(float, object) - def _new_daq_data(self, timestamp, data: bool): - with self._thread_lock: - if self.module_state() != 'locked': - return - - n_new_points = len(self.counts_data) - len(self.ttl_data) - new_ttl_data = [data] * n_new_points - self.ttl_data.extend(new_ttl_data) - - - @QtCore.Slot(float, object) - def _new_laser_data(self, timestamp, data: ReaderVal): - with self._thread_lock: - if self.module_state() != 'locked': - return - self.laser_status_data += [data] - - - def clear_data(self): - with self._thread_lock: - self.laser_status_data = [] - self.wavelength_data = [] - self.counts_data = [] - self.ttl_data = [] - self.valid_counts_data = [] - self.valid_wavelength_data = [] - self._curr_wavelength = 0.0 - self._time_since_last_wl_change = time.perf_counter() - - - # TODO: Clean this up. I don't like the logic. - @QtCore.Slot() - def save_data(self) -> None: - ds = TextDataStorage( - root_dir=self.save_dir, # Use the configurable save directory - column_formats='.15e' - ) - array = np.column_stack(( - self.valid_wavelength_data, - self.valid_counts_data, - self.ttl_data, - self.wavelength_data, - self.counts_data, - )) - ds.save_data(array) - - - def __update_data(self): - with self._thread_lock: - if self.module_state() != 'locked': - return - - # TODO: Make this more efficient. It's not a good idea to recreate the valid data lists every time. - ######################### UPDATE VALID DATA ######################### - common_length = min(len(self.counts_data), len(self.wavelength_data), len(self.ttl_data)) - mask = self.ttl_data[:common_length] - self.valid_counts_data = np.array(self.counts_data[:common_length])[mask].tolist() - self.valid_wavelength_data = np.array(self.wavelength_data[:common_length])[mask].tolist() - - # update valid counts - # counts_mask = self.ttl_data[len(self.valid_counts_data):new_common_length] - # new_counts = np.array(self.counts_data[len(self.valid_counts_data):new_common_length])[counts_mask].tolist() - # self.valid_counts_data.extend(new_counts) - - # # update valid wavelength - # wavelength_mask = self.ttl_data[len(self.valid_wavelength_data):new_common_length] - # new_wavelength = np.array(self.wavelength_data[len(self.valid_wavelength_data):new_common_length])[wavelength_mask].tolist() - # self.valid_wavelength_data.extend(new_wavelength) - - # Emit the new data signal - self.sigNewData.emit(time.perf_counter(), self.valid_wavelength_data, self.valid_counts_data) - - - def __update_scan(self): - with self._thread_lock: - if self.module_state() != 'locked': - self.log.debug("Watchdog: module not locked, skipping") - return - - if not self.laser_status_data: - self.log.debug("Watchdog: no laser status yet") - return - - last_status = self.laser_status_data[-1] - self.log.debug(f"Watchdog: last laser status = {last_status}") - - # 1) If laser is actively scanning, nothing to do - if last_status == 1: - self.log.debug("Watchdog: laser is scanning → ok") - return - - # 2) If laser idle, check for normal scan end - if last_status == 0: - if self._check_for_end_of_scan(): - self.log.info("Watchdog: reached end wavelength, stopping scan") - self.stop_scan() - return - else: - self.log.debug("Watchdog: laser idle but not at end yet") - - # 3) Detect stalled motion by wavemeter - if self._update_wavelength_motion(): - self.log.debug(f"Watchdog: motion detected, updated curr_wavelength to {self._curr_wavelength}") - return # reset timer, leave scan running - - # 4) Timeout: no motion for > timeout_s - if time.perf_counter() - self._time_since_last_wl_change > self._laser_timeout_s: - self.log.warning("Watchdog: motion timeout exceeded, restarting scan") - self._restart_from_last_valid() - - # ——— Helpers —————————————————————————————————————————————— - def _check_for_end_of_scan(self) -> bool: - """ - In order for the scan to be considered finished, - 1. There must be a valid wavelength (wl with ttl high) that is close to the start wavelength - 2. The last valid wavelength must be close to the stop wavelength - """ - - # First check - tol = 1e-4 - if not self.wavelength_data: - self.log.debug("Motion check: no wavelength data yet") - return False - n = min(len(self.wavelength_data), len(self.ttl_data), len(self.counts_data)) - mask = self.ttl_data[:n] - valid_wl = np.asarray(self.wavelength_data[:n])[mask] - - if np.any(np.isclose(valid_wl, self.start_wavelength, atol=tol)) and \ - np.any(np.isclose(valid_wl, self.stop_wavelength, atol=tol)): - self.log.debug("Check end-of-scan: found valid wavelength close to start and stop") - return True - else: - return False - - - def _update_wavelength_motion(self) -> bool: - """ - Check if the wavemeter has recorded a new wavelength since last seen. - If so, update curr_wavelength & reset timeout timer. - Returns True if motion detected. - """ - if not self.wavelength_data: - self.log.debug("Motion check: no wavelength data yet") - return False - - latest = self.wavelength_data[-1] - delta = abs(self._curr_wavelength - latest) - threshold = 2e-5 - - if delta > threshold: - self.log.info(f"Motion detected: Δλ={delta:.2e} nm > {threshold:.2e}") - self._curr_wavelength = latest - self._time_since_last_wl_change = time.perf_counter() - return True - - self.log.debug(f"No motion: Δλ={delta:.2e} nm ≤ threshold {threshold:.2e}") - return False - - def _restart_from_last_valid(self): - """ - Find the last TTL‐valid wavelength, stop the scan, and restart - from there to the end_wavelength. - """ - n = min(len(self.wavelength_data), len(self.ttl_data), len(self.counts_data)) - mask = self.ttl_data[:n] - valid = [wl for wl, ok in zip(self.wavelength_data[:n], mask) if ok] - - if valid: - last_wl = valid[-1] - else: - last_wl = self.start_wavelength - self.log.warning("No valid TTL points found → restarting from start_wavelength") - - self.log.info(f"Restarting scan from λ={last_wl} to {self.stop_wavelength}") - self._laser().stop_scan() - time.sleep(1) - self._laser().set_wavelengths(last_wl, self.stop_wavelength) - self._laser().start_scan() - - - - -class TerascanLogicData: - def __init__(self): - """Initialize the TerascanLogicData class with empty data lists. - - The valid data lists are equal in length and calculated using ttl_data. - """ - self.counts_data = [] - self.wavelength_data = [] - self.ttl_data = [] - - self.valid_counts_data = [] - self.valid_wavelength_data = [] - - def get_counts_data(self) -> List[float]: - """Get the counts data. - - Returns: - list: The counts data. - """ - return self.counts_data - - def get_wavelength_data(self) -> List[float]: - """Get the wavelength data. - - Returns: - list: The wavelength data. - """ - return self.wavelength_data - - def get_ttl_data(self) -> List[bool]: - """Get the TTL data. - - Returns: - list: The TTL data. - """ - return self.ttl_data - - def add_counts_data(self, counts_data: List[float]): - """Add counts data to the TerascanLogicData object. - - Args: - counts_data (list): A list of counts data to be added. - """ - self.counts_data.extend(counts_data) - - def add_wavelength_data(self, wavelength_data: List[float]): - """Add wavelength data to the TerascanLogicData object. - - Args: - wavelength_data (list): A list of wavelength data to be added. - """ - self.wavelength_data.extend(wavelength_data) - - def add_ttl_data(self, ttl_data: List[bool]): - """Add TTL data to the TerascanLogicData object. - - Args: - ttl_data (list): A list of TTL data to be added. - """ - self.ttl_data.extend(ttl_data) - new_common_length = min(len(self.counts_data), len(self.wavelength_data), len(self.ttl_data)) - - # update valid counts - counts_mask = self.ttl_data[len(self.valid_counts_data):new_common_length] - new_counts = np.array(self.counts_data[len(self.valid_counts_data):new_common_length])[counts_mask].tolist() - self.valid_counts_data.extend(new_counts) - - # update valid wavelength - wavelength_mask = self.ttl_data[len(self.valid_wavelength_data):new_common_length] - new_wavelength = np.array(self.wavelength_data[len(self.valid_wavelength_data):new_common_length])[wavelength_mask].tolist() - self.valid_wavelength_data.extend(new_wavelength) \ No newline at end of file diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index 9a32fa3..646fe0e 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -245,13 +245,13 @@ def set_scan_rate(self, scan_rate: int): with self._thread_lock: if self.module_state() == 'idle': # auto-choose type from the enum name - name = TeraScanRate(scan_rate).name + name = self._laser().TeraScanRate(scan_rate).name if name.startswith('SCAN_RATE_MEDIUM'): - self.scan_type = TeraScanType.SCAN_TYPE_MEDIUM.value + self.scan_type = self._laser().TeraScanType.SCAN_TYPE_MEDIUM.value elif name.startswith('SCAN_RATE_LINE'): - self.scan_type = TeraScanType.SCAN_TYPE_LINE.value + self.scan_type = self._laser().TeraScanType.SCAN_TYPE_LINE.value else: - self.scan_type = TeraScanType.SCAN_TYPE_FINE.value + self.scan_type = self._laser().TeraScanType.SCAN_TYPE_FINE.value self.scan_rate = scan_rate self.sigScanTypeChanged.emit(self.scan_type) self.sigScanRateChanged.emit(scan_rate) From 0098f78d36df9b1c10dede0cc17dee652636eda2 Mon Sep 17 00:00:00 2001 From: lange50 Date: Wed, 30 Apr 2025 10:14:26 -0400 Subject: [PATCH 15/16] Consolidate TeraScanType and TeraScanRate into SolstisLaser To keep logic and laser from having copied enums, I put the enum classes as a subclass in solstis laser. We still need to get ried of the redundant ocde in get_scan_type etc., and we also need to get rid of the redundant code where solsits_funcs also has the enum. --- src/qudi/gui/terascan/terascan_gui.py | 40 ------------------------ src/qudi/hardware/laser/solstis_laser.py | 29 +++++++++-------- src/qudi/logic/terascan_logic.py | 36 --------------------- 3 files changed, 16 insertions(+), 89 deletions(-) diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index db77418..ec70aff 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -13,46 +13,6 @@ from qudi.util.paths import get_artwork_dir from qudi.util.colordefs import QudiPalettePale as palette -from enum import Enum - - -# ---------------------------------------------------------------------- -# enums (kept local – same numeric values as in the logic) -# ---------------------------------------------------------------------- -class TeraScanType(Enum): - SCAN_TYPE_MEDIUM = 1 - SCAN_TYPE_FINE = 2 - SCAN_TYPE_LINE = 3 - - -class TeraScanRate(Enum): - SCAN_RATE_MEDIUM_100_GHZ = 4 - SCAN_RATE_MEDIUM_50_GHZ = 5 - SCAN_RATE_MEDIUM_20_GHZ = 6 - SCAN_RATE_MEDIUM_15_GHZ = 7 - SCAN_RATE_MEDIUM_10_GHZ = 8 - SCAN_RATE_MEDIUM_5_GHZ = 9 - SCAN_RATE_MEDIUM_2_GHZ = 10 - SCAN_RATE_MEDIUM_1_GHZ = 11 - SCAN_RATE_FINE_LINE_20_GHZ = 12 - SCAN_RATE_FINE_LINE_10_GHZ = 13 - SCAN_RATE_FINE_LINE_5_GHZ = 14 - SCAN_RATE_FINE_LINE_2_GHZ = 15 - SCAN_RATE_FINE_LINE_1_GHZ = 16 - SCAN_RATE_FINE_LINE_500_MHZ = 17 - SCAN_RATE_FINE_LINE_200_MHZ = 18 - SCAN_RATE_FINE_LINE_100_MHZ = 19 - SCAN_RATE_FINE_LINE_50_MHZ = 20 - SCAN_RATE_FINE_LINE_20_MHZ = 21 - SCAN_RATE_FINE_LINE_10_MHZ = 22 - SCAN_RATE_FINE_LINE_5_MHZ = 23 - SCAN_RATE_FINE_LINE_2_MHZ = 24 - SCAN_RATE_FINE_LINE_1_MHZ = 25 - SCAN_RATE_LINE_500_KHZ = 26 - SCAN_RATE_LINE_200_KHZ = 27 - SCAN_RATE_LINE_100_KHZ = 28 - SCAN_RATE_LINE_50_KHZ = 29 - pg.setConfigOption('useOpenGL', True) diff --git a/src/qudi/hardware/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py index 4a2e18c..980b7e7 100644 --- a/src/qudi/hardware/laser/solstis_laser.py +++ b/src/qudi/hardware/laser/solstis_laser.py @@ -15,8 +15,8 @@ You should have received a copy of the GNU Lesser General Public License along with qudi. If not, see . """ +from __future__ import annotations from typing import List - from PySide2 import QtCore import time from enum import Enum @@ -296,9 +296,11 @@ def get_scan_types(self) -> dict: return { 'Medium': TeraScanType.SCAN_TYPE_MEDIUM, 'Fine': TeraScanType.SCAN_TYPE_FINE, - 'Line': TeraScanType.SCAN_TYPE_LINE + # 'Line': TeraScanType.SCAN_TYPE_LINE } + + # TODO: This is not the right way to do this. There is copied code. @property def get_scan_rates(self) -> dict: scan_type = self._scan_type @@ -337,16 +339,16 @@ def get_scan_rates(self) -> dict: '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 - } + # 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') @@ -397,7 +399,8 @@ def __status_update(self): else: self.statusvar = 0 except solstis.SolstisError as e: - self.log.exception(f'Failure getting status: {e.message}') + # self.log.exception(f'Failure getting status: {e.message}') + self.log.warning(f'Failure getting status: {e.message}') self.statusvar = -1 timestamp = time.perf_counter() self.sigNewData.emit(timestamp, self.statusvar) diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index 646fe0e..d0f8161 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -16,42 +16,6 @@ from qudi.interface.daq_reader_interface import ReaderVal -from enum import Enum - -# TODO: This is a copy of the one in the logic module. We should probably move this to a common place. -class TeraScanType(Enum): - SCAN_TYPE_MEDIUM = 1 - SCAN_TYPE_FINE = 2 - SCAN_TYPE_LINE = 3 - -class TeraScanRate(Enum): - SCAN_RATE_MEDIUM_100_GHZ = 4 - SCAN_RATE_MEDIUM_50_GHZ = 5 - SCAN_RATE_MEDIUM_20_GHZ = 6 - SCAN_RATE_MEDIUM_15_GHZ = 7 - SCAN_RATE_MEDIUM_10_GHZ = 8 - SCAN_RATE_MEDIUM_5_GHZ = 9 - SCAN_RATE_MEDIUM_2_GHZ = 10 - SCAN_RATE_MEDIUM_1_GHZ = 11 - SCAN_RATE_FINE_LINE_20_GHZ = 12 - SCAN_RATE_FINE_LINE_10_GHZ = 13 - SCAN_RATE_FINE_LINE_5_GHZ = 14 - SCAN_RATE_FINE_LINE_2_GHZ = 15 - SCAN_RATE_FINE_LINE_1_GHZ = 16 - SCAN_RATE_FINE_LINE_500_MHZ = 17 - SCAN_RATE_FINE_LINE_200_MHZ = 18 - SCAN_RATE_FINE_LINE_100_MHZ = 19 - SCAN_RATE_FINE_LINE_50_MHZ = 20 - SCAN_RATE_FINE_LINE_20_MHZ = 21 - SCAN_RATE_FINE_LINE_10_MHZ = 22 - SCAN_RATE_FINE_LINE_5_MHZ = 23 - SCAN_RATE_FINE_LINE_2_MHZ = 24 - SCAN_RATE_FINE_LINE_1_MHZ = 25 - SCAN_RATE_LINE_500_KHZ = 26 - SCAN_RATE_LINE_200_KHZ = 27 - SCAN_RATE_LINE_100_KHZ = 28 - SCAN_RATE_LINE_50_KHZ = 29 - # Find a way to append valid data to the lists instead of creating new ones. class TerascanLogic(LogicBase): From a03f29a21a1a5ca175d935da62e3b701f560720a Mon Sep 17 00:00:00 2001 From: lange50 Date: Wed, 4 Jun 2025 10:45:55 -0400 Subject: [PATCH 16/16] Add spectral wandering.ipynb I added a file to scan many times using terascan_logic. I also changed save_data so you can specify a path. --- notebooks/spectral_wandering.ipynb | 109 ++++++++++++++++++++ src/qudi/gui/terascan/terascan_gui.py | 143 +++++++++++++++++--------- src/qudi/logic/terascan_logic.py | 16 ++- 3 files changed, 214 insertions(+), 54 deletions(-) create mode 100644 notebooks/spectral_wandering.ipynb diff --git a/notebooks/spectral_wandering.ipynb b/notebooks/spectral_wandering.ipynb new file mode 100644 index 0000000..ef8f969 --- /dev/null +++ b/notebooks/spectral_wandering.ipynb @@ -0,0 +1,109 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9d68bde7", + "metadata": {}, + "outputs": [], + "source": [ + "from time import time\n", + "import os\n", + "\n", + "\"\"\"\n", + "This script runs a series of scans using the terascan_logic module.\n", + "Set the start and stop wavelengths in the gui, as well as the number of scans.\n", + "\"\"\"\n", + "\n", + "tl = terascan_logic" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7f66367e", + "metadata": {}, + "outputs": [ + { + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[24], line 12\u001b[0m\n\u001b[0;32m 9\u001b[0m started_scans \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0\u001b[39m\n\u001b[0;32m 11\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[1;32m---> 12\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[43mtl\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmodule_state\u001b[49m() \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124midle\u001b[39m\u001b[38;5;124m'\u001b[39m: \u001b[38;5;66;03m# i.e. the scan is not running yet\u001b[39;00m\n\u001b[0;32m 13\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m started_scans \u001b[38;5;241m>\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m 14\u001b[0m time_list\u001b[38;5;241m.\u001b[39mappend(time() \u001b[38;5;241m-\u001b[39m start_time)\n", + "File \u001b[1;32m~\\anaconda3\\envs\\qudi-env\\lib\\site-packages\\rpyc\\core\\netref.py:148\u001b[0m, in \u001b[0;36mBaseNetref.__getattribute__\u001b[1;34m(self, name)\u001b[0m\n\u001b[0;32m 146\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mobject\u001b[39m\u001b[38;5;241m.\u001b[39m\u001b[38;5;21m__getattribute__\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__array__\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 147\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m--> 148\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43msyncreq\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconsts\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mHANDLE_GETATTR\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mname\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32m~\\anaconda3\\envs\\qudi-env\\lib\\site-packages\\rpyc\\core\\netref.py:63\u001b[0m, in \u001b[0;36msyncreq\u001b[1;34m(proxy, handler, *args)\u001b[0m\n\u001b[0;32m 51\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Performs a synchronous request on the given proxy object.\u001b[39;00m\n\u001b[0;32m 52\u001b[0m \u001b[38;5;124;03mNot intended to be invoked directly.\u001b[39;00m\n\u001b[0;32m 53\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 60\u001b[0m \u001b[38;5;124;03m:returns: the result of the operation\u001b[39;00m\n\u001b[0;32m 61\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 62\u001b[0m conn \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mobject\u001b[39m\u001b[38;5;241m.\u001b[39m\u001b[38;5;21m__getattribute__\u001b[39m(proxy, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m____conn__\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m---> 63\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mconn\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msync_request\u001b[49m\u001b[43m(\u001b[49m\u001b[43mhandler\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mproxy\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32m~\\anaconda3\\envs\\qudi-env\\lib\\site-packages\\rpyc\\core\\protocol.py:718\u001b[0m, in \u001b[0;36mConnection.sync_request\u001b[1;34m(self, handler, *args)\u001b[0m\n\u001b[0;32m 715\u001b[0m _async_res \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39masync_request(handler, \u001b[38;5;241m*\u001b[39margs, timeout\u001b[38;5;241m=\u001b[39mtimeout)\n\u001b[0;32m 716\u001b[0m \u001b[38;5;66;03m# _async_res is an instance of AsyncResult, the value property invokes Connection.serve via AsyncResult.wait\u001b[39;00m\n\u001b[0;32m 717\u001b[0m \u001b[38;5;66;03m# So, the _recvlock can be acquired multiple times by the owning thread and warrants the use of RLock\u001b[39;00m\n\u001b[1;32m--> 718\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_async_res\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalue\u001b[49m\n", + "File \u001b[1;32m~\\anaconda3\\envs\\qudi-env\\lib\\site-packages\\rpyc\\core\\async_.py:106\u001b[0m, in \u001b[0;36mAsyncResult.value\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 98\u001b[0m \u001b[38;5;129m@property\u001b[39m\n\u001b[0;32m 99\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mvalue\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[0;32m 100\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Returns the result of the operation. If the result has not yet\u001b[39;00m\n\u001b[0;32m 101\u001b[0m \u001b[38;5;124;03m arrived, accessing this property will wait for it. If the result does\u001b[39;00m\n\u001b[0;32m 102\u001b[0m \u001b[38;5;124;03m not arrive before the expiry time elapses, :class:`AsyncResultTimeout`\u001b[39;00m\n\u001b[0;32m 103\u001b[0m \u001b[38;5;124;03m is raised. If the returned result is an exception, it will be raised\u001b[39;00m\n\u001b[0;32m 104\u001b[0m \u001b[38;5;124;03m here. Otherwise, the result is returned directly.\u001b[39;00m\n\u001b[0;32m 105\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m--> 106\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mwait\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 107\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_is_exc:\n\u001b[0;32m 108\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_obj\n", + "File \u001b[1;32m~\\anaconda3\\envs\\qudi-env\\lib\\site-packages\\rpyc\\core\\async_.py:51\u001b[0m, in \u001b[0;36mAsyncResult.wait\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 44\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Waits for the result to arrive. If the AsyncResult object has an\u001b[39;00m\n\u001b[0;32m 45\u001b[0m \u001b[38;5;124;03mexpiry set, and the result did not arrive within that timeout,\u001b[39;00m\n\u001b[0;32m 46\u001b[0m \u001b[38;5;124;03man :class:`AsyncResultTimeout` exception is raised\"\"\"\u001b[39;00m\n\u001b[0;32m 47\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_is_ready \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mexpired):\n\u001b[0;32m 48\u001b[0m \u001b[38;5;66;03m# Serve the connection since we are not ready. Suppose\u001b[39;00m\n\u001b[0;32m 49\u001b[0m \u001b[38;5;66;03m# the reply for our seq is served. The callback is this class\u001b[39;00m\n\u001b[0;32m 50\u001b[0m \u001b[38;5;66;03m# so __call__ sets our obj and _is_ready to true.\u001b[39;00m\n\u001b[1;32m---> 51\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_conn\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mserve\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_ttl\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 53\u001b[0m \u001b[38;5;66;03m# Check if we timed out before result was ready\u001b[39;00m\n\u001b[0;32m 54\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_is_ready:\n", + "File \u001b[1;32m~\\anaconda3\\envs\\qudi-env\\lib\\site-packages\\rpyc\\core\\protocol.py:438\u001b[0m, in \u001b[0;36mConnection.serve\u001b[1;34m(self, timeout, wait_for_lock)\u001b[0m\n\u001b[0;32m 436\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m 437\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;66;03m# Ensure data is initialized\u001b[39;00m\n\u001b[1;32m--> 438\u001b[0m data \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_channel\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpoll\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_channel\u001b[38;5;241m.\u001b[39mrecv()\n\u001b[0;32m 439\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[0;32m 440\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_recvlock\u001b[38;5;241m.\u001b[39mrelease()\n", + "File \u001b[1;32m~\\anaconda3\\envs\\qudi-env\\lib\\site-packages\\rpyc\\core\\channel.py:47\u001b[0m, in \u001b[0;36mChannel.poll\u001b[1;34m(self, timeout)\u001b[0m\n\u001b[0;32m 45\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mpoll\u001b[39m(\u001b[38;5;28mself\u001b[39m, timeout):\n\u001b[0;32m 46\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"polls the underlying steam for data, waiting up to *timeout* seconds\"\"\"\u001b[39;00m\n\u001b[1;32m---> 47\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstream\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpoll\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32m~\\anaconda3\\envs\\qudi-env\\lib\\site-packages\\rpyc\\core\\stream.py:48\u001b[0m, in \u001b[0;36mStream.poll\u001b[1;34m(self, timeout)\u001b[0m\n\u001b[0;32m 46\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[0;32m 47\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m---> 48\u001b[0m rl \u001b[38;5;241m=\u001b[39m \u001b[43mp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpoll\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtimeout\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtimeleft\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 49\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m select_error:\n\u001b[0;32m 50\u001b[0m ex \u001b[38;5;241m=\u001b[39m sys\u001b[38;5;241m.\u001b[39mexc_info()[\u001b[38;5;241m1\u001b[39m]\n", + "File \u001b[1;32m~\\anaconda3\\envs\\qudi-env\\lib\\site-packages\\rpyc\\lib\\compat.py:164\u001b[0m, in \u001b[0;36mSelectingPoll.poll\u001b[1;34m(self, timeout)\u001b[0m\n\u001b[0;32m 162\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m [] \u001b[38;5;66;03m# need to return an empty array in this case\u001b[39;00m\n\u001b[0;32m 163\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m--> 164\u001b[0m rl, wl, _ \u001b[38;5;241m=\u001b[39m \u001b[43mselect\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrlist\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mwlist\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 165\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m [(fd, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;28;01mfor\u001b[39;00m fd \u001b[38;5;129;01min\u001b[39;00m rl] \u001b[38;5;241m+\u001b[39m [(fd, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mw\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;28;01mfor\u001b[39;00m fd \u001b[38;5;129;01min\u001b[39;00m wl]\n", + "\u001b[1;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "start_time = time()\n", + "\n", + "data_save_dir = os.path.join('D:', 'qudi_data', 'multi_scan', '00000')\n", + "\n", + "time_list = []\n", + "data_file_list = []\n", + "\n", + "n_scans = 2\n", + "started_scans = 0\n", + "tl.clear_data()\n", + "\n", + "while True:\n", + " if tl.module_state() == 'idle': # i.e. the scan is not running yet\n", + " if started_scans > 0:\n", + " time_list.append(time() - start_time)\n", + " data_file_list.append(tl.save_data(data_save_dir))\n", + " tl.clear_data()\n", + " \n", + " \n", + " if started_scans < n_scans:\n", + " tl.start_scan()\n", + " started_scans += 1\n", + " else:\n", + " print(f\"Completed {n_scans} scans.\")\n", + " break\n", + " \n", + " elif tl.module_state() == 'locked':\n", + " continue" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73f3c420", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "qudi", + "language": "python", + "name": "qudi" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/qudi/gui/terascan/terascan_gui.py b/src/qudi/gui/terascan/terascan_gui.py index ec70aff..277cbde 100644 --- a/src/qudi/gui/terascan/terascan_gui.py +++ b/src/qudi/gui/terascan/terascan_gui.py @@ -1,11 +1,31 @@ # -*- coding: utf-8 -*- -__all__ = ['TerascanGui'] +"""Modified Terascan GUI module + +Changes compared to original version +------------------------------------ +1. Added optional down–sampling (averaging of consecutive points) to smooth the + displayed trace + • new checkbox `Enable Downsampling` + • new spin‑box `Points per Downsample Bin` +2. Extended `__update_gui()` so that – when the checkbox is ticked – the x and + y arrays shown in the plot are replaced by a rebinned version where each + bin contains *n* original points averaged together. + • the routine gracefully handles traces whose length is *not* an integer + multiple of *n* by discarding the leftover (< n) tail ­– the underlying + data kept by the logic is **not** altered. +3. Added preference `self._downsample_points` (stored as a `StatusVar`) and + matching slot for live updates. +4. No other data‑paths were changed; logic communication stays untouched. + +Written for PySide2/pyqtgraph just like the original code. +""" import numpy as np import os +from typing import List + from PySide2 import QtCore, QtGui, QtWidgets import pyqtgraph as pg -from typing import List from qudi.core.module import GuiBase from qudi.core.connector import Connector @@ -13,16 +33,12 @@ from qudi.util.paths import get_artwork_dir from qudi.util.colordefs import QudiPalettePale as palette - pg.setConfigOption('useOpenGL', True) # ────────────────────────────────────────────────────────────────────── class TerascanGui(GuiBase): - """ - GUI module for the Terascan logic. unchanged layout – just patched - so the module loads and stays in sync with the logic. - """ + """GUI module for the Terascan logic with running‑average and optional down‑sampling.""" # ▸▸ GUI → logic signals sigStartMeasurement = QtCore.Signal() @@ -36,27 +52,25 @@ class TerascanGui(GuiBase): # connector _terascan_logic = Connector(name='terascan_logic', interface='TerascanLogic') - # one GUI preference + # user preferences _running_avg_points = StatusVar(name='running_avg_points', default=5) + _downsample_points = StatusVar(name='downsample_points', default=4) - # ─────────────── Qudi life-cycle ─────────────────────────────── + # ─────────────── Qudi life‑cycle ──────────────────────────────── def on_activate(self): - # create the main window + # build the main window self._mw = TerascanMainWindow() - # ← populate start/stop λ in nm from the logic’s µm values - # TODO: Should I do this with a signal from the logic instead? - # TODO: Why isn't this working? + + # populate λ limits & combo‑boxes from the logic logic = self._terascan_logic() self._mw.start_wavelength.setValue(logic.get_start_wavelength()) self._mw.stop_wavelength.setValue(logic.get_stop_wavelength()) - # same for rate and teyp self._mw.scan_rate.setCurrentIndex(logic.get_scan_rate()) self._mw.scan_type.setCurrentIndex(logic.get_scan_type()) - # populate combo-boxes with entries from the logic - for txt, scan_type in self._terascan_logic().scan_types.items(): + for txt, scan_type in logic.scan_types.items(): self._mw.scan_type.addItem(txt, scan_type) - for txt, scan_rate in self._terascan_logic().scan_rates.items(): + for txt, scan_rate in logic.scan_rates.items(): self._mw.scan_rate.addItem(txt, scan_rate) # ▸ GUI widgets → local slots @@ -68,9 +82,8 @@ def on_activate(self): self._mw.scan_rate.currentIndexChanged.connect(self._scan_rate_changed) # ▸ logic → GUI - logic = self._terascan_logic() - logic.sigNewData.connect(self._update_data) # fixed signature - logic.sigScanTypeChanged.connect(self._set_scan_type) # corrected names + logic.sigNewData.connect(self._update_data) + logic.sigScanTypeChanged.connect(self._set_scan_type) logic.sigScanRateChanged.connect(self._set_scan_rate) logic.sigScanStarted.connect(self._scan_started) logic.sigScanStopped.connect(self._scan_stopped) @@ -84,18 +97,22 @@ def on_activate(self): self.sigSetScanRate.connect(logic.set_scan_rate, QtCore.Qt.QueuedConnection) self.sigSaveData.connect(logic.save_data, QtCore.Qt.QueuedConnection) - # keep copies of live data + # live data copies self.wavelength_data: List[float] = [] - self.counts_data: List[float] = [] + self.counts_data: List[float] = [] - # 250 ms plot refresh + # 250 ms GUI refresh timer self.__timer = QtCore.QTimer(self) self.__timer.timeout.connect(self.__update_gui) self.__timer.start(250) - # restore running-avg preference + # restore stored preferences self._mw.spin_avg_points.setValue(self._running_avg_points) + self._mw.spin_downsample_points.setValue(self._downsample_points) + + # preference change handlers self._mw.spin_avg_points.valueChanged.connect(self._update_running_avg_points) + self._mw.spin_downsample_points.valueChanged.connect(self._update_downsample_points) self.show() @@ -107,7 +124,6 @@ def on_deactivate(self): self.__timer.stop() self.__timer.timeout.disconnect() - self._mw.close() # ─────────── GUI → logic handler slots ─────────────────────────── @@ -136,8 +152,6 @@ def _start_stop_pressed(self): if self._mw.start_stop_button.text() == 'Start Measurement': self.sigStartMeasurement.emit() self._terascan_logic().clear_data() - # TODO: This should be a signal - self._terascan_logic().clear_data() start_wl = self._terascan_logic().start_wavelength stop_wl = self._terascan_logic().stop_wavelength self._mw.plot_widget.setXRange(start_wl, stop_wl) @@ -175,7 +189,7 @@ def _set_scan_rate(self, scan_rate): def _update_data(self, _timestamp, wavelength_data, counts_data): """Slot connected to logic.sigNewData""" self.wavelength_data = list(wavelength_data) - self.counts_data = list(counts_data) + self.counts_data = list(counts_data) # ─────────── GUI housekeeping ──────────────────────────────────── @QtCore.Slot() @@ -183,44 +197,61 @@ def __update_gui(self): if not self.wavelength_data: return - x_array = self.wavelength_data - y_array = self.counts_data + # work on *copies* – never touch the originals + x_array = np.asarray(self.wavelength_data, dtype=float) + y_array = np.asarray(self.counts_data, dtype=float) + # optional running average (convolution) if self._mw.checkbox_running_avg.isChecked(): window = self._mw.spin_avg_points.value() if 1 < window <= len(y_array): - y_array = np.convolve(y_array, - np.ones(window) / window, - mode='same') + kernel = np.ones(window, dtype=float) / float(window) + y_array = np.convolve(y_array, kernel, mode='same') + + # optional down‑sampling / binning + if self._mw.checkbox_downsample.isChecked(): + n = self._mw.spin_downsample_points.value() + if n > 1 and len(x_array) >= n: + # cut off the non‑divisible tail so reshape() works + trim = len(x_array) - (len(x_array) % n) + if trim: + x_array = x_array[:trim] + y_array = y_array[:trim] + x_array = x_array.reshape(-1, n).mean(axis=1) + y_array = y_array.reshape(-1, n).mean(axis=1) self._mw.data_item.setData(x=x_array, y=y_array) + # preference setters ------------------------------------------------ @QtCore.Slot(int) def _update_running_avg_points(self, pts): self._running_avg_points = pts - # expose the window to Qudi’s tray show-action + @QtCore.Slot(int) + def _update_downsample_points(self, pts): + self._downsample_points = pts + + # expose the window to Qudi’s tray action -------------------------------- def show(self): self._mw.show() self._mw.raise_() - + def _save_data(self) -> None: - """Forward the File ▸ Save Data action to the logic module.""" self.sigSaveData.emit() # ────────────────────────────────────────────────────────────────────── -# unchanged TerascanMainWindow – kept verbatim -# ( only inside file so import works in-place ) +# Modified TerascanMainWindow – only additions are the down‑sampling controls # ────────────────────────────────────────────────────────────────────── class TerascanMainWindow(QtWidgets.QMainWindow): """ Main window for Terascan measurement """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setWindowTitle('Terascan Measurement') self.resize(1250, 500) - # menu bar + # menu bar ------------------------------------------------------ menu_bar = QtWidgets.QMenuBar() menu = menu_bar.addMenu('File') self.action_save_data = QtWidgets.QAction('Save Data') @@ -236,7 +267,7 @@ def __init__(self, *args, **kwargs): menu.addAction(action_close) self.setMenuBar(menu_bar) - # status-bar + # status‑bar ---------------------------------------------------- self._statusbar = self.statusBar() self._progress_bar = QtWidgets.QProgressBar() self._progress_bar.setRange(0, 100) @@ -250,7 +281,7 @@ def __init__(self, *args, **kwargs): self._statusbar.addWidget(self._locked_indicator) self._statusbar.addWidget(self._progress_bar) - # widgets + # widgets ------------------------------------------------------- self.start_wavelength_label = QtWidgets.QLabel('Start Wavelength (nm)') self.start_wavelength = _spinbox() @@ -265,21 +296,27 @@ def __init__(self, *args, **kwargs): self.plot_widget = pg.PlotWidget() self.plot_widget.setLabel('bottom', text='Wavelength', units='um') - self.plot_widget.setLabel('left', text='Counts') - self.data_item = pg.PlotDataItem( - pen=pg.mkPen(palette.c1, style=QtCore.Qt.SolidLine)) + self.plot_widget.setLabel('left', text='Counts') + self.data_item = pg.PlotDataItem(pen=pg.mkPen(palette.c1, style=QtCore.Qt.SolidLine)) self.plot_widget.addItem(self.data_item) - # running average controls - self.checkbox_running_avg = QtWidgets.QCheckBox("Enable Running Average") - self.label_avg_points = QtWidgets.QLabel("Points in Rolling Average:") - self.spin_avg_points = QtWidgets.QSpinBox() + # running average controls ------------------------------------ + self.checkbox_running_avg = QtWidgets.QCheckBox('Enable Running Average') + self.label_avg_points = QtWidgets.QLabel('Points in Rolling Average:') + self.spin_avg_points = QtWidgets.QSpinBox() self.spin_avg_points.setRange(1, 9999) self.spin_avg_points.setValue(5) + # NEW: down‑sampling controls ---------------------------------- + self.checkbox_downsample = QtWidgets.QCheckBox('Enable Downsampling') + self.label_downsample_points = QtWidgets.QLabel('Points per Downsample Bin:') + self.spin_downsample_points = QtWidgets.QSpinBox() + self.spin_downsample_points.setRange(2, 9999) + self.spin_downsample_points.setValue(4) + self.start_stop_button = QtWidgets.QPushButton('Start Measurement') - # layout + # layout -------------------------------------------------------- layout = QtWidgets.QGridLayout() layout.addWidget(self.plot_widget, 0, 0, 4, 4) @@ -293,14 +330,22 @@ def __init__(self, *args, **kwargs): controls.addWidget(lab, 0, QtCore.Qt.AlignBottom) controls.addWidget(wid, 0, QtCore.Qt.AlignTop) + # running average controls controls.addWidget(self.checkbox_running_avg) controls.addWidget(self.label_avg_points) controls.addWidget(self.spin_avg_points) + controls.addSpacing(10) + # ‑‑ new down‑sample controls + controls.addWidget(self.checkbox_downsample) + controls.addWidget(self.label_downsample_points) + controls.addWidget(self.spin_downsample_points) + controls.addStretch() controls.addWidget(self.start_stop_button) layout.addLayout(controls, 0, 5, 5, 1) layout.setColumnStretch(1, 1) + central = QtWidgets.QWidget() central.setLayout(layout) self.setCentralWidget(central) diff --git a/src/qudi/logic/terascan_logic.py b/src/qudi/logic/terascan_logic.py index d0f8161..295a789 100644 --- a/src/qudi/logic/terascan_logic.py +++ b/src/qudi/logic/terascan_logic.py @@ -166,7 +166,7 @@ def start_scan(self): def stop_scan(self): with self._thread_lock: if self.module_state() == 'locked': - self.module_state.unlock() + self.module_state.unlock() # This is everything you need to know if the scan is running or not. self.sigScanStopped.emit() self._laser().stop_scan() @@ -292,17 +292,21 @@ def clear_data(self): @QtCore.Slot() - def save_data(self) -> None: + def save_data(self, save_dir=None) -> None: """ Save self.valid_wavelength_data and self.valid_counts_data to a new 8-digit, zero-padded .dat file in self.save_dir, e.g. '00000001.dat'. """ + + if save_dir is None: + save_dir = self.save_dir + # 1) Make sure directory exists - os.makedirs(self.save_dir, exist_ok=True) + os.makedirs(save_dir, exist_ok=True) # 2) Find existing files of form '########.dat' existing = [] - for fn in os.listdir(self.save_dir): + for fn in os.listdir(save_dir): if re.fullmatch(r'\d{8}\.dat', fn): existing.append(int(fn[:8])) # 3) Determine next index @@ -317,7 +321,7 @@ def save_data(self) -> None: # 5) Instantiate storage and save storage = TextDataStorage( - root_dir=self.save_dir, + root_dir=save_dir, comments='# ', delimiter='\t', file_extension='.dat', @@ -326,6 +330,8 @@ def save_data(self) -> None: ) # save_data returns (file_path, timestamp, (rows, columns)) storage.save_data(data, filename=filename) + + return save_dir