Skip to content

Commit 6434a9c

Browse files
committedFeb 11, 2016
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 ...
2 parents 912274c + fcf7a6b commit 6434a9c

File tree

25 files changed

+898
-550
lines changed

25 files changed

+898
-550
lines changed
 

‎README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ nanosecond timing resolution and sub-microsecond latency.
1212
Technologies employed include Python, Migen, MiSoC/mor1kx, LLVM and llvmlite.
1313

1414
Website:
15-
http://m-labs.hk/artiq
15+
https://m-labs.hk/artiq
1616

1717
Copyright (C) 2014-2016 M-Labs Limited. Licensed under GNU GPL version 3.

‎artiq/applets/big_number.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python3.5
22

3-
from quamash import QtWidgets
3+
from PyQt5 import QtWidgets
44

55
from artiq.applets.simple import SimpleApplet
66

@@ -11,7 +11,7 @@ def __init__(self, args):
1111
self.setDigitCount(args.digit_count)
1212
self.dataset_name = args.dataset
1313

14-
def data_changed(self, data, mod):
14+
def data_changed(self, data, mods):
1515
try:
1616
n = float(data[self.dataset_name][1])
1717
except (KeyError, ValueError, TypeError):

‎artiq/applets/plot_hist.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python3.5
2+
3+
import numpy as np
4+
import PyQt5 # make sure pyqtgraph imports Qt5
5+
import pyqtgraph
6+
7+
from artiq.applets.simple import SimpleApplet
8+
9+
10+
class HistogramPlot(pyqtgraph.PlotWidget):
11+
def __init__(self, args):
12+
pyqtgraph.PlotWidget.__init__(self)
13+
self.args = args
14+
15+
def data_changed(self, data, mods):
16+
try:
17+
y = data[self.args.y][1]
18+
if self.args.x is None:
19+
x = None
20+
else:
21+
x = data[self.args.x][1]
22+
except KeyError:
23+
return
24+
if x is None:
25+
x = list(range(len(y)+1))
26+
27+
if len(y) and len(x) == len(y) + 1:
28+
self.clear()
29+
self.plot(x, y, stepMode=True, fillLevel=0,
30+
brush=(0, 0, 255, 150))
31+
32+
33+
def main():
34+
applet = SimpleApplet(HistogramPlot)
35+
applet.add_dataset("y", "Y values")
36+
applet.add_dataset("x", "Bin boundaries", required=False)
37+
applet.run()
38+
39+
if __name__ == "__main__":
40+
main()

‎artiq/applets/plot_xy.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env python3.5
2+
3+
import numpy as np
4+
import PyQt5 # make sure pyqtgraph imports Qt5
5+
import pyqtgraph
6+
7+
from artiq.applets.simple import SimpleApplet
8+
9+
10+
class XYPlot(pyqtgraph.PlotWidget):
11+
def __init__(self, args):
12+
pyqtgraph.PlotWidget.__init__(self)
13+
self.args = args
14+
15+
def data_changed(self, data, mods):
16+
try:
17+
y = data[self.args.y][1]
18+
except KeyError:
19+
return
20+
x = data.get(self.args.x, (False, None))[1]
21+
if x is None:
22+
x = list(range(len(y)))
23+
error = data.get(self.args.error, (False, None))[1]
24+
fit = data.get(self.args.fit, (False, None))[1]
25+
26+
if not len(y) or len(y) != len(x):
27+
return
28+
if error is not None and hasattr(error, "__len__"):
29+
if not len(error):
30+
error = None
31+
elif len(error) != len(y):
32+
return
33+
if fit is not None:
34+
if not len(fit):
35+
fit = None
36+
elif len(fit) != len(y):
37+
return
38+
39+
self.clear()
40+
self.plot(x, y, pen=None, symbol="x")
41+
if error is not None:
42+
# See https://github.com/pyqtgraph/pyqtgraph/issues/211
43+
if hasattr(error, "__len__") and not isinstance(error, np.ndarray):
44+
error = np.array(error)
45+
errbars = pg.ErrorBarItem(x=np.array(x), y=np.array(y), height=error)
46+
self.addItem(errbars)
47+
if fit is not None:
48+
self.plot(x, fit)
49+
50+
51+
def main():
52+
applet = SimpleApplet(XYPlot)
53+
applet.add_dataset("y", "Y values")
54+
applet.add_dataset("x", "X values", required=False)
55+
applet.add_dataset("error", "Error bars for each X value", required=False)
56+
applet.add_dataset("fit", "Fit values for each X value", required=False)
57+
applet.run()
58+
59+
if __name__ == "__main__":
60+
main()

‎artiq/applets/plot_xy_hist.py

+105-38
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,136 @@
11
#!/usr/bin/env python3.5
22

3-
from pyqtgraph.Qt import QtGui, QtCore
4-
import pyqtgraph as pg
5-
63
import numpy as np
4+
from PyQt5 import QtWidgets
5+
import pyqtgraph
6+
7+
from artiq.applets.simple import SimpleApplet
8+
9+
10+
def _compute_ys(histogram_bins, histograms_counts):
11+
bin_centers = np.empty(len(histogram_bins)-1)
12+
for i in range(len(bin_centers)):
13+
bin_centers[i] = (histogram_bins[i] + histogram_bins[i+1])/2
14+
15+
ys = np.empty(histograms_counts.shape[0])
16+
for n, counts in enumerate(histograms_counts):
17+
ys[n] = sum(bin_centers*counts)/sum(counts)
18+
return ys
719

8-
class XYHistPlot:
9-
def __init__(self):
10-
self.graphics_window = pg.GraphicsWindow(title="XY/Histogram")
11-
self.graphics_window.resize(1000,600)
12-
self.graphics_window.setWindowTitle("XY/Histogram")
1320

14-
self.xy_plot = self.graphics_window.addPlot()
21+
# pyqtgraph.GraphicsWindow fails to behave like a regular Qt widget
22+
# and breaks embedding. Do not use as top widget.
23+
class XYHistPlot(QtWidgets.QSplitter):
24+
def __init__(self, args):
25+
QtWidgets.QSplitter.__init__(self)
26+
self.resize(1000,600)
27+
self.setWindowTitle("XY/Histogram")
28+
29+
self.xy_plot = pyqtgraph.PlotWidget()
30+
self.insertWidget(0, self.xy_plot)
1531
self.xy_plot_data = None
1632
self.arrow = None
33+
self.selected_index = None
1734

18-
self.hist_plot = self.graphics_window.addPlot()
35+
self.hist_plot = pyqtgraph.PlotWidget()
36+
self.insertWidget(1, self.hist_plot)
1937
self.hist_plot_data = None
2038

21-
def set_data(self, xs, histograms_bins, histograms_counts):
22-
ys = np.empty_like(xs)
23-
ys.fill(np.nan)
24-
for n, (bins, counts) in enumerate(zip(histograms_bins,
25-
histograms_counts)):
26-
bin_centers = np.empty(len(bins)-1)
27-
for i in range(len(bin_centers)):
28-
bin_centers[i] = (bins[i] + bins[i+1])/2
29-
ys[n] = sum(bin_centers*counts)/sum(bin_centers)
39+
self.args = args
3040

41+
def _set_full_data(self, xs, histogram_bins, histograms_counts):
42+
self.xy_plot.clear()
43+
self.hist_plot.clear()
44+
self.xy_plot_data = None
45+
self.hist_plot_data = None
46+
self.arrow = None
47+
self.selected_index = None
48+
49+
self.histogram_bins = histogram_bins
50+
51+
ys = _compute_ys(self.histogram_bins, histograms_counts)
3152
self.xy_plot_data = self.xy_plot.plot(x=xs, y=ys,
3253
pen=None,
3354
symbol="x", symbolSize=20)
34-
self.xy_plot_data.sigPointsClicked.connect(self.point_clicked)
35-
for point, bins, counts in zip(self.xy_plot_data.scatter.points(),
36-
histograms_bins, histograms_counts):
37-
point.histogram_bins = bins
55+
self.xy_plot_data.sigPointsClicked.connect(self._point_clicked)
56+
for index, (point, counts) in (
57+
enumerate(zip(self.xy_plot_data.scatter.points(),
58+
histograms_counts))):
59+
point.histogram_index = index
3860
point.histogram_counts = counts
3961

