I'm currently working on a real time display system using 
pyqtgraph/anaconda3 and I'm having trouble optimizing the code to not clog 
the cpu. I'm currently plotting 600 points at all times * 2 sets of data 
per plot * 21 plots of data which is roughly 25000 points at all times. The 
code struggles to handle that many points (it crashes at 500s due to cpu 
maxing out at 100% running this code alone) and I'm willing to cut it to 
300 points of data per dataset (5 minutes of real time data which is barely 
meeting our purpose of analyzing real time data), but even plotting 300 
points clogs up ~60% of our cpu on an industrial computer which is not 
optimal since we're running other software at the same time (this code if 
possible can only take up ~20% of our cpu). This code reads data from 
another program that's processing the data, but for testing purposes a 
dummy file is being uses to simulate it.

 So I'm hoping that instead of redrawing the whole plot from scratch, 
there's an alternative to 'append' new points to an already drawn graph and 
only have to redraw the plot every 10 secs instead of every second (which 
is how often I receive new data). Y axis - data, X axis - time(seconds).

 If that's confusing I'm hoping for
0 - > 300 points hopefully being appended to avoid cpu clogging

when 300 points is plotted -> clear() -> plot the last 290 points and 
append the next 10 values

repeat until program ends. 

Any advice / alternatives will be much appreciated. I've attached my code 
for reference.

-- 
You received this message because you are subscribed to the Google Groups 
"pyqtgraph" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/pyqtgraph/027fac66-26ce-4e70-a198-f3cec1be25dc%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
from pyqtgraph.Qt import QtGui, QtCore
import itertools as it
import sys
import re
import glob
import os
import pyqtgraph as pg
import numpy as np
import time 
import pandas
import subprocess
import argparse
from PyQt4 import QtGui


'''
Widgets Buttons Documentation Begins 
-------------------------------------------------------------------------------------------

Submit Function:

Precondition: Display the zooming option widget as a form 
text should be 'Baseline' and text2 should be 'Time relative to Now'

Postcondition: The user should select the baseline and time in minutes
and press the submit button. The arguments will be checked for errors and passed into the
main program and then invoke to, in this example, /home/corr/Desktop/RealTimePlotting/singlePlot.py
to display a singular graph of the selected baseline relative to the starting time.
'''

def submit():
    global text, text2
    alist = []
    baseline = text.text()
    print("baseline is ", baseline)
    position = 0
    time_start = int(text2.text())
    time_end = int(text3.text())
    found = "False"
    lower = "False"
    if adict_upper[baseline]:
        print(baseline, " Found as file ", adict_upper[baseline])
        file2 = adict_upper[baseline]
        position = list(adict_upper.keys()).index(baseline)
        alist.append(file2)
        found = "True"
    if adict_lower[baseline]:
        print(baseline, " Found as file ", adict_lower[baseline])
        file = adict_lower[baseline]
        alist.append(file)
        found = "True"
        lower = "True"
    if found == "False":
        print("Baseline ", baseline, " Does Not Exist.")
        return
    if time_start < 0:
        print("Time Error: Not Possible To Have A Negative Time.")
        return
    if time_end > file_len(file2):
        print("Time Error: We Have Not Recorded For More Than ", time_end, " Seconds.")
        return
    if switch == "True":
        alist.append("AMP")
    else:
        alist.append("PH")
    alist.append(text2.text())
    sf = str(skipfirst)
    sl = str(skiplast)
    alist.append(sf)
    alist.append(sl)
    alist.append(text3.text())
    alist.append(baseline)
    subprocess.Popen(["python", "/home/corr/Desktop/RealTimePlotting/singlePlot_ver2.py"] + alist) 

'''
Function: Toggling between Amp and Phase

Precondition: The starting graph is Amplitude vs. Time. The variable tracking the 
change is switch in the main window. 

Postcondition: If the button is pressed, then switch is changed from True to
False, and likewise the other way around. The function will also change the 
title of the first plot to display to user of the current graph. If the graph 
is Amp, then the function invokes autorange. If the graph is Phase, then the 
function invokes setYRange and YLimits to display y values between -180 and 180.
'''     
def toggle_Amp_Ph():
    global switch
    if switch == "True":
        switch = "False"
        for i in range(amount):
            lsb[i].clear()
            usb[i].clear()
            timetrack[i] = 0
            wlist[i].setYRange(-180, 180, padding=0) 
            wlist[i].setLimits(yMin=-180, yMax=180)
        wlist[0].setLabels(title="Phase vs. Time")
    else:
        switch = "True"
        wlist[0].setLabels(title="Amplitude vs. Time")
        for i in range(amount):
            lsb[i].clear()
            usb[i].clear()
            timetrack[i] = 0
            wlist[i].enableAutoRange()
