Using PyQt with QtAgg in Jupyterlab – IV – simple PyQt and MPL application with background worker and receiver threads
As you read this post you are probably interested in Machine Learning [ML] and hopefully in Linux systems as a ML-platform as well. This post series wants to guide you over a bridge between the standard tool-set of Python3 notebooks in Jupyterlab for the control of ML-algorithms and graphical Qt-applications on your Linux desktop. The objective is to become more independent of some limitations of the browser based Jupyterlab notebooks.
One aspect is the use of graphical Qt-based control elements (as e.g. buttons, etc.) in desktop windows. On the other hand we want to use background threads to produce (ML) data which we later, e.g. during training runs, display in Qt windows. Background threads will also enable us to run smaller code in other cells of our notebook during long ML-runs. We are also confident that we can keep up the interactivity of both our Qt windows and Jupyterlab during such runs.
We will later use the callback machinery of Keras based ML-runs to produce ML-data and other information about a running ML-algorithm in the background of Jupyterlab. These data will be sent to Matplotlib- and Qt callback-functions in Jupyterlab which then update Qt windows.
Knowledge gained so far …
During the previous posts we have gathered enough information to now build an example PyQt application, which utilizes two background threads.
- Using PyQt with QtAgg in Jupyterlab – I – a first simple example
- Using PyQt with QtAgg in Jupyterlab – II – excursion on threads, signals and events
- Using PyQt with QtAgg in Jupyterlab – III – a simple pattern for background threads
We have seen that QtAgg, a backend bridge for producing Matplotlib [MPL] plots in Qt windows, can be used for full fledged PyQt applications, too. In the first post we became familiar with some useful Qt-widgets and the general structure of Qt-Apps.
In the 2nd and 3rd posts we have learned that both Matplotlib figures and Qt-widgets must be controlled by the main thread associated with our Jupyterlab notebook. A Qt event loop is started in this thread by QtAgg for us. We have also noted that background threads controlled by QThread-objects can send signals which end up serialized in the Qt event queue of the main thread. From there they can be handled asynchronously, but in timely order by callbacks, which in turn update Qt-widgets for MPL-plots and other information. The 3rd post discussed a general pattern to employ both a raw data producing worker thread and a receiver thread to prepare the data for eventual foreground handling.
Objective of this post
In this post I will discuss a simple application that produces data with the help of two background threads according to the pattern discussed in the previous post. All data and information will periodically be sent from the background to callbacks in the main thread. Although we only use one main Qt window the structure of the application includes all key elements to serve as a blueprint for more complex situations. We will in particular discuss how to stop the background jobs and their threads in a regular way. An interesting side topic will be how one captures print output to stdout from background jobs.
Level of this post: Advanced. Some experience with Jupyterlab, QtAgg, Matplotlib and (asynchronous) PyQt is required. The first three posts of this series provide (in my opinion) a quick, though steep learning curve for PyQt newbies.
Our PyQt application will contain three major elements in a vertical layout:
- Two buttons to start and stop two background threads. These threads provide data for a sine-curve with steadily growing frequency and some related information text.
- A Qt-widget for a Matplotlib figure to display the changing sine curve.
- A series of QTextEdit widgets to display messages from the background and from callbacks in the foreground.
Our pattern requires the following threads: A “worker thread” periodically creates raw data and puts them into Python queues. A “receiver thread” reads out the queues and refines the data.
In our case the receiver thread will add additional information and data. Then signals are used to communicate with callbacks in the main thread. We send all data for widget and figure updates directly with the signals. This is done for demonstration purposes. We could also have used supplemental data queues for the purpose of inter-thread data exchange. For plotting we use Matplotlib and the related Figure.canvas-widget provided by QtAgg.
So, we have a main thread with a Qt event loop (and of course a loop for Jupyterlab REPL interaction) and two background threads which perform some (simple) asynchronous data production for us.
Our challenge: Qt and Matplotlib control with Python code in a Jupyterlab notebook
- setup and start of two background threads – a worker thread and a receiver thread,
- association of worker and receiver objects to the named threads with a respective affinity,
- asynchronous inter-thread communication and data exchange via signals,
- updates of Qt-widgets and integrated Matplotlib figures,
- spinning the Qt-event-loop in the main thread to ensure quick widget updates,
- a regular stop of thread activities and a removal of thread-related objects,
- checking interactivity of both the Jupyterlab and the Qt-interface,
- stability of the plot-production against potentially conflicting commands from the main thread.
All via code executed in cells of a Python notebook. An additional topic is:
- capturing print-commands in the background and transmission of the text to the foreground.