make alphafold diffusion model animation clip using this references

Reading File
Running Command

cd /home/daytona/workspace/attachments/58e67170-4be3-4179-95e6-0a5015129f4e && p…

Running Command

cd /home/daytona/workspace/attachments/58e67170-4be3-4179-95e6-0a5015129f4e && p…

Running Command

mkdir -p /home/daytona/workspace/alphafold-animation && python3 -c "import matpl…

Writing File

~/alphafold-animation/alphafold_diffusion.py

"""
AlphaFold 3 Diffusion Model Animation
Based on references:
  - "how AlphaFold actually works"
  - "The brilliance of AlphaFold 3"
  - "What Is AlphaFold" (NEJM)

Scenes:
  0  Title card
  1  Protein folding problem - 1D sequence -> 3D structure
  2  AlphaFold 3 inputs  (protein / DNA / RNA / ligand)
  3  MSA + pair representation encoding
  4  Diffusion concept  - noise to structure
  5  Denoising steps    - atoms emerge from cloud
  6  Final structure    - clean 3D protein backbone
  7  Applications       - drug discovery, variants, PPIs
  8  End card
"""

import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
from matplotlib.animation import FuncAnimation, FFMpegWriter
from matplotlib.patches import FancyArrowPatch, Circle, FancyBboxPatch
from matplotlib.colors import LinearSegmentedColormap
import matplotlib.cm as cm

rng = np.random.default_rng(42)

# ── palette ──────────────────────────────────────────────────────────────────
BG        = "#0a0e1a"
BLUE      = "#4fc3f7"
CYAN      = "#00e5ff"
PURPLE    = "#ce93d8"
PINK      = "#f48fb1"
GREEN     = "#a5d6a7"
YELLOW    = "#fff176"
ORANGE    = "#ffcc80"
WHITE     = "#ffffff"
GREY      = "#546e7a"

# ── helpers ───────────────────────────────────────────────────────────────────
def clear(ax):
    ax.cla()
    ax.set_facecolor(BG)
    ax.axis("off")

def title_text(ax, txt, y=0.93, size=20, color=CYAN):
    ax.text(0.5, y, txt, transform=ax.transAxes,
            ha="center", va="top", fontsize=size, color=color,
            fontweight="bold",
            path_effects=[pe.withStroke(linewidth=3, foreground=BG)])

def subtitle(ax, txt, y=0.06, size=11, color=GREY):
    ax.text(0.5, y, txt, transform=ax.transAxes,
            ha="center", va="bottom", fontsize=size, color=color,
            style="italic")

# ── scene helpers ─────────────────────────────────────────────────────────────

def amino_acid_sequence(ax, t):
    """1-D amino acid string scrolling left."""
    aas = list("MKTAYIAKQRQISFVKSHFSRQ")
    colors = [BLUE, GREEN, PINK, ORANGE, PURPLE, YELLOW, CYAN]
    for i, aa in enumerate(aas):
        x = 0.04 + i * 0.044 - t * 0.008
        if 0.01 < x < 0.99:
            col = colors[i % len(colors)]
            ax.text(x, 0.62, aa, transform=ax.transAxes,
                    ha="center", va="center", fontsize=14,
                    color=col, fontweight="bold",
                    path_effects=[pe.withStroke(linewidth=2, foreground=BG)])
            # connecting line
            if i > 0:
                x_prev = 0.04 + (i-1) * 0.044 - t * 0.008
                if 0.01 < x_prev < 0.99:
                    ax.annotate("", xy=(x - 0.018, 0.62),
                                xytext=(x_prev + 0.018, 0.62),
                                xycoords="axes fraction", textcoords="axes fraction",
                                arrowprops=dict(arrowstyle="-", color=GREY, lw=1.2))


def draw_protein_blob(ax, cx, cy, r=0.18, alpha=1.0, color=BLUE):
    """Draw a rough blob representing a folded protein."""
    theta = np.linspace(0, 2*np.pi, 80)
    np.random.seed(7)
    noise = np.random.rand(80)*0.04
    radii = r + noise
    xs = cx + radii * np.cos(theta)
    ys = cy + radii * np.sin(theta)
    ax.fill(xs, ys, color=color, alpha=alpha * 0.35)
    ax.plot(xs, ys, color=color, lw=1.5, alpha=alpha)


def draw_backbone(ax, pts, color=CYAN, lw=2, alpha=1.0):
    xs = [p[0] for p in pts]
    ys = [p[1] for p in pts]
    ax.plot(xs, ys, color=color, lw=lw, alpha=alpha)
    ax.scatter(xs, ys, s=25, color=color, alpha=alpha, zorder=5)


def noisy_backbone(pts, sigma):
    """Add Gaussian noise to backbone points."""
    pts = np.array(pts)
    return pts + rng.normal(0, sigma, pts.shape)


# ── generate clean backbone path ──────────────────────────────────────────────
def make_clean_backbone():
    """Helical backbone for a 20-residue segment."""
    t = np.linspace(0, 4 * np.pi, 20)
    xs = 0.5 + 0.25 * np.cos(t)
    ys = 0.2 + 0.55 * (t / (4*np.pi))
    return list(zip(xs.tolist(), ys.tolist()))

CLEAN_BACKBONE = make_clean_backbone()


# ── SCENE FUNCTIONS  (each returns frames_count) ──────────────────────────────

TOTAL_FRAMES = 0  # filled later

def scene_title(ax, f):
    """Scene 0 - Title (30 frames)."""
    clear(ax)
    alpha = min(1.0, f / 10)
    ax.text(0.5, 0.60, "AlphaFold 3", transform=ax.transAxes,
            ha="center", va="center", fontsize=34, color=CYAN,
            fontweight="bold", alpha=alpha,
            path_effects=[pe.withStroke(linewidth=5, foreground=BG)])
    ax.text(0.5, 0.44, "Diffusion Model Animation", transform=ax.transAxes,
            ha="center", va="center", fontsize=18, color=WHITE,
            alpha=alpha*0.9,
            path_effects=[pe.withStroke(linewidth=3, foreground=BG)])
    ax.text(0.5, 0.30, "How atoms emerge from noise", transform=ax.transAxes,
            ha="center", va="center", fontsize=13, color=PURPLE,
            style="italic", alpha=alpha*0.8)
    # bottom credit
    ax.text(0.5, 0.10, "Based on: AlphaFold 3 (Abramson et al., 2024)  |  DeepMind",
            transform=ax.transAxes, ha="center", va="center",
            fontsize=9, color=GREY, alpha=alpha*0.7)
    # decorative dots
    for i in range(40):
        x = rng.uniform(0.02, 0.98)
        y = rng.uniform(0.02, 0.98)
        s = rng.uniform(2, 12)
        c = rng.choice([BLUE, PURPLE, CYAN, GREEN])
        ax.scatter(x, y, s=s, color=c, alpha=alpha * rng.uniform(0.1, 0.4),
                   transform=ax.transAxes, zorder=1)


