Skip to content

Commit

Permalink
Merge pull request #2 from hoodlabpurdue/laser_auto_restart
Browse files Browse the repository at this point in the history
Laser auto restart + Photon Counting GUI
  • Loading branch information
bcerjan authored Mar 28, 2025
2 parents 0950fb1 + 7ba0e37 commit ddaec16
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 6 deletions.
10 changes: 9 additions & 1 deletion cfg/terascan.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
global:
# list of modules to load when starting
startup_modules: [terascan_gui]
startup_modules: [photon_counts_time_average_gui, terascan_gui]

# Module server configuration for accessing qudi GUI/logic/hardware modules from remote clients
remote_modules_server:
Expand Down Expand Up @@ -38,6 +38,13 @@ gui:
connect:
terascan_logic: terascan_logic

photon_counts_time_average_gui:
module.Class: 'swabian.photon_counts_time_average_gui.PhotonCountsTimeAverageGui'
connect:
swabian_timetagger: 'swabian_timetagger'
options:
ring_buffer_length_s: 5

logic:
terascan_logic:
module.Class: 'terascan_logic.TerascanLogic'
Expand All @@ -48,6 +55,7 @@ logic:
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 before we automatically restart the laser scan


# daq_reader_logic:
Expand Down
93 changes: 93 additions & 0 deletions src/qudi/gui/swabian/photon_counts_time_average_gui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-

__all__ = ['PhotonCountsTimeAverageGui']

import numpy as np
import os
from PySide2 import QtCore, QtGui
from typing import List
from time import sleep
from collections import deque

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.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')

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(
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

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().sigPhotonCounts.disconnect(self._counts_changed)

self._mw.start_button.clicked.disconnect()

# disable update timer:
self.__timer.stop()
self.__timer.timeout.disconnect()
self.__timer = None

self._mw.close()

def show(self) -> None:
""" Mandatory method to show the 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)
69 changes: 69 additions & 0 deletions src/qudi/gui/swabian/photon_counts_time_average_main_window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- 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)
10 changes: 9 additions & 1 deletion src/qudi/gui/terascan/terascan_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
class TerascanGui(GuiBase):
""" Terascan Measurement GUI
exaple config for copy-paste:
example config for copy-paste:
terascan_gui:
module.Class: 'terascan.terascan_gui.TerascanGui'
connect:
Expand Down Expand Up @@ -112,6 +112,9 @@ 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)

# Show the main window and raise it above all others
self.show()
Expand Down Expand Up @@ -243,10 +246,15 @@ def _update_ui(self, running: bool) -> None:
self._mw.start_stop_button.setText('Start Measurement')
self._mw._statusbar.showMessage('Ready')

# TODO: Make this more efficient. There is no reason to redraw the entire plot each time.
def _update_plot(self) -> None:
if (len(self._data) == 0):
return


# x, y = self._mw.data_item.getData()
# new_count = len(self._data) - len(x)

img = np.zeros((len(self._data),2))
for i in range(len(self._data) - 1):
img[i][0] = self._data[i].wavelength*1e-3
Expand Down
20 changes: 19 additions & 1 deletion src/qudi/hardware/laser/solstis_laser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from typing import List

from PySide2 import QtCore
from time import time

from qudi.core.configoption import ConfigOption
from qudi.core.statusvariable import StatusVar
Expand Down Expand Up @@ -79,6 +80,8 @@ 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.
Expand Down Expand Up @@ -237,6 +240,7 @@ def start_scan(self) -> bool:
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

Expand All @@ -257,6 +261,19 @@ def stop_scan(self) -> bool:
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)"""
Expand Down Expand Up @@ -358,7 +375,8 @@ def set_scan_rate(self, scan_rate: int):


def __status_update(self):
if self.module_state() == 'locked':
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()
Expand Down
36 changes: 33 additions & 3 deletions src/qudi/logic/terascan_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class TerascanLogic(LogicBase):
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
"""

# declare connectors
Expand All @@ -53,6 +53,8 @@ class TerascanLogic(LogicBase):
_record_length_ms = ConfigOption(name='record_length_ms',
default=1,
missing='info')

_laser_timeout_s = ConfigOption(name='laser_timeout_s', default=10)

# status variables:
_start_wavelength = StatusVar('start_wavelength', default=0.75)
Expand All @@ -65,6 +67,7 @@ class TerascanLogic(LogicBase):
_laser_locked = StatusVar('laser_locked', default=False)
_current_data = StatusVar('current_data', default=[]) # list of TerascanData

_last_locked: float = 0

# Update signals, e.g. for GUI module
sigWavelengthUpdated = QtCore.Signal(float)
Expand All @@ -76,6 +79,7 @@ class TerascanLogic(LogicBase):
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()
Expand All @@ -87,7 +91,7 @@ class TerascanLogic(LogicBase):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.__timer = None
self._thread_lock = RecursiveMutex()

def on_activate(self):
Expand All @@ -101,6 +105,7 @@ def on_activate(self):
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)
Expand All @@ -124,9 +129,17 @@ def on_activate(self):
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):
pass
self.__timer.stop()
self.__timer.timeout.disconnect()
self.__timer = None

@property
def locked(self) -> bool:
Expand All @@ -146,6 +159,9 @@ def start_scan(self):
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()
Expand Down Expand Up @@ -243,3 +259,17 @@ def _new_daq_data(self, data: List[ReaderVal]):
# self.sigStopCounting.emit()

self.sigLaserLocked.emit(self._laser_locked)


#### 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)

0 comments on commit ddaec16

Please sign in to comment.