Chapter 2: Molecular Conformations and Rotation#

1. Introduction#

Welcome to the study of molecular flexibility, a cornerstone of modern drug design. Molecules are not static—they constantly change shape through bond rotations. These different 3D arrangements, called conformations, determine how drugs interact with biological targets. Understanding conformational flexibility is essential in drug discovery because:

  • Only certain conformations fit into protein binding sites

  • Molecular shape affects absorption, distribution, and metabolism

  • Energy barriers between conformations influence bioavailability


2. Key Concepts and Definitions#

This section consolidates the core terminology essential for understanding molecular flexibility in drug discovery.

  • Conformation: Different 3D spatial arrangements of atoms arising from rotation around single bonds (without breaking bonds). Bioactive conformation means the specific 3D shape (conformer) a molecule must adopt to bind to its biological target and elicit a response.

  • Conformer: A specific conformational isomer

  • Torsion (Dihedral) Angle: The angle between two planes defined by a sequence of four connected atoms (A-B-C-D). It is the standard way to measure the degree of rotation around the central B-C bond.

  • Rotational Energy Barrier: The energy required to rotate around a single bond. This barrier arises from steric hindrance (atomic clashes), making some conformations more stable (lower energy) than others. Forcing a drug into a high-energy conformation weakens its binding affinity.

  • Rotatable Bond: A single, non-ring bond connecting two non-terminal, heavy atoms (non-hydrogen). Rotation around this bond allows the molecule to adopt different conformations.


3. Main Content#

3.1 Types of Conformations and Rotational Barriers#

Molecular conformations arise from rotation around single (σ) bonds. The energy varies with rotation angle due to:

  • Steric hindrance: Repulsion between electron clouds of nearby atoms

  • Torsional strain: Eclipsing interactions between adjacent bonds

  • Electronic effects: Hyperconjugation and orbital interactions

Lower energy means greater stability because the molecule has minimized its potential for change.

Think of it like a ball on a hill:

  • High Energy (Unstable): A ball perched at the top of a hill has high potential energy. It is unstable because the slightest nudge will make it roll away.

  • Low Energy (Stable): A ball resting at the bottom of a valley has low potential energy. It is stable because it naturally stays there and resists moving.

Molecules behave the same way: they naturally shed excess energy (release heat) to reach a “relaxed” state where they are less likely to react or break apart.

Hide code cell source

import numpy as np
from rdkit import Chem
from rdkit.Chem import AllChem, rdMolTransforms
from IPython.display import HTML, display

def create_ethanol():
    """Create and optimize ethanol molecule"""
    mol = Chem.MolFromSmiles('CCO')
    mol = Chem.AddHs(mol)
    AllChem.EmbedMolecule(mol, randomSeed=42)
    AllChem.MMFFOptimizeMolecule(mol, maxIters=200)
    return mol

def create_biphenyl():
    """Create and optimize biphenyl molecule"""
    mol = Chem.MolFromSmiles('c1ccccc1-c2ccccc2')
    mol = Chem.AddHs(mol)
    AllChem.EmbedMolecule(mol, randomSeed=42)
    AllChem.MMFFOptimizeMolecule(mol, maxIters=200)
    return mol

def get_conformer_at_angle_ethanol(mol, angle_deg):
    """Generate ethanol conformer at specified dihedral angle"""
    mol_copy = Chem.Mol(mol)
    conf = mol_copy.GetConformer()
    
    atom_ids = [0, 1, 2, 3]
    rdMolTransforms.SetDihedralDeg(conf, atom_ids[0], atom_ids[1], 
                                    atom_ids[2], atom_ids[3], angle_deg)
    
    props = AllChem.MMFFGetMoleculeProperties(mol_copy)
    ff = AllChem.MMFFGetMoleculeForceField(mol_copy, props)
    energy = ff.CalcEnergy()
    
    return mol_copy, energy

