From adbb217d556b4155fe48f346c3b2d978480beb32 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Thu, 7 Jan 2016 20:22:44 +0800 Subject: [PATCH 01/44] applets/simple: send embed RPC --- artiq/applets/simple.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 50c851cbe..94ba27c00 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -1,9 +1,10 @@ import argparse import asyncio -from quamash import QEventLoop, QtWidgets, QtCore +from quamash import QEventLoop, QtWidgets, QtGui, QtCore from artiq.protocols.sync_struct import Subscriber +from artiq.protocols.pc_rpc import Client class SimpleApplet: @@ -13,11 +14,20 @@ class SimpleApplet: self.argparser = argparse.ArgumentParser(description=cmd_description) group = self.argparser.add_argument_group("data server") group.add_argument( - "--server", default="::1", - help="hostname or IP to connect to") + "--server-notify", default="::1", + help="hostname or IP to connect to for dataset notifications") group.add_argument( - "--port", default=3250, type=int, - help="TCP port to connect to") + "--port-notify", default=3250, type=int, + help="TCP port to connect to for dataset notifications") + group = self.argparser.add_argument_group("GUI server") + group.add_argument( + "--server-gui", default="::1", + help="hostname or IP to connect to for GUI control") + group.add_argument( + "--port-gui", default=6501, type=int, + help="TCP port to connect to for GUI control") + group.add_argument("--embed", default=None, type=int, + help="embed main widget into existing window") self._arggroup_datasets = self.argparser.add_argument_group("datasets") def add_dataset(self, name, help=None): @@ -36,6 +46,22 @@ class SimpleApplet: def create_main_widget(self): self.main_widget = self.main_widget_class(self.args) + # Qt window embedding is ridiculously buggy, and empirical testing + # has shown that the following procedure must be followed exactly: + # 1. applet creates widget + # 2. applet creates native window without showing it, and get its ID + # 3. applet sends the ID to host, host embeds the widget + # 4. applet shows the widget + # Doing embedding the other way around (using QWindow.setParent in the + # applet) breaks resizing; furthermore the host needs to know our + # window ID to request graceful termination by closing the window. + if self.args.embed is not None: + win_id = int(self.main_widget.winId()) + remote = Client(self.args.server_gui, self.args.port_gui, "applets") + try: + remote.embed(self.args.embed, win_id) + finally: + remote.close_rpc() self.main_widget.show() def sub_init(self, data): @@ -49,7 +75,7 @@ class SimpleApplet: self.subscriber = Subscriber("datasets", self.sub_init, self.sub_mod) self.loop.run_until_complete(self.subscriber.connect( - self.args.server, self.args.port)) + self.args.server_notify, self.args.port_notify)) def run(self): self.args_init() From e106ee3f9003164b99ef7c7c153788aca9979440 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Thu, 7 Jan 2016 20:23:17 +0800 Subject: [PATCH 02/44] gui: basic applet dock editing --- artiq/frontend/artiq_gui.py | 18 +++-- artiq/gui/applets.py | 135 ++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 artiq/gui/applets.py diff --git a/artiq/frontend/artiq_gui.py b/artiq/frontend/artiq_gui.py index 9ce3aa151..079eedb51 100755 --- a/artiq/frontend/artiq_gui.py +++ b/artiq/frontend/artiq_gui.py @@ -11,10 +11,10 @@ from quamash import QEventLoop, QtGui, QtCore from pyqtgraph import dockarea from artiq.tools import * -from artiq.protocols.pc_rpc import AsyncioClient +from artiq.protocols.pc_rpc import AsyncioClient, Server from artiq.gui.models import ModelSubscriber from artiq.gui import (state, experiments, shortcuts, explorer, - moninj, datasets, schedule, log, console) + moninj, datasets, applets, schedule, log, console) def get_argparser(): @@ -111,6 +111,9 @@ def main(): d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"]) smgr.register(d_datasets) + appletmgr = applets.AppletManager(dock_area) + smgr.register(appletmgr) + if os.name != "nt": d_ttl_dds = moninj.MonInj() loop.run_until_complete(d_ttl_dds.start(args.server, args.port_notify)) @@ -129,9 +132,11 @@ def main(): if os.name != "nt": 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) + dock_area.addDock(appletmgr.main_dock, "above", d_ttl_dds.ttl_dock) + dock_area.addDock(d_datasets, "above", appletmgr.main_dock) else: - dock_area.addDock(d_datasets, "top") + dock_area.addDock(appletmgr.main_dock, "top") + dock_area.addDock(d_datasets, "above", appletmgr.main_dock) dock_area.addDock(d_shortcuts, "above", d_datasets) dock_area.addDock(d_explorer, "above", d_shortcuts) dock_area.addDock(d_console, "bottom") @@ -147,6 +152,11 @@ def main(): if d_log0 is not None: dock_area.addDock(d_log0, "right", d_explorer) + # start RPC server + rpc_server = Server({"applets": appletmgr.rpc}) + loop.run_until_complete(rpc_server.start("::1", 6501)) + atexit_register_coroutine(rpc_server.stop) + # run win.show() loop.run_until_complete(win.exit_request.wait()) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py new file mode 100644 index 000000000..4eb79fb53 --- /dev/null +++ b/artiq/gui/applets.py @@ -0,0 +1,135 @@ +import logging + +from quamash import QtCore, QtGui, QtWidgets +from pyqtgraph import dockarea + + +logger = logging.getLogger(__name__) + + +class AppletDock(dockarea.Dock): + def __init__(self, token, name): + dockarea.Dock.__init__(self, "applet" + str(token), + label="Applet: " + name, + closable=True) + self.setMinimumSize(QtCore.QSize(500, 400)) + + def capture(self, win_id): + self.captured_window = QtGui.QWindow.fromWinId(win_id) + self.captured_widget = QtWidgets.QWidget.createWindowContainer(captured_window) + self.addWidget(captured_widget) + + def terminate(self): + if hasattr(self, "captured_window"): + self.captured_window.close() + self.captured_widget.deleteLater() + del self.captured_window + del self.captured_widget + + +class AppletsDock(dockarea.Dock): + def __init__(self, manager): + self.manager = manager + + dockarea.Dock.__init__(self, "Applets") + self.setMinimumSize(QtCore.QSize(850, 450)) + + self.table = QtWidgets.QTableWidget(0, 3) + self.table.setHorizontalHeaderLabels(["Enable", "Name", "Command"]) + self.table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + self.table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self.table.horizontalHeader().setStretchLastSection(True) + self.table.horizontalHeader().setResizeMode( + QtGui.QHeaderView.ResizeToContents) + self.table.verticalHeader().setResizeMode( + QtGui.QHeaderView.ResizeToContents) + self.table.verticalHeader().hide() + self.table.setTextElideMode(QtCore.Qt.ElideNone) + self.addWidget(self.table) + + self.table.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) + new_action = QtGui.QAction("New applet", self.table) + new_action.triggered.connect(self.new) + self.table.addAction(new_action) + restart_action = QtGui.QAction("Restart selected applet", self.table) + self.table.addAction(restart_action) + delete_action = QtGui.QAction("Delete selected applet", self.table) + delete_action.triggered.connect(self.delete) + self.table.addAction(delete_action) + + self.table.cellChanged.connect(self.cell_changed) + + def cell_changed(self, row, column): + if column == 0: + item = self.table.item(row, column) + if item.checkState() == QtCore.Qt.Checked: + command = self.table.item(row, 2) + if command: + command = command.text() + name = self.table.item(row, 1) + if name is None: + name = "" + else: + name = name.text() + token = self.manager.create(name, command) + item.applet_token = token + else: + token = getattr(item, "applet_token", None) + if token is not None: + # cell_changed is emitted at row creation + self.manager.delete(token) + item.applet_token = None + + def new(self): + row = self.table.rowCount() + self.table.insertRow(row) + checkbox = QtWidgets.QTableWidgetItem() + checkbox.setFlags(QtCore.Qt.ItemIsSelectable | + QtCore.Qt.ItemIsUserCheckable | + QtCore.Qt.ItemIsEnabled) + checkbox.setCheckState(QtCore.Qt.Unchecked) + self.table.setItem(row, 0, checkbox) + + def delete(self): + selection = self.table.selectedRanges() + if selection: + self.table.deleteRow(selection[0].topRow()) + + +class AppletManagerRPC: + def __init__(self, parent): + self.parent = parent + + def embed(self, token, win_id): + self.parent.embed(token, win_id) + + +class AppletManager: + def __init__(self, dock_area): + self.dock_area = dock_area + self.main_dock = AppletsDock(self) + self.rpc = AppletManagerRPC(self) + self.applet_docks = dict() + + def embed(self, token, win_id): + if token not in self.applet_docks: + logger.warning("Ignored incorrect embed token %d for winid 0x%x", + token, win_id) + return + + def create(self, name, command): + token = next(iter(set(range(len(self.applet_docks) + 1)) + - self.applet_docks.keys())) + dock = AppletDock(token, name) + self.applet_docks[token] = dock + self.dock_area.floatDock(dock) + return token + + def delete(self, token): + del self.applet_docks[token] + + def save_state(self): + return dict() + + def restore_state(self, state): + pass From 597c2e4b177d68991967dc42661a8b81c3642272 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Fri, 8 Jan 2016 13:04:36 +0800 Subject: [PATCH 03/44] gui/applets: startup and embedding working --- artiq/frontend/artiq_gui.py | 1 + artiq/gui/applets.py | 47 ++++++++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/artiq/frontend/artiq_gui.py b/artiq/frontend/artiq_gui.py index 079eedb51..e83c0012a 100755 --- a/artiq/frontend/artiq_gui.py +++ b/artiq/frontend/artiq_gui.py @@ -112,6 +112,7 @@ def main(): smgr.register(d_datasets) appletmgr = applets.AppletManager(dock_area) + atexit_register_coroutine(appletmgr.stop) smgr.register(appletmgr) if os.name != "nt": diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 4eb79fb53..a085ffa5f 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -1,4 +1,6 @@ import logging +import asyncio +import shlex from quamash import QtCore, QtGui, QtWidgets from pyqtgraph import dockarea @@ -8,23 +10,50 @@ logger = logging.getLogger(__name__) class AppletDock(dockarea.Dock): - def __init__(self, token, name): + def __init__(self, token, name, command): dockarea.Dock.__init__(self, "applet" + str(token), label="Applet: " + name, closable=True) self.setMinimumSize(QtCore.QSize(500, 400)) + self.token = token + self.applet_name = name + self.command = command + + async def start(self): + command = self.command.format(embed_token=self.token) + logger.debug("starting command %s for %s", command, self.applet_name) + try: + self.process = await asyncio.create_subprocess_exec( + *shlex.split(command)) + except FileNotFoundError: + logger.warning("Applet %s failed to start", self.applet_name) + else: + logger.warning("Applet %s exited", self.applet_name) def capture(self, win_id): + logger.debug("capturing window 0x%x for %s", win_id, self.applet_name) self.captured_window = QtGui.QWindow.fromWinId(win_id) - self.captured_widget = QtWidgets.QWidget.createWindowContainer(captured_window) - self.addWidget(captured_widget) + self.captured_widget = QtWidgets.QWidget.createWindowContainer( + self.captured_window) + self.addWidget(self.captured_widget) - def terminate(self): + async def terminate(self): if hasattr(self, "captured_window"): self.captured_window.close() self.captured_widget.deleteLater() del self.captured_window del self.captured_widget + if hasattr(self, "process"): + try: + await asyncio.wait_for(self.process.wait(), 2.0) + except: + logger.warning("Applet %s failed to exit, killing", + self.applet_name) + try: + self.process.kill() + except ProcessLookupError: + pass + await self.process.wait() class AppletsDock(dockarea.Dock): @@ -116,18 +145,24 @@ class AppletManager: logger.warning("Ignored incorrect embed token %d for winid 0x%x", token, win_id) return + self.applet_docks[token].capture(win_id) def create(self, name, command): token = next(iter(set(range(len(self.applet_docks) + 1)) - - self.applet_docks.keys())) - dock = AppletDock(token, name) + - self.applet_docks.keys())) + dock = AppletDock(token, name, command) self.applet_docks[token] = dock self.dock_area.floatDock(dock) + asyncio.ensure_future(dock.start()) return token def delete(self, token): del self.applet_docks[token] + async def stop(self): + for dock in self.applet_docks.values(): + await dock.terminate() + def save_state(self): return dict() From 1ea73be56d85a61f2c8f963645e4aa873def4e3c Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Fri, 8 Jan 2016 20:07:02 +0800 Subject: [PATCH 04/44] gui/applets: all basic functionality, no state save/restore --- artiq/gui/applets.py | 67 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index a085ffa5f..a0788b9d9 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -1,6 +1,7 @@ import logging import asyncio import shlex +from functools import partial from quamash import QtCore, QtGui, QtWidgets from pyqtgraph import dockarea @@ -19,16 +20,19 @@ class AppletDock(dockarea.Dock): self.applet_name = name self.command = command + def rename(self, name): + self.applet_name = name + self.label.setText("Applet: " + name) + async def start(self): command = self.command.format(embed_token=self.token) logger.debug("starting command %s for %s", command, self.applet_name) try: self.process = await asyncio.create_subprocess_exec( *shlex.split(command)) - except FileNotFoundError: - logger.warning("Applet %s failed to start", self.applet_name) - else: - logger.warning("Applet %s exited", self.applet_name) + except: + logger.warning("Applet %s failed to start", self.applet_name, + exc_info=True) def capture(self, win_id): logger.debug("capturing window 0x%x for %s", win_id, self.applet_name) @@ -54,11 +58,17 @@ class AppletDock(dockarea.Dock): except ProcessLookupError: pass await self.process.wait() + del self.process + + async def restart(self): + await self.terminate() + await self.start() class AppletsDock(dockarea.Dock): def __init__(self, manager): self.manager = manager + self.token_to_checkbox = dict() dockarea.Dock.__init__(self, "Applets") self.setMinimumSize(QtCore.QSize(850, 450)) @@ -81,6 +91,7 @@ class AppletsDock(dockarea.Dock): new_action.triggered.connect(self.new) self.table.addAction(new_action) restart_action = QtGui.QAction("Restart selected applet", self.table) + restart_action.triggered.connect(self.restart) self.table.addAction(restart_action) delete_action = QtGui.QAction("Delete selected applet", self.table) delete_action.triggered.connect(self.delete) @@ -102,12 +113,26 @@ class AppletsDock(dockarea.Dock): name = name.text() token = self.manager.create(name, command) item.applet_token = token + self.token_to_checkbox[token] = item else: token = getattr(item, "applet_token", None) if token is not None: # cell_changed is emitted at row creation self.manager.delete(token) - item.applet_token = None + elif column == 1 or column == 2: + new_value = self.table.item(row, column).text() + token = getattr(self.table.item(row, 0), "applet_token", None) + if token is not None: + if column == 1: + self.manager.rename(token, new_value) + else: + self.manager.set_command(token, new_value) + + def disable_token(self, token): + checkbox_item = self.token_to_checkbox[token] + checkbox_item.applet_token = None + del self.token_to_checkbox[token] + checkbox_item.setCheckState(QtCore.Qt.Unchecked) def new(self): row = self.table.rowCount() @@ -119,10 +144,22 @@ class AppletsDock(dockarea.Dock): checkbox.setCheckState(QtCore.Qt.Unchecked) self.table.setItem(row, 0, checkbox) + def restart(self): + selection = self.table.selectedRanges() + if selection: + row = selection[0].topRow() + token = getattr(self.table.item(row, 0), "applet_token", None) + if token is not None: + asyncio.ensure_future(self.manager.restart(token)) + def delete(self): selection = self.table.selectedRanges() if selection: - self.table.deleteRow(selection[0].topRow()) + row = selection[0].topRow() + token = getattr(self.table.item(row, 0), "applet_token", None) + if token is not None: + self.manager.delete(token) + self.table.deleteRow(row) class AppletManagerRPC: @@ -154,11 +191,27 @@ class AppletManager: self.applet_docks[token] = dock self.dock_area.floatDock(dock) asyncio.ensure_future(dock.start()) + dock.sigClosed.connect(partial(self.on_dock_closed, token)) return token - def delete(self, token): + def on_dock_closed(self, token): + asyncio.ensure_future(self.applet_docks[token].terminate()) + self.main_dock.disable_token(token) del self.applet_docks[token] + def delete(self, token): + # This in turns calls on_dock_closed and main_dock.disable_token + self.applet_docks[token].close() + + def rename(self, token, name): + self.applet_docks[token].rename(name) + + def set_command(self, token, command): + self.applet_docks[token].command = command + + async def restart(self, token): + await self.applet_docks[token].restart() + async def stop(self): for dock in self.applet_docks.values(): await dock.terminate() From 2199eadb9962811f19f0c78891c62d7c5a246fe2 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sat, 9 Jan 2016 19:29:20 +0800 Subject: [PATCH 05/44] gui/applets: save/restore state --- artiq/gui/applets.py | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index a0788b9d9..b52b54905 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -143,6 +143,7 @@ class AppletsDock(dockarea.Dock): QtCore.Qt.ItemIsEnabled) checkbox.setCheckState(QtCore.Qt.Unchecked) self.table.setItem(row, 0, checkbox) + return row def restart(self): selection = self.table.selectedRanges() @@ -161,6 +162,27 @@ class AppletsDock(dockarea.Dock): self.manager.delete(token) self.table.deleteRow(row) + def save_state(self): + state = [] + for row in range(self.table.rowCount()): + enabled = self.table.item(row, 0).checkState() == QtCore.Qt.Checked + name = self.table.item(row, 1).text() + command = self.table.item(row, 2).text() + state.append((enabled, name, command)) + return state + + def restore_state(self, state): + for enabled, name, command in state: + row = self.new() + item = QtWidgets.QTableWidgetItem() + item.setText(name) + self.table.setItem(row, 1, item) + item = QtWidgets.QTableWidgetItem() + item.setText(command) + self.table.setItem(row, 2, item) + if enabled: + self.table.item(row, 0).setCheckState(QtCore.Qt.Checked) + class AppletManagerRPC: def __init__(self, parent): @@ -176,6 +198,7 @@ class AppletManager: self.main_dock = AppletsDock(self) self.rpc = AppletManagerRPC(self) self.applet_docks = dict() + self.workaround_pyqtgraph_bug = False def embed(self, token, win_id): if token not in self.applet_docks: @@ -189,7 +212,12 @@ class AppletManager: - self.applet_docks.keys())) dock = AppletDock(token, name, command) self.applet_docks[token] = dock - self.dock_area.floatDock(dock) + # If a dock is floated and then dock state is restored, pyqtgraph + # leaves a "phantom" window open. + if self.workaround_pyqtgraph_bug: + self.dock_area.addDock(dock) + else: + self.dock_area.floatDock(dock) asyncio.ensure_future(dock.start()) dock.sigClosed.connect(partial(self.on_dock_closed, token)) return token @@ -217,7 +245,9 @@ class AppletManager: await dock.terminate() def save_state(self): - return dict() + return self.main_dock.save_state() def restore_state(self, state): - pass + self.workaround_pyqtgraph_bug = True + self.main_dock.restore_state(state) + self.workaround_pyqtgraph_bug = False From 4136ff68cf0137fb6adaa0d59c6df3b746be3ed2 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sun, 10 Jan 2016 18:18:47 +0800 Subject: [PATCH 06/44] gui/applets/simple: support for optional datasets --- artiq/applets/simple.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 94ba27c00..f11f58ca3 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -30,11 +30,14 @@ class SimpleApplet: help="embed main widget into existing window") self._arggroup_datasets = self.argparser.add_argument_group("datasets") - def add_dataset(self, name, help=None): - if help is None: - self._arggroup_datasets.add_argument(name) + def add_dataset(self, name, help=None, required=True): + kwargs = dict() + if help is not None: + kwargs["help"] = help + if required: + self._arggroup_datasets.add_argument(name, **kwargs) else: - self._arggroup_datasets.add_argument(name, help=help) + self._arggroup_datasets.add_argument("--" + name, **kwargs) def args_init(self): self.args = self.argparser.parse_args() From db06e73372d065ba31cc4843fae0545c22471a6f Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sun, 10 Jan 2016 18:19:34 +0800 Subject: [PATCH 07/44] gui/applets: add XY plot --- artiq/applets/plot_xy.py | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100755 artiq/applets/plot_xy.py diff --git a/artiq/applets/plot_xy.py b/artiq/applets/plot_xy.py new file mode 100755 index 000000000..4b8221f42 --- /dev/null +++ b/artiq/applets/plot_xy.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3.5 + +import numpy as np +import pyqtgraph + +from artiq.applets.simple import SimpleApplet + + +class XYPlot(pyqtgraph.PlotWidget): + def __init__(self, args): + pyqtgraph.PlotWidget.__init__(self) + self.args = args + + def data_changed(self, data, mods): + try: + y = data[self.args.y][1] + except KeyError: + return + x = data.get(self.args.x, (False, None))[1] + if x is None: + x = list(range(len(y))) + error = data.get(self.args.error, (False, None))[1] + fit = data.get(self.args.fit, (False, None))[1] + + if not len(y) or len(y) != len(x): + return + if error is not None and hasattr(error, "__len__"): + if not len(error): + error = None + elif len(error) != len(y): + return + if fit is not None: + if not len(fit): + fit = None + elif len(fit) != len(y): + return + + self.clear() + self.plot(x, y, pen=None, symbol="x") + if error is not None: + # See https://github.com/pyqtgraph/pyqtgraph/issues/211 + if hasattr(error, "__len__") and not isinstance(error, np.ndarray): + error = np.array(error) + errbars = pg.ErrorBarItem(x=np.array(x), y=np.array(y), height=error) + self.addItem(errbars) + if fit is not None: + self.plot(x, fit) + + +def main(): + applet = SimpleApplet(XYPlot) + applet.add_dataset("y", "Y values") + applet.add_dataset("x", "X values", required=False) + applet.add_dataset("error", "Error bars for each X value", required=False) + applet.add_dataset("fit", "Fit values for each X value", required=False) + applet.run() + +if __name__ == "__main__": + main() From 26a6e8c5dee9dadcf18f08fc42d150a8aa66ed72 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sun, 10 Jan 2016 18:23:31 +0800 Subject: [PATCH 08/44] gui/applets: add histogram plot --- artiq/applets/plot_hist.py | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100755 artiq/applets/plot_hist.py diff --git a/artiq/applets/plot_hist.py b/artiq/applets/plot_hist.py new file mode 100755 index 000000000..cfaf6a36c --- /dev/null +++ b/artiq/applets/plot_hist.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3.5 + +import numpy as np +import pyqtgraph + +from artiq.applets.simple import SimpleApplet + + +class HistogramPlot(pyqtgraph.PlotWidget): + def __init__(self, args): + pyqtgraph.PlotWidget.__init__(self) + self.args = args + + def data_changed(self, data, mods): + try: + y = data[self.args.y][1] + if self.args.x is None: + x = None + else: + x = data[self.args.x][1] + except KeyError: + return + if x is None: + x = list(range(len(y)+1)) + + if len(y) and len(x) == len(y) + 1: + self.clear() + self.plot(x, y, stepMode=True, fillLevel=0, + brush=(0, 0, 255, 150)) + + +def main(): + applet = SimpleApplet(HistogramPlot) + applet.add_dataset("y", "Y values") + applet.add_dataset("x", "X values", required=False) + applet.run() + +if __name__ == "__main__": + main() From e6e6b27ba9b65ffe3ca5a29976cdfaec76db3a30 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sun, 10 Jan 2016 18:32:46 +0800 Subject: [PATCH 09/44] gui: remove displays (use applets instead) --- artiq/frontend/artiq_gui.py | 1 - artiq/gui/datasets.py | 88 --------------- artiq/gui/displays.py | 217 ------------------------------------ 3 files changed, 306 deletions(-) delete mode 100644 artiq/gui/displays.py diff --git a/artiq/frontend/artiq_gui.py b/artiq/frontend/artiq_gui.py index e83c0012a..3c0f656fc 100755 --- a/artiq/frontend/artiq_gui.py +++ b/artiq/frontend/artiq_gui.py @@ -109,7 +109,6 @@ def main(): rpc_clients["experiment_db"]) d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"]) - smgr.register(d_datasets) appletmgr = applets.AppletManager(dock_area) atexit_register_coroutine(appletmgr.stop) diff --git a/artiq/gui/datasets.py b/artiq/gui/datasets.py index 0d8d2b65d..0e341c082 100644 --- a/artiq/gui/datasets.py +++ b/artiq/gui/datasets.py @@ -9,7 +9,6 @@ from pyqtgraph import LayoutWidget from artiq.tools import short_format from artiq.gui.models import DictSyncTreeSepModel -from artiq.gui.displays import * try: QSortFilterProxyModel = QtCore.QSortFilterProxyModel @@ -35,12 +34,6 @@ class Model(DictSyncTreeSepModel): raise ValueError -def _get_display_type_name(display_cls): - for name, (_, cls) in display_types.items(): - if cls is display_cls: - return name - - class DatasetsDock(dockarea.Dock): def __init__(self, dialog_parent, dock_area, datasets_sub): dockarea.Dock.__init__(self, "Datasets") @@ -62,19 +55,6 @@ class DatasetsDock(dockarea.Dock): self.table_model = Model(dict()) datasets_sub.add_setmodel_callback(self.set_model) - datasets_sub.notify_cbs.append(self.on_mod) - - add_display_box = QtGui.QGroupBox("Add display") - grid.addWidget(add_display_box, 1, 1) - display_grid = QtGui.QGridLayout() - add_display_box.setLayout(display_grid) - - for n, name in enumerate(display_types.keys()): - btn = QtGui.QPushButton(name) - display_grid.addWidget(btn, n, 0) - btn.clicked.connect(partial(self.create_dialog, name)) - - self.displays = dict() def _search_datasets(self): if hasattr(self, "table_model_filter"): @@ -86,71 +66,3 @@ class DatasetsDock(dockarea.Dock): self.table_model_filter = QSortFilterProxyModel() self.table_model_filter.setSourceModel(self.table_model) self.table.setModel(self.table_model_filter) - - def update_display_data(self, dsp): - filtered_data = {k: self.table_model.backing_store[k][1] - for k in dsp.data_sources() - if k in self.table_model.backing_store} - dsp.update_data(filtered_data) - - def on_mod(self, mod): - if mod["action"] == "init": - for display in self.displays.values(): - display.update_data(self.table_model.backing_store) - return - - if mod["path"]: - source = mod["path"][0] - elif mod["action"] == "setitem": - source = mod["key"] - else: - return - - for display in self.displays.values(): - if source in display.data_sources(): - self.update_display_data(display) - - def create_dialog(self, ty): - dlg_class = display_types[ty][0] - dlg = dlg_class(self.dialog_parent, None, dict(), - sorted(self.table_model.backing_store.keys()), - partial(self.create_display, ty, None)) - dlg.open() - - def create_display(self, ty, prev_name, name, settings): - if prev_name is not None and prev_name in self.displays: - raise NotImplementedError - dsp_class = display_types[ty][1] - dsp = dsp_class(name, settings) - self.displays[name] = dsp - self.update_display_data(dsp) - - def on_close(): - del self.displays[name] - dsp.sigClosed.connect(on_close) - self.dock_area.floatDock(dsp) - return dsp - - def save_state(self): - r = dict() - for name, display in self.displays.items(): - r[name] = { - "ty": _get_display_type_name(type(display)), - "settings": display.settings, - "state": display.save_state() - } - return r - - def restore_state(self, state): - for name, desc in state.items(): - try: - dsp = self.create_display(desc["ty"], None, name, - desc["settings"]) - except: - logger.warning("Failed to create display '%s'", name, - exc_info=True) - try: - dsp.restore_state(desc["state"]) - except: - logger.warning("Failed to restore display state of '%s'", - name, exc_info=True) diff --git a/artiq/gui/displays.py b/artiq/gui/displays.py deleted file mode 100644 index 80c4e689c..000000000 --- a/artiq/gui/displays.py +++ /dev/null @@ -1,217 +0,0 @@ -from collections import OrderedDict -import numpy as np - -from quamash import QtGui -import pyqtgraph as pg -from pyqtgraph import dockarea - - -class _BaseSettings(QtGui.QDialog): - def __init__(self, parent, window_title, prev_name, create_cb): - QtGui.QDialog.__init__(self, parent=parent) - self.setWindowTitle(window_title) - - self.grid = QtGui.QGridLayout() - self.setLayout(self.grid) - - self.grid.addWidget(QtGui.QLabel("Name:"), 0, 0) - self.name = QtGui.QLineEdit() - self.grid.addWidget(self.name, 0, 1) - if prev_name is not None: - self.name.setText(prev_name) - - def on_accept(): - create_cb(self.name.text(), self.get_input()) - self.accepted.connect(on_accept) - - def add_buttons(self): - buttons = QtGui.QDialogButtonBox( - QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) - self.grid.addWidget(buttons, self.grid.rowCount(), 0, 1, 2) - buttons.accepted.connect(self.accept) - buttons.rejected.connect(self.reject) - - def accept(self): - if self.name.text() and self.validate_input(): - QtGui.QDialog.accept(self) - - def validate_input(self): - raise NotImplementedError - - def get_input(self): - raise NotImplementedError - - -class _SimpleSettings(_BaseSettings): - def __init__(self, parent, prev_name, prev_settings, - result_list, create_cb): - _BaseSettings.__init__(self, parent, self._window_title, - prev_name, create_cb) - - self.result_widgets = dict() - for row, (has_none, key) in enumerate(self._result_keys): - self.grid.addWidget(QtGui.QLabel(key.capitalize() + ":")) - w = QtGui.QComboBox() - self.grid.addWidget(w, row + 1, 1) - if has_none: - w.addItem("") - w.addItems(result_list) - w.setEditable(True) - if key in prev_settings: - w.setEditText(prev_settings[key]) - self.result_widgets[key] = w - self.add_buttons() - - def validate_input(self): - return all(w.currentText() for w in self.result_widgets.values()) - - def get_input(self): - return {k: v.currentText() for k, v in self.result_widgets.items()} - - -class NumberDisplaySettings(_SimpleSettings): - _window_title = "Number display" - _result_keys = [(False, "result")] - - -class NumberDisplay(dockarea.Dock): - def __init__(self, name, settings): - dockarea.Dock.__init__(self, "Display: " + name, closable=True) - self.settings = settings - self.number = QtGui.QLCDNumber() - self.number.setDigitCount(10) - self.addWidget(self.number) - - def data_sources(self): - return {self.settings["result"]} - - def update_data(self, data): - result = self.settings["result"] - try: - n = float(data[result]) - except: - n = "---" - self.number.display(n) - - def save_state(self): - return None - - def restore_state(self, state): - pass - - -class XYDisplaySettings(_SimpleSettings): - _window_title = "XY plot" - _result_keys = [(False, "y"), (True, "x"), (True, "error"), (True, "fit")] - - -class XYDisplay(dockarea.Dock): - def __init__(self, name, settings): - dockarea.Dock.__init__(self, "XY: " + name, closable=True) - self.settings = settings - self.plot = pg.PlotWidget() - self.addWidget(self.plot) - - def data_sources(self): - s = {self.settings["y"]} - for k in "x", "error", "fit": - if self.settings[k] != "": - s.add(self.settings[k]) - return s - - def update_data(self, data): - result_y = self.settings["y"] - result_x = self.settings["x"] - result_error = self.settings["error"] - result_fit = self.settings["fit"] - - try: - y = data[result_y] - except KeyError: - return - x = data.get(result_x, None) - if x is None: - x = list(range(len(y))) - error = data.get(result_error, None) - fit = data.get(result_fit, None) - - if not len(y) or len(y) != len(x): - return - if error is not None and hasattr(error, "__len__"): - if not len(error): - error = None - elif len(error) != len(y): - return - if fit is not None: - if not len(fit): - fit = None - elif len(fit) != len(y): - return - - self.plot.clear() - self.plot.plot(x, y, pen=None, symbol="x") - if error is not None: - # See https://github.com/pyqtgraph/pyqtgraph/issues/211 - if hasattr(error, "__len__") and not isinstance(error, np.ndarray): - error = np.array(error) - errbars = pg.ErrorBarItem(x=np.array(x), y=np.array(y), height=error) - self.plot.addItem(errbars) - if fit is not None: - self.plot.plot(x, fit) - - def save_state(self): - return self.plot.saveState() - - def restore_state(self, state): - self.plot.restoreState(state) - - -class HistogramDisplaySettings(_SimpleSettings): - _window_title = "Histogram" - _result_keys = [(False, "y"), (True, "x")] - - -class HistogramDisplay(dockarea.Dock): - def __init__(self, name, settings): - dockarea.Dock.__init__(self, "Histogram: " + name, closable=True) - self.settings = settings - self.plot = pg.PlotWidget() - self.addWidget(self.plot) - - def data_sources(self): - s = {self.settings["y"]} - if self.settings["x"] != "": - s.add(self.settings["x"]) - return s - - def update_data(self, data): - result_y = self.settings["y"] - result_x = self.settings["x"] - try: - y = data[result_y] - if result_x == "": - x = None - else: - x = data[result_x] - except KeyError: - return - if x is None: - x = list(range(len(y)+1)) - - if len(y) and len(x) == len(y) + 1: - self.plot.clear() - self.plot.plot(x, y, stepMode=True, fillLevel=0, - brush=(0, 0, 255, 150)) - - def save_state(self): - return self.plot.saveState() - - def restore_state(self, state): - self.plot.restoreState(state) - - -display_types = OrderedDict([ - ("Number", (NumberDisplaySettings, NumberDisplay)), - ("XY", (XYDisplaySettings, XYDisplay)), - ("Histogram", (HistogramDisplaySettings, HistogramDisplay)) -]) From 6a972eb59132c8d8b3b9cf00e16a86a99cc802db Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sun, 10 Jan 2016 18:38:24 +0800 Subject: [PATCH 10/44] gui: remove pyqt4 support code --- artiq/gui/datasets.py | 7 +------ artiq/gui/log.py | 9 ++------- artiq/gui/shortcuts.py | 9 ++------- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/artiq/gui/datasets.py b/artiq/gui/datasets.py index 0e341c082..b70388fa3 100644 --- a/artiq/gui/datasets.py +++ b/artiq/gui/datasets.py @@ -10,11 +10,6 @@ from pyqtgraph import LayoutWidget from artiq.tools import short_format from artiq.gui.models import DictSyncTreeSepModel -try: - QSortFilterProxyModel = QtCore.QSortFilterProxyModel -except AttributeError: - QSortFilterProxyModel = QtGui.QSortFilterProxyModel - logger = logging.getLogger(__name__) @@ -63,6 +58,6 @@ class DatasetsDock(dockarea.Dock): def set_model(self, model): self.table_model = model - self.table_model_filter = QSortFilterProxyModel() + self.table_model_filter = QtCore.QSortFilterProxyModel() self.table_model_filter.setSourceModel(self.table_model) self.table.setModel(self.table_model_filter) diff --git a/artiq/gui/log.py b/artiq/gui/log.py index 6ab5d0ae0..c6d9c4706 100644 --- a/artiq/gui/log.py +++ b/artiq/gui/log.py @@ -8,11 +8,6 @@ from pyqtgraph import dockarea, LayoutWidget from artiq.gui.tools import log_level_to_name -try: - QSortFilterProxyModel = QtCore.QSortFilterProxyModel -except AttributeError: - QSortFilterProxyModel = QtGui.QSortFilterProxyModel - class Model(QtCore.QAbstractTableModel): def __init__(self, init): @@ -107,9 +102,9 @@ class Model(QtCore.QAbstractTableModel): return v[3] -class _LogFilterProxyModel(QSortFilterProxyModel): +class _LogFilterProxyModel(QtCore.QSortFilterProxyModel): def __init__(self, min_level, freetext): - QSortFilterProxyModel.__init__(self) + QtCore.QSortFilterProxyModel.__init__(self) self.min_level = min_level self.freetext = freetext diff --git a/artiq/gui/shortcuts.py b/artiq/gui/shortcuts.py index 07c39f5c4..db05bfa2b 100644 --- a/artiq/gui/shortcuts.py +++ b/artiq/gui/shortcuts.py @@ -1,13 +1,8 @@ import logging from functools import partial -from quamash import QtGui, QtCore +from quamash import QtGui, QtCore, QtWidgets from pyqtgraph import dockarea -try: - from quamash import QtWidgets - QShortcut = QtWidgets.QShortcut -except: - QShortcut = QtGui.QShortcut logger = logging.getLogger(__name__) @@ -66,7 +61,7 @@ class ShortcutsDock(dockarea.Dock): "open": open, "submit": submit } - shortcut = QShortcut("F" + str(i+1), main_window) + shortcut = QtWidgets.QShortcut("F" + str(i+1), main_window) shortcut.setContext(QtCore.Qt.ApplicationShortcut) shortcut.activated.connect(partial(self._activated, i)) From e2c7578e48d486d59c58558187b555df01ac840c Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sun, 10 Jan 2016 21:48:17 +0800 Subject: [PATCH 11/44] gui: reorganize experiment/entry code --- artiq/gui/{scan.py => entries.py} | 117 +++++++++++++++++++++++++++-- artiq/gui/experiments.py | 118 ++---------------------------- 2 files changed, 118 insertions(+), 117 deletions(-) rename artiq/gui/{scan.py => entries.py} (67%) diff --git a/artiq/gui/scan.py b/artiq/gui/entries.py similarity index 67% rename from artiq/gui/scan.py rename to artiq/gui/entries.py index 125a06736..f6b423404 100644 --- a/artiq/gui/scan.py +++ b/artiq/gui/entries.py @@ -10,6 +10,100 @@ from artiq.gui.tools import disable_scroll_wheel logger = logging.getLogger(__name__) +class _StringEntry(QtGui.QLineEdit): + def __init__(self, argument): + QtGui.QLineEdit.__init__(self) + self.setText(argument["state"]) + def update(text): + argument["state"] = text + self.textEdited.connect(update) + + @staticmethod + def state_to_value(state): + return state + + @staticmethod + def default_state(procdesc): + return procdesc.get("default", "") + + +class _BooleanEntry(QtGui.QCheckBox): + def __init__(self, argument): + QtGui.QCheckBox.__init__(self) + self.setChecked(argument["state"]) + def update(checked): + argument["state"] = bool(checked) + self.stateChanged.connect(update) + + @staticmethod + def state_to_value(state): + return state + + @staticmethod + def default_state(procdesc): + return procdesc.get("default", False) + + +class _EnumerationEntry(QtGui.QComboBox): + def __init__(self, argument): + QtGui.QComboBox.__init__(self) + disable_scroll_wheel(self) + choices = argument["desc"]["choices"] + self.addItems(choices) + idx = choices.index(argument["state"]) + self.setCurrentIndex(idx) + def update(index): + argument["state"] = choices[index] + self.currentIndexChanged.connect(update) + + @staticmethod + def state_to_value(state): + return state + + @staticmethod + def default_state(procdesc): + if "default" in procdesc: + return procdesc["default"] + else: + return procdesc["choices"][0] + + +class _NumberEntry(QtGui.QDoubleSpinBox): + def __init__(self, argument): + QtGui.QDoubleSpinBox.__init__(self) + disable_scroll_wheel(self) + procdesc = argument["desc"] + scale = procdesc["scale"] + self.setDecimals(procdesc["ndecimals"]) + self.setSingleStep(procdesc["step"]/scale) + if procdesc["min"] is not None: + self.setMinimum(procdesc["min"]/scale) + else: + self.setMinimum(float("-inf")) + if procdesc["max"] is not None: + self.setMaximum(procdesc["max"]/scale) + else: + self.setMaximum(float("inf")) + if procdesc["unit"]: + self.setSuffix(" " + procdesc["unit"]) + + self.setValue(argument["state"]/scale) + def update(value): + argument["state"] = value*scale + self.valueChanged.connect(update) + + @staticmethod + def state_to_value(state): + return state + + @staticmethod + def default_state(procdesc): + if "default" in procdesc: + return procdesc["default"] + else: + return 0.0 + + class _NoScan(LayoutWidget): def __init__(self, procdesc, state): LayoutWidget.__init__(self) @@ -38,7 +132,7 @@ class _NoScan(LayoutWidget): self.value.valueChanged.connect(update) -class _Range(LayoutWidget): +class _RangeScan(LayoutWidget): def __init__(self, procdesc, state): LayoutWidget.__init__(self) @@ -90,7 +184,8 @@ class _Range(LayoutWidget): self.max.valueChanged.connect(update_max) self.npoints.valueChanged.connect(update_npoints) -class _Explicit(LayoutWidget): + +class _ExplicitScan(LayoutWidget): def __init__(self, state): LayoutWidget.__init__(self) @@ -109,7 +204,7 @@ class _Explicit(LayoutWidget): self.value.textEdited.connect(update) -class ScanController(LayoutWidget): +class _ScanEntry(LayoutWidget): def __init__(self, argument): LayoutWidget.__init__(self) self.argument = argument @@ -121,9 +216,9 @@ class ScanController(LayoutWidget): state = argument["state"] self.widgets = OrderedDict() self.widgets["NoScan"] = _NoScan(procdesc, state["NoScan"]) - self.widgets["LinearScan"] = _Range(procdesc, state["LinearScan"]) - self.widgets["RandomScan"] = _Range(procdesc, state["RandomScan"]) - self.widgets["ExplicitScan"] = _Explicit(state["ExplicitScan"]) + self.widgets["LinearScan"] = _RangeScan(procdesc, state["LinearScan"]) + self.widgets["RandomScan"] = _RangeScan(procdesc, state["RandomScan"]) + self.widgets["ExplicitScan"] = _ExplicitScan(state["ExplicitScan"]) for widget in self.widgets.values(): self.stack.addWidget(widget) @@ -181,3 +276,13 @@ class ScanController(LayoutWidget): self.stack.setCurrentWidget(self.widgets[ty]) self.argument["state"]["selected"] = ty break + + +argty_to_entry = { + "PYONValue": _StringEntry, + "BooleanValue": _BooleanEntry, + "EnumerationValue": _EnumerationEntry, + "NumberValue": _NumberEntry, + "StringValue": _StringEntry, + "Scannable": _ScanEntry +} diff --git a/artiq/gui/experiments.py b/artiq/gui/experiments.py index 9412404b1..3747fe92b 100644 --- a/artiq/gui/experiments.py +++ b/artiq/gui/experiments.py @@ -7,117 +7,13 @@ from quamash import QtGui, QtCore from pyqtgraph import dockarea, LayoutWidget -from artiq.gui.tools import log_level_to_name, disable_scroll_wheel -from artiq.gui.scan import ScanController +from artiq.gui.tools import log_level_to_name +from artiq.gui.entries import argty_to_entry logger = logging.getLogger(__name__) -class _StringEntry(QtGui.QLineEdit): - def __init__(self, argument): - QtGui.QLineEdit.__init__(self) - self.setText(argument["state"]) - def update(text): - argument["state"] = text - self.textEdited.connect(update) - - @staticmethod - def state_to_value(state): - return state - - @staticmethod - def default_state(procdesc): - return procdesc.get("default", "") - - -class _BooleanEntry(QtGui.QCheckBox): - def __init__(self, argument): - QtGui.QCheckBox.__init__(self) - self.setChecked(argument["state"]) - def update(checked): - argument["state"] = bool(checked) - self.stateChanged.connect(update) - - @staticmethod - def state_to_value(state): - return state - - @staticmethod - def default_state(procdesc): - return procdesc.get("default", False) - - -class _EnumerationEntry(QtGui.QComboBox): - def __init__(self, argument): - QtGui.QComboBox.__init__(self) - disable_scroll_wheel(self) - choices = argument["desc"]["choices"] - self.addItems(choices) - idx = choices.index(argument["state"]) - self.setCurrentIndex(idx) - def update(index): - argument["state"] = choices[index] - self.currentIndexChanged.connect(update) - - @staticmethod - def state_to_value(state): - return state - - @staticmethod - def default_state(procdesc): - if "default" in procdesc: - return procdesc["default"] - else: - return procdesc["choices"][0] - - -class _NumberEntry(QtGui.QDoubleSpinBox): - def __init__(self, argument): - QtGui.QDoubleSpinBox.__init__(self) - disable_scroll_wheel(self) - procdesc = argument["desc"] - scale = procdesc["scale"] - self.setDecimals(procdesc["ndecimals"]) - self.setSingleStep(procdesc["step"]/scale) - if procdesc["min"] is not None: - self.setMinimum(procdesc["min"]/scale) - else: - self.setMinimum(float("-inf")) - if procdesc["max"] is not None: - self.setMaximum(procdesc["max"]/scale) - else: - self.setMaximum(float("inf")) - if procdesc["unit"]: - self.setSuffix(" " + procdesc["unit"]) - - self.setValue(argument["state"]/scale) - def update(value): - argument["state"] = value*scale - self.valueChanged.connect(update) - - @staticmethod - def state_to_value(state): - return state - - @staticmethod - def default_state(procdesc): - if "default" in procdesc: - return procdesc["default"] - else: - return 0.0 - - -_argty_to_entry = { - "PYONValue": _StringEntry, - "BooleanValue": _BooleanEntry, - "EnumerationValue": _EnumerationEntry, - "NumberValue": _NumberEntry, - "StringValue": _StringEntry, - "Scannable": ScanController -} - - # Experiment URLs come in two forms: # 1. repo: # (file name and class name to be retrieved from explist) @@ -153,7 +49,7 @@ class _ArgumentEditor(QtGui.QTreeWidget): self.addTopLevelItem(QtGui.QTreeWidgetItem(["No arguments"])) for name, argument in arguments.items(): - entry = _argty_to_entry[argument["desc"]["ty"]](argument) + entry = argty_to_entry[argument["desc"]["ty"]](argument) widget_item = QtGui.QTreeWidgetItem([name]) self._arg_to_entry_widgetitem[name] = entry, widget_item @@ -211,14 +107,14 @@ class _ArgumentEditor(QtGui.QTreeWidget): argument = self.manager.get_submission_arguments(self.expurl)[name] procdesc = arginfo[name][0] - state = _argty_to_entry[procdesc["ty"]].default_state(procdesc) + 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) + entry = argty_to_entry[procdesc["ty"]](argument) self._arg_to_entry_widgetitem[name] = entry, widget_item self.setItemWidget(widget_item, 1, entry) @@ -466,7 +362,7 @@ class ExperimentManager: def initialize_submission_arguments(self, expurl, arginfo): arguments = OrderedDict() for name, (procdesc, group) in arginfo.items(): - state = _argty_to_entry[procdesc["ty"]].default_state(procdesc) + state = argty_to_entry[procdesc["ty"]].default_state(procdesc) arguments[name] = { "desc": procdesc, "group": group, @@ -510,7 +406,7 @@ class ExperimentManager: argument_values = dict() for name, argument in arguments.items(): - entry_cls = _argty_to_entry[argument["desc"]["ty"]] + entry_cls = argty_to_entry[argument["desc"]["ty"]] argument_values[name] = entry_cls.state_to_value(argument["state"]) expid = { From 38cdeb0a32c39699ea5f2101e244586aa4f4055f Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sun, 10 Jan 2016 22:12:00 +0800 Subject: [PATCH 12/44] applets/simple: support mod buffering --- artiq/applets/big_number.py | 2 +- artiq/applets/simple.py | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/artiq/applets/big_number.py b/artiq/applets/big_number.py index bb26f22ba..7eb60cbcd 100755 --- a/artiq/applets/big_number.py +++ b/artiq/applets/big_number.py @@ -11,7 +11,7 @@ class NumberWidget(QtWidgets.QLCDNumber): self.setDigitCount(args.digit_count) self.dataset_name = args.dataset - def data_changed(self, data, mod): + def data_changed(self, data, mods): try: n = float(data[self.dataset_name][1]) except (KeyError, ValueError, TypeError): diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index f11f58ca3..93e293267 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -8,10 +8,15 @@ from artiq.protocols.pc_rpc import Client class SimpleApplet: - def __init__(self, main_widget_class, cmd_description=None): + def __init__(self, main_widget_class, cmd_description=None, + default_update_delay=0.0): self.main_widget_class = main_widget_class self.argparser = argparse.ArgumentParser(description=cmd_description) + self.argparser.add_argument("--update-delay", type=float, + default=default_update_delay, + help="time to wait after a mod (buffering other mods) " + "before updating (default: %(default).2f)") group = self.argparser.add_argument_group("data server") group.add_argument( "--server-notify", default="::1", @@ -71,8 +76,20 @@ class SimpleApplet: self.data = data return data + def flush_mod_buffer(self): + self.main_widget.data_changed(self.data, self.mod_buffer) + del self.mod_buffer + def sub_mod(self, mod): - self.main_widget.data_changed(self.data, mod) + if self.args.update_delay: + if hasattr(self, "mod_buffer"): + self.mod_buffer.append(mod) + else: + self.mod_buffer = [mod] + asyncio.get_event_loop().call_later(self.args.update_delay, + self.flush_mod_buffer) + else: + self.main_widget.data_changed(self.data, [mod]) def create_subscriber(self): self.subscriber = Subscriber("datasets", From 341bbdee6beb2d7b4ec161b57a2e5c98f3b843b1 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 13 Jan 2016 05:51:53 -0700 Subject: [PATCH 13/44] examples: add histograms demo --- examples/master/repository/histograms.py | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 examples/master/repository/histograms.py diff --git a/examples/master/repository/histograms.py b/examples/master/repository/histograms.py new file mode 100644 index 000000000..c0c660708 --- /dev/null +++ b/examples/master/repository/histograms.py @@ -0,0 +1,35 @@ +from time import sleep + +import numpy as np + +from artiq import * + + +class Histograms(EnvExperiment): + """Histograms demo""" + def build(self): + pass + + def run(self): + nbins = 50 + npoints = 20 + + bin_boundaries = np.linspace(-10, 30, nbins + 1) + self.set_dataset("hd_bins", bin_boundaries, + broadcast=True, save=False) + + xs = np.empty(npoints) + xs.fill(np.nan) + xs = self.set_dataset("hd_xs", xs, + broadcast=True, save=False) + + counts = np.empty((npoints, nbins)) + counts = self.set_dataset("hd_counts", counts, + broadcast=True, save=False) + + for i in range(npoints): + histogram, _ = np.histogram(np.random.normal(i, size=1000), + bin_boundaries) + counts[i] = histogram + xs[i] = i % 8 + sleep(0.3) From e37e0bdc1c5a7c2ca30457e591387783ac34c178 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 13 Jan 2016 05:52:33 -0700 Subject: [PATCH 14/44] applets/plot_xy_hist: use applets.simple and datasets --- artiq/applets/plot_xy_hist.py | 87 ++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/artiq/applets/plot_xy_hist.py b/artiq/applets/plot_xy_hist.py index 71edd43be..b74dd724d 100755 --- a/artiq/applets/plot_xy_hist.py +++ b/artiq/applets/plot_xy_hist.py @@ -1,69 +1,84 @@ #!/usr/bin/env python3.5 -from pyqtgraph.Qt import QtGui, QtCore -import pyqtgraph as pg - import numpy as np +import pyqtgraph -class XYHistPlot: - def __init__(self): - self.graphics_window = pg.GraphicsWindow(title="XY/Histogram") - self.graphics_window.resize(1000,600) - self.graphics_window.setWindowTitle("XY/Histogram") +from artiq.applets.simple import SimpleApplet - self.xy_plot = self.graphics_window.addPlot() + +class XYHistPlot(pyqtgraph.GraphicsWindow): + def __init__(self, args): + pyqtgraph.GraphicsWindow.__init__(self, title="XY/Histogram") + self.resize(1000,600) + self.setWindowTitle("XY/Histogram") + + self.xy_plot = self.addPlot() self.xy_plot_data = None self.arrow = None - self.hist_plot = self.graphics_window.addPlot() + self.hist_plot = self.addPlot() self.hist_plot_data = None - def set_data(self, xs, histograms_bins, histograms_counts): + self.args = args + + def _set_full_data(self, xs, histogram_bins, histograms_counts): + self.xy_plot.clear() + self.hist_plot.clear() + self.xy_plot_data = None + self.hist_plot_data = None + self.arrow = None + + self.histogram_bins = histogram_bins + bin_centers = np.empty(len(histogram_bins)-1) + for i in range(len(bin_centers)): + bin_centers[i] = (histogram_bins[i] + histogram_bins[i+1])/2 + ys = np.empty_like(xs) - ys.fill(np.nan) - for n, (bins, counts) in enumerate(zip(histograms_bins, - histograms_counts)): - bin_centers = np.empty(len(bins)-1) - for i in range(len(bin_centers)): - bin_centers[i] = (bins[i] + bins[i+1])/2 - ys[n] = sum(bin_centers*counts)/sum(bin_centers) + for n, counts in enumerate(histograms_counts): + ys[n] = sum(bin_centers*counts)/sum(counts) self.xy_plot_data = self.xy_plot.plot(x=xs, y=ys, pen=None, symbol="x", symbolSize=20) - self.xy_plot_data.sigPointsClicked.connect(self.point_clicked) - for point, bins, counts in zip(self.xy_plot_data.scatter.points(), - histograms_bins, histograms_counts): - point.histogram_bins = bins + self.xy_plot_data.sigPointsClicked.connect(self._point_clicked) + for point, counts in zip(self.xy_plot_data.scatter.points(), + histograms_counts): point.histogram_counts = counts self.hist_plot_data = self.hist_plot.plot( - stepMode=True, fillLevel=0, - brush=(0, 0, 255, 150)) + stepMode=True, fillLevel=0, + brush=(0, 0, 255, 150)) - def point_clicked(self, data_item, spot_items): + def _point_clicked(self, data_item, spot_items): spot_item = spot_items[0] position = spot_item.pos() if self.arrow is None: - self.arrow = pg.ArrowItem(angle=-120, tipAngle=30, baseAngle=20, - headLen=40, tailLen=40, tailWidth=8, - pen=None, brush="y") + self.arrow = pyqtgraph.ArrowItem( + angle=-120, tipAngle=30, baseAngle=20, headLen=40, + tailLen=40, tailWidth=8, pen=None, brush="y") self.arrow.setPos(position) # NB: temporary glitch if addItem is done before setPos self.xy_plot.addItem(self.arrow) else: self.arrow.setPos(position) - self.hist_plot_data.setData(x=spot_item.histogram_bins, + self.hist_plot_data.setData(x=self.histogram_bins, y=spot_item.histogram_counts) + + def data_changed(self, data, mods): + xs = data[self.args.xs][1] + histogram_bins = data[self.args.histogram_bins][1] + histograms_counts = data[self.args.histograms_counts][1] + self._set_full_data(xs, histogram_bins, histograms_counts) def main(): - app = QtGui.QApplication([]) - plot = XYHistPlot() - plot.set_data(np.array([1, 2, 3, 4, 1]), - np.array([[1, 2, 3], [1, 2, 3], [1, 2, 3], [40, 70, 100], [4, 7, 10, 20]]), - np.array([[1, 1], [2, 3], [10, 20], [3, 1], [100, 67, 102]])) - app.exec_() + applet = SimpleApplet(XYHistPlot) + applet.add_dataset("xs", "1D array of point abscissas") + applet.add_dataset("histogram_bins", + "1D array of histogram bin boundaries") + applet.add_dataset("histograms_counts", + "2D array of histogram counts, for each point") + applet.run() -if __name__ == '__main__': +if __name__ == "__main__": main() From 81a86b2b043401f7d036364e9aedf9d7b584e34b Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 13 Jan 2016 06:53:23 -0700 Subject: [PATCH 15/44] applets/plot_xy_hist: do not clear arrow/histogram when unrelated data is modified --- artiq/applets/plot_xy_hist.py | 64 +++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/artiq/applets/plot_xy_hist.py b/artiq/applets/plot_xy_hist.py index b74dd724d..8f31615e1 100755 --- a/artiq/applets/plot_xy_hist.py +++ b/artiq/applets/plot_xy_hist.py @@ -6,6 +6,17 @@ import pyqtgraph from artiq.applets.simple import SimpleApplet +def _compute_ys(histogram_bins, histograms_counts): + bin_centers = np.empty(len(histogram_bins)-1) + for i in range(len(bin_centers)): + bin_centers[i] = (histogram_bins[i] + histogram_bins[i+1])/2 + + ys = np.empty(histograms_counts.shape[0]) + for n, counts in enumerate(histograms_counts): + ys[n] = sum(bin_centers*counts)/sum(counts) + return ys + + class XYHistPlot(pyqtgraph.GraphicsWindow): def __init__(self, args): pyqtgraph.GraphicsWindow.__init__(self, title="XY/Histogram") @@ -15,6 +26,7 @@ class XYHistPlot(pyqtgraph.GraphicsWindow): self.xy_plot = self.addPlot() self.xy_plot_data = None self.arrow = None + self.selected_index = None self.hist_plot = self.addPlot() self.hist_plot_data = None @@ -27,28 +39,36 @@ class XYHistPlot(pyqtgraph.GraphicsWindow): self.xy_plot_data = None self.hist_plot_data = None self.arrow = None + self.selected_index = None self.histogram_bins = histogram_bins - bin_centers = np.empty(len(histogram_bins)-1) - for i in range(len(bin_centers)): - bin_centers[i] = (histogram_bins[i] + histogram_bins[i+1])/2 - - ys = np.empty_like(xs) - for n, counts in enumerate(histograms_counts): - ys[n] = sum(bin_centers*counts)/sum(counts) + ys = _compute_ys(self.histogram_bins, histograms_counts) self.xy_plot_data = self.xy_plot.plot(x=xs, y=ys, pen=None, symbol="x", symbolSize=20) self.xy_plot_data.sigPointsClicked.connect(self._point_clicked) - for point, counts in zip(self.xy_plot_data.scatter.points(), - histograms_counts): + for index, (point, counts) in ( + enumerate(zip(self.xy_plot_data.scatter.points(), + histograms_counts))): + point.histogram_index = index point.histogram_counts = counts self.hist_plot_data = self.hist_plot.plot( stepMode=True, fillLevel=0, brush=(0, 0, 255, 150)) + def _set_partial_data(self, xs, histograms_counts): + ys = _compute_ys(self.histogram_bins, histograms_counts) + self.xy_plot_data.setData(x=xs, y=ys, + pen=None, + symbol="x", symbolSize=20) + for index, (point, counts) in ( + enumerate(zip(self.xy_plot_data.scatter.points(), + histograms_counts))): + point.histogram_index = index + point.histogram_counts = counts + def _point_clicked(self, data_item, spot_items): spot_item = spot_items[0] position = spot_item.pos() @@ -61,14 +81,38 @@ class XYHistPlot(pyqtgraph.GraphicsWindow): self.xy_plot.addItem(self.arrow) else: self.arrow.setPos(position) + self.selected_index = spot_item.histogram_index self.hist_plot_data.setData(x=self.histogram_bins, y=spot_item.histogram_counts) + def _can_use_partial(self, mods): + if self.hist_plot_data is None: + return False + for mod in mods: + if mod["action"] != "setitem": + return False + if mod["path"] == [self.args.xs, 1]: + if mod["key"] == self.selected_index: + return False + elif mod["path"][:2] == [self.args.histograms_counts, 1]: + if len(mod["path"]) > 2: + index = mod["path"][2] + else: + index = mod["key"] + if index == self.selected_index: + return False + else: + return False + return True + def data_changed(self, data, mods): xs = data[self.args.xs][1] histogram_bins = data[self.args.histogram_bins][1] histograms_counts = data[self.args.histograms_counts][1] - self._set_full_data(xs, histogram_bins, histograms_counts) + if self._can_use_partial(mods): + self._set_partial_data(xs, histograms_counts) + else: + self._set_full_data(xs, histogram_bins, histograms_counts) def main(): From 7661b37c197da72ddcbe511b37e4fd84bbd300e0 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 13 Jan 2016 07:04:55 -0700 Subject: [PATCH 16/44] applets/simple: filter mods --- artiq/applets/simple.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 93e293267..933485c89 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -34,6 +34,7 @@ class SimpleApplet: group.add_argument("--embed", default=None, type=int, help="embed main widget into existing window") self._arggroup_datasets = self.argparser.add_argument_group("datasets") + self.dataset_args = set() def add_dataset(self, name, help=None, required=True): kwargs = dict() @@ -43,9 +44,12 @@ class SimpleApplet: self._arggroup_datasets.add_argument(name, **kwargs) else: self._arggroup_datasets.add_argument("--" + name, **kwargs) + self.dataset_args.add(name) def args_init(self): self.args = self.argparser.parse_args() + self.datasets = {getattr(self.args, arg.replace("-", "_")) + for arg in self.dataset_args} def quamash_init(self): app = QtWidgets.QApplication([]) @@ -76,11 +80,24 @@ class SimpleApplet: self.data = data return data + def filter_mod(self, mod): + if mod["action"] == "init": + return True + if mod["path"]: + return mod["path"][0] in self.datasets + elif mod["action"] in {"setitem", "delitem"}: + return mod["key"] in self.datasets + else: + return False + def flush_mod_buffer(self): self.main_widget.data_changed(self.data, self.mod_buffer) del self.mod_buffer def sub_mod(self, mod): + if not self.filter_mod(mod): + return + if self.args.update_delay: if hasattr(self, "mod_buffer"): self.mod_buffer.append(mod) From 8ad151b4d851aa3584db8e3e0f916fcfd3117bbd Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 13 Jan 2016 07:05:30 -0700 Subject: [PATCH 17/44] applets/plot_xy_hist: do not crash when datasets are missing --- artiq/applets/plot_xy_hist.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/artiq/applets/plot_xy_hist.py b/artiq/applets/plot_xy_hist.py index 8f31615e1..f22c628ce 100755 --- a/artiq/applets/plot_xy_hist.py +++ b/artiq/applets/plot_xy_hist.py @@ -106,9 +106,12 @@ class XYHistPlot(pyqtgraph.GraphicsWindow): return True def data_changed(self, data, mods): - xs = data[self.args.xs][1] - histogram_bins = data[self.args.histogram_bins][1] - histograms_counts = data[self.args.histograms_counts][1] + try: + xs = data[self.args.xs][1] + histogram_bins = data[self.args.histogram_bins][1] + histograms_counts = data[self.args.histograms_counts][1] + except KeyError: + return if self._can_use_partial(mods): self._set_partial_data(xs, histograms_counts) else: From 3d56ea5c7120212f216ca88a58d75d0711100851 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 13 Jan 2016 07:22:19 -0700 Subject: [PATCH 18/44] applets/plot_xy_hist: use normal Qt widget as top-level to workaround pyqtgraph.GraphicsWindow misbehaviour with embedding --- artiq/applets/plot_xy_hist.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/artiq/applets/plot_xy_hist.py b/artiq/applets/plot_xy_hist.py index f22c628ce..2a97a324c 100755 --- a/artiq/applets/plot_xy_hist.py +++ b/artiq/applets/plot_xy_hist.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3.5 import numpy as np +from quamash import QtWidgets import pyqtgraph from artiq.applets.simple import SimpleApplet @@ -17,18 +18,22 @@ def _compute_ys(histogram_bins, histograms_counts): return ys -class XYHistPlot(pyqtgraph.GraphicsWindow): +# pyqtgraph.GraphicsWindow fails to behave like a regular Qt widget +# and breaks embedding. Do not use as top widget. +class XYHistPlot(QtWidgets.QSplitter): def __init__(self, args): - pyqtgraph.GraphicsWindow.__init__(self, title="XY/Histogram") + QtWidgets.QSplitter.__init__(self) self.resize(1000,600) self.setWindowTitle("XY/Histogram") - self.xy_plot = self.addPlot() + self.xy_plot = pyqtgraph.PlotWidget() + self.insertWidget(0, self.xy_plot) self.xy_plot_data = None self.arrow = None self.selected_index = None - self.hist_plot = self.addPlot() + self.hist_plot = pyqtgraph.PlotWidget() + self.insertWidget(1, self.hist_plot) self.hist_plot_data = None self.args = args From cc3a45d4e48345a879a5f594aa5d5ea2b70b5f63 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Fri, 15 Jan 2016 10:11:13 -0700 Subject: [PATCH 19/44] gui/applets: fix applet removal --- artiq/gui/applets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index b52b54905..6c58005c8 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -160,7 +160,7 @@ class AppletsDock(dockarea.Dock): token = getattr(self.table.item(row, 0), "applet_token", None) if token is not None: self.manager.delete(token) - self.table.deleteRow(row) + self.table.removeRow(row) def save_state(self): state = [] From 9acf8b7c1a5923ab08bc4cabb5bb79b16620736b Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Fri, 15 Jan 2016 10:15:06 -0700 Subject: [PATCH 20/44] gui/applets: templates --- artiq/gui/applets.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 6c58005c8..c3f0d92c2 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -1,5 +1,6 @@ import logging import asyncio +import sys import shlex from functools import partial @@ -25,7 +26,8 @@ class AppletDock(dockarea.Dock): self.label.setText("Applet: " + name) async def start(self): - command = self.command.format(embed_token=self.token) + command = self.command.format(python=sys.executable, + embed_token=self.token) logger.debug("starting command %s for %s", command, self.applet_name) try: self.process = await asyncio.create_subprocess_exec( @@ -65,6 +67,22 @@ class AppletDock(dockarea.Dock): await self.start() +_templates = [ + ("Big number", "{python} -m artiq.applets.big_number " + "--embed {embed_token} NUMBER_DATASET"), + ("Histogram", "{python} -m artiq.applets.plot_hist " + "--embed {embed_token} COUNTS_DATASET " + "--x BIN_BOUNDARIES_DATASET"), + ("XY", "{python} -m artiq.applets.plot_xy " + "--embed {embed_token} Y_DATASET --x X_DATASET " + "--error ERROR_DATASET --fit FIT_DATASET"), + ("XY + Histogram", "{python} -m artiq.applets.plot_xy_hist " + "--embed {embed_token} X_DATASET " + "HIST_BIN_BOUNDARIES_DATASET " + "HISTS_COUNTS_DATASET"), +] + + class AppletsDock(dockarea.Dock): def __init__(self, manager): self.manager = manager @@ -90,6 +108,14 @@ class AppletsDock(dockarea.Dock): new_action = QtGui.QAction("New applet", self.table) new_action.triggered.connect(self.new) self.table.addAction(new_action) + templates_menu = QtGui.QMenu() + for name, template in _templates: + action = QtGui.QAction(name, self.table) + action.triggered.connect(partial(self.new_template, template)) + templates_menu.addAction(action) + restart_action = QtGui.QAction("New applet from template", self.table) + restart_action.setMenu(templates_menu) + self.table.addAction(restart_action) restart_action = QtGui.QAction("Restart selected applet", self.table) restart_action.triggered.connect(self.restart) self.table.addAction(restart_action) @@ -143,8 +169,14 @@ class AppletsDock(dockarea.Dock): QtCore.Qt.ItemIsEnabled) checkbox.setCheckState(QtCore.Qt.Unchecked) self.table.setItem(row, 0, checkbox) + self.table.setItem(row, 1, QtWidgets.QTableWidgetItem()) + self.table.setItem(row, 2, QtWidgets.QTableWidgetItem()) return row + def new_template(self, template): + row = self.new() + self.table.item(row, 2).setText(template) + def restart(self): selection = self.table.selectedRanges() if selection: From 331ac37505b2c4422b9f949e116e5d13064d2088 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Fri, 15 Jan 2016 12:50:09 -0700 Subject: [PATCH 21/44] applets/plot_hist: better help message --- artiq/applets/plot_hist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artiq/applets/plot_hist.py b/artiq/applets/plot_hist.py index cfaf6a36c..54f459d91 100755 --- a/artiq/applets/plot_hist.py +++ b/artiq/applets/plot_hist.py @@ -32,7 +32,7 @@ class HistogramPlot(pyqtgraph.PlotWidget): def main(): applet = SimpleApplet(HistogramPlot) applet.add_dataset("y", "Y values") - applet.add_dataset("x", "X values", required=False) + applet.add_dataset("x", "Bin boundaries", required=False) applet.run() if __name__ == "__main__": From f9a6ba1884993579684e6e9cb4e04ff49ce46f33 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sat, 16 Jan 2016 13:12:16 -0700 Subject: [PATCH 22/44] gui/applets: add shortcuts --- artiq/gui/applets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index c3f0d92c2..87a40516b 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -117,9 +117,13 @@ class AppletsDock(dockarea.Dock): restart_action.setMenu(templates_menu) self.table.addAction(restart_action) restart_action = QtGui.QAction("Restart selected applet", self.table) + restart_action.setShortcut("CTRL+R") + restart_action.setShortcutContext(QtCore.Qt.WidgetShortcut) restart_action.triggered.connect(self.restart) self.table.addAction(restart_action) delete_action = QtGui.QAction("Delete selected applet", self.table) + delete_action.setShortcut("DELETE") + delete_action.setShortcutContext(QtCore.Qt.WidgetShortcut) delete_action.triggered.connect(self.delete) self.table.addAction(delete_action) From 1d92c0874e498ea5df4134d79700101e807f4e55 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Fri, 29 Jan 2016 23:21:22 +0100 Subject: [PATCH 23/44] gui/log: use QFontDatabase for fixed font --- artiq/gui/log.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/artiq/gui/log.py b/artiq/gui/log.py index c6d9c4706..460441c07 100644 --- a/artiq/gui/log.py +++ b/artiq/gui/log.py @@ -22,8 +22,7 @@ class Model(QtCore.QAbstractTableModel): timer.timeout.connect(self.timer_tick) timer.start(100) - self.fixed_font = QtGui.QFont() - self.fixed_font.setFamily("Monospace") + self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) self.white = QtGui.QBrush(QtGui.QColor(255, 255, 255)) self.black = QtGui.QBrush(QtGui.QColor(0, 0, 0)) From 741b11c26d59632e3efdaed47cdd894a78fd0461 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 09:59:15 +0100 Subject: [PATCH 24/44] applets: basic embedding OK --- artiq/applets/simple.py | 127 ++++++++++++++++------ artiq/frontend/artiq_gui.py | 21 ++-- artiq/gui/applets.py | 211 ++++++++++++++++-------------------- 3 files changed, 197 insertions(+), 162 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 933485c89..8fc5ceafc 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -4,7 +4,25 @@ import asyncio from quamash import QEventLoop, QtWidgets, QtGui, QtCore from artiq.protocols.sync_struct import Subscriber -from artiq.protocols.pc_rpc import Client +from artiq.protocols import pyon +from artiq.protocols.pipe_ipc import AsyncioChildComm + + +class AppletIPCClient(AsyncioChildComm): + def write_pyon(self, obj): + self.write(pyon.encode(obj).encode() + b"\n") + + async def read_pyon(self): + line = await self.readline() + return pyon.decode(line.decode()) + + async def embed(self, win_id): + self.write_pyon({"action": "embed", + "win_id": win_id}) + reply = await self.read_pyon() + if reply["action"] != "embed_done": + raise ValueError("Got erroneous reply to embed request", + reply) class SimpleApplet: @@ -13,27 +31,31 @@ class SimpleApplet: self.main_widget_class = main_widget_class self.argparser = argparse.ArgumentParser(description=cmd_description) + self.argparser.add_argument("--update-delay", type=float, default=default_update_delay, help="time to wait after a mod (buffering other mods) " "before updating (default: %(default).2f)") - group = self.argparser.add_argument_group("data server") - group.add_argument( - "--server-notify", default="::1", - help="hostname or IP to connect to for dataset notifications") - group.add_argument( - "--port-notify", default=3250, type=int, - help="TCP port to connect to for dataset notifications") - group = self.argparser.add_argument_group("GUI server") - group.add_argument( - "--server-gui", default="::1", - help="hostname or IP to connect to for GUI control") - group.add_argument( - "--port-gui", default=6501, type=int, - help="TCP port to connect to for GUI control") - group.add_argument("--embed", default=None, type=int, - help="embed main widget into existing window") + self._arggroup_datasets = self.argparser.add_argument_group("datasets") + + subparsers = self.argparser.add_subparsers(dest="mode") + subparsers.required = True + + parser_sa = subparsers.add_parser("standalone", + help="run standalone, connect to master directly") + parser_sa.add_argument( + "--server", default="::1", + help="hostname or IP to connect to") + parser_sa.add_argument( + "--port", default=3250, type=int, + help="TCP port to connect to") + + parser_em = subparsers.add_parser("embedded", + help="embed into GUI") + parser_em.add_argument("ipc_address", + help="address for pipe_ipc") + self.dataset_args = set() def add_dataset(self, name, help=None, required=True): @@ -56,6 +78,25 @@ class SimpleApplet: self.loop = QEventLoop(app) asyncio.set_event_loop(self.loop) + def ipc_init(self): + if self.args.mode == "standalone": + # nothing to do + pass + elif self.args.mode == "embedded": + self.ipc = AppletIPCClient(self.args.ipc_address) + self.loop.run_until_complete(self.ipc.connect()) + else: + raise NotImplementedError + + def ipc_close(self): + if self.args.mode == "standalone": + # nothing to do + pass + elif self.args.mode == "embedded": + self.ipc.close() + else: + raise NotImplementedError + def create_main_widget(self): self.main_widget = self.main_widget_class(self.args) # Qt window embedding is ridiculously buggy, and empirical testing @@ -65,15 +106,10 @@ class SimpleApplet: # 3. applet sends the ID to host, host embeds the widget # 4. applet shows the widget # Doing embedding the other way around (using QWindow.setParent in the - # applet) breaks resizing; furthermore the host needs to know our - # window ID to request graceful termination by closing the window. - if self.args.embed is not None: + # applet) breaks resizing. + if self.args.mode == "embedded": win_id = int(self.main_widget.winId()) - remote = Client(self.args.server_gui, self.args.port_gui, "applets") - try: - remote.embed(self.args.embed, win_id) - finally: - remote.close_rpc() + self.loop.run_until_complete(self.ipc.embed(win_id)) self.main_widget.show() def sub_init(self, data): @@ -81,6 +117,10 @@ class SimpleApplet: return data def filter_mod(self, mod): + if self.args.mode == "embedded": + # the parent already filters for us + return True + if mod["action"] == "init": return True if mod["path"]: @@ -108,21 +148,40 @@ class SimpleApplet: else: self.main_widget.data_changed(self.data, [mod]) - def create_subscriber(self): - self.subscriber = Subscriber("datasets", - self.sub_init, self.sub_mod) - self.loop.run_until_complete(self.subscriber.connect( - self.args.server_notify, self.args.port_notify)) + def subscribe(self): + if self.args.mode == "standalone": + self.subscriber = Subscriber("datasets", + self.sub_init, self.sub_mod) + self.loop.run_until_complete(self.subscriber.connect( + self.args.server_notify, self.args.port_notify)) + elif self.args.mode == "embedded": + # TODO + pass + else: + raise NotImplementedError + + def unsubscribe(self): + if self.args.mode == "standalone": + self.loop.run_until_complete(self.subscriber.close()) + elif self.args.mode == "embedded": + # nothing to do + pass + else: + raise NotImplementedError def run(self): self.args_init() self.quamash_init() try: - self.create_main_widget() - self.create_subscriber() + self.ipc_init() try: - self.loop.run_forever() + self.create_main_widget() + self.subscribe() + try: + self.loop.run_forever() + finally: + self.unsubscribe() finally: - self.loop.run_until_complete(self.subscriber.close()) + self.ipc_close() finally: self.loop.close() diff --git a/artiq/frontend/artiq_gui.py b/artiq/frontend/artiq_gui.py index 06de89683..d4e36445b 100755 --- a/artiq/frontend/artiq_gui.py +++ b/artiq/frontend/artiq_gui.py @@ -12,7 +12,7 @@ from pyqtgraph import dockarea from artiq import __artiq_dir__ as artiq_dir from artiq.tools import * -from artiq.protocols.pc_rpc import AsyncioClient, Server +from artiq.protocols.pc_rpc import AsyncioClient from artiq.gui.models import ModelSubscriber from artiq.gui import (state, experiments, shortcuts, explorer, moninj, datasets, applets, schedule, log, console) @@ -111,9 +111,9 @@ def main(): d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"]) - appletmgr = applets.AppletManager(dock_area) - atexit_register_coroutine(appletmgr.stop) - smgr.register(appletmgr) + d_applets = applets.AppletsDock(dock_area) + atexit_register_coroutine(d_applets.stop) + smgr.register(d_applets) if os.name != "nt": d_ttl_dds = moninj.MonInj() @@ -133,11 +133,11 @@ def main(): if os.name != "nt": 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(appletmgr.main_dock, "above", d_ttl_dds.ttl_dock) - dock_area.addDock(d_datasets, "above", appletmgr.main_dock) + dock_area.addDock(d_applets, "above", d_ttl_dds.ttl_dock) + dock_area.addDock(d_datasets, "above", d_applets) else: - dock_area.addDock(appletmgr.main_dock, "top") - dock_area.addDock(d_datasets, "above", appletmgr.main_dock) + dock_area.addDock(d_applets, "top") + dock_area.addDock(d_datasets, "above", d_applets) dock_area.addDock(d_shortcuts, "above", d_datasets) dock_area.addDock(d_explorer, "above", d_shortcuts) dock_area.addDock(d_console, "bottom") @@ -153,11 +153,6 @@ def main(): if d_log0 is not None: dock_area.addDock(d_log0, "right", d_explorer) - # start RPC server - rpc_server = Server({"applets": appletmgr.rpc}) - loop.run_until_complete(rpc_server.start("::1", 6501)) - atexit_register_coroutine(rpc_server.stop) - # run win.show() loop.run_until_complete(win.exit_request.wait()) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 87a40516b..755491f17 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -7,17 +7,47 @@ from functools import partial from quamash import QtCore, QtGui, QtWidgets from pyqtgraph import dockarea +from artiq.protocols import pyon +from artiq.protocols.pipe_ipc import AsyncioParentComm + logger = logging.getLogger(__name__) +class AppletIPCServer(AsyncioParentComm): + def __init__(self, capture_cb): + AsyncioParentComm.__init__(self) + self.capture_cb = capture_cb + + def write_pyon(self, obj): + self.write(pyon.encode(obj).encode() + b"\n") + + async def read_pyon(self): + line = await self.readline() + return pyon.decode(line.decode()) + + async def serve(self): + while True: + obj = await self.read_pyon() + try: + action = obj["action"] + if action == "embed": + self.capture_cb(obj["win_id"]) + self.write_pyon({"action": "embed_done"}) + else: + raise ValueError("unknown action in applet request") + except: + logger.warning("error processing applet request", + exc_info=True) + self.write_pyon({"action": "error"}) + + class AppletDock(dockarea.Dock): - def __init__(self, token, name, command): - dockarea.Dock.__init__(self, "applet" + str(token), + def __init__(self, name, command): + dockarea.Dock.__init__(self, "applet" + str(id(self)), # XXX label="Applet: " + name, closable=True) self.setMinimumSize(QtCore.QSize(500, 400)) - self.token = token self.applet_name = name self.command = command @@ -26,41 +56,38 @@ class AppletDock(dockarea.Dock): self.label.setText("Applet: " + name) async def start(self): + self.ipc = AppletIPCServer(self.capture) command = self.command.format(python=sys.executable, - embed_token=self.token) + ipc_address=self.ipc.get_address()) logger.debug("starting command %s for %s", command, self.applet_name) try: - self.process = await asyncio.create_subprocess_exec( - *shlex.split(command)) + await self.ipc.create_subprocess(*shlex.split(command)) except: logger.warning("Applet %s failed to start", self.applet_name, exc_info=True) + asyncio.ensure_future(self.ipc.serve()) def capture(self, win_id): logger.debug("capturing window 0x%x for %s", win_id, self.applet_name) - self.captured_window = QtGui.QWindow.fromWinId(win_id) - self.captured_widget = QtWidgets.QWidget.createWindowContainer( - self.captured_window) - self.addWidget(self.captured_widget) + captured_window = QtGui.QWindow.fromWinId(win_id) + captured_widget = QtWidgets.QWidget.createWindowContainer( + captured_window) + self.addWidget(captured_widget) async def terminate(self): - if hasattr(self, "captured_window"): - self.captured_window.close() - self.captured_widget.deleteLater() - del self.captured_window - del self.captured_widget if hasattr(self, "process"): + # TODO: send IPC termination request try: - await asyncio.wait_for(self.process.wait(), 2.0) + await asyncio.wait_for(self.ipc.process.wait(), 2.0) except: logger.warning("Applet %s failed to exit, killing", self.applet_name) try: - self.process.kill() + self.ipc.process.kill() except ProcessLookupError: pass - await self.process.wait() - del self.process + await self.ipc.process.wait() + del self.ipc async def restart(self): await self.terminate() @@ -69,24 +96,25 @@ class AppletDock(dockarea.Dock): _templates = [ ("Big number", "{python} -m artiq.applets.big_number " - "--embed {embed_token} NUMBER_DATASET"), + "embedded {ipc_address} NUMBER_DATASET"), ("Histogram", "{python} -m artiq.applets.plot_hist " - "--embed {embed_token} COUNTS_DATASET " + "embedded {ipc_address} COUNTS_DATASET " "--x BIN_BOUNDARIES_DATASET"), ("XY", "{python} -m artiq.applets.plot_xy " - "--embed {embed_token} Y_DATASET --x X_DATASET " + "embedded {ipc_address} Y_DATASET --x X_DATASET " "--error ERROR_DATASET --fit FIT_DATASET"), ("XY + Histogram", "{python} -m artiq.applets.plot_xy_hist " - "--embed {embed_token} X_DATASET " + "embedded {ipc_address} X_DATASET " "HIST_BIN_BOUNDARIES_DATASET " "HISTS_COUNTS_DATASET"), ] class AppletsDock(dockarea.Dock): - def __init__(self, manager): - self.manager = manager - self.token_to_checkbox = dict() + def __init__(self, dock_area): + self.dock_area = dock_area + self.dock_to_checkbox = dict() + self.workaround_pyqtgraph_bug = False dockarea.Dock.__init__(self, "Applets") self.setMinimumSize(QtCore.QSize(850, 450)) @@ -129,6 +157,18 @@ class AppletsDock(dockarea.Dock): self.table.cellChanged.connect(self.cell_changed) + def create(self, name, command): + dock = AppletDock(name, command) + # If a dock is floated and then dock state is restored, pyqtgraph + # leaves a "phantom" window open. + if self.workaround_pyqtgraph_bug: + self.dock_area.addDock(dock) + else: + self.dock_area.floatDock(dock) + asyncio.ensure_future(dock.start()) + dock.sigClosed.connect(partial(self.on_dock_closed, dock)) + return dock + def cell_changed(self, row, column): if column == 0: item = self.table.item(row, column) @@ -141,27 +181,28 @@ class AppletsDock(dockarea.Dock): name = "" else: name = name.text() - token = self.manager.create(name, command) - item.applet_token = token - self.token_to_checkbox[token] = item + dock = self.create(name, command) + item.applet_dock = dock + self.dock_to_checkbox[dock] = item else: - token = getattr(item, "applet_token", None) - if token is not None: - # cell_changed is emitted at row creation - self.manager.delete(token) + dock = getattr(item, "applet_dock", None) + if dock is not None: + # This calls self.on_dock_closed + dock.close() elif column == 1 or column == 2: new_value = self.table.item(row, column).text() - token = getattr(self.table.item(row, 0), "applet_token", None) - if token is not None: + dock = getattr(self.table.item(row, 0), "applet_dock", None) + if dock is not None: if column == 1: - self.manager.rename(token, new_value) + dock.rename(new_value) else: - self.manager.set_command(token, new_value) + dock.command = new_value - def disable_token(self, token): - checkbox_item = self.token_to_checkbox[token] - checkbox_item.applet_token = None - del self.token_to_checkbox[token] + def on_dock_closed(self, dock): + asyncio.ensure_future(dock.terminate()) + checkbox_item = self.dock_to_checkbox[dock] + checkbox_item.applet_dock = None + del self.dock_to_checkbox[dock] checkbox_item.setCheckState(QtCore.Qt.Unchecked) def new(self): @@ -185,19 +226,26 @@ class AppletsDock(dockarea.Dock): selection = self.table.selectedRanges() if selection: row = selection[0].topRow() - token = getattr(self.table.item(row, 0), "applet_token", None) - if token is not None: - asyncio.ensure_future(self.manager.restart(token)) + dock = getattr(self.table.item(row, 0), "applet_dock", None) + if dock is not None: + asyncio.ensure_future(dock.restart()) def delete(self): selection = self.table.selectedRanges() if selection: row = selection[0].topRow() - token = getattr(self.table.item(row, 0), "applet_token", None) - if token is not None: - self.manager.delete(token) + dock = getattr(self.table.item(row, 0), "applet_dock", None) + if dock is not None: + # This calls self.on_dock_closed + dock.close() self.table.removeRow(row) + async def stop(self): + for row in range(self.table.rowCount()): + dock = getattr(self.table.item(row, 0), "applet_dock", None) + if dock is not None: + await dock.terminate() + def save_state(self): state = [] for row in range(self.table.rowCount()): @@ -208,6 +256,7 @@ class AppletsDock(dockarea.Dock): return state def restore_state(self, state): + self.workaround_pyqtgraph_bug = True for enabled, name, command in state: row = self.new() item = QtWidgets.QTableWidgetItem() @@ -218,72 +267,4 @@ class AppletsDock(dockarea.Dock): self.table.setItem(row, 2, item) if enabled: self.table.item(row, 0).setCheckState(QtCore.Qt.Checked) - - -class AppletManagerRPC: - def __init__(self, parent): - self.parent = parent - - def embed(self, token, win_id): - self.parent.embed(token, win_id) - - -class AppletManager: - def __init__(self, dock_area): - self.dock_area = dock_area - self.main_dock = AppletsDock(self) - self.rpc = AppletManagerRPC(self) - self.applet_docks = dict() - self.workaround_pyqtgraph_bug = False - - def embed(self, token, win_id): - if token not in self.applet_docks: - logger.warning("Ignored incorrect embed token %d for winid 0x%x", - token, win_id) - return - self.applet_docks[token].capture(win_id) - - def create(self, name, command): - token = next(iter(set(range(len(self.applet_docks) + 1)) - - self.applet_docks.keys())) - dock = AppletDock(token, name, command) - self.applet_docks[token] = dock - # If a dock is floated and then dock state is restored, pyqtgraph - # leaves a "phantom" window open. - if self.workaround_pyqtgraph_bug: - self.dock_area.addDock(dock) - else: - self.dock_area.floatDock(dock) - asyncio.ensure_future(dock.start()) - dock.sigClosed.connect(partial(self.on_dock_closed, token)) - return token - - def on_dock_closed(self, token): - asyncio.ensure_future(self.applet_docks[token].terminate()) - self.main_dock.disable_token(token) - del self.applet_docks[token] - - def delete(self, token): - # This in turns calls on_dock_closed and main_dock.disable_token - self.applet_docks[token].close() - - def rename(self, token, name): - self.applet_docks[token].rename(name) - - def set_command(self, token, command): - self.applet_docks[token].command = command - - async def restart(self, token): - await self.applet_docks[token].restart() - - async def stop(self): - for dock in self.applet_docks.values(): - await dock.terminate() - - def save_state(self): - return self.main_dock.save_state() - - def restore_state(self, state): - self.workaround_pyqtgraph_bug = True - self.main_dock.restore_state(state) self.workaround_pyqtgraph_bug = False From 8a912105cb2441d0f6a5602e2c2e279d3bc20610 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 14:05:00 +0100 Subject: [PATCH 25/44] protocols/pyon: remove FlatFileDB --- artiq/protocols/pyon.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/artiq/protocols/pyon.py b/artiq/protocols/pyon.py index 238ff2198..b7abfa93a 100644 --- a/artiq/protocols/pyon.py +++ b/artiq/protocols/pyon.py @@ -202,23 +202,3 @@ def load_file(filename): """Parses the specified file and returns the decoded Python object.""" with open(filename, "r") as f: return decode(f.read()) - - -class FlatFileDB: - def __init__(self, filename): - self.filename = filename - self.data = pyon.load_file(self.filename) - - def save(self): - pyon.store_file(self.filename, self.data) - - def get(self, key): - return self.data[key] - - def set(self, key, value): - self.data[key] = value - self.save() - - def delete(self, key): - del self.data[key] - self.save() From 8be0696b39cb68d404be59d36f5ef5fa519b47bc Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 14:08:14 +0100 Subject: [PATCH 26/44] protocols/pyon: set support --- artiq/protocols/pyon.py | 15 +++++++++------ artiq/test/serialization.py | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/artiq/protocols/pyon.py b/artiq/protocols/pyon.py index b7abfa93a..10b5412aa 100644 --- a/artiq/protocols/pyon.py +++ b/artiq/protocols/pyon.py @@ -35,6 +35,7 @@ _encode_map = { bytes: "bytes", tuple: "tuple", list: "list", + set: "set", dict: "dict", wrapping_int: "number", Fraction: "fraction", @@ -98,6 +99,12 @@ class _Encoder: r += "]" return r + def encode_set(self, x): + r = "{" + r += ", ".join([self.encode(item) for item in x]) + r += "}" + return r + def encode_dict(self, x): r = "{" if not self.pretty or len(x) < 2: @@ -149,9 +156,7 @@ class _Encoder: def encode(x, pretty=False): """Serializes a Python object and returns the corresponding string in - Python syntax. - - """ + Python syntax.""" return _Encoder(pretty).encode(x) @@ -181,9 +186,7 @@ _eval_dict = { def decode(s): """Parses a string in the Python syntax, reconstructs the corresponding - object, and returns it. - - """ + object, and returns it.""" return eval(s, _eval_dict, {}) diff --git a/artiq/test/serialization.py b/artiq/test/serialization.py index 88cf9b18e..9c23481b9 100644 --- a/artiq/test/serialization.py +++ b/artiq/test/serialization.py @@ -10,6 +10,7 @@ from artiq.protocols import pyon _pyon_test_object = { (1, 2): [(3, 4.2), (2, )], Fraction(3, 4): np.linspace(5, 10, 1), + {"testing", "sets"}, "a": np.int8(9), "b": np.int16(-98), "c": np.int32(42), "d": np.int64(-5), "e": np.uint8(8), "f": np.uint16(5), "g": np.uint32(4), "h": np.uint64(9), "x": np.float16(9.0), "y": np.float32(9.0), "z": np.float64(9.0), From 8844fba4c98b5ed518a1beed64dc3bd7229e7af1 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 14:35:37 +0100 Subject: [PATCH 27/44] applets: clean shutdown --- artiq/applets/simple.py | 42 ++++++++++++++++++++++--- artiq/gui/applets.py | 69 ++++++++++++++++++++++++----------------- 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 8fc5ceafc..24a76e464 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -1,3 +1,4 @@ +import logging import argparse import asyncio @@ -8,7 +9,13 @@ from artiq.protocols import pyon from artiq.protocols.pipe_ipc import AsyncioChildComm +logger = logging.getLogger(__name__) + + class AppletIPCClient(AsyncioChildComm): + def set_close_cb(self, close_cb): + self.close_cb = close_cb + def write_pyon(self, obj): self.write(pyon.encode(obj).encode() + b"\n") @@ -17,12 +24,37 @@ class AppletIPCClient(AsyncioChildComm): return pyon.decode(line.decode()) async def embed(self, win_id): + # This function is only called when not subscribed to anything, + # so the only normal replies are embed_done and terminate. self.write_pyon({"action": "embed", "win_id": win_id}) reply = await self.read_pyon() - if reply["action"] != "embed_done": - raise ValueError("Got erroneous reply to embed request", - reply) + if reply["action"] == "terminate": + self.close_cb() + elif reply["action"] != "embed_done": + logger.error("unexpected action reply to embed request: %s", + action) + self.close_cb() + + async def listen(self): + while True: + obj = await self.read_pyon() + try: + action = obj["action"] + if action == "terminate": + self.close_cb() + return + else: + raise ValueError("unknown action in applet request") + except: + logger.error("error processing applet request", + exc_info=True) + self.close_cb() + + def subscribe(self, datasets): + self.write_pyon({"action": "subscribe", + "datasets": datasets}) + asyncio.ensure_future(self.listen()) class SimpleApplet: @@ -108,6 +140,7 @@ class SimpleApplet: # Doing embedding the other way around (using QWindow.setParent in the # applet) breaks resizing. if self.args.mode == "embedded": + self.ipc.set_close_cb(self.main_widget.close) win_id = int(self.main_widget.winId()) self.loop.run_until_complete(self.ipc.embed(win_id)) self.main_widget.show() @@ -155,8 +188,7 @@ class SimpleApplet: self.loop.run_until_complete(self.subscriber.connect( self.args.server_notify, self.args.port_notify)) elif self.args.mode == "embedded": - # TODO - pass + self.ipc.subscribe(self.datasets) else: raise NotImplementedError diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 755491f17..909069796 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -7,18 +7,14 @@ from functools import partial from quamash import QtCore, QtGui, QtWidgets from pyqtgraph import dockarea -from artiq.protocols import pyon from artiq.protocols.pipe_ipc import AsyncioParentComm +from artiq.protocols import pyon logger = logging.getLogger(__name__) class AppletIPCServer(AsyncioParentComm): - def __init__(self, capture_cb): - AsyncioParentComm.__init__(self) - self.capture_cb = capture_cb - def write_pyon(self, obj): self.write(pyon.encode(obj).encode() + b"\n") @@ -26,25 +22,40 @@ class AppletIPCServer(AsyncioParentComm): line = await self.readline() return pyon.decode(line.decode()) - async def serve(self): - while True: - obj = await self.read_pyon() - try: - action = obj["action"] - if action == "embed": - self.capture_cb(obj["win_id"]) - self.write_pyon({"action": "embed_done"}) - else: - raise ValueError("unknown action in applet request") - except: - logger.warning("error processing applet request", - exc_info=True) - self.write_pyon({"action": "error"}) + async def serve(self, embed_cb): + try: + while True: + obj = await self.read_pyon() + try: + action = obj["action"] + if action == "embed": + embed_cb(obj["win_id"]) + self.write_pyon({"action": "embed_done"}) + elif action == "subscribe": + print("applet subscribed: ", obj["datasets"]) + else: + raise ValueError("unknown action in applet request") + except: + logger.warning("error processing applet request", + exc_info=True) + self.write_pyon({"action": "error"}) + except asyncio.CancelledError: + pass + except: + logger.error("error processing data from applet, " + "server stopped", exc_info=True) + + def start(self, embed_cb): + self.server_task = asyncio.ensure_future(self.serve(embed_cb)) + + async def stop(self): + self.server_task.cancel() + await asyncio.wait([self.server_task]) class AppletDock(dockarea.Dock): def __init__(self, name, command): - dockarea.Dock.__init__(self, "applet" + str(id(self)), # XXX + dockarea.Dock.__init__(self, "applet" + str(id(self)), # TODO label="Applet: " + name, closable=True) self.setMinimumSize(QtCore.QSize(500, 400)) @@ -56,7 +67,7 @@ class AppletDock(dockarea.Dock): self.label.setText("Applet: " + name) async def start(self): - self.ipc = AppletIPCServer(self.capture) + self.ipc = AppletIPCServer() command = self.command.format(python=sys.executable, ipc_address=self.ipc.get_address()) logger.debug("starting command %s for %s", command, self.applet_name) @@ -65,18 +76,18 @@ class AppletDock(dockarea.Dock): except: logger.warning("Applet %s failed to start", self.applet_name, exc_info=True) - asyncio.ensure_future(self.ipc.serve()) + self.ipc.start(self.embed) - def capture(self, win_id): + def embed(self, win_id): logger.debug("capturing window 0x%x for %s", win_id, self.applet_name) - captured_window = QtGui.QWindow.fromWinId(win_id) - captured_widget = QtWidgets.QWidget.createWindowContainer( - captured_window) - self.addWidget(captured_widget) + embed_window = QtGui.QWindow.fromWinId(win_id) + embed_widget = QtWidgets.QWidget.createWindowContainer(embed_window) + self.addWidget(embed_widget) async def terminate(self): - if hasattr(self, "process"): - # TODO: send IPC termination request + if hasattr(self, "ipc"): + await self.ipc.stop() + self.ipc.write_pyon({"action": "terminate"}) try: await asyncio.wait_for(self.ipc.process.wait(), 2.0) except: From f25b5442e76cb3f0f0eadc6145ae1f679db64472 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 16:26:02 +0100 Subject: [PATCH 28/44] applets: properly name docks to support state save/restore --- artiq/gui/applets.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 909069796..626195d87 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -54,8 +54,8 @@ class AppletIPCServer(AsyncioParentComm): class AppletDock(dockarea.Dock): - def __init__(self, name, command): - dockarea.Dock.__init__(self, "applet" + str(id(self)), # TODO + def __init__(self, uid, name, command): + dockarea.Dock.__init__(self, "applet" + str(uid), label="Applet: " + name, closable=True) self.setMinimumSize(QtCore.QSize(500, 400)) @@ -125,6 +125,7 @@ class AppletsDock(dockarea.Dock): def __init__(self, dock_area): self.dock_area = dock_area self.dock_to_checkbox = dict() + self.applet_uids = set() self.workaround_pyqtgraph_bug = False dockarea.Dock.__init__(self, "Applets") @@ -168,8 +169,8 @@ class AppletsDock(dockarea.Dock): self.table.cellChanged.connect(self.cell_changed) - def create(self, name, command): - dock = AppletDock(name, command) + def create(self, uid, name, command): + dock = AppletDock(uid, name, command) # If a dock is floated and then dock state is restored, pyqtgraph # leaves a "phantom" window open. if self.workaround_pyqtgraph_bug: @@ -192,17 +193,17 @@ class AppletsDock(dockarea.Dock): name = "" else: name = name.text() - dock = self.create(name, command) + dock = self.create(item.applet_uid, name, command) item.applet_dock = dock self.dock_to_checkbox[dock] = item else: - dock = getattr(item, "applet_dock", None) + dock = item.applet_dock if dock is not None: # This calls self.on_dock_closed dock.close() elif column == 1 or column == 2: new_value = self.table.item(row, column).text() - dock = getattr(self.table.item(row, 0), "applet_dock", None) + dock = self.table.item(row, 0).applet_dock if dock is not None: if column == 1: dock.rename(new_value) @@ -217,6 +218,10 @@ class AppletsDock(dockarea.Dock): checkbox_item.setCheckState(QtCore.Qt.Unchecked) def new(self): + uid = next(iter(set(range(len(self.applet_uids) + 1)) + - self.applet_uids)) + self.applet_uids.add(uid) + row = self.table.rowCount() self.table.insertRow(row) checkbox = QtWidgets.QTableWidgetItem() @@ -224,6 +229,8 @@ class AppletsDock(dockarea.Dock): QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled) checkbox.setCheckState(QtCore.Qt.Unchecked) + checkbox.applet_uid = uid + checkbox.applet_dock = None self.table.setItem(row, 0, checkbox) self.table.setItem(row, 1, QtWidgets.QTableWidgetItem()) self.table.setItem(row, 2, QtWidgets.QTableWidgetItem()) @@ -237,7 +244,7 @@ class AppletsDock(dockarea.Dock): selection = self.table.selectedRanges() if selection: row = selection[0].topRow() - dock = getattr(self.table.item(row, 0), "applet_dock", None) + dock = self.table.item(row, 0).applet_dock if dock is not None: asyncio.ensure_future(dock.restart()) @@ -245,15 +252,18 @@ class AppletsDock(dockarea.Dock): selection = self.table.selectedRanges() if selection: row = selection[0].topRow() - dock = getattr(self.table.item(row, 0), "applet_dock", None) + item = self.table.item(row, 0) + dock = item.applet_dock if dock is not None: # This calls self.on_dock_closed dock.close() + self.applet_uids.remove(item.applet_uid) self.table.removeRow(row) + async def stop(self): for row in range(self.table.rowCount()): - dock = getattr(self.table.item(row, 0), "applet_dock", None) + dock = self.table.item(row, 0).applet_dock if dock is not None: await dock.terminate() From de99e7f830f9e1cc9a2c4dea5f5cd898559e42de Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 19:20:07 +0100 Subject: [PATCH 29/44] applets: handle dataset mutations --- artiq/applets/simple.py | 16 ++++++++++++--- artiq/frontend/artiq_gui.py | 2 +- artiq/gui/applets.py | 41 ++++++++++++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 24a76e464..7da0c0a31 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -4,7 +4,7 @@ import asyncio from quamash import QEventLoop, QtWidgets, QtGui, QtCore -from artiq.protocols.sync_struct import Subscriber +from artiq.protocols.sync_struct import Subscriber, process_mod from artiq.protocols import pyon from artiq.protocols.pipe_ipc import AsyncioChildComm @@ -37,6 +37,7 @@ class AppletIPCClient(AsyncioChildComm): self.close_cb() async def listen(self): + data = None while True: obj = await self.read_pyon() try: @@ -44,6 +45,13 @@ class AppletIPCClient(AsyncioChildComm): if action == "terminate": self.close_cb() return + elif action == "mod": + mod = obj["mod"] + if mod["action"] == "init": + data = self.init_cb(mod["struct"]) + else: + process_mod(data, mod) + self.mod_cb(mod) else: raise ValueError("unknown action in applet request") except: @@ -51,9 +59,11 @@ class AppletIPCClient(AsyncioChildComm): exc_info=True) self.close_cb() - def subscribe(self, datasets): + def subscribe(self, datasets, init_cb, mod_cb): self.write_pyon({"action": "subscribe", "datasets": datasets}) + self.init_cb = init_cb + self.mod_cb = mod_cb asyncio.ensure_future(self.listen()) @@ -188,7 +198,7 @@ class SimpleApplet: self.loop.run_until_complete(self.subscriber.connect( self.args.server_notify, self.args.port_notify)) elif self.args.mode == "embedded": - self.ipc.subscribe(self.datasets) + self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod) else: raise NotImplementedError diff --git a/artiq/frontend/artiq_gui.py b/artiq/frontend/artiq_gui.py index d4e36445b..ddbcc0c48 100755 --- a/artiq/frontend/artiq_gui.py +++ b/artiq/frontend/artiq_gui.py @@ -111,7 +111,7 @@ def main(): d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"]) - d_applets = applets.AppletsDock(dock_area) + d_applets = applets.AppletsDock(dock_area, sub_clients["datasets"]) atexit_register_coroutine(d_applets.stop) smgr.register(d_applets) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 626195d87..6df993804 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -15,6 +15,11 @@ logger = logging.getLogger(__name__) class AppletIPCServer(AsyncioParentComm): + def __init__(self, datasets_sub): + AsyncioParentComm.__init__(self) + self.datasets_sub = datasets_sub + self.datasets = set() + def write_pyon(self, obj): self.write(pyon.encode(obj).encode() + b"\n") @@ -22,7 +27,25 @@ class AppletIPCServer(AsyncioParentComm): line = await self.readline() return pyon.decode(line.decode()) + def _synthesize_init(self, data): + struct = {k: v for k, v in data.items() if k in self.datasets} + return {"action": "init", + "struct": struct} + + def _on_mod(self, mod): + if mod["action"] == "init": + mod = self._synthesize_init(mod["struct"]) + else: + if mod["path"]: + if mod["path"][0] not in self.datasets: + return + elif mod["action"] in {"setitem", "delitem"}: + if mod["key"] not in self.datasets: + return + self.write_pyon({"action": "mod", "mod": mod}) + async def serve(self, embed_cb): + self.datasets_sub.notify_cbs.append(self._on_mod) try: while True: obj = await self.read_pyon() @@ -32,7 +55,11 @@ class AppletIPCServer(AsyncioParentComm): embed_cb(obj["win_id"]) self.write_pyon({"action": "embed_done"}) elif action == "subscribe": - print("applet subscribed: ", obj["datasets"]) + self.datasets = obj["datasets"] + if self.datasets_sub.model is not None: + mod = self._synthesize_init( + self.datasets_sub.model.backing_store) + self.write_pyon({"action": "mod", "mod": mod}) else: raise ValueError("unknown action in applet request") except: @@ -44,6 +71,8 @@ class AppletIPCServer(AsyncioParentComm): except: logger.error("error processing data from applet, " "server stopped", exc_info=True) + finally: + self.datasets_sub.notify_cbs.remove(self._on_mod) def start(self, embed_cb): self.server_task = asyncio.ensure_future(self.serve(embed_cb)) @@ -54,11 +83,12 @@ class AppletIPCServer(AsyncioParentComm): class AppletDock(dockarea.Dock): - def __init__(self, uid, name, command): + def __init__(self, datasets_sub, uid, name, command): dockarea.Dock.__init__(self, "applet" + str(uid), label="Applet: " + name, closable=True) self.setMinimumSize(QtCore.QSize(500, 400)) + self.datasets_sub = datasets_sub self.applet_name = name self.command = command @@ -67,7 +97,7 @@ class AppletDock(dockarea.Dock): self.label.setText("Applet: " + name) async def start(self): - self.ipc = AppletIPCServer() + self.ipc = AppletIPCServer(self.datasets_sub) command = self.command.format(python=sys.executable, ipc_address=self.ipc.get_address()) logger.debug("starting command %s for %s", command, self.applet_name) @@ -122,8 +152,9 @@ _templates = [ class AppletsDock(dockarea.Dock): - def __init__(self, dock_area): + def __init__(self, dock_area, datasets_sub): self.dock_area = dock_area + self.datasets_sub = datasets_sub self.dock_to_checkbox = dict() self.applet_uids = set() self.workaround_pyqtgraph_bug = False @@ -170,7 +201,7 @@ class AppletsDock(dockarea.Dock): self.table.cellChanged.connect(self.cell_changed) def create(self, uid, name, command): - dock = AppletDock(uid, name, command) + dock = AppletDock(self.datasets_sub, uid, name, command) # If a dock is floated and then dock state is restored, pyqtgraph # leaves a "phantom" window open. if self.workaround_pyqtgraph_bug: From 7584b02d6638065384a4c652b9d377594bba1609 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 19:23:50 +0100 Subject: [PATCH 30/44] applets: fix error message text --- artiq/applets/simple.py | 4 ++-- artiq/gui/applets.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 7da0c0a31..0acc18360 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -53,9 +53,9 @@ class AppletIPCClient(AsyncioChildComm): process_mod(data, mod) self.mod_cb(mod) else: - raise ValueError("unknown action in applet request") + raise ValueError("unknown action in parent message") except: - logger.error("error processing applet request", + logger.error("error processing parent message", exc_info=True) self.close_cb() diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 6df993804..f69c7d0f1 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -61,9 +61,9 @@ class AppletIPCServer(AsyncioParentComm): self.datasets_sub.model.backing_store) self.write_pyon({"action": "mod", "mod": mod}) else: - raise ValueError("unknown action in applet request") + raise ValueError("unknown action in applet message") except: - logger.warning("error processing applet request", + logger.warning("error processing applet message", exc_info=True) self.write_pyon({"action": "error"}) except asyncio.CancelledError: From d873c25b8baa8fe2e10d3226028fa3cc3fc195db Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 19:32:40 +0100 Subject: [PATCH 31/44] Use Qt5 --- artiq/frontend/artiq_gui.py | 6 ++++-- conda/artiq/meta.yaml | 1 + setup.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/artiq/frontend/artiq_gui.py b/artiq/frontend/artiq_gui.py index 06de89683..eb1e9cbb7 100755 --- a/artiq/frontend/artiq_gui.py +++ b/artiq/frontend/artiq_gui.py @@ -5,11 +5,13 @@ import asyncio import atexit import os -# Quamash must be imported first so that pyqtgraph picks up the Qt binding -# it has chosen. +import PyQt5 from quamash import QEventLoop, QtGui, QtCore +assert QtGui is PyQt5.QtGui +# pyqtgraph will pick up any already imported Qt binding. from pyqtgraph import dockarea + from artiq import __artiq_dir__ as artiq_dir from artiq.tools import * from artiq.protocols.pc_rpc import AsyncioClient, Server diff --git a/conda/artiq/meta.yaml b/conda/artiq/meta.yaml index 28b474860..c0ebf8442 100644 --- a/conda/artiq/meta.yaml +++ b/conda/artiq/meta.yaml @@ -46,6 +46,7 @@ requirements: - sphinx-argparse - h5py - dateutil + - pyqt 5.* - quamash - pyqtgraph - pygit2 diff --git a/setup.py b/setup.py index 1f5c651bd..e40670709 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ if sys.version_info[:3] < (3, 5, 1): requirements = [ "sphinx", "sphinx-argparse", "pyserial", "numpy", "scipy", - "python-dateutil", "prettytable", "h5py", + "python-dateutil", "prettytable", "h5py", "pyqt5", "quamash", "pyqtgraph", "pygit2", "aiohttp", "llvmlite_artiq", "pythonparser", "python-Levenshtein", "lit", "OutputCheck", From 1830d6a62e6dc99712b166d68002786a29fcd32d Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 19:33:35 +0100 Subject: [PATCH 32/44] setup.py: fix PyQt5 package name --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e40670709..b29706dc6 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ if sys.version_info[:3] < (3, 5, 1): requirements = [ "sphinx", "sphinx-argparse", "pyserial", "numpy", "scipy", - "python-dateutil", "prettytable", "h5py", "pyqt5", + "python-dateutil", "prettytable", "h5py", "PyQt5", "quamash", "pyqtgraph", "pygit2", "aiohttp", "llvmlite_artiq", "pythonparser", "python-Levenshtein", "lit", "OutputCheck", From 13a8f9c7ca2d6afe4103cfb8534cdb66989abb92 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 19:42:08 +0100 Subject: [PATCH 33/44] setup.py: give up trying to check for PyQt --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b29706dc6..10bd8982e 100755 --- a/setup.py +++ b/setup.py @@ -10,9 +10,10 @@ if sys.version_info[:3] < (3, 5, 1): raise Exception("You need Python 3.5.1+") +# Depends on PyQt5, but setuptools cannot check for it. requirements = [ "sphinx", "sphinx-argparse", "pyserial", "numpy", "scipy", - "python-dateutil", "prettytable", "h5py", "PyQt5", + "python-dateutil", "prettytable", "h5py", "quamash", "pyqtgraph", "pygit2", "aiohttp", "llvmlite_artiq", "pythonparser", "python-Levenshtein", "lit", "OutputCheck", From 4733c4ba1e70db26d9b86756a238d02b9a4bcf03 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 21:57:07 +0100 Subject: [PATCH 34/44] gui/applets: save dock UID in state --- artiq/gui/applets.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index f69c7d0f1..6281e70a4 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -248,9 +248,10 @@ class AppletsDock(dockarea.Dock): del self.dock_to_checkbox[dock] checkbox_item.setCheckState(QtCore.Qt.Unchecked) - def new(self): - uid = next(iter(set(range(len(self.applet_uids) + 1)) - - self.applet_uids)) + def new(self, uid=None): + if uid is None: + uid = next(iter(set(range(len(self.applet_uids) + 1)) + - self.applet_uids)) self.applet_uids.add(uid) row = self.table.rowCount() @@ -301,16 +302,17 @@ class AppletsDock(dockarea.Dock): def save_state(self): state = [] for row in range(self.table.rowCount()): + uid = self.table.item(row, 0).applet_uid enabled = self.table.item(row, 0).checkState() == QtCore.Qt.Checked name = self.table.item(row, 1).text() command = self.table.item(row, 2).text() - state.append((enabled, name, command)) + state.append((uid, enabled, name, command)) return state def restore_state(self, state): self.workaround_pyqtgraph_bug = True - for enabled, name, command in state: - row = self.new() + for uid, enabled, name, command in state: + row = self.new(uid) item = QtWidgets.QTableWidgetItem() item.setText(name) self.table.setItem(row, 1, item) From 1a2596de8e5e0371744ded237035038028563c34 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 22:24:23 +0100 Subject: [PATCH 35/44] examples/histogram: artiq -> artiq.experiment --- examples/master/repository/histograms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/master/repository/histograms.py b/examples/master/repository/histograms.py index c0c660708..b8c283712 100644 --- a/examples/master/repository/histograms.py +++ b/examples/master/repository/histograms.py @@ -2,7 +2,7 @@ from time import sleep import numpy as np -from artiq import * +from artiq.experiment import * class Histograms(EnvExperiment): From 67327b1e618c8ce4c59d146f5179a30fe0be5d1c Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 22:25:02 +0100 Subject: [PATCH 36/44] applets: avoid argparse subparser mess --- artiq/applets/simple.py | 59 +++++++++++++---------------------------- artiq/gui/applets.py | 8 +++--- 2 files changed, 23 insertions(+), 44 deletions(-) diff --git a/artiq/applets/simple.py b/artiq/applets/simple.py index 0acc18360..75aec014e 100644 --- a/artiq/applets/simple.py +++ b/artiq/applets/simple.py @@ -79,24 +79,20 @@ class SimpleApplet: help="time to wait after a mod (buffering other mods) " "before updating (default: %(default).2f)") - self._arggroup_datasets = self.argparser.add_argument_group("datasets") - - subparsers = self.argparser.add_subparsers(dest="mode") - subparsers.required = True - - parser_sa = subparsers.add_parser("standalone", - help="run standalone, connect to master directly") - parser_sa.add_argument( + group = self.argparser.add_argument_group("standalone mode (default)") + group.add_argument( "--server", default="::1", - help="hostname or IP to connect to") - parser_sa.add_argument( + help="hostname or IP of the master to connect to " + "for dataset notifications " + "(ignored in embedded mode)") + group.add_argument( "--port", default=3250, type=int, help="TCP port to connect to") - parser_em = subparsers.add_parser("embedded", - help="embed into GUI") - parser_em.add_argument("ipc_address", - help="address for pipe_ipc") + self.argparser.add_argument("--embed", default=None, + help="embed into GUI", metavar="IPC_ADDRESS") + + self._arggroup_datasets = self.argparser.add_argument_group("datasets") self.dataset_args = set() @@ -121,23 +117,13 @@ class SimpleApplet: asyncio.set_event_loop(self.loop) def ipc_init(self): - if self.args.mode == "standalone": - # nothing to do - pass - elif self.args.mode == "embedded": - self.ipc = AppletIPCClient(self.args.ipc_address) + if self.args.embed is not None: + self.ipc = AppletIPCClient(self.args.embed) self.loop.run_until_complete(self.ipc.connect()) - else: - raise NotImplementedError def ipc_close(self): - if self.args.mode == "standalone": - # nothing to do - pass - elif self.args.mode == "embedded": + if self.args.embed is not None: self.ipc.close() - else: - raise NotImplementedError def create_main_widget(self): self.main_widget = self.main_widget_class(self.args) @@ -149,7 +135,7 @@ class SimpleApplet: # 4. applet shows the widget # Doing embedding the other way around (using QWindow.setParent in the # applet) breaks resizing. - if self.args.mode == "embedded": + if self.args.embed is not None: self.ipc.set_close_cb(self.main_widget.close) win_id = int(self.main_widget.winId()) self.loop.run_until_complete(self.ipc.embed(win_id)) @@ -160,7 +146,7 @@ class SimpleApplet: return data def filter_mod(self, mod): - if self.args.mode == "embedded": + if self.args.embed is not None: # the parent already filters for us return True @@ -192,24 +178,17 @@ class SimpleApplet: self.main_widget.data_changed(self.data, [mod]) def subscribe(self): - if self.args.mode == "standalone": + if self.args.embed is None: self.subscriber = Subscriber("datasets", self.sub_init, self.sub_mod) self.loop.run_until_complete(self.subscriber.connect( - self.args.server_notify, self.args.port_notify)) - elif self.args.mode == "embedded": - self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod) + self.args.server, self.args.port)) else: - raise NotImplementedError + self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod) def unsubscribe(self): - if self.args.mode == "standalone": + if self.args.embed is None: self.loop.run_until_complete(self.subscriber.close()) - elif self.args.mode == "embedded": - # nothing to do - pass - else: - raise NotImplementedError def run(self): self.args_init() diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 6281e70a4..868842018 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -137,15 +137,15 @@ class AppletDock(dockarea.Dock): _templates = [ ("Big number", "{python} -m artiq.applets.big_number " - "embedded {ipc_address} NUMBER_DATASET"), + "--embed {ipc_address} NUMBER_DATASET"), ("Histogram", "{python} -m artiq.applets.plot_hist " - "embedded {ipc_address} COUNTS_DATASET " + "--embed {ipc_address} COUNTS_DATASET " "--x BIN_BOUNDARIES_DATASET"), ("XY", "{python} -m artiq.applets.plot_xy " - "embedded {ipc_address} Y_DATASET --x X_DATASET " + "--embed {ipc_address} Y_DATASET --x X_DATASET " "--error ERROR_DATASET --fit FIT_DATASET"), ("XY + Histogram", "{python} -m artiq.applets.plot_xy_hist " - "embedded {ipc_address} X_DATASET " + "--embed {ipc_address} X_DATASET " "HIST_BIN_BOUNDARIES_DATASET " "HISTS_COUNTS_DATASET"), ] From 44a1efa601679d673c3a7e4aa8412276d6766622 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 22:25:20 +0100 Subject: [PATCH 37/44] applets: make sure pyqtgraph imports qt5 --- artiq/applets/big_number.py | 2 +- artiq/applets/plot_hist.py | 1 + artiq/applets/plot_xy.py | 1 + artiq/applets/plot_xy_hist.py | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/artiq/applets/big_number.py b/artiq/applets/big_number.py index 7eb60cbcd..8d13170b8 100755 --- a/artiq/applets/big_number.py +++ b/artiq/applets/big_number.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3.5 -from quamash import QtWidgets +from PyQt5 import QtWidgets from artiq.applets.simple import SimpleApplet diff --git a/artiq/applets/plot_hist.py b/artiq/applets/plot_hist.py index 54f459d91..ff77365fc 100755 --- a/artiq/applets/plot_hist.py +++ b/artiq/applets/plot_hist.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3.5 import numpy as np +import PyQt5 # make sure pyqtgraph imports Qt5 import pyqtgraph from artiq.applets.simple import SimpleApplet diff --git a/artiq/applets/plot_xy.py b/artiq/applets/plot_xy.py index 4b8221f42..413d90cb2 100755 --- a/artiq/applets/plot_xy.py +++ b/artiq/applets/plot_xy.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3.5 import numpy as np +import PyQt5 # make sure pyqtgraph imports Qt5 import pyqtgraph from artiq.applets.simple import SimpleApplet diff --git a/artiq/applets/plot_xy_hist.py b/artiq/applets/plot_xy_hist.py index 2a97a324c..0bf8987f3 100755 --- a/artiq/applets/plot_xy_hist.py +++ b/artiq/applets/plot_xy_hist.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3.5 import numpy as np -from quamash import QtWidgets +from PyQt5 import QtWidgets import pyqtgraph from artiq.applets.simple import SimpleApplet From d1e54c86b109a4d4743d43863cf34a89271e0a70 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Mon, 8 Feb 2016 22:39:20 +0100 Subject: [PATCH 38/44] gui/applets: log warning if IPC address not in command --- artiq/gui/applets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/artiq/gui/applets.py b/artiq/gui/applets.py index 868842018..a9fc4acf5 100644 --- a/artiq/gui/applets.py +++ b/artiq/gui/applets.py @@ -98,6 +98,9 @@ class AppletDock(dockarea.Dock): async def start(self): self.ipc = AppletIPCServer(self.datasets_sub) + if "{ipc_address}" not in self.command: + logger.warning("IPC address missing from command for %s", + self.applet_name) command = self.command.format(python=sys.executable, ipc_address=self.ipc.get_address()) logger.debug("starting command %s for %s", command, self.applet_name) From f3f667be5bacbd9794575fe526f78396e43aa1fd Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Tue, 9 Feb 2016 10:47:38 +0100 Subject: [PATCH 39/44] conda: fix pyqt package name --- conda/artiq/meta.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda/artiq/meta.yaml b/conda/artiq/meta.yaml index c0ebf8442..8202d2bc8 100644 --- a/conda/artiq/meta.yaml +++ b/conda/artiq/meta.yaml @@ -46,7 +46,7 @@ requirements: - sphinx-argparse - h5py - dateutil - - pyqt 5.* + - pyqt5 - quamash - pyqtgraph - pygit2 From 2ce1e6a1e9d035cd083b871f7345b8d4605ec1a9 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 10 Feb 2016 06:33:41 +0100 Subject: [PATCH 40/44] master/log: do not break lines --- artiq/master/log.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/artiq/master/log.py b/artiq/master/log.py index d542eac08..e3f0bae9f 100644 --- a/artiq/master/log.py +++ b/artiq/master/log.py @@ -24,9 +24,9 @@ class LogBufferHandler(logging.Handler): def emit(self, record): message = self.format(record) - for part in message.split("\n"): - self.log_buffer.log(record.levelno, record.source, record.created, - part) + self.log_buffer.log(record.levelno, record.source, record.created, + message) + def log_args(parser): group = parser.add_argument_group("logging") From 97ba77f4b56f8cabd0ac6cc13dd74baa7dd1d7c6 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 10 Feb 2016 06:34:11 +0100 Subject: [PATCH 41/44] gui/log: top cell alignment --- artiq/gui/log.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/artiq/gui/log.py b/artiq/gui/log.py index b35f2d639..afa981593 100644 --- a/artiq/gui/log.py +++ b/artiq/gui/log.py @@ -81,6 +81,8 @@ class Model(QtCore.QAbstractTableModel): if (role == QtCore.Qt.FontRole and index.column() == 1): return self.fixed_font + elif role == QtCore.Qt.TextAlignmentRole: + return QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop elif role == QtCore.Qt.BackgroundRole: level = self.entries[index.row()][0] if level >= logging.ERROR: From 64263b75d077a6ef6e66f3d1c10e70cd6a987a5d Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 10 Feb 2016 17:20:29 +0100 Subject: [PATCH 42/44] use https for m-labs.hk --- README.rst | 2 +- conda/artiq-kc705-nist_clock/meta.yaml | 2 +- conda/artiq-kc705-nist_qc1/meta.yaml | 2 +- conda/artiq-kc705-nist_qc2/meta.yaml | 2 +- conda/artiq/meta.yaml | 2 +- doc/manual/introduction.rst | 2 +- setup.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 8b620fcac..721f7c9bb 100644 --- a/README.rst +++ b/README.rst @@ -12,6 +12,6 @@ nanosecond timing resolution and sub-microsecond latency. Technologies employed include Python, Migen, MiSoC/mor1kx, LLVM and llvmlite. Website: -http://m-labs.hk/artiq +https://m-labs.hk/artiq Copyright (C) 2014-2016 M-Labs Limited. Licensed under GNU GPL version 3. diff --git a/conda/artiq-kc705-nist_clock/meta.yaml b/conda/artiq-kc705-nist_clock/meta.yaml index 5b3e1a600..3b1c5b5d9 100644 --- a/conda/artiq-kc705-nist_clock/meta.yaml +++ b/conda/artiq-kc705-nist_clock/meta.yaml @@ -22,6 +22,6 @@ requirements: - artiq {{ "{tag} py_{number}+git{hash}".format(tag=environ.get("GIT_DESCRIBE_TAG"), number=environ.get("GIT_DESCRIBE_NUMBER"), hash=environ.get("GIT_DESCRIBE_HASH")[1:]) if "GIT_DESCRIBE_TAG" in environ else "" }} about: - home: http://m-labs.hk/artiq + home: https://m-labs.hk/artiq license: GPL summary: 'Bitstream, BIOS and runtime for NIST_QC2 on the KC705 board' diff --git a/conda/artiq-kc705-nist_qc1/meta.yaml b/conda/artiq-kc705-nist_qc1/meta.yaml index 000674b69..18a3aeda4 100644 --- a/conda/artiq-kc705-nist_qc1/meta.yaml +++ b/conda/artiq-kc705-nist_qc1/meta.yaml @@ -22,6 +22,6 @@ requirements: - artiq {{ "{tag} py_{number}+git{hash}".format(tag=environ.get("GIT_DESCRIBE_TAG"), number=environ.get("GIT_DESCRIBE_NUMBER"), hash=environ.get("GIT_DESCRIBE_HASH")[1:]) if "GIT_DESCRIBE_TAG" in environ else "" }} about: - home: http://m-labs.hk/artiq + home: https://m-labs.hk/artiq license: GPL summary: 'Bitstream, BIOS and runtime for NIST_QC1 on the KC705 board' diff --git a/conda/artiq-kc705-nist_qc2/meta.yaml b/conda/artiq-kc705-nist_qc2/meta.yaml index 833c6c395..977a899c6 100644 --- a/conda/artiq-kc705-nist_qc2/meta.yaml +++ b/conda/artiq-kc705-nist_qc2/meta.yaml @@ -22,6 +22,6 @@ requirements: - artiq {{ "{tag} py_{number}+git{hash}".format(tag=environ.get("GIT_DESCRIBE_TAG"), number=environ.get("GIT_DESCRIBE_NUMBER"), hash=environ.get("GIT_DESCRIBE_HASH")[1:]) if "GIT_DESCRIBE_TAG" in environ else "" }} about: - home: http://m-labs.hk/artiq + home: https://m-labs.hk/artiq license: GPL summary: 'Bitstream, BIOS and runtime for NIST_QC2 on the KC705 board' diff --git a/conda/artiq/meta.yaml b/conda/artiq/meta.yaml index 8202d2bc8..d65c55bb3 100644 --- a/conda/artiq/meta.yaml +++ b/conda/artiq/meta.yaml @@ -60,6 +60,6 @@ test: - artiq about: - home: http://m-labs.hk/artiq + home: https://m-labs.hk/artiq license: GPL summary: 'ARTIQ (Advanced Real-Time Infrastructure for Quantum physics) is a next-generation control system for quantum information experiments. It is being developed in partnership with the Ion Storage Group at NIST, and its applicability reaches beyond ion trapping.' diff --git a/doc/manual/introduction.rst b/doc/manual/introduction.rst index fd7a58f99..edaf16519 100644 --- a/doc/manual/introduction.rst +++ b/doc/manual/introduction.rst @@ -20,4 +20,4 @@ Technologies employed include Python, Migen, MiSoC/mor1kx, LLVM and llvmlite. ARTIQ is licensed under 3-clause BSD. Website: -http://m-labs.hk/artiq +https://m-labs.hk/artiq diff --git a/setup.py b/setup.py index 10bd8982e..6c0fd59f3 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ setup( cmdclass=versioneer.get_cmdclass(), author="M-Labs / NIST Ion Storage Group", author_email="sb@m-labs.hk", - url="http://m-labs.hk/artiq", + url="https://m-labs.hk/artiq", description="A control system for trapped-ion experiments", long_description=open("README.rst").read(), license="GPL", From b68035d162ed0ac84f3593a95f62722a5ffd759a Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 10 Feb 2016 23:37:21 +0100 Subject: [PATCH 43/44] manual/installing: refresh --- doc/manual/installing.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/manual/installing.rst b/doc/manual/installing.rst index 7d90db700..f2aba9b58 100644 --- a/doc/manual/installing.rst +++ b/doc/manual/installing.rst @@ -14,6 +14,11 @@ But you can also :ref:`install from sources `. Installing using conda ---------------------- +.. warning:: + Conda packages are supported for Linux (64-bit) and Windows (32- and 64-bit). Users of other + operating systems (32-bit Linux, BSD, ...) should install from source. + + Installing Anaconda or Miniconda ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -21,10 +26,6 @@ Installing Anaconda or Miniconda * Or install the more minimalistic Miniconda (choose Python 3.5) from http://conda.pydata.org/miniconda.html -.. warning:: - If you are installing on Windows, choose the Windows 32-bit version regardless of whether you have - a 32-bit or 64-bit Windows. - After installing either Anaconda or Miniconda, open a new terminal and make sure the following command works:: $ conda From fcf7a6be2e9ea235db47c40df962386d889698c9 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Wed, 10 Feb 2016 23:42:24 +0100 Subject: [PATCH 44/44] Revert "conda: restrict binutils-or1k-linux dependency to linux." This reverts commit 5bead8b83f8fb09529c87a024d3c8b134f7fb3b6. --- conda/artiq/meta.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda/artiq/meta.yaml b/conda/artiq/meta.yaml index d65c55bb3..e5bdf5439 100644 --- a/conda/artiq/meta.yaml +++ b/conda/artiq/meta.yaml @@ -51,7 +51,7 @@ requirements: - pyqtgraph - pygit2 - aiohttp - - binutils-or1k-linux # [linux] + - binutils-or1k-linux - pythonparser - levenshtein