'''
Function: Toggling between Upper Baseline or Lower Baseline

Precondition: The starting baseline is Lower. The variable tracking 
the change between baseline is switch_UL.
-If switch_UL = "True", then graph is displaying Lower Baseline files
-If switch_UL = "False", then graph is displaying Upper Baseline files

Postcondition: When the button is clicked, switch_UL will change 
from "True" to "False", or the other way around. The label text will
also change to display USB or LSB names in different font size depending
on window size. The window will clear off all points and depending on
whether is Phase or Amplitude graph, the graph will autorange or
remains between -180 and 180 y range.
'''
def toggle_USB_LSB():
    global switch_UL, update 
    if switch_UL == "True":
        switch_UL = "False"
        if switch_size == "True":
            labelStyle = {'color':'#000000', 'font-size':'10pt'}
        else:
            labelStyle = {'color':'#000000', 'font-size':'6pt'} 
        for i in range(amount):
            wlist[i].getAxis('left').setLabel(llis[i], **labelStyle) 
    else:
        switch_UL = "True"
        if switch_size == "True":
            labelStyle = {'color':'#000000', 'font-size':'10pt'}
        else:
            labelStyle = {'color':'#000000', 'font-size':'6pt'}
        for i in range(amount):
            wlist[i].getAxis('left').setLabel(ulis[i], **labelStyle)
    for i in range(amount):
        lsb[i].clear()
        timetrack[i] = 0
        if switch == "True":
            wlist[i].enableAutoRange() 
'''
Function: Toggling between Window Size - Fit or Stretch

Precondition: The starting window size is 4000. The variable
tracking the between fit or stretch is switch_size.
switch_size = "True" then the window size is 4000
switch_size = "False" then the windowsize is 940

Postcondition: When the button is clicked, switch_size
changes from "True" to "False", or the other way around.
Labels will also get change accordingly to the screen size
for readability. If window size is 4000, then the font size
is 10pt. If window size is 940, then font size is 6pt.
'''

def toggle_Fit_Stretch():
    global switch_size
    if switch_size == "True":
        for i in range(21):
            labelStyle = {'color':'#000000', 'font-size':'6pt'}
            lsb[i].setData(size=3)
            usb[i].setData(size=3)
            wlist[i].getAxis('left').setLabel(namelist[i], **labelStyle)
        window_size = 940
        view.setFixedHeight(window_size)
        switch_size = "False"
    else:
        labelStyle = {'color':'#000000', 'font-size':'10pt'}
        for i in range(21):
            lsb[i].setData(size=5)
            usb[i].setData(size=5)
            wlist[i].getAxis('left').setLabel(namelist[i], **labelStyle) 
        window_size = 4000
        view.setFixedHeight(window_size)
        switch_size = "True"

'''
Widgets Buttons Documentation Ends 
-------------------------------------------------------------------------------------------
'''

'''
Helper Functions Begins
-------------------------------------------------------------------------------------------
Function: Tail

Precondition: takes an open file and the n amount of lines from the end.

Postcondition: Using an efficient algorithm, returns the x amount of lines
from the declared n value from the end of the file to parse.  
'''

def compare_len(file1, file2):
    len1 = file_len(file1)
    len2 = file_len(file2)
    alist = [len1, len2]
    return min(alist) 

def tail(f, lines, _buffer=20000000000):
    # place holder for the lines found
    lines_found = []
 
    # block counter will be multiplied by buffer
    # to get the block size from the end
    block_counter = -1
 
    # loop until we find X lines
    while len(lines_found) < lines:
        try:
            f.seek(block_counter * _buffer, os.SEEK_END)
        except IOError:  # either file is too small, or too many lines requested
            f.seek(0)
            lines_found = f.readlines()
            break
 
        lines_found = f.readlines()
 
        # we found enough lines, get out
        if len(lines_found) > lines:
            break
 
        # decrement the block counter to get the
        # next X bytes
        block_counter -= 1
 
    return lines_found[-lines:]
 