4062
self.hist_plot_data = self.hist_plot.plot(
41-
stepMode=True, fillLevel=0,
42-
brush=(0, 0, 255, 150))
63+
stepMode=True, fillLevel=0,
64+
brush=(0, 0, 255, 150))
65+
66+
def _set_partial_data(self, xs, histograms_counts):
67+
ys = _compute_ys(self.histogram_bins, histograms_counts)
68+
self.xy_plot_data.setData(x=xs, y=ys,
69+
pen=None,
70+
symbol="x", symbolSize=20)
71+
for index, (point, counts) in (
72+
enumerate(zip(self.xy_plot_data.scatter.points(),
73+
histograms_counts))):
74+
point.histogram_index = index
75+
point.histogram_counts = counts
4376

44-
def point_clicked(self, data_item, spot_items):
77+
def _point_clicked(self, data_item, spot_items):
4578
spot_item = spot_items[0]
4679
position = spot_item.pos()
4780
if self.arrow is None:
48-
self.arrow = pg.ArrowItem(angle=-120, tipAngle=30, baseAngle=20,
49-
headLen=40, tailLen=40, tailWidth=8,
50-
pen=None, brush="y")
81+
self.arrow = pyqtgraph.ArrowItem(
82+
angle=-120, tipAngle=30, baseAngle=20, headLen=40,
83+
tailLen=40, tailWidth=8, pen=None, brush="y")
5184
self.arrow.setPos(position)
5285
# NB: temporary glitch if addItem is done before setPos
5386
self.xy_plot.addItem(self.arrow)
5487
else:
5588
self.arrow.setPos(position)
56-
self.hist_plot_data.setData(x=spot_item.histogram_bins,
89+
self.selected_index = spot_item.histogram_index
90+
self.hist_plot_data.setData(x=self.histogram_bins,
5791
y=spot_item.histogram_counts)
92+
93+
def _can_use_partial(self, mods):
94+
if self.hist_plot_data is None:
95+
return False
96+
for mod in mods:
97+
if mod["action"] != "setitem":
98+
return False
99+
if mod["path"] == [self.args.xs, 1]:
100+
if mod["key"] == self.selected_index:
101+
return False
102+
elif mod["path"][:2] == [self.args.histograms_counts, 1]:
103+
if len(mod["path"]) > 2:
104+
index = mod["path"][2]
105+
else:
106+
index = mod["key"]
107+
if index == self.selected_index:
108+
return False
109+
else:
110+
return False
111+
return True
112+
113+
def data_changed(self, data, mods):
114+
try:
115+
xs = data[self.args.xs][1]
116+
histogram_bins = data[self.args.histogram_bins][1]
117+
histograms_counts = data[self.args.histograms_counts][1]
118+
except KeyError:
119+
return
120+
if self._can_use_partial(mods):
121+
self._set_partial_data(xs, histograms_counts)
122+
else:
123+
self._set_full_data(xs, histogram_bins, histograms_counts)
58124

59125

60126
def main():
61-
app = QtGui.QApplication([])
62-
plot = XYHistPlot()
63-
plot.set_data(np.array([1, 2, 3, 4, 1]),
64-
np.array([[1, 2, 3], [1, 2, 3], [1, 2, 3], [40, 70, 100], [4, 7, 10, 20]]),
65-
np.array([[1, 1], [2, 3], [10, 20], [3, 1], [100, 67, 102]]))
66-
app.exec_()
67-
68-
if __name__ == '__main__':
127+
applet = SimpleApplet(XYHistPlot)
128+
applet.add_dataset("xs", "1D array of point abscissas")
129+
applet.add_dataset("histogram_bins",
130+
"1D array of histogram bin boundaries")
131+
applet.add_dataset("histograms_counts",
132+
"2D array of histogram counts, for each point")
133+
applet.run()
134+
135+
if __name__ == "__main__":
69136
main()

‎artiq/applets/simple.py

+162-19
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,208 @@
1+
import logging
12
import argparse
23
import asyncio
34

4-
from quamash import QEventLoop, QtWidgets, QtCore
5+
from quamash import QEventLoop, QtWidgets, QtGui, QtCore
56

6-
from artiq.protocols.sync_struct import Subscriber
7+
from artiq.protocols.sync_struct import Subscriber, process_mod
8+
from artiq.protocols import pyon
9+
from artiq.protocols.pipe_ipc import AsyncioChildComm
10+
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class AppletIPCClient(AsyncioChildComm):
16+
def set_close_cb(self, close_cb):
17+
self.close_cb = close_cb
18+
19+
def write_pyon(self, obj):
20+
self.write(pyon.encode(obj).encode() + b"\n")
21+
22+
async def read_pyon(self):
23+
line = await self.readline()
24+
return pyon.decode(line.decode())
25+
26+
async def embed(self, win_id):
27+
# This function is only called when not subscribed to anything,
28+
# so the only normal replies are embed_done and terminate.
29+
self.write_pyon({"action": "embed",
30+
"win_id": win_id})
31+
reply = await self.read_pyon()
32+
if reply["action"] == "terminate":
33+
self.close_cb()
34+
elif reply["action"] != "embed_done":
35+
logger.error("unexpected action reply to embed request: %s",
36+
action)
37+
self.close_cb()
38+
39+
async def listen(self):
40+
data = None
41+
while True:
42+
obj = await self.read_pyon()
43+
try:
44+
action = obj["action"]
45+
if action == "terminate":
46+
self.close_cb()
47+
return
48+
elif action == "mod":
49+
mod = obj["mod"]
50+
if mod["action"] == "init":
51+
data = self.init_cb(mod["struct"])
52+
else:
53+
process_mod(data, mod)
54+
self.mod_cb(mod)
55+
else:
56+
raise ValueError("unknown action in parent message")
57+
except:
58+
logger.error("error processing parent message",
59+
exc_info=True)
60+
self.close_cb()
61+
62+
def subscribe(self, datasets, init_cb, mod_cb):
63+
self.write_pyon({"action": "subscribe",
64+
"datasets": datasets})
65+
self.init_cb = init_cb
66+
self.mod_cb = mod_cb
67+
asyncio.ensure_future(self.listen())
768

869

970
class SimpleApplet:
10-
def __init__(self, main_widget_class, cmd_description=None):
71+
def __init__(self, main_widget_class, cmd_description=None,
72+
default_update_delay=0.0):
1173
self.main_widget_class = main_widget_class
1274

1375
self.argparser = argparse.ArgumentParser(description=cmd_description)
14-
group = self.argparser.add_argument_group("data server")
76+
77+
self.argparser.add_argument("--update-delay", type=float,
78+
default=default_update_delay,
79+
help="time to wait after a mod (buffering other mods) "
80+
"before updating (default: %(default).2f)")
81+
82+
group = self.argparser.add_argument_group("standalone mode (default)")
1583
group.add_argument(
1684
"--server", default="::1",
17-
help="hostname or IP to connect to")
85+
help="hostname or IP of the master to connect to "
86+
"for dataset notifications "
87+
"(ignored in embedded mode)")
1888
group.add_argument(
1989
"--port", default=3250, type=int,
2090
help="TCP port to connect to")
91+
92+
self.argparser.add_argument("--embed", default=None,
93+
help="embed into GUI", metavar="IPC_ADDRESS")
94+
2195
self._arggroup_datasets = self.argparser.add_argument_group("datasets")
2296

