"""
Score Matching Animation: score field ∇log p(x) + Langevin dynamics
@fminxyz Series 2, Post 2 — 5 марта 2026
1080x1080 px, 2 fps, 25 сек (50 frames)
"""

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.colors import LinearSegmentedColormap

OUTPUT = "/root/Strategy/content/drafts/score_matching_animation.mp4"
FPS = 2
N_FRAMES = 50

# ── Gaussian mixture params ──────────────────────────────────────────────────
MU1 = np.array([-1.8, 0.0])
MU2 = np.array([1.8, 0.0])
SIGMA = 0.75
W1, W2 = 0.5, 0.5

rng = np.random.default_rng(42)


def gauss(x, y, mu, sigma):
    return np.exp(-((x - mu[0])**2 + (y - mu[1])**2) / (2 * sigma**2))


def mixture(x, y):
    return W1 * gauss(x, y, MU1, SIGMA) + W2 * gauss(x, y, MU2, SIGMA)


def score(x, y):
    """∇ log p(x) = ∇p(x) / p(x)  (analytically for Gaussian mixture)"""
    eps = 1e-12
    g1 = gauss(x, y, MU1, SIGMA)
    g2 = gauss(x, y, MU2, SIGMA)
    p = W1 * g1 + W2 * g2

    dpx = W1 * g1 * (-(x - MU1[0]) / SIGMA**2) + W2 * g2 * (-(x - MU2[0]) / SIGMA**2)
    dpy = W1 * g1 * (-(y - MU1[1]) / SIGMA**2) + W2 * g2 * (-(y - MU2[1]) / SIGMA**2)

    return dpx / (p + eps), dpy / (p + eps)


# ── Grids ────────────────────────────────────────────────────────────────────
x_fine = np.linspace(-4, 4, 200)
y_fine = np.linspace(-3, 3, 150)
Xf, Yf = np.meshgrid(x_fine, y_fine)
P = mixture(Xf, Yf)

x_q = np.linspace(-3.5, 3.5, 18)
y_q = np.linspace(-2.5, 2.5, 14)
Xq, Yq = np.meshgrid(x_q, y_q)
Sx, Sy = score(Xq, Yq)
S_mag = np.sqrt(Sx**2 + Sy**2 + 1e-12)
# Clip and normalize for display
S_cap = np.clip(S_mag, 0, 6)
Sx_n = Sx / S_mag * S_cap / 6
Sy_n = Sy / S_mag * S_cap / 6

# ── Langevin dynamics simulation ─────────────────────────────────────────────
N_PART = 18
# Start particles spread randomly
pts = rng.uniform(-3.5, 3.5, (N_PART, 2))
pts[:, 1] = rng.uniform(-2.5, 2.5, N_PART)

ALPHA = 0.5  # step size
NOISE_STD = 0.15
trajectories = [pts.copy()]
for _ in range(N_FRAMES - 30):
    sx_p, sy_p = score(pts[:, 0], pts[:, 1])
    pts[:, 0] = pts[:, 0] + ALPHA * sx_p + rng.normal(0, NOISE_STD, N_PART)
    pts[:, 1] = pts[:, 1] + ALPHA * sy_p + rng.normal(0, NOISE_STD, N_PART)
    pts[:, 0] = np.clip(pts[:, 0], -4, 4)
    pts[:, 1] = np.clip(pts[:, 1], -3, 3)
    trajectories.append(pts.copy())

# ── Color maps ───────────────────────────────────────────────────────────────
bg_color = '#0d0d1a'
cmap_dist = LinearSegmentedColormap.from_list(
    'dist', ['#0d0d1a', '#1a1a4e', '#2244aa', '#4488ff', '#88ccff'], N=256
)

# ── Figure setup ─────────────────────────────────────────────────────────────
DPI = 108  # 108 * 10 = 1080
fig, ax = plt.subplots(figsize=(10, 10), dpi=DPI, facecolor=bg_color)
ax.set_facecolor(bg_color)