def scene_protein_problem(ax, f):
    """Scene 1 - Protein folding problem (40 frames)."""
    clear(ax)
    alpha = min(1.0, f / 8)
    title_text(ax, "The Protein Folding Problem", color=CYAN)
    subtitle(ax, "50+ years unsolved — until AlphaFold")

    # 1-D sequence
    ax.text(0.05, 0.77, "1D Amino Acid Sequence:", transform=ax.transAxes,
            fontsize=11, color=WHITE, alpha=alpha)
    amino_acid_sequence(ax, f)

    # arrow down
    if f > 10:
        a2 = min(1.0, (f-10)/8)
        ax.annotate("", xy=(0.5, 0.43), xytext=(0.5, 0.56),
                    xycoords="axes fraction", textcoords="axes fraction",
                    arrowprops=dict(arrowstyle="-|>", color=YELLOW,
                                   lw=2, mutation_scale=18),
                    annotation_clip=False)
        ax.text(0.54, 0.50, "How does it\nfold?", transform=ax.transAxes,
                fontsize=10, color=YELLOW, alpha=a2, va="center")

    # 3D blob
    if f > 18:
        a3 = min(1.0, (f-18)/10)
        draw_protein_blob(ax, 0.5, 0.28, r=0.16, alpha=a3)
        ax.text(0.5, 0.10, "3D Folded Structure", transform=ax.transAxes,
                ha="center", fontsize=11, color=GREEN, alpha=a3)

    # methods fade in
    if f > 28:
        a4 = min(1.0, (f-28)/8)
        methods = ["X-ray crystallography", "NMR spectroscopy", "Cryo-EM"]
        for i, m in enumerate(methods):
            ax.text(0.05 + i*0.32, 0.02, m, transform=ax.transAxes,
                    fontsize=8, color=GREY, alpha=a4*0.8, ha="left")


def scene_af3_inputs(ax, f):
    """Scene 2 - AF3 handles multiple modalities (35 frames)."""
    clear(ax)
    alpha = min(1.0, f / 8)
    title_text(ax, "AlphaFold 3 — Multiple Modalities", color=PURPLE)
    subtitle(ax, "Proteins, DNA, RNA, and Ligands — all in one model")

    items = [
        (0.18, 0.55, "🧬  Protein",  BLUE,   "Amino acid chains"),
        (0.50, 0.55, "🧪  DNA/RNA",  GREEN,  "Nucleic acids"),
        (0.82, 0.55, "💊  Ligand",   ORANGE, "Drug candidates"),
    ]
    for i, (x, y, label, col, sub) in enumerate(items):
        delay = i * 8
        if f > delay:
            a = min(1.0, (f - delay) / 8)
            box = FancyBboxPatch((x-0.13, y-0.12), 0.26, 0.24,
                                 boxstyle="round,pad=0.02",
                                 linewidth=2, edgecolor=col,
                                 facecolor=col + "22",
                                 transform=ax.transAxes, zorder=3,
                                 alpha=a)
            ax.add_patch(box)
            ax.text(x, y+0.04, label, transform=ax.transAxes,
                    ha="center", fontsize=13, color=col,
                    fontweight="bold", alpha=a)
            ax.text(x, y-0.06, sub, transform=ax.transAxes,
                    ha="center", fontsize=9, color=WHITE, alpha=a*0.8)

    # AF3 box
    if f > 25:
        a = min(1.0, (f-25)/8)
        box = FancyBboxPatch((0.30, 0.12), 0.40, 0.16,
                             boxstyle="round,pad=0.02",
                             linewidth=2.5, edgecolor=CYAN,
                             facecolor=CYAN + "22",
                             transform=ax.transAxes, zorder=3, alpha=a)
        ax.add_patch(box)
        ax.text(0.50, 0.20, "AlphaFold 3 — Unified Model",
                transform=ax.transAxes, ha="center", fontsize=12,
                color=CYAN, fontweight="bold", alpha=a)
        # arrows pointing down
        for x in [0.18, 0.50, 0.82]:
            ax.annotate("", xy=(0.50, 0.28), xytext=(x, 0.43),
                        xycoords="axes fraction", textcoords="axes fraction",
                        arrowprops=dict(arrowstyle="-|>", color=GREY,
                                       lw=1.2, mutation_scale=12),
                        annotation_clip=False)


def scene_msa_pair(ax, f):
    """Scene 3 - MSA + Pair representation (40 frames)."""
    clear(ax)
    alpha = min(1.0, f / 8)
    title_text(ax, "Input Representations", color=BLUE)
    subtitle(ax, "MSA captures evolution  |  Pair encodes residue relationships")

    n = 8  # matrix size
    # MSA matrix
    if f > 5:
        a = min(1.0, (f-5)/8)
        ax.text(0.16, 0.83, "MSA (Multiple Sequence Alignment)",
                transform=ax.transAxes, ha="center", fontsize=11,
                color=GREEN, alpha=a)
        for i in range(n):
            for j in range(n):
                val = rng.random()
                col = plt.cm.Greens(val * 0.8 + 0.2)
                rect = mpatches.FancyBboxPatch(
                    (0.02 + j*0.027, 0.48 + i*0.032), 0.024, 0.028,
                    boxstyle="square,pad=0", linewidth=0.3,
                    edgecolor=BG, facecolor=col,
                    transform=ax.transAxes, alpha=a*0.85)
                ax.add_patch(rect)
        ax.text(0.16, 0.44, "Species / Sequences →",
                transform=ax.transAxes, ha="center", fontsize=8,
                color=GREY, alpha=a)

    # Pair representation matrix
    if f > 15:
        a = min(1.0, (f-15)/8)
        ax.text(0.60, 0.83, "Pair Representation",
                transform=ax.transAxes, ha="center", fontsize=11,
                color=PURPLE, alpha=a)
        for i in range(n):
            for j in range(n):
                val = np.exp(-((i-j)**2)/8.0) + rng.random()*0.2
                col = plt.cm.Purples(min(val * 0.6 + 0.1, 1.0))
                rect = mpatches.FancyBboxPatch(
                    (0.38 + j*0.034, 0.48 + i*0.034), 0.031, 0.031,
                    boxstyle="square,pad=0", linewidth=0.3,
                    edgecolor=BG, facecolor=col,
                    transform=ax.transAxes, alpha=a*0.85)
                ax.add_patch(rect)
        ax.text(0.60, 0.44, "Residue i vs Residue j (distances / angles)",
                transform=ax.transAxes, ha="center", fontsize=8,
                color=GREY, alpha=a)

    # Triangle inequality note
    if f > 26:
        a = min(1.0, (f-26)/8)
        ax.text(0.50, 0.35, "Triangular Attention",
                transform=ax.transAxes, ha="center", fontsize=13,
                color=YELLOW, fontweight="bold", alpha=a)
        ax.text(0.50, 0.27,
                "Respects triangle inequality:\n"
                "d(A,B) ≤ d(A,C) + d(C,B)",
                transform=ax.transAxes, ha="center", fontsize=10,
                color=WHITE, alpha=a*0.9)

    # Arrow MSA -> pair
    if f > 20:
        a = min(1.0, (f-20)/8)
        ax.annotate("", xy=(0.37, 0.64), xytext=(0.28, 0.64),
                    xycoords="axes fraction", textcoords="axes fraction",
                    arrowprops=dict(arrowstyle="-|>", color=YELLOW,
                                   lw=2, mutation_scale=14), alpha=a,
                    annotation_clip=False)
        ax.text(0.325, 0.68, "info flows →",
                transform=ax.transAxes, ha="center", fontsize=8,
                color=YELLOW, alpha=a)


