Make alphafold triangular inequality rule animation clip using this references

Reading File
Running Command

cd /home/daytona/workspace/attachments/fb0bf012-9624-47a8-a5a3-c37526c10ced && p…

Running Command

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

Writing File

~/alphafold-animation/animate.py

"""
AlphaFold Triangular Inequality Animation
Based on:
  - "how AlphaFold actually works" (triangular attention explanation)
  - "What Is AlphaFold" NEJM (triangle inequality theorem in pair representation)
  - "The brilliance of AlphaFold 3" (triangles as protein backbone)

Scenes:
  0  Title card
  1  Pair representation intro — amino acid nodes + distance edges
  2  Standard attention VIOLATION — A-B updated ignoring C, inequality broken (red flash)
  3  Triangle inequality rule — d(A,B) <= d(A,C) + d(C,B), geometric proof
  4  Triangular attention FIX — attention over all pairs involving the edge, green tick
  5  AlphaFold context — how this bakes consistent geometry into the model
  6  Outro / summary 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 FFMpegWriter
from matplotlib.patches import FancyArrowPatch, Arc, FancyBboxPatch, Circle
import matplotlib.transforms as transforms
import math

# ── palette ────────────────────────────────────────────────────────────────────
BG      = "#0d1117"
BLUE    = "#4fc3f7"
CYAN    = "#26c6da"
GREEN   = "#69f0ae"
RED     = "#ef5350"
ORANGE  = "#ffa726"
PURPLE  = "#ce93d8"
YELLOW  = "#fff176"
WHITE   = "#e8eaf6"
GREY    = "#546e7a"
DARKBLUE= "#1565c0"

FPS  = 30
W, H = 1280, 720

fig, ax = plt.subplots(figsize=(W/100, H/100), dpi=100)
fig.patch.set_facecolor(BG)

writer = FFMpegWriter(fps=FPS, metadata={"title": "AlphaFold Triangular Inequality"},
                      codec="libx264", extra_args=["-pix_fmt", "yuv420p", "-crf", "18"])

output_path = "/home/daytona/workspace/alphafold-animation/alphafold_triangular_inequality.mp4"

# ── helpers ────────────────────────────────────────────────────────────────────
def clear(ax):
    ax.cla()
    ax.set_facecolor(BG)
    ax.set_xlim(0, 16)
    ax.set_ylim(0, 9)
    ax.axis("off")

def node(ax, xy, label, color=BLUE, r=0.38, fontsize=14, zorder=8):
    c = Circle(xy, r, color=color, zorder=zorder)
    ax.add_patch(c)
    ax.text(xy[0], xy[1], label, ha="center", va="center",
            fontsize=fontsize, fontweight="bold", color=BG, zorder=zorder+1)

def edge(ax, p1, p2, color=GREY, lw=2, zorder=3, alpha=1.0, style="-"):
    ax.plot([p1[0], p2[0]], [p1[1], p2[1]], color=color, lw=lw,
            zorder=zorder, alpha=alpha, linestyle=style)

def edge_label(ax, p1, p2, txt, color=WHITE, offset=(0, 0.28), fontsize=11):
    mx, my = (p1[0]+p2[0])/2 + offset[0], (p1[1]+p2[1])/2 + offset[1]
    ax.text(mx, my, txt, ha="center", va="center", fontsize=fontsize,
            color=color, fontweight="bold",
            path_effects=[pe.withStroke(linewidth=3, foreground=BG)])

def title_text(ax, txt, y=8.3, fontsize=22, color=CYAN):
    ax.text(8, y, txt, ha="center", va="center", fontsize=fontsize,
            color=color, fontweight="bold",
            path_effects=[pe.withStroke(linewidth=4, foreground=BG)])

def body_text(ax, lines, x=8, y_start=7.5, dy=0.55, fontsize=12, color=WHITE):
    for i, l in enumerate(lines):
        ax.text(x, y_start - i*dy, l, ha="center", va="center",
                fontsize=fontsize, color=color,
                path_effects=[pe.withStroke(linewidth=3, foreground=BG)])

def badge(ax, xy, txt, color=GREEN, fontsize=11):
    ax.text(xy[0], xy[1], txt, ha="center", va="center",
            fontsize=fontsize, color=BG, fontweight="bold",
            bbox=dict(boxstyle="round,pad=0.4", facecolor=color, edgecolor="none", zorder=15))

def highlight_box(ax, x, y, w, h, color=GREEN, alpha=0.12, zorder=2):
    r = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.1",
                       facecolor=color, edgecolor=color, linewidth=2,
                       alpha=alpha, zorder=zorder)
    ax.add_patch(r)

# node positions (consistent across scenes)
A = (4.5,  5.2)
B = (11.5, 5.2)
C = (8.0,  2.2)

def draw_abc_triangle(ax, alpha_ab=1, alpha_ac=1, alpha_bc=1,
                      col_ab=GREY, col_ac=GREY, col_bc=GREY,
                      lw=2.5, show_nodes=True,
                      node_colors=(BLUE, BLUE, BLUE)):
    edge(ax, A, B, color=col_ab, lw=lw, alpha=alpha_ab)
    edge(ax, A, C, color=col_ac, lw=lw, alpha=alpha_ac)
    edge(ax, B, C, color=col_bc, lw=lw, alpha=alpha_bc)
    if show_nodes:
        node(ax, A, "A", color=node_colors[0])
        node(ax, B, "B", color=node_colors[1])
        node(ax, C, "C", color=node_colors[2])

# ── total frame budget ─────────────────────────────────────────────────────────
SCENE_FRAMES = {
    "title":        90,   # 3 s
    "pair_repr":   120,   # 4 s
    "violation":   120,   # 4 s
    "theorem":     150,   # 5 s
    "tri_attn":    150,   # 5 s
    "context":     120,   # 4 s
    "outro":        90,   # 3 s
}
TOTAL = sum(SCENE_FRAMES.values())
print(f"Total frames: {TOTAL}  ({TOTAL/FPS:.1f} s)")

# ── easing ─────────────────────────────────────────────────────────────────────
def ease(t):
    """smooth-step"""
    t = max(0.0, min(1.0, t))
    return t * t * (3 - 2*t)

def lerp(a, b, t):
    return a + (b-a)*t

# ── render ─────────────────────────────────────────────────────────────────────
with writer.saving(fig, output_path, dpi=100):

    # ── SCENE 0: TITLE ──────────────────────────────────────────────────────────
    total_f = SCENE_FRAMES["title"]
    for f in range(total_f):
        t = f / total_f
        clear(ax)
        alpha = ease(t * 2) if t < 0.5 else 1.0
        # decorative faint triangle
        for lw, a in [(8, 0.05), (4, 0.08), (2, 0.12)]:
            edge(ax, A, B, color=CYAN,   lw=lw, alpha=a*alpha)
            edge(ax, A, C, color=CYAN,   lw=lw, alpha=a*alpha)
            edge(ax, B, C, color=CYAN,   lw=lw, alpha=a*alpha)
        draw_abc_triangle(ax, col_ab=CYAN, col_ac=CYAN, col_bc=CYAN,
                          lw=2, node_colors=(CYAN, CYAN, CYAN))
        ax.text(8, 7.8, "AlphaFold & the Triangular Inequality",
                ha="center", va="center", fontsize=24, color=WHITE,
                fontweight="bold", alpha=alpha,
                path_effects=[pe.withStroke(linewidth=5, foreground=BG)])
        ax.text(8, 7.1, "How triangular attention keeps protein geometry consistent",
                ha="center", va="center", fontsize=14, color=CYAN, alpha=alpha,
                path_effects=[pe.withStroke(linewidth=3, foreground=BG)])
        ax.text(8, 0.55,
                "Sources: 'how AlphaFold actually works' | NEJM 'What Is AlphaFold' | 'The brilliance of AlphaFold 3'",
                ha="center", va="center", fontsize=8.5, color=GREY, alpha=alpha)
        writer.grab_frame()

    # ── SCENE 1: PAIR REPRESENTATION ────────────────────────────────────────────
    total_f = SCENE_FRAMES["pair_repr"]
    for f in range(total_f):
        t = f / total_f
        clear(ax)
        title_text(ax, "Pair Representation in AlphaFold", y=8.4, fontsize=20)

        # fade-in edges then nodes
        e_alpha = ease(min(1, t * 3))
        n_alpha = ease(max(0, t*3 - 1))

        edge(ax, A, B, color=BLUE,   lw=2.5, alpha=e_alpha)
        edge(ax, A, C, color=BLUE,   lw=2.5, alpha=e_alpha)
        edge(ax, B, C, color=BLUE,   lw=2.5, alpha=e_alpha)

        if n_alpha > 0.05:
            node(ax, A, "A", color=BLUE)
            node(ax, B, "B", color=BLUE)
            node(ax, C, "C", color=BLUE)

        edge_label(ax, A, B, "d(A,B)", color=YELLOW,  offset=(0,  0.35))
        edge_label(ax, A, C, "d(A,C)", color=ORANGE,  offset=(-0.55, -0.1))
        edge_label(ax, B, C, "d(B,C)", color=ORANGE,  offset=( 0.55, -0.1))

        body_text(ax, [
            "Every amino-acid pair (i, j) stores a learned representation",
            "encoding distances and angles between residues.",
            "",
            '"...the pair representation might encode distances between",',
            '"each of the amino acids, or the distances and angles relative',
            ' to each other."  — how AlphaFold actually works',
        ], y_start=7.6, dy=0.48, fontsize=11, color=WHITE)
        writer.grab_frame()

    # ── SCENE 2: VIOLATION ──────────────────────────────────────────────────────
    total_f = SCENE_FRAMES["violation"]
    for f in range(total_f):
        t = f / total_f
        clear(ax)
        title_text(ax, "Problem: Standard Attention Violates Geometry", y=8.4,
                   fontsize=18, color=RED)

        # Standard attention: only looks at A-B, ignores triangle
        pulse = 0.5 + 0.5*math.sin(t * math.pi * 6)  # pulsating
        if t < 0.4:
            # show all edges normally first
            draw_abc_triangle(ax, col_ab=WHITE, col_ac=GREY, col_bc=GREY, lw=2.5)
            edge_label(ax, A, B, "A↔B updated separately", color=YELLOW, offset=(0, 0.42), fontsize=11)
        else:
            # highlight violation
            viol_alpha = ease((t - 0.4) / 0.3)
            draw_abc_triangle(ax, col_ab=RED, col_ac=GREY, col_bc=GREY,
                              lw=3 + pulse, alpha_ab=1)
            # draw big X over the triangle
            x_alpha = ease(max(0, (t - 0.55) / 0.25))
            for dx, dy in [(-0.6, -0.6), (0.6, 0.6)]:
                ax.annotate("", xy=(8+dx, 4.1+dy), xytext=(8-dx, 4.1-dy),
                            arrowprops=dict(arrowstyle="-", color=RED,
                                           lw=4, alpha=x_alpha))

            # false distance label
            ax.text(8, 4.1, "✗", ha="center", va="center", fontsize=60,
                    color=RED, alpha=x_alpha*0.7, zorder=10)

            edge_label(ax, A, C, "d(A,C)", color=GREY,   offset=(-0.55,-0.1))
            edge_label(ax, B, C, "d(B,C)", color=GREY,   offset=( 0.55,-0.1))

        body_text(ax, [
            '"Standard attention on pairs A-B updates d(A,B)',
            " separately from d(A,C) and d(B,C).",
            " This can produce d(A,B) > d(A,C) + d(C,B) — impossible in real space!\"",
            "  — how AlphaFold actually works",
        ], y_start=1.8, dy=0.48, fontsize=11, color=WHITE)
        writer.grab_frame()

    # ── SCENE 3: TRIANGLE INEQUALITY THEOREM ────────────────────────────────────
    total_f = SCENE_FRAMES["theorem"]
    for f in range(total_f):
        t = f / total_f
        clear(ax)
        title_text(ax, "The Triangle Inequality Theorem", y=8.45, fontsize=20, color=YELLOW)

        # Draw clean triangle
        draw_abc_triangle(ax, col_ab=CYAN, col_ac=GREEN, col_bc=GREEN, lw=3,
                          node_colors=(CYAN, CYAN, GREEN))

        # Animate the inequality writing
        # Phase 1 (0-0.3): show d(A,C) + d(C,B) label
        # Phase 2 (0.3-0.6): show >= d(A,B) label
        # Phase 3 (0.6-1.0): show the rule box

        t1 = ease(min(1, t / 0.3))
        t2 = ease(max(0, (t - 0.3) / 0.3))
        t3 = ease(max(0, (t - 0.65) / 0.3))

        edge_label(ax, A, B, "d(A,B)", color=CYAN,  offset=(0,  0.40), fontsize=13)
        edge_label(ax, A, C, "d(A,C)", color=GREEN, offset=(-0.55,-0.1), fontsize=13)
        edge_label(ax, B, C, "d(C,B)", color=GREEN, offset=( 0.55,-0.1), fontsize=13)

        # inequality text
        if t1 > 0.05:
            ax.text(8, 7.15, "d(A,C)  +  d(C,B)", ha="center", va="center",
                    fontsize=15, color=GREEN, alpha=t1, fontweight="bold",
                    path_effects=[pe.withStroke(linewidth=3, foreground=BG)])
        if t2 > 0.05:
            ax.text(8, 6.65, "≥   d(A,B)", ha="center", va="center",
                    fontsize=15, color=CYAN, alpha=t2, fontweight="bold",
                    path_effects=[pe.withStroke(linewidth=3, foreground=BG)])

        if t3 > 0.05:
            highlight_box(ax, 2.5, 6.3, 11, 1.1, color=YELLOW, alpha=0.13*t3)
            ax.text(8, 6.85,
                    "The sum of any two sides of a triangle\n"
                    "is always ≥ the third side.",
                    ha="center", va="center", fontsize=13,
                    color=YELLOW, alpha=t3, fontweight="bold",
                    path_effects=[pe.withStroke(linewidth=3, foreground=BG)])

        # source quote
        body_text(ax, [
            '"...the triangle inequality theorem, where the sum of two edges',
            ' on a triangle must be equal to or greater than the third."',
            "  — What Is AlphaFold, NEJM",
        ], y_start=1.65, dy=0.48, fontsize=11, color=GREY)
        writer.grab_frame()

    # ── SCENE 4: TRIANGULAR ATTENTION FIX ───────────────────────────────────────
    total_f = SCENE_FRAMES["tri_attn"]
    for f in range(total_f):
        t = f / total_f
        clear(ax)
        title_text(ax, "Fix: Triangular Attention", y=8.45, fontsize=20, color=GREEN)

        # Phase 1 (0-0.35): show all edges, highlight A-B
        # Phase 2 (0.35-0.7): show update arrow looping through C
        # Phase 3 (0.7-1.0): show green tick + label

        t1 = ease(min(1, t / 0.35))
        t2 = ease(max(0, (t-0.35)/0.35))
        t3 = ease(max(0, (t-0.70)/0.30))

        draw_abc_triangle(ax, col_ab=CYAN, col_ac=GREEN, col_bc=GREEN, lw=2.5,
                          node_colors=(CYAN, CYAN, GREEN))

        edge_label(ax, A, B, "d(A,B)",  color=CYAN,  offset=(0,   0.40), fontsize=13)
        edge_label(ax, A, C, "d(A,C)",  color=GREEN, offset=(-0.55,-0.1), fontsize=13)
        edge_label(ax, B, C, "d(C,B)",  color=GREEN, offset=( 0.55,-0.1), fontsize=13)

        if t2 > 0.05:
            # Animated "attention flow" arrow: A -> C -> B, updating A-B
            # draw curved arc from A to C to B
            prog = t2
            # arc from A through C to B
            pts_x = [A[0], C[0], B[0]]
            pts_y = [A[1], C[1], B[1]]
            n = max(2, int(prog * 40))
            xs = np.linspace(A[0], B[0], n)
            # quadratic bezier through C
            ys = []
            for i_pt in range(n):
                s = i_pt/(max(n-1,1))
                # quadratic bezier
                y_b = (1-s)**2*A[1] + 2*(1-s)*s*C[1] + s**2*B[1]
                ys.append(y_b)
            ax.plot(xs, ys, color=ORANGE, lw=3, alpha=0.9*t2, zorder=6,
                    linestyle="--")
            # arrowhead at current progress point
            idx = min(n-1, int(prog * (n-1)))
            if idx > 0:
                ax.annotate("", xy=(xs[min(idx, n-1)], ys[min(idx, n-1)]),
                            xytext=(xs[max(idx-1,0)], ys[max(idx-1,0)]),
                            arrowprops=dict(arrowstyle="-|>", color=ORANGE,
                                           lw=2, mutation_scale=18),
                            zorder=7)

            ax.text(8, 3.9 - 0.3*t2,
                    "d(A,B) update attends to ALL pairs involving A and B\n(i.e. A-C and B-C are consulted too)",
                    ha="center", va="center", fontsize=12,
                    color=ORANGE, alpha=t2, fontweight="bold",
                    path_effects=[pe.withStroke(linewidth=3, foreground=BG)])

        if t3 > 0.05:
            ax.text(8, 7.4, "✓  Triangular Attention", ha="center", va="center",
                    fontsize=18, color=GREEN, alpha=t3, fontweight="bold",
                    path_effects=[pe.withStroke(linewidth=4, foreground=BG)])

        body_text(ax, [
            '"...instead of looking at all pairs separately, get the attention mechanism',
            " to pay attention to all pairs involving A and B.",
            ' This is called triangular attention." — how AlphaFold actually works',
        ], y_start=1.65, dy=0.48, fontsize=11, color=GREY)
        writer.grab_frame()

    # ── SCENE 5: CONTEXT IN ALPHAFOLD ───────────────────────────────────────────
    total_f = SCENE_FRAMES["context"]
    for f in range(total_f):
        t = f / total_f
        clear(ax)
        title_text(ax, "Why This Matters for Protein Structure", y=8.45,
                   fontsize=18, color=PURPLE)

        # draw mini protein backbone as series of amino-acid triangles
        n_aa = 6
        cx0, cy0 = 2.0, 4.8
        spacing = 2.0
        color_cycle = [BLUE, CYAN, GREEN, YELLOW, ORANGE, PURPLE]
        t_show = ease(min(1, t * 2.5))
        for i in range(n_aa):
            show_alpha = ease(max(0, t_show - i*0.15))
            if show_alpha < 0.02:
                continue
            cx = cx0 + i*spacing
            cy = cy0 + (0.3 if i%2==0 else -0.3)
            col = color_cycle[i % len(color_cycle)]
            # small triangle representing amino acid backbone
            ta = np.array([cx-0.3, cy+0.35])
            tb = np.array([cx+0.3, cy+0.35])
            tc = np.array([cx,     cy-0.25])
            for p1, p2 in [(ta,tb),(tb,tc),(tc,ta)]:
                ax.plot([p1[0],p2[0]], [p1[1],p2[1]], color=col,
                        lw=2, alpha=show_alpha*0.9)
            ax.text(cx, cy+0.08, str(i+1), ha="center", va="center",
                    fontsize=9, color=col, alpha=show_alpha, fontweight="bold")
            # connect to next with dashed line
            if i < n_aa-1:
                nx = cx0 + (i+1)*spacing
                ny = cy0 + (0.3 if (i+1)%2==0 else -0.3)
                ax.plot([cx+0.3, nx-0.3], [cy+0.35, ny+0.35],
                        color=GREY, lw=1.5, linestyle="--", alpha=show_alpha*0.6)

        t3 = ease(max(0, (t-0.35)/0.45))
        body_text(ax, [
            "Each amino acid acts as a rigid triangle in 3D space.",
            "Triangular attention ensures the pair representation encodes",
            "distances that are geometrically consistent — no impossible gaps.",
            "",
            '"...the basic idea is to treat each amino acid as if it\'s a triangle."',
            "  — how AlphaFold actually works",
        ], y_start=7.7, dy=0.52, fontsize=11.5, color=WHITE)
        writer.grab_frame()

    # ── SCENE 6: OUTRO ──────────────────────────────────────────────────────────
    total_f = SCENE_FRAMES["outro"]
    for f in range(total_f):
        t = f / total_f
        clear(ax)
        alpha_out = 1.0 - ease(max(0, (t-0.75)/0.25))

        draw_abc_triangle(ax, col_ab=CYAN, col_ac=GREEN, col_bc=GREEN,
                          lw=2, node_colors=(CYAN, CYAN, GREEN))

        ax.text(8, 8.1, "Summary", ha="center", va="center",
                fontsize=22, color=WHITE, fontweight="bold", alpha=alpha_out,
                path_effects=[pe.withStroke(linewidth=5, foreground=BG)])

        bullets = [
            (GREEN,  "Pair representation stores distances/angles between all residue pairs"),
            (RED,    "Standard attention can violate the triangle inequality"),
            (YELLOW, "Triangle inequality: d(A,B) ≤ d(A,C) + d(C,B)"),
            (CYAN,   "Triangular attention consults all pairs sharing an endpoint"),
            (PURPLE, "Result: geometrically consistent, physically plausible structures"),
        ]
        for i, (col, txt) in enumerate(bullets):
            t_b = ease(max(0, t*6 - i))
            ax.text(2.0, 6.9 - i*0.75, "●", ha="left", va="center",
                    fontsize=13, color=col, alpha=t_b*alpha_out,
                    path_effects=[pe.withStroke(linewidth=2, foreground=BG)])
            ax.text(2.55, 6.9 - i*0.75, txt, ha="left", va="center",
                    fontsize=11.5, color=WHITE, alpha=t_b*alpha_out,
                    path_effects=[pe.withStroke(linewidth=2, foreground=BG)])

        ax.text(8, 0.55,
                "Sources: 'how AlphaFold actually works' | NEJM 'What Is AlphaFold' | 'The brilliance of AlphaFold 3'",
                ha="center", va="center", fontsize=8.5, color=GREY, alpha=alpha_out)
        writer.grab_frame()

print("Done!")
Running Command

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

Running Command

ffmpeg -version 2>&1 | head -3 && python3 -c "import matplotlib; print(matplotli…

Writing File

~/alphafold-animation/animate.py

"""
AlphaFold Triangular Inequality Animation
Saves frames to PNG files, then encodes with ffmpeg.
"""

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 FancyBboxPatch, Circle
import os
import math
import subprocess

# ── palette ────────────────────────────────────────────────────────────────────
BG      = "#0d1117"
BLUE    = "#4fc3f7"
CYAN    = "#26c6da"
GREEN   = "#69f0ae"
RED     = "#ef5350"
ORANGE  = "#ffa726"
PURPLE  = "#ce93d8"
YELLOW  = "#fff176"
WHITE   = "#e8eaf6"
GREY    = "#546e7a"

FPS  = 30
W, H = 1280, 720

FRAMES_DIR = "/home/daytona/workspace/alphafold-animation/frames"
os.makedirs(FRAMES_DIR, exist_ok=True)

fig, ax = plt.subplots(figsize=(W/100, H/100), dpi=100)
fig.patch.set_facecolor(BG)

# ── helpers ────────────────────────────────────────────────────────────────────
def clear(ax):
    ax.cla()
    ax.set_facecolor(BG)
    ax.set_xlim(0, 16)
    ax.set_ylim(0, 9)
    ax.axis("off")

def node(ax, xy, label, color=BLUE, r=0.38, fontsize=14, zorder=8):
    c = Circle(xy, r, color=color, zorder=zorder)
    ax.add_patch(c)
    ax.text(xy[0], xy[1], label, ha="center", va="center",
            fontsize=fontsize, fontweight="bold", color=BG, zorder=zorder+1)

def edge(ax, p1, p2, color=GREY, lw=2, zorder=3, alpha=1.0, style="-"):
    ax.plot([p1[0], p2[0]], [p1[1], p2[1]], color=color, lw=lw,
            zorder=zorder, alpha=alpha, linestyle=style)

def edge_label(ax, p1, p2, txt, color=WHITE, offset=(0, 0.28), fontsize=11):
    mx, my = (p1[0]+p2[0])/2 + offset[0], (p1[1]+p2[1])/2 + offset[1]
    ax.text(mx, my, txt, ha="center", va="center", fontsize=fontsize,
            color=color, fontweight="bold",
            path_effects=[pe.withStroke(linewidth=3, foreground=BG)])

def title_text(ax, txt, y=8.3, fontsize=22, color=CYAN):
    ax.text(8, y, txt, ha="center", va="center", fontsize=fontsize,
            color=color, fontweight="bold",
            path_effects=[pe.withStroke(linewidth=4, foreground=BG)])

def body_text(ax, lines, x=8, y_start=7.5, dy=0.55, fontsize=12, color=WHITE):
    for i, l in enumerate(lines):
        ax.text(x, y_start - i*dy, l, ha="center", va="center",
                fontsize=fontsize, color=color,
                path_effects=[pe.withStroke(linewidth=3, foreground=BG)])

def highlight_box(ax, x, y, w, h, color=GREEN, alpha=0.12, zorder=2):
    r = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.1",
                       facecolor=color, edgecolor=color, linewidth=2,
                       alpha=alpha, zorder=zorder)
    ax.add_patch(r)

# node positions
A = (4.5,  5.2)
B = (11.5, 5.2)
C = (8.0,  2.2)

def draw_abc_triangle(ax, alpha_ab=1, alpha_ac=1, alpha_bc=1,
                      col_ab=GREY, col_ac=GREY, col_bc=GREY,
                      lw=2.5, show_nodes=True,
                      node_colors=(BLUE, BLUE, BLUE)):
    edge(ax, A, B, color=col_ab, lw=lw, alpha=alpha_ab)
    edge(ax, A, C, color=col_ac, lw=lw, alpha=alpha_ac)
    edge(ax, B, C, color=col_bc, lw=lw, alpha=alpha_bc)
    if show_nodes:
        node(ax, A, "A", color=node_colors[0])
        node(ax, B, "B", color=node_colors[1])
        node(ax, C, "C", color=node_colors[2])

def ease(t):
    t = max(0.0, min(1.0, t))
    return t * t * (3 - 2*t)

SCENE_FRAMES = {
    "title":        90,
    "pair_repr":   120,
    "violation":   120,
    "theorem":     150,
    "tri_attn":    150,
    "context":     120,
    "outro":        90,
}
TOTAL = sum(SCENE_FRAMES.values())
print(f"Total frames: {TOTAL}  ({TOTAL/FPS:.1f} s)")

frame_idx = 0

def save_frame():
    global frame_idx
    fig.savefig(f"{FRAMES_DIR}/frame_{frame_idx:05d}.png",
                dpi=100, facecolor=BG, bbox_inches=None)
    frame_idx += 1

# ── SCENE 0: TITLE ────────────────────────────────────────────────────────────
total_f = SCENE_FRAMES["title"]
for f in range(total_f):
    t = f / total_f
    clear(ax)
    alpha = ease(t * 2) if t < 0.5 else 1.0
    for lw, a in [(8, 0.05), (4, 0.08), (2, 0.12)]:
        edge(ax, A, B, color=CYAN, lw=lw, alpha=a*alpha)
        edge(ax, A, C, color=CYAN, lw=lw, alpha=a*alpha)
        edge(ax, B, C, color=CYAN, lw=lw, alpha=a*alpha)
    draw_abc_triangle(ax, col_ab=CYAN, col_ac=CYAN, col_bc=CYAN,
                      lw=2, node_colors=(CYAN, CYAN, CYAN))
    ax.text(8, 7.8, "AlphaFold & the Triangular Inequality",
            ha="center", va="center", fontsize=24, color=WHITE,
            fontweight="bold", alpha=alpha,
            path_effects=[pe.withStroke(linewidth=5, foreground=BG)])
    ax.text(8, 7.1, "How triangular attention keeps protein geometry consistent",
            ha="center", va="center", fontsize=14, color=CYAN, alpha=alpha,
            path_effects=[pe.withStroke(linewidth=3, foreground=BG)])
    ax.text(8, 0.55,
            "Sources: 'how AlphaFold actually works'  |  NEJM 'What Is AlphaFold'  |  'The brilliance of AlphaFold 3'",
            ha="center", va="center", fontsize=8.5, color=GREY, alpha=alpha)
    save_frame()

# ── SCENE 1: PAIR REPRESENTATION ─────────────────────────────────────────────
total_f = SCENE_FRAMES["pair_repr"]
for f in range(total_f):
    t = f / total_f
    clear(ax)
    title_text(ax, "Pair Representation in AlphaFold", y=8.4, fontsize=20)
    e_alpha = ease(min(1, t * 3))
    edge(ax, A, B, color=BLUE, lw=2.5, alpha=e_alpha)
    edge(ax, A, C, color=BLUE, lw=2.5, alpha=e_alpha)
    edge(ax, B, C, color=BLUE, lw=2.5, alpha=e_alpha)
    if e_alpha > 0.3:
        node(ax, A, "A", color=BLUE)
        node(ax, B, "B", color=BLUE)
        node(ax, C, "C", color=BLUE)
    edge_label(ax, A, B, "d(A,B)", color=YELLOW, offset=(0,   0.40))
    edge_label(ax, A, C, "d(A,C)", color=ORANGE, offset=(-0.55,-0.1))
    edge_label(ax, B, C, "d(B,C)", color=ORANGE, offset=( 0.55,-0.1))
    body_text(ax, [
        "Every amino-acid pair (i, j) stores a learned representation",
        "encoding distances and angles between residues.",
        "",
        '"...the pair representation might encode distances between each',
        ' of the amino acids, or the distances and angles relative to each other."',
        "  \u2014 how AlphaFold actually works",
    ], y_start=7.6, dy=0.48, fontsize=11, color=WHITE)
    save_frame()

# ── SCENE 2: VIOLATION ───────────────────────────────────────────────────────
total_f = SCENE_FRAMES["violation"]
for f in range(total_f):
    t = f / total_f
    clear(ax)
    title_text(ax, "Problem: Standard Attention Violates Geometry", y=8.4,
               fontsize=18, color=RED)
    pulse = 0.5 + 0.5*math.sin(t * math.pi * 6)
    if t < 0.4:
        draw_abc_triangle(ax, col_ab=WHITE, col_ac=GREY, col_bc=GREY, lw=2.5)
        edge_label(ax, A, B, "A\u2194B updated separately", color=YELLOW, offset=(0, 0.42), fontsize=11)
    else:
        draw_abc_triangle(ax, col_ab=RED, col_ac=GREY, col_bc=GREY,
                          lw=3 + pulse)
        x_alpha = ease(max(0, (t - 0.55) / 0.25))
        ax.text(8, 4.1, "\u2717", ha="center", va="center", fontsize=60,
                color=RED, alpha=x_alpha*0.7, zorder=10)
        edge_label(ax, A, C, "d(A,C)", color=GREY, offset=(-0.55,-0.1))
        edge_label(ax, B, C, "d(B,C)", color=GREY, offset=( 0.55,-0.1))
    body_text(ax, [
        '"Standard attention on pairs A-B updates d(A,B)',
        " separately from d(A,C) and d(B,C).",
        " This can produce d(A,B) > d(A,C)+d(C,B) \u2014 impossible in real space!\"",
        "  \u2014 how AlphaFold actually works",
    ], y_start=1.8, dy=0.48, fontsize=11, color=WHITE)
    save_frame()

# ── SCENE 3: TRIANGLE INEQUALITY THEOREM ────────────────────────────────────
total_f = SCENE_FRAMES["theorem"]
for f in range(total_f):
    t = f / total_f
    clear(ax)
    title_text(ax, "The Triangle Inequality Theorem", y=8.45, fontsize=20, color=YELLOW)
    draw_abc_triangle(ax, col_ab=CYAN, col_ac=GREEN, col_bc=GREEN, lw=3,
                      node_colors=(CYAN, CYAN, GREEN))
    t1 = ease(min(1, t / 0.3))
    t2 = ease(max(0, (t - 0.3) / 0.3))
    t3 = ease(max(0, (t - 0.65) / 0.3))
    edge_label(ax, A, B, "d(A,B)", color=CYAN,  offset=(0,   0.40), fontsize=13)
    edge_label(ax, A, C, "d(A,C)", color=GREEN, offset=(-0.55,-0.1), fontsize=13)
    edge_label(ax, B, C, "d(C,B)", color=GREEN, offset=( 0.55,-0.1), fontsize=13)
    if t1 > 0.05:
        ax.text(8, 7.15, "d(A,C)  +  d(C,B)", ha="center", va="center",
                fontsize=15, color=GREEN, alpha=t1, fontweight="bold",
                path_effects=[pe.withStroke(linewidth=3, foreground=BG)])
    if t2 > 0.05:
        ax.text(8, 6.65, "\u2265   d(A,B)", ha="center", va="center",
                fontsize=15, color=CYAN, alpha=t2, fontweight="bold",
                path_effects=[pe.withStroke(linewidth=3, foreground=BG)])
    if t3 > 0.05:
        highlight_box(ax, 2.5, 6.3, 11, 1.1, color=YELLOW, alpha=0.13*t3)
        ax.text(8, 6.85,
                "The sum of any two sides of a triangle is always \u2265 the third side.",
                ha="center", va="center", fontsize=13,
                color=YELLOW, alpha=t3, fontweight="bold",
                path_effects=[pe.withStroke(linewidth=3, foreground=BG)])
    body_text(ax, [
        '"...the triangle inequality theorem, where the sum of two edges',
        ' on a triangle must be equal to or greater than the third."',
        "  \u2014 What Is AlphaFold, NEJM",
    ], y_start=1.65, dy=0.48, fontsize=11, color=GREY)
    save_frame()

# ── SCENE 4: TRIANGULAR ATTENTION FIX ───────────────────────────────────────
total_f = SCENE_FRAMES["tri_attn"]
for f in range(total_f):
    t = f / total_f
    clear(ax)
    title_text(ax, "Fix: Triangular Attention", y=8.45, fontsize=20, color=GREEN)
    t1 = ease(min(1, t / 0.35))
    t2 = ease(max(0, (t-0.35)/0.35))
    t3 = ease(max(0, (t-0.70)/0.30))
    draw_abc_triangle(ax, col_ab=CYAN, col_ac=GREEN, col_bc=GREEN, lw=2.5,
                      node_colors=(CYAN, CYAN, GREEN))
    edge_label(ax, A, B, "d(A,B)", color=CYAN,  offset=(0,   0.40), fontsize=13)
    edge_label(ax, A, C, "d(A,C)", color=GREEN, offset=(-0.55,-0.1), fontsize=13)
    edge_label(ax, B, C, "d(C,B)", color=GREEN, offset=( 0.55,-0.1), fontsize=13)
    if t2 > 0.05:
        prog = t2
        n = max(2, int(prog * 50))
        xs = np.linspace(A[0], B[0], n)
        ys = []
        for i_pt in range(n):
            s = i_pt / max(n-1, 1)
            y_b = (1-s)**2*A[1] + 2*(1-s)*s*C[1] + s**2*B[1]
            ys.append(y_b)
        ax.plot(xs, ys, color=ORANGE, lw=3, alpha=0.9*t2, zorder=6, linestyle="--")
        if n > 1:
            idx = min(n-1, int(prog*(n-1)))
            if idx > 0:
                ax.annotate("", xy=(xs[idx], ys[idx]),
                            xytext=(xs[max(idx-1,0)], ys[max(idx-1,0)]),
                            arrowprops=dict(arrowstyle="-|>", color=ORANGE,
                                           lw=2, mutation_scale=18),
                            zorder=7)
        ax.text(8, 3.7,
                "d(A,B) update attends to ALL pairs involving A and B\n"
                "(d(A,C) and d(B,C) are consulted together)",
                ha="center", va="center", fontsize=12,
                color=ORANGE, alpha=t2, fontweight="bold",
                path_effects=[pe.withStroke(linewidth=3, foreground=BG)])
    if t3 > 0.05:
        ax.text(8, 7.4, "\u2713  Triangular Attention", ha="center", va="center",
                fontsize=18, color=GREEN, alpha=t3, fontweight="bold",
                path_effects=[pe.withStroke(linewidth=4, foreground=BG)])
    body_text(ax, [
        '"...instead of looking at all pairs separately, get the attention mechanism',
        " to pay attention to all pairs involving A and B.",
        ' This is called triangular attention." \u2014 how AlphaFold actually works',
    ], y_start=1.65, dy=0.48, fontsize=11, color=GREY)
    save_frame()

# ── SCENE 5: CONTEXT IN ALPHAFOLD ───────────────────────────────────────────
total_f = SCENE_FRAMES["context"]
for f in range(total_f):
    t = f / total_f
    clear(ax)
    title_text(ax, "Why This Matters for Protein Structure", y=8.45,
               fontsize=18, color=PURPLE)
    n_aa = 6
    cx0, cy0 = 2.0, 4.8
    spacing = 2.0
    color_cycle = [BLUE, CYAN, GREEN, YELLOW, ORANGE, PURPLE]
    t_show = ease(min(1, t * 2.5))
    for i in range(n_aa):
        show_alpha = ease(max(0, t_show - i*0.15))
        if show_alpha < 0.02:
            continue
        cx = cx0 + i*spacing
        cy = cy0 + (0.3 if i%2==0 else -0.3)
        col = color_cycle[i % len(color_cycle)]
        ta = np.array([cx-0.3, cy+0.35])
        tb = np.array([cx+0.3, cy+0.35])
        tc = np.array([cx,     cy-0.25])
        for p1, p2 in [(ta,tb),(tb,tc),(tc,ta)]:
            ax.plot([p1[0],p2[0]], [p1[1],p2[1]], color=col,
                    lw=2, alpha=show_alpha*0.9)
        ax.text(cx, cy+0.08, str(i+1), ha="center", va="center",
                fontsize=9, color=col, alpha=show_alpha, fontweight="bold")
        if i < n_aa-1:
            nx = cx0 + (i+1)*spacing
            ny = cy0 + (0.3 if (i+1)%2==0 else -0.3)
            ax.plot([cx+0.3, nx-0.3], [cy+0.35, ny+0.35],
                    color=GREY, lw=1.5, linestyle="--", alpha=show_alpha*0.6)
    body_text(ax, [
        "Each amino acid acts as a rigid triangle in 3D space.",
        "Triangular attention ensures the pair representation encodes",
        "distances that are geometrically consistent \u2014 no impossible gaps.",
        "",
        '"...the basic idea is to treat each amino acid as if it\'s a triangle."',
        "  \u2014 how AlphaFold actually works",
    ], y_start=7.7, dy=0.52, fontsize=11.5, color=WHITE)
    save_frame()

# ── SCENE 6: OUTRO ───────────────────────────────────────────────────────────
total_f = SCENE_FRAMES["outro"]
for f in range(total_f):
    t = f / total_f
    clear(ax)
    alpha_out = 1.0 - ease(max(0, (t-0.75)/0.25))
    draw_abc_triangle(ax, col_ab=CYAN, col_ac=GREEN, col_bc=GREEN,
                      lw=2, node_colors=(CYAN, CYAN, GREEN))
    ax.text(8, 8.1, "Summary", ha="center", va="center",
            fontsize=22, color=WHITE, fontweight="bold", alpha=alpha_out,
            path_effects=[pe.withStroke(linewidth=5, foreground=BG)])
    bullets = [
        (GREEN,  "Pair representation stores distances/angles between all residue pairs"),
        (RED,    "Standard attention can violate the triangle inequality"),
        (YELLOW, "Triangle inequality: d(A,B) \u2264 d(A,C) + d(C,B)"),
        (CYAN,   "Triangular attention consults all pairs sharing an endpoint"),
        (PURPLE, "Result: geometrically consistent, physically plausible structures"),
    ]
    for i, (col, txt) in enumerate(bullets):
        t_b = ease(max(0, t*6 - i))
        ax.text(2.0, 6.9 - i*0.75, "\u25cf", ha="left", va="center",
                fontsize=13, color=col, alpha=t_b*alpha_out,
                path_effects=[pe.withStroke(linewidth=2, foreground=BG)])
        ax.text(2.55, 6.9 - i*0.75, txt, ha="left", va="center",
                fontsize=11.5, color=WHITE, alpha=t_b*alpha_out,
                path_effects=[pe.withStroke(linewidth=2, foreground=BG)])
    ax.text(8, 0.55,
            "Sources: 'how AlphaFold actually works'  |  NEJM 'What Is AlphaFold'  |  'The brilliance of AlphaFold 3'",
            ha="center", va="center", fontsize=8.5, color=GREY, alpha=alpha_out)
    save_frame()

plt.close(fig)
print(f"Saved {frame_idx} frames to {FRAMES_DIR}")

# Encode with ffmpeg
output_path = "/home/daytona/workspace/alphafold-animation/alphafold_triangular_inequality.mp4"
cmd = [
    "ffmpeg", "-y",
    "-framerate", str(FPS),
    "-i", f"{FRAMES_DIR}/frame_%05d.png",
    "-c:v", "libx264",
    "-pix_fmt", "yuv420p",
    "-crf", "18",
    "-preset", "fast",
    output_path
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
    import os
    size = os.path.getsize(output_path)
    print(f"Encoded successfully: {output_path} ({size/1024/1024:.1f} MB)")
else:
    print("FFMPEG ERROR:", result.stderr[-1000:])
Editing File

~/alphafold-animation/animate.py

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

cat > /home/daytona/workspace/alphafold-animation/animate.py << 'PYEOF' import n…

Running Command

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

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

Your credits are exhausted. Your daily credits reset tomorrow, or you can purchase a top-up for more.

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

Your credits are exhausted. Your daily credits reset tomorrow, or you can purchase a top-up for more.
This is a shared conversation. Sign in to Orris to start your own chat.