Monday, February 15, 2021

Animated GIFs for Scientific Visualization - An Example Antenna Array Animation using Python, Matplotlib & GIMP

This animation shows how the radiation patterns from an array of antennas as more antennas are added. Using an animation its a lot easier to quickly see how the patterns changes with the number of antennas. Saving the results as a GIF file rather than an animation using Python allows the results to be used in a website or in a Powerpoint presentation. As of Matplotlib 3.3, there is not a gif export capability, but this king of animation can be generated using Python, Matplotlib, and GIMP. 


Discussion

Often scientific information and simulation results need to be visualization to be explained and understood. Live animations in a script are useful, but distributing scripts has a lot of issues. Creating movies (mp4, avi) sometime have issues with encoders. GIF animations are a reliable way to share a lot of information. They can be embedded in PowerPoint, send via email, or embedded in a website with minimal effort and the results just work. 

Once a script to generate a Matplotlib animation, it can usually be modified to generate a sequence of images and save them using the 'savefig' command. Then there are several tools that can convert the images to an animated GIF. With GIMP, the GIF can be generated in a a few minutes and there are a lot of options to control the speed, resolution, and resolution of the result.


Steps to Generate An Animated GIF

  1. Generate a sequence of images of identical size using Matplotlib and save using savefig.
  2. Open GIMP
    • File -> Load the plots as layers
      • Select all image files
    • Filters -> Animation -> Optimize (for GIF)
      • Image->Mode->Index
      • Generate optimum palette
      • Maximum number of colors: 256
      • Color dithering: Positioned
      • Enable dithering of transparency: checked
      • Select Convert
    • Filter -> Animation -> Optimize (GIF)
    • File -> Export
      • Name the file with a .gif extension
      • Select 'Export'
      • On new dialog
        • Select as animation
        • Set timing to 20 msec

    References

    Sample Script


    
    # -*- coding: utf-8 -*-
    
    import matplotlib.pyplot as plt
    from matplotlib.colors import BoundaryNorm
    from matplotlib.ticker import MaxNLocator
    import numpy as np
    
    
    
    # make these smaller to increase the resolution
    dx, dy = 0.005, 0.005
    
    # list of antenna locations
    wavelength = 0.1
    spacing = 0.25
    x_list = np.array([0,1,-1,2,-2,3,-3,4,-4,5,-5,6,-6,7,-7])*spacing*wavelength
    y_list = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
    phase_deg_list = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,] 
    phase_offset_deg_list = range(0,720,9)
    
    num_antennas = [1+int(pp*8/720)*2 for pp in phase_offset_deg_list]
    
    
    
    for idx,(phase_anim,antennas) in enumerate(zip(phase_offset_deg_list,num_antennas)):
        
        # generate 2 2d grids for the x & y bounds
        y, x = np.mgrid[slice(-1, 1 + dy, dy),
                        slice(-1, 1 + dx, dx)]
        
        z_list= []
        for xx,yy,phase_offset in zip(x_list[:antennas],y_list,phase_deg_list):
            phase_shift = 2*np.pi*(phase_offset + phase_anim)/360
            zz = np.real(np.exp(1j*phase_shift)*np.exp(np.pi*2/wavelength*-1j*np.sqrt((x-xx)**2 + (y-yy)**2)))
            z_list += [zz]
        
        
        z = sum(z_list)/len(z_list)
        
        # x and y are bounds, so z should be the value *inside* those bounds.
        # Therefore, remove the last value from the z array.
        z = z[:-1, :-1]
        levels = MaxNLocator(nbins=15).tick_values(z.min(), z.max())
        
        
        # pick the desired colormap, sensible levels, and define a normalization
        # instance which takes data values and translates those into levels.
        cmap = plt.get_cmap('bwr')
        norm = BoundaryNorm(levels, ncolors=cmap.N, clip=True)
        
        fig, (ax1) = plt.subplots(figsize=(12,10),dpi=75)
        
        
        # contours are *point* based plots, so convert our bound into point
        # centers
        ax1.plot(x_list,y_list,'o',linestyle='',markerfacecolor='black')
        ax1.plot(x_list[:antennas],y_list[:antennas],'ko',linestyle='',markerfacecolor='white')
        cf = ax1.contourf(x[:-1, :-1] + dx/2.,
                          y[:-1, :-1] + dy/2., z, levels=levels,
                          cmap=cmap,
                          vmin=-1, vmax=1.0)
        #plt.clim(-1,1)
        fig.colorbar(cf, ax=ax1)
        ax1.set_title('Number of Antennas = %i' % antennas)
        
        
        # adjust spacing between subplots so `ax1` title and `ax0` tick labels
        # don't overlap
        fig.tight_layout()
        plt.savefig('image-%03i.png' % idx)
        
        plt.show()
    
    

    No comments:

    Post a Comment