Custom Nonbonded Potential: Yukawa on rigid bodies

Here we define a custom force class where particles interact through a Yukawa potential and a soft repulsion,

\begin{equation} w(r) / k_BT = \frac{\lambda_Bz_iz_j}{r}e^{-r/\lambda_D} + 4\beta\epsilon_{ij} \left ( \frac{\sigma_{ij}}{r}\right )^{12} \end{equation}

where $\lambda_B=e^2/4\pi\epsilon_0\epsilon_rk_BT$ and $\lambda_D=(4\pi\lambda_B\sum \rho_iz_i^2)^{-1/2}$ are the Bjerrum and Debye lengths, respectively. $\rho_i$ is the number density of the $i$th ion. In this example we also create two rigid bodies using harmonic bonds to constrain the positions.

Some comments:

  1. The potential is defined in CustomNonbonded is defined in cg.zml and must return energy in kJ/mol.
  2. The Bjerrum and Debye lengths are set via global parameters

In [7]:
%matplotlib inline
import numpy as np
from __future__ import print_function
from simtk.openmm import app
import simtk.openmm as mm
from simtk import unit
from sys import stdout, exit
import math
import mdtraj as mdtraj
from itertools import combinations

Simulation setup


In [8]:
cutoff           = 50*unit.angstrom
useMinimize      = True
epsilon_r        = 80.
temperature      = 300*unit.kelvin
kT               = unit.BOLTZMANN_CONSTANT_kB*temperature
timestep         = 10*unit.femtoseconds;
steps_eq         = 5000
steps_production = 2e4
steps_total      = steps_eq + steps_production

Convenience functions

A set of independent functions, useful for setting up OpenMM.


In [9]:
def findForce(system, forcetype, add=True):
    """ Finds a specific force in the system force list - added if not found."""
    for force in system.getForces():
        if isinstance(force, forcetype):
            return force
    if add==True:
        system.addForce(forcetype())
        return findForce(system, forcetype)
    return None

def setGlobalForceParameter(force, key, value):
    for i in range(force.getNumGlobalParameters()):
        if force.getGlobalParameterName(i)==key:
            print('setting force parameter', key, '=', value)
            force.setGlobalParameterDefaultValue(i, value);    

def atomIndexInResidue(residue):
    """ list of atom index in residue """
    index=[]
    for a in list(residue.atoms()):
        index.append(a.index)
    return index

def getResiduePositions(residue, positions):
    """ Returns array w. atomic positions of residue """
    ndx = atomIndexInResidue(residue)
    return np.array(positions)[ndx]

def uniquePairs(index):
    """ list of unique, internal pairs """
    return list(combinations( range(index[0],index[-1]+1),2 ) )

def addHarmonicConstraint(harmonicforce, pairlist, positions, threshold, k):
    """ add harmonic bonds between pairs if distance is smaller than threshold """
    print('Constraint force constant =', k)
    for i,j in pairlist:
        distance = unit.norm( positions[i]-positions[j] )
        if distance<threshold:
            harmonicforce.addBond( i,j,
                                   distance.value_in_unit(unit.nanometer),
                                   k.value_in_unit( unit.kilojoule/unit.nanometer**2/unit.mole ))
            print("added harmonic bond between", i, j, 'with distance',distance)

def addExclusions(nonbondedforce, pairlist):
    """ add nonbonded exclusions between pairs """
    for i,j in pairlist:
        nonbondedforce.addExclusion(i,j)

def rigidifyResidue(residue, harmonicforce, positions, nonbondedforce=None,
                    threshold=6.0*unit.angstrom, k=2500*unit.kilojoule/unit.nanometer**2/unit.mole):
    """ make residue rigid by adding constraints and nonbonded exclusions """
    index    = atomIndexInResidue(residue)
    pairlist = uniquePairs(index)
    addHarmonicConstraint(harmonic, pairlist, pdb.positions, threshold, k)
    if nonbondedforce is not None:
        for i,j in pairlist:
            print('added nonbonded exclusion between', i, j)
            nonbonded.addExclusion(i,j)
            
def centerOfMass(positions, box):
    """ Calculates the geometric center taking into account periodic boundaries
    
    More here: https://en.wikipedia.org/wiki/Center_of_mass#Systems_with_periodic_boundary_conditions
    """
    theta=np.divide(positions, box).astype(np.float) * 2*np.pi
    x1=np.array( [np.cos(theta[:,0]).mean(), np.cos(theta[:,1]).mean(), np.cos(theta[:,2]).mean()] )
    x2=np.array( [np.sin(theta[:,0]).mean(), np.sin(theta[:,1]).mean(), np.sin(theta[:,2]).mean()] )
    return box * (np.arctan2(-x1,-x2)+np.pi) / (2*np.pi)

