Making Gifs with Matplotlib and Recaman

Exploring the Recaman sequence and making gifs with matplotlib.

Making Gifs with Matplotlib and Recaman

Making Gifs with Matplotlib and Recaman

This notebook is meant to be a tutorial on generating an animated gif using matplotlib. The subject to be animated is an implementation of the Racaman Sequence.

Helpful Resources:

In [1]:
import matplotlib.pyplot as plt
from matplotlib import patches
from matplotlib import animation
from matplotlib import rc
import os
from typing import List, Union
%matplotlib inline
from IPython.display import HTML

Define the Sequence

First we need to create an implementation of the Recaman Sequence. Here the set object is used to maintain a list of values of n, that have already been seen. While a set (sequence) is a useful object to check for member presence (due to hashing), a list (recaman_series) is used to maintain sequence ordering.

In [2]:
def recaman(n: int) -> List[int]:
    """
    Compute the values in the Recaman Sequence up to a N members
    Args:
        n (int): number of sequence members to compute

    Returns:
        recaman_series (list): The Recaman Sequence up to N members
    """
    
    # Check to see if the sequence count is <= 0
    if n <= 0 or not isinstance(n, int):
        raise ValueError(f'Invalid Input, must be positive integer')
    
    # initialize a set (monitor previous seen values) and a list (container for sequence results)
    sequence = {0}
    recaman_series = [0]

    previous_value = 0
    for i in range(1, n):
        # check backwards direction since the sequence must go back if possible
        current_value = previous_value - i
        # If the value is negative or already exists.
        if (current_value < 0 or current_value in sequence):
            current_value = previous_value + i

        sequence.add(current_value)
        recaman_series.append(current_value)
        previous_value = current_value  # move the current value forward
    return recaman_series

We want to grab a small set of the sequence to make sure it returns the expected results

In [3]:
N = 15
print(f'The first {N} members of the Recaman Sequence are: {recaman(N)}')
The first 15 members of the Recaman Sequence are: [0, 1, 3, 6, 2, 7, 13, 20, 12, 21, 11, 22, 10, 23, 9]

Plotting

In order to plot the sequence in the semi-circle style, we need to first be able to calculate the appropriate plot arcs. This is done using matplotlib's patches library to generate the appropriate arc segment for each member of the sequence.

In [4]:
def compute_arcs(sequence: List[int]) -> List:
    """
    Compute each of the arc segments for each member of a Recaman Sequence
    to be plotted using matplotlib
    Args:
        sequence (list): A Recaman Sequence

    Returns:
        arcs (list): A collection of matplotlib arc segments for
            each member of the sequence
        
    """
    arcs = []
    for i in range(len(sequence) - 1):
        # midway point should be between sequence members on the x-axis
        arc_center = (0.5 * (sequence[i] + sequence[i + 1]), 0)
        # arc heigh and width is proportional to the sequence element number
        arc_width = abs(sequence[i + 1] - sequence[i])
        arc_height = abs(sequence[i + 1] - sequence[i])
        # slternate orientation of the arc (convex up or down)
        arc_angle = (pow(-1, i + 1) + 1) / 2 * 180
        # arc will always span 180 degrees
        start_angle = 180
        end_angle = 360
        # create an arc segment
        cur_arc = patches.Arc(
            arc_center,
            arc_width,
            arc_height,
            angle=arc_angle,
            theta1=start_angle,
            theta2=end_angle,
            linewidth=1,
            fill=False
        )
        arcs.append(cur_arc)
    return arcs

With the ability to calculate the arc segment for each member of the sequence, a figure can be plotted