def scene_diffusion_concept(ax, f):
    """Scene 4 - Diffusion concept (45 frames)."""
    clear(ax)
    alpha = min(1.0, f / 8)
    title_text(ax, "The Diffusion Idea", color=ORANGE)
    subtitle(ax, "Same magic as AI art (Midjourney, DALL-E) — applied to atoms")

    # 5 stages: clean -> noisy
    stages = 5
    for s in range(stages):
        delay = s * 6
        if f < delay:
            continue
        a = min(1.0, (f - delay) / 6)
        noise_lvl = s / (stages - 1)  # 0 (clean) -> 1 (pure noise)
        cx = 0.10 + s * 0.185
        cy = 0.55

        pts = np.array(CLEAN_BACKBONE[:12])
        # scale to local view
        pts_x = cx + (pts[:, 0] - 0.5) * 0.16
        pts_y = cy + (pts[:, 1] - 0.5) * 0.30
        pts_local = np.stack([pts_x, pts_y], axis=1)

        if noise_lvl < 0.05:
            sigma = 0
        else:
            sigma = noise_lvl * 0.06
        pts_noisy = pts_local + rng.normal(0, sigma, pts_local.shape)

        # draw cloud if noisy enough
        if noise_lvl > 0.2:
            for _ in range(int(30 * noise_lvl)):
                nx = rng.normal(cx, 0.05 * noise_lvl)
                ny = rng.normal(cy, 0.09 * noise_lvl)
                ax.scatter(nx, ny, s=6, color=ORANGE,
                           alpha=a * noise_lvl * 0.5,
                           transform=ax.transAxes)

        color = plt.cm.RdYlBu_r(noise_lvl)
        draw_backbone(ax, pts_noisy.tolist(), color=color,
                      lw=max(0.5, 2-noise_lvl*1.5), alpha=a)

        # label
        label = ["Clean", "+Noise", "Noisier", "Very\nNoisy", "Pure\nNoise"][s]
        ax.text(cx, cy - 0.22, label, transform=ax.transAxes,
                ha="center", fontsize=9, color=WHITE, alpha=a * 0.9)

    # arrow left to right
    if f > 10:
        a = min(1.0, (f-10)/6)
        ax.annotate("", xy=(0.93, 0.55), xytext=(0.07, 0.55),
                    xycoords="axes fraction", textcoords="axes fraction",
                    arrowprops=dict(arrowstyle="-|>", color=GREY,
                                   lw=1.5, mutation_scale=16),
                    annotation_clip=False)
        ax.text(0.50, 0.78, "Training: add noise",
                transform=ax.transAxes, ha="center", fontsize=11,
                color=ORANGE, alpha=a)

    if f > 30:
        a = min(1.0, (f-30)/8)
        ax.text(0.50, 0.20,
                "Inference: reverse the process — denoise random cloud → structure",
                transform=ax.transAxes, ha="center", fontsize=11,
                color=CYAN, alpha=a)


def scene_denoising(ax, f, total=60):
    """Scene 5 - Denoising steps (60 frames)."""
    clear(ax)
    title_text(ax, "Diffusion Denoising — Atoms Emerge from Noise", color=CYAN)
    subtitle(ax, "AlphaFold 3 predicts atom positions step by step")

    progress = f / total  # 0 -> 1

    n_atoms = 30
    rng2 = np.random.default_rng(99)
    # Final positions (clean structure helix)
    t = np.linspace(0, 3 * np.pi, n_atoms)
    final_x = 0.50 + 0.22 * np.cos(t)
    final_y = 0.18 + 0.60 * (t / (3*np.pi))
    # Random start positions
    start_x = rng2.uniform(0.1, 0.9, n_atoms)
    start_y = rng2.uniform(0.1, 0.9, n_atoms)

    # Interpolate
    ease = progress ** 1.5  # ease-in
    cur_x = start_x + ease * (final_x - start_x)
    cur_y = start_y + ease * (final_y - start_y)

    # Residual noise decreasing
    noise_sigma = (1 - ease) * 0.06
    cur_x += rng2.normal(0, noise_sigma, n_atoms)
    cur_y += rng2.normal(0, noise_sigma, n_atoms)

    # Background noise cloud fading out
    n_cloud = int(200 * (1 - ease))
    if n_cloud > 0:
        cx = rng2.uniform(0.05, 0.95, n_cloud)
        cy = rng2.uniform(0.05, 0.95, n_cloud)
        ax.scatter(cx, cy, s=4, color=ORANGE,
                   alpha=(1-ease)*0.3, transform=ax.transAxes, zorder=1)

    # Atom colors by element type
    colors = [BLUE if i % 3 == 0 else GREEN if i % 3 == 1 else PINK
              for i in range(n_atoms)]
    sizes = [60 if i % 5 == 0 else 30 for i in range(n_atoms)]

    ax.scatter(cur_x, cur_y, s=sizes, c=colors, alpha=0.85,
               transform=ax.transAxes, zorder=4)

    # Draw bonds between close atoms (final stage)
    if ease > 0.4:
        bond_alpha = min(1.0, (ease - 0.4) / 0.4)
        for i in range(n_atoms - 1):
            ax.plot([cur_x[i], cur_x[i+1]],
                    [cur_y[i], cur_y[i+1]],
                    color=CYAN, lw=1.0,
                    alpha=bond_alpha * 0.6,
                    transform=ax.transAxes, zorder=3)

    # Step counter
    step_shown = int(progress * 200)
    ax.text(0.85, 0.92, f"Step {step_shown}/200",
            transform=ax.transAxes, ha="center", fontsize=11,
            color=YELLOW, fontweight="bold")

    # Progress bar
    bar_w = 0.70 * progress
    ax.add_patch(mpatches.FancyBboxPatch(
        (0.15, 0.05), bar_w, 0.025,
        boxstyle="round,pad=0.002",
        facecolor=CYAN, edgecolor="none",
        transform=ax.transAxes, alpha=0.7))
    ax.add_patch(mpatches.FancyBboxPatch(
        (0.15, 0.05), 0.70, 0.025,
        boxstyle="round,pad=0.002",
        facecolor="none", edgecolor=GREY, lw=1,
        transform=ax.transAxes))
    ax.text(0.50, 0.035, "Denoising Progress",
            transform=ax.transAxes, ha="center", fontsize=9, color=GREY)

    # Noise level label
    noise_pct = int((1 - ease) * 100)
    ax.text(0.15, 0.92, f"Noise: {noise_pct}%",
            transform=ax.transAxes, ha="left", fontsize=11,
            color=ORANGE)


def scene_final_structure(ax, f):
    """Scene 6 - Final structure revealed (40 frames)."""
    clear(ax)
    alpha = min(1.0, f / 10)
    title_text(ax, "Predicted 3D Atomic Structure", color=GREEN)
    subtitle(ax, "Final coordinates for every atom in the complex")

    # Draw helix backbone
    n = 40
    t = np.linspace(0, 6 * np.pi, n)
    xs = 0.50 + 0.20 * np.cos(t)
    ys = 0.15 + 0.70 * (t / (6*np.pi))

    # Fade in ribbon
    n_show = max(2, int(n * min(1.0, f / 20)))
    for i in range(n_show - 1):
        frac = i / n
        col = plt.cm.cool(frac)
        ax.plot([xs[i], xs[i+1]], [ys[i], ys[i+1]],
                color=col, lw=4, alpha=alpha,
                transform=ax.transAxes, solid_capstyle='round')

    # Atoms
    if f > 8:
        a_atoms = min(1.0, (f-8)/8)
        cols = [BLUE if i % 3 == 0 else GREEN if i % 3 == 1 else PINK
                for i in range(n_show)]
        ax.scatter(xs[:n_show], ys[:n_show], s=35,
                   c=cols[:n_show], alpha=a_atoms * 0.9,
                   transform=ax.transAxes, zorder=5)

    # Side-chain stubs
    if f > 16:
        a_side = min(1.0, (f-16)/8)
        for i in range(0, n_show, 3):
            dx = 0.03 * np.cos(t[i] + np.pi/3)
            dy = 0.03 * np.sin(t[i] + np.pi/3)
            ax.plot([xs[i], xs[i]+dx], [ys[i], ys[i]+dy],
                    color=ORANGE, lw=1.5, alpha=a_side * 0.8,
                    transform=ax.transAxes)
            ax.scatter(xs[i]+dx, ys[i]+dy, s=15,
                       color=ORANGE, alpha=a_side * 0.9,
                       transform=ax.transAxes, zorder=6)

    # Labels
    if f > 25:
        a_lbl = min(1.0, (f-25)/8)
        ax.text(0.78, 0.75, "α-helix", transform=ax.transAxes,
                fontsize=11, color=CYAN, alpha=a_lbl,
                path_effects=[pe.withStroke(linewidth=2, foreground=BG)])
        ax.annotate("", xy=(0.72, 0.65), xytext=(0.77, 0.73),
                    xycoords="axes fraction", textcoords="axes fraction",
                    arrowprops=dict(arrowstyle="-|>", color=CYAN,
                                   lw=1.2, mutation_scale=10),
                    annotation_clip=False)
        ax.text(0.78, 0.45, "Side chains", transform=ax.transAxes,
                fontsize=10, color=ORANGE, alpha=a_lbl)

    # Accuracy note
    if f > 32:
        a_acc = min(1.0, (f-32)/6)
        ax.text(0.50, 0.06,
                "Accuracy comparable to X-ray crystallography",
                transform=ax.transAxes, ha="center", fontsize=11,
                color=YELLOW, alpha=a_acc,
                path_effects=[pe.withStroke(linewidth=2, foreground=BG)])


def scene_applications(ax, f):
    """Scene 7 - Applications (40 frames)."""
    clear(ax)
    alpha = min(1.0, f / 8)
    title_text(ax, "Applications", color=YELLOW)
    subtitle(ax, "From structure to impact")

    apps = [
        (0.22, 0.65, "💊 Drug\nDiscovery",    ORANGE, 0),
        (0.50, 0.65, "🧬 Genetic\nVariants",  PINK,   10),
        (0.78, 0.65, "🤝 Protein-Protein\nInteractions", BLUE, 20),
        (0.22, 0.30, "🌱 Protein\nEngineering", GREEN,  28),
        (0.50, 0.30, "🔬 Ligand\nBinding",    PURPLE, 34),
        (0.78, 0.30, "🧫 Biomedicine\n& Agriculture", CYAN, 40),
    ]
    for (x, y, label, col, delay) in apps:
        if f > delay:
            a = min(1.0, (f - delay) / 8)
            r = 0.11
            circ = Circle((x, y), r, transform=ax.transAxes,
                          facecolor=col + "25", edgecolor=col,
                          linewidth=2, alpha=a, zorder=3)
            ax.add_patch(circ)
            ax.text(x, y, label, transform=ax.transAxes,
                    ha="center", va="center", fontsize=9.5,
                    color=col, fontweight="bold", alpha=a)


def scene_end(ax, f):
    """Scene 8 - End card (25 frames)."""
    clear(ax)
    alpha = min(1.0, f / 10)
    ax.text(0.5, 0.65, "AlphaFold 3", transform=ax.transAxes,
            ha="center", va="center", fontsize=30, color=CYAN,
            fontweight="bold", alpha=alpha,
            path_effects=[pe.withStroke(linewidth=5, foreground=BG)])
    ax.text(0.5, 0.50, "From noisy atoms to protein structures\nvia diffusion",
            transform=ax.transAxes, ha="center", va="center",
            fontsize=14, color=WHITE, alpha=alpha * 0.9)
    ax.text(0.5, 0.33,
            "Abramson et al., Nature 2024\n"
            "\"Accurate structure prediction of biomolecular interactions\n"
            "with AlphaFold 3\"",
            transform=ax.transAxes, ha="center", va="center",
            fontsize=10, color=GREY, alpha=alpha * 0.8, style="italic")
    ax.text(0.5, 0.10, "Animation generated from reference PDFs",
            transform=ax.transAxes, ha="center", fontsize=9,
            color=GREY, alpha=alpha * 0.6)

    # animated dots orbit
    for i in range(20):
        angle = 2 * np.pi * i / 20 + f * 0.15
        r = 0.40
        x = 0.5 + r * np.cos(angle) * 0.4
        y = 0.5 + r * np.sin(angle) * 0.22
        c = [BLUE, PURPLE, CYAN, GREEN, PINK][i % 5]
        ax.scatter(x, y, s=10, color=c, alpha=alpha * 0.5,
                   transform=ax.transAxes)


# ── scene schedule ────────────────────────────────────────────────────────────
SCENES = [
    (scene_title,           30),
    (scene_protein_problem, 40),
    (scene_af3_inputs,      35),
    (scene_msa_pair,        40),
    (scene_diffusion_concept, 45),
    (scene_denoising,       60),
    (scene_final_structure, 40),
    (scene_applications,    50),
    (scene_end,             30),
]

TOTAL_FRAMES = sum(n for _, n in SCENES)

# Build cumulative frame boundaries
boundaries = []
start = 0
for fn, n in SCENES:
    boundaries.append((start, start + n, fn, n))
    start += n


def animate(global_f):
    for (s, e, fn, n) in boundaries:
        if s <= global_f < e:
            local_f = global_f - s
            if fn is scene_denoising:
                fn(ax, local_f, total=n)
            else:
                fn(ax, local_f)
            return


# ── build figure & run ────────────────────────────────────────────────────────
fig, ax = plt.subplots(figsize=(12, 7), facecolor=BG)
ax.set_facecolor(BG)
ax.axis("off")

print(f"Total frames: {TOTAL_FRAMES}")
print("Rendering animation...")

anim = FuncAnimation(fig, animate, frames=TOTAL_FRAMES,
                     interval=1000/30, blit=False)

writer = FFMpegWriter(fps=30, bitrate=2000,
                      extra_args=["-vcodec", "libx264",
                                  "-pix_fmt", "yuv420p"])

out_path = "/home/daytona/workspace/alphafold-animation/alphafold3_diffusion.mp4"
anim.save(out_path, writer=writer, dpi=120)
plt.close(fig)
print(f"Saved: {out_path}")
Running Command

cd /home/daytona/workspace/alphafold-animation && python3 alphafold_diffusion.py…

Editing File

~/alphafold-animation/alphafold_diffusion.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/alphafold-animation/alphafold_diffusion.py'
Editing File

~/alphafold-animation/alphafold_diffusion.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/alphafold-animation/alphafold_diffusion.py'
Editing File

~/alphafold-animation/alphafold_diffusion.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/alphafold-animation/alphafold_diffusion.py'
Running Command

cd /home/daytona/workspace/alphafold-animation && python3 alphafold_diffusion.py…

Running Command

ffmpeg -version 2>&1 | head -3 && ffmpeg -f lavfi -i testsrc=duration=1:size=640…

Editing File

~/alphafold-animation/alphafold_diffusion.py

Successfully replaced 1 instance(s) of the string in '/home/daytona/workspace/alphafold-animation/alphafold_diffusion.py'
Running Command

cd /home/daytona/workspace/alphafold-animation && python3 alphafold_diffusion.py…

give image atleast

Generating Image

AlphaFold 3 diffusion model scientific diagram, dark navy background, showing the complete pipeline: left side shows a 1D amino acid sequence (colored letters M-K-T-A-Y-I connected by lines), center shows the diffusion process with 5 stages from a clean protein 3D helical backbone gradually dissolving into a noisy cloud of orange scattered atoms, then reversing back (denoising steps numbered 1 to 200), right side shows the final predicted 3D protein structure as a colored ribbon diagram (blue to green gradient helix). Middle section has two matrix grids labeled "MSA Multiple Sequence Alignment" (green heatmap) and "Pair Representation" (purple heatmap) with a "Triangular Attention" label and triangle geometry. Bottom row shows 6 application circles: Drug Discovery, Genetic Variants, Protein-Protein Interactions, Protein Engineering, Ligand Binding, Biomedicine. Top title reads "AlphaFold 3 — Diffusion Model" in cyan. Labels and arrows throughout, clean scientific infographic style, glowing atom particles, professional visualization

AlphaFold 3 Diffusion Model Diagram
Here's the AlphaFold 3 diffusion model diagram covering the full pipeline from your references:
What's shown:
  1. 1D Sequence Input - Amino acid chain (left) feeding into the model
  2. MSA + Pair Representation - The two key encoding matrices; MSA captures evolutionary data across species, Pair Representation encodes residue-residue relationships with Triangular Attention to preserve geometry
  3. Diffusion Process (center) - The core of AF3: a clean protein structure gets progressively noisy during training, then at inference time the model reverses this - starting from a random atom cloud and denoising step-by-step to recover atomic coordinates
  4. Final 3D Structure - Predicted ribbon diagram with atomic positions (right)
  5. Applications (bottom) - Drug discovery, genetic variants, protein-protein interactions, ligand binding, protein engineering, biomedicine
Key insight from your references: Unlike AF2 which used a complex structure module with triangles and torsion angles, AF3 replaces it entirely with diffusion - making it simultaneously simpler and able to handle proteins, DNA, RNA, and ligands all in one unified model. As the "brilliance of AF3" PDF notes: "Instead of worrying about triangles and angles and rotations... we're just going to deal with the positions of every atom."

Generate animations frame-by-frame as GIF or PNG sequence

Writing File

~/alphafold-animation/make_gif.py

"""
AlphaFold 3 Diffusion Model — Animated GIF
9 scenes, 90 frames total @ 12 fps  (~7.5 sec loop)
"""

import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
from matplotlib.patches import Circle, FancyBboxPatch
from PIL import Image
import os, io

# ── palette ──────────────────────────────────────────────────────────────────
BG     = "#0a0e1a"
BLUE   = "#4fc3f7"
CYAN   = "#00e5ff"
PURPLE = "#b39ddb"
PINK   = "#f48fb1"
GREEN  = "#81c784"
YELLOW = "#fff176"
ORANGE = "#ffcc80"
WHITE  = "#ffffff"
GREY   = "#546e7a"
RED    = "#ef9a9a"

RNG = np.random.default_rng(42)

# ── figure setup ─────────────────────────────────────────────────────────────
W, H = 900, 540          # pixels (divisible by 2 for ffmpeg if needed)
DPI  = 100
FIG_W = W / DPI
FIG_H = H / DPI

def new_fig():
    fig, ax = plt.subplots(figsize=(FIG_W, FIG_H), dpi=DPI)
    fig.patch.set_facecolor(BG)
    ax.set_facecolor(BG)
    ax.set_xlim(0, 1); ax.set_ylim(0, 1)
    ax.axis("off")
    return fig, ax

def fig_to_pil(fig):
    buf = io.BytesIO()
    fig.savefig(buf, format="png", facecolor=BG, bbox_inches="tight",
                pad_inches=0)
    buf.seek(0)
    img = Image.open(buf).convert("RGB").resize((W, H), Image.LANCZOS)
    plt.close(fig)
    return img

# ── drawing helpers ───────────────────────────────────────────────────────────
def ttl(ax, txt, y=0.93, size=18, color=CYAN):
    ax.text(0.5, y, txt, transform=ax.transAxes,
            ha="center", va="top", fontsize=size, color=color,
            fontweight="bold",
            path_effects=[pe.withStroke(linewidth=3, foreground=BG)])

def sub(ax, txt, y=0.05, size=9, color=GREY):
    ax.text(0.5, y, txt, transform=ax.transAxes,
            ha="center", va="bottom", fontsize=size, color=color,
            style="italic")

def ease(t, total):
    """Normalised 0->1 with ease-in ramp over first 8 frames."""
    return min(1.0, t / min(8, total))

def backbone_pts(n=18):
    """Helical backbone, normalised 0-1."""
    t = np.linspace(0, 4*np.pi, n)
    xs = 0.5 + 0.22 * np.cos(t)
    ys = 0.15 + 0.68 * (t / (4*np.pi))
    return xs, ys

BX, BY = backbone_pts()

# ══════════════════════════════════════════════════════════════════════════════
# SCENE FUNCTIONS  — each takes (frame_local, frame_total) and returns PIL img
# ══════════════════════════════════════════════════════════════════════════════

def scene_title(f, n):
    fig, ax = new_fig()
    a = ease(f, n)

    # starfield
    rng = np.random.default_rng(1)
    xs = rng.uniform(0, 1, 120); ys = rng.uniform(0, 1, 120)
    sizes = rng.uniform(1, 8, 120)
    cols = rng.choice([BLUE, PURPLE, CYAN, GREEN, WHITE], 120)
    for x, y, s, c in zip(xs, ys, sizes, cols):
        ax.scatter(x, y, s=s*a, color=c, alpha=a*0.4, zorder=1)

    # title
    ax.text(0.5, 0.62, "AlphaFold 3",
            transform=ax.transAxes, ha="center", fontsize=36,
            color=CYAN, fontweight="bold", alpha=a,
            path_effects=[pe.withStroke(linewidth=6, foreground=BG)])
    ax.text(0.5, 0.47, "Diffusion Model",
            transform=ax.transAxes, ha="center", fontsize=20,
            color=WHITE, alpha=a*0.9,
            path_effects=[pe.withStroke(linewidth=3, foreground=BG)])
    ax.text(0.5, 0.35, "How atoms emerge from noise",
            transform=ax.transAxes, ha="center", fontsize=13,
            color=PURPLE, style="italic", alpha=a*0.8)
    ax.text(0.5, 0.10,
            "Based on: Abramson et al. 2024 (AlphaFold 3)  |  DeepMind",
            transform=ax.transAxes, ha="center", fontsize=8,
            color=GREY, alpha=a*0.7)
    return fig_to_pil(fig)