Setup simulation


In [10]:
pdb        = app.PDBFile('squares.pdb')
forcefield = app.ForceField('yukawa.xml')
system     = forcefield.createSystem(pdb.topology, nonbondedMethod=app.CutoffPeriodic, nonbondedCutoff=cutoff )
box        = np.array(pdb.topology.getPeriodicBoxVectors()).diagonal()

harmonic   = findForce(system, mm.HarmonicBondForce)
nonbonded  = findForce(system, mm.CustomNonbondedForce)

setGlobalForceParameter(nonbonded, 'lB', 0.7*unit.nanometer)
setGlobalForceParameter(nonbonded, 'kappa', 0.0)

for residue in pdb.topology.residues():
    p = getResiduePositions(residue, pdb.positions)
    print(centerOfMass(p, box))
    rigidifyResidue(residue, harmonicforce=harmonic, nonbondedforce=nonbonded, positions=pdb.positions)
                        
integrator = mm.LangevinIntegrator(temperature, 1.0/unit.picoseconds, timestep)

integrator.setConstraintTolerance(0.0001)


setting force parameter lB = 0.7 nm
setting force parameter kappa = 0.0
[Quantity(value=2.3, unit=nanometer) Quantity(value=2.3, unit=nanometer)
 Quantity(value=2.5, unit=nanometer)]
Constraint force constant = 2500 kJ/(nm**2 mol)
added harmonic bond between 0 1 with distance 0.4 nm
added harmonic bond between 0 2 with distance 0.565685424949 nm
added harmonic bond between 0 3 with distance 0.4 nm
added harmonic bond between 1 2 with distance 0.4 nm
added harmonic bond between 1 3 with distance 0.565685424949 nm
added harmonic bond between 2 3 with distance 0.4 nm
added nonbonded exclusion between 0 1
added nonbonded exclusion between 0 2
added nonbonded exclusion between 0 3
added nonbonded exclusion between 1 2
added nonbonded exclusion between 1 3
added nonbonded exclusion between 2 3
[Quantity(value=2.3, unit=nanometer) Quantity(value=2.3, unit=nanometer)
 Quantity(value=1.9000000000000001, unit=nanometer)]
Constraint force constant = 2500 kJ/(nm**2 mol)
added harmonic bond between 4 5 with distance 0.4 nm
added harmonic bond between 4 6 with distance 0.565685424949 nm
added harmonic bond between 4 7 with distance 0.4 nm
added harmonic bond between 5 6 with distance 0.4 nm
added harmonic bond between 5 7 with distance 0.565685424949 nm
added harmonic bond between 6 7 with distance 0.4 nm
added nonbonded exclusion between 4 5
added nonbonded exclusion between 4 6
added nonbonded exclusion between 4 7
added nonbonded exclusion between 5 6
added nonbonded exclusion between 5 7
added nonbonded exclusion between 6 7

Run simulation


In [11]:
simulation = app.Simulation(pdb.topology, system, integrator)
simulation.context.setPositions(pdb.positions)

if useMinimize:
    print('Minimizing...')
    simulation.minimizeEnergy()

print('Equilibrating...')
simulation.context.setVelocitiesToTemperature(300*unit.kelvin)
simulation.step(steps_eq)

simulation.reporters.append(mdtraj.reporters.HDF5Reporter('trajectory.h5', 100))
simulation.reporters.append(
    app.StateDataReporter(stdout, int(steps_total/10), step=True, 
    potentialEnergy=True, temperature=True, progress=True, remainingTime=False, 
    speed=True, totalSteps=steps_total, volume=True, separator='\t'))

print('Production...')
simulation.step(steps_production)

print('Done!')


Minimizing...
Equilibrating...
Production...
#"Progress (%)"	"Step"	"Potential Energy (kJ/mole)"	"Temperature (K)"	"Box Volume (nm^3)"	"Speed (ns/day)"
30.0%	7500	11.0131477032	365.096087233	1000.0	0
40.0%	10000	4.8144297977	471.471481815	1000.0	2.73e+03
50.0%	12500	12.0211700662	206.068451567	1000.0	2.68e+03
60.0%	15000	17.2765251058	457.505593603	1000.0	2.62e+03
70.0%	17500	19.6206031144	303.497468184	1000.0	2.61e+03
80.0%	20000	10.5103588598	354.879128936	1000.0	2.57e+03
90.0%	22500	6.9912172528	555.040070009	1000.0	2.56e+03
100.0%	25000	4.45059069199	277.202268479	1000.0	2.56e+03
Done!

In [ ]: