-
Notifications
You must be signed in to change notification settings - Fork 201
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 ...
Showing
25 changed files
with
898 additions
and
550 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,69 +1,136 @@ | ||
#!/usr/bin/env python3.5 | ||
|
||
from pyqtgraph.Qt import QtGui, QtCore | ||
import pyqtgraph as pg | ||
|
||
import numpy as np | ||
from PyQt5 import QtWidgets | ||
import pyqtgraph | ||
|
||
from artiq.applets.simple import SimpleApplet | ||
|
||
|
||
def _compute_ys(histogram_bins, histograms_counts): | ||
bin_centers = np.empty(len(histogram_bins)-1) | ||
for i in range(len(bin_centers)): | ||
bin_centers[i] = (histogram_bins[i] + histogram_bins[i+1])/2 | ||
|
||
ys = np.empty(histograms_counts.shape[0]) | ||
for n, counts in enumerate(histograms_counts): | ||
ys[n] = sum(bin_centers*counts)/sum(counts) | ||
return ys | ||
|
||
class XYHistPlot: | ||
def __init__(self): | ||
self.graphics_window = pg.GraphicsWindow(title="XY/Histogram") | ||
self.graphics_window.resize(1000,600) | ||
self.graphics_window.setWindowTitle("XY/Histogram") | ||
|
||
self.xy_plot = self.graphics_window.addPlot() | ||
# pyqtgraph.GraphicsWindow fails to behave like a regular Qt widget | ||
# and breaks embedding. Do not use as top widget. | ||
class XYHistPlot(QtWidgets.QSplitter): | ||
def __init__(self, args): | ||
QtWidgets.QSplitter.__init__(self) | ||
self.resize(1000,600) | ||
self.setWindowTitle("XY/Histogram") | ||
|
||
self.xy_plot = pyqtgraph.PlotWidget() | ||
self.insertWidget(0, self.xy_plot) | ||
self.xy_plot_data = None | ||
self.arrow = None | ||
self.selected_index = None | ||
|
||
self.hist_plot = self.graphics_window.addPlot() | ||
self.hist_plot = pyqtgraph.PlotWidget() | ||
self.insertWidget(1, self.hist_plot) | ||
self.hist_plot_data = None | ||
|
||
def set_data(self, xs, histograms_bins, histograms_counts): | ||
ys = np.empty_like(xs) | ||
ys.fill(np.nan) | ||
for n, (bins, counts) in enumerate(zip(histograms_bins, | ||
histograms_counts)): | ||
bin_centers = np.empty(len(bins)-1) | ||
for i in range(len(bin_centers)): | ||
bin_centers[i] = (bins[i] + bins[i+1])/2 | ||
ys[n] = sum(bin_centers*counts)/sum(bin_centers) | ||
self.args = args | ||
|
||
def _set_full_data(self, xs, histogram_bins, histograms_counts): | ||
self.xy_plot.clear() | ||
self.hist_plot.clear() | ||
self.xy_plot_data = None | ||
self.hist_plot_data = None | ||
self.arrow = None | ||
self.selected_index = None | ||
|
||
self.histogram_bins = histogram_bins | ||
|
||
ys = _compute_ys(self.histogram_bins, histograms_counts) | ||
self.xy_plot_data = self.xy_plot.plot(x=xs, y=ys, | ||
pen=None, | ||
symbol="x", symbolSize=20) | ||
self.xy_plot_data.sigPointsClicked.connect(self.point_clicked) | ||
for point, bins, counts in zip(self.xy_plot_data.scatter.points(), | ||
histograms_bins, histograms_counts): | ||
point.histogram_bins = bins | ||
self.xy_plot_data.sigPointsClicked.connect(self._point_clicked) | ||
for index, (point, counts) in ( | ||
enumerate(zip(self.xy_plot_data.scatter.points(), | ||
histograms_counts))): | ||
point.histogram_index = index | ||
point.histogram_counts = counts | ||
|
||
self.hist_plot_data = self.hist_plot.plot( | ||
stepMode=True, fillLevel=0, | ||
brush=(0, 0, 255, 150)) | ||
stepMode=True, fillLevel=0, | ||
brush=(0, 0, 255, 150)) | ||
|
||
def _set_partial_data(self, xs, histograms_counts): | ||
ys = _compute_ys(self.histogram_bins, histograms_counts) | ||
self.xy_plot_data.setData(x=xs, y=ys, | ||
pen=None, | ||
symbol="x", symbolSize=20) | ||
for index, (point, counts) in ( | ||
enumerate(zip(self.xy_plot_data.scatter.points(), | ||
histograms_counts))): | ||
point.histogram_index = index | ||
point.histogram_counts = counts | ||
|
||
def point_clicked(self, data_item, spot_items): | ||
def _point_clicked(self, data_item, spot_items): | ||
spot_item = spot_items[0] | ||
position = spot_item.pos() | ||
if self.arrow is None: | ||
self.arrow = pg.ArrowItem(angle=-120, tipAngle=30, baseAngle=20, | ||
headLen=40, tailLen=40, tailWidth=8, | ||
pen=None, brush="y") | ||
self.arrow = pyqtgraph.ArrowItem( | ||
angle=-120, tipAngle=30, baseAngle=20, headLen=40, | ||
tailLen=40, tailWidth=8, pen=None, brush="y") | ||
self.arrow.setPos(position) | ||
# NB: temporary glitch if addItem is done before setPos | ||
self.xy_plot.addItem(self.arrow) | ||
else: | ||
self.arrow.setPos(position) | ||
self.hist_plot_data.setData(x=spot_item.histogram_bins, | ||
self.selected_index = spot_item.histogram_index | ||
self.hist_plot_data.setData(x=self.histogram_bins, | ||
y=spot_item.histogram_counts) | ||
|
||
def _can_use_partial(self, mods): | ||
if self.hist_plot_data is None: | ||
return False | ||
for mod in mods: | ||
if mod["action"] != "setitem": | ||
return False | ||
if mod["path"] == [self.args.xs, 1]: | ||
if mod["key"] == self.selected_index: | ||
return False | ||
elif mod["path"][:2] == [self.args.histograms_counts, 1]: | ||
if len(mod["path"]) > 2: | ||
index = mod["path"][2] | ||
else: | ||
index = mod["key"] | ||
if index == self.selected_index: | ||
return False | ||
else: | ||
return False | ||
return True | ||
|
||
def data_changed(self, data, mods): | ||
try: | ||
xs = data[self.args.xs][1] | ||
histogram_bins = data[self.args.histogram_bins][1] | ||
histograms_counts = data[self.args.histograms_counts][1] | ||
except KeyError: | ||
return | ||
if self._can_use_partial(mods): | ||
self._set_partial_data(xs, histograms_counts) | ||
else: | ||
self._set_full_data(xs, histogram_bins, histograms_counts) | ||
|
||
|
||
def main(): | ||
app = QtGui.QApplication([]) | ||
plot = XYHistPlot() | ||
plot.set_data(np.array([1, 2, 3, 4, 1]), | ||
np.array([[1, 2, 3], [1, 2, 3], [1, 2, 3], [40, 70, 100], [4, 7, 10, 20]]), | ||
np.array([[1, 1], [2, 3], [10, 20], [3, 1], [100, 67, 102]])) | ||
app.exec_() | ||
|
||
if __name__ == '__main__': | ||
applet = SimpleApplet(XYHistPlot) | ||
applet.add_dataset("xs", "1D array of point abscissas") | ||
applet.add_dataset("histogram_bins", | ||
"1D array of histogram bin boundaries") | ||
applet.add_dataset("histograms_counts", | ||
"2D array of histogram counts, for each point") | ||
applet.run() | ||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,65 +1,208 @@ | ||
import logging | ||
import argparse | ||
import asyncio | ||
|
||
from quamash import QEventLoop, QtWidgets, QtCore | ||
from quamash import QEventLoop, QtWidgets, QtGui, QtCore | ||
|
||
from artiq.protocols.sync_struct import Subscriber | ||
from artiq.protocols.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: | ||
def __init__(self, main_widget_class, cmd_description=None): | ||
def __init__(self, main_widget_class, cmd_description=None, | ||
default_update_delay=0.0): | ||
self.main_widget_class = main_widget_class | ||
|
||
self.argparser = argparse.ArgumentParser(description=cmd_description) | ||
group = self.argparser.add_argument_group("data server") | ||
|
||
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( | ||
"--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( | ||
"--port", default=3250, type=int, | ||
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") | ||
|
||
def add_dataset(self, name, help=None): | ||
if help is None: | ||
self._arggroup_datasets.add_argument(name) | ||
self.dataset_args = set() | ||
|
||
def add_dataset(self, name, help=None, required=True): | ||
kwargs = dict() | ||
if help is not None: | ||
kwargs["help"] = help | ||
if required: | ||
self._arggroup_datasets.add_argument(name, **kwargs) | ||
else: | ||
self._arggroup_datasets.add_argument(name, help=help) | ||
self._arggroup_datasets.add_argument("--" + name, **kwargs) | ||
self.dataset_args.add(name) | ||
|
||
def args_init(self): | ||
self.args = self.argparser.parse_args() | ||
self.datasets = {getattr(self.args, arg.replace("-", "_")) | ||
for arg in self.dataset_args} | ||
|
||
def quamash_init(self): | ||
app = QtWidgets.QApplication([]) | ||
self.loop = QEventLoop(app) | ||
asyncio.set_event_loop(self.loop) | ||
|
||
def ipc_init(self): | ||
if self.args.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): | ||
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() | ||
|
||
def sub_init(self, data): | ||
self.data = data | ||
return data | ||
|
||
def filter_mod(self, mod): | ||
if self.args.embed is not None: | ||
# the parent already filters for us | ||
return True | ||
|
||
if mod["action"] == "init": | ||
return True | ||
if mod["path"]: | ||
return mod["path"][0] in self.datasets | ||
elif mod["action"] in {"setitem", "delitem"}: | ||
return mod["key"] in self.datasets | ||
else: | ||
return False | ||
|
||
def flush_mod_buffer(self): | ||
self.main_widget.data_changed(self.data, self.mod_buffer) | ||
del self.mod_buffer | ||
|
||
def sub_mod(self, mod): | ||
self.main_widget.data_changed(self.data, mod) | ||
if not self.filter_mod(mod): | ||
return | ||
|
||
if self.args.update_delay: | ||
if hasattr(self, "mod_buffer"): | ||
self.mod_buffer.append(mod) | ||
else: | ||
self.mod_buffer = [mod] | ||
asyncio.get_event_loop().call_later(self.args.update_delay, | ||
self.flush_mod_buffer) | ||
else: | ||
self.main_widget.data_changed(self.data, [mod]) | ||
|
||
def subscribe(self): | ||
if self.args.embed is None: | ||
self.subscriber = Subscriber("datasets", | ||
self.sub_init, self.sub_mod) | ||
self.loop.run_until_complete(self.subscriber.connect( | ||
self.args.server, self.args.port)) | ||
else: | ||
self.ipc.subscribe(self.datasets, self.sub_init, self.sub_mod) | ||
|
||
def create_subscriber(self): | ||
self.subscriber = Subscriber("datasets", | ||
self.sub_init, self.sub_mod) | ||
self.loop.run_until_complete(self.subscriber.connect( | ||
self.args.server, self.args.port)) | ||
def unsubscribe(self): | ||
if self.args.embed is None: | ||
self.loop.run_until_complete(self.subscriber.close()) | ||
|
||
def run(self): | ||
self.args_init() | ||
self.quamash_init() | ||
try: | ||
self.create_main_widget() | ||
self.create_subscriber() | ||
self.ipc_init() | ||
try: | ||
self.loop.run_forever() | ||
self.create_main_widget() | ||
self.subscribe() | ||
try: | ||
self.loop.run_forever() | ||
finally: | ||
self.unsubscribe() | ||
finally: | ||
self.loop.run_until_complete(self.subscriber.close()) | ||
self.ipc_close() | ||
finally: | ||
self.loop.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters