Merge branch 'master' into subprocess-termination

* master: (44 commits)
  Revert "conda: restrict binutils-or1k-linux dependency to linux."
  manual/installing: refresh
  use https for m-labs.hk
  gui/log: top cell alignment
  master/log: do not break lines
  conda: fix pyqt package name
  gui/applets: log warning if IPC address not in command
  applets: make sure pyqtgraph imports qt5
  applets: avoid argparse subparser mess
  examples/histogram: artiq -> artiq.experiment
  gui/applets: save dock UID in state
  setup.py: give up trying to check for PyQt
  setup.py: fix PyQt5 package name
  Use Qt5
  applets: fix error message text
  applets: handle dataset mutations
  applets: properly name docks to support state save/restore
  applets: clean shutdown
  protocols/pyon: set support
  protocols/pyon: remove FlatFileDB
  ...
This commit is contained in:
Robert Jördens 2016-02-11 09:24:45 +01:00
commit 6434a9cd5f
25 changed files with 898 additions and 550 deletions

View File

@ -12,6 +12,6 @@ nanosecond timing resolution and sub-microsecond latency.
Technologies employed include Python, Migen, MiSoC/mor1kx, LLVM and llvmlite. Technologies employed include Python, Migen, MiSoC/mor1kx, LLVM and llvmlite.
Website: Website:
http://m-labs.hk/artiq https://m-labs.hk/artiq
Copyright (C) 2014-2016 M-Labs Limited. Licensed under GNU GPL version 3. Copyright (C) 2014-2016 M-Labs Limited. Licensed under GNU GPL version 3.

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3.5 #!/usr/bin/env python3.5
from quamash import QtWidgets from PyQt5 import QtWidgets
from artiq.applets.simple import SimpleApplet from artiq.applets.simple import SimpleApplet
@ -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):

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

@ -0,0 +1,40 @@
#!/usr/bin/env python3.5
import numpy as np
import PyQt5 # make sure pyqtgraph imports Qt5
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()

60
artiq/applets/plot_xy.py Executable file
View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3.5
import numpy as np
import PyQt5 # make sure pyqtgraph imports Qt5
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 PyQt5 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,65 +1,208 @@
import logging
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, process_mod
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")
async def read_pyon(self):
line = await self.readline()
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"] == "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):
data = None
while True:
obj = await self.read_pyon()
try:
action = obj["action"]
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 parent message")
except:
logger.error("error processing parent message",
exc_info=True)
self.close_cb()
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())
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)
group = self.argparser.add_argument_group("data server")
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("standalone mode (default)")
group.add_argument( group.add_argument(
"--server", default="::1", "--server", default="::1",
help="hostname or IP to connect to") help="hostname or IP of the master to connect to "
"for dataset notifications "
"(ignored in embedded mode)")
group.add_argument( group.add_argument(
"--port", default=3250, type=int, "--port", default=3250, type=int,
help="TCP port to connect to") help="TCP port to connect to")
self.argparser.add_argument("--embed", default=None,
help="embed into GUI", metavar="IPC_ADDRESS")
self._arggroup_datasets = self.argparser.add_argument_group("datasets") self._arggroup_datasets = self.argparser.add_argument_group("datasets")
def add_dataset(self, name, help=None): self.dataset_args = set()
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: 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([])
self.loop = QEventLoop(app) self.loop = QEventLoop(app)
asyncio.set_event_loop(self.loop) asyncio.set_event_loop(self.loop)
def ipc_init(self):
if self.args.embed is not None:
self.ipc = AppletIPCClient(self.args.embed)
self.loop.run_until_complete(self.ipc.connect())
def ipc_close(self):
if self.args.embed is not None:
self.ipc.close()
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.
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))
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 sub_mod(self, mod): def filter_mod(self, mod):
self.main_widget.data_changed(self.data, mod) if self.args.embed is not None:
# the parent already filters for us
return True
def create_subscriber(self): 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)
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 subscribe(self):
if self.args.embed is None:
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, self.args.port))
else:
self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod)
def unsubscribe(self):
if self.args.embed is None:
self.loop.run_until_complete(self.subscriber.close())
def run(self): def run(self):
self.args_init() self.args_init()
self.quamash_init() self.quamash_init()
try:
self.ipc_init()
try: try:
self.create_main_widget() self.create_main_widget()
self.create_subscriber() self.subscribe()
try: try:
self.loop.run_forever() self.loop.run_forever()
finally: finally:
self.loop.run_until_complete(self.subscriber.close()) self.unsubscribe()
finally:
self.ipc_close()
finally: finally:
self.loop.close() self.loop.close()

View File