In [5]:
def plot_recaman(sequence: List[int]) -> None:
    """
    Plot a semi-circular representation of a Recaman Sequence
    Args:
        sequence (list): A Recaman Sequence

    Returns:
        N/A
    """
    
    # create the arcs associated with each sequence member
    arcs = compute_arcs(sequence)
    # create a figure and all each of the arc segments in order
    fig, ax = plt.subplots(figsize=(9.5, 6))
    for arc in arcs:
        ax.add_patch(arc)
    # set the plot limits to encompas the entire sequence
    buffer_factor = 1.1  # buffer to give the figure breathing room
    ax.set_xlim(-(buffer_factor - 1) * max(sequence), buffer_factor * max(sequence))
    ax.set_ylim(-0.5 * buffer_factor * len(sequence), 0.5 * buffer_factor * len(sequence))
    ax.set_aspect('equal')
    ax.set_title(f'Recaman Sequence\nN=({len(sequence)})')
    fig.tight_layout()
    plt.show()
    
In [6]:
N = 25
plot_recaman(recaman(N))

Animated Gif

To add some interest to the plot, we can animate it so that each arc segment is drawn for each member of the sequence.

In [7]:
def animate_recaman(sequence: List,
                    duration_sec: int = 5,
                    bitrate: int = 1000,
                    dpi: int = 100,
                    embedded: bool = False,
                    ) -> Union[str, HTML]:
    """
    Animate the plotting of a Recaman Sequence
    Args:
        sequence (list): Recaman sequence
        duration_sec (int): duration of the animation, determines frames per second (fps)
        bitrate (int): bits per second used to compress the output animation
        dpi (int): dots per inch of the resulting animtaion
        embedded (bool): flag to embed the animation as html5 (for use in a notebook)

    Returns:
        save_filename (str): The file name of the saved animation
        html_video (HTML): A HTML5 encoded video of the animation

    """
    fig, ax = plt.subplots(dpi=dpi)
    edge_factor = 1.05  # set a 5% buffer to give the graph breathing room
    # set the width of the plot to cover the sequence span while centering it
    ax.set_xlim(-(edge_factor - 1) * max(sequence), edge_factor * max(sequence))
    #set the height of the graph to cover the arcs
    ax.set_ylim(-0.5 * edge_factor * len(sequence), 0.5 * edge_factor * len(sequence))
    ax.set_aspect('equal')
    ax.set_title(f"Recaman's Squence\nN={len(sequence)}")
    # add a watermark to the figure
    fig.text(0.95, 0.05, '@BrentonMallen',
             fontsize=12, color='black',
             ha='right', va='bottom', alpha=0.75)

    # generate the arcs to plot
    arcs = compute_arcs(sequence)
    # calculate the fps based off desired duration
    fps = len(sequence) // duration_sec
    
    if embedded:
        
        def animate(i):
            """
            Function used to update the figure
            """
            return ax.add_patch(i),

        anim = animation.FuncAnimation(fig,
                                       animate,
                                       frames=arcs,
                                       interval=100,
                                       blit=True
                                      )
        plt.close()  # prevent the static figure from displaying (the final frame)
        html_video = HTML(anim.to_html5_video())
        return html_video
        
    GifWriter = animation.ImageMagickFileWriter(fps,
                                             bitrate=bitrate
                                             )
    save_filename = f'recaman_{len(sequence)}.gif'
    with GifWriter.saving(fig, save_filename, dpi=dpi):
        for arc in arcs:
            ax.add_patch(arc)
            GifWriter.grab_frame()
    return f'Animation saved to: {save_filename}'

Embedding the Animation

To display the animation in this notebook, it has to be created as a HTML5 video and embedded. To do this, the FuncAnimation function is used. It differs from the ImageMagickFileWriter (used to save a Gif as a file) in that it requires a set of frames and a function to definte the animation.

In this case, frames in FuncAnimation is the list of arcs generated (an iterable) and the function animate is used to add each arc to the figure as it updates for each frame.

In [8]:
# specify that we want the animation to be displayed using html5 (html is default)
rc('animation', html='html5')

animate_recaman(recaman(100), embedded=True)
Out[8]:

Saving The Gif to File

The animate_recaman function is written in a way that allows for saving the gif off to a file or converting it to an HTML5 video so that it can be embedded in a notebook (as seen here)

Note:

The GifWriter will generate temporary png files in the same directory for each frame and then combine those to generate the gif. If for some reason, the GifWriter fails, those files will remain and have to be manually deleted.