Slow FuncAnimation Script - python
I use python 3.6 (the newest one) with the spider interpreter on a windows 10 PC with more-than-average hardware and wrote a script that enables me to continuously measure and plot the frequencies of two channels on an Agilent frequency counter and saves the data to a txt file. I furthermore have to convert the script to a .exe file (using pyinstaller) in order to distribute it to several measuring PCs.
Everything works fine, even from the .exe file, until the measuring time reaches about 2000 seconds. The software then starts to become very slow until it even show the "window not answering" thing that windows does while plotting.
I tried to activate the bliting-function of FuncAnimate, but when I do so, it only shows a white window.
Therefore I am now looking for options to enhance the speed of my software, especially at high data amounts, without cutting of the lots and lots of data (they are needed to be seen from t=0 to t=whatever).
Why does the blit=True kill my animation? Or is there a better way to plot these amounts of data fast and over and over again?
My code:
#Importing all required additional Python packages and sub-packages/functions
import visa, time, tkinter,sys
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg as FigureCanvas
from tkinter import messagebox, filedialog
from matplotlib.figure import Figure
import matplotlib.animation as animation
#Initialize the tkinter main window (graphical user interface window)
root=tkinter.Tk()
root.geometry('1200x900')
#Initialize the VISA resource manager and define a list object
rm=visa.ResourceManager('C:\\Windows\\System32\\visa32.dll')
liste=[]
#lists all available VISA resources and adds them to the list object
#one entry per VISA instrument
string=str(rm.list_resources())[1:-1]
liste.append(string.split('\'')[1:-1])
#opens a message box for each object in the list (each instrument)
#if user chosses "yes" for a VISA resource, the software tries to access the device
#if the device is present, the for-loop is left, otherwise the whole software quits
for i in liste[0]:
box=messagebox.askyesno('VISA Resources','Is this the correct VISA-Resource?'+'\n'+str(i))
if box==True:
try: inst=rm.open_resource(i)
except:
messagebox.showerror('Wrong Resource','The VISA resource was wrong!')
root.destroy()
sys.exit()
break
elif box==False: continue
#checks if the VISA resource is actually existent and present
try: inst
except:
messagebox.showerror('No Resource found','No VISA Resource was chosen.')
root.destroy()
sys.exit()
#opens a file dialog window and safes the chosen location as "filename"
#furthermore checks if the user specified a valid path or quited by using "cancel"
#if the user clicked "cancel", the software quits
filename=filedialog.asksaveasfilename()
if len(filename)==0:
messagebox.showerror('No File','No file location was specified.')
root.destroy()
sys.exit()
#definition of variables as well as the update function
x_data,y_data1, y_data2, y_data3=[],[],[],[]
def update(frame):
#create X-data, seconds since .clock was first called
x_data.append(time.clock())
#read out current freq on channel1 and float it (originaly returned as string)
value1=float(inst.query('MEAS:FREQ? 10 MHz, 0.1 Hz, (#1)'))
#add data to list
y_data1.append(value1)
#define and automatically adjust subplot limits
subplot1.set_ylim(min(y_data1)-1,max(y_data1)+1)
subplot1.set_xlim(min(x_data)-1,max(x_data)+1)
#define subplot title and labels
subplot1.set_title('Channel 1')
subplot1.set_xlabel('Time')
subplot1.set_ylabel('Frequency')
#same as above for second channel
value2=float(inst.query('MEAS:FREQ? 10 MHz, 0.1 Hz, (#2)'))
y_data2.append(value2)
subplot2.set_ylim(min(y_data2)-1,max(y_data2)+1)
subplot2.set_xlim(min(x_data)-1,max(x_data)+1)
subplot2.set_title('Channel 2')
subplot2.set_xlabel('Time')
subplot2.set_ylabel('Frequency')
#calculates and plots the difference of the upper two channels
y_data3.append(value1-value2)
subplot3.set_ylim(min(y_data3)-1,max(y_data3)+1)
subplot3.set_xlim(min(x_data)-1,max(x_data)+1)
subplot3.set_title('Difference')
subplot3.set_xlabel('Time')
subplot3.set_ylabel('Frequency')
#plots the subplots in the main plot frame
subplot1.plot(x_data,y_data1,'b')
subplot2.plot(x_data, y_data2,'r')
subplot3.plot(x_data, y_data3,'g')
#writes all data do a new file defined before
newfile.write(str(time.clock())+', '+str(value1)+', ' +str(value2)+'\n')
#enables the code to make use of the defined variables/data
return x_data, y_data1, y_data2, y_data3
#create a global boolean variable and set it to "True"
global boolean
boolean=True
#define a Pause function using the global boolean variable
def Pause():
global boolean
#if the boolean is True, the animation stops and the variable is set to False
if boolean==True:
anim.event_source.stop()
boolean=False
#if the boolean is False, the animation continues and the variable is set to True
elif boolean==False:
anim.event_source.start()
boolean=True
#define a Quit function that quits the application and closes the created file
def Quit():
newfile.close()
root.destroy()
#define a function that applies the user input data aquisition time to the animation
def SpeedApply():
anim.event_source.interval=int(float(Interv.get())*1000)
#create and place different buttons that call the defined functions upon buttonclick
QuitBut=tkinter.Button(text='Quit', command=Quit)
QuitBut.place(x=15,y=15)
StartBut=tkinter.Button(text='Pause/Resume',command=Pause)
StartBut.place(x=55, y=15)
Interv=tkinter.Spinbox(root,values=(0.1,0.2,0.5,1,1.5,2), width=8)
Interv.place(x=160, y=17)
Interv.delete(0,'end')
Interv.insert(0,1)
Speedbut=tkinter.Button(text='Apply Speed', command=SpeedApply)
Speedbut.place(x=250, y=15)
#create the figure needed to plot the animated data
figure=Figure(figsize=(8,8), dpi=100)
subplot1=figure.add_subplot(311)
subplot2=figure.add_subplot(312)
subplot3=figure.add_subplot(313)
figure.subplots_adjust(hspace=0.6)
#create a tkinter canvas, needed to embedd the figure into a tkinter root window
canvas=FigureCanvas(figure,root)
canvas.draw()
#canvas.start_event_loop(0.001)
canvas.get_tk_widget().place(x=25,y=50, height=850, width=1150)
#create the newfile where the data will be stored lateron
newfile=open(filename+time.strftime('%d')+time.strftime('%m')+time.strftime('%y')+'.txt','w')
newfile.write('Time, Channel 1, Channel 2\n')
#animation calling the update function upon the figure in the canvas with an interval of 1 second
anim=animation.FuncAnimation(figure,update, blit=False, interval=1000)
#tkinter mainloop, needed to react to user input in tkinter GUI
root.mainloop()
Okay, as requested, a simpler script showing my problem:
import time, tkinter
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg as FigureCanvas
from matplotlib.figure import Figure
import matplotlib.animation as animation
#Initialize the tkinter main window (graphical user interface window)
root=tkinter.Tk()
root.geometry('1200x900')
value1=1
value2=2
#definition of variables as well as the update function
x_data,y_data1, y_data2, y_data3=[],[],[],[]
def update(frame):
#create X-data, seconds since .clock was first called
x_data.append(time.clock())
global value1, value2, value3
value1=value1+2
#add data to list
y_data1.append(value1)
#define and automatically adjust subplot limits
subplot1.set_ylim(min(y_data1)-1,max(y_data1)+1)
subplot1.set_xlim(min(x_data)-1,max(x_data)+1)
#define subplot title and labels
subplot1.set_title('Channel 1')
subplot1.set_xlabel('Time')
subplot1.set_ylabel('Frequency')
#same as above for second channel
value2=value2+1
y_data2.append(value2)
subplot2.set_ylim(min(y_data2)-1,max(y_data2)+1)
subplot2.set_xlim(min(x_data)-1,max(x_data)+1)
subplot2.set_title('Channel 2')
subplot2.set_xlabel('Time')
subplot2.set_ylabel('Frequency')
#calculates and plots the difference of the upper two channels
y_data3.append(value1-value2)
subplot3.set_ylim(min(y_data3)-1,max(y_data3)+1)
subplot3.set_xlim(min(x_data)-1,max(x_data)+1)
subplot3.set_title('Difference')
subplot3.set_xlabel('Time')
subplot3.set_ylabel('Frequency')
#plots the subplots in the main plot frame
subplot1.plot(x_data,y_data1,'b')
subplot2.plot(x_data, y_data2,'r')
subplot3.plot(x_data, y_data3,'g')
#enables the code to make use of the defined variables/data
return x_data, y_data1, y_data2, y_data3
#create a global boolean variable and set it to "True"
global boolean
boolean=True
#define a Quit function that quits the application and closes the created file
def Quit():
root.destroy()
#create and place different buttons that call the defined functions upon buttonclick
QuitBut=tkinter.Button(text='Quit', command=Quit)
QuitBut.place(x=15,y=15)
#create the figure needed to plot the animated data
figure=Figure(figsize=(8,8), dpi=100)
subplot1=figure.add_subplot(311)
subplot2=figure.add_subplot(312)
subplot3=figure.add_subplot(313)
figure.subplots_adjust(hspace=0.6)
#create a tkinter canvas, needed to embedd the figure into a tkinter root window
canvas=FigureCanvas(figure,root)
canvas.draw()
#canvas.start_event_loop(0.001)
canvas.get_tk_widget().place(x=25,y=50, height=850, width=1150)
#animation calling the update function upon the figure in the canvas with an interval of 1 second
anim=animation.FuncAnimation(figure,update, blit=False, interval=100)
#tkinter mainloop, needed to react to user input in tkinter GUI
root.mainloop()
I left the "beautyful-makings" like titles in the code.
To actually see the problem I am having you would have to run that script for at least 600 seconds, the problem becomes "stronger" after 2000 seconds.
It seems that a large part of your issues is having to recompute the entire lists min and max each time,
e.g. your subplot x and y min/max values, rather than processing your entire appended list, you should use a variable to hold the lowest and highest values so far compare the current piece of input data to them, and if it meets condition update them,
subplot1.set_ylim(min(y_data1)-1,max(y_data1)+1)
subplot1.set_xlim(min(x_data)-1,max(x_data)+1)
You also appear to be resetting up the subplot labels every single update, now I am not as familiar with GUI's in TKinter, but i feel this could be covered in an initilisation definition,
subplot1.set_title('Channel 1')
subplot1.set_xlabel('Time')
subplot1.set_ylabel('Frequency')
The first suggestion should keep your program running in a more consistent time, rather than incrementally slowing down, the other should be able to reduce how much your doing per update.
Related
changing label color 2 times within a funtion in tkinter python
I have a funtion, that parses the log. I am updating the label color and text at different lines within the same funtion. I see that only the last changed color and text only reflecting in the UI. i do not see the intermediate color and text changes. Below is my code: def OnclickLogParser(self): if LogFileName == '': messagebox.showinfo("Error", "Please select a valid Log") if LogPathName == '': messagebox.showinfo("Error", "Please select a valid Log Path") self.lb_log_status.configure(bg="#08DFE8", fg="#010101", text='Parsing inProgress...') m_logParser = CAdpBrrLogParser() m_logReader = CAdpBrrLogReader('mrr', m_logParser) status = m_logReader.readFile(LogPathName) if status == True: self.lb_log_status.configure(bg="#F6F50B", fg="#010101", text='Log Ready') self.btn_log_start["state"] = "normal" global m_injector m_injector = CAdpUdpDataInjector() you can see that i am changing the color and text of lb_log_status at two different places. The time gap between those two lines would be around 3 -5 secs. But i can not witness the first color change. '
Tkinter must process events regularly to perform any UI operations. If you want to see the color changes here you have to give Tk a chance to process the configuration and paint events that get generated when you re-configure a widget. Your code just immediately starts processing the log and will not process any events until this function exits and you return to the Tk mainloop() which is the event processing method. You would see the same problem if you used a progress bar widget and tried to update the progress during processing. One way to resolve this is to use the Tk after() method and schedule your processing in chunks that take a limited amount of time per chunk. After reading and processing a chunk call after() again to schedule the next chunk. Another method is to put the processing on a worker thread and use event_generate() to post events back to the Tk thread to announce progress and completion.
Python program becoming unstable after multiple simulations
I apologize if this has an easy answer, but I have been searching everywhere and trying everything I could think of to solve this problem to no avail. Basically I have a differential equation solver that simulates heat flow in a plate, and every time I click a Tkinter button, I would like to run a fresh simulation/animation. The problem is that the program tends to become unstable after multiple calls for new simulations. I knew something was awry when I allowed the user to specify whether an animation would loop continuously. If an animation was allowed to loop, subsequent simulations (looping or not) would slow down and the CPU would remain loaded as if the animation was still looping somewhere in the background after it should have been replaced with the new one. Here are the relevant portions of my code, please let me know if I'm doing something dumb. class GUI(object): def __init__(self,root): self.print_button = tk.Button(self.root, text='Run Simulation', command=self.run).grid() def run(self): simulation = HeatSim(self) simulation.compute(self) simulation.animate(self) class HeatSim(GUI): def __init__(self,gui): # Initialize variables here def compute(self,gui): # Compute values of data matrix def animate(self,gui): frames = 100 fig = plt.figure() ax = fig.add_subplot(111) # Canvas used to display the animation embedded within a tkinter window canvas = FigureCanvasTkAgg(fig, master=gui.root) canvas.show() canvas.get_tk_widget().grid() images = [] for i in range(frames): im = ax.imshow(data[:,:,i],cmap='hot',animated=True) images.append([im]) anim = animation.ArtistAnimation(fig,images,interval=0,blit=True, repeat=0) canvas.show() # Main program loop root = tk.Tk() gui = GUI(root) root.mainloop()
Optimize Matplotlib plot that dynamically updates and remains interactive
Ok, so as the question suggests, I am chasing the best method to create a plot that: 1) Is independent of the main program/script, therefore making changes to the plot will have no adverse (locking or other) effect on the main program/script 2) Can be interacted with via matplotlib's default GUI or other custom GUI Now, what I have so far for the above criteria: 1) Use the multiprocessing module and its multiprocessing Queue to pass X,Y information to a separate process that appends to current X,Y data and refreshes the plot. This basically addresses criteria 1. 2) Ensuring matplotlib's interactive mode is turned on (ion()), an infinite while True: loop checks the aforementioned queue and if there is no X,Y information, allow's processing of GUI events via pyplot.pause(interval in seconds) To simplify things, below is my code to accept incoming X,Y data and plot it in pseudocode. This code is run in a different process with a Queue feeding it X,Y data. from matplotlib import pyplot def run(self): p = pyplot while True: if there's nothing in the Queue: redraw and allow GUI events (p.pause(0.0001)) continue else: command = get X,Y info from Queue if command equals X,Y data: update plots x data (axes.set_ydata()) update plots y data (axes.set_ydata()) update axis data limits (axes.relim()) update axis view limits (axes.autoscale_view()) redraw and allow GUI events i.e. interaction for 0.0001 seconds (p.pause(0.0001)) elif if command equals the "exit loop" value: break turn of interactive mode (p.ioff()) keep plot open and block process until closed by user (p.show()) So can the above be optimized to increase interactivity with the figure GUI? potentially by allowing GUI events to be serviced independently of the plot being updated? This method becomes slower the more updates it has to do (i.e. if there are more plots on the same figure to update) Any clarifications, just ask :)
I would pass the x,y-data via queue (as already planned), but would use the get-command in combination with the block-argument. That way, your drawing-function blocks until there are elements in the queue. If there are not, the function pauses and all other events in your application can be processed. Something like that: def process_data(inqueue): #create your plot for infile in iter(inqueue.get(block=True), "STOP"): #update plot until inqueue.get returns "STOP" #waits for elements in the queue enter code here def main(): inqueue = Queue() process = Process(target=process_data, args=(inqueue,) #can be stopped by putting "STOP" via inqueue.put into the queue process.start() #put some data in the queue
Acessing data inside a Matplotlib GUI callback function
I am new to Python, and somewhat new to object oriented programming. Can anyone explain what is going on and how things are typically done with a matplotlib GUI callback? I've taken the "event_handling example code" from the Matplotlib website and stripped it down for clarity. When you run this code it makes a plot, and if you press a key on the keyboard the press function is called. The press function is passed only event, but somehow every other variable from main program level appears inside the call to press but as a global variable, is this normal for functions? I can print the value of x, but if I try to change it then it makes a local variable version, worse yet now I have seemingly no way to access the global version anymore? #!/usr/bin/env python import numpy as np import matplotlib.pyplot as plt x=np.random.rand(3) y=np.random.rand(3) def press(event): print(x) print('Local Var:', locals().keys()) print('Global Var:', globals().keys()) fig, ax = plt.subplots() fig.canvas.mpl_connect('key_press_event', press) ax.plot(x,y) plt.show() I have searched and had quite a hard time finding any reference that explains how to access or properly pass useful data in and out of the callback function so that a GUI event can do something useful, like update some data or feature of a plot? So lets say I wanted to have the callback function modify y and re-plot the data. How is that typically done?
you have global access to x inside your callback, but can't modify it unless you specify it global. def press(event): global x ... locals().keys() and globals().keys() are printing namespaces; I am unsure why you need to do that. Your callback receives an event that you can use and manipulate inside the function. Here is an example: #!/usr/bin/env python import numpy as np import matplotlib.pyplot as plt x=np.random.rand(3) y=np.random.rand(3) def press(event): print(event, event.key) fig, ax = plt.subplots() fig.canvas.mpl_connect('key_press_event', press) ax.plot(x,y) plt.show() click on the plot window to set the focus. pressing f should print the event object and set the plot full screen pressing f again, will print f and restore the size of the window pressing s will print s and will offer to save your figure etc... To learn more about how you can manipulate events, look up backend_bases in the very rich matplotlib web site. For example, you can set mouse_clicks events that allow you to capture canvas coordinates to add points or modify figures...
PyVisa and Printing New Data
I am trying to use Pyvisa to capture data from one of my channels of my Keithly 2701 DMM. I get a static onetime response via temp = keithly.ask('SCPI COmmand'), but what I want to do is constantly print new data without setting any predefined size, i.e capture 300 data points. I want to determine when to stop the capture if I have seen a trend over 10000 data points, or, in another experiment, I might see a trend after 2500 data points. from pylab import * from visa import instrument inst = SerialInstument(args) while new data: print inst.aks('channel')
while True: print inst.ask('channel') time.sleep(1) You can then ctrl-c to stop the loop when you see fit. The above script is simple - it just puts numbers on the screen until you kill it. I find it useful to plot the data from PyVISA in real time, using matplotlib. I found this to be buggy in pyplot mode (I got lots of blank screens when I turned off interactive mode, ymmv) so I embedded it into a tkinter window, as follows: import matplotlib matplotlib.use('TkAgg') # this has to go before the other imports from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure import Tkinter as Tk import visa # set up a PyVISA instrument and a list for the data data = [] keithley = visa.instrument('GPIB0::whatever') # make a Tkinter window root = Tk.Tk() # add a matplotlib figure to the Tk window fig = Figure() ax = fig.add_subplot(111) canv = FigureCanvasTkAgg(fig, master=root) canv.show() canv.get_tk_widget().pack(fill='both', expand=True) # a function that is called periodically by the event loop def plot_update(): # add a new number to the data data.append(keithley.ask('SCPI:COMM:AND?')) # replot the data in the Tk window ax.clear() ax.plot(data) fig.tight_layout() canv.draw() # wait a second before the next plot root.after(1000, plot_update) root.after(1000, plot_update) root.mainloop() It may not seem like much, but we gradually developed a short script like this into a rather capable instrument control program ;)