23-
def add_dataset(self, name, help=None):
24-
if help is None:
25-
self._arggroup_datasets.add_argument(name)
97+
self.dataset_args = set()
98+
99+
def add_dataset(self, name, help=None, required=True):
100+
kwargs = dict()
101+
if help is not None:
102+
kwargs["help"] = help
103+
if required:
104+
self._arggroup_datasets.add_argument(name, **kwargs)
26105
else:
27-
self._arggroup_datasets.add_argument(name, help=help)
106+
self._arggroup_datasets.add_argument("--" + name, **kwargs)
107+
self.dataset_args.add(name)
28108

29109
def args_init(self):
30110
self.args = self.argparser.parse_args()
111+
self.datasets = {getattr(self.args, arg.replace("-", "_"))
112+
for arg in self.dataset_args}
31113

32114
def quamash_init(self):
33115
app = QtWidgets.QApplication([])
34116
self.loop = QEventLoop(app)
35117
asyncio.set_event_loop(self.loop)
36118

119+
def ipc_init(self):
120+
if self.args.embed is not None:
121+
self.ipc = AppletIPCClient(self.args.embed)
122+
self.loop.run_until_complete(self.ipc.connect())
123+
124+
def ipc_close(self):
125+
if self.args.embed is not None:
126+
self.ipc.close()
127+
37128
def create_main_widget(self):
38129
self.main_widget = self.main_widget_class(self.args)
130+
# Qt window embedding is ridiculously buggy, and empirical testing
131+
# has shown that the following procedure must be followed exactly:
132+
# 1. applet creates widget
133+
# 2. applet creates native window without showing it, and get its ID
134+
# 3. applet sends the ID to host, host embeds the widget
135+
# 4. applet shows the widget
136+
# Doing embedding the other way around (using QWindow.setParent in the
137+
# applet) breaks resizing.
138+
if self.args.embed is not None:
139+
self.ipc.set_close_cb(self.main_widget.close)
140+
win_id = int(self.main_widget.winId())
141+
self.loop.run_until_complete(self.ipc.embed(win_id))
39142
self.main_widget.show()
40143

41144
def sub_init(self, data):
42145
self.data = data
43146
return data
44147

148+
def filter_mod(self, mod):
149+
if self.args.embed is not None:
150+
# the parent already filters for us
151+
return True
152+
153+
if mod["action"] == "init":
154+
return True
155+
if mod["path"]:
156+
return mod["path"][0] in self.datasets
157+
elif mod["action"] in {"setitem", "delitem"}:
158+
return mod["key"] in self.datasets
159+
else:
160+
return False
161+
162+
def flush_mod_buffer(self):
163+
self.main_widget.data_changed(self.data, self.mod_buffer)
164+
del self.mod_buffer
165+
45166
def sub_mod(self, mod):
46-
self.main_widget.data_changed(self.data, mod)
167+
if not self.filter_mod(mod):
168+
return
169+
170+
if self.args.update_delay:
171+
if hasattr(self, "mod_buffer"):
172+
self.mod_buffer.append(mod)
173+
else:
174+
self.mod_buffer = [mod]
175+
asyncio.get_event_loop().call_later(self.args.update_delay,
176+
self.flush_mod_buffer)
177+
else:
178+
self.main_widget.data_changed(self.data, [mod])
179+
180+
def subscribe(self):
181+
if self.args.embed is None:
182+
self.subscriber = Subscriber("datasets",
183+
self.sub_init, self.sub_mod)
184+
self.loop.run_until_complete(self.subscriber.connect(
185+
self.args.server, self.args.port))
186+
else:
187+
self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod)
47188

48-
def create_subscriber(self):
49-
self.subscriber = Subscriber("datasets",
50-
self.sub_init, self.sub_mod)
51-
self.loop.run_until_complete(self.subscriber.connect(
52-
self.args.server, self.args.port))
189+
def unsubscribe(self):
190+
if self.args.embed is None:
191+
self.loop.run_until_complete(self.subscriber.close())
53192

54193
def run(self):
55194
self.args_init()
56195
self.quamash_init()
57196
try:
58-
self.create_main_widget()
59-
self.create_subscriber()
197+
self.ipc_init()
60198
try:
61-
self.loop.run_forever()
199+
self.create_main_widget()
200+
self.subscribe()
201+
try:
202+
self.loop.run_forever()
203+
finally:
204+
self.unsubscribe()
62205
finally:
63-
self.loop.run_until_complete(self.subscriber.close())
206+
self.ipc_close()
64207
finally:
65208
self.loop.close()

‎artiq/frontend/artiq_gui.py

+13-6
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55
import atexit
66
import os
77

8-
# Quamash must be imported first so that pyqtgraph picks up the Qt binding
9-
# it has chosen.
8+
import PyQt5
109
from quamash import QEventLoop, QtGui, QtCore
10+
assert QtGui is PyQt5.QtGui
11+
# pyqtgraph will pick up any already imported Qt binding.
1112
from pyqtgraph import dockarea
1213

14+
1315
from artiq import __artiq_dir__ as artiq_dir
1416
from artiq.tools import *
1517
from artiq.protocols.pc_rpc import AsyncioClient
1618
from artiq.gui.models import ModelSubscriber
1719
from artiq.gui import (state, experiments, shortcuts, explorer,
18-
moninj, datasets, schedule, log, console)
20+
moninj, datasets, applets, schedule, log, console)
1921

2022

2123
def get_argparser():
@@ -110,7 +112,10 @@ def main():
110112
rpc_clients["experiment_db"])
111113

112114
d_datasets = datasets.DatasetsDock(win, dock_area, sub_clients["datasets"])
113-
smgr.register(d_datasets)
115+
116+
d_applets = applets.AppletsDock(dock_area, sub_clients["datasets"])
117+
atexit_register_coroutine(d_applets.stop)
118+
smgr.register(d_applets)
114119

115120
if os.name != "nt":
116121
d_ttl_dds = moninj.MonInj()
@@ -130,9 +135,11 @@ def main():
130135
if os.name != "nt":
131136
dock_area.addDock(d_ttl_dds.dds_dock, "top")
132137
dock_area.addDock(d_ttl_dds.ttl_dock, "above", d_ttl_dds.dds_dock)
133-
dock_area.addDock(d_datasets, "above", d_ttl_dds.ttl_dock)
138+
dock_area.addDock(d_applets, "above", d_ttl_dds.ttl_dock)
139+
dock_area.addDock(d_datasets, "above", d_applets)
134140
else:
135-
dock_area.addDock(d_datasets, "top")
141+
dock_area.addDock(d_applets, "top")
142+
dock_area.addDock(d_datasets, "above", d_applets)
136143
dock_area.addDock(d_shortcuts, "above", d_datasets)
137144
dock_area.addDock(d_explorer, "above", d_shortcuts)
138145
dock_area.addDock(d_console, "bottom")

‎artiq/gui/applets.py

