Chapter 2: Physicochemical Properties of Molecules#

1. Introduction#

In this section, we will explore the core physicochemical properties, particularly ionization states of drugs, that govern a drug’s journey through the human body.

The ionization state of a drug molecule fundamentally determines its journey through the human body. A drug’s pKa—the pH at which a molecule is 50% ionized—governs its absorption across membranes, distribution to tissues, and accumulation in specific compartments. Since the human body presents a diverse pH landscape (stomach pH ~1.5, blood pH ~7.4, intestinal lumen pH ~6-8), understanding pKa is essential for predicting drug behavior and optimizing therapeutic outcomes.


2. Key Concepts and Definitions#

  • pKa (acid dissociation constant): The pH at which 50% of a compound exists in its ionized form and 50% in its neutral form; indicates the tendency of a molecule to donate (acids) or accept (bases) a proton.

  • Ionization state: The charged or uncharged form of a molecule at a given pH; acids ionize by losing H⁺ (becoming negatively charged), while bases ionize by gaining H⁺ (becoming positively charged).

  • Physiological pH: The pH of different body compartments (stomach ~2, intestine ~6.5, blood ~7.4); determines the predominant ionization state of drugs in each location.

  • pH-partition hypothesis: Uncharged (neutral) molecules cross lipid membranes more readily than charged molecules; predicts that weak acids are better absorbed from acidic environments and weak bases from basic environments.


3. Main Content#

3.1 Understanding Ionization: Acids vs. Bases#

Acidic drugs (e.g., aspirin, ibuprofen, warfarin) donate protons and become negatively charged when ionized:

  • Below their pKa: Predominantly neutral (uncharged) → membrane permeable

  • Above their pKa: Predominantly ionized (charged, COO⁻) → water soluble but membrane impermeable

Test your knowledge with the interactive below: can you identify which protons are removed from these acidic drugs?

Hide code cell source

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

def identify_acidic_hydrogens(mol):
    """
    Identify acidic hydrogen atoms in a molecule.
    Returns list of hydrogen atom indices that are acidic.
    """
    acidic_h_indices = []
    
    # Add explicit hydrogens if not present
    mol = Chem.AddHs(mol)
    
    for atom in mol.GetAtoms():
        if atom.GetSymbol() != 'H':
            continue
            
        # Get the heavy atom this H is attached to
        neighbors = atom.GetNeighbors()
        if len(neighbors) != 1:
            continue
            
        heavy_atom = neighbors[0]
        heavy_symbol = heavy_atom.GetSymbol()
        
        # Check for acidic hydrogen patterns
        is_acidic = False
        
        # 1. Carboxylic acid O-H
        if heavy_symbol == 'O':
            # Check if oxygen is part of C(=O)OH
            for bond in heavy_atom.GetBonds():
                other_atom = bond.GetOtherAtom(heavy_atom)
                if other_atom.GetSymbol() == 'C':
                    # Check if this carbon has a C=O
                    for c_bond in other_atom.GetBonds():
                        if c_bond.GetBondType() == Chem.BondType.DOUBLE:
                            c_neighbor = c_bond.GetOtherAtom(other_atom)
                            if c_neighbor.GetSymbol() == 'O':
                                is_acidic = True
                                break
        
        # 2. Sulfonamide N-H (R-SO2-NH-R)
        elif heavy_symbol == 'N':
            for bond in heavy_atom.GetBonds():
                other_atom = bond.GetOtherAtom(heavy_atom)
                if other_atom.GetSymbol() == 'S':
                    # Check if sulfur has SO2 group
                    o_count = sum(1 for n in other_atom.GetNeighbors() if n.GetSymbol() == 'O')
                    if o_count >= 2:
                        is_acidic = True
                        break
            
            # 3. Amide N-H in heterocycles (like barbiturates, imides)
            if not is_acidic and heavy_atom.IsInRing():
                # Check if N is part of imide or urea-like structure
                for bond in heavy_atom.GetBonds():
                    other_atom = bond.GetOtherAtom(heavy_atom)
                    if other_atom.GetSymbol() == 'C':
                        # Check for C=O attached to this carbon
                        for c_bond in other_atom.GetBonds():
                            if c_bond.GetBondType() == Chem.BondType.DOUBLE:
                                c_neighbor = c_bond.GetOtherAtom(other_atom)
                                if c_neighbor.GetSymbol() == 'O':
                                    is_acidic = True
                                    break
        
        if is_acidic:
            acidic_h_indices.append(atom.GetIdx())
    
    return acidic_h_indices

