Skip to content

Commit

Permalink
Merge branch 'develop' into BECATRUE/269/restart-fetching-dataset-list
Browse files Browse the repository at this point in the history
  • Loading branch information
BECATRUE authored Apr 3, 2024
2 parents 43797e4 + be70741 commit 94b8eee
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 22 deletions.
1 change: 1 addition & 0 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
pip install websockets
pip install numpy
pip install pyqtgraph
pip install git+https://github.com/m-labs/sipyco.git
pip install git+https://github.com/snu-quiqcl/qiwis.git@${{ env.QIWIS_VERSION }}
- name: Analyze the code with pylint
run: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
pip install numpy
pip install pyqtgraph
sudo apt-get install python3-pyqt5
pip install git+https://github.com/m-labs/sipyco.git
pip install git+https://github.com/snu-quiqcl/qiwis.git@${{ env.QIWIS_VERSION }}
- name: Run the unit tests and check coverage
run: |
Expand Down
97 changes: 81 additions & 16 deletions iquip/apps/dataviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@

MAX_INT = 2**31 - 1

def filter_dataset_list(names: List[str]) -> List[str]:
"""Returns a new list excluding "*.parameters" and "*.units".
Args:
names: Dataset name list which includes "*.parameters" and "*.units".
"""
return [name for name in names if not name.endswith((".parameters", ".units"))]


def p_1(threshold: int, array: np.ndarray) -> float:
"""Returns P1 given threshold and photon count array.
Expand Down Expand Up @@ -366,9 +375,11 @@ class _RemotePart(QWidget):
dateHourChanged(date, hour): The target date and hour are changed.
The argument date is a string in ISO format.
The argument hour is a number from 0 to 23, or None if it is not set.
ridClicked(rid): The target RID is clicked.
"""

dateHourChanged = pyqtSignal(str, object)
ridClicked = pyqtSignal(str)

def __init__(self, parent: Optional[QWidget] = None):
"""Extended."""
Expand All @@ -384,6 +395,7 @@ def __init__(self, parent: Optional[QWidget] = None):
self.hourSpinBox.setRange(0, 23)
self.hourSpinBox.setSuffix("h")
self.ridComboBox = QComboBox(self)
self.ridComboBox.setEditable(True)
layout = QHBoxLayout(self)
layout.addWidget(self.dateEdit)
layout.addWidget(self.hourCheckBox)
Expand All @@ -394,6 +406,7 @@ def __init__(self, parent: Optional[QWidget] = None):
self.hourCheckBox.clicked.connect(self.hourSpinBox.setEnabled)
self.hourCheckBox.stateChanged.connect(self.updateRidComboBox)
self.hourSpinBox.valueChanged.connect(self.updateRidComboBox)
self.ridComboBox.currentTextChanged.connect(self.ridClicked)

@pyqtSlot()
def updateRidComboBox(self):
Expand Down Expand Up @@ -873,20 +886,12 @@ def __init__(self, ip: str, port: int, parent: Optional[QObject] = None):
self.url = f"ws://{ip}:{port}/dataset/master/list/"
self.websocket: ClientConnection

def _filter(self, names: List[str]) -> List[str]:
"""Returns a new list excluding "*.parameters" and "*.units".
Args:
names: Dataset name list which includes "*.parameters" and "*.units".
"""
return [name for name in names if not name.endswith((".parameters", ".units"))]

def stop(self):
"""Stops the thread."""
try:
self.websocket.close()
except WebSocketException:
logger.exception("Failed to stop fetching the dataset name list.")
logger.exception("Failed to stop fetching the dataset name list in ARTIQ master.")

def run(self):
"""Overridden."""
Expand All @@ -899,7 +904,7 @@ def run(self):
if self.websocket.ping().wait(5):
continue
break # connection is lost
self.fetched.emit(self._filter(json.loads(response)))
self.fetched.emit(filter_dataset_list(json.loads(response)))
except Exception: # pylint: disable=broad-exception-caught
logger.exception("Failed to fetch the dataset name list.")

Expand Down Expand Up @@ -1040,14 +1045,47 @@ def run(self):
self.fetched.emit(response.json())


class DataViewerApp(qiwis.BaseApp):
class _RemoteListThread(QThread):
"""QThread for fetching the list of datasets in a specific RID.
Signals:
fetched(datasets): Dataset name list is fetched.
Attributes:
url: GET request url.
params: GET request parameters.
"""

fetched = pyqtSignal(list)

def __init__(self, rid: int, ip: str, port: int, parent: Optional[QObject] = None):
"""Extended.
Args:
rid: See _RemotePart.ridClicked signal.
ip, port: IP address and PORT number of the proxy server.
"""
super().__init__(parent=parent)
self.url = f"http://{ip}:{port}/dataset/rid/list/"
self.params = {"rid": rid}