+327
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import logging
2+
import asyncio
3+
import sys
4+
import shlex
5+
from functools import partial
6+
7+
from quamash import QtCore, QtGui, QtWidgets
8+
from pyqtgraph import dockarea
9+
10+
from artiq.protocols.pipe_ipc import AsyncioParentComm
11+
from artiq.protocols import pyon
12+
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class AppletIPCServer(AsyncioParentComm):
18+
def __init__(self, datasets_sub):
19+
AsyncioParentComm.__init__(self)
20+
self.datasets_sub = datasets_sub
21+
self.datasets = set()
22+
23+
def write_pyon(self, obj):
24+
self.write(pyon.encode(obj).encode() + b"\n")
25+
26+
async def read_pyon(self):
27+
line = await self.readline()
28+
return pyon.decode(line.decode())
29+
30+
def _synthesize_init(self, data):
31+
struct = {k: v for k, v in data.items() if k in self.datasets}
32+
return {"action": "init",
33+
"struct": struct}
34+
35+
def _on_mod(self, mod):
36+
if mod["action"] == "init":
37+
mod = self._synthesize_init(mod["struct"])
38+
else:
39+
if mod["path"]:
40+
if mod["path"][0] not in self.datasets:
41+
return
42+
elif mod["action"] in {"setitem", "delitem"}:
43+
if mod["key"] not in self.datasets:
44+
return
45+
self.write_pyon({"action": "mod", "mod": mod})
46+
47+
async def serve(self, embed_cb):
48+
self.datasets_sub.notify_cbs.append(self._on_mod)
49+
try:
50+
while True:
51+
obj = await self.read_pyon()
52+
try:
53+
action = obj["action"]
54+
if action == "embed":
55+
embed_cb(obj["win_id"])
56+
self.write_pyon({"action": "embed_done"})
57+
elif action == "subscribe":
58+
self.datasets = obj["datasets"]
59+
if self.datasets_sub.model is not None:
60+
mod = self._synthesize_init(
61+
self.datasets_sub.model.backing_store)
62+
self.write_pyon({"action": "mod", "mod": mod})
63+
else:
64+
raise ValueError("unknown action in applet message")
65+
except:
66+
logger.warning("error processing applet message",
67+
exc_info=True)
68+
self.write_pyon({"action": "error"})
69+
except asyncio.CancelledError:
70+
pass
71+
except:
72+
logger.error("error processing data from applet, "
73+
"server stopped", exc_info=True)
74+
finally:
75+
self.datasets_sub.notify_cbs.remove(self._on_mod)
76+
77+
def start(self, embed_cb):
78+
self.server_task = asyncio.ensure_future(self.serve(embed_cb))
79+
80+
async def stop(self):
81+
self.server_task.cancel()
82+
await asyncio.wait([self.server_task])
83+
84+
85+
class AppletDock(dockarea.Dock):
86+
def __init__(self, datasets_sub, uid, name, command):
87+
dockarea.Dock.__init__(self, "applet" + str(uid),
88+
label="Applet: " + name,
89+
closable=True)
90+
self.setMinimumSize(QtCore.QSize(500, 400))
91+
self.datasets_sub = datasets_sub
92+
self.applet_name = name
93+
self.command = command
94+
95+
def rename(self, name):
96+
self.applet_name = name
97+
self.label.setText("Applet: " + name)
98+
99+
async def start(self):
100+
self.ipc = AppletIPCServer(self.datasets_sub)
101+
if "{ipc_address}" not in self.command:
102+
logger.warning("IPC address missing from command for %s",
103+
self.applet_name)
104+
command = self.command.format(python=sys.executable,
105+
ipc_address=self.ipc.get_address())
106+
logger.debug("starting command %s for %s", command, self.applet_name)
107+
try:
108+
await self.ipc.create_subprocess(*shlex.split(command))
109+
except:
110+
logger.warning("Applet %s failed to start", self.applet_name,
111+
exc_info=True)
112+
self.ipc.start(self.embed)
113+
114+
def embed(self, win_id):
115+
logger.debug("capturing window 0x%x for %s", win_id, self.applet_name)
116+
embed_window = QtGui.QWindow.fromWinId(win_id)
117+
embed_widget = QtWidgets.QWidget.createWindowContainer(embed_window)
118+
self.addWidget(embed_widget)
119+
120+
async def terminate(self):
121+
if hasattr(self, "ipc"):
122+
await self.ipc.stop()
123+
self.ipc.write_pyon({"action": "terminate"})
124+
try:
125+
await asyncio.wait_for(self.ipc.process.wait(), 2.0)
126+
except:
127+
logger.warning("Applet %s failed to exit, killing",
128+
self.applet_name)
129+
try:
130+
self.ipc.process.kill()
131+
except ProcessLookupError:
132+
pass
133+
await self.ipc.process.wait()
134+
del self.ipc
135+
136+
async def restart(self):
137+
await self.terminate()
138+
await self.start()
139+
140+
141+
_templates = [
142+
("Big number", "{python} -m artiq.applets.big_number "
143+
"--embed {ipc_address} NUMBER_DATASET"),
144+
("Histogram", "{python} -m artiq.applets.plot_hist "
145+
"--embed {ipc_address} COUNTS_DATASET "
146+
"--x BIN_BOUNDARIES_DATASET"),
147+
("XY", "{python} -m artiq.applets.plot_xy "
148+
"--embed {ipc_address} Y_DATASET --x X_DATASET "
149+
"--error ERROR_DATASET --fit FIT_DATASET"),
150+
("XY + Histogram", "{python} -m artiq.applets.plot_xy_hist "
151+
"--embed {ipc_address} X_DATASET "
152+
"HIST_BIN_BOUNDARIES_DATASET "
153+
"HISTS_COUNTS_DATASET"),
154+
]
155+
156+
157+
class AppletsDock(dockarea.Dock):
158+
def __init__(self, dock_area, datasets_sub):
159+
self.dock_area = dock_area
160+
self.datasets_sub = datasets_sub
161+
self.dock_to_checkbox = dict()
162+
self.applet_uids = set()
163+
self.workaround_pyqtgraph_bug = False
164+
165+
dockarea.Dock.__init__(self, "Applets")
166+
self.setMinimumSize(QtCore.QSize(850, 450))
167+
168+
self.table = QtWidgets.QTableWidget(0, 3)
169+
self.table.setHorizontalHeaderLabels(["Enable", "Name", "Command"])
170+
self.table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
171+
self.table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
172+
self.table.horizontalHeader().setStretchLastSection(True)
173+
self.table.horizontalHeader().setResizeMode(
174+
QtGui.QHeaderView.ResizeToContents)
175+
self.table.verticalHeader().setResizeMode(
176+
QtGui.QHeaderView.ResizeToContents)
177+
self.table.verticalHeader().hide()
178+
self.table.setTextElideMode(QtCore.Qt.ElideNone)
179+
self.addWidget(self.table)
180+
181+
self.table.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
182+
new_action = QtGui.QAction("New applet", self.table)
183+
new_action.triggered.connect(self.new)
184+
self.table.addAction(new_action)
185+
templates_menu = QtGui.QMenu()
186+
for name, template in _templates:
187+
action = QtGui.QAction(name, self.table)
188+
action.triggered.connect(partial(self.new_template, template))
189+
templates_menu.addAction(action)
190+
restart_action = QtGui.QAction("New applet from template", self.table)
191+
restart_action.setMenu(templates_menu)
192+
self.table.addAction(restart_action)
193+
restart_action = QtGui.QAction("Restart selected applet", self.table)
194+
restart_action.setShortcut("CTRL+R")
195+
restart_action.setShortcutContext(QtCore.Qt.WidgetShortcut)
196+
restart_action.triggered.connect(self.restart)
197+
self.table.addAction(restart_action)
198+
delete_action = QtGui.QAction("Delete selected applet", self.table)
199+
delete_action.setShortcut("DELETE")
200+
delete_action.setShortcutContext(QtCore.Qt.WidgetShortcut)
201+
delete_action.triggered.connect(self.delete)
202+
self.table.addAction(delete_action)
203+
204+
self.table.cellChanged.connect(self.cell_changed)
205+
206+
def create(self, uid, name, command):
207+
dock = AppletDock(self.datasets_sub, uid, name, command)
208+
# If a dock is floated and then dock state is restored, pyqtgraph
209+
# leaves a "phantom" window open.
210+
if self.workaround_pyqtgraph_bug:
211+
self.dock_area.addDock(dock)
212+
else:
213+
self.dock_area.floatDock(dock)
214+
asyncio.ensure_future(dock.start())
215+
dock.sigClosed.connect(partial(self.on_dock_closed, dock))
216+
return dock
217+
218+
def cell_changed(self, row, column):
219+
if column == 0:
220+
item = self.table.item(row, column)
221+
if item.checkState() == QtCore.Qt.Checked:
222+
command = self.table.item(row, 2)
223+
if command:
224+
command = command.text()
225+
name = self.table.item(row, 1)
226+
if name is None:
227+
name = ""
228+
else:
229+
name = name.text()
230+
dock = self.create(item.applet_uid, name, command)
231+
item.applet_dock = dock
232+
self.dock_to_checkbox[dock] = item
233+
else:
234+
dock = item.applet_dock
235+
if dock is not None:
236+
# This calls self.on_dock_closed
237+
dock.close()
238+
elif column == 1 or column == 2:
239+
new_value = self.table.item(row, column).text()
240+
dock = self.table.item(row, 0).applet_dock
241+
if dock is not None:
242+
if column == 1:
243+
dock.rename(new_value)
244+
else:
245+
dock.command = new_value
246+
247+
def on_dock_closed(self, dock):
248+
asyncio.ensure_future(dock.terminate())
249+
checkbox_item = self.dock_to_checkbox[dock]
250+
checkbox_item.applet_dock = None
251+
del self.dock_to_checkbox[dock]
252+
checkbox_item.setCheckState(QtCore.Qt.Unchecked)
253+
254+
def new(self, uid=None):
255+
if uid is None:
256+
uid = next(iter(set(range(len(self.applet_uids) + 1))
257+
- self.applet_uids))
258+
self.applet_uids.add(uid)
259+
260+
row = self.table.rowCount()
261+
self.table.insertRow(row)
262+
checkbox = QtWidgets.QTableWidgetItem()
263+
checkbox.setFlags(QtCore.Qt.ItemIsSelectable |
264+
QtCore.Qt.ItemIsUserCheckable |
265+
QtCore.Qt.ItemIsEnabled)
266+
checkbox.setCheckState(QtCore.Qt.Unchecked)
267+
checkbox.applet_uid = uid
268+
checkbox.applet_dock = None
269+
self.table.setItem(row, 0, checkbox)
270+
self.table.setItem(row, 1, QtWidgets.QTableWidgetItem())
271+
self.table.setItem(row, 2, QtWidgets.QTableWidgetItem())
272+
return row
273+
274+
def new_template(self, template):
275+
row = self.new()
276+
self.table.item(row, 2).setText(template)
277+
278+
def restart(self):
279+
selection = self.table.selectedRanges()
280+
if selection:
281+
row = selection[0].topRow()
282+
dock = self.table.item(row, 0).applet_dock
283+
if dock is not None:
284+
asyncio.ensure_future(dock.restart())
285+
286+
def delete(self):
287+
selection = self.table.selectedRanges()
288+
if selection:
289+
row = selection[0].topRow()
290+
item = self.table.item(row, 0)
291+
dock = item.applet_dock
292+
if dock is not None:
293+
# This calls self.on_dock_closed
294+
dock.close()
295+
self.applet_uids.remove(item.applet_uid)
296+
self.table.removeRow(row)
297+
298+
299+
async def stop(self):
300+
for row in range(self.table.rowCount()):
301+
dock = self.table.item(row, 0).applet_dock
302+
if dock is not None:
303+
await dock.terminate()
304+
305+
def save_state(self):
306+
state = []
307+
for row in range(self.table.rowCount()):
308+
uid = self.table.item(row, 0).applet_uid
309+
enabled = self.table.item(row, 0).checkState() == QtCore.Qt.Checked
310+
name = self.table.item(row, 1).text()
311+
command = self.table.item(row, 2).text()
312+
state.append((uid, enabled, name, command))
313+
return state
314+
315+
def restore_state(self, state):
316+
self.workaround_pyqtgraph_bug = True
317+
for uid, enabled, name, command in state:
318+
row = self.new(uid)
319+
item = QtWidgets.QTableWidgetItem()
320+
item.setText(name)
321+
self.table.setItem(row, 1, item)
322+
item = QtWidgets.QTableWidgetItem()
323+
item.setText(command)
324+
self.table.setItem(row, 2, item)
325+
if enabled:
326+
self.table.item(row, 0).setCheckState(QtCore.Qt.Checked)
327+
self.workaround_pyqtgraph_bug = False

