"""AlphaDynamics interactive demo on Hugging Face Spaces. User pastes a peptide sequence, gets: - interactive Ramachandran density plot (Plotly) - per-residue panels for short peptides - basin populations table (alpha-R, beta, PPII, alpha-L) - downloadable .npz with the raw trajectory tensor Free CPU tier. Limits: max 50 residues, max 16 trajectories, max 1000 steps. """ from __future__ import annotations import os import tempfile from pathlib import Path import gradio as gr import numpy as np import plotly.graph_objects as go from plotly.subplots import make_subplots # alphadynamics is in requirements.txt — installed at Space build time from alphadynamics import predict_torsion_ensemble # ---------------------------------------------------------------------------- # Constants — protect free CPU tier # ---------------------------------------------------------------------------- MAX_RESIDUES = 50 MAX_ENSEMBLE = 16 MAX_STEPS = 1000 EXAMPLES = [ ["AAAY", 8, 500], ["KLVFFAE", 8, 500], # amyloid-β fragment ["GNNQQNY", 8, 500], # Sup35 prion peptide ["AAYAA", 4, 300], # quick test ["MAEHLLLY", 8, 500], # random short ["FVNQHLCGSHLVEALYL", 4, 300], # insulin B chain N-terminus ] # ---------------------------------------------------------------------------- # Plotting # ---------------------------------------------------------------------------- def make_ramachandran_figure(traj: np.ndarray, sequence: str) -> go.Figure: n_ens, n_t, n_res, _ = traj.shape has_per_res = 1 <= n_res <= 12 if has_per_res: ncols = min(4, n_res) nrows_per_res = (n_res + ncols - 1) // ncols specs = [[{"colspan": ncols, "rowspan": 1}] + [None] * (ncols - 1)] for _ in range(nrows_per_res): specs.append([{} for _ in range(ncols)]) titles = [f"{sequence} — aggregate density"] for i in range(n_res): aa = sequence[i] if i < len(sequence) else "?" titles.append(f"{aa}{i+1}") # pad total_cells = ncols + nrows_per_res * ncols titles += [""] * max(0, total_cells - len(titles)) fig = make_subplots( rows=1 + nrows_per_res, cols=ncols, specs=specs, subplot_titles=titles, horizontal_spacing=0.04, vertical_spacing=0.10, ) else: fig = make_subplots( rows=1, cols=1, subplot_titles=[ f"{sequence} — Ramachandran ({n_res} residues, {n_ens}×{n_t} samples)" ], ) ncols = 1 nrows_per_res = 0 phi_all = np.degrees(traj[..., 0].flatten()) psi_all = np.degrees(traj[..., 1].flatten()) fig.add_trace( go.Histogram2dContour( x=phi_all, y=psi_all, colorscale="Viridis", ncontours=20, contours=dict(coloring="fill", showlines=False), line=dict(width=0), showscale=True, colorbar=dict(title="density", len=0.45 if has_per_res else 0.85, y=0.78 if has_per_res else 0.5, x=1.02), hovertemplate="φ=%{x:.0f}°
ψ=%{y:.0f}°
density=%{z:.4f}", ), row=1, col=1, ) for px, py, label, color in [ (-60, -45, "α-R", "white"), (-120, 130, "β", "white"), (-60, 140, "PPII", "white"), (60, 50, "α-L (forbidden)", "orange"), ]: fig.add_annotation( x=px, y=py, text=f"{label}", showarrow=False, font=dict(color=color, size=12), xref="x1", yref="y1", ) if has_per_res: for i in range(n_res): row = 2 + (i // ncols) col = 1 + (i % ncols) phi_i = np.degrees(traj[:, :, i, 0].flatten()) psi_i = np.degrees(traj[:, :, i, 1].flatten()) fig.add_trace( go.Histogram2dContour( x=phi_i, y=psi_i, colorscale="Viridis", ncontours=15, contours=dict(coloring="fill", showlines=False), line=dict(width=0), showscale=False, hovertemplate="φ=%{x:.0f}°
ψ=%{y:.0f}°", ), row=row, col=col, ) for axis in fig.layout: if axis.startswith("xaxis"): fig.layout[axis].update( range=[-180, 180], tickvals=[-180, -90, 0, 90, 180], ticksuffix="°", gridcolor="rgba(255,255,255,0.15)", zerolinecolor="rgba(255,255,255,0.4)", zerolinewidth=1, ) elif axis.startswith("yaxis"): suffix = axis[len("yaxis"):] fig.layout[axis].update( range=[-180, 180], tickvals=[-180, -90, 0, 90, 180], ticksuffix="°", gridcolor="rgba(255,255,255,0.15)", zerolinecolor="rgba(255,255,255,0.4)", zerolinewidth=1, scaleanchor="x" + suffix, scaleratio=1, ) fig.update_layout( template="plotly_dark", height=350 + (250 * nrows_per_res if has_per_res else 0), margin=dict(l=70, r=120, t=80, b=60), font=dict(family="Inter, system-ui, -apple-system, sans-serif", size=12), hovermode="closest", ) return fig def basin_table_md(traj: np.ndarray) -> str: phi = np.degrees(traj[..., 0].flatten()) psi = np.degrees(traj[..., 1].flatten()) def b(plo, phi_, slo, shi): return ((phi >= plo) & (phi <= phi_) & (psi >= slo) & (psi <= shi)).mean() * 100 return ( "| Basin | Population |\n" "|---|---:|\n" f"| α-R helix (φ ≈ -60, ψ ≈ -45) | **{b(-130,-30,-90,30):.1f}%** |\n" f"| β-sheet (φ ≈ -120, ψ ≈ 120) | **{b(-180,-90,70,180):.1f}%** |\n" f"| PPII extended (φ ≈ -60, ψ ≈ 140) | **{b(-90,-30,100,180):.1f}%** |\n" f"| α-L (sterically forbidden region) | **{b(30,100,-10,90):.1f}%** ← should be near 0 |\n" ) # ---------------------------------------------------------------------------- # Inference # ---------------------------------------------------------------------------- _AA_VOCAB = set("ACDEFGHIKLMNPQRSTVWYX") def predict(sequence: str, n_ensemble: int, rollout_steps: int): sequence = (sequence or "").strip().upper() if not sequence: raise gr.Error("Please enter a peptide sequence (1-letter amino-acid code).") if len(sequence) > MAX_RESIDUES: raise gr.Error( f"Free demo limited to {MAX_RESIDUES} residues. " f"For longer peptides, install locally: pip install alphadynamics" ) bad = sorted(set(sequence) - _AA_VOCAB) if bad: raise gr.Error( f"Sequence contains non-standard residues: {bad}. " f"Use only one-letter codes from {sorted(_AA_VOCAB)}." ) n_ensemble = max(1, min(int(n_ensemble), MAX_ENSEMBLE)) rollout_steps = max(50, min(int(rollout_steps), MAX_STEPS)) traj = predict_torsion_ensemble( sequence, n_ensemble=n_ensemble, rollout_steps=rollout_steps, seed=42, device="cpu", show_progress=False, ) # save NPZ for download out_path = Path(tempfile.gettempdir()) / f"alphadynamics_{sequence}_torsions.npz" np.savez_compressed( out_path, sequence=sequence, torsions=traj, torsion_units="radians", torsion_axes="(ensemble, time, residues, [phi, psi])", n_ensemble=n_ensemble, rollout_steps=rollout_steps, model_name="ad_transfer_v2_clean", ) # Generate backbone PDB (NEW v0.4.0) try: from alphadynamics import trajectory_to_pdb, trajectory_diagnostics pdb_path = Path(tempfile.gettempdir()) / f"alphadynamics_{sequence}_backbone.pdb" # Use ensemble member 0, subsample to 50 frames for fast download member = traj[0] if len(member) > 50: idx = np.linspace(0, len(member) - 1, 50).astype(int) member = member[idx] trajectory_to_pdb(member, sequence, str(pdb_path)) diag = trajectory_diagnostics(member) diag_md = ( f"\n\n**3D backbone diagnostics** (Cα-only, 50 frames):\n" f"- Radius of gyration: {diag['rg_mean']:.2f} ± {diag['rg_std']:.2f} Å\n" f"- End-to-end distance: {diag['end_to_end_mean']:.2f} ± {diag['end_to_end_std']:.2f} Å" ) except Exception as e: pdb_path = None diag_md = f"\n\n*(PDB rebuild unavailable: {e})*" fig = make_ramachandran_figure(traj, sequence) info = ( f"### `{sequence}` — {len(sequence)} residue{'s' if len(sequence) > 1 else ''}\n\n" f"**{n_ensemble}** trajectories × **{rollout_steps}** steps " f"= **{n_ensemble * rollout_steps * len(sequence):,}** torsion samples\n\n" + basin_table_md(traj) + diag_md ) return fig, info, str(out_path), str(pdb_path) if pdb_path else None # ---------------------------------------------------------------------------- # Gradio UI # ---------------------------------------------------------------------------- DESCRIPTION = """ # 🧬 AlphaDynamics — Protein Torsion Dynamics from Sequence A tiny (~123K param) neural propagator that predicts the **Ramachandran density** of a peptide's backbone (φ, ψ) angles **from sequence alone**. On the canonical 4AA benchmark: **2.39× lower JSD** than Microsoft Timewarp at **3000× fewer parameters**. Cross-validated against Top8000 PDB statistics. 📦 `pip install alphadynamics` · 🐙 [GitHub](https://github.com/krisss0mecom/AlphaDynamics) · 🤗 [Model card](https://huggingface.co/krissss0/alphadynamics) **This demo is CPU-only** — limited to 50 residues / 16 trajectories / 1000 steps. For longer peptides or larger ensembles, install locally. """ NOTES = """ ### What this tool does Predicts an ensemble of (φ, ψ) torsion-angle trajectories for any peptide sequence (4–100 residues recommended, capped at 50 on this demo). Useful for: - **Quick conformational triage** before launching expensive MD simulations - **Comparing sequence variants / mutants** side by side - **Estimating α-helix / β-sheet / PPII basin populations** - **Teaching biochemistry** with live, interactive Ramachandran plots - **AI-for-biology baselines and benchmarks** ### Honest limits - Density only, **not** kinetics (no transition rates / dwell times) - Backbone only, **no** side-chain rotamers (χ angles) - Monomer only, **no** multimer / aggregation - Best for 4–100 residue peptides; reliability degrades outside ### How to read the Ramachandran plot - **α-R region** (φ ≈ -60, ψ ≈ -45) → right-handed alpha-helix - **β-sheet region** (φ ≈ -120, ψ ≈ 120) → extended / beta-strand conformations - **PPII region** (φ ≈ -60, ψ ≈ 140) → polyproline-II extended (very common in short peptides in solution) - **α-L region** (φ ≈ 60, ψ ≈ 50) → left-handed helix, sterically forbidden for almost all amino acids; should be close to 0% if the model honors physics ### Architecture (one paragraph) A residue's (φ, ψ) is treated as a phase pair on a torus. An MLP emits per-residue oscillator parameters from sequence + position + current angles. A phase-flow ODE integrates 64 coupled phase oscillators with RK4. The result is decoded into a mixture of axis-independent von Mises distributions, sampled, and rolled out autoregressively. This is the protein-dynamics application of a multi-year line of work on phase oscillators (REZON hardware, phase-entanglement-rc, theta-gamma neural coupling). """ with gr.Blocks(title="AlphaDynamics") as demo: gr.Markdown(DESCRIPTION) with gr.Row(): with gr.Column(scale=1): seq_input = gr.Textbox( label="Peptide sequence", placeholder="e.g. KLVFFAE", value="AAAY", max_lines=1, ) with gr.Row(): n_ens_input = gr.Slider( minimum=1, maximum=MAX_ENSEMBLE, step=1, value=8, label="Trajectories (more = smoother density)", ) rs_input = gr.Slider( minimum=50, maximum=MAX_STEPS, step=50, value=500, label="Steps per trajectory", ) predict_btn = gr.Button("Predict torsion dynamics 🚀", variant="primary") info_md = gr.Markdown(label="Basin populations") file_out = gr.File(label="Download trajectory (.npz)") pdb_out = gr.File(label="Download backbone PDB (NEW v0.4.0)") gr.Markdown( "💡 **3D viewer:** open the downloaded `.pdb` in " "[PyMOL](https://pymol.org/) / VMD / ChimeraX, or browse online at " "[krisss0mecom.github.io/AlphaDynamics/examples/3d_movie_demo](https://krisss0mecom.github.io/AlphaDynamics/examples/3d_movie_demo/viewer.html)" ) with gr.Column(scale=2): plot_out = gr.Plot(label="Ramachandran density") predict_btn.click( fn=predict, inputs=[seq_input, n_ens_input, rs_input], outputs=[plot_out, info_md, file_out, pdb_out], ) gr.Examples( examples=EXAMPLES, inputs=[seq_input, n_ens_input, rs_input], label="Try a known peptide:", ) with gr.Accordion("📖 About this tool / how to read the plot", open=False): gr.Markdown(NOTES) gr.Markdown( "---\n" "Created by **Krzysztof Gwozdz** 🇵🇱 · Apache 2.0 · " "[Cite](https://huggingface.co/krissss0/alphadynamics) · " "[GitHub Issues](https://github.com/krisss0mecom/AlphaDynamics/issues) for feedback" ) if __name__ == "__main__": demo.queue(max_size=10).launch(server_name="0.0.0.0")