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:
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.
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
N = 15
print(f'The first {N} members of the Recaman Sequence are: {recaman(N)}')
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.
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
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()
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.
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.
# specify that we want the animation to be displayed using html5 (html is default)
rc('animation', html='html5')
animate_recaman(recaman(100), embedded=True)
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.