Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: m-labs/artiq
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 2b506a3c4bd7
Choose a base ref
...
head repository: m-labs/artiq
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 66927da70c8b
Choose a head ref
  • 10 commits
  • 4 files changed
  • 1 contributor

Commits on May 7, 2016

  1. browser: tiny simplification

    jordens committed May 7, 2016
    Copy the full SHA
    0c74034 View commit details
  2. Copy the full SHA
    d99d19d View commit details
  3. Copy the full SHA
    9f1bef1 View commit details
  4. Copy the full SHA
    9ef2826 View commit details
  5. Copy the full SHA
    77b84a7 View commit details
  6. Copy the full SHA
    ee81608 View commit details
  7. Copy the full SHA
    47c7b04 View commit details
  8. Copy the full SHA
    5332c19 View commit details
  9. Copy the full SHA
    4016e5a View commit details
  10. Copy the full SHA
    66927da View commit details
Showing with 442 additions and 48 deletions.
  1. +406 −0 artiq/browser/experiments.py
  2. +7 −27 artiq/browser/files.py
  3. +12 −20 artiq/frontend/artiq_browser.py
  4. +17 −1 artiq/gui/tools.py
406 changes: 406 additions & 0 deletions artiq/browser/experiments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,406 @@
import asyncio
import logging
import os
from functools import partial
from collections import OrderedDict

from PyQt5 import QtCore, QtGui, QtWidgets
import h5py

from artiq import __artiq_dir__ as artiq_dir
from artiq.gui.tools import LayoutWidget, log_level_to_name, getOpenFileName
from artiq.gui.entries import argty_to_entry
from artiq.protocols import pyon
from artiq.master.worker import Worker

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):
def __init__(self, dock):
QtWidgets.QTreeWidget.__init__(self)
self.setColumnCount(3)
self.header().setStretchLastSection(False)
try:
set_resize_mode = self.header().setSectionResizeMode
except AttributeError:
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)
self.setSelectionMode(self.NoSelection)
self.setHorizontalScrollMode(self.ScrollPerPixel)
self.setVerticalScrollMode(self.ScrollPerPixel)

self.viewport().installEventFilter(_WheelFilter(self.viewport()))

self._groups = dict()
self._arg_to_entry_widgetitem = dict()
self._dock = dock

if not self._dock.arguments:
self.addTopLevelItem(QtWidgets.QTreeWidgetItem(["No arguments"]))

for name, argument in self._dock.arguments.items():
try:
entry = argty_to_entry[argument["desc"]["ty"]](argument)
except:
print(name, argument)
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")
recompute_argument.setIcon(
QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_BrowserReload))
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")
recompute_arguments.setIcon(
QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_BrowserReload))
recompute_arguments.clicked.connect(self._recompute_arguments_clicked)

buttons = LayoutWidget()
buttons.addWidget(recompute_arguments, 1, 1)
for i, s in enumerate((1, 0, 0, 1)):
buttons.layout.setColumnStretch(i, s)
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):
asyncio.ensure_future(self._dock.recompute_arguments())

def _recompute_argument_clicked(self, name):
asyncio.ensure_future(self._recompute_argument(name))

async def _recompute_argument(self, name):
try:
arginfo = await self._dock.compute_arginfo()
except:
logger.error("Could not recompute argument '%s' of '%s'",
name, self._dock.expurl, exc_info=True)
return
argument = self._dock.arguments[name]

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()

def __init__(self, area, expurl, arguments, worker_handlers):
QtWidgets.QMdiSubWindow.__init__(self)
self.setWindowTitle(expurl)
self.setWindowIcon(QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_FileDialogContentsView))
self.setAcceptDrops(True)

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)

self._area = area
self.expurl = expurl
self.worker_handlers = worker_handlers
self.arguments = arguments

self.argeditor = _ArgumentEditor(self)
self.layout.addWidget(self.argeditor, 0, 0, 1, 5)
self.layout.setRowStretch(0, 1)

self.options = {"log_level": logging.WARNING}

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(
log_level_to_name(self.options["log_level"])))

def update_log_level(index):
self.options["log_level"] = getattr(logging,
log_level.currentText())
log_level.currentIndexChanged.connect(update_log_level)
self.log_level = log_level

submit = QtWidgets.QPushButton("Submit")
submit.setIcon(QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_DialogOkButton))
submit.setToolTip("Schedule the experiment (Ctrl+Return)")
submit.setShortcut("CTRL+RETURN")
submit.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
self.layout.addWidget(submit, 1, 4, 2, 1)
submit.clicked.connect(self.submit_clicked)

reqterm = QtWidgets.QPushButton("Terminate instances")
reqterm.setIcon(QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_DialogCancelButton))
reqterm.setToolTip("Request termination of instances (Ctrl+Backspace)")
reqterm.setShortcut("CTRL+BACKSPACE")
reqterm.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
self.layout.addWidget(reqterm, 3, 4)
reqterm.clicked.connect(self.reqterm_clicked)

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":
logger.info("loading HDF5 arguments from %s", uri.path())
asyncio.ensure_future(self._load_hdf5_task(uri.path()))
ev.acceptProposedAction()

