PyQt5 Crashes While Plotting with PyQtGraph in Real Time

Viewed 44

I'm currently using a LabView DAQ program, but I need some functionality that LabView doesn't offer. So I created a program using Python that reads inputs from the NIDAQ, converts them, plots to the screen, and stores the values in a local database. To optimize the system, I use a thread to read and process the pressure card, a thread to read the thermocouple card, and a thread to pull the channels I need and combine them into a Numpy array to pass to the main GUI thread. Since the different channels plot to different axes, I loop through the array and plot them to their individual axis.

I wrote a DAQ simulator that generates semi-random values so I don't have to carry the hardware around. I think I have the bare minimum code to simulate the problem, still using two of the worker threads.

I've tried multiple ways to plot the data, and this is the best I've been able to get; about 5 minutes and the GUI becomes unresponsive, and occasionally a full crash. I've profiled the program without any useful results. Or, I just don't know how to interpret the results.

Python Memory Profiler Plot

import sys
import time
import numpy as np
import pyqtgraph as pg
from PyQt5 import QtCore
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QWidget, QMainWindow, QTreeWidget, QTreeWidgetItem, QFrame,  QTabWidget, QApplication

pg.mkQApp()
pg.setConfigOption('useOpenGL', True)

webster = {0: {'name': 'Tubing'}, 
           1: {'name': 'Annulus'}}
output_array = np.full((20, 16), -1.0)

output_array[0, 0] = 0
output_array[0, 1] = 0
output_array[0, 6] = 0
output_array[0, 7] = 0
output_array[0, 8] = 0
output_array[0, 13] = 0

output_array[1, 0] = 1
output_array[1, 1] = 0
output_array[1, 6] = 0
output_array[1, 7] = 0
output_array[1, 8] = 1
output_array[1, 13] = 0

p_buffer = np.zeros((1, 1), dtype=np.float64)
p_cal = np.array([[-1.000001, -1.000001, -1.000001, -1.000001, -1.000001], [5, 5, 5, 5, 5], [50, 50, 50, 50, 50]], dtype=float)

p_fluctuation = 0.004

run_thread = False
recording = True
daq_time = time.time()

d_symbol = '\u00b0'
settings = ('', 0, 2, 0, 20000, 0, 0, 2, 0, 400, 0, 0, 2, 0, 100000, -100000)
color_list = [(0, 0, 0), (255, 0, 0), (0, 0, 255), (0, 255, 0), (0, 255, 255), (255, 0, 255), (255, 100, 0), (255, 0, 150), (138, 29, 29), (17, 12, 168)]

# Pressure Inputs DAQ Thread
class PressureThread(QtCore.QThread):
    p_array = pyqtSignal(np.ndarray)

    def __init__(self, parent=None):
        QtCore.QThread.__init__(self, parent)

    def run(self):
        global run_thread, p_buffer, p_cal, p_fluctuation, recording

        while run_thread:
            if recording:
                p_buffer = np.random.uniform(low=0.995, high=1.01, size=(5, 10))
                # Average buffer
                p_mean_array = np.mean(p_buffer, axis=1)
                # Add x to negative x1
                add_array = np.add(p_mean_array, p_cal[0:1])
                # Multiply by y2 divided by x2 minus x1
                mult_array = np.multiply(add_array, 4)
                inter_array = np.add(p_cal[0:1], p_cal[1:2])
                comp_array = np.divide(mult_array, inter_array)
                # Create and combine arrays of metric and imperial 0-10k and 0-5k transducers
                twenty_k_psi_array = comp_array * 20000
                twenty_k_mpa_array = comp_array * 20000 * 6.89476
                five_k_psi_array = comp_array * 5000
                five_k_mpa_array = comp_array * 5000 * 6.89476
                p_full_array = np.stack((twenty_k_psi_array, twenty_k_mpa_array, five_k_psi_array, five_k_mpa_array))
                self.p_array.emit(p_full_array)
                time.sleep(0.1)
            else:
                pass

    def stop(self):
        self.exit()


