Coulomb Particles

In this reciple, we're going to set up a system of interating point charges "by hand", using the OpenMM API.


In [1]:
import numpy as np
from simtk import openmm as mm
from simtk.openmm import app
from simtk.unit import *

Set some constants. For this example, we want to set up a system with only coulomb interactions. The OpenMM NonbondedForce computes both the columb and LJ interaction, so to have a system containing only a coulomb force, we can simply set the LJ well depth parameter to zero.

Mathematically, $ V_{LJ} = 4\varepsilon \left[ \left(\frac{\sigma}{r}\right)^{12} - \left(\frac{\sigma}{r}\right)^{6} \right] $. Thus if $\varepsilon=0$, then $V_{LJ} = 0$.


In [2]:
N_PARTICLES = 3
MASS        = 1.0*dalton
CHARGE      = 1.0*elementary_charge
LJ_SIGMA    = 1.0*nanometer
LJ_EPSILON  = 0.0*kilojoule_per_mole

To get started, create a new System from scratch. The System object is basically a full description of the Hamiltonian of the physical system you'll be simulating. We have to tell the system first how many particles we'll be simulating, and what their masses are.


In [3]:
system = mm.System()

for i in range(N_PARTICLES):
    system.addParticle(MASS)

# In case you don't want to look up the documentation, you can get the help
# text associated with each method like this.
print(help(system.addParticle))


Help on method addParticle in module simtk.openmm.openmm:

addParticle(self, *args) method of simtk.openmm.openmm.System instance
    addParticle(System self, double mass) -> int
    
    Add a particle to the System. If the mass is 0, Integrators will ignore the particle and not modify its position or velocity. This is most often used for virtual sites, but can also be used as a way to prevent a particle from moving.
       Parameters:
        - mass the mass of the particle (in atomic mass units)

None

Each System can contain zero or more forces, which specify individual terms in the Hamiltonian. There are Forces for the bonded interactions, like HarmonicBondForce, the nonbonded interactions, etc.

We'll add a NonbondedForce, which computes the coulomb and leonard jones interaction.


In [4]:
force = mm.NonbondedForce()
force.setNonbondedMethod(mm.NonbondedForce.NoCutoff)

# The force needs to be told about the charge and leonard jones parameters for each of the particles
for i in range(N_PARTICLES):
    force.addParticle(CHARGE, LJ_SIGMA, LJ_EPSILON)

# Now that we've created the force, we can add it to our system.
system.addForce(force)


Out[4]:
0

OpenMM's XMLSerializer can take a System object and save a full description of it to XML. This is pretty useful just so we can check out what's in our system, to make sure our force looks right


In [5]:
print mm.XmlSerializer.serialize(system)


<?xml version="1.0" ?>
<System type="System" version="1">
	<PeriodicBoxVectors>
		<A x="2" y="0" z="0" />
		<B x="0" y="2" z="0" />
		<C x="0" y="0" z="2" />
	</PeriodicBoxVectors>
	<Particles>
		<Particle mass="1" />
		<Particle mass="1" />
		<Particle mass="1" />
	</Particles>
	<Constraints />
	<Forces>
		<Force cutoff="1" dispersionCorrection="1" ewaldTolerance=".0005" method="0" rfDielectric="78.3" type="NonbondedForce" version="1">
			<Particles>
				<Particle eps="0" q="1" sig="1" />
				<Particle eps="0" q="1" sig="1" />
				<Particle eps="0" q="1" sig="1" />
			</Particles>
			<Exceptions />
		</Force>
	</Forces>
</System>

To actually run our simulation, we two more things. The first is an integrator, which specifies some way to evolve the system over time. The second is a Context.

In OpenMM, the Context is a class that handles the actual device that we're running on. It manages the memory, and basically controls the actual physical instantiation of our system and integrator on specific hardware.


In [6]:
integrator = mm.VerletIntegrator(0.002 * picosecond)
context = mm.Context(system, integrator)
print('Running on', context.getPlatform().getName())


('Running on', 'CUDA')

The System and Integrator don't actually know about any specific set of positions or velocity. But that is information that the context needs to know. Let's just give it some random positions


In [7]:
context.setPositions(np.random.randn(N_PARTICLES, 3))
context.setVelocities(np.zeros((N_PARTICLES, 3)))

for i in range(10):
    integrator.step(10)
    state = context.getState(getEnergy=True)
    print state.getPotentialEnergy()


156.376487732 kJ/mol
153.532646179 kJ/mol
149.228919983 kJ/mol
143.885364532 kJ/mol
137.912307739 kJ/mol
131.648445129 kJ/mol
125.341999054 kJ/mol
119.159023285 kJ/mol
113.202007294 kJ/mol
107.528697968 kJ/mol