‎artiq/gui/datasets.py

+1-94
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@
99

1010
from artiq.tools import short_format
1111
from artiq.gui.models import DictSyncTreeSepModel
12-
from artiq.gui.displays import *
13-
14-
try:
15-
QSortFilterProxyModel = QtCore.QSortFilterProxyModel
16-
except AttributeError:
17-
QSortFilterProxyModel = QtGui.QSortFilterProxyModel
1812

1913

2014
logger = logging.getLogger(__name__)
@@ -35,12 +29,6 @@ def convert(self, k, v, column):
3529
raise ValueError
3630

3731

38-
def _get_display_type_name(display_cls):
39-
for name, (_, cls) in display_types.items():
40-
if cls is display_cls:
41-
return name
42-
43-
4432
class DatasetsDock(dockarea.Dock):
4533
def __init__(self, dialog_parent, dock_area, datasets_sub):
4634
dockarea.Dock.__init__(self, "Datasets")
@@ -62,19 +50,6 @@ def __init__(self, dialog_parent, dock_area, datasets_sub):
6250

6351
self.table_model = Model(dict())
6452
datasets_sub.add_setmodel_callback(self.set_model)
65-
datasets_sub.notify_cbs.append(self.on_mod)
66-
67-
add_display_box = QtGui.QGroupBox("Add display")
68-
grid.addWidget(add_display_box, 1, 1)
69-
display_grid = QtGui.QGridLayout()
70-
add_display_box.setLayout(display_grid)
71-
72-
for n, name in enumerate(display_types.keys()):
73-
btn = QtGui.QPushButton(name)
74-
display_grid.addWidget(btn, n, 0)
75-
btn.clicked.connect(partial(self.create_dialog, name))
76-
77-
self.displays = dict()
7853

7954
def _search_datasets(self):
8055
if hasattr(self, "table_model_filter"):
@@ -83,74 +58,6 @@ def _search_datasets(self):
8358

8459
def set_model(self, model):
8560
self.table_model = model
86-
self.table_model_filter = QSortFilterProxyModel()
61+
self.table_model_filter = QtCore.QSortFilterProxyModel()
8762
self.table_model_filter.setSourceModel(self.table_model)
8863
self.table.setModel(self.table_model_filter)
89-
90-
def update_display_data(self, dsp):
91-
filtered_data = {k: self.table_model.backing_store[k][1]
92-
for k in dsp.data_sources()
93-
if k in self.table_model.backing_store}
94-
dsp.update_data(filtered_data)
95-
96-
def on_mod(self, mod):
97-
if mod["action"] == "init":
98-
for display in self.displays.values():
99-
display.update_data(self.table_model.backing_store)
100-
return
101-
102-
if mod["path"]:
103-
source = mod["path"][0]
104-
elif mod["action"] == "setitem":
105-
source = mod["key"]
106-
else:
107-
return
108-
109-
for display in self.displays.values():
110-
if source in display.data_sources():
111-
self.update_display_data(display)
112-
113-
def create_dialog(self, ty):
114-
dlg_class = display_types[ty][0]
115-
dlg = dlg_class(self.dialog_parent, None, dict(),
116-
sorted(self.table_model.backing_store.keys()),
117-
partial(self.create_display, ty, None))
118-
dlg.open()
119-
120-
def create_display(self, ty, prev_name, name, settings):
121-
if prev_name is not None and prev_name in self.displays:
122-
raise NotImplementedError
123-
dsp_class = display_types[ty][1]
124-
dsp = dsp_class(name, settings)
125-
self.displays[name] = dsp
126-
self.update_display_data(dsp)
127-
128-
def on_close():
129-
del self.displays[name]
130-
dsp.sigClosed.connect(on_close)
131-
self.dock_area.floatDock(dsp)
132-
return dsp
133-
134-
def save_state(self):
135-
r = dict()
136-
for name, display in self.displays.items():
137-
r[name] = {
138-
"ty": _get_display_type_name(type(display)),
139-
"settings": display.settings,
140-
"state": display.save_state()
141-
}
142-
return r
143-
144-
def restore_state(self, state):
145-
for name, desc in state.items():
146-
try:
147-
dsp = self.create_display(desc["ty"], None, name,
148-
desc["settings"])
149-
except:
150-
logger.warning("Failed to create display '%s'", name,
151-
exc_info=True)
152-
try:
153-
dsp.restore_state(desc["state"])
154-
except:
155-
logger.warning("Failed to restore display state of '%s'",
156-
name, exc_info=True)

