Ava
Ava

Reputation: 1

Why does my PyQt5 and Vispy application only update the GUI when adding sleep in QThread's loop?

Basically I am trying to implement a PyQt5 application for real-time data visualization using the Vispy library. In the code below, I am trying to plot a sinewave with 10000 samples per second for 100 seconds. I run the data creation/update on a QThread and the graph's GUI update on the main thread. However, running the code causes the GUI to freeze up and not update within the specified intervals.

from PyQt5.QtGui import QCloseEvent
from vispy.app import use_app, Timer
import numpy as np
from PyQt5 import QtWidgets, QtCore
from vispy import scene, visuals
import time
from scipy import signal
from collections import deque
import cProfile
import pstats


class MainWindow(QtWidgets.QMainWindow):
    closing = QtCore.pyqtSignal()
    def __init__(self, canvas_wrapper, *args, **kwargs):

    
        super(MainWindow, self).__init__(*args, **kwargs)
        
        central_widget = QtWidgets.QWidget()
        main_layout = QtWidgets.QHBoxLayout()

        
        self.canvas_wrapper = canvas_wrapper

        main_layout.addWidget(self.canvas_wrapper.canvas.native)

        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)
    
    def closeEvent(self, event):
        print("Closing main window")
        self.closing.emit()
        return super().closeEvent(event)


class CanvasWrapper:
    def __init__(self, update_interval = .016): 

        self.canvas = scene.SceneCanvas(keys='interactive', size=(600, 600), show=True)
        self.grid = self.canvas.central_widget.add_grid()

        title = scene.Label("Test Plot", color='white')
        title.height_max = 40

        self.grid.add_widget(title, row=0, col=0, col_span=2)

        self.yaxis = scene.AxisWidget(orientation='left',
                         axis_label='Y Axis',
                         axis_font_size=12,
                         axis_label_margin=50,
                         tick_label_margin=10)
        
        self.yaxis.width_max = 80
        self.grid.add_widget(self.yaxis, row=1, col=0)

        self.xaxis = scene.AxisWidget(orientation='bottom',
                         axis_label='X Axis',
                         axis_font_size=12,
                         axis_label_margin=50,
                         tick_label_margin=20)

        self.xaxis.height_max = 80
        self.grid.add_widget(self.xaxis, row=2, col=1)
        
        right_padding = self.grid.add_widget(row=1, col=2, row_span=1)
        right_padding.width_max = 50

        self.view = self.grid.add_view(row=1, col=1, border_color='white')
        self.view.camera = "panzoom"
        
        self.data = np.empty((2, 2))
        self.line = scene.Line(self.data, parent=self.view.scene)


        self.xaxis.link_view(self.view)
        self.yaxis.link_view(self.view)

        self.update_interval = update_interval
        self.last_update_time = time.time()


    def update_data(self, newData):
        if self.should_update():

            data_array = newData["data"]
            data_array = np.array(data_array)

            x_min, x_max = data_array[:, 0].min(), data_array[:, 0].max()
            y_min, y_max = data_array[:, 1].min(), data_array[:, 1].max()
            
           
            self.view.camera.set_range(x=(x_min, x_max), y=(y_min, y_max))
            self.line.set_data(data_array)

            
    
    def should_update(self):
        current_time = time.time()
        if current_time - self.last_update_time >= self.update_interval:
            self.last_update_time = current_time
            return True
        return False


class DataSource(QtCore.QObject):
    new_data = QtCore.pyqtSignal(dict)
    finished = QtCore.pyqtSignal()

    def __init__(self, sample_rate, seconds, seconds_to_display=15, q = 100, parent = None):
        super().__init__(parent)
        self.count = 0
        self.q = q
        self.should_end = False
        self.sample_rate = sample_rate

        self.num_samples = seconds*sample_rate
        self.seconds_to_display = seconds_to_display

        size = self.seconds_to_display*self.sample_rate

        self.buffer = deque(maxlen=size)
        


    def run_data_creation(self):
        print("Run Data Creation is starting")
        for count in range (self.num_samples):
            if self.should_end:
                print("Data saw it was told to end")
                break
        
            self.update(self.count)
            self.count += 1

            data_dict = {
                "data": self.buffer,
            }
            self.new_data.emit(data_dict)

        print("Data source finished")
        self.finished.emit()
    
    def stop_data(self):
        print("Data source is quitting...")
        self.should_end = True

    def update(self, count):

        x_value = count / self.sample_rate
        y_value = np.sin((count / self.sample_rate) * np.pi)

        self.buffer.append([x_value, y_value])
        

