Skip to content

Commit

Permalink
Fix photon counts window
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lange50 committed Apr 30, 2025
1 parent 747b624 commit c2dd374
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 1,018 deletions.
299 changes: 224 additions & 75 deletions src/qudi/gui/swabian/photon_counts_time_average_gui.py
Original file line number Diff line number Diff line change
@@ -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)

# -------------------------------------------------------------------------
# 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()
69 changes: 0 additions & 69 deletions src/qudi/gui/swabian/photon_counts_time_average_main_window.py

This file was deleted.

Loading

0 comments on commit c2dd374

Please sign in to comment.