'''
Function: File Length

Precondition: Takes a file name 

Postcondition: return how many lines the file hasattr
'''
def file_len(fname):
    j = 0
    with open(fname) as f:
        for j, l in enumerate(f):
            pass
    return j + 1
'''
Function: Calculate Total

Precondition: Takes in a Python list. 

Postcondition: Return the mean/ average of the list contents

'''
def calc_total(alist):
    if not alist:
       return
    return np.mean(alist) 

'''
Function: Calculate Amplitude 

Precondition: Takes in the average of Real and Imaginary numbers
x = Real
y = Imaginary

Postcondition: return the value of the Amplitude formula
'''
def calc_Amplitude(x, y):
    if not x:
        return
    if not y:
        return
    x2 = x*x
    y2 = y*y
    z = x2+y2
    w = np.sqrt(z)
    return w
'''
Function: Calculate Phase

Precondition: Takes in the average of Real and Imaginary numbers

Postcondition: Return the value of Phase formula
'''
def calc_Phase(x, y): 
    if not x:
        return
    if not y:
        return
    return (np.arctan2(y, x)*(180/np.pi))

'''
Function: Get Arguments, from argparse

Precondition: Initialize --fb and --fl as the argument to pass from command line
--fb = skipping the x amount of channels from the beginning
--fl = skipping the x amount of channels from the ending

Postcondition: Return the values that the users pass in to main, otherwise return 0 for 
both variables. 
'''
def get_args():
    parser = argparse.ArgumentParser(description="Retrieving command line arguments for plotting purposes")
    parser.add_argument('--fb', type=int, help='Skip the first amount of channel(s)', default=-7)
    parser.add_argument('--fl', type=int, help='Skip the last amount of channel(s)', default=-7)
    args = parser.parse_args()
    fb = args.fb
    fl = args.fl
    return fb, fl
'''
Function: Insensitive Globbing

Precondition: Takes a regular glob pattern

Postcondition: algorithmically transform the glob pattern to insensitively search for the pattern
and return a list of all the objects that matched with the insensitive pattern. 
'''
def insensitive_glob(pattern):
    def either(c):
        return '[%s%s]'%(c.lower(),c.upper()) if c.isalpha() else c
    return glob.glob(''.join(map(either,pattern)))
'''
Helper Functions Ends
-------------------------------------------------------------------------------------------
'''
'''
Main Updating and Loading Function Begins
-------------------------------------------------------------------------------------------
Function: track

Precondition: Takes a file name and the position with respect to timetrack and counter list

Postcondition: Given the filen name and position. Use pandas to open and parse the data where
it updates last, given by the counter[position], and parse all the new content accordingly. 
While in the for loop, the algorithm updates the counter[position] and timetrack[position],
parse all the real and imaginary numbers, and calculates it depending the value of 
switch (Phase or Amp). 
'''
def track(lfile, ufile, position):
    global counter, timetrack 
    try: 
        dataframe = pandas.read_csv(lfile, skiprows=counter[position])
        dataframe2 = pandas.read_csv(ufile, skiprows=counter[position])
        thislist = list(dataframe.values.flatten())
        thislist2 = list(dataframe2.values.flatten())
    except ValueError as e:
        print(e)
    except OSError as o:
        sys.exit("Finished Reading. Exiting.")
    try:
        if not thislist:
            return
    except UnboundLocalError as u:
        return
    len1 = len(thislist)
    len2 = len(thislist2)
    if len1 > len2:
        size = len2
    else:
        size = len1
    for j in range(size):
        l = thislist[j]
        l2 = thislist2[j]
        l.replace('\n', '')
        l2.replace('\n', '')
        data = l.replace('i', 'j').split()
        data2 = l2.replace('i', 'j').split()
        data = list(map(complex, data))
        data2 = list(map(complex, data2))
        a = np.array(data)
        a2 = np.array(data2)
        counter[position]= counter[position]+1
        ilist = list(a.imag) 
        rlist = list(a.real) 
        ilist2 = list(a.imag)
        rlist2 = list(a.real)
        if skipfirst > 0:
            ilist = ilist[skipfirst:]
            rlist = rlist[skipfirst:]
            ilist2 = ilist2[skipfirst:]
            rlist2 = rlist2[skipfirst:]
        if skiplast > 0:
            ilist = ilist[:-skiplast]
            rlist = rlist[:-skiplast] 
            ilist = ilist2[:-skiplast]
            rlist = rlist2[:-skiplast]
        R = calc_total(rlist)
        I = calc_total(ilist)
        R2 = calc_total(rlist2)
        I2 = calc_total(ilist2)
        if switch == "True":
            point = calc_Amplitude(R, I)
            point2 = calc_Amplitude(R2, I2)
        else: 
            point = calc_Phase(R, I)
            point2 = calc_Phase(R2, I2)
        if point == point2:
            print("Matching at ", counter[position])
        if not point2:
            print("No Point2")
        x = [counter[position]]
        y = [point]
        z = [point2] 
        lsb[position].addPoints(x, y)
        usb[position].addPoints(x, z)
        timetrack[position] = timetrack[position] + 1

