-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
6 changed files
with
234 additions
and
1,018 deletions.
There are no files selected for viewing
299 changes: 224 additions & 75 deletions
299
src/qudi/gui/swabian/photon_counts_time_average_gui.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
69
src/qudi/gui/swabian/photon_counts_time_average_main_window.py
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.