def draw_frame(i):
    ax.clear()
    ax.set_facecolor(bg_color)
    ax.set_xlim(-4, 4)
    ax.set_ylim(-3, 3)
    ax.set_aspect('equal')
    ax.axis('off')

    # ── Phase logic ──────────────────────────────────────────────────────────
    # Phase 1: frames 0-14 → show distribution
    # Phase 2: frames 15-29 → reveal score field
    # Phase 3: frames 30-49 → Langevin dynamics

    # Always: distribution background
    dist_alpha = min(1.0, (i + 1) / 8)
    levels = np.linspace(0.005, P.max() * 0.98, 15)
    ax.contourf(Xf, Yf, P, levels=levels, cmap=cmap_dist, alpha=dist_alpha)
    ax.contour(Xf, Yf, P, levels=levels[::3], colors='#4488ff', alpha=0.25, linewidths=0.6)

    if i < 15:
        # Phase 1: distribution only
        fade = min(1.0, (i + 1) / 6)
        ax.text(0, -2.6, 'p(x) — смесь двух гауссиан',
                ha='center', fontsize=15, color='#8899cc',
                fontfamily='monospace', alpha=fade)
        ax.text(0, 2.65,
                'Score Matching: ∇log p(x)',
                ha='center', fontsize=17, fontweight='bold', color='white',
                fontfamily='monospace', alpha=fade)

    elif i < 30:
        # Phase 2: score field appears
        t = (i - 15) / 14
        q_alpha = min(t * 1.5, 0.9)
        ax.quiver(Xq, Yq, Sx_n, Sy_n,
                  color='#00e57a', alpha=q_alpha,
                  scale=16, headwidth=4, headlength=5,
                  width=0.004, zorder=5)
        ax.text(0, -2.6, '∇log p(x) — каждая стрелка ведёт к моде',
                ha='center', fontsize=14, color='#00e57a',
                fontfamily='monospace', alpha=q_alpha)
        ax.text(0, 2.65, 'Score: ∇log p(x)',
                ha='center', fontsize=17, fontweight='bold', color='white',
                fontfamily='monospace')

    else:
        # Phase 3: Langevin dynamics
        j = i - 30  # frame index in trajectory
        j = min(j, len(trajectories) - 1)

        ax.quiver(Xq, Yq, Sx_n, Sy_n,
                  color='#00e57a', alpha=0.25,
                  scale=16, headwidth=4, headlength=5,
                  width=0.003, zorder=4)

        # Draw trajectory tails
        tail_len = min(j + 1, 8)
        for p_idx in range(N_PART):
            if j > 0:
                t_start = max(0, j - tail_len + 1)
                traj = np.array([trajectories[k][p_idx]
                                 for k in range(t_start, j + 1)])
                ax.plot(traj[:, 0], traj[:, 1], '-',
                        color='#ff7700', alpha=0.35, linewidth=1.0, zorder=5)
            pos = trajectories[j][p_idx]
            ax.plot(pos[0], pos[1], 'o',
                    color='#ff9933', markersize=8, alpha=0.92, zorder=6,
                    markeredgecolor='white', markeredgewidth=0.5)

        ax.text(0, -2.6, 'Ланжевен: xₜ₊₁ = xₜ + α·∇log p(xₜ) + √(2α)·ξ',
                ha='center', fontsize=12, color='#ff9933',
                fontfamily='monospace')
        ax.text(0, 2.65, 'Langevin Dynamics → Sampling',
                ha='center', fontsize=17, fontweight='bold', color='white',
                fontfamily='monospace')

    # Step counter
    ax.text(3.8, -2.8, f't={i:02d}',
            ha='right', fontsize=11, color='#445566', fontfamily='monospace')


anim = animation.FuncAnimation(fig, draw_frame, frames=N_FRAMES,
                                interval=1000 // FPS)
anim.save(OUTPUT, writer='ffmpeg', fps=FPS, dpi=DPI,
          extra_args=['-vcodec', 'libx264', '-pix_fmt', 'yuv420p',
                      '-crf', '22', '-preset', 'fast'])
plt.close()
print(f"Saved: {OUTPUT}")