'''
Function: Load

Precondition: Takes a file name and a position

Postcondition: Using the file name and position, it will refresh
all the content of timetrack and counter list, accordingly
to the position with the help of the tail function to get the
last 15 point values and graph it out. The program will 
start from the there. 
'''
def load(lfile, ufile, position):
    global counter, timetrack 
    flist = [lfile, ufile]
    trail = 0
    len = compare_len(lfile, ufile) 
    if len < 15:
        counter[position] = 0 
        trail = 0 
    else:
        counter[position] = len - 15   
        trail = 15
    with open(lfile, 'r') as f, open(ufile, 'r') as f2:
      alist = tail(f, trail)
      blist = tail(f2, trail)
      for l, l2 in zip(alist, blist):
        l.replace('\n', '')
        l2.replace('\n', '')
        data = l.replace('i', 'j').split()
        data2 = l2.replace('i', 'j').split()
        data = list(map(complex, data))
        data2 = list(map(complex, data2))
        a = np.array(data)
        a2 = np.array(data2)
        counter[position]= counter[position]+1
        ilist= list(a.imag)
        ilist2 = list(a2.imag)
        rlist = list(a.real)
        rlist2 = list(a2.real)
        if skipfirst > 0:
          ilist = ilist[skipfirst:]
          rlist = rlist[skipfirst:]
          ilist2 = ilist2[skipfirst:]
          rlist2 = rlist2[skipfirst:]
        if skiplast > 0:
          ilist = ilist[:-skiplast]
          rlist = rlist[:-skiplast]
          ilist2 = ilist2[:-skiplast]
          rlist2 = rlist2[:-skiplast]
        R = calc_total(rlist)
        I = calc_total(ilist)
        R2 = calc_total(rlist2)
        I2 = calc_total(ilist2)
        point = calc_Amplitude(R, I)
        point2 = calc_Amplitude(R2, I2)
        x = []
        y = []
        z = []
        x.append(counter[position])
        y.append(point)
        z.append(point2)   
        lsb[position].addPoints(x, y)
        usb[position].addPoints(x, z)
        timetrack[position] = timetrack[position] + 1

'''
Function: update

Precondition: Gets called in main with QTimer class. 

Postcondition: Update will run through 21 baselines each second.
This will update the plotting of each graph, timetrack, and 
counter accordingly to the position through the enumaration
(0,1,2,...,20) with the function track. And depending on switch_UL,
it will pass the names of lower or upper baselines. It also clears all
the figures after 150 time integrations through timetrack.
'''
def update():
    for i, key in enumerate(lis):
        track(adict_lower[key], adict_upper[key], i)
        if timetrack[i] >= 75: 
            lsb[i].clear() 
            usb[i].clear()
            timetrack[i] = 0
            if switch == "False":
                wlist[i].setYRange(-180, 180, padding=0)
                wlist[i].setLimits(yMin=-180, yMax=180)
            else:
                wlist[i].enableAutoRange()
 

skipfirst, skiplast = get_args() 
if skipfirst == -7 or skiplast == -7:
  if skipfirst == -7:
    print("--fb was not passed") 
  if skiplast == -7:
    print("--fl was not passed")
  sys.exit("Please Pass Both '--fb=[int]' and '--fl=[int]' Arguments When Starting This Program.")

'''
Containers to keep track of data
note that:

adict_lower looks like { '01':lsb-01.cor, '02':lsb-02.cor, ... }
so that the key is the baseline and the item is the file name associated with the baseline
similarly, adict_upper will looks like that too
'''
adict_lower = {}
adict_upper = {}
counter=[0]*21
tracker=[0]*21
timetrack=[0]*21

