Skill Tracer
Skills › Scientific Agent Skills › molecular-dynamics

molecular-dynamics

skill · 477 lines · MIT · skills/molecular-dynamics/SKILL.md

The facts

Its description

Runs and analyzes molecular dynamics simulations with OpenMM and MDAnalysis. Sets up protein/small molecule systems, defines force fields, runs energy minimization and production MD, and analyzes trajectories (RMSD, RMSF, contact maps, free energy surfaces). For structural biology, drug binding, and biophysics.

The description the agent reads to decide when to use it.

From its settingsline 3

Runs scripts

bin/python

Scripts the text names. Found by matching words, so treat them as pointers.

Found by matching wordsline 29

Files it mentions

references/mdanalysis_analysis.md, references/system_preparation.md

Files the text names, such as ones to read or write. Found by matching words, so treat them as pointers.

Found by matching wordsline 362 line 450

Licence

MIT, from the skill file

The terms its author shares it under, which let it be shown here.

From its settingsline 4 licence text

Other settings

compatibility: Requires Python 3.11+ with OpenMM and MDAnalysis; matplotlib for plots. Optional PDBFixer and OpenFF need separate installation. Network access for installation; local simulation and analysis run offline. metadata: version: "1.3" last-reviewed: "2026-10-01" skill-author: Kuan-lin Huang

More settings at the top of the file, shown as written.

From its settingsline 5 line 6
Other files in this skill (2) Close

The text

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1---
2name: molecular-dynamics
3description: Runs and analyzes molecular dynamics simulations with OpenMM and MDAnalysis. Sets up protein/small molecule systems, defines force fields, runs energy minimization and production MD, and analyzes trajectories (RMSD, RMSF, contact maps, free energy surfaces). For structural biology, drug binding, and biophysics.
4license: MIT
5compatibility: Requires Python 3.11+ with OpenMM and MDAnalysis; matplotlib for plots. Optional PDBFixer and OpenFF need separate installation. Network access for installation; local simulation and analysis run offline.
6metadata:
7 version: "1.3"
8 last-reviewed: "2026-10-01"
9 skill-author: Kuan-lin Huang
10---
12# Molecular Dynamics

Molecular Dynamics

