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