From c382fac8f24850bc7c10001d92dd2687796faaaa Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Fri, 27 Nov 2015 19:30:05 +0800 Subject: [PATCH] gui: experiment docks (WIP) --- artiq/frontend/artiq_gui.py | 36 +- artiq/gui/experiments.py | 351 +++++++++++++++++ artiq/gui/explorer.py | 369 ++---------------- artiq/gui/scan.py | 19 +- artiq/language/environment.py | 19 +- examples/master/repository/arguments_demo.py | 4 +- .../coredevice_examples/photon_histogram.py | 4 +- .../simple/blink_forever.py | 15 +- .../coredevice_examples/transport.py | 8 +- 9 files changed, 448 insertions(+), 377 deletions(-) create mode 100644 artiq/gui/experiments.py diff --git a/artiq/frontend/artiq_gui.py b/artiq/frontend/artiq_gui.py index 0e1cf5dad..1ab99299b 100755 --- a/artiq/frontend/artiq_gui.py +++ b/artiq/frontend/artiq_gui.py @@ -13,7 +13,8 @@ from pyqtgraph import dockarea from artiq.tools import * from artiq.protocols.pc_rpc import AsyncioClient from artiq.gui.models import ModelSubscriber -from artiq.gui import state, explorer, moninj, datasets, schedule, log, console +from artiq.gui import (state, experiments, explorer, + moninj, datasets, schedule, log, console) def get_argparser(): @@ -85,23 +86,26 @@ def main(): # initialize main window win = MainWindow(app, args.server) - area = dockarea.DockArea() - smgr.register(area) + dock_area = dockarea.DockArea() + smgr.register(dock_area) smgr.register(win) - win.setCentralWidget(area) + win.setCentralWidget(dock_area) status_bar = QtGui.QStatusBar() status_bar.showMessage("Connected to {}".format(args.server)) win.setStatusBar(status_bar) # create UI components - d_explorer = explorer.ExplorerDock(win, status_bar, + expmgr = experiments.ExperimentManager(status_bar, dock_area, + sub_clients["explist"], + sub_clients["schedule"], + rpc_clients["schedule"]) + d_explorer = explorer.ExplorerDock(win, status_bar, expmgr, sub_clients["explist"], - sub_clients["schedule"], rpc_clients["schedule"], rpc_clients["repository"]) smgr.register(d_explorer) - d_datasets = datasets.DatasetsDock(win, area, sub_clients["datasets"]) + d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"]) smgr.register(d_datasets) if os.name != "nt": @@ -112,7 +116,7 @@ def main(): d_schedule = schedule.ScheduleDock( status_bar, rpc_clients["schedule"], sub_clients["schedule"]) - logmgr = log.LogDockManager(area, sub_clients["log"]) + logmgr = log.LogDockManager(dock_area, sub_clients["log"]) smgr.register(logmgr) d_console = console.ConsoleDock(sub_clients["datasets"], @@ -120,14 +124,14 @@ def main(): # lay out docks if os.name != "nt": - area.addDock(d_ttl_dds.dds_dock, "top") - area.addDock(d_ttl_dds.ttl_dock, "above", d_ttl_dds.dds_dock) - area.addDock(d_datasets, "above", d_ttl_dds.ttl_dock) + dock_area.addDock(d_ttl_dds.dds_dock, "top") + dock_area.addDock(d_ttl_dds.ttl_dock, "above", d_ttl_dds.dds_dock) + dock_area.addDock(d_datasets, "above", d_ttl_dds.ttl_dock) else: - area.addDock(d_datasets, "top") - area.addDock(d_explorer, "above", d_datasets) - area.addDock(d_console, "bottom") - area.addDock(d_schedule, "above", d_console) + dock_area.addDock(d_datasets, "top") + dock_area.addDock(d_explorer, "above", d_datasets) + dock_area.addDock(d_console, "bottom") + dock_area.addDock(d_schedule, "above", d_console) # load/initialize state smgr.load() @@ -137,7 +141,7 @@ def main(): # create first log dock if not already in state d_log0 = logmgr.first_log_dock() if d_log0 is not None: - area.addDock(d_log0, "right", d_explorer) + dock_area.addDock(d_log0, "right", d_explorer) # run win.show() diff --git a/artiq/gui/experiments.py b/artiq/gui/experiments.py new file mode 100644 index 000000000..48317f204 --- /dev/null +++ b/artiq/gui/experiments.py @@ -0,0 +1,351 @@ +import logging +import asyncio +from functools import partial +from collections import OrderedDict + +from quamash import QtGui, QtCore + +from pyqtgraph import dockarea + +from artiq.gui.scan import ScanController + + +logger = logging.getLogger(__name__) + + +class _StringEntry(QtGui.QLineEdit): + def __init__(self, argdesc): + QtGui.QLineEdit.__init__(self) + + @staticmethod + def default(argdesc): + return "" + + def get_argument_value(self): + return self.text() + + def set_argument_value(self, value): + self.setText(value) + + +class _BooleanEntry(QtGui.QCheckBox): + def __init__(self, argdesc): + QtGui.QCheckBox.__init__(self) + + @staticmethod + def default(argdesc): + return False + + def get_argument_value(self): + return self.isChecked() + + def set_argument_value(self, value): + self.setChecked(value) + + +class _EnumerationEntry(QtGui.QComboBox): + def __init__(self, argdesc): + QtGui.QComboBox.__init__(self) + self.choices = argdesc["choices"] + self.addItems(self.choices) + + @staticmethod + def default(argdesc): + return argdesc["choices"][0] + + def get_argument_value(self): + return self.choices[self.currentIndex()] + + def set_argument_value(self, value): + idx = self.choices.index(value) + self.setCurrentIndex(idx) + + +class _NumberEntry(QtGui.QDoubleSpinBox): + def __init__(self, argdesc): + QtGui.QDoubleSpinBox.__init__(self) + self.scale = argdesc["scale"] + self.setDecimals(argdesc["ndecimals"]) + self.setSingleStep(argdesc["step"]/self.scale) + if argdesc["min"] is not None: + self.setMinimum(argdesc["min"]/self.scale) + else: + self.setMinimum(float("-inf")) + if argdesc["max"] is not None: + self.setMaximum(argdesc["max"]/self.scale) + else: + self.setMaximum(float("inf")) + if argdesc["unit"]: + self.setSuffix(" " + argdesc["unit"]) + + @staticmethod + def default(argdesc): + return 0.0 + + def get_argument_value(self): + return self.value()*self.scale + + def set_argument_value(self, value): + self.setValue(value/self.scale) + + +_argty_to_entry = { + "PYONValue": _StringEntry, + "BooleanValue": _BooleanEntry, + "EnumerationValue": _EnumerationEntry, + "NumberValue": _NumberEntry, + "StringValue": _StringEntry, + "Scannable": ScanController +} + + +class _ArgumentEditor(QtGui.QTreeWidget): + def __init__(self, arguments): + QtGui.QTreeWidget.__init__(self) + self.setColumnCount(2) + self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents) + self.header().setVisible(False) + self.setSelectionMode(QtGui.QAbstractItemView.NoSelection) + self.setHorizontalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) + self.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel) + + self._groups = dict() + self._args_to_entries = dict() + + if not arguments: + self.addTopLevelItem(QtGui.QTreeWidgetItem(["No arguments", ""])) + + for n, (name, (argdesc, group, value)) in enumerate(arguments.items()): + entry = _argty_to_entry[argdesc["ty"]](argdesc) + entry.set_argument_value(value) + self._args_to_entries[name] = entry + + widget_item = QtGui.QTreeWidgetItem([name, ""]) + if group is None: + self.addTopLevelItem(widget_item) + else: + self._get_group(group).addChild(widget_item) + self.setItemWidget(widget_item, 1, entry) + + 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 get_argument_values(self): + return {arg: entry.get_argument_value() + for arg, entry in self._args_to_entries.items()} + + def save_state(self): + expanded = [] + for k, v in self._groups.items(): + if v.isExpanded(): + expanded.append(k) + argument_values = self.get_argument_values() + return { + "expanded": expanded, + "argument_values": argument_values + } + + def restore_state(self, state): + for arg, value in state["argument_values"].items(): + try: + entry = self._args_to_entries[arg] + entry.set_argument_value(value) + except: + logger.warning("failed to restore value of argument %s", arg, + exc_info=True) + for e in state["expanded"]: + try: + self._groups[e].setExpanded(True) + except KeyError: + pass + + +class _ExperimentDock(dockarea.Dock): + def __init__(self, manager, expname): + dockarea.Dock.__init__(self, "Experiment: " + expname, + closable=True, size=(1500, 500)) + self.manager = manager + self.expname = expname + + self.argeditor = _ArgumentEditor( + manager.get_submission_arguments(expname)) + self.addWidget(self.argeditor, 0, 0, colspan=4) + + self.datetime = QtGui.QDateTimeEdit() + self.datetime.setDisplayFormat("MMM d yyyy hh:mm:ss") + self.datetime.setDate(QtCore.QDate.currentDate()) + self.datetime.dateTimeChanged.connect( + lambda: self.datetime_en.setChecked(True)) + self.datetime_en = QtGui.QCheckBox("Due date:") + self.addWidget(self.datetime_en, 1, 0, colspan=2) + self.addWidget(self.datetime, 1, 2, colspan=2) + + self.pipeline = QtGui.QLineEdit() + self.pipeline.setText("main") + self.addWidget(QtGui.QLabel("Pipeline:"), 2, 0, colspan=2) + self.addWidget(self.pipeline, 2, 2, colspan=2) + + self.priority = QtGui.QSpinBox() + self.priority.setRange(-99, 99) + self.addWidget(QtGui.QLabel("Priority:"), 3, 0) + self.addWidget(self.priority, 3, 1) + + self.flush = QtGui.QCheckBox("Flush") + self.flush.setToolTip("Flush the pipeline before starting the experiment") + self.addWidget(self.flush, 3, 2) + + self.log_level = QtGui.QComboBox() + self.log_level.addItems(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]) + self.log_level.setCurrentIndex(1) + self.log_level.setToolTip("Minimum level for log entry production") + self.addWidget(self.log_level, 3, 3) + + submit = QtGui.QPushButton("Submit") + submit.setToolTip("Schedule the selected experiment (Ctrl+Return)") + self.addWidget(submit, 4, 0, colspan=4) + submit.clicked.connect(self.submit_clicked) + + def submit_clicked(self): + self.manager.submit(self.expname) + + +class ExperimentManager: + def __init__(self, status_bar, dock_area, + explist_sub, schedule_sub, + schedule_ctl): + self.status_bar = status_bar + self.dock_area = dock_area + self.schedule_ctl = schedule_ctl + + self.submission_scheduling = dict() + self.submission_options = dict() + self.submission_arguments = dict() + + self.explist = dict() + explist_sub.add_setmodel_callback(self.set_explist_model) + self.schedule = dict() + schedule_sub.add_setmodel_callback(self.set_schedule_model) + + self.open_experiments = dict() + + def set_explist_model(self, model): + self.explist = model.backing_store + + def set_schedule_model(self, model): + self.schedule = model.backing_store + + def get_submission_scheduling(self, expname): + if expname in self.submission_scheduling: + return self.submission_scheduling[expname] + else: + scheduling = { + "pipeline_name": "main", + "priority": 0, + "due_date": None, + "flush": False + } + self.submission_scheduling[expname] = scheduling + return scheduling + + def get_submission_options(self, expname): + if expname in self.submission_options: + return self.submission_options[expname] + else: + options = { + "log_level": logging.WARNING, + "repo_rev": None + } + self.submission_options = options + return options + + def get_submission_arguments(self, expname): + if expname in self.submission_arguments: + return self.submission_arguments[expname] + else: + arguments = OrderedDict() + arginfo = self.explist[expname]["arguments"] + for name, (procdesc, group) in arginfo: + argdesc = dict(procdesc) + if "default" in argdesc: + value = argdesc["default"] + del argdesc["default"] + else: + value = _argty_to_entry[argdesc["ty"]].default(argdesc) + arguments[name] = argdesc, group, value + self.submission_arguments[expname] = arguments + return arguments + + def open_experiment(self, expname): + if expname in self.open_experiments: + return + dock = _ExperimentDock(self, expname) + self.open_experiments[expname] = dock + self.dock_area.addDock(dock) + self.dock_area.floatDock(dock) + dock.sigClosed.connect(partial(self.on_dock_closed, expname)) + + def on_dock_closed(self, expname): + del self.open_experiments[expname] + + async def _submit_task(self, *args): + rid = await self.schedule_ctl.submit(*args) + self.status_bar.showMessage("Submitted RID {}".format(rid)) + + def submit(self, expname): + expinfo = self.explist[expname] + scheduling = self.get_submission_scheduling(expname) + options = self.get_submission_options(expname) + arguments = self.get_submission_arguments(expname) + argument_values = {k: v[2] for k, v in arguments.items()} + + expid = { + "log_level": options["log_level"], + "repo_rev": options["repo_rev"], + "file": expinfo["file"], + "class_name": expinfo["class_name"], + "arguments": argument_values, + } + asyncio.ensure_future(self._submit_task( + scheduling["pipeline_name"], + expid, + scheduling["priority"], scheduling["due_date"], + scheduling["flush"])) + + async def _request_term_multiple(self, rids): + for rid in rids: + try: + await self.schedule_ctl.request_termination(rid) + except: + # May happen if the experiment has terminated by itself + # while we were terminating others. + logger.debug("failed to request termination of RID %d", + rid, exc_info=True) + + def request_inst_term(self, expname): + expinfo = self.explist[expname] + rids = [] + for rid, desc in self.schedule.items(): + expid = desc["expid"] + if ("repo_rev" in expid # only consider runs from repository + and expid["file"] == expinfo["file"] + and expid["class_name"] == expinfo["class_name"]): + rids.append(rid) + asyncio.ensure_future(self._request_term_multiple(rids)) + + def save_state(self): + return dict() + + def restore_state(self): + if self.open_experiments: + raise NotImplementedError diff --git a/artiq/gui/explorer.py b/artiq/gui/explorer.py index 71ebcf5bc..9d5ae8a4e 100644 --- a/artiq/gui/explorer.py +++ b/artiq/gui/explorer.py @@ -5,284 +5,63 @@ from quamash import QtGui, QtCore from pyqtgraph import dockarea from pyqtgraph import LayoutWidget -from artiq.protocols import pyon from artiq.gui.models import DictSyncTreeSepModel -from artiq.gui.scan import ScanController from artiq.gui.shortcuts import ShortcutManager class Model(DictSyncTreeSepModel): def __init__(self, init): self.explorer = None - DictSyncTreeSepModel.__init__(self, - "/", - ["Experiment"], - init) + DictSyncTreeSepModel.__init__(self, "/", ["Experiment"], init) def __setitem__(self, k, v): DictSyncTreeSepModel.__setitem__(self, k, v) - if self.explorer is not None: - if k == self.explorer.selected_key: - self.explorer.update_selection(k, k) - - -class _FreeValueEntry(QtGui.QLineEdit): - def __init__(self, procdesc): - QtGui.QLineEdit.__init__(self) - if "default" in procdesc: - self.set_argument_value(procdesc["default"]) - - def get_argument_value(self): - return pyon.decode(self.text()) - - def set_argument_value(self, value): - self.setText(pyon.encode(value)) - - -class _BooleanEntry(QtGui.QCheckBox): - def __init__(self, procdesc): - QtGui.QCheckBox.__init__(self) - if "default" in procdesc: - self.set_argument_value(procdesc["default"]) - - def get_argument_value(self): - return self.isChecked() - - def set_argument_value(self, value): - self.setChecked(value) - - -class _EnumerationEntry(QtGui.QComboBox): - def __init__(self, procdesc): - QtGui.QComboBox.__init__(self) - self.choices = procdesc["choices"] - self.addItems(self.choices) - if "default" in procdesc: - self.set_argument_value(procdesc["default"]) - - def get_argument_value(self): - return self.choices[self.currentIndex()] - - def set_argument_value(self, value): - idx = self.choices.index(value) - self.setCurrentIndex(idx) - - -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) - if procdesc["min"] is not None: - self.setMinimum(procdesc["min"]/self.scale) - else: - self.setMinimum(float("-inf")) - if procdesc["max"] is not None: - self.setMaximum(procdesc["max"]/self.scale) - else: - self.setMaximum(float("inf")) - if procdesc["unit"]: - self.setSuffix(" " + procdesc["unit"]) - if "default" in procdesc: - self.set_argument_value(procdesc["default"]) - - def get_argument_value(self): - return self.value()*self.scale - - def set_argument_value(self, value): - self.setValue(value/self.scale) - - -class _StringEntry(QtGui.QLineEdit): - def __init__(self, procdesc): - QtGui.QLineEdit.__init__(self) - if "default" in procdesc: - self.set_argument_value(procdesc["default"]) - - def get_argument_value(self): - return self.text() - - def set_argument_value(self, value): - self.setText(value) - - -_procty_to_entry = { - "FreeValue": _FreeValueEntry, - "BooleanValue": _BooleanEntry, - "EnumerationValue": _EnumerationEntry, - "NumberValue": _NumberEntry, - "StringValue": _StringEntry, - "Scannable": ScanController -} - - -class _ArgumentEditor(QtGui.QTreeWidget): - def __init__(self, main_window): - QtGui.QTreeWidget.__init__(self) - self.setColumnCount(2) - self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents) - self.header().setVisible(False) - self.setSelectionMode(QtGui.QAbstractItemView.NoSelection) - - self.main_window = main_window - self._groups = dict() - self.set_arguments([]) - - 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() - - if not arguments: - self.addTopLevelItem(QtGui.QTreeWidgetItem(["No arguments", ""])) - - self._args_to_entries = dict() - for n, (name, (procdesc, group)) in enumerate(arguments): - entry = _procty_to_entry[procdesc["ty"]](procdesc) - self._args_to_entries[name] = entry - - widget_item = QtGui.QTreeWidgetItem([name, ""]) - if group is None: - self.addTopLevelItem(widget_item) - else: - self._get_group(group).addChild(widget_item) - self.setItemWidget(widget_item, 1, entry) - - def get_argument_values(self, show_error_message): - r = dict() - for arg, entry in self._args_to_entries.items(): - try: - r[arg] = entry.get_argument_value() - except Exception as e: - if show_error_message: - msgbox = QtGui.QMessageBox(self.main_window) - msgbox.setWindowTitle("Error") - msgbox.setText("Failed to obtain value for argument '{}':\n{}" - .format(arg, str(e))) - msgbox.setStandardButtons(QtGui.QMessageBox.Ok) - msgbox.show() - return None - return r - - 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 - - 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 + # TODO + #if self.explorer is not None and k == self.explorer.selected_key: + # self.explorer.update_selection(k, k) class ExplorerDock(dockarea.Dock): - def __init__(self, main_window, status_bar, - explist_sub, schedule_sub, - schedule_ctl, repository_ctl): + def __init__(self, main_window, status_bar, exp_manager, + explist_sub, schedule_ctl, repository_ctl): dockarea.Dock.__init__(self, "Explorer", size=(1500, 500)) self.main_window = main_window self.status_bar = status_bar - self.schedule_sub = schedule_sub + self.exp_manager = exp_manager self.schedule_ctl = schedule_ctl - self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal) - self.addWidget(self.splitter) - - grid = LayoutWidget() - self.splitter.addWidget(grid) - self.el = QtGui.QTreeView() self.el.setHeaderHidden(True) self.el.setSelectionBehavior(QtGui.QAbstractItemView.SelectItems) - self.el.selectionChanged = self._selection_changed - self.selected_key = None - grid.addWidget(self.el, 0, 0, colspan=4) + self.addWidget(self.el, 0, 0, colspan=2) + self.el.doubleClicked.connect(self.open_clicked) - 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) - self.datetime_en = QtGui.QCheckBox("Due date:") - grid.addWidget(self.datetime_en, 1, 0, colspan=2) - grid.addWidget(self.datetime, 1, 2, colspan=2) - - self.pipeline = QtGui.QLineEdit() - self.pipeline.setText("main") - 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) - - self.flush = QtGui.QCheckBox("Flush") - self.flush.setToolTip("Flush the pipeline before starting the experiment") - grid.addWidget(self.flush, 3, 2) - - self.log_level = QtGui.QComboBox() - self.log_level.addItems(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]) - self.log_level.setCurrentIndex(1) - self.log_level.setToolTip("Minimum level for log entry production") - grid.addWidget(self.log_level, 3, 3) + open = QtGui.QPushButton("Open") + open.setToolTip("Open the selected experiment (Return)") + self.addWidget(open, 1, 0) + open.clicked.connect(self.open_clicked) submit = QtGui.QPushButton("Submit") submit.setToolTip("Schedule the selected experiment (Ctrl+Return)") - grid.addWidget(submit, 4, 0, colspan=4) + self.addWidget(submit, 1, 1) submit.clicked.connect(self.submit_clicked) - self.argeditor = _ArgumentEditor(self.main_window) - self.splitter.addWidget(self.argeditor) - self.splitter.setSizes([grid.minimumSizeHint().width(), 1000]) - self.argeditor_states = dict() self.explist_model = Model(dict()) explist_sub.add_setmodel_callback(self.set_model) - self.shortcuts = ShortcutManager(self.main_window, self) self.el.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) + open_action = QtGui.QAction("Open", self.el) + open_action.triggered.connect(self.open_clicked) + open_action.setShortcut("RETURN") + self.el.addAction(open_action) submit_action = QtGui.QAction("Submit", self.el) submit_action.triggered.connect(self.submit_clicked) submit_action.setShortcut("CTRL+RETURN") self.el.addAction(submit_action) reqterm_action = QtGui.QAction("Request termination of instances", self.el) - reqterm_action.triggered.connect(self.request_inst_term) + reqterm_action.triggered.connect(self.reqterm_clicked) reqterm_action.setShortcut("CTRL+BACKSPACE") self.el.addAction(reqterm_action) @@ -306,118 +85,40 @@ class ExplorerDock(dockarea.Dock): self.explist_model = model self.el.setModel(model) - def update_selection(self, selected, deselected): - if deselected is not None: - self.argeditor_states[deselected] = self.argeditor.save_state() - - if selected is not None: - expinfo = self.explist_model.backing_store[selected] - self.argeditor.set_arguments(expinfo["arguments"]) - 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() + def _get_selected_expname(self): + selection = self.el.selectedIndexes() if selection: return self.explist_model.index_to_key(selection[0]) else: return None - def _selection_changed(self, selected, deselected): - self.update_selection(self._sel_to_key(selected), - self._sel_to_key(deselected)) + def open_clicked(self): + expname = self._get_selected_expname() + if expname is not None: + self.exp_manager.open_experiment(expname) + + def submit_clicked(self): + expname = self._get_selected_expname() + if expname is not None: + self.exp_manager.submit(expname) + + def reqterm_clicked(self): + expname = self._get_selected_expname() + if expname is not None: + self.exp_manager.request_inst_term(expname) def save_state(self): - idx = self.el.selectedIndexes() - if idx: - key = self.explist_model.index_to_key(idx[0]) - if key is not None: - self.argeditor_states[key] = self.argeditor.save_state() return { - "argeditor": self.argeditor_states, "shortcuts": self.shortcuts.save_state() } def restore_state(self, state): try: - argeditor_states = state["argeditor"] shortcuts_state = state["shortcuts"] except KeyError: return - self.argeditor_states = argeditor_states self.shortcuts.restore_state(shortcuts_state) - def enable_duedate(self): - self.datetime_en.setChecked(True) - - async def submit_task(self, pipeline_name, file, class_name, arguments, - priority, due_date, flush): - expid = { - "log_level": getattr(logging, self.log_level.currentText()), - "repo_rev": None, - "file": file, - "class_name": class_name, - "arguments": arguments, - } - rid = await self.schedule_ctl.submit(pipeline_name, expid, - priority, due_date, flush) - self.status_bar.showMessage("Submitted RID {}".format(rid)) - - 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() - asyncio.ensure_future(self.submit_task(pipeline, expinfo["file"], - expinfo["class_name"], - arguments, priority, due_date, - flush)) - - def submit_clicked(self): - if self.selected_key is not None: - if self.datetime_en.isChecked(): - due_date = self.datetime.dateTime().toMSecsSinceEpoch()/1000 - else: - due_date = None - self.submit(self.pipeline.text(), - self.selected_key, - self.priority.value(), - due_date, - self.flush.isChecked()) - - async def request_term_multiple(self, rids): - for rid in rids: - try: - await self.schedule_ctl.request_termination(rid) - except: - pass - - def request_inst_term(self): - if self.selected_key is not None: - expinfo = self.explist_model.backing_store[self.selected_key] - if self.schedule_sub.model is not None: - current_schedule = self.schedule_sub.model.backing_store - rids = [] - for rid, desc in current_schedule.items(): - expid = desc["expid"] - if ("repo_rev" in expid # only consider runs from repository - and expid["file"] == expinfo["file"] - and expid["class_name"] == expinfo["class_name"]): - rids.append(rid) - asyncio.ensure_future(self.request_term_multiple(rids)) - def edit_shortcuts(self): experiments = sorted(self.explist_model.backing_store.keys()) self.shortcuts.edit(experiments) diff --git a/artiq/gui/scan.py b/artiq/gui/scan.py index da172dc26..c63b4cc90 100644 --- a/artiq/gui/scan.py +++ b/artiq/gui/scan.py @@ -56,18 +56,18 @@ class _Range(LayoutWidget): class ScanController(LayoutWidget): - def __init__(self, procdesc): + def __init__(self, argdesc): LayoutWidget.__init__(self) self.stack = QtGui.QStackedWidget() self.addWidget(self.stack, 1, 0, colspan=4) - self.scale = procdesc["scale"] + self.scale = argdesc["scale"] - gmin, gmax = procdesc["global_min"], procdesc["global_max"] - gstep = procdesc["global_step"] - unit = procdesc["unit"] - ndecimals = procdesc["ndecimals"] + gmin, gmax = argdesc["global_min"], argdesc["global_max"] + gstep = argdesc["global_step"] + unit = argdesc["unit"] + ndecimals = argdesc["ndecimals"] self.v_noscan = QtGui.QDoubleSpinBox() self.v_noscan.setDecimals(ndecimals) @@ -110,10 +110,9 @@ class ScanController(LayoutWidget): radiobuttons.addButton(b) b.toggled.connect(self.select_page) - if "default" in procdesc: - self.set_argument_value(procdesc["default"]) - else: - self.noscan.setChecked(True) + @staticmethod + def default(argdesc): + return {"ty": "NoScan", "value": 0.0} def select_page(self): if self.noscan.isChecked(): diff --git a/artiq/language/environment.py b/artiq/language/environment.py index 8628da8d5..8c2711b47 100644 --- a/artiq/language/environment.py +++ b/artiq/language/environment.py @@ -1,9 +1,11 @@ from collections import OrderedDict from inspect import isclass +from artiq.protocols import pyon + __all__ = ["NoDefault", - "FreeValue", "BooleanValue", "EnumerationValue", + "PYONValue", "BooleanValue", "EnumerationValue", "NumberValue", "StringValue", "HasEnvironment", "Experiment", "EnvExperiment", "is_experiment"] @@ -40,9 +42,16 @@ class _SimpleArgProcessor: return d -class FreeValue(_SimpleArgProcessor): - """An argument that can be an arbitrary Python value.""" - pass +class PYONValue(_SimpleArgProcessor): + """An argument that can be any PYON-serializable value.""" + def process(self, x): + return pyon.decode(x) + + def describe(self): + d = {"ty": self.__class__.__name__} + if hasattr(self, "default_value"): + d["default"] = pyon.encode(self.default_value) + return d class BooleanValue(_SimpleArgProcessor): @@ -166,7 +175,7 @@ class HasEnvironment: if self.__parent is not None and key not in self.__kwargs: return self.__parent.get_argument(key, processor, group) if processor is None: - processor = FreeValue() + processor = PYONValue() self.requested_args[key] = processor, group try: argval = self.__kwargs[key] diff --git a/examples/master/repository/arguments_demo.py b/examples/master/repository/arguments_demo.py index a1a903c7a..7b65e0bb9 100644 --- a/examples/master/repository/arguments_demo.py +++ b/examples/master/repository/arguments_demo.py @@ -37,7 +37,7 @@ class SubComponent2(HasEnvironment): class ArgumentsDemo(EnvExperiment): def build(self): - self.setattr_argument("free_value", FreeValue(None)) + self.setattr_argument("pyon_value", PYONValue(None)) self.setattr_argument("number", NumberValue(42e-6, unit="us", scale=1e-6, ndecimals=4)) @@ -59,7 +59,7 @@ class ArgumentsDemo(EnvExperiment): logging.info("logging test: info") logging.debug("logging test: debug") - print(self.free_value) + print(self.pyon_value) print(self.boolean) print(self.enum) print(self.number) diff --git a/examples/master/repository/coredevice_examples/photon_histogram.py b/examples/master/repository/coredevice_examples/photon_histogram.py index a42552d53..84e6f62c2 100644 --- a/examples/master/repository/coredevice_examples/photon_histogram.py +++ b/examples/master/repository/coredevice_examples/photon_histogram.py @@ -13,8 +13,8 @@ class PhotonHistogram(EnvExperiment): self.setattr_device("bdd_sw") self.setattr_device("pmt") - self.setattr_argument("nbins", FreeValue(100)) - self.setattr_argument("repeats", FreeValue(100)) + self.setattr_argument("nbins", NumberValue(100)) + self.setattr_argument("repeats", NumberValue(100)) self.setattr_dataset("cool_f", 230*MHz) self.setattr_dataset("detect_f", 220*MHz) diff --git a/examples/master/repository/coredevice_examples/simple/blink_forever.py b/examples/master/repository/coredevice_examples/simple/blink_forever.py index 9abd3968f..75ff7e524 100644 --- a/examples/master/repository/coredevice_examples/simple/blink_forever.py +++ b/examples/master/repository/coredevice_examples/simple/blink_forever.py @@ -4,10 +4,17 @@ from artiq import * class BlinkForever(EnvExperiment): def build(self): self.setattr_device("core") - self.setattr_device("led") + self.setattr_device("ttl0") + self.setattr_device("ttl1") + + def hello(self, i): + print("Hello world", i) @kernel def run(self): - while True: - self.led.pulse(100*ms) - delay(100*ms) + for i in range(80000): + self.ttl0.pulse(40*us) + self.ttl1.pulse(40*us) + delay(40*us) + for i in range(7): + self.hello(i) diff --git a/examples/master/repository/coredevice_examples/transport.py b/examples/master/repository/coredevice_examples/transport.py index 57100d244..97c5abb7e 100644 --- a/examples/master/repository/coredevice_examples/transport.py +++ b/examples/master/repository/coredevice_examples/transport.py @@ -22,10 +22,10 @@ class Transport(EnvExperiment): self.setattr_device("pmt") self.setattr_device("electrodes") - self.setattr_argument("wait_at_stop", FreeValue(100*us)) - self.setattr_argument("speed", FreeValue(1.5)) - self.setattr_argument("repeats", FreeValue(100)) - self.setattr_argument("nbins", FreeValue(100)) + self.setattr_argument("wait_at_stop", NumberValue(100*us)) + self.setattr_argument("speed", NumberValue(1.5)) + self.setattr_argument("repeats", NumberValue(100)) + self.setattr_argument("nbins", NumberValue(100)) def calc_waveforms(self, stop): t = transport_data["t"][:stop]*self.speed