Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

ARTIQ Applets

Akshay Sawhney edited this page Jul 31, 2023 · 10 revisions

For writing applets, the best examples are dds (each channel is the same, repeated for a few times), and pmt (a GUI that doesn't have repeated elements). They represent two typical categories of applets.

Structure of an ARTIQ applet

This section describes how to write a general ARTIQ applet that connects to a LabRAD server example_server. Some familiarity with LabRAD servers are expected before proceeding to read the section. The example_server has a get_value() setting to get a value, and a set_value() setting to set a value. When the value is changed by another LabRAD client, the server sends out a value_changed signal. We want to write an applet to show a QDoubleSpinBox which can show and change this value.

An ARTIQ applet should inherit on both QWidget and jax.JaxApplet. QWidget enables building an applet GUI, and JaxApplet contains functions to make LabRAD connections. The module also needs to import artiq.applets.simple.SimpleApplet, which is a wrapper on the applet to connect to ARTIQ, process command line arguments, and enable embedding in a dashboard.

from PyQt5 import QtWidgets, QtCore, QtGui
from artiq.applets.simple import SimpleApplet
from jax import JaxApplet


class ExampleApplet(QtWidgets.QWidget, JaxApplet):
    """Connects to a `example_server` and controls an example device."""

The __init__ method takes in an extra positional argument args that correspond to command line arguments supplied by the user when running the applet. A typical command line argument is --ip which can be used to define the LabRAD manager IP address to connect to. More details about command line arguments will be introduced later. The other keyword arguments are sent to the parent classes using super().__init__.

After __init__ method of the parent classes are called, we build the GUI, which is typically disabled until the server is connected and the initial values are populated correctly. We also make a connection to LabRAD.

    def __init__(self, args, **kwds):
        super().__init__(**kwds)
        self.setDisabled(True)  # disable the whole GUI
        self.init_ui()
        self.setup_gui_listeners()
        self.connect_to_labrad(args.ip)  # connects to LabRAD, defined in JaxApplet

    def init_ui(self):
        layout = QtWidgets.QGridLayout()
        label = "Value:"
        layout.addWidget(label, 0, 0)
        self.spinbox = QtWidgets.QDoubleSpinBox()
        layout.addWidget(self.spinbox, 1, 0)
        self.setLayout(layout)

    def setup_gui_listeners(self):
        self.spinbox.valueChanged.connect(self.spinbox_value_changed)  # will be defined later

connect_to_labrad is a function defined in the JaxApplet class that connects to LabRAD to the IP address specified in the command line argument (default is the local computer) in another thread. We cannot connect to LabRAD in the main thread because twisted (which LabRAD requires) cannot work in the same event loop as qasync (which PyQt5 with asyncio requires).

Because connect_to_labrad runs in a different thread, self.connect_to_labrad is not a blocking function (it will not wait for LabRAD to connect). If you add any code immediately after connect_to_labrad in the __init__ function, there is no guarantee that the LabRAD connection will be made before the following code is ran. Instead, at the end of connect_to_labrad, it will call the labrad_connected method, which we need to reimplement in this class. In labrad_connected, we connect to the server, populates the initial values, and listens to signals. Note the async and await keywords in the code below.

    async def labrad_connected(self):
        """self.cxn is defined in connect_to_labrad."""
        await self.connect_server()
        self.setup_cxn_listeners()  # will be defined later

    async def server_connected(self):
        """Connects to the server and populates the value in the GUI."""
        self.server = self.cxn.get_server("example_server")
        value = await self.server.get_value()
        self.set_spinbox_value(value)
        self.setDisabled(False)  # the GUI can be enabled after the initialization.
        await self.connect_to_signals()  # connects to signals to update the GUI.

    def set_spinbox_value(self, value):
        self.spinbox.blockSignal(True)  # avoids triggering the valueChanged event.
        self.spinbox.setValue(value)
        self.spinbox.blockSignal(False)

    async def connect_to_signals(self):
        # random integer. If the same client listens to two signals, they must use different IDs.
        SIGNALID = 129484
        await self.server.value_changed(SIGNALID)
        self.server.addListener(listener=self.server_value_changed,  # will be defined later
                                source=None, ID=SIGNALID)

We also set up listeners for server connection and disconnection. These listeners enable us to disable the GUI when the server disconnects, and enable the GUI and initializes the values again when the server connects. They are not necessary, but they make the GUI more robust against server restarting.

    def setup_cxn_listeners(self):
        """Listens to when the server is connected or disconnected."""
        # To run any async function using LabRAD from a sync function,
        # we need to wrap the function with `self.run_in_labrad_loop`, which is defined in JaxApplet.
        self.cxn.add_on_connect("example_server", self.run_in_labrad_loop(self.server_connected))
        # since `server_disconnected` is a sync function, we don't need to wrap it.
        self.cxn.add_on_disconnect("example_server", self.server_disconnected)

    def server_disconnected(self):
        self.setDisabled(True)  # disable the applet when the server is disconnected.

We write functions to handle when the value is changed in the GUI, or when the value is changed in the server:

    def spinbox_value_changed(self, value):
        # we need to handle LabRAD (async) in this sync function.
        # so we need to write an async worker, and wrap it with `self.run_in_labrad_loop`
        async def worker():
            await self.server.set_value(value)

        self.run_in_labrad_loop(worker)()

    def server_value_changed(self, signal, value):
        self.set_spinbox_value(value)

Finally, we write instructions of how to run this applet

def main():
    """Handles `python -m __module_path__` calls."""
    applet = SimpleApplet(ExampleApplet)
    # adds IP address as an argument.
    # If your client uses a config to get the IP address, the following line should be removed.
    ExampleApplet.add_labrad_ip_argument(applet)
    applet.run()


if __name__ == "__main__":
    """Handles `python __file_path__` calls."""
    main()

Full code

The entire file is attached. I did not spend time testing it, so there might be bugs. The structure should be correct though.

from PyQt5 import QtWidgets, QtCore, QtGui
from artiq.applets.simple import SimpleApplet
from jax import JaxApplet


class ExampleApplet(QtWidgets.QWidget, JaxApplet):
    """Connects to a `example_server` and controls an example device."""

    def __init__(self, args, **kwds):
        super().__init__(**kwds)
        self.setDisabled(True)  # disable the whole GUI
        self.init_ui()
        self.setup_gui_listeners()
        self.connect_to_labrad(args.ip)  # connects to LabRAD, defined in JaxApplet

    def init_ui(self):
        layout = QtWidgets.QGridLayout()
        label = "Value:"
        layout.addWidget(label, 0, 0)
        self.spinbox = QtWidgets.QDoubleSpinBox()
        layout.addWidget(self.spinbox, 1, 0)
        self.setLayout(layout)

    def setup_gui_listeners(self):
        self.spinbox.valueChanged.connect(self.spinbox_value_changed)  # will be defined later

    async def labrad_connected(self):
        """self.cxn is defined in connect_to_labrad."""
        await self.connect_server()
        self.setup_cxn_listeners()  # will be defined later

    async def server_connected(self):
        """Connects to the server and populates the value in the GUI."""
        self.server = self.cxn.get_server("example_server")
        value = await self.server.get_value()
        self.set_spinbox_value(value)
        self.setDisabled(False)  # the GUI can be enabled after the initialization.
        await self.connect_to_signals()  # connects to signals to update the GUI.

    def set_spinbox_value(self, value):
        self.spinbox.blockSignal(True)  # avoids triggering the valueChanged event.
        self.spinbox.setValue(value)
        self.spinbox.blockSignal(False)

    async def connect_to_signals(self):
        # random integer. If the same client listens to two signals, they must use different IDs.
        SIGNALID = 129484
        await self.server.value_changed(SIGNALID)
        self.server.addListener(listener=self.server_value_changed,  # will be defined later
                                source=None, ID=SIGNALID)

    def setup_cxn_listeners(self):
        """Listens to when the server is connected or disconnected."""
        # To run any async function using LabRAD from a sync function,
        # we need to wrap the function with `self.run_in_labrad_loop`, which is defined in JaxApplet.
        self.cxn.add_on_connect("example_server", self.run_in_labrad_loop(self.server_connected))
        # since `server_disconnected` is a sync function, we don't need to wrap it.
        self.cxn.add_on_disconnect("example_server", self.server_disconnected)

    def server_disconnected(self):
        self.setDisabled(True)  # disable the applet when the server is disconnected.

    def spinbox_value_changed(self, value):
        # we need to handle LabRAD (async) in this sync function.
        # so we need to write an async worker, and wrap it with `self.run_in_labrad_loop`
        async def worker():
            await self.server.set_value(value)

        self.run_in_labrad_loop(worker)()

    def server_value_changed(self, signal, value):
        self.set_spinbox_value(value)

def main():
    """Handles `python -m __module_path__` calls."""
    applet = SimpleApplet(ExampleApplet)
    # adds IP address as an argument.
    # If your client uses a config to get the IP address, the following line should be removed.
    ExampleApplet.add_labrad_ip_argument(applet)
    applet.run()


if __name__ == "__main__":
    """Handles `python __file_path__` calls."""
    main()

To run this file (saved as $PYTHONPATH/repo/applets/example_applet.py), either run

python -m repo.applets.example_applets

Or run

python __PYTHONPATH__/repo/applets/example_applet.py

The first command is recommended as the first command is shorter if you include the full file path in the second one. They will try to connect to the example_server on the local computer. To connect to another computer, you need to add --ip __ip_address__ after the command, e.g.

python -m repo.applets.example_applets 192.168.1.101

Notes

### LabRAD imports

We cannot import labrad or any component of it (e.g. labrad.units.WithUnit) before connect_to_labrad finishes. They can be imported in the labrad_connected function if needed. You can assign an instance variable to it to keep a reference to the imported module.

More command line arguments

You can add more command line arguments if you want to. See https://github.com/m-labs/artiq/blob/master/artiq/applets/big_number.py#L25-L26 for an example.

WinError when running the command

WinError is typically due to failure to connect to ARTIQ master. If ARTIQ master is not running on the local computer, you will see this error. To avoid this, you need to add a --server command line argument pointing to the computer that ARTIQ master is running on.

QPainter errors and GUI crashes after resizing the window

These are due to threading errors. For a simple GUI this should not be an issue. See the wavemeter applets in pydux if this is an issue for you.