lis = ['01', '02', '03', '04', '05', '06', '12', '13', '14', '15',
 '16', '23', '24', '25', '26', '34', '35', '36', '45', '46', '56']

namelist = ['0-1', '0-2', '0-3', '0-4', '0-5', '0-6', '1-2'
, '1-3', '1-4', '1-5', '1-6', '2-3', '2-4', '2-5', '2-6', 
'3-4', '3-5', '3-6', '4-5', '4-6', '5-6']

'''
Populating the dictionaries with upper and lower files
'''
os.chdir('read21baselines')
upper_sideline = insensitive_glob('*usb*')
lower_sideline = insensitive_glob('*lsb*')
amount = len(upper_sideline)
print("There are ", amount, " files in this directory.")
print("Upper Sideline Files: ") 
for stri in upper_sideline:
    print(stri) 
    li = re.findall(r'\d+', stri)
    adict_upper[li[0]] = stri
print("Lower Sidelines Files: ")
for stri in lower_sideline:
    print(stri)
    li = re.findall(r'\d+', stri)
    adict_lower[li[0]] = stri
switch_UL = "False"
switch = "True"
switch_size = "True"
filechannel = upper_sideline

'''
Initalizing the Main Window Application
'''
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
app = QtGui.QApplication(['Graphing Running']) 
sa = pg.QtGui.QScrollArea() #Makes it possible to scroll in window
view = pg.GraphicsLayoutWidget() #View is used to display multiple plots
w = pg.LayoutWidget() #The form application for zooming option
w2 = pg.LayoutWidget() #The form application for toggling options
view.setFixedHeight(4000) 
view.setFixedWidth(1285)
sa.setWidget(view)
pg.setConfigOptions(antialias=True) #Makes the window response faster
sa.show()
wlist = []
lsb = []
usb = []
blist = [lsb, usb]
'''
Initalizing the widgets buttons and text area
'''
btn = QtGui.QPushButton('Submit')
btn2 = QtGui.QPushButton('Toggle Amp/Ph')
btn4 = QtGui.QPushButton('Toggle Fit/Stretch')
text = QtGui.QLineEdit('Baseline')
text2 = QtGui.QLineEdit('Start Time')
text3 = QtGui.QLineEdit('End Time')
 
w.addWidget(text)
w.nextCol()
w.nextRow()
w.addWidget(text2)
w.nextRow()
w.addWidget(text3)
w.nextRow()
w.addWidget(btn)
w2.addWidget(btn2)
w2.nextRow()
w2.addWidget(btn4) 

'''
Generating the 21 plots and associating a ScatterPlotItem with each plot
Note that lower files are default starting data generator 
'''
for i in range(amount): 
    labelStyle = {'color':'#000000', 'font-size':'10pt'}
    w1 = view.addPlot()
    wlist.append(w1)
    wlist[i].getAxis('left').setLabel(namelist[i], **labelStyle)
    scatterplot1 = pg.ScatterPlotItem(name=lis[i], size=5 , pen=pg.mkPen(None), brush=pg.mkBrush(0, 0, 0, 255))
    scatterplot2 = pg.ScatterPlotItem(name=lis[i], size=5, pen=pg.mkPen(None), brush=pg.mkBrush(255, 0, 0, 255))
    lsb.append(scatterplot1)
    usb.append(scatterplot2)
    wlist[i].addItem(scatterplot1)
    wlist[i].addItem(scatterplot2)
    view.nextRow()

'''
In case that it crashed, go through the 21 files and grab the last 15 points through load function
'''
for i, key in enumerate(lis):
    load(adict_lower[key], adict_upper[key], i)

wlist[0].setLabels(title='Amplitude vs. Time' ) #Set the title of the first graph
'''
Connect the button with their respective functions 
Ex:
Clicking the 'submit' button will invoke the function submit()
'''
btn.clicked.connect(submit)
btn2.clicked.connect(toggle_Amp_Ph)
btn4.clicked.connect(toggle_Fit_Stretch)
w2.show()
w.show()

'''
set timer as QTimer object, a thread-like object, and runs 
the function update every one minute as long as the 
Application is running.
'''
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(1000)
 
if __name__ == '__main__':
    import sys
    if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
        QtGui.QApplication.instance().exec_()

Reply via email to