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.
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