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/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.py b/src/qudi/gui/terascan/terascan_gui.py
index a6882f8..277cbde 100644
--- a/src/qudi/gui/terascan/terascan_gui.py
+++ b/src/qudi/gui/terascan/terascan_gui.py
@@ -1,62 +1,79 @@
# -*- 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 PySide2 import QtCore, QtGui
from typing import List
-from time import sleep
-from qudi.util.datastorage import TextDataStorage
+from PySide2 import QtCore, QtGui, QtWidgets
+import pyqtgraph as pg
+
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)
-from qudi.logic.terascan_logic import TerascanData
+# ──────────────────────────────────────────────────────────────────────
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 for outgoing control signals to logic
+ """GUI module for the Terascan logic with running‑average and optional down‑sampling."""
+
+ # ▸▸ GUI → logic signals
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
_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:
+ # user preferences
_running_avg_points = StatusVar(name='running_avg_points', default=5)
+ _downsample_points = StatusVar(name='downsample_points', default=4)
- def on_activate(self) -> None:
- # Initialize the main window and set wavelength controls:
+ # ─────────────── Qudi life‑cycle ────────────────────────────────
+ def on_activate(self):
+ # build the main window
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():
+ # 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())
+ self._mw.scan_rate.setCurrentIndex(logic.get_scan_rate())
+ self._mw.scan_type.setCurrentIndex(logic.get_scan_type())
+
+ 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)
- # Connect GUI internal signals
+ # ▸ 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,177 +81,280 @@ 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
- 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 output signals 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.sigSetWavelengths.connect(
- self._terascan_logic().configure_scan, 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._data = []
-
- # Set up update timer for plot updates
- 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)
-
- # Restore running average points from the StatusVar:
+ # ▸ logic → GUI
+ 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)
+
+ # ▸ 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)
+
+ # live data copies
+ self.wavelength_data: List[float] = []
+ self.counts_data: List[float] = []
+
+ # 250 ms GUI refresh timer
+ self.__timer = QtCore.QTimer(self)
+ self.__timer.timeout.connect(self.__update_gui)
+ self.__timer.start(250)
+
+ # restore stored preferences
self._mw.spin_avg_points.setValue(self._running_avg_points)
- # Connect changes of the spin box to update our status variable
+ 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()
- 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._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_()
-
- # Handlers from the UI:
+ # ─────────── GUI → logic handler slots ───────────────────────────
@QtCore.Slot(float)
- def _start_changed(self, wave: float) -> None:
- self._start_wavelength = wave
- self.sigSetWavelengths.emit(self._start_wavelength, self._stop_wavelength)
+ def _start_changed(self, wl):
+ self.sigSetStartWavelength.emit(wl)
@QtCore.Slot(float)
- def _stop_changed(self, wave: float) -> None:
- self._stop_wavelength = wave
- self.sigSetWavelengths.emit(self._start_wavelength, self._stop_wavelength)
+ 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._update_ui(True)
- self.__timer.start(250)
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._update_ui(False)
- self.__timer.stop()
self.sigStopMeasurement.emit()
- @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)
+ # ─────────── 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)
- # Handlers from the Logic:
- @QtCore.Slot()
- def _scan_finished(self) -> None:
+ 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(object)
- def _receive_data(self, data: List[TerascanData]) -> None:
- self._data = data
-
- @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)))
+ @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 _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])
- 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))
-
- # Private internal functions:
- def _update_ui(self, running: bool) -> None:
- if 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')
-
- def _update_plot(self):
- # Make a local snapshot of the data
- local_data = self._data[:]
- if not local_data:
+ def __update_gui(self):
+ if not self.wavelength_data:
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])
+ # 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)
- # If running average is enabled, apply a rolling average
+ # optional running average (convolution)
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)
+ window = self._mw.spin_avg_points.value()
+ if 1 < window <= len(y_array):
+ kernel = np.ones(window, dtype=float) / float(window)
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
+ # 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
@QtCore.Slot(int)
- def _update_running_avg_points(self, points: int) -> None:
- self._running_avg_points = points
+ 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:
+ self.sigSaveData.emit()
+
+
+# ──────────────────────────────────────────────────────────────────────
+# 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 = 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()
+ 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)
+
+ # 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)
+ )
+ self._statusbar.addWidget(self._locked_indicator)
+ self._statusbar.addWidget(self._progress_bar)
+
+ # widgets -------------------------------------------------------
+ self.start_wavelength_label = QtWidgets.QLabel('Start Wavelength (nm)')
+ self.start_wavelength = _spinbox()
+
+ self.stop_wavelength_label = QtWidgets.QLabel('Stop Wavelength (nm)')
+ 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.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.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()
+ 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 = QtWidgets.QGridLayout()
+ layout.addWidget(self.plot_widget, 0, 0, 4, 4)
+
+ 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)
+
+ # 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)
+
+
+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/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.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/laser/dummy_solstis.py b/src/qudi/hardware/laser/dummy_solstis.py
new file mode 100644
index 0000000..332d95e
--- /dev/null
+++ b/src/qudi/hardware/laser/dummy_solstis.py
@@ -0,0 +1,372 @@
+# -*- 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
+
+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.
+ 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=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)
+
+
+
+
+ # 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
+ self._test_status = {"in_progress": False}
+
+ 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
+
+ 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
+ """
+ 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
+ """
+ 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
+ """
+
+ return -1
+
+
+
+ def get_laser_state(self):
+ """ Get laser operation state
+
+ @return LaserState: laser state
+ """
+ return self._test_status
+
+
+ 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
+
+ # TODO: Implement this function
+ @QtCore.Slot(float)
+ def restart_scan(self, wavelength: float) -> bool:
+ """Restart a scan from a specified wavelength"""
+ 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)"""
+ 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/laser/solstis_laser.py b/src/qudi/hardware/laser/solstis_laser.py
index 795fefa..980b7e7 100644
--- a/src/qudi/hardware/laser/solstis_laser.py
+++ b/src/qudi/hardware/laser/solstis_laser.py
@@ -15,21 +15,22 @@
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
-from time import time
+import time
+from enum import Enum
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
-
+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 +43,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)
@@ -53,8 +57,8 @@ class SolstisLaser(ScanningLaserInterface):
# status variables:
- _start_wavelength = StatusVar('start_wavelength', default=0.78)
- _end_wavelength = StatusVar('end_wavelength', default=0.785)
+ _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)
@@ -62,7 +66,14 @@ class SolstisLaser(ScanningLaserInterface):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__timer = None
- self._wavelength = -1
+ self._current_wavelength = -1
+
+ self.statusvar = 0
+ """ statusvar
+ 0 = idle
+ 1 = running
+ -1 = error state
+ """
def on_activate(self):
""" Activate module.
@@ -75,13 +86,10 @@ 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()
self._scan_rate = self.get_default_scan_rate()
-
- self._scan_started = time()
def on_deactivate(self):
""" Deactivate module.
@@ -125,12 +133,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)
@@ -228,8 +231,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,
@@ -239,8 +242,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
@@ -253,26 +254,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.sigScanFinished.emit()
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}')
- 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')
+ # self.log.exception(f'Scan stop failure: {e.message}')
+ self.log.warning(f'Scan stop failure: {e.message}')
return False
def pause_scan(self):
@@ -302,13 +290,17 @@ 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 {
'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
@@ -347,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')
@@ -374,10 +366,75 @@ 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()
- if status['in_progress'] == False:
- self.sigScanFinished.emit()
- self.module_state.unlock()
\ No newline at end of file
+ try:
+ 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 = 0
+ except solstis.SolstisError as e:
+ # 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)
+
+
+ class TeraScanType(Enum):
+ SCAN_TYPE_MEDIUM = 1
+ SCAN_TYPE_FINE = 2
+ # SCAN_TYPE_LINE = 3 # We don't have this capability yet
+
+ 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/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..2648eb4 100644
--- a/src/qudi/hardware/timetagger/swabian_tagger.py
+++ b/src/qudi/hardware/timetagger/swabian_tagger.py
@@ -1,37 +1,22 @@
# -*- 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
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.
+# 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,56 @@ class SwabianTimeTagger(FastCounterInterface):
options:
channels:
photon_counts: 0
- dark_counts: 1
-
"""
+ ####################### SIGNALS ####################
+ sigNewData = QtCore.Signal(float, object) # timestamp, data
- _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.__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._tagger = TimeTagger.createTimeTagger()
+ self._counter = TimeTagger.Counter(
+ tagger=self._tagger,
+ channels=[1],
+ binwidth=1_000_000_000,
+ n_values=1_000
+ )
- 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.statusvar = 0 # 0 = unconfigured, 1 = idle, 2 = running, 3 = paused, -1 = error state
+ 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
- @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.__timer.start(10) # call in 10 ms intervals
- self.sigScanStarted.emit()
+
+ # TODO: Should be a slot i think
+ def start_reading(self):
+ if self.module_state() == 'idle':
+ self.module_state.lock()
+ self._counter.start()
+ self.statusvar = 2
- 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 +90,31 @@ 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.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/hardware/wavemeter/wavemeter.py b/src/qudi/hardware/wavemeter/wavemeter.py
new file mode 100644
index 0000000..b09231b
--- /dev/null
+++ b/src/qudi/hardware/wavemeter/wavemeter.py
@@ -0,0 +1,91 @@
+# -*- 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
+from qudi.core.module import Base
+
+
+# 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_
+ """
+
+ ####################### SIGNALS ####################
+ sigNewData = QtCore.Signal(float, object) # timestamp, data
+
+ _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 * 1e9) # convert to nm
+
+
+ def __update_data(self):
+ with self._thread_lock:
+ if self.module_state() == 'locked':
+ self._data = self.get_wavelength()
+ timestamp = time.perf_counter()
+ self.sigNewData.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..295a789 100644
--- a/src/qudi/logic/terascan_logic.py
+++ b/src/qudi/logic/terascan_logic.py
@@ -1,30 +1,23 @@
+import os
+import re
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 TerascanData():
- wavelength: float
- counts: int
-
- def __init__(self, wavelength: float, counts: int):
- self.wavelength = wavelength
- self.counts = counts
+# 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
@@ -45,110 +38,95 @@ class TerascanLogic(LogicBase):
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')
+ #################### 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')
- # declare config options
- _record_length_ms = ConfigOption(name='record_length_ms',
- default=1,
- missing='info')
-
+ #################### 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)
-
- _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)
+ #################### 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
- # 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
+ #################### SIGNALS FOR OTHER LOGIC MODULES ####################
+ sigNewData = QtCore.Signal(float, object, object) # timestamp, wl, counts
+ sigScanStarted = QtCore.Signal()
+ sigScanStopped = QtCore.Signal()
+ sigScanRateChanged = QtCore.Signal(int)
+ sigScanTypeChanged = QtCore.Signal(int)
+ sigStartWavelengthChanged = QtCore.Signal(float)
+ sigStopWavelengthChanged = QtCore.Signal(float)
- 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()
+
+ ##################### 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()
-
- # 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)
+ ##################### 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)
-
- # 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.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 locked(self) -> bool:
- return self._laser_locked
@property
def scan_types(self) -> dict:
@@ -158,155 +136,408 @@ 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 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()
+ 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()
- @QtCore.Slot()
def stop_scan(self):
with self._thread_lock:
if self.module_state() == 'locked':
- self.sigStopScan.emit()
- 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()
+
- @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
+ self.sigStartWavelengthChanged.emit(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
+ self.sigStopWavelengthChanged.emit(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.sigSetLaserScanRate.emit(scan_rate)
+ # auto-choose type from the enum name
+ name = self._laser().TeraScanRate(scan_rate).name
+ if name.startswith('SCAN_RATE_MEDIUM'):
+ self.scan_type = self._laser().TeraScanType.SCAN_TYPE_MEDIUM.value
+ elif name.startswith('SCAN_RATE_LINE'):
+ self.scan_type = self._laser().TeraScanType.SCAN_TYPE_LINE.value
+ else:
+ 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)
+
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.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
+ 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()
- def _laser_scan_started(self):
- pass
- @QtCore.Slot()
- def _laser_scan_finished(self):
+ @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:
- self.sigScanFinished.emit()
- self.sigStopCounting.emit()
- self.module_state.unlock()
+ # 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
- @QtCore.Slot(np.ndarray)
- def _new_wavemeter_data(self, data: np.ndarray):
+ 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' and self._laser_locked:
- wave = data[0][0]
- self._current_wavelength = wave # ?
- self.sigWavelengthUpdated.emit(wave)
+ if self.module_state() != 'locked':
+ return
+ self.laser_status_data += [data]
+
- @QtCore.Slot(np.ndarray)
- def _process_counter_data(self, data: np.ndarray):
+ def clear_data(self):
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]):
+ 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()
+
+
+ @QtCore.Slot()
+ 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(save_dir, exist_ok=True)
+
+ # 2) Find existing files of form '########.dat'
+ existing = []
+ for fn in os.listdir(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
+ ]).T # shape (N, 2)
+
+ # 5) Instantiate storage and save
+ storage = TextDataStorage(
+ root_dir=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)
+
+ return save_dir
+
+
+
+ def __update_data(self):
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 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)
- 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.
+ 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
"""
- 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
+ # 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:
- self.log.warning('Unknown scan type. Not removing data.')
- return
+ return False
- 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
+ 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
-
- #### 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)
-
+ 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/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