‎artiq/gui/displays.py

-217
This file was deleted.

‎artiq/gui/scan.py ‎artiq/gui/entries.py

+111-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,100 @@
1010
logger = logging.getLogger(__name__)
1111

1212

13+
class _StringEntry(QtGui.QLineEdit):
14+
def __init__(self, argument):
15+
QtGui.QLineEdit.__init__(self)
16+
self.setText(argument["state"])
17+
def update(text):
18+
argument["state"] = text
19+
self.textEdited.connect(update)
20+
21+
@staticmethod
22+
def state_to_value(state):
23+
return state
24+
25+
@staticmethod
26+
def default_state(procdesc):
27+
return procdesc.get("default", "")
28+
29+
30+
class _BooleanEntry(QtGui.QCheckBox):
31+
def __init__(self, argument):
32+
QtGui.QCheckBox.__init__(self)
33+
self.setChecked(argument["state"])
34+
def update(checked):
35+
argument["state"] = bool(checked)
36+
self.stateChanged.connect(update)
37+
38+
@staticmethod
39+
def state_to_value(state):
40+
return state
41+
42+
@staticmethod
43+
def default_state(procdesc):
44+
return procdesc.get("default", False)
45+
46+
47+
class _EnumerationEntry(QtGui.QComboBox):
48+
def __init__(self, argument):
49+
QtGui.QComboBox.__init__(self)
50+
disable_scroll_wheel(self)
51+
choices = argument["desc"]["choices"]
52+
self.addItems(choices)
53+
idx = choices.index(argument["state"])
54+
self.setCurrentIndex(idx)
55+
def update(index):
56+
argument["state"] = choices[index]
57+
self.currentIndexChanged.connect(update)
58+
59+
@staticmethod
60+
def state_to_value(state):
61+
return state
62+
63+
@staticmethod
64+
def default_state(procdesc):
65+
if "default" in procdesc:
66+
return procdesc["default"]
67+
else:
68+
return procdesc["choices"][0]
69+
70+
71+
class _NumberEntry(QtGui.QDoubleSpinBox):
72+
def __init__(self, argument):
73+
QtGui.QDoubleSpinBox.__init__(self)
74+
disable_scroll_wheel(self)
75+
procdesc = argument["desc"]
76+
scale = procdesc["scale"]
77+
self.setDecimals(procdesc["ndecimals"])
78+
self.setSingleStep(procdesc["step"]/scale)
79+
if procdesc["min"] is not None:
80+
self.setMinimum(procdesc["min"]/scale)
81+
else:
82+
self.setMinimum(float("-inf"))
83+
if procdesc["max"] is not None:
84+
self.setMaximum(procdesc["max"]/scale)
85+
else:
86+
self.setMaximum(float("inf"))
87+
if procdesc["unit"]:
88+
self.setSuffix(" " + procdesc["unit"])
89+
90+
self.setValue(argument["state"]/scale)
91+
def update(value):
92+
argument["state"] = value*scale
93+
self.valueChanged.connect(update)
94+
95+
@staticmethod
96+
def state_to_value(state):
97+
return state
98+
99+
@staticmethod
100+
def default_state(procdesc):
101+
if "default" in procdesc:
102+
return procdesc["default"]
103+
else:
104+
return 0.0
105+
106+
13107
class _NoScan(LayoutWidget):
14108
def __init__(self, procdesc, state):
15109
LayoutWidget.__init__(self)
@@ -38,7 +132,7 @@ def update(value):
38132
self.value.valueChanged.connect(update)
39133

40134

41-
class _Range(LayoutWidget):
135+
class _RangeScan(LayoutWidget):
42136
def __init__(self, procdesc, state):
43137
LayoutWidget.__init__(self)
44138

@@ -90,7 +184,8 @@ def update_npoints(value):
90184
self.max.valueChanged.connect(update_max)
91185
self.npoints.valueChanged.connect(update_npoints)
92186

93-
class _Explicit(LayoutWidget):
187+
188+
class _ExplicitScan(LayoutWidget):
94189
def __init__(self, state):
95190
LayoutWidget.__init__(self)
96191

@@ -109,7 +204,7 @@ def update(text):
109204
self.value.textEdited.connect(update)
110205

111206

112-
class ScanController(LayoutWidget):
207+
class _ScanEntry(LayoutWidget):
113208
def __init__(self, argument):
114209
LayoutWidget.__init__(self)
115210
self.argument = argument
@@ -121,9 +216,9 @@ def __init__(self, argument):
121216
state = argument["state"]
122217
self.widgets = OrderedDict()
123218
self.widgets["NoScan"] = _NoScan(procdesc, state["NoScan"])
124-
self.widgets["LinearScan"] = _Range(procdesc, state["LinearScan"])
125-
self.widgets["RandomScan"] = _Range(procdesc, state["RandomScan"])
126-
self.widgets["ExplicitScan"] = _Explicit(state["ExplicitScan"])
219+
self.widgets["LinearScan"] = _RangeScan(procdesc, state["LinearScan"])
220+
self.widgets["RandomScan"] = _RangeScan(procdesc, state["RandomScan"])
221+
self.widgets["ExplicitScan"] = _ExplicitScan(state["ExplicitScan"])
127222
for widget in self.widgets.values():
128223
self.stack.addWidget(widget)
129224

@@ -181,3 +276,13 @@ def _scan_type_toggled(self):
181276
self.stack.setCurrentWidget(self.widgets[ty])
182277
self.argument["state"]["selected"] = ty
183278
break
279+
280+
281+
argty_to_entry = {
282+
"PYONValue": _StringEntry,
283+
"BooleanValue": _BooleanEntry,
284+
"EnumerationValue": _EnumerationEntry,
285+
"NumberValue": _NumberEntry,
286+
"StringValue": _StringEntry,
287+
"Scannable": _ScanEntry
288+
}

‎artiq/gui/experiments.py

+7-111
Original file line numberDiff line numberDiff line change
@@ -7,117 +7,13 @@
77

88
from pyqtgraph import dockarea, LayoutWidget
99

10-
from artiq.gui.tools import log_level_to_name, disable_scroll_wheel
11-
from artiq.gui.scan import ScanController
10+
from artiq.gui.tools import log_level_to_name
11+
from artiq.gui.entries import argty_to_entry
1212

1313

1414
logger = logging.getLogger(__name__)
1515

1616