# Input Calculation Thread
class SlideRuleThread(QtCore.QThread):

    tree_out = pyqtSignal(str)
    arrays = pyqtSlot(np.ndarray, name='arrays')

    def __init__(self, parent=None):
        QtCore.QThread.__init__(self, parent)

        global run_thread, recording, daq_time, webster, output_array
        run_thread = True

        try:
            self.thread1 = PressureThread()
            self.thread1.start()
            self.thread1.p_array.connect(self.worker)

        except Exception as e:
            print(e)

    def run(self):

        print('Running')

    def worker(self, arrays):
        global recording, daq_time, webster, output_array

        shape = arrays.shape
        current_time = time.time()
        elapsed_time = round(float(current_time - daq_time), 4)
    
        for row in output_array:
            ref_row = row[0]
            if ref_row != -1:
                # Values shift left
                row[2] = row[4]
                row[3] = row[5]
                row[4] = elapsed_time  # New value to x2

            if shape == (4, 1, 5):
                if row[1] == 0:  # Pressure array
                    if row[6] == 0:  # Pressure
                        value = arrays[int(row[7]), 0, int(row[8])]  # New y value
                        row[5] = round(value, 2)  # New value to y2
                        
                    elif row[6] == 2:  # Pressure load
                        value = (arrays[int(row[7]), 0, int(row[8])] * row[9]) + (arrays[int(row[10]), 0, int(row[11])] * row[12])  # New calculation
                        row[5] = round(value, 2)  # New value to y2
                        
        sign = ''
        self.tree_out.emit(sign)

    def stop(self):
        self.thread1.exit()
        self.exit()


# Plot Element Builder
class MultiLine(pg.QtWidgets.QGraphicsPathItem):
    def __init__(self, x, y, p):
        global color_list
        """x and y are 2D arrays of shape (Nplots, Nsamples)"""
    
        x_array = np.array(x)
        y_array = np.array(y)
    
        self.path = pg.arrayToQPath(x_array.flatten(), y_array.flatten())  # self.path = pg.arrayToQPath(x.flatten(), y.flatten(), connect.flatten())
        pg.QtWidgets.QGraphicsPathItem.__init__(self, self.path)
        self.setPen(pg.mkPen(color_list[p], width=1))
    def shape(self): # override because QGraphicsPathItem.shape is too expensive.
        return pg.QtWidgets.QGraphicsItem.shape(self)
    def boundingRect(self):
        return self.path.boundingRect()