def run(self):
"""Overridden."""
try:
response = requests.get(self.url, params=self.params, timeout=5)
response.raise_for_status()
except requests.exceptions.RequestException:
logger.exception("Failed to fetch the dataset name list in a specific RID.")
return
self.fetched.emit(filter_dataset_list(response.json()))


class DataViewerApp(qiwis.BaseApp): # pylint: disable=too-many-instance-attributes
"""App for data visualization.
Attributes:
frame: DataViewerFrame instance.
realtimeFetcherThread, realtimeListThread, ridListOfDateHourThread:
The most recently executed _RealtimeFetcherThread, _RealtimeListThread, and
_RidListOfDateHourThread instance, respectively.
*Thread: The most recently executed thread instance corresponding to the name.
policy: Data policy instance. None if there is currently no data.
axis: The current plot axis parameter indices. See SimpleScanDataPolicy.extract().
dataPointIndex: The most recently selected data point index.
Expand All @@ -1060,20 +1098,24 @@ def __init__(self, name: str, parent: Optional[QObject] = None):
self.realtimeFetcherThread: Optional[_RealtimeFetcherThread] = None
self.realtimeListThread: Optional[_RealtimeListThread] = None
self.ridListOfDateHourThread: _RidListOfDateHourThread
self.remoteListThread: _RemoteListThread
self.policy: Optional[SimpleScanDataPolicy] = None
self.axis: Tuple[int, ...] = ()
self.dataPointIndex: Tuple[int, ...] = ()
self.startRealtimeDatasetListThread()
realtimePart, remotePart = (self.frame.sourceWidget.stack.widget(buttonId)
for buttonId in SourceWidget.ButtonId)
# signal connection
realtimePart.syncToggled.connect(self._toggleSync)
realtimePart.restartButton.clicked.connect(self.startRealtimeDatasetListThread)
remotePart.dateHourChanged.connect(self.startRidListOfDateHourThread)
remotePart.ridClicked.connect(self.startRemoteListThread)
self.frame.sourceWidget.modeClicked.connect(self.switchSourceMode)
self.frame.sourceWidget.axisApplied.connect(self.setAxis)
self.frame.dataPointWidget.dataTypeChanged.connect(self.setDataType)
self.frame.dataPointWidget.thresholdChanged.connect(self.setThreshold)
self.frame.mainPlotWidget.dataClicked.connect(self.selectDataPoint)
remotePart.updateRidComboBox()

@pyqtSlot(int)
def switchSourceMode(self, buttonId: int):
Expand All @@ -1090,7 +1132,7 @@ def switchSourceMode(self, buttonId: int):
remotePart: _RemotePart = self.frame.sourceWidget.stack.widget(
SourceWidget.ButtonId.REMOTE
)
remotePart.updateRidComboBox()
self.startRemoteListThread(remotePart.ridComboBox.currentText())

def startRealtimeDatasetListThread(self):
"""Creates and starts a new _RealtimeListThread instance."""
Expand Down Expand Up @@ -1160,7 +1202,11 @@ def synchronize(self):

@pyqtSlot(str, object)
def startRidListOfDateHourThread(self, date: str, hour: Optional[int]):
"""Creates and starts a new _RidListOfDateHourThread instance."""
"""Creates and starts a new _RidListOfDateHourThread instance.
Args:
See _RemotePart.dateHourChanged signal.
"""
self.ridListOfDateHourThread = _RidListOfDateHourThread(
date,
hour,
Expand All @@ -1184,6 +1230,25 @@ def updateRidList(self, rids: List[int]):
remotePart.ridComboBox.clear()
remotePart.ridComboBox.addItems(list(map(str, rids)))

@pyqtSlot(str)
def startRemoteListThread(self, rid: str):
"""Creates and starts a new _RemoteListThread instance.
Args:
See _RemotePart.ridClicked signal.
"""
self.frame.sourceWidget.datasetBox.clear()
if not rid: # no selected RID
return
self.remoteListThread = _RemoteListThread(
int(rid),
self.constants.proxy_ip, # pylint: disable=no-member
self.constants.proxy_port, # pylint: disable=no-member
)
self.remoteListThread.fetched.connect(self._updateDatasetBox, type=Qt.QueuedConnection)
self.remoteListThread.finished.connect(self.remoteListThread.deleteLater)
self.remoteListThread.start()

@pyqtSlot(np.ndarray, list, list)
def setDataset(
self,
Expand Down
22 changes: 19 additions & 3 deletions iquip/apps/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
QCheckBox, QDoubleSpinBox, QGridLayout, QGroupBox, QHBoxLayout,
QLabel, QPushButton, QSlider, QVBoxLayout, QWidget
)
from websockets.exceptions import WebSocketException
from websockets.sync.client import connect

import qiwis
Expand Down Expand Up @@ -134,6 +133,8 @@ class TTLControllerFrame(QWidget):
Each key is a TTL channel name, and its value is the corresponding TTLControllerWidget.
overrideOnButton: Button for turning on the override of all TTL devices.
overrideOffButton: Button for turning off the override of all TTL devices.
restartButton: Button for restarting to synchronize TTL status. Once the button is clicked,
it is disabled. It will be enabled once the thread fetching TTL status is finished.
Signals:
overrideChangeRequested(override): Requested to change the override value.
Expand Down Expand Up @@ -166,6 +167,8 @@ def __init__(self, ttlInfo: Dict[str, str], parent: Optional[QWidget] = None):
overrideButtonBox = QGroupBox("Override", self)
self.overrideOnButton = QPushButton("ON", self)
self.overrideOffButton = QPushButton("OFF", self)
self.restartButton = QPushButton("Restart", self)
self.restartButton.setEnabled(False)
# layout
overrideButtonLayout = QHBoxLayout(overrideButtonBox)
overrideButtonLayout.addWidget(self.overrideOnButton)
Expand All @@ -174,11 +177,13 @@ def __init__(self, ttlInfo: Dict[str, str], parent: Optional[QWidget] = None):
layout.addLayout(ttlWidgetLayout)
layout.addStretch()
layout.addWidget(overrideButtonBox)
layout.addWidget(self.restartButton)
# signal connection
self.overrideOnButton.clicked.connect(
functools.partial(self.overrideChangeRequested.emit, True))
self.overrideOffButton.clicked.connect(
functools.partial(self.overrideChangeRequested.emit, False))
self.restartButton.clicked.connect(functools.partial(self.restartButton.setEnabled, False))


class _TTLStatusThread(QThread):
Expand Down Expand Up @@ -218,10 +223,16 @@ def run(self):
try:
with connect(self.url) as websocket:
websocket.send(json.dumps(self.devices))
for response in websocket:
while True:
try:
response = websocket.recv(5)
except TimeoutError:
if websocket.ping().wait(5):
continue
break # connection is lost
status = json.loads(response)
self.fetched.emit(status)
except WebSocketException:
except Exception: # pylint: disable=broad-exception-caught
logger.exception("Failed to fetch the modifications of TTL status.")


Expand Down Expand Up @@ -1004,6 +1015,7 @@ def __init__(
self.ttlControllerFrame.overrideChangeRequested.connect(
functools.partial(self._setTTLOverride, list(self.ttlToName))
)
self.ttlControllerFrame.restartButton.clicked.connect(self._startTTLStatusThread)
for name_, device in ttlInfo.items():
self.ttlControllerFrame.ttlWidgets[name_].levelChangeRequested.connect(
functools.partial(self._setTTLLevel, [device])
Expand Down Expand Up @@ -1138,6 +1150,10 @@ def _startTTLStatusThread(self):
devices = list(self.ttlToName)
self.ttlStatusThread = _TTLStatusThread(self.proxy_ip, self.proxy_port, devices)
self.ttlStatusThread.fetched.connect(self._updateTTLStatus, type=Qt.QueuedConnection)
self.ttlStatusThread.finished.connect(
functools.partial(self.ttlControllerFrame.restartButton.setEnabled, True),
type=Qt.QueuedConnection
)
self.ttlStatusThread.finished.connect(self.ttlStatusThread.deleteLater)
self.ttlStatusThread.start()

Expand Down
14 changes: 11 additions & 3 deletions iquip/apps/scheduler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""App module for showing the schedule for experiments."""

