Animated graph models with python interactive

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.