# Main GUI Window
class MyWindow(QMainWindow):

    # Initial Inputs and Variables
    def __init__(self):
        super(MyWindow, self).__init__()
        self.setGeometry(0, 0, 1920, 1080)
        self.initUI()
        self.showMaximized()
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

        global run_thread, recording, daq_online

        self.graph = False

        self.right_left_axis = pg.AxisItem('right')
        self.right_right_axis = pg.AxisItem('right')
        self.right_left_vb = pg.ViewBox()
        self.right_right_vb = pg.ViewBox()
        self.main_plot_view = pg.GraphicsView()
        self.main_plot_view_layout = pg.GraphicsLayout()

        # GUI Items
    def initUI(self):

        # TOP LEFT FRAME
        fra_data = QFrame(self)
        fra_data.setGeometry(QtCore.QRect(20, 30, 350, 210))
        fra_data.setFrameShape(QFrame.Panel)
        fra_data.setFrameShadow(QFrame.Raised)
        fra_data.lineWidth()
        fra_data.midLineWidth()
        fra_data.setLineWidth(5)

        self.list_cha = QTreeWidget(fra_data)
        self.list_cha.setColumnCount(3)
        self.list_cha.setHeaderLabels(['Channel', 'Data', ''])
        self.list_cha.setGeometry(10, 10, 330, 190)
        self.list_cha.setColumnWidth(0, 180)
        self.list_cha.setColumnWidth(1, 100)
        self.list_cha.setColumnWidth(2, 40)
        self.list_cha.addTopLevelItem(QTreeWidgetItem(0))
        self.list_cha.topLevelItem(0).setText(0, 'Pressure1')
        self.list_cha.topLevelItem(0).setText(1, 'Stopped')
        self.list_cha.topLevelItem(0).setText(2, 'psi')
        self.list_cha.addTopLevelItem(QTreeWidgetItem(1))
        self.list_cha.topLevelItem(1).setText(0, 'Pressure2')
        self.list_cha.topLevelItem(1).setText(1, 'Stopped')
        self.list_cha.topLevelItem(1).setText(2, 'psi')

        # Tabs
        fra_tabs = QFrame(self)
        fra_tabs.setGeometry(QtCore.QRect(400, 30, 1500, 950))
        fra_tabs.lineWidth()
        fra_tabs.midLineWidth()
        fra_tabs.setLineWidth(5)

        tabs = QTabWidget(fra_tabs)

        daq_tab = QWidget()

        tabs.resize(1500, 950)
        tabs.addTab(daq_tab, 'DAQ')

        # DAQ Tab
        self.fra_plot = QFrame(daq_tab)
        self.fra_plot.setFrameShape(QFrame.StyledPanel)
        self.fra_plot.setGeometry(10, 40, 1480, 860)

        self.mpW = pg.PlotWidget(self.fra_plot, axisItems={'bottom': pg.DateAxisItem(utcOffset=0)})
    
        self.mpW.move(5, 5)
        self.mpW.setBackground('w')
        self.mpW.resize(1460, 850)

        self.main_axis = self.mpW.plotItem

        self.graph = True

        # Plot Settings
        self.main_axis.showGrid(x=True, y=True, alpha=1)
        self.main_axis.setLabel('bottom', '(hh:mm:ss)')
        self.main_axis.setTitle('Just DAQ Profiler', color='b', size='20pt')

        # Axis Labels and Units
        self.main_axis.getAxis('left').setLabel('Pressure (psi)', color='#0000ff')

        # Axis Ranges
        self.main_axis.vb.setYRange(0, 100, padding=0)

        # Adjustable Horizontal Reference Lines
        self.main_axis.vb.addItem(pg.InfiniteLine(pos=100 - ((100 - 0) * 0.1), angle=0, pen=(0, 255, 0),
                                                movable=True))
        self.main_axis.vb.addItem(pg.InfiniteLine(pos=0 + ((100 - 0) * 0.1), angle=0, pen=(0, 255, 0),
                                                movable=True))

        self.mpW.show()

        self.thread3 = SlideRuleThread()
        self.thread3.start()
        self.thread3.tree_out.connect(self.treeUpdater)

    # Updates Values in Channel List Tree
    def treeUpdater(self, sign):
        
        global output_array

        for row in output_array:
            tree_row = int(row[0])
            if tree_row != -1:
                value = str(row[5])
                self.list_cha.topLevelItem(tree_row).setText(1, value)
            
                time_one = float(row[4])
                time_two = float(row[2])
                list_time = [time_two, time_one]
                value_one = float(row[5])
                value_two = float(row[3])
                list_values = [value_two, value_one]
            
                lines = MultiLine(list_time, list_values, tree_row)
                self.main_axis.addItem(lines)


def main():

    app = QApplication(sys.argv)
    win = MyWindow()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

The last time I commented out "self.main_axis.addItem(lines)", the code ran for 5 hours and was still perfectly responsive. But, if I uncomment it, it runs for about 4 to 5 minutes before it slowly dies and then crashes. Which is weird, because the PyQtGraph examples I've tried plotted 100,000 data points in 0.01 seconds, and in 5 minutes with 2 channels updating at 10 Hz, this is only 6,000 data points.

I'm new to Python, this is my first program, so I'm sure there's lots of things I'm doing incorrectly, or could be improved. But, through all my searching and program tweaking over the last couple months, I can't find any other examples that are similar or explain what is going on. I'm hoping someone here can point me in the right direction. Thanks.

System: Windows 10 Pro 64bit, 16 GB RAM, Intel Core i7 @ 2.30GHz Visual Studio Code: 1.71.0 Python: 3.10.6 PyQtGraph: 0.12.4 Numpy: 1.23.2

0 Answers
Related