17-
class _StringEntry(QtGui.QLineEdit):
18-
def __init__(self, argument):
19-
QtGui.QLineEdit.__init__(self)
20-
self.setText(argument["state"])
21-
def update(text):
22-
argument["state"] = text
23-
self.textEdited.connect(update)
24-
25-
@staticmethod
26-
def state_to_value(state):
27-
return state
28-
29-
@staticmethod
30-
def default_state(procdesc):
31-
return procdesc.get("default", "")
32-
33-
34-
class _BooleanEntry(QtGui.QCheckBox):
35-
def __init__(self, argument):
36-
QtGui.QCheckBox.__init__(self)
37-
self.setChecked(argument["state"])
38-
def update(checked):
39-
argument["state"] = bool(checked)
40-
self.stateChanged.connect(update)
41-
42-
@staticmethod
43-
def state_to_value(state):
44-
return state
45-
46-
@staticmethod
47-
def default_state(procdesc):
48-
return procdesc.get("default", False)
49-
50-
51-
class _EnumerationEntry(QtGui.QComboBox):
52-
def __init__(self, argument):
53-
QtGui.QComboBox.__init__(self)
54-
disable_scroll_wheel(self)
55-
choices = argument["desc"]["choices"]
56-
self.addItems(choices)
57-
idx = choices.index(argument["state"])
58-
self.setCurrentIndex(idx)
59-
def update(index):
60-
argument["state"] = choices[index]
61-
self.currentIndexChanged.connect(update)
62-
63-
@staticmethod
64-
def state_to_value(state):
65-
return state
66-
67-
@staticmethod
68-
def default_state(procdesc):
69-
if "default" in procdesc:
70-
return procdesc["default"]
71-
else:
72-
return procdesc["choices"][0]
73-
74-
75-
class _NumberEntry(QtGui.QDoubleSpinBox):
76-
def __init__(self, argument):
77-
QtGui.QDoubleSpinBox.__init__(self)
78-
disable_scroll_wheel(self)
79-
procdesc = argument["desc"]
80-
scale = procdesc["scale"]
81-
self.setDecimals(procdesc["ndecimals"])
82-
self.setSingleStep(procdesc["step"]/scale)
83-
if procdesc["min"] is not None:
84-
self.setMinimum(procdesc["min"]/scale)
85-
else:
86-
self.setMinimum(float("-inf"))
87-
if procdesc["max"] is not None:
88-
self.setMaximum(procdesc["max"]/scale)
89-
else:
90-
self.setMaximum(float("inf"))
91-
if procdesc["unit"]:
92-
self.setSuffix(" " + procdesc["unit"])
93-
94-
self.setValue(argument["state"]/scale)
95-
def update(value):
96-
argument["state"] = value*scale
97-
self.valueChanged.connect(update)
98-
99-
@staticmethod
100-
def state_to_value(state):
101-
return state
102-
103-
@staticmethod
104-
def default_state(procdesc):
105-
if "default" in procdesc:
106-
return procdesc["default"]
107-
else:
108-
return 0.0
109-
110-
111-
_argty_to_entry = {
112-
"PYONValue": _StringEntry,
113-
"BooleanValue": _BooleanEntry,
114-
"EnumerationValue": _EnumerationEntry,
115-
"NumberValue": _NumberEntry,
116-
"StringValue": _StringEntry,
117-
"Scannable": ScanController
118-
}
119-
120-
12117
# Experiment URLs come in two forms:
12218
# 1. repo:<experiment name>
12319
# (file name and class name to be retrieved from explist)
@@ -153,7 +49,7 @@ def __init__(self, manager, dock, expurl):
15349
self.addTopLevelItem(QtGui.QTreeWidgetItem(["No arguments"]))
15450

15551
for name, argument in arguments.items():
156-
entry = _argty_to_entry[argument["desc"]["ty"]](argument)
52+
entry = argty_to_entry[argument["desc"]["ty"]](argument)
15753
widget_item = QtGui.QTreeWidgetItem([name])
15854
self._arg_to_entry_widgetitem[name] = entry, widget_item
15955

@@ -211,14 +107,14 @@ async def _recompute_argument(self, name):
211107
argument = self.manager.get_submission_arguments(self.expurl)[name]
212108

213109
procdesc = arginfo[name][0]
214-
state = _argty_to_entry[procdesc["ty"]].default_state(procdesc)
110+
state = argty_to_entry[procdesc["ty"]].default_state(procdesc)
215111
argument["desc"] = procdesc
216112
argument["state"] = state
217113

218114
old_entry, widget_item = self._arg_to_entry_widgetitem[name]
219115
old_entry.deleteLater()
220116

221-
entry = _argty_to_entry[procdesc["ty"]](argument)
117+
entry = argty_to_entry[procdesc["ty"]](argument)
222118
self._arg_to_entry_widgetitem[name] = entry, widget_item
223119
self.setItemWidget(widget_item, 1, entry)
224120

@@ -466,7 +362,7 @@ def get_submission_options(self, expurl):
466362
def initialize_submission_arguments(self, expurl, arginfo):
467363
arguments = OrderedDict()
468364
for name, (procdesc, group) in arginfo.items():
469-
state = _argty_to_entry[procdesc["ty"]].default_state(procdesc)
365+
state = argty_to_entry[procdesc["ty"]].default_state(procdesc)
470366
arguments[name] = {
471367
"desc": procdesc,
472368
"group": group,
@@ -512,7 +408,7 @@ def submit(self, expurl):
512408

513409
argument_values = dict()
514410
for name, argument in arguments.items():
515-
entry_cls = _argty_to_entry[argument["desc"]["ty"]]
411+
entry_cls = argty_to_entry[argument["desc"]["ty"]]
516412
argument_values[name] = entry_cls.state_to_value(argument["state"])
517413

518414
expid = {

‎artiq/gui/log.py

+5-9
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@
99

1010
from artiq.gui.tools import log_level_to_name
1111

12-
try:
13-
QSortFilterProxyModel = QtCore.QSortFilterProxyModel
14-
except AttributeError:
15-
QSortFilterProxyModel = QtGui.QSortFilterProxyModel
16-
1712

1813
def _make_wrappable(row, width=30):
1914
level, source, time, msg = row
@@ -34,8 +29,7 @@ def __init__(self, init):
3429
timer.timeout.connect(self.timer_tick)
3530
timer.start(100)
3631

37-
self.fixed_font = QtGui.QFont()
38-
self.fixed_font.setFamily("Monospace")
32+
self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
3933

4034
self.white = QtGui.QBrush(QtGui.QColor(255, 255, 255))
4135
self.black = QtGui.QBrush(QtGui.QColor(0, 0, 0))
@@ -87,6 +81,8 @@ def data(self, index, role):
8781
if (role == QtCore.Qt.FontRole
8882
and index.column() == 1):
8983
return self.fixed_font
84+
elif role == QtCore.Qt.TextAlignmentRole:
85+
return QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
9086
elif role == QtCore.Qt.BackgroundRole:
9187
level = self.entries[index.row()][0]
9288
if level >= logging.ERROR:
@@ -114,9 +110,9 @@ def data(self, index, role):
114110
time.strftime("%m/%d %H:%M:%S", time.localtime(v[2])))
115111

116112

117-
class _LogFilterProxyModel(QSortFilterProxyModel):
113+
class _LogFilterProxyModel(QtCore.QSortFilterProxyModel):
118114
def __init__(self, min_level, freetext):
119-
QSortFilterProxyModel.__init__(self)
115+
QtCore.QSortFilterProxyModel.__init__(self)
120116
self.min_level = min_level
121117
self.freetext = freetext
122118

‎artiq/gui/shortcuts.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import logging
22
from functools import partial
33

4-
from quamash import QtGui, QtCore
4+
from quamash import QtGui, QtCore, QtWidgets
55
from pyqtgraph import dockarea
6-
try:
7-
from quamash import QtWidgets
8-
QShortcut = QtWidgets.QShortcut
9-
except:
10-
QShortcut = QtGui.QShortcut
116

127

138
logger = logging.getLogger(__name__)
@@ -66,7 +61,7 @@ def __init__(self, main_window, exp_manager):
6661
"open": open,
6762
"submit": submit
6863
}
69-
shortcut = QShortcut("F" + str(i+1), main_window)
64+
shortcut = QtWidgets.QShortcut("F" + str(i+1), main_window)
7065
shortcut.setContext(QtCore.Qt.ApplicationShortcut)
7166
shortcut.activated.connect(partial(self._activated, i))
7267