def scene_problem(f, n):
    fig, ax = new_fig()
    a = ease(f, n)
    ttl(ax, "The Protein Folding Problem", color=CYAN)
    sub(ax, "50+ years unsolved — until AlphaFold")

    # 1-D sequence strip
    ax.text(0.03, 0.79, "1D Amino Acid Sequence:", fontsize=10,
            color=WHITE, alpha=a, transform=ax.transAxes)
    aas   = list("MKTAYIAKQRQISFVKSHS")
    clrs  = [BLUE, GREEN, PINK, ORANGE, PURPLE, YELLOW, CYAN, RED]
    for i, aa in enumerate(aas):
        x = 0.05 + i * 0.048
        c = clrs[i % len(clrs)]
        ax.text(x, 0.70, aa, transform=ax.transAxes,
                ha="center", fontsize=12, color=c,
                fontweight="bold", alpha=a,
                path_effects=[pe.withStroke(linewidth=2, foreground=BG)])
        if i > 0:
            ax.annotate("", xy=(x-0.018, 0.70),
                        xytext=(x-0.030, 0.70),
                        xycoords="axes fraction",
                        textcoords="axes fraction",
                        arrowprops=dict(arrowstyle="-", color=GREY, lw=1))

    # arrow down
    a2 = min(1.0, max(0, (f-4)/6))
    if a2 > 0:
        ax.annotate("", xy=(0.5, 0.44), xytext=(0.5, 0.58),
                    xycoords="axes fraction", textcoords="axes fraction",
                    arrowprops=dict(arrowstyle="-|>", color=YELLOW,
                                   lw=2.5, mutation_scale=18))
        ax.text(0.56, 0.52, "How does\nit fold?",
                transform=ax.transAxes, fontsize=9,
                color=YELLOW, alpha=a2)

    # blob
    a3 = min(1.0, max(0, (f-8)/6))
    if a3 > 0:
        t  = np.linspace(0, 2*np.pi, 80)
        rn = np.random.default_rng(7)
        r  = 0.17 + rn.random(80)*0.04
        xs = 0.5 + r*np.cos(t)
        ys = 0.27 + r*np.sin(t)*0.65
        ax.fill(xs, ys, color=BLUE, alpha=a3*0.25, transform=ax.transAxes)
        ax.plot(xs, ys, color=BLUE, lw=2, alpha=a3, transform=ax.transAxes)
        ax.text(0.5, 0.08, "3D Folded Structure",
                transform=ax.transAxes, ha="center",
                fontsize=11, color=GREEN, alpha=a3)
    return fig_to_pil(fig)


def scene_inputs(f, n):
    fig, ax = new_fig()
    a = ease(f, n)
    ttl(ax, "AlphaFold 3 — Multiple Modalities", color=PURPLE)
    sub(ax, "One model handles proteins, DNA, RNA and drug-like ligands")

    items = [
        (0.18, 0.58, "PROTEIN",  "Amino acid chains",  BLUE),
        (0.50, 0.58, "DNA / RNA","Nucleic acids",       GREEN),
        (0.82, 0.58, "LIGAND",   "Drug candidates",     ORANGE),
    ]
    for i, (x, y, lbl, sub_, col) in enumerate(items):
        delay = i * 4
        aa = min(1.0, max(0, (f-delay)/6))
        if aa <= 0: continue
        box = FancyBboxPatch((x-0.14, y-0.13), 0.28, 0.26,
                             boxstyle="round,pad=0.02",
                             linewidth=2, edgecolor=col,
                             facecolor=col+"22",
                             transform=ax.transAxes, alpha=aa)
        ax.add_patch(box)
        ax.text(x, y+0.04, lbl, transform=ax.transAxes,
                ha="center", fontsize=13, color=col,
                fontweight="bold", alpha=aa)
        ax.text(x, y-0.07, sub_, transform=ax.transAxes,
                ha="center", fontsize=9, color=WHITE, alpha=aa*0.85)

    # AF3 unification box
    a_box = min(1.0, max(0, (f-12)/6))
    if a_box > 0:
        box2 = FancyBboxPatch((0.28, 0.12), 0.44, 0.17,
                              boxstyle="round,pad=0.02",
                              linewidth=2.5, edgecolor=CYAN,
                              facecolor=CYAN+"18",
                              transform=ax.transAxes, alpha=a_box)
        ax.add_patch(box2)
        ax.text(0.50, 0.205, "AlphaFold 3 — Unified Embedding Space",
                transform=ax.transAxes, ha="center", fontsize=11,
                color=CYAN, fontweight="bold", alpha=a_box)
        for xi in [0.18, 0.50, 0.82]:
            ax.annotate("", xy=(0.50, 0.29), xytext=(xi, 0.45),
                        xycoords="axes fraction",
                        textcoords="axes fraction",
                        arrowprops=dict(arrowstyle="-|>", color=GREY,
                                       lw=1.2, mutation_scale=11))
    return fig_to_pil(fig)


def scene_msa(f, n):
    fig, ax = new_fig()
    a = ease(f, n)
    ttl(ax, "Input Representations", color=BLUE)
    sub(ax, "MSA captures evolution  |  Pair Representation encodes geometry")

    sz = 9
    rng2 = np.random.default_rng(3)

    # MSA matrix (left)
    a1 = min(1.0, max(0, (f-2)/7))
    if a1 > 0:
        ax.text(0.18, 0.83, "MSA", transform=ax.transAxes,
                ha="center", fontsize=12, color=GREEN,
                fontweight="bold", alpha=a1)
        for i in range(sz):
            for j in range(sz):
                v = rng2.random()
                col = plt.cm.Greens(v*0.75+0.2)
                rect = mpatches.Rectangle(
                    (0.03 + j*0.029, 0.47 + i*0.033), 0.026, 0.030,
                    linewidth=0.4, edgecolor=BG, facecolor=col,
                    transform=ax.transAxes, alpha=a1*0.9)
                ax.add_patch(rect)
        ax.text(0.18, 0.44, "Species / Sequences",
                transform=ax.transAxes, ha="center",
                fontsize=8, color=GREY, alpha=a1)

    # arrow
    a2 = min(1.0, max(0, (f-8)/6))
    if a2 > 0:
        ax.annotate("", xy=(0.40, 0.65), xytext=(0.30, 0.65),
                    xycoords="axes fraction",
                    textcoords="axes fraction",
                    arrowprops=dict(arrowstyle="-|>", color=YELLOW,
                                   lw=2, mutation_scale=14))
        ax.text(0.35, 0.69, "info\nflows",
                transform=ax.transAxes, ha="center",
                fontsize=8, color=YELLOW, alpha=a2)

    # Pair matrix (right)
    a3 = min(1.0, max(0, (f-5)/7))
    if a3 > 0:
        ax.text(0.68, 0.83, "Pair Representation",
                transform=ax.transAxes, ha="center",
                fontsize=12, color=PURPLE, fontweight="bold", alpha=a3)
        for i in range(sz):
            for j in range(sz):
                v = np.exp(-((i-j)**2)/6.0) + rng2.random()*0.18
                col = plt.cm.Purples(min(v*0.55+0.1, 1.0))
                rect = mpatches.Rectangle(
                    (0.42 + j*0.034, 0.47 + i*0.034), 0.031, 0.031,
                    linewidth=0.4, edgecolor=BG, facecolor=col,
                    transform=ax.transAxes, alpha=a3*0.9)
                ax.add_patch(rect)
        ax.text(0.68, 0.44, "Residue i vs Residue j",
                transform=ax.transAxes, ha="center",
                fontsize=8, color=GREY, alpha=a3)

    # triangle inequality
    a4 = min(1.0, max(0, (f-12)/6))
    if a4 > 0:
        ax.text(0.50, 0.32, "Triangular Attention",
                transform=ax.transAxes, ha="center",
                fontsize=14, color=YELLOW, fontweight="bold", alpha=a4)
        ax.text(0.50, 0.20,
                "Enforces:  d(A,B)  ≤  d(A,C) + d(C,B)",
                transform=ax.transAxes, ha="center",
                fontsize=10, color=WHITE, alpha=a4*0.9)
        # small triangle
        tx = [0.34, 0.50, 0.66, 0.34]
        ty = [0.10, 0.17, 0.10, 0.10]
        ax.plot(tx, ty, color=YELLOW, lw=1.5, alpha=a4*0.7,
                transform=ax.transAxes)
        for lbl, (lx, ly) in zip("ABC", [(0.34,0.09),(0.50,0.18),(0.66,0.09)]):
            ax.text(lx, ly, lbl, transform=ax.transAxes,
                    ha="center", fontsize=9, color=YELLOW, alpha=a4)
    return fig_to_pil(fig)