Raw lines 1 to 13
14## Overview
16Molecular dynamics (MD) simulation computationally models the time evolution of molecular systems by integrating Newton's equations of motion. This skill covers two complementary tools:
18- **OpenMM** (https://openmm.org/): High-performance MD simulation engine with GPU support, Python API, and flexible force field support
19- **MDAnalysis** (https://mdanalysis.org/): Python library for reading, writing, and analyzing MD trajectories from all major simulation packages
21**Reviewed versions:** OpenMM 8.6.1 and MDAnalysis 2.10.0. Reference-platform
22smoke checks use small synthetic systems; GPU performance and scientific convergence
23are not established by them. OpenMM latest API pages identify a development build,
24so the installed 8.6.1 API is the executable reference.
26**Installation into a dedicated environment:**
27```bash
28uv venv --python 3.12 .venv-md
29uv pip install --python .venv-md/bin/python "openmm==8.6.1" "MDAnalysis==2.10.0" matplotlib pandas
30# Windows: use .venv-md/Scripts/python.exe instead
31```

Overview

Molecular dynamics (MD) simulation computationally models the time evolution of molecular systems by integrating Newton's equations of motion. This skill covers two complementary tools:

  • OpenMM (https://openmm.org/): High-performance MD simulation engine with GPU support, Python API, and flexible force field support
  • MDAnalysis (https://mdanalysis.org/): Python library for reading, writing, and analyzing MD trajectories from all major simulation packages

Reviewed versions: OpenMM 8.6.1 and MDAnalysis 2.10.0. Reference-platform smoke checks use small synthetic systems; GPU performance and scientific convergence are not established by them. OpenMM latest API pages identify a development build, so the installed 8.6.1 API is the executable reference.

Installation into a dedicated environment:

uv venv --python 3.12 .venv-md
uv pip install --python .venv-md/bin/python "openmm==8.6.1" "MDAnalysis==2.10.0" matplotlib pandas
# Windows: use .venv-md/Scripts/python.exe instead
Raw lines 14 to 32
Runs code bin/python :29
33## When to Use This Skill
35Use molecular dynamics when:
37- **Protein stability analysis**: How does a mutation affect protein dynamics?
38- **Drug binding simulations**: Characterize binding mode and residence time of a ligand
39- **Conformational sampling**: Explore protein flexibility and conformational changes
40- **Protein-protein interaction**: Model interface dynamics and binding energetics
41- **RMSD/RMSF analysis**: Quantify structural fluctuations from a reference structure
42- **Free energy estimation**: Compute binding free energy or conformational free energy
43- **Membrane simulations**: Model proteins in lipid bilayers
44- **Intrinsically disordered proteins**: Study IDR conformational ensembles

When to Use This Skill

Use molecular dynamics when:

  • Protein stability analysis: How does a mutation affect protein dynamics?
  • Drug binding simulations: Characterize binding mode and residence time of a ligand
  • Conformational sampling: Explore protein flexibility and conformational changes
  • Protein-protein interaction: Model interface dynamics and binding energetics
  • RMSD/RMSF analysis: Quantify structural fluctuations from a reference structure
  • Free energy estimation: Compute binding free energy or conformational free energy
  • Membrane simulations: Model proteins in lipid bilayers
  • Intrinsically disordered proteins: Study IDR conformational ensembles
Raw lines 33 to 45
46## Core Workflow: OpenMM Simulation
48Examples require system-specific preparation and validation. Review biological
49assembly, alternate locations, missing loops, termini, protonation, disulfides,
50ligands and cofactors before parameterization. An MD trajectory alone does not
51establish binding free energy or residence time.
53The functions below form one Python module: execute their import blocks together.
54Use a new output prefix for every stage to avoid overwriting earlier results.

Core Workflow: OpenMM Simulation

Examples require system-specific preparation and validation. Review biological assembly, alternate locations, missing loops, termini, protonation, disulfides, ligands and cofactors before parameterization. An MD trajectory alone does not establish binding free energy or residence time.

The functions below form one Python module: execute their import blocks together. Use a new output prefix for every stage to avoid overwriting earlier results.

Raw lines 46 to 55
56### 1. System Preparation
58```python
59from openmm.app import *
60from openmm import *
61from openmm.unit import *
63def prepare_system_from_pdb(pdb_file, forcefield_name="amber14-all.xml",
64 water_model="amber14/tip3pfb.xml", ph=7.0):
65 """
66 Prepare an OpenMM system from a PDB file.
68 Args:
69 pdb_file: Path to cleaned PDB file (use PDBFixer for raw PDB files)
70 forcefield_name: Force field XML file
71 water_model: Water model XML file
73 Returns:
74 modeller, system
75 """
76 # Load PDB
77 pdb = PDBFile(pdb_file)
79 # Load force field
80 forcefield = ForceField(forcefield_name, water_model)
82 # Add hydrogens and solvate
83 modeller = Modeller(pdb.topology, pdb.positions)
84 modeller.addHydrogens(forcefield, pH=ph)
86 # TIP3P geometry also serves TIP3P-FB; XML supplies the parameters.
87 # 10 Å padding; added salt excludes neutralizing counterions.
88 modeller.addSolvent(
89 forcefield,
90 model='tip3p',
91 padding=10*angstroms,
92 ionicStrength=0.15*molar, neutralize=True
93 )
95 print(f"System: {modeller.topology.getNumAtoms()} atoms, "
96 f"{modeller.topology.getNumResidues()} residues")
98 # Create system
99 system = forcefield.createSystem(
100 modeller.topology,
101 nonbondedMethod=PME, # Particle Mesh Ewald for long-range electrostatics
102 nonbondedCutoff=1.0*nanometer,
103 constraints=HBonds, # Constrain bonds involving H (not intermolecular H-bonds)
104 rigidWater=True,
105 ewaldErrorTolerance=0.0005
106 )
108 return modeller, system
109```

1. System Preparation

from openmm.app import *
from openmm import *
from openmm.unit import *

def prepare_system_from_pdb(pdb_file, forcefield_name="amber14-all.xml",
                              water_model="amber14/tip3pfb.xml", ph=7.0):
    """
    Prepare an OpenMM system from a PDB file.

    Args:
        pdb_file: Path to cleaned PDB file (use PDBFixer for raw PDB files)
        forcefield_name: Force field XML file
        water_model: Water model XML file

    Returns:
        modeller, system
    """
    # Load PDB
    pdb = PDBFile(pdb_file)

    # Load force field
    forcefield = ForceField(forcefield_name, water_model)

    # Add hydrogens and solvate
    modeller = Modeller(pdb.topology, pdb.positions)
    modeller.addHydrogens(forcefield, pH=ph)

    # TIP3P geometry also serves TIP3P-FB; XML supplies the parameters.
    # 10 Å padding; added salt excludes neutralizing counterions.
    modeller.addSolvent(
        forcefield,
        model='tip3p',
        padding=10*angstroms,
        ionicStrength=0.15*molar, neutralize=True
    )

    print(f"System: {modeller.topology.getNumAtoms()} atoms, "
          f"{modeller.topology.getNumResidues()} residues")

    # Create system
    system = forcefield.createSystem(
        modeller.topology,
        nonbondedMethod=PME,         # Particle Mesh Ewald for long-range electrostatics
        nonbondedCutoff=1.0*nanometer,
        constraints=HBonds,           # Constrain bonds involving H (not intermolecular H-bonds)
        rigidWater=True,
        ewaldErrorTolerance=0.0005
    )

    return modeller, system
Raw lines 56 to 110
111### 2. Energy Minimization
113```python
114from openmm.app import *
115from openmm import *
116from openmm.unit import *
118def minimize_energy(modeller, system, output_pdb="minimized.pdb",
119 max_iterations=1000, tolerance=10.0, platform_name=None):
120 """
121 Energy minimize the system to remove steric clashes.
123 Args:
124 modeller: Modeller object with topology and positions
125 system: OpenMM System
126 output_pdb: Path to save minimized structure
127 max_iterations: Maximum minimization steps
128 tolerance: Convergence criterion in kJ/mol/nm
130 Returns:
131 simulation object with minimized positions
132 """
133 # Keep the integrator at 2 fs for the subsequent NVT/NPT examples.
134 integrator = LangevinMiddleIntegrator(300*kelvin, 1/picosecond, 0.002*picoseconds)
136 # Automatic platform selection, or an explicit tested platform (e.g. CPU).
137 # A registered GPU plugin does not prove a working device or driver.
138 platform = Platform.getPlatformByName(platform_name) if platform_name else None
139 simulation = Simulation(modeller.topology, system, integrator, platform)
140 simulation.context.setPositions(modeller.positions)
142 # Check initial energy
143 state = simulation.context.getState(getEnergy=True)
144 print(f"Initial energy: {state.getPotentialEnergy()}")
146 # Minimize
147 simulation.minimizeEnergy(
148 tolerance=tolerance*kilojoules_per_mole/nanometer,
149 maxIterations=max_iterations
150 )
152 state = simulation.context.getState(getEnergy=True, getPositions=True)
153 print(f"Minimized energy: {state.getPotentialEnergy()}")
155 # Save minimized structure
156 with open(output_pdb, 'w') as f:
157 PDBFile.writeFile(simulation.topology, state.getPositions(), f)
159 return simulation
160```

2. Energy Minimization

from openmm.app import *
from openmm import *
from openmm.unit import *

def minimize_energy(modeller, system, output_pdb="minimized.pdb",
                     max_iterations=1000, tolerance=10.0, platform_name=None):
    """
    Energy minimize the system to remove steric clashes.

    Args:
        modeller: Modeller object with topology and positions
        system: OpenMM System
        output_pdb: Path to save minimized structure
        max_iterations: Maximum minimization steps
        tolerance: Convergence criterion in kJ/mol/nm

    Returns:
        simulation object with minimized positions
    """
    # Keep the integrator at 2 fs for the subsequent NVT/NPT examples.
    integrator = LangevinMiddleIntegrator(300*kelvin, 1/picosecond, 0.002*picoseconds)

    # Automatic platform selection, or an explicit tested platform (e.g. CPU).
    # A registered GPU plugin does not prove a working device or driver.
    platform = Platform.getPlatformByName(platform_name) if platform_name else None
    simulation = Simulation(modeller.topology, system, integrator, platform)
    simulation.context.setPositions(modeller.positions)

    # Check initial energy
    state = simulation.context.getState(getEnergy=True)
    print(f"Initial energy: {state.getPotentialEnergy()}")

    # Minimize
    simulation.minimizeEnergy(
        tolerance=tolerance*kilojoules_per_mole/nanometer,
        maxIterations=max_iterations
    )

    state = simulation.context.getState(getEnergy=True, getPositions=True)
    print(f"Minimized energy: {state.getPotentialEnergy()}")

    # Save minimized structure
    with open(output_pdb, 'w') as f:
        PDBFile.writeFile(simulation.topology, state.getPositions(), f)

    return simulation
Raw lines 111 to 161
162### 3. NVT Equilibration
164```python
165from openmm.app import *
166from openmm import *
167from openmm.unit import *
169def run_nvt_equilibration(simulation, n_steps=50000, temperature=300,
170 report_interval=1000, output_prefix="nvt"):
171 """
172 NVT equilibration: constant N, V, T.
173 Equilibrate velocities to target temperature.
175 Args:
176 simulation: OpenMM Simulation (after minimization)
177 n_steps: Number of MD steps (50000 × 2fs = 100 ps)
178 temperature: Temperature in Kelvin
179 report_interval: Steps between data reports
180 output_prefix: File prefix for trajectory and log
181 """
182 # This example is unrestrained; add validated restraints before Context creation.
183 if any("Barostat" in type(f).__name__ and f.getFrequency() > 0
184 for f in simulation.system.getForces()):
185 raise ValueError("NVT requires all barostats disabled or absent")
187 # Set both the thermostat target and initial velocities.
188 simulation.integrator.setTemperature(temperature*kelvin)
189 simulation.context.setVelocitiesToTemperature(temperature*kelvin)
191 # Add reporters
192 simulation.reporters = []
194 # Log file
195 simulation.reporters.append(
196 StateDataReporter(
197 f"{output_prefix}_log.txt",
198 report_interval,
199 step=True,
200 potentialEnergy=True,
201 kineticEnergy=True,
202 temperature=True,
203 volume=True,
204 speed=True
205 )
206 )
208 # DCD trajectory (compact binary format)
209 simulation.reporters.append(
210 DCDReporter(f"{output_prefix}_traj.dcd", report_interval)
211 )
213 duration = (n_steps * simulation.integrator.getStepSize()).value_in_unit(picoseconds)
214 print(f"Running NVT equilibration: {n_steps} steps ({duration:.1f} ps)")
215 simulation.step(n_steps)
216 print("NVT equilibration complete")
218 return simulation
219```

3. NVT Equilibration

from openmm.app import *
from openmm import *
from openmm.unit import *

def run_nvt_equilibration(simulation, n_steps=50000, temperature=300,
                            report_interval=1000, output_prefix="nvt"):
    """
    NVT equilibration: constant N, V, T.
    Equilibrate velocities to target temperature.

    Args:
        simulation: OpenMM Simulation (after minimization)
        n_steps: Number of MD steps (50000 × 2fs = 100 ps)
        temperature: Temperature in Kelvin
        report_interval: Steps between data reports
        output_prefix: File prefix for trajectory and log
    """
    # This example is unrestrained; add validated restraints before Context creation.
    if any("Barostat" in type(f).__name__ and f.getFrequency() > 0
           for f in simulation.system.getForces()):
        raise ValueError("NVT requires all barostats disabled or absent")

    # Set both the thermostat target and initial velocities.
    simulation.integrator.setTemperature(temperature*kelvin)
    simulation.context.setVelocitiesToTemperature(temperature*kelvin)

    # Add reporters
    simulation.reporters = []

    # Log file
    simulation.reporters.append(
        StateDataReporter(
            f"{output_prefix}_log.txt",
            report_interval,
            step=True,
            potentialEnergy=True,
            kineticEnergy=True,
            temperature=True,
            volume=True,
            speed=True
        )
    )

    # DCD trajectory (compact binary format)
    simulation.reporters.append(
        DCDReporter(f"{output_prefix}_traj.dcd", report_interval)
    )

    duration = (n_steps * simulation.integrator.getStepSize()).value_in_unit(picoseconds)
    print(f"Running NVT equilibration: {n_steps} steps ({duration:.1f} ps)")
    simulation.step(n_steps)
    print("NVT equilibration complete")

    return simulation
Raw lines 162 to 220
221### 4. NPT Equilibration and Production
223Call this function first with a dedicated NPT equilibration prefix. Inspect density,
224energy, structure and replicate stability before a separate production call; the
225default duration is an example, not an equilibration or convergence criterion.
227```python
228def run_npt_production(simulation, n_steps=500000, temperature=300, pressure=1.0,
229 report_interval=5000, output_prefix="npt"):
230 """
231 NPT production run: constant N, P, T.
233 Args:
234 n_steps: Production steps (500000 × 2fs = 1 ns)
235 temperature: Temperature in Kelvin
236 pressure: Pressure in bar
237 report_interval: Steps between reports
238 """
239 # Keep the Langevin thermostat and barostat at the same temperature.
240 simulation.integrator.setTemperature(temperature*kelvin)
241 system = simulation.system
242 barostats = [f for f in system.getForces() if "Barostat" in type(f).__name__]
243 if not system.usesPeriodicBoundaryConditions():
244 raise ValueError("NPT requires a periodic system")
245 if not barostats:
246 system.addForce(MonteCarloBarostat(pressure*bar, temperature*kelvin, 25))
247 simulation.context.reinitialize(preserveState=True)
248 elif len(barostats) != 1 or not isinstance(barostats[0], MonteCarloBarostat):
249 raise ValueError("This example supports one isotropic MonteCarloBarostat")
250 else:
251 barostats[0].setDefaultPressure(pressure*bar)
252 barostats[0].setDefaultTemperature(temperature*kelvin)
253 barostats[0].setFrequency(25)
254 # Existing Context parameters must also be updated on repeated calls.
255 simulation.context.setParameter(MonteCarloBarostat.Pressure(), pressure)
256 simulation.context.setParameter(MonteCarloBarostat.Temperature(), temperature)
258 # Update reporters
259 simulation.reporters = []
260 simulation.reporters.append(
261 StateDataReporter(
262 f"{output_prefix}_log.txt",
263 report_interval,
264 step=True,
265 potentialEnergy=True,
266 temperature=True,
267 density=True,
268 speed=True
269 )
270 )
271 simulation.reporters.append(
272 DCDReporter(f"{output_prefix}_traj.dcd", report_interval)
273 )
275 # Save checkpoints
276 simulation.reporters.append(
277 CheckpointReporter(f"{output_prefix}_checkpoint.chk", 50000)
278 )
280 duration = (n_steps * simulation.integrator.getStepSize()).value_in_unit(nanoseconds)
281 print(f"Running NPT stage: {n_steps} steps ({duration:.3f} ns)")
282 simulation.step(n_steps)
283 simulation.saveCheckpoint(f"{output_prefix}_checkpoint.chk")
284 simulation.saveState(f"{output_prefix}_state.xml")
285 print("NPT stage complete")
286 return simulation
287```

4. NPT Equilibration and Production

Call this function first with a dedicated NPT equilibration prefix. Inspect density, energy, structure and replicate stability before a separate production call; the default duration is an example, not an equilibration or convergence criterion.

def run_npt_production(simulation, n_steps=500000, temperature=300, pressure=1.0,
                        report_interval=5000, output_prefix="npt"):
    """
    NPT production run: constant N, P, T.

    Args:
        n_steps: Production steps (500000 × 2fs = 1 ns)
        temperature: Temperature in Kelvin
        pressure: Pressure in bar
        report_interval: Steps between reports
    """
    # Keep the Langevin thermostat and barostat at the same temperature.
    simulation.integrator.setTemperature(temperature*kelvin)
    system = simulation.system
    barostats = [f for f in system.getForces() if "Barostat" in type(f).__name__]
    if not system.usesPeriodicBoundaryConditions():
        raise ValueError("NPT requires a periodic system")
    if not barostats:
        system.addForce(MonteCarloBarostat(pressure*bar, temperature*kelvin, 25))
        simulation.context.reinitialize(preserveState=True)
    elif len(barostats) != 1 or not isinstance(barostats[0], MonteCarloBarostat):
        raise ValueError("This example supports one isotropic MonteCarloBarostat")
    else:
        barostats[0].setDefaultPressure(pressure*bar)
        barostats[0].setDefaultTemperature(temperature*kelvin)
        barostats[0].setFrequency(25)
    # Existing Context parameters must also be updated on repeated calls.
    simulation.context.setParameter(MonteCarloBarostat.Pressure(), pressure)
    simulation.context.setParameter(MonteCarloBarostat.Temperature(), temperature)

    # Update reporters
    simulation.reporters = []
    simulation.reporters.append(
        StateDataReporter(
            f"{output_prefix}_log.txt",
            report_interval,
            step=True,
            potentialEnergy=True,
            temperature=True,
            density=True,
            speed=True
        )
    )
    simulation.reporters.append(
        DCDReporter(f"{output_prefix}_traj.dcd", report_interval)
    )

    # Save checkpoints
    simulation.reporters.append(
        CheckpointReporter(f"{output_prefix}_checkpoint.chk", 50000)
    )

    duration = (n_steps * simulation.integrator.getStepSize()).value_in_unit(nanoseconds)
    print(f"Running NPT stage: {n_steps} steps ({duration:.3f} ns)")
    simulation.step(n_steps)
    simulation.saveCheckpoint(f"{output_prefix}_checkpoint.chk")
    simulation.saveState(f"{output_prefix}_state.xml")
    print("NPT stage complete")
    return simulation
Raw lines 221 to 288
289## Trajectory Analysis with MDAnalysis

Trajectory Analysis with MDAnalysis

Raw lines 289 to 290
291### 1. Load Trajectory
293Use the solvated topology with exactly the DCD atom count and order (e.g. the
294`minimized.pdb` above), not the original unsolvated input. MDAnalysis converts
295lengths to Å and time to ps by default; OpenMM bare coordinates use nm. Preserve
296frame box vectors and actual timestamps; do not infer time from frame number.
298```python
299import MDAnalysis as mda
300from MDAnalysis.analysis import rms, align, contacts
301import numpy as np
302import matplotlib.pyplot as plt
304def load_trajectory(topology_file, trajectory_file):
305 """
306 Load an MD trajectory with MDAnalysis.
308 Args:
309 topology_file: PDB, PSF, or other topology file
310 trajectory_file: DCD, XTC, TRR, or other trajectory
311 """
312 u = mda.Universe(topology_file, trajectory_file)
313 print(f"Universe: {u.atoms.n_atoms} atoms, {u.trajectory.n_frames} frames")
314 first, last = u.trajectory[0].time, u.trajectory[-1].time
315 print(f"Time range: {first:g} to {last:g} ps")
316 u.trajectory[0]
317 return u
318```

1. Load Trajectory

Use the solvated topology with exactly the DCD atom count and order (e.g. the minimized.pdb above), not the original unsolvated input. MDAnalysis converts lengths to Å and time to ps by default; OpenMM bare coordinates use nm. Preserve frame box vectors and actual timestamps; do not infer time from frame number.

import MDAnalysis as mda
from MDAnalysis.analysis import rms, align, contacts
import numpy as np
import matplotlib.pyplot as plt

def load_trajectory(topology_file, trajectory_file):
    """
    Load an MD trajectory with MDAnalysis.

    Args:
        topology_file: PDB, PSF, or other topology file
        trajectory_file: DCD, XTC, TRR, or other trajectory
    """
    u = mda.Universe(topology_file, trajectory_file)
    print(f"Universe: {u.atoms.n_atoms} atoms, {u.trajectory.n_frames} frames")
    first, last = u.trajectory[0].time, u.trajectory[-1].time
    print(f"Time range: {first:g} to {last:g} ps")
    u.trajectory[0]
    return u
Raw lines 291 to 319
320### 2. RMSD Analysis
322```python
323def compute_rmsd(u, selection="backbone", reference_frame=0):
324 """
325 Compute RMSD of selected atoms relative to reference frame.
327 Args:
328 u: MDAnalysis Universe
329 selection: Atom selection string (MDAnalysis syntax)
330 reference_frame: Frame index for reference structure
332 Returns:
333 numpy array with columns [frame index, time in ps, RMSD in Angstroms]
334 """
335 # RMSD performs its own fit; avoid rotating the stored trajectory here.
336 # Make the selected molecule whole before analysis of periodic trajectories.
337 R = rms.RMSD(u, select=selection, ref_frame=reference_frame)
338 R.run()
340 rmsd_data = R.results.rmsd # columns: frame, time, RMSD
341 return rmsd_data
343def plot_rmsd(rmsd_data, title="RMSD over time", output_file="rmsd.png"):
344 """Plot RMSD over simulation time."""
345 fig, ax = plt.subplots(figsize=(10, 4))
346 ax.plot(rmsd_data[:, 1] / 1000, rmsd_data[:, 2], 'b-', linewidth=0.5)
347 ax.set_xlabel("Time (ns)")
348 ax.set_ylabel("RMSD (Å)")
349 ax.set_title(title)
350 ax.axhline(rmsd_data[:, 2].mean(), color='r', linestyle='--',
351 label=f'Mean: {rmsd_data[:, 2].mean():.2f} Å')
352 ax.legend()
353 plt.tight_layout()
354 plt.savefig(output_file, dpi=150)
355 return fig
356```

2. RMSD Analysis

def compute_rmsd(u, selection="backbone", reference_frame=0):
    """
    Compute RMSD of selected atoms relative to reference frame.

    Args:
        u: MDAnalysis Universe
        selection: Atom selection string (MDAnalysis syntax)
        reference_frame: Frame index for reference structure

    Returns:
        numpy array with columns [frame index, time in ps, RMSD in Angstroms]
    """
    # RMSD performs its own fit; avoid rotating the stored trajectory here.
    # Make the selected molecule whole before analysis of periodic trajectories.
    R = rms.RMSD(u, select=selection, ref_frame=reference_frame)
    R.run()

    rmsd_data = R.results.rmsd  # columns: frame, time, RMSD
    return rmsd_data

def plot_rmsd(rmsd_data, title="RMSD over time", output_file="rmsd.png"):
    """Plot RMSD over simulation time."""
    fig, ax = plt.subplots(figsize=(10, 4))
    ax.plot(rmsd_data[:, 1] / 1000, rmsd_data[:, 2], 'b-', linewidth=0.5)
    ax.set_xlabel("Time (ns)")
    ax.set_ylabel("RMSD (Å)")
    ax.set_title(title)
    ax.axhline(rmsd_data[:, 2].mean(), color='r', linestyle='--',
               label=f'Mean: {rmsd_data[:, 2].mean():.2f} Å')
    ax.legend()
    plt.tight_layout()
    plt.savefig(output_file, dpi=150)
    return fig
Raw lines 320 to 357
358### 3. RMSF Analysis (Per-Residue Flexibility)
360Make molecules whole and align a separate structural-analysis copy before RMSF;
361RMSF does not align. Do not run periodic contacts on a rotated trajectory whose
362box was not rotated. See [analysis reference](references/mdanalysis_analysis.md)
363for alignment, PCA, DSSP, hydrogen bonds and population-derived free energy.
365```python
366def compute_rmsf(u, selection="protein and name CA", start_frame=0):
367 """
368 Compute per-residue RMSF (flexibility).
370 Returns:
371 residue_keys, rmsf_values; keys are (resindex, segid, resid, resname)
372 """
373 # Select atoms
374 atoms = u.select_atoms(selection)
376 if not len(atoms):
377 raise ValueError("RMSF selection is empty")
378 # A multi-atom selection returns mean atomic RMSF, not COM RMSF.
379 R = rms.RMSF(atoms)
380 R.run(start=start_frame)
382 # Average by residue
383 resids = []
384 rmsf_per_res = []
385 for res in u.select_atoms(selection).residues:
386 res_atoms = res.atoms.intersection(atoms)
387 if len(res_atoms) > 0:
388 resids.append((res.ix, res.segid, res.resid, res.resname))
389 # RMSF is indexed within the selected AtomGroup, not the Universe.
390 rmsf_per_res.append(R.results.rmsf[atoms.resindices == res.ix].mean())
392 return resids, np.array(rmsf_per_res)
393```

3. RMSF Analysis (Per-Residue Flexibility)

Make molecules whole and align a separate structural-analysis copy before RMSF; RMSF does not align. Do not run periodic contacts on a rotated trajectory whose box was not rotated. See analysis reference for alignment, PCA, DSSP, hydrogen bonds and population-derived free energy.

def compute_rmsf(u, selection="protein and name CA", start_frame=0):
    """
    Compute per-residue RMSF (flexibility).

    Returns:
        residue_keys, rmsf_values; keys are (resindex, segid, resid, resname)
    """
    # Select atoms
    atoms = u.select_atoms(selection)

    if not len(atoms):
        raise ValueError("RMSF selection is empty")
    # A multi-atom selection returns mean atomic RMSF, not COM RMSF.
    R = rms.RMSF(atoms)
    R.run(start=start_frame)

    # Average by residue
    resids = []
    rmsf_per_res = []
    for res in u.select_atoms(selection).residues:
        res_atoms = res.atoms.intersection(atoms)
        if len(res_atoms) > 0:
            resids.append((res.ix, res.segid, res.resid, res.resname))
            # RMSF is indexed within the selected AtomGroup, not the Universe.
            rmsf_per_res.append(R.results.rmsf[atoms.resindices == res.ix].mean())

    return resids, np.array(rmsf_per_res)
Raw lines 358 to 394
395### 4. Protein-Ligand Contacts
397```python
398def analyze_contacts(u, protein_sel="protein", ligand_sel="resname LIG",
399 radius=4.5, start_frame=0, periodic=True):
400 """
401 Track protein-ligand contacts over trajectory.
403 Args:
404 radius: Atom-pair cutoff in Angstroms; any pair defines a residue contact.
405 periodic: Require box data and use minimum-image distances.
406 Returns sets of (resindex, segid, resid, resname), one per analyzed frame.
407 """
408 from MDAnalysis.lib.distances import distance_array
410 protein = u.select_atoms(protein_sel)
411 ligand = u.select_atoms(ligand_sel)
413 if not len(protein) or not len(ligand):
414 raise ValueError("Protein and ligand selections must both be nonempty")
415 if not np.isfinite(radius) or radius <= 0:
416 raise ValueError("radius must be finite and positive")
417 contact_frames = []
418 for ts in u.trajectory[start_frame:]:
419 if periodic and (ts.dimensions is None or not np.all(np.isfinite(ts.dimensions))
420 or np.any(ts.dimensions[:3] <= 0)):
421 raise ValueError("Periodic contacts require valid frame box dimensions")
422 # This includes hydrogen atoms unless the caller selects heavy atoms.
423 distances = contacts.contact_matrix(
424 distance_array(protein.positions, ligand.positions, box=ts.dimensions if periodic else None),
425 radius,
426 )
427 contact_residues = set()
428 for i in range(distances.shape[0]):
429 if distances[i].any():
430 res = protein[i].residue
431 contact_residues.add((res.ix, res.segid, res.resid, res.resname))
432 contact_frames.append(contact_residues)
434 return contact_frames
435```

4. Protein-Ligand Contacts

def analyze_contacts(u, protein_sel="protein", ligand_sel="resname LIG",
                      radius=4.5, start_frame=0, periodic=True):
    """
    Track protein-ligand contacts over trajectory.

    Args:
        radius: Atom-pair cutoff in Angstroms; any pair defines a residue contact.
        periodic: Require box data and use minimum-image distances.
    Returns sets of (resindex, segid, resid, resname), one per analyzed frame.
    """
    from MDAnalysis.lib.distances import distance_array

    protein = u.select_atoms(protein_sel)
    ligand = u.select_atoms(ligand_sel)

    if not len(protein) or not len(ligand):
        raise ValueError("Protein and ligand selections must both be nonempty")
    if not np.isfinite(radius) or radius <= 0:
        raise ValueError("radius must be finite and positive")
    contact_frames = []
    for ts in u.trajectory[start_frame:]:
        if periodic and (ts.dimensions is None or not np.all(np.isfinite(ts.dimensions))
                         or np.any(ts.dimensions[:3] <= 0)):
            raise ValueError("Periodic contacts require valid frame box dimensions")
        # This includes hydrogen atoms unless the caller selects heavy atoms.
        distances = contacts.contact_matrix(
            distance_array(protein.positions, ligand.positions, box=ts.dimensions if periodic else None),
            radius,
        )
        contact_residues = set()
        for i in range(distances.shape[0]):
            if distances[i].any():
                res = protein[i].residue
                contact_residues.add((res.ix, res.segid, res.resid, res.resname))
        contact_frames.append(contact_residues)

    return contact_frames
Raw lines 395 to 436
437## Force Fields and Preparation
439Choose a validated protein/ligand/water/ion combination for the scientific system.
440The working example retains AMBER14/TIP3P-FB; it is not a universal recommendation.
441Current OpenMM also bundles `amber19-all.xml` (ff19SB, DNA OL21, RNA OL3, lipid21),
442and `charmm36_2024.xml` with its own `charmm36_2024/water.xml`. Generic TIP3P and
443CHARMM-modified TIP3P are not interchangeable. IDP ensembles are especially
444sensitive to protein-water balance. Four-site waters need extra particles and a
445matching Modeller geometry, unlike the three-site example above.
447AMBER protein XML does not parameterize arbitrary ligands. Use a reviewed GAFF2
448or OpenFF route with explicit stereochemistry, protonation, bond orders, atom
449mapping and charge method. A ligand-only Interchange is not a protein-ligand system.
450See [preparation reference](references/system_preparation.md) for conservative
451PDBFixer and OpenFF examples, installation requirements and current release details.

Force Fields and Preparation

Choose a validated protein/ligand/water/ion combination for the scientific system. The working example retains AMBER14/TIP3P-FB; it is not a universal recommendation. Current OpenMM also bundles amber19-all.xml (ff19SB, DNA OL21, RNA OL3, lipid21), and charmm36_2024.xml with its own charmm36_2024/water.xml. Generic TIP3P and CHARMM-modified TIP3P are not interchangeable. IDP ensembles are especially sensitive to protein-water balance. Four-site waters need extra particles and a matching Modeller geometry, unlike the three-site example above.

AMBER protein XML does not parameterize arbitrary ligands. Use a reviewed GAFF2 or OpenFF route with explicit stereochemistry, protonation, bond orders, atom mapping and charge method. A ligand-only Interchange is not a protein-ligand system. See preparation reference for conservative PDBFixer and OpenFF examples, installation requirements and current release details.

Raw lines 437 to 452
453## Validation and Provenance
455- Start with minimization; finite energy does not establish a valid structure.
456- Use staged NVT → NPT equilibration → production. This example has no restraints.
457- A 2 fs step with constrained H-containing bonds is a starting choice. Larger
458 steps/HMR require integrator, stability and observable validation for that system.
459- Use per-frame boxes for periodic distances. Make molecules whole for structural
460 observables using trusted bond topology; unwrapping cannot infer missing bonds.
461- Assess burn-in, autocorrelation, effective samples, replicas and uncertainties.
462 Short stable temperature/density traces do not establish conformational sampling.
463- Save topology/atom order, System/Integrator XML, force-field files/versions,
464 seeds, platform/precision, parameters and analysis selections. Checkpoints are
465 platform/hardware/version specific; XML states are more portable but do not
466 retain all internal random-generator state. Test restarting the actual setup.

Validation and Provenance

  • Start with minimization; finite energy does not establish a valid structure.
  • Use staged NVT → NPT equilibration → production. This example has no restraints.
  • A 2 fs step with constrained H-containing bonds is a starting choice. Larger steps/HMR require integrator, stability and observable validation for that system.
  • Use per-frame boxes for periodic distances. Make molecules whole for structural observables using trusted bond topology; unwrapping cannot infer missing bonds.
  • Assess burn-in, autocorrelation, effective samples, replicas and uncertainties. Short stable temperature/density traces do not establish conformational sampling.
  • Save topology/atom order, System/Integrator XML, force-field files/versions, seeds, platform/precision, parameters and analysis selections. Checkpoints are platform/hardware/version specific; XML states are more portable but do not retain all internal random-generator state. Test restarting the actual setup.
Raw lines 453 to 467
468## Additional Resources
470- **OpenMM documentation**: https://openmm.org/documentation.html
471- **MDAnalysis user guide**: https://docs.mdanalysis.org/
472- **GROMACS** (alternative MD engine): https://manual.gromacs.org/
473- **NAMD** (alternative): https://www.ks.uiuc.edu/Research/namd/
474- **CHARMM-GUI** (web-based system builder): https://charmm-gui.org/
475- **AmberTools** (free Amber tools): https://ambermd.org/AmberTools.php
476- **OpenMM paper**: Eastman P et al. (2017) PLOS Computational Biology. PMID: 28278240
477- **MDAnalysis paper**: Michaud-Agrawal N et al. (2011) J Computational Chemistry. PMID: 21500218

Additional Resources

  • OpenMM documentation: https://openmm.org/documentation.html
  • MDAnalysis user guide: https://docs.mdanalysis.org/
  • GROMACS (alternative MD engine): https://manual.gromacs.org/
  • NAMD (alternative): https://www.ks.uiuc.edu/Research/namd/
  • CHARMM-GUI (web-based system builder): https://charmm-gui.org/
  • AmberTools (free Amber tools): https://ambermd.org/AmberTools.php
  • OpenMM paper: Eastman P et al. (2017) PLOS Computational Biology. PMID: 28278240
  • MDAnalysis paper: Michaud-Agrawal N et al. (2011) J Computational Chemistry. PMID: 21500218
Raw lines 468 to 477

From K-Dense-AI/scientific-agent-skills at 5f2bc03, 2026-10-26. Not read by a model yet. Summary and techniques by Claude Opus 5.5 on 2026-11-06.