@ -5,17 +5,19 @@ import asyncio
import atexit import atexit
import os import os
# Quamash must be imported first so that pyqtgraph picks up the Qt binding import PyQt5
# it has chosen.
from quamash import QEventLoop, QtGui, QtCore 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 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
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 +112,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)
d_applets = applets.AppletsDock(dock_area, sub_clients["datasets"])
atexit_register_coroutine(d_applets.stop)
smgr.register(d_applets)
if os.name != "nt": if os.name != "nt":
d_ttl_dds = moninj.MonInj() d_ttl_dds = moninj.MonInj()
@ -130,9 +135,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(d_applets, "above", d_ttl_dds.ttl_dock)
dock_area.addDock(d_datasets, "above", d_applets)
else: 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_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")

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

@ -0,0 +1,327 @@
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.pipe_ipc import AsyncioParentComm
from artiq.protocols import pyon
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")
async def read_pyon(self):
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()
try:
action = obj["action"]
if action == "embed":
embed_cb(obj["win_id"])
self.write_pyon({"action": "embed_done"})
elif action == "subscribe":
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 message")
except:
logger.warning("error processing applet message",
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)
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))
async def stop(self):
self.server_task.cancel()
await asyncio.wait([self.server_task])
class AppletDock(dockarea.Dock):
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
def rename(self, name):
self.applet_name = name
self.label.setText("Applet: " + name)
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)
try:
await self.ipc.create_subprocess(*shlex.split(command))
except:
logger.warning("Applet %s failed to start", self.applet_name,
exc_info=True)
self.ipc.start(self.embed)
def embed(self, win_id):
logger.debug("capturing window 0x%x for %s", win_id, self.applet_name)
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, "ipc"):
await self.ipc.stop()
self.ipc.write_pyon({"action": "terminate"})
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 "
"--embed {ipc_address} NUMBER_DATASET"),
("Histogram", "{python} -m artiq.applets.plot_hist "
"--embed {ipc_address} COUNTS_DATASET "
"--x BIN_BOUNDARIES_DATASET"),
("XY", "{python} -m artiq.applets.plot_xy "
"--embed {ipc_address} Y_DATASET --x X_DATASET "
"--error ERROR_DATASET --fit FIT_DATASET"),
("XY + Histogram", "{python} -m artiq.applets.plot_xy_hist "
"--embed {ipc_address} X_DATASET "
"HIST_BIN_BOUNDARIES_DATASET "
"HISTS_COUNTS_DATASET"),
]
class AppletsDock(dockarea.Dock):
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
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, 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:
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(item.applet_uid, name, command)
item.applet_dock = dock
self.dock_to_checkbox[dock] = item
else:
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 = self.table.item(row, 0).applet_dock
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, 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()
self.table.insertRow(row)
checkbox = QtWidgets.QTableWidgetItem()
checkbox.setFlags(QtCore.Qt.ItemIsSelectable |
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())
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 = self.table.item(row, 0).applet_dock
if dock is not None:
asyncio.ensure_future(dock.restart())
def delete(self):
selection = self.table.selectedRanges()
if selection:
row = selection[0].topRow()
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 = self.table.item(row, 0).applet_dock
if dock is not None:
await dock.terminate()
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((uid, enabled, name, command))
return state
def restore_state(self, state):
self.workaround_pyqtgraph_bug = True
for uid, enabled, name, command in state:
row = self.new(uid)
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

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))
@ -87,6 +81,8 @@ class Model(QtCore.QAbstractTableModel):
if (role == QtCore.Qt.FontRole if (role == QtCore.Qt.FontRole
and index.column() == 1): and index.column() == 1):
return self.fixed_font return self.fixed_font
elif role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
elif role == QtCore.Qt.BackgroundRole: elif role == QtCore.Qt.BackgroundRole:
level = self.entries[index.row()][0] level = self.entries[index.row()][0]
if level >= logging.ERROR: if level >= logging.ERROR:
@ -114,9 +110,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

@ -24,9 +24,9 @@ class LogBufferHandler(logging.Handler):
def emit(self, record): def emit(self, record):
message = self.format(record) message = self.format(record)
for part in message.split("\n"):
self.log_buffer.log(record.levelno, record.source, record.created, self.log_buffer.log(record.levelno, record.source, record.created,
part) message)
def log_args(parser): def log_args(parser):
group = parser.add_argument_group("logging") group = parser.add_argument_group("logging")

View File

