Merge branch 'applets'

This commit is contained in:
Sebastien Bourdeauducq 2016-02-08 14:55:53 +01:00
commit dc955d46c9
14 changed files with 741 additions and 499 deletions

View File

@ -11,7 +11,7 @@ class NumberWidget(QtWidgets.QLCDNumber):
self.setDigitCount(args.digit_count) self.setDigitCount(args.digit_count)
self.dataset_name = args.dataset self.dataset_name = args.dataset
def data_changed(self, data, mod): def data_changed(self, data, mods):
try: try:
n = float(data[self.dataset_name][1]) n = float(data[self.dataset_name][1])
except (KeyError, ValueError, TypeError): except (KeyError, ValueError, TypeError):

39
artiq/applets/plot_hist.py Executable file
View File

@ -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
artiq/applets/plot_xy.py Executable file
View File

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

View File

@ -1,69 +1,136 @@
#!/usr/bin/env python3.5 #!/usr/bin/env python3.5
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import numpy as np import numpy as np
from quamash import QtWidgets
import pyqtgraph
class XYHistPlot: from artiq.applets.simple import SimpleApplet
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()
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
# 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.xy_plot_data = None
self.arrow = 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 self.hist_plot_data = None
def set_data(self, xs, histograms_bins, histograms_counts): self.args = args
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)
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, self.xy_plot_data = self.xy_plot.plot(x=xs, y=ys,
pen=None, pen=None,
symbol="x", symbolSize=20) symbol="x", symbolSize=20)
self.xy_plot_data.sigPointsClicked.connect(self.point_clicked) self.xy_plot_data.sigPointsClicked.connect(self._point_clicked)
for point, bins, counts in zip(self.xy_plot_data.scatter.points(), for index, (point, counts) in (
histograms_bins, histograms_counts): enumerate(zip(self.xy_plot_data.scatter.points(),
point.histogram_bins = bins histograms_counts))):
point.histogram_index = index
point.histogram_counts = counts point.histogram_counts = counts
self.hist_plot_data = self.hist_plot.plot( self.hist_plot_data = self.hist_plot.plot(
stepMode=True, fillLevel=0, stepMode=True, fillLevel=0,
brush=(0, 0, 255, 150)) brush=(0, 0, 255, 150))
def point_clicked(self, data_item, spot_items): 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] spot_item = spot_items[0]
position = spot_item.pos() position = spot_item.pos()
if self.arrow is None: if self.arrow is None:
self.arrow = pg.ArrowItem(angle=-120, tipAngle=30, baseAngle=20, self.arrow = pyqtgraph.ArrowItem(
headLen=40, tailLen=40, tailWidth=8, angle=-120, tipAngle=30, baseAngle=20, headLen=40,
pen=None, brush="y") tailLen=40, tailWidth=8, pen=None, brush="y")
self.arrow.setPos(position) self.arrow.setPos(position)
# NB: temporary glitch if addItem is done before setPos # NB: temporary glitch if addItem is done before setPos
self.xy_plot.addItem(self.arrow) self.xy_plot.addItem(self.arrow)
else: else:
self.arrow.setPos(position) 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) 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(): def main():
app = QtGui.QApplication([]) applet = SimpleApplet(XYHistPlot)
plot = XYHistPlot() applet.add_dataset("xs", "1D array of point abscissas")
plot.set_data(np.array([1, 2, 3, 4, 1]), applet.add_dataset("histogram_bins",
np.array([[1, 2, 3], [1, 2, 3], [1, 2, 3], [40, 70, 100], [4, 7, 10, 20]]), "1D array of histogram bin boundaries")
np.array([[1, 1], [2, 3], [10, 20], [3, 1], [100, 67, 102]])) applet.add_dataset("histograms_counts",
app.exec_() "2D array of histogram counts, for each point")
applet.run()
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -1,33 +1,55 @@
import argparse import argparse
import asyncio 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.sync_struct import Subscriber
from artiq.protocols.pc_rpc import Client
class SimpleApplet: 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.main_widget_class = main_widget_class
self.argparser = argparse.ArgumentParser(description=cmd_description) 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 = self.argparser.add_argument_group("data server")
group.add_argument( group.add_argument(
"--server", default="::1", "--server-notify", default="::1",
help="hostname or IP to connect to") help="hostname or IP to connect to for dataset notifications")
group.add_argument( group.add_argument(
"--port", default=3250, type=int, "--port-notify", default=3250, type=int,
help="TCP port to connect to") 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") self._arggroup_datasets = self.argparser.add_argument_group("datasets")
self.dataset_args = set()
def add_dataset(self, name, help=None): def add_dataset(self, name, help=None, required=True):
if help is None: kwargs = dict()
self._arggroup_datasets.add_argument(name) if help is not None:
kwargs["help"] = help
if required:
self._arggroup_datasets.add_argument(name, **kwargs)
else: 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): def args_init(self):
self.args = self.argparser.parse_args() self.args = self.argparser.parse_args()
self.datasets = {getattr(self.args, arg.replace("-", "_"))
for arg in self.dataset_args}
def quamash_init(self): def quamash_init(self):
app = QtWidgets.QApplication([]) app = QtWidgets.QApplication([])
@ -36,20 +58,61 @@ class SimpleApplet:
def create_main_widget(self): def create_main_widget(self):
self.main_widget = self.main_widget_class(self.args) 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() self.main_widget.show()
def sub_init(self, data): def sub_init(self, data):
self.data = data self.data = data
return 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): 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): def create_subscriber(self):
self.subscriber = Subscriber("datasets", self.subscriber = Subscriber("datasets",
self.sub_init, self.sub_mod) self.sub_init, self.sub_mod)
self.loop.run_until_complete(self.subscriber.connect( 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): def run(self):
self.args_init() self.args_init()

View File

@ -12,10 +12,10 @@ from pyqtgraph import dockarea
from artiq import __artiq_dir__ as artiq_dir from artiq import __artiq_dir__ as artiq_dir
from artiq.tools import * 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.models import ModelSubscriber
from artiq.gui import (state, experiments, shortcuts, explorer, from artiq.gui import (state, experiments, shortcuts, explorer,
moninj, datasets, schedule, log, console) moninj, datasets, applets, schedule, log, console)
def get_argparser(): def get_argparser():
@ -110,7 +110,10 @@ def main():
rpc_clients["experiment_db"]) rpc_clients["experiment_db"])
d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"]) d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"])
smgr.register(d_datasets)
appletmgr = applets.AppletManager(dock_area)
atexit_register_coroutine(appletmgr.stop)
smgr.register(appletmgr)
if os.name != "nt": if os.name != "nt":
d_ttl_dds = moninj.MonInj() d_ttl_dds = moninj.MonInj()
@ -130,9 +133,11 @@ def main():
if os.name != "nt": if os.name != "nt":
dock_area.addDock(d_ttl_dds.dds_dock, "top") 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_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: 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_shortcuts, "above", d_datasets)
dock_area.addDock(d_explorer, "above", d_shortcuts) dock_area.addDock(d_explorer, "above", d_shortcuts)
dock_area.addDock(d_console, "bottom") dock_area.addDock(d_console, "bottom")
@ -148,6 +153,11 @@ def main():
if d_log0 is not None: if d_log0 is not None:
dock_area.addDock(d_log0, "right", d_explorer) 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 # run
win.show() win.show()
loop.run_until_complete(win.exit_request.wait()) loop.run_until_complete(win.exit_request.wait())

