🏔️ The Live Folding Landscape¶
Visualizing the Energy Funnel Theory of Protein Folding¶
🎯 Learning Objectives¶
In this tutorial, we explore one of the most profound concepts in structural biology: the Energy Funnel. Proteins do not fold by random searching (which would take longer than the age of the universe, known as Levinthal's Paradox). Instead, they follow a rugged energy landscape that guides them toward their most stable, "native" state.
You will learn:
- 🧮 How to generate a Ramachandran-style Energy Surface for a peptide
- 🔥 How to run Simulated Annealing using the
synth-pdbphysics engine - 📊 How to visualize the folding trajectory on a 3D energy landscape
- 🎨 How canonical secondary structures (α-helix, β-sheet) appear as energy minima
💡 Scientific Context: The energy landscape theory, pioneered by Wolynes, Onuchic, and others in the 1990s, revolutionized our understanding of protein folding. It explains how proteins can fold rapidly despite astronomical conformational possibilities.
# 🔧 Environment Detection & Setup (Works in VS Code + Google Colab)
import os
import sys
# Detect environment
IN_COLAB = 'google.colab' in sys.modules
if IN_COLAB:
print("🌐 Running in Google Colab")
# Install synth-pdb if not already installed
try:
import synth_pdb
print(" ✅ synth-pdb already installed")
except ImportError:
print(" 📦 Installing synth-pdb...")
!pip install -q synth-pdb py3Dmol biotite
print(" ✅ Installation complete")
# Colab uses 'notebook' renderer for Plotly
import plotly.io as pio
pio.renderers.default = 'colab'
else:
print("💻 Running in local Jupyter environment (VS Code/JupyterLab)")
# Add parent directory to path for local development
sys.path.append(os.path.abspath('../../'))
print("✅ Environment configured successfully!")
import os
import sys
import ipywidgets as widgets
import numpy as np
import plotly.graph_objects as go
import plotly.io as pio
import py3Dmol
from IPython.display import clear_output, display
# --- UNIVERSAL SETUP ---
from synth_pdb import EnergyMinimizer, PDBValidator, PeptideGenerator, PeptideResult
# Initialize variables
X, Y, Z = np.array([]), np.array([]), np.array([])
traj_phi, traj_psi, energies = np.array([]), np.array([]), []
trajectory_structs = []
# Custom color scheme for scientific visualization
ENERGY_COLORSCALE = 'Viridis' # Perceptually uniform, colorblind-friendly
TRAJECTORY_COLOR = '#FF6B35' # Vibrant orange for contrast
print("✅ Environment Ready | synth-pdb v1.0 | Enhanced Visualization Mode")
📚 Theoretical Foundation¶
The Energy Landscape Paradigm¶
The protein folding energy landscape can be described mathematically as:
$$E(\phi, \psi) = E_{\text{bond}} + E_{\text{angle}} + E_{\text{dihedral}} + E_{\text{vdW}} + E_{\text{elec}}$$Where:
- $\phi$ (phi) = backbone dihedral angle C-N-Cα-C
- $\psi$ (psi) = backbone dihedral angle N-Cα-C-N
- $E_{\text{vdW}}$ = van der Waals interactions (steric clashes)
- $E_{\text{elec}}$ = electrostatic interactions
Ramachandran Regions¶
| Region | φ (degrees) | ψ (degrees) | Secondary Structure |
|---|---|---|---|
| α-helix | -60 | -45 | Right-handed helix |
| β-sheet | -120 | +120 | Extended strand |
| PPII | -75 | +145 | Polyproline II helix |
| Left α | +60 | +45 | Left-handed helix (rare) |
⚠️ Note: Glycine (no side chain) can access all regions. Proline (cyclic) is restricted to φ ≈ -60°.
1️⃣ Creating the Energy Landscape with Ramachandran Overlays¶
We'll systematically scan the φ/ψ space and calculate the potential energy at each point. This creates a 2D energy surface that reveals the "allowed" and "forbidden" regions of conformational space.
🔬 Computational Details:
- Grid resolution: 15×15 (225 conformations)
- Force field: AMBER14
- Energy calculation: Single-point (no minimization)
- Expected runtime: ~30 seconds
minimizer = EnergyMinimizer()
def get_energy_for_angles(phi, psi, sequence="ALA-ALA-ALA"):
"""Calculate energy for a given phi/psi of the central residue."""
gen = PeptideGenerator(sequence)
phis = [-57.0, phi, -57.0] # Alpha helix flanking
psis = [-47.0, psi, -47.0]
try:
res = gen.generate(phi_list=phis, psi_list=psis)
energy = minimizer.calculate_energy(res)
return energy if energy is not None else 10000.0
except:
return 10000.0
# Generate energy grid
res_grid = 15
phis = np.linspace(-180, 180, res_grid)
psis = np.linspace(-180, 180, res_grid)
X, Y = np.meshgrid(phis, psis)
Z = np.zeros_like(X)
print("🔄 Generating Energy Landscape...")
print(" This will sample 225 conformations across φ/ψ space")
print(" " + "="*50)
for i in range(res_grid):
for j in range(res_grid):
val = get_energy_for_angles(X[i,j], Y[i,j])
Z[i,j] = min(val, 5000) # Cap extreme energies
# Progress indicator
progress = (i + 1) / res_grid * 100
bar_length = int(progress / 2)
bar = "█" * bar_length + "░" * (50 - bar_length)
print(f"\r [{bar}] {progress:.0f}%", end="")
print("\n " + "="*50)
print("✅ Energy Landscape Complete!")
print(f" Min Energy: {Z.min():.1f} kJ/mol")
print(f" Max Energy: {Z.max():.1f} kJ/mol")
print(f" Energy Range: {Z.max() - Z.min():.1f} kJ/mol")
2️⃣ Running the Folding Trajectory¶
Now we simulate the folding process using iterative energy minimization. Starting from a random conformation, we allow the peptide to relax toward its energy minimum.
🧪 Simulation Protocol:
- Initial state: Random conformation
- Algorithm: Steepest descent minimization
- Steps: 15 refinement cycles
- Iterations per cycle: 20
- Tracking: Energy, φ/ψ angles, structure snapshots
sequence = "ALA-ALA-ALA-ALA-ALA"
gen = PeptideGenerator(sequence)
res = gen.generate(conformation="random")
trajectory_structs = []
energies = []
phi_psi_history = []
validator = PDBValidator(res.pdb)
print("🔥 Starting Folding Simulation...")
print(" Peptide: 5×Alanine (Poly-A)")
print(" " + "="*60)
import tempfile
for step in range(15):
# Energy minimization
with tempfile.NamedTemporaryFile(suffix='.pdb', mode='w', delete=False) as f_in:
f_in.write(res.pdb)
f_in.close()
with tempfile.NamedTemporaryFile(suffix='.pdb', delete=False) as f_out:
f_out.close()
minimizer.minimize(f_in.name, f_out.name, max_iterations=20)
with open(f_out.name) as r:
updated_pdb = r.read()
res = PeptideResult(updated_pdb)
os.unlink(f_in.name)
os.unlink(f_out.name)
# Record trajectory
trajectory_structs.append(res.structure.copy())
current_energy = minimizer.calculate_energy(res)
energies.append(current_energy)
# Track central residue angles
angles = validator.calculate_dihedrals(res)
phi_psi_history.append([angles['phi'][2], angles['psi'][2]])
# Progress with energy display
energy_change = energies[-1] - energies[-2] if len(energies) > 1 else 0
arrow = "↓" if energy_change < 0 else "↑" if energy_change > 0 else "→"
print(f" Step {step+1:2d}/15 | Energy: {energies[-1]:7.2f} kJ/mol {arrow} Δ={energy_change:+6.2f}")
traj_phi = np.array([p[0] for p in phi_psi_history])
traj_psi = np.array([p[1] for p in phi_psi_history])
print(" " + "="*60)
print("✅ Folding Complete!")
print(f" Initial Energy: {energies[0]:.2f} kJ/mol")
print(f" Final Energy: {energies[-1]:.2f} kJ/mol")
print(f" Total ΔE: {energies[-1] - energies[0]:.2f} kJ/mol")
print(f" Final φ/ψ: ({traj_phi[-1]:.1f}°, {traj_psi[-1]:.1f}°)")
3️⃣ The Interactive 3D Energy Funnel¶
This visualization combines:
- 🗺️ Energy surface (blue gradient) showing the conformational landscape
- 🎯 Folding trajectory (orange path) showing the actual folding route
- 📍 Ramachandran regions (annotated) showing canonical secondary structures
🎨 Interaction Tips:
- Rotate: Click and drag
- Zoom: Scroll or pinch
- Pan: Right-click and drag
- Hover: See exact φ/ψ/E values
import os
if len(X) == 0 or len(traj_phi) == 0:
print("⚠️ Please run the previous cells first to generate data.")
else:
# Create figure with Ramachandran annotations
fig = go.Figure()
# Energy surface
fig.add_trace(go.Surface(
z=Z, x=X, y=Y,
colorscale=ENERGY_COLORSCALE,
opacity=0.85,
name='Energy Surface',
colorbar={
'title': "Energy<br>(kJ/mol)",
'tickmode': "linear",
'tick0': Z.min(),
'dtick': (Z.max() - Z.min()) / 5
},
hovertemplate='φ: %{x:.1f}°<br>ψ: %{y:.1f}°<br>E: %{z:.1f} kJ/mol<extra></extra>'
))
# Folding trajectory
fig.add_trace(go.Scatter3d(
x=traj_phi, y=traj_psi, z=energies,
mode='markers+lines',
marker={
'size': 8,
'color': np.arange(len(energies)),
'colorscale': 'Hot',
'showscale': True,
'colorbar': {
'title': "Step",
'x': 1.15
},
'line': {'color': 'white', 'width': 2}
},
line={'color': TRAJECTORY_COLOR, 'width': 6},
name='Folding Path',
hovertemplate='Step %{marker.color}<br>φ: %{x:.1f}°<br>ψ: %{y:.1f}°<br>E: %{z:.1f} kJ/mol<extra></extra>'
))
# Add Ramachandran region annotations
annotations = [
{'x': -60, 'y': -45, 'z': Z.max()*0.3, 'text': "α-helix", 'showarrow': False,
'font': {'size': 14, 'color': 'white'}, 'bgcolor': 'rgba(0,100,200,0.7)'},
{'x': -120, 'y': 120, 'z': Z.max()*0.3, 'text': "β-sheet", 'showarrow': False,
'font': {'size': 14, 'color': 'white'}, 'bgcolor': 'rgba(200,100,0,0.7)'},
{'x': -75, 'y': 145, 'z': Z.max()*0.3, 'text': "PPII", 'showarrow': False,
'font': {'size': 14, 'color': 'white'}, 'bgcolor': 'rgba(100,200,0,0.7)'}
]
# Layout
fig.update_layout(
title={
'text': '🏔️ Protein Folding Energy Landscape<br><sub>Ramachandran Space with Folding Trajectory</sub>',
'x': 0.5,
'xanchor': 'center',
'font': {'size': 20}
},
scene={
'xaxis': {'title': 'Phi φ (degrees)', 'backgroundcolor': 'rgb(20,20,20)', 'gridcolor': 'rgb(50,50,50)'},
'yaxis': {'title': 'Psi ψ (degrees)', 'backgroundcolor': 'rgb(20,20,20)', 'gridcolor': 'rgb(50,50,50)'},
'zaxis': {'title': 'Potential Energy (kJ/mol)', 'backgroundcolor': 'rgb(20,20,20)', 'gridcolor': 'rgb(50,50,50)'},
'camera': {
'eye': {'x': 1.5, 'y': 1.5, 'z': 1.3}
}
},
width=1000,
height=800,
template='plotly_dark',
showlegend=True,
legend={
'x': 0.02,
'y': 0.98,
'bgcolor': 'rgba(0,0,0,0.5)'
}
)
fig.show(renderer='json' if os.getenv('CI') else None)
(kJ/mol)", 'tickmode': "linear", 'tick0': Z.min(), 'dtick': (Z.max() - Z.min()) / 5 }, hovertemplate='φ: %{x:.1f}°
ψ: %{y:.1f}°
E: %{z:.1f} kJ/mol
φ: %{x:.1f}°
ψ: %{y:.1f}°
E: %{z:.1f} kJ/mol
Ramachandran Space with Folding Trajectory', 'x': 0.5, 'xanchor': 'center', 'font': {'size': 20} }, scene={ 'xaxis': {'title': 'Phi φ (degrees)', 'backgroundcolor': 'rgb(20,20,20)', 'gridcolor': 'rgb(50,50,50)'}, 'yaxis': {'title': 'Psi ψ (degrees)', 'backgroundcolor': 'rgb(20,20,20)', 'gridcolor': 'rgb(50,50,50)'}, 'zaxis': {'title': 'Potential Energy (kJ/mol)', 'backgroundcolor': 'rgb(20,20,20)', 'gridcolor': 'rgb(50,50,50)'}, 'camera': { 'eye': {'x': 1.5, 'y': 1.5, 'z': 1.3} } }, width=1000, height=800, template='plotly_dark', showlegend=True, legend={ 'x': 0.02, 'y': 0.98, 'bgcolor': 'rgba(0,0,0,0.5)' } ) fig.show(renderer='json' if os.getenv('CI') else None)
4️⃣ Molecular Structure Viewer¶
Browse through the folding trajectory snapshots to see the backbone actually condensing into its stable form.
🔍 Visualization Features:
- Stick representation with spectrum coloring (N→C terminus: blue→red)
- Sphere representation for atoms
- Interactive rotation and zoom
- Real-time energy display
import os
IN_CI = bool(os.getenv("CI"))
if not IN_CI:
import io
import py3Dmol
from biotite.structure.io.pdb import PDBFile
viewer_output = widgets.Output()
info_label = widgets.HTML(
"<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); "
"color: white; padding: 15px; border-radius: 10px; font-family: sans-serif; "
"box-shadow: 0 4px 6px rgba(0,0,0,0.3);'>"
"<b>🧬 Select a snapshot to begin</b></div>"
)
def display_molecule(index):
"""Render structure using py3Dmol with enhanced styling."""
if len(trajectory_structs) == 0:
return
# Update info panel
energy_delta = energies[index] - energies[0] if index > 0 else 0
info_label.value = f"""
<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; padding: 15px; border-radius: 10px;
font-family: sans-serif; box-shadow: 0 4px 6px rgba(0,0,0,0.3);'>
<b>📸 Snapshot {index}</b> |
Energy: <span style='color: #FFD700;'>{energies[index]:.1f} kJ/mol</span> |
ΔE: <span style='color: {'#00FF00' if energy_delta < 0 else '#FF6B6B'};'>{energy_delta:+.1f} kJ/mol</span> |
φ/ψ: <span style='color: #87CEEB;'>({traj_phi[index]:.1f}°, {traj_psi[index]:.1f}°)</span>
</div>
"""
with viewer_output:
clear_output(wait=True)
# Get PDB string
pdb_file = PDBFile()
pdb_file.set_structure(trajectory_structs[index])
sink = io.StringIO()
pdb_file.write(sink)
pdb_str = sink.getvalue()
# Create viewer
view = py3Dmol.view(width=600, height=450)
view.addModel(pdb_str, 'pdb')
# Enhanced styling
view.setStyle({
'stick': {'colorscheme': 'chainHetatm', 'radius': 0.15},
'sphere': {'scale': 0.25, 'colorscheme': 'chainHetatm'}
})
view.setBackgroundColor('#1a1a1a')
view.zoomTo()
view_widget = view.show()
display(view_widget)
def on_slider_change(change):
display_molecule(change['new'])
# Create slider with custom styling
slider = widgets.IntSlider(
value=0,
min=0,
max=max(0, len(trajectory_structs)-1),
step=1,
description='Snapshot:',
continuous_update=False,
layout=widgets.Layout(width='600px'),
style={'description_width': '80px'}
)
slider.observe(on_slider_change, names='value')
# Display
display(widgets.VBox([
info_label,
slider,
viewer_output
]))
# 3Dmol.js initializes asynchronously in the browser. Calling view.show()
# at kernel execution time races with the library bootstrap and produces
# the 'failed to load' error. Show a placeholder instead; the first
# slider drag fires display_molecule() after 3Dmol.js is fully ready.
with viewer_output:
from IPython.display import HTML as _HTML
from IPython.display import display as _disp
_disp(_HTML(
'<div style="text-align:center;padding:40px;color:#aaa;'
'border:1px dashed #555;border-radius:8px;'
'font-style:italic;background:#1a1a1a;">'
'🔄 Move the slider above to load the 3D structure'
'</div>'
))
# (Widget output skipped in CI)
🎓 Key Takeaways¶
- Energy Landscapes are Rugged: The folding pathway navigates through multiple local minima
- Ramachandran Constraints: Only certain φ/ψ combinations are sterically allowed
- Funnel Topology: Proteins fold via a funnel-shaped landscape, not a single pathway
- Secondary Structure Stability: α-helices and β-sheets occupy deep energy wells
📖 Further Reading¶
- Dill & MacCallum (2012). "The Protein-Folding Problem, 50 Years On." Science 338:1042-1046. DOI: 10.1126/science.1219021
- Onuchic et al. (1997). "Theory of protein folding: the energy landscape perspective." Annu Rev Phys Chem 48:545-600. DOI: 10.1146/annurev.physchem.48.1.545
- Ramachandran et al. (1963). "Stereochemistry of polypeptide chain configurations." J Mol Biol 7:95-99. DOI: 10.1016/S0022-2836(63)80023-6
🚀 Next Steps¶
Try modifying the code to:
- Use different amino acid sequences (e.g.,
GLY-ALA-GLYto see glycine flexibility) - Increase grid resolution for smoother landscapes
- Compare different force fields
- Analyze multi-domain proteins
🎉 Tutorial Complete!
You've successfully visualized the protein folding energy landscape using synth-pdb
Continue exploring the other interactive tutorials to learn more!