async def _recompute_arguments(self, overrides={}):
try:
arginfo = await self._area.compute_arginfo(self.expurl)
except:
logger.error("Could not recompute arguments of '%s'",
self.expurl, exc_info=True)
return
for k, v in overrides.items():
arginfo[k][0]["default"] = v
self.arguments = self._area.initialize_submission_arguments(arginfo)

self.argeditor.deleteLater()
self.argeditor = _ArgumentEditor(self)
self.layout.addWidget(self.argeditor, 0, 0, 1, 5)

async def _load_hdf5_task(self, filename):
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

await self._recompute_arguments(arguments)

def submit_clicked(self):
try:
pass # TODO
except:
# May happen when experiment has been removed
# from repository/explist
logger.error("Failed to submit '%s'",
self.expurl, exc_info=True)

def reqterm_clicked(self):
try:
pass # TODO
except:
# May happen when experiment has been removed
# from repository/explist
logger.error("Failed to request termination of instances of '%s'",
self.expurl, exc_info=True)

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()),
"options": self.options,
}

def restore_state(self, state):
self.argeditor.restore_state(state["argeditor"])
self.restoreGeometry(QtCore.QByteArray(state["geometry"]))
self.options = state["options"]


class ExperimentsArea(QtWidgets.QMdiArea):
def __init__(self, root, datasets_sub):
QtWidgets.QMdiArea.__init__(self)
self.pixmap = QtGui.QPixmap(os.path.join(
artiq_dir, "gui", "logo20.svg"))
self.current_dir = root
self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
self.setFocusPolicy(QtCore.Qt.StrongFocus)

action = QtWidgets.QAction("&Open experiment", self)
action.setShortcut(QtGui.QKeySequence("CTRL+o"))
action.setShortcutContext(QtCore.Qt.WidgetShortcut)
action.triggered.connect(self._select_experiment)
self.addAction(action)

self.open_experiments = []

self.worker_handlers = {
"get_device_db": lambda: None,
"get_device": lambda k: None,
"get_dataset": lambda k: 0, # TODO
"update_dataset": lambda k, v: None,
}

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):
return {"experiments": [{
"expurl": dock.expurl,
"arguments": dock.arguments,
"dock": dock.save_state(),
} for dock in self.open_experiments]}

def restore_state(self, state):
if self.open_experiments:
raise NotImplementedError
for ex_state in state["experiments"]:
dock = self.open_experiment(ex_state["expurl"],
ex_state["arguments"])
dock.restore_state(ex_state["dock"])

def _select_experiment(self):
asyncio.ensure_future(self._select_experiment_task())

async def _select_experiment_task(self):
try:
file = await getOpenFileName(
self, "Open experiment", self.current_dir,
"Experiments (*.py);;All files (*.*)")
except asyncio.CancelledError:
return
self.current_dir = os.path.dirname(file)
logger.info("opening experiment %s", file)
description = await self.examine(file)
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()

async def compute_arginfo(self, expurl):
class_name, file = expurl.split("@", maxsplit=1)
desc = await self.examine(file)
return desc[class_name]["arginfo"]

def open_experiment(self, expurl, arguments):
try:
dock = _ExperimentDock(self, expurl, arguments,
self.worker_handlers)
except:
logger.warning("Failed to create experiment dock for %s, "
"retrying with arguments reset", expurl,
exc_info=True)
dock = _ExperimentDock(self, expurl, {}, self.worker_handlers)
asyncio.ensure_future(dock._recompute_arguments())
self.addSubWindow(dock)
dock.show()
dock.sigClosed.connect(partial(self.on_dock_closed, dock))
self.open_experiments.append(dock)
return dock

def on_dock_closed(self, dock):
self.open_experiments.remove(dock)
34 changes: 7 additions & 27 deletions artiq/browser/files.py
Original file line number Diff line number Diff line change
@@ -14,12 +14,10 @@ def open_h5(info):
info.suffix() == "h5"):
return
try:
f = h5py.File(info.filePath(), "r")
return h5py.File(info.filePath(), "r")
except:
logger.warning("unable to read HDF5 file %s", info.filePath(),
exc_info=True)
return
return f


class ThumbnailIconProvider(QtWidgets.QFileIconProvider):
@@ -86,7 +84,7 @@ def wheelEvent(self, ev):


class FilesDock(QtWidgets.QDockWidget):
def __init__(self, datasets, main_window, browse_root="", select=None):
def __init__(self, datasets, browse_root="", select=None):
QtWidgets.QDockWidget.__init__(self, "Files")
self.setObjectName("Files")
self.setFeatures(self.DockWidgetMovable | self.DockWidgetFloatable)
@@ -95,7 +93,6 @@ def __init__(self, datasets, main_window, browse_root="", select=None):
self.setWidget(self.splitter)

