Commit fabb38d1 authored by peastman's avatar peastman
Browse files

Merge pull request #802 from swails/fix-charmm-box-vecs

Fix for unit cell vector calculation
parents 26790496 918d17ac
...@@ -39,6 +39,7 @@ import forcefield as ff ...@@ -39,6 +39,7 @@ import forcefield as ff
import element as elem import element as elem
import simtk.unit as unit import simtk.unit as unit
import simtk.openmm as mm import simtk.openmm as mm
from simtk.openmm.app.internal.unitcell import computePeriodicBoxVectors
# Enumerated values for implicit solvent model # Enumerated values for implicit solvent model
...@@ -141,7 +142,8 @@ class AmberPrmtopFile(object): ...@@ -141,7 +142,8 @@ class AmberPrmtopFile(object):
# Set the periodic box size. # Set the periodic box size.
if prmtop.getIfBox(): if prmtop.getIfBox():
top.setUnitCellDimensions(tuple(x.value_in_unit(unit.nanometer) for x in prmtop.getBoxBetaAndDimensions()[1:4])*unit.nanometer) box = prmtop.getBoxBetaAndDimensions()
top.setPeriodicBoxVectors(computePeriodicBoxVectors(*(box[1:4] + box[0:1]*3)))
def createSystem(self, nonbondedMethod=ff.NoCutoff, nonbondedCutoff=1.0*unit.nanometer, def createSystem(self, nonbondedMethod=ff.NoCutoff, nonbondedCutoff=1.0*unit.nanometer,
constraints=None, rigidWater=True, implicitSolvent=None, constraints=None, rigidWater=True, implicitSolvent=None,
......
...@@ -8,7 +8,7 @@ Structures at Stanford, funded under the NIH Roadmap for Medical Research, ...@@ -8,7 +8,7 @@ Structures at Stanford, funded under the NIH Roadmap for Medical Research,
grant U54 GM072970. See https://simtk.org. This code was originally part of grant U54 GM072970. See https://simtk.org. This code was originally part of
the ParmEd program and was ported for use with OpenMM. the ParmEd program and was ported for use with OpenMM.
Copyright (c) 2014 the Authors Copyright (c) 2014-2015 the Authors
Author: Jason M. Swails Author: Jason M. Swails
Contributors: Contributors:
...@@ -44,6 +44,7 @@ from simtk.openmm.app import (forcefield as ff, Topology, element) ...@@ -44,6 +44,7 @@ from simtk.openmm.app import (forcefield as ff, Topology, element)
from simtk.openmm.app.amberprmtopfile import HCT, OBC1, OBC2, GBn, GBn2 from simtk.openmm.app.amberprmtopfile import HCT, OBC1, OBC2, GBn, GBn2
from simtk.openmm.app.internal.customgbforces import (GBSAHCTForce, from simtk.openmm.app.internal.customgbforces import (GBSAHCTForce,
GBSAOBC1Force, GBSAOBC2Force, GBSAGBnForce, GBSAGBn2Force, convertParameters) GBSAOBC1Force, GBSAOBC2Force, GBSAGBnForce, GBSAGBn2Force, convertParameters)
from simtk.openmm.app.internal.unitcell import computePeriodicBoxVectors
# CHARMM imports # CHARMM imports
from simtk.openmm.app.internal.charmm.topologyobjects import ( from simtk.openmm.app.internal.charmm.topologyobjects import (
ResidueList, AtomList, TrackedList, Bond, Angle, Dihedral, ResidueList, AtomList, TrackedList, Bond, Angle, Dihedral,
...@@ -744,7 +745,7 @@ class CharmmPsfFile(object): ...@@ -744,7 +745,7 @@ class CharmmPsfFile(object):
Parameters: Parameters:
- a, b, c (floats) : Lengths of the periodic cell - a, b, c (floats) : Lengths of the periodic cell
- alpha, beta, gamma (floats, optional) : Angles between the - alpha, beta, gamma (floats, optional) : Angles between the
periodic cells. periodic cell vectors.
""" """
try: try:
# Since we are setting the box, delete the cached box lengths if we # Since we are setting the box, delete the cached box lengths if we
...@@ -752,8 +753,7 @@ class CharmmPsfFile(object): ...@@ -752,8 +753,7 @@ class CharmmPsfFile(object):
del self._boxLengths del self._boxLengths
except AttributeError: except AttributeError:
pass pass
self.box_vectors = _box_vectors_from_lengths_angles(a, b, c, self.box_vectors = computePeriodicBoxVectors(a, b, c, alpha, beta, gamma)
alpha, beta, gamma)
# If we already have a _topology instance, then we have possibly changed # If we already have a _topology instance, then we have possibly changed
# the existence of box information (whether or not this is a periodic # the existence of box information (whether or not this is a periodic
# system), so delete any cached reference to a topology so it's # system), so delete any cached reference to a topology so it's
...@@ -1523,58 +1523,6 @@ class CharmmPsfFile(object): ...@@ -1523,58 +1523,6 @@ class CharmmPsfFile(object):
""" Deletes the CMAP terms from the CHARMM PSF """ """ Deletes the CMAP terms from the CHARMM PSF """
self.cmap_list = TrackedList() self.cmap_list = TrackedList()
def _box_vectors_from_lengths_angles(a, b, c, alpha, beta, gamma):
"""
This method takes the lengths and angles from a unit cell and creates unit
cell vectors.
Parameters:
- a (unit, dimension length): Length of the first vector
- b (unit, dimension length): Length of the second vector
- c (unit, dimension length): Length of the third vector
- alpha (float): Angle between b and c in degrees
- beta (float): Angle between a and c in degrees
- gamma (float): Angle between a and b in degrees
Returns:
Tuple of box vectors (as Vec3 instances)
"""
if not (u.is_quantity(a) and u.is_quantity(b) and u.is_quantity(c)):
raise TypeError('a, b, and c must be units of dimension length')
if u.is_quantity(alpha): alpha = alpha.value_in_unit(u.degree)
if u.is_quantity(beta): beta = beta.value_in_unit(u.degree)
if u.is_quantity(gamma): gamma = gamma.value_in_unit(u.degree)
a = a.value_in_unit(u.angstrom)
b = b.value_in_unit(u.angstrom)
c = c.value_in_unit(u.angstrom)
if alpha <= 2 * pi and beta <= 2 * pi and gamma <= 2 * pi:
raise ValueError('box angles must be given in degrees')
alpha *= pi / 180
beta *= pi / 180
gamma *= pi / 180
av = Vec3(a, 0.0, 0.0) * u.angstrom
bx = b * cos(gamma)
by = b * sin(gamma)
bz = 0.0
cx = c * cos(beta)
cy = c * (cos(alpha) - cos(beta) * cos(gamma))
cz = sqrt(c * c - cx * cx - cy * cy)
# Make sure any components that are close to zero are set to zero exactly
if abs(bx) < TINY: bx = 0.0
if abs(by) < TINY: by = 0.0
if abs(cx) < TINY: cx = 0.0
if abs(cy) < TINY: cy = 0.0
if abs(cz) < TINY: cz = 0.0
bv = Vec3(bx, by, bz) * u.angstrom
cv = Vec3(cx, cy, cz) * u.angstrom
return (av, bv, cv)
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
def set_molecules(atom_list): def set_molecules(atom_list):
......
...@@ -52,6 +52,7 @@ except: ...@@ -52,6 +52,7 @@ except:
import simtk.unit as units import simtk.unit as units
import simtk.openmm import simtk.openmm
from simtk.openmm.app import element as elem from simtk.openmm.app import element as elem
from simtk.openmm.app.internal.unitcell import computePeriodicBoxVectors
from simtk.openmm.vec3 import Vec3 from simtk.openmm.vec3 import Vec3
import customgbforces as customgb import customgbforces as customgb
...@@ -691,7 +692,6 @@ def readAmberSystem(prmtop_filename=None, prmtop_loader=None, shake=None, gbmode ...@@ -691,7 +692,6 @@ def readAmberSystem(prmtop_filename=None, prmtop_loader=None, shake=None, gbmode
>>> system = readAmberSystem(prmtop_filename) >>> system = readAmberSystem(prmtop_filename)
""" """
if prmtop_filename is None and prmtop_loader is None: if prmtop_filename is None and prmtop_loader is None:
raise Exception("Must specify a filename or loader") raise Exception("Must specify a filename or loader")
if prmtop_filename is not None and prmtop_loader is not None: if prmtop_filename is not None and prmtop_loader is not None:
...@@ -709,9 +709,6 @@ def readAmberSystem(prmtop_filename=None, prmtop_loader=None, shake=None, gbmode ...@@ -709,9 +709,6 @@ def readAmberSystem(prmtop_filename=None, prmtop_loader=None, shake=None, gbmode
if prmtop.getIfPert()>0: if prmtop.getIfPert()>0:
raise Exception("perturbation not currently supported") raise Exception("perturbation not currently supported")
if prmtop.getIfBox()>1:
raise Exception("only standard periodic boxes are currently supported")
if prmtop.has_scee_scnb and (scee is not None or scnb is not None): if prmtop.has_scee_scnb and (scee is not None or scnb is not None):
warnings.warn("1-4 scaling parameters in topology file are being ignored. " warnings.warn("1-4 scaling parameters in topology file are being ignored. "
"This is not recommended unless you know what you are doing.") "This is not recommended unless you know what you are doing.")
...@@ -820,17 +817,7 @@ def readAmberSystem(prmtop_filename=None, prmtop_loader=None, shake=None, gbmode ...@@ -820,17 +817,7 @@ def readAmberSystem(prmtop_filename=None, prmtop_loader=None, shake=None, gbmode
# System is periodic. # System is periodic.
# Set periodic box vectors for periodic system # Set periodic box vectors for periodic system
(boxBeta, boxX, boxY, boxZ) = prmtop.getBoxBetaAndDimensions() (boxBeta, boxX, boxY, boxZ) = prmtop.getBoxBetaAndDimensions()
boxBeta = boxBeta.value_in_unit(units.degrees) xVec, yVec, zVec = computePeriodicBoxVectors(boxX, boxY, boxZ, boxBeta, boxBeta, boxBeta)
boxX = boxX.value_in_unit(units.angstroms)
boxY = boxY.value_in_unit(units.angstroms)
boxZ = boxZ.value_in_unit(units.angstroms)
tmp = [[0.0, 0.0, 0.0],[0.0, 0.0, 0.0],[0.0, 0.0, 0.0]]
_box_vectors_from_lengths_angles([boxX, boxY, boxZ],
[boxBeta, boxBeta, boxBeta],
tmp)
xVec = units.Quantity(tmp[0], units.angstroms)
yVec = units.Quantity(tmp[1], units.angstroms)
zVec = units.Quantity(tmp[2], units.angstroms)
system.setDefaultPeriodicBoxVectors(xVec, yVec, zVec) system.setDefaultPeriodicBoxVectors(xVec, yVec, zVec)
# Set cutoff. # Set cutoff.
...@@ -1286,10 +1273,10 @@ class AmberAsciiRestart(object): ...@@ -1286,10 +1273,10 @@ class AmberAsciiRestart(object):
except (IndexError, ValueError): except (IndexError, ValueError):
raise ValueError('Could not parse box line in %s' % raise ValueError('Could not parse box line in %s' %
self.filename) self.filename)
lengths = tmp[:3] lengths = tmp[:3] * units.angstroms
angles = tmp[3:] angles = tmp[3:] * units.degrees
_box_vectors_from_lengths_angles(lengths, angles, boxVectors) self.boxVectors = computePeriodicBoxVectors(lengths[0], lengths[1],
self.boxVectors = [units.Quantity(Vec3(*x), units.angstrom) for x in boxVectors] lengths[2], angles[0], angles[1], angles[2])
class AmberNetcdfRestart(object): class AmberNetcdfRestart(object):
""" """
...@@ -1360,11 +1347,12 @@ class AmberNetcdfRestart(object): ...@@ -1360,11 +1347,12 @@ class AmberNetcdfRestart(object):
if ('cell_lengths' in ncfile.variables and if ('cell_lengths' in ncfile.variables and
'cell_angles' in ncfile.variables): 'cell_angles' in ncfile.variables):
self.boxVectors = np.zeros((3,3), np.float32) self.boxVectors = np.zeros((3,3), np.float32)
_box_vectors_from_lengths_angles( leng = units.Quantity(ncfile.variables['cell_lengths'][:],
ncfile.variables['cell_lengths'][:], units.angstroms)
ncfile.variables['cell_angles'][:], angl = units.Quantity(ncfile.variables['cell_angles'][:],
self.boxVectors, units.degrees)
) self.boxVectors = computePeriodicBoxVectors(leng[0], leng[1],
leng[2], angl[0], angl[1], angl[2])
if 'time' in ncfile.variables: if 'time' in ncfile.variables:
self.time = ncfile.variables['time'].getValue() self.time = ncfile.variables['time'].getValue()
finally: finally:
...@@ -1375,52 +1363,18 @@ class AmberNetcdfRestart(object): ...@@ -1375,52 +1363,18 @@ class AmberNetcdfRestart(object):
self.coordinates = [Vec3(*x) for x in self.coordinates] self.coordinates = [Vec3(*x) for x in self.coordinates]
if self.velocities is not None: if self.velocities is not None:
self.velocities = [Vec3(*x) for x in self.velocities] self.velocities = [Vec3(*x) for x in self.velocities]
else:
if self.boxVectors is not None: if self.boxVectors is not None:
self.boxVectors = [Vec3(*x) for x in self.boxVectors] self.boxVectors = np.asarray(self.boxVectors.value_in_unit(units.nanometers))
self.boxVectors = units.Quantity(self.boxVectors, units.nanometers)
# Now add the units # Now add the units
self.coordinates = units.Quantity(self.coordinates, units.angstroms) self.coordinates = units.Quantity(self.coordinates, units.angstroms)
if self.velocities is not None: if self.velocities is not None:
self.velocities = units.Quantity(self.velocities, self.velocities = units.Quantity(self.velocities,
units.angstroms/units.picoseconds) units.angstroms/units.picoseconds)
if self.boxVectors is not None:
self.boxVectors = [units.Quantity(x, units.angstroms) for x in self.boxVectors]
self.time = units.Quantity(self.time, units.picosecond) self.time = units.Quantity(self.time, units.picosecond)
def _box_vectors_from_lengths_angles(lengths, angles, boxVectors):
"""
Converts lengths and angles into a series of box vectors and modifies
boxVectors in-place (it must be a mutable sequence)
Parameters
----------
lengths : 3-element array of floats
Lengths of the 3 periodic box vectors
angles : 3-element array of floats
Angles (in degrees) between the 3 periodic box vectors
boxVectors : mutable 3x3 sequence
"""
alpha = angles[0] * pi / 180.0
beta = angles[1] * pi / 180.0
gamma = angles[2] * pi / 180.0
boxVectors[0][0] = lengths[0]
boxVectors[1][0] = lengths[1] * cos(gamma)
boxVectors[1][1] = lengths[1] * sin(gamma)
boxVectors[2][0] = cx = lengths[2] * cos(beta)
boxVectors[2][1] = cy = lengths[2] * (cos(alpha) - cos(beta) * cos(gamma))
boxVectors[2][2] = sqrt(lengths[2]*lengths[2] - cx*cx - cy*cy)
boxVectors[0][1] = boxVectors[0][2] = boxVectors[1][2] = 0.0
# Now make sure any vector close to zero is zero exactly
for i in range(3):
for j in range(3):
if abs(boxVectors[i][j]) < TINY:
boxVectors[i][j] = 0.0
def readAmberCoordinates(filename, asNumpy=False): def readAmberCoordinates(filename, asNumpy=False):
""" """
Read atomic coordinates (and optionally, box vectors) from Amber formatted coordinate file. Read atomic coordinates (and optionally, box vectors) from Amber formatted coordinate file.
......
...@@ -32,16 +32,24 @@ __author__ = "Peter Eastman" ...@@ -32,16 +32,24 @@ __author__ = "Peter Eastman"
__version__ = "1.0" __version__ = "1.0"
from simtk.openmm import Vec3 from simtk.openmm import Vec3
from simtk.unit import nanometers, is_quantity, norm, dot from simtk.unit import nanometers, is_quantity, norm, dot, radians
import math import math
def computePeriodicBoxVectors(a_length, b_length, c_length, alpha, beta, gamma): def computePeriodicBoxVectors(a_length, b_length, c_length, alpha, beta, gamma):
"""Convert lengths and angles to periodic box vectors. """Convert lengths and angles to periodic box vectors.
Lengths should be given in nanometers and angles in radians. Lengths should be given in nanometers and angles in radians (or as Quantity
instances)
""" """
if is_quantity(a_length): a_length = a_length.value_in_unit(nanometers)
if is_quantity(b_length): b_length = b_length.value_in_unit(nanometers)
if is_quantity(c_length): c_length = c_length.value_in_unit(nanometers)
if is_quantity(alpha): alpha = alpha.value_in_unit(radians)
if is_quantity(beta): beta = beta.value_in_unit(radians)
if is_quantity(gamma): gamma = gamma.value_in_unit(radians)
# Compute the vectors. # Compute the vectors.
a = [a_length, 0, 0] a = [a_length, 0, 0]
...@@ -76,8 +84,10 @@ def computeLengthsAndAngles(periodicBoxVectors): ...@@ -76,8 +84,10 @@ def computeLengthsAndAngles(periodicBoxVectors):
Lengths are returned in nanometers and angles in radians. Lengths are returned in nanometers and angles in radians.
""" """
if is_quantity(periodicBoxVectors):
(a, b, c) = vectors.value_in_unit(nanometers) (a, b, c) = vectors.value_in_unit(nanometers)
else:
a, b, c = vectors
a_length = norm(a) a_length = norm(a)
b_length = norm(b) b_length = norm(b)
c_length = norm(c) c_length = norm(c)
......
...@@ -60,5 +60,29 @@ class TestAmberInpcrdFile(unittest.TestCase): ...@@ -60,5 +60,29 @@ class TestAmberInpcrdFile(unittest.TestCase):
self.assertTrue(inpcrd.boxVectors is None) self.assertTrue(inpcrd.boxVectors is None)
self.assertTrue(inpcrd.velocities is None) self.assertTrue(inpcrd.velocities is None)
def test_CrdBoxTruncoct(self):
# Check that the box vectors come out correct.
inpcrd = AmberInpcrdFile('systems/tz2.truncoct.rst7')
ac = Vec3(42.4388485, 0.0, 0.0) * angstroms
bc = Vec3(-14.146281691908937, 40.011730483685835, 0.0) * angstroms
cc = Vec3(-14.146281691908937, -20.0058628205162, 34.651176446201672) * angstroms
a, b, c = inpcrd.getBoxVectors()
diffa = ac - a
diffb = bc - b
diffc = cc - c
self.assertAlmostEqual(norm(diffa)/angstroms, 0)
self.assertAlmostEqual(norm(diffb)/angstroms, 0)
self.assertAlmostEqual(norm(diffc)/angstroms, 0)
# Make sure angles and lengths come out about right
la = norm(a).in_units_of(angstroms)
lb = norm(b).in_units_of(angstroms)
lc = norm(c).in_units_of(angstroms)
self.assertAlmostEqual(la/angstroms, 42.4388485, 6)
self.assertAlmostEqual(lb/angstroms, 42.4388485, 6)
self.assertAlmostEqual(lc/angstroms, 42.4388485, 6)
self.assertAlmostEqual(dot(a,b)/la/lb, cos(109.4712190*degrees), 6)
self.assertAlmostEqual(dot(a,c)/la/lc, cos(109.4712190*degrees), 6)
self.assertAlmostEqual(dot(b,c)/lc/lb, cos(109.4712190*degrees), 6)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -9,6 +9,7 @@ prmtop1 = AmberPrmtopFile('systems/alanine-dipeptide-explicit.prmtop') ...@@ -9,6 +9,7 @@ prmtop1 = AmberPrmtopFile('systems/alanine-dipeptide-explicit.prmtop')
prmtop2 = AmberPrmtopFile('systems/alanine-dipeptide-implicit.prmtop') prmtop2 = AmberPrmtopFile('systems/alanine-dipeptide-implicit.prmtop')
prmtop3 = AmberPrmtopFile('systems/ff14ipq.parm7') prmtop3 = AmberPrmtopFile('systems/ff14ipq.parm7')
prmtop4 = AmberPrmtopFile('systems/Mg_water.prmtop') prmtop4 = AmberPrmtopFile('systems/Mg_water.prmtop')
prmtop5 = AmberPrmtopFile('systems/tz2.truncoct.parm7')
inpcrd3 = AmberInpcrdFile('systems/ff14ipq.rst7') inpcrd3 = AmberInpcrdFile('systems/ff14ipq.rst7')
inpcrd4 = AmberInpcrdFile('systems/Mg_water.inpcrd') inpcrd4 = AmberInpcrdFile('systems/Mg_water.inpcrd')
...@@ -230,6 +231,31 @@ class TestAmberPrmtopFile(unittest.TestCase): ...@@ -230,6 +231,31 @@ class TestAmberPrmtopFile(unittest.TestCase):
# Amber using this force field. # Amber using this force field.
self.assertAlmostEqual(-7307.2735621/ene, 1, places=3) self.assertAlmostEqual(-7307.2735621/ene, 1, places=3)
def test_triclinicParm(self):
""" Check that triclinic unit cells work correctly """
system = prmtop5.createSystem(nonbondedMethod=PME)
refa = Vec3(4.48903851, 0.0, 0.0) * nanometer
refb = Vec3(-1.4963460492639706, 4.232306137924705, 0.0) * nanometer
refc = Vec3(-1.4963460492639706, -2.116152812842565, 3.6652847799064165) * nanometer
a, b, c = system.getDefaultPeriodicBoxVectors()
la = norm(a)
lb = norm(b)
lc = norm(c)
diffa = a - refa
diffb = b - refb
diffc = c - refc
self.assertAlmostEqual(norm(diffa)/nanometers, 0)
self.assertAlmostEqual(norm(diffb)/nanometers, 0)
self.assertAlmostEqual(norm(diffc)/nanometers, 0)
self.assertAlmostEqual(dot(a, b)/la/lb, cos(109.4712190*degrees))
self.assertAlmostEqual(dot(a, c)/la/lc, cos(109.4712190*degrees))
self.assertAlmostEqual(dot(c, b)/lc/lb, cos(109.4712190*degrees))
self.assertAlmostEqual(la/nanometers, 4.48903851)
self.assertAlmostEqual(lb/nanometers, 4.48903851)
self.assertAlmostEqual(lc/nanometers, 4.48903851)
# Now make sure that the context builds correctly; then we can bail
self.assertTrue(Context(system, VerletIntegrator(1*femtoseconds)))
def test_ImplicitSolventForces(self): def test_ImplicitSolventForces(self):
"""Compute forces for different implicit solvent types, and compare them to ones generated with a previous version of OpenMM to ensure they haven't changed.""" """Compute forces for different implicit solvent types, and compare them to ones generated with a previous version of OpenMM to ensure they haven't changed."""
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment