import numpy as np
from rdkit import Chem
from rdkit.Chem import AllChem, rdMolTransforms, Descriptors
from IPython.display import HTML, display
def find_rotatable_bond(mol):
"""Find a rotatable bond and return the 4 atoms defining a dihedral"""
mol = Chem.AddHs(mol)
AllChem.EmbedMolecule(mol, randomSeed=42)
for bond in mol.GetBonds():
if bond.GetBondType() == Chem.BondType.SINGLE:
atom1_idx = bond.GetBeginAtomIdx()
atom2_idx = bond.GetEndAtomIdx()
atom1 = mol.GetAtomWithIdx(atom1_idx)
atom2 = mol.GetAtomWithIdx(atom2_idx)
if atom1.GetAtomicNum() == 1 or atom2.GetAtomicNum() == 1:
continue
neighbors1 = [x.GetIdx() for x in atom1.GetNeighbors() if x.GetIdx() != atom2_idx]
neighbors2 = [x.GetIdx() for x in atom2.GetNeighbors() if x.GetIdx() != atom1_idx]
if neighbors1 and neighbors2:
dihedral = [neighbors1[0], atom1_idx, atom2_idx, neighbors2[0]]
return dihedral, mol
return None, mol
def create_molecule_examples():
"""Create pairs of flexible vs. restricted molecules"""
examples = {
'ethanol': {
'name': 'Ethanol Free-Rotation vs Restricted Movement in Rings',
'flexible': {
'smiles': 'CCO',
'desc': 'Ethanol (free C-C rotation)',
'dihedral': [0, 1, 2, 3]
},
'restricted': {
'smiles': 'C1CO1',
'desc': 'Ethylene oxide (locked in ring)',
'dihedral': [0, 1, 2, 3]
}
},
'butane': {
'name': 'Single vs Double Bond',
'flexible': {
'smiles': 'CCCC',
'desc': 'Butane (rotatable C-C single bond)',
'dihedral': None
},
'restricted': {
'smiles': 'CC=CC',
'desc': '2-Butene (rigid C=C double bond)',
'dihedral': None
}
},
'biphenyl': {
'name': 'Ortho-Substitution Effect',
'flexible': {
'smiles': 'c1ccccc1-c2ccccc2',
'desc': 'Biphenyl (free aryl-aryl rotation)',
'dihedral': None
},
'restricted': {
'smiles': 'Cc1ccccc1-c2ccccc2C',
'desc': '2,2\'-Dimethylbiphenyl (restricted)',
'dihedral': None
}
},
'peptide': {
'name': 'Ring Formation (Peptide-like)',
'flexible': {
'smiles': 'CC(N)C(=O)NCC',
'desc': 'Linear dipeptide fragment',
'dihedral': None
},
'restricted': {
'smiles': 'C1CNC(=O)C1',
'desc': 'Cyclic lactam (β-lactam-like)',
'dihedral': None
}
}
}
return examples
def prepare_molecule(smiles, dihedral_hint=None):
"""Prepare molecule and find rotatable dihedral"""
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None, None
if dihedral_hint:
mol_3d = Chem.AddHs(mol)
AllChem.EmbedMolecule(mol_3d, randomSeed=42)
AllChem.MMFFOptimizeMolecule(mol_3d, maxIters=200)
return mol_3d, dihedral_hint
else:
dihedral, mol_3d = find_rotatable_bond(mol)
if dihedral:
AllChem.MMFFOptimizeMolecule(mol_3d, maxIters=200)
return mol_3d, dihedral
def get_conformer_at_angle(mol, dihedral_atoms, angle_deg):
"""Generate conformer at specified dihedral angle"""
mol_copy = Chem.Mol(mol)
conf = mol_copy.GetConformer()
try:
rdMolTransforms.SetDihedralDeg(conf, *dihedral_atoms, angle_deg)
props = AllChem.MMFFGetMoleculeProperties(mol_copy)
if props is None:
return mol_copy, 0.0
ff = AllChem.MMFFGetMoleculeForceField(mol_copy, props)
energy = ff.CalcEnergy() if ff else 0.0
return mol_copy, energy
except:
return mol_copy, 0.0
def generate_conformer_trajectory(mol, dihedral_atoms, num_points=73):
"""Generate conformational trajectory"""
if mol is None or dihedral_atoms is None:
return None, None, None
angles = np.linspace(0, 360, num_points)
trajectory_xyz = ""
energies = []
for angle in angles:
mol_conf, energy = get_conformer_at_angle(mol, dihedral_atoms, angle)
trajectory_xyz += Chem.MolToXYZBlock(mol_conf)
energies.append(energy)
min_e = min(energies)
energies = [e - min_e for e in energies]
return trajectory_xyz, energies, angles
def calculate_barrier_height(energies):
"""Calculate rotational barrier from energy profile"""
if energies is None or len(energies) == 0:
return 0.0
return max(energies)
def create_comparison_viewer(flexible_data, restricted_data, example_info, width=900):
"""Create vertical comparison viewer with separate controls"""
flex_traj, flex_energies, flex_angles = flexible_data
rest_traj, rest_energies, rest_angles = restricted_data
if not all([flex_traj, rest_traj, flex_energies, rest_energies]):
return HTML("<div style='color: red; padding: 20px;'>Error: Could not generate trajectories</div>")
flex_barrier = calculate_barrier_height(flex_energies)
rest_barrier = calculate_barrier_height(rest_energies)
viewer_id = f"viewer_{np.random.randint(100000, 999999)}"
num_frames = len(flex_energies)
html_content = f"""
<style>
.viewer-container-{viewer_id} {{
position: relative !important;
width: 100% !important;
height: 300px !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
}}
.viewer-container-{viewer_id} canvas {{
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
}}
</style>
<div style="font-family: Arial, sans-serif; max-width: {width}px; margin: 20px auto;">
<div style="border: 3px solid #d9534f; border-radius: 10px; padding: 15px; background: white; margin-bottom: 20px;">
<div style="text-align: center; margin-bottom: 10px;">
<h3 style="color: #d9534f; margin: 0;">{example_info['flexible']['desc']}</h3>
<p style="color: #666; font-size: 13px; margin: 5px 0;">High flexibility - lower rotational barrier</p>
</div>
<div style="display: flex; gap: 20px; align-items: center;">
<div class="viewer-container-{viewer_id}" id="{viewer_id}_flex" style="flex: 1;"></div>
<canvas id="chart_flex_{viewer_id}" width="450" height="250" style="flex: 1;"></canvas>
</div>
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-top: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<label style="font-weight: bold; font-size: 14px;">
Dihedral Angle: <span id="flexAngleDisplay_{viewer_id}" style="color: #d9534f;">0°</span>
</label>
<div>
<strong>Energy:</strong>
<span id="flexEnergyDisplay_{viewer_id}" style="font-size: 16px; color: #d9534f;">{flex_energies[0]:.3f}</span> kcal/mol
<span style="margin-left: 15px; color: #666;">(Barrier: {flex_barrier:.2f} kcal/mol)</span>
</div>
</div>
<input type="range" id="flexSlider_{viewer_id}" min="0" max="{num_frames - 1}" value="0"
style="width: 100%; height: 8px; cursor: pointer;">
</div>
</div>
<div style="border: 3px solid #28a745; border-radius: 10px; padding: 15px; background: white; margin-bottom: 20px;">
<div style="text-align: center; margin-bottom: 10px;">
<h3 style="color: #28a745; margin: 0;">{example_info['restricted']['desc']}</h3>
<p style="color: #666; font-size: 13px; margin: 5px 0;">Restricted rotation - higher rotational barrier</p>
</div>
<div style="display: flex; gap: 20px; align-items: center;">
<div class="viewer-container-{viewer_id}" id="{viewer_id}_rest" style="flex: 1;"></div>
<canvas id="chart_rest_{viewer_id}" width="450" height="250" style="flex: 1;"></canvas>
</div>
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-top: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<label style="font-weight: bold; font-size: 14px;">
Dihedral Angle: <span id="restAngleDisplay_{viewer_id}" style="color: #28a745;">0°</span>
</label>
<div>
<strong>Energy:</strong>
<span id="restEnergyDisplay_{viewer_id}" style="font-size: 16px; color: #28a745;">{rest_energies[0]:.3f}</span> kcal/mol
<span style="margin-left: 15px; color: #666;">(Barrier: {rest_barrier:.2f} kcal/mol)</span>
</div>
</div>
<input type="range" id="restSlider_{viewer_id}" min="0" max="{num_frames - 1}" value="0"
style="width: 100%; height: 8px; cursor: pointer;">
</div>
</div>
</div>
<script src="https://3Dmol.csb.pitt.edu/build/3Dmol-min.js"></script>
<script>
(function() {{
setTimeout(function() {{
let flexContainer = document.getElementById('{viewer_id}_flex');
let restContainer = document.getElementById('{viewer_id}_rest');
let config_flex = {{
backgroundColor: '#fffef7',
disableFog: true
}};
let config_rest = {{
backgroundColor: '#f0fff4',
disableFog: true
}};
let viewer_flex = $3Dmol.createViewer(flexContainer, config_flex);
let viewer_rest = $3Dmol.createViewer(restContainer, config_rest);
viewer_flex.addModelsAsFrames(`{flex_traj}`, 'xyz');
viewer_rest.addModelsAsFrames(`{rest_traj}`, 'xyz');
viewer_flex.setStyle({{}}, {{stick: {{radius: 0.15}}, sphere: {{scale: 0.3}}}});
viewer_rest.setStyle({{}}, {{stick: {{radius: 0.15}}, sphere: {{scale: 0.3}}}});
viewer_flex.zoomTo();
viewer_rest.zoomTo();
viewer_flex.render();
viewer_rest.render();
let flex_energies = {flex_energies};
let rest_energies = {rest_energies};
let angles = {flex_angles.tolist()};
let flexFrame = 0;
let restFrame = 0;
let flexSlider = document.getElementById('flexSlider_{viewer_id}');
let restSlider = document.getElementById('restSlider_{viewer_id}');
let flexAngleDisplay = document.getElementById('flexAngleDisplay_{viewer_id}');
let restAngleDisplay = document.getElementById('restAngleDisplay_{viewer_id}');
let flexEnergyDisplay = document.getElementById('flexEnergyDisplay_{viewer_id}');
let restEnergyDisplay = document.getElementById('restEnergyDisplay_{viewer_id}');
function drawChart(canvasId, energies, color, label, currentFrame) {{
let canvas = document.getElementById(canvasId);
if (!canvas) return;
let ctx = canvas.getContext('2d');
let w = canvas.width;
let h = canvas.height;
let pad = {{l: 60, r: 20, t: 35, b: 50}};
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = '#fafafa';
ctx.fillRect(0, 0, w, h);
let maxE = Math.max(...energies);
let plotW = w - pad.l - pad.r;
let plotH = h - pad.t - pad.b;
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {{
let y = pad.t + (plotH / 5) * i;
ctx.beginPath();
ctx.moveTo(pad.l, y);
ctx.lineTo(pad.l + plotW, y);
ctx.stroke();
}}
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(pad.l, pad.t);
ctx.lineTo(pad.l, pad.t + plotH);
ctx.lineTo(pad.l + plotW, pad.t + plotH);
ctx.stroke();
ctx.strokeStyle = color;
ctx.lineWidth = 2.5;
ctx.beginPath();
for (let i = 0; i < energies.length; i++) {{
let x = pad.l + (i / (energies.length - 1)) * plotW;
let y = pad.t + plotH - (energies[i] / maxE) * plotH;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}}
ctx.stroke();
let cx = pad.l + (currentFrame / (energies.length - 1)) * plotW;
let cy = pad.t + plotH - (energies[currentFrame] / maxE) * plotH;
ctx.fillStyle = '#f44336';
ctx.beginPath();
ctx.arc(cx, cy, 6, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#333';
ctx.font = '11px Arial';
ctx.textAlign = 'right';
for (let i = 0; i <= 5; i++) {{
let e = maxE * (1 - i / 5);
let y = pad.t + (plotH / 5) * i;
ctx.fillText(e.toFixed(2), pad.l - 5, y + 4);
}}
ctx.textAlign = 'center';
for (let angle of [0, 60, 120, 180, 240, 300, 360]) {{
let x = pad.l + (angle / 360) * plotW;
ctx.fillText(angle + '°', x, pad.t + plotH + 18);
}}
ctx.font = 'bold 12px Arial';
ctx.save();
ctx.translate(18, h / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillText('Energy (kcal/mol)', 0, 0);
ctx.restore();
ctx.textAlign = 'center';
ctx.fillText('Dihedral Angle (degrees)', pad.l + plotW / 2, h - 10);
ctx.font = 'bold 14px Arial';
ctx.fillStyle = color;
ctx.fillText(label, w / 2, 22);
}}
function updateFlex() {{
flexAngleDisplay.textContent = angles[flexFrame].toFixed(0) + '°';
flexEnergyDisplay.textContent = flex_energies[flexFrame].toFixed(3);
viewer_flex.setFrame(flexFrame);
viewer_flex.render();
drawChart('chart_flex_{viewer_id}', flex_energies, '#d9534f', 'Flexible - Energy Profile', flexFrame);
}}
function updateRest() {{
restAngleDisplay.textContent = angles[restFrame].toFixed(0) + '°';
restEnergyDisplay.textContent = rest_energies[restFrame].toFixed(3);
viewer_rest.setFrame(restFrame);
viewer_rest.render();
drawChart('chart_rest_{viewer_id}', rest_energies, '#28a745', 'Restricted - Energy Profile', restFrame);
}}
flexSlider.addEventListener('input', function() {{
flexFrame = parseInt(this.value);
updateFlex();
}});
restSlider.addEventListener('input', function() {{
restFrame = parseInt(this.value);
updateRest();
}});
updateFlex();
updateRest();
window.addEventListener('resize', function() {{
viewer_flex.resize();
viewer_rest.resize();
viewer_flex.render();
viewer_rest.render();
}});
}}, 100);
}})();
</script>
"""
return HTML(html_content)
def demonstrate_conformational_restriction(example='ethanol'):
"""
Demonstrate conformational restriction strategies
Parameters:
-----------
example : str
One of: 'ethanol', 'butane', 'biphenyl', 'peptide'
"""
examples = create_molecule_examples()
if example not in examples:
return None
example_info = examples[example]
flex_info = example_info['flexible']
flex_mol, flex_dihedral = prepare_molecule(flex_info['smiles'], flex_info['dihedral'])
if flex_mol is None or flex_dihedral is None:
return None
flex_data = generate_conformer_trajectory(flex_mol, flex_dihedral)
rest_info = example_info['restricted']
rest_mol, rest_dihedral = prepare_molecule(rest_info['smiles'], rest_info['dihedral'])
if rest_mol is None or rest_dihedral is None:
return None
rest_data = generate_conformer_trajectory(rest_mol, rest_dihedral)
if all([flex_data[0], rest_data[0]]):
viewer = create_comparison_viewer(flex_data, rest_data, example_info)
display(viewer)
return viewer
else:
return None
viewer1 = demonstrate_conformational_restriction('ethanol')