289
artiq/gui/applets.py Normal file
View File

@ -0,0 +1,289 @@
import logging
import asyncio
import sys
import shlex
from functools import partial
from quamash import QtCore, QtGui, QtWidgets
from pyqtgraph import dockarea
logger = logging.getLogger(__name__)
class AppletDock(dockarea.Dock):
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
def rename(self, name):
self.applet_name = name
self.label.setText("Applet: " + name)
async def start(self):
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(
*shlex.split(command))
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)
self.captured_window = QtGui.QWindow.fromWinId(win_id)
self.captured_widget = QtWidgets.QWidget.createWindowContainer(
self.captured_window)
self.addWidget(self.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"):
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()
del self.process
async def restart(self):
await self.terminate()
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
self.token_to_checkbox = dict()
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 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
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)
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()
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()
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:
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.removeRow(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):
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

View File

@ -9,12 +9,6 @@ from pyqtgraph import LayoutWidget
from artiq.tools import short_format from artiq.tools import short_format
from artiq.gui.models import DictSyncTreeSepModel from artiq.gui.models import DictSyncTreeSepModel
from artiq.gui.displays import *
try:
QSortFilterProxyModel = QtCore.QSortFilterProxyModel
except AttributeError:
QSortFilterProxyModel = QtGui.QSortFilterProxyModel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -35,12 +29,6 @@ class Model(DictSyncTreeSepModel):
raise ValueError 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): class DatasetsDock(dockarea.Dock):
def __init__(self, dialog_parent, dock_area, datasets_sub): def __init__(self, dialog_parent, dock_area, datasets_sub):
dockarea.Dock.__init__(self, "Datasets") dockarea.Dock.__init__(self, "Datasets")
@ -62,19 +50,6 @@ class DatasetsDock(dockarea.Dock):
self.table_model = Model(dict()) self.table_model = Model(dict())
datasets_sub.add_setmodel_callback(self.set_model) 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): def _search_datasets(self):
if hasattr(self, "table_model_filter"): if hasattr(self, "table_model_filter"):
@ -83,74 +58,6 @@ class DatasetsDock(dockarea.Dock):
def set_model(self, model): def set_model(self, model):
self.table_model = 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_model_filter.setSourceModel(self.table_model)
self.table.setModel(self.table_model_filter) 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)

