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