class Main:
    def __init__(self, sample_rate, seconds, seconds_to_display):
        # Set up the application to use PyQt5
        self.app = use_app("pyqt5")
        self.app.create()

        # Set some parameters for the data source
        self.sample_rate, self.seconds, self.seconds_to_display = sample_rate, seconds, seconds_to_display

        # Create the canvas wrapper and main window
        self.canvas_wrapper = CanvasWrapper()
        self.win = MainWindow(self.canvas_wrapper)

        # Set up threading for the data source
        self.data_thread = QtCore.QThread(parent=self.win)
        self.data_source = DataSource(self.sample_rate, self.seconds)

        # Move the data source to the thread
        self.data_source.moveToThread(self.data_thread)

        # Connect signals and slots for data updates and thread management
        self.setup_connections()

    def setup_connections(self):
        self.data_source.new_data.connect(self.canvas_wrapper.update_data)
        self.data_thread.started.connect(self.data_source.run_data_creation)
        self.data_source.finished.connect(self.data_thread.quit, QtCore.Qt.DirectConnection)
        self.win.closing.connect(self.data_source.stop_data, QtCore.Qt.DirectConnection)
        self.data_thread.finished.connect(self.data_source.deleteLater)

    def run(self):
        self.win.show()
        self.data_thread.start()
        
        self.app.run()
        self.data_thread.quit()
        self.data_thread.wait(5000)


def profile_run():
    visualization_app = Main(10000, 100, 10)
    visualization_app.run()

if __name__ == "__main__":
    cProfile.run('profile_run()', 'profile_out')
    stats = pstats.Stats('profile_out')

    stats.sort_stats('cumulative')

    stats.print_stats(10)

    stats.sort_stats('time').print_stats(10)

Only when I add a sleep in run_data_creation does the GUI update (although really slowly even with a small sleep). Why does QThread do this? How do I fix it?

    def run_data_creation(self):
        print("Run Data Creation is starting")
        for count in range (self.num_samples):
            if self.should_end:
                print("Data saw it was told to end")
                break
        
            self.update(self.count)
            self.count += 1
            time.sleep(.0000001)

            data_dict = {
                "data": self.buffer,
            }
            self.new_data.emit(data_dict)

        print("Data source finished")
        self.finished.emit()
    

Upvotes: 0

Views: 116

Answers (1)

djhoese
djhoese

Reputation: 3667

Answer 2

If you comment out the two vispy lines in update_data and optionally add some prints, you'll notice that the GUI still acts the same. That is it freezes. So this has nothing to do with vispy.

As a general rule for realtime apps, any non-visualization operations should be in the separate thread (the data source). This includes your update interval logic. Put another way, only send data when the visualization should be updated.

In terms of the Qt event loop, you're basically dumping thousands of events onto the queue of tasks Qt has to process. Qt can't process any new UI events until it finishes handling of your data events. This basically ends up being a for loop in python over thousands of events.

The solution, at least to start, is to handle all your update interval stuff inside the data source thread. Only emit the signal when the data is ready to be displayed. Plus, 10000 points per second is a lot of data to view in realtime if that's your goal. You may need to work on averaging or subsetting the data to reduce how often you send updated data.

Answer 1

It looks like this is based on the vispy realtime data examples, right? When I wrote these I don't remember how much testing I did with a non-sleeping data source so there is always a chance that this code does not behave as I expect.

You mention this isn't updating in the "specified intervals", what do you mean by that? It could be that you are flooding the GUI with so many update events that it isn't able to update before you see the final result. Without your sleep (correct me if I'm wrong) you're basically going from the first iteration to the last iteration as fast as the CPU can go, right? In this case, what would you expect to see? In a more realistic example the data source creating the data would take some actual time, but your example creates all the data instantly. Are you sure the application is hanging or is it just done producing all the data?

If I'm wrong about all the above, then one difference I see is your use of deque which I have very little experience with. I'm wondering if you see any difference in behavior if you instead make a new numpy array inside your data source for every iteration.

Upvotes: 0

Related Questions