import datetime
import enum
import functools
import json
Expand All @@ -12,7 +13,6 @@
)
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtWidgets import QAction, QPushButton, QTableView, QVBoxLayout, QWidget
from websockets.exceptions import WebSocketException
from websockets.sync.client import connect

import qiwis
Expand Down Expand Up @@ -58,7 +58,13 @@ def run(self):
"""
try:
with connect(self.url) as websocket:
for response in websocket:
while True:
try:
response = websocket.recv(5)
except TimeoutError:
if websocket.ping().wait(5):
continue
break # connection is lost
schedule = []
for rid, info in json.loads(response).items():
expid = info["expid"]
Expand All @@ -73,7 +79,7 @@ def run(self):
arguments=expid["arguments"]
))
self.fetched.emit(schedule)
except WebSocketException:
except Exception: # pylint: disable=broad-exception-caught
logger.exception("Failed to fetch the schedule.")


Expand Down Expand Up @@ -177,6 +183,8 @@ def data(self, index: QModelIndex, role: Qt.ItemDataRole = Qt.DisplayRole) -> An
f"{key}: {round(value, 9) if isinstance(value, (int, float)) else value}"
for key, value in data.items()
])
if column == ScheduleModel.InfoFieldId.DUE_DATE and data:
return datetime.datetime.fromtimestamp(data).isoformat()
return data

def headerData(self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole) -> Any:
Expand Down
Loading

0 comments on commit 94b8eee

Please sign in to comment.