def get_conformer_at_angle_biphenyl(mol, angle_deg):
    """Generate biphenyl conformer at specified dihedral angle"""
    mol_copy = Chem.Mol(mol)
    conf = mol_copy.GetConformer()
    
    atom_ids = [4, 5, 6, 7]
    
    rdMolTransforms.SetDihedralDeg(conf, atom_ids[0], atom_ids[1], 
                                    atom_ids[2], atom_ids[3], angle_deg)
    
    props = AllChem.MMFFGetMoleculeProperties(mol_copy)
    ff = AllChem.MMFFGetMoleculeForceField(mol_copy, props)
    energy = ff.CalcEnergy()
    
    return mol_copy, energy

def generate_conformer_trajectory(mol, molecule_type='ethanol', num_points=73):
    """Generate trajectory of conformers across full rotation"""
    angles = np.linspace(0, 360, num_points)
    trajectory_xyz = ""
    energies = []
    
    for angle in angles:
        if molecule_type == 'ethanol':
            mol_conf, energy = get_conformer_at_angle_ethanol(mol, angle)
        else:
            mol_conf, energy = get_conformer_at_angle_biphenyl(mol, 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 create_dual_conformer_viewer(ethanol_data, biphenyl_data, width=900):
    """Create interactive viewer showing both ethanol and biphenyl conformational analysis"""
    
    eth_traj, eth_energies, eth_angles = ethanol_data
    biph_traj, biph_energies, biph_angles = biphenyl_data
    
    num_frames = len(eth_energies)
    viewer_id = f"viewer_{np.random.randint(100000, 999999)}"
    
    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 #2196F3; border-radius: 10px; padding: 15px; background: white; margin-bottom: 20px;">
            <div style="text-align: center; margin-bottom: 10px;">
                <h3 style="color: #2196F3; margin: 0;">Ethanol (CCO)</h3>
                <p style="color: #666; font-size: 13px; margin: 5px 0;">Aliphatic C-C single bond rotation</p>
            </div>
            <div style="display: flex; gap: 20px; align-items: center;">
                <div class="viewer-container-{viewer_id}" id="{viewer_id}_ethanol" style="flex: 1;"></div>
                <canvas id="chart_eth_{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="ethAngleDisplay_{viewer_id}" style="color: #2196F3;">0°</span>
                    </label>
                    <div>
                        <strong>Energy:</strong> 
                        <span id="ethEnergyDisplay_{viewer_id}" style="font-size: 16px; color: #2196F3;">{eth_energies[0]:.3f}</span> kcal/mol
                    </div>
                </div>
                <input type="range" id="ethSlider_{viewer_id}" min="0" max="{num_frames - 1}" value="0" 
                       style="width: 100%; height: 8px; cursor: pointer;">
            </div>
        </div>
        
        <div style="border: 3px solid #9C27B0; border-radius: 10px; padding: 15px; background: white; margin-bottom: 20px;">
            <div style="text-align: center; margin-bottom: 10px;">
                <h3 style="color: #9C27B0; margin: 0;">Biphenyl (Ph-Ph)</h3>
                <p style="color: #666; font-size: 13px; margin: 5px 0;">Aromatic C-C single bond rotation</p>
            </div>
            <div style="display: flex; gap: 20px; align-items: center;">
                <div class="viewer-container-{viewer_id}" id="{viewer_id}_biphenyl" style="flex: 1;"></div>
                <canvas id="chart_biph_{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="biphAngleDisplay_{viewer_id}" style="color: #9C27B0;">0°</span>
                    </label>
                    <div>
                        <strong>Energy:</strong> 
                        <span id="biphEnergyDisplay_{viewer_id}" style="font-size: 16px; color: #9C27B0;">{biph_energies[0]:.3f}</span> kcal/mol
                    </div>
                </div>
                <input type="range" id="biphSlider_{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 ethContainer = document.getElementById('{viewer_id}_ethanol');
            let biphContainer = document.getElementById('{viewer_id}_biphenyl');
            
            let viewer_eth = $3Dmol.createViewer(ethContainer, {{backgroundColor: '#f0f8ff'}});
            let viewer_biph = $3Dmol.createViewer(biphContainer, {{backgroundColor: '#f3e5f5'}});
            
            viewer_eth.addModelsAsFrames(`{eth_traj}`, 'xyz');
            viewer_biph.addModelsAsFrames(`{biph_traj}`, 'xyz');
            
            viewer_eth.setStyle({{}}, {{stick: {{radius: 0.15}}, sphere: {{scale: 0.3}}}});
            viewer_biph.setStyle({{}}, {{stick: {{radius: 0.15}}, sphere: {{scale: 0.3}}}});
            
            viewer_eth.zoomTo();
            viewer_biph.zoomTo();
            viewer_eth.render();
            viewer_biph.render();
            
            let eth_energies = {eth_energies};
            let biph_energies = {biph_energies};
            let angles = {eth_angles.tolist()};
            let ethFrame = 0;
            let biphFrame = 0;
            
            let ethSlider = document.getElementById('ethSlider_{viewer_id}');
            let biphSlider = document.getElementById('biphSlider_{viewer_id}');
            let ethAngleDisplay = document.getElementById('ethAngleDisplay_{viewer_id}');
            let biphAngleDisplay = document.getElementById('biphAngleDisplay_{viewer_id}');
            let ethEnergyDisplay = document.getElementById('ethEnergyDisplay_{viewer_id}');
            let biphEnergyDisplay = document.getElementById('biphEnergyDisplay_{viewer_id}');
            
            function drawChart(canvasId, energies, color, title, 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();
                
                ctx.fillStyle = '#4CAF50';
                for (let angle of [60, 180, 300]) {{
                    let idx = Math.round(angle / 360 * (energies.length - 1));
                    let x = pad.l + (idx / (energies.length - 1)) * plotW;
                    let y = pad.t + plotH - (energies[idx] / maxE) * plotH;
                    ctx.beginPath();
                    ctx.arc(x, y, 4, 0, 2 * Math.PI);
                    ctx.fill();
                }}
                
                ctx.fillStyle = '#FF5722';
                for (let angle of [0, 120, 240, 360]) {{
                    let idx = Math.round(angle / 360 * (energies.length - 1));
                    let x = pad.l + (idx / (energies.length - 1)) * plotW;
                    let y = pad.t + plotH - (energies[idx] / maxE) * plotH;
                    ctx.beginPath();
                    ctx.arc(x, y, 4, 0, 2 * Math.PI);
                    ctx.fill();
                }}
                
                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(title, w / 2, 22);
                
                ctx.font = '10px Arial';
                ctx.textAlign = 'left';
                ctx.fillStyle = '#4CAF50';
                ctx.fillText('● Staggered', pad.l + 5, h - 25);
                ctx.fillStyle = '#FF5722';
                ctx.fillText('● Eclipsed', pad.l + 90, h - 25);
            }}
            
            function updateEthanol() {{
                ethAngleDisplay.textContent = angles[ethFrame].toFixed(0) + '°';
                ethEnergyDisplay.textContent = eth_energies[ethFrame].toFixed(3);
                viewer_eth.setFrame(ethFrame);
                viewer_eth.render();
                drawChart('chart_eth_{viewer_id}', eth_energies, '#2196F3', 'Ethanol Energy Profile', ethFrame);
            }}
            
            function updateBiphenyl() {{
                biphAngleDisplay.textContent = angles[biphFrame].toFixed(0) + '°';
                biphEnergyDisplay.textContent = biph_energies[biphFrame].toFixed(3);
                viewer_biph.setFrame(biphFrame);
                viewer_biph.render();
                drawChart('chart_biph_{viewer_id}', biph_energies, '#9C27B0', 'Biphenyl Energy Profile', biphFrame);
            }}
            
            ethSlider.addEventListener('input', function() {{
                ethFrame = parseInt(this.value);
                updateEthanol();
            }});
            
            biphSlider.addEventListener('input', function() {{
                biphFrame = parseInt(this.value);
                updateBiphenyl();
            }});
            
            updateEthanol();
            updateBiphenyl();
            
            window.addEventListener('resize', function() {{
                viewer_eth.resize();
                viewer_biph.resize();
                viewer_eth.render();
                viewer_biph.render();
            }});
        }}, 100);
    }})();
    </script>
    """
    
    return HTML(html_content)

def main():
    eth_mol = create_ethanol()
    eth_traj, eth_energies, eth_angles = generate_conformer_trajectory(eth_mol, 'ethanol', num_points=73)
    
    biph_mol = create_biphenyl()
    biph_traj, biph_energies, biph_angles = generate_conformer_trajectory(biph_mol, 'biphenyl', num_points=73)
    
    ethanol_data = (eth_traj, eth_energies, eth_angles)
    biphenyl_data = (biph_traj, biph_energies, biph_angles)
    
    viewer = create_dual_conformer_viewer(ethanol_data, biphenyl_data)
    display(viewer)
    
    return viewer

if __name__ == "__main__":
    viewer = main()

Ethanol (CCO)

Aliphatic C-C single bond rotation

Energy: 0.609 kcal/mol

Biphenyl (Ph-Ph)

Aromatic C-C single bond rotation

Energy: 6.812 kcal/mol

3.2 Conformational Analysis in Drug Design#

Impact on binding affinity: The energetic cost of adopting the bioactive conformation is called the conformational penalty. If a drug’s preferred (lowest energy) conformation in solution differs significantly from its bioactive conformation, binding affinity decreases.

Case Study: Macrocyclic drugs Macrocycles like cyclosporine (immunosuppressant) have restricted conformational flexibility due to their ring structure. This pre-organization reduces the entropic penalty upon binding, often leading to improved binding affinity despite their size violating Lipinski’s rules.

Conformational restriction strategies:

  1. Introducing rings: Converts flexible chains into rigid structures (e.g., converting linear to cyclic peptides)

  2. Double bonds: Creates rotational barriers >40 kcal/mol

  3. Ortho-substitution: Bulky groups restrict rotation around aryl-aryl bonds

Hide code cell source

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')

Ethanol (free C-C rotation)

High flexibility - lower rotational barrier

Energy: 0.609 kcal/mol (Barrier: 1.47 kcal/mol)

Ethylene oxide (locked in ring)

Restricted rotation - higher rotational barrier

Energy: 0.000 kcal/mol (Barrier: 0.00 kcal/mol)

3.3 Rotatable Bonds and Drug-Likeness#

The number of rotatable bonds is a key predictor of oral bioavailability:

Property

Threshold

Rationale

Rotatable bonds

≤ 10

High flexibility → poor membrane permeability

Rotatable bonds

≤ 7

Preferred for CNS drugs (more rigid)

Why fewer is better:

  • Flexible molecules lose more entropy upon binding → weaker affinity

  • High flexibility → larger polar surface area exposure → poor permeability

  • More conformations → higher desolvation penalty

A key strategy in drug design is rigidification: reducing a molecule’s flexibility to lower the entropic penalty of binding. This can be achieved by forming rings, introducing bulky groups that restrict rotation, or creating internal hydrogen bonds.


4. Summary and Key Takeaways#

In this section, we’ve explored how molecular flexibility, quantified by the Number of Rotatable Bonds (NRB), is a critical factor in drug design. We learned to identify rotatable bonds, understand their impact on a drug’s ability to adopt a bioactive conformation, and appreciate the energetic trade-offs involved.

  • Rotatable Bonds Drive Flexibility: The primary source of conformational flexibility in a molecule is rotation around single, non-ring bonds between non-terminal heavy atoms.

  • Bioactive Conformation is Key: A drug must adopt a specific 3D shape to bind its target. Flexibility allows the drug to find this shape, but too much flexibility incurs an entropic penalty that weakens binding.

  • NRB is a Practical Guideline: Counting rotatable bonds (NRB ≤ 10 is a common rule for oral drugs) provides a rapid, effective heuristic for assessing a molecule’s drug-like potential.

  • Rigidification is a Design Strategy: Medicinal chemists intentionally reduce flexibility through techniques like ring formation to pre-organize a drug into its active shape, often improving potency and selectivity.

  • Flexibility is a Balance: The goal is not to simply minimize flexibility, but to achieve an optimal balance that allows for target binding while maintaining favorable properties like solubility and oral bioavailability.