2

I'm working on a fairly simple matplotlib animation using animation.FuncAnimation and it takes a very long time to run; about 0.81s per frame and 180s for the 220 frames in an 11 second long video. I've already incorporated the standard performance improvements, namely not creating Artist objects in the animation loop and returning all updated Artists objects so that bliting can be done but neither has had a significant impact on performance. I've timed the contents of the newFrame function and it only takes about 0.6ms to run. My code is below.

Any other suggestions how how to speed this up or parallelize it?

EDIT: It looks like the performance issues are largely to do with the subplots. Reducing the number of subplots reduces the execution time dramatically. Not really a great solution but noteworthy

EDIT 2: Commenting out axsArray[a,b].minorticks_on() doubles the performance to ~0.4s per frame. This brings performance more in line with what I expect based on previous experience with matplotlib animations, though still incredibly slow for a simple plot.

#!/usr/bin/env python3

from timeit import default_timer

import matplotlib
import matplotlib.animation as animation
import matplotlib.pyplot as plt
import numpy as np
import os
import sys

matplotlib.use("Agg")

plt.close('all')
start = default_timer()


# ==============================================================================
def main():
    # ==========================================================================
    # Settings
    # ==========================================================================
    # Check for CLI arguments
    if len(sys.argv) == 2:
        cliFPS = int(sys.argv[1])
    elif len(sys.argv) > 2:
        raise TypeError(f"Too many ({len(sys.argv)}) command line arguments given")

    # Data settings
    loadPath = ''
    yPos     = 16  # y position of the x line to load
    zPos     = 16  # z position of the x line to load

    # Plot Settings
    supTitleText  = "Time Evolution of Initial Conditions"
    densityColor  = 'blue'                      # color of the density plot
    velocityColor = 'purple'                    # color of the velocity plots
    magneticColor = 'tab:orange'                # color of the magnetic field plots
    pressureColor = 'green'                     # color of the pressure plot
    ieColor       = 'red'                       # color of the specific internal energy plot
    linestyle     = '-'                         # The line style
    linewidth     = 0.5                         # How wide to make the lines
    marker        = "."                         # Marker kind for points
    markersize    = 3                           # Size of the marker
    figSizeScale  = 2.                          # Scaling factor for the figure size
    figHeight     = 4.8 * figSizeScale          # height of the plot in inches, default is 4.8
    figWidth      = 7.0 * figSizeScale          # width of the plot in inches, default is 6.4
    padPercent    = 0.05                        # How many percent larger the limits should be than the data

    # Video Settings
    OutFile       = "mvp.mp4"                   # Output filename
    Duration      = 10.                         # How long the video is in seconds
    dpi           = 150                         # Dots per inch
    index         = 0                           # Initialize index
    initIndex     = 0                           # Index for init frames
    fps           = cliFPS if ("cliFPS" in locals()) else 20  # Framerate
    FrameTime     = (1./fps) * 1000             # Frametime in milliseconds
    totFrames     = int(fps * Duration)         # Total number of frames (floor)
    # ==========================================================================
    # End settings
    # ==========================================================================

    # Load data
    (densityData, velocityXData, velocityYData, velocityZData, pressureData,
     ieData, magneticXData, magneticYData, magneticZData, positions,
     timeStepNum, dims, physicalSize) = loadData(loadPath, yPos, zPos)

    # Compute which time steps to plot
    if timeStepNum.size >= totFrames:
        floatSamples    = np.arange(0, timeStepNum.size, timeStepNum.size/totFrames)
        timeStepSamples = np.asarray(np.floor(floatSamples), dtype="int")
    else:  # if the number of simulation steps is less than the total number of frames
        totFrames = timeStepNum.size
        fps       = np.ceil(totFrames/Duration)
        FrameTime = (1./fps) * 1000
        timeStepSamples = np.arange(0, timeStepNum.size, 1, dtype="int")

    # Insert the initial second of the initial conditions
    timeStepSamples = np.insert(timeStepSamples, 0, [0]*fps)

    # Get the plot limits
    densityLowLim, densityHighLim     = computeLimit(densityData, padPercent)
    ieLowLim, ieHighLim               = computeLimit(ieData, padPercent)
    pressureLowLim, pressureHighLim   = computeLimit(pressureData, padPercent)
    velocityXLowLim, velocityXHighLim = computeLimit(velocityXData, padPercent)
    velocityYLowLim, velocityYHighLim = computeLimit(velocityYData, padPercent)
    velocityZLowLim, velocityZHighLim = computeLimit(velocityXData, padPercent)
    magneticXLowLim, magneticXHighLim = computeLimit(magneticXData, padPercent)
    magneticYLowLim, magneticYHighLim = computeLimit(magneticYData, padPercent)
    magneticZLowLim, magneticZHighLim = computeLimit(magneticZData, padPercent)

    # Set up plots
    # Create 9 subplots
    fig, subPlot = plt.subplots(3, 3, figsize = (figWidth, figHeight))

    # Super Title
    titleText = fig.suptitle(supTitleText)

    # Shared x-label
    subPlot[2,0].set_xlabel("Position")
    subPlot[2,1].set_xlabel("Position")
    subPlot[2,2].set_xlabel("Position")

    # Set values for the subplots
    densityPlot,   subPlot = setupSubPlots('Density',         densityLowLim,   densityHighLim,   positions, densityData,   linestyle, linewidth, marker, markersize, densityColor,  subPlot)
    pressurePlot,  subPlot = setupSubPlots('Pressure',        pressureLowLim,  pressureHighLim,  positions, pressureData,  linestyle, linewidth, marker, markersize, pressureColor, subPlot)
    iePlot,        subPlot = setupSubPlots('Internal Energy', ieLowLim,        ieHighLim,        positions, ieData,        linestyle, linewidth, marker, markersize, ieColor,       subPlot)
    velocityXPlot, subPlot = setupSubPlots('$V_x$',           velocityXLowLim, velocityXHighLim, positions, velocityXData, linestyle, linewidth, marker, markersize, velocityColor, subPlot)
    velocityYPlot, subPlot = setupSubPlots('$V_y$',           velocityYLowLim, velocityYHighLim, positions, velocityYData, linestyle, linewidth, marker, markersize, velocityColor, subPlot)
    velocityZPlot, subPlot = setupSubPlots('$V_z$',           velocityZLowLim, velocityZHighLim, positions, velocityZData, linestyle, linewidth, marker, markersize, velocityColor, subPlot)
    magneticXPlot, subPlot = setupSubPlots('$B_x$',           magneticXLowLim, magneticXHighLim, positions, magneticXData, linestyle, linewidth, marker, markersize, magneticColor, subPlot)
    magneticYPlot, subPlot = setupSubPlots('$B_y$',           magneticYLowLim, magneticZHighLim, positions, magneticYData, linestyle, linewidth, marker, markersize, magneticColor, subPlot)
    magneticZPlot, subPlot = setupSubPlots('$B_z$',           magneticZLowLim, magneticZHighLim, positions, magneticZData, linestyle, linewidth, marker, markersize, magneticColor, subPlot)

    # Layout
    plt.tight_layout()
    fig.subplots_adjust(top=0.88)

    # Generate animation
    simulation = animation.FuncAnimation(fig,
                                         newFrame,
                                         fargs = (fig, supTitleText, densityPlot, pressurePlot, iePlot,
                                                  velocityXPlot, velocityYPlot, velocityZPlot,
                                                  magneticXPlot, magneticYPlot, magneticZPlot,
                                                  densityData, pressureData, ieData,
                                                  velocityXData, velocityYData, velocityZData,
                                                  magneticXData, magneticYData, magneticZData,
                                                  timeStepSamples, titleText),
                                         blit = True,
                                         frames = timeStepSamples,
                                         interval = FrameTime,
                                         repeat = False)

    FFwriter = animation.FFMpegWriter(bitrate=1000,
                                      fps=fps,
                                      codec='libx264',
                                      extra_args=['-crf','28','-preset','ultrafast','-pix_fmt','yuv420p'])
    simulation.save(filename=OutFile, writer = FFwriter)
    # simulation.save(filename=OutFile, fps=fps, dpi=dpi)
    print(f"\n\nAnimation complete. Framerate: {fps} fps, Total Number of Frames: {timeStepSamples.size}")
# ==============================================================================

# ==============================================================================
def loadData(path, yPos, zPos):

    numFiles = 870
    dims = [32, 32, 32]
    physicalSize = [1,1,1]

    np.random.random((numFiles, dims[0]))

    # Allocate Arrays
    densityData        = np.random.random((numFiles, dims[0]))
    velocityXData      = np.random.random((numFiles, dims[0]))
    velocityYData      = np.random.random((numFiles, dims[0]))
    velocityZData      = np.random.random((numFiles, dims[0]))
    pressureData       = np.random.random((numFiles, dims[0]))
    ieData             = np.random.random((numFiles, dims[0]))
    magneticXData      = np.random.random((numFiles, dims[0]))
    magneticYData      = np.random.random((numFiles, dims[0]))
    magneticZData      = np.random.random((numFiles, dims[0]))
    timeStepNum        = np.arange(0, numFiles, 1)
    positions          = np.linspace(0., 1, dims[0])

    return (densityData, velocityXData, velocityYData, velocityZData,
            pressureData, ieData, magneticXData, magneticYData, magneticZData,
            positions, timeStepNum, dims, physicalSize)
# ==============================================================================

# ==============================================================================
def setupSubPlots(fieldName, lowLim, highLim, positions, data, linestyle, linewidth, marker, markersize, color, axsArray):
    # Get the subplot coordinates to set
    if fieldName == 'Density':
        a, b = (0,0)
    elif fieldName == 'Pressure':
        a, b = (0,1)
    elif fieldName == 'Internal Energy':
        a, b = (0,2)
    elif fieldName == '$V_x$':
        a, b = (1,0)
    elif fieldName == '$V_y$':
        a, b = (1,1)
    elif fieldName == '$V_z$':
        a, b = (1,2)
    elif fieldName == '$B_x$':
        a, b = (2,0)
    elif fieldName == '$B_y$':
        a, b = (2,1)
    elif fieldName == '$B_z$':
        a, b = (2,2)
    else:
        raise ValueError('setSubPlots received invalid fieldName')

    # Set plot parameters
    axsArray[a,b].set_ylim(lowLim, highLim)
    axsArray[a,b].set_ylabel(fieldName)
    axsArray[a,b].minorticks_on()
    axsArray[a,b].grid(which = "both")

    # Set initial values
    returnPlot, = axsArray[a,b].plot(positions,
                                     data[0,:],
                                     linestyle  = linestyle,
                                     linewidth  = linewidth,
                                     marker     = marker,
                                     markersize = markersize,
                                     color      = color,
                                     label      = fieldName,
                                     animated   = True)

    return returnPlot, axsArray
# ==============================================================================

# ==============================================================================
def computeLimit(dataSet, padPercent):
    pad = np.max(np.abs([dataSet.min(), dataSet.max()])) * padPercent
    if pad == 0:
        # if the dataset doesn't exist or is zero return reasonable limits
        return -1, 1
    else:
        lowLim  = dataSet.min() - pad
        highLim = dataSet.max() + pad
        return lowLim, highLim
# ==============================================================================

# ==============================================================================
def newFrame(idx, fig, supTitleText, densityPlot, pressurePlot, iePlot,
             velocityXPlot, velocityYPlot, velocityZPlot,
             magneticXPlot, magneticYPlot, magneticZPlot,
             densityData, pressureData, ieData,
             velocityXData, velocityYData, velocityZData,
             magneticXData, magneticYData, magneticZData,
             timeStepSamples, titleText):
    titleText.set_text(f"{supTitleText} \n Time Step: {idx}")

    densityPlot  .set_ydata(densityData[idx,:])
    pressurePlot .set_ydata(pressureData[idx,:])
    iePlot       .set_ydata(ieData[idx,:])

    velocityXPlot.set_ydata(velocityXData[idx,:])
    velocityYPlot.set_ydata(velocityYData[idx,:])
    velocityZPlot.set_ydata(velocityZData[idx,:])

    magneticXPlot.set_ydata(magneticXData[idx,:])
    magneticYPlot.set_ydata(magneticYData[idx,:])
    magneticZPlot.set_ydata(magneticZData[idx,:])

    # Report progress
    if not hasattr(newFrame, "counter"):
        newFrame.counter = -1  # Accounts for first call which is performed before the animation starts
        print()

    if newFrame.counter >= 0:
        print(f'Animation is {100*(newFrame.counter/timeStepSamples.shape[0]):.1f}% complete', end='\r')

    newFrame.counter += 1

    # The return is required to make blit work
    return (titleText, densityPlot, pressurePlot, iePlot,
            velocityXPlot, velocityYPlot, velocityZPlot,
            magneticXPlot, magneticYPlot, magneticZPlot)
# ==============================================================================

main()

print(f'Time to execute: {round(default_timer()-start,2)} seconds')
bcaddy
  • 73
  • 5
  • Have you tested your code with [line_profiler](https://stackoverflow.com/a/23888912/2912349)? – Paul Brodersen Apr 21 '22 at 18:16
  • No but I timed a couple of potential problem lines. Tldr is that it’s almost entirely in the save function. The rest takes ~1s – bcaddy Apr 23 '22 at 03:50

0 Answers0