Ticket #642: forcefield-1.py

File forcefield-1.py, 242.0 KB (added by tic20@…, 8 years ago)

Added by email2trac

Line 
1"""
2forcefield.py: Constructs OpenMM System objects based on a Topology and an XML force field description
3
4This is part of the OpenMM molecular simulation toolkit originating from
5Simbios, the NIH National Center for Physics-Based Simulation of
6Biological Structures at Stanford, funded under the NIH Roadmap for
7Medical Research, grant U54 GM072970. See https://simtk.org.
8
9Portions copyright (c) 2012-2016 Stanford University and the Authors.
10Authors: Peter Eastman, Mark Friedrichs
11Contributors:
12
13Permission is hereby granted, free of charge, to any person obtaining a
14copy of this software and associated documentation files (the "Software"),
15to deal in the Software without restriction, including without limitation
16the rights to use, copy, modify, merge, publish, distribute, sublicense,
17and/or sell copies of the Software, and to permit persons to whom the
18Software is furnished to do so, subject to the following conditions:
19
20The above copyright notice and this permission notice shall be included in
21all copies or substantial portions of the Software.
22
23THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
26THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
27DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
28OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
29USE OR OTHER DEALINGS IN THE SOFTWARE.
30"""
31from __future__ import absolute_import, print_function
32
33__author__ = "Peter Eastman"
34__version__ = "1.0"
35
36import os
37import itertools
38import xml.etree.ElementTree as etree
39import math
40from math import sqrt, cos
41from copy import deepcopy
42from heapq import heappush, heappop
43from collections import defaultdict
44import simtk.openmm as mm
45import simtk.unit as unit
46from . import element as elem
47from simtk.openmm.app import Topology
48from simtk.openmm.app.internal.singleton import Singleton
49
50def _convertParameterToNumber(param):
51 if unit.is_quantity(param):
52 if param.unit.is_compatible(unit.bar):
53 return param / unit.bar
54 return param.value_in_unit_system(unit.md_unit_system)
55 return float(param)
56
57def _parseFunctions(element):
58 """Parse the attributes on an XML tag to find any tabulated functions it defines."""
59 functions = []
60 for function in element.findall('Function'):
61 values = [float(x) for x in function.text.split()]
62 if 'type' in function.attrib:
63 functionType = function.attrib['type']
64 else:
65 functionType = 'Continuous1D'
66 params = {}
67 for key in function.attrib:
68 if key.endswith('size'):
69 params[key] = int(function.attrib[key])
70 elif key.endswith('min') or key.endswith('max'):
71 params[key] = float(function.attrib[key])
72 functions.append((function.attrib['name'], functionType, values, params))
73 return functions
74
75def _createFunctions(force, functions):
76 """Add TabulatedFunctions to a Force based on the information that was recorded by _parseFunctions()."""
77 for (name, type, values, params) in functions:
78 if type == 'Continuous1D':
79 force.addTabulatedFunction(name, mm.Continuous1DFunction(values, params['min'], params['max']))
80 elif type == 'Continuous2D':
81 force.addTabulatedFunction(name, mm.Continuous2DFunction(params['xsize'], params['ysize'], values, params['xmin'], params['xmax'], params['ymin'], params['ymax']))
82 elif type == 'Continuous3D':
83 force.addTabulatedFunction(name, mm.Continuous2DFunction(params['xsize'], params['ysize'], params['zsize'], values, params['xmin'], params['xmax'], params['ymin'], params['ymax'], params['zmin'], params['zmax']))
84 elif type == 'Discrete1D':
85 force.addTabulatedFunction(name, mm.Discrete1DFunction(values))
86 elif type == 'Discrete2D':
87 force.addTabulatedFunction(name, mm.Discrete2DFunction(params['xsize'], params['ysize'], values))
88 elif type == 'Discrete3D':
89 force.addTabulatedFunction(name, mm.Discrete2DFunction(params['xsize'], params['ysize'], params['zsize'], values))
90
91# Enumerated values for nonbonded method
92
93class NoCutoff(Singleton):
94 def __repr__(self):
95 return 'NoCutoff'
96NoCutoff = NoCutoff()
97
98class CutoffNonPeriodic(Singleton):
99 def __repr__(self):
100 return 'CutoffNonPeriodic'
101CutoffNonPeriodic = CutoffNonPeriodic()
102
103class CutoffPeriodic(Singleton):
104 def __repr__(self):
105 return 'CutoffPeriodic'
106CutoffPeriodic = CutoffPeriodic()
107
108class Ewald(Singleton):
109 def __repr__(self):
110 return 'Ewald'
111Ewald = Ewald()
112
113class PME(Singleton):
114 def __repr__(self):
115 return 'PME'
116PME = PME()
117
118# Enumerated values for constraint type
119
120class HBonds(Singleton):
121 def __repr__(self):
122 return 'HBonds'
123HBonds = HBonds()
124
125class AllBonds(Singleton):
126 def __repr__(self):
127 return 'AllBonds'
128AllBonds = AllBonds()
129
130class HAngles(Singleton):
131 def __repr__(self):
132 return 'HAngles'
133HAngles = HAngles()
134
135# A map of functions to parse elements of the XML file.
136
137parsers = {}
138
139class ForceField(object):
140 """A ForceField constructs OpenMM System objects based on a Topology."""
141
142 def __init__(self, *files):
143 """Load one or more XML files and create a ForceField object based on them.
144
145 Parameters
146 ----------
147 files : list
148 A list of XML files defining the force field. Each entry may
149 be an absolute file path, a path relative to the current working
150 directory, a path relative to this module's data subdirectory
151 (for built in force fields), or an open file-like object with a
152 read() method from which the forcefield XML data can be loaded.
153 """
154 self._atomTypes = {}
155 self._templates = {}
156 self._patches = {}
157 self._templatePatches = {}
158 self._templateSignatures = {None:[]}
159 self._atomClasses = {'':set()}
160 self._forces = []
161 self._scripts = []
162 self._templateGenerators = []
163 self.loadFile(files)
164
165 def loadFile(self, files):
166 """Load an XML file and add the definitions from it to this ForceField.
167
168 Parameters
169 ----------
170 files : string or file or tuple
171 An XML file or tuple of XML files containing force field definitions.
172 Each entry may be either an absolute file path, a path relative to the current working
173 directory, a path relative to this module's data subdirectory (for
174 built in force fields), or an open file-like object with a read()
175 method from which the forcefield XML data can be loaded.
176 """
177
178 if not isinstance(files, tuple):
179 files = (files,)
180
181 trees = []
182
183 for file in files:
184 try:
185 # this handles either filenames or open file-like objects
186 tree = etree.parse(file)
187 except IOError:
188 tree = etree.parse(os.path.join(os.path.dirname(__file__), 'data', file))
189 except Exception as e:
190 # Fail with an error message about which file could not be read.
191 # TODO: Also handle case where fallback to 'data' directory encounters problems,
192 # but this is much less worrisome because we control those files.
193 msg = str(e) + '\n'
194 if hasattr(file, 'name'):
195 filename = file.name
196 else:
197 filename = str(file)
198 msg += "ForceField.loadFile() encountered an error reading file '%s'\n" % filename
199 raise Exception(msg)
200
201 trees.append(tree)
202
203
204 # Load the atom types.
205
206 for tree in trees:
207 if tree.getroot().find('AtomTypes') is not None:
208 for type in tree.getroot().find('AtomTypes').findall('Type'):
209 self.registerAtomType(type.attrib)
210
211 # Load the residue templates.
212
213 for tree in trees:
214 if tree.getroot().find('Residues') is not None:
215 for residue in tree.getroot().find('Residues').findall('Residue'):
216 resName = residue.attrib['name']
217 template = ForceField._TemplateData(resName)
218 if 'override' in residue.attrib:
219 template.overrideLevel = int(residue.attrib['override'])
220 atomIndices = {}
221 for atom in residue.findall('Atom'):
222 params = {}
223 for key in atom.attrib:
224 if key not in ('name', 'type'):
225 params[key] = _convertParameterToNumber(atom.attrib[key])
226 atomName = atom.attrib['name']
227 if atomName in atomIndices:
228 raise ValueError('Residue '+resName+' contains multiple atoms named '+atomName)
229 atomIndices[atomName] = len(template.atoms)
230 typeName = atom.attrib['type']
231 template.atoms.append(ForceField._TemplateAtomData(atomName, typeName, self._atomTypes[typeName].element, params))
232 for site in residue.findall('VirtualSite'):
233 template.virtualSites.append(ForceField._VirtualSiteData(site, atomIndices))
234 for bond in residue.findall('Bond'):
235 if 'atomName1' in bond.attrib:
236 template.addBondByName(bond.attrib['atomName1'], bond.attrib['atomName2'])
237 else:
238 template.addBond(int(bond.attrib['from']), int(bond.attrib['to']))
239 for bond in residue.findall('ExternalBond'):
240 if 'atomName' in bond.attrib:
241 template.addExternalBondByName(bond.attrib['atomName'])
242 else:
243 template.addExternalBond(int(bond.attrib['from']))
244 for patch in residue.findall('AllowPatch'):
245 patchName = patch.attrib['name']
246 if ':' in patchName:
247 colonIndex = patchName.find(':')
248 self.registerTemplatePatch(resName, patchName[:colonIndex], int(patchName[colonIndex+1:])-1)
249 else:
250 self.registerTemplatePatch(resName, patchName, 0)
251 self.registerResidueTemplate(template)
252
253 # Load the patch defintions.
254
255 for tree in trees:
256 if tree.getroot().find('Patches') is not None:
257 for patch in tree.getroot().find('Patches').findall('Patch'):
258 patchName = patch.attrib['name']
259 if 'residues' in patch.attrib:
260 numResidues = int(patch.attrib['residues'])
261 else:
262 numResidues = 1
263 patchData = ForceField._PatchData(patchName, numResidues)
264 allAtomNames = set()
265 for atom in patch.findall('AddAtom'):
266 params = {}
267 for key in atom.attrib:
268 if key not in ('name', 'type'):
269 params[key] = _convertParameterToNumber(atom.attrib[key])
270 atomName = atom.attrib['name']
271 if atomName in allAtomNames:
272 raise ValueError('Patch '+patchName+' contains multiple atoms named '+atomName)
273 allAtomNames.add(atomName)
274 atomDescription = ForceField._PatchAtomData(atomName)
275 typeName = atom.attrib['type']
276 patchData.addedAtoms[atomDescription.residue].append(ForceField._TemplateAtomData(atomDescription.name, typeName, self._atomTypes[typeName].element, params))
277 for atom in patch.findall('ChangeAtom'):
278 params = {}
279 for key in atom.attrib:
280 if key not in ('name', 'type'):
281 params[key] = _convertParameterToNumber(atom.attrib[key])
282 atomName = atom.attrib['name']
283 if atomName in allAtomNames:
284 raise ValueError('Patch '+patchName+' contains multiple atoms named '+atomName)
285 allAtomNames.add(atomName)
286 atomDescription = ForceField._PatchAtomData(atomName)
287 typeName = atom.attrib['type']
288 patchData.changedAtoms[atomDescription.residue].append(ForceField._TemplateAtomData(atomDescription.name, typeName, self._atomTypes[typeName].element, params))
289 for atom in patch.findall('RemoveAtom'):
290 atomName = atom.attrib['name']
291 if atomName in allAtomNames:
292 raise ValueError('Patch '+patchName+' contains multiple atoms named '+atomName)
293 allAtomNames.add(atomName)
294 atomDescription = ForceField._PatchAtomData(atomName)
295 patchData.deletedAtoms.append(atomDescription)
296 for bond in patch.findall('AddBond'):
297 atom1 = ForceField._PatchAtomData(bond.attrib['atomName1'])
298 atom2 = ForceField._PatchAtomData(bond.attrib['atomName2'])
299 patchData.addedBonds.append((atom1, atom2))
300 for bond in patch.findall('RemoveBond'):
301 atom1 = ForceField._PatchAtomData(bond.attrib['atomName1'])
302 atom2 = ForceField._PatchAtomData(bond.attrib['atomName2'])
303 patchData.deletedBonds.append((atom1, atom2))
304 for bond in patch.findall('AddExternalBond'):
305 atom = ForceField._PatchAtomData(bond.attrib['atomName'])
306 patchData.addedExternalBonds.append(atom)
307 for bond in patch.findall('RemoveExternalBond'):
308 atom = ForceField._PatchAtomData(bond.attrib['atomName'])
309 patchData.deletedExternalBonds.append(atom)
310 for residue in patch.findall('ApplyToResidue'):
311 name = residue.attrib['name']
312 if ':' in name:
313 colonIndex = name.find(':')
314 self.registerTemplatePatch(name[colonIndex+1:], patchName, int(name[:colonIndex])-1)
315 else:
316 self.registerTemplatePatch(name, patchName, 0)
317 self.registerPatch(patchData)
318
319 # Load force definitions
320
321 for tree in trees:
322 for child in tree.getroot():
323 if child.tag in parsers:
324 parsers[child.tag](child, self)
325
326 # Load scripts
327
328 for tree in trees:
329 for node in tree.getroot().findall('Script'):
330 self.registerScript(node.text)
331
332 def getGenerators(self):
333 """Get the list of all registered generators."""
334 return self._forces
335
336 def registerGenerator(self, generator):
337 """Register a new generator."""
338 self._forces.append(generator)
339
340 def registerAtomType(self, parameters):
341 """Register a new atom type."""
342 name = parameters['name']
343 if name in self._atomTypes:
344 raise ValueError('Found multiple definitions for atom type: '+name)
345 atomClass = parameters['class']
346 mass = _convertParameterToNumber(parameters['mass'])
347 element = None
348 if 'element' in parameters:
349 element = parameters['element']
350 if not isinstance(element, elem.Element):
351 element = elem.get_by_symbol(element)
352 self._atomTypes[name] = ForceField._AtomType(name, atomClass, mass, element)
353 if atomClass in self._atomClasses:
354 typeSet = self._atomClasses[atomClass]
355 else:
356 typeSet = set()
357 self._atomClasses[atomClass] = typeSet
358 typeSet.add(name)
359 self._atomClasses[''].add(name)
360
361 def registerResidueTemplate(self, template):
362 """Register a new residue template."""
363 if template.name in self._templates:
364 # There is already a template with this name, so check the override levels.
365
366 existingTemplate = self._templates[template.name]
367 if template.overrideLevel < existingTemplate.overrideLevel:
368 # The existing one takes precedence, so just return.
369 return
370 if template.overrideLevel > existingTemplate.overrideLevel:
371 # We need to delete the existing template.
372 del self._templates[template.name]
373 existingSignature = _createResidueSignature([atom.element for atom in existingTemplate.atoms])
374 self._templateSignatures[existingSignature].remove(existingTemplate)
375 else:
376 raise ValueError('Residue template %s with the same override level %d already exists.' % (template.name, template.overrideLevel))
377
378 # Register the template.
379
380 self._templates[template.name] = template
381 signature = _createResidueSignature([atom.element for atom in template.atoms])
382 if signature in self._templateSignatures:
383 self._templateSignatures[signature].append(template)
384 else:
385 self._templateSignatures[signature] = [template]
386
387 def registerPatch(self, patch):
388 """Register a new patch that can be applied to templates."""
389 self._patches[patch.name] = patch
390
391 def registerTemplatePatch(self, residue, patch, patchResidueIndex):
392 """Register that a particular patch can be used with a particular residue."""
393 if residue not in self._templatePatches:
394 self._templatePatches[residue] = []
395 self._templatePatches[residue].append((patch, patchResidueIndex))
396
397 def registerScript(self, script):
398 """Register a new script to be executed after building the System."""
399 self._scripts.append(script)
400
401 def registerTemplateGenerator(self, generator):
402 """Register a residue template generator that can be used to parameterize residues that do not match existing forcefield templates.
403
404 This functionality can be used to add handlers to parameterize small molecules or unnatural/modified residues.
405
406 .. CAUTION:: This method is experimental, and its API is subject to change.
407
408 Parameters
409 ----------
410 generator : function
411 A function that will be called when a residue is encountered that does not match an existing forcefield template.
412
413 When a residue without a template is encountered, the `generator` function is called with:
414
415 ::
416 success = generator(forcefield, residue)
417 ```
418
419 where `forcefield` is the calling `ForceField` object and `residue` is a simtk.openmm.app.topology.Residue object.
420
421 `generator` must conform to the following API:
422 ::
423 Parameters
424 ----------
425 forcefield : simtk.openmm.app.ForceField
426 The ForceField object to which residue templates and/or parameters are to be added.
427 residue : simtk.openmm.app.Topology.Residue
428 The residue topology for which a template is to be generated.
429
430 Returns
431 -------
432 success : bool
433 If the generator is able to successfully parameterize the residue, `True` is returned.
434 If the generator cannot parameterize the residue, it should return `False` and not modify `forcefield`.
435
436 The generator should either register a residue template directly with `forcefield.registerResidueTemplate(template)`
437 or it should call `forcefield.loadFile(file)` to load residue definitions from an ffxml file.
438
439 It can also use the `ForceField` programmatic API to add additional atom types (via `forcefield.registerAtomType(parameters)`)
440 or additional parameters.
441
442 """
443 self._templateGenerators.append(generator)
444
445 def _findAtomTypes(self, attrib, num):
446 """Parse the attributes on an XML tag to find the set of atom types for each atom it involves.
447
448 Parameters
449 ----------
450 attrib : dict of attributes
451 The dictionary of attributes for an XML parameter tag.
452 num : int
453 The number of atom specifiers (e.g. 'class1' through 'class4') to extract.
454
455 Returns
456 -------
457 types : list
458 A list of atom types that match.
459
460 """
461 types = []
462 for i in range(num):
463 if num == 1:
464 suffix = ''
465 else:
466 suffix = str(i+1)
467 classAttrib = 'class'+suffix
468 typeAttrib = 'type'+suffix
469 if classAttrib in attrib:
470 if typeAttrib in attrib:
471 raise ValueError('Specified both a type and a class for the same atom: '+str(attrib))
472 if attrib[classAttrib] not in self._atomClasses:
473 types.append(None) # Unknown atom class
474 else:
475 types.append(self._atomClasses[attrib[classAttrib]])
476 elif typeAttrib in attrib:
477 if attrib[typeAttrib] == '':
478 types.append(self._atomClasses[''])
479 elif attrib[typeAttrib] not in self._atomTypes:
480 types.append(None) # Unknown atom type
481 else:
482 types.append([attrib[typeAttrib]])
483 else:
484 types.append(None) # Unknown atom type
485 return types
486
487 def _parseTorsion(self, attrib):
488 """Parse the node defining a torsion."""
489 types = self._findAtomTypes(attrib, 4)
490 if None in types:
491 return None
492 torsion = PeriodicTorsion(types)
493 index = 1
494 while 'phase%d'%index in attrib:
495 torsion.periodicity.append(int(attrib['periodicity%d'%index]))
496 torsion.phase.append(_convertParameterToNumber(attrib['phase%d'%index]))
497 torsion.k.append(_convertParameterToNumber(attrib['k%d'%index]))
498 index += 1
499 return torsion
500
501 class _SystemData(object):
502 """Inner class used to encapsulate data about the system being created."""
503 def __init__(self):
504 self.atomType = {}
505 self.atomParameters = {}
506 self.atoms = []
507 self.excludeAtomWith = []
508 self.virtualSites = {}
509 self.bonds = []
510 self.angles = []
511 self.propers = []
512 self.impropers = []
513 self.atomBonds = []
514 self.isAngleConstrained = []
515 self.constraints = {}
516
517 def addConstraint(self, system, atom1, atom2, distance):
518 """Add a constraint to the system, avoiding duplicate constraints."""
519 key = (min(atom1, atom2), max(atom1, atom2))
520 if key in self.constraints:
521 if self.constraints[key] != distance:
522 raise ValueError('Two constraints were specified between atoms %d and %d with different distances' % (atom1, atom2))
523 else:
524 self.constraints[key] = distance
525 system.addConstraint(atom1, atom2, distance)
526
527 def recordMatchedAtomParameters(self, residue, template, matches):
528 """Record parameters for atoms based on having matched a residue to a template."""
529 matchAtoms = dict(zip(matches, residue.atoms()))
530 for atom, match in zip(residue.atoms(), matches):
531 self.atomType[atom] = template.atoms[match].type
532 self.atomParameters[atom] = template.atoms[match].parameters
533 for site in template.virtualSites:
534 if match == site.index:
535 self.virtualSites[atom] = (site, [matchAtoms[i].index for i in site.atoms], matchAtoms[site.excludeWith].index)
536
537 class _TemplateData(object):
538 """Inner class used to encapsulate data about a residue template definition."""
539 def __init__(self, name):
540 self.name = name
541 self.atoms = []
542 self.virtualSites = []
543 self.bonds = []
544 self.externalBonds = []
545 self.overrideLevel = 0
546
547 def getAtomIndexByName(self, atom_name):
548 """Look up an atom index by atom name, providing a helpful error message if not found."""
549 for (index, atom) in enumerate(self.atoms):
550 if atom.name == atom_name:
551 return index
552
553 # Provide a helpful error message if atom name not found.
554 msg = "Atom name '%s' not found in residue template '%s'.\n" % (atom_name, self.name)
555 msg += "Possible atom names are: %s" % str(atomIndices.keys())
556 raise ValueError(msg)
557
558 def addBond(self, atom1, atom2):
559 """Add a bond between two atoms in a template given their indices in the template."""
560 self.bonds.append((atom1, atom2))
561 self.atoms[atom1].bondedTo.append(atom2)
562 self.atoms[atom2].bondedTo.append(atom1)
563
564 def addBondByName(self, atom1_name, atom2_name):
565 """Add a bond between two atoms in a template given their atom names."""
566 atom1 = self.getAtomIndexByName(atom1_name)
567 atom2 = self.getAtomIndexByName(atom2_name)
568 self.addBond(atom1, atom2)
569
570 def addExternalBond(self, atom_index):
571 """Designate that an atom in a residue template has an external bond, using atom index within template."""
572 self.externalBonds.append(atom_index)
573 self.atoms[atom_index].externalBonds += 1
574
575 def addExternalBondByName(self, atom_name):
576 """Designate that an atom in a residue template has an external bond, using atom name within template."""
577 atom = self.getAtomIndexByName(atom_name)
578 self.addExternalBond(atom)
579
580 class _TemplateAtomData(object):
581 """Inner class used to encapsulate data about an atom in a residue template definition."""
582 def __init__(self, name, type, element, parameters={}):
583 self.name = name
584 self.type = type
585 self.element = element
586 self.parameters = parameters
587 self.bondedTo = []
588 self.externalBonds = 0
589
590 class _BondData(object):
591 """Inner class used to encapsulate data about a bond."""
592 def __init__(self, atom1, atom2):
593 self.atom1 = atom1
594 self.atom2 = atom2
595 self.isConstrained = False
596 self.length = 0.0
597
598 class _VirtualSiteData(object):
599 """Inner class used to encapsulate data about a virtual site."""
600 def __init__(self, node, atomIndices):
601 attrib = node.attrib
602 self.type = attrib['type']
603 if self.type == 'average2':
604 numAtoms = 2
605 self.weights = [float(attrib['weight1']), float(attrib['weight2'])]
606 elif self.type == 'average3':
607 numAtoms = 3
608 self.weights = [float(attrib['weight1']), float(attrib['weight2']), float(attrib['weight3'])]
609 elif self.type == 'outOfPlane':
610 numAtoms = 3
611 self.weights = [float(attrib['weight12']), float(attrib['weight13']), float(attrib['weightCross'])]
612 elif self.type == 'localCoords':
613 numAtoms = 3
614 self.originWeights = [float(attrib['wo1']), float(attrib['wo2']), float(attrib['wo3'])]
615 self.xWeights = [float(attrib['wx1']), float(attrib['wx2']), float(attrib['wx3'])]
616 self.yWeights = [float(attrib['wy1']), float(attrib['wy2']), float(attrib['wy3'])]
617 self.localPos = [float(attrib['p1']), float(attrib['p2']), float(attrib['p3'])]
618 else:
619 raise ValueError('Unknown virtual site type: %s' % self.type)
620 if 'siteName' in attrib:
621 self.index = atomIndices[attrib['siteName']]
622 self.atoms = [atomIndices[attrib['atomName%d'%(i+1)]] for i in range(numAtoms)]
623 else:
624 self.index = int(attrib['index'])
625 self.atoms = [int(attrib['atom%d'%(i+1)]) for i in range(numAtoms)]
626 if 'excludeWith' in attrib:
627 self.excludeWith = int(attrib['excludeWith'])
628 else:
629 self.excludeWith = self.atoms[0]
630
631 class _PatchData(object):
632 """Inner class used to encapsulate data about a patch definition."""
633 def __init__(self, name, numResidues):
634 self.name = name
635 self.numResidues = numResidues
636 self.addedAtoms = [[] for i in range(numResidues)]
637 self.changedAtoms = [[] for i in range(numResidues)]
638 self.deletedAtoms = []
639 self.addedBonds = []
640 self.deletedBonds = []
641 self.addedExternalBonds = []
642 self.deletedExternalBonds = []
643
644 def createPatchedTemplates(self, templates):
645 """Apply this patch to a set of templates, creating new modified ones."""
646 if len(templates) != self.numResidues:
647 raise ValueError("Patch '%s' expected %d templates, received %d", (self.name, self.numResidues, len(templates)))
648
649 # Construct a new version of each template.
650
651 newTemplates = []
652 for index, template in enumerate(templates):
653 newTemplate = ForceField._TemplateData("%s-%s" % (template.name, self.name))
654 newTemplates.append(newTemplate)
655
656 # Build the list of atoms in it.
657
658 for atom in template.atoms:
659 if not any(deleted.name == atom.name and deleted.residue == index for deleted in self.deletedAtoms):
660 newTemplate.atoms.append(ForceField._TemplateAtomData(atom.name, atom.type, atom.element, atom.parameters))
661 for atom in self.addedAtoms[index]:
662 if any(a.name == atom.name for a in newTemplate.atoms):
663 raise ValueError("Patch '%s' adds an atom with the same name as an existing atom: %s" % (self.name, atom.name))
664 newTemplate.atoms.append(ForceField._TemplateAtomData(atom.name, atom.type, atom.element, atom.parameters))
665 oldAtomIndex = dict([(atom.name, i) for i, atom in enumerate(template.atoms)])
666 newAtomIndex = dict([(atom.name, i) for i, atom in enumerate(newTemplate.atoms)])
667 for atom in self.changedAtoms[index]:
668 if atom.name not in newAtomIndex:
669 raise ValueError("Patch '%s' modifies nonexistent atom '%s' in template '%s'" % (self.name, atom.name, template.name))
670 newTemplate.atoms[newAtomIndex[atom.name]] = ForceField._TemplateAtomData(atom.name, atom.type, atom.element, atom.parameters)
671
672 # Copy over the virtual sites, translating the atom indices.
673
674 indexMap = dict([(oldAtomIndex[name], newAtomIndex[name]) for name in newAtomIndex if name in oldAtomIndex])
675 for site in template.virtualSites:
676 if site.index in indexMap and all(i in indexMap for i in site.atoms):
677 newSite = deepcopy(site)
678 newSite.index = indexMap[site.index]
679 newSite.atoms = [indexMap[i] for i in site.atoms]
680 newTemplate.virtualSites.append(newSite)
681
682 # Build the lists of bonds and external bonds.
683
684 atomMap = dict([(template.atoms[i], indexMap[i]) for i in indexMap])
685 deletedBonds = [(atom1.name, atom2.name) for atom1, atom2 in self.deletedBonds if atom1.residue == index and atom2.residue == index]
686 for atom1, atom2 in template.bonds:
687 a1 = template.atoms[atom1]
688 a2 = template.atoms[atom2]
689 if a1 in atomMap and a2 in atomMap and (a1.name, a2.name) not in deletedBonds and (a2.name, a1.name) not in deletedBonds:
690 newTemplate.addBond(atomMap[a1], atomMap[a2])
691 deletedExternalBonds = [atom.name for atom in self.deletedExternalBonds if atom.residue == index]
692 for atom in template.externalBonds:
693 if template.atoms[atom].name not in deletedExternalBonds:
694 newTemplate.addExternalBond(indexMap[atom])
695 for atom1, atom2 in self.addedBonds:
696 if atom1.residue == index and atom2.residue == index:
697 newTemplate.addBondByName(atom1.name, atom2.name)
698 elif atom1.residue == index:
699 newTemplate.addExternalBondByName(atom1.name)
700 elif atom2.residue == index:
701 newTemplate.addExternalBondByName(atom2.name)
702 for atom in self.addedExternalBonds:
703 newTemplate.addExternalBondByName(atom.name)
704 return newTemplates
705
706 class _PatchAtomData(object):
707 """Inner class used to encapsulate data about an atom in a patch definition."""
708 def __init__(self, description):
709 if ':' in description:
710 colonIndex = description.find(':')
711 self.residue = int(description[:colonIndex])-1
712 self.name = description[colonIndex+1:]
713 else:
714 self.residue = 0
715 self.name = description
716
717 class _AtomType(object):
718 """Inner class used to record atom types and associated properties."""
719 def __init__(self, name, atomClass, mass, element):
720 self.name = name
721 self.atomClass = atomClass
722 self.mass = mass
723 self.element = element
724
725 class _AtomTypeParameters(object):
726 """Inner class used to record parameter values for atom types."""
727 def __init__(self, forcefield, forceName, atomTag, paramNames):
728 self.ff = forcefield
729 self.forceName = forceName
730 self.atomTag = atomTag
731 self.paramNames = paramNames
732 self.paramsForType = {}
733 self.extraParamsForType = {}
734
735 def registerAtom(self, parameters, expectedParams=None):
736 if expectedParams is None:
737 expectedParams = self.paramNames
738 types = self.ff._findAtomTypes(parameters, 1)
739 if None not in types:
740 values = {}
741 extraValues = {}
742 for key in parameters:
743 if key in expectedParams:
744 values[key] = _convertParameterToNumber(parameters[key])
745 else:
746 extraValues[key] = parameters[key]
747 if len(values) < len(expectedParams):
748 for key in expectedParams:
749 if key not in values:
750 raise ValueError('%s: No value specified for "%s"' % (self.forceName, key))
751 for t in types[0]:
752 self.paramsForType[t] = values
753 self.extraParamsForType[t] = extraValues
754
755 def parseDefinitions(self, element):
756 """"Load the definitions from an XML element."""
757 expectedParams = list(self.paramNames)
758 excludedParams = [node.attrib['name'] for node in element.findall('UseAttributeFromResidue')]
759 for param in excludedParams:
760 if param not in expectedParams:
761 raise ValueError('%s: <UseAttributeFromResidue> specified an invalid attribute: %s' % (self.forceName, param))
762 expectedParams.remove(param)
763 for atom in element.findall(self.atomTag):
764 for param in excludedParams:
765 if param in atom.attrib:
766 raise ValueError('%s: The attribute "%s" appeared in both <%s> and <UseAttributeFromResidue> tags' % (self.forceName, param, self.atomTag))
767 self.registerAtom(atom.attrib, expectedParams)
768
769 def getAtomParameters(self, atom, data):
770 """Get the parameter values for a particular atom."""
771 t = data.atomType[atom]
772 p = data.atomParameters[atom]
773 if t in self.paramsForType:
774 values = self.paramsForType[t]
775 result = [None]*len(self.paramNames)
776 for i, name in enumerate(self.paramNames):
777 if name in values:
778 result[i] = values[name]
779 elif name in p:
780 result[i] = p[name]
781 else:
782 raise ValueError('%s: No value specified for "%s"' % (self.forceName, name))
783 return result
784 else:
785 raise ValueError('%s: No parameters defined for atom type %s' % (self.forceName, t))
786
787 def getExtraParameters(self, atom, data):
788 """Get extra parameter values for an atom that appeared in the <Atom> tag but were not included in paramNames."""
789 t = data.atomType[atom]
790 if t in self.paramsForType:
791 return self.extraParamsForType[t]
792 else:
793 raise ValueError('%s: No parameters defined for atom type %s' % (self.forceName, t))
794
795
796 def _getResidueTemplateMatches(self, res, bondedToAtom, templateSignatures=None, ignoreExternalBonds=False):
797 """Return the residue template matches, or None if none are found.
798
799 Parameters
800 ----------
801 res : Topology.Residue
802 The residue for which template matches are to be retrieved.
803 bondedToAtom : list of set of int
804 bondedToAtom[i] is the set of atoms bonded to atom index i
805
806 Returns
807 -------
808 template : _ForceFieldTemplate
809 The matching forcefield residue template, or None if no matches are found.
810 matches : list
811 a list specifying which atom of the template each atom of the residue
812 corresponds to, or None if it does not match the template
813
814 """
815 template = None
816 matches = None
817 if templateSignatures is None:
818 templateSignatures = self._templateSignatures
819 signature = _createResidueSignature([atom.element for atom in res.atoms()])
820 if signature in templateSignatures:
821 allMatches = []
822 '''
823 Search through once including external bonds. We only want to
824 consider the ignoreExternalBonds switch if no complete template
825 is found, otherwise we run into all sorts of problems.
826 '''
827 for t in templateSignatures[signature]:
828 match = _matchResidue(res, t, bondedToAtom)
829 if match is not None:
830 allMatches.append((t, match))
831 if len(allMatches) == 0 and ignoreExternalBonds:
832 for t in templateSignatures[signature]:
833 match = _matchResidue(res, t, bondedToAtom, ignoreExternalBonds)
834 if match is not None:
835 allMatches.append((t, match))
836 if len(allMatches) == 1:
837 template = allMatches[0][0]
838 matches = allMatches[0][1]
839 elif len(allMatches) > 1:
840 raise Exception('Multiple matching templates found for residue %d (%s): %s.' % (res.index+1, res.name, ', '.join(match[0].name for match in allMatches)))
841 return [template, matches]
842
843 def _buildBondedToAtomList(self, topology):
844 """Build a list of which atom indices are bonded to each atom.
845
846 Parameters
847 ----------
848 topology : Topology
849 The Topology whose bonds are to be indexed.
850
851 Returns
852 -------
853 bondedToAtom : list of set of int
854 bondedToAtom[index] is the set of atom indices bonded to atom `index`
855
856 """
857 bondedToAtom = []
858 for atom in topology.atoms():
859 bondedToAtom.append(set())
860 for (atom1, atom2) in topology.bonds():
861 bondedToAtom[atom1.index].add(atom2.index)
862 bondedToAtom[atom2.index].add(atom1.index)
863 return bondedToAtom
864
865 def getUnmatchedResidues(self, topology):
866 """Return a list of Residue objects from specified topology for which no forcefield templates are available.
867
868 .. CAUTION:: This method is experimental, and its API is subject to change.
869
870 Parameters
871 ----------
872 topology : Topology
873 The Topology whose residues are to be checked against the forcefield residue templates.
874
875 Returns
876 -------
877 unmatched_residues : list of Residue
878 List of Residue objects from `topology` for which no forcefield residue templates are available.
879 Note that multiple instances of the same residue appearing at different points in the topology may be returned.
880
881 This method may be of use in generating missing residue templates or diagnosing parameterization failures.
882 """
883 # Find the template matching each residue, compiling a list of residues for which no templates are available.
884 bondedToAtom = self._buildBondedToAtomList(topology)
885 unmatched_residues = list() # list of unmatched residues
886 for res in topology.residues():
887 # Attempt to match one of the existing templates.
888 [template, matches] = self._getResidueTemplateMatches(res, bondedToAtom)
889 if matches is None:
890 # No existing templates match.
891 unmatched_residues.append(res)
892
893 return unmatched_residues
894
895 def getMatchingTemplates(self, topology, ignoreExternalBonds=False):
896 """Return a list of forcefield residue templates matching residues in the specified topology.
897
898 .. CAUTION:: This method is experimental, and its API is subject to change.
899
900 Parameters
901 ----------
902 topology : Topology
903 The Topology whose residues are to be checked against the forcefield residue templates.
904 ignoreExternalBonds : bool=False
905 If true, ignore external bonds when matching residues to templates.
906 Returns
907 -------
908 templates : list of _TemplateData
909 List of forcefield residue templates corresponding to residues in the topology.
910 templates[index] is template corresponding to residue `index` in topology.residues()
911
912 This method may be of use in debugging issues related to parameter assignment.
913 """
914 # Find the template matching each residue, compiling a list of residues for which no templates are available.
915 bondedToAtom = self._buildBondedToAtomList(topology)
916 templates = list() # list of templates matching the corresponding residues
917 for res in topology.residues():
918 # Attempt to match one of the existing templates.
919 [template, matches] = self._getResidueTemplateMatches(res, bondedToAtom, ignoreExternalBonds=ignoreExternalBonds)
920 # Raise an exception if we have found no templates that match.
921 if matches is None:
922 raise ValueError('No template found for residue %d (%s). %s' % (res.index+1, res.name, _findMatchErrors(self, res)))
923 else:
924 templates.append(template)
925
926 return templates
927
928 def generateTemplatesForUnmatchedResidues(self, topology):
929 """Generate forcefield residue templates for residues in specified topology for which no forcefield templates are available.
930
931 .. CAUTION:: This method is experimental, and its API is subject to change.
932
933 Parameters
934 ----------
935 topology : Topology
936 The Topology whose residues are to be checked against the forcefield residue templates.
937
938 Returns
939 -------
940 templates : list of _TemplateData
941 List of forcefield residue templates corresponding to residues in `topology` for which no forcefield templates are currently available.
942 Atom types will be set to `None`, but template name, atom names, elements, and connectivity will be taken from corresponding Residue objects.
943 residues : list of Residue
944 List of Residue objects that were used to generate the templates.
945 `residues[index]` is the Residue that was used to generate the template `templates[index]`
946
947 """
948 # Get a non-unique list of unmatched residues.
949 unmatched_residues = self.getUnmatchedResidues(topology)
950 # Generate a unique list of unmatched residues by comparing fingerprints.
951 bondedToAtom = self._buildBondedToAtomList(topology)
952 unique_unmatched_residues = list() # list of unique unmatched Residue objects from topology
953 templates = list() # corresponding _TemplateData templates
954 signatures = set()
955 for residue in unmatched_residues:
956 signature = _createResidueSignature([ atom.element for atom in residue.atoms() ])
957 template = _createResidueTemplate(residue)
958 is_unique = True
959 if signature in signatures:
960 # Signature is the same as an existing residue; check connectivity.
961 for check_residue in unique_unmatched_residues:
962 matches = _matchResidue(check_residue, template, bondedToAtom, False)
963 if matches is not None:
964 is_unique = False
965 if is_unique:
966 # Residue is unique.
967 unique_unmatched_residues.append(residue)
968 signatures.add(signature)
969 templates.append(template)
970
971 return [templates, unique_unmatched_residues]
972
973 def createSystem(self, topology, nonbondedMethod=NoCutoff, nonbondedCutoff=1.0*unit.nanometer,
974 constraints=None, rigidWater=True, removeCMMotion=True, hydrogenMass=None, residueTemplates=dict(),
975 ignoreExternalBonds=False, **args):
976 """Construct an OpenMM System representing a Topology with this force field.
977
978 Parameters
979 ----------
980 topology : Topology
981 The Topology for which to create a System
982 nonbondedMethod : object=NoCutoff
983 The method to use for nonbonded interactions. Allowed values are
984 NoCutoff, CutoffNonPeriodic, CutoffPeriodic, Ewald, or PME.
985 nonbondedCutoff : distance=1*nanometer
986 The cutoff distance to use for nonbonded interactions
987 constraints : object=None
988 Specifies which bonds and angles should be implemented with constraints.
989 Allowed values are None, HBonds, AllBonds, or HAngles.
990 rigidWater : boolean=True
991 If true, water molecules will be fully rigid regardless of the value
992 passed for the constraints argument
993 removeCMMotion : boolean=True
994 If true, a CMMotionRemover will be added to the System
995 hydrogenMass : mass=None
996 The mass to use for hydrogen atoms bound to heavy atoms. Any mass
997 added to a hydrogen is subtracted from the heavy atom to keep
998 their total mass the same.
999 residueTemplates : dict=dict()
1000 Key: Topology Residue object
1001 Value: string, name of _TemplateData residue template object to use for (Key) residue.
1002 This allows user to specify which template to apply to particular Residues
1003 in the event that multiple matching templates are available (e.g Fe2+ and Fe3+
1004 templates in the ForceField for a monoatomic iron ion in the topology).
1005 ignoreExternalBonds : boolean=False
1006 If true, ignore external bonds when matching residues to templates. This is
1007 useful when the Topology represents one piece of a larger molecule, so chains are
1008 not terminated properly. This option can create ambiguities where multiple
1009 templates match the same residue. If that happens, use the residueTemplates
1010 argument to specify which one to use.
1011 args
1012 Arbitrary additional keyword arguments may also be specified.
1013 This allows extra parameters to be specified that are specific to
1014 particular force fields.
1015
1016 Returns
1017 -------
1018 system
1019 the newly created System
1020 """
1021 data = ForceField._SystemData()
1022 data.atoms = list(topology.atoms())
1023 for atom in data.atoms:
1024 data.excludeAtomWith.append([])
1025
1026 # Make a list of all bonds
1027
1028 for bond in topology.bonds():
1029 data.bonds.append(ForceField._BondData(bond[0].index, bond[1].index))
1030
1031 # Record which atoms are bonded to each other atom
1032
1033 bondedToAtom = []
1034 for i in range(len(data.atoms)):
1035 bondedToAtom.append(set())
1036 data.atomBonds.append([])
1037 for i in range(len(data.bonds)):
1038 bond = data.bonds[i]
1039 bondedToAtom[bond.atom1].add(bond.atom2)
1040 bondedToAtom[bond.atom2].add(bond.atom1)
1041 data.atomBonds[bond.atom1].append(i)
1042 data.atomBonds[bond.atom2].append(i)
1043
1044 # Find the template matching each residue and assign atom types.
1045
1046 unmatchedResidues = []
1047 for chain in topology.chains():
1048 for res in chain.residues():
1049 if res in residueTemplates:
1050 tname = residueTemplates[res]
1051 template = self._templates[tname]
1052 matches = _matchResidue(res, template, bondedToAtom, ignoreExternalBonds)
1053 if matches is None:
1054 raise Exception('User-supplied template %s does not match the residue %d (%s)' % (tname, res.index+1, res.name))
1055 else:
1056 # Attempt to match one of the existing templates.
1057 [template, matches] = self._getResidueTemplateMatches(res, bondedToAtom, ignoreExternalBonds=ignoreExternalBonds)
1058 if matches is None:
1059 unmatchedResidues.append(res)
1060 else:
1061 data.recordMatchedAtomParameters(res, template, matches)
1062
1063 # Try to apply patches to find matches for any unmatched residues.
1064
1065 if len(unmatchedResidues) > 0:
1066 unmatchedResidues = _applyPatchesToMatchResidues(self, data, unmatchedResidues, bondedToAtom, ignoreExternalBonds)
1067
1068 # If we still haven't found a match for a residue, attempt to use residue template generators to create
1069 # new templates (and potentially atom types/parameters).
1070
1071 for res in unmatchedResidues:
1072 # A template might have been generated on an earlier iteration of this loop.
1073 [template, matches] = self._getResidueTemplateMatches(res, bondedToAtom, ignoreExternalBonds=ignoreExternalBonds)
1074 if matches is None:
1075 # Try all generators.
1076 for generator in self._templateGenerators:
1077 if generator(self, res):
1078 # This generator has registered a new residue template that should match.
1079 [template, matches] = self._getResidueTemplateMatches(res, bondedToAtom, ignoreExternalBonds=ignoreExternalBonds)
1080 if matches is None:
1081 # Something went wrong because the generated template does not match the residue signature.
1082 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)))
1083 else:
1084 # We successfully generated a residue template. Break out of the for loop.
1085 break
1086 if matches is None:
1087 raise ValueError('No template found for residue %d (%s). %s' % (res.index+1, res.name, _findMatchErrors(self, res)))
1088 else:
1089 data.recordMatchedAtomParameters(res, template, matches)
1090
1091 # Create the System and add atoms
1092
1093 sys = mm.System()
1094 for atom in topology.atoms():
1095 # Look up the atom type name, returning a helpful error message if it cannot be found.
1096 if atom not in data.atomType:
1097 raise Exception("Could not identify atom type for atom '%s'." % str(atom))
1098 typename = data.atomType[atom]
1099
1100 # Look up the type name in the list of registered atom types, returning a helpful error message if it cannot be found.
1101 if typename not in self._atomTypes:
1102 msg = "Could not find typename '%s' for atom '%s' in list of known atom types.\n" % (typename, str(atom))
1103 msg += "Known atom types are: %s" % str(self._atomTypes.keys())
1104 raise Exception(msg)
1105
1106 # Add the particle to the OpenMM system.
1107 mass = self._atomTypes[typename].mass
1108 sys.addParticle(mass)
1109
1110 # Adjust hydrogen masses if requested.
1111
1112 if hydrogenMass is not None:
1113 if not unit.is_quantity(hydrogenMass):
1114 hydrogenMass *= unit.dalton
1115 for atom1, atom2 in topology.bonds():
1116 if atom1.element == elem.hydrogen:
1117 (atom1, atom2) = (atom2, atom1)
1118 if atom2.element == elem.hydrogen and atom1.element not in (elem.hydrogen, None):
1119 transferMass = hydrogenMass-sys.getParticleMass(atom2.index)
1120 sys.setParticleMass(atom2.index, hydrogenMass)
1121 sys.setParticleMass(atom1.index, sys.getParticleMass(atom1.index)-transferMass)
1122
1123 # Set periodic boundary conditions.
1124
1125 boxVectors = topology.getPeriodicBoxVectors()
1126 if boxVectors is not None:
1127 sys.setDefaultPeriodicBoxVectors(boxVectors[0], boxVectors[1], boxVectors[2])
1128 elif nonbondedMethod not in [NoCutoff, CutoffNonPeriodic]:
1129 raise ValueError('Requested periodic boundary conditions for a Topology that does not specify periodic box dimensions')
1130
1131 # Make a list of all unique angles
1132
1133 uniqueAngles = set()
1134 for bond in data.bonds:
1135 for atom in bondedToAtom[bond.atom1]:
1136 if atom != bond.atom2:
1137 if atom < bond.atom2:
1138 uniqueAngles.add((atom, bond.atom1, bond.atom2))
1139 else:
1140 uniqueAngles.add((bond.atom2, bond.atom1, atom))
1141 for atom in bondedToAtom[bond.atom2]:
1142 if atom != bond.atom1:
1143 if atom > bond.atom1:
1144 uniqueAngles.add((bond.atom1, bond.atom2, atom))
1145 else:
1146 uniqueAngles.add((atom, bond.atom2, bond.atom1))
1147 data.angles = sorted(list(uniqueAngles))
1148
1149 # Make a list of all unique proper torsions
1150
1151 uniquePropers = set()
1152 for angle in data.angles:
1153 for atom in bondedToAtom[angle[0]]:
1154 if atom not in angle:
1155 if atom < angle[2]:
1156 uniquePropers.add((atom, angle[0], angle[1], angle[2]))
1157 else:
1158 uniquePropers.add((angle[2], angle[1], angle[0], atom))
1159 for atom in bondedToAtom[angle[2]]:
1160 if atom not in angle:
1161 if atom > angle[0]:
1162 uniquePropers.add((angle[0], angle[1], angle[2], atom))
1163 else:
1164 uniquePropers.add((atom, angle[2], angle[1], angle[0]))
1165 data.propers = sorted(list(uniquePropers))
1166
1167 # Make a list of all unique improper torsions
1168
1169 for atom in range(len(bondedToAtom)):
1170 bondedTo = bondedToAtom[atom]
1171 if len(bondedTo) > 2:
1172 for subset in itertools.combinations(bondedTo, 3):
1173 data.impropers.append((atom, subset[0], subset[1], subset[2]))
1174
1175 # Identify bonds that should be implemented with constraints
1176
1177 if constraints == AllBonds or constraints == HAngles:
1178 for bond in data.bonds:
1179 bond.isConstrained = True
1180 elif constraints == HBonds:
1181 for bond in data.bonds:
1182 atom1 = data.atoms[bond.atom1]
1183 atom2 = data.atoms[bond.atom2]
1184 bond.isConstrained = atom1.name.startswith('H') or atom2.name.startswith('H')
1185 if rigidWater:
1186 for bond in data.bonds:
1187 atom1 = data.atoms[bond.atom1]
1188 atom2 = data.atoms[bond.atom2]
1189 if atom1.residue.name == 'HOH' and atom2.residue.name == 'HOH':
1190 bond.isConstrained = True
1191
1192 # Identify angles that should be implemented with constraints
1193
1194 if constraints == HAngles:
1195 for angle in data.angles:
1196 atom1 = data.atoms[angle[0]]
1197 atom2 = data.atoms[angle[1]]
1198 atom3 = data.atoms[angle[2]]
1199 numH = 0
1200 if atom1.name.startswith('H'):
1201 numH += 1
1202 if atom3.name.startswith('H'):
1203 numH += 1
1204 data.isAngleConstrained.append(numH == 2 or (numH == 1 and atom2.name.startswith('O')))
1205 else:
1206 data.isAngleConstrained = len(data.angles)*[False]
1207 if rigidWater:
1208 for i in range(len(data.angles)):
1209 angle = data.angles[i]
1210 atom1 = data.atoms[angle[0]]
1211 atom2 = data.atoms[angle[1]]
1212 atom3 = data.atoms[angle[2]]
1213 if atom1.residue.name == 'HOH' and atom2.residue.name == 'HOH' and atom3.residue.name == 'HOH':
1214 data.isAngleConstrained[i] = True
1215
1216 # Add virtual sites
1217
1218 for atom in data.virtualSites:
1219 (site, atoms, excludeWith) = data.virtualSites[atom]
1220 index = atom.index
1221 data.excludeAtomWith[excludeWith].append(index)
1222 if site.type == 'average2':
1223 sys.setVirtualSite(index, mm.TwoParticleAverageSite(atoms[0], atoms[1], site.weights[0], site.weights[1]))
1224 elif site.type == 'average3':
1225 sys.setVirtualSite(index, mm.ThreeParticleAverageSite(atoms[0], atoms[1], atoms[2], site.weights[0], site.weights[1], site.weights[2]))
1226 elif site.type == 'outOfPlane':
1227 sys.setVirtualSite(index, mm.OutOfPlaneSite(atoms[0], atoms[1], atoms[2], site.weights[0], site.weights[1], site.weights[2]))
1228 elif site.type == 'localCoords':
1229 sys.setVirtualSite(index, mm.LocalCoordinatesSite(atoms[0], atoms[1], atoms[2],
1230 mm.Vec3(site.originWeights[0], site.originWeights[1], site.originWeights[2]),
1231 mm.Vec3(site.xWeights[0], site.xWeights[1], site.xWeights[2]),
1232 mm.Vec3(site.yWeights[0], site.yWeights[1], site.yWeights[2]),
1233 mm.Vec3(site.localPos[0], site.localPos[1], site.localPos[2])))
1234
1235 # Add forces to the System
1236
1237 for force in self._forces:
1238 force.createForce(sys, data, nonbondedMethod, nonbondedCutoff, args)
1239 if removeCMMotion:
1240 sys.addForce(mm.CMMotionRemover())
1241
1242 # Let force generators do postprocessing
1243
1244 for force in self._forces:
1245 if 'postprocessSystem' in dir(force):
1246 force.postprocessSystem(sys, data, args)
1247
1248 # Execute scripts found in the XML files.
1249
1250 for script in self._scripts:
1251 exec(script, locals())
1252 return sys
1253
1254
1255def _findBondsForExclusions(data, sys):
1256 """Create a list of bonds to use when identifying exclusions."""
1257 bondIndices = []
1258 for bond in data.bonds:
1259 bondIndices.append((bond.atom1, bond.atom2))
1260
1261 # If a virtual site does *not* share exclusions with another atom, add a bond between it and its first parent atom.
1262
1263 for i in range(sys.getNumParticles()):
1264 if sys.isVirtualSite(i):
1265 (site, atoms, excludeWith) = data.virtualSites[data.atoms[i]]
1266 if excludeWith is None:
1267 bondIndices.append((i, site.getParticle(0)))
1268
1269 # Certain particles, such as lone pairs and Drude particles, share exclusions with a parent atom.
1270 # If the parent atom does not interact with an atom, the child particle does not either.
1271
1272 for atom1, atom2 in bondIndices:
1273 for child1 in data.excludeAtomWith[atom1]:
1274 bondIndices.append((child1, atom2))
1275 for child2 in data.excludeAtomWith[atom2]:
1276 bondIndices.append((child1, child2))
1277 for child2 in data.excludeAtomWith[atom2]:
1278 bondIndices.append((atom1, child2))
1279 for atom in data.atoms:
1280 for child in data.excludeAtomWith[atom.index]:
1281 bondIndices.append((child, atom.index))
1282 return bondIndices
1283
1284def _countResidueAtoms(elements):
1285 """Count the number of atoms of each element in a residue."""
1286 counts = {}
1287 for element in elements:
1288 if element in counts:
1289 counts[element] += 1
1290 else:
1291 counts[element] = 1
1292 return counts
1293
1294
1295def _createResidueSignature(elements):
1296 """Create a signature for a residue based on the elements of the atoms it contains."""
1297 counts = _countResidueAtoms(elements)
1298 sig = []
1299 for c in counts:
1300 if c is not None:
1301 sig.append((c, counts[c]))
1302 sig.sort(key=lambda x: -x[0].mass)
1303
1304 # Convert it to a string.
1305
1306 s = ''
1307 for element, count in sig:
1308 s += element.symbol+str(count)
1309 return s
1310
1311def _matchResidue(res, template, bondedToAtom, ignoreExternalBonds=False):
1312 """Determine whether a residue matches a template and return a list of corresponding atoms.
1313
1314 Parameters
1315 ----------
1316 res : Residue
1317 The residue to check
1318 template : _TemplateData
1319 The template to compare it to
1320 bondedToAtom : list
1321 Enumerates which other atoms each atom is bonded to
1322 ignoreExternalBonds : bool
1323 If true, ignore external bonds when matching templates
1324
1325 Returns
1326 -------
1327 list
1328 a list specifying which atom of the template each atom of the residue
1329 corresponds to, or None if it does not match the template
1330 """
1331 atoms = list(res.atoms())
1332 numAtoms = len(atoms)
1333 if numAtoms != len(template.atoms):
1334 return None
1335
1336 # Translate from global to local atom indices, and record the bonds for each atom.
1337
1338 renumberAtoms = {}
1339 for i in range(numAtoms):
1340 renumberAtoms[atoms[i].index] = i
1341 bondedTo = []
1342 externalBonds = []
1343 for atom in atoms:
1344 bonds = [renumberAtoms[x] for x in bondedToAtom[atom.index] if x in renumberAtoms]
1345 bondedTo.append(bonds)
1346 externalBonds.append(0 if ignoreExternalBonds else len([x for x in bondedToAtom[atom.index] if x not in renumberAtoms]))
1347
1348 # For each unique combination of element and number of bonds, make sure the residue and
1349 # template have the same number of atoms.
1350
1351 residueTypeCount = {}
1352 for i, atom in enumerate(atoms):
1353 key = (atom.element, len(bondedTo[i]), externalBonds[i])
1354 if key not in residueTypeCount:
1355 residueTypeCount[key] = 1
1356 residueTypeCount[key] += 1
1357 templateTypeCount = {}
1358 for i, atom in enumerate(template.atoms):
1359 key = (atom.element, len(atom.bondedTo), 0 if ignoreExternalBonds else atom.externalBonds)
1360 if key not in templateTypeCount:
1361 templateTypeCount[key] = 1
1362 templateTypeCount[key] += 1
1363 if residueTypeCount != templateTypeCount:
1364 return None
1365
1366 # Identify template atoms that could potentially be matches for each atom.
1367
1368 candidates = [[] for i in range(numAtoms)]
1369 for i in range(numAtoms):
1370 for j, atom in enumerate(template.atoms):
1371 if (atom.element is not None and atom.element != atoms[i].element) or (atom.element is None and atom.name != atoms[i].name):
1372 continue
1373 if len(atom.bondedTo) != len(bondedTo[i]):
1374 continue
1375 if not ignoreExternalBonds and atom.externalBonds != externalBonds[i]:
1376 continue
1377 candidates[i].append(j)
1378
1379 # Find an optimal ordering for matching atoms. This means 1) start with the one that has the fewest options,
1380 # and 2) follow with ones that are bonded to an already matched atom.
1381
1382 searchOrder = []
1383 atomsToOrder = set(range(numAtoms))
1384 efficientAtomSet = set()
1385 efficientAtomHeap = []
1386 while len(atomsToOrder) > 0:
1387 if len(efficientAtomSet) == 0:
1388 fewestNeighbors = numAtoms+1
1389 for i in atomsToOrder:
1390 if len(candidates[i]) < fewestNeighbors:
1391 nextAtom = i
1392 fewestNeighbors = len(candidates[i])
1393 else:
1394 nextAtom = heappop(efficientAtomHeap)[1]
1395 efficientAtomSet.remove(nextAtom)
1396 searchOrder.append(nextAtom)
1397 atomsToOrder.remove(nextAtom)
1398 for i in bondedTo[nextAtom]:
1399 if i in atomsToOrder:
1400 if i not in efficientAtomSet:
1401 efficientAtomSet.add(i)
1402 heappush(efficientAtomHeap, (len(candidates[i]), i))
1403 inverseSearchOrder = [0]*numAtoms
1404 for i in range(numAtoms):
1405 inverseSearchOrder[searchOrder[i]] = i
1406 bondedTo = [[inverseSearchOrder[bondedTo[i][j]] for j in range(len(bondedTo[i]))] for i in searchOrder]
1407 candidates = [candidates[i] for i in searchOrder]
1408
1409 # Recursively match atoms.
1410
1411 matches = numAtoms*[0]
1412 hasMatch = numAtoms*[False]
1413 if _findAtomMatches(template, bondedTo, matches, hasMatch, candidates, 0):
1414 return [matches[inverseSearchOrder[i]] for i in range(numAtoms)]
1415 return None
1416
1417
1418def _getAtomMatchCandidates(template, bondedTo, matches, candidates, position):
1419 """Get a list of template atoms that are potential matches for the next atom."""
1420 for bonded in bondedTo[position]:
1421 if bonded < position:
1422 # This atom is bonded to another one for which we already have a match, so only consider
1423 # template atoms that *that* one is bonded to.
1424 return template.atoms[matches[bonded]].bondedTo
1425 return candidates[position]
1426
1427
1428def _findAtomMatches(template, bondedTo, matches, hasMatch, candidates, position):
1429 """This is called recursively from inside _matchResidue() to identify matching atoms."""
1430 if position == len(matches):
1431 return True
1432 for i in _getAtomMatchCandidates(template, bondedTo, matches, candidates, position):
1433 atom = template.atoms[i]
1434 if not hasMatch[i] and i in candidates[position]:
1435 # See if the bonds for this identification are consistent
1436
1437 allBondsMatch = all((bonded > position or matches[bonded] in atom.bondedTo for bonded in bondedTo[position]))
1438 if allBondsMatch:
1439 # This is a possible match, so try matching the rest of the residue.
1440
1441 matches[position] = i
1442 hasMatch[i] = True
1443 if _findAtomMatches(template, bondedTo, matches, hasMatch, candidates, position+1):
1444 return True
1445 hasMatch[i] = False
1446 return False
1447
1448
1449def _applyPatchesToMatchResidues(forcefield, data, residues, bondedToAtom, ignoreExternalBonds):
1450 """Try to apply patches to find matches for residues."""
1451 # Start by creating all templates than can be created by applying a combination of one-residue patches
1452 # to a single template. The number of these is usually not too large, and they often cover a large fraction
1453 # of residues.
1454
1455 patchedTemplateSignatures = {}
1456 patchedTemplates = {}
1457 for name, template in forcefield._templates.items():
1458 if name in forcefield._templatePatches:
1459 patches = [forcefield._patches[patchName] for patchName, patchResidueIndex in forcefield._templatePatches[name] if forcefield._patches[patchName].numResidues == 1]
1460 if len(patches) > 0:
1461 newTemplates = []
1462 patchedTemplates[name] = newTemplates
1463 _generatePatchedSingleResidueTemplates(template, patches, 0, newTemplates)
1464 for patchedTemplate in newTemplates:
1465 signature = _createResidueSignature([atom.element for atom in patchedTemplate.atoms])
1466 if signature in patchedTemplateSignatures:
1467 patchedTemplateSignatures[signature].append(patchedTemplate)
1468 else:
1469 patchedTemplateSignatures[signature] = [patchedTemplate]
1470
1471 # Now see if any of those templates matches any of the residues.
1472
1473 unmatchedResidues = []
1474 for res in residues:
1475 [template, matches] = forcefield._getResidueTemplateMatches(res, bondedToAtom, patchedTemplateSignatures, ignoreExternalBonds)
1476 if matches is None:
1477 unmatchedResidues.append(res)
1478 else:
1479 data.recordMatchedAtomParameters(res, template, matches)
1480 if len(unmatchedResidues) == 0:
1481 return []
1482
1483 # We need to consider multi-residue patches. This can easily lead to a combinatorial explosion, so we make a simplifying
1484 # assumption: that no residue is affected by more than one multi-residue patch (in addition to any number of single-residue
1485 # patches). Record all multi-residue patches, and the templates they can be applied to.
1486
1487 patches = {}
1488 maxPatchSize = 0
1489 for patch in forcefield._patches.values():
1490 if patch.numResidues > 1:
1491 patches[patch.name] = [[] for i in range(patch.numResidues)]
1492 maxPatchSize = max(maxPatchSize, patch.numResidues)
1493 if maxPatchSize == 0:
1494 return unmatchedResidues # There aren't any multi-residue patches
1495 for templateName in forcefield._templatePatches:
1496 for patchName, patchResidueIndex in forcefield._templatePatches[templateName]:
1497 if patchName in patches:
1498 # The patch should accept this template, *and* all patched versions of it generated above.
1499 patches[patchName][patchResidueIndex].append(forcefield._templates[templateName])
1500 if templateName in patchedTemplates:
1501 patches[patchName][patchResidueIndex] += patchedTemplates[templateName]
1502
1503 # Record which unmatched residues are bonded to each other.
1504
1505 bonds = set()
1506 topology = residues[0].chain.topology
1507 for atom1, atom2 in topology.bonds():
1508 if atom1.residue != atom2.residue:
1509 res1 = atom1.residue
1510 res2 = atom2.residue
1511 if res1 in unmatchedResidues and res2 in unmatchedResidues:
1512 bond = tuple(sorted((res1, res2), key=lambda x: x.index))
1513 if bond not in bonds:
1514 bonds.add(bond)
1515
1516 # Identify clusters of unmatched residues that are all bonded to each other. These are the ones we'll
1517 # try to apply multi-residue patches to.
1518
1519 clusterSize = 2
1520 clusters = bonds
1521 while clusterSize <= maxPatchSize:
1522 # Try to apply patches to clusters of this size.
1523
1524 for patchName in patches:
1525 patch = forcefield._patches[patchName]
1526 if patch.numResidues == clusterSize:
1527 matchedClusters = _matchToMultiResiduePatchedTemplates(data, clusters, patch, patches[patchName], bondedToAtom, ignoreExternalBonds)
1528 for cluster in matchedClusters:
1529 for residue in cluster:
1530 unmatchedResidues.remove(residue)
1531 bonds = set(bond for bond in bonds if bond[0] in unmatchedResidues and bond[1] in unmatchedResidues)
1532
1533 # Now extend the clusters to find ones of the next size up.
1534
1535 largerClusters = set()
1536 for cluster in clusters:
1537 for bond in bonds:
1538 if bond[0] in cluster and bond[1] not in cluster:
1539 newCluster = tuple(sorted(cluster+(bond[1],), key=lambda x: x.index))
1540 largerClusters.add(newCluster)
1541 elif bond[1] in cluster and bond[0] not in cluster:
1542 newCluster = tuple(sorted(cluster+(bond[0],), key=lambda x: x.index))
1543 largerClusters.add(newCluster)
1544 if len(largerClusters) == 0:
1545 # There are no clusters of this size or larger
1546 break
1547 clusters = largerClusters
1548 clusterSize += 1
1549
1550 return unmatchedResidues
1551
1552
1553def _generatePatchedSingleResidueTemplates(template, patches, index, newTemplates):
1554 """Apply all possible combinations of a set of single-residue patches to a template."""
1555 try:
1556 patchedTemplate = patches[index].createPatchedTemplates([template])[0]
1557 newTemplates.append(patchedTemplate)
1558 except:
1559 # This probably means the patch is inconsistent with another one that has already been applied,
1560 # so just ignore it.
1561 patchedTemplate = None
1562
1563 # Call this function recursively to generate combinations of patches.
1564
1565 if index+1 < len(patches):
1566 _generatePatchedSingleResidueTemplates(template, patches, index+1, newTemplates)
1567 if patchedTemplate is not None:
1568 _generatePatchedSingleResidueTemplates(patchedTemplate, patches, index+1, newTemplates)
1569
1570
1571def _matchToMultiResiduePatchedTemplates(data, clusters, patch, residueTemplates, bondedToAtom, ignoreExternalBonds):
1572 """Apply a multi-residue patch to templates, then try to match them against clusters of residues."""
1573 matchedClusters = []
1574 selectedTemplates = [None]*patch.numResidues
1575 _applyMultiResiduePatch(data, clusters, patch, residueTemplates, selectedTemplates, 0, matchedClusters, bondedToAtom, ignoreExternalBonds)
1576 return matchedClusters
1577
1578
1579def _applyMultiResiduePatch(data, clusters, patch, candidateTemplates, selectedTemplates, index, matchedClusters, bondedToAtom, ignoreExternalBonds):
1580 """This is called recursively to apply a multi-residue patch to all possible combinations of templates."""
1581
1582 if index < patch.numResidues:
1583 for template in candidateTemplates[index]:
1584 selectedTemplates[index] = template
1585 _applyMultiResiduePatch(data, clusters, patch, candidateTemplates, selectedTemplates, index+1, matchedClusters, bondedToAtom, ignoreExternalBonds)
1586 else:
1587 # We're at the deepest level of the recursion. We've selected a template for each residue, so apply the patch,
1588 # then try to match it against clusters.
1589
1590 try:
1591 patchedTemplates = patch.createPatchedTemplates(selectedTemplates)
1592 except:
1593 # This probably means the patch is inconsistent with another one that has already been applied,
1594 # so just ignore it.
1595 raise
1596 return
1597 newlyMatchedClusters = []
1598 for cluster in clusters:
1599 for residues in itertools.permutations(cluster):
1600 residueMatches = []
1601 for residue, template in zip(residues, patchedTemplates):
1602 matches = _matchResidue(residue, template, bondedToAtom, ignoreExternalBonds)
1603 if matches is None:
1604 residueMatches = None
1605 break
1606 else:
1607 residueMatches.append(matches)
1608 if residueMatches is not None:
1609 # We successfully matched the template to the residues. Record the parameters.
1610
1611 for i in range(patch.numResidues):
1612 data.recordMatchedAtomParameters(residues[i], patchedTemplates[i], residueMatches[i])
1613 newlyMatchedClusters.append(cluster)
1614 break
1615
1616 # Record which clusters were successfully matched.
1617
1618 matchedClusters += newlyMatchedClusters
1619 for cluster in newlyMatchedClusters:
1620 clusters.remove(cluster)
1621
1622
1623def _findMatchErrors(forcefield, res):
1624 """Try to guess why a residue failed to match any template and return an error message."""
1625 residueCounts = _countResidueAtoms([atom.element for atom in res.atoms()])
1626 numResidueAtoms = sum(residueCounts.values())
1627 numResidueHeavyAtoms = sum(residueCounts[element] for element in residueCounts if element not in (None, elem.hydrogen))
1628
1629 # Loop over templates and see how closely each one might match.
1630
1631 bestMatchName = None
1632 numBestMatchAtoms = 3*numResidueAtoms
1633 numBestMatchHeavyAtoms = 2*numResidueHeavyAtoms
1634 for templateName in forcefield._templates:
1635 template = forcefield._templates[templateName]
1636 templateCounts = _countResidueAtoms([atom.element for atom in template.atoms])
1637
1638 # Does the residue have any atoms that clearly aren't in the template?
1639
1640 if any(element not in templateCounts or templateCounts[element] < residueCounts[element] for element in residueCounts):
1641 continue
1642
1643 # If there are too many missing atoms, discard this template.
1644
1645 numTemplateAtoms = sum(templateCounts.values())
1646 numTemplateHeavyAtoms = sum(templateCounts[element] for element in templateCounts if element not in (None, elem.hydrogen))
1647 if numTemplateAtoms > numBestMatchAtoms:
1648 continue
1649 if numTemplateHeavyAtoms > numBestMatchHeavyAtoms:
1650 continue
1651
1652 # If this template has the same number of missing atoms as our previous best one, look at the name
1653 # to decide which one to use.
1654
1655 if numTemplateAtoms == numBestMatchAtoms:
1656 if bestMatchName == res.name or res.name not in templateName:
1657 continue
1658
1659 # Accept this as our new best match.
1660
1661 bestMatchName = templateName
1662 numBestMatchAtoms = numTemplateAtoms
1663 numBestMatchHeavyAtoms = numTemplateHeavyAtoms
1664 numBestMatchExtraParticles = len([atom for atom in template.atoms if atom.element is None])
1665
1666 # Return an appropriate error message.
1667
1668 if numBestMatchAtoms == numResidueAtoms:
1669 chainResidues = list(res.chain.residues())
1670 if len(chainResidues) > 1 and (res == chainResidues[0] or res == chainResidues[-1]):
1671 return 'The set of atoms matches %s, but the bonds are different. Perhaps the chain is missing a terminal group?' % bestMatchName
1672 return 'The set of atoms matches %s, but the bonds are different.' % bestMatchName
1673 if bestMatchName is not None:
1674 if numBestMatchHeavyAtoms == numResidueHeavyAtoms:
1675 numResidueExtraParticles = len([atom for atom in res.atoms() if atom.element is None])
1676 if numResidueExtraParticles == 0 and numBestMatchExtraParticles == 0:
1677 return 'The set of atoms is similar to %s, but it is missing %d hydrogen atoms.' % (bestMatchName, numBestMatchAtoms-numResidueAtoms)
1678 if numBestMatchExtraParticles-numResidueExtraParticles == numBestMatchAtoms-numResidueAtoms:
1679 return 'The set of atoms is similar to %s, but it is missing %d extra particles. You can add them with Modeller.addExtraParticles().' % (bestMatchName, numBestMatchAtoms-numResidueAtoms)
1680 return 'The set of atoms is similar to %s, but it is missing %d atoms.' % (bestMatchName, numBestMatchAtoms-numResidueAtoms)
1681 return 'This might mean your input topology is missing some atoms or bonds, or possibly that you are using the wrong force field.'
1682
1683def _createResidueTemplate(residue):
1684 """Create a _TemplateData template from a Residue object.
1685
1686 Parameters
1687 ----------
1688 residue : Residue
1689 The Residue from which the template is to be constructed.
1690
1691 Returns
1692 -------
1693 template : _TemplateData
1694 The residue template, with atom types set to None.
1695
1696 This method may be useful in creating new residue templates for residues without templates defined by the ForceField.
1697
1698 """
1699 template = ForceField._TemplateData(residue.name)
1700 for atom in residue.atoms():
1701 template.atoms.append(ForceField._TemplateAtomData(atom.name, None, atom.element))
1702 for (atom1,atom2) in residue.internal_bonds():
1703 template.addBondByName(atom1.name, atom2.name)
1704 residue_atoms = [ atom for atom in residue.atoms() ]
1705 for (atom1,atom2) in residue.external_bonds():
1706 if atom1 in residue_atoms:
1707 template.addExternalBondByName(atom1.name)
1708 elif atom2 in residue_atoms:
1709 template.addExternalBondByName(atom2.name)
1710 return template
1711
1712# The following classes are generators that know how to create Force subclasses and add them to a System that is being
1713# created. Each generator class must define two methods: 1) a static method that takes an etree Element and a ForceField,
1714# and returns the corresponding generator object; 2) a createForce() method that constructs the Force object and adds it
1715# to the System. The static method should be added to the parsers map.
1716
1717## @private
1718class HarmonicBondGenerator(object):
1719 """A HarmonicBondGenerator constructs a HarmonicBondForce."""
1720
1721 def __init__(self, forcefield):
1722 self.ff = forcefield
1723 self.bondsForAtomType = defaultdict(set)
1724 self.types1 = []
1725 self.types2 = []
1726 self.length = []
1727 self.k = []
1728
1729 def registerBond(self, parameters):
1730 types = self.ff._findAtomTypes(parameters, 2)
1731 if None not in types:
1732 index = len(self.types1)
1733 self.types1.append(types[0])
1734 self.types2.append(types[1])
1735 for t in types[0]:
1736 self.bondsForAtomType[t].add(index)
1737 for t in types[1]:
1738 self.bondsForAtomType[t].add(index)
1739 self.length.append(_convertParameterToNumber(parameters['length']))
1740 self.k.append(_convertParameterToNumber(parameters['k']))
1741
1742 @staticmethod
1743 def parseElement(element, ff):
1744 existing = [f for f in ff._forces if isinstance(f, HarmonicBondGenerator)]
1745 if len(existing) == 0:
1746 generator = HarmonicBondGenerator(ff)
1747 ff.registerGenerator(generator)
1748 else:
1749 generator = existing[0]
1750 for bond in element.findall('Bond'):
1751 generator.registerBond(bond.attrib)
1752
1753 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
1754 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
1755 existing = [f for f in existing if type(f) == mm.HarmonicBondForce]
1756 if len(existing) == 0:
1757 force = mm.HarmonicBondForce()
1758 sys.addForce(force)
1759 else:
1760 force = existing[0]
1761 for bond in data.bonds:
1762 type1 = data.atomType[data.atoms[bond.atom1]]
1763 type2 = data.atomType[data.atoms[bond.atom2]]
1764 for i in self.bondsForAtomType[type1]:
1765 types1 = self.types1[i]
1766 types2 = self.types2[i]
1767 if (type1 in types1 and type2 in types2) or (type1 in types2 and type2 in types1):
1768 bond.length = self.length[i]
1769 if bond.isConstrained:
1770 data.addConstraint(sys, bond.atom1, bond.atom2, self.length[i])
1771 elif self.k[i] != 0:
1772 force.addBond(bond.atom1, bond.atom2, self.length[i], self.k[i])
1773 break
1774
1775parsers["HarmonicBondForce"] = HarmonicBondGenerator.parseElement
1776
1777
1778## @private
1779class HarmonicAngleGenerator(object):
1780 """A HarmonicAngleGenerator constructs a HarmonicAngleForce."""
1781
1782 def __init__(self, forcefield):
1783 self.ff = forcefield
1784 self.anglesForAtom2Type = defaultdict(list)
1785 self.types1 = []
1786 self.types2 = []
1787 self.types3 = []
1788 self.angle = []
1789 self.k = []
1790
1791 def registerAngle(self, parameters):
1792 types = self.ff._findAtomTypes(parameters, 3)
1793 if None not in types:
1794 index = len(self.types1)
1795 self.types1.append(types[0])
1796 self.types2.append(types[1])
1797 self.types3.append(types[2])
1798 for t in types[1]:
1799 self.anglesForAtom2Type[t].append(index)
1800 self.angle.append(_convertParameterToNumber(parameters['angle']))
1801 self.k.append(_convertParameterToNumber(parameters['k']))
1802
1803 @staticmethod
1804 def parseElement(element, ff):
1805 existing = [f for f in ff._forces if isinstance(f, HarmonicAngleGenerator)]
1806 if len(existing) == 0:
1807 generator = HarmonicAngleGenerator(ff)
1808 ff.registerGenerator(generator)
1809 else:
1810 generator = existing[0]
1811 for angle in element.findall('Angle'):
1812 generator.registerAngle(angle.attrib)
1813
1814 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
1815 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
1816 existing = [f for f in existing if type(f) == mm.HarmonicAngleForce]
1817 if len(existing) == 0:
1818 force = mm.HarmonicAngleForce()
1819 sys.addForce(force)
1820 else:
1821 force = existing[0]
1822 for (angle, isConstrained) in zip(data.angles, data.isAngleConstrained):
1823 type1 = data.atomType[data.atoms[angle[0]]]
1824 type2 = data.atomType[data.atoms[angle[1]]]
1825 type3 = data.atomType[data.atoms[angle[2]]]
1826 for i in self.anglesForAtom2Type[type2]:
1827 types1 = self.types1[i]
1828 types2 = self.types2[i]
1829 types3 = self.types3[i]
1830 if (type1 in types1 and type2 in types2 and type3 in types3) or (type1 in types3 and type2 in types2 and type3 in types1):
1831 if isConstrained:
1832 # Find the two bonds that make this angle.
1833
1834 bond1 = None
1835 bond2 = None
1836 for bond in data.atomBonds[angle[1]]:
1837 atom1 = data.bonds[bond].atom1
1838 atom2 = data.bonds[bond].atom2
1839 if atom1 == angle[0] or atom2 == angle[0]:
1840 bond1 = bond
1841 elif atom1 == angle[2] or atom2 == angle[2]:
1842 bond2 = bond
1843
1844 # Compute the distance between atoms and add a constraint
1845
1846 if bond1 is not None and bond2 is not None:
1847 l1 = data.bonds[bond1].length
1848 l2 = data.bonds[bond2].length
1849 if l1 is not None and l2 is not None:
1850 length = sqrt(l1*l1 + l2*l2 - 2*l1*l2*cos(self.angle[i]))
1851 data.addConstraint(sys, angle[0], angle[2], length)
1852 elif self.k[i] != 0:
1853 force.addAngle(angle[0], angle[1], angle[2], self.angle[i], self.k[i])
1854 break
1855
1856parsers["HarmonicAngleForce"] = HarmonicAngleGenerator.parseElement
1857
1858
1859## @private
1860class PeriodicTorsion(object):
1861 """A PeriodicTorsion records the information for a periodic torsion definition."""
1862
1863 def __init__(self, types):
1864 self.types1 = types[0]
1865 self.types2 = types[1]
1866 self.types3 = types[2]
1867 self.types4 = types[3]
1868 self.periodicity = []
1869 self.phase = []
1870 self.k = []
1871
1872## @private
1873class PeriodicTorsionGenerator(object):
1874 """A PeriodicTorsionGenerator constructs a PeriodicTorsionForce."""
1875
1876 def __init__(self, forcefield):
1877 self.ff = forcefield
1878 self.proper = []
1879 self.improper = []
1880
1881 def registerProperTorsion(self, parameters):
1882 torsion = self.ff._parseTorsion(parameters)
1883 if torsion is not None:
1884 self.proper.append(torsion)
1885
1886 def registerImproperTorsion(self, parameters):
1887 torsion = self.ff._parseTorsion(parameters)
1888 if torsion is not None:
1889 self.improper.append(torsion)
1890
1891 @staticmethod
1892 def parseElement(element, ff):
1893 existing = [f for f in ff._forces if isinstance(f, PeriodicTorsionGenerator)]
1894 if len(existing) == 0:
1895 generator = PeriodicTorsionGenerator(ff)
1896 ff.registerGenerator(generator)
1897 else:
1898 generator = existing[0]
1899 for torsion in element.findall('Proper'):
1900 generator.registerProperTorsion(torsion.attrib)
1901 for torsion in element.findall('Improper'):
1902 generator.registerImproperTorsion(torsion.attrib)
1903
1904 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
1905 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
1906 existing = [f for f in existing if type(f) == mm.PeriodicTorsionForce]
1907 if len(existing) == 0:
1908 force = mm.PeriodicTorsionForce()
1909 sys.addForce(force)
1910 else:
1911 force = existing[0]
1912 wildcard = self.ff._atomClasses['']
1913 for torsion in data.propers:
1914 type1 = data.atomType[data.atoms[torsion[0]]]
1915 type2 = data.atomType[data.atoms[torsion[1]]]
1916 type3 = data.atomType[data.atoms[torsion[2]]]
1917 type4 = data.atomType[data.atoms[torsion[3]]]
1918 match = None
1919 for tordef in self.proper:
1920 types1 = tordef.types1
1921 types2 = tordef.types2
1922 types3 = tordef.types3
1923 types4 = tordef.types4
1924 if (type2 in types2 and type3 in types3 and type4 in types4 and type1 in types1) or (type2 in types3 and type3 in types2 and type4 in types1 and type1 in types4):
1925 hasWildcard = (wildcard in (types1, types2, types3, types4))
1926 if match is None or not hasWildcard: # Prefer specific definitions over ones with wildcards
1927 match = tordef
1928 if not hasWildcard:
1929 break
1930 if match is not None:
1931 for i in range(len(match.phase)):
1932 if match.k[i] != 0:
1933 force.addTorsion(torsion[0], torsion[1], torsion[2], torsion[3], match.periodicity[i], match.phase[i], match.k[i])
1934 for torsion in data.impropers:
1935 type1 = data.atomType[data.atoms[torsion[0]]]
1936 type2 = data.atomType[data.atoms[torsion[1]]]
1937 type3 = data.atomType[data.atoms[torsion[2]]]
1938 type4 = data.atomType[data.atoms[torsion[3]]]
1939 match = None
1940 for tordef in self.improper:
1941 types1 = tordef.types1
1942 types2 = tordef.types2
1943 types3 = tordef.types3
1944 types4 = tordef.types4
1945 hasWildcard = (wildcard in (types1, types2, types3, types4))
1946 if match is not None and hasWildcard:
1947 # Prefer specific definitions over ones with wildcards
1948 continue
1949 if type1 in types1:
1950 for (t2, t3, t4) in itertools.permutations(((type2, 1), (type3, 2), (type4, 3))):
1951 if t2[0] in types2 and t3[0] in types3 and t4[0] in types4:
1952 # Workaround to be more consistent with AMBER. It uses wildcards to define most of its
1953 # impropers, which leaves the ordering ambiguous. It then follows some bizarre rules
1954 # to pick the order.
1955 a1 = torsion[t2[1]]
1956 a2 = torsion[t3[1]]
1957 e1 = data.atoms[a1].element
1958 e2 = data.atoms[a2].element
1959 if e1 == e2 and a1 > a2:
1960 (a1, a2) = (a2, a1)
1961 elif e1 != elem.carbon and (e2 == elem.carbon or e1.mass < e2.mass):
1962 (a1, a2) = (a2, a1)
1963 match = (a1, a2, torsion[0], torsion[t4[1]], tordef)
1964 break
1965 if match is not None:
1966 (a1, a2, a3, a4, tordef) = match
1967 for i in range(len(tordef.phase)):
1968 if tordef.k[i] != 0:
1969 force.addTorsion(a1, a2, a3, a4, tordef.periodicity[i], tordef.phase[i], tordef.k[i])
1970
1971parsers["PeriodicTorsionForce"] = PeriodicTorsionGenerator.parseElement
1972
1973
1974## @private
1975class RBTorsion(object):
1976 """An RBTorsion records the information for a Ryckaert-Bellemans torsion definition."""
1977
1978 def __init__(self, types, c):
1979 self.types1 = types[0]
1980 self.types2 = types[1]
1981 self.types3 = types[2]
1982 self.types4 = types[3]
1983 self.c = c
1984
1985## @private
1986class RBTorsionGenerator(object):
1987 """An RBTorsionGenerator constructs an RBTorsionForce."""
1988
1989 def __init__(self, forcefield):
1990 self.ff = forcefield
1991 self.proper = []
1992 self.improper = []
1993
1994 @staticmethod
1995 def parseElement(element, ff):
1996 existing = [f for f in ff._forces if isinstance(f, RBTorsionGenerator)]
1997 if len(existing) == 0:
1998 generator = RBTorsionGenerator(ff)
1999 ff.registerGenerator(generator)
2000 else:
2001 generator = existing[0]
2002 for torsion in element.findall('Proper'):
2003 types = ff._findAtomTypes(torsion.attrib, 4)
2004 if None not in types:
2005 generator.proper.append(RBTorsion(types, [float(torsion.attrib['c'+str(i)]) for i in range(6)]))
2006 for torsion in element.findall('Improper'):
2007 types = ff._findAtomTypes(torsion.attrib, 4)
2008 if None not in types:
2009 generator.improper.append(RBTorsion(types, [float(torsion.attrib['c'+str(i)]) for i in range(6)]))
2010
2011 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2012 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
2013 existing = [f for f in existing if type(f) == mm.RBTorsionForce]
2014 if len(existing) == 0:
2015 force = mm.RBTorsionForce()
2016 sys.addForce(force)
2017 else:
2018 force = existing[0]
2019 wildcard = self.ff._atomClasses['']
2020 for torsion in data.propers:
2021 type1 = data.atomType[data.atoms[torsion[0]]]
2022 type2 = data.atomType[data.atoms[torsion[1]]]
2023 type3 = data.atomType[data.atoms[torsion[2]]]
2024 type4 = data.atomType[data.atoms[torsion[3]]]
2025 match = None
2026 for tordef in self.proper:
2027 types1 = tordef.types1
2028 types2 = tordef.types2
2029 types3 = tordef.types3
2030 types4 = tordef.types4
2031 if (type2 in types2 and type3 in types3 and type4 in types4 and type1 in types1) or (type2 in types3 and type3 in types2 and type4 in types1 and type1 in types4):
2032 hasWildcard = (wildcard in (types1, types2, types3, types4))
2033 if match is None or not hasWildcard: # Prefer specific definitions over ones with wildcards
2034 match = tordef
2035 if not hasWildcard:
2036 break
2037 if match is not None:
2038 force.addTorsion(torsion[0], torsion[1], torsion[2], torsion[3], match.c[0], match.c[1], match.c[2], match.c[3], match.c[4], match.c[5])
2039 for torsion in data.impropers:
2040 type1 = data.atomType[data.atoms[torsion[0]]]
2041 type2 = data.atomType[data.atoms[torsion[1]]]
2042 type3 = data.atomType[data.atoms[torsion[2]]]
2043 type4 = data.atomType[data.atoms[torsion[3]]]
2044 match = None
2045 for tordef in self.improper:
2046 types1 = tordef.types1
2047 types2 = tordef.types2
2048 types3 = tordef.types3
2049 types4 = tordef.types4
2050 hasWildcard = (wildcard in (types1, types2, types3, types4))
2051 if match is not None and hasWildcard:
2052 # Prefer specific definitions over ones with wildcards
2053 continue
2054 if type1 in types1:
2055 for (t2, t3, t4) in itertools.permutations(((type2, 1), (type3, 2), (type4, 3))):
2056 if t2[0] in types2 and t3[0] in types3 and t4[0] in types4:
2057 if hasWildcard:
2058 # Workaround to be more consistent with AMBER. It uses wildcards to define most of its
2059 # impropers, which leaves the ordering ambiguous. It then follows some bizarre rules
2060 # to pick the order.
2061 a1 = torsion[t2[1]]
2062 a2 = torsion[t3[1]]
2063 e1 = data.atoms[a1].element
2064 e2 = data.atoms[a2].element
2065 if e1 == e2 and a1 > a2:
2066 (a1, a2) = (a2, a1)
2067 elif e1 != elem.carbon and (e2 == elem.carbon or e1.mass < e2.mass):
2068 (a1, a2) = (a2, a1)
2069 match = (a1, a2, torsion[0], torsion[t4[1]], tordef)
2070 else:
2071 # There are no wildcards, so the order is unambiguous.
2072 match = (torsion[0], torsion[t2[1]], torsion[t3[1]], torsion[t4[1]], tordef)
2073 break
2074 if match is not None:
2075 (a1, a2, a3, a4, tordef) = match
2076 force.addTorsion(a1, a2, a3, a4, tordef.c[0], tordef.c[1], tordef.c[2], tordef.c[3], tordef.c[4], tordef.c[5])
2077
2078parsers["RBTorsionForce"] = RBTorsionGenerator.parseElement
2079
2080
2081## @private
2082class CMAPTorsion(object):
2083 """A CMAPTorsion records the information for a CMAP torsion definition."""
2084
2085 def __init__(self, types, map):
2086 self.types1 = types[0]
2087 self.types2 = types[1]
2088 self.types3 = types[2]
2089 self.types4 = types[3]
2090 self.types5 = types[4]
2091 self.map = map
2092
2093## @private
2094class CMAPTorsionGenerator(object):
2095 """A CMAPTorsionGenerator constructs a CMAPTorsionForce."""
2096
2097 def __init__(self, forcefield):
2098 self.ff = forcefield
2099 self.torsions = []
2100 self.maps = []
2101
2102 @staticmethod
2103 def parseElement(element, ff):
2104 existing = [f for f in ff._forces if isinstance(f, CMAPTorsionGenerator)]
2105 if len(existing) == 0:
2106 generator = CMAPTorsionGenerator(ff)
2107 ff.registerGenerator(generator)
2108 else:
2109 generator = existing[0]
2110 for map in element.findall('Map'):
2111 values = [float(x) for x in map.text.split()]
2112 size = sqrt(len(values))
2113 if size*size != len(values):
2114 raise ValueError('CMAP must have the same number of elements along each dimension')
2115 generator.maps.append(values)
2116 for torsion in element.findall('Torsion'):
2117 types = ff._findAtomTypes(torsion.attrib, 5)
2118 if None not in types:
2119 generator.torsions.append(CMAPTorsion(types, int(torsion.attrib['map'])))
2120
2121 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2122 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
2123 existing = [f for f in existing if type(f) == mm.CMAPTorsionForce]
2124 if len(existing) == 0:
2125 force = mm.CMAPTorsionForce()
2126 sys.addForce(force)
2127 else:
2128 force = existing[0]
2129 for map in self.maps:
2130 force.addMap(int(sqrt(len(map))), map)
2131
2132 # Find all chains of length 5
2133
2134 uniqueTorsions = set()
2135 for torsion in data.propers:
2136 for bond in (data.bonds[x] for x in data.atomBonds[torsion[0]]):
2137 if bond.atom1 == torsion[0]:
2138 atom = bond.atom2
2139 else:
2140 atom = bond.atom1
2141 if atom != torsion[1]:
2142 uniqueTorsions.add((atom, torsion[0], torsion[1], torsion[2], torsion[3]))
2143 for bond in (data.bonds[x] for x in data.atomBonds[torsion[3]]):
2144 if bond.atom1 == torsion[3]:
2145 atom = bond.atom2
2146 else:
2147 atom = bond.atom1
2148 if atom != torsion[2]:
2149 uniqueTorsions.add((torsion[0], torsion[1], torsion[2], torsion[3], atom))
2150 torsions = sorted(list(uniqueTorsions))
2151 wildcard = self.ff._atomClasses['']
2152 for torsion in torsions:
2153 type1 = data.atomType[data.atoms[torsion[0]]]
2154 type2 = data.atomType[data.atoms[torsion[1]]]
2155 type3 = data.atomType[data.atoms[torsion[2]]]
2156 type4 = data.atomType[data.atoms[torsion[3]]]
2157 type5 = data.atomType[data.atoms[torsion[4]]]
2158 match = None
2159 for tordef in self.torsions:
2160 types1 = tordef.types1
2161 types2 = tordef.types2
2162 types3 = tordef.types3
2163 types4 = tordef.types4
2164 types5 = tordef.types5
2165 if (type1 in types1 and type2 in types2 and type3 in types3 and type4 in types4 and type5 in types5) or (type1 in types5 and type2 in types4 and type3 in types3 and type4 in types2 and type5 in types1):
2166 hasWildcard = (wildcard in (types1, types2, types3, types4, types5))
2167 if match is None or not hasWildcard: # Prefer specific definitions over ones with wildcards
2168 match = tordef
2169 if not hasWildcard:
2170 break
2171 if match is not None:
2172 force.addTorsion(match.map, torsion[0], torsion[1], torsion[2], torsion[3], torsion[1], torsion[2], torsion[3], torsion[4])
2173
2174parsers["CMAPTorsionForce"] = CMAPTorsionGenerator.parseElement
2175
2176
2177## @private
2178class NonbondedGenerator(object):
2179 """A NonbondedGenerator constructs a NonbondedForce."""
2180
2181 SCALETOL = 1e-5
2182
2183 def __init__(self, forcefield, coulomb14scale, lj14scale):
2184 self.ff = forcefield
2185 self.coulomb14scale = coulomb14scale
2186 self.lj14scale = lj14scale
2187 self.params = ForceField._AtomTypeParameters(forcefield, 'NonbondedForce', 'Atom', ('charge', 'sigma', 'epsilon'))
2188
2189 def registerAtom(self, parameters):
2190 self.params.registerAtom(parameters)
2191
2192 @staticmethod
2193 def parseElement(element, ff):
2194 existing = [f for f in ff._forces if isinstance(f, NonbondedGenerator)]
2195 if len(existing) == 0:
2196 generator = NonbondedGenerator(ff, float(element.attrib['coulomb14scale']), float(element.attrib['lj14scale']))
2197 ff.registerGenerator(generator)
2198 else:
2199 # Multiple <NonbondedForce> tags were found, probably in different files. Simply add more types to the existing one.
2200 generator = existing[0]
2201 if abs(generator.coulomb14scale - float(element.attrib['coulomb14scale'])) > NonbondedGenerator.SCALETOL or \
2202 abs(generator.lj14scale - float(element.attrib['lj14scale'])) > NonbondedGenerator.SCALETOL:
2203 raise ValueError('Found multiple NonbondedForce tags with different 1-4 scales')
2204 generator.params.parseDefinitions(element)
2205
2206 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2207 methodMap = {NoCutoff:mm.NonbondedForce.NoCutoff,
2208 CutoffNonPeriodic:mm.NonbondedForce.CutoffNonPeriodic,
2209 CutoffPeriodic:mm.NonbondedForce.CutoffPeriodic,
2210 Ewald:mm.NonbondedForce.Ewald,
2211 PME:mm.NonbondedForce.PME}
2212 if nonbondedMethod not in methodMap:
2213 raise ValueError('Illegal nonbonded method for NonbondedForce')
2214 force = mm.NonbondedForce()
2215 for atom in data.atoms:
2216 values = self.params.getAtomParameters(atom, data)
2217 force.addParticle(values[0], values[1], values[2])
2218 force.setNonbondedMethod(methodMap[nonbondedMethod])
2219 force.setCutoffDistance(nonbondedCutoff)
2220 if 'ewaldErrorTolerance' in args:
2221 force.setEwaldErrorTolerance(args['ewaldErrorTolerance'])
2222 if 'useDispersionCorrection' in args:
2223 force.setUseDispersionCorrection(bool(args['useDispersionCorrection']))
2224 sys.addForce(force)
2225
2226 def postprocessSystem(self, sys, data, args):
2227 # Create the exceptions.
2228
2229 bondIndices = _findBondsForExclusions(data, sys)
2230 nonbonded = [f for f in sys.getForces() if isinstance(f, mm.NonbondedForce)][0]
2231 nonbonded.createExceptionsFromBonds(bondIndices, self.coulomb14scale, self.lj14scale)
2232
2233parsers["NonbondedForce"] = NonbondedGenerator.parseElement
2234
2235
2236## @private
2237class LennardJonesGenerator(object):
2238 """A NBFix generator to construct the L-J force with NBFIX implemented as a lookup table"""
2239
2240 def __init__(self, forcefield, lj14scale):
2241 self.ff = forcefield
2242 self.nbfixTypes = {}
2243 self.lj14scale = lj14scale
2244 self.ljTypes = ForceField._AtomTypeParameters(forcefield, 'LennardJonesForce', 'Atom', ('sigma', 'epsilon'))
2245
2246 def registerNBFIX(self, parameters):
2247 types = self.ff._findAtomTypes(parameters, 2)
2248 if None not in types:
2249 type1 = types[0][0]
2250 type2 = types[1][0]
2251 epsilon = _convertParameterToNumber(parameters['epsilon'])
2252 sigma = _convertParameterToNumber(parameters['sigma'])
2253 self.nbfixTypes[(type1, type2)] = [sigma, epsilon]
2254 self.nbfixTypes[(type2, type1)] = [sigma, epsilon]
2255
2256 def registerLennardJones(self, parameters):
2257 self.ljTypes.registerAtom(parameters)
2258
2259 @staticmethod
2260 def parseElement(element, ff):
2261 existing = [f for f in ff._forces if isinstance(f, LennardJonesGenerator)]
2262 if len(existing) == 0:
2263 generator = LennardJonesGenerator(ff, float(element.attrib['lj14scale']))
2264 ff.registerGenerator(generator)
2265 else:
2266 # Multiple <LennardJonesForce> tags were found, probably in different files
2267 generator = existing[0]
2268 if abs(generator.lj14scale - float(element.attrib['lj14scale'])) > NonbondedGenerator.SCALETOL:
2269 raise ValueError('Found multiple LennardJonesForce tags with different 1-4 scales')
2270 for LJ in element.findall('Atom'):
2271 generator.registerLennardJones(LJ.attrib)
2272 for Nbfix in element.findall('NBFixPair'):
2273 generator.registerNBFIX(Nbfix.attrib)
2274
2275 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2276 # First derive the lookup tables
2277
2278 nbfixTypeSet = set().union(*self.nbfixTypes)
2279 ljIndexList = [None]*len(data.atoms)
2280 numLjTypes = 0
2281 ljTypeList = []
2282 typeMap = {}
2283 for i, atom in enumerate(data.atoms):
2284 atype = data.atomType[atom]
2285 values = tuple(self.ljTypes.getAtomParameters(atom, data))
2286 if values in typeMap and atype not in nbfixTypeSet:
2287 # Only non-NBFIX types can be compressed
2288 ljIndexList[i] = typeMap[values]
2289 else:
2290 typeMap[values] = numLjTypes
2291 ljIndexList[i] = numLjTypes
2292 numLjTypes += 1
2293 ljTypeList.append(atype)
2294 reverseMap = [0]*len(typeMap)
2295 for typeValue in typeMap:
2296 reverseMap[typeMap[typeValue]] = typeValue
2297
2298 # Now everything is assigned. Create the A- and B-coefficient arrays
2299
2300 acoef = [0]*(numLjTypes*numLjTypes)
2301 bcoef = acoef[:]
2302 for m in range(numLjTypes):
2303 for n in range(numLjTypes):
2304 pair = (ljTypeList[m], ljTypeList[n])
2305 if pair in self.nbfixTypes:
2306 epsilon = self.nbfixTypes[pair][1]
2307 sigma = self.nbfixTypes[pair][0]
2308 sigma6 = sigma**6
2309 acoef[m+numLjTypes*n] = 4*epsilon*sigma6*sigma6
2310 bcoef[m+numLjTypes*n] = 4*epsilon*sigma6
2311 continue
2312 else:
2313 sigma = 0.5*(reverseMap[m][0]+reverseMap[n][0])
2314 sigma6 = sigma**6
2315 epsilon = math.sqrt(reverseMap[m][-1]*reverseMap[n][-1])
2316 acoef[m+numLjTypes*n] = 4*epsilon*sigma6*sigma6
2317 bcoef[m+numLjTypes*n] = 4*epsilon*sigma6
2318
2319 self.force = mm.CustomNonbondedForce('acoef(type1, type2)/r^12 - bcoef(type1, type2)/r^6;')
2320 self.force.addTabulatedFunction('acoef', mm.Discrete2DFunction(numLjTypes, numLjTypes, acoef))
2321 self.force.addTabulatedFunction('bcoef', mm.Discrete2DFunction(numLjTypes, numLjTypes, bcoef))
2322 self.force.addPerParticleParameter('type')
2323 if nonbondedMethod in [CutoffPeriodic, Ewald, PME]:
2324 self.force.setNonbondedMethod(mm.CustomNonbondedForce.CutoffPeriodic)
2325 elif nonbondedMethod is NoCutoff:
2326 self.force.setNonbondedMethod(mm.CustomNonbondedForce.NoCutoff)
2327 elif nonbondedMethod is CutoffNonPeriodic:
2328 self.force.setNonbondedMethod(mm.CustomNonbondedForce.CutoffNonPeriodic)
2329 else:
2330 raise AssertionError('Unrecognized nonbonded method [%s]' % nonbondedMethod)
2331
2332 # Add the particles
2333
2334 for i in ljIndexList:
2335 self.force.addParticle((i,))
2336 self.force.setUseLongRangeCorrection(True)
2337 self.force.setCutoffDistance(nonbondedCutoff)
2338 sys.addForce(self.force)
2339
2340 def postprocessSystem(self, sys, data, args):
2341 # Create the exceptions.
2342
2343 bondIndices = _findBondsForExclusions(data, sys)
2344 if self.lj14scale == 1:
2345 # Just exclude the 1-2 and 1-3 interactions.
2346
2347 self.force.createExclusionsFromBonds(bondIndices, 2)
2348 else:
2349 forceCopy = deepcopy(self.force)
2350 forceCopy.createExclusionsFromBonds(bondIndices, 2)
2351 self.force.createExclusionsFromBonds(bondIndices, 3)
2352 if self.force.getNumExclusions() > forceCopy.getNumExclusions() and self.lj14scale != 0:
2353 # We need to create a CustomBondForce and use it to implement the scaled 1-4 interactions.
2354
2355 bonded = mm.CustomBondForce('%g*epsilon*((sigma/r)^12-(sigma/r)^6)' % (4*self.lj14scale))
2356 bonded.addPerBondParameter('sigma')
2357 bonded.addPerBondParameter('epsilon')
2358 sys.addForce(bonded)
2359 skip = set(tuple(forceCopy.getExclusionParticles(i)) for i in range(forceCopy.getNumExclusions()))
2360 for i in range(self.force.getNumExclusions()):
2361 p1,p2 = self.force.getExclusionParticles(i)
2362 a1 = data.atoms[p1]
2363 a2 = data.atoms[p2]
2364 if (p1,p2) not in skip and (p2,p1) not in skip:
2365 type1 = data.atomType[a1]
2366 type2 = data.atomType[a2]
2367 if (type1, type2) in self.nbfixTypes:
2368 sigma, epsilon = self.nbfixTypes[(type1, type2)]
2369 else:
2370 values1 = self.ljTypes.getAtomParameters(a1, data)
2371 values2 = self.ljTypes.getAtomParameters(a2, data)
2372 sigma = 0.5*(values1[0]+values2[0])
2373 epsilon = sqrt(values1[1]*values2[1])
2374 bonded.addBond(p1, p2, (sigma, epsilon))
2375
2376parsers["LennardJonesForce"] = LennardJonesGenerator.parseElement
2377
2378
2379## @private
2380class GBSAOBCGenerator(object):
2381 """A GBSAOBCGenerator constructs a GBSAOBCForce."""
2382
2383 def __init__(self, forcefield):
2384 self.ff = forcefield
2385 self.params = ForceField._AtomTypeParameters(forcefield, 'GBSAOBCForce', 'Atom', ('charge', 'radius', 'scale'))
2386
2387 def registerAtom(self, parameters):
2388 self.params.registerAtom(parameters)
2389
2390 @staticmethod
2391 def parseElement(element, ff):
2392 existing = [f for f in ff._forces if isinstance(f, GBSAOBCGenerator)]
2393 if len(existing) == 0:
2394 generator = GBSAOBCGenerator(ff)
2395 ff.registerGenerator(generator)
2396 else:
2397 # Multiple <GBSAOBCForce> tags were found, probably in different files. Simply add more types to the existing one.
2398 generator = existing[0]
2399 generator.params.parseDefinitions(element)
2400
2401 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2402 methodMap = {NoCutoff:mm.NonbondedForce.NoCutoff,
2403 CutoffNonPeriodic:mm.NonbondedForce.CutoffNonPeriodic,
2404 CutoffPeriodic:mm.NonbondedForce.CutoffPeriodic}
2405 if nonbondedMethod not in methodMap:
2406 raise ValueError('Illegal nonbonded method for GBSAOBCForce')
2407 force = mm.GBSAOBCForce()
2408 for atom in data.atoms:
2409 values = self.params.getAtomParameters(atom, data)
2410 force.addParticle(values[0], values[1], values[2])
2411 force.setNonbondedMethod(methodMap[nonbondedMethod])
2412 force.setCutoffDistance(nonbondedCutoff)
2413 if 'soluteDielectric' in args:
2414 force.setSoluteDielectric(float(args['soluteDielectric']))
2415 if 'solventDielectric' in args:
2416 force.setSolventDielectric(float(args['solventDielectric']))
2417 sys.addForce(force)
2418
2419 def postprocessSystem(self, sys, data, args):
2420 # Disable the reaction field approximation, since it produces bad results when combined with GB.
2421
2422 for force in sys.getForces():
2423 if isinstance(force, mm.NonbondedForce):
2424 force.setReactionFieldDielectric(1.0)
2425
2426parsers["GBSAOBCForce"] = GBSAOBCGenerator.parseElement
2427
2428
2429## @private
2430class CustomBondGenerator(object):
2431 """A CustomBondGenerator constructs a CustomBondForce."""
2432
2433 def __init__(self, forcefield):
2434 self.ff = forcefield
2435 self.types1 = []
2436 self.types2 = []
2437 self.globalParams = {}
2438 self.perBondParams = []
2439 self.paramValues = []
2440
2441 @staticmethod
2442 def parseElement(element, ff):
2443 generator = CustomBondGenerator(ff)
2444 ff.registerGenerator(generator)
2445 generator.energy = element.attrib['energy']
2446 for param in element.findall('GlobalParameter'):
2447 generator.globalParams[param.attrib['name']] = float(param.attrib['defaultValue'])
2448 for param in element.findall('PerBondParameter'):
2449 generator.perBondParams.append(param.attrib['name'])
2450 for bond in element.findall('Bond'):
2451 types = ff._findAtomTypes(bond.attrib, 2)
2452 if None not in types:
2453 generator.types1.append(types[0])
2454 generator.types2.append(types[1])
2455 generator.paramValues.append([float(bond.attrib[param]) for param in generator.perBondParams])
2456
2457 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2458 force = mm.CustomBondForce(self.energy)
2459 sys.addForce(force)
2460 for param in self.globalParams:
2461 force.addGlobalParameter(param, self.globalParams[param])
2462 for param in self.perBondParams:
2463 force.addPerBondParameter(param)
2464 for bond in data.bonds:
2465 type1 = data.atomType[data.atoms[bond.atom1]]
2466 type2 = data.atomType[data.atoms[bond.atom2]]
2467 for i in range(len(self.types1)):
2468 types1 = self.types1[i]
2469 types2 = self.types2[i]
2470 if (type1 in types1 and type2 in types2) or (type1 in types2 and type2 in types1):
2471 force.addBond(bond.atom1, bond.atom2, self.paramValues[i])
2472 break
2473
2474parsers["CustomBondForce"] = CustomBondGenerator.parseElement
2475
2476
2477## @private
2478class CustomAngleGenerator(object):
2479 """A CustomAngleGenerator constructs a CustomAngleForce."""
2480
2481 def __init__(self, forcefield):
2482 self.ff = forcefield
2483 self.types1 = []
2484 self.types2 = []
2485 self.types3 = []
2486 self.globalParams = {}
2487 self.perAngleParams = []
2488 self.paramValues = []
2489
2490 @staticmethod
2491 def parseElement(element, ff):
2492 generator = CustomAngleGenerator(ff)
2493 ff.registerGenerator(generator)
2494 generator.energy = element.attrib['energy']
2495 for param in element.findall('GlobalParameter'):
2496 generator.globalParams[param.attrib['name']] = float(param.attrib['defaultValue'])
2497 for param in element.findall('PerAngleParameter'):
2498 generator.perAngleParams.append(param.attrib['name'])
2499 for angle in element.findall('Angle'):
2500 types = ff._findAtomTypes(angle.attrib, 3)
2501 if None not in types:
2502 generator.types1.append(types[0])
2503 generator.types2.append(types[1])
2504 generator.types3.append(types[2])
2505 generator.paramValues.append([float(angle.attrib[param]) for param in generator.perAngleParams])
2506
2507 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2508 force = mm.CustomAngleForce(self.energy)
2509 sys.addForce(force)
2510 for param in self.globalParams:
2511 force.addGlobalParameter(param, self.globalParams[param])
2512 for param in self.perAngleParams:
2513 force.addPerAngleParameter(param)
2514 for angle in data.angles:
2515 type1 = data.atomType[data.atoms[angle[0]]]
2516 type2 = data.atomType[data.atoms[angle[1]]]
2517 type3 = data.atomType[data.atoms[angle[2]]]
2518 for i in range(len(self.types1)):
2519 types1 = self.types1[i]
2520 types2 = self.types2[i]
2521 types3 = self.types3[i]
2522 if (type1 in types1 and type2 in types2 and type3 in types3) or (type1 in types3 and type2 in types2 and type3 in types1):
2523 force.addAngle(angle[0], angle[1], angle[2], self.paramValues[i])
2524 break
2525
2526parsers["CustomAngleForce"] = CustomAngleGenerator.parseElement
2527
2528
2529## @private
2530class CustomTorsion(object):
2531 """A CustomTorsion records the information for a custom torsion definition."""
2532
2533 def __init__(self, types, paramValues):
2534 self.types1 = types[0]
2535 self.types2 = types[1]
2536 self.types3 = types[2]
2537 self.types4 = types[3]
2538 self.paramValues = paramValues
2539
2540## @private
2541class CustomTorsionGenerator(object):
2542 """A CustomTorsionGenerator constructs a CustomTorsionForce."""
2543
2544 def __init__(self, forcefield):
2545 self.ff = forcefield
2546 self.proper = []
2547 self.improper = []
2548 self.globalParams = {}
2549 self.perTorsionParams = []
2550
2551 @staticmethod
2552 def parseElement(element, ff):
2553 generator = CustomTorsionGenerator(ff)
2554 ff.registerGenerator(generator)
2555 generator.energy = element.attrib['energy']
2556 for param in element.findall('GlobalParameter'):
2557 generator.globalParams[param.attrib['name']] = float(param.attrib['defaultValue'])
2558 for param in element.findall('PerTorsionParameter'):
2559 generator.perTorsionParams.append(param.attrib['name'])
2560 for torsion in element.findall('Proper'):
2561 types = ff._findAtomTypes(torsion.attrib, 4)
2562 if None not in types:
2563 generator.proper.append(CustomTorsion(types, [float(torsion.attrib[param]) for param in generator.perTorsionParams]))
2564 for torsion in element.findall('Improper'):
2565 types = ff._findAtomTypes(torsion.attrib, 4)
2566 if None not in types:
2567 generator.improper.append(CustomTorsion(types, [float(torsion.attrib[param]) for param in generator.perTorsionParams]))
2568
2569 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2570 force = mm.CustomTorsionForce(self.energy)
2571 sys.addForce(force)
2572 for param in self.globalParams:
2573 force.addGlobalParameter(param, self.globalParams[param])
2574 for param in self.perTorsionParams:
2575 force.addPerTorsionParameter(param)
2576 wildcard = self.ff._atomClasses['']
2577 for torsion in data.propers:
2578 type1 = data.atomType[data.atoms[torsion[0]]]
2579 type2 = data.atomType[data.atoms[torsion[1]]]
2580 type3 = data.atomType[data.atoms[torsion[2]]]
2581 type4 = data.atomType[data.atoms[torsion[3]]]
2582 match = None
2583 for tordef in self.proper:
2584 types1 = tordef.types1
2585 types2 = tordef.types2
2586 types3 = tordef.types3
2587 types4 = tordef.types4
2588 if (type2 in types2 and type3 in types3 and type4 in types4 and type1 in types1) or (type2 in types3 and type3 in types2 and type4 in types1 and type1 in types4):
2589 hasWildcard = (wildcard in (types1, types2, types3, types4))
2590 if match is None or not hasWildcard: # Prefer specific definitions over ones with wildcards
2591 match = tordef
2592 if not hasWildcard:
2593 break
2594 if match is not None:
2595 force.addTorsion(torsion[0], torsion[1], torsion[2], torsion[3], match.paramValues)
2596 for torsion in data.impropers:
2597 type1 = data.atomType[data.atoms[torsion[0]]]
2598 type2 = data.atomType[data.atoms[torsion[1]]]
2599 type3 = data.atomType[data.atoms[torsion[2]]]
2600 type4 = data.atomType[data.atoms[torsion[3]]]
2601 match = None
2602 for tordef in self.improper:
2603 types1 = tordef.types1
2604 types2 = tordef.types2
2605 types3 = tordef.types3
2606 types4 = tordef.types4
2607 hasWildcard = (wildcard in (types1, types2, types3, types4))
2608 if match is not None and hasWildcard:
2609 # Prefer specific definitions over ones with wildcards
2610 continue
2611 if type1 in types1:
2612 for (t2, t3, t4) in itertools.permutations(((type2, 1), (type3, 2), (type4, 3))):
2613 if t2[0] in types2 and t3[0] in types3 and t4[0] in types4:
2614 if hasWildcard:
2615 # Workaround to be more consistent with AMBER. It uses wildcards to define most of its
2616 # impropers, which leaves the ordering ambiguous. It then follows some bizarre rules
2617 # to pick the order.
2618 a1 = torsion[t2[1]]
2619 a2 = torsion[t3[1]]
2620 e1 = data.atoms[a1].element
2621 e2 = data.atoms[a2].element
2622 if e1 == e2 and a1 > a2:
2623 (a1, a2) = (a2, a1)
2624 elif e1 != elem.carbon and (e2 == elem.carbon or e1.mass < e2.mass):
2625 (a1, a2) = (a2, a1)
2626 match = (a1, a2, torsion[0], torsion[t4[1]], tordef)
2627 else:
2628 # There are no wildcards, so the order is unambiguous.
2629 match = (torsion[0], torsion[t2[1]], torsion[t3[1]], torsion[t4[1]], tordef)
2630 break
2631 if match is not None:
2632 (a1, a2, a3, a4, tordef) = match
2633 force.addTorsion(a1, a2, a3, a4, tordef.paramValues)
2634
2635parsers["CustomTorsionForce"] = CustomTorsionGenerator.parseElement
2636
2637
2638## @private
2639class CustomNonbondedGenerator(object):
2640 """A CustomNonbondedGenerator constructs a CustomNonbondedForce."""
2641
2642 def __init__(self, forcefield, energy, bondCutoff):
2643 self.ff = forcefield
2644 self.energy = energy
2645 self.bondCutoff = bondCutoff
2646 self.globalParams = {}
2647 self.perParticleParams = []
2648 self.functions = []
2649
2650 @staticmethod
2651 def parseElement(element, ff):
2652 generator = CustomNonbondedGenerator(ff, element.attrib['energy'], int(element.attrib['bondCutoff']))
2653 ff.registerGenerator(generator)
2654 for param in element.findall('GlobalParameter'):
2655 generator.globalParams[param.attrib['name']] = float(param.attrib['defaultValue'])
2656 for param in element.findall('PerParticleParameter'):
2657 generator.perParticleParams.append(param.attrib['name'])
2658 generator.params = ForceField._AtomTypeParameters(ff, 'CustomNonbondedForce', 'Atom', generator.perParticleParams)
2659 generator.params.parseDefinitions(element)
2660 generator.functions += _parseFunctions(element)
2661
2662 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2663 methodMap = {NoCutoff:mm.CustomNonbondedForce.NoCutoff,
2664 CutoffNonPeriodic:mm.CustomNonbondedForce.CutoffNonPeriodic,
2665 CutoffPeriodic:mm.CustomNonbondedForce.CutoffPeriodic}
2666 if nonbondedMethod not in methodMap:
2667 raise ValueError('Illegal nonbonded method for CustomNonbondedForce')
2668 force = mm.CustomNonbondedForce(self.energy)
2669 for param in self.globalParams:
2670 force.addGlobalParameter(param, self.globalParams[param])
2671 for param in self.perParticleParams:
2672 force.addPerParticleParameter(param)
2673 _createFunctions(force, self.functions)
2674 for atom in data.atoms:
2675 values = self.params.getAtomParameters(atom, data)
2676 force.addParticle(values)
2677 force.setNonbondedMethod(methodMap[nonbondedMethod])
2678 force.setCutoffDistance(nonbondedCutoff)
2679 sys.addForce(force)
2680
2681 def postprocessSystem(self, sys, data, args):
2682 # Create the exclusions.
2683
2684 bondIndices = _findBondsForExclusions(data, sys)
2685 nonbonded = [f for f in sys.getForces() if isinstance(f, mm.CustomNonbondedForce)][0]
2686 nonbonded.createExclusionsFromBonds(bondIndices, self.bondCutoff)
2687
2688parsers["CustomNonbondedForce"] = CustomNonbondedGenerator.parseElement
2689
2690
2691## @private
2692class CustomGBGenerator(object):
2693 """A CustomGBGenerator constructs a CustomGBForce."""
2694
2695 def __init__(self, forcefield):
2696 self.ff = forcefield
2697 self.globalParams = {}
2698 self.perParticleParams = []
2699 self.computedValues = []
2700 self.energyTerms = []
2701 self.functions = []
2702
2703 @staticmethod
2704 def parseElement(element, ff):
2705 generator = CustomGBGenerator(ff)
2706 ff.registerGenerator(generator)
2707 for param in element.findall('GlobalParameter'):
2708 generator.globalParams[param.attrib['name']] = float(param.attrib['defaultValue'])
2709 for param in element.findall('PerParticleParameter'):
2710 generator.perParticleParams.append(param.attrib['name'])
2711 generator.params = ForceField._AtomTypeParameters(ff, 'CustomGBForce', 'Atom', generator.perParticleParams)
2712 generator.params.parseDefinitions(element)
2713 computationMap = {"SingleParticle" : mm.CustomGBForce.SingleParticle,
2714 "ParticlePair" : mm.CustomGBForce.ParticlePair,
2715 "ParticlePairNoExclusions" : mm.CustomGBForce.ParticlePairNoExclusions}
2716 for value in element.findall('ComputedValue'):
2717 generator.computedValues.append((value.attrib['name'], value.text, computationMap[value.attrib['type']]))
2718 for term in element.findall('EnergyTerm'):
2719 generator.energyTerms.append((term.text, computationMap[term.attrib['type']]))
2720 generator.functions += _parseFunctions(element)
2721
2722 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2723 methodMap = {NoCutoff:mm.CustomGBForce.NoCutoff,
2724 CutoffNonPeriodic:mm.CustomGBForce.CutoffNonPeriodic,
2725 CutoffPeriodic:mm.CustomGBForce.CutoffPeriodic}
2726 if nonbondedMethod not in methodMap:
2727 raise ValueError('Illegal nonbonded method for CustomGBForce')
2728 force = mm.CustomGBForce()
2729 for param in self.globalParams:
2730 force.addGlobalParameter(param, self.globalParams[param])
2731 for param in self.perParticleParams:
2732 force.addPerParticleParameter(param)
2733 for value in self.computedValues:
2734 force.addComputedValue(value[0], value[1], value[2])
2735 for term in self.energyTerms:
2736 force.addEnergyTerm(term[0], term[1])
2737 _createFunctions(force, self.functions)
2738 for atom in data.atoms:
2739 values = self.params.getAtomParameters(atom, data)
2740 force.addParticle(values)
2741 force.setNonbondedMethod(methodMap[nonbondedMethod])
2742 force.setCutoffDistance(nonbondedCutoff)
2743 sys.addForce(force)
2744
2745parsers["CustomGBForce"] = CustomGBGenerator.parseElement
2746
2747
2748## @private
2749class CustomHbondGenerator(object):
2750 """A CustomHbondGenerator constructs a CustomHbondForce."""
2751
2752 def __init__(self, forcefield):
2753 self.ff = forcefield
2754 self.donorTypes1 = []
2755 self.donorTypes2 = []
2756 self.donorTypes3 = []
2757 self.acceptorTypes1 = []
2758 self.acceptorTypes2 = []
2759 self.acceptorTypes3 = []
2760 self.globalParams = {}
2761 self.perDonorParams = []
2762 self.perAcceptorParams = []
2763 self.donorParamValues = []
2764 self.acceptorParamValues = []
2765 self.functions = []
2766
2767 @staticmethod
2768 def parseElement(element, ff):
2769 generator = CustomHbondGenerator(ff)
2770 ff.registerGenerator(generator)
2771 generator.energy = element.attrib['energy']
2772 generator.bondCutoff = int(element.attrib['bondCutoff'])
2773 generator.particlesPerDonor = int(element.attrib['particlesPerDonor'])
2774 generator.particlesPerAcceptor = int(element.attrib['particlesPerAcceptor'])
2775 if generator.particlesPerDonor < 1 or generator.particlesPerDonor > 3:
2776 raise ValueError('Illegal value for particlesPerDonor for CustomHbondForce')
2777 if generator.particlesPerAcceptor < 1 or generator.particlesPerAcceptor > 3:
2778 raise ValueError('Illegal value for particlesPerAcceptor for CustomHbondForce')
2779 for param in element.findall('GlobalParameter'):
2780 generator.globalParams[param.attrib['name']] = float(param.attrib['defaultValue'])
2781 for param in element.findall('PerDonorParameter'):
2782 generator.perDonorParams.append(param.attrib['name'])
2783 for param in element.findall('PerAcceptorParameter'):
2784 generator.perAcceptorParams.append(param.attrib['name'])
2785 for donor in element.findall('Donor'):
2786 types = ff._findAtomTypes(donor.attrib, 3)[:generator.particlesPerDonor]
2787 if None not in types:
2788 generator.donorTypes1.append(types[0])
2789 if len(types) > 1:
2790 generator.donorTypes2.append(types[1])
2791 if len(types) > 2:
2792 generator.donorTypes3.append(types[2])
2793 generator.donorParamValues.append([float(donor.attrib[param]) for param in generator.perDonorParams])
2794 for acceptor in element.findall('Acceptor'):
2795 types = ff._findAtomTypes(acceptor.attrib, 3)[:generator.particlesPerAcceptor]
2796 if None not in types:
2797 generator.acceptorTypes1.append(types[0])
2798 if len(types) > 1:
2799 generator.acceptorTypes2.append(types[1])
2800 if len(types) > 2:
2801 generator.acceptorTypes3.append(types[2])
2802 generator.acceptorParamValues.append([float(acceptor.attrib[param]) for param in generator.perAcceptorParams])
2803 generator.functions += _parseFunctions(element)
2804
2805 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2806 methodMap = {NoCutoff:mm.CustomHbondForce.NoCutoff,
2807 CutoffNonPeriodic:mm.CustomHbondForce.CutoffNonPeriodic,
2808 CutoffPeriodic:mm.CustomHbondForce.CutoffPeriodic}
2809 if nonbondedMethod not in methodMap:
2810 raise ValueError('Illegal nonbonded method for CustomNonbondedForce')
2811 force = mm.CustomHbondForce(self.energy)
2812 sys.addForce(force)
2813 for param in self.globalParams:
2814 force.addGlobalParameter(param, self.globalParams[param])
2815 for param in self.perDonorParams:
2816 force.addPerDonorParameter(param)
2817 for param in self.perAcceptorParams:
2818 force.addPerAcceptorParameter(param)
2819 _createFunctions(force, self.functions)
2820 force.setNonbondedMethod(methodMap[nonbondedMethod])
2821 force.setCutoffDistance(nonbondedCutoff)
2822
2823 # Add donors.
2824
2825 if self.particlesPerDonor == 1:
2826 for atom in data.atoms:
2827 type1 = data.atomType[atom]
2828 for i in range(len(self.donorTypes1)):
2829 types1 = self.donorTypes1[i]
2830 if type1 in self.donorTypes1[i]:
2831 force.addDonor(atom.index, -1, -1, self.donorParamValues[i])
2832 elif self.particlesPerDonor == 2:
2833 for bond in data.bonds:
2834 type1 = data.atomType[data.atoms[bond.atom1]]
2835 type2 = data.atomType[data.atoms[bond.atom2]]
2836 for i in range(len(self.donorTypes1)):
2837 types1 = self.donorTypes1[i]
2838 types2 = self.donorTypes2[i]
2839 if type1 in types1 and type2 in types2:
2840 force.addDonor(bond.atom1, bond.atom2, -1, self.donorParamValues[i])
2841 elif type1 in types2 and type2 in types1:
2842 force.addDonor(bond.atom2, bond.atom1, -1, self.donorParamValues[i])
2843 else:
2844 for angle in data.angles:
2845 type1 = data.atomType[data.atoms[angle[0]]]
2846 type2 = data.atomType[data.atoms[angle[1]]]
2847 type3 = data.atomType[data.atoms[angle[2]]]
2848 for i in range(len(self.donorTypes1)):
2849 types1 = self.donorTypes1[i]
2850 types2 = self.donorTypes2[i]
2851 types3 = self.donorTypes3[i]
2852 if (type1 in types1 and type2 in types2 and type3 in types3) or (type1 in types3 and type2 in types2 and type3 in types1):
2853 force.addDonor(angle[0], angle[1], angle[2], self.donorParamValues[i])
2854
2855 # Add acceptors.
2856
2857 if self.particlesPerAcceptor == 1:
2858 for atom in data.atoms:
2859 type1 = data.atomType[atom]
2860 for i in range(len(self.acceptorTypes1)):
2861 types1 = self.acceptorTypes1[i]
2862 if type1 in self.acceptorTypes1[i]:
2863 force.addAcceptor(atom.index, -1, -1, self.acceptorParamValues[i])
2864 elif self.particlesPerAcceptor == 2:
2865 for bond in data.bonds:
2866 type1 = data.atomType[data.atoms[bond.atom1]]
2867 type2 = data.atomType[data.atoms[bond.atom2]]
2868 for i in range(len(self.acceptorTypes1)):
2869 types1 = self.acceptorTypes1[i]
2870 types2 = self.acceptorTypes2[i]
2871 if type1 in types1 and type2 in types2:
2872 force.addAcceptor(bond.atom1, bond.atom2, -1, self.acceptorParamValues[i])
2873 elif type1 in types2 and type2 in types1:
2874 force.addAcceptor(bond.atom2, bond.atom1, -1, self.acceptorParamValues[i])
2875 else:
2876 for angle in data.angles:
2877 type1 = data.atomType[data.atoms[angle[0]]]
2878 type2 = data.atomType[data.atoms[angle[1]]]
2879 type3 = data.atomType[data.atoms[angle[2]]]
2880 for i in range(len(self.acceptorTypes1)):
2881 types1 = self.acceptorTypes1[i]
2882 types2 = self.acceptorTypes2[i]
2883 types3 = self.acceptorTypes3[i]
2884 if (type1 in types1 and type2 in types2 and type3 in types3) or (type1 in types3 and type2 in types2 and type3 in types1):
2885 force.addAcceptor(angle[0], angle[1], angle[2], self.acceptorParamValues[i])
2886
2887 # Add exclusions.
2888
2889 for donor in range(force.getNumDonors()):
2890 (d1, d2, d3, params) = force.getDonorParameters(donor)
2891 outerAtoms = set((d1, d2, d3))
2892 if -1 in outerAtoms:
2893 outerAtoms.remove(-1)
2894 excludedAtoms = set(outerAtoms)
2895 for i in range(self.bondCutoff):
2896 newOuterAtoms = set()
2897 for atom in outerAtoms:
2898 for bond in data.atomBonds[atom]:
2899 b = data.bonds[bond]
2900 bondedAtom = (b.atom2 if b.atom1 == atom else b.atom1)
2901 if bondedAtom not in excludedAtoms:
2902 newOuterAtoms.add(bondedAtom)
2903 excludedAtoms.add(bondedAtom)
2904 outerAtoms = newOuterAtoms
2905 for acceptor in range(force.getNumAcceptors()):
2906 (a1, a2, a3, params) = force.getAcceptorParameters(acceptor)
2907 if a1 in excludedAtoms or a2 in excludedAtoms or a3 in excludedAtoms:
2908 force.addExclusion(donor, acceptor)
2909
2910parsers["CustomHbondForce"] = CustomHbondGenerator.parseElement
2911
2912
2913## @private
2914class CustomManyParticleGenerator(object):
2915 """A CustomManyParticleGenerator constructs a CustomManyParticleForce."""
2916
2917 def __init__(self, forcefield, particlesPerSet, energy, permutationMode, bondCutoff):
2918 self.ff = forcefield
2919 self.particlesPerSet = particlesPerSet
2920 self.energy = energy
2921 self.permutationMode = permutationMode
2922 self.bondCutoff = bondCutoff
2923 self.globalParams = {}
2924 self.perParticleParams = []
2925 self.functions = []
2926 self.typeFilters = []
2927
2928 @staticmethod
2929 def parseElement(element, ff):
2930 permutationMap = {"SinglePermutation" : mm.CustomManyParticleForce.SinglePermutation,
2931 "UniqueCentralParticle" : mm.CustomManyParticleForce.UniqueCentralParticle}
2932 generator = CustomManyParticleGenerator(ff, int(element.attrib['particlesPerSet']), element.attrib['energy'], permutationMap[element.attrib['permutationMode']], int(element.attrib['bondCutoff']))
2933 ff.registerGenerator(generator)
2934 for param in element.findall('GlobalParameter'):
2935 generator.globalParams[param.attrib['name']] = float(param.attrib['defaultValue'])
2936 for param in element.findall('PerParticleParameter'):
2937 generator.perParticleParams.append(param.attrib['name'])
2938 for param in element.findall('TypeFilter'):
2939 generator.typeFilters.append((int(param.attrib['index']), [int(x) for x in param.attrib['types'].split(',')]))
2940 generator.params = ForceField._AtomTypeParameters(ff, 'CustomManyParticleForce', 'Atom', generator.perParticleParams)
2941 generator.params.parseDefinitions(element)
2942
2943 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
2944 methodMap = {NoCutoff:mm.CustomManyParticleForce.NoCutoff,
2945 CutoffNonPeriodic:mm.CustomManyParticleForce.CutoffNonPeriodic,
2946 CutoffPeriodic:mm.CustomManyParticleForce.CutoffPeriodic}
2947 if nonbondedMethod not in methodMap:
2948 raise ValueError('Illegal nonbonded method for CustomManyParticleForce')
2949 force = mm.CustomManyParticleForce(self.particlesPerSet, self.energy)
2950 force.setPermutationMode(self.permutationMode)
2951 for param in self.globalParams:
2952 force.addGlobalParameter(param, self.globalParams[param])
2953 for param in self.perParticleParams:
2954 force.addPerParticleParameter(param)
2955 for index, types in self.typeFilters:
2956 force.setTypeFilter(index, types)
2957 for (name, type, values, params) in self.functions:
2958 if type == 'Continuous1D':
2959 force.addTabulatedFunction(name, mm.Continuous1DFunction(values, params['min'], params['max']))
2960 elif type == 'Continuous2D':
2961 force.addTabulatedFunction(name, mm.Continuous2DFunction(params['xsize'], params['ysize'], values, params['xmin'], params['xmax'], params['ymin'], params['ymax']))
2962 elif type == 'Continuous3D':
2963 force.addTabulatedFunction(name, mm.Continuous2DFunction(params['xsize'], params['ysize'], params['zsize'], values, params['xmin'], params['xmax'], params['ymin'], params['ymax'], params['zmin'], params['zmax']))
2964 elif type == 'Discrete1D':
2965 force.addTabulatedFunction(name, mm.Discrete1DFunction(values))
2966 elif type == 'Discrete2D':
2967 force.addTabulatedFunction(name, mm.Discrete2DFunction(params['xsize'], params['ysize'], values))
2968 elif type == 'Discrete3D':
2969 force.addTabulatedFunction(name, mm.Discrete2DFunction(params['xsize'], params['ysize'], params['zsize'], values))
2970 for atom in data.atoms:
2971 values = self.params.getAtomParameters(atom, data)
2972 type = int(self.params.getExtraParameters(atom, data)['filterType'])
2973 force.addParticle(values, type)
2974 force.setNonbondedMethod(methodMap[nonbondedMethod])
2975 force.setCutoffDistance(nonbondedCutoff)
2976 sys.addForce(force)
2977
2978 def postprocessSystem(self, sys, data, args):
2979 # Create exclusions based on bonds.
2980
2981 bondIndices = []
2982 for bond in data.bonds:
2983 bondIndices.append((bond.atom1, bond.atom2))
2984
2985 # If a virtual site does *not* share exclusions with another atom, add a bond between it and its first parent atom.
2986
2987 for i in range(sys.getNumParticles()):
2988 if sys.isVirtualSite(i):
2989 (site, atoms, excludeWith) = data.virtualSites[data.atoms[i]]
2990 if excludeWith is None:
2991 bondIndices.append((i, site.getParticle(0)))
2992
2993 # Certain particles, such as lone pairs and Drude particles, share exclusions with a parent atom.
2994 # If the parent atom does not interact with an atom, the child particle does not either.
2995
2996 for atom1, atom2 in bondIndices:
2997 for child1 in data.excludeAtomWith[atom1]:
2998 bondIndices.append((child1, atom2))
2999 for child2 in data.excludeAtomWith[atom2]:
3000 bondIndices.append((child1, child2))
3001 for child2 in data.excludeAtomWith[atom2]:
3002 bondIndices.append((atom1, child2))
3003
3004 # Create the exclusions.
3005
3006 nonbonded = [f for f in sys.getForces() if isinstance(f, mm.CustomManyParticleForce)][0]
3007 nonbonded.createExclusionsFromBonds(bondIndices, self.bondCutoff)
3008
3009parsers["CustomManyParticleForce"] = CustomManyParticleGenerator.parseElement
3010
3011def getAtomPrint(data, atomIndex):
3012
3013 if (atomIndex < len(data.atoms)):
3014 atom = data.atoms[atomIndex]
3015 returnString = "%4s %4s %5d" % (atom.name, atom.residue.name, atom.residue.index)
3016 else:
3017 returnString = "NA"
3018
3019 return returnString
3020
3021#=============================================================================================
3022
3023def countConstraint(data):
3024
3025 bondCount = 0
3026 angleCount = 0
3027 for bond in data.bonds:
3028 if bond.isConstrained:
3029 bondCount += 1
3030
3031 angleCount = 0
3032 for (angle, isConstrained) in zip(data.angles, data.isAngleConstrained):
3033 if (isConstrained):
3034 angleCount += 1
3035
3036 print("Constraints bond=%d angle=%d total=%d" % (bondCount, angleCount, (bondCount+angleCount)))
3037
3038## @private
3039class AmoebaBondGenerator(object):
3040
3041 #=============================================================================================
3042
3043 """An AmoebaBondGenerator constructs a AmoebaBondForce."""
3044
3045 #=============================================================================================
3046
3047 def __init__(self, cubic, quartic):
3048
3049 self.cubic = cubic
3050 self.quartic = quartic
3051 self.types1 = []
3052 self.types2 = []
3053 self.length = []
3054 self.k = []
3055
3056 #=============================================================================================
3057
3058 @staticmethod
3059 def parseElement(element, forceField):
3060
3061 # <AmoebaBondForce bond-cubic="-25.5" bond-quartic="379.3125">
3062 # <Bond class1="1" class2="2" length="0.1437" k="156900.0"/>
3063
3064 generator = AmoebaBondGenerator(float(element.attrib['bond-cubic']), float(element.attrib['bond-quartic']))
3065 forceField._forces.append(generator)
3066 for bond in element.findall('Bond'):
3067 types = forceField._findAtomTypes(bond.attrib, 2)
3068 if None not in types:
3069 generator.types1.append(types[0])
3070 generator.types2.append(types[1])
3071 generator.length.append(float(bond.attrib['length']))
3072 generator.k.append(float(bond.attrib['k']))
3073 else:
3074 outputString = "AmoebaBondGenerator: error getting types: %s %s" % (
3075 bond.attrib['class1'],
3076 bond.attrib['class2'])
3077 raise ValueError(outputString)
3078
3079 #=============================================================================================
3080
3081 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
3082
3083 #countConstraint(data)
3084
3085 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
3086 existing = [f for f in existing if type(f) == mm.AmoebaBondForce]
3087 if len(existing) == 0:
3088 force = mm.AmoebaBondForce()
3089 sys.addForce(force)
3090 else:
3091 force = existing[0]
3092
3093 force.setAmoebaGlobalBondCubic(self.cubic)
3094 force.setAmoebaGlobalBondQuartic(self.quartic)
3095
3096 for bond in data.bonds:
3097 type1 = data.atomType[data.atoms[bond.atom1]]
3098 type2 = data.atomType[data.atoms[bond.atom2]]
3099 for i in range(len(self.types1)):
3100 types1 = self.types1[i]
3101 types2 = self.types2[i]
3102 if (type1 in types1 and type2 in types2) or (type1 in types2 and type2 in types1):
3103 bond.length = self.length[i]
3104 if bond.isConstrained:
3105 data.addConstraint(sys, bond.atom1, bond.atom2, self.length[i])
3106 elif self.k[i] != 0:
3107 force.addBond(bond.atom1, bond.atom2, self.length[i], self.k[i])
3108 break
3109
3110parsers["AmoebaBondForce"] = AmoebaBondGenerator.parseElement
3111
3112#=============================================================================================
3113# Add angle constraint
3114#=============================================================================================
3115
3116def addAngleConstraint(angle, idealAngle, data, sys):
3117
3118 # Find the two bonds that make this angle.
3119
3120 bond1 = None
3121 bond2 = None
3122 for bond in data.atomBonds[angle[1]]:
3123 atom1 = data.bonds[bond].atom1
3124 atom2 = data.bonds[bond].atom2
3125 if atom1 == angle[0] or atom2 == angle[0]:
3126 bond1 = bond
3127 elif atom1 == angle[2] or atom2 == angle[2]:
3128 bond2 = bond
3129
3130 # Compute the distance between atoms and add a constraint
3131
3132 if bond1 is not None and bond2 is not None:
3133 l1 = data.bonds[bond1].length
3134 l2 = data.bonds[bond2].length
3135 if l1 is not None and l2 is not None:
3136 length = sqrt(l1*l1 + l2*l2 - 2*l1*l2*cos(idealAngle))
3137 data.addConstraint(sys, angle[0], angle[2], length)
3138 return
3139
3140#=============================================================================================
3141## @private
3142class AmoebaAngleGenerator(object):
3143
3144 #=============================================================================================
3145 """An AmoebaAngleGenerator constructs a AmoebaAngleForce."""
3146 #=============================================================================================
3147
3148 def __init__(self, forceField, cubic, quartic, pentic, sextic):
3149
3150 self.forceField = forceField
3151 self.cubic = cubic
3152 self.quartic = quartic
3153 self.pentic = pentic
3154 self.sextic = sextic
3155
3156 self.types1 = []
3157 self.types2 = []
3158 self.types3 = []
3159
3160 self.angle = []
3161 self.k = []
3162
3163 #=============================================================================================
3164
3165 @staticmethod
3166 def parseElement(element, forceField):
3167
3168 # <AmoebaAngleForce angle-cubic="-0.014" angle-quartic="5.6e-05" angle-pentic="-7e-07" angle-sextic="2.2e-08">
3169 # <Angle class1="2" class2="1" class3="3" k="0.0637259642196" angle1="122.00" />
3170
3171 generator = AmoebaAngleGenerator(forceField, float(element.attrib['angle-cubic']), float(element.attrib['angle-quartic']), float(element.attrib['angle-pentic']), float(element.attrib['angle-sextic']))
3172 forceField._forces.append(generator)
3173 for angle in element.findall('Angle'):
3174 types = forceField._findAtomTypes(angle.attrib, 3)
3175 if None not in types:
3176
3177 generator.types1.append(types[0])
3178 generator.types2.append(types[1])
3179 generator.types3.append(types[2])
3180
3181 angleList = []
3182 angleList.append(float(angle.attrib['angle1']))
3183
3184 try:
3185 angleList.append(float(angle.attrib['angle2']))
3186 try:
3187 angleList.append(float(angle.attrib['angle3']))
3188 except:
3189 pass
3190 except:
3191 pass
3192 generator.angle.append(angleList)
3193 generator.k.append(float(angle.attrib['k']))
3194 else:
3195 outputString = "AmoebaAngleGenerator: error getting types: %s %s %s" % (
3196 angle.attrib['class1'],
3197 angle.attrib['class2'],
3198 angle.attrib['class3'])
3199 raise ValueError(outputString)
3200
3201 #=============================================================================================
3202 # createForce is bypassed here since the AmoebaOutOfPlaneBendForce generator must first execute
3203 # and partition angles into in-plane and non-in-plane angles
3204 #=============================================================================================
3205
3206 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
3207 pass
3208
3209 #=============================================================================================
3210 # createForcePostOpBendAngle is called by AmoebaOutOfPlaneBendForce with the list of
3211 # non-in-plane angles
3212 #=============================================================================================
3213
3214 def createForcePostOpBendAngle(self, sys, data, nonbondedMethod, nonbondedCutoff, angleList, args):
3215
3216 # get force
3217
3218 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
3219 existing = [f for f in existing if type(f) == mm.AmoebaAngleForce]
3220
3221 if len(existing) == 0:
3222 force = mm.AmoebaAngleForce()
3223 sys.addForce(force)
3224 else:
3225 force = existing[0]
3226
3227 # set scalars
3228
3229 force.setAmoebaGlobalAngleCubic(self.cubic)
3230 force.setAmoebaGlobalAngleQuartic(self.quartic)
3231 force.setAmoebaGlobalAnglePentic(self.pentic)
3232 force.setAmoebaGlobalAngleSextic(self.sextic)
3233
3234 for angleDict in angleList:
3235 angle = angleDict['angle']
3236 isConstrained = angleDict['isConstrained']
3237
3238 type1 = data.atomType[data.atoms[angle[0]]]
3239 type2 = data.atomType[data.atoms[angle[1]]]
3240 type3 = data.atomType[data.atoms[angle[2]]]
3241 for i in range(len(self.types1)):
3242 types1 = self.types1[i]
3243 types2 = self.types2[i]
3244 types3 = self.types3[i]
3245 if (type1 in types1 and type2 in types2 and type3 in types3) or (type1 in types3 and type2 in types2 and type3 in types1):
3246 if isConstrained and self.k[i] != 0.0:
3247 angleDict['idealAngle'] = self.angle[i][0]
3248 addAngleConstraint(angle, self.angle[i][0]*math.pi/180.0, data, sys)
3249 elif self.k[i] != 0:
3250 lenAngle = len(self.angle[i])
3251 if (lenAngle > 1):
3252 # get k-index by counting number of non-angle hydrogens on the central atom
3253 # based on kangle.f
3254 numberOfHydrogens = 0
3255 for bond in data.atomBonds[angle[1]]:
3256 atom1 = data.bonds[bond].atom1
3257 atom2 = data.bonds[bond].atom2
3258 if (atom1 == angle[1] and atom2 != angle[0] and atom2 != angle[2] and (sys.getParticleMass(atom2)/unit.dalton) < 1.90):
3259 numberOfHydrogens += 1
3260 if (atom2 == angle[1] and atom1 != angle[0] and atom1 != angle[2] and (sys.getParticleMass(atom1)/unit.dalton) < 1.90):
3261 numberOfHydrogens += 1
3262 if (numberOfHydrogens < lenAngle):
3263 angleValue = self.angle[i][numberOfHydrogens]
3264 else:
3265 outputString = "AmoebaAngleGenerator angle index=%d is out of range: [0, %5d] " % (numberOfHydrogens, lenAngle)
3266 raise ValueError(outputString)
3267 else:
3268 angleValue = self.angle[i][0]
3269
3270 angleDict['idealAngle'] = angleValue
3271 force.addAngle(angle[0], angle[1], angle[2], angleValue, self.k[i])
3272 break
3273
3274 #=============================================================================================
3275 # createForcePostOpBendInPlaneAngle is called by AmoebaOutOfPlaneBendForce with the list of
3276 # in-plane angles
3277 #=============================================================================================
3278
3279 def createForcePostOpBendInPlaneAngle(self, sys, data, nonbondedMethod, nonbondedCutoff, angleList, args):
3280
3281 # get force
3282
3283 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
3284 existing = [f for f in existing if type(f) == mm.AmoebaInPlaneAngleForce]
3285
3286 if len(existing) == 0:
3287 force = mm.AmoebaInPlaneAngleForce()
3288 sys.addForce(force)
3289 else:
3290 force = existing[0]
3291
3292 # scalars
3293
3294 force.setAmoebaGlobalInPlaneAngleCubic(self.cubic)
3295 force.setAmoebaGlobalInPlaneAngleQuartic(self.quartic)
3296 force.setAmoebaGlobalInPlaneAnglePentic(self.pentic)
3297 force.setAmoebaGlobalInPlaneAngleSextic(self.sextic)
3298
3299 for angleDict in angleList:
3300
3301 angle = angleDict['angle']
3302 isConstrained = angleDict['isConstrained']
3303
3304 type1 = data.atomType[data.atoms[angle[0]]]
3305 type2 = data.atomType[data.atoms[angle[1]]]
3306 type3 = data.atomType[data.atoms[angle[2]]]
3307
3308 for i in range(len(self.types1)):
3309
3310 types1 = self.types1[i]
3311 types2 = self.types2[i]
3312 types3 = self.types3[i]
3313
3314 if (type1 in types1 and type2 in types2 and type3 in types3) or (type1 in types3 and type2 in types2 and type3 in types1):
3315 angleDict['idealAngle'] = self.angle[i][0]
3316 if (isConstrained and self.k[i] != 0.0):
3317 addAngleConstraint(angle, self.angle[i][0]*math.pi/180.0, data, sys)
3318 else:
3319 force.addAngle(angle[0], angle[1], angle[2], angle[3], self.angle[i][0], self.k[i])
3320 break
3321
3322parsers["AmoebaAngleForce"] = AmoebaAngleGenerator.parseElement
3323
3324#=============================================================================================
3325# Generator for the AmoebaOutOfPlaneBend covalent force; also calls methods in the
3326# AmoebaAngleGenerator to generate the AmoebaAngleForce and
3327# AmoebaInPlaneAngleForce
3328#=============================================================================================
3329
3330## @private
3331class AmoebaOutOfPlaneBendGenerator(object):
3332
3333 #=============================================================================================
3334
3335 """An AmoebaOutOfPlaneBendGenerator constructs a AmoebaOutOfPlaneBendForce."""
3336
3337 #=============================================================================================
3338
3339 def __init__(self, forceField, type, cubic, quartic, pentic, sextic):
3340
3341 self.forceField = forceField
3342 self.type = type
3343 self.cubic = cubic
3344 self.quartic = quartic
3345 self.pentic = pentic
3346 self.sextic = sextic
3347
3348 self.types1 = []
3349 self.types2 = []
3350 self.types3 = []
3351 self.types4 = []
3352
3353 self.ks = []
3354
3355 #=============================================================================================
3356 # Local version of findAtomTypes needed since class indices are 0 (i.e., not recognized)
3357 # for types3 and 4
3358 #=============================================================================================
3359
3360 def findAtomTypes(self, forceField, node, num):
3361 """Parse the attributes on an XML tag to find the set of atom types for each atom it involves."""
3362 types = []
3363 attrib = node.attrib
3364 for i in range(num):
3365 if num == 1:
3366 suffix = ''
3367 else:
3368 suffix = str(i+1)
3369 classAttrib = 'class'+suffix
3370 if classAttrib in attrib:
3371 if attrib[classAttrib] in forceField._atomClasses:
3372 types.append(forceField._atomClasses[attrib[classAttrib]])
3373 else:
3374 types.append(set())
3375 return types
3376
3377 #=============================================================================================
3378
3379 @staticmethod
3380 def parseElement(element, forceField):
3381
3382 # <AmoebaOutOfPlaneBendForce type="ALLINGER" opbend-cubic="-0.014" opbend-quartic="5.6e-05" opbend-pentic="-7e-07" opbend-sextic="2.2e-08">
3383 # <Angle class1="2" class2="1" class3="0" class4="0" k="0.0531474541591"/>
3384 # <Angle class1="3" class2="1" class3="0" class4="0" k="0.0898536095496"/>
3385
3386 # get global scalar parameters
3387
3388 generator = AmoebaOutOfPlaneBendGenerator(forceField, element.attrib['type'],
3389 float(element.attrib['opbend-cubic']),
3390 float(element.attrib['opbend-quartic']),
3391 float(element.attrib['opbend-pentic']),
3392 float(element.attrib['opbend-sextic']))
3393
3394 forceField._forces.append(generator)
3395
3396 for angle in element.findall('Angle'):
3397 types = generator.findAtomTypes(forceField, angle, 4)
3398 if types is not None:
3399
3400 generator.types1.append(types[0])
3401 generator.types2.append(types[1])
3402 generator.types3.append(types[2])
3403 generator.types4.append(types[3])
3404
3405 generator.ks.append(float(angle.attrib['k']))
3406
3407 else:
3408 outputString = "AmoebaOutOfPlaneBendGenerator error getting types: %s %s %s %s." % (
3409 angle.attrib['class1'], angle.attrib['class2'], angle.attrib['class3'], angle.attrib['class4'])
3410 raise ValueError(outputString)
3411
3412 #=============================================================================================
3413 # Get middle atom in a angle
3414 # return index of middle atom or -1 if no middle is found
3415 # This method appears not to be needed since the angle[1] entry appears to always
3416 # be the middle atom. However, was unsure if this is guaranteed
3417 #=============================================================================================
3418
3419 def getMiddleAtom(self, angle, data):
3420
3421 # find atom shared by both bonds making up the angle
3422
3423 middleAtom = -1
3424 for atomIndex in angle:
3425 isMiddle = 0
3426 for bond in data.atomBonds[atomIndex]:
3427 atom1 = data.bonds[bond].atom1
3428 atom2 = data.bonds[bond].atom2
3429 if (atom1 != atomIndex):
3430 partner = atom1
3431 else:
3432 partner = atom2
3433 if (partner == angle[0] or partner == angle[1] or partner == angle[2]):
3434 isMiddle += 1
3435
3436 if (isMiddle == 2):
3437 return atomIndex
3438 return -1
3439
3440 #=============================================================================================
3441
3442 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
3443
3444 # get force
3445
3446 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
3447 existing = [f for f in existing if type(f) == mm.AmoebaOutOfPlaneBendForce]
3448 if len(existing) == 0:
3449 force = mm.AmoebaOutOfPlaneBendForce()
3450 sys.addForce(force)
3451 else:
3452 force = existing[0]
3453
3454 # set scalars
3455
3456 force.setAmoebaGlobalOutOfPlaneBendCubic( self.cubic)
3457 force.setAmoebaGlobalOutOfPlaneBendQuartic(self.quartic)
3458 force.setAmoebaGlobalOutOfPlaneBendPentic( self.pentic)
3459 force.setAmoebaGlobalOutOfPlaneBendSextic( self.sextic)
3460
3461 # this hash is used to insure the out-of-plane-bend bonds
3462 # are only added once
3463
3464 skipAtoms = dict()
3465
3466 # these lists are used in the partitioning of the angles into
3467 # angle and inPlane angles
3468
3469 inPlaneAngles = []
3470 nonInPlaneAngles = []
3471 nonInPlaneAnglesConstrained = []
3472 idealAngles = []*len(data.angles)
3473
3474 for (angle, isConstrained) in zip(data.angles, data.isAngleConstrained):
3475
3476 middleAtom = self.getMiddleAtom(angle, data)
3477 if (middleAtom > -1):
3478 middleType = data.atomType[data.atoms[middleAtom]]
3479 middleCovalency = len(data.atomBonds[middleAtom])
3480 else:
3481 middleType = -1
3482 middleCovalency = -1
3483
3484 # if middle atom has covalency of 3 and
3485 # the types of the middle atom and the partner atom (atom bonded to
3486 # middle atom, but not in angle) match types1 and types2, then
3487 # three out-of-plane bend angles are generated. Three in-plane angle
3488 # are also generated. If the conditions are not satisfied, the angle is marked as 'generic' angle (not a in-plane angle)
3489
3490 if (middleAtom > -1 and middleCovalency == 3 and middleAtom not in skipAtoms):
3491
3492 partners = []
3493 partnerSet = set()
3494 partnerTypes = []
3495 partnerK = []
3496
3497 for bond in data.atomBonds[middleAtom]:
3498 atom1 = data.bonds[bond].atom1
3499 atom2 = data.bonds[bond].atom2
3500 if (atom1 != middleAtom):
3501 partner = atom1
3502 else:
3503 partner = atom2
3504
3505 partnerType = data.atomType[data.atoms[partner]]
3506 for i in range(len(self.types1)):
3507 types1 = self.types1[i]
3508 types2 = self.types2[i]
3509 if (middleType in types2 and partnerType in types1):
3510 partners.append(partner)
3511 partnerSet.add(partner)
3512 partnerTypes.append(partnerType)
3513 partnerK.append(self.ks[i])
3514
3515 if (len(partners) == 3):
3516
3517 force.addOutOfPlaneBend(partners[0], middleAtom, partners[1], partners[2], partnerK[2])
3518 force.addOutOfPlaneBend(partners[0], middleAtom, partners[2], partners[1], partnerK[1])
3519 force.addOutOfPlaneBend(partners[1], middleAtom, partners[2], partners[0], partnerK[0])
3520
3521 # skipAtoms is used to insure angles are only included once
3522
3523 skipAtoms[middleAtom] = set()
3524 skipAtoms[middleAtom].add(partners[0])
3525 skipAtoms[middleAtom].add(partners[1])
3526 skipAtoms[middleAtom].add(partners[2])
3527
3528 # in-plane angle
3529
3530 angleDict = {}
3531 angleList = []
3532 angleList.append(angle[0])
3533 angleList.append(angle[1])
3534 angleList.append(angle[2])
3535 angleDict['angle'] = angleList
3536
3537 angleDict['isConstrained'] = 0
3538
3539 angleSet = set()
3540 angleSet.add(angle[0])
3541 angleSet.add(angle[1])
3542 angleSet.add(angle[2])
3543
3544 for atomIndex in partnerSet:
3545 if (atomIndex not in angleSet):
3546 angleList.append(atomIndex)
3547
3548 inPlaneAngles.append(angleDict)
3549
3550 else:
3551 angleDict = {}
3552 angleDict['angle'] = angle
3553 angleDict['isConstrained'] = isConstrained
3554 nonInPlaneAngles.append(angleDict)
3555 else:
3556 if (middleAtom > -1 and middleCovalency == 3 and middleAtom in skipAtoms):
3557
3558 partnerSet = skipAtoms[middleAtom]
3559
3560 angleDict = {}
3561
3562 angleList = []
3563 angleList.append(angle[0])
3564 angleList.append(angle[1])
3565 angleList.append(angle[2])
3566 angleDict['angle'] = angleList
3567
3568 angleDict['isConstrained'] = isConstrained
3569
3570 angleSet = set()
3571 angleSet.add(angle[0])
3572 angleSet.add(angle[1])
3573 angleSet.add(angle[2])
3574
3575 for atomIndex in partnerSet:
3576 if (atomIndex not in angleSet):
3577 angleList.append(atomIndex)
3578
3579 inPlaneAngles.append(angleDict)
3580
3581 else:
3582 angleDict = {}
3583 angleDict['angle'] = angle
3584 angleDict['isConstrained'] = isConstrained
3585 nonInPlaneAngles.append(angleDict)
3586
3587 # get AmoebaAngleGenerator and add AmoebaAngle and AmoebaInPlaneAngle forces
3588
3589 for force in self.forceField._forces:
3590 if (force.__class__.__name__ == 'AmoebaAngleGenerator'):
3591 force.createForcePostOpBendAngle(sys, data, nonbondedMethod, nonbondedCutoff, nonInPlaneAngles, args)
3592 force.createForcePostOpBendInPlaneAngle(sys, data, nonbondedMethod, nonbondedCutoff, inPlaneAngles, args)
3593
3594 for force in self.forceField._forces:
3595 if (force.__class__.__name__ == 'AmoebaStretchBendGenerator'):
3596 for angleDict in inPlaneAngles:
3597 nonInPlaneAngles.append(angleDict)
3598 force.createForcePostAmoebaBondForce(sys, data, nonbondedMethod, nonbondedCutoff, nonInPlaneAngles, args)
3599
3600parsers["AmoebaOutOfPlaneBendForce"] = AmoebaOutOfPlaneBendGenerator.parseElement
3601
3602#=============================================================================================
3603
3604## @private
3605class AmoebaTorsionGenerator(object):
3606
3607 #=============================================================================================
3608 """An AmoebaTorsionGenerator constructs a AmoebaTorsionForce."""
3609 #=============================================================================================
3610
3611 def __init__(self, torsionUnit):
3612
3613 self.torsionUnit = torsionUnit
3614
3615 self.types1 = []
3616 self.types2 = []
3617 self.types3 = []
3618 self.types4 = []
3619
3620 self.t1 = []
3621 self.t2 = []
3622 self.t3 = []
3623
3624 #=============================================================================================
3625
3626 @staticmethod
3627 def parseElement(element, forceField):
3628
3629 # <AmoebaTorsionForce torsionUnit="0.5">
3630 # <Torsion class1="3" class2="1" class3="2" class4="3" amp1="0.0" angle1="0.0" amp2="0.0" angle2="3.14159265359" amp3="0.0" angle3="0.0" />
3631 # <Torsion class1="3" class2="1" class3="2" class4="6" amp1="0.0" angle1="0.0" amp2="0.0" angle2="3.14159265359" amp3="-0.263592" angle3="0.0" />
3632
3633 generator = AmoebaTorsionGenerator(float(element.attrib['torsionUnit']))
3634 forceField._forces.append(generator)
3635
3636 # collect particle classes and t1,t2,t3,
3637 # where ti=[amplitude_i,angle_i]
3638
3639 for torsion in element.findall('Torsion'):
3640 types = forceField._findAtomTypes(torsion.attrib, 4)
3641 if None not in types:
3642
3643 generator.types1.append(types[0])
3644 generator.types2.append(types[1])
3645 generator.types3.append(types[2])
3646 generator.types4.append(types[3])
3647
3648 for ii in range(1,4):
3649 tInfo = []
3650 suffix = str(ii)
3651 ampName = 'amp' + suffix
3652 tInfo.append(float(torsion.attrib[ampName]))
3653
3654 angName = 'angle' + suffix
3655 tInfo.append(float(torsion.attrib[angName]))
3656
3657 if (ii == 1):
3658 generator.t1.append(tInfo)
3659 elif (ii == 2):
3660 generator.t2.append(tInfo)
3661 elif (ii == 3):
3662 generator.t3.append(tInfo)
3663
3664 else:
3665 outputString = "AmoebaTorsionGenerator: error getting types: %s %s %s %s" % (
3666 torsion.attrib['class1'],
3667 torsion.attrib['class2'],
3668 torsion.attrib['class3'],
3669 torsion.attrib['class4'])
3670 raise ValueError(outputString)
3671
3672 #=============================================================================================
3673
3674 def createForce(self, sys, data, nontorsionedMethod, nontorsionedCutoff, args):
3675
3676 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
3677 existing = [f for f in existing if type(f) == mm.PeriodicTorsionForce]
3678 if len(existing) == 0:
3679 force = mm.PeriodicTorsionForce()
3680 sys.addForce(force)
3681 else:
3682 force = existing[0]
3683
3684 for torsion in data.propers:
3685
3686 type1 = data.atomType[data.atoms[torsion[0]]]
3687 type2 = data.atomType[data.atoms[torsion[1]]]
3688 type3 = data.atomType[data.atoms[torsion[2]]]
3689 type4 = data.atomType[data.atoms[torsion[3]]]
3690
3691 for i in range(len(self.types1)):
3692
3693 types1 = self.types1[i]
3694 types2 = self.types2[i]
3695 types3 = self.types3[i]
3696 types4 = self.types4[i]
3697
3698 # match types in forward or reverse direction
3699
3700 if (type1 in types1 and type2 in types2 and type3 in types3 and type4 in types4) or (type4 in types1 and type3 in types2 and type2 in types3 and type1 in types4):
3701 if self.t1[i][0] != 0:
3702 force.addTorsion(torsion[0], torsion[1], torsion[2], torsion[3], 1, self.t1[i][1], self.t1[i][0])
3703 if self.t2[i][0] != 0:
3704 force.addTorsion(torsion[0], torsion[1], torsion[2], torsion[3], 2, self.t2[i][1], self.t2[i][0])
3705 if self.t3[i][0] != 0:
3706 force.addTorsion(torsion[0], torsion[1], torsion[2], torsion[3], 3, self.t3[i][1], self.t3[i][0])
3707 break
3708
3709parsers["AmoebaTorsionForce"] = AmoebaTorsionGenerator.parseElement
3710
3711#=============================================================================================
3712
3713## @private
3714class AmoebaPiTorsionGenerator(object):
3715
3716 #=============================================================================================
3717
3718 """An AmoebaPiTorsionGenerator constructs a AmoebaPiTorsionForce."""
3719
3720 #=============================================================================================
3721
3722 def __init__(self, piTorsionUnit):
3723 self.piTorsionUnit = piTorsionUnit
3724 self.types1 = []
3725 self.types2 = []
3726 self.k = []
3727
3728 #=============================================================================================
3729
3730 @staticmethod
3731 def parseElement(element, forceField):
3732
3733 # <AmoebaPiTorsionForce piTorsionUnit="1.0">
3734 # <PiTorsion class1="1" class2="3" k="28.6604" />
3735
3736 generator = AmoebaPiTorsionGenerator(float(element.attrib['piTorsionUnit']))
3737 forceField._forces.append(generator)
3738
3739 for piTorsion in element.findall('PiTorsion'):
3740 types = forceField._findAtomTypes(piTorsion.attrib, 2)
3741 if None not in types:
3742 generator.types1.append(types[0])
3743 generator.types2.append(types[1])
3744 generator.k.append(float(piTorsion.attrib['k']))
3745 else:
3746 outputString = "AmoebaPiTorsionGenerator: error getting types: %s %s " % (
3747 piTorsion.attrib['class1'],
3748 piTorsion.attrib['class2'])
3749 raise ValueError(outputString)
3750
3751 #=============================================================================================
3752
3753 def createForce(self, sys, data, nonpiTorsionedMethod, nonpiTorsionedCutoff, args):
3754
3755 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
3756 existing = [f for f in existing if type(f) == mm.AmoebaPiTorsionForce]
3757
3758 if len(existing) == 0:
3759 force = mm.AmoebaPiTorsionForce()
3760 sys.addForce(force)
3761 else:
3762 force = existing[0]
3763
3764 for bond in data.bonds:
3765
3766 # search for bonds with both atoms in bond having covalency == 3
3767
3768 atom1 = bond.atom1
3769 atom2 = bond.atom2
3770
3771 if (len(data.atomBonds[atom1]) == 3 and len(data.atomBonds[atom2]) == 3):
3772
3773 type1 = data.atomType[data.atoms[atom1]]
3774 type2 = data.atomType[data.atoms[atom2]]
3775
3776 for i in range(len(self.types1)):
3777
3778 types1 = self.types1[i]
3779 types2 = self.types2[i]
3780
3781 if (type1 in types1 and type2 in types2) or (type1 in types2 and type2 in types1):
3782
3783 # piTorsionAtom1, piTorsionAtom2 are the atoms bonded to atom1, excluding atom2
3784 # piTorsionAtom5, piTorsionAtom6 are the atoms bonded to atom2, excluding atom1
3785
3786 piTorsionAtom1 = -1
3787 piTorsionAtom2 = -1
3788 piTorsionAtom3 = atom1
3789
3790 piTorsionAtom4 = atom2
3791 piTorsionAtom5 = -1
3792 piTorsionAtom6 = -1
3793
3794 for bond in data.atomBonds[atom1]:
3795 bondedAtom1 = data.bonds[bond].atom1
3796 bondedAtom2 = data.bonds[bond].atom2
3797 if (bondedAtom1 != atom1):
3798 b1 = bondedAtom1
3799 else:
3800 b1 = bondedAtom2
3801 if (b1 != atom2):
3802 if (piTorsionAtom1 == -1):
3803 piTorsionAtom1 = b1
3804 else:
3805 piTorsionAtom2 = b1
3806
3807 for bond in data.atomBonds[atom2]:
3808 bondedAtom1 = data.bonds[bond].atom1
3809 bondedAtom2 = data.bonds[bond].atom2
3810 if (bondedAtom1 != atom2):
3811 b1 = bondedAtom1
3812 else:
3813 b1 = bondedAtom2
3814
3815 if (b1 != atom1):
3816 if (piTorsionAtom5 == -1):
3817 piTorsionAtom5 = b1
3818 else:
3819 piTorsionAtom6 = b1
3820
3821 force.addPiTorsion(piTorsionAtom1, piTorsionAtom2, piTorsionAtom3, piTorsionAtom4, piTorsionAtom5, piTorsionAtom6, self.k[i])
3822
3823parsers["AmoebaPiTorsionForce"] = AmoebaPiTorsionGenerator.parseElement
3824
3825#=============================================================================================
3826
3827## @private
3828class AmoebaTorsionTorsionGenerator(object):
3829
3830 #=============================================================================================
3831 """An AmoebaTorsionTorsionGenerator constructs a AmoebaTorsionTorsionForce."""
3832 #=============================================================================================
3833
3834 def __init__(self):
3835
3836 self.types1 = []
3837 self.types2 = []
3838 self.types3 = []
3839 self.types4 = []
3840 self.types5 = []
3841
3842 self.gridIndex = []
3843
3844 self.grids = []
3845
3846 #=============================================================================================
3847
3848 @staticmethod
3849 def parseElement(element, forceField):
3850
3851 generator = AmoebaTorsionTorsionGenerator()
3852 forceField._forces.append(generator)
3853 maxGridIndex = -1
3854
3855 # <AmoebaTorsionTorsionForce >
3856 # <TorsionTorsion class1="3" class2="1" class3="2" class4="3" class5="1" grid="0" nx="25" ny="25" />
3857
3858 for torsionTorsion in element.findall('TorsionTorsion'):
3859 types = forceField._findAtomTypes(torsionTorsion.attrib, 5)
3860 if None not in types:
3861
3862 generator.types1.append(types[0])
3863 generator.types2.append(types[1])
3864 generator.types3.append(types[2])
3865 generator.types4.append(types[3])
3866 generator.types5.append(types[4])
3867
3868 gridIndex = int(torsionTorsion.attrib['grid'])
3869 if (gridIndex > maxGridIndex):
3870 maxGridIndex = gridIndex
3871
3872 generator.gridIndex.append(gridIndex)
3873 else:
3874 outputString = "AmoebaTorsionTorsionGenerator: error getting types: %s %s %s %s %s" % (
3875 torsionTorsion.attrib['class1'],
3876 torsionTorsion.attrib['class2'],
3877 torsionTorsion.attrib['class3'],
3878 torsionTorsion.attrib['class4'],
3879 torsionTorsion.attrib['class5'] )
3880 raise ValueError(outputString)
3881
3882 # load grid
3883
3884 # xml source
3885
3886 # <TorsionTorsionGrid grid="0" nx="25" ny="25" >
3887 # <Grid angle1="-180.00" angle2="-180.00" f="0.0" fx="2.31064374824e-05" fy="0.0" fxy="-0.0052801799672" />
3888 # <Grid angle1="-165.00" angle2="-180.00" f="-0.66600912" fx="-0.06983370052" fy="-0.075058725744" fxy="-0.0044462732032" />
3889
3890 # output grid:
3891
3892 # grid[x][y][0] = x value
3893 # grid[x][y][1] = y value
3894 # grid[x][y][2] = function value
3895 # grid[x][y][3] = dfdx value
3896 # grid[x][y][4] = dfdy value
3897 # grid[x][y][5] = dfd(xy) value
3898
3899 maxGridIndex += 1
3900 generator.grids = maxGridIndex*[]
3901 for torsionTorsionGrid in element.findall('TorsionTorsionGrid'):
3902
3903 gridIndex = int(torsionTorsionGrid.attrib[ "grid"])
3904 nx = int(torsionTorsionGrid.attrib[ "nx"])
3905 ny = int(torsionTorsionGrid.attrib[ "ny"])
3906
3907 grid = []
3908 gridCol = []
3909
3910 gridColIndex = 0
3911
3912 for gridEntry in torsionTorsionGrid.findall('Grid'):
3913
3914 gridRow = []
3915 gridRow.append(float(gridEntry.attrib['angle1']))
3916 gridRow.append(float(gridEntry.attrib['angle2']))
3917 gridRow.append(float(gridEntry.attrib['f']))
3918 if 'fx' in gridEntry.attrib:
3919 gridRow.append(float(gridEntry.attrib['fx']))
3920 gridRow.append(float(gridEntry.attrib['fy']))
3921 gridRow.append(float(gridEntry.attrib['fxy']))
3922 gridCol.append(gridRow)
3923
3924 gridColIndex += 1
3925 if (gridColIndex == nx):
3926 grid.append(gridCol)
3927 gridCol = []
3928 gridColIndex = 0
3929
3930
3931 if (gridIndex == len(generator.grids)):
3932 generator.grids.append(grid)
3933 else:
3934 while(len(generator.grids) < gridIndex):
3935 generator.grids.append([])
3936 generator.grids[gridIndex] = grid
3937
3938 #=============================================================================================
3939
3940 def getChiralAtomIndex(self, data, sys, atomB, atomC, atomD):
3941
3942 chiralAtomIndex = -1
3943
3944 # if atomC has four bonds, find the
3945 # two bonds that do not include atomB and atomD
3946 # set chiralAtomIndex to one of these, if they are
3947 # not the same atom(type/mass)
3948
3949 if (len(data.atomBonds[atomC]) == 4):
3950 atomE = -1
3951 atomF = -1
3952 for bond in data.atomBonds[atomC]:
3953 bondedAtom1 = data.bonds[bond].atom1
3954 bondedAtom2 = data.bonds[bond].atom2
3955 hit = -1
3956 if ( bondedAtom1 == atomC and bondedAtom2 != atomB and bondedAtom2 != atomD):
3957 hit = bondedAtom2
3958 elif (bondedAtom2 == atomC and bondedAtom1 != atomB and bondedAtom1 != atomD):
3959 hit = bondedAtom1
3960
3961 if (hit > -1):
3962 if (atomE == -1):
3963 atomE = hit
3964 else:
3965 atomF = hit
3966
3967 # raise error if atoms E or F not found
3968
3969 if (atomE == -1 or atomF == -1):
3970 outputString = "getChiralAtomIndex: error getting bonded partners of atomC=%s %d %s" % (atomC.name, atomC.resiude.index, atomC.resiude.name,)
3971 raise ValueError(outputString)
3972
3973 # check for different type/mass between atoms E & F
3974
3975 typeE = int(data.atomType[data.atoms[atomE]])
3976 typeF = int(data.atomType[data.atoms[atomF]])
3977 if (typeE > typeF):
3978 chiralAtomIndex = atomE
3979 if (typeF > typeE):
3980 chiralAtomIndex = atomF
3981
3982 massE = sys.getParticleMass(atomE)/unit.dalton
3983 massF = sys.getParticleMass(atomE)/unit.dalton
3984 if (massE > massF):
3985 chiralAtomIndex = massE
3986 if (massF > massE):
3987 chiralAtomIndex = massF
3988
3989 return chiralAtomIndex
3990
3991 #=============================================================================================
3992
3993 def createForce(self, sys, data, nonpiTorsionedMethod, nonpiTorsionedCutoff, args):
3994
3995 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
3996 existing = [f for f in existing if type(f) == mm.AmoebaTorsionTorsionForce]
3997
3998 if len(existing) == 0:
3999 force = mm.AmoebaTorsionTorsionForce()
4000 sys.addForce(force)
4001 else:
4002 force = existing[0]
4003
4004 for angle in data.angles:
4005
4006 # search for bitorsions; based on TINKER subroutine bitors()
4007
4008 ib = angle[0]
4009 ic = angle[1]
4010 id = angle[2]
4011
4012 for bondIndex in data.atomBonds[ib]:
4013 bondedAtom1 = data.bonds[bondIndex].atom1
4014 bondedAtom2 = data.bonds[bondIndex].atom2
4015 if (bondedAtom1 != ib):
4016 ia = bondedAtom1
4017 else:
4018 ia = bondedAtom2
4019
4020 if (ia != ic and ia != id):
4021 for bondIndex in data.atomBonds[id]:
4022 bondedAtom1 = data.bonds[bondIndex].atom1
4023 bondedAtom2 = data.bonds[bondIndex].atom2
4024 if (bondedAtom1 != id):
4025 ie = bondedAtom1
4026 else:
4027 ie = bondedAtom2
4028
4029 if (ie != ic and ie != ib and ie != ia):
4030
4031 # found candidate set of atoms
4032 # check if types match in order or reverse order
4033
4034 type1 = data.atomType[data.atoms[ia]]
4035 type2 = data.atomType[data.atoms[ib]]
4036 type3 = data.atomType[data.atoms[ic]]
4037 type4 = data.atomType[data.atoms[id]]
4038 type5 = data.atomType[data.atoms[ie]]
4039
4040 for i in range(len(self.types1)):
4041
4042 types1 = self.types1[i]
4043 types2 = self.types2[i]
4044 types3 = self.types3[i]
4045 types4 = self.types4[i]
4046 types5 = self.types5[i]
4047
4048 # match in order
4049
4050 if (type1 in types1 and type2 in types2 and type3 in types3 and type4 in types4 and type5 in types5):
4051 chiralAtomIndex = self.getChiralAtomIndex(data, sys, ib, ic, id)
4052 force.addTorsionTorsion(ia, ib, ic, id, ie, chiralAtomIndex, self.gridIndex[i])
4053
4054 # match in reverse order
4055
4056 elif (type5 in types1 and type4 in types2 and type3 in types3 and type2 in types4 and type1 in types5):
4057 chiralAtomIndex = self.getChiralAtomIndex(data, sys, ib, ic, id)
4058 force.addTorsionTorsion(ie, id, ic, ib, ia, chiralAtomIndex, self.gridIndex[i])
4059
4060 # set grids
4061
4062 for (index, grid) in enumerate(self.grids):
4063 force.setTorsionTorsionGrid(index, grid)
4064
4065parsers["AmoebaTorsionTorsionForce"] = AmoebaTorsionTorsionGenerator.parseElement
4066
4067#=============================================================================================
4068
4069## @private
4070class AmoebaStretchBendGenerator(object):
4071
4072 #=============================================================================================
4073 """An AmoebaStretchBendGenerator constructs a AmoebaStretchBendForce."""
4074 #=============================================================================================
4075
4076 def __init__(self):
4077
4078 self.types1 = []
4079 self.types2 = []
4080 self.types3 = []
4081
4082 self.k1 = []
4083 self.k2 = []
4084
4085 #=============================================================================================
4086
4087 @staticmethod
4088 def parseElement(element, forceField):
4089 generator = AmoebaStretchBendGenerator()
4090 forceField._forces.append(generator)
4091
4092 # <AmoebaStretchBendForce stretchBendUnit="1.0">
4093 # <StretchBend class1="2" class2="1" class3="3" k1="5.25776946506" k2="5.25776946506" />
4094 # <StretchBend class1="2" class2="1" class3="4" k1="3.14005676385" k2="3.14005676385" />
4095
4096 for stretchBend in element.findall('StretchBend'):
4097 types = forceField._findAtomTypes(stretchBend.attrib, 3)
4098 if None not in types:
4099
4100 generator.types1.append(types[0])
4101 generator.types2.append(types[1])
4102 generator.types3.append(types[2])
4103
4104 generator.k1.append(float(stretchBend.attrib['k1']))
4105 generator.k2.append(float(stretchBend.attrib['k2']))
4106
4107 else:
4108 outputString = "AmoebaStretchBendGenerator : error getting types: %s %s %s" % (
4109 stretchBend.attrib['class1'],
4110 stretchBend.attrib['class2'],
4111 stretchBend.attrib['class3'])
4112 raise ValueError(outputString)
4113
4114 #=============================================================================================
4115
4116 # The setup of this force is dependent on AmoebaBondForce and AmoebaAngleForce
4117 # having been called since the ideal bond lengths and angle are needed here.
4118 # As a conseqeunce, createForce() is not implemented since it is not guaranteed that the generator for
4119 # AmoebaBondForce and AmoebaAngleForce have been called prior to AmoebaStretchBendGenerator().
4120 # Instead, createForcePostAmoebaBondForce() is called
4121 # after the generators for AmoebaBondForce and AmoebaAngleForce have been called
4122
4123 #=============================================================================================
4124
4125 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
4126 pass
4127
4128 #=============================================================================================
4129
4130 # Note: request for constrained bonds is ignored.
4131
4132 #=============================================================================================
4133
4134 def createForcePostAmoebaBondForce(self, sys, data, nonbondedMethod, nonbondedCutoff, angleList, args):
4135
4136 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
4137 existing = [f for f in existing if type(f) == mm.AmoebaStretchBendForce]
4138 if len(existing) == 0:
4139 force = mm.AmoebaStretchBendForce()
4140 sys.addForce(force)
4141 else:
4142 force = existing[0]
4143
4144 for angleDict in angleList:
4145
4146 angle = angleDict['angle']
4147 if ('isConstrained' in angleDict):
4148 isConstrained = angleDict['isConstrained']
4149 else:
4150 isConstrained = 0
4151
4152 type1 = data.atomType[data.atoms[angle[0]]]
4153 type2 = data.atomType[data.atoms[angle[1]]]
4154 type3 = data.atomType[data.atoms[angle[2]]]
4155
4156 radian = 57.2957795130
4157 for i in range(len(self.types1)):
4158
4159 types1 = self.types1[i]
4160 types2 = self.types2[i]
4161 types3 = self.types3[i]
4162
4163 # match types
4164 # get ideal bond lengths, bondAB, bondCB
4165 # get ideal angle
4166
4167 if (type2 in types2 and ((type1 in types1 and type3 in types3) or (type3 in types1 and type1 in types3))):
4168 bondAB = -1.0
4169 bondCB = -1.0
4170 swap = 0
4171 for bond in data.atomBonds[angle[1]]:
4172 atom1 = data.bonds[bond].atom1
4173 atom2 = data.bonds[bond].atom2
4174 length = data.bonds[bond].length
4175 if (atom1 == angle[0]):
4176 bondAB = length
4177 if (atom1 == angle[2]):
4178 bondCB = length
4179 if (atom2 == angle[2]):
4180 bondCB = length
4181 if (atom2 == angle[0]):
4182 bondAB = length
4183
4184 # check that ideal angle and bonds are set
4185
4186 if ('idealAngle' not in angleDict):
4187
4188 outputString = "AmoebaStretchBendGenerator: ideal angle is not set for following entry:\n"
4189 outputString += " types: %5s %5s %5s atoms: " % (type1, type2, type3)
4190 outputString += getAtomPrint( data, angle[0] ) + ' '
4191 outputString += getAtomPrint( data, angle[1] ) + ' '
4192 outputString += getAtomPrint( data, angle[2] )
4193 raise ValueError(outputString)
4194
4195 elif (bondAB < 0 or bondCB < 0):
4196
4197 outputString = "AmoebaStretchBendGenerator: bonds not set: %15.7e %15.7e. for following entry:" % (bondAB, bondCB)
4198 outputString += " types: [%5s %5s %5s] atoms: " % (type1, type2, type3)
4199 outputString += getAtomPrint( data, angle[0] ) + ' '
4200 outputString += getAtomPrint( data, angle[1] ) + ' '
4201 outputString += getAtomPrint( data, angle[2] )
4202 raise ValueError(outputString)
4203
4204 else:
4205 force.addStretchBend(angle[0], angle[1], angle[2], bondAB, bondCB, angleDict['idealAngle']/radian, self.k1[i], self.k2[i])
4206
4207 break
4208
4209parsers["AmoebaStretchBendForce"] = AmoebaStretchBendGenerator.parseElement
4210
4211#=============================================================================================
4212
4213## @private
4214class AmoebaVdwGenerator(object):
4215
4216 """A AmoebaVdwGenerator constructs a AmoebaVdwForce."""
4217
4218 #=============================================================================================
4219
4220 def __init__(self, type, radiusrule, radiustype, radiussize, epsilonrule, vdw13Scale, vdw14Scale, vdw15Scale):
4221
4222 self.type = type
4223
4224 self.radiusrule = radiusrule
4225 self.radiustype = radiustype
4226 self.radiussize = radiussize
4227
4228 self.epsilonrule = epsilonrule
4229
4230 self.vdw13Scale = vdw13Scale
4231 self.vdw14Scale = vdw14Scale
4232 self.vdw15Scale = vdw15Scale
4233
4234 #=============================================================================================
4235
4236 @staticmethod
4237 def parseElement(element, forceField):
4238
4239 # <AmoebaVdwForce type="BUFFERED-14-7" radiusrule="CUBIC-MEAN" radiustype="R-MIN" radiussize="DIAMETER" epsilonrule="HHG" vdw-13-scale="0.0" vdw-14-scale="1.0" vdw-15-scale="1.0" >
4240 # <Vdw class="1" sigma="0.371" epsilon="0.46024" reduction="1.0" />
4241 # <Vdw class="2" sigma="0.382" epsilon="0.422584" reduction="1.0" />
4242
4243 existing = [f for f in forceField._forces if isinstance(f, AmoebaVdwGenerator)]
4244 if len(existing) == 0:
4245 generator = AmoebaVdwGenerator(element.attrib['type'], element.attrib['radiusrule'], element.attrib['radiustype'], element.attrib['radiussize'], element.attrib['epsilonrule'],
4246 float(element.attrib['vdw-13-scale']), float(element.attrib['vdw-14-scale']), float(element.attrib['vdw-15-scale']))
4247 forceField.registerGenerator(generator)
4248 generator.params = ForceField._AtomTypeParameters(forceField, 'AmoebaVdwForce', 'Vdw', ('sigma', 'epsilon', 'reduction'))
4249 else:
4250 # Multiple <AmoebaVdwForce> tags were found, probably in different files. Simply add more types to the existing one.
4251 generator = existing[0]
4252 if abs(generator.vdw13Scale - float(element.attrib['vdw-13-scale'])) > NonbondedGenerator.SCALETOL or \
4253 abs(generator.vdw14Scale - float(element.attrib['vdw-14-scale'])) > NonbondedGenerator.SCALETOL or \
4254 abs(generator.vdw15Scale - float(element.attrib['vdw-15-scale'])) > NonbondedGenerator.SCALETOL:
4255 raise ValueError('Found multiple AmoebaVdwForce tags with different scale factors')
4256 if generator.radiusrule != element.attrib['radiusrule'] or generator.epsilonrule != element.attrib['epsilonrule'] or \
4257 generator.radiustype != element.attrib['radiustype'] or generator.radiussize != element.attrib['radiussize']:
4258 raise ValueError('Found multiple AmoebaVdwForce tags with different combining rules')
4259 generator.params.parseDefinitions(element)
4260 two_six = 1.122462048309372
4261
4262 #=============================================================================================
4263
4264 @staticmethod
4265 def getBondedParticleSets(sys, data):
4266
4267 bondedParticleSets = [set() for i in range(len(data.atoms))]
4268 bondIndices = _findBondsForExclusions(data, sys)
4269 for atom1, atom2 in bondIndices:
4270 bondedParticleSets[atom1].add(atom2)
4271 bondedParticleSets[atom2].add(atom1)
4272 return bondedParticleSets
4273
4274 #=============================================================================================
4275
4276 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
4277
4278 sigmaMap = {'ARITHMETIC':1, 'GEOMETRIC':1, 'CUBIC-MEAN':1}
4279 epsilonMap = {'ARITHMETIC':1, 'GEOMETRIC':1, 'HARMONIC':1, 'HHG':1}
4280
4281 force = mm.AmoebaVdwForce()
4282 sys.addForce(force)
4283
4284 # sigma and epsilon combining rules
4285
4286 if ('sigmaCombiningRule' in args):
4287 sigmaRule = args['sigmaCombiningRule'].upper()
4288 if (sigmaRule.upper() in sigmaMap):
4289 force.setSigmaCombiningRule(sigmaRule.upper())
4290 else:
4291 stringList = ' ' . join(str(x) for x in sigmaMap.keys())
4292 raise ValueError( "AmoebaVdwGenerator: sigma combining rule %s not recognized; valid values are %s; using default." % (sigmaRule, stringList) )
4293 else:
4294 force.setSigmaCombiningRule(self.radiusrule)
4295
4296 if ('epsilonCombiningRule' in args):
4297 epsilonRule = args['epsilonCombiningRule'].upper()
4298 if (epsilonRule.upper() in epsilonMap):
4299 force.setEpsilonCombiningRule(epsilonRule.upper())
4300 else:
4301 stringList = ' ' . join(str(x) for x in epsilonMap.keys())
4302 raise ValueError( "AmoebaVdwGenerator: epsilon combining rule %s not recognized; valid values are %s; using default." % (epsilonRule, stringList) )
4303 else:
4304 force.setEpsilonCombiningRule(self.epsilonrule)
4305
4306 # cutoff
4307
4308 if ('vdwCutoff' in args):
4309 force.setCutoff(args['vdwCutoff'])
4310 else:
4311 force.setCutoff(nonbondedCutoff)
4312
4313 # dispersion correction
4314
4315 if ('useDispersionCorrection' in args):
4316 force.setUseDispersionCorrection(bool(args['useDispersionCorrection']))
4317
4318 if (nonbondedMethod == PME):
4319 force.setNonbondedMethod(mm.AmoebaVdwForce.CutoffPeriodic)
4320
4321 # add particles to force
4322
4323 sigmaScale = 1
4324 if self.radiustype == 'SIGMA':
4325 sigmaScale = 1.122462048309372
4326 if self.radiussize == 'DIAMETER':
4327 sigmaScale = 0.5
4328 for (i, atom) in enumerate(data.atoms):
4329 values = self.params.getAtomParameters(atom, data)
4330 # ivIndex = index of bonded partner for hydrogens; otherwise ivIndex = particle index
4331
4332 ivIndex = i
4333 if atom.element == elem.hydrogen and len(data.atomBonds[i]) == 1:
4334 bondIndex = data.atomBonds[i][0]
4335 if (data.bonds[bondIndex].atom1 == i):
4336 ivIndex = data.bonds[bondIndex].atom2
4337 else:
4338 ivIndex = data.bonds[bondIndex].atom1
4339
4340 force.addParticle(ivIndex, values[0]*sigmaScale, values[1], values[2])
4341
4342 # set combining rules
4343
4344 # set particle exclusions: self, 1-2 and 1-3 bonds
4345 # (1) collect in bondedParticleSets[i], 1-2 indices for all bonded partners of particle i
4346 # (2) add 1-2,1-3 and self to exclusion set
4347
4348 bondedParticleSets = AmoebaVdwGenerator.getBondedParticleSets(sys, data)
4349
4350 for (i,atom) in enumerate(data.atoms):
4351
4352 # 1-2 partners
4353
4354 exclusionSet = bondedParticleSets[i].copy()
4355
4356 # 1-3 partners
4357
4358 if (self.vdw13Scale == 0.0):
4359 for bondedParticle in bondedParticleSets[i]:
4360 exclusionSet = exclusionSet.union(bondedParticleSets[bondedParticle])
4361
4362 # self
4363
4364 exclusionSet.add(i)
4365
4366 force.setParticleExclusions(i, tuple(exclusionSet))
4367
4368parsers["AmoebaVdwForce"] = AmoebaVdwGenerator.parseElement
4369
4370#=============================================================================================
4371
4372## @private
4373class AmoebaMultipoleGenerator(object):
4374
4375 #=============================================================================================
4376
4377 """A AmoebaMultipoleGenerator constructs a AmoebaMultipoleForce."""
4378
4379 #=============================================================================================
4380
4381 def __init__(self, forceField):
4382 self.forceField = forceField
4383 self.typeMap = {}
4384
4385 #=============================================================================================
4386 # Set axis type
4387 #=============================================================================================
4388
4389 @staticmethod
4390 def setAxisType(kIndices):
4391
4392 # set axis type
4393
4394 kIndicesLen = len(kIndices)
4395 if (kIndicesLen > 3):
4396 ky = kIndices[3]
4397 else:
4398 ky = 0
4399
4400 if (kIndicesLen > 2):
4401 kx = kIndices[2]
4402 else:
4403 kx = 0
4404
4405 if (kIndicesLen > 1):
4406 kz = kIndices[1]
4407 else:
4408 kz = 0
4409
4410 while(len(kIndices) < 4):
4411 kIndices.append(0)
4412
4413 axisType = mm.AmoebaMultipoleForce.ZThenX
4414 if (kz == 0):
4415 axisType = mm.AmoebaMultipoleForce.NoAxisType
4416 if (kz != 0 and kx == 0):
4417 axisType = mm.AmoebaMultipoleForce.ZOnly
4418 if (kz < 0 or kx < 0):
4419 axisType = mm.AmoebaMultipoleForce.Bisector
4420 if (kx < 0 and ky < 0):
4421 axisType = mm.AmoebaMultipoleForce.ZBisect
4422 if (kz < 0 and kx < 0 and ky < 0):
4423 axisType = mm.AmoebaMultipoleForce.ThreeFold
4424
4425 kIndices[1] = abs(kz)
4426 kIndices[2] = abs(kx)
4427 kIndices[3] = abs(ky)
4428
4429 return axisType
4430
4431 #=============================================================================================
4432
4433 @staticmethod
4434 def parseElement(element, forceField):
4435
4436 # <AmoebaMultipoleForce direct11Scale="0.0" direct12Scale="1.0" direct13Scale="1.0" direct14Scale="1.0" mpole12Scale="0.0" mpole13Scale="0.0" mpole14Scale="0.4" mpole15Scale="0.8" mutual11Scale="1.0" mutual12Scale="1.0" mutual13Scale="1.0" mutual14Scale="1.0" polar12Scale="0.0" polar13Scale="0.0" polar14Intra="0.5" polar14Scale="1.0" polar15Scale="1.0" >
4437 # <Multipole class="1" kz="2" kx="4" c0="-0.22620" d1="0.08214" d2="0.00000" d3="0.34883" q11="0.11775" q21="0.00000" q22="-1.02185" q31="-0.17555" q32="0.00000" q33="0.90410" />
4438 # <Multipole class="2" kz="1" kx="3" c0="-0.15245" d1="0.19517" d2="0.00000" d3="0.19687" q11="-0.20677" q21="0.00000" q22="-0.48084" q31="-0.01672" q32="0.00000" q33="0.68761" />
4439
4440 existing = [f for f in forceField._forces if isinstance(f, AmoebaMultipoleGenerator)]
4441 if len(existing) == 0:
4442 generator = AmoebaMultipoleGenerator(forceField)
4443 forceField.registerGenerator(generator)
4444 else:
4445 # Multiple <AmoebaMultipoleForce> tags were found, probably in different files. Simply add more types to the existing one.
4446 generator = existing[0]
4447
4448 # set type map: [ kIndices, multipoles, AMOEBA/OpenMM axis type]
4449
4450 for atom in element.findall('Multipole'):
4451 types = forceField._findAtomTypes(atom.attrib, 1)
4452 if None not in types:
4453
4454 # k-indices not provided default to 0
4455
4456 kIndices = [int(atom.attrib['type'])]
4457
4458 kStrings = [ 'kz', 'kx', 'ky' ]
4459 for kString in kStrings:
4460 try:
4461 if (atom.attrib[kString]):
4462 kIndices.append(int(atom.attrib[kString]))
4463 except:
4464 pass
4465
4466 # set axis type based on k-Indices
4467
4468 axisType = AmoebaMultipoleGenerator.setAxisType(kIndices)
4469
4470 # set multipole
4471
4472 charge = float(atom.attrib['c0'])
4473
4474 conversion = 1.0
4475 dipole = [ conversion*float(atom.attrib['d1']), conversion*float(atom.attrib['d2']), conversion*float(atom.attrib['d3'])]
4476
4477 quadrupole = []
4478 quadrupole.append(conversion*float(atom.attrib['q11']))
4479 quadrupole.append(conversion*float(atom.attrib['q21']))
4480 quadrupole.append(conversion*float(atom.attrib['q31']))
4481 quadrupole.append(conversion*float(atom.attrib['q21']))
4482 quadrupole.append(conversion*float(atom.attrib['q22']))
4483 quadrupole.append(conversion*float(atom.attrib['q32']))
4484 quadrupole.append(conversion*float(atom.attrib['q31']))
4485 quadrupole.append(conversion*float(atom.attrib['q32']))
4486 quadrupole.append(conversion*float(atom.attrib['q33']))
4487
4488 for t in types[0]:
4489 if (t not in generator.typeMap):
4490 generator.typeMap[t] = []
4491
4492 valueMap = dict()
4493 valueMap['classIndex'] = atom.attrib['type']
4494 valueMap['kIndices'] = kIndices
4495 valueMap['charge'] = charge
4496 valueMap['dipole'] = dipole
4497 valueMap['quadrupole'] = quadrupole
4498 valueMap['axisType'] = axisType
4499 generator.typeMap[t].append(valueMap)
4500
4501 else:
4502 outputString = "AmoebaMultipoleGenerator: error getting type for multipole: %s" % (atom.attrib['class'])
4503 raise ValueError(outputString)
4504
4505 # polarization parameters
4506
4507 for atom in element.findall('Polarize'):
4508 types = forceField._findAtomTypes(atom.attrib, 1)
4509 if None not in types:
4510
4511 classIndex = atom.attrib['type']
4512 polarizability = float(atom.attrib['polarizability'])
4513 thole = float(atom.attrib['thole'])
4514 if (thole == 0):
4515 pdamp = 0
4516 else:
4517 pdamp = pow(polarizability, 1.0/6.0)
4518
4519 pgrpMap = dict()
4520 for index in range(1, 7):
4521 pgrp = 'pgrp' + str(index)
4522 if (pgrp in atom.attrib):
4523 pgrpMap[int(atom.attrib[pgrp])] = -1
4524
4525 for t in types[0]:
4526 if (t not in generator.typeMap):
4527 outputString = "AmoebaMultipoleGenerator: polarize type not present: %s" % (atom.attrib['type'])
4528 raise ValueError(outputString)
4529 else:
4530 typeMapList = generator.typeMap[t]
4531 hit = 0
4532 for (ii, typeMap) in enumerate(typeMapList):
4533
4534 if (typeMap['classIndex'] == classIndex):
4535 typeMap['polarizability'] = polarizability
4536 typeMap['thole'] = thole
4537 typeMap['pdamp'] = pdamp
4538 typeMap['pgrpMap'] = pgrpMap
4539 typeMapList[ii] = typeMap
4540 hit = 1
4541
4542 if (hit == 0):
4543 outputString = "AmoebaMultipoleGenerator: error getting type for polarize: class index=%s not in multipole list?" % (atom.attrib['class'])
4544 raise ValueError(outputString)
4545
4546 else:
4547 outputString = "AmoebaMultipoleGenerator: error getting type for polarize: %s" % (atom.attrib['class'])
4548 raise ValueError(outputString)
4549
4550 #=============================================================================================
4551
4552 def setPolarGroups(self, data, bonded12ParticleSets, force):
4553
4554 for (atomIndex, atom) in enumerate(data.atoms):
4555
4556 # assign multipole parameters via only 1-2 connected atoms
4557
4558 multipoleDict = atom.multipoleDict
4559 pgrpMap = multipoleDict['pgrpMap']
4560 bondedAtomIndices = bonded12ParticleSets[atomIndex]
4561 atom.stage = -1
4562 atom.polarizationGroupSet = list()
4563 atom.polarizationGroups[atomIndex] = 1
4564 for bondedAtomIndex in bondedAtomIndices:
4565 bondedAtomType = int(data.atomType[data.atoms[bondedAtomIndex]])
4566 bondedAtom = data.atoms[bondedAtomIndex]
4567 if (bondedAtomType in pgrpMap):
4568 atom.polarizationGroups[bondedAtomIndex] = 1
4569 bondedAtom.polarizationGroups[atomIndex] = 1
4570
4571 # pgrp11
4572
4573 for (atomIndex, atom) in enumerate(data.atoms):
4574
4575 if (len( data.atoms[atomIndex].polarizationGroupSet) > 0):
4576 continue
4577
4578 group = set()
4579 visited = set()
4580 notVisited = set()
4581 for pgrpAtomIndex in atom.polarizationGroups:
4582 group.add(pgrpAtomIndex)
4583 notVisited.add(pgrpAtomIndex)
4584 visited.add(atomIndex)
4585 while(len(notVisited) > 0):
4586 nextAtom = notVisited.pop()
4587 if (nextAtom not in visited):
4588 visited.add(nextAtom)
4589 for ii in data.atoms[nextAtom].polarizationGroups:
4590 group.add(ii)
4591 if (ii not in visited):
4592 notVisited.add(ii)
4593
4594 pGroup = group
4595 for pgrpAtomIndex in group:
4596 data.atoms[pgrpAtomIndex].polarizationGroupSet.append(pGroup)
4597
4598 for (atomIndex, atom) in enumerate(data.atoms):
4599 atom.polarizationGroupSet[0] = sorted(atom.polarizationGroupSet[0])
4600 force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.PolarizationCovalent11, atom.polarizationGroupSet[0])
4601
4602 # pgrp12
4603
4604 for (atomIndex, atom) in enumerate(data.atoms):
4605
4606 if (len( data.atoms[atomIndex].polarizationGroupSet) > 1):
4607 continue
4608
4609 pgrp11 = set(atom.polarizationGroupSet[0])
4610 pgrp12 = set()
4611 for pgrpAtomIndex in pgrp11:
4612 for bonded12 in bonded12ParticleSets[pgrpAtomIndex]:
4613 pgrp12 = pgrp12.union(data.atoms[bonded12].polarizationGroupSet[0])
4614 pgrp12 = pgrp12 - pgrp11
4615 for pgrpAtomIndex in pgrp11:
4616 data.atoms[pgrpAtomIndex].polarizationGroupSet.append(pgrp12)
4617
4618 for (atomIndex, atom) in enumerate(data.atoms):
4619 atom.polarizationGroupSet[1] = sorted(atom.polarizationGroupSet[1])
4620 force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.PolarizationCovalent12, atom.polarizationGroupSet[1])
4621
4622 # pgrp13
4623
4624 for (atomIndex, atom) in enumerate(data.atoms):
4625
4626 if (len(data.atoms[atomIndex].polarizationGroupSet) > 2):
4627 continue
4628
4629 pgrp11 = set(atom.polarizationGroupSet[0])
4630 pgrp12 = set(atom.polarizationGroupSet[1])
4631 pgrp13 = set()
4632 for pgrpAtomIndex in pgrp12:
4633 for bonded12 in bonded12ParticleSets[pgrpAtomIndex]:
4634 pgrp13 = pgrp13.union(data.atoms[bonded12].polarizationGroupSet[0])
4635 pgrp13 = pgrp13 - pgrp12
4636 pgrp13 = pgrp13 - set(pgrp11)
4637 for pgrpAtomIndex in pgrp11:
4638 data.atoms[pgrpAtomIndex].polarizationGroupSet.append(pgrp13)
4639
4640 for (atomIndex, atom) in enumerate(data.atoms):
4641 atom.polarizationGroupSet[2] = sorted(atom.polarizationGroupSet[2])
4642 force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.PolarizationCovalent13, atom.polarizationGroupSet[2])
4643
4644 # pgrp14
4645
4646 for (atomIndex, atom) in enumerate(data.atoms):
4647
4648 if (len(data.atoms[atomIndex].polarizationGroupSet) > 3):
4649 continue
4650
4651 pgrp11 = set(atom.polarizationGroupSet[0])
4652 pgrp12 = set(atom.polarizationGroupSet[1])
4653 pgrp13 = set(atom.polarizationGroupSet[2])
4654 pgrp14 = set()
4655 for pgrpAtomIndex in pgrp13:
4656 for bonded12 in bonded12ParticleSets[pgrpAtomIndex]:
4657 pgrp14 = pgrp14.union(data.atoms[bonded12].polarizationGroupSet[0])
4658
4659 pgrp14 = pgrp14 - pgrp13
4660 pgrp14 = pgrp14 - pgrp12
4661 pgrp14 = pgrp14 - set(pgrp11)
4662
4663 for pgrpAtomIndex in pgrp11:
4664 data.atoms[pgrpAtomIndex].polarizationGroupSet.append(pgrp14)
4665
4666 for (atomIndex, atom) in enumerate(data.atoms):
4667 atom.polarizationGroupSet[3] = sorted(atom.polarizationGroupSet[3])
4668 force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.PolarizationCovalent14, atom.polarizationGroupSet[3])
4669
4670 #=============================================================================================
4671
4672 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
4673
4674 methodMap = {NoCutoff:mm.AmoebaMultipoleForce.NoCutoff,
4675 PME:mm.AmoebaMultipoleForce.PME}
4676
4677 force = mm.AmoebaMultipoleForce()
4678 sys.addForce(force)
4679 if (nonbondedMethod not in methodMap):
4680 raise ValueError( "AmoebaMultipoleForce: input cutoff method not available." )
4681 else:
4682 force.setNonbondedMethod(methodMap[nonbondedMethod])
4683 force.setCutoffDistance(nonbondedCutoff)
4684
4685 if ('ewaldErrorTolerance' in args):
4686 force.setEwaldErrorTolerance(float(args['ewaldErrorTolerance']))
4687
4688 if ('polarization' in args):
4689 polarizationType = args['polarization']
4690 if (polarizationType.lower() == 'direct'):
4691 force.setPolarizationType(mm.AmoebaMultipoleForce.Direct)
4692 elif (polarizationType.lower() == 'extrapolated'):
4693 force.setPolarizationType(mm.AmoebaMultipoleForce.Extrapolated)
4694 else:
4695 force.setPolarizationType(mm.AmoebaMultipoleForce.Mutual)
4696
4697 if ('aEwald' in args):
4698 force.setAEwald(float(args['aEwald']))
4699
4700 if ('pmeGridDimensions' in args):
4701 force.setPmeGridDimensions(args['pmeGridDimensions'])
4702
4703 if ('mutualInducedMaxIterations' in args):
4704 force.setMutualInducedMaxIterations(int(args['mutualInducedMaxIterations']))
4705
4706 if ('mutualInducedTargetEpsilon' in args):
4707 force.setMutualInducedTargetEpsilon(float(args['mutualInducedTargetEpsilon']))
4708
4709 # add particles to force
4710 # throw error if particle type not available
4711
4712 # get 1-2, 1-3, 1-4, 1-5 bonded sets
4713
4714 # 1-2
4715
4716 bonded12ParticleSets = AmoebaVdwGenerator.getBondedParticleSets(sys, data)
4717
4718 # 1-3
4719
4720 bonded13ParticleSets = []
4721 for i in range(len(data.atoms)):
4722 bonded13Set = set()
4723 bonded12ParticleSet = bonded12ParticleSets[i]
4724 for j in bonded12ParticleSet:
4725 bonded13Set = bonded13Set.union(bonded12ParticleSets[j])
4726
4727 # remove 1-2 and self from set
4728
4729 bonded13Set = bonded13Set - bonded12ParticleSet
4730 selfSet = set()
4731 selfSet.add(i)
4732 bonded13Set = bonded13Set - selfSet
4733 bonded13Set = set(sorted(bonded13Set))
4734 bonded13ParticleSets.append(bonded13Set)
4735
4736 # 1-4
4737
4738 bonded14ParticleSets = []
4739 for i in range(len(data.atoms)):
4740 bonded14Set = set()
4741 bonded13ParticleSet = bonded13ParticleSets[i]
4742 for j in bonded13ParticleSet:
4743 bonded14Set = bonded14Set.union(bonded12ParticleSets[j])
4744
4745 # remove 1-3, 1-2 and self from set
4746
4747 bonded14Set = bonded14Set - bonded12ParticleSets[i]
4748 bonded14Set = bonded14Set - bonded13ParticleSet
4749 selfSet = set()
4750 selfSet.add(i)
4751 bonded14Set = bonded14Set - selfSet
4752 bonded14Set = set(sorted(bonded14Set))
4753 bonded14ParticleSets.append(bonded14Set)
4754
4755 # 1-5
4756
4757 bonded15ParticleSets = []
4758 for i in range(len(data.atoms)):
4759 bonded15Set = set()
4760 bonded14ParticleSet = bonded14ParticleSets[i]
4761 for j in bonded14ParticleSet:
4762 bonded15Set = bonded15Set.union(bonded12ParticleSets[j])
4763
4764 # remove 1-4, 1-3, 1-2 and self from set
4765
4766 bonded15Set = bonded15Set - bonded12ParticleSets[i]
4767 bonded15Set = bonded15Set - bonded13ParticleSets[i]
4768 bonded15Set = bonded15Set - bonded14ParticleSet
4769 selfSet = set()
4770 selfSet.add(i)
4771 bonded15Set = bonded15Set - selfSet
4772 bonded15Set = set(sorted(bonded15Set))
4773 bonded15ParticleSets.append(bonded15Set)
4774
4775 for (atomIndex, atom) in enumerate(data.atoms):
4776 t = data.atomType[atom]
4777 if t in self.typeMap:
4778
4779 multipoleList = self.typeMap[t]
4780 hit = 0
4781 savedMultipoleDict = 0
4782
4783 # assign multipole parameters via only 1-2 connected atoms
4784
4785 for multipoleDict in multipoleList:
4786
4787 if (hit != 0):
4788 break
4789
4790 kIndices = multipoleDict['kIndices']
4791
4792 kz = kIndices[1]
4793 kx = kIndices[2]
4794 ky = kIndices[3]
4795
4796 # assign multipole parameters
4797 # (1) get bonded partners
4798 # (2) match parameter types
4799
4800 bondedAtomIndices = bonded12ParticleSets[atomIndex]
4801 zaxis = -1
4802 xaxis = -1
4803 yaxis = -1
4804 for bondedAtomZIndex in bondedAtomIndices:
4805
4806 if (hit != 0):
4807 break
4808
4809 bondedAtomZType = int(data.atomType[data.atoms[bondedAtomZIndex]])
4810 bondedAtomZ = data.atoms[bondedAtomZIndex]
4811 if (bondedAtomZType == kz):
4812 for bondedAtomXIndex in bondedAtomIndices:
4813 if (bondedAtomXIndex == bondedAtomZIndex or hit != 0):
4814 continue
4815 bondedAtomXType = int(data.atomType[data.atoms[bondedAtomXIndex]])
4816 if (bondedAtomXType == kx):
4817 if (ky == 0):
4818 zaxis = bondedAtomZIndex
4819 xaxis = bondedAtomXIndex
4820 if( bondedAtomXType == bondedAtomZType and xaxis < zaxis ):
4821 swapI = zaxis
4822 zaxis = xaxis
4823 xaxis = swapI
4824 else:
4825 for bondedAtomXIndex in bondedAtomIndices:
4826 bondedAtomX1Type = int(data.atomType[data.atoms[bondedAtomXIndex]])
4827 if( bondedAtomX1Type == kx and bondedAtomXIndex != bondedAtomZIndex and bondedAtomXIndex < xaxis ):
4828 xaxis = bondedAtomXIndex
4829
4830 savedMultipoleDict = multipoleDict
4831 hit = 1
4832 else:
4833 for bondedAtomYIndex in bondedAtomIndices:
4834 if (bondedAtomYIndex == bondedAtomZIndex or bondedAtomYIndex == bondedAtomXIndex or hit != 0):
4835 continue
4836 bondedAtomYType = int(data.atomType[data.atoms[bondedAtomYIndex]])
4837 if (bondedAtomYType == ky):
4838 zaxis = bondedAtomZIndex
4839 xaxis = bondedAtomXIndex
4840 yaxis = bondedAtomYIndex
4841 savedMultipoleDict = multipoleDict
4842 hit = 2
4843
4844 # assign multipole parameters via 1-2 and 1-3 connected atoms
4845
4846 for multipoleDict in multipoleList:
4847
4848 if (hit != 0):
4849 break
4850
4851 kIndices = multipoleDict['kIndices']
4852
4853 kz = kIndices[1]
4854 kx = kIndices[2]
4855 ky = kIndices[3]
4856
4857 # assign multipole parameters
4858 # (1) get bonded partners
4859 # (2) match parameter types
4860
4861 bondedAtom12Indices = bonded12ParticleSets[atomIndex]
4862 bondedAtom13Indices = bonded13ParticleSets[atomIndex]
4863
4864 zaxis = -1
4865 xaxis = -1
4866 yaxis = -1
4867
4868 for bondedAtomZIndex in bondedAtom12Indices:
4869
4870 if (hit != 0):
4871 break
4872
4873 bondedAtomZType = int(data.atomType[data.atoms[bondedAtomZIndex]])
4874 bondedAtomZ = data.atoms[bondedAtomZIndex]
4875
4876 if (bondedAtomZType == kz):
4877 for bondedAtomXIndex in bondedAtom13Indices:
4878
4879 if (bondedAtomXIndex == bondedAtomZIndex or hit != 0):
4880 continue
4881 bondedAtomXType = int(data.atomType[data.atoms[bondedAtomXIndex]])
4882 if (bondedAtomXType == kx and bondedAtomZIndex in bonded12ParticleSets[bondedAtomXIndex]):
4883 if (ky == 0):
4884 zaxis = bondedAtomZIndex
4885 xaxis = bondedAtomXIndex
4886
4887 # select xaxis w/ smallest index
4888
4889 for bondedAtomXIndex in bondedAtom13Indices:
4890 bondedAtomX1Type = int(data.atomType[data.atoms[bondedAtomXIndex]])
4891 if( bondedAtomX1Type == kx and bondedAtomXIndex != bondedAtomZIndex and bondedAtomZIndex in bonded12ParticleSets[bondedAtomXIndex] and bondedAtomXIndex < xaxis ):
4892 xaxis = bondedAtomXIndex
4893
4894 savedMultipoleDict = multipoleDict
4895 hit = 3
4896 else:
4897 for bondedAtomYIndex in bondedAtom13Indices:
4898 if (bondedAtomYIndex == bondedAtomZIndex or bondedAtomYIndex == bondedAtomXIndex or hit != 0):
4899 continue
4900 bondedAtomYType = int(data.atomType[data.atoms[bondedAtomYIndex]])
4901 if (bondedAtomYType == ky and bondedAtomZIndex in bonded12ParticleSets[bondedAtomYIndex]):
4902 zaxis = bondedAtomZIndex
4903 xaxis = bondedAtomXIndex
4904 yaxis = bondedAtomYIndex
4905 savedMultipoleDict = multipoleDict
4906 hit = 4
4907
4908 # assign multipole parameters via only a z-defining atom
4909
4910 for multipoleDict in multipoleList:
4911
4912 if (hit != 0):
4913 break
4914
4915 kIndices = multipoleDict['kIndices']
4916
4917 kz = kIndices[1]
4918 kx = kIndices[2]
4919
4920 zaxis = -1
4921 xaxis = -1
4922 yaxis = -1
4923
4924 for bondedAtomZIndex in bondedAtom12Indices:
4925
4926 if (hit != 0):
4927 break
4928
4929 bondedAtomZType = int(data.atomType[data.atoms[bondedAtomZIndex]])
4930 bondedAtomZ = data.atoms[bondedAtomZIndex]
4931
4932 if (kx == 0 and kz == bondedAtomZType):
4933 zaxis = bondedAtomZIndex
4934 savedMultipoleDict = multipoleDict
4935 hit = 5
4936
4937 # assign multipole parameters via no connected atoms
4938
4939 for multipoleDict in multipoleList:
4940
4941 if (hit != 0):
4942 break
4943
4944 kIndices = multipoleDict['kIndices']
4945
4946 kz = kIndices[1]
4947
4948 zaxis = -1
4949 xaxis = -1
4950 yaxis = -1
4951
4952 if (kz == 0):
4953 savedMultipoleDict = multipoleDict
4954 hit = 6
4955
4956 # add particle if there was a hit
4957
4958 if (hit != 0):
4959
4960 atom.multipoleDict = savedMultipoleDict
4961 atom.polarizationGroups = dict()
4962 newIndex = force.addMultipole(savedMultipoleDict['charge'], savedMultipoleDict['dipole'], savedMultipoleDict['quadrupole'], savedMultipoleDict['axisType'],
4963 zaxis, xaxis, yaxis, savedMultipoleDict['thole'], savedMultipoleDict['pdamp'], savedMultipoleDict['polarizability'])
4964 if (atomIndex == newIndex):
4965 force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.Covalent12, tuple(bonded12ParticleSets[atomIndex]))
4966 force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.Covalent13, tuple(bonded13ParticleSets[atomIndex]))
4967 force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.Covalent14, tuple(bonded14ParticleSets[atomIndex]))
4968 force.setCovalentMap(atomIndex, mm.AmoebaMultipoleForce.Covalent15, tuple(bonded15ParticleSets[atomIndex]))
4969 else:
4970 raise ValueError("Atom %s of %s %d is out of sync!." %(atom.name, atom.residue.name, atom.residue.index))
4971 else:
4972 raise ValueError("Atom %s of %s %d was not assigned." %(atom.name, atom.residue.name, atom.residue.index))
4973 else:
4974 raise ValueError('No multipole type for atom %s %s %d' % (atom.name, atom.residue.name, atom.residue.index))
4975
4976 # set polar groups
4977
4978 self.setPolarGroups(data, bonded12ParticleSets, force)
4979
4980parsers["AmoebaMultipoleForce"] = AmoebaMultipoleGenerator.parseElement
4981
4982#=============================================================================================
4983
4984## @private
4985class AmoebaWcaDispersionGenerator(object):
4986
4987 """A AmoebaWcaDispersionGenerator constructs a AmoebaWcaDispersionForce."""
4988
4989 #=========================================================================================
4990
4991 def __init__(self, epso, epsh, rmino, rminh, awater, slevy, dispoff, shctd):
4992
4993 self.epso = epso
4994 self.epsh = epsh
4995 self.rmino = rmino
4996 self.rminh = rminh
4997 self.awater = awater
4998 self.slevy = slevy
4999 self.dispoff = dispoff
5000 self.shctd = shctd
5001
5002 #=========================================================================================
5003
5004 @staticmethod
5005 def parseElement(element, forceField):
5006
5007 # <AmoebaWcaDispersionForce epso="0.46024" epsh="0.056484" rmino="0.17025" rminh="0.13275" awater="33.428" slevy="1.0" dispoff="0.026" shctd="0.81" >
5008 # <WcaDispersion class="1" radius="0.1855" epsilon="0.46024" />
5009 # <WcaDispersion class="2" radius="0.191" epsilon="0.422584" />
5010
5011 generator = AmoebaWcaDispersionGenerator(element.attrib['epso'],
5012 element.attrib['epsh'],
5013 element.attrib['rmino'],
5014 element.attrib['rminh'],
5015 element.attrib['awater'],
5016 element.attrib['slevy'],
5017 element.attrib['dispoff'],
5018 element.attrib['shctd'])
5019 forceField._forces.append(generator)
5020 generator.params = ForceField._AtomTypeParameters(forceField, 'AmoebaWcaDispersionForce', 'WcaDispersion', ('radius', 'epsilon'))
5021 generator.params.parseDefinitions(element)
5022
5023 #=========================================================================================
5024
5025 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
5026
5027 # get or create force depending on whether it has already been added to the system
5028
5029 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
5030 existing = [f for f in existing if type(f) == mm.AmoebaWcaDispersionForce]
5031 if len(existing) == 0:
5032 force = mm.AmoebaWcaDispersionForce()
5033 sys.addForce(force)
5034 else:
5035 force = existing[0]
5036
5037 # add particles to force
5038 # throw error if particle type not available
5039
5040 force.setEpso( float(self.epso ))
5041 force.setEpsh( float(self.epsh ))
5042 force.setRmino( float(self.rmino ))
5043 force.setRminh( float(self.rminh ))
5044 force.setDispoff(float(self.dispoff))
5045 force.setSlevy( float(self.slevy ))
5046 force.setAwater( float(self.awater ))
5047 force.setShctd( float(self.shctd ))
5048
5049 for atom in data.atoms:
5050 values = self.params.getAtomParameters(atom, data)
5051 force.addParticle(values[0], values[1])
5052
5053parsers["AmoebaWcaDispersionForce"] = AmoebaWcaDispersionGenerator.parseElement
5054
5055#=============================================================================================
5056
5057## @private
5058class AmoebaGeneralizedKirkwoodGenerator(object):
5059
5060 """A AmoebaGeneralizedKirkwoodGenerator constructs a AmoebaGeneralizedKirkwoodForce."""
5061
5062 #=========================================================================================
5063
5064 def __init__(self, forceField, solventDielectric, soluteDielectric, includeCavityTerm, probeRadius, surfaceAreaFactor):
5065
5066 self.forceField = forceField
5067 self.solventDielectric = solventDielectric
5068 self.soluteDielectric = soluteDielectric
5069 self.includeCavityTerm = includeCavityTerm
5070 self.probeRadius = probeRadius
5071 self.surfaceAreaFactor = surfaceAreaFactor
5072
5073 self.radiusTypeMap = {}
5074 self.radiusTypeMap['Bondi'] = {}
5075 bondiMap = self.radiusTypeMap['Bondi']
5076 rscale = 1.03
5077
5078 bondiMap[0] = 0.00
5079 bondiMap[1] = 0.12*rscale
5080 bondiMap[2] = 0.14*rscale
5081 bondiMap[5] = 0.18*rscale
5082
5083 bondiMap[6] = 0.170*rscale
5084 bondiMap[7] = 0.155*rscale
5085 bondiMap[8] = 0.152*rscale
5086 bondiMap[9] = 0.147*rscale
5087
5088 bondiMap[10] = 0.154*rscale
5089 bondiMap[14] = 0.210*rscale
5090 bondiMap[15] = 0.180*rscale
5091 bondiMap[16] = 0.180*rscale
5092
5093 bondiMap[17] = 0.175 *rscale
5094 bondiMap[18] = 0.188*rscale
5095 bondiMap[34] = 0.190*rscale
5096 bondiMap[35] = 0.185*rscale
5097
5098 bondiMap[36] = 0.202*rscale
5099 bondiMap[53] = 0.198*rscale
5100 bondiMap[54] = 0.216*rscale
5101
5102 #=========================================================================================
5103
5104 def getObcShct(self, data, atomIndex):
5105
5106 atom = data.atoms[atomIndex]
5107 atomicNumber = atom.element.atomic_number
5108 shct = -1.0
5109
5110 # shct
5111
5112 if (atomicNumber == 1): # H(1)
5113 shct = 0.85
5114 elif (atomicNumber == 6): # C(6)
5115 shct = 0.72
5116 elif (atomicNumber == 7): # N(7)
5117 shct = 0.79
5118 elif (atomicNumber == 8): # O(8)
5119 shct = 0.85
5120 elif (atomicNumber == 9): # F(9)
5121 shct = 0.88
5122 elif (atomicNumber == 15): # P(15)
5123 shct = 0.86
5124 elif (atomicNumber == 16): # S(16)
5125 shct = 0.96
5126 elif (atomicNumber == 26): # Fe(26)
5127 shct = 0.88
5128
5129 if (shct < 0.0):
5130 raise ValueError( "getObcShct: no GK overlap scale factor for atom %s of %s %d" % (atom.name, atom.residue.name, atom.residue.index) )
5131
5132 return shct
5133
5134 #=========================================================================================
5135
5136 def getAmoebaTypeRadius(self, data, bondedAtomIndices, atomIndex):
5137
5138 atom = data.atoms[atomIndex]
5139 atomicNumber = atom.element.atomic_number
5140 radius = -1.0
5141
5142 if (atomicNumber == 1): # H(1)
5143
5144 radius = 0.132
5145
5146 if (len(bondedAtomIndices) < 1):
5147 raise ValueError( "AmoebaGeneralizedKirkwoodGenerator: error getting atom bonded to %s of %s %d " % (atom.name, atom.residue.name, atom.residue.index) )
5148
5149 for bondedAtomIndex in bondedAtomIndices:
5150 bondedAtomAtomicNumber = data.atoms[bondedAtomIndex].element.atomic_number
5151
5152 if (bondedAtomAtomicNumber == 7):
5153 radius = 0.11
5154 if (bondedAtomAtomicNumber == 8):
5155 radius = 0.105
5156
5157 elif (atomicNumber == 3): # Li(3)
5158 radius = 0.15
5159 elif (atomicNumber == 6): # C(6)
5160
5161 radius = 0.20
5162 if (len(bondedAtomIndices) == 3):
5163 radius = 0.205
5164
5165 elif (len(bondedAtomIndices) == 4):
5166 for bondedAtomIndex in bondedAtomIndices:
5167 bondedAtomAtomicNumber = data.atoms[bondedAtomIndex].element.atomic_number
5168 if (bondedAtomAtomicNumber == 7 or bondedAtomAtomicNumber == 8):
5169 radius = 0.175
5170
5171 elif (atomicNumber == 7): # N(7)
5172 radius = 0.16
5173 elif (atomicNumber == 8): # O(8)
5174 radius = 0.155
5175 if (len(bondedAtomIndices) == 2):
5176 radius = 0.145
5177 elif (atomicNumber == 9): # F(9)
5178 radius = 0.154
5179 elif (atomicNumber == 10):
5180 radius = 0.146
5181 elif (atomicNumber == 11):
5182 radius = 0.209
5183 elif (atomicNumber == 12):
5184 radius = 0.179
5185 elif (atomicNumber == 14):
5186 radius = 0.189
5187 elif (atomicNumber == 15): # P(15)
5188 radius = 0.196
5189 elif (atomicNumber == 16): # S(16)
5190 radius = 0.186
5191 elif (atomicNumber == 17):
5192 radius = 0.182
5193 elif (atomicNumber == 18):
5194 radius = 0.179
5195 elif (atomicNumber == 19):
5196 radius = 0.223
5197 elif (atomicNumber == 20):
5198 radius = 0.191
5199 elif (atomicNumber == 35):
5200 radius = 2.00
5201 elif (atomicNumber == 36):
5202 radius = 0.190
5203 elif (atomicNumber == 37):
5204 radius = 0.226
5205 elif (atomicNumber == 53):
5206 radius = 0.237
5207 elif (atomicNumber == 54):
5208 radius = 0.207
5209 elif (atomicNumber == 55):
5210 radius = 0.263
5211 elif (atomicNumber == 56):
5212 radius = 0.230
5213
5214 if (radius < 0.0):
5215 outputString = "No GK radius for atom %s of %s %d" % (atom.name, atom.residue.name, atom.residue.index)
5216 raise ValueError( outputString )
5217
5218 return radius
5219
5220 #=========================================================================================
5221
5222 def getBondiTypeRadius(self, data, bondedAtomIndices, atomIndex):
5223
5224 bondiMap = self.radiusTypeMap['Bondi']
5225 atom = data.atoms[atomIndex]
5226 atomicNumber = atom.element.atomic_number
5227 if (atomicNumber in bondiMap):
5228 radius = bondiMap[atomicNumber]
5229 else:
5230 outputString = "Warning no Bondi radius for atom %s of %s %d using default value=%f" % (atom.name, atom.residue.name, atom.residue.index, radius)
5231 raise ValueError( outputString )
5232
5233 return radius
5234
5235 #=========================================================================================
5236
5237 @staticmethod
5238 def parseElement(element, forceField):
5239
5240 # <AmoebaGeneralizedKirkwoodForce solventDielectric="78.3" soluteDielectric="1.0" includeCavityTerm="1" probeRadius="0.14" surfaceAreaFactor="-170.351730663">
5241 # <GeneralizedKirkwood type="1" charge="-0.22620" shct="0.79" />
5242 # <GeneralizedKirkwood type="2" charge="-0.15245" shct="0.72" />
5243
5244 generator = AmoebaGeneralizedKirkwoodGenerator(forceField, element.attrib['solventDielectric'], element.attrib['soluteDielectric'],
5245 element.attrib['includeCavityTerm'],
5246 element.attrib['probeRadius'], element.attrib['surfaceAreaFactor'])
5247 forceField._forces.append(generator)
5248
5249 #=========================================================================================
5250
5251 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
5252
5253 if( nonbondedMethod != NoCutoff ):
5254 raise ValueError( "Only the nonbondedMethod=NoCutoff option is available for implicit solvent simulations." )
5255
5256 # check if AmoebaMultipoleForce exists since charges needed
5257 # if it has not been created, raise an error
5258
5259 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
5260 amoebaMultipoleForceList = [f for f in existing if type(f) == mm.AmoebaMultipoleForce]
5261 if (len(amoebaMultipoleForceList) > 0):
5262 amoebaMultipoleForce = amoebaMultipoleForceList[0]
5263 else:
5264 # call AmoebaMultipoleForceGenerator.createForce() to ensure charges have been set
5265
5266 for force in self.forceField._forces:
5267 if (force.__class__.__name__ == 'AmoebaMultipoleGenerator'):
5268 force.createForce(sys, data, nonbondedMethod, nonbondedCutoff, args)
5269
5270 # get or create force depending on whether it has already been added to the system
5271
5272 existing = [f for f in existing if type(f) == mm.AmoebaGeneralizedKirkwoodForce]
5273 if len(existing) == 0:
5274
5275 force = mm.AmoebaGeneralizedKirkwoodForce()
5276 sys.addForce(force)
5277
5278 if ('solventDielectric' in args):
5279 force.setSolventDielectric(float(args['solventDielectric']))
5280 else:
5281 force.setSolventDielectric( float(self.solventDielectric))
5282
5283 if ('soluteDielectric' in args):
5284 force.setSoluteDielectric(float(args['soluteDielectric']))
5285 else:
5286 force.setSoluteDielectric( float(self.soluteDielectric))
5287
5288 if ('includeCavityTerm' in args):
5289 force.setIncludeCavityTerm(int(args['includeCavityTerm']))
5290 else:
5291 force.setIncludeCavityTerm( int(self.includeCavityTerm))
5292
5293 else:
5294 force = existing[0]
5295
5296 # add particles to force
5297 # throw error if particle type not available
5298
5299 force.setProbeRadius( float(self.probeRadius))
5300 force.setSurfaceAreaFactor( float(self.surfaceAreaFactor))
5301
5302 # 1-2
5303
5304 bonded12ParticleSets = AmoebaVdwGenerator.getBondedParticleSets(sys, data)
5305
5306 radiusType = 'Bondi'
5307 for atomIndex in range(0, amoebaMultipoleForce.getNumMultipoles()):
5308 multipoleParameters = amoebaMultipoleForce.getMultipoleParameters(atomIndex)
5309 if (radiusType == 'Amoeba'):
5310 radius = self.getAmoebaTypeRadius(data, bonded12ParticleSets[atomIndex], atomIndex)
5311 else:
5312 radius = self.getBondiTypeRadius(data, bonded12ParticleSets[atomIndex], atomIndex)
5313 #shct = self.getObcShct(data, atomIndex)
5314 shct = 0.69
5315 force.addParticle(multipoleParameters[0], radius, shct)
5316
5317parsers["AmoebaGeneralizedKirkwoodForce"] = AmoebaGeneralizedKirkwoodGenerator.parseElement
5318
5319#=============================================================================================
5320
5321## @private
5322class AmoebaUreyBradleyGenerator(object):
5323
5324 #=============================================================================================
5325 """An AmoebaUreyBradleyGenerator constructs a AmoebaUreyBradleyForce."""
5326 #=============================================================================================
5327
5328 def __init__(self):
5329
5330 self.types1 = []
5331 self.types2 = []
5332 self.types3 = []
5333
5334 self.length = []
5335 self.k = []
5336
5337 #=============================================================================================
5338
5339 @staticmethod
5340 def parseElement(element, forceField):
5341
5342 # <AmoebaUreyBradleyForce>
5343 # <UreyBradley class1="74" class2="73" class3="74" k="16003.8" d="0.15537" />
5344
5345 generator = AmoebaUreyBradleyGenerator()
5346 forceField._forces.append(generator)
5347 for bond in element.findall('UreyBradley'):
5348 types = forceField._findAtomTypes(bond.attrib, 3)
5349 if None not in types:
5350
5351 generator.types1.append(types[0])
5352 generator.types2.append(types[1])
5353 generator.types3.append(types[2])
5354
5355 generator.length.append(float(bond.attrib['d']))
5356 generator.k.append(float(bond.attrib['k']))
5357
5358 else:
5359 outputString = "AmoebaUreyBradleyGenerator: error getting types: %s %s %s" % (
5360 bond.attrib['class1'], bond.attrib['class2'], bond.attrib['class3'])
5361 raise ValueError(outputString)
5362
5363 #=============================================================================================
5364
5365 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
5366
5367 existing = [sys.getForce(i) for i in range(sys.getNumForces())]
5368 existing = [f for f in existing if type(f) == mm.HarmonicBondForce]
5369
5370 if len(existing) == 0:
5371 force = mm.HarmonicBondForce()
5372 sys.addForce(force)
5373 else:
5374 force = existing[0]
5375
5376 for (angle, isConstrained) in zip(data.angles, data.isAngleConstrained):
5377 if (isConstrained):
5378 continue
5379 type1 = data.atomType[data.atoms[angle[0]]]
5380 type2 = data.atomType[data.atoms[angle[1]]]
5381 type3 = data.atomType[data.atoms[angle[2]]]
5382 for i in range(len(self.types1)):
5383 types1 = self.types1[i]
5384 types2 = self.types2[i]
5385 types3 = self.types3[i]
5386 if ((type1 in types1 and type2 in types2 and type3 in types3) or (type3 in types1 and type2 in types2 and type1 in types3)):
5387 force.addBond(angle[0], angle[2], self.length[i], 2*self.k[i])
5388 break
5389
5390parsers["AmoebaUreyBradleyForce"] = AmoebaUreyBradleyGenerator.parseElement
5391
5392#=============================================================================================
5393
5394
5395## @private
5396class DrudeGenerator(object):
5397 """A DrudeGenerator constructs a DrudeForce."""
5398
5399 def __init__(self, forcefield):
5400 self.ff = forcefield
5401 self.typeMap = {}
5402
5403 @staticmethod
5404 def parseElement(element, ff):
5405 existing = [f for f in ff._forces if isinstance(f, DrudeGenerator)]
5406 if len(existing) == 0:
5407 generator = DrudeGenerator(ff)
5408 ff.registerGenerator(generator)
5409 else:
5410 # Multiple <DrudeForce> tags were found, probably in different files. Simply add more types to the existing one.
5411 generator = existing[0]
5412 for particle in element.findall('Particle'):
5413 types = ff._findAtomTypes(particle.attrib, 5)
5414 if None not in types[:2]:
5415 aniso12 = 0.0
5416 aniso34 = 0.0
5417 if 'aniso12' in particle.attrib:
5418 aniso12 = float(particle.attrib['aniso12'])
5419 if 'aniso34' in particle.attrib:
5420 aniso34 = float(particle.attrib['aniso34'])
5421 values = (types[1], types[2], types[3], types[4], float(particle.attrib['charge']), float(particle.attrib['polarizability']), aniso12, aniso34, float(particle.attrib['thole']))
5422 for t in types[0]:
5423 generator.typeMap[t] = values
5424
5425 def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
5426 force = mm.DrudeForce()
5427 if not any(isinstance(f, mm.NonbondedForce) for f in sys.getForces()):
5428 raise ValueError('<DrudeForce> must come after <NonbondedForce> in XML file')
5429
5430 # Add Drude particles.
5431
5432 for atom in data.atoms:
5433 t = data.atomType[atom]
5434 if t in self.typeMap:
5435 # Find other atoms in the residue that affect the Drude particle.
5436 p = [-1, -1, -1, -1]
5437 values = self.typeMap[t]
5438 for atom2 in atom.residue.atoms():
5439 type2 = data.atomType[atom2]
5440 if type2 in values[0]:
5441 p[0] = atom2.index
5442 elif values[1] is not None and type2 in values[1]:
5443 p[1] = atom2.index
5444 elif values[2] is not None and type2 in values[2]:
5445 p[2] = atom2.index
5446 elif values[3] is not None and type2 in values[3]:
5447 p[3] = atom2.index
5448 force.addParticle(atom.index, p[0], p[1], p[2], p[3], values[4], values[5], values[6], values[7])
5449 data.excludeAtomWith[p[0]].append(atom.index)
5450 sys.addForce(force)
5451
5452 def postprocessSystem(self, sys, data, args):
5453 # For every nonbonded exclusion between Drude particles, add a screened pair.
5454
5455 drude = [f for f in sys.getForces() if isinstance(f, mm.DrudeForce)][0]
5456 nonbonded = [f for f in sys.getForces() if isinstance(f, mm.NonbondedForce)][0]
5457 particleMap = {}
5458 for i in range(drude.getNumParticles()):
5459 particleMap[drude.getParticleParameters(i)[0]] = i
5460 for i in range(nonbonded.getNumExceptions()):
5461 (particle1, particle2, charge, sigma, epsilon) = nonbonded.getExceptionParameters(i)
5462 if charge._value == 0 and epsilon._value == 0:
5463 # This is an exclusion.
5464 if particle1 in particleMap and particle2 in particleMap:
5465 # It connects two Drude particles, so add a screened pair.
5466 drude1 = particleMap[particle1]
5467 drude2 = particleMap[particle2]
5468 type1 = data.atomType[data.atoms[particle1]]
5469 type2 = data.atomType[data.atoms[particle2]]
5470 thole1 = self.typeMap[type1][8]
5471 thole2 = self.typeMap[type2][8]
5472 drude.addScreenedPair(drude1, drude2, thole1+thole2)
5473
5474parsers["DrudeForce"] = DrudeGenerator.parseElement
5475
5476#=============================================================================================