self.datasets = datasets
self.main_window = main_window

self.model = QtWidgets.QFileSystemModel()
self.model.setFilter(QtCore.QDir.Drives | QtCore.QDir.NoDotAndDotDot |
@@ -129,7 +126,6 @@ def __init__(self, datasets, main_window, browse_root="", select=None):
self.rl.setModel(self.model)
self.rl.selectionModel().currentChanged.connect(
self.list_current_changed)
self.rl.activated.connect(self.open_experiment)
self.splitter.addWidget(self.rl)

self.restore_selected = select is None
@@ -153,26 +149,9 @@ def list_current_changed(self, current, previous):
with f:
if "datasets" not in f:
return
rd = dict((k, (True, v.value)) for k, v in f["datasets"].items())
rd = {k: (True, v.value) for k, v in f["datasets"].items()}
self.datasets.init(rd)

def open_experiment(self, current):
info = self.model.fileInfo(current)
if info.isDir():
self.rl.setRootIndex(current)
idx = self.rt.model().mapFromSource(current)
self.rt.expand(idx)
self.rt.setCurrentIndex(idx)
return
f = open_h5(info)
if not f:
return
logger.info("loading experiment for %s", info.filePath())
with f:
if "expid" not in f:
return
expid = pyon.decode(f["expid"].value)

def select_dir(self, path):
if not os.path.exists(path):
return
@@ -187,10 +166,11 @@ def scroll_when_loaded(p):
return
self.model.directoryLoaded.disconnect(scroll_when_loaded)
QtCore.QTimer.singleShot(
100, lambda:
self.rt.scrollTo(
100,
lambda: self.rt.scrollTo(
self.rt.model().mapFromSource(self.model.index(path)),
self.rt.PositionAtCenter))
self.rt.PositionAtCenter)
)
self.model.directoryLoaded.connect(scroll_when_loaded)
idx = self.rt.model().mapFromSource(idx)
self.rt.expand(idx)
32 changes: 12 additions & 20 deletions artiq/frontend/artiq_browser.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
import asyncio
import atexit
import os
import logging

from PyQt5 import QtCore, QtGui, QtWidgets
from quamash import QEventLoop
@@ -12,6 +13,10 @@
from artiq.tools import verbosity_args, init_logger, atexit_register_coroutine
from artiq.gui import state, applets, models
from artiq.browser import datasets, files
from artiq.browser import experiments


logger = logging.getLogger(__name__)


def get_argparser():
@@ -60,20 +65,6 @@ def restore_state(self, state):
self.restoreState(QtCore.QByteArray(state["state"]))


class MdiArea(QtWidgets.QMdiArea):
def __init__(self):
QtWidgets.QMdiArea.__init__(self)
self.pixmap = QtGui.QPixmap(os.path.join(artiq_dir, "gui", "logo20.svg"))

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 main():
# initialize application
args = get_argparser().parse_args()
@@ -94,7 +85,13 @@ def main():
status_bar = QtWidgets.QStatusBar()
main_window.setStatusBar(status_bar)

d_files = files.FilesDock(datasets_sub, main_window, args.browse_root,
mdi_area = experiments.ExperimentsArea(args.browse_root, datasets_sub)
mdi_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
mdi_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
main_window.setCentralWidget(mdi_area)
smgr.register(mdi_area)

d_files = files.FilesDock(datasets_sub, args.browse_root,
select=args.select)
smgr.register(d_files)

@@ -105,11 +102,6 @@ def main():
d_datasets = datasets.DatasetsDock(datasets_sub)
smgr.register(d_datasets)

mdi_area = MdiArea()
mdi_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
mdi_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
main_window.setCentralWidget(mdi_area)

main_window.addDockWidget(QtCore.Qt.LeftDockWidgetArea, d_files)
main_window.addDockWidget(QtCore.Qt.BottomDockWidgetArea, d_applets)
main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, d_datasets)
18 changes: 17 additions & 1 deletion artiq/gui/tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import logging

from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5 import QtCore, QtWidgets


def log_level_to_name(level):
@@ -45,3 +46,18 @@ def __init__(self, parent=None):

def addWidget(self, item, row=0, col=0, rowspan=1, colspan=1):
self.layout.addWidget(item, row, col, rowspan, colspan)


async def getOpenFileName(parent, caption, dir, filter):
"""like QtWidgets.QFileDialog.getOpenFileName(), but a coroutine"""
dialog = QtWidgets.QFileDialog(parent, caption, dir, filter)
dialog.setFileMode(dialog.ExistingFile)
dialog.setAcceptMode(dialog.AcceptOpen)
fut = asyncio.Future()

def on_accept():
fut.set_result(dialog.selectedFiles()[0])
dialog.accepted.connect(on_accept)
dialog.rejected.connect(fut.cancel)
dialog.open()
return await fut