@ -35,6 +35,7 @@ _encode_map = {
bytes: "bytes", bytes: "bytes",
tuple: "tuple", tuple: "tuple",
list: "list", list: "list",
set: "set",
dict: "dict", dict: "dict",
wrapping_int: "number", wrapping_int: "number",
Fraction: "fraction", Fraction: "fraction",
@ -98,6 +99,12 @@ class _Encoder:
r += "]" r += "]"
return 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): def encode_dict(self, x):
r = "{" r = "{"
if not self.pretty or len(x) < 2: if not self.pretty or len(x) < 2:
@ -149,9 +156,7 @@ class _Encoder:
def encode(x, pretty=False): def encode(x, pretty=False):
"""Serializes a Python object and returns the corresponding string in """Serializes a Python object and returns the corresponding string in
Python syntax. Python syntax."""
"""
return _Encoder(pretty).encode(x) return _Encoder(pretty).encode(x)
@ -181,9 +186,7 @@ _eval_dict = {
def decode(s): def decode(s):
"""Parses a string in the Python syntax, reconstructs the corresponding """Parses a string in the Python syntax, reconstructs the corresponding
object, and returns it. object, and returns it."""
"""
return eval(s, _eval_dict, {}) return eval(s, _eval_dict, {})
@ -202,23 +205,3 @@ def load_file(filename):
"""Parses the specified file and returns the decoded Python object.""" """Parses the specified file and returns the decoded Python object."""
with open(filename, "r") as f: with open(filename, "r") as f:
return decode(f.read()) 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()

View File

@ -10,6 +10,7 @@ from artiq.protocols import pyon
_pyon_test_object = { _pyon_test_object = {
(1, 2): [(3, 4.2), (2, )], (1, 2): [(3, 4.2), (2, )],
Fraction(3, 4): np.linspace(5, 10, 1), 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), "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), "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), "x": np.float16(9.0), "y": np.float32(9.0), "z": np.float64(9.0),

View File

@ -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 "" }} - 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: about:
home: http://m-labs.hk/artiq home: https://m-labs.hk/artiq
license: GPL license: GPL
summary: 'Bitstream, BIOS and runtime for NIST_QC2 on the KC705 board' summary: 'Bitstream, BIOS and runtime for NIST_QC2 on the KC705 board'

View File

@ -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 "" }} - 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: about:
home: http://m-labs.hk/artiq home: https://m-labs.hk/artiq
license: GPL license: GPL
summary: 'Bitstream, BIOS and runtime for NIST_QC1 on the KC705 board' summary: 'Bitstream, BIOS and runtime for NIST_QC1 on the KC705 board'

View File

@ -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 "" }} - 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: about:
home: http://m-labs.hk/artiq home: https://m-labs.hk/artiq
license: GPL license: GPL
summary: 'Bitstream, BIOS and runtime for NIST_QC2 on the KC705 board' summary: 'Bitstream, BIOS and runtime for NIST_QC2 on the KC705 board'

View File

@ -46,11 +46,12 @@ requirements:
- sphinx-argparse - sphinx-argparse
- h5py - h5py
- dateutil - dateutil
- pyqt5
- quamash - quamash
- pyqtgraph - pyqtgraph
- pygit2 - pygit2
- aiohttp - aiohttp
- binutils-or1k-linux # [linux] - binutils-or1k-linux
- pythonparser - pythonparser
- levenshtein - levenshtein
@ -59,6 +60,6 @@ test:
- artiq - artiq
about: about:
home: http://m-labs.hk/artiq home: https://m-labs.hk/artiq
license: GPL 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.' 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.'

View File

@ -14,6 +14,11 @@ But you can also :ref:`install from sources <install-from-sources>`.
Installing using conda 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 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 * 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:: After installing either Anaconda or Miniconda, open a new terminal and make sure the following command works::
$ conda $ conda

View File

@ -20,4 +20,4 @@ Technologies employed include Python, Migen, MiSoC/mor1kx, LLVM and llvmlite.
ARTIQ is licensed under 3-clause BSD. ARTIQ is licensed under 3-clause BSD.
Website: Website:
http://m-labs.hk/artiq https://m-labs.hk/artiq

View File

@ -0,0 +1,35 @@
from time import sleep
import numpy as np
from artiq.experiment 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)

View File

@ -10,6 +10,7 @@ if sys.version_info[:3] < (3, 5, 1):
raise Exception("You need Python 3.5.1+") raise Exception("You need Python 3.5.1+")
# Depends on PyQt5, but setuptools cannot check for it.
requirements = [ requirements = [
"sphinx", "sphinx-argparse", "pyserial", "numpy", "scipy", "sphinx", "sphinx-argparse", "pyserial", "numpy", "scipy",
"python-dateutil", "prettytable", "h5py", "python-dateutil", "prettytable", "h5py",
@ -45,7 +46,7 @@ setup(
cmdclass=versioneer.get_cmdclass(), cmdclass=versioneer.get_cmdclass(),
author="M-Labs / NIST Ion Storage Group", author="M-Labs / NIST Ion Storage Group",
author_email="sb@m-labs.hk", 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", description="A control system for trapped-ion experiments",
long_description=open("README.rst").read(), long_description=open("README.rst").read(),
license="GPL", license="GPL",