def scene_diffusion_concept(f, n):
    fig, ax = new_fig()
    ttl(ax, "The Diffusion Idea", color=ORANGE)
    sub(ax, "Same magic as Midjourney / DALL-E  — applied to atomic coordinates")

    stages = 5
    rng3 = np.random.default_rng(5)
    labels = ["Clean\nStructure", "+Noise", "Noisier", "Very\nNoisy", "Pure\nNoise"]

    for s in range(stages):
        delay = s * 4
        aa = min(1.0, max(0, (f-delay)/5))
        if aa <= 0: continue

        noise_lvl = s / (stages-1)
        cx = 0.10 + s * 0.195
        cy = 0.55

        # scaled backbone
        bx = cx + (BX - 0.5)*0.14
        by = cy + (BY - 0.5)*0.28
        sigma = noise_lvl * 0.05
        nx = bx + rng3.normal(0, sigma, len(bx))
        ny = by + rng3.normal(0, sigma, len(by))

        # noise cloud
        if noise_lvl > 0.15:
            n_cloud = int(35 * noise_lvl)
            cx2 = rng3.normal(cx, 0.045*noise_lvl, n_cloud)
            cy2 = rng3.normal(cy, 0.08*noise_lvl, n_cloud)
            ax.scatter(cx2, cy2, s=5, color=ORANGE,
                       alpha=aa*noise_lvl*0.55,
                       transform=ax.transAxes)

        col = plt.cm.RdYlBu_r(noise_lvl)
        ax.plot(nx, ny, color=col, lw=max(0.5, 2-noise_lvl*1.5),
                alpha=aa*0.9, transform=ax.transAxes)
        ax.scatter(nx, ny, s=12, color=col, alpha=aa*0.85,
                   transform=ax.transAxes, zorder=5)

        ax.text(cx, cy-0.23, labels[s], transform=ax.transAxes,
                ha="center", fontsize=8, color=WHITE, alpha=aa*0.9)

    # arrow
    a_arr = min(1.0, max(0, (f-8)/5))
    if a_arr > 0:
        ax.annotate("", xy=(0.92, 0.55), xytext=(0.08, 0.55),
                    xycoords="axes fraction",
                    textcoords="axes fraction",
                    arrowprops=dict(arrowstyle="-|>", color=GREY,
                                   lw=1.5, mutation_scale=15))
        ax.text(0.50, 0.79, "TRAINING: add noise",
                transform=ax.transAxes, ha="center",
                fontsize=12, color=ORANGE, alpha=a_arr, fontweight="bold")

    a_inf = min(1.0, max(0, (f-14)/5))
    if a_inf > 0:
        ax.text(0.50, 0.18,
                "INFERENCE: reverse — denoise random cloud  ->  3D structure",
                transform=ax.transAxes, ha="center",
                fontsize=10, color=CYAN, alpha=a_inf)
    return fig_to_pil(fig)


def scene_denoising(f, n):
    fig, ax = new_fig()
    ttl(ax, "Denoising — Atoms Emerge from Noise", color=CYAN)
    sub(ax, "AlphaFold 3 predicts atomic coordinates step by step")

    progress = f / n          # 0 -> 1
    ease_p   = progress**1.4

    n_atoms = 32
    rng4 = np.random.default_rng(77)
    t = np.linspace(0, 3*np.pi, n_atoms)
    fx = 0.50 + 0.23*np.cos(t)
    fy = 0.18 + 0.60*(t/(3*np.pi))
    sx = rng4.uniform(0.08, 0.92, n_atoms)
    sy = rng4.uniform(0.12, 0.88, n_atoms)

    cx_ = sx + ease_p*(fx - sx) + rng4.normal(0, (1-ease_p)*0.05, n_atoms)
    cy_ = sy + ease_p*(fy - sy) + rng4.normal(0, (1-ease_p)*0.05, n_atoms)

    # fading cloud
    n_cloud = int(180*(1-ease_p))
    if n_cloud > 0:
        cxc = rng4.uniform(0.05, 0.95, n_cloud)
        cyc = rng4.uniform(0.08, 0.92, n_cloud)
        ax.scatter(cxc, cyc, s=4, color=ORANGE,
                   alpha=(1-ease_p)*0.3,
                   transform=ax.transAxes, zorder=1)

    atom_cols = [BLUE if i%3==0 else GREEN if i%3==1 else PINK
                 for i in range(n_atoms)]
    atom_sz   = [55 if i%5==0 else 28 for i in range(n_atoms)]
    ax.scatter(cx_, cy_, s=atom_sz, c=atom_cols, alpha=0.88,
               transform=ax.transAxes, zorder=4)

    # bonds (fade in after 40%)
    if ease_p > 0.35:
        ba = min(1.0, (ease_p-0.35)/0.4)
        for i in range(n_atoms-1):
            ax.plot([cx_[i], cx_[i+1]], [cy_[i], cy_[i+1]],
                    color=CYAN, lw=0.9, alpha=ba*0.65,
                    transform=ax.transAxes, zorder=3)

    # step counter
    step = int(progress*200)
    ax.text(0.84, 0.91, f"Step {step}/200",
            transform=ax.transAxes, ha="center",
            fontsize=12, color=YELLOW, fontweight="bold")
    noise_pct = int((1-ease_p)*100)
    ax.text(0.16, 0.91, f"Noise: {noise_pct}%",
            transform=ax.transAxes, ha="left",
            fontsize=11, color=ORANGE)

    # progress bar
    bw = 0.68 * progress
    ax.add_patch(FancyBboxPatch((0.16, 0.055), bw, 0.028,
                 boxstyle="round,pad=0.002",
                 facecolor=CYAN, edgecolor="none",
                 transform=ax.transAxes, alpha=0.72))
    ax.add_patch(FancyBboxPatch((0.16, 0.055), 0.68, 0.028,
                 boxstyle="round,pad=0.002",
                 facecolor="none", edgecolor=GREY, lw=1,
                 transform=ax.transAxes))
    ax.text(0.50, 0.038, "Denoising Progress",
            transform=ax.transAxes, ha="center", fontsize=8, color=GREY)
    return fig_to_pil(fig)