def create_acidic_hydrogen_game():
    """Create interactive game for identifying acidic hydrogens in drug molecules"""
    
    # Define drug molecules with acidic hydrogens
    molecules = {
        'level1': {
            'name': 'Aspirin',
            'smiles': 'CC(=O)Oc1ccccc1C(=O)O',
            'description': 'Anti-inflammatory drug with 1 acidic H',
            'difficulty': 'Beginner',
            'hint': 'Look for the carboxylic acid group (-COOH)'
        },
        'level2': {
            'name': 'Phenobarbital',
            'smiles': 'CCC(C1C(=O)NC(=O)NC1=O)c2ccccc2',
            'description': 'Barbiturate sedative with 2 acidic H',
            'difficulty': 'Intermediate',
            'hint': 'Imide N-H groups in the ring are acidic'
        },
        'level3': {
            'name': 'Sulfamethoxazole',
            'smiles': 'Cc1cc(no1)NS(=O)(=O)c2ccc(cc2)N',
            'description': 'Antibiotic with 1 very acidic H',
            'difficulty': 'Advanced',
            'hint': 'Sulfonamide N-H is highly acidic (pKa ~6)'
        }
    }
    
    # Generate 3D structures and identify acidic hydrogens
    mol_data = {}
    for level, data in molecules.items():
        mol = Chem.MolFromSmiles(data['smiles'])
        mol = Chem.AddHs(mol)
        
        # Generate 3D coordinates
        res = AllChem.EmbedMolecule(mol, randomSeed=42)
        if res == -1:
            AllChem.EmbedMolecule(mol, useRandomCoords=True)
        AllChem.MMFFOptimizeMolecule(mol)
        
        # Identify acidic hydrogens
        acidic_h = identify_acidic_hydrogens(mol)
        
        # Generate XYZ block
        xyz_block = Chem.MolToXYZBlock(mol)
        
        mol_data[level] = {
            'xyz': xyz_block,
            'acidic_h': acidic_h,
            'num_acidic': len(acidic_h),
            'name': data['name'],
            'description': data['description'],
            'difficulty': data['difficulty'],
            'hint': data['hint']
        }
    
    mol_data_json = json.dumps(mol_data)
    viewer_id = f"acidic_viewer_{np.random.randint(100000, 999999)}"
    
    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <script src="https://3Dmol.csb.pitt.edu/build/3Dmol-min.js"></script>
        <style>
            .game-container {{
                font-family: 'Segoe UI', Arial, sans-serif;
                max-width: 800px; margin: 0 auto; padding: 20px;
                background: #fff; border-radius: 12px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.1); border: 1px solid #ddd;
            }}
            .header {{ text-align: center; margin-bottom: 25px; }}
            .header h1 {{ color: #2c3e50; margin: 0; font-size: 24px; }}
            .header p {{ color: #7f8c8d; margin: 5px 0; font-size: 14px; }}
            
            .level-selector {{ display: flex; gap: 10px; margin-bottom: 20px; }}
            .level-btn {{
                flex: 1; padding: 12px; border: 2px solid #eee;
                border-radius: 8px; cursor: pointer; transition: all 0.2s;
                text-align: center; background: #f8f9fa;
            }}
            .level-btn:hover {{ border-color: #e74c3c; background: #fff5f5; }}
            .level-btn.active {{ border-color: #e74c3c; background: #ffebee; color: #c62828; }}
            
            .viewer-wrapper {{
                height: 400px; width: 100%; position: relative;
                border: 2px solid #e74c3c; border-radius: 8px; overflow: hidden;
                background: #fffef7;
            }}
            
            .status-bar {{
                display: flex; justify-content: space-between;
                padding: 15px; background: #f8f9fa; border-radius: 8px; margin-bottom: 15px;
            }}
            .stat-box {{ text-align: center; }}
            .stat-val {{ font-size: 20px; font-weight: bold; color: #2c3e50; }}
            .stat-label {{ font-size: 12px; color: #7f8c8d; text-transform: uppercase; }}
            
            .hint-box {{
                background: #fff3e0; border-left: 4px solid #ff9800;
                padding: 12px; margin-bottom: 15px; border-radius: 4px;
                display: none;
            }}
            .hint-box.show {{ display: block; }}
            
            .completion-msg {{
                display: none; background: #d4edda; color: #155724;
                padding: 15px; border-radius: 8px; text-align: center; margin-top: 15px;
                border: 1px solid #c3e6cb;
            }}
            
            .btn {{ 
                padding: 10px 20px; border: none; border-radius: 6px;
                cursor: pointer; font-weight: 600; transition: all 0.2s;
                margin: 5px;
            }}
            .btn-reset {{ background: #e74c3c; color: white; }}
            .btn-reset:hover {{ background: #c0392b; }}
            .btn-hint {{ background: #f39c12; color: white; }}
            .btn-hint:hover {{ background: #d68910; }}
            
            .button-row {{ 
                display: flex; gap: 10px; margin-top: 15px;
            }}
            .button-row button {{ flex: 1; }}
        </style>
    </head>
    <body>
        <div class="game-container">
            <div class="header">
                <h1>Acidic Hydrogen Identification Challenge</h1>
                <p>Click on acidic hydrogen atoms in drug molecules</p>
            </div>

            <div style="background: #e3f2fd; border-left: 4px solid #2196F3; padding: 15px; margin-bottom: 20px; border-radius: 4px;">
                <h4 style="margin: 0 0 10px 0; color: #1565c0;">📋 Instructions:</h4>
                <ol style="margin: 5px 0; padding-left: 20px; color: #666; font-size: 13px;">
                    <li><strong>Click only on HYDROGEN atoms</strong> that you think are acidic</li>
                    <li>Acidic hydrogens are typically found in:
                        <ul style="margin: 5px 0;">
                            <li>Carboxylic acids (-COOH)</li>
                            <li>Sulfonamides (-SO₂NH-)</li>
                            <li>Imides and certain N-H groups</li>
                        </ul>
                    </li>
                    <li>Green flash = Correct ✓ | Red flash = Incorrect ✗</li>
                    <li>Rotate: drag | Zoom: scroll</li>
                </ol>
            </div>
            
            <div class="level-selector">
                <div class="level-btn active" onclick="window.game_{viewer_id}.setLevel('level1', this)">
                    <strong>Level 1</strong><br><span style="font-size:12px">Aspirin</span>
                </div>
                <div class="level-btn" onclick="window.game_{viewer_id}.setLevel('level2', this)">
                    <strong>Level 2</strong><br><span style="font-size:12px">Phenobarbital</span>
                </div>
                <div class="level-btn" onclick="window.game_{viewer_id}.setLevel('level3', this)">
                    <strong>Level 3</strong><br><span style="font-size:12px">Sulfamethoxazole</span>
                </div>
            </div>

            <div class="status-bar">
                <div class="stat-box">
                    <div class="stat-label">Acidic H Total</div>
                    <div class="stat-val" id="total_{viewer_id}">0</div>
                </div>
                <div class="stat-box">
                    <div class="stat-label">Found</div>
                    <div class="stat-val" style="color:#27ae60" id="found_{viewer_id}">0</div>
                </div>
                <div class="stat-box">
                    <div class="stat-label">Remaining</div>
                    <div class="stat-val" style="color:#e74c3c" id="rem_{viewer_id}">0</div>
                </div>
            </div>
            
            <div id="hint_{viewer_id}" class="hint-box">
                <strong>💡 Hint:</strong> <span id="hint_text_{viewer_id}"></span>
            </div>

            <div id="viewer_{viewer_id}" class="viewer-wrapper"></div>
            
            <div id="msg_{viewer_id}" class="completion-msg">
                <strong>🎉 Excellent!</strong> You identified all acidic hydrogens correctly!
            </div>
            
            <div class="button-row">
                <button class="btn btn-hint" onclick="window.game_{viewer_id}.toggleHint()">
                    💡 Show Hint
                </button>
                <button class="btn btn-reset" onclick="window.game_{viewer_id}.reset()">
                    🔄 Reset Level
                </button>
            </div>
            
            <div style="margin-top:20px; font-size: 12px; color: #666; background: #f0f0f0; padding: 12px; border-radius: 6px;">
                <strong>ℹ️ About Acidic Hydrogens:</strong> Acidic hydrogens can be deprotonated under physiological conditions. 
                The pKa values range from ~4 (carboxylic acids) to ~10 (phenols). In drug molecules, acidic groups 
                affect solubility, absorption, and protein binding.
            </div>
        </div>

        <script>
        window.game_{viewer_id} = (function() {{
            const molecules = {mol_data_json};
            let viewer = null;
            let currentLevel = 'level1';
            let found = new Set();
            let clicked = new Set();
            let hintShown = false;
            
            function init() {{
                viewer = $3Dmol.createViewer("viewer_{viewer_id}", {{
                    backgroundColor: '#fffef7'
                }});
                loadLevel('level1');
            }}
            
            function loadLevel(level) {{
                currentLevel = level;
                found.clear();
                clicked.clear();
                hintShown = false;
                document.getElementById('msg_{viewer_id}').style.display = 'none';
                document.getElementById('hint_{viewer_id}').classList.remove('show');
                
                let data = molecules[level];
                viewer.clear();
                viewer.addModel(data.xyz, "xyz");
                
                // Style all atoms
                viewer.setStyle({{}}, {{
                    stick: {{radius: 0.15}},
                    sphere: {{scale: 0.3}}
                }});
                
                // Make only hydrogens clickable
                viewer.setClickable({{elem: 'H'}}, true, function(atom) {{
                    handleAtomClick(atom);
                }});
                
                viewer.zoomTo();
                viewer.render();
                updateStats();
            }}
            
            function handleAtomClick(atom) {{
                if(clicked.has(atom.index)) return;
                
                let data = molecules[currentLevel];
                let isAcidic = data.acidic_h.includes(atom.index);
                
                if(isAcidic) {{
                    if(!found.has(atom.index)) {{
                        found.add(atom.index);
                        flashAtom(atom, '#2ecc71', true);
                        updateStats();
                        checkWin();
                    }}
                }} else {{
                    flashAtom(atom, '#e74c3c', false);
                }}
            }}
            
            function flashAtom(atom, color, keepHighlight) {{
                // Highlight with color
                viewer.setStyle({{index: atom.index}}, {{
                    stick: {{radius: 0.2}},
                    sphere: {{color: color, scale: 0.7}}
                }});
                viewer.render();
                
                clicked.add(atom.index);
                setTimeout(() => {{
                    if(keepHighlight) {{
                        // Keep green for correct answers
                        viewer.setStyle({{index: atom.index}}, {{
                            stick: {{radius: 0.2}},
                            sphere: {{color: '#2ecc71', scale: 0.5}}
                        }});
                    }} else {{
                        // Revert to normal for incorrect
                        viewer.setStyle({{index: atom.index}}, {{
                            stick: {{radius: 0.15}},
                            sphere: {{scale: 0.3}}
                        }});
                    }}
                    viewer.render();
                    clicked.delete(atom.index);
                }}, 600);
            }}
            
            function updateStats() {{
                let total = molecules[currentLevel].num_acidic;
                let f = found.size;
                document.getElementById('total_{viewer_id}').innerText = total;
                document.getElementById('found_{viewer_id}').innerText = f;
                document.getElementById('rem_{viewer_id}').innerText = total - f;
            }}
            
            function checkWin() {{
                if(found.size === molecules[currentLevel].num_acidic) {{
                    document.getElementById('msg_{viewer_id}').style.display = 'block';
                }}
            }}
            
            setTimeout(init, 100);
            
            return {{
                setLevel: function(level, btnElement) {{
                    let btns = document.querySelectorAll('.level-btn');
                    btns.forEach(b => b.classList.remove('active'));
                    if(btnElement) btnElement.classList.add('active');
                    loadLevel(level);
                }},
                reset: function() {{
                    loadLevel(currentLevel);
                }},
                toggleHint: function() {{
                    let hintBox = document.getElementById('hint_{viewer_id}');
                    let hintText = document.getElementById('hint_text_{viewer_id}');
                    hintText.innerText = molecules[currentLevel].hint;
                    hintBox.classList.toggle('show');
                }}
            }};
        }})();
        </script>
    </body>
    </html>
    """
    
    return HTML(html)

# Run the game
create_acidic_hydrogen_game()

Acidic Hydrogen Identification Challenge

Click on acidic hydrogen atoms in drug molecules

📋 Instructions:

  1. Click only on HYDROGEN atoms that you think are acidic
  2. Acidic hydrogens are typically found in:
    • Carboxylic acids (-COOH)
    • Sulfonamides (-SO₂NH-)
    • Imides and certain N-H groups
  3. Green flash = Correct ✓ | Red flash = Incorrect ✗
  4. Rotate: drag | Zoom: scroll
Level 1
Aspirin
Level 2
Phenobarbital
Level 3
Sulfamethoxazole
Acidic H Total
0
Found
0
Remaining
0
💡 Hint:
🎉 Excellent! You identified all acidic hydrogens correctly!
ℹ️ About Acidic Hydrogens: Acidic hydrogens can be deprotonated under physiological conditions. The pKa values range from ~4 (carboxylic acids) to ~10 (phenols). In drug molecules, acidic groups affect solubility, absorption, and protein binding.

With increasing pH, the following happens,

  1. First, deprotonation of aspirin happens: As the pH rises, acidic protons are removed from the drug molecules.

  2. Next, the proton is transferred to water: Each proton is transferred to a water molecule (1\(H_2O\)) to form a hydronium ion (2\(H_3O^+\)).

  3. Finally, this process results in the acidic drug becoming an ionized, negatively charged species.

Explore the simulation below to visualize the three steps of this process

Hide code cell source

import numpy as np
from rdkit import Chem
from rdkit.Chem import AllChem
from IPython.display import HTML
import json
import random

from rdkit import RDLogger
RDLogger.DisableLog('rdApp.*')  # This hides all RDKit warnings

# --- SETUP COLORS ---
COLORS = {
    'neutral': {'text_primary': '#333333', 'text_secondary': '#666666', 'border': '#dee2e6', 'background': '#f5f5f5'},
    'chart': {'curve': '#9C27B0', 'pka_line': '#FF9800', 'current': '#f44336'}
}

# --- 1. PREPARE MOLECULAR TEMPLATES ---
def prepare_templates():
    # A. Aspirin System
    aspirin = Chem.MolFromSmiles('CC(=O)Oc1ccccc1C(=O)O')
    aspirin = Chem.AddHs(aspirin)
    
    # Identify the acidic proton (H on COOH) and its connected Oxygen
    patt = Chem.MolFromSmarts('C(=O)[OH]')
    matches = aspirin.GetSubstructMatches(patt)
    acidic_h_idx = -1
    acidic_o_idx_orig = -1
    
    if matches:
        # matches[0] is (Carbon, Dbl_O, Sgl_O)
        o_atom_idx = matches[0][2]
        o_atom = aspirin.GetAtomWithIdx(o_atom_idx)
        acidic_o_idx_orig = o_atom_idx
        for neighbor in o_atom.GetNeighbors():
            if neighbor.GetSymbol() == 'H':
                neighbor.SetIsotope(2) 
                acidic_h_idx = neighbor.GetIdx()
    
    AllChem.EmbedMolecule(aspirin, randomSeed=42)
    AllChem.MMFFOptimizeMolecule(aspirin)
    
    # Create Base (RCOO-) by removing the H
    aspirin_base = Chem.RWMol(aspirin)
    aspirin_base.RemoveAtom(acidic_h_idx)
    
    # Calculate new O index in the base molecule (indices shift after removal)
    asp_base_o_idx = acidic_o_idx_orig
    if acidic_h_idx < acidic_o_idx_orig:
        asp_base_o_idx -= 1
    
    # Get H position relative to Aspirin
    conf_asp = aspirin.GetConformer()
    start_pos = np.array(conf_asp.GetAtomPosition(acidic_h_idx))
    
    # B. Water System (Hydronium)
    hydronium = Chem.MolFromSmiles('[OH3+]')
    hydronium = Chem.AddHs(hydronium)
    
    # Mark the 3rd H as the "accepted" one
    target_h_idx = 3 
    hydronium.GetAtomWithIdx(target_h_idx).SetIsotope(2)
    
    # Get the Oxygen index (usually 0 for [OH3+])
    hyd_o_idx_orig = 0
    for atom in hydronium.GetAtoms():
        if atom.GetSymbol() == 'O':
            hyd_o_idx_orig = atom.GetIdx()
            break
            
    AllChem.EmbedMolecule(hydronium, randomSeed=42)
    AllChem.MMFFOptimizeMolecule(hydronium)
    
    # Create Base (H2O) by removing the H
    water_base = Chem.RWMol(hydronium)
    water_base.RemoveAtom(target_h_idx)
    
    # Calculate new O index in water base
    wat_base_o_idx = hyd_o_idx_orig
    if target_h_idx < hyd_o_idx_orig:
        wat_base_o_idx -= 1
    
    conf_hyd = hydronium.GetConformer()
    end_pos = np.array(conf_hyd.GetAtomPosition(target_h_idx))
    
    return aspirin, aspirin_base, start_pos, asp_base_o_idx, hydronium, water_base, end_pos, wat_base_o_idx

# Initialize global templates
ASPIRIN, ASPIRIN_BASE, ASP_H_POS, ASP_O_IDX, HYDRONIUM, WATER_BASE, HYD_H_POS, WAT_O_IDX = prepare_templates()

def create_proton():
    # A single atom molecule for the flying proton
    mol = Chem.MolFromSmiles('[2H]') # Initially Isotope 2
    AllChem.EmbedMolecule(mol)
    return mol

def add_thermal_motion(mol, seed, amplitude=0.15):
    """Add random thermal vibration to molecule"""
    conf = mol.GetConformer()
    np.random.seed(seed)
    for i in range(mol.GetNumAtoms()):
        pos = conf.GetAtomPosition(i)
        noise = np.random.normal(0, amplitude, 3)
        conf.SetAtomPosition(i, [pos.x + noise[0], pos.y + noise[1], pos.z + noise[2]])

def position_molecules_interpolated(fraction_deprot, frame_idx):
    """
    Creates a scene where the proton position is interpolated.
    Manages bonds and coloring dynamically.
    """
    
    pairs_layout = [
        {'asp': (-6, 5, 0),   'wat': (-1.5, 3.5, 0)},
        {'asp': (6, 5, 0),    'wat': (1.5, 3.5, 0)},
        {'asp': (-6, -5, 0),  'wat': (-1.5, -3.5, 0)},
        {'asp': (6, -5, 0),   'wat': (1.5, -3.5, 0)}
    ]
    
    combined_mol = None
    
    # Store data for post-processing bonds and colors
    # List of dicts: {'proton_idx': int, 'asp_o_idx': int, 'wat_o_idx': int, 't': float}
    bond_info = []
    
    current_atom_count = 0
    val = fraction_deprot * 4.0
    
    # 1. BUILD GEOMETRY
    for i, layout in enumerate(pairs_layout):
        asp_offset = np.array(layout['asp'])
        wat_offset = np.array(layout['wat'])
        
        # Calculate progress 't' for this pair
        if val >= i + 1: t = 1.0 
        elif val <= i: t = 0.0
        else: t = val - i
        
        # A. Aspirin Base
        mol_asp = Chem.Mol(ASPIRIN_BASE)
        conf = mol_asp.GetConformer()
        for idx in range(mol_asp.GetNumAtoms()):
            p = conf.GetAtomPosition(idx)
            conf.SetAtomPosition(idx, [p.x + asp_offset[0], p.y + asp_offset[1], p.z + asp_offset[2]])
        add_thermal_motion(mol_asp, seed=frame_idx*1000 + i*100, amplitude=0.1)
        
        if combined_mol is None: combined_mol = mol_asp
        else: combined_mol = Chem.CombineMols(combined_mol, mol_asp)
        
        # Track Aspirin Oxygen Global Index
        global_asp_o = current_atom_count + ASP_O_IDX
        current_atom_count += mol_asp.GetNumAtoms()
        
        # B. Water Base
        mol_wat = Chem.Mol(WATER_BASE)
        conf = mol_wat.GetConformer()
        for idx in range(mol_wat.GetNumAtoms()):
            p = conf.GetAtomPosition(idx)
            conf.SetAtomPosition(idx, [p.x + wat_offset[0], p.y + wat_offset[1], p.z + wat_offset[2]])
        add_thermal_motion(mol_wat, seed=frame_idx*1000 + i*100 + 50, amplitude=0.15)
        
        combined_mol = Chem.CombineMols(combined_mol, mol_wat)
        
        # Track Water Oxygen Global Index
        global_wat_o = current_atom_count + WAT_O_IDX
        current_atom_count += mol_wat.GetNumAtoms()
        
        # C. Proton
        proton = create_proton()
        start_g = asp_offset + ASP_H_POS
        end_g = wat_offset + HYD_H_POS
        curr_pos = start_g * (1 - t) + end_g * t
        proton.GetConformer().SetAtomPosition(0, curr_pos)
        add_thermal_motion(proton, seed=frame_idx*1000 + i*100 + 99, amplitude=0.1)
        
        combined_mol = Chem.CombineMols(combined_mol, proton)
        
        # Track Proton Global Index
        global_prot = current_atom_count # It's the first atom of the single-atom molecule
        current_atom_count += 1
        
        bond_info.append({
            'proton_idx': global_prot,
            'asp_o_idx': global_asp_o,
            'wat_o_idx': global_wat_o,
            't': t
        })

    # 2. ADD BONDS & UPDATE ISOTOPES (Requires Editable Mol)
    rw_mol = Chem.RWMol(combined_mol)
    
    for info in bond_info:
        t = info['t']
        p_idx = info['proton_idx']
        
        # --- COLORING LOGIC ---
        # 0.05 to 0.95 = Active Transfer (Cyan/Isotope 2)
        # Ends = Attached (White/Isotope 0)
        atom = rw_mol.GetAtomWithIdx(p_idx)
        if 0.05 < t < 0.95:
            atom.SetIsotope(2) # Cyan
        else:
            atom.SetIsotope(0) # White
            
        # --- BONDING LOGIC ---
        # t < 0.4: Bonded to Aspirin
        # t > 0.6: Bonded to Water
        # 0.4 - 0.6: Bond Broken (Gap)
        
        if t < 0.4:
            rw_mol.AddBond(p_idx, info['asp_o_idx'], Chem.BondType.SINGLE)
        elif t > 0.6:
            rw_mol.AddBond(p_idx, info['wat_o_idx'], Chem.BondType.SINGLE)
            
    return Chem.MolToMolBlock(rw_mol)

# --- CALCULATIONS ---
def calculate_fraction_deprotonated(pH, pKa):
    return 1 / (1 + 10**(pKa - pH))

def generate_ph_trajectory(pKa=3.5):
    trajectory_data = []
    ph_values = []
    fractions_deprotonated = []
    status_messages = []
    
    # 81 frames from pH 0 to 8
    for i in range(81):
        pH = i * 0.1
        fraction = calculate_fraction_deprotonated(pH, pKa)
        
        # Generate interpolated scene
        scene_sdf = position_molecules_interpolated(fraction, i)
        
        trajectory_data.append(scene_sdf)
        ph_values.append(pH)
        fractions_deprotonated.append(fraction * 100)
        
        if abs(pH - pKa) < 0.2:
            status_messages.append(f"pH {pH:.1f}: Equilibrium (~50% Transfer)")
        elif pH < pKa:
            status_messages.append(f"pH {pH:.1f}: Mostly Protonated (H on Aspirin)")
        else:
            status_messages.append(f"pH {pH:.1f}: Mostly Deprotonated (H on Water)")
            
    return trajectory_data, ph_values, fractions_deprotonated, status_messages, pKa

# --- HTML VIEWER ---
def create_ph_viewer(width=1000):
    trajectory_data, ph_values, fractions, status_messages, pKa = generate_ph_trajectory()
    full_trajectory = "\n$$$$\n".join(trajectory_data) + "\n$$$$\n"
    
    ids = {k: f"{k}_{np.random.randint(1e5, 9e5)}" for k in 
           ['viewer', 'slider', 'ph', 'fraction', 'status', 'canvas']}
    
    html = f"""
    <div style="font-family: Arial, sans-serif; max-width: {width}px; margin: 20px auto;">
        <div style="border: 3px solid {COLORS['chart']['curve']}; border-radius: 10px; padding: 20px; background: white;">
            
            <div style="text-align: center; margin-bottom: 20px;">
                <h2 style="color: {COLORS['chart']['curve']}; margin: 0;">Interactive Proton Transfer</h2>
                <p style="color: #666; font-size: 14px;">Drag the slider to visualize the bond breaking and formation</p>
            </div>
            
            <div style="display: flex; gap: 20px;">
                <div style="flex: 1.5; position: relative;">
                    <div id="{ids['viewer']}" style="width: 100%; height: 450px; border: 2px solid #dee2e6; border-radius: 8px; background: #f0f8ff;"></div>
                    <div style="position: absolute; top: 10px; right: 10px; background: rgba(255,255,255,0.9); padding: 8px; border-radius: 4px; font-size: 12px; border: 1px solid #ccc;">
                        <div style="margin-bottom: 4px;"><span style="color: cyan; font-weight: bold; font-size: 14px;">●</span> Transferring Proton</div>
                        <div style="margin-bottom: 4px;"><span style="color: red; font-weight: bold;">●</span> Oxygen</div>
                        <div><span style="color: grey; font-weight: bold;">●</span> Carbon</div>
                    </div>
                </div>
                
                <div style="flex: 1; display: flex; flex-direction: column;">
                    <canvas id="{ids['canvas']}" width="350" height="300" style="border: 1px solid #eee; border-radius: 8px; margin-bottom: 15px;"></canvas>
                    
                    <div style="background: {COLORS['neutral']['background']}; padding: 15px; border-radius: 8px;">
                        <div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
                            <div style="text-align: center;">
                                <div style="font-size: 12px; color: #666;">pH</div>
                                <div id="{ids['ph']}" style="font-size: 24px; font-weight: bold; color: {COLORS['chart']['curve']};">0.0</div>
                            </div>
                            <div style="text-align: center;">
                                <div style="font-size: 12px; color: #666;">% Deprotonated</div>
                                <div id="{ids['fraction']}" style="font-size: 24px; font-weight: bold; color: #FF6B6B;">0%</div>
                            </div>
                        </div>
                        
                        <input type="range" id="{ids['slider']}" min="0" max="80" value="0" 
                               style="width: 100%; height: 8px; cursor: pointer; background: #ddd; accent-color: {COLORS['chart']['curve']};">
                        
                        <div id="{ids['status']}" style="text-align: center; margin-top: 10px; font-size: 13px; font-weight: bold; color: #444;"></div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <script>
    (function() {{
        setTimeout(function() {{
            const container = document.getElementById('{ids['viewer']}');
            const config = {{backgroundColor: '#f0f8ff', antialias: true}};
            const viewer = $3Dmol.createViewer(container, config);
            
            const trajectory = `{full_trajectory}`;
            const phValues = {json.dumps(ph_values)};
            const fractions = {json.dumps(fractions)};
            const statusMsgs = {json.dumps(status_messages)};
            const pKa = {pKa};
            
            viewer.addModelsAsFrames(trajectory, 'sdf');
            
            // Standard Styles
            viewer.setStyle({{}}, {{stick: {{radius: 0.15, color: 'lightgrey'}}, sphere: {{scale: 0.3, color: 'lightgrey'}} }});
            viewer.setStyle({{elem: 'C'}}, {{stick: {{color: '#909090'}}, sphere: {{scale: 0.3, color: '#909090'}} }});
            viewer.setStyle({{elem: 'O'}}, {{stick: {{color: '#FF0000'}}, sphere: {{scale: 0.35, color: '#FF0000'}} }});
            
            // Default H Style (White) - applies to standard H and inactive transfer H
            viewer.setStyle({{elem: 'H'}}, {{stick: {{color: '#FFFFFF'}}, sphere: {{scale: 0.25, color: '#FFFFFF'}} }});
            
            // SPECIAL STYLE: ACTIVE PROTON (Isotope 2)
            // Only applies when the proton is in flight (mid-transfer)
            viewer.setStyle({{elem: 'H', isotope: 2}}, {{
                sphere: {{color: 'cyan', scale: 0.5}},
                stick: {{color: 'cyan', radius: 0.2}}
            }});
            
            viewer.zoomTo();
            viewer.render();
            
            const slider = document.getElementById('{ids['slider']}');
            const phDisplay = document.getElementById('{ids['ph']}');
            const fracDisplay = document.getElementById('{ids['fraction']}');
            const statusDisplay = document.getElementById('{ids['status']}');
            const canvas = document.getElementById('{ids['canvas']}');
            const ctx = canvas.getContext('2d');
            
            let currentFrame = 0;
            
            function drawChart() {{
                const w = canvas.width;
                const h = canvas.height;
                const pad = 40;
                
                ctx.clearRect(0,0,w,h);
                
                // Draw Axes
                ctx.beginPath();
                ctx.strokeStyle = '#333';
                ctx.lineWidth = 2;
                ctx.moveTo(pad, pad);
                ctx.lineTo(pad, h-pad);
                ctx.lineTo(w-pad, h-pad);
                ctx.stroke();
                
                // Draw Curve
                ctx.beginPath();
                ctx.strokeStyle = '{COLORS['chart']['curve']}';
                ctx.lineWidth = 3;
                for(let i=0; i<fractions.length; i++) {{
                    let x = pad + (i/(fractions.length-1)) * (w-2*pad);
                    let y = (h-pad) - (fractions[i]/100) * (h-2*pad);
                    if(i===0) ctx.moveTo(x,y);
                    else ctx.lineTo(x,y);
                }}
                ctx.stroke();
                
                // Draw pKa line
                let pKaX = pad + (pKa/8.0) * (w-2*pad); 
                ctx.beginPath();
                ctx.strokeStyle = 'orange';
                ctx.setLineDash([5,5]);
                ctx.moveTo(pKaX, pad);
                ctx.lineTo(pKaX, h-pad);
                ctx.stroke();
                ctx.setLineDash([]);
                
                // Draw Current Position Dot
                let currX = pad + (currentFrame/(fractions.length-1)) * (w-2*pad);
                let currY = (h-pad) - (fractions[currentFrame]/100) * (h-2*pad);
                
                ctx.beginPath();
                ctx.fillStyle = '{COLORS['chart']['current']}';
                ctx.arc(currX, currY, 6, 0, 2*Math.PI);
                ctx.fill();
                ctx.strokeStyle = 'white';
                ctx.stroke();
            }}
            
            function update() {{
                viewer.setFrame(currentFrame);
                viewer.render();
                
                phDisplay.innerText = phValues[currentFrame].toFixed(1);
                fracDisplay.innerText = fractions[currentFrame].toFixed(0) + '%';
                statusDisplay.innerText = statusMsgs[currentFrame];
                
                drawChart();
            }}
            
            slider.addEventListener('input', function() {{
                currentFrame = parseInt(this.value);
                update();
            }});
            
            update();
        }}, 100);
    }})();
    </script>
    """
    return HTML(html)

create_ph_viewer()

Interactive Proton Transfer

Drag the slider to visualize the bond breaking and formation

Transferring Proton
Oxygen
Carbon
pH
0.0
% Deprotonated
0%

The same will happen for basic drugs.

Basic drugs (e.g., morphine, propranolol, imatinib) accept protons and become positively charged when ionized:

  • Below their pKa: Predominantly ionized (charged, NH₃⁺) → water soluble but membrane impermeable

  • Above their pKa: Predominantly neutral (uncharged) → membrane permeable

3.2 pKa Across Physiological Compartments#

Drugs absorb most efficiently in their neutral (uncharged) form because this state allows them to easily permeate lipid cell membranes. This is called the pH-partition hypothesis.

The human body’s pH gradient creates region-specific ionization patterns that profoundly affect drug behavior. The critical factor is the relationship between the local pH and the drug’s pKa:

  • weak acids (like Aspirin) remain neutral in high-proton environments (\(pH < pKa\)), making the acidic stomach their optimal absorption site

  • weak bases (like Morphine) remain neutral in low-proton environments (\(pH > pKa\)), making the more alkaline intestines their primary site for uptake

Drug Type

Favors Neutral Form When

Example

pKa

Optimal Absorption Site

Weak acid

pH < pKa

Aspirin

3.5

Stomach (pH 2)

Weak acid

pH < pKa

Ibuprofen

4.4

Stomach/upper intestine

Weak base

pH > pKa

Morphine

8.0

Intestine (pH 6.5-7.4)

Weak base

pH > pKa

Propranolol

9.4

Intestine (pH 6.5-7.4)


4. Summary and Key Takeaways#

In this section, we’ve explored the fundamental physicochemical properties that dictate a drug’s journey through the body.

  • Key Takeaways:

  • pKa determines ionization state: The relationship between a drug’s pKa and the local pH dictates whether it exists as charged (hydrophilic) or neutral (lipophilic) species—fundamental for predicting absorption and distribution.

  • pH-partition guides absorption: Neutral molecules cross membranes readily; acidic drugs are better absorbed from acidic environments (stomach), while basic drugs favor neutral/basic environments (intestine), though intestinal surface area often dominates actual absorption.

  • Body compartments create pH gradients: The GI tract (pH 2→7.4), blood (pH 7.4), intracellular space (pH 7.0), and lysosomes (pH 4.5) each select for different ionization states, enabling ion trapping and affecting drug distribution.

By mastering these concepts, you can now analyze a molecule’s structure and predict its potential for absorption, a core skill in modern medicinal chemistry.