Commit ff55a3a0 authored by peastman's avatar peastman
Browse files

Added ignoreExternalBonds option to createSystem()

parent 31c883b3
...@@ -792,7 +792,7 @@ class ForceField(object): ...@@ -792,7 +792,7 @@ class ForceField(object):
raise ValueError('%s: No parameters defined for atom type %s' % (self.forceName, t)) raise ValueError('%s: No parameters defined for atom type %s' % (self.forceName, t))
def _getResidueTemplateMatches(self, res, bondedToAtom, templateSignatures=None): def _getResidueTemplateMatches(self, res, bondedToAtom, templateSignatures=None, ignoreExternalBonds=False):
"""Return the residue template matches, or None if none are found. """Return the residue template matches, or None if none are found.
Parameters Parameters
...@@ -819,14 +819,14 @@ class ForceField(object): ...@@ -819,14 +819,14 @@ class ForceField(object):
if signature in templateSignatures: if signature in templateSignatures:
allMatches = [] allMatches = []
for t in templateSignatures[signature]: for t in templateSignatures[signature]:
match = _matchResidue(res, t, bondedToAtom) match = _matchResidue(res, t, bondedToAtom, ignoreExternalBonds)
if match is not None: if match is not None:
allMatches.append((t, match)) allMatches.append((t, match))
if len(allMatches) == 1: if len(allMatches) == 1:
template = allMatches[0][0] template = allMatches[0][0]
matches = allMatches[0][1] matches = allMatches[0][1]
elif len(allMatches) > 1: elif len(allMatches) > 1:
raise Exception('Multiple matching templates found for residue %d (%s).' % (res.index+1, res.name)) raise Exception('Multiple matching templates found for residue %d (%s): %s.' % (res.index+1, res.name, ', '.join(match[0].name for match in allMatches)))
return [template, matches] return [template, matches]
def _buildBondedToAtomList(self, topology): def _buildBondedToAtomList(self, topology):
...@@ -881,7 +881,7 @@ class ForceField(object): ...@@ -881,7 +881,7 @@ class ForceField(object):
return unmatched_residues return unmatched_residues
def getMatchingTemplates(self, topology): def getMatchingTemplates(self, topology, ignoreExternalBonds=False):
"""Return a list of forcefield residue templates matching residues in the specified topology. """Return a list of forcefield residue templates matching residues in the specified topology.
.. CAUTION:: This method is experimental, and its API is subject to change. .. CAUTION:: This method is experimental, and its API is subject to change.
...@@ -890,7 +890,8 @@ class ForceField(object): ...@@ -890,7 +890,8 @@ class ForceField(object):
---------- ----------
topology : Topology topology : Topology
The Topology whose residues are to be checked against the forcefield residue templates. The Topology whose residues are to be checked against the forcefield residue templates.
ignoreExternalBonds : bool=False
If true, ignore external bonds when matching residues to templates.
Returns Returns
------- -------
templates : list of _TemplateData templates : list of _TemplateData
...@@ -904,7 +905,7 @@ class ForceField(object): ...@@ -904,7 +905,7 @@ class ForceField(object):
templates = list() # list of templates matching the corresponding residues templates = list() # list of templates matching the corresponding residues
for res in topology.residues(): for res in topology.residues():
# Attempt to match one of the existing templates. # Attempt to match one of the existing templates.
[template, matches] = self._getResidueTemplateMatches(res, bondedToAtom) [template, matches] = self._getResidueTemplateMatches(res, bondedToAtom, ignoreExternalBonds=ignoreExternalBonds)
# Raise an exception if we have found no templates that match. # Raise an exception if we have found no templates that match.
if matches is None: if matches is None:
raise ValueError('No template found for residue %d (%s). %s' % (res.index+1, res.name, _findMatchErrors(self, res))) raise ValueError('No template found for residue %d (%s). %s' % (res.index+1, res.name, _findMatchErrors(self, res)))
...@@ -947,7 +948,7 @@ class ForceField(object): ...@@ -947,7 +948,7 @@ class ForceField(object):
if signature in signatures: if signature in signatures:
# Signature is the same as an existing residue; check connectivity. # Signature is the same as an existing residue; check connectivity.
for check_residue in unique_unmatched_residues: for check_residue in unique_unmatched_residues:
matches = _matchResidue(check_residue, template, bondedToAtom) matches = _matchResidue(check_residue, template, bondedToAtom, False)
if matches is not None: if matches is not None:
is_unique = False is_unique = False
if is_unique: if is_unique:
...@@ -959,7 +960,8 @@ class ForceField(object): ...@@ -959,7 +960,8 @@ class ForceField(object):
return [templates, unique_unmatched_residues] return [templates, unique_unmatched_residues]
def createSystem(self, topology, nonbondedMethod=NoCutoff, nonbondedCutoff=1.0*unit.nanometer, def createSystem(self, topology, nonbondedMethod=NoCutoff, nonbondedCutoff=1.0*unit.nanometer,
constraints=None, rigidWater=True, removeCMMotion=True, hydrogenMass=None, residueTemplates=dict(), **args): constraints=None, rigidWater=True, removeCMMotion=True, hydrogenMass=None, residueTemplates=dict(),
ignoreExternalBonds=False, **args):
"""Construct an OpenMM System representing a Topology with this force field. """Construct an OpenMM System representing a Topology with this force field.
Parameters Parameters
...@@ -985,11 +987,16 @@ class ForceField(object): ...@@ -985,11 +987,16 @@ class ForceField(object):
their total mass the same. their total mass the same.
residueTemplates : dict=dict() residueTemplates : dict=dict()
Key: Topology Residue object Key: Topology Residue object
Value: string, name of _TemplateData residue template object to use for Value: string, name of _TemplateData residue template object to use for (Key) residue.
(Key) residue
This allows user to specify which template to apply to particular Residues This allows user to specify which template to apply to particular Residues
in the event that multiple matching templates are available (e.g Fe2+ and Fe3+ in the event that multiple matching templates are available (e.g Fe2+ and Fe3+
templates in the ForceField for a monoatomic iron ion in the topology). templates in the ForceField for a monoatomic iron ion in the topology).
ignoreExternalBonds : boolean=False
If true, ignore external bonds when matching residues to templates. This is
useful when the Topology represents one piece of a larger molecule, so chains are
not terminated properly. This option can create ambiguities where multiple
templates match the same residue. If that happens, use the residueTemplates
argument to specify which one to use.
args args
Arbitrary additional keyword arguments may also be specified. Arbitrary additional keyword arguments may also be specified.
This allows extra parameters to be specified that are specific to This allows extra parameters to be specified that are specific to
...@@ -1031,12 +1038,12 @@ class ForceField(object): ...@@ -1031,12 +1038,12 @@ class ForceField(object):
if res in residueTemplates: if res in residueTemplates:
tname = residueTemplates[res] tname = residueTemplates[res]
template = self._templates[tname] template = self._templates[tname]
matches = _matchResidue(res, template, bondedToAtom) matches = _matchResidue(res, template, bondedToAtom, ignoreExternalBonds)
if matches is None: if matches is None:
raise Exception('User-supplied template %s does not match the residue %d (%s)' % (tname, res.index+1, res.name)) raise Exception('User-supplied template %s does not match the residue %d (%s)' % (tname, res.index+1, res.name))
else: else:
# Attempt to match one of the existing templates. # Attempt to match one of the existing templates.
[template, matches] = self._getResidueTemplateMatches(res, bondedToAtom) [template, matches] = self._getResidueTemplateMatches(res, bondedToAtom, ignoreExternalBonds=ignoreExternalBonds)
if matches is None: if matches is None:
unmatchedResidues.append(res) unmatchedResidues.append(res)
else: else:
...@@ -1045,20 +1052,20 @@ class ForceField(object): ...@@ -1045,20 +1052,20 @@ class ForceField(object):
# Try to apply patches to find matches for any unmatched residues. # Try to apply patches to find matches for any unmatched residues.
if len(unmatchedResidues) > 0: if len(unmatchedResidues) > 0:
unmatchedResidues = _applyPatchesToMatchResidues(self, data, unmatchedResidues, bondedToAtom) unmatchedResidues = _applyPatchesToMatchResidues(self, data, unmatchedResidues, bondedToAtom, ignoreExternalBonds)
# If we still haven't found a match for a residue, attempt to use residue template generators to create # If we still haven't found a match for a residue, attempt to use residue template generators to create
# new templates (and potentially atom types/parameters). # new templates (and potentially atom types/parameters).
for res in unmatchedResidues: for res in unmatchedResidues:
# A template might have been generated on an earlier iteration of this loop. # A template might have been generated on an earlier iteration of this loop.
[template, matches] = self._getResidueTemplateMatches(res, bondedToAtom) [template, matches] = self._getResidueTemplateMatches(res, bondedToAtom, ignoreExternalBonds=ignoreExternalBonds)
if matches is None: if matches is None:
# Try all generators. # Try all generators.
for generator in self._templateGenerators: for generator in self._templateGenerators:
if generator(self, res): if generator(self, res):
# This generator has registered a new residue template that should match. # This generator has registered a new residue template that should match.
[template, matches] = self._getResidueTemplateMatches(res, bondedToAtom) [template, matches] = self._getResidueTemplateMatches(res, bondedToAtom, ignoreExternalBonds=ignoreExternalBonds)
if matches is None: if matches is None:
# Something went wrong because the generated template does not match the residue signature. # Something went wrong because the generated template does not match the residue signature.
raise Exception('The residue handler %s indicated it had correctly parameterized residue %s, but the generated template did not match the residue signature.' % (generator.__class__.__name__, str(res))) raise Exception('The residue handler %s indicated it had correctly parameterized residue %s, but the generated template did not match the residue signature.' % (generator.__class__.__name__, str(res)))
...@@ -1287,7 +1294,7 @@ def _createResidueSignature(elements): ...@@ -1287,7 +1294,7 @@ def _createResidueSignature(elements):
s += element.symbol+str(count) s += element.symbol+str(count)
return s return s
def _matchResidue(res, template, bondedToAtom): def _matchResidue(res, template, bondedToAtom, ignoreExternalBonds):
"""Determine whether a residue matches a template and return a list of corresponding atoms. """Determine whether a residue matches a template and return a list of corresponding atoms.
Parameters Parameters
...@@ -1298,6 +1305,8 @@ def _matchResidue(res, template, bondedToAtom): ...@@ -1298,6 +1305,8 @@ def _matchResidue(res, template, bondedToAtom):
The template to compare it to The template to compare it to
bondedToAtom : list bondedToAtom : list
Enumerates which other atoms each atom is bonded to Enumerates which other atoms each atom is bonded to
ignoreExternalBonds : bool
If true, ignore external bonds when matching templates
Returns Returns
------- -------
...@@ -1320,7 +1329,7 @@ def _matchResidue(res, template, bondedToAtom): ...@@ -1320,7 +1329,7 @@ def _matchResidue(res, template, bondedToAtom):
for atom in atoms: for atom in atoms:
bonds = [renumberAtoms[x] for x in bondedToAtom[atom.index] if x in renumberAtoms] bonds = [renumberAtoms[x] for x in bondedToAtom[atom.index] if x in renumberAtoms]
bondedTo.append(bonds) bondedTo.append(bonds)
externalBonds.append(len([x for x in bondedToAtom[atom.index] if x not in renumberAtoms])) externalBonds.append(0 if ignoreExternalBonds else len([x for x in bondedToAtom[atom.index] if x not in renumberAtoms]))
# For each unique combination of element and number of bonds, make sure the residue and # For each unique combination of element and number of bonds, make sure the residue and
# template have the same number of atoms. # template have the same number of atoms.
...@@ -1333,7 +1342,7 @@ def _matchResidue(res, template, bondedToAtom): ...@@ -1333,7 +1342,7 @@ def _matchResidue(res, template, bondedToAtom):
residueTypeCount[key] += 1 residueTypeCount[key] += 1
templateTypeCount = {} templateTypeCount = {}
for i, atom in enumerate(template.atoms): for i, atom in enumerate(template.atoms):
key = (atom.element, len(atom.bondedTo), atom.externalBonds) key = (atom.element, len(atom.bondedTo), 0 if ignoreExternalBonds else atom.externalBonds)
if key not in templateTypeCount: if key not in templateTypeCount:
templateTypeCount[key] = 1 templateTypeCount[key] = 1
templateTypeCount[key] += 1 templateTypeCount[key] += 1
...@@ -1349,7 +1358,7 @@ def _matchResidue(res, template, bondedToAtom): ...@@ -1349,7 +1358,7 @@ def _matchResidue(res, template, bondedToAtom):
continue continue
if len(atom.bondedTo) != len(bondedTo[i]): if len(atom.bondedTo) != len(bondedTo[i]):
continue continue
if atom.externalBonds != externalBonds[i]: if not ignoreExternalBonds and atom.externalBonds != externalBonds[i]:
continue continue
candidates[i].append(j) candidates[i].append(j)
...@@ -1423,7 +1432,7 @@ def _findAtomMatches(template, bondedTo, matches, hasMatch, candidates, position ...@@ -1423,7 +1432,7 @@ def _findAtomMatches(template, bondedTo, matches, hasMatch, candidates, position
return False return False
def _applyPatchesToMatchResidues(forcefield, data, residues, bondedToAtom): def _applyPatchesToMatchResidues(forcefield, data, residues, bondedToAtom, ignoreExternalBonds):
"""Try to apply patches to find matches for residues.""" """Try to apply patches to find matches for residues."""
# Start by creating all templates than can be created by applying a combination of one-residue patches # Start by creating all templates than can be created by applying a combination of one-residue patches
# to a single template. The number of these is usually not too large, and they often cover a large fraction # to a single template. The number of these is usually not too large, and they often cover a large fraction
...@@ -1449,7 +1458,7 @@ def _applyPatchesToMatchResidues(forcefield, data, residues, bondedToAtom): ...@@ -1449,7 +1458,7 @@ def _applyPatchesToMatchResidues(forcefield, data, residues, bondedToAtom):
unmatchedResidues = [] unmatchedResidues = []
for res in residues: for res in residues:
[template, matches] = forcefield._getResidueTemplateMatches(res, bondedToAtom, patchedTemplateSignatures) [template, matches] = forcefield._getResidueTemplateMatches(res, bondedToAtom, patchedTemplateSignatures, ignoreExternalBonds)
if matches is None: if matches is None:
unmatchedResidues.append(res) unmatchedResidues.append(res)
else: else:
...@@ -1501,7 +1510,7 @@ def _applyPatchesToMatchResidues(forcefield, data, residues, bondedToAtom): ...@@ -1501,7 +1510,7 @@ def _applyPatchesToMatchResidues(forcefield, data, residues, bondedToAtom):
for patchName in patches: for patchName in patches:
patch = forcefield._patches[patchName] patch = forcefield._patches[patchName]
if patch.numResidues == clusterSize: if patch.numResidues == clusterSize:
matchedClusters = _matchToMultiResiduePatchedTemplates(data, clusters, patch, patches[patchName], bondedToAtom) matchedClusters = _matchToMultiResiduePatchedTemplates(data, clusters, patch, patches[patchName], bondedToAtom, ignoreExternalBonds)
for cluster in matchedClusters: for cluster in matchedClusters:
for residue in cluster: for residue in cluster:
unmatchedResidues.remove(residue) unmatchedResidues.remove(residue)
...@@ -1545,21 +1554,21 @@ def _generatePatchedSingleResidueTemplates(template, patches, index, newTemplate ...@@ -1545,21 +1554,21 @@ def _generatePatchedSingleResidueTemplates(template, patches, index, newTemplate
_generatePatchedSingleResidueTemplates(patchedTemplate, patches, index+1, newTemplates) _generatePatchedSingleResidueTemplates(patchedTemplate, patches, index+1, newTemplates)
def _matchToMultiResiduePatchedTemplates(data, clusters, patch, residueTemplates, bondedToAtom): def _matchToMultiResiduePatchedTemplates(data, clusters, patch, residueTemplates, bondedToAtom, ignoreExternalBonds):
"""Apply a multi-residue patch to templates, then try to match them against clusters of residues.""" """Apply a multi-residue patch to templates, then try to match them against clusters of residues."""
matchedClusters = [] matchedClusters = []
selectedTemplates = [None]*patch.numResidues selectedTemplates = [None]*patch.numResidues
_applyMultiResiduePatch(data, clusters, patch, residueTemplates, selectedTemplates, 0, matchedClusters, bondedToAtom) _applyMultiResiduePatch(data, clusters, patch, residueTemplates, selectedTemplates, 0, matchedClusters, bondedToAtom, ignoreExternalBonds)
return matchedClusters return matchedClusters
def _applyMultiResiduePatch(data, clusters, patch, candidateTemplates, selectedTemplates, index, matchedClusters, bondedToAtom): def _applyMultiResiduePatch(data, clusters, patch, candidateTemplates, selectedTemplates, index, matchedClusters, bondedToAtom, ignoreExternalBonds):
"""This is called recursively to apply a multi-residue patch to all possible combinations of templates.""" """This is called recursively to apply a multi-residue patch to all possible combinations of templates."""
if index < patch.numResidues: if index < patch.numResidues:
for template in candidateTemplates[index]: for template in candidateTemplates[index]:
selectedTemplates[index] = template selectedTemplates[index] = template
_applyMultiResiduePatch(data, clusters, patch, candidateTemplates, selectedTemplates, index+1, matchedClusters, bondedToAtom) _applyMultiResiduePatch(data, clusters, patch, candidateTemplates, selectedTemplates, index+1, matchedClusters, bondedToAtom, ignoreExternalBonds)
else: else:
# We're at the deepest level of the recursion. We've selected a template for each residue, so apply the patch, # We're at the deepest level of the recursion. We've selected a template for each residue, so apply the patch,
# then try to match it against clusters. # then try to match it against clusters.
...@@ -1576,7 +1585,7 @@ def _applyMultiResiduePatch(data, clusters, patch, candidateTemplates, selectedT ...@@ -1576,7 +1585,7 @@ def _applyMultiResiduePatch(data, clusters, patch, candidateTemplates, selectedT
for residues in itertools.permutations(cluster): for residues in itertools.permutations(cluster):
residueMatches = [] residueMatches = []
for residue, template in zip(residues, patchedTemplates): for residue, template in zip(residues, patchedTemplates):
matches = _matchResidue(residue, template, bondedToAtom) matches = _matchResidue(residue, template, bondedToAtom, ignoreExternalBonds)
if matches is None: if matches is None:
residueMatches = None residueMatches = None
break break
......
...@@ -748,6 +748,19 @@ class TestForceField(unittest.TestCase): ...@@ -748,6 +748,19 @@ class TestForceField(unittest.TestCase):
expected = 0.3*ljEnergy(2.5, 1.1, 3) + 0.3*ljEnergy(3.5, sqrt(0.1), 3) + ljEnergy(3.5, 1.5, 4) expected = 0.3*ljEnergy(2.5, 1.1, 3) + 0.3*ljEnergy(3.5, sqrt(0.1), 3) + ljEnergy(3.5, 1.5, 4)
self.assertAlmostEqual(expected, context.getState(getEnergy=True).getPotentialEnergy().value_in_unit(kilojoules_per_mole)) self.assertAlmostEqual(expected, context.getState(getEnergy=True).getPotentialEnergy().value_in_unit(kilojoules_per_mole))
def test_IgnoreExternalBonds(self):
"""Test the ignoreExternalBonds option"""
modeller = Modeller(self.pdb2.topology, self.pdb2.positions)
modeller.delete([next(modeller.topology.residues())])
self.assertRaises(Exception, lambda: self.forcefield2.createSystem(modeller.topology))
system = self.forcefield2.createSystem(modeller.topology, ignoreExternalBonds=True)
templates = self.forcefield2.getMatchingTemplates(modeller.topology, ignoreExternalBonds=True)
self.assertEqual(2, len(templates))
self.assertEqual('ALA', templates[0].name)
self.assertEqual('NME', templates[1].name)
class AmoebaTestForceField(unittest.TestCase): class AmoebaTestForceField(unittest.TestCase):
"""Test the ForceField.createSystem() method with the AMOEBA forcefield.""" """Test the ForceField.createSystem() method with the AMOEBA forcefield."""
......
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