2

I am trying to use a python process to animate a plot as shown below:

from multiprocessing import Process
import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

process_enabled = 1;
print("Process enabled: ", process_enabled)

x = []
y = []
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)        

def start_animation():
                 
   # Set up plot to call animate() function periodically   
   ani = animation.FuncAnimation(fig, animate, fargs=(x, y), interval=1000)
   print("Called animate function")
   plt.show()   
   
# This function is called periodically from FuncAnimation
def animate(i, xs, ys):
    
   fx=[0.045,0.02,0.0,0.04,0.015,-0.01,0.015,0.045,0.035,0.01,
        0.055,0.04,0.02,0.025,0.0,-0.005,-0.005,-0.02,-0.05,-0.03] # fx values        
    
   # Add x and y to lists
   xs.append(dt.datetime.now().strftime('%H:%M:%S.%f'))
   if(i<len(fx)):                     
      ys.append(fx[i])             

   # Draw x and y lists
   ax.clear()
   if(i<len(fx)):   
      ys_stacked = np.stack((np.array(ys),0.1+np.array(ys)),axis=1)
      ax.plot(xs, ys_stacked)
      
   print("Animating")      

   # Format plot
   if(i<len(fx)):
      plt.xticks(rotation=45, ha='right')
      plt.subplots_adjust(bottom=0.30)
      plt.title('Force/Torque Sensor Data')
      plt.ylabel('Fx (N)')    

if(process_enabled):
    
   p_graph = Process(name='Graph', target=start_animation)
   print("Created graph process")

   p_graph.start()
   print("Started graph process")           
   
else:   

   start_animation()

When I disable the process, the start_animation() function works fine and the plot is displayed and the animation begins. However, when the process is enabled, the process starts and then the code breaks at print("Called animate function"). There is no plot window and there are no error messages in the terminal).

I'm new to both multiprocessing in python and indeed matplotlib. Any direction would be much appreciated.

Cheers, Tony

Tony
  • 21
  • 3

1 Answers1

0

I'm trying to solve this same problem, but haven't quite figured it out completely. However, I think I can provide a few useful comments on your question.

To start, is there any reason why you want to handle the animation in a separate process? Your approach seems to work fine within a single process. There's a number of issues you'll need to address to do this. If you truly do require a separate process, then the following might be useful.

First, you won't be able to use your global variables in the 'graph' process, as that process doesn't share the same instances of those variables (see Globals variables and Python multiprocessing).

You can share state between processes, but this is difficult for complex objects that you'd want to share (i.e. plt.figure()). See the multiprocessing reference for more information (https://docs.python.org/3/library/multiprocessing.html#sharing-state-between-processes)

One final suggestion would be to do away with the pyplot interface. This is handy for straightforward scripts and interactive data analysis, but it obfuscates a lot of important things - like knowing which figure, axis etc you're dealing with when you call plt methods.

I've provided an alternative, object-oriented approach using a custom class, that can run your animation (without a separate process):

import sys
from multiprocessing import Process, Queue
import datetime as dt
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.backends.qt_compat import QtWidgets
import matplotlib.animation as animation

class StripChart(FigureCanvasQTAgg):
    def __init__(self):
        self.fig = Figure(figsize=(8,5), dpi=100)
        self.ax = self.fig.add_subplot(111)

        # hold a copy of our torque data
        self.fx = [0.045,0.02,0.0,0.04,0.015,-0.01,0.015,0.045,0.035,0.01,
                   0.055,0.04,0.02,0.025,0.0,-0.005,-0.005,-0.02,-0.05,-0.03]

        super().__init__(self.fig)

        # instantiate the data arrays
        self.xs = []
        self.ys = []

    def start_animation(self):
        print("starting animation")

        # set up the animation
        self.ani = animation.FuncAnimation(self.fig, self.animate, init_func=self.clear_frame,
                                           frames=100, interval=500, blit=False)

    def clear_frame(self):
        self.ax.clear()
        self.ax.plot([], [])


    def animate(self, i):
        print("animate frame")
        # get the current time
        t_now = dt.datetime.now()

        # update trace values
        self.xs.append(t_now.strftime("%H:%M:%S.%f"))
        self.ys.append(self.fx[i % len(self.fx)])

        # keep max len(self.fx) points
        if len(self.xs) > len(self.fx):
            self.xs.pop(0)
            self.ys.pop(0)

        self.ax.clear()
        self.ax.plot(self.xs, self.ys)

        # need to reapply format after clearing axes
        self.fig.autofmt_xdate(rotation=45)
        self.fig.subplots_adjust(bottom=0.30)
        self.ax.set_title('Force/Torque Sensor Data')
        self.ax.set_ylabel('Fx (N)')

if __name__=='__main__':
    # start a new qapplication
    qapp = QtWidgets.QApplication(sys.argv)

    # create our figure in the main process
    strip_chart = StripChart()

    strip_chart.show()
    strip_chart.start_animation()

    # start qt main loop
    qapp.exec()

Things of note in this example:

  • you'll need to have a backend installed in your environment (i.e. pip install pyqt5)
  • I've added an init_func to the animation, you don't really need this as you can call self.ax.clear() in the animate method.
  • If you need better performance for your animation, you can use blit=True but you'll need to modify the clear_frame and animate methods to return the artists that you want to update (see https://jakevdp.github.io/blog/2012/08/18/matplotlib-animation-tutorial/ for more info). One drawback is that you won't be able to update the axis labels with that approach.
  • I've set it up to run infinitely until you close the window

I'm assuming that the reason you want to run the animation in a separate process is that there is some time consuming/CPU intensive task that is involved in either updating the graph data, or drawing all the points. Perhaps you have this embedded in some other UI?

I've tried to execute the animation in a separate process, but you need to pass the instance of the figure that's displayed. As I mentioned this isn't straightforward, although there do appear to be ways to do it (https://stackoverflow.com/a/57793267/13752965). I'll update if I find a working solution.

tdpu
  • 400
  • 3
  • 12
  • @Tony there's actually an example in the matplotlib docs (https://matplotlib.org/stable/gallery/misc/multiprocess_sgskip.html). The main takeaway is that it makes the most sense to instantiate your figure in the process that will do the animating. You can have data piped in from elsewhere, assuming it's a simple type (double, int, etc) – tdpu Jan 12 '22 at 22:42