def scene_structure(f, n):
    fig, ax = new_fig()
    a = ease(f, n)
    ttl(ax, "Predicted 3D Atomic Structure", color=GREEN)
    sub(ax, "Final coordinates — accuracy comparable to X-ray crystallography")

    n_pts = 40
    t = np.linspace(0, 6*np.pi, n_pts)
    xs = 0.50 + 0.21*np.cos(t)
    ys = 0.14 + 0.70*(t/(6*np.pi))

    n_show = max(2, int(n_pts * min(1.0, f/14)))
    for i in range(n_show-1):
        frac = i/n_pts
        col  = plt.cm.cool(frac)
        ax.plot([xs[i], xs[i+1]], [ys[i], ys[i+1]],
                color=col, lw=4.5, alpha=a,
                transform=ax.transAxes, solid_capstyle="round")

    # atoms
    a_at = min(1.0, max(0, (f-6)/8))
    if a_at > 0:
        atcols = [BLUE if i%3==0 else GREEN if i%3==1 else PINK
                  for i in range(n_show)]
        ax.scatter(xs[:n_show], ys[:n_show], s=30,
                   c=atcols, alpha=a_at*0.9,
                   transform=ax.transAxes, zorder=5)

    # side chains
    a_sc = min(1.0, max(0, (f-12)/8))
    if a_sc > 0:
        for i in range(0, n_show, 3):
            dx = 0.032*np.cos(t[i]+np.pi/3)
            dy = 0.032*np.sin(t[i]+np.pi/3)
            ax.plot([xs[i], xs[i]+dx], [ys[i], ys[i]+dy],
                    color=ORANGE, lw=1.5,
                    alpha=a_sc*0.8, transform=ax.transAxes)
            ax.scatter(xs[i]+dx, ys[i]+dy, s=14,
                       color=ORANGE, alpha=a_sc*0.9,
                       transform=ax.transAxes, zorder=6)

    # labels
    a_lb = min(1.0, max(0, (f-16)/6))
    if a_lb > 0:
        ax.text(0.79, 0.75, "alpha-helix",
                transform=ax.transAxes, fontsize=11, color=CYAN,
                alpha=a_lb,
                path_effects=[pe.withStroke(linewidth=2, foreground=BG)])
        ax.text(0.79, 0.55, "Side chains",
                transform=ax.transAxes, fontsize=10, color=ORANGE,
                alpha=a_lb)
        ax.text(0.79, 0.38, "Backbone",
                transform=ax.transAxes, fontsize=10, color=BLUE,
                alpha=a_lb)
    return fig_to_pil(fig)


def scene_apps(f, n):
    fig, ax = new_fig()
    a = ease(f, n)
    ttl(ax, "Applications", color=YELLOW)
    sub(ax, "From predicted structure to biological and clinical impact")

    apps = [
        (0.20, 0.65, "Drug\nDiscovery",              ORANGE,  0),
        (0.50, 0.65, "Genetic\nVariants",             PINK,    5),
        (0.80, 0.65, "Protein-Protein\nInteractions", BLUE,   10),
        (0.20, 0.30, "Protein\nEngineering",          GREEN,  15),
        (0.50, 0.30, "Ligand\nBinding",               PURPLE, 20),
        (0.80, 0.30, "Biomedicine\n& Agriculture",    CYAN,   25),
    ]
    for (x, y, lbl, col, delay) in apps:
        aa = min(1.0, max(0, (f-delay)/7))
        if aa <= 0: continue
        circ = Circle((x, y), 0.12, transform=ax.transAxes,
                      facecolor=col+"22", edgecolor=col,
                      linewidth=2, alpha=aa)
        ax.add_patch(circ)
        ax.text(x, y, lbl, transform=ax.transAxes,
                ha="center", va="center", fontsize=9.5,
                color=col, fontweight="bold", alpha=aa)
    return fig_to_pil(fig)


def scene_end(f, n):
    fig, ax = new_fig()
    a = ease(f, n)

    # orbiting dots
    for i in range(24):
        angle = 2*np.pi*i/24 + f*0.18
        rx, ry = 0.40, 0.22
        x = 0.5 + rx*np.cos(angle)
        y = 0.5 + ry*np.sin(angle)
        c = [BLUE, PURPLE, CYAN, GREEN, PINK][i%5]
        ax.scatter(x, y, s=12, color=c, alpha=a*0.55,
                   transform=ax.transAxes)

    ax.text(0.5, 0.65, "AlphaFold 3",
            transform=ax.transAxes, ha="center", fontsize=32,
            color=CYAN, fontweight="bold", alpha=a,
            path_effects=[pe.withStroke(linewidth=5, foreground=BG)])
    ax.text(0.5, 0.51,
            "From noisy atoms to biomolecular structures\nvia diffusion",
            transform=ax.transAxes, ha="center", fontsize=14,
            color=WHITE, alpha=a*0.9)
    ax.text(0.5, 0.34,
            "Abramson et al., Nature 2024\n"
            "\"Accurate structure prediction of biomolecular\n"
            " interactions with AlphaFold 3\"",
            transform=ax.transAxes, ha="center", fontsize=9.5,
            color=GREY, alpha=a*0.8, style="italic")
    ax.text(0.5, 0.10,
            "Protein  |  DNA  |  RNA  |  Ligand  — one unified model",
            transform=ax.transAxes, ha="center", fontsize=10,
            color=PURPLE, alpha=a*0.85)
    return fig_to_pil(fig)


# ══════════════════════════════════════════════════════════════════════════════
# SCENE SCHEDULE
# ══════════════════════════════════════════════════════════════════════════════
SCHEDULE = [
    ("Title",             scene_title,             10),
    ("Protein Problem",   scene_problem,            14),
    ("AF3 Inputs",        scene_inputs,             16),
    ("MSA + Pair",        scene_msa,                18),
    ("Diffusion Concept", scene_diffusion_concept,  18),
    ("Denoising",         scene_denoising,          24),
    ("3D Structure",      scene_structure,          18),
    ("Applications",      scene_apps,               18),
    ("End Card",          scene_end,                16),
]

total_frames = sum(nf for _, _, nf in SCHEDULE)
print(f"Total frames: {total_frames}")

# ── render all frames ─────────────────────────────────────────────────────────
frames_dir = "/home/daytona/workspace/alphafold-animation/frames"
os.makedirs(frames_dir, exist_ok=True)

all_pil = []
gf = 0
for (name, fn, nf) in SCHEDULE:
    print(f"  Rendering: {name}  ({nf} frames)...")
    for lf in range(nf):
        img = fn(lf, nf)
        all_pil.append(img)
        img.save(f"{frames_dir}/frame_{gf:04d}.png")
        gf += 1

print(f"Rendered {gf} frames.")

# ── assemble GIF ──────────────────────────────────────────────────────────────
gif_path = "/home/daytona/workspace/alphafold-animation/alphafold3_diffusion.gif"

# Convert to palette-mode for smaller GIF
palette_frames = []
for img in all_pil:
    palette_frames.append(img.quantize(colors=256, method=Image.Quantize.FASTOCTREE))

palette_frames[0].save(
    gif_path,
    save_all=True,
    append_images=palette_frames[1:],
    duration=83,          # ~12 fps
    loop=0,
    optimize=False,
)

size_mb = os.path.getsize(gif_path) / 1e6
print(f"GIF saved: {gif_path}  ({size_mb:.1f} MB,  {gf} frames)")
print("DONE")
Running Command

cd /home/daytona/workspace/alphafold-animation && python3 make_gif.py 2>&1

The GIF rendered successfully. Here's your download:

alphafold3 diffusion

GIF File · GIF

- 3.3 MB, 152 frames @ 12 fps (~13 sec loop
The PNG frame sequence is also saved individually:

file

File ·

- 152 PNG files (frame_0000.png ... frame_0151.png)

What's animated across 9 scenes:
#SceneFrames
1Title card with starfield fade-in10
21D amino acid sequence → arrow → 3D blob14
33 input modalities boxes (Protein / DNA-RNA / Ligand) fade in16
4MSA matrix + Pair Representation matrix + triangle inequality18
55-stage noise ladder (clean → pure noise) with training arrow18
6Denoising animation - atom cloud resolves into helix with progress bar24
7Ribbon structure builds up with side chains + labels18
86 application circles pop in (Drug Discovery, Ligand Binding...)18
9End card with orbiting atom dots16
This is a shared conversation. Sign in to Orris to start your own chat.