‎artiq/master/log.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ def __init__(self, log_buffer, *args, **kwargs):
2424

2525
def emit(self, record):
2626
message = self.format(record)
27-
for part in message.split("\n"):
28-
self.log_buffer.log(record.levelno, record.source, record.created,
29-
part)
27+
self.log_buffer.log(record.levelno, record.source, record.created,
28+
message)
29+
3030

3131
def log_args(parser):
3232
group = parser.add_argument_group("logging")

‎artiq/protocols/pyon.py

+9-26
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
bytes: "bytes",
3636
tuple: "tuple",
3737
list: "list",
38+
set: "set",
3839
dict: "dict",
3940
wrapping_int: "number",
4041
Fraction: "fraction",
@@ -98,6 +99,12 @@ def encode_list(self, x):
9899
r += "]"
99100
return r
100101

102+
def encode_set(self, x):
103+
r = "{"
104+
r += ", ".join([self.encode(item) for item in x])
105+
r += "}"
106+
return r
107+
101108
def encode_dict(self, x):
102109
r = "{"
103110
if not self.pretty or len(x) < 2:
@@ -149,9 +156,7 @@ def encode(self, x):
149156

150157
def encode(x, pretty=False):
151158
"""Serializes a Python object and returns the corresponding string in
152-
Python syntax.
153-
154-
"""
159+
Python syntax."""
155160
return _Encoder(pretty).encode(x)
156161

157162

@@ -181,9 +186,7 @@ def _npscalar(ty, data):
181186

182187
def decode(s):
183188
"""Parses a string in the Python syntax, reconstructs the corresponding
184-
object, and returns it.
185-
186-
"""
189+
object, and returns it."""
187190
return eval(s, _eval_dict, {})
188191

189192

@@ -202,23 +205,3 @@ def load_file(filename):
202205
"""Parses the specified file and returns the decoded Python object."""
203206
with open(filename, "r") as f:
204207
return decode(f.read())
205-
206-
207-
class FlatFileDB:
208-
def __init__(self, filename):
209-
self.filename = filename
210-
self.data = pyon.load_file(self.filename)
211-
212-
def save(self):
213-
pyon.store_file(self.filename, self.data)
214-
215-
def get(self, key):
216-
return self.data[key]
217-
218-
def set(self, key, value):
219-
self.data[key] = value
220-
self.save()
221-
222-
def delete(self, key):
223-
del self.data[key]
224-
self.save()

‎artiq/test/test_serialization.py

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
_pyon_test_object = {
1111
(1, 2): [(3, 4.2), (2, )],
1212
Fraction(3, 4): np.linspace(5, 10, 1),
13+
{"testing", "sets"},
1314
"a": np.int8(9), "b": np.int16(-98), "c": np.int32(42), "d": np.int64(-5),
1415
"e": np.uint8(8), "f": np.uint16(5), "g": np.uint32(4), "h": np.uint64(9),
1516
"x": np.float16(9.0), "y": np.float32(9.0), "z": np.float64(9.0),

‎conda/artiq-kc705-nist_clock/meta.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ requirements:
2222
- 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 "" }}
2323

2424
about:
25-
home: http://m-labs.hk/artiq
25+
home: https://m-labs.hk/artiq
2626
license: GPL
2727
summary: 'Bitstream, BIOS and runtime for NIST_QC2 on the KC705 board'

‎conda/artiq-kc705-nist_qc1/meta.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ requirements:
2222
- 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 "" }}
2323

2424
about:
25-
home: http://m-labs.hk/artiq
25+
home: https://m-labs.hk/artiq
2626
license: GPL
2727
summary: 'Bitstream, BIOS and runtime for NIST_QC1 on the KC705 board'

‎conda/artiq-kc705-nist_qc2/meta.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ requirements:
2222
- 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 "" }}
2323

2424
about:
25-
home: http://m-labs.hk/artiq
25+
home: https://m-labs.hk/artiq
2626
license: GPL
2727
summary: 'Bitstream, BIOS and runtime for NIST_QC2 on the KC705 board'

‎conda/artiq/meta.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ requirements:
4646
- sphinx-argparse
4747
- h5py
4848
- dateutil
49+
- pyqt5
4950
- quamash
5051
- pyqtgraph
5152
- pygit2
5253
- aiohttp
53-
- binutils-or1k-linux # [linux]
54+
- binutils-or1k-linux
5455
- pythonparser
5556
- levenshtein
5657

@@ -59,6 +60,6 @@ test:
5960
- artiq
6061

6162
about:
62-
home: http://m-labs.hk/artiq
63+
home: https://m-labs.hk/artiq
6364
license: GPL
6465
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.'

‎doc/manual/installing.rst

+5-4
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@ But you can also :ref:`install from sources <install-from-sources>`.
1414
Installing using conda
1515
----------------------
1616

17+
.. warning::
18+
Conda packages are supported for Linux (64-bit) and Windows (32- and 64-bit). Users of other
19+
operating systems (32-bit Linux, BSD, ...) should install from source.
20+
21+
1722
Installing Anaconda or Miniconda
1823
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1924

2025
* You can either install Anaconda (choose Python 3.5) from https://store.continuum.io/cshop/anaconda/
2126

2227
* Or install the more minimalistic Miniconda (choose Python 3.5) from http://conda.pydata.org/miniconda.html
2328

24-
.. warning::
25-
If you are installing on Windows, choose the Windows 32-bit version regardless of whether you have
26-
a 32-bit or 64-bit Windows.
27-
2829
After installing either Anaconda or Miniconda, open a new terminal and make sure the following command works::
2930

3031
$ conda

‎doc/manual/introduction.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ Technologies employed include Python, Migen, MiSoC/mor1kx, LLVM and llvmlite.
2020
ARTIQ is licensed under 3-clause BSD.
2121

2222
Website:
23-
http://m-labs.hk/artiq
23+
https://m-labs.hk/artiq
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from time import sleep
2+
3+
import numpy as np
4+
5+
from artiq.experiment import *
6+
7+
8+
class Histograms(EnvExperiment):
9+
"""Histograms demo"""
10+
def build(self):
11+
pass
12+
13+
def run(self):
14+
nbins = 50
15+
npoints = 20
16+
17+
bin_boundaries = np.linspace(-10, 30, nbins + 1)
18+
self.set_dataset("hd_bins", bin_boundaries,
19+
broadcast=True, save=False)
20+
21+
xs = np.empty(npoints)
22+
xs.fill(np.nan)
23+
xs = self.set_dataset("hd_xs", xs,
24+
broadcast=True, save=False)
25+
26+
counts = np.empty((npoints, nbins))
27+
counts = self.set_dataset("hd_counts", counts,
28+
broadcast=True, save=False)
29+
30+
for i in range(npoints):
31+
histogram, _ = np.histogram(np.random.normal(i, size=1000),
32+
bin_boundaries)
33+
counts[i] = histogram
34+
xs[i] = i % 8
35+
sleep(0.3)

‎setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
raise Exception("You need Python 3.5.1+")
1111

1212

13+
# Depends on PyQt5, but setuptools cannot check for it.
1314
requirements = [
1415
"sphinx", "sphinx-argparse", "pyserial", "numpy", "scipy",
1516
"python-dateutil", "prettytable", "h5py",
@@ -45,7 +46,7 @@
4546
cmdclass=versioneer.get_cmdclass(),
4647
author="M-Labs / NIST Ion Storage Group",
4748
author_email="sb@m-labs.hk",
48-
url="http://m-labs.hk/artiq",
49+
url="https://m-labs.hk/artiq",
4950
description="A control system for trapped-ion experiments",
5051
long_description=open("README.rst").read(),
5152
license="GPL",

0 commit comments

Comments
 (0)
Please sign in to comment.