During my work with graphs, I inspected a lot of random graph models and their behaviour with interactive notebooks. Here, I needed to create an animation of the evolution of an Erdos-Reny random graph model and save the animation in an .mp4 file. The finished notebook can be found in animate.ipynb alongside the environment.yml for reproduction.
Environment Reproduction
The environment.yml contains:
name: sur-blog-animated-graph-models
channels:
- defaults
- conda-forge
dependencies:
- ipywidgets
- jupyter
- matplotlib
- networkx
- numpy
- pandas
- pip
- plotly
- python>=3.11
- seaborn
- tqdm
and my imports at the top are
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import networkx as nx
import ipywidgets as widgets
from ipywidgets import interact, interactive, fixed, interact_manual
from IPython.display import display
from IPython.display import HTML
You can create the conda/mamba environment with conda env create -f environment.yml
and then activate it through mamba activate sur-blog-animated-graph-models
.
Black background in matplotlib
As I am using the animation in a manim video, I want the background to be black or transparent. For matplotlib I am using the following configuration to achieve this:
cfg_plot = {
'figure.figsize': (24, 24),
'font.weight': 'normal',
'font.size': 30,
'lines.linewidth': 3,
'lines.markersize': 20
}
plt.rcParams.update(cfg_plot)
plt.rcParams.update({
"lines.color": "white",
"patch.edgecolor": "black",
"text.color": "black",
"axes.facecolor": "white",
"axes.edgecolor": "white",
"axes.labelcolor": "white",
"xtick.color": "white",
"ytick.color": "white",
"grid.color": "black",
"figure.facecolor": "black",
"figure.edgecolor": "black",
"savefig.facecolor": "black",
"savefig.edgecolor": "black"
})
plt.style.use('dark_background')
NetworkX Drawing
A function for drawing a single sampled random graph via networkx:
def visualize(n: int, p: float, ax=None, pos=None):
assert n > 0
if 0 > p or p > 1:
print(f"Can not have $p\notin(0,1)$")
return
graph = nx.erdos_renyi_graph(n, p)
pos = nx.circular_layout(graph)
nx.draw_networkx_nodes(graph, pos, node_color="#ffffff", node_size=2000)
nx.draw_networkx_edges(graph, pos, edge_color="#ffffff", width=5)
nx.draw(graph, pos=pos, ax=ax)
In total, the animation should sample graphs of graph order $n = 20$ and position it in a circular layout. I tried calculating a first initial layout (the pos variable) to position all subsequent graphs in the same manner. However, when redrawing some of the graphs, some vertices have been differently positioned and I could not figure out why.
n_frames = 200
p_per_frame = {frame: round(frame/n_frames, 2) for frame in range(n_frames)}
n_vertices = 20
fig, ax = plt.subplots()
graph = nx.Graph()
graph = nx.erdos_renyi_graph(n_vertices, 0.8)
pos = nx.circular_layout(graph)
nx.draw(graph, pos=pos, ax=ax)
plt.clf()
The actual animation works with matplotlib animation.FuncAnimation and an update function in which we clear out the existing figure and re-draw the entire graph. Re-drawing only the edges seemed to be not as straightforward or easy with networkx and the solution worked, so I kept it that way, although it takes way more memory. There’s also a warning for a high number of frames that some memory constraint is exceeded as so many re-drawings of the whole figure have to be computed.
def update(frame):
fig.clf() # Clear the previous plot
visualize(n=30, p=p_per_frame[frame], ax=ax, pos=pos) # Draw new graph
return frame
ani = animation.FuncAnimation(fig=fig, func=update, frames=n_frames, interval=500)
ani.save("erdosrenyi.mp4")
#HTML(ani.to_jshtml()) # You can show the animation live in jupyter notebook
The result is written in a file erdosrenyi.mp4.