View File

@ -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("<None>")
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] != "<None>":
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"] != "<None>":
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 == "<None>":
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))
])

View File

@ -10,6 +10,100 @@ from artiq.gui.tools import disable_scroll_wheel
logger = logging.getLogger(__name__) 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): class _NoScan(LayoutWidget):
def __init__(self, procdesc, state): def __init__(self, procdesc, state):
LayoutWidget.__init__(self) LayoutWidget.__init__(self)
@ -38,7 +132,7 @@ class _NoScan(LayoutWidget):
self.value.valueChanged.connect(update) self.value.valueChanged.connect(update)
class _Range(LayoutWidget): class _RangeScan(LayoutWidget):
def __init__(self, procdesc, state): def __init__(self, procdesc, state):
LayoutWidget.__init__(self) LayoutWidget.__init__(self)
@ -90,7 +184,8 @@ class _Range(LayoutWidget):
self.max.valueChanged.connect(update_max) self.max.valueChanged.connect(update_max)
self.npoints.valueChanged.connect(update_npoints) self.npoints.valueChanged.connect(update_npoints)
class _Explicit(LayoutWidget):
class _ExplicitScan(LayoutWidget):
def __init__(self, state): def __init__(self, state):
LayoutWidget.__init__(self) LayoutWidget.__init__(self)
@ -109,7 +204,7 @@ class _Explicit(LayoutWidget):
self.value.textEdited.connect(update) self.value.textEdited.connect(update)
class ScanController(LayoutWidget): class _ScanEntry(LayoutWidget):
def __init__(self, argument): def __init__(self, argument):
LayoutWidget.__init__(self) LayoutWidget.__init__(self)
self.argument = argument self.argument = argument
@ -121,9 +216,9 @@ class ScanController(LayoutWidget):
state = argument["state"] state = argument["state"]
self.widgets = OrderedDict() self.widgets = OrderedDict()
self.widgets["NoScan"] = _NoScan(procdesc, state["NoScan"]) self.widgets["NoScan"] = _NoScan(procdesc, state["NoScan"])
self.widgets["LinearScan"] = _Range(procdesc, state["LinearScan"]) self.widgets["LinearScan"] = _RangeScan(procdesc, state["LinearScan"])
self.widgets["RandomScan"] = _Range(procdesc, state["RandomScan"]) self.widgets["RandomScan"] = _RangeScan(procdesc, state["RandomScan"])
self.widgets["ExplicitScan"] = _Explicit(state["ExplicitScan"]) self.widgets["ExplicitScan"] = _ExplicitScan(state["ExplicitScan"])
for widget in self.widgets.values(): for widget in self.widgets.values():
self.stack.addWidget(widget) self.stack.addWidget(widget)
@ -181,3 +276,13 @@ class ScanController(LayoutWidget):
self.stack.setCurrentWidget(self.widgets[ty]) self.stack.setCurrentWidget(self.widgets[ty])
self.argument["state"]["selected"] = ty self.argument["state"]["selected"] = ty
break break
argty_to_entry = {
"PYONValue": _StringEntry,
"BooleanValue": _BooleanEntry,
"EnumerationValue": _EnumerationEntry,
"NumberValue": _NumberEntry,
"StringValue": _StringEntry,
"Scannable": _ScanEntry
}

View File

