TIP3P/TIP4P Water Model Simulation in OpenMM#

This notebook simulates a box of water using the TIP4P-D model and analyzes:

  • The velocity autocorrelation function (VACF)

  • The radial distribution function (g®)

We use OpenMM for simulation and MDTraj + NumPy for analysis.

!pip install -q condacolab
import condacolab
condacolab.install()
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/condacolab.py:26
     25 try:
---> 26     import google.colab
     27 except ImportError:

ModuleNotFoundError: No module named 'google'

During handling of the above exception, another exception occurred:

RuntimeError                              Traceback (most recent call last)
Cell In[1], line 2
      1 get_ipython().system('pip install -q condacolab')
----> 2 import condacolab
      3 condacolab.install()

File /opt/hostedtoolcache/Python/3.8.18/x64/lib/python3.8/site-packages/condacolab.py:28
     26     import google.colab
     27 except ImportError:
---> 28     raise RuntimeError("This module must ONLY run as part of a Colab notebook!")
     31 __version__ = "0.1.6"
     32 __author__ = "Jaime Rodríguez-Guerra <jaimergp@users.noreply.github.com>"

RuntimeError: This module must ONLY run as part of a Colab notebook!
%%capture
!conda install -c conda-forge openmm mdtraj parmed
import openmm
from openmm import *
from openmm.app import *
from openmm.unit import nanometers

import matplotlib.pyplot as plt
import mdtraj  as md
import numpy   as np
import pandas  as pd

import openmmtools
from   openmmtools  import testsystems
from openmmtools import testsystems

#waterbox = testsystems.WaterBox(model='tip4pew', box_edge=3.0*nanometers)
waterbox = testsystems.WaterBox(model='tip3p', box_edge=3.0*nanometers)

system = waterbox.system
topology = waterbox.topology
positions = waterbox.positions

from openmm.app import PDBFile
with open("tip3p_box.pdb", "w") as f:
    PDBFile.writeFile(waterbox.topology, waterbox.positions, f)
#import nglview as nv

#pdb = md.load_pdb("tip3p_box.pdb")
#view = nv.show_mdtraj(pdb)
#view.add_ball_and_stick('all')
#view.center_view(zoom=True)
#view

Setup simulation in OpenMM#

nonbondedMethod     = PME
nonbondedCutoff     = 1.0*nanometers
ewaldErrorTolerance = 0.0005
constraints         = HBonds
rigidWater          = True
constraintTolerance = 0.000001
hydrogenMass        = 1.5*amu

dt               = 0.002*picoseconds
temperature      = 300*kelvin
friction         = 1.0/picosecond
pressure         = 1.0*atmospheres
barostatInterval = 25

# Simulation Options
nsteps = int(100000)   # run for 1e6 steps = 1ns

dcdReporter        = DCDReporter('trajectory.dcd', 100)
dataReporter       = StateDataReporter('log.txt', 1000, 
                                 totalSteps=steps,
                                 step=True, 
                                 speed=True, progress=True, 
                                 potentialEnergy=True, 
                                 temperature=True, 
                                 separator='\t')

Run simulation#

# Prepare the Simulation
system.addForce(MonteCarloBarostat(pressure, temperature, barostatInterval))

integrator = LangevinMiddleIntegrator(temperature, friction, dt)
integrator.setConstraintTolerance(constraintTolerance)
simulation = Simulation(topology, system, integrator)
simulation.context.setPositions(positions)

# Minimize 
simulation.minimizeEnergy()

# Simulate
simulation.context.setVelocitiesToTemperature(temperature)
simulation.reporters.append(dcdReporter)
simulation.reporters.append(dataReporter)

simulation.step(nsteps)

Analyize Simulation Results#

# Load trajectory and extract velocities
traj = md.load('trajectory.dcd', top='tip3p_box.pdb')
n_frames = traj.n_frames
n_atoms = traj.n_atoms

RDF#

import numpy as np
import itertools
import matplotlib.pyplot as plt

# Select oxygen atoms
oxygen_indices = traj.topology.select("name O")

# Generate all unique O–O pairs (no self-pairs, no duplicates)
pairs = np.array(list(itertools.combinations(oxygen_indices, 2)))

# Compute radial distribution function
rdf, r = md.compute_rdf(traj, pairs=pairs, r_range=(0.0, 1.0))

# Plot
plt.plot(rdf, r)
plt.title("Radial Distribution Function g(r) (O–O)")
plt.xlabel("r (nm)")
plt.ylabel("g(r)")
plt.grid()
plt.show()
../../_images/1555249e3a1dc79967a27c4282d91314fbc247ee960eb9bff08157300ea9e2c1.png

VACF#

# Compute VACF using NumPy
context = simulation.context
velocities = np.zeros((n_frames, n_atoms, 3))

for i in range(n_frames):
    simulation.context.setState(simulation.context.getState(getVelocities=True))
    velocities[i] = simulation.context.getState(getVelocities=True).getVelocities(asNumpy=True)._value

vacf = np.zeros(n_frames)
v0 = velocities[0]


# Assume: velocities.shape = (n_frames, n_atoms, 3)
n_frames = velocities.shape[0]

# VACF calculation
vacf = np.zeros(n_frames)
for t in range(n_frames):
    vt = velocities[t]
    vacf[t] = np.mean(np.sum(v0 * vt, axis=1))  # dot each atom's velocity, then average

# Normalize
vacf /= vacf[0]

# Time in picoseconds (adjust if your frame spacing is not 1 step = 2 fs)
timestep_fs = 2
frame_interval = 10  # if saved every 10 steps
time = np.arange(n_frames) * timestep_fs * frame_interval * 1e-3  # ps

# Plot
plt.plot(time, vacf)
plt.title("Velocity Autocorrelation Function (VACF)")
plt.xlabel("Time (ps)")
plt.ylabel("C_v(t) / C_v(0)")
plt.grid(True)
plt.tight_layout()
plt.show()
../../_images/a5cbacdec4afb64b56b33dc0646e3f0b6b91f3d946f411843728a0e44ac3c5eb.png