forked from M-Labs/artiq
1
0
Fork 0
artiq/artiq/gui/explorer.py

398 lines
14 KiB
Python
Raw Normal View History

2015-05-24 20:24:07 +08:00
import asyncio
2015-10-20 18:11:50 +08:00
import logging
2015-05-24 20:24:07 +08:00
2015-05-23 01:25:33 +08:00
from quamash import QtGui, QtCore
from pyqtgraph import dockarea
2015-05-23 01:25:33 +08:00
from pyqtgraph import LayoutWidget
2015-05-24 20:24:07 +08:00
from artiq.protocols.sync_struct import Subscriber
2015-07-18 03:28:46 +08:00
from artiq.protocols import pyon
from artiq.gui.tools import DictSyncModel
2015-07-21 23:23:32 +08:00
from artiq.gui.scan import ScanController
2015-10-27 17:59:34 +08:00
from artiq.gui.shortcuts import ShortcutManager
2015-05-24 20:24:07 +08:00
class _ExplistModel(DictSyncModel):
def __init__(self, explorer, parent, init):
self.explorer = explorer
2015-05-24 20:24:07 +08:00
DictSyncModel.__init__(self,
["Experiment"],
parent, init)
def sort_key(self, k, v):
return k
def convert(self, k, v, column):
return k
def __setitem__(self, k, v):
DictSyncModel.__setitem__(self, k, v)
if k == self.explorer.selected_key:
self.explorer.update_selection(k, k)
2015-07-18 03:28:46 +08:00
class _FreeValueEntry(QtGui.QLineEdit):
def __init__(self, procdesc):
QtGui.QLineEdit.__init__(self)
if "default" in procdesc:
2015-08-05 13:35:28 +08:00
self.set_argument_value(procdesc["default"])
2015-07-18 03:28:46 +08:00
def get_argument_value(self):
return pyon.decode(self.text())
2015-08-05 13:35:28 +08:00
def set_argument_value(self, value):
self.setText(pyon.encode(value))
2015-07-18 03:28:46 +08:00
2015-07-18 22:25:08 +08:00
class _BooleanEntry(QtGui.QCheckBox):
def __init__(self, procdesc):
QtGui.QCheckBox.__init__(self)
if "default" in procdesc:
2015-08-05 13:35:28 +08:00
self.set_argument_value(procdesc["default"])
2015-07-18 22:25:08 +08:00
def get_argument_value(self):
return self.isChecked()
2015-08-05 13:35:28 +08:00
def set_argument_value(self, value):
self.setChecked(value)
2015-07-18 22:25:08 +08:00
class _EnumerationEntry(QtGui.QComboBox):
def __init__(self, procdesc):
QtGui.QComboBox.__init__(self)
self.choices = procdesc["choices"]
self.addItems(self.choices)
if "default" in procdesc:
2015-08-05 13:35:28 +08:00
self.set_argument_value(procdesc["default"])
2015-07-18 22:25:08 +08:00
def get_argument_value(self):
return self.choices[self.currentIndex()]
2015-08-05 13:35:28 +08:00
def set_argument_value(self, value):
idx = self.choices.index(value)
self.setCurrentIndex(idx)
2015-07-18 22:25:08 +08:00
class _NumberEntry(QtGui.QDoubleSpinBox):
def __init__(self, procdesc):
QtGui.QDoubleSpinBox.__init__(self)
self.scale = procdesc["scale"]
self.setDecimals(procdesc["ndecimals"])
self.setSingleStep(procdesc["step"]/self.scale)
2015-07-18 22:25:08 +08:00
if procdesc["min"] is not None:
self.setMinimum(procdesc["min"]/self.scale)
2015-08-25 00:56:19 +08:00
else:
self.setMinimum(float("-inf"))
2015-07-18 22:25:08 +08:00
if procdesc["max"] is not None:
self.setMaximum(procdesc["max"]/self.scale)
2015-08-25 00:56:19 +08:00
else:
self.setMaximum(float("inf"))
if procdesc["unit"]:
self.setSuffix(" " + procdesc["unit"])
2015-07-18 22:25:08 +08:00
if "default" in procdesc:
2015-08-05 13:35:28 +08:00
self.set_argument_value(procdesc["default"])
2015-07-18 22:25:08 +08:00
def get_argument_value(self):
return self.value()*self.scale
2015-07-18 22:25:08 +08:00
2015-08-05 13:35:28 +08:00
def set_argument_value(self, value):
self.setValue(value/self.scale)
2015-08-05 13:35:28 +08:00
2015-07-18 22:25:08 +08:00
class _StringEntry(QtGui.QLineEdit):
def __init__(self, procdesc):
QtGui.QLineEdit.__init__(self)
if "default" in procdesc:
2015-08-05 13:35:28 +08:00
self.set_argument_value(procdesc["default"])
2015-07-18 22:25:08 +08:00
def get_argument_value(self):
return self.text()
2015-08-05 13:35:28 +08:00
def set_argument_value(self, value):
self.setText(value)
2015-07-18 22:25:08 +08:00
2015-07-18 03:28:46 +08:00
_procty_to_entry = {
2015-07-18 22:25:08 +08:00
"FreeValue": _FreeValueEntry,
"BooleanValue": _BooleanEntry,
"EnumerationValue": _EnumerationEntry,
"NumberValue": _NumberEntry,
2015-07-21 23:23:32 +08:00
"StringValue": _StringEntry,
"Scannable": ScanController
2015-07-18 03:28:46 +08:00
}
class _ArgumentEditor(QtGui.QTreeWidget):
2015-10-27 17:59:34 +08:00
def __init__(self, main_window):
QtGui.QTreeWidget.__init__(self)
self.setColumnCount(2)
2015-08-24 23:46:54 +08:00
self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
self.header().setVisible(False)
self.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
2015-10-27 17:59:34 +08:00
self.main_window = main_window
2015-08-24 23:46:54 +08:00
self._groups = dict()
self.set_arguments([])
2015-08-24 23:46:54 +08:00
def clear(self):
QtGui.QTreeWidget.clear(self)
self._groups.clear()
def _get_group(self, name):
if name in self._groups:
return self._groups[name]
group = QtGui.QTreeWidgetItem([name, ""])
for c in 0, 1:
group.setBackground(c, QtGui.QBrush(QtGui.QColor(100, 100, 100)))
group.setForeground(c, QtGui.QBrush(QtGui.QColor(220, 220, 255)))
font = group.font(c)
font.setBold(True)
group.setFont(c, font)
self.addTopLevelItem(group)
self._groups[name] = group
return group
def set_arguments(self, arguments):
self.clear()
2015-07-18 03:28:46 +08:00
if not arguments:
self.addTopLevelItem(QtGui.QTreeWidgetItem(["No arguments", ""]))
2015-07-18 03:28:46 +08:00
self._args_to_entries = dict()
2015-08-24 23:46:54 +08:00
for n, (name, (procdesc, group)) in enumerate(arguments):
2015-07-18 03:28:46 +08:00
entry = _procty_to_entry[procdesc["ty"]](procdesc)
self._args_to_entries[name] = entry
widget_item = QtGui.QTreeWidgetItem([name, ""])
2015-08-24 23:46:54 +08:00
if group is None:
self.addTopLevelItem(widget_item)
else:
self._get_group(group).addChild(widget_item)
self.setItemWidget(widget_item, 1, entry)
2015-08-05 13:35:28 +08:00
def get_argument_values(self, show_error_message):
2015-07-18 03:28:46 +08:00
r = dict()
for arg, entry in self._args_to_entries.items():
try:
r[arg] = entry.get_argument_value()
except Exception as e:
2015-08-05 13:35:28 +08:00
if show_error_message:
2015-10-27 17:59:34 +08:00
msgbox = QtGui.QMessageBox(self.main_window)
2015-08-05 13:35:28 +08:00
msgbox.setWindowTitle("Error")
msgbox.setText("Failed to obtain value for argument '{}':\n{}"
.format(arg, str(e)))
2015-08-05 13:35:28 +08:00
msgbox.setStandardButtons(QtGui.QMessageBox.Ok)
msgbox.show()
2015-07-18 03:28:46 +08:00
return None
return r
2015-08-05 13:35:28 +08:00
def set_argument_values(self, arguments, ignore_errors):
for arg, value in arguments.items():
try:
entry = self._args_to_entries[arg]
entry.set_argument_value(value)
except:
if not ignore_errors:
raise
2015-08-24 23:46:54 +08:00
def save_state(self):
expanded = []
for k, v in self._groups.items():
if v.isExpanded():
expanded.append(k)
argument_values = self.get_argument_values(False)
return {
"expanded": expanded,
"argument_values": argument_values
}
def restore_state(self, state):
self.set_argument_values(state["argument_values"], True)
for e in state["expanded"]:
try:
self._groups[e].setExpanded(True)
except KeyError:
pass
2015-07-18 03:28:46 +08:00
class ExplorerDock(dockarea.Dock):
def __init__(self, main_window, status_bar, schedule_ctl, repository_ctl):
dockarea.Dock.__init__(self, "Explorer", size=(1500, 500))
2015-05-23 01:25:33 +08:00
2015-10-27 17:59:34 +08:00
self.main_window = main_window
2015-05-24 20:24:07 +08:00
self.status_bar = status_bar
self.schedule_ctl = schedule_ctl
2015-07-18 03:28:46 +08:00
self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal)
self.addWidget(self.splitter)
2015-05-23 01:25:33 +08:00
grid = LayoutWidget()
2015-07-18 03:28:46 +08:00
self.splitter.addWidget(grid)
2015-05-23 01:25:33 +08:00
2015-05-24 20:24:07 +08:00
self.el = QtGui.QListView()
self.el.selectionChanged = self._selection_changed
self.selected_key = None
2015-05-24 20:24:07 +08:00
grid.addWidget(self.el, 0, 0, colspan=4)
2015-05-23 01:25:33 +08:00
2015-05-24 20:24:07 +08:00
self.datetime = QtGui.QDateTimeEdit()
self.datetime.setDisplayFormat("MMM d yyyy hh:mm:ss")
self.datetime.setDate(QtCore.QDate.currentDate())
self.datetime.dateTimeChanged.connect(self.enable_duedate)
2015-05-28 17:20:58 +08:00
self.datetime_en = QtGui.QCheckBox("Due date:")
2015-10-24 16:00:20 +08:00
grid.addWidget(self.datetime_en, 1, 0, colspan=2)
grid.addWidget(self.datetime, 1, 2, colspan=2)
2015-05-23 01:25:33 +08:00
2015-05-24 20:24:07 +08:00
self.pipeline = QtGui.QLineEdit()
2015-08-05 13:35:28 +08:00
self.pipeline.setText("main")
2015-10-24 16:00:20 +08:00
grid.addWidget(QtGui.QLabel("Pipeline:"), 2, 0, colspan=2)
grid.addWidget(self.pipeline, 2, 2, colspan=2)
self.priority = QtGui.QSpinBox()
self.priority.setRange(-99, 99)
grid.addWidget(QtGui.QLabel("Priority:"), 3, 0)
grid.addWidget(self.priority, 3, 1)
2015-05-23 01:25:33 +08:00
2015-05-28 17:20:58 +08:00
self.flush = QtGui.QCheckBox("Flush")
2015-10-20 18:09:36 +08:00
self.flush.setToolTip("Flush the pipeline before starting the experiment")
2015-10-24 16:00:20 +08:00
grid.addWidget(self.flush, 3, 2)
2015-10-20 18:11:50 +08:00
self.log_level = QtGui.QComboBox()
self.log_level.addItems(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])
2015-10-20 18:11:50 +08:00
self.log_level.setCurrentIndex(1)
self.log_level.setToolTip("Minimum level for log entry production")
2015-10-24 16:00:20 +08:00
grid.addWidget(self.log_level, 3, 3)
2015-05-23 01:25:33 +08:00
submit = QtGui.QPushButton("Submit")
submit.setShortcut("CTRL+RETURN")
submit.setToolTip("Schedule the selected experiment (CTRL+ENTER)")
2015-10-24 16:00:20 +08:00
grid.addWidget(submit, 4, 0, colspan=4)
2015-05-24 20:24:07 +08:00
submit.clicked.connect(self.submit_clicked)
2015-05-23 01:25:33 +08:00
2015-10-27 17:59:34 +08:00
self.argeditor = _ArgumentEditor(self.main_window)
self.splitter.addWidget(self.argeditor)
2015-07-18 03:28:46 +08:00
self.splitter.setSizes([grid.minimumSizeHint().width(), 1000])
2015-10-27 17:59:34 +08:00
self.argeditor_states = dict()
self.shortcuts = ShortcutManager(self.main_window, self)
self.el.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
edit_shortcuts_action = QtGui.QAction("Edit shortcuts", self.el)
edit_shortcuts_action.triggered.connect(self.edit_shortcuts)
self.el.addAction(edit_shortcuts_action)
scan_repository_action = QtGui.QAction("(Re)scan repository HEAD",
self.el)
def scan_repository():
asyncio.ensure_future(repository_ctl.scan_async())
self.status_bar.showMessage("Requested repository scan")
scan_repository_action.triggered.connect(scan_repository)
self.el.addAction(scan_repository_action)
2015-07-18 03:28:46 +08:00
def update_selection(self, selected, deselected):
2015-08-05 13:35:28 +08:00
if deselected:
2015-10-27 17:59:34 +08:00
self.argeditor_states[deselected] = self.argeditor.save_state()
2015-08-05 13:35:28 +08:00
2015-07-18 03:28:46 +08:00
if selected:
expinfo = self.explist_model.backing_store[selected]
self.argeditor.set_arguments(expinfo["arguments"])
2015-10-27 17:59:34 +08:00
if selected in self.argeditor_states:
self.argeditor.restore_state(self.argeditor_states[selected])
self.splitter.insertWidget(1, self.argeditor)
self.selected_key = selected
def _sel_to_key(self, selection):
selection = selection.indexes()
if selection:
row = selection[0].row()
return self.explist_model.row_to_key[row]
else:
return None
def _selection_changed(self, selected, deselected):
self.update_selection(self._sel_to_key(selected),
self._sel_to_key(deselected))
2015-05-24 20:24:07 +08:00
2015-08-05 13:35:28 +08:00
def save_state(self):
idx = self.el.selectedIndexes()
if idx:
row = idx[0].row()
key = self.explist_model.row_to_key[row]
2015-10-27 17:59:34 +08:00
self.argeditor_states[key] = self.argeditor.save_state()
return {
"argeditor": self.argeditor_states,
"shortcuts": self.shortcuts.save_state()
}
2015-08-05 13:35:28 +08:00
def restore_state(self, state):
2015-10-27 17:59:34 +08:00
try:
argeditor_states = state["argeditor"]
shortcuts_state = state["shortcuts"]
except KeyError:
return
self.argeditor_states = argeditor_states
self.shortcuts.restore_state(shortcuts_state)
2015-08-05 13:35:28 +08:00
def enable_duedate(self):
self.datetime_en.setChecked(True)
2015-10-03 19:28:57 +08:00
async def sub_connect(self, host, port):
2015-05-24 20:24:07 +08:00
self.explist_subscriber = Subscriber("explist",
self.init_explist_model)
2015-10-03 19:28:57 +08:00
await self.explist_subscriber.connect(host, port)
2015-05-24 20:24:07 +08:00
2015-10-03 19:28:57 +08:00
async def sub_close(self):
await self.explist_subscriber.close()
2015-05-24 20:24:07 +08:00
def init_explist_model(self, init):
self.explist_model = _ExplistModel(self, self.el, init)
2015-05-24 20:24:07 +08:00
self.el.setModel(self.explist_model)
return self.explist_model
2015-10-27 17:59:34 +08:00
async def submit_task(self, pipeline_name, file, class_name, arguments,
priority, due_date, flush):
2015-05-24 20:24:07 +08:00
expid = {
2015-10-20 18:11:50 +08:00
"log_level": getattr(logging, self.log_level.currentText()),
2015-08-07 15:51:56 +08:00
"repo_rev": None,
2015-05-24 20:24:07 +08:00
"file": file,
2015-07-15 17:08:12 +08:00
"class_name": class_name,
2015-05-24 20:24:07 +08:00
"arguments": arguments,
}
2015-10-03 19:28:57 +08:00
rid = await self.schedule_ctl.submit(pipeline_name, expid,
priority, due_date, flush)
2015-05-24 20:24:07 +08:00
self.status_bar.showMessage("Submitted RID {}".format(rid))
2015-10-27 17:59:34 +08:00
def submit(self, pipeline, key, priority, due_date, flush):
# TODO: refactor explorer and cleanup.
# Argument editors should immediately modify the global state.
expinfo = self.explist_model.backing_store[key]
if key == self.selected_key:
arguments = self.argeditor.get_argument_values(True)
if arguments is None:
# There has been an error. Displaying the error message box
# was done by argeditor.
return
else:
try:
arguments = self.argeditor_states[key]["argument_values"]
except KeyError:
arguments = dict()
2015-10-30 00:50:18 +08:00
asyncio.ensure_future(self.submit_task(pipeline, expinfo["file"],
2015-10-27 17:59:34 +08:00
expinfo["class_name"],
2015-10-30 00:50:18 +08:00
arguments, priority, due_date,
2015-10-27 17:59:34 +08:00
flush))
2015-05-24 20:24:07 +08:00
def submit_clicked(self):
if self.selected_key is not None:
2015-05-24 20:24:07 +08:00
if self.datetime_en.isChecked():
due_date = self.datetime.dateTime().toMSecsSinceEpoch()/1000
else:
due_date = None
2015-10-27 17:59:34 +08:00
self.submit(self.pipeline.text(),
self.selected_key,
self.priority.value(),
due_date,
self.flush.isChecked())
def edit_shortcuts(self):
experiments = sorted(self.explist_model.backing_store.keys())
self.shortcuts.edit(experiments)