@ -7,117 +7,13 @@ from quamash import QtGui, QtCore
from pyqtgraph import dockarea, LayoutWidget from pyqtgraph import dockarea, LayoutWidget
from artiq.gui.tools import log_level_to_name, disable_scroll_wheel from artiq.gui.tools import log_level_to_name
from artiq.gui.scan import ScanController from artiq.gui.entries import argty_to_entry
logger = logging.getLogger(__name__) 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: # Experiment URLs come in two forms:
# 1. repo:<experiment name> # 1. repo:<experiment name>
# (file name and class name to be retrieved from explist) # (file name and class name to be retrieved from explist)
@ -153,7 +49,7 @@ class _ArgumentEditor(QtGui.QTreeWidget):
self.addTopLevelItem(QtGui.QTreeWidgetItem(["No arguments"])) self.addTopLevelItem(QtGui.QTreeWidgetItem(["No arguments"]))
for name, argument in arguments.items(): 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]) widget_item = QtGui.QTreeWidgetItem([name])
self._arg_to_entry_widgetitem[name] = entry, widget_item 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] argument = self.manager.get_submission_arguments(self.expurl)[name]
procdesc = arginfo[name][0] 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["desc"] = procdesc
argument["state"] = state argument["state"] = state
old_entry, widget_item = self._arg_to_entry_widgetitem[name] old_entry, widget_item = self._arg_to_entry_widgetitem[name]
old_entry.deleteLater() 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._arg_to_entry_widgetitem[name] = entry, widget_item
self.setItemWidget(widget_item, 1, entry) self.setItemWidget(widget_item, 1, entry)
@ -466,7 +362,7 @@ class ExperimentManager:
def initialize_submission_arguments(self, expurl, arginfo): def initialize_submission_arguments(self, expurl, arginfo):
arguments = OrderedDict() arguments = OrderedDict()
for name, (procdesc, group) in arginfo.items(): 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] = { arguments[name] = {
"desc": procdesc, "desc": procdesc,
"group": group, "group": group,
@ -512,7 +408,7 @@ class ExperimentManager:
argument_values = dict() argument_values = dict()
for name, argument in arguments.items(): 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"]) argument_values[name] = entry_cls.state_to_value(argument["state"])
expid = { expid = {

View File

@ -9,11 +9,6 @@ from pyqtgraph import dockarea, LayoutWidget
from artiq.gui.tools import log_level_to_name from artiq.gui.tools import log_level_to_name
try:
QSortFilterProxyModel = QtCore.QSortFilterProxyModel
except AttributeError:
QSortFilterProxyModel = QtGui.QSortFilterProxyModel
def _make_wrappable(row, width=30): def _make_wrappable(row, width=30):
level, source, time, msg = row level, source, time, msg = row
@ -34,8 +29,7 @@ class Model(QtCore.QAbstractTableModel):
timer.timeout.connect(self.timer_tick) timer.timeout.connect(self.timer_tick)
timer.start(100) timer.start(100)
self.fixed_font = QtGui.QFont() self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
self.fixed_font.setFamily("Monospace")
self.white = QtGui.QBrush(QtGui.QColor(255, 255, 255)) self.white = QtGui.QBrush(QtGui.QColor(255, 255, 255))
self.black = QtGui.QBrush(QtGui.QColor(0, 0, 0)) self.black = QtGui.QBrush(QtGui.QColor(0, 0, 0))
@ -114,9 +108,9 @@ class Model(QtCore.QAbstractTableModel):
time.strftime("%m/%d %H:%M:%S", time.localtime(v[2]))) time.strftime("%m/%d %H:%M:%S", time.localtime(v[2])))
class _LogFilterProxyModel(QSortFilterProxyModel): class _LogFilterProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, min_level, freetext): def __init__(self, min_level, freetext):
QSortFilterProxyModel.__init__(self) QtCore.QSortFilterProxyModel.__init__(self)
self.min_level = min_level self.min_level = min_level
self.freetext = freetext self.freetext = freetext

View File

@ -1,13 +1,8 @@
import logging import logging
from functools import partial from functools import partial
from quamash import QtGui, QtCore from quamash import QtGui, QtCore, QtWidgets
from pyqtgraph import dockarea from pyqtgraph import dockarea
try:
from quamash import QtWidgets
QShortcut = QtWidgets.QShortcut
except:
QShortcut = QtGui.QShortcut
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -66,7 +61,7 @@ class ShortcutsDock(dockarea.Dock):
"open": open, "open": open,
"submit": submit "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.setContext(QtCore.Qt.ApplicationShortcut)
shortcut.activated.connect(partial(self._activated, i)) shortcut.activated.connect(partial(self._activated, i))

View File

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