Skip to content
Permalink

Comparing changes

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

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: m-labs/artiq
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 70a67a0e3877^
Choose a base ref
...
head repository: m-labs/artiq
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 741b11c26d59
Choose a head ref

Commits on Jan 7, 2016

  1. Copy the full SHA
    adbb217 View commit details
  2. Copy the full SHA
    e106ee3 View commit details

Commits on Jan 8, 2016

  1. Copy the full SHA
    597c2e4 View commit details
  2. Copy the full SHA
    1ea73be View commit details

Commits on Jan 9, 2016

  1. Copy the full SHA
    2199ead View commit details

Commits on Jan 10, 2016

  1. Copy the full SHA
    4136ff6 View commit details
  2. Copy the full SHA
    db06e73 View commit details
  3. Copy the full SHA
    26a6e8c View commit details
  4. Copy the full SHA
    e6e6b27 View commit details
  5. Copy the full SHA
    6a972eb View commit details
  6. Copy the full SHA
    e2c7578 View commit details
  7. Copy the full SHA
    38cdeb0 View commit details

Commits on Jan 13, 2016

  1. Copy the full SHA
    341bbde View commit details
  2. Copy the full SHA
    e37e0bd View commit details
  3. Copy the full SHA
    81a86b2 View commit details
  4. Copy the full SHA
    7661b37 View commit details
  5. Copy the full SHA
    8ad151b View commit details
  6. applets/plot_xy_hist: use normal Qt widget as top-level to workaround…

    … pyqtgraph.GraphicsWindow misbehaviour with embedding
    sbourdeauducq committed Jan 13, 2016
    Copy the full SHA
    3d56ea5 View commit details

Commits on Jan 15, 2016

  1. Copy the full SHA
    cc3a45d View commit details
  2. gui/applets: templates

    sbourdeauducq committed Jan 15, 2016
    Copy the full SHA
    9acf8b7 View commit details
  3. Copy the full SHA
    331ac37 View commit details

Commits on Jan 16, 2016

  1. Copy the full SHA
    f9a6ba1 View commit details

Commits on Jan 29, 2016

  1. Copy the full SHA
    1d92c08 View commit details

Commits on Feb 5, 2016

  1. Copy the full SHA
    70a67a0 View commit details

Commits on Feb 8, 2016

  1. Copy the full SHA
    741b11c View commit details
2 changes: 1 addition & 1 deletion artiq/applets/big_number.py
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ def __init__(self, args):
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):
39 changes: 39 additions & 0 deletions artiq/applets/plot_hist.py
Original file line number Diff line number Diff line change
@@ -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", "Bin boundaries", required=False)
applet.run()

if __name__ == "__main__":
main()
59 changes: 59 additions & 0 deletions artiq/applets/plot_xy.py
Original file line number Diff line number Diff line change
@@ -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()
143 changes: 105 additions & 38 deletions artiq/applets/plot_xy_hist.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,136 @@
#!/usr/bin/env python3.5

from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg

import numpy as np
from quamash import QtWidgets
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:
def __init__(self):
self.graphics_window = pg.GraphicsWindow(title="XY/Histogram")
self.graphics_window.resize(1000,600)
self.graphics_window.setWindowTitle("XY/Histogram")

self.xy_plot = self.graphics_window.addPlot()
# 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):
QtWidgets.QSplitter.__init__(self)
self.resize(1000,600)
self.setWindowTitle("XY/Histogram")

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.graphics_window.addPlot()
self.hist_plot = pyqtgraph.PlotWidget()
self.insertWidget(1, self.hist_plot)
self.hist_plot_data = None

def set_data(self, xs, histograms_bins, histograms_counts):
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)
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.selected_index = None

self.histogram_bins = histogram_bins

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, 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 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))
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):
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.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):
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:
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_()

if __name__ == '__main__':
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__":
main()
162 changes: 142 additions & 20 deletions artiq/applets/simple.py
Original file line number Diff line number Diff line change
@@ -1,65 +1,187 @@
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 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:
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)
group = self.argparser.add_argument_group("data server")
group.add_argument(

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

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")
group.add_argument(
parser_sa.add_argument(
"--port", default=3250, type=int,
help="TCP port to connect to")
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)
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):
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)
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([])
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
# 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.
if self.args.mode == "embedded":
win_id = int(self.main_widget.winId())
self.loop.run_until_complete(self.ipc.embed(win_id))
self.main_widget.show()

def sub_init(self, data):
self.data = data
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"]:
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):
self.main_widget.data_changed(self.data, mod)
if not self.filter_mod(mod):
return

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",
self.sub_init, self.sub_mod)
self.loop.run_until_complete(self.subscriber.connect(
self.args.server, self.args.port))
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()
13 changes: 9 additions & 4 deletions artiq/frontend/artiq_gui.py
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@
from artiq.protocols.pc_rpc import AsyncioClient
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():
@@ -110,7 +110,10 @@ def main():
rpc_clients["experiment_db"])

d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"])
smgr.register(d_datasets)

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()
@@ -130,9 +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(d_datasets, "above", d_ttl_dds.ttl_dock)
dock_area.addDock(d_applets, "above", d_ttl_dds.ttl_dock)
dock_area.addDock(d_datasets, "above", d_applets)
else:
dock_area.addDock(d_datasets, "top")
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")
270 changes: 270 additions & 0 deletions artiq/gui/applets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import logging
import asyncio
import sys
import shlex
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, name, command):
dockarea.Dock.__init__(self, "applet" + str(id(self)), # XXX
label="Applet: " + name,
closable=True)
self.setMinimumSize(QtCore.QSize(500, 400))
self.applet_name = name
self.command = command

def rename(self, name):
self.applet_name = name
self.label.setText("Applet: " + name)

async def start(self):
self.ipc = AppletIPCServer(self.capture)
command = self.command.format(python=sys.executable,
ipc_address=self.ipc.get_address())
logger.debug("starting command %s for %s", command, self.applet_name)
try:
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)
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, "process"):
# TODO: send IPC termination request
try:
await asyncio.wait_for(self.ipc.process.wait(), 2.0)
except:
logger.warning("Applet %s failed to exit, killing",
self.applet_name)
try:
self.ipc.process.kill()
except ProcessLookupError:
pass
await self.ipc.process.wait()
del self.ipc

async def restart(self):
await self.terminate()
await self.start()


_templates = [
("Big number", "{python} -m artiq.applets.big_number "
"embedded {ipc_address} NUMBER_DATASET"),
("Histogram", "{python} -m artiq.applets.plot_hist "
"embedded {ipc_address} COUNTS_DATASET "
"--x BIN_BOUNDARIES_DATASET"),
("XY", "{python} -m artiq.applets.plot_xy "
"embedded {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 "
"HIST_BIN_BOUNDARIES_DATASET "
"HISTS_COUNTS_DATASET"),
]


class AppletsDock(dockarea.Dock):
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))

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

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)
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()
dock = self.create(name, command)
item.applet_dock = dock
self.dock_to_checkbox[dock] = item
else:
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()
dock = getattr(self.table.item(row, 0), "applet_dock", None)
if dock is not None:
if column == 1:
dock.rename(new_value)
else:
dock.command = new_value

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):
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)
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:
row = selection[0].topRow()
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()
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()):
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):
self.workaround_pyqtgraph_bug = True
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)
self.workaround_pyqtgraph_bug = False
95 changes: 1 addition & 94 deletions artiq/gui/datasets.py
Original file line number Diff line number Diff line change
@@ -9,12 +9,6 @@

from artiq.tools import short_format
from artiq.gui.models import DictSyncTreeSepModel
from artiq.gui.displays import *

try:
QSortFilterProxyModel = QtCore.QSortFilterProxyModel
except AttributeError:
QSortFilterProxyModel = QtGui.QSortFilterProxyModel


logger = logging.getLogger(__name__)
@@ -35,12 +29,6 @@ def convert(self, k, v, column):
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 +50,6 @@ def __init__(self, dialog_parent, dock_area, datasets_sub):

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"):
@@ -83,74 +58,6 @@ def _search_datasets(self):

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)

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)
217 changes: 0 additions & 217 deletions artiq/gui/displays.py

This file was deleted.

117 changes: 111 additions & 6 deletions artiq/gui/scan.py → artiq/gui/entries.py
Original file line number Diff line number Diff line change
@@ -10,6 +10,100 @@
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 @@ def update(value):
self.value.valueChanged.connect(update)


class _Range(LayoutWidget):
class _RangeScan(LayoutWidget):
def __init__(self, procdesc, state):
LayoutWidget.__init__(self)

@@ -90,7 +184,8 @@ def update_npoints(value):
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 @@ def update(text):
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 @@ def __init__(self, argument):
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 @@ def _scan_type_toggled(self):
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
}
118 changes: 7 additions & 111 deletions artiq/gui/experiments.py
Original file line number Diff line number Diff line change
@@ -7,117 +7,13 @@

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:<experiment name>
# (file name and class name to be retrieved from explist)
@@ -153,7 +49,7 @@ def __init__(self, manager, dock, expurl):
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 @@ async def _recompute_argument(self, name):
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 @@ def get_submission_options(self, expurl):
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,
@@ -512,7 +408,7 @@ def submit(self, expurl):

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 = {
12 changes: 3 additions & 9 deletions artiq/gui/log.py
Original file line number Diff line number Diff line change
@@ -9,11 +9,6 @@

from artiq.gui.tools import log_level_to_name

try:
QSortFilterProxyModel = QtCore.QSortFilterProxyModel
except AttributeError:
QSortFilterProxyModel = QtGui.QSortFilterProxyModel


def _make_wrappable(row, width=30):
level, source, time, msg = row
@@ -34,8 +29,7 @@ def __init__(self, init):
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))
@@ -114,9 +108,9 @@ def data(self, index, role):
time.strftime("%m/%d %H:%M:%S", time.localtime(v[2])))


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

9 changes: 2 additions & 7 deletions artiq/gui/shortcuts.py
Original file line number Diff line number Diff line change
@@ -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 @@ def __init__(self, main_window, exp_manager):
"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))

35 changes: 35 additions & 0 deletions examples/master/repository/histograms.py
Original file line number Diff line number Diff line change
@@ -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)