artiq/artiq/browser/experiments.py

490 lines
18 KiB
Python
Raw Normal View History

2016-05-06 21:46:42 +08:00
import asyncio
import logging
import os
from functools import partial
2016-05-08 05:22:39 +08:00
from collections import OrderedDict
2016-05-06 21:46:42 +08:00
from PyQt5 import QtCore, QtGui, QtWidgets
2016-05-07 03:06:28 +08:00
import h5py
2016-05-06 21:46:42 +08:00
from artiq import __artiq_dir__ as artiq_dir
2016-05-08 22:16:04 +08:00
from artiq.gui.tools import LayoutWidget, log_level_to_name, get_open_file_name
2016-05-06 21:46:42 +08:00
from artiq.gui.entries import argty_to_entry
from artiq.protocols import pyon
2016-05-20 22:09:49 +08:00
from artiq.master.worker import Worker, log_worker_exception
2016-05-06 21:46:42 +08:00
logger = logging.getLogger(__name__)
class _WheelFilter(QtCore.QObject):
def eventFilter(self, obj, event):
if (event.type() == QtCore.QEvent.Wheel and
event.modifiers() != QtCore.Qt.NoModifier):
event.ignore()
return True
return False
class _ArgumentEditor(QtWidgets.QTreeWidget):
2016-05-08 05:22:39 +08:00
def __init__(self, dock):
2016-05-06 21:46:42 +08:00
QtWidgets.QTreeWidget.__init__(self)
self.setColumnCount(3)
self.header().setStretchLastSection(False)
2016-05-08 05:22:39 +08:00
try:
2016-05-06 21:46:42 +08:00
set_resize_mode = self.header().setSectionResizeMode
2016-05-08 05:22:39 +08:00
except AttributeError:
2016-05-06 21:46:42 +08:00
set_resize_mode = self.header().setResizeMode
set_resize_mode(0, QtWidgets.QHeaderView.ResizeToContents)
set_resize_mode(1, QtWidgets.QHeaderView.Stretch)
set_resize_mode(2, QtWidgets.QHeaderView.ResizeToContents)
self.header().setVisible(False)
2016-05-07 03:06:28 +08:00
self.setSelectionMode(self.NoSelection)
self.setHorizontalScrollMode(self.ScrollPerPixel)
self.setVerticalScrollMode(self.ScrollPerPixel)
2016-05-06 21:46:42 +08:00
self.viewport().installEventFilter(_WheelFilter(self.viewport()))
self._groups = dict()
self._arg_to_entry_widgetitem = dict()
2016-05-08 05:22:39 +08:00
self._dock = dock
2016-05-06 21:46:42 +08:00
2016-05-08 05:22:39 +08:00
if not self._dock.arguments:
2016-05-06 21:46:42 +08:00
self.addTopLevelItem(QtWidgets.QTreeWidgetItem(["No arguments"]))
2016-05-08 05:22:39 +08:00
for name, argument in self._dock.arguments.items():
try:
entry = argty_to_entry[argument["desc"]["ty"]](argument)
except:
print(name, argument)
2016-05-06 21:46:42 +08:00
widget_item = QtWidgets.QTreeWidgetItem([name])
self._arg_to_entry_widgetitem[name] = entry, widget_item
if argument["group"] is None:
self.addTopLevelItem(widget_item)
else:
self._get_group(argument["group"]).addChild(widget_item)
self.setItemWidget(widget_item, 1, entry)
recompute_argument = QtWidgets.QToolButton()
recompute_argument.setToolTip("Re-run the experiment's build "
"method and take the default value")
2016-05-07 03:06:28 +08:00
recompute_argument.setIcon(
QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_BrowserReload))
2016-05-06 21:46:42 +08:00
recompute_argument.clicked.connect(
partial(self._recompute_argument_clicked, name))
fix_layout = LayoutWidget()
fix_layout.addWidget(recompute_argument)
self.setItemWidget(widget_item, 2, fix_layout)
widget_item = QtWidgets.QTreeWidgetItem()
self.addTopLevelItem(widget_item)
recompute_arguments = QtWidgets.QPushButton("Recompute all arguments")
2016-05-07 03:06:28 +08:00
recompute_arguments.setIcon(
QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_BrowserReload))
2016-05-06 21:46:42 +08:00
recompute_arguments.clicked.connect(self._recompute_arguments_clicked)
buttons = LayoutWidget()
buttons.addWidget(recompute_arguments, 1, 1)
2016-05-08 05:22:39 +08:00
for i, s in enumerate((1, 0, 0, 1)):
buttons.layout.setColumnStretch(i, s)
2016-05-06 21:46:42 +08:00
self.setItemWidget(widget_item, 1, buttons)
def _get_group(self, name):
if name in self._groups:
return self._groups[name]
group = QtWidgets.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 _recompute_arguments_clicked(self):
2016-05-09 05:43:33 +08:00
asyncio.ensure_future(self._dock._recompute_arguments())
2016-05-06 21:46:42 +08:00
def _recompute_argument_clicked(self, name):
asyncio.ensure_future(self._recompute_argument(name))
async def _recompute_argument(self, name):
try:
2016-05-08 05:22:39 +08:00
arginfo = await self._dock.compute_arginfo()
2016-05-06 21:46:42 +08:00
except:
logger.error("Could not recompute argument '%s' of '%s'",
2016-05-08 05:22:39 +08:00
name, self._dock.expurl, exc_info=True)
2016-05-06 21:46:42 +08:00
return
2016-05-08 05:22:39 +08:00
argument = self._dock.arguments[name]
2016-05-06 21:46:42 +08:00
procdesc = arginfo[name][0]
state = argty_to_entry[procdesc["ty"]].default_state(procdesc)
argument["desc"] = procdesc
argument["state"] = state
old_entry, widget_item = self._arg_to_entry_widgetitem[name]
old_entry.deleteLater()
entry = argty_to_entry[procdesc["ty"]](argument)
self._arg_to_entry_widgetitem[name] = entry, widget_item
self.setItemWidget(widget_item, 1, entry)
def save_state(self):
expanded = []
for k, v in self._groups.items():
if v.isExpanded():
expanded.append(k)
return {"expanded": expanded}
def restore_state(self, state):
for e in state["expanded"]:
try:
self._groups[e].setExpanded(True)
except KeyError:
pass
log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
class _ExperimentDock(QtWidgets.QMdiSubWindow):
sigClosed = QtCore.pyqtSignal()
2016-05-09 06:32:15 +08:00
def __init__(self, area, expurl, arguments):
2016-05-06 21:46:42 +08:00
QtWidgets.QMdiSubWindow.__init__(self)
self.setWindowTitle(expurl)
self.setWindowIcon(QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_FileDialogContentsView))
2016-05-08 05:56:39 +08:00
self.setAcceptDrops(True)
2016-05-06 21:46:42 +08:00
self.layout = QtWidgets.QGridLayout()
top_widget = QtWidgets.QWidget()
top_widget.setLayout(self.layout)
self.setWidget(top_widget)
self.layout.setSpacing(5)
self.layout.setContentsMargins(5, 5, 5, 5)
2016-05-08 05:22:39 +08:00
self._area = area
self._run_task = None
2016-05-06 21:46:42 +08:00
self.expurl = expurl
2016-05-08 05:22:39 +08:00
self.arguments = arguments
self.options = {"log_level": logging.WARNING}
2016-05-06 21:46:42 +08:00
self.argeditor = _ArgumentEditor(self)
self.layout.addWidget(self.argeditor, 0, 0, 1, 5)
self.layout.setRowStretch(0, 1)
log_level = QtWidgets.QComboBox()
log_level.addItems(log_levels)
log_level.setCurrentIndex(1)
log_level.setToolTip("Minimum level for log entry production")
log_level_label = QtWidgets.QLabel("Logging level:")
log_level_label.setToolTip("Minimum level for log message production")
self.layout.addWidget(log_level_label, 3, 0)
self.layout.addWidget(log_level, 3, 1)
log_level.setCurrentIndex(log_levels.index(
2016-05-07 03:06:28 +08:00
log_level_to_name(self.options["log_level"])))
2016-05-06 21:46:42 +08:00
def update_log_level(index):
2016-05-07 03:06:28 +08:00
self.options["log_level"] = getattr(logging,
log_level.currentText())
2016-05-06 21:46:42 +08:00
log_level.currentIndexChanged.connect(update_log_level)
self.log_level = log_level
load = QtWidgets.QPushButton("Set arguments")
load.setToolTip("Set arguments from currently selected HDF5 "
"file (Ctrl+Space)")
load.setIcon(QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_DialogApplyButton))
load.setShortcut("CTRL+SPACE")
load.clicked.connect(self._load_clicked)
self.layout.addWidget(load, 1, 4)
2016-05-09 06:32:15 +08:00
run = QtWidgets.QPushButton("Analyze")
run.setIcon(QtWidgets.QApplication.style().standardIcon(
2016-05-06 21:46:42 +08:00
QtWidgets.QStyle.SP_DialogOkButton))
2016-05-09 06:32:15 +08:00
run.setToolTip("Run analysis stage (Ctrl+Return)")
run.setShortcut("CTRL+RETURN")
run.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
self.layout.addWidget(run, 2, 4)
run.clicked.connect(self._run_clicked)
self._run = run
2016-05-09 06:32:15 +08:00
terminate = QtWidgets.QPushButton("Terminate")
terminate.setIcon(QtWidgets.QApplication.style().standardIcon(
2016-05-06 21:46:42 +08:00
QtWidgets.QStyle.SP_DialogCancelButton))
2016-05-09 06:32:15 +08:00
terminate.setToolTip("Terminate analysis (Ctrl+Backspace)")
terminate.setShortcut("CTRL+BACKSPACE")
terminate.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
self.layout.addWidget(terminate, 3, 4)
terminate.clicked.connect(self._terminate_clicked)
terminate.setEnabled(False)
self._terminate = terminate
2016-05-06 21:46:42 +08:00
2016-05-08 05:56:39 +08:00
def dragEnterEvent(self, ev):
if ev.mimeData().hasFormat("text/uri-list"):
ev.acceptProposedAction()
def dropEvent(self, ev):
for uri in ev.mimeData().urls():
if uri.scheme() == "file":
2016-06-12 13:11:36 +08:00
logger.debug("Loading HDF5 arguments from %s", uri.path())
asyncio.ensure_future(self.load_hdf5_task(uri.path()))
break
2016-05-08 05:56:39 +08:00
async def compute_arginfo(self):
return await self._area.compute_arginfo(self.expurl)
2016-05-08 05:22:39 +08:00
async def _recompute_arguments(self, overrides={}):
2016-05-06 21:46:42 +08:00
try:
arginfo = await self.compute_arginfo()
2016-05-06 21:46:42 +08:00
except:
2016-05-08 05:22:39 +08:00
logger.error("Could not recompute arguments of '%s'",
2016-05-06 21:46:42 +08:00
self.expurl, exc_info=True)
2016-05-08 05:22:39 +08:00
return
for k, v in overrides.items():
2016-05-25 22:36:27 +08:00
# Some values (e.g. scans) may have multiple defaults in a list
if isinstance(arginfo[k][0].get("default"), list):
arginfo[k][0]["default"].insert(0, v)
else:
arginfo[k][0]["default"] = v
2016-05-08 05:22:39 +08:00
self.arguments = self._area.initialize_submission_arguments(arginfo)
2016-05-06 21:46:42 +08:00
2016-05-08 05:22:39 +08:00
self.argeditor.deleteLater()
self.argeditor = _ArgumentEditor(self)
self.layout.addWidget(self.argeditor, 0, 0, 1, 5)
2016-05-06 21:46:42 +08:00
async def load_hdf5_task(self, filename):
2016-05-06 21:46:42 +08:00
try:
with h5py.File(filename, "r") as f:
expid = f["expid"][()]
expid = pyon.decode(expid)
arguments = expid["arguments"]
except:
logger.error("Could not retrieve expid from HDF5 file",
exc_info=True)
return
try:
self.log_level.setCurrentIndex(log_levels.index(
log_level_to_name(expid["log_level"])))
except:
logger.error("Could not set submission options from HDF5 expid",
exc_info=True)
return
2016-05-08 05:22:39 +08:00
await self._recompute_arguments(arguments)
def _load_clicked(self):
if self._area.dataset is None:
return
asyncio.ensure_future(self.load_hdf5_task(self._area.dataset))
def _run_clicked(self):
2016-05-10 18:49:15 +08:00
class_name, file = self.expurl.split("@", maxsplit=1)
expid = {
"repo_rev": "N/A",
"file": file,
"class_name": class_name,
"log_level": self.options["log_level"],
"arguments": {
name: argty_to_entry[argument["desc"]["ty"]].state_to_value(
argument["state"])
for name, argument in self.arguments.items()},
}
2016-05-13 00:44:03 +08:00
self._run_task = asyncio.ensure_future(self._get_run_task(expid))
self._run.setEnabled(False)
self._terminate.setEnabled(True)
2016-05-13 00:44:03 +08:00
def done(fut):
2016-06-12 13:11:36 +08:00
logger.debug("Analysis done")
2016-05-13 00:44:03 +08:00
self._run_task = None
self._run.setEnabled(True)
self._terminate.setEnabled(False)
2016-05-13 00:44:03 +08:00
self._run_task.add_done_callback(done)
async def _get_run_task(self, expid):
2016-07-03 12:20:23 +08:00
logger.info("Running '%s'...", self.expurl)
worker = Worker(self._area.worker_handlers)
2016-05-08 05:22:39 +08:00
try:
await worker.build(rid=None, pipeline_name="browser",
wd=os.path.abspath("."),
expid=expid, priority=0)
await worker.analyze()
2016-05-08 05:22:39 +08:00
except:
2016-05-31 04:04:49 +08:00
logger.error("Failed to run '%s'", self.expurl)
2016-05-20 22:09:49 +08:00
log_worker_exception()
2016-07-03 12:20:23 +08:00
else:
logger.info("Finished running '%s'", self.expurl)
2016-05-13 00:44:03 +08:00
finally:
await worker.close()
2016-05-08 05:22:39 +08:00
def _terminate_clicked(self):
2016-05-08 05:22:39 +08:00
try:
2016-05-13 00:44:03 +08:00
self._run_task.cancel()
2016-05-08 05:22:39 +08:00
except:
logger.error("Unexpected failure terminating '%s'",
2016-05-08 05:22:39 +08:00
self.expurl, exc_info=True)
2016-05-06 21:46:42 +08:00
def closeEvent(self, event):
self.sigClosed.emit()
QtWidgets.QMdiSubWindow.closeEvent(self, event)
def save_state(self):
return {
"argeditor": self.argeditor.save_state(),
"geometry": bytes(self.saveGeometry()),
2016-05-07 03:06:28 +08:00
"options": self.options,
2016-05-06 21:46:42 +08:00
}
def restore_state(self, state):
self.argeditor.restore_state(state["argeditor"])
self.restoreGeometry(QtCore.QByteArray(state["geometry"]))
2016-05-07 03:06:28 +08:00
self.options = state["options"]
2016-05-06 21:46:42 +08:00
2016-05-10 18:49:15 +08:00
class LocalDatasetDB:
def __init__(self, datasets_sub):
self.datasets_sub = datasets_sub
datasets_sub.add_setmodel_callback(self.init)
2016-05-10 18:49:15 +08:00
def init(self, data):
self._data = data
def get(self, key):
2016-05-13 00:44:03 +08:00
return self._data.backing_store[key][1]
2016-05-10 18:49:15 +08:00
def update(self, mod):
self.datasets_sub.update(mod)
2016-05-10 18:49:15 +08:00
2016-05-06 21:46:42 +08:00
class ExperimentsArea(QtWidgets.QMdiArea):
2016-05-08 05:22:39 +08:00
def __init__(self, root, datasets_sub):
2016-05-06 21:46:42 +08:00
QtWidgets.QMdiArea.__init__(self)
self.pixmap = QtGui.QPixmap(os.path.join(
artiq_dir, "gui", "logo20.svg"))
self.current_dir = root
self.dataset = None
2016-05-06 21:46:42 +08:00
self.open_experiments = []
self._ddb = LocalDatasetDB(datasets_sub)
2016-05-10 18:49:15 +08:00
2016-05-08 05:22:39 +08:00
self.worker_handlers = {
2016-05-08 05:56:39 +08:00
"get_device_db": lambda: None,
"get_device": lambda k: None,
2016-05-10 18:49:15 +08:00
"get_dataset": self._ddb.get,
"update_dataset": self._ddb.update,
2016-05-08 05:22:39 +08:00
}
def dataset_changed(self, path):
self.dataset = path
def dataset_activated(self, path):
sub = self.currentSubWindow()
if sub is None:
return
asyncio.ensure_future(sub.load_hdf5_task(path))
2016-05-08 22:12:43 +08:00
def mousePressEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton:
self.select_experiment()
2016-05-08 22:12:43 +08:00
2016-05-06 21:46:42 +08:00
def paintEvent(self, event):
QtWidgets.QMdiArea.paintEvent(self, event)
painter = QtGui.QPainter(self.viewport())
x = (self.width() - self.pixmap.width())//2
y = (self.height() - self.pixmap.height())//2
painter.setOpacity(0.5)
painter.drawPixmap(x, y, self.pixmap)
def save_state(self):
2016-05-08 05:22:39 +08:00
return {"experiments": [{
"expurl": dock.expurl,
"arguments": dock.arguments,
"dock": dock.save_state(),
} for dock in self.open_experiments]}
2016-05-06 21:46:42 +08:00
def restore_state(self, state):
if self.open_experiments:
raise NotImplementedError
for ex_state in state["experiments"]:
2016-05-08 05:22:39 +08:00
dock = self.open_experiment(ex_state["expurl"],
ex_state["arguments"])
dock.restore_state(ex_state["dock"])
2016-05-06 21:46:42 +08:00
def select_experiment(self):
2016-05-08 05:22:39 +08:00
asyncio.ensure_future(self._select_experiment_task())
async def _select_experiment_task(self):
try:
2016-05-08 22:16:04 +08:00
file = await get_open_file_name(
2016-05-08 05:22:39 +08:00
self, "Open experiment", self.current_dir,
"Experiments (*.py);;All files (*.*)")
except asyncio.CancelledError:
2016-05-06 21:46:42 +08:00
return
2016-05-08 05:22:39 +08:00
self.current_dir = os.path.dirname(file)
2016-06-12 13:11:36 +08:00
logger.debug("Opening experiment %s", file)
2016-05-09 05:32:22 +08:00
try:
description = await self.examine(file)
except:
logger.error("Could not examine experiment '%s'",
file, exc_info=True)
return
2016-05-08 05:22:39 +08:00
for class_name, class_desc in description.items():
expurl = "{}@{}".format(class_name, file)
arguments = self.initialize_submission_arguments(
class_desc["arginfo"])
self.open_experiment(expurl, arguments)
def initialize_submission_arguments(self, arginfo):
arguments = OrderedDict()
for name, (procdesc, group) in arginfo.items():
state = argty_to_entry[procdesc["ty"]].default_state(procdesc)
arguments[name] = {
"desc": procdesc,
"group": group,
"state": state # mutated by entries
}
return arguments
async def examine(self, file):
worker = Worker(self.worker_handlers)
try:
return await worker.examine("examine", file)
finally:
await worker.close()
2016-05-06 21:46:42 +08:00
2016-05-08 05:22:39 +08:00
async def compute_arginfo(self, expurl):
class_name, file = expurl.split("@", maxsplit=1)
2016-05-09 05:32:22 +08:00
try:
desc = await self.examine(file)
except:
logger.error("Could not examine experiment '%s'",
file, exc_info=True)
return
2016-05-08 05:22:39 +08:00
return desc[class_name]["arginfo"]
def open_experiment(self, expurl, arguments):
2016-05-06 21:46:42 +08:00
try:
2016-05-09 06:32:15 +08:00
dock = _ExperimentDock(self, expurl, arguments)
2016-05-06 21:46:42 +08:00
except:
logger.warning("Failed to create experiment dock for %s, "
2016-05-08 05:22:39 +08:00
"retrying with arguments reset", expurl,
2016-05-06 21:46:42 +08:00
exc_info=True)
2016-05-09 06:32:15 +08:00
dock = _ExperimentDock(self, expurl, {})
2016-05-08 05:22:39 +08:00
asyncio.ensure_future(dock._recompute_arguments())
2016-05-06 21:46:42 +08:00
self.addSubWindow(dock)
dock.show()
2016-05-08 05:22:39 +08:00
dock.sigClosed.connect(partial(self.on_dock_closed, dock))
self.open_experiments.append(dock)
2016-05-06 21:46:42 +08:00
return dock
2016-05-08 05:22:39 +08:00
def on_dock_closed(self, dock):
self.open_experiments.remove(dock)