Unverified Commit 834b1294 authored by Peter Eastman's avatar Peter Eastman Committed by GitHub
Browse files

Residue templates can specify constraints (#5197)

* Residue templates can specify constraints

* Patched template generation preserves constraints
parent add95438
...@@ -151,6 +151,15 @@ contains the following tags: ...@@ -151,6 +151,15 @@ contains the following tags:
(Alternatively, the deprecated :code:`from` tag may indicate the atom by (Alternatively, the deprecated :code:`from` tag may indicate the atom by
index instead of name.) index instead of name.)
The :code:`<Residue>` tag may also contain :code:`<Constraint>` tags to specify
pairs of atoms whose distance should be constrained. Each one should include the
attibutes :code:`atomName1` and :code:`atomName2` with the names of the two connected
atoms, and a :code:`distance` attribute with the distance between them in nanometers.
:code:`<Constraint>` tags are only used for constraints that are required by the
force field and should always be included. The more common situation is that a bond
can be either flexible or constrained based on the options the user specifies when
calling :code:`createSystem()`. Bonds of that sort should not have :code:`<Constraint>`
tags.
The :code:`<Residue>` tag may also contain :code:`<VirtualSite>` tags, The :code:`<Residue>` tag may also contain :code:`<VirtualSite>` tags,
as in the following example: as in the following example:
......
...@@ -325,6 +325,8 @@ class ForceField(object): ...@@ -325,6 +325,8 @@ class ForceField(object):
template.addExternalBondByName(bond.attrib['atomName']) template.addExternalBondByName(bond.attrib['atomName'])
else: else:
template.addExternalBond(int(bond.attrib['from'])) template.addExternalBond(int(bond.attrib['from']))
for constraint in residue.findall('Constraint'):
template.addConstraintByName(constraint.attrib['atomName1'], constraint.attrib['atomName2'], float(constraint.attrib['distance']))
for patch in residue.findall('AllowPatch'): for patch in residue.findall('AllowPatch'):
patchName = patch.attrib['name'] patchName = patch.attrib['name']
if ':' in patchName: if ':' in patchName:
...@@ -700,6 +702,7 @@ class ForceField(object): ...@@ -700,6 +702,7 @@ class ForceField(object):
self.virtualSites = [] self.virtualSites = []
self.bonds = [] self.bonds = []
self.externalBonds = [] self.externalBonds = []
self.constraints = []
self.overrideLevel = 0 self.overrideLevel = 0
self.rigidWater = True self.rigidWater = True
self.attributes = {} self.attributes = {}
...@@ -741,6 +744,16 @@ class ForceField(object): ...@@ -741,6 +744,16 @@ class ForceField(object):
atom = self.getAtomIndexByName(atom_name) atom = self.getAtomIndexByName(atom_name)
self.addExternalBond(atom) self.addExternalBond(atom)
def addConstraint(self, atom1, atom2, distance):
"""Add a constrained distance between two atoms in a template given their indices in the template."""
self.constraints.append((atom1, atom2, distance))
def addConstraintByName(self, atom1_name, atom2_name, distance):
"""Add a constrained distance between two atoms in a template given their atom names."""
atom1 = self.getAtomIndexByName(atom1_name)
atom2 = self.getAtomIndexByName(atom2_name)
self.addConstraint(atom1, atom2, distance)
def areParametersIdentical(self, template2, matchingAtoms, matchingAtoms2): def areParametersIdentical(self, template2, matchingAtoms, matchingAtoms2):
"""Get whether this template and another one both assign identical atom types and parameters to all atoms. """Get whether this template and another one both assign identical atom types and parameters to all atoms.
...@@ -757,6 +770,8 @@ class ForceField(object): ...@@ -757,6 +770,8 @@ class ForceField(object):
atoms2 = [template2.atoms[m] for m in matchingAtoms2] atoms2 = [template2.atoms[m] for m in matchingAtoms2]
if any(a1.type != a2.type or a1.parameters != a2.parameters for a1,a2 in zip(atoms1, atoms2)): if any(a1.type != a2.type or a1.parameters != a2.parameters for a1,a2 in zip(atoms1, atoms2)):
return False return False
if set(self.constraints) != set(template2.constraints):
return False
# Properly comparing virtual sites really needs a much more complicated analysis. This simple check # Properly comparing virtual sites really needs a much more complicated analysis. This simple check
# could easily fail for templates containing vsites, even if they're actually identical. Since we # could easily fail for templates containing vsites, even if they're actually identical. Since we
# currently have no force fields that include both patches and vsites, I'm not going to worry about it now. # currently have no force fields that include both patches and vsites, I'm not going to worry about it now.
...@@ -913,6 +928,14 @@ class ForceField(object): ...@@ -913,6 +928,14 @@ class ForceField(object):
for atom in self.addedExternalBonds: for atom in self.addedExternalBonds:
newTemplate.addExternalBondByName(atom.name) newTemplate.addExternalBondByName(atom.name)
# Build the list of constraints.
for atom1, atom2, distance in template.constraints:
a1 = template.atoms[atom1]
a2 = template.atoms[atom2]
if a1 in atomMap and a2 in atomMap:
newTemplate.addConstraint(atomMap[a1], atomMap[a2], distance)
# Add new virtual sites. # Add new virtual sites.
indexMap = dict((i, newAtomIndex[atom.name]) for i, atom in enumerate(self.addedAtoms[index]+self.changedAtoms[index])) indexMap = dict((i, newAtomIndex[atom.name]) for i, atom in enumerate(self.addedAtoms[index]+self.changedAtoms[index]))
...@@ -1221,7 +1244,8 @@ class ForceField(object): ...@@ -1221,7 +1244,8 @@ class ForceField(object):
The cutoff distance to use for nonbonded interactions The cutoff distance to use for nonbonded interactions
constraints : object=None constraints : object=None
Specifies which bonds and angles should be implemented with constraints. Specifies which bonds and angles should be implemented with constraints.
Allowed values are None, HBonds, AllBonds, or HAngles. Allowed values are None, HBonds, AllBonds, or HAngles. Regardless of this value,
any constraints that are explicitly specified by the force field will be added.
rigidWater : boolean=None rigidWater : boolean=None
If true, water molecules will be fully rigid regardless of the value If true, water molecules will be fully rigid regardless of the value
passed for the constraints argument. If None (the default), it uses the passed for the constraints argument. If None (the default), it uses the
...@@ -1381,11 +1405,27 @@ class ForceField(object): ...@@ -1381,11 +1405,27 @@ class ForceField(object):
atom1 = data.atoms[bond.atom1] atom1 = data.atoms[bond.atom1]
atom2 = data.atoms[bond.atom2] atom2 = data.atoms[bond.atom2]
bond.isConstrained = atom1.element is elem.hydrogen or atom2.element is elem.hydrogen bond.isConstrained = atom1.element is elem.hydrogen or atom2.element is elem.hydrogen
matchedTemplateForResidue = {}
for residue, template in templateForResidue.items():
if isinstance(residue, MergedResidue):
for res in residue.residues:
matchedTemplateForResidue[res] = template
else:
matchedTemplateForResidue[residue] = template
for bond in data.bonds: for bond in data.bonds:
atom1 = data.atoms[bond.atom1] atom1 = data.atoms[bond.atom1]
atom2 = data.atoms[bond.atom2] atom2 = data.atoms[bond.atom2]
if rigidResidue[atom1.residue.index] and rigidResidue[atom2.residue.index]: if rigidResidue[atom1.residue.index] and rigidResidue[atom2.residue.index]:
bond.isConstrained = True bond.isConstrained = True
else:
if atom1.residue not in matchedTemplateForResidue:
# This can happen with multi-residue patches, which would need more complex logic to handle.
continue
t1 = data.atomTemplateIndexes[atom1]
t2 = data.atomTemplateIndexes[atom2]
for constraint in template.constraints:
if sorted((t1, t2)) == sorted((constraint[0], constraint[1])):
bond.isConstrained = True
# Identify angles that should be implemented with constraints # Identify angles that should be implemented with constraints
...@@ -1438,6 +1478,28 @@ class ForceField(object): ...@@ -1438,6 +1478,28 @@ class ForceField(object):
if 'postprocessSystem' in dir(force): if 'postprocessSystem' in dir(force):
force.postprocessSystem(sys, data, args) force.postprocessSystem(sys, data, args)
# Add constraints specified in templates. We do this at the very end so they will override
# constraints from bonds or angles.
existingConstraints = {}
for i in range(sys.getNumConstraints()):
p1, p2, d = sys.getConstraintParameters(i)
existingConstraints[(min(p1, p2), max(p1, p2))] = i
for residue, template in templateForResidue.items():
for constraint in template.constraints:
atoms = list(residue.atoms())
atom1 = [a for a in atoms if data.atomTemplateIndexes[a] == constraint[0]]
atom2 = [a for a in atoms if data.atomTemplateIndexes[a] == constraint[1]]
if len(atom1) != 1 or len(atom2) != 1:
raise ValueError('Failed to identify atoms for constraint')
a1 = atom1[0].index
a2 = atom2[0].index
key = (min(a1, a2), max(a1, a2))
if key in existingConstraints:
sys.setConstraintParameters(existingConstraints[key], a1, a2, constraint[2])
else:
existingConstraints[key] = sys.addConstraint(a1, a2, constraint[2])
# Execute scripts found in the XML files. # Execute scripts found in the XML files.
for script in self._scripts: for script in self._scripts:
...@@ -1514,6 +1576,10 @@ class ForceField(object): ...@@ -1514,6 +1576,10 @@ class ForceField(object):
if recordParameters: if recordParameters:
data.recordMatchedAtomParameters(matchedRes, template, matches) data.recordMatchedAtomParameters(matchedRes, template, matches)
templateForResidue[matchedRes] = template templateForResidue[matchedRes] = template
if isinstance(matchedRes, MergedResidue):
for res in matchedRes.residues:
if res in templateForResidue:
del templateForResidue[res]
return templateForResidue return templateForResidue
...@@ -3601,21 +3667,6 @@ def getAtomPrint(data, atomIndex): ...@@ -3601,21 +3667,6 @@ def getAtomPrint(data, atomIndex):
return returnString return returnString
def countConstraint(data):
bondCount = 0
angleCount = 0
for bond in data.bonds:
if bond.isConstrained:
bondCount += 1
angleCount = 0
for (angle, isConstrained) in zip(data.angles, data.isAngleConstrained):
if (isConstrained):
angleCount += 1
print("Constraints bond=%d angle=%d total=%d" % (bondCount, angleCount, (bondCount+angleCount)))
## @private ## @private
class AmoebaBondGenerator(object): class AmoebaBondGenerator(object):
"""An AmoebaBondGenerator constructs a AmoebaBondForce.""" """An AmoebaBondGenerator constructs a AmoebaBondForce."""
......
...@@ -205,6 +205,63 @@ class TestForceField(unittest.TestCase): ...@@ -205,6 +205,63 @@ class TestForceField(unittest.TestCase):
# Make sure flexibleConstraints yields just as many angles as no constraints # Make sure flexibleConstraints yields just as many angles as no constraints
self.assertEqual(af2.getNumAngles(), af3.getNumAngles()) self.assertEqual(af2.getNumAngles(), af3.getNumAngles())
def testTemplateConstraints(self):
"""Test constraints defined by a residue template."""
xml = """
<ForceField>
<AtomTypes>
<Type name="C" class="C" element="C" mass="12.01078"/>
<Type name="O" class="O" element="O" mass="15.99943"/>
<Type name="H" class="H" element="H" mass="1.007947"/>
<Type name="Na+" class="Na+" element="Na" mass="22.99"/>
<Type name="Cl-" class="Cl-" element="Cl" mass="35.45"/>
</AtomTypes>
<Residues>
<Residue name="MEOH">
<Atom name="CB" type="C"/>
<Atom name="OG" type="O"/>
<Atom name="HG" type="H"/>
<Atom name="HB1" type="H"/>
<Atom name="HB2" type="H"/>
<Atom name="HB3" type="H"/>
<Bond atomName1="CB" atomName2="OG"/>
<Bond atomName1="CB" atomName2="HB1"/>
<Bond atomName1="CB" atomName2="HB2"/>
<Bond atomName1="CB" atomName2="HB3"/>
<Bond atomName1="OG" atomName2="HG"/>
<Constraint atomName1="CB" atomName2="OG" distance="1.987"/>
<Constraint atomName1="OG" atomName2="HG" distance="1.123"/>
</Residue>
<Residue name="NA">
<Atom name="NA" type="Na+"/>
</Residue>
<Residue name="CL">
<Atom name="CL" type="Cl-"/>
</Residue>
</Residues>
<HarmonicBondForce>
<Bond class1="C" class2="O" k="100.0" length="2.0"/>
<Bond class1="C" class2="H" k="100.0" length="1.0"/>
<Bond class1="O" class2="H" k="100.0" length="1.1"/>
</HarmonicBondForce>
</ForceField>"""
ff = ForceField(StringIO(xml))
pdb = PDBFile('systems/methanol_ions.pdb')
expected = {None:2, HBonds:5, AllBonds:5}
for constraints in [None, HBonds, AllBonds]:
system = ff.createSystem(pdb.topology, constraints=constraints)
self.assertEqual(expected[constraints], system.getNumConstraints())
lengths = {}
for i in range(system.getNumConstraints()):
p1, p2, length = system.getConstraintParameters(i)
lengths[(min(p1, p2), max(p1, p2))] = length.value_in_unit(nanometer)
self.assertEqual(1.987, lengths[(0, 1)])
self.assertEqual(1.123, lengths[(1, 2)])
if constraints is not None:
self.assertEqual(1.0, lengths[(0, 3)])
self.assertEqual(1.0, lengths[(0, 4)])
self.assertEqual(1.0, lengths[(0, 5)])
def test_ImplicitSolvent(self): def test_ImplicitSolvent(self):
"""Test the four types of implicit solvents using the implicitSolvent """Test the four types of implicit solvents using the implicitSolvent
parameter. parameter.
......
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